Merge branch 'develop' of https://e.coding.net/jikimo-hn/jikimo_sfs/jikimo_sf into feature/制造功能优化

This commit is contained in:
mgw
2025-02-17 15:37:58 +08:00
43 changed files with 520 additions and 369 deletions

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 +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

@@ -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

@@ -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})
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 + '|' + _.Φ
!newTableData[key] ? newTableData[key] = {i: _.index} : '';
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)
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

@@ -177,12 +177,12 @@
</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', ('整体式刀具','刀杆','刀盘','刀片'))]}">

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

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

View File

@@ -69,6 +69,7 @@ class ResMrpWorkOrder(models.Model):
delivery_warning = fields.Selection([('normal', '正常'), ('warning', '告警'), ('overdue', '逾期')], string='时效',
tracking=True)
date_planned_start = fields.Datetime(tracking=True)
@api.depends('processing_panel')
def _compute_processing_panel_selection(self):
@@ -1858,11 +1859,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

@@ -183,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

@@ -611,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:
@@ -844,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

@@ -785,6 +785,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"/>
@@ -807,7 +811,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,7 +19,7 @@
<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"> -->

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

@@ -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

@@ -156,4 +156,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

@@ -402,4 +402,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

@@ -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

@@ -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

@@ -85,6 +85,18 @@ 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.workorder_id.reason,
'detailed_reason': self.workorder_id.detailed_reason,
'processing_panel': self.workorder_id.processing_panel,
'routing_type': self.workorder_id.routing_type,
'handle_result': '待处理' if (self.workorder_id.test_results in ['返工', '报废']
or self.workorder_id.is_rework is True) else '',
'test_results': self.workorder_id.test_results,
'test_report': self.workorder_id.detection_report})],
'is_scrap': True if self.workorder_id.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})

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

@@ -1,3 +1,9 @@
.purchase_order_list_name {
min-width: 62px !important;
}
.section_and_note_text span{
white-space: wrap!important;
overflow: auto!important;
text-overflow: unset!important;
}

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>
@@ -228,6 +229,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,6 +935,17 @@ 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):
"""

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"