Merge branch 'refs/heads/develop' into feature/tool_standard_library_process

# Conflicts:
#	sf_manufacturing/models/mrp_workorder.py
This commit is contained in:
liaodanlong
2025-02-20 15:48:22 +08:00
78 changed files with 1280 additions and 806 deletions

View File

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

View File

@@ -1,4 +1,4 @@
from odoo import models, fields, api
from odoo import models, fields, api, _
from odoo.exceptions import ValidationError
@@ -7,6 +7,14 @@ class CustomAccountMoveLine(models.Model):
_inherit = 'account.move'
_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
def create(self, vals):
for val in vals:

View File

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

View File

@@ -1,3 +1,8 @@
.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;
}
.custom_required_add::before{
content: '*';
color: red;
}

View File

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

View File

@@ -1,53 +0,0 @@
/** @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

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

@@ -1,100 +0,0 @@
.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

@@ -1,60 +0,0 @@
/** @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

@@ -1,23 +0,0 @@
<?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,8 +6,9 @@ import {_t} from "@web/core/l10n/translation";
import {FormStatusIndicator} from "@web/views/form/form_status_indicator/form_status_indicator";
import {ListRenderer} from "@web/views/list/list_renderer";
// 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 {patch} = require("web.utils") 这句话也行
@@ -51,7 +52,6 @@ const tableRequiredList = [
'product_template_id', 'product_uom_qty', 'price_unit','product_id','product_qty',
'name', 'fault_type', 'maintenance_standards', 'Period'
]
patch(FormStatusIndicator.prototype, 'jikimo_frontend.FormStatusIndicator', {
setup() {
owl.onMounted(() => {
@@ -107,33 +107,7 @@ 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', {
setup(){
owl.onMounted(() => {
@@ -191,7 +165,33 @@ patch(ListRenderer.prototype, 'jikimo_frontend.ListRenderer', {
}
}
})
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 = {
@@ -231,7 +231,6 @@ $(function () {
clearInterval(timer)
timer = setInterval(() => {
timer_count++
const dom = $('.custom_required')
let tableDom = $('.table_custom_required')
if (tableDom.length) {
tableDom = tableDom.eq(0).parents('tr').children('.table_custom_required')
@@ -243,17 +242,6 @@ $(function () {
})
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) {
clearInterval(timer)
}

View File

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

View File

@@ -24,9 +24,6 @@
# always loaded
'data': [
'security/ir.model.access.csv',
'data/documents_data.xml',
'wizards/upload_file_wizard_view.xml',
'views/views.xml',
],
# only loaded in demonstration mode

View File

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

View File

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

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

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

View File

@@ -21,12 +21,8 @@ class jikimo_purchase_tier_validation(models.Model):
def button_confirm(self):
for record in self:
# if record.need_validation and record.validation_status != 'validated':
# 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()
for record in self:
if record.state == 'approved':
@@ -39,45 +35,8 @@ class jikimo_purchase_tier_validation(models.Model):
record.message_subscribe([record.partner_id.id])
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):
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'):
current_user = self.env.user.name

View File

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

@@ -1,24 +0,0 @@
<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,76 +23,10 @@
<xpath expr="//header/field[@name='state']" position="replace">
<field name="state" widget="statusbar" statusbar_visible="draft,sent,to approve, approved, purchase" readonly="1"/>
</xpath>
<xpath expr="//header/button[last()]" position="after">
<button name="button_cancel" states="draft,to approve,sent,purchase" string="取消" type="object" data-hotkey="x" />
</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>
</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>
</odoo>

View File

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

View File

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

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

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

View File

@@ -0,0 +1,32 @@
{
'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

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

View File

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

@@ -0,0 +1 @@

View File

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

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

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

View File

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

@@ -0,0 +1,47 @@
<?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,6 +4,7 @@ import json
import logging
from odoo.addons.sf_mrs_connect.controllers.controllers import Sf_Mrs_Connect
from odoo.addons.sf_manufacturing.controllers.controllers import Manufacturing_Connect
from datetime import datetime
_logger = logging.getLogger(__name__)
@@ -30,6 +31,7 @@ class WorkorderExceptionConroller(http.Controller):
workorder = request.env['mrp.workorder'].sudo().search([
('rfid_code', '=', ret['RfidCode']),
('routing_type', '=', 'CNC加工'),
('state', '!=', 'rework')
])
if not workorder:
res = {'Succeed': False, 'ErrorCode': 401, 'Error': '无效的工单'}
@@ -41,7 +43,10 @@ class WorkorderExceptionConroller(http.Controller):
'exception_code': ret.get('coding'),
'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:
res = {'Succeed': False, 'ErrorCode': 202, 'Error': e}
_logger.info('workder_exception error:%s' % e)

View File

@@ -9,6 +9,7 @@ function getDomData() {
table.hide()
const thead = customTable.children('thead')
const tbody = customTable.children('tbody')
const tfooter = customTable.children('tfoot')
const tableData = []
const tbody_child = tbody.children()
@@ -16,30 +17,29 @@ function getDomData() {
for (let v = 0; v < tbody_child_len; v++) { // 将数据取出来到tableData里面
const data = tbody_child[v].innerText.split('\t')
// console.log('dom data',data)
const [index, deep, name, Φ, value] = data
tableData.push({ index, deep, name, Φ, value })
}
const ΦList = [...new Set(tableData.map(_ => _.name))] // ΦList去重
const ΦList = [...new Set(tableData.map(_ => _.Φ))] // ΦList去重
const newTableData = {}
tableData.forEach(_ => {
const key = _.deep + '|' + _.Φ
const key = _.deep + '|' + _.name
!newTableData[key] ? newTableData[key] = { i: _.index } : '';
if (_.Φ) { // 去除没有Φ的脏数据
newTableData[key]['Φ' + _.Φ] = _.value
newTableData[key]['Φ' + _.Φ + 'i'] = _.index
}
})
// console.log('qwdh',tableData, ΦList, newTableData);
// console.log(tableData, ΦList, newTableData);
if (ΦList.filter(_ => _).length == 0) return;
handleThead(thead, ΦList)
handleThead(thead, ΦList, tfooter)
handleTbody(tbody, newTableData, ΦList, table )
}
// 重新设置表头、
function handleThead(thead, ΦList) {
function handleThead(thead, ΦList, tfooter) {
const dom = thead.children().eq(0).children()
const len = dom.length
dom.eq(0).attr('rowspan', 2)
@@ -47,7 +47,11 @@ function handleThead(thead, ΦList) {
len == 5 ? dom.eq(2).attr('rowspan', 2) : ''
dom.eq(-2).attr('colspan', ΦList.length)
dom.eq(-1).remove()
if(tfooter && tfooter.length) {
tfooter.children().each(function () {
$(this).children().eq(-1).remove()
})
}
const tr = document.createElement('tr')
for (let v = 0; v < ΦList.length; v++) {
const th = document.createElement('th')
@@ -68,7 +72,6 @@ function handleTbody(tbody, newTableData, ΦList, table) {
// b = b.split('=')[1].split('%')[0]
// return a - b
// })
// console.log('wqoqw ',ΦList)
data.forEach(_ => {
i++
const tr = $('<tr class="o_data_row"></tr>')
@@ -98,61 +101,6 @@ function handleTbody(tbody, newTableData, ΦList, table) {
// // }
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

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

View File

@@ -140,7 +140,7 @@
<field name="brand_id" required="1"/>
<label for="integral_run_out_accuracy_min" string="端跳精度"
attrs="{'invisible': [('cutting_tool_type', '!=', '整体式刀具')]}"/>
<div class="o_address_format"
<div class="o_address_format multi-line"
attrs="{'invisible': [('cutting_tool_type', '!=', '整体式刀具')]}">
<label for="integral_run_out_accuracy_min" string="最小"/>
<field name="integral_run_out_accuracy_min" class="o_address_zip"
@@ -179,33 +179,33 @@
</group>
<group string="适配刀片形状"
attrs="{'invisible': [('cutting_tool_type', 'in', ('刀柄','夹头','整体式刀具',False))]}">
<field name="fit_blade_shape_id" string="" widget="many2one_radio"/>
<field name="fit_blade_shape_id" string="" widget="many2one_radio" attrs="{'showExpand': True}"/>
</group>
<group string="适合加工方式"
attrs="{'invisible': [('cutting_tool_type', 'not in', ('整体式刀具','刀杆','刀盘','刀片'))]}">
<field name="suitable_machining_method_ids" string=""
widget="custom_many2many_checkboxes"/>
widget="custom_many2many_checkboxes" attrs="{'showExpand': True}"/>
</group>
<group string="刀尖特征"
attrs="{'invisible': [('cutting_tool_type', 'not in', ('整体式刀具','刀杆','刀盘','刀片'))]}">
<field name="blade_tip_characteristics_id" string=""
widget="many2one_radio"/>
widget="many2one_radio" attrs="{'showExpand': True}"/>
</group>
<group attrs="{'invisible': [('cutting_tool_type', 'not in', ('整体式刀具','刀杆','刀盘','刀片'))]}">
<group string="柄部类型" attrs="{'invisible': [('cutting_tool_type', '!=', '整体式刀具')]}">
<field name="handle_type_id" string="" widget="many2one_radio"/>
<field name="handle_type_id" string="" widget="many2one_radio" attrs="{'showExpand': True}"/>
</group>
<group string="压紧方式"
attrs="{'invisible': [('cutting_tool_type', 'not in', ('刀杆','刀盘'))]}">
<field name="compaction_way_id" string="" widget="many2one_radio"/>
<field name="compaction_way_id" string="" widget="many2one_radio" attrs="{'showExpand': True}"/>
</group>
</group>
<group attrs="{'invisible': [('cutting_tool_type', 'not in', ('整体式刀具','刀杆','刀盘','刀片'))]}">
<group string="走刀方向">
<field name="cutting_direction_ids" string="" widget="custom_many2many_checkboxes"/>
<field name="cutting_direction_ids" string="" widget="custom_many2many_checkboxes" attrs="{'showExpand': True}"/>
</group>
<group string="适合冷却方式">
<field name="suitable_coolant_ids" string="" widget="custom_many2many_checkboxes"/>
<field name="suitable_coolant_ids" string="" widget="custom_many2many_checkboxes" attrs="{'showExpand': True}" />
</group>
</group>
<notebook>

View File

@@ -132,6 +132,26 @@ class Sf_Bf_Connect(http.Controller):
request.cr.rollback()
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):

View File

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

View File

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

View File

@@ -19,12 +19,8 @@ class AgvScheduling(models.Model):
_order = 'id desc'
name = fields.Char('任务单号', index=True, copy=False)
def _get_agv_route_type_selection(self):
return self.env['sf.agv.task.route'].fields_get(['route_type'])['route_type']['selection']
agv_route_type = fields.Selection(selection=_get_agv_route_type_selection, string='任务类型', required=True)
agv_route_id = fields.Many2one('sf.agv.task.route', '任务路线')
agv_route_type = fields.Selection(related='agv_route_id.route_type', string='任务类型', required=True)
start_site_id = fields.Many2one('sf.agv.site', '起点接驳站', required=True)
end_site_id = fields.Many2one('sf.agv.site', '终点接驳站', tracking=True)
site_state = fields.Selection([

View File

@@ -235,7 +235,7 @@ class MrpProduction(models.Model):
programming_no = fields.Char('编程单号')
work_state = fields.Char('业务状态')
programming_state = fields.Selection(
[('编程中', '编程中'), ('已编程', '已编程'), ('已编程未下发', '已编程未下发'), ('已下发', '已下发')],
[('编程中', '编程中'), ('已编程', '已编程'), ('已编程未下发', '已编程未下发'), ('已下发', '已下发'), ('已取消', '已取消')],
string='编程状态',
tracking=True)
glb_file = fields.Binary("glb模型文件")
@@ -577,16 +577,19 @@ class MrpProduction(models.Model):
# 编程单更新
# 增加触发时间参数
def update_programming_state(self, trigger_time=None):
def update_programming_state(self, trigger_time=None, reprogramming_reason=None):
try:
manufacturing_type = 'rework'
manufacturing_type = None
if self.is_scrap:
manufacturing_type = 'scrap'
elif self.tool_state == '2':
manufacturing_type = 'invalid_tool_rework'
elif self.is_rework:
manufacturing_type = 'rework'
res = {'programming_no': self.programming_no,
'manufacturing_type': manufacturing_type,
'trigger_time': trigger_time}
'trigger_time': trigger_time,
'reprogramming_reason': reprogramming_reason}
logging.info('res=%s:' % res)
configsettings = self.env['res.config.settings'].get_values()
config_header = Common.get_headers(self, configsettings['token'], configsettings['sf_secret_key'])
@@ -643,6 +646,28 @@ class MrpProduction(models.Model):
logging.info('update_programming_state error:%s' % e)
raise UserError("更新编程单状态失败,请联系管理员")
# 修改编程单状态
def _change_programming_state(self):
try:
res = {"programming_no": self.programming_no, "state": "已取消"}
logging.info('res=%s:' % res)
configsettings = self.env['res.config.settings'].get_values()
config_header = Common.get_headers(self, configsettings['token'], configsettings['sf_secret_key'])
url = '/api/intelligent_programming/set_state'
config_url = configsettings['sf_url'] + url
ret = requests.post(config_url, json=res, data=None, headers=config_header)
ret = ret.json()
result = json.loads(ret['result'])
logging.info('change_programming_state-ret:%s' % result)
if result['status'] == 1:
self.write({'programming_state': '已取消'})
else:
raise UserError(ret['message'])
except Exception as e:
logging.info('change_programming_state error:%s' % e)
raise UserError("修改编程单状态失败,请联系管理员")
# cnc程序获取
def fetchCNC(self, production_names):
cnc = self.env['mrp.production'].search([('id', '=', self.id)])
@@ -655,6 +680,8 @@ class MrpProduction(models.Model):
programme_way = 'manual operation'
else:
programme_way = 'auto'
if cnc.production_type == '人工线下加工':
programme_way = 'manual operation'
if quick_order:
programme_way = 'manual operation'
try:
@@ -1254,6 +1281,7 @@ class MrpProduction(models.Model):
'target': 'new',
'context': {
'default_production_id': self.id,
'default_is_clamping': True if self.workorder_ids.filtered(lambda wk: wk.routing_type == '装夹预调') else False,
'default_workorder_ids': workorder_ids.ids if workorder_ids.ids != [] else self.workorder_ids.ids,
'default_hidden_workorder_ids': ','.join(map(str, work_id_list)) if work_id_list != [] else '',
'default_reprogramming_num': cloud_programming.get('reprogramming_num') if cloud_programming else '',
@@ -1292,7 +1320,7 @@ class MrpProduction(models.Model):
for rework_item in rework_workorder:
pending_workorder = production.workorder_ids.filtered(
lambda m1: m1.state in [
'pending'] and m1.processing_panel == rework_item.processing_panel and m1.routing_type == 'CNC加工')
'pending'] and m1.processing_panel == rework_item.processing_panel and m1.routing_type in ['CNC加工', '人工线下加工'])
if not pending_workorder.cnc_ids:
production.get_new_program(rework_item.processing_panel)
# production.write({'state': 'progress', 'programming_state': '已编程', 'is_rework': False})
@@ -1302,6 +1330,7 @@ class MrpProduction(models.Model):
# 对制造订单所以面的cnc工单的程序用刀进行校验
try:
logging.info(f'已更新制造订单:{productions_not_delivered}')
productions = productions.filtered(lambda p: p.production_type == '自动化产线加工')
productions.production_cnc_tool_checkout()
except Exception as e:
logging.info(f'对cnc工单的程序用刀进行校验报错{e}')
@@ -1334,7 +1363,7 @@ class MrpProduction(models.Model):
if productions:
for production in productions:
panel_workorder = production.workorder_ids.filtered(lambda
pw: pw.processing_panel == processing_panel and pw.routing_type == 'CNC加工' and pw.state not in (
pw: pw.processing_panel == processing_panel and pw.routing_type in ['CNC加工', '人工线下加工'] and pw.state not in (
'rework', 'done'))
if panel_workorder:
if panel_workorder.cmm_ids:
@@ -1360,7 +1389,7 @@ class MrpProduction(models.Model):
'cnc_worksheet': base64.b64encode(open(panel_file_path, 'rb').read())})
logging.info('len(cnc_worksheet):%s' % len(panel_workorder.cnc_worksheet))
pre_workorder = production.workorder_ids.filtered(lambda
ap: ap.routing_type == '装夹预调' and ap.processing_panel == processing_panel and ap.state not in (
ap: ap.routing_type in ['装夹预调', '人工线下加工'] and ap.processing_panel == processing_panel and ap.state not in (
'rework', 'done'))
if pre_workorder:
pre_workorder.write(
@@ -1698,7 +1727,7 @@ class sf_programming_record(models.Model):
programming_method = fields.Selection([
('auto', '自动'),
('manual operation', '人工')], string="编程方式")
current_programming_count = fields.Integer('当前编程次数')
current_programming_count = fields.Integer('重新编程次数')
target_production_id = fields.Char('目标制造单号')
apply_time = fields.Datetime('申请时间')
send_time = fields.Datetime('下发时间')

View File

@@ -1,4 +1,3 @@
import random
import re
import json
import logging
@@ -114,6 +113,7 @@ class ResMrpWorkOrder(models.Model):
record.back_button_display = False
if cur_workorder.is_subcontract:
record.back_button_display = False
date_planned_start = fields.Datetime(tracking=True)
@api.depends('processing_panel')
def _compute_processing_panel_selection(self):
@@ -578,6 +578,32 @@ class ResMrpWorkOrder(models.Model):
("technology", "工艺"), ("customer redrawing", "客户改图")], string="原因", tracking=True)
detailed_reason = fields.Text('详细原因')
is_rework = fields.Boolean(string='是否返工', default=False)
# rework_flag = fields.Boolean(string='返工标志', compute='_compute_rework_flag')
#
# @api.depends('state', 'production_line_state')
# def _compute_rework_flag(self):
# for record in self:
# if record.state == 'done' and record.routing_type == '装夹预调':
# next_workorder = record.production_id.workorder_ids.filtered(
# lambda w: w.sequence == record.sequence + 1)
# if next_workorder and next_workorder.routing_type == 'CNC加工' and next_workorder.state in ['ready',
# 'waiting',
# 'pending'] and next_workorder.production_line_state == '待上产线':
# record.rework_flag = False
# elif next_workorder and next_workorder.routing_type == '表面工艺' and next_workorder.state in ['ready',
# 'waiting',
# 'pending']:
# record.rework_flag = False
# else:
# record.rework_flag = True
# else:
# record.rework_flag = True
#
# def button_rework(self):
# for item in self:
# item.state = 'progress'
# for time_id in item.time_ids:
# time_id.write({'date_end': None})
def button_change_env(self):
self.is_test_env = not self.is_test_env
@@ -1183,7 +1209,8 @@ class ResMrpWorkOrder(models.Model):
if (workorder.production_id.production_type == '人工线下加工'
and workorder.production_id.schedule_state == '已排'
and len(workorder.production_id.picking_ids.filtered(
lambda w: w.state not in ['done', 'cancel'])) == 0):
lambda w: w.state not in ['done', 'cancel'])) == 0
and workorder.production_id.programming_state == '已编程'):
if workorder.is_subcontract is True:
purchase_orders_id = self._get_surface_technics_purchase_ids()
if purchase_orders_id.state == 'purchase':
@@ -1240,7 +1267,6 @@ class ResMrpWorkOrder(models.Model):
mo.get_move_line(workorder.production_id, workorder))
else:
workorder.state = 'waiting'
# 重写工单开始按钮方法
def button_start(self):
# 判断工单状态是否为等待组件
@@ -1407,8 +1433,7 @@ class ResMrpWorkOrder(models.Model):
'detailed_reason': record.detailed_reason,
'processing_panel': record.processing_panel,
'routing_type': record.routing_type,
'handle_result': '待处理' if record.test_results in ['返工',
'报废'] or record.is_rework is True else '',
'handle_result': '待处理' if record.test_results in ['返工', '报废'] or record.is_rework is True else '',
'test_results': record.test_results,
'test_report': record.detection_report})],
'is_scrap': True if record.test_results == '报废' else False
@@ -1425,8 +1450,7 @@ class ResMrpWorkOrder(models.Model):
raise UserError('请先完成该工单的工艺外协再进行操作')
# 表面工艺外协,最后一张工单
workorders = self.production_id.workorder_ids
subcontract_workorders = workorders.filtered(
lambda wo: wo.is_subcontract == True and wo.state != 'cancel').sorted('sequence')
subcontract_workorders = workorders.filtered(lambda wo: wo.is_subcontract == True and wo.state != 'cancel').sorted('sequence')
if self == subcontract_workorders[-1]:
# 给下一个库存移动就绪
self.move_subcontract_workorder_ids[0].move_dest_ids._action_done()
@@ -1649,7 +1673,6 @@ class ResMrpWorkOrder(models.Model):
# 根据工单对应的【作业_个性化记录】配置页签
if any(item.code == 'PTD' for item in mw.routing_workcenter_id.individuation_page_ids):
mw.individuation_page_PTD = True
# =============================================================================================
is_inspect = fields.Boolean('需送检', compute='_compute_is_inspect', store=True, default=False)
@@ -1786,8 +1809,8 @@ class CNCprocessing(models.Model):
# 将FTP的多面的程序单文件下载到临时目录
def download_file_tmp(self, production_no, processing_panel):
remotepath = os.path.join('/home/ftp/ftp_root/NC', 'production_no', 'return', processing_panel)
serverdir = os.path.join('/tmp', 'production_no', 'return', processing_panel)
remotepath = os.path.join('/home/ftp/ftp_root/NC', production_no, 'return', processing_panel)
serverdir = os.path.join('/tmp', production_no, 'return', processing_panel)
ftp_resconfig = self.env['res.config.settings'].get_values()
ftp = FtpController(str(ftp_resconfig['ftp_host']), int(ftp_resconfig['ftp_port']), ftp_resconfig['ftp_user'],
ftp_resconfig['ftp_password'])
@@ -1959,11 +1982,7 @@ class WorkPieceDelivery(models.Model):
feeder_station_destination_id = fields.Many2one('sf.agv.site', '目的接驳站')
task_delivery_time = fields.Datetime('任务下发时间')
task_completion_time = fields.Datetime('任务完成时间')
def _get_agv_route_type_selection(self):
return self.env['sf.agv.task.route'].fields_get(['route_type'])['route_type']['selection']
type = fields.Selection(selection=_get_agv_route_type_selection, string='类型')
type = fields.Selection(related='route_id.route_type', string='类型')
delivery_duration = fields.Float('配送时长', compute='_compute_delivery_duration')
status = fields.Selection(
[('待下发', '待下发'), ('已下发', '待配送'), ('已配送', '已配送'), ('已取消', '已取消')], string='状态',

View File

@@ -150,6 +150,23 @@ class SaleOrder(models.Model):
purchase_embryo)
return super(SaleOrder, self).action_confirm()
def action_show_cancel_wizard(self):
wizard = self.env['sf.sale.order.cancel.wizard'].create({
'order_id': self.id,
})
# 创建关联单据行
self.env['sf.sale.order.cancel.line'].create_from_order(wizard.id, self)
return {
'name': '取消销售订单',
'type': 'ir.actions.act_window',
'res_model': 'sf.sale.order.cancel.wizard',
'view_mode': 'form',
'target': 'new',
'res_id': wizard.id,
}
class SaleOrderLine(models.Model):
_inherit = 'sale.order.line'
part_number = fields.Char('零件图号', related='product_id.part_number', readonly=True)
@@ -166,4 +183,6 @@ class SaleOrderLine(models.Model):
for line in self:
if vals['supply_method'] == 'automation' and line.manual_quotation:
raise UserError('当前(%s)产品为人工编程产品,不能选择自动化产线加工' % ','.join(line.mapped('product_id.name')))
if vals['supply_method'] == 'purchase' and line.is_incoming_material:
raise UserError('当前(%s)产品为客供料,不能选择外购' % ','.join(line.mapped('product_id.name')))
return super(SaleOrderLine, self).write(vals)

View File

@@ -197,7 +197,7 @@ class StockRule(models.Model):
'''
# productions._create_workorder()
#
self.env['stock.move'].sudo().create(productions._get_moves_finished_values())
# self.env['stock.move'].sudo().create(productions._get_moves_finished_values())
productions.filtered(lambda p: (not p.orderpoint_id and p.move_raw_ids) or \
(
p.move_dest_ids.procure_method != 'make_to_order' and not
@@ -289,7 +289,7 @@ class StockRule(models.Model):
if production_item.product_id.id in product_id_to_production_names:
# 同一个产品多个制造订单对应一个编程单和模型库
# 只调用一次fetchCNC并将所有生产订单的名称作为字符串传递
if not production_item.programming_no and production_item.production_type == '自动化产线加工':
if not production_item.programming_no and production_item.production_type in ['自动化产线加工', '人工线下加工']:
if not production_programming.programming_no:
production_item.fetchCNC(
', '.join(product_id_to_production_names[production_item.product_id.id]))
@@ -314,6 +314,11 @@ class StockRule(models.Model):
i += 1
technology_design_values.append(
self.env['sf.technology.design'].json_technology_design_str(k, route, i, False))
elif production_item.production_type == '人工线下加工':
for route in product_routing_workcenter:
i += 1
technology_design_values.append(
self.env['sf.technology.design'].json_technology_design_str('ZM', route, i, False))
else:
for route in product_routing_workcenter:
i += 1
@@ -606,6 +611,18 @@ class StockPicking(models.Model):
return sequence_id
def button_validate(self):
# 校验“收料入库单、客供料入库单”是否已经分配序列号,如果没有分配则自动分配
if self.picking_type_id.use_existing_lots is False and self.picking_type_id.use_create_lots is True:
for move in self.move_ids:
if not move.move_line_nosuggest_ids:
move.action_show_details()
else:
# 对已经生成的序列号做唯一性校验,如果重复则重新生成新的序列号
line_lot_name = [line_id.lot_name for line_id in move.move_line_nosuggest_ids]
lot_ids = self.env['stock.lot'].sudo().search([('name', 'in', line_lot_name)])
if lot_ids:
move.action_clear_lines_show_details()
move.action_show_details()
res = super().button_validate()
picking_type_in = self.env.ref('sf_manufacturing.outcontract_picking_in').id
if res is True and self.picking_type_id.id == picking_type_in:
@@ -839,7 +856,8 @@ class ReStockMove(models.Model):
self.next_serial = self._get_tool_next_serial(self.company_id, self.product_id, self.origin)
else:
self.next_serial = self.env['stock.lot']._get_next_serial(self.company_id, self.product_id)
if self.picking_type_id.sequence_code == 'DL' and not self.move_line_nosuggest_ids:
if (self.picking_type_id.use_existing_lots is False
and self.picking_type_id.use_create_lots is True and not self.move_line_nosuggest_ids):
self.action_assign_serial_show_details()
elif self.product_id.tracking == "lot":
self._put_tool_lot(self.company_id, self.product_id, self.origin)

View File

@@ -192,3 +192,5 @@ access_sf_programming_reason,sf_programming_reason,model_sf_programming_reason,b
access_sf_programming_record,sf_programming_record,model_sf_programming_record,base.group_user,1,1,1,0
access_sf_work_individuation_page,sf_work_individuation_page,model_sf_work_individuation_page,sf_base.group_sf_mrp_user,1,1,1,0
access_sf_work_individuation_page_group_plan_dispatch,sf_work_individuation_page_group_plan_dispatch,model_sf_work_individuation_page,sf_base.group_plan_dispatch,1,1,0,0
access_sf_sale_order_cancel_wizard,sf_sale_order_cancel_wizard,model_sf_sale_order_cancel_wizard,sf_base.group_sf_order_user,1,1,1,0
access_sf_sale_order_cancel_line,sf_sale_order_cancel_line,model_sf_sale_order_cancel_line,sf_base.group_sf_order_user,1,1,1,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
192
193
194
195
196

View File

@@ -117,11 +117,11 @@
<xpath expr="//sheet//group//group//div[3]" position="after">
<field name="production_type" readonly="1"/>
<field name="manual_quotation" readonly="1"
attrs="{'invisible': [('production_type', 'not in', ['自动化产线加工'])]}"/>
attrs="{'invisible': [('production_type', 'not in', ['自动化产线加工', '人工线下加工'])]}"/>
<field name="programming_no" readonly="1"
attrs="{'invisible': [('production_type', 'not in', ['自动化产线加工'])]}"/>
attrs="{'invisible': [('production_type', 'not in', ['自动化产线加工', '人工线下加工'])]}"/>
<field name="programming_state" readonly="1"
attrs="{'invisible': [('production_type', 'not in', ['自动化产线加工'])]}"
attrs="{'invisible': [('production_type', 'not in', ['自动化产线加工', '人工线下加工'])]}"
decoration-success="programming_state == '已编程'"
decoration-warning="programming_state =='编程中'"
decoration-danger="programming_state =='已编程未下发'"/>
@@ -632,6 +632,8 @@
<field name="state" icon="fa-filter" enable_counters="1"/>
<field name="delivery_status" icon="fa-filter" enable_counters="1"/>
<field name="production_type" icon="fa-filter" enable_counters="1"/>
<field name="programming_state" icon="fa-filter" enable_counters="1"/>
</searchpanel>
</xpath>
<filter name='todo' position="replace"/>

View File

@@ -168,6 +168,7 @@
<field name='is_delivery' invisible="1"/>
<field name="is_trayed" invisible="1"/>
<field name="is_inspect" invisible="1"/>
<!-- <field name="rework_flag" invisible="1"/>-->
<!-- <field name='is_send_program_again' invisible="1"/>-->
<!-- 工单form页面的开始停工按钮等 -->
<!-- <button name="button_start" type="object" string="开始" class="btn-success" -->
@@ -216,6 +217,9 @@
attrs="{'invisible': ['|', '|', '|', ('routing_type','!=','装夹预调'),('state','!=','progress'), ('is_trayed', '=', False), ('state', 'in', ('done'))]}"/>
<button name="print_method" type="object" string="打印二维码" class="btn-primary"
attrs="{'invisible': ['|',('routing_type','!=','解除装夹'),('state','!=','done')]}"/>
<!-- <button type="object" class="oe_highlight jikimo_button_confirm" name="button_rework"-->
<!-- string="返工"-->
<!-- attrs='{"invisible": [("rework_flag","=",True)]}' confirm="是否返工"/>-->
</xpath>
<xpath expr="//page[1]" position="before">
<page string="开料要求" attrs='{"invisible": [("routing_type","not in",("切割", "线切割", "人工线下加工"))]}'>
@@ -244,7 +248,7 @@
invisible="1" sum="real duration"/>
<field name="glb_file" readonly="1" widget="Viewer3D" string="加工模型"/>
<field name="manual_quotation" readonly="1"
attrs="{'invisible': [('routing_type', '!=', 'CNC加工')]}"/>
attrs="{'invisible': [('routing_type', 'not in', ['CNC加工', '人工线下加工'])]}"/>
<field name="processing_panel" readonly="1"
attrs='{"invisible": [("routing_type","in",("获取CNC加工程序","切割"))]}'/>
<field name="equipment_id" readonly="1"
@@ -494,7 +498,7 @@
</group>
</page>
<page string="2D加工图纸" attrs="{'invisible': [('routing_type','!=','装夹预调')]}">
<page string="2D加工图纸" attrs='{"invisible": [("routing_type","not in",["装夹预调", "人工线下加工"])]}'>
<field name="machining_drawings" widget="adaptive_viewer"/>
</page>
@@ -566,7 +570,7 @@
</page>
</xpath>
<xpath expr="//page[1]" position="before">
<page string="CNC程序" attrs='{"invisible": [("routing_type","!=","CNC加工")]}'>
<page string="CNC程序" attrs='{"invisible": [("routing_type","not in",["CNC加工", "人工线下加工"])]}'>
<field name="cnc_ids" widget="one2many" string="工作程序" default_order="sequence_number,id"
readonly="0">
<tree>
@@ -590,7 +594,7 @@
</field>
</page>
<page string="CMM程序" attrs='{"invisible": [("routing_type","!=","CNC加工")]}'>
<page string="CMM程序" attrs='{"invisible": [("routing_type","not in",["CNC加工", "人工线下加工"])]}'>
<field name="cmm_ids" widget="one2many" string="CMM程序" readonly="1">
<tree>
<field name="sequence_number"/>
@@ -643,7 +647,7 @@
<field name="inherit_id" ref="sf_manufacturing.view_mrp_production_workorder_tray_form_inherit_sf"/>
<field name="arch" type="xml">
<xpath expr="//form//sheet//group//group[2]" position="replace">
<group string="装夹图纸" attrs="{'invisible': [('routing_type', '!=', '装夹预调')]}">
<group string="装夹图纸" attrs="{'invisible': [('routing_type', 'not in', ['装夹预调', '人工线下加工'])]}">
<!-- 隐藏加工图纸字段名 -->
<field name="processing_drawing" widget="pdf_viewer" string="" readonly="1"/>
<!-- <field name="production_id" invisible="0"/>-->
@@ -786,6 +790,10 @@
<filter name="filter_to_be_issued" string="待下发" domain="[('status', 'in', ['待下发'])]"/>
<filter name="filter_issued" string="已下发" domain="[('status', 'in', ['已下发'])]"/>
<filter name="filter_delivered" string="已配送" domain="[('status', 'in', ['已配送'])]"/>
<separator/>
<filter name="filter_type_to_production_line" string="上产线" domain="[('type', '=', '上产线')]"/>
<filter name="filter_type_to_empty_racks" string="运送空料架" domain="[('type', '=', '运送空料架')]"/>
<filter name="filter_type_production_line_back" string="下产线" domain="[('type', '=', '下产线')]"/>
<field name="rfid_code"/>
<field name="production_id"/>
<field name="feeder_station_start_id"/>
@@ -808,7 +816,7 @@
<field name="res_model">sf.workpiece.delivery</field>
<field name="search_view_id" ref="sf_workpiece_delivery_search"/>
<field name="context">{'search_default_filter_to_be_issued': 1,
'search_default_filter_issued': 1}
'search_default_filter_type_to_production_line': 1}
</field>
<field name="view_mode">tree,form</field>
<field name="domain">

View File

@@ -19,18 +19,34 @@
<field name="supply_method" attrs="{'invisible': [('state', '=', 'draft')], 'required': [('state', '=', 'supply method')]}" />
</xpath>
<xpath expr="//field[@name='order_line']/tree/field[@name='model_glb_file']" position="before">
<field name="part_number" optional="show"/>
<field name="part_number" optional="show" class="section_and_note_text"/>
</xpath>
<!-- <xpath expr="//header/button[@name='action_cancel']" position="attributes"> -->
<!-- <attribute name="attrs">{'invisible': [('state', '!=', 'draft')]}</attribute> -->
<!-- </xpath> -->
<xpath expr="//header/button[@name='action_cancel']" position="attributes">
<attribute name="attrs">{'invisible': [('state', '!=', 'draft')]}</attribute>
</xpath>
<xpath expr="//header/button[@name='action_cancel']" position="attributes">
<attribute name="attrs">{'invisible': [('state', '!=', 'draft')]}</attribute>
<attribute name="attrs">{'invisible': [('state', 'not in', ['draft', 'supply method'])]}</attribute>
<attribute name="confirm">警告:取消操作将不可逆,是否确定要取消该单据?</attribute>
</xpath>
<xpath expr="//header/button[@name='action_quotation_send'][5]" position="attributes">
<attribute name="attrs">{'invisible': ['|','&amp;',('check_status', '!=', 'approved'),('state', 'in', ['draft','cancel','supply method']),'&amp;',('check_status', '=', 'approved'),('state', 'in', ['sale','cancel','supply method'])]}</attribute>
</xpath>
<xpath expr="//header/button[@name='action_cancel']" position="after">
<button
name="action_show_cancel_wizard"
string="取消"
type="object"
attrs="{'invisible': [('state', 'not in', ['sale', 'processing'])]}"
/>
<button
name="action_show_cancel_wizard"
string="取消清单"
type="object"
attrs="{'invisible': [('state', 'not in', ['cancel'])]}"
/>
</xpath>
</field>
</record>

View File

@@ -5,3 +5,4 @@ from . import production_technology_wizard
from . import production_technology_re_adjust_wizard
from . import mrp_workorder_batch_replan_wizard
from . import sf_programming_reason
from . import sale_order_cancel

View File

@@ -35,6 +35,7 @@ class ReworkWizard(models.TransientModel):
is_reprogramming = fields.Boolean(string='申请重新编程', default=False)
is_reprogramming_readonly = fields.Boolean(string='申请重新编程(只读)', default=False)
is_clamp_measure = fields.Boolean(string='保留装夹测量数据', default=True)
is_clamping = fields.Boolean(string='制造订单是否存在装夹预调工单')
reprogramming_num = fields.Integer('重新编程次数', default=0)
programming_state = fields.Selection(
[('待编程', '待编程'), ('编程中', '编程中'), ('已编程', '已编程'), ('已编程未下发', '已编程未下发'),

View File

@@ -13,6 +13,7 @@
<field name="routing_type" invisible="True"/>
<field name="processing_panel_id" invisible="1"/>
<field name="hidden_workorder_ids" class="css_not_available_msg"/>
<field name="is_clamping" invisible="1"/>
<group>
<field name="hidden_workorder_ids" invisible="1"/>
<field options="{'no_create': True,'no_open': True}" readonly="1" name="workorder_ids"
@@ -26,7 +27,7 @@
</tree>
</field>
</group>
<div attrs='{"invisible": [("routing_type","=","装夹预调")]}'>
<div attrs='{"invisible": ["|", ("routing_type","=","装夹预调"), ("is_clamping", "=", False)]}'>
<span style='font-weight:bold;'>保留装夹测量数据
<field name="is_clamp_measure" force_save="1"/>
</span>

View File

@@ -0,0 +1,394 @@
from odoo import models, fields, api
class SFSaleOrderCancelWizard(models.TransientModel):
_name = 'sf.sale.order.cancel.wizard'
_description = '销售订单取消向导'
order_id = fields.Many2one('sale.order', string='销售订单')
related_docs = fields.One2many('sf.sale.order.cancel.line', 'wizard_id', string='相关单据')
has_movement = fields.Boolean(compute='_compute_has_movement', string='是否有异动')
display_message = fields.Char(compute='_compute_display_message', string='显示消息')
@api.model
def default_get(self, fields_list):
defaults = super().default_get(fields_list)
if self._context.get('active_id'):
order = self.env['sale.order'].browse(self._context.get('active_id'))
defaults['order_id'] = order.id
# 创建向导时自动创建关联单据行
wizard = self.create(defaults)
self.env['sf.sale.order.cancel.line'].create_from_order(wizard.id, order)
defaults['related_docs'] = wizard.related_docs.ids
return defaults
@api.depends('related_docs.cancel_reason')
def _compute_has_movement(self):
for wizard in self:
docs_has_movement = any(doc.cancel_reason for doc in wizard.related_docs)
order_canceled = wizard.order_id.state == 'cancel'
wizard.has_movement = docs_has_movement or order_canceled
@api.depends('has_movement', 'related_docs', 'related_docs.doc_state')
def _compute_display_message(self):
for wizard in self:
# 如果没有相关记录,显示为空
if not wizard.related_docs:
wizard.display_message = '无下游单据'
continue
# 检查是否所有记录都是已取消状态
all_canceled = all(doc.doc_state == '已取消' for doc in wizard.related_docs)
if all_canceled:
wizard.display_message = '取消的下游单据如下:'
else:
wizard.display_message = '部分或全部下游单据存在异动,无法取消,详情如下:' if wizard.has_movement else '确认所有下游单据全部取消?'
def action_confirm_cancel(self):
self.ensure_one()
# 取消销售订单关联的采购单
purchase_orders = self.env['purchase.order'].search([
('origin', '=', self.order_id.name)
])
if purchase_orders:
purchase_orders.write({'state': 'cancel'})
# 取消销售订单
result = self.order_id.action_cancel()
# 取消关联的制造订单及其采购单
manufacturing_orders = self.env['mrp.production'].search([
('origin', '=', self.order_id.name)
])
for mo in manufacturing_orders:
# 取消制造订单关联的采购单,但保持关联关系
mo_purchase_orders = self.env['purchase.order'].search([
('origin', '=', mo.name)
])
if mo_purchase_orders:
mo_purchase_orders.write({'state': 'cancel'})
# 取消制造订单的质检单
mo_quality_checks = self.env['quality.check'].search([
('production_id', '=', mo.id)
])
if mo_quality_checks:
mo_quality_checks.write({'quality_state': 'cancel'})
# 取消制造订单
mo.action_cancel()
# 取消制造订单关联的编程单
mo._change_programming_state()
# 取消组件的制造单关联的采购单
for comp_mo in self.env['mrp.production'].search([
('origin', '=', mo.name)
]):
comp_purchase_orders = self.env['purchase.order'].search([
('origin', '=', comp_mo.name)
])
if comp_purchase_orders:
comp_purchase_orders.button_cancel()
return result
class SFSaleOrderCancelLine(models.TransientModel):
_name = 'sf.sale.order.cancel.line'
_description = '销售订单取消行'
wizard_id = fields.Many2one('sf.sale.order.cancel.wizard')
sequence = fields.Integer('序号')
category = fields.Char('大类')
doc_name = fields.Char('单据名称')
operation_type = fields.Char('作业类型')
doc_number = fields.Char('单据编号')
line_number = fields.Char('行号')
product_name = fields.Char('产品名称')
quantity = fields.Float('数量')
doc_state = fields.Char('单据状态')
cancel_reason = fields.Char('禁止取消原因')
@api.model
def create_from_order(self, wizard_id, order):
sequence = 1
lines = []
map_dict = {
'waiting': '等待其他作业',
'to approve': '待批准',
'technology_to_confirmed': '待工艺确认',
'confirmed': '已确认',
'pending': '等待其他工单',
'none': '待处理',
'draft': '询价',
'cancel': '已取消',
'pass': '通过的',
'fail': '失败的',
'done': '已完成',
'rework': '返工',
'purchase': '采购订单',
'ready': '就绪',
'approved': '已批准',
'pending_cam': '待加工',
'progress': '加工中',
'assigned': '就绪'
}
# 检查销售订单
if order.invoice_ids:
a = 0
for invoice in order.invoice_ids:
a += 1
vals = {
'wizard_id': wizard_id,
'sequence': sequence,
'category': '销售',
'doc_name': '销售订单',
'operation_type': '',
'doc_number': invoice.name,
'line_number': a,
'product_name': invoice.product_id.name,
'quantity': invoice.quantity,
'doc_state': invoice.state,
'cancel_reason': '已有异动' if invoice.state != 'draft' else ''
}
lines.append(self.create(vals))
sequence += 1
# 检查交货单
if order.picking_ids:
b = 0
for picking in order.picking_ids:
b += 1
vals = {
'wizard_id': wizard_id,
'sequence': sequence,
'category': '库存',
'doc_name': '交货单',
'operation_type': '调拨',
'doc_number': picking.name,
'line_number': b,
'product_name': picking.product_id.name if picking.product_id else '',
# 'quantity': picking.product_qty if hasattr(picking, 'product_qty') else 0,
'quantity': sum(picking.move_ids.mapped('product_uom_qty') or [0]),
'doc_state': map_dict.get(picking.state, picking.state),
'cancel_reason': '已有异动' if picking.state not in ['draft', 'cancel', 'waiting'] else ''
}
lines.append(self.create(vals))
sequence += 1
# 检查销售订单直接关联的采购单
purchase_orders = self.env['purchase.order'].search([
('origin', '=', order.name)
])
if purchase_orders:
c = 0
for po in purchase_orders:
c += 1
vals = {
'wizard_id': wizard_id,
'sequence': sequence,
'category': '采购',
'doc_name': '询价单',
'operation_type': po.picking_type_id.name,
'doc_number': po.name,
'line_number': c,
'product_name': po.order_line[0].product_id.name if po.order_line else '',
'quantity': po.order_line[0].product_qty if po.order_line else 0,
'doc_state': map_dict.get(po.state, po.state),
'cancel_reason': '已有异动' if po.state not in ['draft', 'cancel'] else ''
}
lines.append(self.create(vals))
sequence += 1
# 检查制造订单
manufacturing_orders = self.env['mrp.production'].search([
('origin', '=', order.name)
])
d = 0
for mo in manufacturing_orders:
# 添加制造订单本身
d += 1
vals = {
'wizard_id': wizard_id,
'sequence': sequence,
'category': '制造',
'doc_name': '制造订单',
'doc_number': mo.name,
'operation_type': '',
'line_number': d,
'product_name': mo.product_id.name,
'quantity': mo.product_qty,
'doc_state': map_dict.get(mo.state, mo.state),
'cancel_reason': '已有异动' if mo.state not in ['technology_to_confirmed', 'cancel'] else ''
}
lines.append(self.create(vals))
sequence += 1
# 检查制造订单关联的采购单
purchase_orders = self.env['purchase.order'].search([
('origin', '=', mo.name)
])
if purchase_orders:
e = 0
for po in purchase_orders:
e += 1
vals = {
'wizard_id': wizard_id,
'sequence': sequence,
'category': '制造',
'doc_name': '询价单',
'doc_number': po.name,
'line_number': e,
'operation_type': po.picking_type_id.name,
'product_name': po.order_line[0].product_id.name if po.order_line else '',
'quantity': po.order_line[0].product_qty if po.order_line else 0,
'doc_state': map_dict.get(po.state, po.state),
'cancel_reason': '已有异动' if po.state not in ['draft', 'cancel'] else ''
}
lines.append(self.create(vals))
sequence += 1
# 检查制造订单的领料单
if mo.picking_ids:
f = 0
for picking in mo.picking_ids:
f += 1
vals = {
'wizard_id': wizard_id,
'sequence': sequence,
'category': '制造',
'doc_name': '库存移动',
'doc_number': picking.name,
'line_number': f,
'operation_type': picking.picking_type_id.name,
'product_name': picking.product_id.name if picking.product_id else '',
'quantity': sum(picking.move_ids.mapped('product_uom_qty') or [0]),
'doc_state': map_dict.get(picking.state, picking.state),
'cancel_reason': '已有异动' if picking.state not in ['draft', 'cancel', 'waiting'] else ''
}
lines.append(self.create(vals))
sequence += 1
# 检查制造订单的工单
if mo.workorder_ids:
g = 0
for workorder in mo.workorder_ids:
g += 1
vals = {
'wizard_id': wizard_id,
'sequence': sequence,
'category': '制造',
'doc_name': '工单',
'doc_number': workorder.name,
'line_number': g,
'operation_type': workorder.workcenter_id.name,
'product_name': mo.product_id.name,
'quantity': workorder.qty_production,
'doc_state': map_dict.get(workorder.state, workorder.state),
'cancel_reason': '已有异动' if workorder.state not in ['draft', 'cancel', 'pending',
'waiting'] else ''
}
lines.append(self.create(vals))
sequence += 1
# 检查制造订单组件的采购单和制造单
for move in mo.move_raw_ids:
# # 检查组件的采购单
# component_pos = self.env['purchase.order'].search([
# ('origin', '=', mo.name),
# ('order_line.product_id', '=', move.product_id.id)
# ])
# for po in component_pos:
# vals = {
# 'wizard_id': wizard_id,
# 'sequence': sequence,
# 'category': '制造',
# 'doc_name': '组件采购单',
# 'operation_type': '组件采购',
# 'doc_number': po.name,
# 'product_name': move.product_id.name,
# 'quantity': po.order_line[0].product_qty if po.order_line else 0,
# 'doc_state': po.state,
# 'cancel_reason': '已有异动' if po.state not in ['draft', 'cancel'] else ''
# }
# lines.append(self.create(vals))
# sequence += 1
# 检查组件的制造单
component_mos = self.env['mrp.production'].search([
('origin', '=', mo.name),
('product_id', '=', move.product_id.id)
])
h = 0
for comp_mo in component_mos:
h += 1
vals = {
'wizard_id': wizard_id,
'sequence': sequence,
'category': '制造',
'doc_name': '组件制造单',
'operation_type': '',
'doc_number': comp_mo.name,
'line_number': h,
'product_name': move.product_id.name,
'quantity': comp_mo.product_qty,
'doc_state': map_dict.get(comp_mo.state, comp_mo.state),
'cancel_reason': '已有异动' if comp_mo.state not in ['technology_to_confirmed'] else ''
}
lines.append(self.create(vals))
sequence += 1
# 检查制造订单的质检单
quality_checks = self.env['quality.check'].search([
('production_id', '=', mo.id)
])
if quality_checks:
i = 0
for check in quality_checks:
i += 1
vals = {
'wizard_id': wizard_id,
'sequence': sequence,
'category': '制造',
'doc_name': '质检单',
'operation_type': '',
'doc_number': check.name,
'line_number': i,
'product_name': check.product_id.name,
'quantity': 1,
'doc_state': map_dict.get(check.quality_state, check.quality_state),
'cancel_reason': '已有异动' if check.quality_state not in ['none', 'cancel'] else ''
}
lines.append(self.create(vals))
sequence += 1
# 检查制造订单的编程单
cloud_programming = mo._cron_get_programming_state()
if cloud_programming:
vals = {
'wizard_id': wizard_id,
'sequence': sequence,
'category': '编程',
'doc_name': '编程单',
'operation_type': '',
'doc_number': cloud_programming['programming_no'],
'line_number': 1,
'product_name': cloud_programming['production_order_no'],
'quantity': 1,
'doc_state': cloud_programming['programming_state'],
'cancel_reason': ''
}
lines.append(self.create(vals))
sequence += 1
unique_lines = {}
for line in lines:
doc_number = line.doc_number
if doc_number not in unique_lines:
unique_lines[doc_number] = line
# 返回去重后的记录列表
return list(unique_lines.values())

View File

@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_sf_sale_order_cancel_wizard" model="ir.ui.view">
<field name="name">sf.sale.order.cancel.wizard.form</field>
<field name="model">sf.sale.order.cancel.wizard</field>
<field name="arch" type="xml">
<form string="下游单据清单">
<group>
<field name="order_id" invisible="1"/>
<field name="has_movement" invisible="1"/>
</group>
<div class="alert alert-warning" role="alert">
<field name="display_message" readonly="1" nolabel="1"/>
</div>
<field name="related_docs">
<tree string="下游单据" create="false" edit="false" delete="false">
<!-- <field name="sequence" string="序号"/> -->
<field name="category" string="大类"/>
<field name="doc_name" string="单据名称"/>
<field name="operation_type" string="作业类型"/>
<field name="doc_number" string="单据编号"/>
<field name="line_number" string="行号"/>
<field name="product_name" string="产品名称"/>
<field name="quantity" string="数量"/>
<field name="doc_state" string="单据状态"/>
<field name="cancel_reason" string="禁止取消原因"/>
</tree>
</field>
<footer>
<button name="action_confirm_cancel"
string="确认取消"
type="object"
class="btn-primary"
attrs="{'invisible': [('has_movement', '=', True)]}"/>
<button string="关闭"
class="btn-secondary"
special="cancel"/>
</footer>
</form>
</field>
</record>
</odoo>

View File

@@ -54,10 +54,7 @@ class WorkpieceDeliveryWizard(models.TransientModel):
}
}
def _get_agv_route_type_selection(self):
return self.env['sf.agv.task.route'].fields_get(['route_type'])['route_type']['selection']
delivery_type = fields.Selection(selection=_get_agv_route_type_selection, string='类型')
delivery_type = fields.Selection(related='route_id.route_type', string='类型')
def dispatch_confirm(self):
if len(self.workorder_ids) < 4:

View File

@@ -68,6 +68,11 @@
<field name="model">stock.picking</field>
</record>
<record id="order_quality_done" model="jikimo.message.bussiness.node">
<field name="name">调拨单质检完成提醒</field>
<field name="model">stock.picking</field>
</record>
<record id="bussiness_mrp_workorder_pre_overdue_warning" model="jikimo.message.bussiness.node">
<field name="name">装夹预调工单逾期预警</field>
<field name="model">mrp.workorder</field>
@@ -156,4 +161,12 @@
<field name="model">product.product</field>
</record>
</data>
<data noupdate="1">
<record id="bussiness_plan_data_tracking" model="jikimo.message.bussiness.node">
<field name="name">计划数据异常跟踪</field>
<field name="model">mrp.workorder</field>
</record>
</data>
</odoo>

View File

@@ -252,6 +252,18 @@
<field name="content">### 订单发货提醒:
单号:发料出库单[{{name}}]({{request_url}})
事项:销售订单{{sale_order_name}}已全部产出并入库,请及时发货</field>
</record>
<record id="template_order_quality_done" model="jikimo.message.template">
<field name="name">调拨单质检完成提醒</field>
<field name="model_id" ref="stock.model_stock_picking"/>
<field name="model">stock.picking</field>
<field name="bussiness_node_id" ref="order_quality_done"/>
<field name="msgtype">markdown</field>
<field name="urgency">normal</field>
<field name="content">### {{picking_type_name}}待处理提醒:
单号:{{name}}
事项:质量检查已完成</field>
</record>
<record id="template_quality_cnc_test" model="jikimo.message.template">
@@ -402,4 +414,19 @@
事项:有{{num}}个质检单需要处理。</field>
</record>
</data>
<data noupdate="1">
<record id="template_plan_data_tracking" model="jikimo.message.template">
<field name="name">计划数据异常跟踪</field>
<field name="model_id" ref="mrp.model_mrp_workorder"/>
<field name="model">mrp.workorder</field>
<field name="bussiness_node_id" ref="bussiness_plan_data_tracking"/>
<field name="msgtype">markdown</field>
<field name="urgency">normal</field>
<field name="content">### 工单计划数据异常删除:
工单号:{{name}}
异动时间:{{write_date}}
</field>
</record>
</data>
</odoo>

View File

@@ -16,6 +16,7 @@ class SFMessageProduct(models.Model):
mrp_production_list = self.env['mrp.production'].sudo().search(
[('product_id', '=', product_product.id)])
production_num = 0
routing_type = None
for mrp_production_info in mrp_production_list:
routing_type = '人工线下加工' if mrp_production_info.production_type == '人工线下加工' else '装夹预调'
mrp_production_ready = mrp_production_info.workorder_ids.filtered(
@@ -23,7 +24,7 @@ class SFMessageProduct(models.Model):
if mrp_production_ready:
production_num += 1
if production_num >= 1:
url = self.get_request_url()
url = self.get_request_url(routing_type)
content = content.replace('{{product_id}}', product_product.name).replace(
'{{number}}', str(production_num)).replace(
'{{request_url}}', url)
@@ -42,11 +43,15 @@ class SFMessageProduct(models.Model):
contents.append(content)
return contents, message_queue_ids
def get_request_url(self):
def get_request_url(self, routing_type):
url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
action_id = self.env.ref('sf_message.mrp_workorder_issued_action').id
menu_id = self.env.ref('mrp.menu_mrp_root').id
active_id = self.env['mrp.workcenter'].sudo().search([('name', '=', '工件装夹中心')]).id
if routing_type == '人工线下加工':
routing_name = '线下工作中心'
else:
routing_name = '工件装夹中心'
active_id = self.env['mrp.workcenter'].sudo().search([('name', '=', routing_name)]).id
# 查询参数
params = {'menu_id': menu_id, 'action': action_id, 'model': 'mrp.workorder',
'view_type': 'list', 'active_id': active_id}

View File

@@ -9,6 +9,8 @@ class SFMessageStockPicking(models.Model):
_description = "库存调拨"
_inherit = ['stock.picking', 'jikimo.message.dispatch']
quality_check_ids = fields.One2many('quality.check', 'picking_id', '质量检测单')
@api.model_create_multi
def create(self, vals):
result = super(SFMessageStockPicking, self).create(vals)
@@ -20,7 +22,8 @@ class SFMessageStockPicking(models.Model):
logging.info('add_queue调拨入库 error:%s' % e)
return result
@api.depends('move_type', 'immediate_transfer', 'move_ids.state', 'move_ids.picking_id')
@api.depends('move_type', 'immediate_transfer', 'move_ids.state', 'move_ids.picking_id',
'quality_check_ids.quality_state')
def _compute_state(self):
super(SFMessageStockPicking, self)._compute_state()
try:
@@ -48,6 +51,8 @@ class SFMessageStockPicking(models.Model):
all_ready_or_done = all(picking.state in ['assigned', 'done'] for picking in stock_picking_list)
if all_ready_or_done:
mrp_production.add_queue('工序外协发料通知')
if all(qc.quality_state in ['pass', 'fail'] for qc in record.quality_check_ids):
record.add_queue('调拨单质检完成提醒')
except Exception as e:
logging.info('add_queue_compute_state error:%s' % e)
@@ -83,6 +88,11 @@ class SFMessageStockPicking(models.Model):
content = self.deal_stock_picking_sfp(message_queue_id)
if content:
contents.append(content)
elif message_queue_id.message_template_id.name == '调拨单质检完成提醒':
content = message_queue_id.message_template_id.content
content = content.replace('{{picking_type_name}}', self.picking_type_id.name).replace(
'{{name}}', self.name)
contents.append(content)
return contents, message_queue_ids
def get_special_url(self, id, tmplate_name, special_name, model_id):

View File

@@ -188,3 +188,10 @@ class SFMessageWork(models.Model):
])
if message_queue_ids:
message_queue_ids.write({'message_status': 'cancel'})
def write(self, vals):
res = super(SFMessageWork, self).write(vals)
if ('leave_id' in vals and vals['leave_id'] is False or 'date_planned_start' in vals and vals['date_planned_start'] is False) \
and self.schedule_state != '未排':
self.add_queue('计划数据异常跟踪')
return res

View File

@@ -61,7 +61,7 @@ class Sf_Mrs_Connect(http.Controller, MultiInheritController):
for panel in ret['processing_panel'].split(','):
# 查询状态为进行中且工序类型为CNC加工的工单
cnc_workorder_has = production.workorder_ids.filtered(
lambda ach: ach.routing_type == 'CNC加工' and ach.state not in ['progress', 'done',
lambda ach: ach.routing_type in ['CNC加工', '人工线下加工'] and ach.state not in ['progress', 'done',
'rework',
'cancel'] and ach.processing_panel == panel)
if cnc_workorder_has:
@@ -76,7 +76,7 @@ class Sf_Mrs_Connect(http.Controller, MultiInheritController):
for panel in ret['processing_panel'].split(','):
# 查询状态为进行中且工序类型为CNC加工的工单
cnc_workorder = productions.workorder_ids.filtered(
lambda ac: ac.routing_type == 'CNC加工' and ac.state not in ['progress', 'done', 'rework'
lambda ac: ac.routing_type in ['CNC加工', '人工线下加工'] and ac.state not in ['progress', 'done', 'rework'
'cancel'] and ac.processing_panel == panel)
if cnc_workorder:
# program_path_tmp_panel = os.path.join('C://Users//43484//Desktop//fsdownload//test',
@@ -91,19 +91,21 @@ class Sf_Mrs_Connect(http.Controller, MultiInheritController):
logging.info('panel_file_path:%s' % panel_file_path)
cnc_workorder.write({'cnc_worksheet': base64.b64encode(open(panel_file_path, 'rb').read())})
pre_workorder = productions.workorder_ids.filtered(
lambda ap: ap.routing_type == '装夹预调' and ap.state not in ['done', 'rework'
lambda ap: ap.routing_type in ['装夹预调', '人工线下加工'] and ap.state not in ['done', 'rework'
'cancel'] and ap.processing_panel == panel)
if pre_workorder:
pre_workorder.write(
{'processing_drawing': base64.b64encode(open(panel_file_path, 'rb').read())})
productions.write({'programming_state': '已编程', 'work_state': '已编程'})
productions.filtered(lambda p: p.production_type == '人工线下加工').write({'manual_quotation': True})
logging.info('已更新制造订单编程状态:%s' % productions.ids)
# 对制造订单所有面的cnc工单的程序用刀进行校验
try:
logging.info(f'已更新制造订单:{productions}')
re_tool_chekout = False
re_tool_chekout = productions.production_cnc_tool_checkout()
productions_temp = productions.filtered(lambda p: p.production_type == '自动化产线加工')
re_tool_chekout = productions_temp.production_cnc_tool_checkout()
if re_tool_chekout:
return json.JSONEncoder().encode({'status': -3, 'message': '对cnc工单的程序用刀进行校验失败'})
except Exception as e:
@@ -200,6 +202,17 @@ class Sf_Mrs_Connect(http.Controller, MultiInheritController):
'send_time': ret['send_time'],
})
logging.info('已创建无效功能刀具的编程记录:%s' % production.name)
elif ret['reprogramming_reason']:
production.programming_record_ids.create({
'number': len(production.programming_record_ids) + 1,
'production_id': production.id,
'reason': ret['reprogramming_reason'],
'programming_method': ret['programme_way'],
'current_programming_count': ret['reprogramming_num'],
'target_production_id': productions_reprogram,
'apply_time': ret['trigger_time'],
'send_time': ret['send_time'],
})
else:
logging.info('无对应状态,不需更新编程记录')

View File

@@ -1988,6 +1988,9 @@ class CuttingSpeed(models.Model):
})
else:
cutting_speed.write({
'standard_library_id': self.env['sf.cutting_tool.standard.library'].search(
[('code', '=', item['standard_library_code'].replace("JKM", result[
'factory_short_name']))]).id,
'execution_standard_id': self.env['sf.international.standards'].search(
[('code', '=', item['execution_standard_code'])]).id,
'material_name_id': self.env['sf.materials.model'].search(
@@ -2040,6 +2043,9 @@ class CuttingSpeed(models.Model):
})
else:
cutting_speed.write({
'standard_library_id': self.env['sf.cutting_tool.standard.library'].search(
[('code', '=', item['standard_library_code'].replace("JKM", result[
'factory_short_name']))]).id,
'execution_standard_id': self.env['sf.international.standards'].search(
[('code', '=', item['execution_standard_code'])]).id,
'material_name_id': self.env['sf.materials.model'].search(
@@ -2130,6 +2136,9 @@ class CuttingSpeed(models.Model):
})
else:
feed_per_tooth.write({
'standard_library_id': self.env['sf.cutting_tool.standard.library'].search(
[('code', '=', item['standard_library_code'].replace("JKM", result[
'factory_short_name']))]).id,
'materials_type_id': self.env['sf.materials.model'].search(
[('materials_no', '=', item['materials_type_code'])]).id,
'cutting_width_depth_id': self.env['sf.cutting.width.depth'].search(
@@ -2168,6 +2177,9 @@ class CuttingSpeed(models.Model):
})
else:
feed_per_tooth.write({
'standard_library_id': self.env['sf.cutting_tool.standard.library'].search(
[('code', '=', item['standard_library_code'].replace("JKM", result[
'factory_short_name']))]).id,
'materials_type_id': self.env['sf.materials.model'].search(
[('materials_no', '=', item['materials_type_code'])]).id,
'cutting_width_depth_id': self.env['sf.cutting.width.depth'].search(
@@ -2454,6 +2466,11 @@ class CuttingToolBasicParameters(models.Model):
else:
self.search([('code', '=', integral_tool_item['code'])]).write({
'name': integral_tool_item['name'],
'standard_library_id': self.env['sf.cutting_tool.standard.library'].search(
[(
'code', '=',
integral_tool_item['standard_library_code'].replace("JKM", result[
'factory_short_name']))]).id,
'total_length': integral_tool_item['total_length'],
'blade_diameter': integral_tool_item['blade_diameter'],
'blade_length': integral_tool_item['blade_length'],
@@ -2516,6 +2533,9 @@ class CuttingToolBasicParameters(models.Model):
else:
self.search([('code', '=', blade_item['code'])]).write({
'name': blade_item['name'],
'standard_library_id': self.env['sf.cutting_tool.standard.library'].search(
[('code', '=', blade_item['standard_library_code'].replace("JKM", result[
'factory_short_name']))]).id,
'length': blade_item['length'],
'thickness': blade_item['thickness'],
'cutting_blade_length': blade_item['cutting_blade_length'],
@@ -2573,6 +2593,9 @@ class CuttingToolBasicParameters(models.Model):
else:
self.search([('code', '=', chuck_item['code'])]).write({
'name': chuck_item['name'],
'standard_library_id': self.env['sf.cutting_tool.standard.library'].search(
[('code', '=', chuck_item['standard_library_code'].replace("JKM", result[
'factory_short_name']))]).id,
'er_size_model': chuck_item['size_model'],
'min_clamping_diameter': chuck_item['clamping_diameter_min'],
'max_clamping_diameter': chuck_item['clamping_diameter_max'],
@@ -2632,6 +2655,9 @@ class CuttingToolBasicParameters(models.Model):
else:
self.search([('code', '=', cutter_arbor_item['code'])]).write({
'name': cutter_arbor_item['name'],
'standard_library_id': self.env['sf.cutting_tool.standard.library'].search(
[('code', '=', cutter_arbor_item['standard_library_code'].replace("JKM", result[
'factory_short_name']))]).id,
'height': cutter_arbor_item['height'],
'width': cutter_arbor_item['width'],
'total_length': cutter_arbor_item['total_length'],
@@ -2697,6 +2723,9 @@ class CuttingToolBasicParameters(models.Model):
else:
self.search([('code', '=', cutter_head_item['code'])]).write({
'name': cutter_head_item['name'],
'standard_library_id': self.env['sf.cutting_tool.standard.library'].search(
[('code', '=', cutter_head_item['standard_library_code'].replace("JKM", result[
'factory_short_name']))]).id,
'install_blade_tip_num': cutter_head_item['number_blade_installed'],
'blade_diameter': cutter_head_item['blade_diameter'],
'cutter_head_diameter': cutter_head_item['cutter_diameter'],
@@ -2727,6 +2756,9 @@ class CuttingToolBasicParameters(models.Model):
[('code', '=', knife_handle_item['code']), ('active', 'in', [True, False])])
val = {
'name': knife_handle_item['name'],
'standard_library_id': self.env['sf.cutting_tool.standard.library'].search(
[('code', '=', knife_handle_item['standard_library_code'].replace("JKM", result[
'factory_short_name']))]).id,
'taper_shank_model': knife_handle_item['taper_shank_model'],
'total_length': knife_handle_item['total_length'],
'flange_shank_length': knife_handle_item['flange_length'],
@@ -2751,9 +2783,6 @@ class CuttingToolBasicParameters(models.Model):
if not knife_handle:
val['code'] = knife_handle_item['code']
val['cutting_tool_type'] = '刀柄'
val['standard_library_id'] = self.env['sf.cutting_tool.standard.library'].search(
[('code', '=', knife_handle_item['standard_library_code'].replace("JKM", result[
'factory_short_name']))]).id
self.create(val)
else:
self.search([('code', '=', knife_handle_item['code'])]).write(val)
@@ -2809,6 +2838,11 @@ class CuttingToolBasicParameters(models.Model):
else:
integral_tool.write({
'name': integral_tool_item['name'],
'standard_library_id': self.env['sf.cutting_tool.standard.library'].search(
[(
'code', '=',
integral_tool_item['standard_library_code'].replace("JKM", result[
'factory_short_name']))]).id,
'total_length': integral_tool_item['total_length'],
'blade_diameter': integral_tool_item['blade_diameter'],
'blade_length': integral_tool_item['blade_length'],
@@ -2871,6 +2905,9 @@ class CuttingToolBasicParameters(models.Model):
else:
blade.write({
'name': blade_item['name'],
'standard_library_id': self.env['sf.cutting_tool.standard.library'].search(
[('code', '=', blade_item['standard_library_code'].replace("JKM", result[
'factory_short_name']))]).id,
'length': blade_item['length'],
'thickness': blade_item['thickness'],
'cutting_blade_length': blade_item['cutting_blade_length'],
@@ -2928,6 +2965,9 @@ class CuttingToolBasicParameters(models.Model):
else:
chuck.write({
'name': chuck_item['name'],
'standard_library_id': self.env['sf.cutting_tool.standard.library'].search(
[('code', '=', chuck_item['standard_library_code'].replace("JKM", result[
'factory_short_name']))]).id,
'er_size_model': chuck_item['size_model'],
'min_clamping_diameter': chuck_item['clamping_diameter_min'],
'max_clamping_diameter': chuck_item['clamping_diameter_max'],
@@ -2987,6 +3027,9 @@ class CuttingToolBasicParameters(models.Model):
else:
cutter_arbor.write({
'name': cutter_arbor_item['name'],
'standard_library_id': self.env['sf.cutting_tool.standard.library'].search(
[('code', '=', cutter_arbor_item['standard_library_code'].replace("JKM", result[
'factory_short_name']))]).id,
'height': cutter_arbor_item['height'],
'width': cutter_arbor_item['width'],
'total_length': cutter_arbor_item['total_length'],
@@ -3053,6 +3096,9 @@ class CuttingToolBasicParameters(models.Model):
else:
cutter_head.write({
'name': cutter_head_item['name'],
'standard_library_id': self.env['sf.cutting_tool.standard.library'].search(
[('code', '=', cutter_head_item['standard_library_code'].replace("JKM", result[
'factory_short_name']))]).id,
'install_blade_tip_num': cutter_head_item['number_blade_installed'],
'blade_diameter': cutter_head_item['blade_diameter'],
'cutter_head_diameter': cutter_head_item['cutter_diameter'],
@@ -3114,6 +3160,9 @@ class CuttingToolBasicParameters(models.Model):
else:
knife_handle.write({
'name': knife_handle_item['name'],
'standard_library_id': self.env['sf.cutting_tool.standard.library'].search(
[('code', '=', knife_handle_item['standard_library_code'].replace("JKM", result[
'factory_short_name']))]).id,
'total_length': knife_handle_item['total_length'],
'taper_shank_model': knife_handle_item['taper_shank_model'],
'flange_shank_length': knife_handle_item['flange_length'],

View File

@@ -14,7 +14,8 @@ class QualityCheck(models.Model):
('waiting', '等待'),
('none', '待处理'),
('pass', '通过的'),
('fail', '失败的')], string='状态', tracking=True, store=True,
('fail', '失败的'),
('cancel', '已取消'), ], string='状态', tracking=True, store=True,
default='none', copy=False, compute='_compute_quality_state')
individuation_page_PTD = fields.Boolean('个性化记录(是否显示后置三元检测[PTD]页签)', related='workorder_id.individuation_page_PTD')
@@ -84,6 +85,17 @@ class QualityCheck(models.Model):
raise ValidationError('请填写【判定结果】里的信息')
if self.test_results == '合格':
raise ValidationError('请重新选择【判定结果】-【检测结果】')
if self.workorder_id.routing_type != 'CNC加工' and self.workorder_id.individuation_page_PTD is False:
self.workorder_id.production_id.write({'detection_result_ids': [(0, 0, {
'rework_reason': self.reason,
'detailed_reason': self.detailed_reason,
'processing_panel': self.workorder_id.processing_panel,
'routing_type': self.workorder_id.routing_type,
'handle_result': '待处理',
'test_results': self.test_results,
'test_report': self.workorder_id.detection_report})],
'is_scrap': True if self.test_results == '报废' else False
})
if self.workorder_id.state not in ['done']:
self.workorder_id.write(
{'test_results': self.test_results, 'reason': self.reason, 'detailed_reason': self.detailed_reason})
@@ -110,4 +122,3 @@ class QualityCheck(models.Model):
return "零件特采发送成功"
else:
raise ValidationError("零件特采发送失败")

View File

@@ -74,6 +74,7 @@
<field name="inherit_id" ref="quality_control.quality_check_view_tree"/>
<field name="arch" type="xml">
<xpath expr="//tree//field[@name='name']" position="after">
<field name="title" string="标准名"/>
<field name="operation_id" invisible="1"/>
</xpath>
</field>
@@ -89,8 +90,15 @@
</xpath>
<xpath expr="//field[@name='product_id']" position="after">
<field name="production_id"/>
</xpath>
</field>
</record>
<record id="quality_control.quality_check_action_main" model="ir.actions.act_window">
<field name="context">{
'is_web_request': True,
'search_default_progress': 1,
'search_default_passed': 1,
'search_default_failed': 1,
}</field>
</record>
</odoo>

View File

@@ -28,6 +28,7 @@
'web.assets_backend': [
'sf_sale/static/js/setTableWidth.js',
'sf_sale/static/src/css/purchase_list.css',
'sf_sale/static/lib/*',
]
},
'demo': [

View File

@@ -517,9 +517,10 @@ class ResUserToSale(models.Model):
@api.model
def _name_search(self, name, args=None, operator='ilike', limit=100, name_get_uid=None):
domain = []
if self._context.get('is_sale'):
if self.env.user.has_group('sf_base.group_sale_director'):
domain = []
pass
elif self.env.user.has_group('sf_base.group_sale_salemanager'):
if self.id != self.env.user.id:
domain = [('id', '=', self.id)]
@@ -528,7 +529,7 @@ class ResUserToSale(models.Model):
return self._search(domain, limit=limit, access_rights_uid=name_get_uid)
elif self._context.get('supplier_rank'):
if self.env.user.has_group('sf_base.group_purchase_director'):
domain = []
pass
elif self.env.user.has_group('sf_base.group_purchase'):
if self.id != self.env.user.id:
domain = [('id', '=', self.id)]

View File

@@ -0,0 +1,18 @@
/** @odoo-module */
import { Component } from "@odoo/owl";
import { registry } from "@web/core/registry";
export class MergeField extends Component {
get mergeValue() {
const data = this.props.record.data;
const v = data?.product_uom_qty
const unit = data?.product_uom[1]
return `${v} ${unit}`
}
}
MergeField.template = "jikimo_sf.MergeField";
registry.category("fields").add("merge_field", MergeField);

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="jikimo_sf.MergeField" owl="1">
<span t-esc="mergeValue"/>
</t>
</templates>

View File

@@ -1,3 +1,11 @@
.purchase_order_list_name {
min-width: 62px !important;
}
.o_list_renderer .o_list_table .o_data_row td.o_data_cell.o_field_cell.o_list_char.section_and_note_text, .section_and_note_text span{
white-space: wrap!important;
overflow: auto!important;
text-overflow: unset!important;
word-wrap: break-word;
word-break: break-all;
}

View File

@@ -139,7 +139,7 @@
</field>
<xpath expr="//field[@name='date_order']" position="after">
<field name="payment_term_id" attrs="{'readonly': ['|', ('invoice_status','=', 'invoiced'), ('state', '=', 'done')]}" options="{'no_create': True}"/>
<field name="contract_summary"/>
<!-- <field name="contract_summary"/>-->
</xpath>
<field name="partner_ref" position="attributes">
<attribute name="attrs">{'readonly': [('state', 'in', ['purchase'])]}

View File

@@ -102,7 +102,7 @@
</field>
<xpath expr="//field[@name='order_line']/tree/field[@name='name']" position="before">
<field name="model_glb_file" widget="Viewer3D" optional="show"
string="模型文件" attrs="{'readonly': [('state', 'in', ['draft'])]}"/>
string="模型文件" attrs="{'readonly': [('state', 'in', ['draft'])], 'isInList': True}"/>
<field name="part_name" optional="hide"/>
</xpath>
<xpath expr="//field[@name='order_line']/tree/field[@name='price_subtotal']" position="after">
@@ -112,6 +112,7 @@
<xpath expr="//field[@name='order_line']/tree/field[@name='product_template_id']" position="attributes">
<attribute name="options">{'no_create': True}</attribute>
<attribute name="context">{'is_sale_order_line': True }</attribute>
<attribute name="class">section_and_note_text</attribute>
</xpath>
<xpath expr="//field[@name='order_line']" position="attributes">
<attribute name="attrs">{'readonly': [('state', 'in', ['cancel','sale'])]}</attribute>
@@ -122,6 +123,13 @@
<field name="manual_quotation" readonly="1"/>
<field name="is_incoming_material" readonly="1"/>
</xpath>
<xpath expr="//field[@name='order_line']/tree/field[@name='product_uom_qty']" position="replace">
<field name="product_uom_qty" string="数量" widget="merge_field" optional="show" />
</xpath>
<xpath expr="//field[@name='order_line']/tree/field[@name='product_uom']" position="attributes">
<attribute name="optional">hide</attribute>
</xpath>
<field name="user_id" position="attributes">
<attribute name="attrs">{'readonly': [('state', 'in', ['cancel','sale'])]}</attribute>
</field>
@@ -228,6 +236,18 @@
</field>
</record>
<record id="view_order_form_inherit_sale_stock_qty_sf" model="ir.ui.view">
<field name="name">sale.order.line.tree.sale.stock.qty.sf</field>
<field name="inherit_id" ref="sale_stock.view_order_form_inherit_sale_stock_qty"/>
<field name="model">sale.order</field>
<field name="arch" type="xml">
<xpath expr="//page/field[@name='order_line']/form/group/group/div[@name='ordered_qty']/widget[@name='qty_at_date_widget']" position="replace">
</xpath>
<xpath expr="//page/field[@name='order_line']/tree/widget[@name='qty_at_date_widget']" position="replace">
</xpath>
</field>
</record>
<record id="view_quotation_with_onboarding_tree_inherit_sf" model="ir.ui.view">
<field name="name">sale.order.quotation.tree.inherit.sf</field>
<field name="model">sale.order</field>

View File

@@ -18,7 +18,7 @@ class StockPicking(models.Model):
@api.depends('name')
def _compute_pro_purchase_count(self):
for sp in self:
if sp:
if sp.name and sp.name != '/':
po_ids = self.env['purchase.order'].sudo().search([
('origin', 'like', sp.name), ('purchase_type', '=', 'standard')])
if po_ids:
@@ -52,7 +52,7 @@ class StockPicking(models.Model):
@api.depends('name')
def _compute_pro_out_purchase_count(self):
for sp in self:
if sp:
if sp.name and sp.name != '/':
po_ids = self.env['purchase.order'].sudo().search([
('origin', 'like', sp.name), ('purchase_type', '=', 'outsourcing')])
if po_ids:

View File

@@ -935,11 +935,28 @@ class SfStockPicking(models.Model):
_inherit = 'stock.picking'
check_in = fields.Char(string='查询是否为入库单', compute='_check_is_in')
product_uom_qty_sp = fields.Float('需求数量', compute='_compute_product_uom_qty_sp', store=True)
@api.depends('move_ids_without_package', 'move_ids_without_package.product_uom_qty')
def _compute_product_uom_qty_sp(self):
for sp in self:
if sp.move_ids_without_package:
sp.product_uom_qty_sp = 0
for move_id in sp.move_ids_without_package:
sp.product_uom_qty_sp += move_id.product_uom_qty
else:
sp.product_uom_qty_sp = 0
def batch_stock_move(self):
"""
批量调拨,非就绪状态的会被忽略,完成后有通知提示
"""
# 对所以调拨单的质检单进行是否完成校验
sp_ids = [sp.id for sp in self]
qc_ids = self.env['quality.check'].sudo().search(
[('picking_id', 'in', sp_ids), ('quality_state', 'in', ['waiting', 'none'])])
if qc_ids:
raise ValidationError(f'单据{[qc.picking_id.name for qc in qc_ids]}未完成质量检查,完成后再试。')
for record in self:
if record.state != 'assigned':
continue

View File

@@ -156,6 +156,9 @@
<xpath expr="//header" position="inside">
<button name="batch_stock_move" type='object' string="批量调拨"/>
</xpath>
<xpath expr="//field[@name='location_dest_id']" position="after">
<field name="product_uom_qty_sp"/>
</xpath>
</field>
</record>

View File

@@ -0,0 +1,3 @@
.model-viewer-in-list {
width: 150px;
}

View File

@@ -63,11 +63,16 @@ StepViewer.supportedTypes = ["binary"];
StepViewer.props = {
...standardFieldProps,
url: {type: String, optional: true},
isInList: {type: Boolean, optional: true},
};
StepViewer.extractProps = ({attrs}) => {
const modifiedAttrs = JSON.parse(attrs.modifiers || '{}');
return {
url: attrs.options.url,
isInList: modifiedAttrs.isInList,
};
};

View File

@@ -5,6 +5,7 @@
<t t-if="props.value">
<model-viewer
t-att-class="props.isInList ? 'model-viewer-in-list' : ''"
t-att-src='props.url'
name="3D model"
alt="3D model"