fix:解决冲突

This commit is contained in:
pengyuthon
2025-09-16 07:00:57 +00:00
41 changed files with 697 additions and 29 deletions
+1 -1
View File
@@ -144,4 +144,4 @@ dmypy.json
# Cython debug symbols
cython_debug/
.DS_Store
.DS_Store
BIN
View File
Binary file not shown.
+2
View File
@@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
+35
View File
@@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
{
'name': 'Collapsible Notebook',
'version': '17.0.1.0.0',
'category': 'Productivity',
'summary': '支持折叠和展开功能的Notebook组件',
'description': """
Collapsible Notebook
===================
这个模块提供了一个支持折叠和展开功能的Notebook组件。
主要功能:
* 可折叠的notebook页面
* 展开/折叠状态记忆
* 美观的UI设计
* 响应式布局
使用方法:
在XML视图中使用 <notebook class="o_collapsible_notebook"> 来启用折叠功能。
""",
'author': 'TX Team',
'website': 'https://www.tx.com',
'depends': ['base', 'web', 'product'],
'assets': {
'web.assets_backend': [
'collapsible_notebook/static/src/css/collapsible_notebook.css',
'collapsible_notebook/static/src/js/collapsible_notebook.js',
],
},
'installable': True,
'auto_install': False,
'application': False,
'license': 'LGPL-3',
}
@@ -0,0 +1,241 @@
/* 可折叠Notebook样式 */
/* 整个Notebook折叠按钮样式 */
.o_notebook_collapse_btn {
display: inline-block;
/* margin-right: 10px; */
/* margin-bottom: 5px; */
/* padding: 6px; */
background-color: transparent;
color: #000;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
font-weight: 500;
transition: all 0.2s ease;
user-select: none;
vertical-align: top;
line-height: 1.4;
width: 30px;
height: 30px;
text-align: center;
}
/* 确保按钮和标签页在同一行 */
.o_notebook_collapse_btn + .nav-tabs {
display: inline-block;
vertical-align: top;
width: calc(100% - 40px); /* 为按钮留出空间 */
}
/* 为包含按钮的容器设置样式 */
.o_notebook .o_notebook_collapse_btn {
float: left;
margin-top: 5px;
width: 30px; /* 固定按钮宽度 */
}
.o_notebook .nav-tabs {
overflow: visible;
white-space: nowrap;
flex-wrap: nowrap;
}
/* 确保导航标签不换行 */
.o_notebook .nav-tabs .nav-item {
display: inline-block;
white-space: nowrap;
}
.o_notebook .nav-tabs .nav-link {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: none;
}
.o_notebook_collapse_btn:hover {
background-color: #f5f5f5;
color: #000;
}
.o_notebook_collapse_btn:active {
background-color: #e9e9e9;
}
.o_notebook_collapse_btn.o_collapsed {
background-color: transparent;
color: #000;
}
.o_notebook_collapse_btn.o_collapsed:hover {
background-color: #f5f5f5;
color: #000;
}
/* 折叠状态的整个Notebook */
.o_collapsible_notebook.o_notebook_collapsed .o_notebook .tab-content {
display: none;
transition: all 0.3s ease;
}
.o_collapsible_notebook.o_notebook_collapsed .o_notebook .nav-tabs {
opacity: 0.7;
transition: opacity 0.3s ease;
}
/* 展开状态的整个Notebook */
.o_collapsible_notebook:not(.o_notebook_collapsed) .o_notebook .tab-content {
display: block;
transition: all 0.3s ease;
}
.o_collapsible_notebook:not(.o_notebook_collapsed) .o_notebook .nav-tabs {
opacity: 1;
transition: opacity 0.3s ease;
}
/* 页面内容区域的过渡效果 */
.o_notebook .tab-pane {
transition: opacity 0.3s ease, height 0.3s ease;
}
/* 响应式设计 */
@media (max-width: 768px) {
.o_notebook_collapse_btn {
font-size: 14px;
padding: 4px;
width: 25px;
height: 25px;
}
.o_notebook_collapse_btn + .nav-tabs {
width: calc(100% - 35px);
}
.o_notebook .nav-tabs .nav-link {
font-size: 12px;
padding: 8px 6px;
}
}
/* 深色主题支持 */
@media (prefers-color-scheme: dark) {
.o_notebook_collapse_btn {
background-color: transparent;
color: #fff;
}
.o_notebook_collapse_btn:hover {
background-color: #333;
color: #fff;
}
.o_notebook_collapse_btn.o_collapsed {
background-color: transparent;
color: #fff;
}
.o_notebook_collapse_btn.o_collapsed:hover {
background-color: #333;
color: #fff;
}
}
/* 动画效果 */
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slideUp {
from {
opacity: 1;
transform: translateY(0);
}
to {
opacity: 0;
transform: translateY(-10px);
}
}
/* 展开动画 */
.o_notebook .tab-pane.expanding {
animation: slideDown 0.3s ease;
}
/* 折叠动画 */
.o_notebook .tab-pane.collapsing {
animation: slideUp 0.3s ease;
}
/* 工具提示样式 */
.o_collapse_btn[title]:hover::after {
content: attr(title);
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
background-color: #333;
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
white-space: nowrap;
z-index: 1000;
margin-bottom: 5px;
}
.o_collapse_btn[title]:hover::before {
content: '';
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
border: 5px solid transparent;
border-top-color: #333;
z-index: 1000;
}
/* 高对比度模式支持 */
@media (prefers-contrast: high) {
.o_notebook_collapse_btn {
border: 2px solid currentColor;
}
}
/* 减少动画模式支持 */
@media (prefers-reduced-motion: reduce) {
.o_notebook .tab-pane,
.o_notebook_collapse_btn,
.o_collapsible_notebook {
transition: none;
}
.o_notebook .tab-pane.expanding,
.o_notebook .tab-pane.collapsing {
animation: none;
}
}
/* 打印样式 */
@media print {
.o_notebook_collapse_btn {
display: none;
}
.o_collapsible_notebook.o_notebook_collapsed .o_notebook .tab-content {
display: block !important;
}
.o_collapsible_notebook.o_notebook_collapsed .o_notebook .nav-tabs {
opacity: 1 !important;
}
}
@@ -0,0 +1,215 @@
// 可折叠Notebook功能
console.log('Collapsible Notebook JS loaded');
/**
* 页面加载完成后自动初始化所有可折叠notebook
*/
function initCollapsibleNotebooks() {
console.log('Initializing collapsible notebooks...');
// 初始化现有的notebook
const existingNotebooks = document.querySelectorAll('.o_collapsible_notebook');
console.log('Found notebooks:', existingNotebooks.length);
if (existingNotebooks.length === 0) {
console.log('No notebooks found, retrying in 500ms...');
setTimeout(initCollapsibleNotebooks, 500);
return;
}
existingNotebooks.forEach((notebook, index) => {
console.log(`Processing notebook ${index}:`, notebook);
if (!notebook.hasAttribute('data-collapsible-initialized')) {
initializeNotebook(notebook);
notebook.setAttribute('data-collapsible-initialized', 'true');
}
});
}
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', function() {
console.log('DOM Content Loaded');
initCollapsibleNotebooks();
});
// 如果DOM已经加载完成,立即初始化
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initCollapsibleNotebooks);
} else {
initCollapsibleNotebooks();
}
// 使用MutationObserver来监听DOM变化,确保在动态加载的内容中也能工作
function setupMutationObserver() {
if (!document.body) {
console.log('Document body not ready, retrying...');
setTimeout(setupMutationObserver, 100);
return;
}
const observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach(function(node) {
if (node.nodeType === Node.ELEMENT_NODE) {
const notebooks = node.querySelectorAll ?
node.querySelectorAll('.o_collapsible_notebook') :
(node.classList && node.classList.contains('o_collapsible_notebook') ? [node] : []);
notebooks.forEach(notebook => {
if (!notebook.hasAttribute('data-collapsible-initialized')) {
console.log('New notebook detected:', notebook);
initializeNotebook(notebook);
notebook.setAttribute('data-collapsible-initialized', 'true');
}
});
}
});
}
});
});
// 开始观察
observer.observe(document.body, {
childList: true,
subtree: true
});
console.log('MutationObserver setup complete');
}
// 设置MutationObserver
setupMutationObserver();
/**
* 初始化单个notebook
*/
function initializeNotebook(notebook) {
console.log('Initializing notebook:', notebook);
// 为整个notebook添加折叠/展开按钮
addNotebookCollapseButton(notebook);
}
/**
* 为整个notebook添加折叠/展开按钮
*/
function addNotebookCollapseButton(notebook) {
console.log('Adding collapse button to notebook:', notebook);
console.log('Notebook HTML structure:', notebook.outerHTML);
// 检查是否已经添加了按钮
if (notebook.querySelector('.o_notebook_collapse_btn')) {
console.log('Collapse button already exists');
return;
}
// 创建notebook折叠按钮
const notebookCollapseBtn = document.createElement('div');
notebookCollapseBtn.className = 'o_notebook_collapse_btn';
notebookCollapseBtn.innerHTML = '<i class="fa fa-chevron-up"></i>';
notebookCollapseBtn.title = '点击折叠/展开整个Notebook';
console.log('Created collapse button:', notebookCollapseBtn);
// 添加点击事件
notebookCollapseBtn.addEventListener('click', (e) => {
console.log('Collapse button clicked');
e.preventDefault();
e.stopPropagation();
toggleNotebook(notebook, notebookCollapseBtn);
});
// 将按钮添加到notebook的nav-tabs前面(与页面标签并排)
try {
const navTabs = notebook.querySelector('.nav-tabs');
if (navTabs && navTabs.parentNode) {
navTabs.parentNode.insertBefore(notebookCollapseBtn, navTabs);
console.log('Button inserted successfully before nav-tabs');
} else if (notebook.parentNode) {
notebook.parentNode.insertBefore(notebookCollapseBtn, notebook);
console.log('Button inserted successfully before notebook (fallback)');
} else {
notebook.insertBefore(notebookCollapseBtn, notebook.firstChild);
console.log('Button inserted successfully inside notebook (fallback)');
}
} catch (error) {
console.error('Error inserting button:', error);
}
// 恢复折叠状态
restoreNotebookState(notebook, notebookCollapseBtn);
}
/**
* 从localStorage恢复notebook折叠状态
*/
function restoreNotebookState(notebook, button) {
try {
const notebookId = notebook.getAttribute('id') || 'default_notebook';
const savedState = localStorage.getItem(`odoo_notebook_collapsed_${notebookId}`);
if (savedState === 'true') {
console.log('Restoring collapsed state for notebook:', notebookId);
notebook.classList.add('o_notebook_collapsed');
button.innerHTML = '<i class="fa fa-chevron-down"></i>';
button.classList.add('o_collapsed');
// 直接设置内容隐藏
const tabContent = notebook.querySelector('.tab-content');
if (tabContent) {
tabContent.style.display = 'none';
}
} else {
console.log('Notebook starts in expanded state:', notebookId);
}
} catch (error) {
console.error('Error restoring notebook state:', error);
}
}
/**
* 切换整个notebook的折叠/展开状态
*/
function toggleNotebook(notebook, button) {
console.log('Toggling notebook:', notebook);
console.log('Notebook classes before:', notebook.className);
const isCollapsed = notebook.classList.contains('o_notebook_collapsed');
console.log('Current state - isCollapsed:', isCollapsed);
// 查找tab-content元素
const tabContent = notebook.querySelector('.tab-content');
console.log('Found tab-content:', tabContent);
if (isCollapsed) {
// 展开notebook
console.log('Expanding notebook');
notebook.classList.remove('o_notebook_collapsed');
button.innerHTML = '<i class="fa fa-chevron-up"></i>';
button.classList.remove('o_collapsed');
if (tabContent) {
tabContent.style.display = 'block';
console.log('Set tab-content display to block');
}
} else {
// 折叠notebook
console.log('Collapsing notebook');
notebook.classList.add('o_notebook_collapsed');
button.innerHTML = '<i class="fa fa-chevron-down"></i>';
button.classList.add('o_collapsed');
if (tabContent) {
tabContent.style.display = 'none';
console.log('Set tab-content display to none');
}
}
console.log('Notebook classes after:', notebook.className);
// 保存状态
const notebookId = notebook.getAttribute('id') || 'default_notebook';
localStorage.setItem(`odoo_notebook_collapsed_${notebookId}`, isCollapsed ? 'false' : 'true');
console.log('State saved to localStorage');
}
+28 -19
View File
@@ -1,5 +1,7 @@
# -*- coding: utf-8 -*-
from datetime import datetime, time
from re import search
from odoo import models, fields, api, exceptions, _
from datetime import timedelta
from dateutil.relativedelta import relativedelta
@@ -111,27 +113,34 @@ class HrAttendance(models.Model):
# 判断是否为上月
record.is_last_month = (first_day_last_month <= record.current_data <= last_day_last_month)
def compute_company_id(self):
employee_date_records = {}
for att in self.env["hr.attendance"].browse(self._context.get('active_ids', self._context.get('active_id'))):
employee_id = att.employee_id.id
# 创建唯一分组键 (员工ID, 日期)
group_key = (employee_id, att.current_data)
# 将记录按员工+日期分组
if group_key not in employee_date_records:
employee_date_records[group_key] = []
employee_date_records[group_key].append(att.id)
def update_att(self):
# 定义要更新的员工姓名列表
target_employees = ["简梓豪", "林紫珊"]
# 定义日期范围:9月1日至9月15日
start_date = fields.Date.from_string("2025-09-01")
end_date = fields.Date.from_string("2025-09-15")
# 处理重复记录:每个员工每天只保留第一条记录
for (employee_id, date), record_ids in employee_date_records.items():
if len(record_ids) > 1:
# 保留第一条记录,其余标记为删除
ids_to_keep = record_ids[0]
ids_to_delete = record_ids[1:]
# 查找目标员工
employees = self.env["hr.employee"].search([
("name", "in", target_employees)
])
# 执行删除操作
if ids_to_delete:
self.env["hr.attendance"].browse(ids_to_delete).unlink()
if not employees:
raise UserError("未找到指定的员工记录")
# 生成日期范围内的所有日期
current_date = start_date
while current_date <= end_date:
for employee in employees:
# 执行考勤详情计算方法
self.env["hr.attendance.detail"].compute_attendance_detail(
employee,
current_date,
is_manual=True
)
current_date = current_date + timedelta(days=1)
return True
Binary file not shown.
@@ -17,7 +17,6 @@ class PropertyMeterArchives(models.Model):
lessee_id = fields.Many2one('property.tenant.information', string='承租方', store=True, compute='_compute_lessee_id')
employee_ids = fields.Many2many('hr.employee', string='抄表人')
epitope_values = fields.Char(string='表位置')
meter_type = fields.Selection([('electric', '电表'), ('water', '水表'), ('smart', '智能电表')], string='仪表类型')
other = fields.Char(string='其他')
meter_date = fields.Date(string='任务日期')
last_dosage = fields.Float(string='上次用量')
@@ -15,7 +15,6 @@ class PropertyMeterWater(models.Model):
maximum_count = fields.Integer(string='最大位数')
lessee_id = fields.Many2one('property.tenant.information', store=True, compute='_compute_lessee_id', string="承租方")
employee_ids = fields.Many2many('hr.employee', string='抄表人')
meter_type = fields.Selection([('electric', '电表'), ('water', '水表')], string='仪表类型')
epitope_values = fields.Char(string='表位置')
other = fields.Char(string='其他')
meter_date = fields.Date(string='任务日期')
@@ -58,7 +58,6 @@
<field name="lessee_id"/>
<field name="employee_ids" widget="many2many_tags"/>
<field name="epitope_values"/>
<field name="meter_type" invisible="1"/>
</group>
<group>
<field name="other"/>
@@ -133,6 +132,7 @@
<field name="name">电表库</field>
<field name="res_model">property.meter.archives</field>
<field name="view_mode">tree,form,search</field>
<field name="domain">[('is_push', '=', False)]</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'tree', 'view_id': ref('yuthon_property.view_property_meter_archives_tree')}),
(0, 0, {'view_mode': 'form', 'view_id': ref('yuthon_property.property_meter_archives_form')})]"/>
@@ -60,7 +60,6 @@
<field name="lessee_id"/>
<field name="employee_ids" widget="many2many_tags"/>
<field name="epitope_values"/>
<field name="meter_type" invisible="1"/>
</group>
<group>
<field name="other"/>
@@ -131,6 +130,7 @@
<field name="name">水表库</field>
<field name="res_model">property.meter.water</field>
<field name="view_mode">tree,form,search</field>
<field name="domain">[('is_push', '=', False)]</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'tree', 'view_id': ref('yuthon_property.view_property_meter_water_tree')}),
(0, 0, {'view_mode': 'form', 'view_id': ref('yuthon_property.property_meter_water_form')})]"/>
@@ -32,7 +32,6 @@ class PropertyMeterWizard(models.Model):
epitope_values = row[0].value
name = row[3].value
type_dict = {'水表': 'water', '电表': 'electric', '电表(电子表)': 'smart', '': ''}
meter_type = type_dict.get(row[4].value)
unit_number = row[1].value
installation_location = row[2].value
if row[5].value:
@@ -63,7 +62,6 @@ class PropertyMeterWizard(models.Model):
'unit_number': unit_number,
'installation_location': installation_location,
'type':type,
'meter_type': meter_type,
'gauge_magnification': gauge_magnification,
'meter_date': meter_date,
'current_read': current_read,
@@ -65,7 +65,6 @@ class PropertyWaterWizard(models.Model):
'unit_number': unit_number,
'installation_location': installation_location,
'type': type,
'meter_type': meter_type,
'gauge_magnification': gauge_magnification,
'meter_date': meter_date,
'current_read': current_read,
@@ -672,7 +672,7 @@
<field name="water_price"/>
<field name="ele_price"/>
</group>
<notebook>
<notebook class="o_collapsible_notebook">
<page string="租金明细">
<field name="breakdown_ids" string=" ">
<tree editable="bottom" delete="false">
@@ -764,7 +764,7 @@
readonly="1"/>
</group>
</group>
<notebook>
<notebook class="o_collapsible_notebook">
<page string="附件">
<group col="2">
<group>
+3
View File
@@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
from . import models
from . import controllers
+35
View File
@@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
{
"name": "Yuthon Web Notice",
"summary": "通知公告模块",
"description": "通知公告模块",
"author": "Yuthon",
"website": "#",
"category": "Yuthon/Yuthon Web Notice",
"version": "17.0.0.1",
"depends": ["base", "web","hr"],
"data": [
"views/templates.xml",
],
"assets": {
"yuthon_web_notice.assets_announcement": [
("include", "web._assets_helpers"),
"web/static/src/scss/pre_variables.scss",
"web/static/lib/bootstrap/scss/_variables.scss",
("include", "web._assets_bootstrap_backend"),
# 字体图标
"web/static/src/libs/fontawesome/css/font-awesome.css",
# include base files from framework
("include", "web._assets_backend"),
("include", "web._assets_core"),
"web/static/src/core/utils/functions.js",
"web/static/src/core/browser/browser.js",
"web/static/src/core/registry.js",
"web/static/src/core/assets.js",
"yuthon_web_notice/static/src/**/*",
],
},
"installable": True,
"application": True,
"license": "AGPL-3",
}
@@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
from . import controllers
@@ -0,0 +1,10 @@
from odoo import http
from odoo.http import request, route
class OwlPlayground(http.Controller):
@http.route(['/web_notice'], type='http', auth='public')
def show_playground(self):
"""
Renders the owl playground page
"""
return request.render('yuthon_web_notice.Announcement')
+1
View File
@@ -0,0 +1 @@
from . import hr_employee
+16
View File
@@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
from odoo import models, fields
class HrEmployee(models.Model):
_inherit = "hr.employee"
def get_full_employee_info(self, employee_id):
"""返回员工完整信息(所有字段)"""
employee = self.browse(employee_id)
if not employee.exists():
return {}
# 读取所有字段
fields = employee.fields_get().keys() # 获取所有字段名
return employee.sudo().read(fields)[0] # 返回字典
@@ -0,0 +1,32 @@
/** @odoo-module **/
import { Component, useState, onWillUnmount, } from "@odoo/owl";
import { useService } from "@web/core/utils/hooks";
export class Announcement extends Component {
static template = "yuthon_web_notice.Announcement";
static props = ["*"];
setup() {
console.log(useService);
debugger;
this.rpc = useService("rpc");
this.employee = useState({});
this.id = this.props.id || "";
onWillUnmount(async () => {
if (this.id) {
await this.loadEmployee(this.id);
}
});
}
async loadEmployee(employeeId) {
try {
this.employee = await this.rpc({
model: "hr.employee",
method: "get_full_employee_info",
args: [parseInt(employeeId)],
});
} catch (err) {
console.error("获取员工信息失败:", err);
}
}
}
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="yuthon_web_notice.Announcement">
<h1>Hello OWL Web</h1>
</t>
</templates>
+51
View File
@@ -0,0 +1,51 @@
/** @odoo-module **/
import { browser } from "@web/core/browser/browser";
import { makeEnv } from "@web/env"; // 引入环境构建工具
import { mount, whenReady } from "@odoo/owl";
import { Announcement } from "./announcement";
import { templates } from "@web/core/assets";
// 当document.body准备好时,挂载 Announcement 组件
whenReady(() => {
const env = makeEnv(); // 创建标准 Odoo 环境
mount(Announcement, document.body, {
templates,
dev: true,
name: "Web Notice",
env,
props: {
id: new URLSearchParams(window.location.search).get("id"),
},
});
});
/**
* 错误日志捕获函数
* 遍历 error 对象的 cause 属性,输出错误堆栈信息(包含链式错误原因)
* @param {Event} ev 浏览器事件对象
*/
function logError(ev) {
ev.preventDefault();
let error = ev?.error || ev.reason;
if (error.seen) {
// 如果错误已经标记过 seen,就不再重复打印
// 场景:mount 失败时,Owl 会抛出错误,如果不处理就会打印两次
return;
}
error.seen = true;
let errorMessage = error.stack;
while (error.cause) {
errorMessage += "\nCaused by: "
errorMessage += error.cause.stack;
error = error.cause;
}
console.error(errorMessage);
}
// 监听全局错误事件(比如未捕获的异常)
browser.addEventListener("error", (ev) => { logError(ev) });
// 监听未捕获的 Promise 错误(unhandledrejection)
browser.addEventListener("unhandledrejection", (ev) => { logError(ev) });
+14
View File
@@ -0,0 +1,14 @@
<odoo>
<data>
<template id="yuthon_web_notice.Announcement" name="Yuthon Web Notice">
<html>
<head>
<link type="image/x-icon" rel="shortcut icon" href="/web/static/img/favicon.ico"/>
<t t-call-assets="yuthon_web_notice.assets_announcement"/>
</head>
<body>
</body>
</html>
</template>
</data>
</odoo>