57 Commits
Author SHA1 Message Date
qitangbo b89e041f46 feat(college): 为「工作条目」「配送成本」补充应用卡片图标 2026-10-29 16:37:16 +08:00
qitangbo f1a124a2f6 feat(college): 重建「平台看板」总览页
仪表盘模块被换成官方新版后,原来的工作台总览页(欢迎语 + 全部仪表盘/图表/地图/
收藏/列表五个统计卡 + 看板卡片)消失,菜单落到一张空白看板上。

在 college 模块内用 OWL 客户端动作重建该页面(不改第三方模块源码):
- 读 ks_dashboard_ninja.board / .item,统计看板数与图表/列表/地图数量
- 点卡片 doAction 打开对应看板(tag=ks_dashboard_ninja + params.ks_dashboard_id)
- 模块未装或无权限时降级为空状态,不报错
2026-10-28 20:12:40 +08:00
qitangbo 9cb740b17c fix(college): 修复 Dashboard Ninja 看板栅格错乱
ks_dashboard_ninja 18.0.2.2.3 初始化 gridstack 时命中 one-column 模式,
容器带上 .grid-stack-1,其 min-width:100% 覆盖了 [gs-w=N] 的 12 栅格宽度,
导致老师/学生看板所有卡片拉成整行宽并互相遮挡(页面呈彩色色块)。

在桌面宽度(>=768px)下解除该 min-width,宽度回归栅格百分比;
窄屏保留原有单列行为。
2026-10-28 19:26:08 +08:00
yuthon c0da213161 college: 实训白名单 yuthon_property_report -> yuthon_issue(补 sps_approval 依赖) 2026-10-26 16:55:04 +08:00
yuthon 0f7955d5bf college: 实训模块白名单 36->50(新增 gzth 14 个自研模块) 2026-10-26 16:03:39 +08:00
qitangboandWorkBuddy 0d8008868a feat(college): 实训白名单定稿 36 个真实可装模块——剔除5个企业版/幽灵模块
- 逐一校验 ir_module_module.state,剔除 accountant/helpdesk/knowledge/
  mrp_workorder/quality_control(CE 源码无文件夹、state=uninstallable,
  学生无法安装,属企业版迁库残留的幽灵记录)
- 去掉 domain 的 ('application','=',True) 条件,使 6 个 application=False
  的原生子模块(delivery/payment/hr_timesheet/hr_work_entry/membership/
  website_blog)也能显示
- 最终 36 个 = 原生 33 + 自研例外 3(yuthon_employee/yuthon_notice/
  yuthon_will_task),已 -u college 升级到本地 school 库验证 0 不可装/0 缺失

Co-Authored-By: WorkBuddy <workbuddy@tencent.com>
2026-10-26 11:36:30 +08:00
qitangbo 8ea25712b5 筛选实训模块白名单:去幽灵/空壳/重叠,补 6 个官方实用应用
按生产库(81.70.146.51)实测重新筛选,仍精确 50 个:

筛除 6 个:
- game_ai_npc / yuthon_school:服务器代码已不存在(幽灵记录,装不上)
- yuthon_applicant:代码不在生产 addons_path 加载路径内,装不了
- school_base:仅一个登录页模板,无模型无业务,与 yuthon_login_theme 重叠
- yuthon_wage / yuthon_attendance:与 yun_product 版 yuthon_salary /
  yuthon_hr_attendance 功能重叠,保留功能更全的 yun_product 版

补入 6 个 Odoo 原生应用(官方模块、application=True、生产库有记录、
未安装,学生可装来实训):calendar、contacts、mass_mailing、
website_event、website_sale、im_livechat

本地 -u college 验证通过;生产库比对 50/50 全部可显示。
2026-10-23 10:19:33 +08:00
qitangbo 1f985e96c7 fix(college): 实训白名单修正47->50——移出3个application=False的自有模块(yuthon_contract/yuthon_hr_employee/yuthon_web,应用列表不显示),换入fleet(车辆)/repair(维修)/hr_contract(员工合同)三个application=True的模块;生产库实测新白名单50/50全部可见;注释补充application标志教训 2026-10-19 17:01:40 +08:00
qitangbo 788203f9a0 feat(college): 实训模块改白名单 domain——自有模块27个全列+Odoo原生精选23个共50个(用户2026-10-19定;替代原类目ID排除方案,类目ID跨库不一致已弃用;生产实测将精确匹配50条) 2026-10-19 10:34:24 +08:00
qitangbo 24eac518af fix(college): 修复分页器隐藏总数的 OwlError——改用模板继承替代错误的 this.root.el
上个提交在 Pager.prototype.setup 里用 this.root.el 加 class,但 OWL 2 的
Component 基类没有 root 属性(ControlPanel 的 this.root 是它自己
useRef("root") 建的,Pager 模板没有该 ref),导致
TypeError: Cannot read properties of undefined (reading 'el'),
后台整个 OWL 生命周期报错崩溃。

改为官方扩展点模板继承:
- 新增 xml/module_list_pager.xml,t-inherit 扩展 web.Pager,
  给 nav 加 t-att-class,env.config 带标记时输出 o_pager_hide_total
- js/module_list_cap.js 删掉 Pager patch 与 onMounted,保留 env 标记
- manifest 注册新 xml
- 已编译级验证:o_pager_hide_total 进入 bundle,日志无模板警告
2026-10-16 18:00:46 +08:00
qitangbo 371ff7a2c8 模块列表分页器隐藏"/总数",只显示1-50
实训模块/应用主页的 ir.module.module 看板:
- KanbanController.setup 在 env.config 打隐藏标记(仅 kanban,list 不受影响)
- Pager.setup 读到标记后挂载时加 class o_pager_hide_total
- JS 注入 CSS 隐藏分页器的" / "分隔 span 和总数 span
- 真实总数照常统计,只是不显示;翻页/锁50行为不变
- 本地已用 ir.qweb._get_asset_content 真实编译 bundle 验证补丁在位
2026-10-16 17:42:52 +08:00
qitangbo befeb1b60c fix(college): 实训模块列表精确收敛到 50 条——按模块名排除 6 个教学实训用不上的Odoo 原生小应用(企业库实测 56→50;类目 ID 跨库不一致的坑写进注释) 2026-10-16 17:11:00 +08:00
qitangbo 11c77c7496 merge: 同步服务器 master(团队页简约化/首页默认产品介绍/manifest 修复/hide_about_edit 注册) 2026-10-16 17:05:30 +08:00
qitangbo ccff84e9f5 chore: 纳入 yuthon_employee 模块(教职工档案,出勤/薪资体系数据根) 2026-10-16 17:04:02 +08:00
qitangbo 77739b113c style(college): 产品介绍页去掉黄色 P 徽标,python 改大写 Python(Hero 栏+页脚) 2026-10-16 17:04:01 +08:00
qitangbo e720156c16 feat(college): 注册 module_list_cap.js——模块列表每页锁 50(应用主页+实训模块;翻页点不动、每页数锁定,搜索不受影响) 2026-10-16 17:04:00 +08:00
qitangbo f300271bf9 修复考试403并同步左侧菜单顺序与中文名
- exam_controller: 兼容学生档案未绑登录账号(paper.user_id为空)的考试访问校验

- menus.xml: 实训管理顺序15、考试根菜单改名'考试管理'顺序25、子菜单改'考试列表'避免重名

- odoosh_menus.xml: 快捷菜单'我的实训'改名'实训管理'
(cherry picked from commit 02a960f62b8f3aae4f0a975e9860567afef4e1a5)
2026-10-15 18:49:30 +08:00
root 079ee13e1f Merge remote-tracking branch 'origin/master'
# Conflicts:
#	college/data/about_pages.xml
2026-10-13 18:08:46 +08:00
qitangbo e5596aa299 Merge pull request '完善教育' (#61) from qtb into master
Reviewed-on: #61
2026-10-13 18:03:07 +08:00
qitangbo 1728ac79b7 完善教育 2026-10-13 17:54:16 +08:00
root 6a977a50ab style(college): 团队介绍页公司名上移第二行并大号黑色加粗,标签行居中 2026-10-13 12:15:42 +08:00
root 23a90b6b1e style(college): 团队介绍页标题区居中,副标题下新增公司名行 2026-10-13 11:31:08 +08:00
root 9fbbdb0c8e style(college): 团队介绍页标题区去掉 Peng Yuthon 品牌行,进一步精简 2026-10-13 11:23:24 +08:00
root d9014ad665 style(college): 团队介绍页改简约风——去掉深色渐变 banner 背景,标题区/页脚改白底轻排版 2026-10-13 11:01:01 +08:00
root 4792c432c2 fix(college): 移除已删除文件 sidenav_tooltip.xml 的 manifest 引用(避免升级时资源包报错) 2026-10-12 19:41:08 +08:00
root 514f897889 Merge remote-tracking branch 'origin/master' 2026-10-12 19:39:52 +08:00
qitangbo 2025b4e344 Merge pull request 'Qtb' (#60) from qtb into master
Reviewed-on: #60
2026-10-12 19:26:46 +08:00
qitangbo fd091b566b 完善教育 2026-10-12 19:21:57 +08:00
agent 6a0f608ec2 feat(college): 登录首页默认改为产品介绍(res.users.action_id 默认+存量迁移) 2026-10-12 18:25:56 +08:00
agent 78477e45de fix(college): 将 hide_about_edit.js/scss 正式注册进 web.assets_backend 资源包 2026-10-12 17:34:00 +08:00
root 30db2702bd Merge remote-tracking branch 'origin/qtb' 2026-10-12 16:55:48 +08:00
qitangbo fea48a2623 fix(college): 关于页编辑按钮对所有用户隐藏(MutationObserver 兜底监听) 2026-10-12 16:53:12 +08:00
李鹏宇 6eb765546d Merge branch 'master' of https://git.pengyuthon.cn/pengyuthon/shcool
# Conflicts:
#	college/__manifest__.py
2026-10-10 16:36:20 +08:00
李鹏宇 cc92428709 列表视图样式 2026-10-10 16:35:35 +08:00
qitangbo e4f44b1287 Merge pull request '完善教育' (#59) from qtb into master
Reviewed-on: #59
2026-10-10 15:07:17 +08:00
qitangbo f1c9059887 完善教育 2026-10-10 11:43:01 +08:00
qitangbo d17e342196 Merge pull request '完善教育' (#58) from qtb into master
Reviewed-on: #58
2026-10-09 21:22:41 +08:00
qitangbo a3fc5395ea 完善教育 2026-10-09 21:22:20 +08:00
qitangbo 260a7347fb Merge pull request '完善教育' (#57) from qtb into master
Reviewed-on: #57
2026-10-09 19:19:26 +08:00
qitangbo 802b8efa39 完善教育 2026-10-09 19:19:05 +08:00
qitangbo ec4c918899 Merge pull request '关于页' (#56) from qtb into master
Reviewed-on: #56
2026-10-09 15:03:20 +08:00
qitangbo 87e8f0c636 关于页 2026-10-09 15:02:41 +08:00
qitangbo 8ea8b089af Merge pull request '完善教育' (#55) from qtb into master
Reviewed-on: #55
2026-10-09 14:23:19 +08:00
qitangbo dfa665bbde 编辑 2026-10-09 14:14:14 +08:00
qitangbo 8939e61094 feat: 新增 recents_limit 模块,继承 patch OmuxNavBar 将导航栏「最近」上限从50改为20(不改动线上源码/yuthon_base) 2026-10-08 21:08:31 +08:00
李鹏宇 0a96d0e868 用户菜单调整 2026-10-08 20:30:38 +08:00
李鹏宇 ab62d0e79b 用户菜单调整 2026-10-08 20:18:01 +08:00
李鹏宇 d84c482c84 用户菜单调整 2026-10-08 19:31:22 +08:00
李鹏宇 7b393414b3 用户菜单调整 2026-10-08 18:55:12 +08:00
李鹏宇 fa5b2c649e 用户菜单调整 2026-10-08 18:07:03 +08:00
李鹏宇 27b17aeaa6 用户菜单调整 2026-10-08 17:35:32 +08:00
qitangbo 246326ed9c Merge pull request '完善教育' (#54) from qtb into master
Reviewed-on: #54
2026-09-29 17:29:48 +08:00
qitangbo c84da2ed10 完善教育 2026-09-29 16:39:32 +08:00
qitangbo 9d5a60af09 Merge pull request '完善教育' (#53) from qtb into master
Reviewed-on: #53
2026-09-23 15:33:46 +08:00
qitangbo d1a217f8cf Merge pull request '当前用户' (#52) from qtb into master
Reviewed-on: #52
2026-09-22 16:40:27 +08:00
qitangbo 4cdbaf9799 Merge pull request '翻译' (#51) from qtb into master
Reviewed-on: #51
2026-09-21 19:41:06 +08:00
qitangbo 63c87c7e56 Merge pull request 'Qtb' (#50) from qtb into master
Reviewed-on: #50
2026-09-21 18:56:34 +08:00
47 changed files with 1503 additions and 37 deletions
+14 -1
View File
@@ -1,6 +1,6 @@
{
'name': '教育',
'version': '18.0.3.0.4',
'version': '18.0.3.3.0',
'summary': 'Odoo 18 教学管理平台 — 师生管理 + 课程管理 + 作业/进展(老师·学生双角色)',
'description': """
Odoo 18 教学管理平台(面向老师与学生,双角色权限)
@@ -54,19 +54,32 @@
'data/shixun_module_data.xml',
'data/module_info_data.xml',
'views/module_info_views.xml',
'views/dashboard_overview_views.xml',
],
'assets': {
'web.assets_backend': [
'college/static/src/scss/college_theme.scss',
'college/static/src/scss/list_zebra.scss',
'college/static/src/js/hide_company_menu.js',
'college/static/src/js/hide_user_menu_items.js',
'college/static/src/js/hide_activity_menu.js',
'college/static/src/js/portal_indicator.js',
'college/static/src/js/hide_about_edit.js',
'college/static/src/scss/hide_about_edit.scss',
'college/static/src/xml/portal_indicator.xml',
'college/static/src/js/module_list_cap.js',
'college/static/src/xml/module_list_pager.xml',
'college/static/src/js/dashboard_overview.js',
'college/static/src/xml/dashboard_overview.xml',
'college/static/src/scss/dashboard_overview.scss',
],
'web.assets_web_dark': [
'college/static/src/scss/college_theme_dark.scss',
'college/static/src/scss/list_zebra_dark.scss',
],
'web.assets_backend_lazy_dark': [
'college/static/src/scss/college_theme_dark.scss',
'college/static/src/scss/list_zebra_dark.scss',
],
},
'application': True,
+19 -2
View File
@@ -16,12 +16,29 @@ class ExamController(http.Controller):
"""学生答题卡式考试页面控制器。"""
def _check_paper_access(self, paper_id, states=None):
"""校验当前登录用户是否为该答卷的学员本人。"""
"""校验当前登录用户是否为该答卷的学员本人。
兼容两种身份关联方式:
- 答卷已关联登录账号(paper.user_id)的,直接用账号比对;
- 答卷未关联账号(如学生档案尚未绑定账号)的,通过当前登录账号
反向查找对应的学生档案,再与学生答卷的 student_id 比对。
"""
paper = request.env['yuthon.school.exam.paper'].sudo().browse(paper_id)
if not paper.exists():
raise http.NotFound()
if paper.user_id.id != request.env.user.id:
current_user = request.env.user
if paper.user_id and paper.user_id.id != current_user.id:
raise AccessError('您无权访问该答卷。')
if not paper.user_id:
# 答卷未绑定账号时,尝试用当前登录账号找到对应学生档案
current_student = request.env['yuthon.school.student'].sudo().search([
('user_id', '=', current_user.id)
], limit=1)
if not current_student or current_student.id != paper.student_id.id:
raise AccessError('您无权访问该答卷。')
if states and paper.state not in states:
raise UserError('当前答卷状态不允许该操作。')
return paper
+57 -3
View File
@@ -103,8 +103,7 @@
<div style="position:absolute;top:-60px;right:-40px;width:220px;height:220px;border-radius:50%;background:rgba(56,189,248,0.16);"></div>
<div style="position:absolute;bottom:-80px;left:-30px;width:260px;height:260px;border-radius:50%;background:rgba(250,204,21,0.12);"></div>
<div style="position:relative;z-index:1;display:inline-flex;align-items:center;gap:10px;padding:8px 18px;background:rgba(255,255,255,0.12);border:1px solid rgba(255,255,255,0.30);border-radius:999px;font-size:15px;font-weight:600;margin-bottom:20px;">
<span style="display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;border-radius:7px;background:#ffd43b;color:#0b1120;font-weight:800;font-family:monospace;">P</span>
python企业实训平台
Python企业实训平台
</div>
<h1 style="position:relative;z-index:1;margin:0 0 12px;font-size:36px;font-weight:800;letter-spacing:1px;color:#fff !important;">从 Python 语法到企业级项目实战</h1>
<p style="position:relative;z-index:1;margin:0 0 22px;font-size:16px;opacity:0.92;font-weight:400;color:#fff !important;">面向高校与职业院校的工程化 Python 实训平台,把企业真实开发场景搬进课堂</p>
@@ -231,9 +230,64 @@
<!-- 页脚 -->
<div style="margin-top:24px;padding:18px 24px;background:#0f172a;color:#94a3b8;font-size:13px;text-align:center;">
python企业实训平台 · 基于 Odoo 18 构建 · 让每一次练习都更接近真实开发
Python企业实训平台 · 基于 Odoo 18 构建 · 让每一次练习都更接近真实开发
</div>
</div>
]]></field>
</record>
<!-- ===================== 团队介绍 ===================== -->
<record id="about_page_team" model="yuthon.about.page">
<field name="name">团队介绍</field>
<field name="category">team</field>
<field name="sequence">40</field>
<field name="active">True</field>
<field name="content"><![CDATA[
<div style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'PingFang SC','Microsoft YaHei',sans-serif;color:#334155;line-height:1.7;">
<!-- 标题区:白底 + 细分隔线,无大色块 -->
<div style="padding:0 0 20px;border-bottom:1px solid #e2e8f0;text-align:center;">
<h1 style="margin:0 0 6px;font-size:28px;font-weight:700;color:#0f172a;text-align:center;">鹏程万里 气宇轩昂</h1>
<p style="margin:0 0 4px;font-size:16px;font-weight:700;color:#0f172a;">鹏宇森数据科技有限公司</p>
<p style="margin:0 0 16px;font-size:14px;color:#64748b;">软件 · 硬件 · AI 智能体</p>
<div style="display:flex;flex-wrap:wrap;gap:8px;justify-content:center;">
<span style="background:#f1f5f9;color:#475569;border-radius:6px;padding:4px 12px;font-size:13px;">软件</span>
<span style="background:#f1f5f9;color:#475569;border-radius:6px;padding:4px 12px;font-size:13px;">硬件</span>
<span style="background:#f1f5f9;color:#475569;border-radius:6px;padding:4px 12px;font-size:13px;">AI 智能体</span>
<span style="background:#f1f5f9;color:#475569;border-radius:6px;padding:4px 12px;font-size:13px;">工程化教育</span>
<span style="background:#f1f5f9;color:#475569;border-radius:6px;padding:4px 12px;font-size:13px;">企业实训</span>
</div>
</div>
<!-- 关于我们 -->
<div style="padding-top:24px;">
<h2 style="display:flex;align-items:center;gap:8px;margin:0 0 12px;font-size:16px;font-weight:700;color:#0f172a;">
<span style="display:inline-block;width:4px;height:15px;border-radius:2px;background:#0ea5e9;"></span>关于我们
</h2>
<p style="margin:0;font-size:14px;color:#475569;">
Peng Yuthon 专注于软件、硬件与 AI 智能体方向的工程化教育,
致力于为高校与职业院校提供真实企业场景驱动的实训解决方案。
</p>
</div>
<!-- 联系方式 -->
<div style="padding-top:26px;">
<h2 style="display:flex;align-items:center;gap:8px;margin:0 0 12px;font-size:16px;font-weight:700;color:#0f172a;">
<span style="display:inline-block;width:4px;height:15px;border-radius:2px;background:#0ea5e9;"></span>联系方式
</h2>
<ul style="list-style:none;padding:0;margin:0;font-size:14px;color:#475569;">
<li style="padding:7px 0;border-bottom:1px solid #f1f5f9;"><b style="color:#0f172a;font-weight:600;">B 站</b> bilibili: pengyuthon</li>
<li style="padding:7px 0;border-bottom:1px solid #f1f5f9;"><b style="color:#0f172a;font-weight:600;">邮箱</b> pengyuthon@163.com</li>
<li style="padding:7px 0;border-bottom:1px solid #f1f5f9;"><b style="color:#0f172a;font-weight:600;">官网</b> pengyuthon.cn</li>
<li style="padding:7px 0;"><b style="color:#0f172a;font-weight:600;">学习平台</b> edu.pengyuthon.cn</li>
</ul>
</div>
<!-- 版权:浅色小字,去掉深色色块 -->
<div style="margin-top:28px;padding-top:14px;border-top:1px solid #e2e8f0;font-size:12px;color:#94a3b8;text-align:center;">
© 2026 Peng Yuthon. All rights reserved.
</div>
</div>
]]></field>
</record>
</odoo>
+41 -16
View File
@@ -8,22 +8,47 @@
<field name="res_model">ir.module.module</field>
<field name="view_mode">kanban,list,form</field>
<field name="view_id" ref="shixun_module_kanban_view"/>
<!-- 过滤掉与教育/物管实训无关的类目,使应用总数收敛到 50(limit=50 时不再出现「加载更多」翻页)。
category_id 为 M2O,domain 在客户端用 Python eval 解析,无法使用 ref(),故此处用真实类目 id:
13=Education(教育) 54=Lucky Theme Backend(主题后端)
71=preview_attachment 15=Data Cleaning(数据清理)
94=Administration(后台) 6=Tools(工具)
10=Productivity(生产力) 11=Calendar(日历)
20=Uncategorized(未分类)
2026-09-18 追加过滤(各类目仅 1 个应用、教学实训用不上),使总数精确收敛到 50:
33=Timesheets(工时单) 55=Lunch(午餐) 37=Live Chat(在线聊天)
59=Social Marketing(社会营销) 21=Events(活动) 89=Sign(签署)
90=Helpdesk(服务台) 92=Field Service(现场服务) 103=Planning(排班)
28=Time Off(休息时间) 32=Recruitment(招聘) 88=Appraisals(评估)
23=Fleet(车辆)
如需保留/恢复某类目,从下面列表中删除对应 id 即可(注意总数会变化)。 -->
<field name="domain">[('application', '=', True), ('category_id', 'not in', [13, 54, 71, 15, 94, 6, 10, 11, 20, 33, 55, 37, 59, 21, 89, 90, 92, 103, 28, 32, 88, 23])]</field>
<field name="context">{}</field>
<!-- 过滤目标:白名单制——只有列在下方 name 清单里的模块才显示(只过滤、不删模块),
使应用列表精确收敛到 50 条(=limit=50,单页封顶,无第 2 页,module_list_cap.js 也硬顶 50)。
方案:2026-10-19 起改用白名单(按模块名,跨库稳定),不再用类目 ID 排除。
2026-10-23 按"只保留 Odoo 非企业版原生模块(可安装)+ 3 个校园自研例外"重新筛选;
2026-10-26 校验可安装性:从 41 剔除 5 个企业版/幽灵模块
(accountant、helpdesk、knowledge、mrp_workorder、quality_control)——它们在本机 CE 源码的
addons_path 中无文件夹、state=uninstallable,学生无法安装。
2026-10-26(二次)把 gzth 提供的自研模块加进培训页(位于 gzth_addons,已挂进 odoo.conf 的
addons_path):原 yuthon_sps_approval 缺依赖 yuthon_issue,后由用户补发 yuthon_issue 模块,现已可安装;
yuthon_property_report 因缺 yuthon_property、且用户要求去掉,已移出白名单并从 gzth_addons 移除。
名单构成(共 50 个):
1) Odoo 原生 33 个
· 财务会计 1:account
· 采购与库存 2:purchase、stock
· 生产制造 2:mrp、maintenance
· 资产运维 2:fleet、repair
· 销售与前台 7:crm、sale_management、point_of_sale、pos_restaurant、
website_sale、delivery、payment
· 人力资源 8:hr、hr_attendance、hr_expense、hr_holidays、hr_recruitment、
hr_contract、hr_timesheet、hr_work_entry
· 网站与营销 8:website、website_slides、website_event、im_livechat、
mass_mailing、survey、membership、website_blog
· 协作与知识 3:project、calendar、contacts
2) 校园自研例外 3 个(用户指定保留):yuthon_employee、yuthon_notice、yuthon_will_task
3) gzth 自研 14 个:soong_workflow_stub、yuthon_account_report、yuthon_datetime_rounding、
yuthon_inspection、yuthon_noprint_county、yuthon_partner、yuthon_party、
yuthon_required_mark、yuthon_sps_approval、yuthon_tianhe、yuthon_ui_custom、
yuthon_web_notice、yuthon_work、yuthon_issue
被筛除:所有 Agent 智能体平台(agent_*)、教学线自研(college、learning_center、
student_organization、odoosh_connector)、宇森游戏自研(game_base、yt_world、
yusen_website)、人事薪酬自研(yuthon_salary、yuthon_hr_attendance)、
以及 5 个企业版模块(accountant、helpdesk、knowledge、mrp_workorder、quality_control)、
yuthon_property_report(用户要求去掉,缺 yuthon_property 依赖)。
注意:domain 已去掉 ('application','=',True),白名单里 application=False 的原生子模块
(delivery/payment/hr_timesheet/hr_work_entry/membership/website_blog)也会显示,故页面=50。
维护:新装的自有模块必须往本清单加,否则不显示;总数超过 50 时
module_list_cap.js 把显示硬顶在 50。 -->
<field name="domain">[('name', 'in', ['account', 'calendar', 'contacts', 'crm', 'delivery', 'fleet', 'hr', 'hr_attendance', 'hr_contract', 'hr_expense', 'hr_holidays', 'hr_recruitment', 'hr_timesheet', 'hr_work_entry', 'im_livechat', 'maintenance', 'mass_mailing', 'membership', 'mrp', 'payment', 'point_of_sale', 'pos_restaurant', 'project', 'purchase', 'repair', 'sale_management', 'soong_workflow_stub', 'stock', 'survey', 'website', 'website_blog', 'website_event', 'website_sale', 'website_slides', 'yuthon_account_report', 'yuthon_datetime_rounding', 'yuthon_employee', 'yuthon_inspection', 'yuthon_noprint_county', 'yuthon_notice', 'yuthon_party', 'yuthon_partner', 'yuthon_issue', 'yuthon_required_mark', 'yuthon_sps_approval', 'yuthon_tianhe', 'yuthon_ui_custom', 'yuthon_web_notice', 'yuthon_will_task', 'yuthon_work'])]</field>
<!-- shixun_hide_empty_categories:搜索面板「类别」列过滤掉计数为 0 的类别
(见 models/ir_module_module_info.py 对 search_panel_select_range 的包装) -->
<field name="context">{'shixun_hide_empty_categories': True}</field>
<field name="search_view_id" ref="shixun_module_search_view"/>
<field name="limit">50</field>
</record>
@@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
from odoo import SUPERUSER_ID, api
def migrate(cr, version):
# 把存量用户的登录首页动作统一改为「产品介绍」。
# 新用户默认值在 models/res_users.py 的 action_id 默认值里设置。
if not version:
return
env = api.Environment(cr, SUPERUSER_ID, {})
action = env.ref("college.action_yuthon_product_intro", raise_if_not_found=False)
if not action:
return
users = env["res.users"].with_context(active_test=False).search([("share", "=", False)])
if users:
users.write({"action_id": action.id})
+1
View File
@@ -14,3 +14,4 @@ from . import ir_module_module_info
from . import exam
from . import exam_import_wizard
from . import question_import_wizard
from . import res_users
+1
View File
@@ -18,6 +18,7 @@ class YuthonAboutPage(models.Model):
('operation', '操作手册'),
('product', '产品介绍'),
('import', '导入说明'),
('team', '团队介绍'),
], string='分类', default='system', required=True)
content = fields.Html(string='内容', sanitize=True, translate=True)
sequence = fields.Integer(string='排序', default=10)
+16
View File
@@ -20,6 +20,13 @@ class SchoolAssignment(models.Model):
ondelete='cascade',
domain="[('state', '=', 'published'), ('teacher_id', '=', uid)]",
help='只能选择自己已发布的课程。')
chapter_id = fields.Many2one(
'yuthon.school.course.chapter',
string='对应章节',
domain="[('course_id', '=', course_id)]",
ondelete='set null',
index=True,
help='该作业所属的课程章节,不选则代表整门课程作业。')
teacher_id = fields.Many2one(
'res.users', string='布置教师',
default=lambda self: self.env.user, required=True)
@@ -111,6 +118,15 @@ class SchoolAssignmentSubmission(models.Model):
_order = 'submit_time desc, id'
_rec_name = 'name'
chapter_id = fields.Many2one(
'yuthon.school.course.chapter',
string='章节',
related='assignment_id.chapter_id',
store=True, index=True,
)
name = fields.Char(string='提交标题', readonly=True, copy=False)
assignment_id = fields.Many2one(
'yuthon.school.assignment', string='作业', required=True, ondelete='cascade')
+14
View File
@@ -106,6 +106,20 @@ class SchoolCourse(models.Model):
chapter_count = fields.Integer(
string='章节数', compute='_compute_chapter_count', store=True)
# 课程作业:一门课对应多条作业记录
assignment_ids = fields.One2many(
'yuthon.school.assignment',
'course_id',
string='作业布置',
)
# 学习进展:一门课对应多条学员进度记录
progress_ids = fields.One2many(
'yuthon.school.progress',
'course_id',
string='学习进展',
)
@api.depends('student_ids')
def _compute_student_count(self):
for record in self:
+10 -3
View File
@@ -107,11 +107,18 @@ class ExamQuestion(models.Model):
@api.onchange('question_type')
def _onchange_question_type(self):
"""切换题型时:判断题给默认正确答案;单选/多选在无选项时补 A/B/C/D。"""
"""切换题型时:判断题给默认正确答案;单选/多选在无选项时补 A/B/C/D;
非选择题清掉默认带出的空选项,否则会被一并提交,触发
ExamQuestionOption.content 的 required 校验。"""
if self.question_type == 'true_false':
self.correct_answer = 'True'
if self.question_type in ('single_choice', 'multiple_choice') and not self.option_ids:
self.option_ids = self._default_option_lines()
if self.question_type in ('single_choice', 'multiple_choice'):
if not self.option_ids:
self.option_ids = self._default_option_lines()
else:
# 切到填空/判断/简答:清掉默认带出的空选项,避免服务端必填校验报错
# 用 (5,0,0) 完整写法,(5,) 简写在 Odoo 18 + Py3.13 下会触发访问冲突
self.option_ids = [(5, 0, 0)]
@api.depends('correct_answer')
def _compute_correct_answer_tf(self):
+15
View File
@@ -48,6 +48,21 @@ class IrModuleModule(models.Model):
help='在「模块信息 / 知识点」页展示的核心概念与使用要点(由 college 维护)',
)
@api.model
def search_panel_select_range(self, field_name, **kwargs):
"""搜索面板类别列:按 context 开关过滤掉计数为 0 的顶层类别。
背景:base 对 ir.module.module 的 search_panel_select_range 做了重写
(ir_module.py),只要顶层类别的子树里存在任何模块就会返回该类别,
即使它名下没有一条记录匹配当前搜索域(计数为 0、界面上不显示数字)。
「实训模块」动作通过 context 开关 shixun_hide_empty_categories 启用本过滤,
不影响原生「应用」页面;hierarchize/expand 等视图属性在该重写分支中不生效。
"""
res = super().search_panel_select_range(field_name, **kwargs)
if field_name == 'category_id' and self.env.context.get('shixun_hide_empty_categories'):
res['values'] = [v for v in res.get('values', []) if v.get('__count')]
return res
@api.model
def populate_module_info(self):
"""为 school 自建模块写入「功能亮点 / 模块介绍」文案。
+15
View File
@@ -0,0 +1,15 @@
# -*- coding: utf-8 -*-
from odoo import fields, models
class ResUsers(models.Model):
_inherit = "res.users"
# 登录首页动作:默认改为「产品介绍」college.action_yuthon_product_intro。
# 只覆盖默认值,影响新建用户;存量用户在 migrations/18.0.3.3.0 里批量迁移。
action_id = fields.Many2one(
"ir.actions.actions",
default=lambda self: self.env.ref(
"college.action_yuthon_product_intro", raise_if_not_found=False
).id,
)
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

+135
View File
@@ -0,0 +1,135 @@
/**
* 「平台看板」总览页(工作台式入口)
*
* 背景:这个入口原来是第三方仪表盘模块(ks_dashboard_ninja)自带的一个定制版
* 「工作台 / Overview」页面:欢迎语 + 全部仪表盘/全部图表/总地图/收藏/列表
* 五个统计卡 + 每个看板一张卡片。该定制版在 2026-10-28 被换成官方新版后
* 这个页面就没了,菜单直接落到一个空白看板上。
*
* 做法:不改第三方模块源码(铁律),在 college 里用 OWL 客户端动作重建这一页:
* - 数据全部走 ORM 读 ks_dashboard_ninja.board / .item(只读,不写)
* - 点卡片用 doAction 打开对应的看板(tag=ks_dashboard_ninja + params.ks_dashboard_id),
* 与模块自带「老师看板」「学生看板」两个动作完全一致的调用方式
* - 模块没装 / 无权限时降级成空页面,不报错
*
* 注意:本文件与 dashboard_overview.xml、dashboard_overview.scss 都必须注册进
* __manifest__.py 的 web.assets_backend,改完要 -u college 并强刷浏览器。
*/
/** @odoo-module **/
import { Component, onWillStart, useState } from "@odoo/owl";
import { registry } from "@web/core/registry";
import { useService } from "@web/core/utils/hooks";
const CHART_TYPES = [
"ks_bar_chart",
"ks_horizontalBar_chart",
"ks_pie_chart",
"ks_doughnut_chart",
"ks_line_chart",
"ks_area_chart",
"ks_polarArea_chart",
"ks_radar_chart",
"ks_scatter_chart",
"ks_funnel_chart",
"ks_bullet_chart",
"ks_kpi",
];
const MAP_TYPE = "ks_map";
const LIST_TYPE = "ks_list_view";
export class CollegeDashboardOverview extends Component {
static template = "college.DashboardOverview";
static props = {
action: { type: Object, optional: true },
actionId: { type: [Number, Boolean], optional: true },
className: { type: String, optional: true },
"*": true,
};
setup() {
this.orm = useService("orm");
this.actionService = useService("action");
this.user = useService("user");
this.state = useState({
loading: true,
userName: "",
boards: [],
stats: { boards: 0, charts: 0, maps: 0, favorites: 0, lists: 0 },
error: false,
});
onWillStart(() => this.loadData());
}
async loadData() {
this.state.userName = this.user.name || "";
try {
const boards = await this.orm.searchRead(
"ks_dashboard_ninja.board",
[],
["name"],
{ order: "id" }
);
const items = await this.orm.searchRead(
"ks_dashboard_ninja.item",
[],
["ks_dashboard_ninja_board_id", "ks_dashboard_item_type"]
);
const counts = {};
let charts = 0;
let maps = 0;
let lists = 0;
for (const item of items) {
const rel = item.ks_dashboard_ninja_board_id;
const boardId = Array.isArray(rel) ? rel[0] : rel;
if (!boardId) {
continue;
}
const type = item.ks_dashboard_item_type || "";
counts[boardId] = (counts[boardId] || 0) + 1;
if (type === LIST_TYPE) {
lists += 1;
} else if (type === MAP_TYPE) {
maps += 1;
} else if (CHART_TYPES.includes(type)) {
charts += 1;
}
}
const list = boards.map((board) => ({
id: board.id,
name: board.name,
itemCount: counts[board.id] || 0,
}));
const withItems = list.filter((board) => board.itemCount > 0);
this.state.boards = withItems.length ? withItems : list;
this.state.stats = {
boards: boards.length,
charts,
maps,
favorites: 0,
lists,
};
} catch {
// 模块未安装或无访问权限时保持空页面
this.state.error = true;
} finally {
this.state.loading = false;
}
}
openBoard(board) {
this.actionService.doAction({
type: "ir.actions.client",
tag: "ks_dashboard_ninja",
name: board.name,
params: { ks_dashboard_id: board.id },
context: { ks_dashboard_id: board.id },
});
}
openManager() {
this.actionService.doAction("ks_dashboard_ninja.board_form_tree_action_window");
}
}
registry.category("actions").add("college_dashboard_overview", CollegeDashboardOverview);
+68
View File
@@ -0,0 +1,68 @@
/**
* 隐藏「关于页」(yuthon.about.page) 表单右上角的「编辑」按钮。
*
* 原理:form 视图已写 edit="false",Odoo 18 控制面板本就不该渲染编辑按钮;
* 但为保险(防止某些角色/动作仍带出编辑入口),这里在 FormController 上 patch:
* 当模型为 yuthon.about.page 时,用 MutationObserver 持续监听控制面板,
* 一旦编辑按钮(无论何时、以何种形式出现)被渲染,立刻隐藏。
*
* 注意:本文件改动需要「升级模块」才会生效(-d <库名> -u college),
* 并强刷浏览器 (Ctrl+Shift+R)。仅 git pull + 重启不会重建前端资源包。
*/
/** @odoo-module **/
import { patch } from "@web/core/utils/patch";
import { FormController } from "@web/views/form/form_controller";
import { onMounted, onWillUnmount } from "@odoo/owl";
const ABOUT_MODEL = "yuthon.about.page";
// 判定一个按钮是否为「编辑」按钮(兼容不同渲染形态)
function isEditButton(btn) {
if (!btn || btn.tagName !== "BUTTON") {
return false;
}
if (btn.classList.contains("o_form_button_edit")) {
return true;
}
if (btn.getAttribute("data-hotkey") === "e") {
return true;
}
const text = (btn.textContent || "").trim();
if (text === "编辑" || text.toLowerCase() === "edit") {
return true;
}
return false;
}
// 在控制面板内隐藏所有编辑按钮
function hideAboutEditButtons(root) {
(root || document).querySelectorAll(".o_control_panel button").forEach((btn) => {
if (isEditButton(btn)) {
btn.style.display = "none";
}
});
}
patch(FormController.prototype, {
setup() {
super.setup();
if (this.props.resModel !== ABOUT_MODEL) {
return;
}
let observer = null;
onMounted(() => {
hideAboutEditButtons(document);
// 控制面板按钮可能异步/重渲染后才出现,持续监听最稳妥
const cp = document.querySelector(".o_control_panel");
const target = cp || document.body;
observer = new MutationObserver(() => hideAboutEditButtons(target));
observer.observe(target, { childList: true, subtree: true });
});
onWillUnmount(() => {
if (observer) {
observer.disconnect();
}
});
},
});
@@ -0,0 +1,18 @@
/**
* 隐藏导航栏右侧的「活动」图标(日历+时钟那个,点击弹出"我的活动"下拉)
*
* 原理:导航栏右侧的系统托盘 (systray) 是一个组件注册表,
* mail 模块在加载时把活动菜单注册了进去(见
* odoo18/addons/mail/static/src/core/web/activity_menu.js 最后一行
* registry.category("systray").add("mail.activity_menu", ...))。
* 这里在资源包加载阶段把它从注册表里摘掉,导航栏就不会渲染它。
*
* 如需恢复:删除本文件并把 __manifest__.py assets 里对应的一行去掉即可。
*
* 注意:本文件改动需要「升级模块」才会生效(-d <库名> -u college),
* 并强刷浏览器 (Ctrl+Shift+R)。
*/
/** @odoo-module **/
import { registry } from "@web/core/registry";
registry.category("systray").remove("mail.activity_menu");
@@ -0,0 +1,57 @@
/**
* 右上角头像下拉菜单:只保留「退出登录」
*
* 原理:Odoo 前端把头像菜单的每个条目注册到
* registry.category("user_menuitems") 里(web / web_tour / hr /
* yuthon_base 的 udoo_om_ux、quick_navigation 等模块都会往里加)。
* 这里在资源包加载阶段把白名单之外的条目全部摘掉,并监听 UPDATE 事件,
* 保证晚于本文件注册的条目(例如以后新装的模块)也会被自动移除,
* 因此不用关心资源加载顺序。
*
* 被移除的条目一览(想恢复哪个,把它的 key 加进 KEEP 即可):
* documentation 文档 (web)
* support 支持 (web)
* shortcuts 快捷键 (web)
* profile 偏好 (web / hr)
* odoo_account Odoo.com 账户 (web)
* install_pwa 安装应用 (web)
* separator 分隔线 (web)
* web_tour.tour_enabled 新手上路 (web_tour)
* appearance 外观 (udoo_om_ux)
* separator42 分隔线 (udoo_om_ux)
* quick_menu_config 快捷菜单配置 (quick_navigation)
*
* 注意:本文件改动需要「升级模块 + 重启」才会生效
* (-d <库名> -u college),并强刷浏览器 (Ctrl+Shift+R)。
*/
/** @odoo-module **/
import { registry } from "@web/core/registry";
const KEEP = new Set(["log_out"]);
const userMenuRegistry = registry.category("user_menuitems");
/**
* 移除所有不在白名单里的菜单项。
* 用 getEntries() 先取快照再删,避免边遍历边删。
*/
function pruneUserMenu() {
const keys = userMenuRegistry.getEntries().map(([key]) => key);
for (const key of keys) {
if (!KEEP.has(key)) {
userMenuRegistry.remove(key);
}
}
}
// 1) 清理已经注册过的条目
pruneUserMenu();
// 2) 监听后续新增:只处理 add,不处理 delete,避免自己触发自己造成递归。
// 注意:Owl 的 EventBus 继承自 EventTarget,trigger 用 CustomEvent 派发,
// 监听函数收到的是事件对象,注册表传的 payload 在 ev.detail 里。
userMenuRegistry.addEventListener("UPDATE", (ev) => {
const { operation, key } = ev.detail || {};
if (operation === "add" && !KEEP.has(key)) {
userMenuRegistry.remove(key);
}
});
+98
View File
@@ -0,0 +1,98 @@
/**
* 模块列表(ir.module.module)每页数量锁定为 50,并隐藏分页器里的 "/ 总数"。
*
* 覆盖两个入口(同一模型、同款 kanban,一起受益):
* 1. 应用主页(Odoo 自带「应用」菜单)
* 2. 实训模块页(college 自定义 action)
*
* 原理(三个 patch + 一个模板继承,全在扩展点上,不碰 Odoo 源码、不删任何模块):
* Patch 1 KanbanController.prototype.modelParams(getter):
* 模型为 ir.module.module 时,把初始每页数 limit 钉为 50,
* 并在 modelConfig 上打一个标记(__yuthon_module_list_cap)。
* 打标记是为了和 list 视图区分——设置里的「模块」列表也是这个模型,
* 但它走 ListController、不打标记,所以完全不受影响。
* Patch 2 DynamicList.prototype.load:
* 这是翻页/手输每页数的唯一数据出口(Pager.onUpdate 最终就是调到
* model.root.load({offset, limit}))。带标记且调用方传了 limit 时,
* 强制压回 offset=0 / limit=50 —— 第 2 页进不去、每页数改了也白改。
* Patch 3 KanbanController.prototype.setup:
* 同样是 ir.module.module 看板时,在 env.config 上打一个标记
* (__yuthon_hide_pager_total)。usePager() 在 setup 里通过
* useSubEnv 把 pagerProps 挂到 env.config,control panel 的
* <Pager t-props="pagerProps"/> 渲染时 env 一路继承到这里。
* 模板继承 xml/module_list_pager.xml:t-inherit 扩展官方 web.Pager 模板,
* 给 nav 加 t-att-class——env.config 带上述标记时输出
* o_pager_hide_total,由本文件注入的 CSS 隐藏 " / " 和总数 span。
* (OWL 组件没有内置 root 属性,勿在 patch 里用 this.root.el;
* 那是 ControlPanel 自己 useRef("root") 建的,Pager 模板没有该 ref。)
*
* 效果:页面永远只显示前 50 个模块,分页器只显示 "1-50"(不显示 "/ 56");
* 搜索照常能用,结果同样只取前 50;其他所有页面零影响。
*
* 注意:本文件必须注册进 __manifest__.py 的 web.assets_backend 才会生效;
* 模板继承文件同样要注册;改后必须 -u college 升级模块 + 强刷浏览器
* (Ctrl+Shift+R),仅 git pull + 重启不会重建前端资源包。
*/
/** @odoo-module **/
import { patch } from "@web/core/utils/patch";
import { KanbanController } from "@web/views/kanban/kanban_controller";
import { DynamicList } from "@web/model/relational_model/dynamic_list";
const MODULE_MODEL = "ir.module.module";
const PAGE_LIMIT = 50;
const CAP_FLAG = "__yuthon_module_list_cap";
const HIDE_TOTAL_FLAG = "__yuthon_hide_pager_total";
const HIDE_TOTAL_STYLE_ID = "yuthon-pager-hide-total-style";
// 隐藏分页器 " / 总数" 的样式(只对带 o_pager_hide_total 的分页器生效)。
// .o_pager_value 后面紧跟的裸 span 就是模板里的 "<span> / </span>",
// 配合 .o_pager_limit(总数)一起隐藏;编辑态(input)时同样只剩输入框。
if (!document.getElementById(HIDE_TOTAL_STYLE_ID)) {
const style = document.createElement("style");
style.id = HIDE_TOTAL_STYLE_ID;
style.textContent = [
".o_pager.o_pager_hide_total .o_pager_value + span,",
".o_pager.o_pager_hide_total .o_pager_limit {",
" display: none !important;",
"}",
].join("\n");
document.head.appendChild(style);
}
// Patch 1:kanban 初始每页数锁 50,并给数据链路打标记(仅 kanban,list 不打)
patch(KanbanController.prototype, {
get modelParams() {
const params = super.modelParams;
if (this.props.resModel === MODULE_MODEL) {
params.limit = PAGE_LIMIT;
if (params.config) {
params.config[CAP_FLAG] = PAGE_LIMIT;
}
}
return params;
},
});
// Patch 3:给分页器链路打隐藏标记(usePager 的 useSubEnv 在 setup 内完成,
// super.setup() 之后 env.config 已就位;此处只动 env.config,
// 不碰 pagerProps 对象本身,避免给 Pager 传未声明的 prop。
// 标记由 xml/module_list_pager.xml 的模板继承读取。)
patch(KanbanController.prototype, {
setup() {
super.setup();
if (this.props.resModel === MODULE_MODEL) {
this.env.config[HIDE_TOTAL_FLAG] = true;
}
},
});
// Patch 2:翻页/手输的唯一数据出口,强制压回第 1 页、每页 50 条
patch(DynamicList.prototype, {
load(params) {
const cap = this.config && this.config[CAP_FLAG];
if (cap && params && "limit" in params) {
params = { ...params, offset: 0, limit: cap };
}
return super.load(params);
},
});
@@ -290,3 +290,22 @@
white-space: nowrap; // 不换行
}
// ============================================================
// Dashboard Ninja(ks_dashboard_ninja)看板栅格修复
// ------------------------------------------------------------
// 现象:老师/学生看板里所有卡片被拉成整行宽并相互遮挡,页面看上去是
// 一排彩色色块(图标、数字、标题都被后一张卡片盖住)。
// 原因:该模块初始化 gridstack 时会切到 one-column 模式,容器上带
// `.grid-stack-1`,随之命中 `.grid-stack.grid-stack-1 > .grid-stack-item
// { min-width: 100% }`,把节点真正的 12 栅格宽度([gs-w="N"] 的百分比)
// 覆盖掉了;节点自身的 gs-x/gs-y/gs-w/gs-h 其实都是对的。
// 处理:桌面宽度下解除这个 min-width,让宽度回归 gridstack 的栅格百分比。
// 窄屏(<768px)保留原有单列行为。
// ============================================================
@media (min-width: 768px) {
.grid-stack.grid-stack-1 > .grid-stack-item {
min-width: 0 !important;
}
}
@@ -0,0 +1,264 @@
// ============================================================
// 「平台看板」总览页样式
// 对应组件:college/static/src/js/dashboard_overview.js
// 版式对齐原来那版工作台:欢迎语 + 一排柔和色统计卡 + 看板卡片
// ============================================================
.o_cd_root {
padding: 24px 28px 40px;
min-height: 100%;
background: #f7f9fc;
overflow-y: auto;
}
// ---------- 头部 ----------
.o_cd_header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 22px;
flex-wrap: wrap;
}
.o_cd_welcome {
margin: 0;
font-size: 22px;
font-weight: 600;
color: #212529;
}
.o_cd_header_actions {
display: flex;
align-items: center;
gap: 10px;
}
.o_cd_btn {
height: 36px;
padding: 0 16px;
border-radius: 8px;
border: 1px solid transparent;
font-size: 14px;
line-height: 34px;
cursor: pointer;
transition: all 0.15s ease;
&.o_cd_btn_light {
background: #fff;
border-color: #e3e7ee;
color: #3f444b;
&:hover {
border-color: #cfd6e0;
background: #fbfcfe;
}
}
&.o_cd_btn_primary {
background: #e8564b;
border-color: #e8564b;
color: #fff;
&:hover {
background: #d94a40;
border-color: #d94a40;
}
}
}
// ---------- 统计卡 ----------
.o_cd_stats {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 18px;
margin-bottom: 26px;
}
.o_cd_stat {
display: flex;
align-items: center;
justify-content: space-between;
padding: 18px 20px;
border-radius: 14px;
min-height: 92px;
box-shadow: 0 1px 2px rgba(16, 24, 40, 0.04);
}
.o_cd_stat_text {
display: flex;
flex-direction: column;
gap: 4px;
min-width: 0;
}
.o_cd_stat_num {
font-size: 26px;
font-weight: 600;
line-height: 1.1;
color: #1f2937;
}
.o_cd_stat_label {
font-size: 13px;
color: #4b5563;
white-space: nowrap;
}
.o_cd_stat_icon {
font-size: 26px;
color: #2f343a;
opacity: 0.75;
flex: 0 0 auto;
margin-left: 10px;
}
.o_cd_stat_pink { background: #fde3e4; }
.o_cd_stat_sand { background: #fbeccd; }
.o_cd_stat_mint { background: #ddf3e4; }
.o_cd_stat_lilac { background: #f0e6fb; }
.o_cd_stat_sky { background: #e6eefc; }
// ---------- 看板卡片 ----------
.o_cd_boards {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 380px));
gap: 20px;
}
.o_cd_board {
background: #fff;
border: 1px solid #edf0f5;
border-radius: 14px;
padding: 14px;
cursor: pointer;
transition: box-shadow 0.15s ease, transform 0.15s ease;
&:hover {
box-shadow: 0 8px 20px rgba(16, 24, 40, 0.08);
transform: translateY(-1px);
}
}
.o_cd_board_thumb {
position: relative;
height: 150px;
border-radius: 10px;
background:
radial-gradient(120% 120% at 12% 10%, #ffffff 0%, #f2f5fa 55%, #e9eef7 100%);
border: 1px solid #eef1f6;
display: flex;
align-items: flex-end;
gap: 22px;
padding: 16px 18px;
overflow: hidden;
}
.o_cd_thumb_chart {
flex: 1 1 auto;
height: 100%;
display: flex;
align-items: flex-end;
gap: 8px;
.o_cd_thumb_bar {
flex: 1 1 0;
border-radius: 4px 4px 2px 2px;
background: linear-gradient(180deg, #9db8e8 0%, #7f9fd6 100%);
}
}
.o_cd_thumb_pie {
flex: 0 0 66px;
width: 66px;
height: 66px;
border-radius: 50%;
background: conic-gradient(#e8a0a4 0 33%, #9db8e8 33% 68%, #b9dfc8 68% 100%);
border: 3px solid #fff;
box-shadow: 0 1px 4px rgba(16, 24, 40, 0.08);
margin-bottom: 4px;
}
.o_cd_board_info {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 10px;
margin-top: 12px;
}
.o_cd_board_name {
font-size: 15px;
font-weight: 600;
color: #212529;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.o_cd_board_meta {
font-size: 13px;
color: #6b7280;
flex: 0 0 auto;
}
.o_cd_board_link {
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid #f1f3f7;
font-size: 13px;
color: #e8564b;
display: flex;
align-items: center;
justify-content: flex-end;
gap: 6px;
}
// ---------- 空状态 ----------
.o_cd_empty {
padding: 60px 20px;
text-align: center;
color: #6b7280;
background: #fff;
border: 1px dashed #e3e7ee;
border-radius: 14px;
}
.o_cd_empty_icon {
font-size: 34px;
color: #c3cad6;
display: block;
margin-bottom: 12px;
}
.o_cd_empty_title {
font-size: 15px;
font-weight: 600;
color: #374151;
margin-bottom: 6px;
}
.o_cd_empty_hint {
font-size: 13px;
}
// ---------- 窄屏 ----------
@media (max-width: 1200px) {
.o_cd_stats {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
@media (max-width: 768px) {
.o_cd_root {
padding: 16px;
}
.o_cd_stats {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.o_cd_boards {
grid-template-columns: 1fr;
}
}
@@ -0,0 +1,6 @@
/* 隐藏「关于页」(yuthon.about.page) 表单右上角的「编辑」按钮 —— JS 的 CSS 兜底 */
.o_about_full_form {
.o_form_button_edit {
display: none !important;
}
}
+99
View File
@@ -0,0 +1,99 @@
// ============================================================
// 列表视图:单元格网格线 + 斑马纹 + 悬停高亮(全局生效,纯 CSS)
// 由 Odoo 17 的实现改写并适配 Odoo 18。写进 college 模块,
// 不改 odoo18 源码、不给任何视图加 class、不碰业务代码。
//
// 注意:这里用 SCSS 嵌套来表达长选择器链,**不要**把选择器字符串存进
// SCSS 变量(`$x: body.o_web_client ...`)—— libsass 会把 `body.o_web_client`
// 当成表达式解析报 "Invalid CSS after ...: expected expression"。
//
// 遵守的约束(对应参考提示里的 6 个坑):
// 1. 纯 CSS 挂 web.assets_backend,不继承 QWeb 模板(web.ListView 在 18 里同样没注册成
// ir.model.data,用 inherit_id 会报 External ID not found)
// 2. 不依赖后端 _get_view 注入 class;18 的列表 view_type / arch 根标签同样是 list
// 3. td 只写 border / padding / 背景,绝不写 color、font-weight —— Odoo 原生
// decoration-*(text-success / text-muted / fw-bold 加在 tr 上、靠继承生效)不被盖掉
// 4. 斑马纹 / 悬停写在 td 上(tr 的背景常被主题覆盖),并排除选中行与装饰行
// 5. 选择器长链 + 关键属性 !important,压过主题样式
// 6. 改完要 `-u college` 重建资源包,再 Ctrl+F5 强刷
//
// Odoo 18 与 17 的差异(下面已逐条处理):
// · 未分组列表带 .table-striped,Bootstrap 5.3 用 --bs-table-bg-type 给单元格上色
// → 我们的背景直接写在 td 上并加 !important,稳定压过原生条纹(原生顺序同样是 even)
// · 分组列表的「分组头」行是 tbody > tr > th(不是 td),所以边框规则额外覆盖 tbody th
// · 选中行除 .table-info 外还会带 .o_data_row_selected → 一并排除
// · 可编辑列表(editable="top|bottom")的必填 / 校验失败单元格靠 border-bottom 表达,
// 这里显式还原,避免被网格线的 !important 盖掉(业务提示不能丢)
// · 表格是 border-collapse: collapse,相邻边框会自动合并,无需去重加粗
// ============================================================
$lz-border: #b6c0cc;
$lz-stripe: #F1F6FD; // 偶数数据行
$lz-hover: #cfe2fb; // 悬停数据行
body.o_web_client .o_action_manager .o_list_view {
// 表格本体:不要外框,白底(奇数行的“白”就来自这里)
.o_list_table {
border-collapse: collapse !important;
border: none !important;
background-color: #ffffff !important;
// 表头单元格:加粗 + 统一边框 / 内边距 / 左对齐;底色沿用主题,不写死
thead th {
font-weight: 700 !important;
border: 1px solid $lz-border !important;
padding: 11px 14px !important;
text-align: left !important;
}
// 数据单元格:只声明边框与内边距(颜色、字重交给 tr 继承)
tbody td {
border: 1px solid $lz-border !important;
padding: 10px 14px !important;
}
// 分组头单元格(Odoo 18 里是 tbody th):给网格线,但不动它的字色 / 字重
tbody th {
border: 1px solid $lz-border !important;
padding: 10px 14px !important;
}
// 去掉最外层四边(上 / 下 / 左 / 右),只保留单元格之间的内部网格
thead tr:first-child > * {
border-top: none !important;
}
tbody tr:last-child > * {
border-bottom: none !important;
}
thead tr > *:first-child,
tbody tr > *:first-child {
border-left: none !important;
}
thead tr > *:last-child,
tbody tr > *:last-child {
border-right: none !important;
}
// 可编辑列表:还原 Odoo 的「必填 / 校验失败」提示线(它们用 border-bottom 表达)
tbody tr.o_selected_row > td.o_required_modifier:not(.o_readonly_modifier) {
border-bottom-color: $o-input-border-required !important;
}
tbody tr.o_selected_row > td.o_invalid_cell:not(.o_readonly_modifier) {
border-bottom-color: $danger !important;
}
// 斑马纹:偶数数据行淡蓝,奇数行保持白色(排除选中行、装饰行)
tbody tr.o_data_row:nth-child(even):not(.table-info):not(.o_data_row_selected):not([class*="text-"]) > td {
background-color: $lz-stripe !important;
}
// 悬停高亮:排除选中行,同样写在 td 上
tbody tr.o_data_row:not(.table-info):not(.o_data_row_selected):hover > td {
background-color: $lz-hover !important;
}
}
.o_list_table * {
box-sizing: border-box !important;
}
}
@@ -0,0 +1,40 @@
// ============================================================
// 列表视图网格 / 斑马纹 —— 深色模式补充
// 只注册在 web.assets_web_dark 与 web.assets_backend_lazy_dark(深色主题才加载),
// 浅色模式完全不受影响;颜色按本项目深色约定硬编码
// (页底 #111827、卡片面 #1e293b、描边 #334155),不依赖主题 SCSS 变量。
//
// 选择器比 list_zebra.scss 多一层 .o_list_renderer,特异性更高
// → 无论两个 bundle 的先后顺序如何,都能稳定覆盖浅色值。
// (同样用嵌套写法,不把选择器存进变量。)
// ============================================================
$lz-d-border: #334155;
$lz-d-surface: #1e293b; // 表格底色
$lz-d-stripe: #26344a; // 偶数行
$lz-d-hover: #2f4d78; // 悬停
body.o_web_client .o_action_manager .o_list_view .o_list_renderer {
// 表格本体:深色面,不加外框
.o_list_table {
background-color: $lz-d-surface !important;
border: none !important;
// 网格线换成深色描边(表头 / 数据 / 分组头)
thead th,
tbody td,
tbody th {
border-color: $lz-d-border !important;
}
// 斑马纹(排除选中行、装饰行,与浅色版一致)
tbody tr.o_data_row:nth-child(even):not(.table-info):not(.o_data_row_selected):not([class*="text-"]) > td {
background-color: $lz-d-stripe !important;
}
// 悬停高亮
tbody tr.o_data_row:not(.table-info):not(.o_data_row_selected):hover > td {
background-color: $lz-d-hover !important;
}
}
}
@@ -0,0 +1,96 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<!-- 「平台看板」总览页:欢迎语 + 五个统计卡 + 看板卡片(点卡片进入对应看板) -->
<t t-name="college.DashboardOverview">
<div class="o_cd_root">
<div class="o_cd_header">
<h1 class="o_cd_welcome">
欢迎 <span t-esc="state.userName"/>
</h1>
<div class="o_cd_header_actions">
<button type="button" class="o_cd_btn o_cd_btn_light" t-on-click="openManager">
全部仪表板
</button>
<button type="button" class="o_cd_btn o_cd_btn_primary" t-on-click="openManager">
<i class="fa fa-plus me-1"/>添加新仪表板
</button>
</div>
</div>
<div class="o_cd_stats">
<div class="o_cd_stat o_cd_stat_pink">
<div class="o_cd_stat_text">
<div class="o_cd_stat_num" t-esc="state.stats.boards"/>
<div class="o_cd_stat_label">全部仪表盘</div>
</div>
<div class="o_cd_stat_icon"><i class="fa fa-th-large"/></div>
</div>
<div class="o_cd_stat o_cd_stat_sand">
<div class="o_cd_stat_text">
<div class="o_cd_stat_num" t-esc="state.stats.charts"/>
<div class="o_cd_stat_label">全部图表</div>
</div>
<div class="o_cd_stat_icon"><i class="fa fa-bar-chart"/></div>
</div>
<div class="o_cd_stat o_cd_stat_mint">
<div class="o_cd_stat_text">
<div class="o_cd_stat_num" t-esc="state.stats.maps"/>
<div class="o_cd_stat_label">总地图</div>
</div>
<div class="o_cd_stat_icon"><i class="fa fa-map-o"/></div>
</div>
<div class="o_cd_stat o_cd_stat_lilac">
<div class="o_cd_stat_text">
<div class="o_cd_stat_num" t-esc="state.stats.favorites"/>
<div class="o_cd_stat_label">收藏的仪表盘</div>
</div>
<div class="o_cd_stat_icon"><i class="fa fa-bookmark-o"/></div>
</div>
<div class="o_cd_stat o_cd_stat_sky">
<div class="o_cd_stat_text">
<div class="o_cd_stat_num" t-esc="state.stats.lists"/>
<div class="o_cd_stat_label">全部列表</div>
</div>
<div class="o_cd_stat_icon"><i class="fa fa-list-ul"/></div>
</div>
</div>
<div class="o_cd_boards" t-if="!state.loading &amp;&amp; state.boards.length">
<div class="o_cd_board"
t-foreach="state.boards"
t-as="board"
t-key="board.id"
t-on-click="() => this.openBoard(board)">
<div class="o_cd_board_thumb">
<div class="o_cd_thumb_chart">
<span class="o_cd_thumb_bar" t-att-style="'height:' + (30 + (board.id * 13) % 45) + '%'"/>
<span class="o_cd_thumb_bar" t-att-style="'height:' + (55 + (board.id * 7) % 35) + '%'"/>
<span class="o_cd_thumb_bar" t-att-style="'height:' + (40 + (board.id * 17) % 40) + '%'"/>
<span class="o_cd_thumb_bar" t-att-style="'height:' + (65 + (board.id * 5) % 30) + '%'"/>
</div>
<div class="o_cd_thumb_pie"/>
</div>
<div class="o_cd_board_info">
<div class="o_cd_board_name" t-esc="board.name"/>
<div class="o_cd_board_meta">
<t t-esc="board.itemCount"/> 图表
</div>
</div>
<div class="o_cd_board_link">
查看仪表板 <i class="fa fa-angle-right"/>
</div>
</div>
</div>
<div class="o_cd_empty" t-if="!state.loading &amp;&amp; !state.boards.length">
<i class="fa fa-bar-chart o_cd_empty_icon"/>
<div class="o_cd_empty_title">还没有可用的仪表盘</div>
<div class="o_cd_empty_hint">点右上角「添加新仪表板」创建第一个。</div>
</div>
</div>
</t>
</templates>
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<!--
模块列表分页器隐藏 "/ 总数"(配合 js/module_list_cap.js)。
KanbanController.setup 在 env.config 上打 __yuthon_hide_pager_total 标记
(仅 ir.module.module 看板),Pager 的 env 一路继承该 config。
这里给官方 web.Pager 的 nav 加条件 class:带标记时输出
o_pager_hide_total,JS 注入的 CSS 随即将 " / " 与总数隐藏。
为什么走模板继承而不是 patch Pager.prototype.setup 操作 DOM:
OWL 组件没有内置 root 属性(ControlPanel 的 this.root 是它自己
useRef("root") 建的,Pager 模板没有该 ref),模板继承是官方
扩展点,且 Pager 每次重建都会自动带上 class。
-->
<t t-inherit="web.Pager" t-inherit-mode="extension">
<xpath expr="//nav" position="attributes">
<attribute name="t-att-class">(env.config &amp;&amp; env.config.__yuthon_hide_pager_total ? 'o_pager_hide_total ' : '') + 'o_pager d-flex gap-2 h-100'</attribute>
</xpath>
</t>
</templates>
+2 -1
View File
@@ -7,7 +7,8 @@
<field name="name">yuthon.about.page.form</field>
<field name="model">yuthon.about.page</field>
<field name="arch" type="xml">
<form class="o_about_full_form">
<!-- create/edit 关掉:关于页内容由代码预置维护,页面右上角不再显示「创建/编辑」按钮 -->
<form class="o_about_full_form" create="false" edit="false">
<field name="content" widget="html" readonly="1" class="o_about_full"/>
</form>
</field>
+19 -2
View File
@@ -33,8 +33,8 @@
invisible="[('state', '!=', 'published')]"/>
<button name="action_to_draft" type="object" string="转为草稿"
invisible="[('state', '=', 'draft')]"/>
<field name="state" widget="statusbar"
statusbar_visible="draft,published,archived"/>
<!-- <field name="state" widget="statusbar"-->
<!-- statusbar_visible="draft,published,archived"/>-->
</header>
<sheet>
<group>
@@ -104,6 +104,23 @@
<page string="业务情景">
<field name="business_scenario"/>
</page>
<page string="学习进度" groups="college.group_teacher,base.group_system">
<field name="progress_ids" mode="kanban,list" readonly="1"
context="{'kanban_view_ref': 'college.yuthon_school_progress_view_kanban', 'tree_view_ref': 'college.yuthon_school_progress_view_list'}"/>
</page>
<page string="作业布置" groups="college.group_teacher,base.group_system">
<field name="assignment_ids">
<list>
<field name="name" string="作业标题"/>
<field name="chapter_id" string="对应章节"/>
<field name="due_date" string="截止日期"/>
<field name="submission_count" string="提交数"/>
<field name="state" string="状态"/>
</list>
</field>
</page>
</notebook>
</sheet>
<chatter/>
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- 「平台看板」总览页的客户端动作(组件见 static/src/js/dashboard_overview.js)
说明:菜单「平台看板」仍沿用仪表盘模块的菜单记录,只把它的 action 指到这里;
这样不新增重复入口,也不改第三方模块源码。 -->
<record id="action_college_dashboard_overview" model="ir.actions.client">
<field name="name">平台看板</field>
<field name="tag">college_dashboard_overview</field>
</record>
</odoo>
+8 -2
View File
@@ -44,9 +44,13 @@
<field name="name" placeholder="请输入作业标题"/>
</h1>
</div>
<group>
<group>
<field name="course_id"/>
<field name="chapter_id"
domain="[('course_id', '=', course_id)]"
context="{'default_course_id': course_id}"/>
<field name="teacher_id" readonly="1"/>
<field name="due_date"/>
</group>
@@ -55,6 +59,7 @@
<field name="attachment_filename" invisible="1"/>
</group>
</group>
<notebook>
<page string="作业说明">
<field name="description"/>
@@ -119,7 +124,7 @@
<field name="name">yuthon.school.assignment.submission.list</field>
<field name="model">yuthon.school.assignment.submission</field>
<field name="arch" type="xml">
<list string="作业提交" multi_edit="1">
<list string="作业提交" multi_edit="1" create="0">
<header>
<button name="action_open_batch_wizard"
type="object" string="批量批改"
@@ -146,7 +151,8 @@
<field name="arch" type="xml">
<kanban default_group_by="state"
default_order="due_date desc, id"
class="college-course-kanban college-kanban-homework o_kanban_mobile">
class="college-course-kanban college-kanban-homework o_kanban_mobile"
create="0">
<field name="assignment_id"/>
<field name="student_id"/>
<field name="score"/>
+16 -2
View File
@@ -18,7 +18,7 @@
<!-- ===================== 我的实训(顶级菜单,置顶;作为实训相关功能的容器) ===================== -->
<menuitem id="menu_yuthon_training"
name="我的实训"
name="实训管理"
web_icon="college,static/src/img/training.png"
sequence="1"
groups="college.group_teacher,college.group_student,base.group_system"/>
@@ -179,7 +179,7 @@ else:
sequence="10"
groups="college.group_teacher,base.group_system"/>
<menuitem id="menu_yuthon_exam_manage"
name="考试管理"
name="考试列表"
parent="menu_yuthon_exam_root"
action="action_yuthon_school_exam"
sequence="20"
@@ -340,6 +340,20 @@ else:
<field name="target">current</field>
</record>
<!-- 团队介绍:打开预置记录 about_page_team -->
<record id="action_yuthon_about_team" model="ir.actions.act_window">
<field name="name">团队介绍</field>
<field name="res_model">yuthon.about.page</field>
<field name="view_mode">form</field>
<field name="res_id" ref="about_page_team"/>
<field name="target">current</field>
</record>
<menuitem id="menu_yuthon_about_team"
name="团队介绍"
parent="menu_yuthon_about"
action="action_yuthon_about_team"
sequence="30"/>
<!-- 课程导入动作(关联 yuthon.school.course.import.wizard) -->
<record id="action_yuthon_school_course_import_wizard" model="ir.actions.act_window">
<field name="name">课程导入</field>
+7 -3
View File
@@ -27,8 +27,8 @@ class LearningCourse(models.Model):
syllabus_ids = fields.One2many('course.syllabus', 'course_id', string='教学大纲/教案')
syllabus_count = fields.Integer(string='大纲/教案数', compute='_compute_syllabus_count', store=True)
# 按类型统计
syllabus_count_total = fields.Integer(string='大纲总数', compute='_compute_syllabus_count')
lesson_plan_count = fields.Integer(string='教案总数', compute='_compute_syllabus_count')
syllabus_count_total = fields.Integer(string='大纲总数', compute='_compute_syllabus_type_count')
lesson_plan_count = fields.Integer(string='教案总数', compute='_compute_syllabus_type_count')
# ==================== 统计资源(通过章节关联) ====================
resource_count = fields.Integer(string='资源总数', compute='_compute_resource_count', store=True)
video_count = fields.Integer(string='视频数', compute='_compute_resource_count', store=True)
@@ -56,10 +56,14 @@ class LearningCourse(models.Model):
for record in self:
record.chapter_count = len(record.chapter_ids)
@api.depends('syllabus_ids', 'syllabus_ids.syllabus_type')
@api.depends('syllabus_ids')
def _compute_syllabus_count(self):
for record in self:
record.syllabus_count = len(record.syllabus_ids)
@api.depends('syllabus_ids', 'syllabus_ids.syllabus_type')
def _compute_syllabus_type_count(self):
for record in self:
record.syllabus_count_total = len(record.syllabus_ids.filtered(lambda s: s.syllabus_type == 'syllabus'))
record.lesson_plan_count = len(record.syllabus_ids.filtered(lambda s: s.syllabus_type == 'lesson_plan'))
+1 -1
View File
@@ -3,6 +3,6 @@
<!-- 让「我的实训」直接打开「我的实验环境」(My Lab Environment),
去掉中间的「实验环境」子菜单层。 -->
<menuitem id="college.menu_yuthon_training"
name="我的实训"
name="实训管理"
action="action_odoosh_my_env"/>
</odoo>
+2
View File
@@ -0,0 +1,2 @@
# recents_limit —— 纯前端补丁模块,无 Python 模型
# 仅通过 JS patch 把导航栏「最近」分组上限从 50 改为 20。
+26
View File
@@ -0,0 +1,26 @@
{
'name': 'Recents Limit (20)',
'version': '18.0.1.0.0',
'summary': '把导航栏「最近」分组上限从 50 改为 20(继承 udoo_om_ux,不改动线上源码)',
'description': """
通过前端 patch 继承 udoo_om_ux 的 OmuxNavBar,把「最近访问」的分组上限
从默认的 50 截到 20。
实现方式(不改动 udoo_om_ux 与 yuthon_base 的任何源码):
1. setup 时把历史 localStorage 中可能 >20 的 recents 截断到 20;
2. 监听 UDOO:FRC 事件,在每次新增「最近」后把上限压回 20。
由于整个会话期间 this.uo.recents 恒 <= 20,beforeunload 持久化到
localStorage 时自然也 <= 20,无需再 patch beforeunload 逻辑。
""",
'category': 'Tools',
'depends': ['udoo_om_ux', 'web'],
'assets': {
'web.assets_backend': [
'recents_limit/static/src/js/recents_limit.js',
],
},
'installable': True,
'application': False,
'license': 'LGPL-3',
'author': 'Yuthon',
}
@@ -0,0 +1,29 @@
/** @odoo-module **/
import { patch } from "@web/core/utils/patch";
import { OmuxNavBar } from "@udoo_om_ux/webclient/navbar/navbar";
import { useBus } from "@web/core/utils/hooks";
// 「最近访问」分组的目标上限(线上 udoo_om_ux 默认为 50)
const MAX_RECENTS = 20;
patch(OmuxNavBar.prototype, {
setup() {
super.setup();
// 1) 处理从历史 localStorage 加载进来的 >20 条「最近」
if (this.uo?.recents?.length > MAX_RECENTS) {
this.uo.recents.length = MAX_RECENTS;
}
// 2) 实时:每次新增「最近」访问后,把上限压回 20。
// 原 OmuxNavBar 在 UDOO:FRC 事件里会把上限维持在 50,
// 这里在其之后(先注册者先触发)再截到 20,
// 于是整个会话期间 this.uo.recents 恒 <= 20,
// 连带 beforeunload 持久化到 localStorage 也自然 <= 20。
useBus(this.env.bus, "UDOO:FRC", () => {
if (this.uo?.recents?.length > MAX_RECENTS) {
this.uo.recents.length = MAX_RECENTS;
}
});
},
});
@@ -27,7 +27,9 @@ class StudentOrganization(models.Model):
parent_path = fields.Char(index=True)
# 完整名称(显示层级)
full_name = fields.Char(string='完整名称', compute='_compute_full_name', store=True)
# recursive=True: full_name 依赖 parent_id.full_name(自引用层级),
# 必须声明,否则 Odoo 18 报 UserWarning 且父级改名时子级不会重算
full_name = fields.Char(string='完整名称', compute='_compute_full_name', store=True, recursive=True)
# 负责人信息(学院用)
manager_id = fields.Many2one('res.users', string='负责人')
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

+1
View File
@@ -0,0 +1 @@
from . import models
+33
View File
@@ -0,0 +1,33 @@
{
'name': 'Yuthon 员工管理',
'version': '18.0.1.0.0',
'summary': '大学人事基础:教职工档案(工号/部门/岗位/基本工资)',
'description': """
教学模块①(基础层):yuthon_employee
======================================
定义教职工(员工)主数据模型 yuthon.employee,是整个「出勤-薪资」体系的
数据根(root)。后续 yuthon_attendance、yuthon_salary 都依赖本模块。
教学要点:
- 模型定义:_name / _description
- 字段类型:Char / Selection / Float / Date / Boolean
- 唯一约束:_sql_constraints(工号不可重复)
- 模块组合:被其它模块通过 _inherit 追加反向关联(attendance_ids / payslip_ids)
- ACL:ir.model.access.csv 授权
""",
'author': 'Yuthon',
'website': '',
'category': 'Education',
'depends': ['base', 'web'],
'data': [
'security/ir.model.access.csv',
'views/employee_views.xml',
],
'demo': [
'data/demo.xml',
],
'license': 'LGPL-3',
'installable': True,
'application': True,
'auto_install': False,
}
+39
View File
@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- 演示数据:3 名教职工(数据根)。出勤/薪资 demo 通过 ref 引用这里的 id。 -->
<record id="employee_zhang" model="yuthon.employee">
<field name="employee_no">T2024001</field>
<field name="name">张三</field>
<field name="gender">male</field>
<field name="department">teaching</field>
<field name="job_title">讲师</field>
<field name="phone">13800000001</field>
<field name="email">zhangsan@school.edu</field>
<field name="base_salary">8000.0</field>
<field name="hire_date">2024-09-01</field>
</record>
<record id="employee_li" model="yuthon.employee">
<field name="employee_no">A2024002</field>
<field name="name">李四</field>
<field name="gender">female</field>
<field name="department">admin</field>
<field name="job_title">教务员</field>
<field name="phone">13800000002</field>
<field name="email">lisi@school.edu</field>
<field name="base_salary">6000.0</field>
<field name="hire_date">2024-09-01</field>
</record>
<record id="employee_wang" model="yuthon.employee">
<field name="employee_no">T2024003</field>
<field name="name">王五</field>
<field name="gender">male</field>
<field name="department">teaching</field>
<field name="job_title">副教授</field>
<field name="phone">13800000003</field>
<field name="email">wangwu@school.edu</field>
<field name="base_salary">12000.0</field>
<field name="hire_date">2024-09-01</field>
</record>
</odoo>
+1
View File
@@ -0,0 +1 @@
from . import employee
+74
View File
@@ -0,0 +1,74 @@
# -*- coding: utf-8 -*-
from odoo import fields, models, api
class Employee(models.Model):
"""教职工(员工)主数据模型。
这是「出勤-薪资」体系的**数据根**:出勤记录挂在员工下,
工资条在月底把「员工基本工资 + 当月出勤聚合结果」join 在一起算出来。
"""
_name = 'yuthon.employee'
_description = '教职工(员工)'
_order = 'employee_no'
# ---- 基础信息 ----
employee_no = fields.Char(
string='工号',
required=True,
index=True,
help='唯一工号,如 T2024001(教师)/ A2024002(行政)',
)
name = fields.Char(
string='姓名',
required=True,
)
gender = fields.Selection(
[('male', '男'), ('female', '女')],
string='性别',
)
department = fields.Selection(
[
('teaching', '教学部'),
('admin', '行政部'),
('logistics', '后勤部'),
('support', '教辅部'),
],
string='部门',
required=True,
default='teaching',
)
job_title = fields.Char(
string='岗位 / 职称',
help='如:讲师、副教授、教务员',
)
phone = fields.Char(string='联系电话')
email = fields.Char(string='邮箱')
# ---- 薪资相关(被 yuthon_salary 读取)----
base_salary = fields.Float(
string='基本工资',
default=0.0,
help='教学演示用 Float;生产环境建议改用 Monetary + currency_id',
)
hire_date = fields.Date(string='入职日期')
active = fields.Boolean(
string='在职',
default=True,
)
# ---- 反向关联(由 yuthon_attendance / yuthon_salary 通过 _inherit 追加)----
# attendance_ids / payslip_ids 不在此定义,避免基础模块反向依赖业务模块。
# 这是「模块解耦」的核心:根模块不知道子模块的存在。
# ---- 约束 ----
_sql_constraints = [
('uniq_employee_no', 'unique(employee_no)', '工号已存在,不可重复!'),
]
@api.constrains('base_salary')
def _check_base_salary(self):
for rec in self:
if rec.base_salary < 0:
raise models.ValidationError('基本工资不能为负数。')
@@ -0,0 +1,2 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_yuthon_employee,yuthon.employee,yuthon_employee.model_yuthon_employee,base.group_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_yuthon_employee yuthon.employee yuthon_employee.model_yuthon_employee base.group_user 1 1 1 1
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

+88
View File
@@ -0,0 +1,88 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- ============ 列表视图 ============ -->
<record id="view_yuthon_employee_list" model="ir.ui.view">
<field name="name">yuthon.employee.list</field>
<field name="model">yuthon.employee</field>
<field name="arch" type="xml">
<list string="教职工">
<field name="employee_no"/>
<field name="name"/>
<field name="gender"/>
<field name="department"/>
<field name="job_title"/>
<field name="base_salary"/>
<field name="active" optional="hide"/>
</list>
</field>
</record>
<!-- ============ 表单视图 ============ -->
<record id="view_yuthon_employee_form" model="ir.ui.view">
<field name="name">yuthon.employee.form</field>
<field name="model">yuthon.employee</field>
<field name="arch" type="xml">
<form string="教职工">
<sheet>
<group>
<group>
<field name="employee_no"/>
<field name="name"/>
<field name="gender"/>
<field name="department"/>
<field name="job_title"/>
</group>
<group>
<field name="phone"/>
<field name="email"/>
<field name="base_salary"/>
<field name="hire_date"/>
<field name="active"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<!-- ============ 搜索视图 ============ -->
<record id="view_yuthon_employee_search" model="ir.ui.view">
<field name="name">yuthon.employee搜索</field>
<field name="model">yuthon.employee</field>
<field name="arch" type="xml">
<search string="搜索教职工">
<field name="employee_no"/>
<field name="name"/>
<field name="department"/>
<separator/>
<filter name="filter_teaching" string="教学部"
domain="[('department', '=', 'teaching')]"/>
<filter name="filter_active" string="在职"
domain="[('active', '=', True)]"/>
<separator/>
<group expand="0" string="分组">
<filter name="groupby_department" string="按部门"
context="{'group_by': 'department'}"/>
</group>
</search>
</field>
</record>
<!-- ============ 动作 ============ -->
<record id="action_yuthon_employee" model="ir.actions.act_window">
<field name="name">教职工</field>
<field name="res_model">yuthon.employee</field>
<field name="view_mode">list,form</field>
<field name="search_view_id" ref="view_yuthon_employee_search"/>
</record>
<!-- ============ 根菜单(人事管理) ============ -->
<menuitem id="menu_yuthon_hr_root" name="人事管理"
web_icon="yuthon_employee,static/description/icon.png"
sequence="10"/>
<menuitem id="menu_yuthon_employee" name="教职工"
parent="menu_yuthon_hr_root"
action="action_yuthon_employee" sequence="20"/>
</odoo>