diff --git a/college/__manifest__.py b/college/__manifest__.py index ef7efdd..f521aa9 100644 --- a/college/__manifest__.py +++ b/college/__manifest__.py @@ -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', diff --git a/college/controllers/exam_controller.py b/college/controllers/exam_controller.py index c735a96..d782760 100644 --- a/college/controllers/exam_controller.py +++ b/college/controllers/exam_controller.py @@ -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 diff --git a/college/data/about_pages.xml b/college/data/about_pages.xml index f0d93f2..4d81631 100644 --- a/college/data/about_pages.xml +++ b/college/data/about_pages.xml @@ -103,8 +103,7 @@
- P - python企业实训平台 + Python企业实训平台

从 Python 语法到企业级项目实战

面向高校与职业院校的工程化 Python 实训平台,把企业真实开发场景搬进课堂

@@ -231,7 +230,7 @@
- python企业实训平台 · 基于 Odoo 18 构建 · 让每一次练习都更接近真实开发 + Python企业实训平台 · 基于 Odoo 18 构建 · 让每一次练习都更接近真实开发
]]> @@ -244,52 +243,47 @@ 40 True - -
- -
-
- - -
- P - Peng Yuthon -
- -

鹏程万里 气宇轩昂

-

软件 · 硬件 · AI 智能体

- - -
- 软件 - 硬件 - AI 智能体 - 工程化教育 - 企业实训 +
+ +
+

鹏程万里 气宇轩昂

+

鹏宇森数据科技有限公司

+

软件 · 硬件 · AI 智能体

+
+ 软件 + 硬件 + AI 智能体 + 工程化教育 + 企业实训
- - -
-

关于我们

-

+ +

+

+ 关于我们 +

+

Peng Yuthon 专注于软件、硬件与 AI 智能体方向的工程化教育, 致力于为高校与职业院校提供真实企业场景驱动的实训解决方案。

+
-

联系方式

-
    -
  • B 站:bilibili: pengyuthon
  • -
  • 邮箱:pengyuthon@163.com
  • -
  • 官网:pengyuthon.cn
  • -
  • 学习平台:edu.pengyuthon.cn
  • + +
    +

    + 联系方式 +

    +
      +
    • B 站 bilibili: pengyuthon
    • +
    • 邮箱 pengyuthon@163.com
    • +
    • 官网 pengyuthon.cn
    • +
    • 学习平台 edu.pengyuthon.cn
    - -
    + +
    © 2026 Peng Yuthon. All rights reserved.
    diff --git a/college/data/shixun_module_data.xml b/college/data/shixun_module_data.xml index 0e135ce..bcd6305 100644 --- a/college/data/shixun_module_data.xml +++ b/college/data/shixun_module_data.xml @@ -8,21 +8,24 @@ ir.module.module kanban,list,form - - [('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])] + + [('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'])] {'shixun_hide_empty_categories': True} diff --git a/college/migrations/18.0.3.3.0/post-migration.py b/college/migrations/18.0.3.3.0/post-migration.py new file mode 100644 index 0000000..468379b --- /dev/null +++ b/college/migrations/18.0.3.3.0/post-migration.py @@ -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}) diff --git a/college/models/__init__.py b/college/models/__init__.py index 501250a..66f1401 100644 --- a/college/models/__init__.py +++ b/college/models/__init__.py @@ -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 diff --git a/college/models/res_users.py b/college/models/res_users.py new file mode 100644 index 0000000..1efd9a0 --- /dev/null +++ b/college/models/res_users.py @@ -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, + ) diff --git a/college/static/src/js/module_list_cap.js b/college/static/src/js/module_list_cap.js new file mode 100644 index 0000000..ac583e6 --- /dev/null +++ b/college/static/src/js/module_list_cap.js @@ -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 的 + * 渲染时 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 就是模板里的 " / ", +// 配合 .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); + }, +}); diff --git a/college/static/src/scss/hide_about_edit.scss b/college/static/src/scss/hide_about_edit.scss index 732e9e2..89335e8 100644 --- a/college/static/src/scss/hide_about_edit.scss +++ b/college/static/src/scss/hide_about_edit.scss @@ -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; + } } diff --git a/college/static/src/xml/module_list_pager.xml b/college/static/src/xml/module_list_pager.xml new file mode 100644 index 0000000..478d37a --- /dev/null +++ b/college/static/src/xml/module_list_pager.xml @@ -0,0 +1,21 @@ + + + + + + (env.config && env.config.__yuthon_hide_pager_total ? 'o_pager_hide_total ' : '') + 'o_pager d-flex gap-2 h-100' + + + diff --git a/college/views/menus.xml b/college/views/menus.xml index 1bf9e7c..c745c41 100644 --- a/college/views/menus.xml +++ b/college/views/menus.xml @@ -18,7 +18,7 @@ @@ -179,7 +179,7 @@ else: sequence="10" groups="college.group_teacher,base.group_system"/> diff --git a/yuthon_employee/__init__.py b/yuthon_employee/__init__.py new file mode 100644 index 0000000..0650744 --- /dev/null +++ b/yuthon_employee/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/yuthon_employee/__manifest__.py b/yuthon_employee/__manifest__.py new file mode 100644 index 0000000..9b2097d --- /dev/null +++ b/yuthon_employee/__manifest__.py @@ -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, +} diff --git a/yuthon_employee/data/demo.xml b/yuthon_employee/data/demo.xml new file mode 100644 index 0000000..0ba1ade --- /dev/null +++ b/yuthon_employee/data/demo.xml @@ -0,0 +1,39 @@ + + + + + T2024001 + 张三 + male + teaching + 讲师 + 13800000001 + zhangsan@school.edu + 8000.0 + 2024-09-01 + + + + A2024002 + 李四 + female + admin + 教务员 + 13800000002 + lisi@school.edu + 6000.0 + 2024-09-01 + + + + T2024003 + 王五 + male + teaching + 副教授 + 13800000003 + wangwu@school.edu + 12000.0 + 2024-09-01 + + diff --git a/yuthon_employee/models/__init__.py b/yuthon_employee/models/__init__.py new file mode 100644 index 0000000..f7b5da3 --- /dev/null +++ b/yuthon_employee/models/__init__.py @@ -0,0 +1 @@ +from . import employee diff --git a/yuthon_employee/models/employee.py b/yuthon_employee/models/employee.py new file mode 100644 index 0000000..29c564b --- /dev/null +++ b/yuthon_employee/models/employee.py @@ -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('基本工资不能为负数。') diff --git a/yuthon_employee/security/ir.model.access.csv b/yuthon_employee/security/ir.model.access.csv new file mode 100644 index 0000000..a7bf7eb --- /dev/null +++ b/yuthon_employee/security/ir.model.access.csv @@ -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 diff --git a/yuthon_employee/static/description/icon.png b/yuthon_employee/static/description/icon.png new file mode 100644 index 0000000..2b2766a Binary files /dev/null and b/yuthon_employee/static/description/icon.png differ diff --git a/yuthon_employee/views/employee_views.xml b/yuthon_employee/views/employee_views.xml new file mode 100644 index 0000000..0018dbb --- /dev/null +++ b/yuthon_employee/views/employee_views.xml @@ -0,0 +1,88 @@ + + + + + + yuthon.employee.list + yuthon.employee + + + + + + + + + + + + + + + + yuthon.employee.form + yuthon.employee + +
    + + + + + + + + + + + + + + + + + + +
    +
    +
    + + + + yuthon.employee搜索 + yuthon.employee + + + + + + + + + + + + + + + + + + + 教职工 + yuthon.employee + list,form + + + + + + + +