Compare commits

..

1 Commits

Author SHA1 Message Date
liaodanlong
186ac391ea 并发创建制造订单的工单 2025-01-21 15:53:09 +08:00
183 changed files with 1667 additions and 25218 deletions

View File

@@ -20,7 +20,7 @@
'version': '0.1', 'version': '0.1',
# any module necessary for this one to work correctly # any module necessary for this one to work correctly
'depends': ['base', 'account', 'l10n_cn'], 'depends': ['base', 'account'],
# always loaded # always loaded
'data': [ 'data': [

View File

@@ -1,4 +1,4 @@
from odoo import models, fields, api, _ from odoo import models, fields, api
from odoo.exceptions import ValidationError from odoo.exceptions import ValidationError
@@ -7,14 +7,6 @@ class CustomAccountMoveLine(models.Model):
_inherit = 'account.move' _inherit = 'account.move'
_description = "account move line" _description = "account move line"
fapiao = fields.Char(string='发票号', size=20, copy=False, tracking=True, required=True)
@api.constrains('fapiao')
def _check_fapiao(self):
for record in self:
if record.fapiao and (len(record.fapiao) != 20 or not record.fapiao.isdecimal()):
raise ValidationError(_("Fapiao number is an 20-digit number. Please enter a correct one."))
@api.model_create_multi @api.model_create_multi
def create(self, vals): def create(self, vals):
for val in vals: for val in vals:

View File

@@ -21,8 +21,8 @@
'web.assets_qweb': [ 'web.assets_qweb': [
], ],
'web.assets_backend': [ 'web.assets_backend': [
# 'jikimo_frontend/static/src/fields/custom_many2many_checkboxes/*', 'jikimo_frontend/static/src/fields/custom_many2many_checkboxes/*',
# 'jikimo_frontend/static/src/fields/Many2OneRadioField/*', 'jikimo_frontend/static/src/fields/Many2OneRadioField/*',
# 移除odoo相关标识 # 移除odoo相关标识
'jikimo_frontend/static/src/bye_odoo/*', 'jikimo_frontend/static/src/bye_odoo/*',
'jikimo_frontend/static/src/scss/custom_style.scss', 'jikimo_frontend/static/src/scss/custom_style.scss',

View File

@@ -1,8 +1,3 @@
.o_list_renderer .o_list_table tbody > tr > td:not(.o_list_record_selector):not(.o_handle_cell):not(.o_list_button):not(.o_list_record_remove){ .o_list_renderer .o_list_table tbody > tr > td:not(.o_list_record_selector):not(.o_handle_cell):not(.o_list_button):not(.o_list_record_remove){
border:1px solid #dee2e6 !important; border:1px solid #dee2e6 !important;
}
.custom_required_add::before{
content: '*';
color: red;
} }

View File

@@ -0,0 +1,3 @@
.many2one_radio_field {
display: inline-block;
}

View File

@@ -0,0 +1,53 @@
/** @odoo-module **/
import { RadioField } from "@web/views/fields/radio/radio_field"; // 导入单选按钮组件
import { registry } from "@web/core/registry";
export class Many2OneRadioField extends RadioField {
// 你可以重写或者添加一些方法和属性
// 例如你可以重写setup方法来添加一些事件监听器或者初始化一些变量
setup() {
super.setup(); // 调用父类的setup方法
// 你自己的代码
}
onImageClick(event) {
// 放大图片逻辑
// 获取图片元素
const img = event.target;
const close = img.nextSibling;
// 实现放大图片逻辑
// 比如使用 CSS 放大
img.parentElement.classList.add('zoomed');
close.classList.add('img_close');
}
onCloseClick(event) {
const close = event.target;
const img = close.previousSibling;
img.parentElement.classList.remove('zoomed');
close.classList.remove('img_close');
}
get items() {
return Many2OneRadioField.getItems(this.props.name, this.props.record);
}
static getItems(fieldName, record) {
switch (record.fields[fieldName].type) {
case "selection":
return record.fields[fieldName].selection;
case "many2one": {
const value = record.preloadedData[fieldName] || [];
return value.map((item) => [item.id, item.display_name, item.image]);
}
default:
return [];
}
}
}
Many2OneRadioField.template = "jikimo_frontend.Many2OneRadioField";
// MyCustomWidget.supportedTypes = ['many2many'];
registry.category("fields").add("many2one_radio", Many2OneRadioField);

View File

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="jikimo_frontend.Many2OneRadioField" owl="1">
<div
role="radiogroup"
t-attf-class="o_{{ props.orientation }}"
t-att-aria-label="string"
>
<t t-foreach="items" t-as="item" t-key="item[0]">
<div class="form-check o_radio_item many2one_radio_field" aria-atomic="true">
<input
type="radio"
class="form-check-input o_radio_input"
t-att-checked="item[0] === value"
t-att-disabled="props.readonly"
t-att-name="id"
t-att-data-value="item[0]"
t-att-data-index="item_index"
t-att-id="`${id}_${item[0]}`"
t-on-change="() => this.onChange(item)"
/>
<label class="form-check-label o_form_label" t-att-for="`${id}_${item[0]}`" t-esc="item[1]" />
<div t-on-dblclick="onImageClick">
<t>
<img t-att-src="item[2]" width="50" height="50"/>
<div class="close" t-on-click="onCloseClick">×</div>
</t>
</div>
</div>
</t>
</div>
</t>
</templates>

View File

@@ -0,0 +1,100 @@
.processing-capabilities-grid {
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 10px;
width: 100%;
}
.grid-item {
display: flex;
align-items: center;
}
.item-content {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
}
/*控制图片大小*/
.item-icon {
width: 50px;
height: 50px;
margin-bottom: 5px;
margin-top: 15px;
}
.item-label {
font-size: 12px;
word-break: break-word;
}
@media (max-width: 1200px) {
.processing-capabilities-grid {
grid-template-columns: repeat(4, 1fr);
}
}
@media (max-width: 768px) {
.processing-capabilities-grid {
grid-template-columns: repeat(3, 1fr);
}
}
@media (max-width: 480px) {
.processing-capabilities-grid {
grid-template-columns: repeat(2, 1fr);
}
}
.image-preview-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.9);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
opacity: 0;
transition: opacity 0.3s ease;
}
.image-preview-container.show {
opacity: 1;
}
.image-preview {
max-width: 90%;
max-height: 90%;
object-fit: contain;
box-shadow: 0 0 20px rgba(255, 255, 255, 0.2);
border-radius: 5px;
transform: scale(0.9);
transition: transform 0.3s ease;
}
.image-preview-container.show .image-preview {
transform: scale(1);
}
.image-preview-close {
position: absolute;
top: 20px;
right: 30px;
color: #fff;
font-size: 40px;
font-weight: bold;
transition: 0.3s;
cursor: pointer;
opacity: 0.7;
}
.image-preview-close:hover,
.image-preview-close:focus {
opacity: 1;
text-decoration: none;
cursor: pointer;
}

View File

@@ -0,0 +1,60 @@
/** @odoo-module **/
import {Many2ManyCheckboxesField} from "@web/views/fields/many2many_checkboxes/many2many_checkboxes_field";
import {registry} from "@web/core/registry";
export class MyCustomWidget extends Many2ManyCheckboxesField {
setup() {
super.setup();
}
onImageClick(event, src) {
event.preventDefault();
event.stopPropagation();
// 创建预览框
const previewContainer = document.createElement('div');
previewContainer.className = 'image-preview-container';
const previewImg = document.createElement('img');
previewImg.src = src;
previewImg.className = 'image-preview';
// 设置放大的预览图片大小
previewImg.style.width = '600px';
previewImg.style.height = 'auto'; // 保持宽高比
const closeButton = document.createElement('span');
closeButton.innerHTML = '&times;';
closeButton.className = 'image-preview-close';
previewContainer.appendChild(previewImg);
previewContainer.appendChild(closeButton);
document.body.appendChild(previewContainer);
// 添加关闭预览的事件监听器
const closePreview = () => {
previewContainer.classList.remove('show');
setTimeout(() => {
document.body.removeChild(previewContainer);
}, 300);
};
closeButton.addEventListener('click', closePreview);
// 点击预览框外部也可以关闭
previewContainer.addEventListener('click', (e) => {
if (e.target === previewContainer) {
closePreview();
}
});
// 使用 setTimeout 来触发过渡效果
setTimeout(() => {
previewContainer.classList.add('show');
}, 10);
}
}
MyCustomWidget.template = "jikimo_frontend.MyCustomWidget";
registry.category("fields").add("custom_many2many_checkboxes", MyCustomWidget);

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="jikimo_frontend.MyCustomWidget" owl="1">
<div aria-atomic="true" class="many2many_flex processing-capabilities-grid">
<t t-foreach="items" t-as="item" t-key="item[0]">
<div class="grid-item">
<CheckBox
value="isSelected(item)"
disabled="props.readonly"
onChange="(ev) => this.onChange(item[0], ev)"
>
<div class="item-content">
<img t-att-src="item[2]" class="item-icon" t-on-click="(ev) => this.onImageClick(ev, item[2])"/>
<span class="item-label"><t t-esc="item[1]"/></span>
</div>
</CheckBox>
</div>
</t>
</div>
</t>
</templates>

View File

@@ -6,9 +6,8 @@ import {_t} from "@web/core/l10n/translation";
import {FormStatusIndicator} from "@web/views/form/form_status_indicator/form_status_indicator"; import {FormStatusIndicator} from "@web/views/form/form_status_indicator/form_status_indicator";
import {ListRenderer} from "@web/views/list/list_renderer"; import {ListRenderer} from "@web/views/list/list_renderer";
// import {StatusBarField} from "@web/views/fields/statusbar/statusbar_field"; // import {StatusBarField} from "@web/views/fields/statusbar/statusbar_field";
import {FormLabel} from "@web/views/form/form_label";
import { fieldVisualFeedback } from "@web/views/fields/field";
import {Field} from "@web/views/fields/field";
var Dialog = require('web.Dialog'); var Dialog = require('web.Dialog');
// var {patch} = require("web.utils") 这句话也行 // var {patch} = require("web.utils") 这句话也行
@@ -52,6 +51,7 @@ const tableRequiredList = [
'product_template_id', 'product_uom_qty', 'price_unit','product_id','product_qty', 'product_template_id', 'product_uom_qty', 'price_unit','product_id','product_qty',
'name', 'fault_type', 'maintenance_standards', 'Period' 'name', 'fault_type', 'maintenance_standards', 'Period'
] ]
patch(FormStatusIndicator.prototype, 'jikimo_frontend.FormStatusIndicator', { patch(FormStatusIndicator.prototype, 'jikimo_frontend.FormStatusIndicator', {
setup() { setup() {
owl.onMounted(() => { owl.onMounted(() => {
@@ -107,7 +107,33 @@ patch(FormStatusIndicator.prototype, 'jikimo_frontend.FormStatusIndicator', {
} }
); );
patch(Field.prototype, 'jikimo_frontend.Field', {
setup() {
owl.onMounted(this.setRequired);
return this._super(...arguments);
},
setRequired() {
const id = this.props.id
const isRequired = filedRequiredList[id]
if(id == 'number_of_axles') {
console.log(isRequired)
}
if(isRequired) {
let dom;
dom = $(`label[for=${id}]`)
if(isRequired.multiple && dom.length > 1) {
dom = dom.eq(-1)
dom = dom.parent().parent().next().find('label')
}
if(isRequired.noLabel) {
dom = dom.parent().parent()
}
let t = dom.html()
t = '<i class="c* r" style="color: red;margin-left: -4px">*</i>' + t
dom.html(t)
}
}
})
patch(ListRenderer.prototype, 'jikimo_frontend.ListRenderer', { patch(ListRenderer.prototype, 'jikimo_frontend.ListRenderer', {
setup(){ setup(){
owl.onMounted(() => { owl.onMounted(() => {
@@ -115,18 +141,9 @@ patch(ListRenderer.prototype, 'jikimo_frontend.ListRenderer', {
this.setRequired() this.setRequired()
this.listherHeaderBodyNum() this.listherHeaderBodyNum()
}) })
owl.onPatched(() => { owl.onPatched(() => {
this.listherHeaderBodyNum() this.listherHeaderBodyNum()
}) })
const treeModifiers = this.getFieldModifiers(this.props.archInfo.__rawArch);
if(treeModifiers) {
this.props.merge_key = treeModifiers.merge_key;
this.props.merge_fields = treeModifiers.merge_fields.split(',');
const data = this.setColumns(this.props.merge_key);
owl.onMounted(() => {
this.mergeColumns(this.props.merge_fields, data)
})
}
return this._super(...arguments); return this._super(...arguments);
}, },
setRequired() { setRequired() {
@@ -172,107 +189,40 @@ patch(ListRenderer.prototype, 'jikimo_frontend.ListRenderer', {
} catch (e) { } catch (e) {
console.log(e) console.log(e)
} }
},
setColumns( merge_key) {
merge_key = merge_key.split(',')
const data = this.props.list.records
let sourceIndex = 0;
let sourceValue = merge_key.reduce((acc, key) => {
acc[key] = '';
return acc;
}, {});
data.forEach((item, index) => {
if(!item.colspan) {
item.colspan = 1;
}
const itemValue = merge_key.reduce((acc, key) => {
acc[key] = item.data[key];
return acc;
}, {});
if(JSON.stringify(itemValue) == JSON.stringify(sourceValue)) {
data[sourceIndex].colspan ++ ;
item.colspan = 0;
} else {
sourceIndex = index;
sourceValue = itemValue;
}
})
return data
},
getFieldModifiers(xmlString) {
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(xmlString, "text/xml");
// 提取 <tree> 的 modifiers
const treeElement = xmlDoc.querySelector("tree");
const treeModifiers = treeElement.getAttribute("modifiers");
if (treeModifiers) {
const parsedTreeModifiers = JSON.parse(treeModifiers);
console.log("Tree Modifiers:", parsedTreeModifiers);
return parsedTreeModifiers;
}
return null;
},
mergeColumns(merge_fields, data) {
const dom = this.tableRef.el
const thead = $(dom).children('thead')
const tbody = $(dom).children('tbody')
let row_no = 0
tbody.children('tr.o_data_row').each(function () {
const tr = $(this)
const td = tr.children('td')
const index = $(this).index()
const col = data[index].colspan
row_no ++
if(col == 0) {
row_no --
}
td.eq(0).text(row_no).attr('rowspan', col)
if(col == 0) {
td.eq(0).remove()
}
td.each(function () {
if(merge_fields.indexOf($(this).attr('name')) >= 0) {
$(this).attr('rowspan', col)
if(col == 0) {
$(this).remove()
}
}
})
})
}
})
patch(FormLabel.prototype, 'jikimo_frontend.FormLabel', {
get className() {
const { invalid, empty, readonly } = fieldVisualFeedback(
this.props.fieldInfo.FieldComponent,
this.props.record,
this.props.fieldName,
this.props.fieldInfo
);
const classes = this.props.className ? [this.props.className] : [];
const otherRequired = filedRequiredList[this.props.fieldName]
if(this.props.fieldInfo?.rawAttrs?.class?.indexOf('custom_required') >= 0 || otherRequired) {
classes.push('custom_required_add')
}
if (invalid) {
classes.push("o_field_invalid");
}
if (empty) {
classes.push("o_form_label_empty");
}
if (readonly) {
classes.push("o_form_label_readonly");
}
return classes.join(" ");
} }
}) })
// 根据进度条设置水印
// const statusbar_params = {
// '已完工': 'bg-primary',
// '完成': 'bg-primary',
// '采购订单': 'bg-primary',
// '作废': 'bg-danger',
// '封存(报废)': 'bg-danger',
// }
// patch(StatusBarField.prototype, 'jikimo_frontend.StatusBarField', {
// setup() {
// owl.onMounted(this.ribbons);
// return this._super(...arguments);
// },
// ribbons() {
// try {
// const dom = $('.o_form_sheet.position-relative')
// const status = statusbar_params[this.currentName]
// if(status && dom.length) {
// dom.prepend(`<div class="o_widget o_widget_web_ribbon">
// <div class="ribbon ribbon-top-right">
// <span class="bg-opacity-75 ${status}" title="">${this.currentName}</span>
// </div>
// </div>`)
// }
// } catch (e) {
// console.log(e)
// }
// }
// })
$(function () { $(function () {
function customRequired() { function customRequired() {
@@ -281,6 +231,7 @@ $(function () {
clearInterval(timer) clearInterval(timer)
timer = setInterval(() => { timer = setInterval(() => {
timer_count++ timer_count++
const dom = $('.custom_required')
let tableDom = $('.table_custom_required') let tableDom = $('.table_custom_required')
if (tableDom.length) { if (tableDom.length) {
tableDom = tableDom.eq(0).parents('tr').children('.table_custom_required') tableDom = tableDom.eq(0).parents('tr').children('.table_custom_required')
@@ -292,6 +243,17 @@ $(function () {
}) })
clearInterval(timer) clearInterval(timer)
} }
if (dom.length) {
dom.each(function () {
const requiredDom = $(this).parent().prev().find('label')
let t = requiredDom.html()
if (t && t.indexOf('c*') < 0) {
t = '<i class="c*" style="color: red;margin-left: -4px">*</i>' + t
}
requiredDom.html(t)
})
clearInterval(timer)
}
if (timer_count == 20) { if (timer_count == 20) {
clearInterval(timer) clearInterval(timer)
} }

View File

@@ -10,6 +10,7 @@
</t> </t>
<!-- 暂存,同一份文件中有问题,拆分后正常工作 --> <!-- 暂存,同一份文件中有问题,拆分后正常工作 -->
<!-- <t t-name="og.web.ListRenderer.Rows" t-inherit="web.ListRenderer.Rows" t-inherit-mode="extension"> --> <!-- <t t-name="og.web.ListRenderer.Rows" t-inherit="web.ListRenderer.Rows" t-inherit-mode="extension"> -->

View File

@@ -1,3 +0,0 @@
# -*- coding: utf-8 -*-
from . import models
from . import wizard

View File

@@ -1,25 +0,0 @@
# -*- coding: utf-8 -*-
{
'name': '机企猫 采购申请',
'version': '16.0.1.0.0',
'summary': """ 机企猫 采购申请 """,
'author': '机企猫',
'website': 'https://bfw.jikimo.com',
'category': 'purchase',
'depends': ['sf_manufacturing', 'purchase_request'],
'data': [
'views/sale_order_view.xml',
'views/mrp_production.xml',
'views/purchase_request_view.xml',
'wizard/purchase_request_line_make_purchase_order_view.xml',
],
# 'assets': {
# 'web.assets_backend': [
# 'jikimo_purchase_request/static/src/**/*'
# ],
# },
'application': True,
'installable': True,
'auto_install': False,
'license': 'LGPL-3',
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +0,0 @@
# -*- coding: utf-8 -*-
from . import product_template
from . import purchase_request
from . import sale_order
from . import mrp_production
from . import purchase_order
from . import stock_rule

View File

@@ -1,39 +0,0 @@
from odoo import fields, models, api, _
class MrpProduction(models.Model):
_inherit = 'mrp.production'
pr_mp_count = fields.Integer('采购申请单数量', compute='_compute_pr_mp_count', store=True)
@api.depends('state')
def _compute_pr_mp_count(self):
for item in self:
pr_ids = self.env['purchase.request'].sudo().search([('origin', 'like', item.name)])
if pr_ids:
item.pr_mp_count = len(pr_ids)
else:
item.pr_mp_count = 0
def action_view_pr_mp(self):
"""
采购请求
"""
self.ensure_one()
pr_ids = self.env['purchase.request'].sudo().search([('origin', 'like', self.name)])
action = {
'res_model': 'purchase.request',
'type': 'ir.actions.act_window',
}
if len(pr_ids) == 1:
action.update({
'view_mode': 'form',
'res_id': pr_ids[0].id,
})
else:
action.update({
'name': _("%s生成采购请求单", self.name),
'domain': [('id', 'in', pr_ids)],
'view_mode': 'tree,form',
})
return action

View File

@@ -1,19 +0,0 @@
from odoo import models, fields
class ProductTemplate(models.Model):
_inherit = 'product.template'
purchase_request_id = fields.Many2one('purchase.request', string='采购申请')
def no_bom_product_create(self, product_id, item, order_id, route_type, i, finish_product):
""" 创建坯料时,复制采购申请 """
template_id = super(ProductTemplate, self).no_bom_product_create(product_id, item, order_id, route_type, i,
finish_product)
template_id.purchase_request = product_id.purchase_request
return template_id
def copy_template(self, product_template_id):
""" 复制成品模板时,复制采购申请 """
super(ProductTemplate, self).copy_template(product_template_id)
self.purchase_request = product_template_id.purchase_request

View File

@@ -1,16 +0,0 @@
from odoo import api, fields, models, _
class PurchaseOrder(models.Model):
_inherit = 'purchase.order'
state = fields.Selection([
('draft', '询价'),
('sent', '发送询价'),
('to approve', '待批准'),
("approved", "已批准"),
('purchase', '采购订单'),
('done', '完成'),
('cancel', '取消'),
('rejected', '已驳回')
], string='Status', readonly=True, index=True, copy=False, default='draft', tracking=True)

View File

@@ -1,100 +0,0 @@
import re
import ast
from odoo import models, fields, api
class PurchaseRequest(models.Model):
_inherit = 'purchase.request'
_description = '采购申请'
# 为state添加取消状态
state = fields.Selection(
selection_add=[('cancel', '已取消')],
ondelete={'cancel': 'set default'} # 添加 ondelete 策略
)
rule_new_add = fields.Boolean('采购请求为规则创建', default=False, compute='_compute_state', store=True)
@api.depends('state')
def _compute_state(self):
for pr in self:
if pr.state != 'draft' and pr.rule_new_add:
pr.rule_new_add = False
def action_view_purchase_order(self):
action = super(PurchaseRequest, self).action_view_purchase_order()
origin_context = ast.literal_eval(action['context'])
if 'search_default_draft' in origin_context:
origin_context.pop('search_default_draft')
action['context'] = origin_context
return action
class PurchaseRequestLine(models.Model):
_inherit = 'purchase.request.line'
_description = '采购申请明细'
origin = fields.Char(string="Source Document")
part_number = fields.Char('零件图号', store=True, compute='_compute_part_number')
part_name = fields.Char('零件名称', store=True, compute='_compute_part_number')
related_product = fields.Many2one('product.product', string='关联产品',
help='经此产品工艺加工成的成品')
supply_method = fields.Selection([
('automation', "自动化产线加工"),
('manual', "人工线下加工"),
('purchase', "外购"),
('outsourcing', "委外加工"),
], string='供货方式', compute='_compute_supply_method', store=True)
@api.depends('origin')
def _compute_supply_method(self):
for prl in self:
order_ids = []
if not prl.origin:
continue
origin = [origin.replace(' ', '') for origin in prl.origin.split(',')]
if 'S' in prl.origin:
# 原单据是销售订单
order_ids = self.env['sale.order'].sudo().search([('name', 'in', origin)]).ids
elif 'MO' in prl.origin:
# 原单据是制造订单
mp_ids = self.env['mrp.production'].sudo().search([('name', 'in', origin)])
order_ids = [mp_id.sale_order_id.id for mp_id in mp_ids] if mp_ids else []
elif 'WH' in prl.origin:
# 原单据是调拨单
sp_ids = self.env['stock.picking'].sudo().search([('name', 'in', origin)])
order_ids = [sp_id.sale_order_id.id for sp_id in sp_ids] if sp_ids else []
order_line = self.env['sale.order.line'].sudo().search(
[('product_id', '=', prl.product_id.id), ('order_id', 'in', order_ids)])
if order_line:
prl.supply_method = order_line[0].supply_method
else:
prl.supply_method = None
@api.depends('product_id')
def _compute_part_number(self):
for record in self:
if record.part_number and record.part_name:
continue
if record.product_id.categ_id.name == '坯料':
product_name = ''
match = re.search(r'(S\d{5}-\d)', record.product_id.name)
# 如果匹配成功,提取结果
if match:
product_name = match.group(0)
sale_order_name = ''
match_sale = re.search(r'S(\d+)', record.product_id.name)
if match_sale:
sale_order_name = match_sale.group(0)
sale_order = self.env['sale.order'].sudo().search(
[('name', '=', sale_order_name)])
if sale_order:
filtered_order_line = sale_order.order_line.filtered(
lambda order_line: re.search(f'{product_name}$', order_line.product_id.name)
)
record.part_number = filtered_order_line.product_id.part_number
record.part_name = filtered_order_line.product_id.part_name
else:
record.part_number = record.product_id.part_number
record.part_name = record.product_id.part_name

View File

@@ -1,50 +0,0 @@
from odoo import fields, models, api, _
class StatusChange(models.Model):
_inherit = 'sale.order'
# def action_confirm(self):
# res = super(StatusChange, self).action_confirm()
# # 采购申请自动确认
# pr_ids = self.env["purchase.request"].sudo().search(
# [('origin', 'like', self.name), ('rule_new_add', '=', True)])
# if pr_ids:
# pr_ids.write({'need_validation': False})
# pr_ids.write({"state": "approved"})
# return res
purchase_request_purchase_order_count = fields.Integer('采购申请单数量', compute='_compute_purchase_request_count',
store=True)
@api.depends('state')
def _compute_purchase_request_count(self):
for so in self:
pr_ids = self.env['purchase.request'].sudo().search([('origin', 'like', so.name)])
if pr_ids:
so.purchase_request_purchase_order_count = len(pr_ids)
else:
so.purchase_request_purchase_order_count = 0
def action_view_purchase_request_purchase_orders(self):
"""
采购请求
"""
self.ensure_one()
pr_ids = self.env['purchase.request'].sudo().search([('origin', 'like', self.name)])
action = {
'res_model': 'purchase.request',
'type': 'ir.actions.act_window',
}
if len(pr_ids) == 1:
action.update({
'view_mode': 'form',
'res_id': pr_ids[0].id,
})
else:
action.update({
'name': _("%s生成采购请求单", self.name),
'domain': [('id', 'in', pr_ids)],
'view_mode': 'tree,form',
})
return action

View File

@@ -1,56 +0,0 @@
from odoo import api, fields, models
class StockRule(models.Model):
_inherit = "stock.rule"
def create_purchase_request(self, procurement_group):
"""
Create a purchase request containing procurement order product.
"""
procurement = procurement_group[0]
rule = procurement_group[1]
purchase_request_model = self.env["purchase.request"]
purchase_request_line_model = self.env["purchase.request.line"]
cache = {}
pr = self.env["purchase.request"]
domain = rule._make_pr_get_domain(procurement.values)
if domain in cache:
pr = cache[domain]
elif domain:
pr = self.env["purchase.request"].search([dom for dom in domain])
pr = pr[0] if pr else False
cache[domain] = pr
if not pr:
request_data = rule._prepare_purchase_request(
procurement.origin, procurement.values
)
request_data.update({'rule_new_add': True})
pr = purchase_request_model.create(request_data)
cache[domain] = pr
elif (
not pr.origin
or procurement.origin not in pr.origin.split(", ")
and procurement.origin != "/"
):
if pr.origin:
if procurement.origin:
pr.write({"origin": pr.origin + ", " + procurement.origin})
else:
pr.write({"origin": procurement.origin})
# Create Line
request_line_data = rule._prepare_purchase_request_line(pr, procurement)
request_line_data.update({'origin': procurement.origin})
purchase_request_line_model.create(request_line_data)
def _run_buy(self, procurements):
res = super(StockRule, self)._run_buy(procurements)
# 判断是否根据规则生成新的采购申请单据,如果生成则修改状态为 approved
origins = list(set([procurement[0].origin for procurement in procurements]))
for origin in origins:
pr_ids = self.env["purchase.request"].sudo().search(
[('origin', 'like', origin), ('rule_new_add', '=', True), ('state', '=', 'draft')])
if pr_ids:
pr_ids.write({'need_validation': False})
pr_ids.write({"state": "approved", 'need_validation': True, 'rule_new_add': False})
return res

View File

@@ -1,21 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="mrp_production_inherited_form_purchase_request" model="ir.ui.view">
<field name="name">mrp.production.inherited.form.purchase.request</field>
<field name="model">mrp.production</field>
<field name="inherit_id" ref="mrp.mrp_production_form_view"/>
<field name="arch" type="xml">
<xpath expr="//button[@name='action_view_mo_delivery']" position="before">
<button class="oe_stat_button" name="action_view_pr_mp" type="object" icon="fa-credit-card"
attrs="{'invisible': [('pr_mp_count', '=', 0)]}">
<div class="o_field_widget o_stat_info">
<span class="o_stat_value">
<field name="pr_mp_count"/>
</span>
<span class="o_stat_text">采购申请</span>
</div>
</button>
</xpath>
</field>
</record>
</odoo>

View File

@@ -1,66 +0,0 @@
<odoo>
<record id="view_purchase_request_form_sf" model="ir.ui.view">
<field name="name">purchase.request.sf.form</field>
<field name="model">purchase.request</field>
<field name="inherit_id" ref="purchase_request.view_purchase_request_form"/>
<field name="arch" type="xml">
<xpath expr="//button[@name='button_draft']" position="attributes">
<attribute name="string">重置草稿</attribute>
</xpath>
<xpath expr="//field[@name='line_ids']//field[@name='purchased_qty']" position="after">
<field name="supply_method"/>
</xpath>
<xpath expr="//field[@name='line_ids']//field[@name='name']" position="replace">
<field name="related_product"/>
<field name="part_number"/>
<field name="part_name"/>
</xpath>
</field>
</record>
<record id="view_purchase_request_line_tree_sf" model="ir.ui.view">
<field name="name">purchase.request.line.sf.tree</field>
<field name="model">purchase.request.line</field>
<field name="inherit_id" ref="purchase_request.purchase_request_line_tree"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='requested_by']" position="replace">
<field name="supply_method"/>
</xpath>
<xpath expr="//field[@name='assigned_to']" position="attributes">
<attribute name="invisible">True</attribute>
</xpath>
<xpath expr="//field[@name='name']" position="attributes">
<attribute name="invisible">True</attribute>
</xpath>
<xpath expr="//field[@name='supplier_id']" position="after">
<field name="requested_by" widget="many2one_avatar_user"/>
<field name="assigned_to" widget="many2one_avatar_user" invisible="1"/>
</xpath>
<xpath expr="//field[@name='purchased_qty']" position="attributes">
<attribute name="string">采购数量</attribute>
</xpath>
<xpath expr="//field[@name='purchase_state']" position="attributes">
<attribute name="string">订单状态</attribute>
</xpath>
<xpath expr="//field[@name='product_id']" position="after">
<field name="related_product"/>
<field name="part_number"/>
<field name="part_name" invisible="1"/>
</xpath>
</field>
</record>
<record id="view_purchase_request_line_search_sf" model="ir.ui.view">
<field name="name">purchase.request.line.sf.search</field>
<field name="model">purchase.request.line</field>
<field name="inherit_id" ref="purchase_request.purchase_request_line_search"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='product_id']" position="after">
<field name="supply_method"/>
<field name="related_product"/>
<field name="part_number"/>
<field name="part_name"/>
</xpath>
</field>
</record>
</odoo>

View File

@@ -1,19 +0,0 @@
<odoo>
<record id="sale_order_inherited_form_purchase_request_sf" model="ir.ui.view">
<field name="name">sale.order.inherited.form.purchase.request</field>
<field name="model">sale.order</field>
<field name="inherit_id" ref="sale_purchase.sale_order_inherited_form_purchase"/>
<field name="arch" type="xml">
<xpath expr="//button[@name='action_preview_sale_order']" position="before">
<button class="oe_stat_button" name="action_view_purchase_request_purchase_orders" type="object" icon="fa-credit-card"
groups='purchase.group_purchase_user'
attrs="{'invisible': [('purchase_request_purchase_order_count', '=', 0)]}">
<div class="o_field_widget o_stat_info">
<span class="o_stat_value"><field name="purchase_request_purchase_order_count"/></span>
<span class="o_stat_text">采购申请</span>
</div>
</button>
</xpath>
</field>
</record>
</odoo>

View File

@@ -1,3 +0,0 @@
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0)
from . import purchase_request_line_make_purchase_order

View File

@@ -1,108 +0,0 @@
# Copyright 2018-2019 ForgeFlow, S.L.
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0).
from datetime import datetime
from odoo import _, api, fields, models
from odoo.exceptions import UserError, ValidationError
from odoo.tools import get_lang
class PurchaseRequestLineMakePurchaseOrder(models.TransientModel):
_inherit = "purchase.request.line.make.purchase.order"
def make_purchase_order(self):
res = []
purchase_obj = self.env["purchase.order"]
po_line_obj = self.env["purchase.order.line"]
purchase = False
if len(set([item_id.line_id.supply_method for item_id in self.item_ids])) > 1:
raise ValidationError('不同供货方式不可合并创建询价单!')
for item in self.item_ids:
line = item.line_id
if item.product_qty <= 0.0:
raise UserError(_("Enter a positive quantity."))
if self.purchase_order_id:
purchase = self.purchase_order_id
if not purchase:
po_data = self._prepare_purchase_order(
line.request_id.picking_type_id,
line.request_id.group_id,
line.company_id,
line.request_id.origin,
)
purchase = purchase_obj.create(po_data)
# Look for any other PO line in the selected PO with same
# product and UoM to sum quantities instead of creating a new
# po line
domain = self._get_order_line_search_domain(purchase, item)
available_po_lines = po_line_obj.search(domain)
new_pr_line = True
# If Unit of Measure is not set, update from wizard.
if not line.product_uom_id:
line.product_uom_id = item.product_uom_id
# Allocation UoM has to be the same as PR line UoM
alloc_uom = line.product_uom_id
wizard_uom = item.product_uom_id
if available_po_lines and not item.keep_description:
new_pr_line = False
po_line = available_po_lines[0]
po_line.purchase_request_lines = [(4, line.id)]
po_line.move_dest_ids |= line.move_dest_ids
po_line_product_uom_qty = po_line.product_uom._compute_quantity(
po_line.product_uom_qty, alloc_uom
)
wizard_product_uom_qty = wizard_uom._compute_quantity(
item.product_qty, alloc_uom
)
all_qty = min(po_line_product_uom_qty, wizard_product_uom_qty)
self.create_allocation(po_line, line, all_qty, alloc_uom)
else:
po_line_data = self._prepare_purchase_order_line(purchase, item)
if item.keep_description:
po_line_data["name"] = item.name
po_line = po_line_obj.create(po_line_data)
po_line_product_uom_qty = po_line.product_uom._compute_quantity(
po_line.product_uom_qty, alloc_uom
)
wizard_product_uom_qty = wizard_uom._compute_quantity(
item.product_qty, alloc_uom
)
all_qty = min(po_line_product_uom_qty, wizard_product_uom_qty)
self.create_allocation(po_line, line, all_qty, alloc_uom)
self._post_process_po_line(item, po_line, new_pr_line)
res.append(purchase.id)
purchase_requests = self.item_ids.mapped("request_id")
purchase_requests.button_in_progress()
return {
"domain": [("id", "in", res)],
"name": _("RFQ"),
"view_mode": "tree,form",
"res_model": "purchase.order",
"view_id": False,
"context": False,
"type": "ir.actions.act_window",
}
def _check_valid_request_line(self, request_line_ids):
for line in self.env["purchase.request.line"].browse(request_line_ids):
if line.request_id.state not in ["approved", "in_progress"]:
raise UserError(
_("采购申请 %s 未审批或未进行中")
% line.request_id.name
)
super(PurchaseRequestLineMakePurchaseOrder, self)._check_valid_request_line(request_line_ids)
@api.model
def check_group(self, request_lines):
# 去掉合并必须同一采购组的限制
pass
class PurchaseRequestLineMakePurchaseOrderItem(models.TransientModel):
_inherit = "purchase.request.line.make.purchase.order.item"
supply_method = fields.Selection(related='line_id.supply_method', string='供货方式')

View File

@@ -1,13 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="view_purchase_request_line_make_purchase_order_sf" model="ir.ui.view">
<field name="name">Purchase Request Line Make Purchase Order sf</field>
<field name="model">purchase.request.line.make.purchase.order</field>
<field name="inherit_id" ref="purchase_request.view_purchase_request_line_make_purchase_order"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='item_ids']//field[@name='keep_description']" position="before">
<field name="supply_method"/>
</xpath>
</field>
</record>
</odoo>

View File

@@ -1,3 +0,0 @@
# -*- coding: utf-8 -*-
from . import models

View File

@@ -1,28 +0,0 @@
# -*- coding: utf-8 -*-
{
'name': "机企猫 采购审批流程",
'summary': """
Short (1 phrase/line) summary of the module's purpose, used as
subtitle on modules listing or apps.openerp.com""",
'description': """
Long description of module's purpose
""",
'author': "My Company",
'website': "https://www.yourcompany.com",
# Categories can be used to filter modules in modules listing
# Check https://github.com/odoo/odoo/blob/16.0/odoo/addons/base/data/ir_module_category_data.xml
# for the full list
'category': 'Uncategorized',
'version': '0.1',
# any module necessary for this one to work correctly
'depends': ['purchase_request_tier_validation'],
# always loaded
'data': [
],
}

View File

@@ -1,24 +0,0 @@
from odoo import models, fields, api, _
from odoo.exceptions import ValidationError
import logging
_logger = logging.getLogger(__name__)
class PurchaseRequest(models.Model):
_inherit = 'purchase.request'
def _validate_tier(self, tiers=False):
res = super(PurchaseRequest, self)._validate_tier(tiers)
# 检查是否所有审批都已通过
all_approved = all(
tier_review.status == 'approved'
for tier_review in self.review_ids
)
if self.review_ids and all_approved: # 确保有审批记录
self.state = 'approved'
return res

View File

@@ -1,4 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from . import controllers
from . import models from . import models
from . import wizards from . import wizards

View File

@@ -1,12 +1,14 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
{ {
'name': "机企猫 采购申请审批流程", 'name': "机企猫 采购审批流程",
'summary': """ 'summary': """
采购申请审批流程""", Short (1 phrase/line) summary of the module's purpose, used as
subtitle on modules listing or apps.openerp.com""",
'description': """ 'description': """
采购申请审批流程""", Long description of module's purpose
""",
'author': "My Company", 'author': "My Company",
'website': "https://www.yourcompany.com", 'website': "https://www.yourcompany.com",
@@ -22,6 +24,9 @@
# always loaded # always loaded
'data': [ 'data': [
'security/ir.model.access.csv',
'data/documents_data.xml',
'wizards/upload_file_wizard_view.xml',
'views/views.xml', 'views/views.xml',
], ],
# only loaded in demonstration mode # only loaded in demonstration mode

View File

@@ -1,3 +1,3 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from . import models from . import controllers

View File

@@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# from odoo import http
# class JikimoPurchaseTierValidation(http.Controller):
# @http.route('/jikimo_purchase_tier_validation/jikimo_purchase_tier_validation', auth='public')
# def index(self, **kw):
# return "Hello, world"
# @http.route('/jikimo_purchase_tier_validation/jikimo_purchase_tier_validation/objects', auth='public')
# def list(self, **kw):
# return http.request.render('jikimo_purchase_tier_validation.listing', {
# 'root': '/jikimo_purchase_tier_validation/jikimo_purchase_tier_validation',
# 'objects': http.request.env['jikimo_purchase_tier_validation.jikimo_purchase_tier_validation'].search([]),
# })
# @http.route('/jikimo_purchase_tier_validation/jikimo_purchase_tier_validation/objects/<model("jikimo_purchase_tier_validation.jikimo_purchase_tier_validation"):obj>', auth='public')
# def object(self, obj, **kw):
# return http.request.render('jikimo_purchase_tier_validation.object', {
# 'object': obj
# })

View File

@@ -0,0 +1,11 @@
<?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">10</field>
</record>
</data>
</odoo>

View File

@@ -0,0 +1,30 @@
<odoo>
<data>
<!--
<record id="object0" model="jikimo_purchase_tier_validation.jikimo_purchase_tier_validation">
<field name="name">Object 0</field>
<field name="value">0</field>
</record>
<record id="object1" model="jikimo_purchase_tier_validation.jikimo_purchase_tier_validation">
<field name="name">Object 1</field>
<field name="value">10</field>
</record>
<record id="object2" model="jikimo_purchase_tier_validation.jikimo_purchase_tier_validation">
<field name="name">Object 2</field>
<field name="value">20</field>
</record>
<record id="object3" model="jikimo_purchase_tier_validation.jikimo_purchase_tier_validation">
<field name="name">Object 3</field>
<field name="value">30</field>
</record>
<record id="object4" model="jikimo_purchase_tier_validation.jikimo_purchase_tier_validation">
<field name="name">Object 4</field>
<field name="value">40</field>
</record>
-->
</data>
</odoo>

File diff suppressed because it is too large Load Diff

View File

@@ -9,8 +9,6 @@ class jikimo_purchase_tier_validation(models.Model):
_name = 'purchase.order' _name = 'purchase.order'
_inherit = ['purchase.order', 'tier.validation'] _inherit = ['purchase.order', 'tier.validation']
_description = "采购订单" _description = "采购订单"
_state_from = ["draft", "to approve", "rejected"]
_state_to = ["approved", "purchase"]
_tier_validation_buttons_xpath = "/form/header/button[@id='draft_confirm'][1]" _tier_validation_buttons_xpath = "/form/header/button[@id='draft_confirm'][1]"
@@ -22,9 +20,13 @@ class jikimo_purchase_tier_validation(models.Model):
is_upload_contract_file = fields.Boolean(string='是否已上传合同文件', default=False) is_upload_contract_file = fields.Boolean(string='是否已上传合同文件', default=False)
def button_confirm(self): def button_confirm(self):
# for record in self: for record in self:
# if record.need_validation and not record.validation_status == 'validated': # if record.need_validation and record.validation_status != 'validated':
# raise ValidationError(_('请先完成审批。')) # raise ValidationError(_('此操作需要至少对一条记录进行审批。\n请发起审批申请。'))
if record.state in ['to approve']:
raise ValidationError(_('请先完成审批。'))
# if record.state == 'approved':
# record.state = 'purchase'
res = super(jikimo_purchase_tier_validation, self).button_confirm() res = super(jikimo_purchase_tier_validation, self).button_confirm()
for record in self: for record in self:
if record.state == 'approved': if record.state == 'approved':
@@ -37,8 +39,45 @@ class jikimo_purchase_tier_validation(models.Model):
record.message_subscribe([record.partner_id.id]) record.message_subscribe([record.partner_id.id])
return res return res
# def button_confirm(self):
# self = self.with_context(skip_validation=True)
# return super().button_confirm()
#
# def _check_state_conditions(self, vals):
# self.ensure_one()
# if self._context.get('skip_validation'):
# return False
# return (
# self._check_state_from_condition()
# and vals.get(self._state_field) in self._state_to
# )
def request_validation(self): def request_validation(self):
for record in self: for record in self:
error_messages = []
# 检查必填字段
required_fields = {
'partner_ref': '合同名称',
'contract_number': '合同编号'
}
missing_fields = [
name for field, name in required_fields.items()
if not record[field]
]
if missing_fields:
error_messages.append('* 如下字段要求必须填写:%s' % ''.join(missing_fields))
# 检查合同文件
if not record.contract_document_id:
error_messages.append('* 必须点击上传合同文件')
# 如果有任何错误,一次性显示所有错误信息
if error_messages:
raise ValidationError('\n'.join(error_messages))
# 添加通知消息 # 添加通知消息
if hasattr(record, 'message_post'): if hasattr(record, 'message_post'):
current_user = self.env.user.name current_user = self.env.user.name
@@ -70,6 +109,11 @@ class jikimo_purchase_tier_validation(models.Model):
return res return res
def _rejected_tier(self, tiers=False):
res = super(jikimo_purchase_tier_validation, self)._rejected_tier(tiers)
self.state = 'draft'
return res
@api.model @api.model
def _get_under_validation_exceptions(self): def _get_under_validation_exceptions(self):
res = super(jikimo_purchase_tier_validation, self)._get_under_validation_exceptions() res = super(jikimo_purchase_tier_validation, self)._get_under_validation_exceptions()

View File

@@ -0,0 +1,2 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_ir_attachment_wizard,ir.attachment.wizard,model_ir_attachment_wizard,base.group_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_ir_attachment_wizard ir.attachment.wizard model_ir_attachment_wizard base.group_user 1 1 1 1

View File

@@ -0,0 +1,24 @@
<odoo>
<data>
<!--
<template id="listing">
<ul>
<li t-foreach="objects" t-as="object">
<a t-attf-href="#{ root }/objects/#{ object.id }">
<t t-esc="object.display_name"/>
</a>
</li>
</ul>
</template>
<template id="object">
<h1><t t-esc="object.display_name"/></h1>
<dl>
<t t-foreach="object._fields" t-as="field">
<dt><t t-esc="field"/></dt>
<dd><t t-esc="object[field]"/></dd>
</t>
</dl>
</template>
-->
</data>
</odoo>

View File

@@ -23,10 +23,76 @@
<xpath expr="//header/field[@name='state']" position="replace"> <xpath expr="//header/field[@name='state']" position="replace">
<field name="state" widget="statusbar" statusbar_visible="draft,sent,to approve, approved, purchase" readonly="1"/> <field name="state" widget="statusbar" statusbar_visible="draft,sent,to approve, approved, purchase" readonly="1"/>
</xpath> </xpath>
<xpath expr="//header/button[last()]" position="after"> <xpath expr="//header/button[last()]" position="after">
<button name="button_cancel" states="draft,to approve,sent,purchase" string="取消" type="object" data-hotkey="x" /> <button name="button_cancel" states="draft,to approve,sent,purchase" string="取消" type="object" data-hotkey="x" />
</xpath> </xpath>
<xpath expr="//header/button[@name='action_rfq_send'][1]" position="before">
<field name="validation_status" invisible="1"/>
<field name="is_upload_contract_file" invisible="1"/>
<button name="upload_contract_file" string="上传合同" type="object" class="oe_highlight" attrs="{'invisible': ['|', '|', ('validation_status', '!=', 'no'), ('is_upload_contract_file', '=', True), ('state', 'not in', ['draft', 'sent'])]}"/>]}"/>
<button name="delete_contract_file" string="删除合同" type="object" class="oe_highlight" attrs="{'invisible': ['|', ('validation_status', '!=', 'no'), ('is_upload_contract_file', '=', False)]}"/>
</xpath>
<xpath expr="//notebook/page[1]" position="before">
<page string="合同" name="contract_documents"
attrs="{'invisible': [('contract_document_id', '=', False)]}"
autofocus="autofocus">
<group>
<group>
<field name="contract_document_id" invisible="1"/>
<field name="contract_file_name" invisible="1"/>
<field name="contract_file"
widget="adaptive_viewer"
filename="contract_file_name"/>
</group>
</group>
</page>
</xpath>
</field> </field>
</record> </record>
<!-- actions opening views on models -->
<!--
<record model="ir.actions.act_window" id="jikimo_purchase_tier_validation.action_window">
<field name="name">jikimo_purchase_tier_validation window</field>
<field name="res_model">jikimo_purchase_tier_validation.jikimo_purchase_tier_validation</field>
<field name="view_mode">tree,form</field>
</record>
-->
<!-- server action to the one above -->
<!--
<record model="ir.actions.server" id="jikimo_purchase_tier_validation.action_server">
<field name="name">jikimo_purchase_tier_validation server</field>
<field name="model_id" ref="model_jikimo_purchase_tier_validation_jikimo_purchase_tier_validation"/>
<field name="state">code</field>
<field name="code">
action = {
"type": "ir.actions.act_window",
"view_mode": "tree,form",
"res_model": model._name,
}
</field>
</record>
-->
<!-- Top menu item -->
<!--
<menuitem name="jikimo_purchase_tier_validation" id="jikimo_purchase_tier_validation.menu_root"/>
-->
<!-- menu categories -->
<!--
<menuitem name="Menu 1" id="jikimo_purchase_tier_validation.menu_1" parent="jikimo_purchase_tier_validation.menu_root"/>
<menuitem name="Menu 2" id="jikimo_purchase_tier_validation.menu_2" parent="jikimo_purchase_tier_validation.menu_root"/>
-->
<!-- actions -->
<!--
<menuitem name="List" id="jikimo_purchase_tier_validation.menu_1_list" parent="jikimo_purchase_tier_validation.menu_1"
action="jikimo_purchase_tier_validation.action_window"/>
<menuitem name="Server to list" id="jikimo_purchase_tier_validation" parent="jikimo_purchase_tier_validation.menu_2"
action="jikimo_purchase_tier_validation.action_server"/>
-->
</data> </data>
</odoo> </odoo>

View File

@@ -1 +1,2 @@
from . import comment_wizard from . import upload_file_wizard
from . import comment_wizard

View File

@@ -0,0 +1,114 @@
from odoo import models, fields, api, _
class IrAttachmentWizard(models.TransientModel):
_name = 'ir.attachment.wizard'
_description = '文件上传向导'
attachment = fields.Binary(string='选择文件', required=True)
filename = fields.Char(string='文件名')
res_model = fields.Char()
res_id = fields.Integer()
# def action_upload_file(self):
# self.ensure_one()
# # 首先创建 ir.attachment
# attachment = self.env['ir.attachment'].create({
# 'name': self.filename,
# 'type': 'binary',
# 'datas': self.attachment,
# 'res_model': self.res_model,
# 'res_id': self.res_id,
# })
#
# # 获取默认的文档文件夹
# workspace = self.env['documents.folder'].search([('name', '=', '采购合同')], limit=1)
#
# # 创建 documents.document 记录
# document = self.env['documents.document'].create({
# 'name': self.filename,
# 'attachment_id': attachment.id,
# 'folder_id': workspace.id,
# 'res_model': self.res_model,
# 'res_id': self.res_id,
# })
#
# return {
# 'type': 'ir.actions.client',
# 'tag': 'display_notification',
# 'params': {
# 'title': _('成功'),
# 'message': _('文件上传成功'),
# 'type': 'success',
# }
# }
def action_upload_file(self):
self.ensure_one()
# 获取当前用户的 partner_id
current_partner = self.env.user.partner_id
# 首先创建 ir.attachment
attachment = self.env['ir.attachment'].create({
'name': self.filename,
'type': 'binary',
'datas': self.attachment,
'res_model': self.res_model,
'res_id': self.res_id,
})
# 获取默认的文档文件夹
workspace = self.env['documents.folder'].search([('name', '=', '采购合同')], limit=1)
# 创建 documents.document 记录
document = self.env['documents.document'].create({
'name': self.filename,
'attachment_id': attachment.id,
'folder_id': workspace.id,
'res_model': self.res_model,
'res_id': self.res_id,
'partner_id': current_partner.id,
})
# 更新采购订单的合同文档字段
purchase_order = self.env['purchase.order'].browse(self.res_id)
purchase_order.write({
'contract_document_id': document.id,
'is_upload_contract_file': True
})
# 显示成功消息并关闭向导
message = {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': _('成功'),
'message': _('文件上传成功'),
'type': 'success',
'sticky': False, # 自动消失
'next': {
'type': 'ir.actions.act_window_close'
}
}
}
return message
# def action_upload_file(self):
# self.ensure_one()
# attachment = self.env['ir.attachment'].create({
# 'name': self.filename,
# 'type': 'binary',
# 'datas': self.attachment,
# 'res_model': self.res_model,
# 'res_id': self.res_id,
# })
# return {
# 'type': 'ir.actions.client',
# 'tag': 'display_notification',
# 'params': {
# 'title': _('成功'),
# 'message': _('文件上传成功'),
# 'type': 'success',
# }
# }

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_upload_file_wizard_form" model="ir.ui.view">
<field name="name">ir.attachment.wizard.form</field>
<field name="model">ir.attachment.wizard</field>
<field name="arch" type="xml">
<form string="上传文件">
<group>
<field name="attachment" widget="binary" filename="filename" options="{'accepted_file_extensions': '.pdf,.doc,.docx,.jpg,.jpeg,.png'}"/>
<field name="filename" invisible="1"/>
<field name="res_model" invisible="1"/>
<field name="res_id" invisible="1"/>
</group>
<footer>
<button name="action_upload_file" string="确认上传" type="object" class="btn-primary"/>
<button string="取消" class="btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
</odoo>

View File

@@ -9,6 +9,5 @@ class MrpBom(models.Model):
# 成品的供应商从模板中获取 # 成品的供应商从模板中获取
if product_type == 'product': if product_type == 'product':
if product.product_tmpl_id.seller_ids: bom_id.subcontractor_id = product.product_tmpl_id.seller_ids.partner_id.id
bom_id.subcontractor_id = product.product_tmpl_id.seller_ids[-1].partner_id.id
return bom_id return bom_id

View File

@@ -1,3 +0,0 @@
from . import models
from . import controllers
from . import wizards

View File

@@ -1,32 +0,0 @@
{
'name': '机企猫 测试助手',
'version': '16.0.1.0.0',
'category': 'Technical',
'summary': '测试数据初始化工具',
'description': """
用于初始化测试环境数据的工具模块
""",
'author': 'Jikimo',
'website': 'www.jikimo.com',
'depends': [
'base',
'sale_management',
'purchase',
'mrp',
'stock',
'account'
],
'data': [
'security/ir.model.access.csv',
'wizards/jikimo_data_clean_wizard.xml',
],
'assets': {
'web.assets_backend': [
'jikimo_test_assistant/static/src/js/data_clean_confirm.js',
],
},
'installable': True,
'application': False,
'auto_install': False,
'license': 'LGPL-3',
}

View File

@@ -1,2 +0,0 @@
# -*- coding: utf-8 -*-
from . import main

View File

@@ -1,86 +0,0 @@
from odoo import http
import logging
import os
import json
import sys
_logger = logging.getLogger(__name__)
class Main(http.Controller):
@http.route('/api/pdf2image', type='http', auth='public', methods=['POST'], csrf=False)
def convert_pdf_to_image(self, **kwargs):
"""将PDF文件转换为图片文件
Returns:
dict: 包含转换后图片url的字典
"""
res = {}
try:
# 检查poppler是否可用
# if sys.platform.startswith('win'):
# if not os.environ.get('POPPLER_PATH'):
# return {
# 'code': 400,
# 'msg': '请先配置POPPLER_PATH环境变量'
# }
# else:
# import shutil
# if not shutil.which('pdftoppm'):
# return {
# 'code': 400,
# 'msg': '请先安装poppler-utils'
# }
# 获取上传的PDF文件
pdf_file = kwargs.get('file')
if not pdf_file:
res = {'code': 400, 'msg': '未找到上传的PDF文件'}
# 检查文件类型
if not pdf_file.filename.lower().endswith('.pdf'):
res = {'code': 400, 'msg': '请上传PDF格式的文件'}
# 读取PDF文件内容
pdf_content = pdf_file.read()
# 使用pdf2image转换
from pdf2image import convert_from_bytes
import tempfile
# 转换PDF
with tempfile.TemporaryDirectory() as path:
images = convert_from_bytes(pdf_content)
image_urls = []
# 保存每一页为图片
for i, image in enumerate(images):
image_path = os.path.join(path, f'page_{i+1}.jpg')
image.save(image_path, 'JPEG')
# 将图片保存到ir.attachment
with open(image_path, 'rb') as img_file:
attachment = http.request.env['ir.attachment'].sudo().create({
'name': f'page_{i+1}.jpg',
'datas': img_file.read(),
'type': 'binary',
'access_token': kwargs.get('access_token') or '123'
})
image_urls.append({
'page': i+1,
'url': f'/web/content/{attachment.id}'
})
res = {
'code': 200,
'msg': '转换成功',
'data': image_urls
}
except Exception as e:
_logger.error('PDF转换失败: %s', str(e))
res = {
'code': 500,
'msg': f'转换失败: {str(e)}'
}
return json.JSONEncoder().encode(res)

View File

@@ -1 +0,0 @@

View File

@@ -1,2 +0,0 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_jikimo_data_clean_wizard,jikimo_test_assistant.jikimo_data_clean_wizard,model_jikimo_data_clean_wizard,base.group_system,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_jikimo_data_clean_wizard jikimo_test_assistant.jikimo_data_clean_wizard model_jikimo_data_clean_wizard base.group_system 1 1 1 1

View File

@@ -1,50 +0,0 @@
odoo.define('jikimo_test_assistant.action_clean_data_confirm', function (require) {
const core = require('web.core');
const ajax = require('web.ajax');
const Dialog = require('web.Dialog');
var rpc = require('web.rpc');
var _t = core._t;
async function action_clean_data_confirm(parent, {params}) {
let message = "确认清理数据?<br/>"
message += "日期:"+ params.date + "以前<br/>"
message += "模型:" + params.model_names.join('')
const dialog = new Dialog(parent, {
title: "确认",
$content: $('<div>').append(message),
buttons: [
{ text: "确认", classes: 'btn-primary jikimo_button_confirm', close: true, click: () => actionCleanDataConfirm(parent, params) },
{ text: "取消", close: true },
],
});
dialog.open();
async function actionCleanDataConfirm(parent, params) {
rpc.query({
model: 'jikimo.data.clean.wizard',
method: 'action_clean_data',
args: [params.active_id],
kwargs: {
context: params.context,
}
}).then(res => {
parent.services.action.doAction({
'type': 'ir.actions.client',
'tag': 'display_notification',
'target': 'new',
'params': {
'message': '数据清理成功!',
'type': 'success',
'sticky': false,
'next': {'type': 'ir.actions.act_window_close'},
}
});
})
}
}
core.action_registry.add('action_clean_data_confirm', action_clean_data_confirm);
return action_clean_data_confirm;
});

View File

@@ -1,2 +0,0 @@
# -*- coding: utf-8 -*-
from . import jikimo_data_clean_wizard

View File

@@ -1,99 +0,0 @@
from odoo import models, fields, api
from datetime import datetime
import logging
_logger = logging.getLogger(__name__)
class JikimoDataCleanWizard(models.TransientModel):
_name = 'jikimo.data.clean.wizard'
_description = '业务数据清理'
date = fields.Date(string='截止日期', required=True, default=fields.Date.context_today)
model_ids = fields.Many2many('ir.model', string='业务模型', domain=[
('model', 'in', [
'sale.order', # 销售订单
'purchase.order', # 采购订单
'mrp.production', # 生产订单
'stock.picking', # 库存调拨
'account.move', # 会计凭证
])
])
def action_clean_data(self):
self.ensure_one()
model_list = self.model_ids.mapped('model')
# 销售订单清理(排除已交付,已锁定,已取消)
if 'sale.order' in model_list:
self.model_cancel('sale.order', except_states=['delivered', 'done', 'cancel'])
# 采购订单清理(排除采购订单,已锁定,已取消)
if 'purchase.order' in model_list:
self.model_cancel('purchase.order', except_states=['purchase', 'done', 'cancel'])
# 生产订单清理(排除返工,报废,完成,已取消)
if 'mrp.production' in model_list:
self.model_cancel('mrp.production', except_states=['rework', 'scrap', 'done', 'cancel'])
# 工单清理 (排除返工,完成,已取消)
if 'mrp.workorder' in model_list:
self.model_cancel('mrp.production', except_states=['rework', 'done', 'cancel'])
# 排程单清理 (排除已完成,已取消)
if 'mrp.workorder' in model_list:
self.model_cancel('mrp.production', except_states=['finished', 'cancel'])
# 工单库存移动 (排除完成,已取消)
if 'stock.move' in model_list:
self.model_cancel('stock.move')
# 库存调拨清理 (排除完成,已取消)
if 'stock.picking' in model_list:
self.model_cancel('stock.picking')
# 会计凭证清理 (排除已过账,已取消)
if 'account.move' in model_list:
self.model_cancel('account.move', except_states=['posted', 'cancel'])
return True
def model_cancel(self, model_name, state_field='state', to_state='cancel',except_states=('done', 'cancel')):
table = self.env[model_name]._table
if isinstance(except_states, list):
except_states = tuple(except_states)
sql = """
UPDATE
%s SET %s = '%s'
WHERE
create_date < '%s'
AND state NOT IN %s
""" % (table, state_field, to_state, self.date.strftime('%Y-%m-%d'), except_states)
self.env.cr.execute(sql)
self.env.cr.commit()
@api.model
def get_confirm_message(self):
date_str = self.date.strftime('%Y-%m-%d') if self.date else ''
model_names = ', '.join([model.name for model in self.model_ids])
return {
'date': date_str,
'model_names': model_names
}
def action_clean_data_confirm(self):
model_names = self.model_ids.mapped('display_name')
return {
'type': 'ir.actions.client',
'tag': 'action_clean_data_confirm',
'params': {
'model_names': model_names,
'date': self.date,
'active_id': self.id,
'context': self.env.context
}
}

View File

@@ -1,47 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Form View -->
<record id="view_jikimo_data_clean_form" model="ir.ui.view">
<field name="name">jikimo.data.clean.wizard.form</field>
<field name="model">jikimo.data.clean.wizard</field>
<field name="arch" type="xml">
<form string="业务数据清理">
<sheet>
<group>
<field name="date"/>
<field name="model_ids" widget="many2many_tags"/>
</group>
</sheet>
<footer>
<button name="action_clean_data_confirm"
string="确认清理"
type="object"
class="btn-primary"/>
<button special="cancel"
string="取消"
class="btn-secondary"/>
</footer>
</form>
</field>
</record>
<!-- Action -->
<record id="action_jikimo_data_clean" model="ir.actions.act_window">
<field name="name">业务数据清理</field>
<field name="res_model">jikimo.data.clean.wizard</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
<!-- Menu -->
<menuitem id="menu_test_root"
name="测试"
parent="base.menu_custom"
sequence="100"/>
<menuitem id="menu_jikimo_data_clean"
name="业务数据清理"
parent="menu_test_root"
action="action_jikimo_data_clean"
sequence="10"/>
</odoo>

View File

@@ -4,7 +4,6 @@ import json
import logging import logging
from odoo.addons.sf_mrs_connect.controllers.controllers import Sf_Mrs_Connect from odoo.addons.sf_mrs_connect.controllers.controllers import Sf_Mrs_Connect
from odoo.addons.sf_manufacturing.controllers.controllers import Manufacturing_Connect from odoo.addons.sf_manufacturing.controllers.controllers import Manufacturing_Connect
from datetime import datetime
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
@@ -31,7 +30,6 @@ class WorkorderExceptionConroller(http.Controller):
workorder = request.env['mrp.workorder'].sudo().search([ workorder = request.env['mrp.workorder'].sudo().search([
('rfid_code', '=', ret['RfidCode']), ('rfid_code', '=', ret['RfidCode']),
('routing_type', '=', 'CNC加工'), ('routing_type', '=', 'CNC加工'),
('state', '!=', 'rework')
]) ])
if not workorder: if not workorder:
res = {'Succeed': False, 'ErrorCode': 401, 'Error': '无效的工单'} res = {'Succeed': False, 'ErrorCode': 401, 'Error': '无效的工单'}
@@ -43,10 +41,7 @@ class WorkorderExceptionConroller(http.Controller):
'exception_code': ret.get('coding'), 'exception_code': ret.get('coding'),
'exception_content': ret.get('Error', '') 'exception_content': ret.get('Error', '')
}) })
# 申请重新编程
workorder.production_id.update_programming_state(trigger_time=datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
reprogramming_reason=ret.get('Error', ''))
workorder.production_id.write({'programming_state': '编程中', 'work_state': '编程中', 'is_rework': False})
except Exception as e: except Exception as e:
res = {'Succeed': False, 'ErrorCode': 202, 'Error': e} res = {'Succeed': False, 'ErrorCode': 202, 'Error': e}
_logger.info('workder_exception error:%s' % e) _logger.info('workder_exception error:%s' % e)

View File

@@ -8,7 +8,7 @@
<field name="arch" type="xml"> <field name="arch" type="xml">
<xpath expr="//notebook/page[last()]" position="after"> <xpath expr="//notebook/page[last()]" position="after">
<field name="routing_type" invisible="1"/> <field name="routing_type" invisible="1"/>
<page string="异常记录" name="workorder_exception" attrs='{"invisible": ["!", ("individuation_page_list", "ilike", "ER")]}'> <page string="异常记录" name="workorder_exception" attrs="{'invisible': [('routing_type', '!=', 'CNC加工')]}">
<field name="exception_ids" nolabel="1" readonly="1"> <field name="exception_ids" nolabel="1" readonly="1">
<tree create="false" delete="false" edit="false"> <tree create="false" delete="false" edit="false">
<field name="exception_content" string="反馈的异常/问题信息"/> <field name="exception_content" string="反馈的异常/问题信息"/>

View File

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

View File

@@ -8,7 +8,7 @@
'sequence': 120, 'sequence': 120,
'summary': 'Control the quality of your products', 'summary': 'Control the quality of your products',
'website': 'https://www.odoo.com/app/quality', 'website': 'https://www.odoo.com/app/quality',
'depends': ['quality', 'sf_manufacturing', 'base_import'], 'depends': ['quality', 'sf_manufacturing'],
'description': """ 'description': """
Quality Control Quality Control
=============== ===============
@@ -20,15 +20,12 @@ Quality Control
""", """,
'data': [ 'data': [
'data/quality_control_data.xml', 'data/quality_control_data.xml',
'wizard/import_complex_model.xml',
'wizard/quality_wizard_view.xml',
'report/worksheet_custom_reports.xml', 'report/worksheet_custom_reports.xml',
'report/worksheet_custom_report_templates.xml', 'report/worksheet_custom_report_templates.xml',
'views/quality_views.xml', 'views/quality_views.xml',
'views/product_views.xml', 'views/product_views.xml',
'views/stock_move_views.xml', 'views/stock_move_views.xml',
'views/stock_picking_views.xml', 'views/stock_picking_views.xml',
'views/quality.check.measures.line.xml',
'wizard/quality_check_wizard_views.xml', 'wizard/quality_check_wizard_views.xml',
'security/ir.model.access.csv', 'security/ir.model.access.csv',
], ],

View File

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

View File

@@ -1,120 +0,0 @@
# -*- 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:document_id>', type='http', auth='public')
def get_public_report(self, document_id, **kw):
"""提供公开访问PDF报告的控制器"""
document = request.env['documents.document'].sudo().browse(int(document_id))
# 安全检查:确保只有质检报告文档可以被访问
if document.exists() and document.res_model == 'quality.check':
# 获取PDF内容
pdf_content = document.raw
# 返回PDF内容
return request.make_response(
pdf_content,
headers=[
('Content-Type', 'application/pdf'),
('Content-Disposition', f'inline; filename={document.name}.pdf')
]
)
return request.not_found()
@http.route('/quality/report/not_published', type='http', auth='public')
def get_not_published_report(self, **kw):
"""提供未发布报告的控制器"""
return "报告尚未发布"

View File

@@ -10,12 +10,6 @@ from odoo import api, models, fields, _
from odoo.api import depends from odoo.api import depends
from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT, float_round from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT, float_round
from odoo.osv.expression import OR 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): class QualityPoint(models.Model):
@@ -40,8 +34,7 @@ class QualityPoint(models.Model):
('day', 'Days'), ('day', 'Days'),
('week', 'Weeks'), ('week', 'Weeks'),
('month', 'Months')], default="day") # TDE RENAME ? ('month', 'Months')], default="day") # TDE RENAME ?
is_lot_tested_fractionally = fields.Boolean(string="Lot Tested Fractionally", is_lot_tested_fractionally = fields.Boolean(string="Lot Tested Fractionally", help="Determines if only a fraction of the lot should be tested")
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") 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 ? norm = fields.Float('Norm', digits='Quality Tests') # TDE RENAME ?
tolerance_min = fields.Float('Min Tolerance', digits='Quality Tests') tolerance_min = fields.Float('Min Tolerance', digits='Quality Tests')
@@ -70,7 +63,7 @@ class QualityPoint(models.Model):
if n > 1: if n > 1:
point.average = mean point.average = mean
point.standard_deviation = sqrt(s / (n - 1)) point.standard_deviation = sqrt( s / ( n - 1))
elif n == 1: elif n == 1:
point.average = mean point.average = mean
point.standard_deviation = 0.0 point.standard_deviation = 0.0
@@ -101,7 +94,7 @@ class QualityPoint(models.Model):
checks = self.env['quality.check'].search([ checks = self.env['quality.check'].search([
('point_id', '=', self.id), ('point_id', '=', self.id),
('create_date', '>=', date_previous.strftime(DEFAULT_SERVER_DATETIME_FORMAT))], limit=1) ('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() return super(QualityPoint, self).check_execute_now()
def _get_type_default_domain(self): def _get_type_default_domain(self):
@@ -130,491 +123,13 @@ class QualityPoint(models.Model):
class QualityCheck(models.Model): class QualityCheck(models.Model):
_inherit = "quality.check" _inherit = "quality.check"
part_name = fields.Char('零件名称', related='product_id.part_name', readonly=False, store=True) part_name = fields.Char('零件名称', compute='_compute_part_name_number', readonly=True)
part_number = fields.Char('零件图号', related='product_id.part_number', readonly=False, store=True) part_number = fields.Char('零件图号', compute='_compute_part_name_number', readonly=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:
if record.picking_id:
total_qty = 0
for move in record.picking_id.move_ids_without_package:
if move.product_id == record.product_id:
total_qty = int(move.product_uom_qty)
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)
report_number_name = fields.Char('出厂检验报告编号名称', compute='_compute_report_number_name')
old_report_name = fields.Char('旧出厂检验报告编号', default='')
@api.depends('serial_number', 'part_number')
def _compute_report_number_name(self):
for record in self:
str_serial_number = '0' + str(record.serial_number) if record.serial_number < 10 else str(
record.serial_number)
str_part_number = record.part_number if record.part_number else ''
record.report_number_name = f'FQC{str_part_number}{str_serial_number}'
# 出厂检验报告、关联文档的数据
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='质检员', compute='_compute_quality_manager')
@api.depends('measure_line_ids')
def _compute_quality_manager(self):
for record in self:
if record.measure_line_ids:
record.quality_manager = record.env.user.id
else:
record.quality_manager = False
# 流水号(从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. 获取已发布的文档文件夹
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'))
# 2. 先创建空文档记录
doc_vals = {
'name': self.report_number_name,
'mimetype': 'application/pdf',
'res_id': self.id,
'folder_id': workspace.id,
'res_model': self._name,
}
doc = self.env['documents.document'].create(doc_vals)
# 3. 关联文档到质检记录
self.write({
'report_number_id': doc.id,
'quality_state': 'pass'
})
# 4. 获取报告动作并生成PDF此时二维码将包含正确的文档ID
report_action = self.env.ref('sf_quality.action_report_quality_inspection')
pdf_content, _ = report_action._render_qweb_pdf(
report_ref=report_action.report_name,
res_ids=self.ids
)
# 5. 更新文档内容
doc.write({
'raw': pdf_content
})
# 6. 记录发布历史
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
})
# 7. 更新其他信息
self.serial_number += 1
if self.publish_status == 'canceled' and self.picking_id.state == 'done':
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'
})
if self.is_factory_report_uploaded:
# 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"""
self.ensure_one()
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
if self.report_number_id:
print(f"{base_url}/quality/report/{self.report_number_id.id}")
return f"{base_url}/quality/report/{self.report_number_id.id}"
else:
return f"{base_url}/quality/report/not_published"
def upload_factory_report(self):
"""
上传出厂检验报告到加工订单明细中
将当前质检单的出厂检验报告上传到对应的加工订单明细中
"""
self.ensure_one()
if not self.report_content:
raise UserError(_('当前质检单没有出厂检验报告,请先发布报告'))
if not self.product_id.model_name:
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,
"model_name": self.product_id.model_name,
"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
if not order_ref:
raise UserError(_('无法找到相关的调拨单或来源单据'))
if not self.product_id.model_name:
raise UserError(_('产品模型名称为空'))
try:
# 准备请求数据
payload = {
"order_ref": order_ref,
"model_name": self.product_id.model_name
}
# 将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') @depends('product_id')
def _compute_material_name(self): def _compute_part_name_number(self):
for record in self: for record in self:
materials_id_name = record.product_id.materials_id.name if record.product_id.materials_id else '' record.part_number = record.product_id.part_number
materials_type_name = record.product_id.materials_type_id.name if record.product_id.materials_type_id else '' record.part_name = record.product_id.part_name
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) failure_message = fields.Html(related='point_id.failure_message', readonly=True)
measure = fields.Float('Measure', default=0.0, digits='Quality Tests', tracking=True) measure = fields.Float('Measure', default=0.0, digits='Quality Tests', tracking=True)
measure_success = fields.Selection([ measure_success = fields.Selection([
@@ -626,8 +141,7 @@ class QualityCheck(models.Model):
tolerance_max = fields.Float('Max Tolerance', related='point_id.tolerance_max', readonly=True) tolerance_max = fields.Float('Max Tolerance', related='point_id.tolerance_max', readonly=True)
warning_message = fields.Text(compute='_compute_warning_message') warning_message = fields.Text(compute='_compute_warning_message')
norm_unit = fields.Char(related='point_id.norm_unit', readonly=True) 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", qty_to_test = fields.Float(compute="_compute_qty_to_test", string="Quantity to Test", help="Quantity of product to test within the lot")
help="Quantity of product to test within the lot")
qty_tested = fields.Float(string="Quantity Tested", help="Quantity of product tested within the lot") qty_tested = fields.Float(string="Quantity Tested", help="Quantity of product tested within the lot")
measure_on = fields.Selection([ measure_on = fields.Selection([
('operation', 'Operation'), ('operation', 'Operation'),
@@ -636,8 +150,7 @@ class QualityCheck(models.Model):
help="""Operation = One quality check is requested at the operation level. help="""Operation = One quality check is requested at the operation level.
Product = A quality check is requested per product. 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.""") 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, 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")
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_name = fields.Char('Lot/Serial Number Name')
lot_line_id = fields.Many2one('stock.lot', store=True, compute='_compute_lot_line_id') lot_line_id = fields.Many2one('stock.lot', store=True, compute='_compute_lot_line_id')
qty_line = fields.Float(compute='_compute_qty_line', string="Quantity") qty_line = fields.Float(compute='_compute_qty_line', string="Quantity")
@@ -718,8 +231,7 @@ class QualityCheck(models.Model):
def _compute_qty_to_test(self): def _compute_qty_to_test(self):
for qc in self: for qc in self:
if qc.is_lot_tested_fractionally: if qc.is_lot_tested_fractionally:
qc.qty_to_test = float_round(qc.qty_line * qc.testing_percentage_within_lot / 100, 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")
precision_rounding=self.product_id.uom_id.rounding, rounding_method="UP")
else: else:
qc.qty_to_test = qc.qty_line qc.qty_to_test = qc.qty_line
@@ -874,8 +386,7 @@ class QualityAlert(models.Model):
class ProductTemplate(models.Model): class ProductTemplate(models.Model):
_inherit = "product.template" _inherit = "product.template"
quality_control_point_qty = fields.Integer(compute='_compute_quality_check_qty', quality_control_point_qty = fields.Integer(compute='_compute_quality_check_qty', groups='quality.group_quality_user')
groups='quality.group_quality_user')
quality_pass_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') quality_fail_qty = fields.Integer(compute='_compute_quality_check_qty', groups='quality.group_quality_user')
@@ -883,16 +394,14 @@ class ProductTemplate(models.Model):
def _compute_quality_check_qty(self): def _compute_quality_check_qty(self):
for product_tmpl in 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_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( product_tmpl.quality_control_point_qty = product_tmpl.with_context(active_test=product_tmpl.active).product_variant_ids._count_quality_points()
active_test=product_tmpl.active).product_variant_ids._count_quality_points()
def action_see_quality_control_points(self): def action_see_quality_control_points(self):
self.ensure_one() self.ensure_one()
action = self.env["ir.actions.actions"]._for_xml_id("quality_control.quality_point_action") 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) 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), domain_in_products_or_categs = ['|', ('product_ids', 'in', self.product_variant_ids.ids), ('product_category_ids', 'parent_of', self.categ_id.ids)]
('product_category_ids', 'parent_of', self.categ_id.ids)]
domain_no_products_and_categs = [('product_ids', '=', False), ('product_category_ids', '=', False)] 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]) action['domain'] = OR([domain_in_products_or_categs, domain_no_products_and_categs])
return action return action
@@ -903,10 +412,10 @@ class ProductTemplate(models.Model):
action['context'] = dict(self.env.context, default_product_id=self.product_variant_id.id, create=False) action['context'] = dict(self.env.context, default_product_id=self.product_variant_id.id, create=False)
action['domain'] = [ action['domain'] = [
'|', '|',
('product_id', 'in', self.product_variant_ids.ids), ('product_id', 'in', self.product_variant_ids.ids),
'&', '&',
('measure_on', '=', 'operation'), ('measure_on', '=', 'operation'),
('picking_id.move_ids.product_tmpl_id', '=', self.id), ('picking_id.move_ids.product_tmpl_id', '=', self.id),
] ]
return action return action
@@ -914,8 +423,7 @@ class ProductTemplate(models.Model):
class ProductProduct(models.Model): class ProductProduct(models.Model):
_inherit = "product.product" _inherit = "product.product"
quality_control_point_qty = fields.Integer(compute='_compute_quality_check_qty', quality_control_point_qty = fields.Integer(compute='_compute_quality_check_qty', groups='quality.group_quality_user')
groups='quality.group_quality_user')
quality_pass_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') quality_fail_qty = fields.Integer(compute='_compute_quality_check_qty', groups='quality.group_quality_user')
@@ -929,10 +437,10 @@ class ProductProduct(models.Model):
quality_pass_qty = 0 quality_pass_qty = 0
domain = [ domain = [
'|', '|',
('product_id', 'in', self.ids), ('product_id', 'in', self.ids),
'&', '&',
('measure_on', '=', 'operation'), ('measure_on', '=', 'operation'),
('picking_id.move_ids.product_id', 'in', self.ids), ('picking_id.move_ids.product_id', 'in', self.ids),
('company_id', '=', self.env.company.id), ('company_id', '=', self.env.company.id),
('quality_state', '!=', 'none') ('quality_state', '!=', 'none')
] ]
@@ -956,8 +464,7 @@ class ProductProduct(models.Model):
_, where_clause, where_clause_args = query.get_sql() _, where_clause, where_clause_args = query.get_sql()
additional_where_clause = self._additional_quality_point_where_clause() additional_where_clause = self._additional_quality_point_where_clause()
where_clause += additional_where_clause where_clause += additional_where_clause
parent_category_ids = [int(parent_id) for parent_id in parent_category_ids = [int(parent_id) for parent_id in self.categ_id.parent_path.split('/')[:-1]] if self.categ_id else []
self.categ_id.parent_path.split('/')[:-1]] if self.categ_id else []
self.env.cr.execute(""" self.env.cr.execute("""
SELECT COUNT(*) SELECT COUNT(*)
@@ -978,7 +485,7 @@ class ProductProduct(models.Model):
) )
) )
""" % (where_clause,), where_clause_args + [self.ids, parent_category_ids] """ % (where_clause,), where_clause_args + [self.ids, parent_category_ids]
) )
return self.env.cr.fetchone()[0] return self.env.cr.fetchone()[0]
def action_see_quality_control_points(self): def action_see_quality_control_points(self):
@@ -986,8 +493,7 @@ class ProductProduct(models.Model):
action = self.product_tmpl_id.action_see_quality_control_points() action = self.product_tmpl_id.action_see_quality_control_points()
action['context'].update(default_product_ids=self.ids) action['context'].update(default_product_ids=self.ids)
domain_in_products_or_categs = ['|', ('product_ids', 'in', self.ids), domain_in_products_or_categs = ['|', ('product_ids', 'in', self.ids), ('product_category_ids', 'parent_of', self.categ_id.ids)]
('product_category_ids', 'parent_of', self.categ_id.ids)]
domain_no_products_and_categs = [('product_ids', '=', False), ('product_category_ids', '=', False)] 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]) action['domain'] = OR([domain_in_products_or_categs, domain_no_products_and_categs])
return action return action
@@ -998,75 +504,12 @@ class ProductProduct(models.Model):
action['context'] = dict(self.env.context, default_product_id=self.id, create=False) action['context'] = dict(self.env.context, default_product_id=self.id, create=False)
action['domain'] = [ action['domain'] = [
'|', '|',
('product_id', '=', self.id), ('product_id', '=', self.id),
'&', '&',
('measure_on', '=', 'operation'), ('measure_on', '=', 'operation'),
('picking_id.move_ids.product_id', '=', self.id), ('picking_id.move_ids.product_id', '=', self.id),
] ]
return action return action
def _additional_quality_point_where_clause(self): def _additional_quality_point_where_clause(self):
return "" 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

@@ -81,38 +81,18 @@ class StockPicking(models.Model):
return quality_pickings return quality_pickings
def action_cancel(self): def action_cancel(self):
"""
调拨单取消后,关联取消质量检查单
"""
context = self.env.context
if not context.get('cancel_check_picking') and self.sudo().mapped('check_ids').filtered(
lambda x: x.quality_state in ['pass', 'fail']):
self.env.cr.rollback()
return {
'type': 'ir.actions.act_window',
'res_model': 'picking.check.cancel.wizard',
'name': '取消质检单',
'view_mode': 'form',
'target': 'new',
'context': {
'default_picking_id': self.id,
'cancel_check_picking': True}
}
elif self.check_ids.filtered(lambda x: x.quality_state != 'cancel'):
self.sudo().mapped('check_ids').filtered(lambda x: x.quality_state != 'cancel').write({
'quality_state': 'cancel'
})
res = super(StockPicking, self).action_cancel() res = super(StockPicking, self).action_cancel()
# self.sudo().mapped('check_ids').filtered(lambda x: x.quality_state == 'none').unlink() self.sudo().mapped('check_ids').filtered(lambda x: x.quality_state == 'none').unlink()
return res return res
def action_open_quality_check_picking(self): def action_open_quality_check_picking(self):
action = self.env["ir.actions.actions"]._for_xml_id("quality_control.quality_check_action_picking") action = self.env["ir.actions.actions"]._for_xml_id("quality_control.quality_check_action_picking")
action['context'] = { action['context'] = self.env.context.copy()
action['context'].update({
'search_default_picking_id': [self.id], 'search_default_picking_id': [self.id],
'default_picking_id': self.id, 'default_picking_id': self.id,
'show_lots_text': self.show_lots_text, 'show_lots_text': self.show_lots_text,
} })
return action return action
def button_quality_alert(self): def button_quality_alert(self):

View File

@@ -1,7 +1,2 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink 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_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
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

View File

@@ -4,11 +4,3 @@
min-height: 250px; min-height: 250px;
} }
} }
.measureTableSequence {
width: 58px;
}
.measureTable .o_list_table_ungrouped {
min-width: auto;
}

View File

@@ -1,44 +0,0 @@
<?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" class="measureTable">
<!-- <field name="sequence" class="measureTableSequence"/> -->
<!-- <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" attrs="{'invisible': [('parent.publish_status', '=', 'published')]}"/>
</tree>
</field>
</record>
<record id="quality_check_measure_line_form" model="ir.ui.view">
<field name="name">quality.check.measure.line.form</field>
<field name="model">quality.check.measure.line</field>
<field name="arch" type="xml">
<form>
<sheet>
<group>
<group>
<field name="measure_item"/>
<field name="remark"/>
</group>
<group>
<field name="measure_result"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
</odoo>

View File

@@ -255,7 +255,6 @@
<field name="quality_state" widget="statusbar"/> <field name="quality_state" widget="statusbar"/>
</header> </header>
<sheet> <sheet>
<field name="is_out_check" invisible="1"/>
<div class="oe_button_box" name="button_box"> <div class="oe_button_box" name="button_box">
<button name="action_see_alerts" icon="fa-bell" type="object" class="oe_stat_button" <button name="action_see_alerts" icon="fa-bell" type="object" class="oe_stat_button"
attrs="{'invisible': [('alert_count', '=', 0)]}"> attrs="{'invisible': [('alert_count', '=', 0)]}">
@@ -265,17 +264,10 @@
<group> <group>
<group> <group>
<field name="company_id" invisible="1"/> <field name="company_id" invisible="1"/>
<field name="categ_type" invisible="1"/>
<field name="product_id" attrs="{'invisible' : [('measure_on', '=', 'operation')]}"/> <field name="product_id" attrs="{'invisible' : [('measure_on', '=', 'operation')]}"/>
<field name="part_name" attrs="{'invisible': [('categ_type', '!=', '成品')], 'readonly': [('publish_status', '=', 'published')]}"/> <field name="measure_on" attrs="{'readonly': [('point_id', '!=', False)]}"/>
<field name="part_number" attrs="{'invisible': [('categ_type', '!=', '成品')], 'readonly': [('publish_status', '=', 'published')]}"/> <field name="part_name"/>
<field name="material_name" attrs="{'invisible': [('categ_type', '!=', '成品')]}"/> <field name="part_number"/>
<field name="total_qty" attrs="{'invisible': ['|', ('measure_on', '!=', 'product'), ('is_out_check', '=', False)]}"/>
<field name="check_qty" attrs="{'invisible': ['|', ('measure_on', '!=', 'product'), ('is_out_check', '=', False)], 'readonly': [('publish_status', '=', 'published')]}"/>
<!-- <field name="categ_type"/> -->
<field name="report_number_id" attrs="{'invisible': ['|', ('measure_on', '!=', 'product'), ('is_out_check', '=', False)]}"/>
<field name="column_nums" invisible="1"/>
<field name="publish_status" invisible="1"/>
<field name="show_lot_text" invisible="1"/> <field name="show_lot_text" invisible="1"/>
<field name="move_line_id" invisible="1"/> <field name="move_line_id" invisible="1"/>
<field name="product_tracking" invisible="1"/> <field name="product_tracking" invisible="1"/>
@@ -308,47 +300,23 @@
<field name="alert_ids" invisible="1"/> <field name="alert_ids" invisible="1"/>
</group> </group>
<group> <group>
<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" <field name="picking_id"
attrs="{'invisible': [('quality_state', 'in', ('pass', 'fail')), ('picking_id', '=', False)]}"/> attrs="{'invisible': [('quality_state', 'in', ('pass', 'fail')), ('picking_id', '=', False)]}"/>
<field name="point_id"/>
<field string="Type" name="test_type_id" options="{'no_open': True, 'no_create': True}"
attrs="{'readonly': [('point_id', '!=', False)]}"/>
<field name="control_date" invisible="1"/> <field name="control_date" invisible="1"/>
<field name="partner_id" string="Partner"
attrs="{'invisible': [('partner_id', '=', False)]}"/>
<field name="team_id"/> <field name="team_id"/>
<field name="company_id" groups="base.group_multi_company"/> <field name="company_id" groups="base.group_multi_company"/>
<field name="user_id" string="Control Person" invisible="1"/> <field name="user_id" string="Control Person" invisible="1"/>
<field name="measure_operator" string="操机员" attrs="{'invisible': ['|', ('measure_on', '!=', 'product'), ('is_out_check', '=', False)], 'readonly': [('publish_status', '=', 'published')]}"/> <field name="partner_id" string="Partner"
attrs="{'invisible': [('partner_id', '=', False)]}"/>
</group> </group>
</group> </group>
<group attrs="{'invisible': [('test_type', '!=', 'picture')]}"> <group attrs="{'invisible': [('test_type', '!=', 'picture')]}">
<field name="picture" widget="image"/> <field name="picture" widget="image"/>
</group> </group>
<notebook> <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="添加测量值" attrs="{'invisible': [('publish_status', '=', 'published')]}"/>
<button name="remove_measure_line" type="object" class="btn-primary" string="删除测量值" attrs="{'invisible': [('publish_status', '=', 'published')]}"/>
<button name="%(quality_control.import_complex_model_wizard)d" string="上传"
type="action"
class="btn-primary"
attrs="{'force_show':1, 'invisible': [('publish_status', '=', 'published')]}"
context="{'default_model_name': 'quality.check.measure.line', 'default_check_id': id}"/>
</div>
<br/>
<div class="o_row">
<field name="measure_line_ids" widget="tree" attrs="{'readonly': [('publish_status', '=', 'published')]}"/>
</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"> <page string="Notes" name="notes">
<group> <group>
<field name="report_result"/> <field name="report_result"/>
@@ -358,18 +326,6 @@
</group> </group>
</page> </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> </notebook>
</sheet> </sheet>
<div class="oe_chatter"> <div class="oe_chatter">

View File

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

View File

@@ -1,439 +0,0 @@
# -*- 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')
check_id = fields.Many2one(string='质检单', comodel_name='quality.check')
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文件'))
if self.check_id.measure_line_ids:
self.sudo().check_id.measure_line_ids.unlink()
# 解码文件数据
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
# 从第二行开始读取数据(跳过表头)
max_columns = 1
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 '', # 备注列
}
for i in range(1, 6):
if measure_line_vals.get(f'measure_value{i}'):
if i > max_columns:
max_columns = i
self.env['quality.check.measure.line'].create(measure_line_vals)
valid_data_imported = True
quality_check.column_nums = max_columns
# 检查是否有有效数据被导入
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):
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url') + '/quality_control/static/src/binary/出厂检验报告上传模版.xlsx'
# 只有当原始 URL 使用 http 时才替换为 https
if base_url.startswith("http://"):
excel_url = base_url.replace("http://", "https://")
else:
excel_url = base_url
value = dict(
type='ir.actions.act_url',
target='self',
url=excel_url,
)
return value

View File

@@ -1,33 +0,0 @@
<?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

@@ -62,8 +62,7 @@ class QualityCheckWizard(models.TransientModel):
def do_pass(self): def do_pass(self):
if self.test_type == 'picture' and not self.picture: if self.test_type == 'picture' and not self.picture:
raise UserError('请先上传照片') raise UserError('You must provide a picture before validating')
# raise UserError('You must provide a picture before validating')
self.current_check_id.do_pass() self.current_check_id.do_pass()
return self.action_generate_next_window() return self.action_generate_next_window()
@@ -113,25 +112,3 @@ class QualityCheckWizard(models.TransientModel):
default_current_check_id=self.current_check_id.id, default_current_check_id=self.current_check_id.id,
) )
return action return action
# 对于成品出库的出厂检验报告,增加发布按钮
def publish(self):
self.current_check_id._check_part_number()
self.current_check_id._check_measure_line()
self.current_check_id._check_check_qty_and_total_qty()
self.current_check_id._do_publish_implementation()
class PickingCheckCancelWizard(models.TransientModel):
_name = 'picking.check.cancel.wizard'
_description = 'picking check cancel wizard'
picking_id = fields.Many2one('stock.picking', 'stock picking')
def confirm_picking_check(self):
res = self.picking_id.action_cancel()
return res
def cancel_picking_check(self):
# 这里是取消后的逻辑
return {'type': 'ir.actions.act_window_close'}

View File

@@ -79,10 +79,7 @@
attrs="{'invisible': ['|', ('quality_state', '!=', 'none'), ('test_type', '!=', 'passfail')]}" data-hotkey="x"/> 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_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 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" /> <button string="Cancel" class="btn btn-secondary" special="cancel" data-hotkey="z" />
</footer> </footer>
</form> </form>
</field> </field>
@@ -121,21 +118,4 @@
<field name="context">{}</field> <field name="context">{}</field>
<field name="target">new</field> <field name="target">new</field>
</record> </record>
<!-- ================================================================================================== -->
<record id="picking_check_cancel_wizard_form" model="ir.ui.view">
<field name="name">picking.check.cancel.wizard</field>
<field name="model">picking.check.cancel.wizard</field>
<field name="arch" type="xml">
<form string="Quality Check Failed">
<div>质量检查单已完成,继续取消吗?</div>
<div class="'color': 'red'">注意:关联质量检查单也将被取消。</div>
<footer>
<button name="confirm_picking_check" type="object" class="btn-primary" string="确认"/>
<button name="cancel_picking_check" type="object" string="取消"/>
</footer>
</form>
</field>
</record>
</odoo> </odoo>

View File

@@ -1,20 +0,0 @@
# -*- 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

@@ -1,37 +0,0 @@
<?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

@@ -65,7 +65,7 @@
<field name="lot_id" position="after"> <field name="lot_id" position="after">
<field name="workorder_id" invisible="1"/> <field name="workorder_id" invisible="1"/>
<field name="production_id" invisible="1"/> <field name="production_id" invisible="1"/>
<!-- <field name="finished_lot_id" attrs="{'invisible': [('finished_lot_id', '=', False)]}" groups="stock.group_production_lot"/> --> <field name="finished_lot_id" attrs="{'invisible': [('finished_lot_id', '=', False)]}" groups="stock.group_production_lot"/>
</field> </field>
<xpath expr="//field[@name='lot_id']" position="after"> <xpath expr="//field[@name='lot_id']" position="after">
<field name="lot_id" attrs="{'invisible': [('workorder_id', '=', False)]}" groups="stock.group_production_lot" string="Component Lot/Serial"/> <field name="lot_id" attrs="{'invisible': [('workorder_id', '=', False)]}" groups="stock.group_production_lot" string="Component Lot/Serial"/>

View File

@@ -421,4 +421,3 @@ class EmbryoRedundancy(models.Model):
width = fields.Float('宽度(mm)', required=True) width = fields.Float('宽度(mm)', required=True)
height = fields.Float('高度(mm)', required=True) height = fields.Float('高度(mm)', required=True)
active = fields.Boolean('有效', default=True) active = fields.Boolean('有效', default=True)
remark = fields.Char('描述')

View File

@@ -57,42 +57,15 @@ class MrsMaterialModel(models.Model):
remark = fields.Text("备注") remark = fields.Text("备注")
gain_way = fields.Selection( gain_way = fields.Selection(
[("自加工", "自加工"), ("外协", "委外加工"), ("采购", "采购")], [("自加工", "自加工"), ("外协", "委外加工"), ("采购", "采购")],
default="采购", string="获取方式") default="", string="获取方式")
supplier_ids = fields.One2many('sf.supplier.sort', 'materials_model_id', string='供应商') supplier_ids = fields.One2many('sf.supplier.sort', 'materials_model_id', string='供应商')
active = fields.Boolean('有效', default=True) active = fields.Boolean('有效', default=True)
def write(self, vals): @api.constrains("gain_way")
res = super(MrsMaterialModel, self).write(vals) def _check_supplier_ids(self):
if not self.gain_way: for item in self:
self.gain_way = '采购' if item.gain_way in ('外协', '采购') and not item.supplier_ids:
if not self.supplier_ids: raise UserError("请添加供应商")
supplier_id = self.env['res.partner'].search([('name', 'like', '%傲派%')], limit=1)
if not supplier_id:
supplier_id = self.env['res.partner'].create({
'name': '湖南傲派自动化设备有限公司',
'supplier_rank':1,
})
self.supplier_ids = [(0, 0, {'materials_model_id': self.id, 'partner_id': supplier_id.id or False})]
return res
@api.model
def create(self, vals):
res = super(MrsMaterialModel, self).create(vals)
if not vals.get('supplier_ids'):
supplier_id = self.env['res.partner'].search([('name', 'like', '%傲派%')], limit=1)
if not supplier_id:
supplier_id = self.env['res.partner'].create({
'name': '湖南傲派自动化设备有限公司',
'supplier_rank': 1,
})
res.supplier_ids = [(0, 0, {'materials_model_id': res.id, 'partner_id': supplier_id.id or False})]
return res
else:
return res
# @api.constrains("gain_way")
# def _check_supplier_ids(self):
# for item in self:
# if item.gain_way in ('外协', '采购') and not item.supplier_ids:
# raise UserError("请添加供应商")
class MrsProductionProcessCategory(models.Model): class MrsProductionProcessCategory(models.Model):

View File

@@ -32,7 +32,6 @@ class FixtureModel(models.Model):
multi_mounting_type_id = fields.Many2one('sf.multi_mounting.type', string="联装类型") multi_mounting_type_id = fields.Many2one('sf.multi_mounting.type', string="联装类型")
brand_id = fields.Many2one('sf.machine.brand', string="品牌") brand_id = fields.Many2one('sf.machine.brand', string="品牌")
model_file = fields.Binary(string="图片") model_file = fields.Binary(string="图片")
glb_url = fields.Char(string="图片")
status = fields.Boolean('状态') status = fields.Boolean('状态')
active = fields.Boolean('有效', default=False) active = fields.Boolean('有效', default=False)

View File

@@ -187,7 +187,7 @@ class CuttingToolModel(models.Model):
def _get_ids(self, cutting_tool_type_code, factory_short_name): def _get_ids(self, cutting_tool_type_code, factory_short_name):
cutting_tool_type_ids = [] cutting_tool_type_ids = []
for item in cutting_tool_type_code: for item in cutting_tool_type_code:
cutting_tool_type = self.search([('code', '=', item)]) cutting_tool_type = self.search([('code', '=', item.replace("JKM", factory_short_name))])
if cutting_tool_type: if cutting_tool_type:
cutting_tool_type_ids.append(cutting_tool_type.id) cutting_tool_type_ids.append(cutting_tool_type.id)
return [(6, 0, cutting_tool_type_ids)] return [(6, 0, cutting_tool_type_ids)]

View File

@@ -9,7 +9,6 @@ function getDomData() {
table.hide() table.hide()
const thead = customTable.children('thead') const thead = customTable.children('thead')
const tbody = customTable.children('tbody') const tbody = customTable.children('tbody')
const tfooter = customTable.children('tfoot')
const tableData = [] const tableData = []
const tbody_child = tbody.children() const tbody_child = tbody.children()
@@ -17,29 +16,30 @@ function getDomData() {
for (let v = 0; v < tbody_child_len; v++) { // 将数据取出来到tableData里面 for (let v = 0; v < tbody_child_len; v++) { // 将数据取出来到tableData里面
const data = tbody_child[v].innerText.split('\t') const data = tbody_child[v].innerText.split('\t')
// console.log('dom data',data)
const [index, deep, name, Φ, value] = data const [index, deep, name, Φ, value] = data
tableData.push({ index, deep, name, Φ, value }) tableData.push({index, deep, name, Φ, value})
} }
const ΦList = [...new Set(tableData.map(_ => _.Φ))] // ΦList去重 const ΦList = [...new Set(tableData.map(_ => _.name))] // ΦList去重
const newTableData = {} const newTableData = {}
tableData.forEach(_ => { tableData.forEach(_ => {
const key = _.deep + '|' + _.name const key = _.deep + '|' + _.Φ
!newTableData[key] ? newTableData[key] = { i: _.index } : ''; !newTableData[key] ? newTableData[key] = {i: _.index} : '';
if (_.Φ) { // 去除没有Φ的脏数据 if (_.Φ) { // 去除没有Φ的脏数据
newTableData[key]['Φ' + _.Φ] = _.value newTableData[key]['Φ' + _.Φ] = _.value
newTableData[key]['Φ' + _.Φ + 'i'] = _.index newTableData[key]['Φ' + _.Φ + 'i'] = _.index
} }
}) })
// console.log(tableData, ΦList, newTableData); // console.log('qwdh',tableData, ΦList, newTableData);
if (ΦList.filter(_ => _).length == 0) return; if (ΦList.filter(_ => _).length == 0) return;
handleThead(thead, ΦList, tfooter) handleThead(thead, ΦList)
handleTbody(tbody, newTableData, ΦList, table ) handleTbody(tbody, newTableData, ΦList, table)
} }
// 重新设置表头、 // 重新设置表头、
function handleThead(thead, ΦList, tfooter) { function handleThead(thead, ΦList) {
const dom = thead.children().eq(0).children() const dom = thead.children().eq(0).children()
const len = dom.length const len = dom.length
dom.eq(0).attr('rowspan', 2) dom.eq(0).attr('rowspan', 2)
@@ -47,11 +47,7 @@ function handleThead(thead, ΦList, tfooter) {
len == 5 ? dom.eq(2).attr('rowspan', 2) : '' len == 5 ? dom.eq(2).attr('rowspan', 2) : ''
dom.eq(-2).attr('colspan', ΦList.length) dom.eq(-2).attr('colspan', ΦList.length)
dom.eq(-1).remove() dom.eq(-1).remove()
if(tfooter && tfooter.length) {
tfooter.children().each(function () {
$(this).children().eq(-1).remove()
})
}
const tr = document.createElement('tr') const tr = document.createElement('tr')
for (let v = 0; v < ΦList.length; v++) { for (let v = 0; v < ΦList.length; v++) {
const th = document.createElement('th') const th = document.createElement('th')
@@ -72,6 +68,7 @@ function handleTbody(tbody, newTableData, ΦList, table) {
// b = b.split('=')[1].split('%')[0] // b = b.split('=')[1].split('%')[0]
// return a - b // return a - b
// }) // })
// console.log('wqoqw ',ΦList)
data.forEach(_ => { data.forEach(_ => {
i++ i++
const tr = $('<tr class="o_data_row"></tr>') const tr = $('<tr class="o_data_row"></tr>')
@@ -101,6 +98,61 @@ function handleTbody(tbody, newTableData, ΦList, table) {
// // } // // }
tbody.append(tr) tbody.append(tr)
}) })
// $(document).click(function (e) {
// if ($(e.target).attr('coustomTd')) {
// const orginV = $('[coustomInput=1]').children('input').val()
// $('[coustomInput=1]').parent().html(orginV)
// const v = $(e.target).attr('val')
// console.log($(e.target));
// $(e.target).html('')
// const input = $('<div coustomInput="1" name="feed_per_tooth" class="o_field_widget o_field_char"><input class="o_input" type="text" autocomplete="off" maxlength="20"></div>')
// input.children('input').val(v)
// $(e.target).append(input)
// input.children('input').focus()
// input.children('input').select()
// } else if ($(e.target).attr('coustomInput')) {
//
// } else {
// const orginV = $('[coustomInput=1]').children('input').val()
// $('[coustomInput=1]').parent().html(orginV)
// const v = $(e.target).attr('val')
// }
// })
// $(document).off('change') // 防止重复绑定
// $(document).on('change', '[coustomInput] input', function () {
// $(this).parents('td').attr('val', $(this).val());
// var eve1 = new Event('change');
// var eve2 = new Event('input');
// var eve3 = new Event('click');
// const i = $(this).parents('td').attr('col');
// let patchDom = table.find('tbody').children('tr').eq(i - 1);
//
// if (patchDom.length === 0) {
// console.error('No such row found');
// return;
// }
//
// patchDom = patchDom.children().eq(-1);
//
// setTimeout(() => {
// if (patchDom.length === 0) {
// console.error('No such cell found');
// return;
// }
// patchDom[0].dispatchEvent(eve3); // Simulate click event
//
// setTimeout(() => {
// patchDom = patchDom.find('input');
// if (patchDom.length === 0) {
// console.error('No input found in the target cell');
// return;
// }
// patchDom.val($(this).val());
// patchDom[0].dispatchEvent(eve2);
// patchDom[0].dispatchEvent(eve1);
// }, 200);
// }, 500);
// });
} }

View File

@@ -24,18 +24,4 @@
.o_search_panel.account_root { .o_search_panel.account_root {
flex: unset !important; flex: unset !important;
}
.multi-line {
display: flex;
flex-wrap: nowrap;
> label.o_form_label {
width: 52px;
}
> span {
flex: 1;
}
> div {
flex: 2
}
} }

View File

@@ -645,7 +645,6 @@
<field name="long"/> <field name="long"/>
<field name="width"/> <field name="width"/>
<field name="height"/> <field name="height"/>
<field name="remark"/>
</tree> </tree>
</field> </field>
</record> </record>

View File

@@ -262,13 +262,13 @@
<group> <group>
<field name="materials_no" readonly="1" force_save="1"/> <field name="materials_no" readonly="1" force_save="1"/>
<field name="gain_way" required="0"/> <field name="gain_way" required="0"/>
<field name="density" readonly="1" required="1" class="custom_required"/> <field name="tensile_strength" required="1"/>
<field name="hardness" required="1"/>
<field name="density" readonly="1"/>
</group> </group>
<group> <group>
<field name="rough_machining" required="1"/> <field name="rough_machining" required="1"/>
<field name="finish_machining" required="1"/> <field name="finish_machining" required="1"/>
<field name="tensile_strength" required="1"/>
<field name="hardness" required="1"/>
<field name="need_h" default="false" readonly="1"/> <field name="need_h" default="false" readonly="1"/>
<field name="mf_materia_post" attrs="{'invisible':[('need_h','=',False)]} " <field name="mf_materia_post" attrs="{'invisible':[('need_h','=',False)]} "
readonly="1"/> readonly="1"/>
@@ -297,7 +297,7 @@
<record model="ir.ui.view" id="sf_materials_model_tree"> <record model="ir.ui.view" id="sf_materials_model_tree">
<field name="model">sf.materials.model</field> <field name="model">sf.materials.model</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<tree string="材料型号" delete="0" create="0"> <tree string="材料型号" delete="0">
<field name="materials_no"/> <field name="materials_no"/>
<field name="materials_code"/> <field name="materials_code"/>
<field name="name"/> <field name="name"/>

View File

@@ -158,8 +158,6 @@
<!-- <field name="upload_model_file" widget="many2many_binary"/>--> <!-- <field name="upload_model_file" widget="many2many_binary"/>-->
<field name="model_file" widget="Viewer3D" string="模型" readonly="1" force_save="1" <field name="model_file" widget="Viewer3D" string="模型" readonly="1" force_save="1"
attrs="{'invisible': [('model_file', '=', False)]}"/> attrs="{'invisible': [('model_file', '=', False)]}"/>
<field name="glb_url" widget="Viewer3D" string="模型" readonly="1" force_save="1"
attrs="{'invisible': [('glb_url', '=', False)]}"/>
</group> </group>
</group> </group>
<notebook> <notebook>

View File

@@ -112,8 +112,6 @@
<field name="cutting_tool_material_id"/> <field name="cutting_tool_material_id"/>
<field name="cutting_tool_type_id"/> <field name="cutting_tool_type_id"/>
<field name="brand_id"/> <field name="brand_id"/>
<field name="create_date" optional="hide"/>
<field name="write_date" string="修改时间" optional="hide"/>
</tree> </tree>
</field> </field>
</record> </record>
@@ -140,7 +138,7 @@
<field name="brand_id" required="1"/> <field name="brand_id" required="1"/>
<label for="integral_run_out_accuracy_min" string="端跳精度" <label for="integral_run_out_accuracy_min" string="端跳精度"
attrs="{'invisible': [('cutting_tool_type', '!=', '整体式刀具')]}"/> attrs="{'invisible': [('cutting_tool_type', '!=', '整体式刀具')]}"/>
<div class="o_address_format multi-line" <div class="o_address_format"
attrs="{'invisible': [('cutting_tool_type', '!=', '整体式刀具')]}"> attrs="{'invisible': [('cutting_tool_type', '!=', '整体式刀具')]}">
<label for="integral_run_out_accuracy_min" string="最小"/> <label for="integral_run_out_accuracy_min" string="最小"/>
<field name="integral_run_out_accuracy_min" class="o_address_zip" <field name="integral_run_out_accuracy_min" class="o_address_zip"
@@ -179,33 +177,33 @@
</group> </group>
<group string="适配刀片形状" <group string="适配刀片形状"
attrs="{'invisible': [('cutting_tool_type', 'in', ('刀柄','夹头','整体式刀具',False))]}"> attrs="{'invisible': [('cutting_tool_type', 'in', ('刀柄','夹头','整体式刀具',False))]}">
<field name="fit_blade_shape_id" string="" widget="many2one_radio" attrs="{'showExpand': True}"/> <field name="fit_blade_shape_id" string="" widget="many2one_radio"/>
</group> </group>
<group string="适合加工方式" <group string="适合加工方式"
attrs="{'invisible': [('cutting_tool_type', 'not in', ('整体式刀具','刀杆','刀盘','刀片'))]}"> attrs="{'invisible': [('cutting_tool_type', 'not in', ('整体式刀具','刀杆','刀盘','刀片'))]}">
<field name="suitable_machining_method_ids" string="" <field name="suitable_machining_method_ids" string=""
widget="custom_many2many_checkboxes" attrs="{'showExpand': True}"/> widget="custom_many2many_checkboxes"/>
</group> </group>
<group string="刀尖特征" <group string="刀尖特征"
attrs="{'invisible': [('cutting_tool_type', 'not in', ('整体式刀具','刀杆','刀盘','刀片'))]}"> attrs="{'invisible': [('cutting_tool_type', 'not in', ('整体式刀具','刀杆','刀盘','刀片'))]}">
<field name="blade_tip_characteristics_id" string="" <field name="blade_tip_characteristics_id" string=""
widget="many2one_radio" attrs="{'showExpand': True}"/> widget="many2one_radio"/>
</group> </group>
<group attrs="{'invisible': [('cutting_tool_type', 'not in', ('整体式刀具','刀杆','刀盘','刀片'))]}"> <group attrs="{'invisible': [('cutting_tool_type', 'not in', ('整体式刀具','刀杆','刀盘','刀片'))]}">
<group string="柄部类型" attrs="{'invisible': [('cutting_tool_type', '!=', '整体式刀具')]}"> <group string="柄部类型" attrs="{'invisible': [('cutting_tool_type', '!=', '整体式刀具')]}">
<field name="handle_type_id" string="" widget="many2one_radio" attrs="{'showExpand': True}"/> <field name="handle_type_id" string="" widget="many2one_radio"/>
</group> </group>
<group string="压紧方式" <group string="压紧方式"
attrs="{'invisible': [('cutting_tool_type', 'not in', ('刀杆','刀盘'))]}"> attrs="{'invisible': [('cutting_tool_type', 'not in', ('刀杆','刀盘'))]}">
<field name="compaction_way_id" string="" widget="many2one_radio" attrs="{'showExpand': True}"/> <field name="compaction_way_id" string="" widget="many2one_radio"/>
</group> </group>
</group> </group>
<group attrs="{'invisible': [('cutting_tool_type', 'not in', ('整体式刀具','刀杆','刀盘','刀片'))]}"> <group attrs="{'invisible': [('cutting_tool_type', 'not in', ('整体式刀具','刀杆','刀盘','刀片'))]}">
<group string="走刀方向"> <group string="走刀方向">
<field name="cutting_direction_ids" string="" widget="custom_many2many_checkboxes" attrs="{'showExpand': True}"/> <field name="cutting_direction_ids" string="" widget="custom_many2many_checkboxes"/>
</group> </group>
<group string="适合冷却方式"> <group string="适合冷却方式">
<field name="suitable_coolant_ids" string="" widget="custom_many2many_checkboxes" attrs="{'showExpand': True}" /> <field name="suitable_coolant_ids" string="" widget="custom_many2many_checkboxes"/>
</group> </group>
</group> </group>
<notebook> <notebook>
@@ -319,28 +317,28 @@
<field name="knife_handle_basic_parameters_ids" <field name="knife_handle_basic_parameters_ids"
attrs="{'invisible': [('cutting_tool_type', '!=', '刀柄')]}"> attrs="{'invisible': [('cutting_tool_type', '!=', '刀柄')]}">
<tree editable="bottom" delete="1"> <tree editable="bottom" class="center" delete="1">
<field name="cutting_tool_type" invisible="1"/> <field name="cutting_tool_type" invisible="1"/>
<field name="name"/> <field name="name"/>
<field name="taper_shank_model"/> <field name="taper_shank_model"/>
<field name="total_length"/> <field name="total_length"/>
<field name="shank_length"/> <field name="shank_length"/>
<field name="shank_diameter" class="diameter"/> <field name="shank_diameter" class="diameter"/>
<field name="flange_shank_length" optional="hide"/> <field name="flange_shank_length"/>
<field name="flange_diameter" optional="hide"/> <field name="flange_diameter"/>
<field name="diameter_slip_accuracy" optional="hide"/> <field name="diameter_slip_accuracy"/>
<field name="dynamic_balance_class" optional="hide"/> <field name="dynamic_balance_class"/>
<field name="min_clamping_diameter" class="diameter"/> <field name="min_clamping_diameter" class="diameter"/>
<field name="max_clamping_diameter" class="diameter"/> <field name="max_clamping_diameter" class="diameter"/>
<field name="max_rotate_speed" optional="hide"/> <field name="max_rotate_speed"/>
<field name="fit_chuck_size"/> <field name="fit_chuck_size"/>
<field name="nut" optional="hide"/> <field name="nut"/>
<field name="spanner" string="适配锁紧扳手型号" optional="hide"/> <field name="spanner" string="适配锁紧扳手型号"/>
<field name="clamping_mode" optional="hide"/> <field name="clamping_mode"/>
<field name="tool_changing_time" optional="hide"/> <field name="tool_changing_time"/>
<field name="cooling_model" optional="hide"/> <field name="cooling_model"/>
<field name="is_quick_cutting" optional="hide"/> <field name="is_quick_cutting"/>
<field name="is_safe_lock" optional="hide"/> <field name="is_safe_lock"/>
</tree> </tree>
</field> </field>
<field name="chuck_basic_parameters_ids" <field name="chuck_basic_parameters_ids"

View File

@@ -29,7 +29,7 @@ class Sf_Bf_Connect(http.Controller):
bfm_process_order_list = json.loads(kw['bfm_process_order_list']) bfm_process_order_list = json.loads(kw['bfm_process_order_list'])
order_id = request.env['sale.order'].with_user(request.env.ref("base.user_admin")).sale_order_create( order_id = request.env['sale.order'].with_user(request.env.ref("base.user_admin")).sale_order_create(
company_id, kw['delivery_name'], kw['delivery_telephone'], kw['delivery_address'], company_id, kw['delivery_name'], kw['delivery_telephone'], kw['delivery_address'],
kw['delivery_end_date'], kw['payments_way'], kw['pay_way'], model_display_version=kw.get('model_display_version')) kw['delivery_end_date'], kw['payments_way'], kw['pay_way'])
i = 1 i = 1
# 给sale_order的default_code字段赋值 # 给sale_order的default_code字段赋值
aa = request.env['sale.order'].sudo().search([('name', '=', order_id.name)]) aa = request.env['sale.order'].sudo().search([('name', '=', order_id.name)])
@@ -132,26 +132,6 @@ class Sf_Bf_Connect(http.Controller):
request.cr.rollback() request.cr.rollback()
return json.JSONEncoder().encode(res) return json.JSONEncoder().encode(res)
@http.route('/api/bfm_cancel_order', type='http', auth='sf_token', methods=['GET', 'POST'], csrf=False,
cors="*")
def get_bfm_cancel_order(self, **kw):
"""
业务平台取消销售订单
:param kw:
:return:
"""
res = {'status': 1, 'message': '工厂取消销售订单成功'}
logging.info('get_bfm_cancel_order:%s' % kw['order_number'])
try:
sale_order_info = request.env['sale.order'].sudo().search([('name', '=', kw['order_number'])])
sale_order_info._action_cancel()
return json.JSONEncoder().encode(res)
except Exception as e:
logging.error('get_bfm_cancel_order error: %s' % e)
res['status'] = -1
res['message'] = '工厂取消销售订单失败,请联系管理员'
return json.JSONEncoder().encode(res)
class jdElcp(http.Controller): class jdElcp(http.Controller):

View File

@@ -72,7 +72,7 @@ class StatusChange(models.Model):
logging.info('函数已经执行=============') logging.info('函数已经执行=============')
# 使用super()来调用原始方法(在本例中为'sale.order'模型的'action_cancel'方法) # 使用super()来调用原始方法(在本例中为'sale.order'模型的'action_cancel'方法)
res = super(StatusChange, self.with_context(disable_cancel_warning=True)).action_cancel() res = super(StatusChange, self).action_cancel()
# 原有方法执行后进行额外的操作如调用外部API # 原有方法执行后进行额外的操作如调用外部API
logging.info('函数已经执行=============2') logging.info('函数已经执行=============2')

View File

@@ -32,7 +32,7 @@
<field name="is_bill" invisible="True"/> <field name="is_bill" invisible="True"/>
<field name="logistics_status" invisible="True"/> <field name="logistics_status" invisible="True"/>
<field name="logistics_way" invisible="True"/> <field name="logistics_way" invisible="True"/>
<!-- <button string="物流下单" name="create_order" type="object" confirm="是否确认物流下单" class="btn-primary" --> <button string="物流下单" name="create_order" type="object" confirm="是否确认物流下单" class="btn-primary"
attrs="{'invisible': ['|', '|', '|', ('check_out', '!=', 'OUT'), ('state', '!=', 'assigned'), ('is_bill', '=', True), ('logistics_way', '=', '自提')]}"/> attrs="{'invisible': ['|', '|', '|', ('check_out', '!=', 'OUT'), ('state', '!=', 'assigned'), ('is_bill', '=', True), ('logistics_way', '=', '自提')]}"/>
<button string="获取物流面单" name="get_bill" type="object" confirm="是否获取物流面单" class="btn-primary" <button string="获取物流面单" name="get_bill" type="object" confirm="是否获取物流面单" class="btn-primary"
attrs="{'invisible': ['|', '|', '|', '|', ('check_out', '!=', 'OUT'), ('state', '!=', 'assigned'), ('logistics_status', '=', '2'), ('is_bill', '=', False), ('logistics_way', '=', '自提')]}"/> attrs="{'invisible': ['|', '|', '|', '|', ('check_out', '!=', 'OUT'), ('state', '!=', 'assigned'), ('logistics_status', '=', '2'), ('is_bill', '=', False), ('logistics_way', '=', '自提')]}"/>

View File

@@ -44,7 +44,7 @@ class ResProductTemplate(models.Model):
else: else:
return self.env.ref('sf_dlm.product_uom_cubic_millimeter') return self.env.ref('sf_dlm.product_uom_cubic_millimeter')
model_file = fields.Binary('模型文件') # model_file = fields.Binary('模型文件')
# 胚料的库存路线设置 # 胚料的库存路线设置
# def _get_routes(self, route_type): # def _get_routes(self, route_type):

View File

@@ -6,7 +6,7 @@
<field name="inherit_id" ref="sf_manufacturing.view_mrp_production_workorder_tray_form_inherit_sf"/> <field name="inherit_id" ref="sf_manufacturing.view_mrp_production_workorder_tray_form_inherit_sf"/>
<field name="arch" type="xml"> <field name="arch" type="xml">
<xpath expr="//page[1]" position="before"> <xpath expr="//page[1]" position="before">
<page string="开料要求" attrs='{"invisible": ["!", ("individuation_page_list", "ilike", "CMR")]}'> <page string="开料要求" attrs='{"invisible": [("routing_type","!=","切割")]}'>
<group> <group>
<group> <group>
<field name="product_tmpl_id_materials_id" widget="many2one"/> <field name="product_tmpl_id_materials_id" widget="many2one"/>

View File

@@ -16,21 +16,15 @@
<field name='categ_id' class="custom_required" attrs="{'readonly': [('id', '!=', False)]}"/> <field name='categ_id' class="custom_required" attrs="{'readonly': [('id', '!=', False)]}"/>
<field name='is_bfm' invisible="1"/> <field name='is_bfm' invisible="1"/>
<field name='categ_type' invisible="1"/> <field name='categ_type' invisible="1"/>
<field name='glb_url' invisible="1"/>
<field name='part_name' attrs="{'invisible': [('categ_type', '!=', '成品')]}"/> <field name='part_name' attrs="{'invisible': [('categ_type', '!=', '成品')]}"/>
<field name='part_number' attrs="{'invisible': [('categ_type', '!=', '成品')]}"/> <field name='part_number' attrs="{'invisible': [('categ_type', '!=', '成品')]}"/>
<field name='manual_quotation' attrs="{'invisible':[('glb_url', '=', False)]}"/> <field name='manual_quotation' attrs="{'invisible':[('upload_model_file', '=', [])]}"/>
<field name="is_customer_provided" attrs="{'invisible': [('categ_type', 'not in', ['成品', '坯料'])], 'readonly': True}" /> <field name="is_customer_provided" attrs="{'invisible': [('categ_type', 'not in', ['成品', '坯料'])], 'readonly': True}" />
<field name="model_name" invisible="1"/> <field name="upload_model_file"
<field name="upload_model_file" widget="many2many_binary" attrs="{'invisible': [('upload_model_file', '=', False)]}"/> widget="many2many_binary"
<field name="model_url" attrs="{'invisible': ['|', '|',('categ_type', '!=', '成品'),('categ_type', '=', False),('is_bfm','=', True)]}"/>
widget="binary_download"
filename_field="model_name"
attrs="{'invisible': ['|', '|',('categ_type', '!=', '成品'),('categ_type', '=', False),('model_url', '=', False)]}"/>
<field name="model_file" widget="Viewer3D" string="模型" readonly="1" force_save="1" <field name="model_file" widget="Viewer3D" string="模型" readonly="1" force_save="1"
attrs="{'invisible': ['|','|', ('categ_type', '!=', '成品'),('categ_type', '=', False),('model_file', '=', False)]}"/> attrs="{'invisible': ['|','|', ('categ_type', '!=', '成品'),('categ_type', '=', False),('model_file', '=', False)]}"/>
<field name="glb_url" widget="Viewer3D" string="模型" readonly="1" force_save="1"
attrs="{'invisible': ['|','|', ('categ_type', '!=', '成品'),('categ_type', '=', False),('glb_url', '=', False)]}"/>
<field name='cutting_tool_type' invisible="1"/> <field name='cutting_tool_type' invisible="1"/>
<field name="fixture_material_type" invisible="1"/> <field name="fixture_material_type" invisible="1"/>
<field name="embryo_model_type_id" string="模型类型" options="{'no_create': True}" <field name="embryo_model_type_id" string="模型类型" options="{'no_create': True}"
@@ -74,7 +68,6 @@
</field> </field>
<xpath expr="//field[@name='uom_id']" position="before"> <xpath expr="//field[@name='uom_id']" position="before">
<field name="is_manual_processing" attrs="{'invisible': [('categ_type', 'not in', ['成品', '坯料'])], 'readonly': True}" /> <field name="is_manual_processing" attrs="{'invisible': [('categ_type', 'not in', ['成品', '坯料'])], 'readonly': True}" />
<field name="auto_machining" attrs="{'invisible': [('categ_type', 'not in', ['成品', '坯料'])], 'readonly': True}" />
</xpath> </xpath>
<xpath expr="//label[@for='volume']" position="before"> <xpath expr="//label[@for='volume']" position="before">
<label for="length" string="尺寸" <label for="length" string="尺寸"

View File

@@ -5,7 +5,7 @@ import json
import base64 import base64
import logging import logging
import psycopg2 import psycopg2
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta
from odoo import http, fields from odoo import http, fields
from odoo.http import request from odoo.http import request
@@ -414,7 +414,7 @@ class Sf_Dashboard_Connect(http.Controller):
# 工单计划量切换为CNC工单 # 工单计划量切换为CNC工单
plan_data_total_counts = work_order_obj.search_count( plan_data_total_counts = work_order_obj.search_count(
[('production_line_id.name', '=', line), ('id', '!=', 8061), [('production_id.production_line_id.name', '=', line),
('state', 'in', ['ready', 'progress', 'done']), ('routing_type', '=', 'CNC加工')]) ('state', 'in', ['ready', 'progress', 'done']), ('routing_type', '=', 'CNC加工')])
# # 工单完成量 # # 工单完成量
@@ -423,13 +423,13 @@ class Sf_Dashboard_Connect(http.Controller):
# 工单完成量切换为CNC工单 # 工单完成量切换为CNC工单
plan_data_finish_counts = work_order_obj.search_count( plan_data_finish_counts = work_order_obj.search_count(
[('production_line_id.name', '=', line), [('production_id.production_line_id.name', '=', line),
('state', 'in', ['done']), ('routing_type', '=', 'CNC加工')]) ('state', 'in', ['done']), ('routing_type', '=', 'CNC加工')])
# 超期完成量 # 超期完成量
# 搜索所有已经完成的工单 # 搜索所有已经完成的工单
plan_data_overtime = work_order_obj.search([ plan_data_overtime = work_order_obj.search([
('production_line_id.name', '=', line), ('production_id.production_line_id.name', '=', line),
('state', 'in', ['done']), ('state', 'in', ['done']),
('routing_type', '=', 'CNC加工') ('routing_type', '=', 'CNC加工')
]) ])
@@ -448,14 +448,9 @@ class Sf_Dashboard_Connect(http.Controller):
]) ])
# 过滤出那些检测结果状态为 '返工' 或 '报废' 的记录 # 过滤出那些检测结果状态为 '返工' 或 '报废' 的记录
# faulty_plans = plan_data.filtered(lambda p: any( faulty_plans = plan_data.filtered(lambda p: any(
# result.test_results in ['返工', '报废'] for result in p.production_id.detection_result_ids result.test_results in ['返工', '报废'] for result in p.production_id.detection_result_ids
# )) ))
faulty_plans = request.env['quality.check'].sudo().search([
('operation_id.name', '=', 'CNC加工'),
('quality_state', 'in', ['fail'])
])
# 查找制造订单取消与归档的数量 # 查找制造订单取消与归档的数量
cancel_order_count = production_obj.search_count( cancel_order_count = production_obj.search_count(
@@ -571,8 +566,7 @@ class Sf_Dashboard_Connect(http.Controller):
:return: :return:
""" """
res = {'status': 1, 'message': '成功', 'data': {}} res = {'status': 1, 'message': '成功', 'data': {}}
# plan_obj = request.env['sf.production.plan'].sudo() plan_obj = request.env['sf.production.plan'].sudo()
# plan_obj = request.env['mrp.workorder'].sudo().search([('routing_type', '=', 'CNC加工')])
line_list = ast.literal_eval(kw['line_list']) line_list = ast.literal_eval(kw['line_list'])
begin_time_str = kw['begin_time'].strip('"') begin_time_str = kw['begin_time'].strip('"')
end_time_str = kw['end_time'].strip('"') end_time_str = kw['end_time'].strip('"')
@@ -610,7 +604,7 @@ class Sf_Dashboard_Connect(http.Controller):
return date_list return date_list
for line in line_list: for line in line_list:
date_field_name = 'date_finished' # 替换为你模型中的实际字段名 date_field_name = 'actual_end_time' # 替换为你模型中的实际字段名
order_counts = [] order_counts = []
if time_unit == 'hour': if time_unit == 'hour':
@@ -622,19 +616,11 @@ class Sf_Dashboard_Connect(http.Controller):
for time_interval in time_intervals: for time_interval in time_intervals:
start_time, end_time = time_interval start_time, end_time = time_interval
# orders = plan_obj.search([ orders = plan_obj.search([
# ('production_line_id.name', '=', line),
# ('state', 'in', ['done']),
# (date_field_name, '>=', start_time.strftime('%Y-%m-%d %H:%M:%S')),
# (date_field_name, '<=', end_time.strftime('%Y-%m-%d %H:%M:%S')) # 包括结束时间
# ])
orders = request.env['mrp.workorder'].sudo().search([
('routing_type', '=', 'CNC加工'), # 将第一个条件合并进来
('production_line_id.name', '=', line), ('production_line_id.name', '=', line),
('state', 'in', ['done']), ('state', 'in', ['finished']),
(date_field_name, '>=', start_time.strftime('%Y-%m-%d %H:%M:%S')), (date_field_name, '>=', start_time.strftime('%Y-%m-%d %H:%M:%S')),
(date_field_name, '<=', end_time.strftime('%Y-%m-%d %H:%M:%S')) (date_field_name, '<=', end_time.strftime('%Y-%m-%d %H:%M:%S')) # 包括结束时间
]) ])
# 使用小时和分钟作为键,确保每个小时的数据有独立的键 # 使用小时和分钟作为键,确保每个小时的数据有独立的键
@@ -651,22 +637,18 @@ class Sf_Dashboard_Connect(http.Controller):
for date in date_list: for date in date_list:
next_day = date + timedelta(days=1) next_day = date + timedelta(days=1)
orders = request.env['mrp.workorder'].sudo().search( orders = plan_obj.search([('production_line_id.name', '=', line), ('state', 'in', ['finished']),
[('production_id.production_line_id.name', '=', line), ('state', 'in', ['done']), (date_field_name, '>=', date.strftime('%Y-%m-%d 00:00:00')),
('routing_type', '=', 'CNC加工'), (date_field_name, '<', next_day.strftime('%Y-%m-%d 00:00:00'))
(date_field_name, '>=', date.strftime('%Y-%m-%d 00:00:00')), ])
(date_field_name, '<', next_day.strftime('%Y-%m-%d 00:00:00'))
])
rework_orders = request.env['mrp.workorder'].sudo().search( rework_orders = plan_obj.search(
[('production_id.production_line_id.name', '=', line), ('state', 'in', ['rework']), [('production_line_id.name', '=', line), ('production_id.state', 'in', ['rework']),
('routing_type', '=', 'CNC加工'),
(date_field_name, '>=', date.strftime('%Y-%m-%d 00:00:00')), (date_field_name, '>=', date.strftime('%Y-%m-%d 00:00:00')),
(date_field_name, '<', next_day.strftime('%Y-%m-%d 00:00:00')) (date_field_name, '<', next_day.strftime('%Y-%m-%d 00:00:00'))
]) ])
not_passed_orders = request.env['mrp.workorder'].sudo().search( not_passed_orders = plan_obj.search(
[('production_id.production_line_id.name', '=', line), ('state', 'in', ['scrap', 'cancel']), [('production_line_id.name', '=', line), ('production_id.state', 'in', ['scrap', 'cancel']),
('routing_type', '=', 'CNC加工'),
(date_field_name, '>=', date.strftime('%Y-%m-%d 00:00:00')), (date_field_name, '>=', date.strftime('%Y-%m-%d 00:00:00')),
(date_field_name, '<', next_day.strftime('%Y-%m-%d 00:00:00')) (date_field_name, '<', next_day.strftime('%Y-%m-%d 00:00:00'))
]) ])
@@ -768,14 +750,11 @@ class Sf_Dashboard_Connect(http.Controller):
for line in line_list: for line in line_list:
# 未完成订单 # 未完成订单
# not_done_orders = plan_obj.search( not_done_orders = plan_obj.search(
# [('production_line_id.name', '=', line), ('state', 'not in', ['finished']), [('production_line_id.name', '=', line), ('state', 'not in', ['finished']),
# ('production_id.state', 'not in', ['cancel', 'done']), ('active', '=', True) ('production_id.state', 'not in', ['cancel', 'done']), ('active', '=', True)
# ])
not_done_orders = request.env['mrp.workorder'].sudo().search(
[('production_line_id.name', '=', line), ('state', 'in', ['ready', 'progress']),
('routing_type', '=', 'CNC加工')
]) ])
# print(not_done_orders)
# 完成订单 # 完成订单
# 获取当前时间并计算24小时前的时间 # 获取当前时间并计算24小时前的时间
@@ -827,18 +806,16 @@ class Sf_Dashboard_Connect(http.Controller):
'draft': '待排程', 'draft': '待排程',
'done': '已排程', 'done': '已排程',
'processing': '生产中', 'processing': '生产中',
'finished': '已完成', 'finished': '已完成'
'ready': '待加工',
'progress': '生产中',
} }
line_dict = { line_dict = {
'sequence': id_to_sequence[order.id], 'sequence': id_to_sequence[order.id],
'workorder_name': order.production_id.name, 'workorder_name': order.name,
'blank_name': blank_name, 'blank_name': blank_name,
'material': material, 'material': material,
'dimensions': dimensions, 'dimensions': dimensions,
'order_qty': 1, 'order_qty': order.product_qty,
'state': state_dict[order.state], 'state': state_dict[order.state],
} }
@@ -919,17 +896,15 @@ class Sf_Dashboard_Connect(http.Controller):
cur.execute(sql2, (item,)) cur.execute(sql2, (item,))
result2 = cur.fetchall() result2 = cur.fetchall()
# print('result2========', result2)
#
for row in result: for row in result:
res['data'][item] = {'idle_count': row[0]} res['data'][item] = {'idle_count': row[0]}
alarm_count = [] alarm_count = []
for row in result2: for row in result2:
alarm_count.append(row[1]) alarm_count.append(row[1])
if row[0]: if row[0]:
if float(row[0]) >= 28800: total_alarm_time += abs(float(row[0]))
continue
# total_alarm_time += abs(float(row[0]))
total_alarm_time += float(row[0])
else: else:
total_alarm_time += 0.0 total_alarm_time += 0.0
if len(list(set(alarm_count))) == 1: if len(list(set(alarm_count))) == 1:
@@ -939,7 +914,6 @@ class Sf_Dashboard_Connect(http.Controller):
alarm_count_num = 1 alarm_count_num = 1
else: else:
alarm_count_num = len(list(set(alarm_count))) alarm_count_num = len(list(set(alarm_count)))
res['data'][item]['total_alarm_time'] = total_alarm_time / 3600 res['data'][item]['total_alarm_time'] = total_alarm_time / 3600
res['data'][item]['alarm_count_num'] = alarm_count_num res['data'][item]['alarm_count_num'] = alarm_count_num
@@ -967,7 +941,7 @@ class Sf_Dashboard_Connect(http.Controller):
# machine_list = ast.literal_eval(kw['machine_list']) # machine_list = ast.literal_eval(kw['machine_list'])
# for item in machine_list: # for item in machine_list:
# machine_data = equipment_obj.search([('code', '=', item)]) # machine_data = equipment_obj.search([('code', '=', item)])
for log in maintenance_logs_obj.search([], order='id desc', limit=30): for log in maintenance_logs_obj.search([]):
res['data'].append({ res['data'].append({
'name': log.name, 'name': log.name,
'alarm_time': log.alarm_time.strftime('%Y-%m-%d %H:%M:%S'), 'alarm_time': log.alarm_time.strftime('%Y-%m-%d %H:%M:%S'),
@@ -1155,9 +1129,8 @@ class Sf_Dashboard_Connect(http.Controller):
cur.execute(""" cur.execute("""
SELECT * FROM device_data SELECT * FROM device_data
WHERE device_name = %s WHERE device_name = %s
AND time >= CURRENT_DATE -- 今日 00:00:00 AND time::date = CURRENT_DATE
AND time < CURRENT_DATE + 1 -- 明日 00:00:00 AND device_state != '离线'
AND device_state in ('待机', '警告', '运行中')
ORDER BY time ASC ORDER BY time ASC
LIMIT 1; LIMIT 1;
""", (item,)) """, (item,))
@@ -1169,9 +1142,8 @@ class Sf_Dashboard_Connect(http.Controller):
cur.execute(""" cur.execute("""
SELECT * FROM device_data SELECT * FROM device_data
WHERE device_name = %s WHERE device_name = %s
AND time >= CURRENT_DATE -- 今日 00:00:00 AND time::date = CURRENT_DATE
AND time < CURRENT_DATE + 1 -- 明日 00:00:00 AND device_state != '离线'
AND device_state in ('待机', '警告', '运行中')
ORDER BY time DESC ORDER BY time DESC
LIMIT 1; LIMIT 1;
""", (item,)) """, (item,))
@@ -1191,23 +1163,23 @@ class Sf_Dashboard_Connect(http.Controller):
cur.execute(""" cur.execute("""
SELECT * FROM device_data SELECT * FROM device_data
WHERE device_name = %s WHERE device_name = %s
AND time >= DATE_TRUNC('MONTH', CURRENT_DATE) AND EXTRACT(YEAR FROM time) = EXTRACT(YEAR FROM CURRENT_DATE)
AND time < DATE_TRUNC('MONTH', CURRENT_DATE) + INTERVAL '1 MONTH' AND EXTRACT(MONTH FROM time) = EXTRACT(MONTH FROM CURRENT_DATE)
AND device_state in ('待机', '警告', '运行中') AND device_state != '离线'
ORDER BY time ASC ORDER BY time ASC
LIMIT 1; LIMIT 1;
""", (item,)) """, (item,))
first_month = fetch_result_as_dict(cur) first_month = fetch_result_as_dict(cur)
# print("当月第一条记录:", first_month) # print("当月第一条记录(非离线):", first_month)
# 获取当月最新一条记录 # 获取当月最新一条记录排除device_state等于离线的记录
with conn.cursor() as cur: with conn.cursor() as cur:
cur.execute(""" cur.execute("""
SELECT * FROM device_data SELECT * FROM device_data
WHERE device_name = %s WHERE device_name = %s
AND time >= DATE_TRUNC('MONTH', CURRENT_DATE) AND EXTRACT(YEAR FROM time) = EXTRACT(YEAR FROM CURRENT_DATE)
AND time < DATE_TRUNC('MONTH', CURRENT_DATE) + INTERVAL '1 MONTH' AND EXTRACT(MONTH FROM time) = EXTRACT(MONTH FROM CURRENT_DATE)
AND device_state in ('待机', '警告', '运行中') AND device_state != '离线'
ORDER BY time DESC ORDER BY time DESC
LIMIT 1; LIMIT 1;
""", (item,)) """, (item,))
@@ -1228,7 +1200,7 @@ class Sf_Dashboard_Connect(http.Controller):
cur.execute(""" cur.execute("""
SELECT * FROM device_data SELECT * FROM device_data
WHERE device_name = %s WHERE device_name = %s
AND device_state in ('待机', '警告', '运行中') AND device_state != '离线'
ORDER BY time ASC ORDER BY time ASC
LIMIT 1; LIMIT 1;
""", (item,)) """, (item,))
@@ -1240,7 +1212,7 @@ class Sf_Dashboard_Connect(http.Controller):
cur.execute(""" cur.execute("""
SELECT * FROM device_data SELECT * FROM device_data
WHERE device_name = %s WHERE device_name = %s
AND device_state in ('待机', '警告', '运行中') AND device_state != '离线'
ORDER BY time DESC ORDER BY time DESC
LIMIT 1; LIMIT 1;
""", (item,)) """, (item,))
@@ -1318,7 +1290,7 @@ class Sf_Dashboard_Connect(http.Controller):
cur.execute(""" cur.execute("""
SELECT * FROM device_data SELECT * FROM device_data
WHERE device_name = %s WHERE device_name = %s
AND device_state in ('待机', '警告', '运行中') AND process_time IS NOT NULL AND device_state != '离线' AND process_time IS NOT NULL
ORDER BY time DESC ORDER BY time DESC
LIMIT 1; LIMIT 1;
""", (item,)) """, (item,))
@@ -1327,7 +1299,7 @@ class Sf_Dashboard_Connect(http.Controller):
cur.execute(""" cur.execute("""
SELECT * FROM device_data SELECT * FROM device_data
WHERE device_name = %s WHERE device_name = %s
AND device_state in ('待机', '警告', '运行中') AND time >= %s AND process_time IS NOT NULL AND device_state != '离线' AND time >= %s AND process_time IS NOT NULL
ORDER BY time ASC ORDER BY time ASC
LIMIT 1; LIMIT 1;
""", (item, time_threshold)) """, (item, time_threshold))
@@ -1357,7 +1329,7 @@ class Sf_Dashboard_Connect(http.Controller):
for result in results: for result in results:
alarm_last_24_nums.append(result[1]) alarm_last_24_nums.append(result[1])
if result[0]: if result[0]:
if float(result[0]) >= 28800: if float(result[0]) >= 1000:
continue continue
alarm_last_24_time += float(result[0]) alarm_last_24_time += float(result[0])
else: else:
@@ -1375,7 +1347,7 @@ class Sf_Dashboard_Connect(http.Controller):
for result in results: for result in results:
alarm_all_nums.append(result[1]) alarm_all_nums.append(result[1])
if result[0]: if result[0]:
if float(result[0]) >= 28800: if float(result[0]) >= 1000:
continue continue
alarm_all_time += float(result[0]) alarm_all_time += float(result[0])
else: else:
@@ -1410,207 +1382,3 @@ class Sf_Dashboard_Connect(http.Controller):
conn.close() conn.close()
return json.dumps(res) return json.dumps(res)
@http.route('/api/utilization/rate', type='http', auth='public', methods=['GET', 'POST'], csrf=False, cors="*")
def UtilizationRate(self, **kw):
"""
获取稼动率
"""
logging.info("kw=:%s" % kw)
res = {'status': 1, 'message': '成功', 'data': {}}
# 获取请求的机床数据
machine_list = ast.literal_eval(kw['machine_list'])
line = kw['line']
orders = request.env['mrp.workorder'].sudo().search([
('routing_type', '=', 'CNC加工'), # 将第一个条件合并进来
('production_line_id.name', '=', line),
('state', 'in', ['done'])
])
faulty_plans = request.env['quality.check'].sudo().search([
('operation_id.name', '=', 'CNC加工'),
('quality_state', 'in', ['fail'])
])
# 计算时间范围
now = datetime.now()
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
month_start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
total_power_on_time = 0
month_power_on_time = 0
today_power_on_time = 0
today_power_on_dict = {}
today_data = []
month_data = []
today_check_ng = []
month_check_ng = []
total_alarm_time = 0
today_alarm_time = 0
month_alarm_time = 0
for order in orders:
time = datetime.strptime(order.date_finished, "%Y-%m-%d %H:%M:%S")
if time >= today_start:
today_data.append(order)
if time >= month_start:
month_data.append(order)
for faulty_plan in faulty_plans:
time = faulty_plan.write_date
if time >= today_start:
today_check_ng.append(faulty_plan)
if time >= month_start:
month_check_ng.append(faulty_plan)
# 连接数据库
conn = psycopg2.connect(**db_config)
for item in machine_list:
with conn.cursor() as cur:
cur.execute("""
(
SELECT power_on_time, 'latest' AS record_type
FROM device_data
WHERE device_name = %s
AND power_on_time IS NOT NULL
ORDER BY time DESC
LIMIT 1
)
UNION ALL
(
SELECT power_on_time, 'month_first' AS record_type
FROM device_data
WHERE device_name = %s
AND power_on_time IS NOT NULL
AND time >= date_trunc('month', CURRENT_DATE) -- ✅ 修复日期函数
AND time < (date_trunc('month', CURRENT_DATE) + INTERVAL '1 month')::date
ORDER BY time ASC
LIMIT 1
)
UNION ALL
(
SELECT power_on_time, 'day_first' AS record_type
FROM device_data
WHERE device_name = %s
AND power_on_time IS NOT NULL
AND time::date = CURRENT_DATE -- ✅ 更高效的写法
ORDER BY time ASC
LIMIT 1
);
""", (item, item, item))
results = cur.fetchall()
print(results)
if len(results) >= 1:
total_power_on_time += convert_to_seconds(results[0][0])
else:
total_power_on_time += 0
if len(results) >= 2:
month_power_on_time += convert_to_seconds(results[1][0])
else:
month_power_on_time += 0
if len(results) >= 3:
today_power_on_time += convert_to_seconds(results[2][0])
today_power_on_dict[item] = today_power_on_time
else:
today_power_on_time += 0
print(total_power_on_time, month_power_on_time, today_power_on_time)
with conn.cursor() as cur:
cur.execute("""
SELECT DISTINCT ON (alarm_start_time) alarm_time, alarm_start_time
FROM device_data
WHERE device_name = %s AND alarm_start_time IS NOT NULL
ORDER BY alarm_start_time, time;
""", (item,))
results = cur.fetchall()
today_data = []
month_data = []
for record in results:
if record[0]:
if float(record[0]) >= 28800:
continue
total_alarm_time += float(record[0])
else:
total_alarm_time += 0.0
alarm_start = datetime.strptime(record[1], "%Y-%m-%d %H:%M:%S")
if alarm_start >= today_start:
today_data.append(record)
if alarm_start >= month_start:
month_data.append(record)
for today in today_data:
if today[0]:
if float(today[0]) >= 28800:
continue
today_alarm_time += float(today[0])
else:
today_alarm_time += 0.0
for month in month_data:
if month[0]:
if float(month[0]) >= 28800:
continue
month_alarm_time += float(month[0])
else:
month_alarm_time += 0.0
conn.close()
print('报警时间=============', total_alarm_time, month_alarm_time, today_alarm_time)
logging.info("报警时间=%s" % total_alarm_time)
logging.info("报警时间=%s" % month_alarm_time)
logging.info("报警时间=%s" % today_alarm_time)
# 计算时间开动率(累计、月、日)
if total_power_on_time:
total_power_on_rate = (total_power_on_time - total_alarm_time) / total_power_on_time
else:
total_power_on_rate = 0
if month_power_on_time:
month_power_on_rate = (total_power_on_time - month_power_on_time - month_alarm_time) / month_power_on_time
else:
month_power_on_rate = 0
if today_power_on_time:
today_power_on_rate = (total_power_on_time - today_power_on_time - today_alarm_time) / today_power_on_time
else:
today_power_on_rate = 0
print("总开动率: %s" % total_power_on_rate)
print("月开动率: %s" % month_power_on_rate)
print("日开动率: %s" % today_power_on_rate)
# 计算性能开动率(累计、月、日)
print('===========',orders)
print(len(orders))
total_performance_rate = len(orders) * 30 * 60 / (total_power_on_time - total_alarm_time)
month_performance_rate = len(month_data) * 30 * 60 / (month_power_on_time - month_alarm_time)
today_performance_rate = len(today_data) * 30 * 60 / (today_power_on_time - today_alarm_time) if today_power_on_time != 0 else 0
print("总性能率: %s" % total_performance_rate)
print("月性能率: %s" % month_performance_rate)
print("日性能率: %s" % today_performance_rate)
# 计算累计合格率
total_pass_rate = (len(orders) - len(today_check_ng)) / len(orders) if len(orders) != 0 else 0
month_pass_rate = (len(month_data) - len(month_check_ng)) / len(month_data) if len(month_data) != 0 else 0
today_pass_rate = (len(today_data) - len(today_check_ng)) / len(today_data) if len(today_data) != 0 else 0
print("总合格率: %s" % total_pass_rate)
print("月合格率: %s" % month_pass_rate)
print("日合格率: %s" % today_pass_rate)
# # 返回数据
# res['data'][item] = {
# 'total_utilization_rate': total_power_on_rate * total_performance_rate * total_pass_rate,
# 'month_utilization_rate': month_power_on_rate * month_performance_rate * month_pass_rate,
# 'today_utilization_rate': today_power_on_rate * today_performance_rate * today_pass_rate,
# }
res['data'] = {
'total_utilization_rate': total_power_on_rate * total_performance_rate * total_pass_rate,
'month_utilization_rate': month_power_on_rate * month_performance_rate * month_pass_rate,
'today_utilization_rate': today_power_on_rate * today_performance_rate * today_pass_rate,
}
return json.dumps(res)

View File

@@ -7,7 +7,7 @@
<field name="arch" type="xml"> <field name="arch" type="xml">
<xpath expr="//page[last()-3]" position="before"> <xpath expr="//page[last()-3]" position="before">
<!-- <page string="下发记录" attrs='{"invisible": [("routing_type","!=","CNC加工")]}'>--> <!-- <page string="下发记录" attrs='{"invisible": [("routing_type","!=","CNC加工")]}'>-->
<page string="下发记录" attrs='{"invisible": ["!", ("individuation_page_list", "ilike", "HDR")]}'> <page string="下发记录" attrs='{"invisible": [("routing_type","!=","CNC加工")]}'>
<field name="delivery_records"> <field name="delivery_records">
<tree create="false"> <tree create="false">
<field name="delivery_type"/> <field name="delivery_type"/>

View File

@@ -6,7 +6,7 @@
<field name="inherit_id" ref="mrp.mrp_production_workorder_form_view_inherit"/> <field name="inherit_id" ref="mrp.mrp_production_workorder_form_view_inherit"/>
<field name="arch" type="xml"> <field name="arch" type="xml">
<xpath expr="//page[last()-3]" position="before"> <xpath expr="//page[last()-3]" position="before">
<page string="机床信息" attrs='{"invisible": ["!", ("individuation_page_list", "ilike", "MTI")]}'> <page string="机床信息" attrs='{"invisible": [("routing_type","!=","CNC加工")]}'>
<group string="机床信息"> <group string="机床信息">
<group> <group>
<field name="machine_tool_name"/> <field name="machine_tool_name"/>

View File

@@ -27,7 +27,6 @@
'wizard/production_technology_re_adjust_wizard_views.xml', 'wizard/production_technology_re_adjust_wizard_views.xml',
'wizard/mrp_workorder_batch_replan_wizard_views.xml', 'wizard/mrp_workorder_batch_replan_wizard_views.xml',
'wizard/sf_programming_reason_views.xml', 'wizard/sf_programming_reason_views.xml',
'wizard/sale_order_cancel_views.xml',
'views/mrp_views_menus.xml', 'views/mrp_views_menus.xml',
'views/agv_scheduling_views.xml', 'views/agv_scheduling_views.xml',
'views/stock_lot_views.xml', 'views/stock_lot_views.xml',

View File

@@ -596,9 +596,6 @@ class Manufacturing_Connect(http.Controller):
if panel_workorder: if panel_workorder:
panel_workorder.write({'production_line_state': '已下产线'}) panel_workorder.write({'production_line_state': '已下产线'})
workorder.write({'state': 'to be detected'}) workorder.write({'state': 'to be detected'})
workorder.check_ids.filtered(
lambda ch: ch.quality_state == 'waiting').write(
{'quality_state': 'none'})
else: else:
res = {'Succeed': False, 'ErrorCode': 204, res = {'Succeed': False, 'ErrorCode': 204,
'Error': 'DeviceId为%s没有对应的已配送工件数据' % ret['DeviceId']} 'Error': 'DeviceId为%s没有对应的已配送工件数据' % ret['DeviceId']}

View File

@@ -27,8 +27,7 @@ class JikimoSaleRoutePicking(Sf_Bf_Connect):
bfm_process_order_list = json.loads(kw['bfm_process_order_list']) bfm_process_order_list = json.loads(kw['bfm_process_order_list'])
order_id = request.env['sale.order'].with_user(request.env.ref("base.user_admin")).sale_order_create( order_id = request.env['sale.order'].with_user(request.env.ref("base.user_admin")).sale_order_create(
company_id, kw['delivery_name'], kw['delivery_telephone'], kw['delivery_address'], company_id, kw['delivery_name'], kw['delivery_telephone'], kw['delivery_address'],
kw['delivery_end_date'], kw['payments_way'], kw['pay_way'], kw['order_number'], state='draft', kw['delivery_end_date'], kw['payments_way'], kw['pay_way'], kw['order_number'], state='draft')
model_display_version=kw.get('model_display_version'))
i = 1 i = 1
# 给sale_order的default_code字段赋值 # 给sale_order的default_code字段赋值
# aa = request.env['sale.order'].sudo().search([('name', '=', order_id.name)]) # aa = request.env['sale.order'].sudo().search([('name', '=', order_id.name)])
@@ -46,7 +45,6 @@ class JikimoSaleRoutePicking(Sf_Bf_Connect):
order_id.with_user(request.env.ref("base.user_admin")).sale_order_create_line(product, item) order_id.with_user(request.env.ref("base.user_admin")).sale_order_create_line(product, item)
i += 1 i += 1
res['factory_order_no'] = order_id.name res['factory_order_no'] = order_id.name
order_id.confirm_to_supply_method()
except Exception as e: except Exception as e:
traceback_error = traceback.format_exc() traceback_error = traceback.format_exc()
logging.error('get_bfm_process_order_list error: %s' % traceback_error) logging.error('get_bfm_process_order_list error: %s' % traceback_error)

View File

@@ -4,63 +4,5 @@
<field name="code">PTD</field> <field name="code">PTD</field>
<field name="name">后置三元检测</field> <field name="name">后置三元检测</field>
</record> </record>
<record model="sf.work.individuation.page" id="sf_work_individuation_page_2">
<field name="code">WCP</field>
<field name="name">工件装夹</field>
</record>
<record model="sf.work.individuation.page" id="sf_work_individuation_page_3">
<field name="code">ITD_PP</field>
<field name="name">前置三元检测定位参数</field>
</record>
<record model="sf.work.individuation.page" id="sf_work_individuation_page_4">
<field name="code">2D_MD</field>
<field name="name">2D加工图纸</field>
</record>
<record model="sf.work.individuation.page" id="sf_work_individuation_page_5">
<field name="code">QIS</field>
<field name="name">质检标准</field>
</record>
<record model="sf.work.individuation.page" id="sf_work_individuation_page_6">
<field name="code">WD</field>
<field name="name">工件配送</field>
</record>
<record model="sf.work.individuation.page" id="sf_work_individuation_page_9">
<field name="code">CNC_P</field>
<field name="name">CNC程序</field>
</record>
<record model="sf.work.individuation.page" id="sf_work_individuation_page_10">
<field name="code">CMM_P</field>
<field name="name">CMM程序</field>
</record>
<record model="sf.work.individuation.page" id="sf_work_individuation_page_11">
<field name="code">MTI</field>
<field name="name">机床信息</field>
</record>
<record model="sf.work.individuation.page" id="sf_work_individuation_page_12">
<field name="code">HDR</field>
<field name="name">下发记录</field>
</record>
<record model="sf.work.individuation.page" id="sf_work_individuation_page_13">
<field name="code">ER</field>
<field name="name">异常记录</field>
</record>
<record model="sf.work.individuation.page" id="sf_work_individuation_page_14">
<field name="code">DCP</field>
<field name="name">解除装夹</field>
</record>
<record model="sf.work.individuation.page" id="sf_work_individuation_page_15">
<field name="code">CMR</field>
<field name="name">开料要求</field>
</record>
<!-- 原生页签先不进行配置 -->
<!-- <record model="sf.work.individuation.page" id="sf_work_individuation_page_7">-->
<!-- <field name="code">ML</field>-->
<!-- <field name="name">物料</field>-->
<!-- </record>-->
<!-- <record model="sf.work.individuation.page" id="sf_work_individuation_page_8">-->
<!-- <field name="code">TT</field>-->
<!-- <field name="name">时间跟踪</field>-->
<!-- </record>-->
</data> </data>
</odoo> </odoo>

Some files were not shown because too many files have changed in this diff Show More