完善教育 #62
@@ -1,6 +1,6 @@
|
||||
{
|
||||
'name': '教育',
|
||||
'version': '18.0.3.2.0',
|
||||
'version': '18.0.3.3.0',
|
||||
'summary': 'Odoo 18 教学管理平台 — 师生管理 + 课程管理 + 作业/进展(老师·学生双角色)',
|
||||
'description': """
|
||||
Odoo 18 教学管理平台(面向老师与学生,双角色权限)
|
||||
@@ -63,7 +63,11 @@
|
||||
'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',
|
||||
],
|
||||
'web.assets_web_dark': [
|
||||
'college/static/src/scss/college_theme_dark.scss',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,7 +230,7 @@
|
||||
|
||||
<!-- 页脚 -->
|
||||
<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>
|
||||
@@ -244,52 +243,47 @@
|
||||
<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:#fff;line-height:1.65;">
|
||||
<!-- 顶部 banner:深蓝科技风 -->
|
||||
<div style="background:linear-gradient(135deg,#0f172a 0%,#1e40af 50%,#0ea5e9 100%);padding:72px 32px 56px;text-align:center;position:relative;overflow:hidden;">
|
||||
<!-- 装饰圆 -->
|
||||
<div style="position:absolute;top:-60px;left:-60px;width:220px;height:220px;background:rgba(255,255,255,0.06);border-radius:50%;"></div>
|
||||
<div style="position:absolute;bottom:-40px;right:-40px;width:180px;height:180px;background:rgba(255,255,255,0.05);border-radius:50%;"></div>
|
||||
|
||||
<!-- 顶部胶囊标签 -->
|
||||
<div style="display:inline-flex;align-items:center;background:rgba(255,255,255,0.10);border:1px solid rgba(255,255,255,0.18);border-radius:999px;padding:6px 18px 6px 8px;margin-bottom:28px;position:relative;z-index:1;">
|
||||
<span style="display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;background:#facc15;color:#0f172a;border-radius:8px;font-weight:800;font-size:14px;margin-right:10px;">P</span>
|
||||
<span style="font-size:14px;color:#e2e8f0;letter-spacing:0.5px;">Peng Yuthon</span>
|
||||
</div>
|
||||
|
||||
<h1 style="font-size:42px;font-weight:800;margin:0 0 16px;color:#fff !important;position:relative;z-index:1;">鹏程万里 气宇轩昂</h1>
|
||||
<p style="font-size:18px;margin:0 0 36px;color:#cbd5e1;position:relative;z-index:1;">软件 · 硬件 · AI 智能体</p>
|
||||
|
||||
<!-- 底部胶囊标签 -->
|
||||
<div style="display:flex;flex-wrap:wrap;justify-content:center;gap:12px;position:relative;z-index:1;">
|
||||
<span style="background:rgba(255,255,255,0.08);border:1px solid rgba(255,255,255,0.15);border-radius:999px;padding:8px 18px;font-size:14px;color:#e2e8f0;">软件</span>
|
||||
<span style="background:rgba(255,255,255,0.08);border:1px solid rgba(255,255,255,0.15);border-radius:999px;padding:8px 18px;font-size:14px;color:#e2e8f0;">硬件</span>
|
||||
<span style="background:rgba(255,255,255,0.08);border:1px solid rgba(255,255,255,0.15);border-radius:999px;padding:8px 18px;font-size:14px;color:#e2e8f0;">AI 智能体</span>
|
||||
<span style="background:rgba(255,255,255,0.08);border:1px solid rgba(255,255,255,0.15);border-radius:999px;padding:8px 18px;font-size:14px;color:#e2e8f0;">工程化教育</span>
|
||||
<span style="background:rgba(255,255,255,0.08);border:1px solid rgba(255,255,255,0.15);border-radius:999px;padding:8px 18px;font-size:14px;color:#e2e8f0;">企业实训</span>
|
||||
<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:36px 28px;background:#fff;color:#334155;">
|
||||
<h2 style="color:#0f172a;font-size:20px;margin:0 0 16px;">关于我们</h2>
|
||||
<p style="margin:0 0 28px;font-size:15px;color:#475569;">
|
||||
<!-- 关于我们 -->
|
||||
<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>
|
||||
|
||||
<h2 style="color:#0f172a;font-size:20px;margin:0 0 16px;">联系方式</h2>
|
||||
<ul style="list-style:none;padding:0;margin:0;font-size:15px;line-height:2;color:#475569;">
|
||||
<li><b style="color:#0f172a;">B 站:</b>bilibili: pengyuthon</li>
|
||||
<li><b style="color:#0f172a;">邮箱:</b>pengyuthon@163.com</li>
|
||||
<li><b style="color:#0f172a;">官网:</b>pengyuthon.cn</li>
|
||||
<li><b style="color:#0f172a;">学习平台:</b>edu.pengyuthon.cn</li>
|
||||
<!-- 联系方式 -->
|
||||
<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="padding:18px 24px;background:#0f172a;color:#94a3b8;font-size:13px;text-align:center;">
|
||||
<!-- 版权:浅色小字,去掉深色色块 -->
|
||||
<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>
|
||||
|
||||
@@ -8,21 +8,24 @@
|
||||
<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>
|
||||
<!-- 过滤目标:白名单制——只有列在下方 name 清单里的模块才显示(只过滤、不删模块),
|
||||
使应用列表精确收敛到 50 条(limit=50,无第 2 页)。
|
||||
方案演变:2026-10-16 曾用「类目 ID 排除 + 模块名排除」收敛到 50,但类目 ID
|
||||
跨数据库不一致(企业库与本地库映射完全不同,极易误伤,旧注释的映射已作废);
|
||||
2026-10-19 改为白名单:按模块名(跨库稳定),且显示内容完全可控。
|
||||
名单构成(共 50 个):
|
||||
1) 自有模块 27 个——agent_* 智能体平台、college(教育)、school_base、
|
||||
student_organization、learning_center、odoosh_connector、yuthon_* 人事薪酬系列、
|
||||
game_* / yt_world / yusen_website 宇森游戏与官网;
|
||||
2) Odoo 原生精选 23 个——销售/CRM/采购/库存/财务/POS/项目/人事/制造/质量/
|
||||
维护/服务台/知识/网站/在线学习等教学实训常用套件。
|
||||
未列入的说明:
|
||||
- school(未安装,且与 college 重名「教育」,避免列表出现两张同名卡片);
|
||||
- lucky_theme_backend / many2many_tags_preview / omux_menu_center(第三方 UI 模块);
|
||||
- 其余 Odoo 原生小应用(estate/will_task/data_recycle/room/lunch 等)。
|
||||
维护:新装的自有模块必须往本清单里加,否则不会显示;总数超过 50 时
|
||||
module_list_cap.js 把显示硬顶在 50(第 2 页点不动、每页数锁定)。 -->
|
||||
<field name="domain">[('application', '=', True), ('name', 'in', ['account', 'accountant', 'agent_api', 'agent_channel', 'agent_core', 'agent_market', 'agent_payment', 'agent_portal', 'college', 'crm', 'game_ai_npc', 'game_base', 'helpdesk', 'hr', 'hr_attendance', 'hr_expense', 'hr_holidays', 'hr_recruitment', 'knowledge', 'learning_center', 'maintenance', 'mrp', 'mrp_workorder', 'odoosh_connector', 'point_of_sale', 'pos_restaurant', 'project', 'purchase', 'quality_control', 'sale_management', 'school_base', 'stock', 'student_organization', 'survey', 'website', 'website_slides', 'yt_world', 'yusen_website', 'yuthon_applicant', 'yuthon_attendance', 'yuthon_contract', 'yuthon_employee', 'yuthon_hr_attendance', 'yuthon_hr_employee', 'yuthon_notice', 'yuthon_salary', 'yuthon_school', 'yuthon_wage', 'yuthon_web', 'yuthon_will_task'])]</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>
|
||||
|
||||
@@ -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})
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
@@ -1,7 +1,6 @@
|
||||
// 关于页(yuthon.about.page)为只读展示页:
|
||||
// form 视图已设 edit="false",但教师组有该模型写权限时控制面板仍会渲染「编辑」按钮。
|
||||
// 这里用 CSS 做兜底隐藏(若按钮恰好在 form 内),控制面板中的按钮由同名的 JS patch 处理。
|
||||
.o_form_view.o_about_full_form .o_form_button_edit,
|
||||
.o_form_view.o_about_full_form button[data-hotkey="e"] {
|
||||
display: none !important;
|
||||
/* 隐藏「关于页」(yuthon.about.page) 表单右上角的「编辑」按钮 —— JS 的 CSS 兜底 */
|
||||
.o_about_full_form {
|
||||
.o_form_button_edit {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 && env.config.__yuthon_hide_pager_total ? 'o_pager_hide_total ' : '') + 'o_pager d-flex gap-2 h-100'</attribute>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -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"
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
<!-- 让「我的实训」直接打开「我的实验环境」(My Lab Environment),
|
||||
去掉中间的「实验环境」子菜单层。 -->
|
||||
<menuitem id="college.menu_yuthon_training"
|
||||
name="我的实训"
|
||||
name="实训管理"
|
||||
action="action_odoosh_my_env"/>
|
||||
</odoo>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
from . import models
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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>
|
||||
@@ -0,0 +1 @@
|
||||
from . import employee
|
||||
@@ -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
|
||||
|
Binary file not shown.
|
After Width: | Height: | Size: 7.3 KiB |
@@ -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>
|
||||
Reference in New Issue
Block a user