Merge branch 'dev' of https://gitee.com/jinan-pengyusen-data/gzth into dev
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
# k_ry_mobile_form - 科瑞移动端表单优化模块
|
||||
|
||||
## 功能说明
|
||||
|
||||
此模块为 Odoo 17 的 form 视图添加移动端优化功能:
|
||||
- 在手机浏览器中,form 视图 header 的工具按钮栏会悬浮在屏幕底部
|
||||
- 提供类似正常 APP 的底部按钮栏体验
|
||||
- 桌面端不受影响,保持原有显示方式
|
||||
|
||||
## 依赖
|
||||
|
||||
- `base`
|
||||
- `web`
|
||||
|
||||
## 安装
|
||||
|
||||
1. 将模块放置在 Odoo 的 addons 目录下
|
||||
2. 更新应用列表
|
||||
3. 安装 `k_ry_mobile_form` 模块
|
||||
|
||||
## 技术实现
|
||||
|
||||
### 文件结构
|
||||
|
||||
```
|
||||
k_ry_mobile_form/
|
||||
├── __init__.py
|
||||
├── __manifest__.py
|
||||
├── README.md
|
||||
└── static/
|
||||
└── src/
|
||||
├── js/
|
||||
│ └── status_bar_buttons.js # 覆盖 StatusBarButtons 组件模板
|
||||
├── xml/
|
||||
│ └── status_bar_buttons.xml # 新的模板定义
|
||||
└── scss/
|
||||
└── status_bar_buttons.scss # 移动端底部悬浮栏样式
|
||||
```
|
||||
|
||||
### 实现原理
|
||||
|
||||
1. **JavaScript 覆盖**:使用 `patch` 方法覆盖 `StatusBarButtons` 组件的模板
|
||||
2. **模板替换**:提供新的模板,根据 `env.isSmall` 判断显示方式
|
||||
3. **样式隔离**:使用媒体查询确保移动端和桌面端样式互不影响
|
||||
|
||||
## 使用说明
|
||||
|
||||
安装模块后,无需额外配置。在手机浏览器中打开任何包含 header 的 form 视图时,工具按钮栏会自动显示在底部。
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 此模块只影响 form 视图的 header 按钮栏
|
||||
- 桌面端完全不受影响
|
||||
- 支持 iOS 设备的底部安全区域(safe area)
|
||||
@@ -2,8 +2,381 @@
|
||||
|
||||
import { StatusBarButtons } from "@web/views/form/status_bar_buttons/status_bar_buttons";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { useEffect } from "@odoo/owl";
|
||||
|
||||
// 覆盖 StatusBarButtons 组件的模板
|
||||
// 检测是否为移动设备(包括iOS Safari)
|
||||
const isMobileDevice = () => {
|
||||
if (typeof window === 'undefined') return false;
|
||||
|
||||
// 检测触摸设备
|
||||
const hasTouch = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
|
||||
|
||||
// 检测用户代理(iOS设备)
|
||||
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
|
||||
|
||||
// 检测屏幕宽度
|
||||
const isSmallScreen = window.innerWidth <= 767;
|
||||
|
||||
// 检测是否为移动设备(综合考虑)
|
||||
return hasTouch && (isIOS || isSmallScreen);
|
||||
};
|
||||
|
||||
// 覆盖 StatusBarButtons 组件,添加移动端检测逻辑
|
||||
patch(StatusBarButtons, {
|
||||
template: "k_ry_mobile_form.StatusBarButtons",
|
||||
setup() {
|
||||
// 如果 env.isSmall 判断不准确,强制显示移动端按钮
|
||||
if (typeof window !== 'undefined') {
|
||||
useEffect(() => {
|
||||
const checkAndFixMobileButtons = () => {
|
||||
try {
|
||||
// 如果是移动设备但 env.isSmall 为 false,强制显示移动端按钮
|
||||
if (isMobileDevice()) {
|
||||
const mobileContainers = document.querySelectorAll('.o_statusbar_buttons_mobile_fixed');
|
||||
if (mobileContainers.length === 0) return;
|
||||
|
||||
mobileContainers.forEach(container => {
|
||||
try {
|
||||
// 使用内联样式强制显示,优先级最高
|
||||
container.style.setProperty('display', 'block', 'important');
|
||||
container.style.setProperty('visibility', 'visible', 'important');
|
||||
container.style.setProperty('opacity', '1', 'important');
|
||||
container.style.setProperty('position', 'fixed', 'important');
|
||||
container.style.setProperty('bottom', '0', 'important');
|
||||
container.style.setProperty('left', '0', 'important');
|
||||
container.style.setProperty('right', '0', 'important');
|
||||
container.style.setProperty('z-index', '99999', 'important');
|
||||
// 强制设置背景色,防止被主题覆盖
|
||||
container.style.setProperty('background-color', '#ffffff', 'important');
|
||||
container.style.setProperty('background', '#ffffff', 'important');
|
||||
|
||||
// 同时修复容器内的按钮
|
||||
const buttons = container.querySelectorAll('button, a.btn, .btn, a[role="button"], .o_statusbar_buttons > * > button, .o_statusbar_buttons > * > a');
|
||||
buttons.forEach(button => {
|
||||
try {
|
||||
// 强制设置按钮可见性
|
||||
button.style.setProperty('visibility', 'visible', 'important');
|
||||
button.style.setProperty('opacity', '1', 'important');
|
||||
button.style.setProperty('display', 'inline-block', 'important');
|
||||
button.style.setProperty('font-size', '14px', 'important');
|
||||
button.style.setProperty('font-weight', '500', 'important');
|
||||
// 防止文字被设置为透明
|
||||
button.style.setProperty('text-indent', '0', 'important');
|
||||
|
||||
// 检查按钮颜色
|
||||
const computedStyle = getComputedStyle(button);
|
||||
const bgColor = computedStyle.backgroundColor;
|
||||
const textColor = computedStyle.color;
|
||||
|
||||
// 如果背景色是透明或白色,且文字颜色也是白色或透明,强制设置颜色
|
||||
const isTransparent = bgColor === 'transparent' || bgColor === 'rgba(0, 0, 0, 0)';
|
||||
const isWhiteBg = bgColor === 'rgb(255, 255, 255)' || bgColor === '#ffffff' || bgColor === '#fff';
|
||||
const isWhiteText = textColor === 'rgb(255, 255, 255)' || textColor === '#ffffff' || textColor === '#fff';
|
||||
const isTransparentText = textColor === 'transparent' || textColor === 'rgba(0, 0, 0, 0)';
|
||||
|
||||
// 如果按钮不可见,强制设置颜色
|
||||
if (isTransparent || (isWhiteBg && (isWhiteText || isTransparentText)) || textColor === 'rgba(0, 0, 0, 0)') {
|
||||
// 根据按钮类型设置颜色
|
||||
const hasPrimary = button.classList.contains('btn-primary') || button.classList.contains('o_statusbar_status');
|
||||
const hasSecondary = button.classList.contains('btn-secondary');
|
||||
const hasSuccess = button.classList.contains('btn-success');
|
||||
const hasDanger = button.classList.contains('btn-danger');
|
||||
const hasWarning = button.classList.contains('btn-warning');
|
||||
const hasInfo = button.classList.contains('btn-info');
|
||||
|
||||
if (hasPrimary || hasSuccess) {
|
||||
button.style.setProperty('background-color', '#007bff', 'important');
|
||||
button.style.setProperty('color', '#ffffff', 'important');
|
||||
} else if (hasSecondary) {
|
||||
button.style.setProperty('background-color', '#6c757d', 'important');
|
||||
button.style.setProperty('color', '#ffffff', 'important');
|
||||
} else if (hasDanger) {
|
||||
button.style.setProperty('background-color', '#dc3545', 'important');
|
||||
button.style.setProperty('color', '#ffffff', 'important');
|
||||
} else if (hasWarning) {
|
||||
button.style.setProperty('background-color', '#ffc107', 'important');
|
||||
button.style.setProperty('color', '#212529', 'important');
|
||||
} else if (hasInfo) {
|
||||
button.style.setProperty('background-color', '#17a2b8', 'important');
|
||||
button.style.setProperty('color', '#ffffff', 'important');
|
||||
} else {
|
||||
// 默认蓝色按钮
|
||||
button.style.setProperty('background-color', '#007bff', 'important');
|
||||
button.style.setProperty('color', '#ffffff', 'important');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Error fixing button in setup:', e);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn('Error fixing container in setup:', e);
|
||||
}
|
||||
});
|
||||
|
||||
// 隐藏桌面端按钮
|
||||
try {
|
||||
const desktopButtons = document.querySelectorAll('.o_form_statusbar > .o_statusbar_buttons:not(.o_statusbar_buttons_mobile_fixed .o_statusbar_buttons)');
|
||||
desktopButtons.forEach(btn => {
|
||||
btn.style.setProperty('display', 'none', 'important');
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn('Error hiding desktop buttons in setup:', e);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Error in checkAndFixMobileButtons:', e);
|
||||
}
|
||||
};
|
||||
|
||||
// 延迟执行,确保DOM已渲染
|
||||
setTimeout(checkAndFixMobileButtons, 100);
|
||||
|
||||
// 监听窗口大小变化
|
||||
const handleResize = () => {
|
||||
setTimeout(checkAndFixMobileButtons, 100);
|
||||
};
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
};
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// iOS Safari修复:确保移动端按钮在iOS上可点击和可见
|
||||
// 使用MutationObserver监听DOM变化,确保新添加的按钮也能正常工作
|
||||
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
|
||||
let fixTimeout = null;
|
||||
let observer = null;
|
||||
let checkInterval = null;
|
||||
|
||||
// 防抖函数,避免频繁执行
|
||||
const debounce = (func, wait) => {
|
||||
return function executedFunction(...args) {
|
||||
const later = () => {
|
||||
clearTimeout(fixTimeout);
|
||||
func(...args);
|
||||
};
|
||||
clearTimeout(fixTimeout);
|
||||
fixTimeout = setTimeout(later, wait);
|
||||
};
|
||||
};
|
||||
|
||||
const fixIOSButtons = () => {
|
||||
try {
|
||||
// 确保移动端按钮容器可见
|
||||
const containers = document.querySelectorAll('.o_statusbar_buttons_mobile_fixed');
|
||||
if (containers.length === 0) return;
|
||||
|
||||
containers.forEach(container => {
|
||||
try {
|
||||
// 如果是移动设备,确保容器可见
|
||||
if (isMobileDevice()) {
|
||||
// 使用 setProperty 的 important 标志强制设置样式
|
||||
container.style.setProperty('display', 'block', 'important');
|
||||
container.style.setProperty('visibility', 'visible', 'important');
|
||||
container.style.setProperty('opacity', '1', 'important');
|
||||
container.style.setProperty('position', 'fixed', 'important');
|
||||
container.style.setProperty('bottom', '0', 'important');
|
||||
container.style.setProperty('left', '0', 'important');
|
||||
container.style.setProperty('right', '0', 'important');
|
||||
container.style.setProperty('z-index', '99999', 'important');
|
||||
// 强制设置背景色,防止被主题覆盖
|
||||
container.style.setProperty('background-color', '#ffffff', 'important');
|
||||
container.style.setProperty('background', '#ffffff', 'important');
|
||||
}
|
||||
|
||||
// 修复按钮的交互性和可见性
|
||||
const buttons = container.querySelectorAll('button, a.btn, .btn, a[role="button"], .o_statusbar_buttons > * > button, .o_statusbar_buttons > * > a');
|
||||
buttons.forEach(button => {
|
||||
try {
|
||||
// 使用 setProperty 强制设置样式
|
||||
button.style.setProperty('pointer-events', 'auto', 'important');
|
||||
button.style.setProperty('cursor', 'pointer', 'important');
|
||||
button.style.setProperty('touch-action', 'manipulation', 'important');
|
||||
button.style.setProperty('visibility', 'visible', 'important');
|
||||
button.style.setProperty('opacity', '1', 'important');
|
||||
button.style.setProperty('display', 'inline-block', 'important');
|
||||
// 确保按钮有最小高度和字体大小
|
||||
button.style.setProperty('min-height', '44px', 'important');
|
||||
button.style.setProperty('font-size', '14px', 'important');
|
||||
button.style.setProperty('font-weight', '500', 'important');
|
||||
// 防止文字被设置为透明
|
||||
button.style.setProperty('text-indent', '0', 'important');
|
||||
|
||||
// 检查按钮颜色
|
||||
const computedStyle = getComputedStyle(button);
|
||||
const bgColor = computedStyle.backgroundColor;
|
||||
const textColor = computedStyle.color;
|
||||
|
||||
// 如果背景色是透明或白色,且文字颜色也是白色或透明,强制设置颜色
|
||||
const isTransparent = bgColor === 'transparent' || bgColor === 'rgba(0, 0, 0, 0)';
|
||||
const isWhiteBg = bgColor === 'rgb(255, 255, 255)' || bgColor === '#ffffff' || bgColor === '#fff';
|
||||
const isWhiteText = textColor === 'rgb(255, 255, 255)' || textColor === '#ffffff' || textColor === '#fff';
|
||||
const isTransparentText = textColor === 'transparent' || textColor === 'rgba(0, 0, 0, 0)';
|
||||
|
||||
// 如果按钮不可见,强制设置颜色
|
||||
if (isTransparent || (isWhiteBg && (isWhiteText || isTransparentText)) || textColor === 'rgba(0, 0, 0, 0)') {
|
||||
// 根据按钮类型设置颜色
|
||||
const hasPrimary = button.classList.contains('btn-primary') || button.classList.contains('o_statusbar_status');
|
||||
const hasSecondary = button.classList.contains('btn-secondary');
|
||||
const hasSuccess = button.classList.contains('btn-success');
|
||||
const hasDanger = button.classList.contains('btn-danger');
|
||||
const hasWarning = button.classList.contains('btn-warning');
|
||||
const hasInfo = button.classList.contains('btn-info');
|
||||
|
||||
if (hasPrimary || hasSuccess) {
|
||||
button.style.setProperty('background-color', '#007bff', 'important');
|
||||
button.style.setProperty('color', '#ffffff', 'important');
|
||||
} else if (hasSecondary) {
|
||||
button.style.setProperty('background-color', '#6c757d', 'important');
|
||||
button.style.setProperty('color', '#ffffff', 'important');
|
||||
} else if (hasDanger) {
|
||||
button.style.setProperty('background-color', '#dc3545', 'important');
|
||||
button.style.setProperty('color', '#ffffff', 'important');
|
||||
} else if (hasWarning) {
|
||||
button.style.setProperty('background-color', '#ffc107', 'important');
|
||||
button.style.setProperty('color', '#212529', 'important');
|
||||
} else if (hasInfo) {
|
||||
button.style.setProperty('background-color', '#17a2b8', 'important');
|
||||
button.style.setProperty('color', '#ffffff', 'important');
|
||||
} else {
|
||||
// 默认蓝色按钮
|
||||
button.style.setProperty('background-color', '#007bff', 'important');
|
||||
button.style.setProperty('color', '#ffffff', 'important');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Error fixing button:', e);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn('Error fixing container:', e);
|
||||
}
|
||||
});
|
||||
|
||||
// 在移动设备上,确保原始状态栏中的按钮被隐藏
|
||||
if (isMobileDevice()) {
|
||||
try {
|
||||
const desktopButtons = document.querySelectorAll('.o_form_statusbar > .o_statusbar_buttons:not(.o_statusbar_buttons_mobile_fixed .o_statusbar_buttons)');
|
||||
desktopButtons.forEach(btn => {
|
||||
btn.style.setProperty('display', 'none', 'important');
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn('Error hiding desktop buttons:', e);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Error in fixIOSButtons:', e);
|
||||
}
|
||||
};
|
||||
|
||||
// 防抖版本的修复函数
|
||||
const debouncedFixIOSButtons = debounce(fixIOSButtons, 300);
|
||||
|
||||
// 初始化函数:确保DOM准备好后再设置observer
|
||||
const initIOSButtonFix = () => {
|
||||
try {
|
||||
// 先执行一次修复
|
||||
fixIOSButtons();
|
||||
|
||||
// 确保document.body存在后再创建observer
|
||||
if (document.body && !observer) {
|
||||
// 监听DOM变化,确保动态添加的按钮也能正常工作
|
||||
observer = new MutationObserver((mutations) => {
|
||||
// 只处理相关的变化
|
||||
const hasRelevantChange = mutations.some(mutation => {
|
||||
if (mutation.type === 'childList') {
|
||||
return Array.from(mutation.addedNodes).some(node =>
|
||||
node.nodeType === 1 && (
|
||||
node.classList?.contains('o_statusbar_buttons_mobile_fixed') ||
|
||||
node.querySelector?.('.o_statusbar_buttons_mobile_fixed')
|
||||
)
|
||||
);
|
||||
}
|
||||
if (mutation.type === 'attributes') {
|
||||
return mutation.target.classList?.contains('o_statusbar_buttons_mobile_fixed') ||
|
||||
mutation.target.closest?.('.o_statusbar_buttons_mobile_fixed');
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (hasRelevantChange) {
|
||||
debouncedFixIOSButtons();
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true,
|
||||
attributeFilter: ['style', 'class']
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Error initializing button fix:', e);
|
||||
}
|
||||
};
|
||||
|
||||
// 页面加载后执行
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initIOSButtonFix, { once: true });
|
||||
} else {
|
||||
// 使用setTimeout确保DOM完全准备好,不阻塞页面加载
|
||||
setTimeout(initIOSButtonFix, 100);
|
||||
}
|
||||
|
||||
// 监听窗口大小变化和方向变化(iOS Safari 横竖屏切换)
|
||||
window.addEventListener('resize', () => {
|
||||
debouncedFixIOSButtons();
|
||||
}, { passive: true });
|
||||
|
||||
window.addEventListener('orientationchange', () => {
|
||||
setTimeout(debouncedFixIOSButtons, 200);
|
||||
}, { passive: true });
|
||||
|
||||
// 定期检查按钮是否可见(防止被其他样式覆盖)
|
||||
if (isMobileDevice()) {
|
||||
// 延迟启动定时器,避免影响页面加载
|
||||
setTimeout(() => {
|
||||
checkInterval = setInterval(() => {
|
||||
try {
|
||||
const containers = document.querySelectorAll('.o_statusbar_buttons_mobile_fixed');
|
||||
if (containers.length > 0) {
|
||||
containers.forEach(container => {
|
||||
try {
|
||||
const computedStyle = getComputedStyle(container);
|
||||
// 如果按钮被隐藏,强制显示
|
||||
if (computedStyle.display === 'none' ||
|
||||
computedStyle.visibility === 'hidden' ||
|
||||
parseFloat(computedStyle.opacity) < 0.1) {
|
||||
fixIOSButtons();
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Error checking container:', e);
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Error in interval check:', e);
|
||||
}
|
||||
}, 2000); // 改为每2秒检查一次,减少频率
|
||||
}, 2000);
|
||||
|
||||
// 页面卸载时清除定时器和observer
|
||||
window.addEventListener('beforeunload', () => {
|
||||
if (checkInterval) {
|
||||
clearInterval(checkInterval);
|
||||
}
|
||||
if (observer) {
|
||||
observer.disconnect();
|
||||
}
|
||||
if (fixTimeout) {
|
||||
clearTimeout(fixTimeout);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,41 +1,171 @@
|
||||
// Mobile: 底部悬浮栏样式
|
||||
.o_statusbar_buttons_mobile_fixed {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1050; // 确保在其他元素之上,但低于模态框
|
||||
background-color: $o-view-background-color;
|
||||
border-top: 1px solid $border-color;
|
||||
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1);
|
||||
padding: map-get($spacers, 2) map-get($spacers, 3);
|
||||
// 使用高优先级选择器,防止被其他主题样式覆盖
|
||||
.o_statusbar_buttons_mobile_fixed,
|
||||
body .o_statusbar_buttons_mobile_fixed,
|
||||
html body .o_statusbar_buttons_mobile_fixed {
|
||||
position: fixed !important;
|
||||
bottom: 0 !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
z-index: 99999 !important; // 比主题的9999更高,确保在最上层
|
||||
// 强制设置背景色,防止被主题覆盖
|
||||
background-color: #ffffff !important;
|
||||
background: #ffffff !important;
|
||||
border-top: 1px solid $border-color !important;
|
||||
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1) !important;
|
||||
padding: map-get($spacers, 2) map-get($spacers, 3) !important;
|
||||
// 为底部悬浮栏预留空间,避免内容被遮挡(支持 iOS safe area)
|
||||
padding-bottom: calc(#{map-get($spacers, 2)} + env(safe-area-inset-bottom, 0px));
|
||||
padding-bottom: calc(#{map-get($spacers, 2)} + env(safe-area-inset-bottom, 0px)) !important;
|
||||
// iOS Safari 修复:确保元素可交互
|
||||
-webkit-transform: translateZ(0) !important;
|
||||
transform: translateZ(0) !important;
|
||||
// iOS Safari 修复:防止滚动时fixed定位失效
|
||||
-webkit-backface-visibility: hidden !important;
|
||||
backface-visibility: hidden !important;
|
||||
// 强制显示属性
|
||||
display: block !important;
|
||||
visibility: visible !important;
|
||||
opacity: 1 !important;
|
||||
// 确保元素不会被其他样式隐藏
|
||||
overflow: visible !important;
|
||||
clip: auto !important;
|
||||
clip-path: none !important;
|
||||
}
|
||||
|
||||
.o_statusbar_buttons {
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
max-width: 100% !important;
|
||||
width: 100% !important;
|
||||
// iOS Safari 修复:确保容器可交互
|
||||
pointer-events: auto !important;
|
||||
-webkit-transform: translateZ(0) !important;
|
||||
transform: translateZ(0) !important;
|
||||
// 强制显示
|
||||
display: flex !important;
|
||||
visibility: visible !important;
|
||||
opacity: 1 !important;
|
||||
|
||||
> * {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
flex: 1 !important;
|
||||
min-width: 0 !important;
|
||||
// iOS Safari 修复:确保子元素可交互
|
||||
pointer-events: auto !important;
|
||||
// 强制显示
|
||||
display: block !important;
|
||||
visibility: visible !important;
|
||||
opacity: 1 !important;
|
||||
|
||||
> button {
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
> button,
|
||||
> a,
|
||||
> .btn {
|
||||
width: 100% !important;
|
||||
white-space: nowrap !important;
|
||||
overflow: hidden !important;
|
||||
text-overflow: ellipsis !important;
|
||||
// 确保按钮有足够的点击区域
|
||||
min-height: 44px;
|
||||
min-height: 44px !important;
|
||||
// iOS Safari 修复:确保按钮可点击
|
||||
cursor: pointer !important;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0.1) !important;
|
||||
touch-action: manipulation !important;
|
||||
// iOS Safari 修复:防止文本选择影响点击
|
||||
-webkit-user-select: none !important;
|
||||
user-select: none !important;
|
||||
-webkit-touch-callout: none !important;
|
||||
// iOS Safari 修复:确保按钮在层级之上
|
||||
position: relative !important;
|
||||
z-index: 1 !important;
|
||||
// iOS Safari 修复:确保pointer-events正确
|
||||
pointer-events: auto !important;
|
||||
// iOS Safari 修复:防止transform影响点击
|
||||
-webkit-transform: translateZ(0) !important;
|
||||
transform: translateZ(0) !important;
|
||||
// 强制显示按钮
|
||||
display: inline-block !important;
|
||||
visibility: visible !important;
|
||||
opacity: 1 !important;
|
||||
// 确保按钮背景和文字可见,防止被主题覆盖
|
||||
background-color: #007bff !important;
|
||||
background: #007bff !important;
|
||||
color: #ffffff !important;
|
||||
// 确保字体大小可见
|
||||
font-size: 14px !important;
|
||||
font-weight: 500 !important;
|
||||
// 确保有边框和内边距
|
||||
border: 1px solid #0056b3 !important;
|
||||
border-radius: 4px !important;
|
||||
padding: 8px 16px !important;
|
||||
text-align: center !important;
|
||||
// 防止文字被设置为透明
|
||||
text-indent: 0 !important;
|
||||
}
|
||||
|
||||
// 特殊处理不同类型的按钮,确保都有可见的样式
|
||||
> button.btn-primary,
|
||||
> a.btn-primary,
|
||||
> .btn-primary {
|
||||
background-color: #007bff !important;
|
||||
background: #007bff !important;
|
||||
color: #ffffff !important;
|
||||
border-color: #0056b3 !important;
|
||||
}
|
||||
|
||||
> button.btn-secondary,
|
||||
> a.btn-secondary,
|
||||
> .btn-secondary {
|
||||
background-color: #6c757d !important;
|
||||
background: #6c757d !important;
|
||||
color: #ffffff !important;
|
||||
border-color: #545b62 !important;
|
||||
}
|
||||
|
||||
> button.btn-success,
|
||||
> a.btn-success,
|
||||
> .btn-success {
|
||||
background-color: #28a745 !important;
|
||||
background: #28a745 !important;
|
||||
color: #ffffff !important;
|
||||
border-color: #1e7e34 !important;
|
||||
}
|
||||
|
||||
> button.btn-danger,
|
||||
> a.btn-danger,
|
||||
> .btn-danger {
|
||||
background-color: #dc3545 !important;
|
||||
background: #dc3545 !important;
|
||||
color: #ffffff !important;
|
||||
border-color: #c82333 !important;
|
||||
}
|
||||
|
||||
> button.btn-warning,
|
||||
> a.btn-warning,
|
||||
> .btn-warning {
|
||||
background-color: #ffc107 !important;
|
||||
background: #ffc107 !important;
|
||||
color: #212529 !important;
|
||||
border-color: #e0a800 !important;
|
||||
}
|
||||
|
||||
> button.btn-info,
|
||||
> a.btn-info,
|
||||
> .btn-info {
|
||||
background-color: #17a2b8 !important;
|
||||
background: #17a2b8 !important;
|
||||
color: #ffffff !important;
|
||||
border-color: #117a8b !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 桌面端隐藏移动端悬浮栏
|
||||
|
||||
// 桌面端隐藏移动端悬浮栏(但保留触摸设备的显示)
|
||||
@include media-breakpoint-up(md) {
|
||||
.o_statusbar_buttons_mobile_fixed {
|
||||
display: none !important;
|
||||
.o_statusbar_buttons_mobile_fixed,
|
||||
body .o_statusbar_buttons_mobile_fixed,
|
||||
html body .o_statusbar_buttons_mobile_fixed {
|
||||
// 只在非触摸设备上隐藏(桌面浏览器)
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +174,7 @@
|
||||
.o_form_statusbar {
|
||||
// 隐藏状态栏中的按钮容器,但保留字段等其他内容
|
||||
> .o_statusbar_buttons:not(.o_statusbar_buttons_mobile_fixed .o_statusbar_buttons) {
|
||||
display: none;
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,3 +183,59 @@
|
||||
padding-bottom: 80px; // 为底部悬浮栏预留空间
|
||||
}
|
||||
}
|
||||
|
||||
// iOS Safari 特殊处理:确保在 iOS 设备上按钮可见
|
||||
// 使用高优先级选择器,防止被其他主题样式覆盖
|
||||
@media (hover: none) and (pointer: coarse) {
|
||||
.o_statusbar_buttons_mobile_fixed,
|
||||
body .o_statusbar_buttons_mobile_fixed,
|
||||
html body .o_statusbar_buttons_mobile_fixed {
|
||||
display: block !important;
|
||||
visibility: visible !important;
|
||||
opacity: 1 !important;
|
||||
position: fixed !important;
|
||||
bottom: 0 !important;
|
||||
z-index: 99999 !important;
|
||||
}
|
||||
|
||||
.o_form_statusbar > .o_statusbar_buttons:not(.o_statusbar_buttons_mobile_fixed .o_statusbar_buttons),
|
||||
body .o_form_statusbar > .o_statusbar_buttons:not(.o_statusbar_buttons_mobile_fixed .o_statusbar_buttons) {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 通用移动设备强制显示规则(防止被其他主题覆盖)
|
||||
@media (max-width: 767px) {
|
||||
.o_statusbar_buttons_mobile_fixed,
|
||||
body .o_statusbar_buttons_mobile_fixed,
|
||||
html body .o_statusbar_buttons_mobile_fixed {
|
||||
display: block !important;
|
||||
visibility: visible !important;
|
||||
opacity: 1 !important;
|
||||
position: fixed !important;
|
||||
bottom: 0 !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
z-index: 99999 !important;
|
||||
// 强制背景色,防止被主题覆盖
|
||||
background-color: #ffffff !important;
|
||||
background: #ffffff !important;
|
||||
}
|
||||
|
||||
// 确保按钮容器内的所有按钮都可见
|
||||
.o_statusbar_buttons_mobile_fixed .o_statusbar_buttons,
|
||||
body .o_statusbar_buttons_mobile_fixed .o_statusbar_buttons,
|
||||
html body .o_statusbar_buttons_mobile_fixed .o_statusbar_buttons {
|
||||
button, a, .btn {
|
||||
// 防止文字被设置为透明
|
||||
color: #ffffff !important;
|
||||
text-indent: 0 !important;
|
||||
// 确保有背景色
|
||||
background-color: #007bff !important;
|
||||
background: #007bff !important;
|
||||
// 确保字体可见
|
||||
font-size: 14px !important;
|
||||
font-weight: 500 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,24 +4,20 @@
|
||||
<!-- 覆盖 web.StatusBarButtons 模板,实现移动端底部悬浮栏 -->
|
||||
<t t-name="k_ry_mobile_form.StatusBarButtons">
|
||||
<t t-set="slots" t-value="visibleSlotNames" />
|
||||
<t t-if="env.isSmall">
|
||||
<!-- Mobile: 底部悬浮栏 -->
|
||||
<div class="o_statusbar_buttons_mobile_fixed">
|
||||
<div class="o_statusbar_buttons d-flex align-items-center justify-content-center gap-2">
|
||||
<t t-foreach="slots" t-as="slot" t-key="slot">
|
||||
<t t-slot="{{ slot }}" />
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<!-- Desktop: 正常显示 -->
|
||||
<div class="o_statusbar_buttons d-flex align-items-center align-content-around flex-wrap gap-1">
|
||||
<!-- 移动端:底部悬浮栏(始终渲染,通过CSS控制显示) -->
|
||||
<div class="o_statusbar_buttons_mobile_fixed" t-att-style="env.isSmall ? '' : 'display: none;'">
|
||||
<div class="o_statusbar_buttons d-flex align-items-center justify-content-center gap-2">
|
||||
<t t-foreach="slots" t-as="slot" t-key="slot">
|
||||
<t t-slot="{{ slot }}" />
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
<!-- 桌面端:正常显示 -->
|
||||
<div class="o_statusbar_buttons d-flex align-items-center align-content-around flex-wrap gap-1" t-att-style="env.isSmall ? 'display: none;' : ''">
|
||||
<t t-foreach="slots" t-as="slot" t-key="slot">
|
||||
<t t-slot="{{ slot }}" />
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
||||
@@ -92,7 +92,7 @@ class RoomBooking(models.Model):
|
||||
def _onchange_meeting_type_id(self):
|
||||
if self.meeting_type_id:
|
||||
self.room_users_ids = self.meeting_type_id.users_ids
|
||||
self.host = self.meeting_type_id.host_user_id if self.meeting_type_id.host_user_id else False
|
||||
self.host = self.meeting_type_id.host_user_id.id if self.meeting_type_id.host_user_id else False
|
||||
else:
|
||||
self.room_users_ids = [(5, 0, 0)]
|
||||
self.host = False
|
||||
|
||||
@@ -559,6 +559,22 @@ class SoongWorkflowMixin(models.AbstractModel):
|
||||
comment_note_id = btn_rec.node_id.comment_id.id
|
||||
else:
|
||||
comment_note_id = False
|
||||
# 加签人员过滤
|
||||
if self.log_ids:
|
||||
log_node_ids = self.log_ids.mapped('node_id.id')
|
||||
log_nodes = self.env['soong.workflow.node'].browse(log_node_ids)
|
||||
all_node_employees = log_nodes.mapped('employee_ids')
|
||||
else:
|
||||
current_node = node_list[state_list.index(state)]
|
||||
all_node_employees = current_node.employee_ids
|
||||
|
||||
log_done_by_users = self.log_ids.mapped('done_by')
|
||||
log2_done_by_users = self.log_ids2.mapped('done_by')
|
||||
all_done_by_users = log_done_by_users | log2_done_by_users
|
||||
done_by_employees = self.env['hr.employee'].search([
|
||||
('user_id', 'in', all_done_by_users.ids)
|
||||
])
|
||||
all_filter_employees = all_node_employees | done_by_employees
|
||||
return {
|
||||
"name": _("评论"),
|
||||
"type": "ir.actions.act_window",
|
||||
@@ -572,6 +588,7 @@ class SoongWorkflowMixin(models.AbstractModel):
|
||||
'default_comment_note_id': comment_note_id,
|
||||
"default_code": self.code,
|
||||
"default_res_model": self._name,
|
||||
"default_filter_personnel_ids": [(6, 0, all_filter_employees.ids)],
|
||||
"default_node_btn_id": btn_rec.id,
|
||||
"default_is_supervising": is_supervising,
|
||||
"default_trans_link_id": self.env['trans.link'].search(trans_domain, limit=1).id if btn_rec.name == '转交' else False,
|
||||
|
||||
@@ -86,15 +86,57 @@ class SoongWorkflowNode(models.Model):
|
||||
is_carbon_copy = fields.Boolean(string="抄送/传阅")
|
||||
is_archive = fields.Boolean(string="归档")
|
||||
is_apostille = fields.Boolean(string="加签", default=False)
|
||||
employee_ids = fields.Many2many('hr.employee', string='抄送/传阅人员')
|
||||
department1_ids = fields.Many2many(
|
||||
"hr.department",
|
||||
string="抄送部门",
|
||||
relation="soong_workflow_node_department_rel",
|
||||
column1="node_id",
|
||||
column2="department_id"
|
||||
)
|
||||
user_role_ids = fields.Many2many("soong.user.role", string="抄送角色", )
|
||||
users_role_group_ids = fields.Many2many('soong.user.role.group', string='抄送角色组')
|
||||
employee_ids = fields.Many2many(
|
||||
'hr.employee',
|
||||
string='抄送/传阅人员',
|
||||
relation="soong_workflow_node_employee_total_rel",
|
||||
column1="node_id",
|
||||
column2="employee_total_id"
|
||||
)
|
||||
is_end = fields.Boolean(string="结束", default=False)
|
||||
is_confirm = fields.Boolean(string="确认", default=False)
|
||||
department_id = fields.Many2many('hr.department', string='部门转入范围')
|
||||
department_id = fields.Many2many(
|
||||
'hr.department',
|
||||
string='部门转入范围',
|
||||
relation="soong_workflow_node_transfer_department_rel",
|
||||
column1="node_id",
|
||||
column2="transfer_department_id"
|
||||
)
|
||||
comment_id = fields.Many2one('soong.note', string="固定意见")
|
||||
approval_concatenation = fields.Selection([('chuan', '串申'), ('bing', '并申')], string="审批形式", default="bing",
|
||||
help='并:任何人均给意见,最后一个完成的人转交到下一节点.'
|
||||
'串:都会收到消息,但任意一个人转交后,即到下一节点,其他人将不再显示,相当于收回.')
|
||||
|
||||
@api.onchange('department1_ids', 'user_role_ids', 'users_role_group_ids', 'employee1_ids')
|
||||
def _onchange_sync_employee_ids(self):
|
||||
for record in self:
|
||||
final_employees = self.env['hr.employee']
|
||||
role_group_roles = record.users_role_group_ids.mapped('user_role_ids')
|
||||
all_target_roles = record.user_role_ids | role_group_roles
|
||||
if all_target_roles:
|
||||
role_employees = self.env['hr.employee'].search([
|
||||
'|',
|
||||
('user_role_id', 'in', all_target_roles.ids),
|
||||
('user_role_ids', 'in', all_target_roles.ids)
|
||||
])
|
||||
final_employees |= role_employees
|
||||
|
||||
if record.department1_ids:
|
||||
dept_employees = self.env['hr.employee'].search([
|
||||
('department_id', 'in', record.department1_ids.ids)
|
||||
])
|
||||
final_employees |= dept_employees
|
||||
record.employee_ids = final_employees if final_employees else [(5, 0, 0)]
|
||||
|
||||
@api.onchange("is_apostille")
|
||||
def onchange_is_apostille(self):
|
||||
for record in self:
|
||||
|
||||
@@ -543,19 +543,20 @@ class SoongWorkflowRecord(models.Model):
|
||||
def reminders_record(self):
|
||||
record_id = self.env[self.res_model].search([('id', '=', self.res_id)])
|
||||
menu_id = self.env['ir.ui.menu'].search([('name', '=', '我的流程')])
|
||||
target_url = f'https://oa.thtzjt.com/web#aciton=4299&model=soong.workflow.record&view_type=list&cids=1-2-3-4&menu_id={menu_id.id}'
|
||||
for rec in record_id.log_ids:
|
||||
if rec.approval_state in ['0', '1']:
|
||||
url=f'https://oa.thtzjt.com/web#aciton=4299&model=soong.workflow.record&view_type=list&cids=1-2-3-4&menu_id={menu_id.id}'
|
||||
url = self.sudo().env["wecom.apps"].generate_oauth_login_url(target_url, rec.done_by.id)
|
||||
description = "标题: {}\n".format(record_id.name) + self.env.user.name + ' ' + str(fields.Datetime.now() + timedelta(hours=8))
|
||||
self.sudo().env["wecom.apps"].sync_send_message_textcard(category='oa', user_id=rec.done_by.employee_id.wecom_userid,
|
||||
self.sudo().env["wecom.apps"].sync_send_message_textcard(category='oa', user_id=rec.done_by.employee_ids.wecom_userid,
|
||||
title=record_id._description + '催办传阅/抄送,请尽快处理',
|
||||
description=description, url=url,
|
||||
btntxt="查看详情")
|
||||
for rec in record_id.log_ids2:
|
||||
if rec.approval_state == '0':
|
||||
url=f'https://oa.thtzjt.com/web#aciton=4299&model=soong.workflow.record&view_type=list&cids=1-2-3-4&menu_id={menu_id.id}'
|
||||
url = self.sudo().env["wecom.apps"].generate_oauth_login_url(target_url, rec.done_by.id)
|
||||
description = "标题: {}\n".format(record_id.name) + self.env.user.name + ' ' + str(fields.Datetime.now() + timedelta(hours=8))
|
||||
self.sudo().env["wecom.apps"].sync_send_message_textcard(category='oa', user_id=rec.done_by.employee_id.wecom_userid,
|
||||
self.sudo().env["wecom.apps"].sync_send_message_textcard(category='oa', user_id=rec.done_by.employee_ids.wecom_userid,
|
||||
title=record_id._description + '催办传阅/抄送,请尽快查看',
|
||||
description=description, url=url,
|
||||
btntxt="查看详情")
|
||||
|
||||
@@ -37,6 +37,12 @@
|
||||
<field name="flow_start"/>
|
||||
<field name="flow_end" invisible="1"/>
|
||||
<field name="is_carbon_copy"/>
|
||||
<field name="user_role_ids" widget="many2many_tags" invisible="not is_carbon_copy"
|
||||
options="{'no_create': True}"/>
|
||||
<field name="users_role_group_ids" widget="many2many_tags" invisible="not is_carbon_copy"
|
||||
options="{'no_create': True}"/>
|
||||
<field name="department1_ids" widget="many2many_tags" invisible="not is_carbon_copy"
|
||||
options="{'no_create': True}"/>
|
||||
<field name="employee_ids" widget="many2many_tags" invisible="not is_carbon_copy"
|
||||
options="{'no_create': True}"/>
|
||||
<field name="is_apostille"/>
|
||||
|
||||
@@ -85,7 +85,7 @@
|
||||
<field name="model">soong.workflow.record</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="流程查询" action="open_record" type="object"
|
||||
decoration-danger="approve_state == 'end'" delete="false"
|
||||
delete="false"
|
||||
create="false" default_order="write_date2 desc, id desc" expand="1" limit="10">
|
||||
<header>
|
||||
<button name="admin_end" type="object" string="管理员强制结束" class="oe_highlight"
|
||||
@@ -133,7 +133,7 @@
|
||||
<field name="model">soong.workflow.record</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="流程查询" type="object"
|
||||
decoration-danger="approve_state == 'end'" delete="false"
|
||||
delete="false"
|
||||
create="false" default_order="write_date2 desc, id desc" expand="1" limit="10">
|
||||
<header>
|
||||
<button name="admin_end" type="object" string="管理员强制结束" class="oe_highlight"
|
||||
@@ -227,7 +227,7 @@
|
||||
<field name="model">soong.workflow.record</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="流程查询" action="open_record" type="object"
|
||||
decoration-danger="approve_state == 'end'" delete="false"
|
||||
delete="false"
|
||||
create="false" default_order="write_date2 desc, id desc" expand="1" limit="10">
|
||||
<header>
|
||||
<button name="admin_end" type="object" string="管理员强制结束" class="oe_highlight"
|
||||
@@ -263,7 +263,7 @@
|
||||
<field name="model">soong.workflow.record</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="流程查询" action="open_record" type="object"
|
||||
decoration-danger="approve_state == 'end'" delete="false"
|
||||
delete="false"
|
||||
create="false" default_order="write_date2 desc, id desc" expand="1" limit="10">
|
||||
<field name="code"/>
|
||||
<field name="workflow_id" string="审批类型"/>
|
||||
@@ -285,7 +285,7 @@
|
||||
<field name="model">soong.workflow.record</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="流程查询" action="open_record" type="object"
|
||||
decoration-danger="approve_state == 'end'" delete="false"
|
||||
delete="false"
|
||||
create="false" default_order="write_date2 desc, id desc" expand="1" limit="10">
|
||||
<header>
|
||||
<button name="admin_end" type="object" string="管理员强制结束" class="oe_highlight"
|
||||
@@ -376,7 +376,7 @@
|
||||
<field name="context">{'header_search_default_approve_state': 1, 'header_search_default_to_do':1,
|
||||
'check_employee': 1}
|
||||
</field>
|
||||
<field name="domain">[('is_to_do', '=', True), ('is_company', '=', True)]</field>
|
||||
<field name="domain">[('is_to_do', '=', True)]</field>
|
||||
<field name="search_view_id" ref="soong_workflow_record_search1"/>
|
||||
<field name="view_id" ref="soong_workflow_record_my_tree"/>
|
||||
</record>
|
||||
|
||||
@@ -43,9 +43,9 @@ class SoongWorkflowCommentWizard(models.TransientModel):
|
||||
)
|
||||
|
||||
employee_trans_domain_ids = fields.Many2many('hr.employee', 'employee_trans_domain_ids_rel', string="传阅人员", store=True)
|
||||
|
||||
filter_personnel_ids = fields.Many2many('hr.employee', 'filter_personnel_rel', 'custom_id', 'employee_id', string="过滤人员", store=True)
|
||||
employee_ids = fields.Many2many('hr.employee', 'employee_ids_rel', 'custom_id', 'employee_id', string="传阅人员", store=True)
|
||||
circulated_employee_ids = fields.Many2many('hr.employee', 'circulated_employee_rel', 'custom_id', 'employee_id', help='只可以看不可以评论,看过以后状态自动变为已阅', string="抄送/传阅人员", store=True)
|
||||
circulated_employee_ids = fields.Many2many('hr.employee', 'circulated_employee_rel', 'custom_id', 'employee_id', help='只可以看不可以评论,看过以后状态自动变为已阅', string="加签人员", store=True)
|
||||
dep_manager_employee_ids = fields.Many2many('hr.employee', 'dep_manager_employee_rel', 'custom_id', 'employee_id', string="承办", store=True)
|
||||
add_employee_ids = fields.Many2many('hr.employee', 'add_employee_rel', 'custom_id', 'employee_id', string="加签人员", help='点击确认以后评论的人员', store=True)
|
||||
supervising_employee_ids = fields.Many2many('hr.employee', 'supervising_employee_rel', 'custom_id', 'employee_id', string="督办人员", store=True)
|
||||
@@ -470,19 +470,19 @@ class SoongWorkflowCommentWizard(models.TransientModel):
|
||||
('name', 'ilike', self.trans_link_id.name),
|
||||
('workflow_id', '=', record_id.workflow_id.id)
|
||||
])
|
||||
|
||||
# 创建下一节点的审批记录
|
||||
self.sudo().env["soong.workflow.log"].create({
|
||||
"code": self.code,
|
||||
"model": self.res_model,
|
||||
"res_id": self.res_id,
|
||||
"node_id": next_node_id.id,
|
||||
"workflow_id": self.node_btn_id.workflow_id.id,
|
||||
"done_by": self.employee_ids.user_id.id if self.employee_ids else query_user_id,
|
||||
"node_btn_name": "0",
|
||||
"lase_click_date": fields.Datetime.now(),
|
||||
"approval_state": "1", # 待审批
|
||||
})
|
||||
for employee_id in self.employee_ids:
|
||||
# 创建下一节点的审批记录
|
||||
self.sudo().env["soong.workflow.log"].create({
|
||||
"code": self.code,
|
||||
"model": self.res_model,
|
||||
"res_id": self.res_id,
|
||||
"node_id": next_node_id.id,
|
||||
"workflow_id": self.node_btn_id.workflow_id.id,
|
||||
"done_by": employee_id.user_id.id if self.employee_ids else query_user_id,
|
||||
"node_btn_name": "3",
|
||||
"lase_click_date": fields.Datetime.now(),
|
||||
"approval_state": "1", # 待审批
|
||||
})
|
||||
|
||||
# 更新业务记录状态
|
||||
related_record = self.env[self.res_model].browse(self.res_id)
|
||||
@@ -490,6 +490,13 @@ class SoongWorkflowCommentWizard(models.TransientModel):
|
||||
user_ids = [employee.user_id.id for employee in self.employee_ids if employee.user_id]
|
||||
related_record.self_users_ids = [(6, 0, user_ids)] if user_ids else [(5,)]
|
||||
record_id.write({'state': self.trans_link_id.name})
|
||||
workflow_record_id = self.env['soong.workflow.record'].sudo().search([('code', '=', record_id.code)])
|
||||
if self.employee_ids:
|
||||
workflow_record_id.users_ids = [(4, employee.user_id.id) for employee in self.employee_ids if
|
||||
employee.user_id]
|
||||
workflow_record_id.employee_ids = self.employee_ids.ids
|
||||
workflow_record_id.approve_state = "pending" # 待审批
|
||||
|
||||
for employee in self.circulated_employee_ids:
|
||||
record_id = self.env[self.res_model].search([('id', '=', self.res_id)])
|
||||
self.sudo().env["soong.workflow.record.copy"].create({
|
||||
@@ -546,7 +553,7 @@ class SoongWorkflowCommentWizard(models.TransientModel):
|
||||
is_work_remind = False
|
||||
current_user = self.env.user
|
||||
current_datetime = fields.Datetime.now()
|
||||
is_admin = current_user.has_group('soong_workflow.group_soong_workflow_manager')
|
||||
is_admin = current_user.has_group('base.group_system')
|
||||
if is_admin:
|
||||
record_id.log_ids[-1].sudo().write({
|
||||
'comment': self.comment,
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
<group invisible="1">
|
||||
<field name="approval_concatenation" widget="radio"/>
|
||||
<field name="invalid_link_ids" widget="many2many_tags"/>
|
||||
<field name="filter_personnel_ids" widget="many2many_tags"/>
|
||||
<field name="user_role_ids"/>
|
||||
<field name="comment"/>
|
||||
</group>
|
||||
@@ -34,7 +35,7 @@
|
||||
<group>
|
||||
<!-- 归档弹窗新增“本公司”勾选,控制抄送人员是否限定当前公司 -->
|
||||
<field name="is_this_company"/>
|
||||
<field name="circulated_employee_ids" widget="many2many_tags" options="{'no_create': True}" domain="[('company_id', '=', company_id)] if is_this_company else []"/>
|
||||
<field name="circulated_employee_ids" widget="many2many_tags" options="{'no_create': True}" domain="[('company_id', '=', company_id), ('id', 'not in' , filter_personnel_ids)] if is_this_company else [('id', 'not in' , filter_personnel_ids)]"/>
|
||||
<field name="document_ids" string="附件" widget="preview_many2many" force_save="1"/>
|
||||
</group>
|
||||
</group>
|
||||
@@ -48,7 +49,7 @@
|
||||
<field name="comment_note" placeholder="请输入意见内容..." style="width: 100%;"/>
|
||||
</div>
|
||||
<group>
|
||||
<field name="circulated_employee_ids" widget="many2many_tags" options="{'no_create': True}" domain = "[('company_id', '=', company_id)]"/>
|
||||
<field name="circulated_employee_ids" widget="many2many_tags" options="{'no_create': True}" domain = "[('company_id', '=', company_id),('id', 'not in' , filter_personnel_ids)]"/>
|
||||
</group>
|
||||
</group>
|
||||
<group invisible="node_btn_name != '确认'">
|
||||
@@ -58,7 +59,7 @@
|
||||
<field name="comment_note" placeholder="请输入意见内容..." style="width: 100%;"/>
|
||||
</div>
|
||||
<group>
|
||||
<field name="circulated_employee_ids" widget="many2many_tags" options="{'no_create': True}" domain = "[('company_id', '=', company_id)]"/>
|
||||
<field name="circulated_employee_ids" widget="many2many_tags" options="{'no_create': True}" domain = "[('company_id', '=', company_id), ('id', 'not in' , filter_personnel_ids)]"/>
|
||||
</group>
|
||||
</group>
|
||||
<group invisible="node_btn_name != '分发'">
|
||||
@@ -93,8 +94,8 @@
|
||||
<field name="is_selection"/>
|
||||
<field name="company_id" invisible="1"/>
|
||||
<field name="employee_ids1" widget="many2many_tags" string="人员范围" invisible="1"/>
|
||||
<field name="employee_ids" widget="many2many_tags" string="人员范围" domain = "[('company_id', '=', company_id), ('id', 'in', employee_ids1)] if is_this_company else [('id', 'in', employee_ids1)]" options="{'no_create': True}"/>
|
||||
<field name="circulated_employee_ids" invisible="node_btn_name == '退回'" widget="many2many_tags" options="{'no_create': True}" domain = "[('company_id', '=', company_id)]"/>
|
||||
<field name="employee_ids" widget="many2many_tags" string="人员范围" domain = "[('company_id', '=', company_id), ('id', 'in', employee_ids1)] if is_this_company else [('id', 'in', employee_ids1)]" options="{'no_create': True}" context="{'tree_view_ref':'yuthon_hr_employee.view_employee_phone_tree'}"/>
|
||||
<field name="circulated_employee_ids" invisible="node_btn_name == '退回'" widget="many2many_tags" options="{'no_create': True}" domain = "[('company_id', '=', company_id),('id', 'not in' , filter_personnel_ids)]"/>
|
||||
<!-- <field name="add_employee_ids" invisible="node_btn_name == '退回'" widget="many2many_tags" options="{'no_create': True}"/>-->
|
||||
</group>
|
||||
</group>
|
||||
@@ -109,7 +110,7 @@
|
||||
<!-- 分发弹窗:本公司开关控制人员筛选范围 -->
|
||||
<field name="is_this_company"/>
|
||||
<field name="dep_manager_employee_ids" widget="many2many_tags" string="承办" options="{'no_create': True}" domain="[('company_id', '=', company_id)] if is_this_company else []"/>
|
||||
<field name="circulated_employee_ids" widget="many2many_tags" options="{'no_create': True}" domain="[('company_id', '=', company_id)] if is_this_company else []"/>
|
||||
<field name="circulated_employee_ids" widget="many2many_tags" options="{'no_create': True}" domain="[('company_id', '=', company_id), ('id', 'not in' , filter_personnel_ids)] if is_this_company else [('id', 'not in' , filter_personnel_ids)]"/>
|
||||
<field name="supervising_employee_ids" widget="many2many_tags" invisible="not is_supervising" options="{'no_create': True}"/>
|
||||
</group>
|
||||
<footer>
|
||||
|
||||
@@ -35,6 +35,8 @@ class IrUiMenu(models.Model):
|
||||
is_phone = fields.Boolean(string="手机菜单",default=False)
|
||||
is_pc = fields.Boolean(string="PC菜单",default=True)
|
||||
|
||||
badge_function = fields.Char(string="菜单角标计算函数")
|
||||
|
||||
@api.model
|
||||
@tools.ormcache_context('self._uid', 'debug', keys=('lang',))
|
||||
def load_menus(self, debug):
|
||||
@@ -43,7 +45,7 @@ class IrUiMenu(models.Model):
|
||||
:return: the menu root
|
||||
:rtype: dict('children': menu_nodes)
|
||||
"""
|
||||
fields = ['name', 'sequence', 'parent_id', 'action', 'web_icon','show_badge_count']
|
||||
fields = ['name', 'sequence', 'parent_id', 'action', 'web_icon','show_badge_count','badge_function']
|
||||
menu_roots = self.get_user_roots()
|
||||
menu_roots_data = menu_roots.read(fields) if menu_roots else []
|
||||
menu_root = {
|
||||
@@ -166,7 +168,10 @@ class IrUiMenu(models.Model):
|
||||
# 不仅仅是 form 表单,获取 model
|
||||
res_model = action_record.res_model
|
||||
if res_model in self.env:
|
||||
if hasattr(self.env[res_model], 'get_menu_badge_count'):
|
||||
# 先检查 menu['badge_function']
|
||||
if 'badge_function' in menu and menu['badge_function'] and hasattr(self.env[res_model], menu['badge_function']):
|
||||
badge_count = getattr(self.env[res_model], menu['badge_function'])(action_record)
|
||||
elif hasattr(self.env[res_model], 'get_menu_badge_count'):
|
||||
badge_count = self.env[res_model].get_menu_badge_count(action_record)
|
||||
elif hasattr(self.env[res_model], 'get_no_read_count'):
|
||||
badge_count = self.env[res_model].get_no_read_count()
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='sequence']" position="after">
|
||||
<field name="show_badge_count"/>
|
||||
<field name="badge_function"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
@@ -111,7 +111,7 @@ class HrContract(models.Model):
|
||||
'target': 'current',
|
||||
'context': {
|
||||
'default_work_end': False,
|
||||
'default_self_users_ids':[(6, 0, [self.employee_id.user_id.id])],
|
||||
'default_self_users_ids':[(6, 0, [self.env.user.id])],
|
||||
'default_contract_id': contract_ids.id,
|
||||
'default_employee_id': contract_ids.employee_id.id
|
||||
}}
|
||||
@@ -172,7 +172,7 @@ class HrContract(models.Model):
|
||||
"default_employee_id": self.employee_id.id,
|
||||
"default_current_status": self.employee_id.contract_id.date_end,
|
||||
"default_department1_id": self.employee_id.department_id.id,
|
||||
"default_renewal_date": self.employee_id.contract_id.date_end + relativedelta(years=3),
|
||||
"default_renewal_date": self.employee_id.contract_id.date_end + relativedelta(years=3) if self.employee_id.contract_id.date_end else False,
|
||||
"default_contract_id": self.id,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -555,7 +555,8 @@ class HolidaysRequestBatch(models.Model):
|
||||
if self.users_id.employee_ids.company_id.name == '集团本部':
|
||||
overtime_records = self.env['vacation.over.line'].search([
|
||||
('employee_id', '=', self.users_id.employee_ids.id),
|
||||
('start_time', '>=', datetime(2026, 1, 1)),
|
||||
('start_time', '>=', datetime(2025, 12, 1)),
|
||||
('is_visible', '=', False),
|
||||
('is_void', '=', False),
|
||||
('text', '!=', '上月继承额度'),
|
||||
], order='start_time ASC')
|
||||
|
||||
@@ -95,6 +95,8 @@
|
||||
'yuthon_hr_attendance/static/src/xml/button_detail_wechat.xml',
|
||||
'yuthon_hr_attendance/static/src/components/*/*.xml',
|
||||
'yuthon_hr_attendance/static/src/css/*.css',
|
||||
'yuthon_hr_attendance/static/src/js/button_vacation_confirm.js',
|
||||
'yuthon_hr_attendance/static/src/xml/button_vacation_confirm.xml',
|
||||
'yuthon_hr_attendance/static/src/js/planning_gantt_renderer.js',
|
||||
'yuthon_hr_attendance/static/src/xml/planning_gantt_renderer.xml',
|
||||
'yuthon_hr_attendance/static/src/js/button_vacation_put.js',
|
||||
|
||||
@@ -90,7 +90,7 @@ class AppletAttendance(Controller):
|
||||
'state_result': 不可打卡原因 'xxx'
|
||||
}
|
||||
"""
|
||||
_logger.info(f"-----{kw}-----------/yuthon/att/today---------接口收到响应-------------------------------")
|
||||
_logger.info(f"------/yuthon/att/today---------接口收到响应-------------------------------")
|
||||
current_datetime = fields.Datetime.now() + timedelta(hours=8)
|
||||
current_date = fields.Date.today() + timedelta(hours=8)
|
||||
|
||||
@@ -205,14 +205,12 @@ class AppletAttendance(Controller):
|
||||
state = 'yes'
|
||||
wifi = kw['wifi_name']
|
||||
_logger.info(f'{wifi}---wifi可以打卡----------')
|
||||
_logger.info(f'{wifi}---wifi可以打卡----------')
|
||||
_logger.info(f'{wifi}---wifi可以打卡----------')
|
||||
if self.is_workday(current_date):
|
||||
attendance_id_class = f"{shift_name} {start_time_str}-{end_time_str}"
|
||||
else:
|
||||
attendance_id_class = f"{shift_name}休息"
|
||||
data = {
|
||||
'location': self_address['formatted_address'],
|
||||
'location': " ",
|
||||
'user_id': user_id.id,
|
||||
'class': f"{attendance_id_class}",
|
||||
'state_result': state_result,
|
||||
@@ -222,7 +220,6 @@ class AppletAttendance(Controller):
|
||||
'check_out_status': check_out_status,
|
||||
'state': state,
|
||||
},
|
||||
_logger.info(f'{data}--{message}-----------')
|
||||
return_data = {'data': data, 'code': '200', 'message': message, }
|
||||
return json.dumps(return_data)
|
||||
|
||||
|
||||
@@ -219,9 +219,14 @@ class OvertimeApprovals(models.Model):
|
||||
if company_id:
|
||||
company = self.env['res.company'].browse(company_id)
|
||||
if company.name == '天河物管':
|
||||
if vals.get('duration'):
|
||||
if (vals['duration']) % 1 != 0.5:
|
||||
raise ValidationError("时长不对!请重新编辑")
|
||||
duration = vals.get('duration')
|
||||
if duration is None:
|
||||
raise ValidationError("时长不能为空!请填写有效的加班时长")
|
||||
if duration < 1:
|
||||
raise ValidationError(f"时长不能小于1!当前填写的是{duration},请重新编辑")
|
||||
if (duration * 2) % 1 != 0:
|
||||
raise ValidationError(
|
||||
f"时长格式错误!请填写1、1.5、2、2.5...等以0.5递增的数值,当前填写的是{duration}")
|
||||
return super(OvertimeApprovals, self).create(vals_list)
|
||||
|
||||
@api.onchange('is_issues')
|
||||
|
||||
@@ -12,8 +12,28 @@ class InheritHrLeave(models.Model):
|
||||
caption = fields.Char(string="标题", compute="_compute_application_reason")
|
||||
employee_id = fields.Many2one('hr.employee', string="发起人")
|
||||
leave_stats = fields.Char(string="请假统计", compute="_compute_leave_stats")
|
||||
is_filing = fields.Boolean(string="归档")
|
||||
application_reason = fields.Char(string="申请原因", compute="_compute_application_reason")
|
||||
|
||||
def confirm(self):
|
||||
now = datetime.now()
|
||||
current_month = now.month
|
||||
current_year = now.year
|
||||
if current_month == 1:
|
||||
last_month = 12
|
||||
last_month_year = current_year - 1
|
||||
else:
|
||||
last_month = current_month - 1
|
||||
last_month_year = current_year
|
||||
last_month_first_day = datetime(last_month_year, last_month, 1)
|
||||
current_month_first_day = datetime(current_year, current_month, 1)
|
||||
valid_leaves = self.filtered(lambda l:
|
||||
fields.Datetime.to_datetime(l.date_from) >= last_month_first_day and
|
||||
fields.Datetime.to_datetime(l.date_from) < current_month_first_day
|
||||
)
|
||||
if valid_leaves:
|
||||
valid_leaves.write({'is_filing': True})
|
||||
|
||||
@api.depends('batch_id', 'hour_batch_id')
|
||||
def _compute_application_reason(self):
|
||||
for rec in self:
|
||||
|
||||
@@ -85,7 +85,7 @@ class YuthonVacationReports(models.Model):
|
||||
|
||||
attendance_message_log_ids = fields.One2many('attendance.message.log', 'vacation_id')
|
||||
admin_confirms = fields.Boolean(string="是否管理员确认")
|
||||
|
||||
is_filing = fields.Boolean(string="归档")
|
||||
vacation_state = fields.Selection(
|
||||
selection=[
|
||||
('not_sent', "未发送"),
|
||||
@@ -961,6 +961,7 @@ class VacationOverline(models.Model):
|
||||
department_id = fields.Many2one('hr.department', related='employee_id.department_id', string="部门")
|
||||
company_id = fields.Many2one('res.company', related='employee_id.company_id', string="公司")
|
||||
workflow_record_id = fields.Many2one('soong.workflow.record', string='流程')
|
||||
is_visible = fields.Boolean(string='是否隐藏')
|
||||
update_type= fields.Selection([('add', '增加'), ('reduce', '减少')], string='更新类型')
|
||||
workday = fields.Float(string='工作日')
|
||||
last_balance = fields.Float(string='上年结余', compute='_compute_last_balance', store=True)
|
||||
@@ -986,7 +987,7 @@ class VacationOverline(models.Model):
|
||||
carry_forward_quantity = fields.Float(string="结转时长")
|
||||
validity = fields.Char(string="有效期")
|
||||
carry_result = fields.Selection([('success', '成功'), ('failed', '失败')], string="结转结果")
|
||||
is_active = fields.Boolean(string="归档")
|
||||
is_filing = fields.Boolean(string="归档")
|
||||
carry_forward_state = fields.Selection([('pending', '待结转'),('completed', '已结转'), ('partial', '部分结转')
|
||||
],string='结转状态',
|
||||
default='pending',
|
||||
@@ -994,7 +995,34 @@ class VacationOverline(models.Model):
|
||||
remaining_quantity = fields.Float(string='剩余数', compute="_compute_remaining_quantity", store=True)
|
||||
|
||||
def confirm(self):
|
||||
pass
|
||||
domain = [
|
||||
'|',
|
||||
['is_initial_number', '=', True],
|
||||
'&',
|
||||
['is_initial_number', '=', False],
|
||||
'&',
|
||||
['update_type', '=', 'add'],
|
||||
'&',
|
||||
['workflow_record_id', '!=', False],
|
||||
'!',
|
||||
'&',
|
||||
['start_time', '>=', '2025-12-01 00:00:00'],
|
||||
['start_time', '<', '2026-01-01 00:00:00']
|
||||
]
|
||||
filtered_records = self.search(domain)
|
||||
if not filtered_records:
|
||||
return
|
||||
records_to_update = filtered_records.filtered(
|
||||
lambda r: fields.float_compare(r.total, r.carry_forward_quantity) == 0
|
||||
)
|
||||
if records_to_update:
|
||||
records_to_update.write({'is_filing': True})
|
||||
|
||||
def update_is_visible(self):
|
||||
over_ids = self.env["vacation.over.line"].browse(
|
||||
self._context.get('active_ids', self._context.get('active_id')))
|
||||
for rec in over_ids:
|
||||
rec.is_visible = True
|
||||
|
||||
def update_line(self):
|
||||
over_ids = self.env["vacation.over.line"].browse(
|
||||
@@ -1181,9 +1209,15 @@ class VacationMealLine(models.Model):
|
||||
_description = '误餐明细'
|
||||
|
||||
vacation_id = fields.Many2one('yuthon.vacation.reports', string='关联假期额度记录')
|
||||
employee_id = fields.Many2one('hr.employee', string='员工')
|
||||
is_filing = fields.Boolean(string="归档")
|
||||
employee_id = fields.Many2one('hr.employee', string='员工', compute='_compute_employee_id', store=True)
|
||||
department_id = fields.Many2one('hr.department', related='employee_id.department_id', string="部门", store=True)
|
||||
company_id = fields.Many2one('res.company', related='employee_id.company_id', string="公司", store=True)
|
||||
workflow_record_id = fields.Many2one('soong.workflow.record', string='流程')
|
||||
vacation_date = fields.Date(related='vacation_id.vacation_date', string='误餐日期')
|
||||
update_type= fields.Selection([('add', '增加'), ('reduce', '减少')], string='更新类型')
|
||||
meal_name = fields.Char(string="标题", compute="_compute_overtime", store=True)
|
||||
start_date = fields.Date(string="日期", compute="_compute_overtime", store=True)
|
||||
update_date = fields.Date(string='更新时间')
|
||||
update_lines = fields.Float(string='更新额度')
|
||||
is_soon = fields.Boolean(string="流程修改")
|
||||
@@ -1194,6 +1228,58 @@ class VacationMealLine(models.Model):
|
||||
text = fields.Text(string='备注')
|
||||
is_confirm = fields.Boolean(string="是否确认")
|
||||
|
||||
def update_line(self):
|
||||
meal_ids = self.env["vacation.meal.line"].browse(
|
||||
self._context.get('active_ids', self._context.get('active_id')))
|
||||
for rec in meal_ids:
|
||||
rec._compute_employee_id()
|
||||
rec._compute_overtime()
|
||||
|
||||
def confirm(self):
|
||||
now = datetime.now()
|
||||
current_month = now.month
|
||||
current_year = now.year
|
||||
if current_month == 1:
|
||||
last_month = 12
|
||||
last_month_year = current_year - 1
|
||||
else:
|
||||
last_month = current_month - 1
|
||||
last_month_year = current_year
|
||||
last_month_first_day = datetime(last_month_year, last_month, 1)
|
||||
current_month_first_day = datetime(current_year, current_month, 1)
|
||||
valid_leaves = self.filtered(lambda l:
|
||||
fields.Datetime.to_datetime(l.start_date) >= last_month_first_day and
|
||||
fields.Datetime.to_datetime(l.start_date) < current_month_first_day
|
||||
)
|
||||
if valid_leaves:
|
||||
valid_leaves.write({'is_filing': True})
|
||||
|
||||
@api.depends('workflow_record_id')
|
||||
def _compute_overtime(self):
|
||||
for rec in self:
|
||||
rec.start_date = False
|
||||
res_model = rec.workflow_record_id.res_model
|
||||
res_id = rec.workflow_record_id.res_id
|
||||
if res_model and res_id and res_model in self.env:
|
||||
if res_model == 'overtime.approval':
|
||||
overtime_id = rec.env[res_model].search([('id', '=', res_id)], limit=1)
|
||||
if overtime_id:
|
||||
rec.start_date = overtime_id.start_time.date() or ''
|
||||
else:
|
||||
go_approval_id = rec.env[res_model].search([('id', '=', res_id)], limit=1)
|
||||
if go_approval_id:
|
||||
rec.start_date = go_approval_id.date_start.date() or ''
|
||||
rec.meal_name = rec.workflow_record_id.name or ''
|
||||
|
||||
|
||||
@api.depends('vacation_id')
|
||||
def _compute_employee_id(self):
|
||||
for rec in self:
|
||||
if rec.vacation_id:
|
||||
rec.employee_id = rec.vacation_id.employee_id.id
|
||||
elif not rec.employee_id:
|
||||
rec.employee_id = False
|
||||
|
||||
def action_void(self):
|
||||
self.is_void = True
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
/** @odoo-module */
|
||||
import { ListController } from "@web/views/list/list_controller";
|
||||
import { registry } from '@web/core/registry';
|
||||
import { listView } from '@web/views/list/list_view';
|
||||
|
||||
export class CustomAttendanceVacationConfirmController extends ListController {
|
||||
setup() {
|
||||
super.setup();
|
||||
}
|
||||
|
||||
async getAction(lock_type) {
|
||||
const action = await this.orm.call('vacation.over.line', 'confirm', [lock_type], {context: this.props.context});
|
||||
action["context"] = {
|
||||
...action.context,
|
||||
...this.model.root.context,
|
||||
}
|
||||
return action
|
||||
}
|
||||
}
|
||||
|
||||
registry.category("views").add("custom_vacation_confirm_button", {
|
||||
...listView,
|
||||
Controller: CustomAttendanceVacationConfirmController,
|
||||
buttonTemplate: "vacation_confirm_module.ListView.Buttons",
|
||||
});
|
||||
@@ -9,6 +9,19 @@ export class CustomAttendanceVacationPutController extends ListController {
|
||||
|
||||
}
|
||||
openHrLeaveWizard() {
|
||||
const now = new Date();
|
||||
const lastMonthYear = now.getMonth() === 0 ? now.getFullYear() - 1 : now.getFullYear();
|
||||
const lastMonth = now.getMonth() === 0 ? 11 : now.getMonth() - 1;
|
||||
const lastMonthFirstDay = new Date(lastMonthYear, lastMonth, 1);
|
||||
const currentMonthFirstDay = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||
const formatDate = (date) => {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
return `${year}-${month}-${day} 00:00:00`;
|
||||
};
|
||||
const lastMonthStart = formatDate(lastMonthFirstDay);
|
||||
const lastMonthEnd = formatDate(currentMonthFirstDay);
|
||||
this.actionService.doAction({
|
||||
type: 'ir.actions.act_window',
|
||||
res_model: 'hr.leave',
|
||||
@@ -18,6 +31,11 @@ export class CustomAttendanceVacationPutController extends ListController {
|
||||
views: [[false, 'tree']],
|
||||
target: 'new', // 弹窗形式打开
|
||||
context: {'tree_view_ref': "yuthon_hr_attendance.view_hr_leave_statement_tree"},
|
||||
domain: [
|
||||
'&',
|
||||
['date_from', '>=', lastMonthStart],
|
||||
['date_from', '<', lastMonthEnd]
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -33,17 +51,14 @@ export class CustomAttendanceVacationPutController extends ListController {
|
||||
context: {'tree_view_ref': "yuthon_hr_attendance.view_yuthon_vacation_overtime_briefly_tree"},
|
||||
domain: [
|
||||
'|',
|
||||
// 条件1:is_initial_number=true → 无视时间,直接显示
|
||||
['is_initial_number', '=', true],
|
||||
// 条件组2:is_initial_number=false + add + workflow_record_id≠空 + 非12月
|
||||
'&', // 且:作用于「条件2」和「运算符&的结果」
|
||||
'&',
|
||||
['is_initial_number', '=', false],
|
||||
'&', // 且:作用于「条件3」和「运算符&的结果」
|
||||
'&',
|
||||
['update_type', '=', 'add'],
|
||||
'&', // 且:作用于「条件4」和「运算符!的结果」
|
||||
'&',
|
||||
['workflow_record_id', '!=', false],
|
||||
// 时间过滤:只作用于第二类记录
|
||||
'!', // 取反:不是2025年12月
|
||||
'!',
|
||||
'&',
|
||||
['start_time', '>=', '2025-12-01 00:00:00'],
|
||||
['start_time', '<', '2026-01-01 00:00:00']
|
||||
@@ -52,16 +67,35 @@ export class CustomAttendanceVacationPutController extends ListController {
|
||||
}
|
||||
|
||||
openYuthonVacationWizard() {
|
||||
const now = new Date();
|
||||
const lastMonthYear = now.getMonth() === 0 ? now.getFullYear() - 1 : now.getFullYear();
|
||||
const lastMonth = now.getMonth() === 0 ? 11 : now.getMonth() - 1;
|
||||
const lastMonthFirstDay = new Date(lastMonthYear, lastMonth, 1);
|
||||
const currentMonthFirstDay = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||
const formatDate = (date) => {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
return `${year}-${month}-${day} 00:00:00`;
|
||||
};
|
||||
const lastMonthStart = formatDate(lastMonthFirstDay);
|
||||
const lastMonthEnd = formatDate(currentMonthFirstDay);
|
||||
this.actionService.doAction({
|
||||
type: 'ir.actions.act_window',
|
||||
res_model: 'yuthon.vacation.reports',
|
||||
res_model: 'vacation.meal.line',
|
||||
name: '误餐情况',
|
||||
view_mode: 'tree',
|
||||
view_type: 'form',
|
||||
views: [[false, 'tree']],
|
||||
target: 'new',
|
||||
context: {'tree_view_ref': "yuthon_hr_attendance.view_yuthon_vacation_meal_tree"},
|
||||
domain: [['meal_fee', '!=', 0]]
|
||||
context: {'tree_view_ref': "yuthon_hr_attendance.view_vacation_meal_line_tree",
|
||||
'group_by': ['employee_id'],
|
||||
},
|
||||
domain: [
|
||||
'&',
|
||||
['start_date', '>=', lastMonthStart],
|
||||
['start_date', '<', lastMonthEnd]
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<templates>
|
||||
<t t-name="vacation_confirm_module.ListView.Buttons" t-inherit="web.ListView.Buttons">
|
||||
<xpath expr="//div[hasclass('o_list_buttons')]" position="after">
|
||||
<button type="button" class="btn btn-primary" style="margin-left: 10px;" t-on-click="getAction">
|
||||
确认
|
||||
</button>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -4,17 +4,18 @@
|
||||
<field name="name">hr.leave.statement.tree</field>
|
||||
<field name="model">hr.leave</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="请假情况" editable="bottom" limit="15">
|
||||
<tree string="请假情况" editable="bottom" limit="15" create="false" delete="false">
|
||||
<field name="caption"/>
|
||||
<field name="employee_id"/>
|
||||
<field name="employee_company_id"/>
|
||||
<field name="holiday_status_id"/>
|
||||
<field name="date_from"/>
|
||||
<field name="date_from" groups="base.group_system"/>
|
||||
<field name="date_to"/>
|
||||
<field name="number_of_days" string="请假时长(天)"/>
|
||||
<field name="leave_stats"/>
|
||||
<field name="application_reason"/>
|
||||
<field name="state"/>
|
||||
<field name="is_filing" column_invisibe="1"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
@@ -40,6 +41,16 @@
|
||||
<field name="name">请假情况</field>
|
||||
<field name="res_model">hr.leave</field>
|
||||
<field name="view_mode">tree,search</field>
|
||||
<field name="domain">[('is_filing','=', False)]</field>
|
||||
<field name="context">{'search_default_now_month': 1, 'search_default_group_by_employee_id':1}</field>
|
||||
<field name="view_id" ref="view_hr_leave_statement_tree"/>
|
||||
<field name="search_view_id" ref="view_hr_leave_statement_search"/>
|
||||
</record>
|
||||
<record id="view_yuthon_holiday_statement_active_action" model="ir.actions.act_window">
|
||||
<field name="name">请假归档</field>
|
||||
<field name="res_model">hr.leave</field>
|
||||
<field name="view_mode">tree,search</field>
|
||||
<field name="domain">[('is_filing','=', True)]</field>
|
||||
<field name="context">{'search_default_now_month': 1, 'search_default_group_by_employee_id':1}</field>
|
||||
<field name="view_id" ref="view_hr_leave_statement_tree"/>
|
||||
<field name="search_view_id" ref="view_hr_leave_statement_search"/>
|
||||
|
||||
@@ -1,15 +1,36 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<record id="view_yuthon_vacation_meal_tree" model="ir.ui.view">
|
||||
<field name="name">yuthon.vacation.reports.tree</field>
|
||||
<field name="model">yuthon.vacation.reports</field>
|
||||
<record id="view_vacation_meal_line_tree" model="ir.ui.view">
|
||||
<field name="name">vacation.meal.line.tree</field>
|
||||
<field name="model">vacation.meal.line</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="误餐情况">
|
||||
<tree string="误餐情况" create="false" delete="false">
|
||||
<field name="employee_id"/>
|
||||
<field name="meal_fee"/>
|
||||
<field name="meal_name"/>
|
||||
<field name="start_date" column_invisible="1"/>
|
||||
<field name="update_lines"/>
|
||||
<field name="is_filing" column_invisible="1"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
<record id="view_yuthon_vacation_meal_search" model="ir.ui.view">
|
||||
<field name="name">vacation.meal.line.search</field>
|
||||
<field name="model">vacation.meal.line</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="误餐情况">
|
||||
<field name="employee_id"/>
|
||||
<field name="department_id"/>
|
||||
<filter string="员工" name="group_by_employee_id" domain="[]" context="{'group_by': 'employee_id'}"/>
|
||||
<filter name="now_month" string="当月" domain="[('vacation_date', '>=', (context_today() + relativedelta(day=1)).strftime('%Y-%m-%d')),
|
||||
('vacation_date', '<', (context_today() + relativedelta(months=+1, day=1)).strftime('%Y-%m-%d'))]"/>
|
||||
<header_search class="header-search-inline">
|
||||
<field name="vacation_date"/>
|
||||
<field name="employee_id"/>
|
||||
<field name="department_id"/>
|
||||
</header_search>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
<record id="view_yuthon_vacation_reports_tree" model="ir.ui.view">
|
||||
<field name="name">yuthon.vacation.reports.tree</field>
|
||||
<field name="model">yuthon.vacation.reports</field>
|
||||
@@ -518,7 +539,7 @@
|
||||
<field name="name">vacation.over.line.overtime.briefly.tree</field>
|
||||
<field name="model">vacation.over.line</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="加班情况弹窗" editable="bottom">
|
||||
<tree string="加班情况弹窗" editable="bottom" js_class="custom_vacation_confirm_button" create="0">
|
||||
<field name="employee_id"/>
|
||||
<field name="last_balance" groups="yuthon_salary.group_lc_user,yuthon_salary.group_wg_user"/>
|
||||
<field name="valid_date" groups="yuthon_salary.group_lc_user,yuthon_salary.group_wg_user"/>
|
||||
@@ -526,6 +547,7 @@
|
||||
<field name="public_holidays"/>
|
||||
<field name="total"/>
|
||||
<field name="carry_forward_quantity"/>
|
||||
<field name="carry_forward_date"/>
|
||||
<field name="remaining_quantity"/>
|
||||
</tree>
|
||||
</field>
|
||||
@@ -540,7 +562,8 @@
|
||||
<field name="department_id"/>
|
||||
<field name="carry_forward_quantity"/>
|
||||
<field name="carry_forward_date"/>
|
||||
<field name="is_active"/>
|
||||
<field name="is_filing" column_invisible="1"/>
|
||||
<field name="is_visible" column_invisible="1"/>
|
||||
<field name="carry_result"/>
|
||||
</tree>
|
||||
</field>
|
||||
@@ -560,6 +583,7 @@
|
||||
<field name="carry_forward_date"/>
|
||||
<field name="carry_forward_state"/>
|
||||
<field name="remaining_quantity"/>
|
||||
<field name="is_visible" column_invisible="1"/>
|
||||
<field name="validity"/>
|
||||
</tree>
|
||||
</field>
|
||||
@@ -588,7 +612,7 @@
|
||||
<field name="name">加班情况</field>
|
||||
<field name="res_model">vacation.over.line</field>
|
||||
<field name="view_mode">tree</field>
|
||||
<field name="domain">['|', ('workflow_record_id','!=', False), ('is_initial_number', '=', True), ('update_type', '=', 'add')]</field>
|
||||
<field name="domain">['|', ('workflow_record_id','!=', False), ('is_initial_number', '=', True), ('update_type', '=', 'add'), ('is_filing','=', False), ('is_visible', '=', False)]</field>
|
||||
<field name="context">{'search_default_now_month': 1, 'search_default_group_by_employee_id':1}</field>
|
||||
<field name="view_id" ref="view_yuthon_vacation_overtime_tree"/>
|
||||
<field name="search_view_id" ref="view_yuthon_vacation_overtime_search"/>
|
||||
@@ -597,7 +621,7 @@
|
||||
<field name="name">加班情况(归档)</field>
|
||||
<field name="res_model">vacation.over.line</field>
|
||||
<field name="view_mode">tree</field>
|
||||
<field name="domain">['|', ('workflow_record_id','!=', False), ('is_initial_number', '=', True), ('update_type', '=', 'add'), ('is_active', '=', True)]</field>
|
||||
<field name="domain">['|', ('workflow_record_id','!=', False), ('is_initial_number', '=', True), ('update_type', '=', 'add'), ('is_filing','=', True), ('is_visible', '=', False)]</field>
|
||||
<field name="context">{'search_default_group_by_employee_id':1}</field>
|
||||
<field name="view_id" ref="view_yuthon_vacation_overtime_active_tree"/>
|
||||
<field name="search_view_id" ref="view_yuthon_vacation_overtime_search"/>
|
||||
@@ -609,20 +633,55 @@
|
||||
<field name="state">code</field>
|
||||
<field name="code">action = model.update_line()</field>
|
||||
</record>
|
||||
<record id="update_is_visible" model="ir.actions.server">
|
||||
<field name="name">一次性</field>
|
||||
<field name="model_id" ref="yuthon_hr_attendance.model_vacation_over_line"/>
|
||||
<field name="binding_model_id" ref="yuthon_hr_attendance.model_vacation_over_line"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">action = model.is_visible()</field>
|
||||
</record>
|
||||
<record id="view_yuthon_vacation_meal_action" model="ir.actions.act_window">
|
||||
<field name="name">误餐情况</field>
|
||||
<field name="res_model">yuthon.vacation.reports</field>
|
||||
<field name="res_model">vacation.meal.line</field>
|
||||
<field name="view_mode">tree</field>
|
||||
<field name="domain">[('meal_fee', '!=', 0)]</field>
|
||||
<field name="domain">[('is_filing','=', False)]</field>
|
||||
<field name="context">{'search_default_now_month': 1, 'search_default_group_by_employee_id':1}</field>
|
||||
<field name="search_view_id" ref="view_yuthon_vacation_reports_search"/>
|
||||
<field name="search_view_id" ref="view_yuthon_vacation_meal_search"/>
|
||||
<field name="view_ids" eval="[(5, 0, 0),
|
||||
(0, 0, {'view_mode': 'tree', 'view_id': ref('yuthon_hr_attendance.view_yuthon_vacation_meal_tree')}),]"/>
|
||||
(0, 0, {'view_mode': 'tree', 'view_id': ref('yuthon_hr_attendance.view_vacation_meal_line_tree')}),]"/>
|
||||
</record>
|
||||
<record id="update_vacation_meal_line" model="ir.actions.server">
|
||||
<field name="name">触发计算逻辑</field>
|
||||
<field name="model_id" ref="yuthon_hr_attendance.model_vacation_meal_line"/>
|
||||
<field name="binding_model_id" ref="yuthon_hr_attendance.model_vacation_meal_line"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">action = model.update_line()</field>
|
||||
</record>
|
||||
<record id="view_yuthon_vacation_meal_active_action" model="ir.actions.act_window">
|
||||
<field name="name">误餐归档</field>
|
||||
<field name="res_model">vacation.meal.line</field>
|
||||
<field name="view_mode">tree</field>
|
||||
<field name="domain">[('is_filing','=', True)]</field>
|
||||
<field name="context">{'search_default_now_month': 1, 'search_default_group_by_employee_id':1}</field>
|
||||
<field name="search_view_id" ref="view_yuthon_vacation_meal_search"/>
|
||||
<field name="view_ids" eval="[(5, 0, 0),
|
||||
(0, 0, {'view_mode': 'tree', 'view_id': ref('yuthon_hr_attendance.view_vacation_meal_line_tree')}),]"/>
|
||||
</record>
|
||||
<record id="view_yuthon_vacation_reports_action" model="ir.actions.act_window">
|
||||
<field name="name">考勤确认单台账</field>
|
||||
<field name="res_model">yuthon.vacation.reports</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="domain">[('is_filing','=', False)]</field>
|
||||
<field name="context">{'search_default_now_month': 1, 'search_default_group_by_employee_id':1}</field>
|
||||
<field name="view_ids" eval="[(5, 0, 0),
|
||||
(0, 0, {'view_mode': 'tree', 'view_id': ref('yuthon_hr_attendance.view_yuthon_vacation_reports_tree')}),
|
||||
(0, 0, {'view_mode': 'form', 'view_id': ref('yuthon_hr_attendance.view_yuthon_vacation_reports_form')})]"/>
|
||||
</record>
|
||||
<record id="view_yuthon_vacation_reports_active_action" model="ir.actions.act_window">
|
||||
<field name="name">考勤确认单归档</field>
|
||||
<field name="res_model">yuthon.vacation.reports</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="domain">[('is_filing','=', True)]</field>
|
||||
<field name="context">{'search_default_now_month': 1, 'search_default_group_by_employee_id':1}</field>
|
||||
<field name="view_ids" eval="[(5, 0, 0),
|
||||
(0, 0, {'view_mode': 'tree', 'view_id': ref('yuthon_hr_attendance.view_yuthon_vacation_reports_tree')}),
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
'data/employee_recruitment_time_data.xml',
|
||||
'data/publish_applicant_setting_data.xml',
|
||||
'security/ir.model.access.csv',
|
||||
'security/company_rule.xml',
|
||||
'views/yuthon_hr_employee_views.xml',
|
||||
'views/yuthon_hr_department_views.xml',
|
||||
'views/applicant_tage_setting_views.xml',
|
||||
|
||||
@@ -590,6 +590,17 @@
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
<!--手机转交-->
|
||||
<record id="view_employee_phone_tree" model="ir.ui.view">
|
||||
<field name="name">hr.employee.member</field>
|
||||
<field name="model">hr.employee</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="人员" create="false" delete="false" editable="bottom">
|
||||
<field name="name" readonly="1"/>
|
||||
<field name="department_id" readonly="1" options='{"no_open": True, "no_create": True}'/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
<record id="view_employee_inherit_filter" model="ir.ui.view">
|
||||
<field name="name">hr.employee.inherit.search</field>
|
||||
<field name="model">hr.employee</field>
|
||||
|
||||
@@ -80,34 +80,20 @@
|
||||
<field name="model">issue.approval.decision</field>
|
||||
<field name="arch" type="xml">
|
||||
<search>
|
||||
<searchpanel>
|
||||
<field name="decision_type_on_id"/>
|
||||
</searchpanel>
|
||||
<group expand="1">
|
||||
<filter name="group_decision_type_on_id" string="事项类别上级"
|
||||
context="{'group_by': 'decision_type_on_id'}"/>
|
||||
<filter name="group_decision_type_id" string="事项类别" context="{'group_by': 'decision_type_id'}"/>
|
||||
<filter string="适用范围" name="group_scope" context="{'group_by': 'scope'}"/>
|
||||
<filter name="group_management_matters_id" string="管理事项" context="{'group_by': 'management_matters_id'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="issue_approval_decision_search" model="ir.ui.view">
|
||||
<field name="name">issue.approval.decision.search</field>
|
||||
<field name="model">issue.approval.decision</field>
|
||||
<field name="priority">20</field>
|
||||
<field name="arch" type="xml">
|
||||
<search>
|
||||
<field name="specifics"/>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="issue_approval_decision_action" model="ir.actions.act_window">
|
||||
<field name="name">决策清单</field>
|
||||
<field name="res_model">issue.approval.decision</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="view_mode">tree,form,search</field>
|
||||
<field name="context">{'group_decision_type_id': 1, 'group_management_matters_id': 1}</field>
|
||||
</record>
|
||||
<record id="all_update_issue" model="ir.actions.server">
|
||||
|
||||
@@ -198,7 +198,7 @@
|
||||
<tree editable="bottom">
|
||||
<field name="ordinal"/>
|
||||
<field name="issue_library_id" column_invisible="1"/>
|
||||
<field name="issue_approval_decision_id" class="list_ids_width"/>
|
||||
<field name="issue_approval_decision_id" class="list_ids_width" context="{'search_default_group_scope': 1}"/>
|
||||
<field name="council_id" readonly="1" groups="yuthon_salary.group_jtbb_user" string="党总支"/>
|
||||
<field name="council_id" readonly="1" groups="yuthon_salary.group_wg_user, yuthon_salary.group_lc_user" string="党支部"/>
|
||||
<field name="team_meet_id" readonly="1"/>
|
||||
@@ -408,7 +408,7 @@
|
||||
<field name="issue_library_line_ids">
|
||||
<tree editable="bottom">
|
||||
<field name="issue_library_id" column_invisible="1"/>
|
||||
<field name="issue_approval_decision_id" class="list_ids_width"/>
|
||||
<field name="issue_approval_decision_id" class="list_ids_width" context="{'search_default_group_scope': 1}"/>
|
||||
<field name="council_id" readonly="1"/>
|
||||
<field name="district_state_id" readonly="1"/>
|
||||
<field name="district_government_id" readonly="1"/>
|
||||
@@ -565,7 +565,7 @@
|
||||
<tree editable="bottom">
|
||||
<field name="ordinal"/>
|
||||
<field name="issue_library_id" column_invisible="1"/>
|
||||
<field name="issue_approval_decision_id" class="list_ids_width"/>
|
||||
<field name="issue_approval_decision_id" class="list_ids_width" context="{'search_default_group_scope': 1}"/>
|
||||
<field name="team_meet_id" readonly="1"/>
|
||||
<field name="district_state_id" readonly="1"/>
|
||||
<field name="district_government_id" readonly="1"/>
|
||||
@@ -712,7 +712,7 @@
|
||||
<tree editable="bottom">
|
||||
<field name="ordinal"/>
|
||||
<field name="issue_library_id" column_invisible="1"/>
|
||||
<field name="issue_approval_decision_id" class="list_ids_width"/>
|
||||
<field name="issue_approval_decision_id" class="list_ids_width" context="{'search_default_group_scope': 1}"/>
|
||||
<field name="board_meet_id" readonly="1"/>
|
||||
<field name="district_state_id" readonly="1"/>
|
||||
<field name="district_government_id" readonly="1"/>
|
||||
|
||||
@@ -39,7 +39,6 @@
|
||||
<button name="open_record" type="object" string="详情" class="btn-primary"/>
|
||||
<field name="party_approval_id" column_invisible="1"/>
|
||||
<field name="issue_id" column_invisible="1"/>
|
||||
<button name="open_record" type="object" string="详情" class="btn-primary"/>
|
||||
<field name="name"/>
|
||||
<field name="will_based"/>
|
||||
<field name="department_id"/>
|
||||
|
||||
@@ -100,7 +100,7 @@ class YuthonOfficial(models.Model):
|
||||
'state': '申请人',
|
||||
'code': self.env['ir.sequence'].next_by_code('sub_enterprises_code'),
|
||||
'official_partner_id': official_partner_id.id,
|
||||
'reason_application': 'main_text',
|
||||
'reason_application': self.main_text,
|
||||
'document_ids': [(6, 0, all_document_ids)] if all_document_ids else False
|
||||
})
|
||||
workflow_id = self.env['soong.workflow.record'].search([('code', '=', self.code)], limit=1).id
|
||||
|
||||
@@ -43,6 +43,7 @@ class YouthonReport extends Component {
|
||||
localeText: AG_GRID_LOCALE_CN,
|
||||
theme:agGrid.themeMaterial,
|
||||
columnDefs: this.config.columns || [],
|
||||
groupDefaultExpanded:1, // 分组展开
|
||||
rowData: [],
|
||||
sideBar: true,
|
||||
defaultColDef: { sortable: true, filter: true, width: 200,minWidth: 150,flex: false, resizable: false },
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<div class="row g-4">
|
||||
|
||||
<!-- 未处理违约金 -->
|
||||
<div class="col-md-4">
|
||||
<div class="col-md-4">
|
||||
<div class="stat-card danger bg-white">
|
||||
<div class="stat-label mb-2">未处理违约金</div>
|
||||
<div class="stat-value text-dark">¥245,890.00</div>
|
||||
@@ -19,14 +19,14 @@
|
||||
|
||||
<!-- 已收回违约金 -->
|
||||
<div class="col-md-4">
|
||||
<div class="stat-card success bg-white">
|
||||
<div class="stat-card success bg-white">
|
||||
<div class="stat-label mb-2">已收回违约金</div>
|
||||
<div class="stat-value text-dark">¥1,320,560.00</div>
|
||||
<div class="stat-change text-success mt-1">
|
||||
▲ 8.2% <small class="text-muted">较上月</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 违约合同数 -->
|
||||
<div class="col-md-4">
|
||||
@@ -111,6 +111,15 @@
|
||||
<button class="btn btn-success" t-on-click="onExportExcel">导出</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--表 标题 居中 -->
|
||||
<div class="row mb-2">
|
||||
<div class="col-12 text-center">
|
||||
<h5>报表名称</h5>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
{"headerName": "申请原因", "field": "申请原因", "flex": 1},
|
||||
{"headerName": "发起时间", "field": "发起时间", "flex": 1},
|
||||
{"headerName": "最后操作时间", "field": "最后操作时间", "flex": 1},
|
||||
{"headerName": "审批状态", "field": "审批状态", "flex": 1}
|
||||
{"headerName": "审批状态", "field": "审批状态", "flex": 1,'rowGroup':true}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,7 @@ from odoo.exceptions import ValidationError, UserError
|
||||
from pypinyin import pinyin, Style
|
||||
import xlrd
|
||||
import base64
|
||||
import datetime
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import datetime, timedelta, date
|
||||
import calendar
|
||||
|
||||
|
||||
@@ -73,9 +72,11 @@ class SalaryImportWizard(models.TransientModel):
|
||||
('employee_id', '=', employee.id),
|
||||
], limit=1)
|
||||
if not payslip:
|
||||
date_from = datetime.date(self.select_year, self.select_month, 1)
|
||||
last_day = calendar.monthrange(self.select_year, self.select_month)[1]
|
||||
date_to = datetime.date(self.select_year, self.select_month, last_day)
|
||||
year = int(self.select_year)
|
||||
month = int(self.select_month)
|
||||
date_from = date(year, month, 1)
|
||||
last_day = calendar.monthrange(year, month)[1]
|
||||
date_to = date(year, month, last_day)
|
||||
payslip_vals = {
|
||||
'name': employee.name,
|
||||
'employee_id': employee.id,
|
||||
@@ -95,6 +96,7 @@ class SalaryImportWizard(models.TransientModel):
|
||||
current_payslip_lines = payslip.line_ids.filtered(lambda l: l.name)
|
||||
current_valid_names = {line.name.strip() for line in current_payslip_lines}
|
||||
global_valid_line_names.update(current_valid_names)
|
||||
payslip.compute_sheet()
|
||||
|
||||
for line in payslip.line_ids:
|
||||
line_name = line.name
|
||||
|
||||
@@ -711,6 +711,35 @@
|
||||
action="yuthon_performance.yuthon_employee_approval_action1"
|
||||
sequence="7"/>
|
||||
|
||||
<menuitem id="yuthon_vacation_active_menu"
|
||||
parent="human_resources_menu_root_board"
|
||||
name="归档"
|
||||
sequence="99"/>
|
||||
<menuitem id="yuthon_vacation_report_active_menu"
|
||||
parent="yuthon_vacation_active_menu"
|
||||
name="考勤确认单归档"
|
||||
action="yuthon_hr_attendance.view_yuthon_vacation_reports_active_action"
|
||||
sequence="1"
|
||||
/>
|
||||
<menuitem id="yuthon_holiday_active_menu"
|
||||
parent="yuthon_vacation_active_menu"
|
||||
name="请假归档"
|
||||
action="yuthon_hr_attendance.view_yuthon_holiday_statement_active_action"
|
||||
sequence="10"
|
||||
/>
|
||||
<menuitem id="yuthon_overtime_active_menu"
|
||||
parent="yuthon_vacation_active_menu"
|
||||
name="加班归档"
|
||||
action="yuthon_hr_attendance.action_overtime_approval_active_report"
|
||||
sequence="20"
|
||||
/>
|
||||
<menuitem id="yuthon_menu_active_menu"
|
||||
parent="yuthon_vacation_active_menu"
|
||||
name="误餐归档"
|
||||
action="yuthon_hr_attendance.view_yuthon_vacation_meal_active_action"
|
||||
sequence="30"
|
||||
/>
|
||||
|
||||
<!--我的-->
|
||||
<menuitem id="holiday_manage_menu_root_board" name="我的" web_icon="yuthon_work_brand,static/description/holiday.png" sequence="3">
|
||||
<menuitem id="menu_workflow_approval_request_new" name="新建流程" action="soong_workflow.action_kklw_workflow_approval_request" sequence="5"/>
|
||||
|
||||
Reference in New Issue
Block a user