Compare commits
57
Commits
d73bd82769
...
qtb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b89e041f46 | ||
|
|
f1a124a2f6 | ||
|
|
9cb740b17c | ||
|
|
c0da213161 | ||
|
|
0f7955d5bf | ||
|
|
0d8008868a | ||
|
|
8ea25712b5 | ||
|
|
1f985e96c7 | ||
|
|
788203f9a0 | ||
|
|
24eac518af | ||
|
|
371ff7a2c8 | ||
|
|
befeb1b60c | ||
|
|
11c77c7496 | ||
|
|
ccff84e9f5 | ||
|
|
77739b113c | ||
|
|
e720156c16 | ||
|
|
f300271bf9 | ||
|
|
079ee13e1f | ||
|
|
e5596aa299 | ||
|
|
1728ac79b7 | ||
|
|
6a977a50ab | ||
|
|
23a90b6b1e | ||
|
|
9fbbdb0c8e | ||
|
|
d9014ad665 | ||
|
|
4792c432c2 | ||
|
|
514f897889 | ||
|
|
2025b4e344 | ||
|
|
fd091b566b | ||
|
|
6a0f608ec2 | ||
|
|
78477e45de | ||
|
|
30db2702bd | ||
|
|
fea48a2623 | ||
|
|
6eb765546d | ||
|
|
cc92428709 | ||
|
|
e4f44b1287 | ||
|
|
f1c9059887 | ||
|
|
d17e342196 | ||
|
|
a3fc5395ea | ||
|
|
260a7347fb | ||
|
|
802b8efa39 | ||
|
|
ec4c918899 | ||
|
|
87e8f0c636 | ||
|
|
8ea8b089af | ||
|
|
dfa665bbde | ||
|
|
8939e61094 | ||
|
|
0a96d0e868 | ||
|
|
ab62d0e79b | ||
|
|
d84c482c84 | ||
|
|
7b393414b3 | ||
|
|
fa5b2c649e | ||
|
|
27b17aeaa6 | ||
|
|
246326ed9c | ||
|
|
c84da2ed10 | ||
|
|
9d5a60af09 | ||
|
|
d1a217f8cf | ||
|
|
4cdbaf9799 | ||
|
|
63c87c7e56 |
+14
-1
@@ -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,
|
||||
|
||||
@@ -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,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>
|
||||
|
||||
@@ -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})
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
@@ -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):
|
||||
|
||||
@@ -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 自建模块写入「功能亮点 / 模块介绍」文案。
|
||||
|
||||
@@ -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 |
@@ -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);
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 && 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 && !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 && env.config.__yuthon_hide_pager_total ? 'o_pager_hide_total ' : '') + 'o_pager d-flex gap-2 h-100'</attribute>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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'))
|
||||
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
<!-- 让「我的实训」直接打开「我的实验环境」(My Lab Environment),
|
||||
去掉中间的「实验环境」子菜单层。 -->
|
||||
<menuitem id="college.menu_yuthon_training"
|
||||
name="我的实训"
|
||||
name="实训管理"
|
||||
action="action_odoosh_my_env"/>
|
||||
</odoo>
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
# recents_limit —— 纯前端补丁模块,无 Python 模型
|
||||
# 仅通过 JS patch 把导航栏「最近」分组上限从 50 改为 20。
|
||||
@@ -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 |
@@ -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