Compare commits

..

No commits in common. "master" and "dev_haoran" have entirely different histories.

62 changed files with 0 additions and 4198 deletions

View File

@ -1,3 +0,0 @@
from . import models
from . import controllers
from . import wizards

View File

@ -1,36 +0,0 @@
{
'name': '教育',
'version': '18.0.2.2.0',
'summary': 'Odoo 18 教学平台 — 体系教材 + 组织架构 + 学习跟踪',
'description': """
Odoo 18 框架学习教学平台
- 体系教材教材 章节 视频带学时统计
- 组织架构学校班级老师学生基础信息管理
- 学习跟踪学生进度跟踪教师可查看
- 树形学习目录 Odoo 18 知识体系分类
- 支持上传本地教学视频内置播放器
""",
'author': 'Yuthon',
'website': '',
'category': 'Education',
'depends': ['base', 'web', 'mail'],
'data': [
'security/ir.model.access.csv',
'data/demo.xml',
'views/video_category_views.xml',
'views/video_playback_views.xml',
'views/textbook_views.xml',
'views/organization_views.xml',
'views/progress_views.xml',
'views/elective_views.xml',
'views/progress_wizard_views.xml',
'views/course_views.xml',
'views/exam_views.xml',
'views/forum_post_views.xml',
'views/menus.xml',
],
'license': 'LGPL-3',
'installable': True,
'application': True,
'auto_install': False,
}

View File

@ -1 +0,0 @@
from . import main

View File

@ -1,77 +0,0 @@
import base64
import os
import re
from odoo import http
from odoo.http import request
class VideoPlaybackController(http.Controller):
@http.route('/yuthon_school/video/<int:video_id>', type='http', auth='user', methods=['GET'])
def serve_video(self, video_id, **kwargs):
"""提供视频流服务,支持 Range 请求以实现视频进度拖拽"""
video = request.env['yuthon.school.video'].sudo().browse(video_id)
if not video.exists() or not video.video_file:
return request.not_found()
video_data = self._decode_video(video.video_file)
file_size = len(video_data)
mime_map = {
'mp4': 'video/mp4',
'webm': 'video/webm',
'ogg': 'video/ogg',
'ogv': 'video/ogg',
'mov': 'video/quicktime',
'mkv': 'video/x-matroska',
'avi': 'video/x-msvideo',
}
mime_type = 'video/mp4'
if video.video_filename:
ext = os.path.splitext(video.video_filename)[1].lower().lstrip('.')
mime_type = mime_map.get(ext, 'video/mp4')
# 处理 Range 请求(视频进度条拖拽需要)
range_header = request.httprequest.headers.get('Range')
if range_header:
range_match = re.search(r'bytes=(\d+)-(\d*)', range_header)
if range_match:
start = int(range_match.group(1))
end = int(range_match.group(2)) if range_match.group(2) else file_size - 1
if start >= file_size:
headers = [('Content-Range', f'bytes */{file_size}')]
return request.make_response(b'', headers, status=416)
end = min(end, file_size - 1)
chunk = video_data[start:end + 1]
headers = [
('Content-Type', mime_type),
('Content-Length', str(len(chunk))),
('Content-Range', f'bytes {start}-{end}/{file_size}'),
('Accept-Ranges', 'bytes'),
]
return request.make_response(chunk, headers, status=206)
# 返回完整视频内容(在线播放)
headers = [
('Content-Type', mime_type),
('Content-Length', str(file_size)),
('Accept-Ranges', 'bytes'),
]
return request.make_response(video_data, headers)
@staticmethod
def _decode_video(raw_file):
"""解码 Odoo 18 Binary 字段数据"""
if isinstance(raw_file, bytes):
try:
return base64.b64decode(raw_file, validate=True)
except Exception:
return raw_file
elif isinstance(raw_file, str):
return base64.b64decode(raw_file)
return b''

View File

@ -1,510 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!--
Demo 演示数据
============
安装模块时勾选「加载演示数据」即可自动创建以下种子数据:
- 学习分类树4 个分类)
- 1 所学校(求知中学)
- 3 位老师
- 2 个班级(入门班 / 进阶班)
- 10 个学生(每班 5 人)
- 1 本教材5 个章节)
- 2 门选修课(演示 Many2many
- 若干学习进度记录
教学知识点XML 数据加载、noupdate 标签、外键 ref 引用、
Many2many eval 写法 [(6, 0, [ids])]
-->
<!-- ============ 学习分类树 ============ -->
<record id="demo_category_root" model="yuthon.school.category">
<field name="name">Odoo 18 开发</field>
<field name="level">beginner</field>
</record>
<record id="demo_category_basic" model="yuthon.school.category">
<field name="name">基础入门</field>
<field name="parent_id" ref="demo_category_root"/>
<field name="level">beginner</field>
</record>
<record id="demo_category_model" model="yuthon.school.category">
<field name="name">模型开发</field>
<field name="parent_id" ref="demo_category_root"/>
<field name="level">intermediate</field>
</record>
<record id="demo_category_view" model="yuthon.school.category">
<field name="name">视图开发</field>
<field name="parent_id" ref="demo_category_root"/>
<field name="level">intermediate</field>
</record>
<!-- ============ 学校 ============ -->
<record id="demo_school" model="yuthon.school.school">
<field name="name">求知中学</field>
<field name="address">北京市海淀区中关村南大街 1 号</field>
<field name="phone">010-88888001</field>
<field name="principal">赵校长</field>
</record>
<!-- ============ 老师 ============ -->
<record id="demo_teacher_zhang" model="yuthon.school.teacher">
<field name="name">张老师</field>
<field name="school_id" ref="demo_school"/>
<field name="phone">138-0000-0001</field>
<field name="subject">Odoo 模型开发</field>
</record>
<record id="demo_teacher_li" model="yuthon.school.teacher">
<field name="name">李老师</field>
<field name="school_id" ref="demo_school"/>
<field name="phone">138-0000-0002</field>
<field name="subject">Odoo 视图开发</field>
</record>
<record id="demo_teacher_wang" model="yuthon.school.teacher">
<field name="name">王老师</field>
<field name="school_id" ref="demo_school"/>
<field name="phone">138-0000-0003</field>
<field name="subject">Odoo 部署运维</field>
</record>
<!-- ============ 班级 ============ -->
<record id="demo_class_beginner" model="yuthon.school.class">
<field name="name">Odoo 18 入门班</field>
<field name="school_id" ref="demo_school"/>
<field name="teacher_id" ref="demo_teacher_zhang"/>
<field name="grade">2026 级</field>
</record>
<record id="demo_class_advanced" model="yuthon.school.class">
<field name="name">Odoo 18 进阶班</field>
<field name="school_id" ref="demo_school"/>
<field name="teacher_id" ref="demo_teacher_li"/>
<field name="grade">2026 级</field>
</record>
<!-- ============ 学生(入门班 5 人) ============ -->
<record id="demo_student_01" model="yuthon.school.student">
<field name="name">陈小明</field>
<field name="student_no">S20260001</field>
<field name="class_id" ref="demo_class_beginner"/>
<field name="gender">male</field>
<field name="phone">139-0000-0001</field>
<field name="enrollment_date">2026-03-01</field>
</record>
<record id="demo_student_02" model="yuthon.school.student">
<field name="name">林小芳</field>
<field name="student_no">S20260002</field>
<field name="class_id" ref="demo_class_beginner"/>
<field name="gender">female</field>
<field name="phone">139-0000-0002</field>
<field name="enrollment_date">2026-03-01</field>
</record>
<record id="demo_student_03" model="yuthon.school.student">
<field name="name">黄小杰</field>
<field name="student_no">S20260003</field>
<field name="class_id" ref="demo_class_beginner"/>
<field name="gender">male</field>
<field name="phone">139-0000-0003</field>
<field name="enrollment_date">2026-03-01</field>
</record>
<record id="demo_student_04" model="yuthon.school.student">
<field name="name">刘小婷</field>
<field name="student_no">S20260004</field>
<field name="class_id" ref="demo_class_beginner"/>
<field name="gender">female</field>
<field name="phone">139-0000-0004</field>
<field name="enrollment_date">2026-03-01</field>
</record>
<record id="demo_student_05" model="yuthon.school.student">
<field name="name">周小强</field>
<field name="student_no">S20260005</field>
<field name="class_id" ref="demo_class_beginner"/>
<field name="gender">male</field>
<field name="phone">139-0000-0005</field>
<field name="enrollment_date">2026-03-01</field>
</record>
<!-- ============ 学生(进阶班 5 人) ============ -->
<record id="demo_student_06" model="yuthon.school.student">
<field name="name">吴小燕</field>
<field name="student_no">S20260006</field>
<field name="class_id" ref="demo_class_advanced"/>
<field name="gender">female</field>
<field name="phone">139-0000-0006</field>
<field name="enrollment_date">2026-03-01</field>
</record>
<record id="demo_student_07" model="yuthon.school.student">
<field name="name">郑小伟</field>
<field name="student_no">S20260007</field>
<field name="class_id" ref="demo_class_advanced"/>
<field name="gender">male</field>
<field name="phone">139-0000-0007</field>
<field name="enrollment_date">2026-03-01</field>
</record>
<record id="demo_student_08" model="yuthon.school.student">
<field name="name">孙小丽</field>
<field name="student_no">S20260008</field>
<field name="class_id" ref="demo_class_advanced"/>
<field name="gender">female</field>
<field name="phone">139-0000-0008</field>
<field name="enrollment_date">2026-03-01</field>
</record>
<record id="demo_student_09" model="yuthon.school.student">
<field name="name">胡小军</field>
<field name="student_no">S20260009</field>
<field name="class_id" ref="demo_class_advanced"/>
<field name="gender">male</field>
<field name="phone">139-0000-0009</field>
<field name="enrollment_date">2026-03-01</field>
</record>
<record id="demo_student_10" model="yuthon.school.student">
<field name="name">高小静</field>
<field name="student_no">S20260010</field>
<field name="class_id" ref="demo_class_advanced"/>
<field name="gender">female</field>
<field name="phone">139-0000-0010</field>
<field name="enrollment_date">2026-03-01</field>
</record>
<!-- ============ 教材 + 章节 ============ -->
<record id="demo_textbook" model="yuthon.school.textbook">
<field name="name">Odoo 18 框架开发实战</field>
<field name="category_id" ref="demo_category_root"/>
<field name="difficulty">beginner</field>
<field name="state">published</field>
<field name="description">从零开始学习 Odoo 18 框架开发,涵盖模块结构、模型定义、视图架构、权限机制等核心知识点。</field>
</record>
<record id="demo_chapter_01" model="yuthon.school.chapter">
<field name="name">第一章 模块结构与清单文件</field>
<field name="textbook_id" ref="demo_textbook"/>
<field name="sequence">10</field>
<field name="summary">了解 Odoo 模块的目录结构和 __manifest__.py 清单文件</field>
<field name="key_points">__manifest__.py 字段、data 文件加载顺序、addons_path 配置</field>
<field name="hours">2.0</field>
</record>
<record id="demo_chapter_02" model="yuthon.school.chapter">
<field name="name">第二章 模型定义与字段类型</field>
<field name="textbook_id" ref="demo_textbook"/>
<field name="sequence">20</field>
<field name="summary">掌握 Odoo 模型定义、字段类型和关系字段</field>
<field name="key_points">Char/Integer/Float/Selection、Many2one/One2many/Many2many、compute/store</field>
<field name="hours">4.0</field>
</record>
<record id="demo_chapter_03" model="yuthon.school.chapter">
<field name="name">第三章 视图架构</field>
<field name="textbook_id" ref="demo_textbook"/>
<field name="sequence">30</field>
<field name="summary">学习 list/form/search/kanban 四种核心视图</field>
<field name="key_points">list 视图、form 视图、search 视图、kanban 看板视图</field>
<field name="hours">3.0</field>
</record>
<record id="demo_chapter_04" model="yuthon.school.chapter">
<field name="name">第四章 权限与安全机制</field>
<field name="textbook_id" ref="demo_textbook"/>
<field name="sequence">40</field>
<field name="summary">理解 ir.model.access.csv 和 ir.rule 记录规则</field>
<field name="key_points">模型访问权限、记录规则、用户组</field>
<field name="hours">2.5</field>
</record>
<record id="demo_chapter_05" model="yuthon.school.chapter">
<field name="name">第五章 控制器与路由</field>
<field name="textbook_id" ref="demo_textbook"/>
<field name="sequence">50</field>
<field name="summary">学习 HTTP 控制器和 URL 路由</field>
<field name="key_points">Controller、@http.route、请求处理、模板渲染</field>
<field name="hours">3.0</field>
</record>
<!-- ============ 选修课Many2many 演示) ============ -->
<record id="demo_elective_owl" model="yuthon.school.elective">
<field name="name">OWL 前端框架入门</field>
<field name="teacher_id" ref="demo_teacher_li"/>
<field name="description">学习 Odoo Web Library (OWL) 框架基础,理解组件化前端开发。</field>
<field name="student_ids" eval="[(6, 0, [ref('demo_student_01'), ref('demo_student_02'), ref('demo_student_03')])]"/>
</record>
<record id="demo_elective_workflow" model="yuthon.school.elective">
<field name="name">Odoo 工作流开发</field>
<field name="teacher_id" ref="demo_teacher_zhang"/>
<field name="description">深入学习 Odoo 工作流机制,掌握状态机和业务流程设计。</field>
<field name="student_ids" eval="[(6, 0, [ref('demo_student_06'), ref('demo_student_07'), ref('demo_student_08')])]"/>
</record>
<!-- ============ 学习进度记录 ============ -->
<record id="demo_progress_01" model="yuthon.school.progress">
<field name="student_id" ref="demo_student_01"/>
<field name="textbook_id" ref="demo_textbook"/>
<field name="chapter_id" ref="demo_chapter_01"/>
<field name="progress">100</field>
<field name="completed">True</field>
<field name="last_study_date">2026-07-20 10:00:00</field>
</record>
<record id="demo_progress_02" model="yuthon.school.progress">
<field name="student_id" ref="demo_student_01"/>
<field name="textbook_id" ref="demo_textbook"/>
<field name="chapter_id" ref="demo_chapter_02"/>
<field name="progress">65</field>
<field name="completed">False</field>
<field name="last_study_date">2026-07-25 14:00:00</field>
</record>
<record id="demo_progress_03" model="yuthon.school.progress">
<field name="student_id" ref="demo_student_02"/>
<field name="textbook_id" ref="demo_textbook"/>
<field name="chapter_id" ref="demo_chapter_01"/>
<field name="progress">100</field>
<field name="completed">True</field>
<field name="last_study_date">2026-07-22 09:30:00</field>
</record>
<record id="demo_progress_04" model="yuthon.school.progress">
<field name="student_id" ref="demo_student_06"/>
<field name="textbook_id" ref="demo_textbook"/>
<field name="chapter_id" ref="demo_chapter_03"/>
<field name="progress">40</field>
<field name="completed">False</field>
<field name="last_study_date">2026-07-26 16:00:00</field>
</record>
<record id="demo_progress_05" model="yuthon.school.progress">
<field name="student_id" ref="demo_student_07"/>
<field name="textbook_id" ref="demo_textbook"/>
<field name="chapter_id" ref="demo_chapter_01"/>
<field name="progress">0</field>
<field name="completed">False</field>
<field name="last_study_date">2026-07-28 08:00:00</field>
</record>
<!-- ============ 第二本教材 + 章节 ============ -->
<record id="demo_textbook_owl" model="yuthon.school.textbook">
<field name="name">Odoo 18 前端 OWL 实战</field>
<field name="category_id" ref="demo_category_view"/>
<field name="difficulty">intermediate</field>
<field name="state">published</field>
<field name="description">面向前端开发者的 OWL 组件化开发教程,覆盖组件、事件、状态与渲染。</field>
</record>
<record id="demo_chapter_owl_01" model="yuthon.school.chapter">
<field name="name">OWL 组件基础</field>
<field name="textbook_id" ref="demo_textbook_owl"/>
<field name="sequence">10</field>
<field name="hours">2.5</field>
<field name="summary">组件定义、props、状态</field>
<field name="key_points">Component、useState、props</field>
</record>
<record id="demo_chapter_owl_02" model="yuthon.school.chapter">
<field name="name">事件与渲染</field>
<field name="textbook_id" ref="demo_textbook_owl"/>
<field name="sequence">20</field>
<field name="hours">3.0</field>
<field name="summary">事件绑定、条件渲染、列表渲染</field>
<field name="key_points">t-on、t-if、t-foreach</field>
</record>
<!-- ============ 教学视频(占位文件,仅用于演示视频管理功能) ============ -->
<record id="demo_video_01" model="yuthon.school.video">
<field name="name">模块结构详解</field>
<field name="chapter_id" ref="demo_chapter_01"/>
<field name="category_id" ref="demo_category_basic"/>
<field name="duration">12:30</field>
<field name="difficulty">beginner</field>
<field name="video_filename">demo_lesson_01.mp4</field>
<field name="video_file" type="base64">AAAA</field>
<field name="description">讲解 Odoo 模块的目录结构与清单文件。</field>
<field name="key_points">__manifest__.py、data 加载顺序</field>
</record>
<record id="demo_video_02" model="yuthon.school.video">
<field name="name">模型与字段类型</field>
<field name="chapter_id" ref="demo_chapter_02"/>
<field name="category_id" ref="demo_category_model"/>
<field name="duration">18:05</field>
<field name="difficulty">intermediate</field>
<field name="video_filename">demo_lesson_02.mp4</field>
<field name="video_file" type="base64">AAAA</field>
<field name="description">Char/Integer/Selection 与关系字段。</field>
</record>
<record id="demo_video_03" model="yuthon.school.video">
<field name="name">OWL 组件基础</field>
<field name="chapter_id" ref="demo_chapter_owl_01"/>
<field name="category_id" ref="demo_category_view"/>
<field name="duration">15:40</field>
<field name="difficulty">intermediate</field>
<field name="video_filename">demo_lesson_03.mp4</field>
<field name="video_file" type="base64">AAAA</field>
<field name="description">OWL 组件定义与状态管理。</field>
</record>
<!-- ============ 课程 ============ -->
<record id="demo_course_fullstack" model="yuthon.school.course">
<field name="name">Odoo 18 全栈开发工程师</field>
<field name="code">ODOO-101</field>
<field name="category_id" ref="demo_category_root"/>
<field name="teacher_id" ref="demo_teacher_zhang"/>
<field name="difficulty">beginner</field>
<field name="state">ongoing</field>
<field name="description">零基础到独立开发 Odoo 18 模块,覆盖模型、视图、权限、控制器。</field>
<field name="textbook_ids" eval="[(6, 0, [ref('demo_textbook'), ref('demo_textbook_owl')])]"/>
<field name="student_ids" eval="[(6, 0, [ref('demo_student_01'), ref('demo_student_02'), ref('demo_student_03'), ref('demo_student_06')])]"/>
</record>
<record id="demo_course_frontend" model="yuthon.school.course">
<field name="name">Odoo 前端 OWL 专项</field>
<field name="code">ODOO-201</field>
<field name="category_id" ref="demo_category_view"/>
<field name="teacher_id" ref="demo_teacher_li"/>
<field name="difficulty">intermediate</field>
<field name="state">recruiting</field>
<field name="description">深入 OWL 框架,掌握组件化前端开发。</field>
<field name="textbook_ids" eval="[(6, 0, [ref('demo_textbook_owl')])]"/>
<field name="student_ids" eval="[(6, 0, [ref('demo_student_07'), ref('demo_student_08'), ref('demo_student_09')])]"/>
</record>
<!-- ============ 课程大纲 ============ -->
<record id="demo_syllabus_01" model="yuthon.school.syllabus">
<field name="name">模块开发与权限设计</field>
<field name="week">第 1-2 周</field>
<field name="course_id" ref="demo_course_fullstack"/>
<field name="chapter_ids" eval="[(6, 0, [ref('demo_chapter_01'), ref('demo_chapter_04')])]"/>
<field name="objective">掌握模块清单、模型定义与 ACL 权限机制。</field>
<field name="content"><![CDATA[<p>通过动手实现一个完整模块,理解 Odoo 的开发全流程。</p>]]></field>
</record>
<record id="demo_syllabus_02" model="yuthon.school.syllabus">
<field name="name">视图与前端交互</field>
<field name="week">第 3-4 周</field>
<field name="course_id" ref="demo_course_fullstack"/>
<field name="chapter_ids" eval="[(6, 0, [ref('demo_chapter_03')])]"/>
<field name="objective">熟练使用 list/form/search/kanban 四种视图。</field>
</record>
<record id="demo_syllabus_03" model="yuthon.school.syllabus">
<field name="name">OWL 组件与状态管理</field>
<field name="week">第 1-2 周</field>
<field name="course_id" ref="demo_course_frontend"/>
<field name="chapter_ids" eval="[(6, 0, [ref('demo_chapter_owl_01'), ref('demo_chapter_owl_02')])]"/>
<field name="objective">能用 OWL 编写组件化前端页面。</field>
</record>
<!-- ============ 训练考核 ============ -->
<record id="demo_exam_01" model="yuthon.school.exam">
<field name="name">模块开发单元测验</field>
<field name="course_id" ref="demo_course_fullstack"/>
<field name="student_id" ref="demo_student_01"/>
<field name="exam_type">quiz</field>
<field name="exam_date">2026-07-21</field>
<field name="score">92</field>
<field name="state">done</field>
<field name="note">概念清晰,注意 compute 字段的 store 用法。</field>
</record>
<record id="demo_exam_02" model="yuthon.school.exam">
<field name="name">模块开发单元测验</field>
<field name="course_id" ref="demo_course_fullstack"/>
<field name="student_id" ref="demo_student_02"/>
<field name="exam_type">quiz</field>
<field name="exam_date">2026-07-21</field>
<field name="score">55</field>
<field name="state">done</field>
<field name="note">关系字段部分需要加强。</field>
</record>
<record id="demo_exam_03" model="yuthon.school.exam">
<field name="name">OWL 期中练习</field>
<field name="course_id" ref="demo_course_frontend"/>
<field name="student_id" ref="demo_student_07"/>
<field name="exam_type">practice</field>
<field name="exam_date">2026-07-28</field>
<field name="score">88</field>
<field name="state">done</field>
</record>
<record id="demo_exam_04" model="yuthon.school.exam">
<field name="name">OWL 期中练习</field>
<field name="course_id" ref="demo_course_frontend"/>
<field name="student_id" ref="demo_student_08"/>
<field name="exam_type">practice</field>
<field name="exam_date">2026-07-28</field>
<field name="score">0</field>
<field name="state">pending</field>
</record>
<!-- ============ 学习交流 ============ -->
<record id="demo_post_01" model="yuthon.school.forum_post">
<field name="name">Many2many 的 (6,0,[ids]) 怎么理解?</field>
<field name="post_type">question</field>
<field name="author_name">陈小明</field>
<field name="author_type">student</field>
<field name="course_id" ref="demo_course_fullstack"/>
<field name="content"><![CDATA[<p>在 demo 数据里看到 <code>[(6,0,[ids])]</code>,这个 6 和 0 分别代表什么?</p>]]></field>
<field name="like_count">3</field>
</record>
<record id="demo_post_reply_01" model="yuthon.school.forum_post">
<field name="name">回复Many2many 的 (6,0,[ids])</field>
<field name="parent_id" ref="demo_post_01"/>
<field name="post_type">discuss</field>
<field name="author_name">张老师</field>
<field name="author_type">teacher</field>
<field name="content"><![CDATA[<p>6 表示「替换」0 是保留位,[ids] 是要关联的记录 id 列表。整体含义是把当前记录的 M2M 关系替换为给定列表。</p>]]></field>
<field name="like_count">5</field>
</record>
<record id="demo_post_02" model="yuthon.school.forum_post">
<field name="name">分享compute 字段一定要加 store 吗?</field>
<field name="post_type">share</field>
<field name="author_name">林小芳</field>
<field name="author_type">student</field>
<field name="course_id" ref="demo_course_fullstack"/>
<field name="content"><![CDATA[<p>不是必须。不加 store 每次读取都实时计算;加 store 会落库并随依赖字段变化重算,适合列表排序/搜索场景。</p>]]></field>
<field name="like_count">7</field>
</record>
<record id="demo_post_03" model="yuthon.school.forum_post">
<field name="name">OWL 里 t-on 和原生 onclick 有什么区别?</field>
<field name="post_type">question</field>
<field name="author_name">郑小伟</field>
<field name="author_type">student</field>
<field name="course_id" ref="demo_course_frontend"/>
<field name="content"><![CDATA[<p>用 t-on 绑定事件和直接写 onclick 有什么不同?</p>]]></field>
<field name="like_count">1</field>
</record>
</odoo>

View File

@ -1,10 +0,0 @@
from . import video_playback
from . import video_category
from . import textbook
from . import organization
from . import progress
from . import elective
from . import yuthon_school_course
from . import yuthon_school_syllabus
from . import yuthon_school_exam
from . import yuthon_school_forum_post

View File

@ -1,41 +0,0 @@
from odoo import fields, models, api
class Elective(models.Model):
_name = 'yuthon.school.elective'
_description = '选修课'
name = fields.Char(
string='选修课名称',
required=True,
help='Odoo 工作流开发、OWL 前端框架入门',
)
description = fields.Text(
string='课程简介',
)
teacher_id = fields.Many2one(
'yuthon.school.teacher',
string='授课教师',
)
student_ids = fields.Many2many(
'yuthon.school.student',
'yuthon_elective_student_rel',
'elective_id',
'student_id',
string='选课学生',
help='选择选修该课程的学生Many2many 关系演示)',
)
student_count = fields.Integer(
string='选课人数',
compute='_compute_student_count',
store=True,
)
active = fields.Boolean(
string='启用',
default=True,
)
@api.depends('student_ids')
def _compute_student_count(self):
for record in self:
record.student_count = len(record.student_ids)

View File

@ -1,167 +0,0 @@
from odoo import fields, models, api
class School(models.Model):
_name = 'yuthon.school.school'
_description = '学校'
name = fields.Char(
string='学校名称',
required=True,
)
address = fields.Char(
string='地址',
)
phone = fields.Char(
string='联系电话',
)
principal = fields.Char(
string='校长',
)
class_ids = fields.One2many(
'yuthon.school.class',
'school_id',
string='班级',
)
class_count = fields.Integer(
string='班级数',
compute='_compute_counts',
)
teacher_ids = fields.One2many(
'yuthon.school.teacher',
'school_id',
string='教师',
)
teacher_count = fields.Integer(
string='教师数',
compute='_compute_counts',
)
active = fields.Boolean(
string='启用',
default=True,
)
@api.depends('class_ids', 'teacher_ids')
def _compute_counts(self):
for record in self:
record.class_count = len(record.class_ids)
record.teacher_count = len(record.teacher_ids)
class SchoolClass(models.Model):
_name = 'yuthon.school.class'
_description = '班级'
name = fields.Char(
string='班级名称',
required=True,
)
school_id = fields.Many2one(
'yuthon.school.school',
string='所属学校',
required=True,
ondelete='cascade',
)
teacher_id = fields.Many2one(
'yuthon.school.teacher',
string='班主任',
)
grade = fields.Char(
string='年级',
)
student_ids = fields.One2many(
'yuthon.school.student',
'class_id',
string='学生',
)
student_count = fields.Integer(
string='学生数',
compute='_compute_student_count',
store=True,
)
active = fields.Boolean(
string='启用',
default=True,
)
@api.depends('student_ids')
def _compute_student_count(self):
for record in self:
record.student_count = len(record.student_ids)
class Teacher(models.Model):
_name = 'yuthon.school.teacher'
_description = '老师'
name = fields.Char(
string='姓名',
required=True,
)
school_id = fields.Many2one(
'yuthon.school.school',
string='所属学校',
required=True,
ondelete='cascade',
)
phone = fields.Char(
string='联系电话',
)
subject = fields.Char(
string='教授方向',
help='Odoo 模型开发、视图开发、部署运维等',
)
class_ids = fields.One2many(
'yuthon.school.class',
'teacher_id',
string='负责班级',
)
active = fields.Boolean(
string='启用',
default=True,
)
class Student(models.Model):
_name = 'yuthon.school.student'
_description = '学生'
name = fields.Char(
string='姓名',
required=True,
)
student_no = fields.Char(
string='学号',
)
class_id = fields.Many2one(
'yuthon.school.class',
string='所属班级',
ondelete='restrict',
)
school_id = fields.Many2one(
'yuthon.school.school',
string='所属学校',
related='class_id.school_id',
store=True,
)
gender = fields.Selection(
[('male', ''), ('female', '')],
string='性别',
)
phone = fields.Char(
string='联系电话',
)
enrollment_date = fields.Date(
string='入学日期',
)
elective_ids = fields.Many2many(
'yuthon.school.elective',
'yuthon_elective_student_rel',
'student_id',
'elective_id',
string='已选选修课',
)
active = fields.Boolean(
string='启用',
default=True,
)

View File

@ -1,84 +0,0 @@
from odoo import fields, models, api
from odoo.exceptions import ValidationError
class StudyProgress(models.Model):
_name = 'yuthon.school.progress'
_description = '学习进度'
_order = 'student_id, textbook_id, chapter_id, sequence'
_inherit = ['mail.thread', 'mail.activity.mixin']
student_id = fields.Many2one(
'yuthon.school.student',
string='学生',
required=True,
ondelete='cascade',
)
textbook_id = fields.Many2one(
'yuthon.school.textbook',
string='教材',
ondelete='restrict',
)
chapter_id = fields.Many2one(
'yuthon.school.chapter',
string='章节',
ondelete='restrict',
)
video_id = fields.Many2one(
'yuthon.school.video',
string='视频',
ondelete='restrict',
)
sequence = fields.Integer(
string='排序',
default=10,
)
progress = fields.Float(
string='进度(%)',
default=0.0,
help='0-100表示该视频的学习完成百分比',
)
completed = fields.Boolean(
string='已完成',
default=False,
)
state = fields.Selection(
[('not_started', '未开始'), ('in_progress', '进行中'), ('completed', '已完成')],
string='学习状态',
compute='_compute_state',
store=True,
)
last_study_date = fields.Datetime(
string='最后学习时间',
)
teacher_id = fields.Many2one(
'yuthon.school.teacher',
string='跟踪教师',
related='student_id.class_id.teacher_id',
store=True,
help='该学生所在班级的班主任,用于教师查看进度',
)
active = fields.Boolean(
string='启用',
default=True,
)
@api.depends('progress', 'completed')
def _compute_state(self):
"""根据进度和完成标记自动计算学习状态"""
for record in self:
if record.completed or record.progress >= 100:
record.state = 'completed'
elif record.progress > 0:
record.state = 'in_progress'
else:
record.state = 'not_started'
@api.constrains('progress')
def _check_progress_range(self):
"""验证进度值必须在 0-100 之间"""
for record in self:
if record.progress < 0 or record.progress > 100:
raise ValidationError(
f"进度值 {record.progress} 无效,必须在 0 到 100 之间"
)

View File

@ -1,147 +0,0 @@
from odoo import fields, models, api
class Textbook(models.Model):
_name = 'yuthon.school.textbook'
_description = '教材'
_order = 'category_id, sequence, name'
name = fields.Char(
string='教材名称',
required=True,
help='Odoo 18 基础入门、Odoo 18 视图开发实战',
)
sequence = fields.Integer(
string='排序',
default=10,
)
category_id = fields.Many2one(
'yuthon.school.category',
string='学习分类',
ondelete='restrict',
help='该教材所属的学习分类',
)
description = fields.Text(
string='教材简介',
help='教材的内容概要和适用人群',
)
difficulty = fields.Selection(
[('beginner', '入门'), ('intermediate', '进阶'), ('advanced', '高级')],
string='难度等级',
default='beginner',
)
state = fields.Selection(
[('draft', '草稿'), ('published', '已发布')],
string='状态',
default='draft',
)
total_hours = fields.Float(
string='总学时',
compute='_compute_total_hours',
store=True,
help='自动统计各章学时之和',
)
total_chapters = fields.Integer(
string='章节数',
compute='_compute_total_chapters',
store=True,
)
total_videos = fields.Integer(
string='视频数',
compute='_compute_total_videos',
store=True,
)
chapter_ids = fields.One2many(
'yuthon.school.chapter',
'textbook_id',
string='章节',
)
active = fields.Boolean(
string='启用',
default=True,
)
@api.depends('chapter_ids.hours')
def _compute_total_hours(self):
for record in self:
record.total_hours = sum(record.chapter_ids.mapped('hours'))
@api.depends('chapter_ids')
def _compute_total_chapters(self):
for record in self:
record.total_chapters = len(record.chapter_ids)
@api.depends('chapter_ids.video_ids')
def _compute_total_videos(self):
for record in self:
record.total_videos = sum(len(ch.video_ids) for ch in record.chapter_ids)
def action_confirm(self):
"""发布教材:将状态从草稿改为已发布"""
self.write({'state': 'published'})
def action_set_draft(self):
"""退回草稿:将状态从已发布改回草稿"""
self.write({'state': 'draft'})
class Chapter(models.Model):
_name = 'yuthon.school.chapter'
_description = '章节'
_order = 'textbook_id, sequence, name'
_sql_constraints = [
('unique_chapter_seq',
'unique(textbook_id, sequence)',
'同一教材下章节序号不能重复!'),
]
name = fields.Char(
string='章节标题',
required=True,
)
sequence = fields.Integer(
string='排序',
default=10,
)
textbook_id = fields.Many2one(
'yuthon.school.textbook',
string='所属教材',
required=True,
ondelete='cascade',
)
summary = fields.Text(
string='内容概述',
help='本章涵盖的内容概述',
)
key_points = fields.Text(
string='知识要点',
help='本章需要掌握的关键知识点',
)
hours = fields.Float(
string='学时',
default=1.0,
help='本章预计学习时长(小时)',
)
video_ids = fields.One2many(
'yuthon.school.video',
'chapter_id',
string='视频',
)
video_count = fields.Integer(
string='视频数',
compute='_compute_video_count',
store=True,
)
@api.depends('video_ids')
def _compute_video_count(self):
for record in self:
record.video_count = len(record.video_ids)
@api.onchange('video_ids')
def _onchange_video_ids(self):
"""当视频列表变化时,自动建议学时(每个视频按 0.5 学时计算)"""
if self.video_ids:
suggested = max(1.0, len(self.video_ids) * 0.5)
self.hours = suggested

View File

@ -1,74 +0,0 @@
from odoo import fields, models, api
class VideoCategory(models.Model):
_name = 'yuthon.school.category'
_description = '学习目录'
_order = 'complete_name'
_parent_store = True
name = fields.Char(
string='目录名称',
required=True,
help='Odoo 基础入门、模型开发、视图开发等',
)
parent_id = fields.Many2one(
'yuthon.school.category',
string='上级目录',
index=True,
ondelete='restrict',
help='选择上级目录以建立层级结构',
)
parent_path = fields.Char(
index=True,
unaccent=False,
)
child_ids = fields.One2many(
'yuthon.school.category',
'parent_id',
string='下级目录',
)
complete_name = fields.Char(
string='完整路径',
compute='_compute_complete_name',
store=True,
recursive=True,
help='显示完整路径Odoo 开发 > 模型开发 > 关系字段',
)
level = fields.Selection(
[('beginner', '入门'), ('intermediate', '进阶'), ('advanced', '高级')],
string='难度等级',
default='beginner',
help='该目录对应的学习难度',
)
description = fields.Text(
string='目录说明',
help='该学习目录的详细说明',
)
video_ids = fields.One2many(
'yuthon.school.video',
'category_id',
string='教学视频',
)
video_count = fields.Integer(
string='视频数量',
compute='_compute_video_count',
store=True,
)
active = fields.Boolean(
string='启用',
default=True,
)
@api.depends('name', 'parent_id.complete_name')
def _compute_complete_name(self):
for record in self:
if record.parent_id:
record.complete_name = f'{record.parent_id.complete_name} > {record.name}'
else:
record.complete_name = record.name
@api.depends('video_ids')
def _compute_video_count(self):
for record in self:
record.video_count = len(record.video_ids)

View File

@ -1,147 +0,0 @@
import markupsafe
from odoo import fields, models, api
from odoo.exceptions import ValidationError
class VideoPlayback(models.Model):
_name = 'yuthon.school.video'
_description = '教学视频'
_order = 'category_id, sequence, name'
name = fields.Char(
string='视频标题',
required=True,
help='Odoo 18 模块结构详解、Binary 字段用法等',
)
sequence = fields.Integer(
string='排序',
default=10,
help='同一目录下的视频排序序号',
)
category_id = fields.Many2one(
'yuthon.school.category',
string='学习目录',
ondelete='restrict',
help='该视频所属的学习目录分类',
)
chapter_id = fields.Many2one(
'yuthon.school.chapter',
string='所属章节',
ondelete='restrict',
help='该视频所属的教材章节',
)
difficulty = fields.Selection(
[('beginner', '入门'), ('intermediate', '进阶'), ('advanced', '高级')],
string='难度等级',
default='beginner',
help='该视频的学习难度',
)
duration = fields.Char(
string='视频时长',
help='15:30 表示 15 分钟 30 秒',
)
video_file = fields.Binary(
string='视频文件',
attachment=True,
required=True,
help='上传本地教学视频(支持 MP4、WebM、OGG 等格式)',
)
video_filename = fields.Char(
string='文件名',
help='视频文件的原始文件名',
)
file_size_display = fields.Char(
string='文件大小',
compute='_compute_file_info',
help='视频文件的大小',
)
description = fields.Text(
string='视频简介',
help='该教学视频的内容概要',
)
key_points = fields.Text(
string='知识要点',
help='该视频涵盖的关键知识点',
)
active = fields.Boolean(
string='启用',
default=True,
)
video_player = fields.Html(
string='视频播放器',
compute='_compute_video_player',
sanitize=False,
)
@api.depends('video_file', 'video_filename')
def _compute_file_info(self):
"""计算文件大小,显示为可读格式"""
for record in self:
if record.video_file:
encoded = record.video_file
if isinstance(encoded, bytes):
padding = 2 if encoded.endswith(b'==') else (1 if encoded.endswith(b'=') else 0)
else:
padding = 2 if encoded.endswith('==') else (1 if encoded.endswith('=') else 0)
size = (len(encoded) * 3) // 4 - padding
if size < 1024:
record.file_size_display = f'{size} B'
elif size < 1024 * 1024:
record.file_size_display = f'{size / 1024:.1f} KB'
elif size < 1024 * 1024 * 1024:
record.file_size_display = f'{size / (1024 * 1024):.1f} MB'
else:
record.file_size_display = f'{size / (1024 * 1024 * 1024):.1f} GB'
else:
record.file_size_display = '0 B'
@api.depends('video_file', 'video_filename')
def _compute_video_player(self):
"""生成视频播放器HTML"""
mime_map = {
'mp4': 'video/mp4',
'webm': 'video/webm',
'ogg': 'video/ogg',
'ogv': 'video/ogg',
'mov': 'video/quicktime',
'mkv': 'video/x-matroska',
'avi': 'video/x-msvideo',
}
for record in self:
if record.video_file and record.id:
mime_type = 'video/mp4'
if record.video_filename:
ext = record.video_filename.rsplit('.', 1)[-1].lower() if '.' in record.video_filename else ''
mime_type = mime_map.get(ext, 'video/mp4')
html = (
'<div style="text-align: center; padding: 15px; '
'background: #000; border-radius: 8px;">'
'<video controls preload="metadata" '
'style="width: 100%; max-width: 900px; border-radius: 4px;">'
f'<source src="/yuthon_school/video/{record.id}" type="{mime_type}">'
'<p style="color: #fff; padding: 20px;">'
'您的浏览器不支持视频播放。</p>'
'</video></div>'
)
record.video_player = markupsafe.Markup(html)
else:
record.video_player = markupsafe.Markup(
'<div style="text-align: center; color: #999; '
'padding: 30px; background: #f5f5f5; border-radius: 8px;">'
'<p>请上传视频文件并保存后查看播放。</p></div>'
)
@api.constrains('video_filename')
def _check_video_file(self):
"""验证上传的文件是否为视频格式"""
allowed_extensions = ['mp4', 'webm', 'ogg', 'ogv', 'mov', 'mkv', 'avi']
for record in self:
if record.video_filename:
ext = record.video_filename.rsplit('.', 1)[-1].lower() if '.' in record.video_filename else ''
if ext and ext not in allowed_extensions:
raise ValidationError(
f'请上传视频文件支持格式MP4、WebM、OGG、MOV、MKV、AVI当前文件格式为 .{ext}'
)

View File

@ -1,111 +0,0 @@
from odoo import fields, models, api
class Course(models.Model):
_name = 'yuthon.school.course'
_description = '课程'
_order = 'sequence, name'
name = fields.Char(
string='课程名称',
required=True,
help='Odoo 18 全栈开发工程师认证课程',
)
code = fields.Char(
string='课程编号',
help='ODOO-101',
)
sequence = fields.Integer(
string='排序',
default=10,
)
category_id = fields.Many2one(
'yuthon.school.category',
string='学习分类',
ondelete='restrict',
)
teacher_id = fields.Many2one(
'yuthon.school.teacher',
string='授课教师',
)
difficulty = fields.Selection(
[('beginner', '入门'), ('intermediate', '进阶'), ('advanced', '高级')],
string='难度等级',
default='beginner',
)
state = fields.Selection(
[('draft', '草稿'), ('recruiting', '招生中'),
('ongoing', '进行中'), ('finished', '已结课')],
string='状态',
default='draft',
)
description = fields.Text(
string='课程简介',
help='课程目标、适合人群、考核方式等',
)
textbook_ids = fields.Many2many(
'yuthon.school.textbook',
'yuthon_course_textbook_rel',
'course_id', 'textbook_id',
string='关联教材',
help='本课程涵盖的教材Many2many 演示)',
)
syllabus_ids = fields.One2many(
'yuthon.school.syllabus',
'course_id',
string='课程大纲',
)
student_ids = fields.Many2many(
'yuthon.school.student',
'yuthon_course_student_rel',
'course_id', 'student_id',
string='选课学生',
help='选修本课程的学生Many2many 演示)',
)
total_hours = fields.Float(
string='总学时',
compute='_compute_total_hours',
store=True,
help='自动汇总关联教材各章学时',
)
enrollment_count = fields.Integer(
string='选课人数',
compute='_compute_counts',
store=True,
)
syllabus_count = fields.Integer(
string='大纲条目',
compute='_compute_counts',
store=True,
)
active = fields.Boolean(
string='启用',
default=True,
)
@api.depends('textbook_ids.chapter_ids.hours')
def _compute_total_hours(self):
for record in self:
record.total_hours = sum(
chapter.hours
for textbook in record.textbook_ids
for chapter in textbook.chapter_ids
)
@api.depends('student_ids', 'syllabus_ids')
def _compute_counts(self):
for record in self:
record.enrollment_count = len(record.student_ids)
record.syllabus_count = len(record.syllabus_ids)
def action_recruit(self):
"""开始招生"""
self.write({'state': 'recruiting'})
def action_start(self):
"""开课"""
self.write({'state': 'ongoing'})
def action_finish(self):
"""结课"""
self.write({'state': 'finished'})

View File

@ -1,71 +0,0 @@
from odoo import fields, models, api
class Exam(models.Model):
_name = 'yuthon.school.exam'
_description = '训练考核'
_order = 'exam_date desc, id'
name = fields.Char(
string='考核名称',
required=True,
help='Odoo 18 模块开发单元测验',
)
course_id = fields.Many2one(
'yuthon.school.course',
string='课程',
ondelete='restrict',
)
student_id = fields.Many2one(
'yuthon.school.student',
string='学生',
required=True,
ondelete='cascade',
)
exam_type = fields.Selection(
[('quiz', '测验'), ('practice', '练习'),
('midterm', '期中'), ('final', '期末')],
string='考核类型',
default='quiz',
)
exam_date = fields.Date(
string='考核日期',
)
score = fields.Float(
string='得分',
default=0.0,
)
total_score = fields.Float(
string='满分',
default=100.0,
)
pass_score = fields.Float(
string='及格线',
default=60.0,
)
state = fields.Selection(
[('pending', '待考'), ('done', '已考')],
string='状态',
default='pending',
)
is_pass = fields.Boolean(
string='是否及格',
compute='_compute_is_pass',
store=True,
)
note = fields.Text(
string='评语',
)
active = fields.Boolean(
string='启用',
default=True,
)
@api.depends('score', 'pass_score')
def _compute_is_pass(self):
for record in self:
record.is_pass = bool(record.score >= record.pass_score)
def action_mark_done(self):
"""标记已考"""
self.write({'state': 'done'})

View File

@ -1,65 +0,0 @@
from odoo import fields, models, api
class ForumPost(models.Model):
_name = 'yuthon.school.forum_post'
_description = '学习交流'
_order = 'create_date desc, id'
name = fields.Char(
string='主题',
required=True,
help='Many2many 的 (6,0,[ids]) 怎么理解?',
)
content = fields.Html(
string='内容',
sanitize=True,
)
author_name = fields.Char(
string='作者',
help='发帖人姓名(教学演示用,避免关联复杂权限)',
)
author_type = fields.Selection(
[('student', '学生'), ('teacher', '老师')],
string='作者类型',
default='student',
)
course_id = fields.Many2one(
'yuthon.school.course',
string='关联课程',
ondelete='restrict',
)
post_type = fields.Selection(
[('question', '提问'), ('share', '分享'), ('discuss', '讨论')],
string='类型',
default='question',
)
like_count = fields.Integer(
string='点赞数',
default=0,
)
parent_id = fields.Many2one(
'yuthon.school.forum_post',
string='回复对象',
ondelete='cascade',
help='非空表示这是某帖的回复(实现楼中楼)',
)
reply_ids = fields.One2many(
'yuthon.school.forum_post',
'parent_id',
string='回复',
)
reply_count = fields.Integer(
string='回复数',
compute='_compute_reply_count',
store=True,
)
active = fields.Boolean(
string='启用',
default=True,
)
@api.depends('reply_ids')
def _compute_reply_count(self):
for record in self:
record.reply_count = len(record.reply_ids)

View File

@ -1,46 +0,0 @@
from odoo import fields, models, api
class Syllabus(models.Model):
_name = 'yuthon.school.syllabus'
_description = '课程大纲'
_order = 'sequence, id'
name = fields.Char(
string='大纲条目',
required=True,
help='如:模块开发与权限设计',
)
sequence = fields.Integer(
string='排序',
default=10,
)
week = fields.Char(
string='周次',
help='如:第 1 周',
)
course_id = fields.Many2one(
'yuthon.school.course',
string='所属课程',
required=True,
ondelete='cascade',
)
chapter_ids = fields.Many2many(
'yuthon.school.chapter',
'yuthon_syllabus_chapter_rel',
'syllabus_id', 'chapter_id',
string='覆盖章节',
help='本条目对应的教材章节Many2many 演示)',
)
objective = fields.Text(
string='教学目标',
help='学完本条目应掌握的能力',
)
content = fields.Html(
string='大纲内容',
sanitize=True,
)
active = fields.Boolean(
string='启用',
default=True,
)

View File

@ -1,16 +0,0 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_yuthon_school_video_user,yuthon.school.video.user,model_yuthon_school_video,base.group_user,1,1,1,1
access_yuthon_school_category_user,yuthon.school.category.user,model_yuthon_school_category,base.group_user,1,1,1,1
access_yuthon_school_textbook_user,yuthon.school.textbook.user,model_yuthon_school_textbook,base.group_user,1,1,1,1
access_yuthon_school_chapter_user,yuthon.school.chapter.user,model_yuthon_school_chapter,base.group_user,1,1,1,1
access_yuthon_school_school_user,yuthon.school.school.user,model_yuthon_school_school,base.group_user,1,1,1,1
access_yuthon_school_class_user,yuthon.school.class.user,model_yuthon_school_class,base.group_user,1,1,1,1
access_yuthon_school_teacher_user,yuthon.school.teacher.user,model_yuthon_school_teacher,base.group_user,1,1,1,1
access_yuthon_school_student_user,yuthon.school.student.user,model_yuthon_school_student,base.group_user,1,1,1,1
access_yuthon_school_progress_user,yuthon.school.progress.user,model_yuthon_school_progress,base.group_user,1,1,1,1
access_yuthon_school_elective_user,yuthon.school.elective.user,model_yuthon_school_elective,base.group_user,1,1,1,1
access_yuthon_school_progress_wizard_user,yuthon.school.progress.wizard.user,model_yuthon_school_progress_wizard,base.group_user,1,1,1,1
access_yuthon_school_course_user,yuthon.school.course.user,model_yuthon_school_course,base.group_user,1,1,1,1
access_yuthon_school_syllabus_user,yuthon.school.syllabus.user,model_yuthon_school_syllabus,base.group_user,1,1,1,1
access_yuthon_school_exam_user,yuthon.school.exam.user,model_yuthon_school_exam,base.group_user,1,1,1,1
access_yuthon_school_forum_post_user,yuthon.school.forum_post.user,model_yuthon_school_forum_post,base.group_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_yuthon_school_video_user yuthon.school.video.user model_yuthon_school_video base.group_user 1 1 1 1
3 access_yuthon_school_category_user yuthon.school.category.user model_yuthon_school_category base.group_user 1 1 1 1
4 access_yuthon_school_textbook_user yuthon.school.textbook.user model_yuthon_school_textbook base.group_user 1 1 1 1
5 access_yuthon_school_chapter_user yuthon.school.chapter.user model_yuthon_school_chapter base.group_user 1 1 1 1
6 access_yuthon_school_school_user yuthon.school.school.user model_yuthon_school_school base.group_user 1 1 1 1
7 access_yuthon_school_class_user yuthon.school.class.user model_yuthon_school_class base.group_user 1 1 1 1
8 access_yuthon_school_teacher_user yuthon.school.teacher.user model_yuthon_school_teacher base.group_user 1 1 1 1
9 access_yuthon_school_student_user yuthon.school.student.user model_yuthon_school_student base.group_user 1 1 1 1
10 access_yuthon_school_progress_user yuthon.school.progress.user model_yuthon_school_progress base.group_user 1 1 1 1
11 access_yuthon_school_elective_user yuthon.school.elective.user model_yuthon_school_elective base.group_user 1 1 1 1
12 access_yuthon_school_progress_wizard_user yuthon.school.progress.wizard.user model_yuthon_school_progress_wizard base.group_user 1 1 1 1
13 access_yuthon_school_course_user yuthon.school.course.user model_yuthon_school_course base.group_user 1 1 1 1
14 access_yuthon_school_syllabus_user yuthon.school.syllabus.user model_yuthon_school_syllabus base.group_user 1 1 1 1
15 access_yuthon_school_exam_user yuthon.school.exam.user model_yuthon_school_exam base.group_user 1 1 1 1
16 access_yuthon_school_forum_post_user yuthon.school.forum_post.user model_yuthon_school_forum_post base.group_user 1 1 1 1

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -1,148 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- 课程:列表视图 -->
<record id="view_yuthon_school_course_list" model="ir.ui.view">
<field name="name">yuthon.school.course.list</field>
<field name="model">yuthon.school.course</field>
<field name="arch" type="xml">
<list string="课程">
<field name="name"/>
<field name="code"/>
<field name="category_id"/>
<field name="teacher_id"/>
<field name="difficulty"/>
<field name="state"/>
<field name="total_hours"/>
<field name="enrollment_count"/>
<field name="syllabus_count"/>
</list>
</field>
</record>
<!-- 课程:表单视图 -->
<record id="view_yuthon_school_course_form" model="ir.ui.view">
<field name="name">yuthon.school.course.form</field>
<field name="model">yuthon.school.course</field>
<field name="arch" type="xml">
<form>
<header>
<button name="action_recruit" type="object" string="开始招生"
class="btn-primary" invisible="state != 'draft'"/>
<button name="action_start" type="object" string="开课"
invisible="state not in ('draft', 'recruiting')"/>
<button name="action_finish" type="object" string="结课"
invisible="state != 'ongoing'"/>
<field name="state" widget="statusbar" statusbar_visible="draft,recruiting,ongoing,finished"/>
</header>
<sheet>
<group>
<group>
<field name="name"/>
<field name="code"/>
<field name="category_id"/>
<field name="teacher_id"/>
</group>
<group>
<field name="difficulty"/>
<field name="total_hours"/>
<field name="enrollment_count"/>
<field name="syllabus_count"/>
</group>
</group>
<field name="description" placeholder="课程目标、适合人群、考核方式……"/>
<notebook>
<page string="关联教材" name="textbooks">
<field name="textbook_ids" widget="many2many">
<list>
<field name="name"/>
<field name="difficulty"/>
<field name="total_chapters"/>
<field name="total_hours"/>
<field name="total_videos"/>
<field name="state"/>
</list>
<form>
<field name="name"/>
<field name="difficulty"/>
<field name="state"/>
<field name="description"/>
</form>
</field>
</page>
<page string="课程大纲" name="syllabus">
<field name="syllabus_ids">
<list editable="bottom">
<field name="sequence" optional="show"/>
<field name="week"/>
<field name="name"/>
<field name="chapter_ids" widget="many2many_tags"/>
<field name="objective" optional="hide"/>
</list>
<form>
<group>
<field name="name"/>
<field name="week"/>
<field name="course_id" invisible="1"/>
</group>
<group>
<field name="chapter_ids" widget="many2many_tags"
domain="[('textbook_id', 'in', parent.textbook_ids)]"/>
</group>
<field name="objective" placeholder="学完本条目应掌握的能力"/>
<field name="content"/>
</form>
</field>
</page>
<page string="选课学生" name="students">
<field name="student_ids" widget="many2many">
<list>
<field name="name"/>
<field name="student_no"/>
<field name="class_id"/>
<field name="school_id"/>
</list>
<form>
<field name="name"/>
<field name="student_no"/>
<field name="class_id"/>
</form>
</field>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<!-- 课程:搜索视图 -->
<record id="view_yuthon_school_course_search" model="ir.ui.view">
<field name="name">yuthon.school.course.search</field>
<field name="model">yuthon.school.course</field>
<field name="arch" type="xml">
<search>
<field name="name"/>
<field name="code"/>
<field name="category_id"/>
<field name="teacher_id"/>
<field name="difficulty"/>
<field name="state"/>
</search>
</field>
</record>
<!-- 课程:动作 -->
<record id="action_yuthon_course" model="ir.actions.act_window">
<field name="name">课程管理</field>
<field name="res_model">yuthon.school.course</field>
<field name="view_mode">list,form</field>
<field name="search_view_id" ref="view_yuthon_school_course_search"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
暂无课程,点击「新建」创建一门课程(关联教材、大纲与选课学生)。
</p>
</field>
</record>
</odoo>

View File

@ -1,90 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- List View -->
<record id="view_yuthon_school_elective_list" model="ir.ui.view">
<field name="name">yuthon.school.elective.list</field>
<field name="model">yuthon.school.elective</field>
<field name="arch" type="xml">
<list string="选修课">
<field name="name"/>
<field name="teacher_id"/>
<field name="student_count"/>
<field name="student_ids" widget="many2many_tags"
options="{'color_field': 'none'}"/>
</list>
</field>
</record>
<!-- Form View -->
<record id="view_yuthon_school_elective_form" model="ir.ui.view">
<field name="name">yuthon.school.elective.form</field>
<field name="model">yuthon.school.elective</field>
<field name="arch" type="xml">
<form string="选修课">
<sheet>
<group>
<group>
<field name="name"/>
<field name="teacher_id"/>
</group>
<group>
<field name="student_count"/>
<field name="active" invisible="1"/>
</group>
</group>
<group>
<field name="description"/>
</group>
<notebook>
<page string="选课学生" name="student_ids">
<field name="student_ids">
<list editable="bottom">
<field name="name"/>
<field name="student_no"/>
<field name="class_id"/>
</list>
</field>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<!-- Search View -->
<record id="view_yuthon_school_elective_search" model="ir.ui.view">
<field name="name">yuthon.school.elective.search</field>
<field name="model">yuthon.school.elective</field>
<field name="arch" type="xml">
<search string="搜索选修课">
<field name="name"/>
<field name="teacher_id"/>
<field name="student_ids"/>
<separator/>
<group expand="0" string="分组">
<filter name="groupby_teacher" string="授课教师"
context="{'group_by': 'teacher_id'}"/>
</group>
</search>
</field>
</record>
<!-- Action -->
<record id="action_yuthon_school_elective" model="ir.actions.act_window">
<field name="name">选修课</field>
<field name="res_model">yuthon.school.elective</field>
<field name="view_mode">list,form</field>
<field name="search_view_id" ref="view_yuthon_school_elective_search"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
暂无选修课记录
</p>
<p>
选修课与学生之间是 Many2many多对多关系
一个学生可选多门选修课,一门选修课可有多个学生。
</p>
</field>
</record>
</odoo>

View File

@ -1,84 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- 训练考核:列表视图 -->
<record id="view_yuthon_school_exam_list" model="ir.ui.view">
<field name="name">yuthon.school.exam.list</field>
<field name="model">yuthon.school.exam</field>
<field name="arch" type="xml">
<list string="训练考核" decoration-success="is_pass" decoration-danger="is_pass == False and state == 'done'">
<field name="name"/>
<field name="course_id"/>
<field name="student_id"/>
<field name="exam_type"/>
<field name="exam_date"/>
<field name="score"/>
<field name="total_score"/>
<field name="is_pass" optional="show"/>
<field name="state"/>
</list>
</field>
</record>
<!-- 训练考核:表单视图 -->
<record id="view_yuthon_school_exam_form" model="ir.ui.view">
<field name="name">yuthon.school.exam.form</field>
<field name="model">yuthon.school.exam</field>
<field name="arch" type="xml">
<form>
<header>
<button name="action_mark_done" type="object" string="标记已考"
class="btn-primary" invisible="state != 'pending'"/>
<field name="state" widget="statusbar" statusbar_visible="pending,done"/>
</header>
<sheet>
<group>
<group>
<field name="name"/>
<field name="course_id"/>
<field name="student_id"/>
<field name="exam_type"/>
</group>
<group>
<field name="exam_date"/>
<field name="score"/>
<field name="total_score"/>
<field name="pass_score"/>
<field name="is_pass" readonly="1"/>
</group>
</group>
<field name="note" placeholder="评语……"/>
</sheet>
</form>
</field>
</record>
<!-- 训练考核:搜索视图 -->
<record id="view_yuthon_school_exam_search" model="ir.ui.view">
<field name="name">yuthon.school.exam.search</field>
<field name="model">yuthon.school.exam</field>
<field name="arch" type="xml">
<search>
<field name="name"/>
<field name="course_id"/>
<field name="student_id"/>
<field name="exam_type"/>
<field name="state"/>
<filter string="已及格" name="passed" domain="[('is_pass', '=', True)]"/>
<filter string="未及格" name="failed" domain="[('is_pass', '=', False), ('state', '=', 'done')]"/>
</search>
</field>
</record>
<!-- 训练考核:动作 -->
<record id="action_yuthon_exam" model="ir.actions.act_window">
<field name="name">训练考核</field>
<field name="res_model">yuthon.school.exam</field>
<field name="view_mode">list,form</field>
<field name="search_view_id" ref="view_yuthon_school_exam_search"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
暂无考核记录,点击「新建」登记一次测验、练习或考试成绩。
</p>
</field>
</record>
</odoo>

View File

@ -1,91 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- 学习交流:列表视图(仅显示主帖,回复在表单中展开) -->
<record id="view_yuthon_school_forum_post_list" model="ir.ui.view">
<field name="name">yuthon.school.forum_post.list</field>
<field name="model">yuthon.school.forum_post</field>
<field name="arch" type="xml">
<list string="学习交流" decoration-info="post_type == 'share'">
<field name="name"/>
<field name="post_type"/>
<field name="author_name"/>
<field name="author_type"/>
<field name="course_id"/>
<field name="like_count"/>
<field name="reply_count"/>
<field name="create_date"/>
</list>
</field>
</record>
<!-- 学习交流:表单视图 -->
<record id="view_yuthon_school_forum_post_form" model="ir.ui.view">
<field name="name">yuthon.school.forum_post.form</field>
<field name="model">yuthon.school.forum_post</field>
<field name="arch" type="xml">
<form>
<sheet>
<group>
<group>
<field name="name"/>
<field name="post_type"/>
<field name="course_id"/>
</group>
<group>
<field name="author_name"/>
<field name="author_type"/>
<field name="like_count"/>
</group>
</group>
<field name="content" placeholder="详细描述你的问题或分享……"/>
<separator string="回复"/>
<field name="reply_ids">
<list>
<field name="name"/>
<field name="author_name"/>
<field name="author_type"/>
<field name="like_count"/>
<field name="create_date"/>
</list>
<form>
<group>
<field name="name"/>
<field name="author_name"/>
<field name="author_type"/>
</group>
<field name="content"/>
</form>
</field>
</sheet>
</form>
</field>
</record>
<!-- 学习交流:搜索视图 -->
<record id="view_yuthon_school_forum_post_search" model="ir.ui.view">
<field name="name">yuthon.school.forum_post.search</field>
<field name="model">yuthon.school.forum_post</field>
<field name="arch" type="xml">
<search>
<field name="name"/>
<field name="course_id"/>
<field name="post_type"/>
<field name="author_type"/>
</search>
</field>
</record>
<!-- 学习交流:动作 -->
<record id="action_yuthon_forum_post" model="ir.actions.act_window">
<field name="name">学习交流</field>
<field name="res_model">yuthon.school.forum_post</field>
<field name="view_mode">list,form</field>
<field name="search_view_id" ref="view_yuthon_school_forum_post_search"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
还没有交流帖,点击「新建」发起一个提问、分享或讨论。
</p>
</field>
</record>
</odoo>

View File

@ -1,70 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- 说明:原「学习进度」菜单已移至「课程管理」下,作为「学习进展」 -->
<!-- ============ 课程管理 ============ -->
<menuitem id="menu_yuthon_course_root"
name="课程管理"
web_icon="school,static/description/icon.png"
sequence="10"/>
<menuitem id="menu_yuthon_course"
name="课程管理"
parent="menu_yuthon_course_root"
action="action_yuthon_course"
sequence="10"/>
<menuitem id="menu_yuthon_course_progress"
name="学习进展"
parent="menu_yuthon_course_root"
action="action_yuthon_school_progress"
sequence="20"/>
<menuitem id="menu_yuthon_exam"
name="训练考核"
parent="menu_yuthon_course_root"
action="action_yuthon_exam"
sequence="30"/>
<menuitem id="menu_yuthon_forum_post"
name="学习交流"
parent="menu_yuthon_course_root"
action="action_yuthon_forum_post"
sequence="40"/>
<!-- ============ 组织架构 ============ -->
<menuitem id="menu_yuthon_school_organization"
name="组织架构"
web_icon="school,static/description/icon1.png"
sequence="20"/>
<menuitem id="menu_yuthon_school_school"
name="学校管理"
parent="menu_yuthon_school_organization"
action="action_yuthon_school_school"
sequence="10"/>
<menuitem id="menu_yuthon_school_class"
name="班级管理"
parent="menu_yuthon_school_organization"
action="action_yuthon_school_class"
sequence="20"/>
<menuitem id="menu_yuthon_school_teacher"
name="老师管理"
parent="menu_yuthon_school_organization"
action="action_yuthon_school_teacher"
sequence="30"/>
<menuitem id="menu_yuthon_school_student"
name="学生管理"
parent="menu_yuthon_school_organization"
action="action_yuthon_school_student"
sequence="40"/>
<menuitem id="menu_yuthon_school_elective"
name="选修课"
parent="menu_yuthon_school_organization"
action="action_yuthon_school_elective"
sequence="50"/>
</odoo>

View File

@ -1,336 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- ============ 学校 ============ -->
<record id="view_yuthon_school_school_list" model="ir.ui.view">
<field name="name">yuthon.school.school.list</field>
<field name="model">yuthon.school.school</field>
<field name="arch" type="xml">
<list string="学校">
<field name="name"/>
<field name="principal"/>
<field name="phone"/>
<field name="class_count"/>
<field name="teacher_count"/>
</list>
</field>
</record>
<record id="view_yuthon_school_school_form" model="ir.ui.view">
<field name="name">yuthon.school.school.form</field>
<field name="model">yuthon.school.school</field>
<field name="arch" type="xml">
<form string="学校">
<sheet>
<group>
<group>
<field name="name"/>
<field name="principal"/>
<field name="phone"/>
</group>
<group>
<field name="address"/>
<field name="class_count"/>
<field name="teacher_count"/>
<field name="active" invisible="1"/>
</group>
</group>
<notebook>
<page string="班级" name="class_page">
<field name="class_ids" nolabel="1">
<list>
<field name="name"/>
<field name="grade"/>
<field name="teacher_id"/>
<field name="student_count"/>
</list>
</field>
</page>
<page string="教师" name="teacher_page">
<field name="teacher_ids" nolabel="1">
<list>
<field name="name"/>
<field name="phone"/>
<field name="subject"/>
</list>
</field>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record id="view_yuthon_school_school_search" model="ir.ui.view">
<field name="name">yuthon.school.school.search</field>
<field name="model">yuthon.school.school</field>
<field name="arch" type="xml">
<search string="搜索学校">
<field name="name"/>
<field name="principal"/>
</search>
</field>
</record>
<record id="action_yuthon_school_school" model="ir.actions.act_window">
<field name="name">学校管理</field>
<field name="res_model">yuthon.school.school</field>
<field name="view_mode">list,form</field>
<field name="search_view_id" ref="view_yuthon_school_school_search"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
创建第一所学校
</p>
</field>
</record>
<!-- ============ 班级 ============ -->
<record id="view_yuthon_school_class_list" model="ir.ui.view">
<field name="name">yuthon.school.class.list</field>
<field name="model">yuthon.school.class</field>
<field name="arch" type="xml">
<list string="班级">
<field name="name"/>
<field name="school_id"/>
<field name="grade"/>
<field name="teacher_id"/>
<field name="student_count"/>
</list>
</field>
</record>
<record id="view_yuthon_school_class_form" model="ir.ui.view">
<field name="name">yuthon.school.class.form</field>
<field name="model">yuthon.school.class</field>
<field name="arch" type="xml">
<form string="班级">
<sheet>
<group>
<group>
<field name="name"/>
<field name="school_id"/>
<field name="teacher_id"/>
<field name="grade"/>
</group>
<group>
<field name="student_count"/>
<field name="active" invisible="1"/>
</group>
</group>
<notebook>
<page string="学生" name="student_page">
<field name="student_ids" nolabel="1">
<list editable="bottom">
<field name="student_no"/>
<field name="name"/>
<field name="gender"/>
<field name="phone"/>
<field name="enrollment_date"/>
</list>
</field>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record id="view_yuthon_school_class_search" model="ir.ui.view">
<field name="name">yuthon.school.class.search</field>
<field name="model">yuthon.school.class</field>
<field name="arch" type="xml">
<search string="搜索班级">
<field name="name"/>
<field name="school_id"/>
<field name="teacher_id"/>
<separator/>
<group expand="0" string="分组">
<filter name="groupby_school" string="学校"
context="{'group_by': 'school_id'}"/>
</group>
</search>
</field>
</record>
<record id="action_yuthon_school_class" model="ir.actions.act_window">
<field name="name">班级管理</field>
<field name="res_model">yuthon.school.class</field>
<field name="view_mode">list,form</field>
<field name="search_view_id" ref="view_yuthon_school_class_search"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
创建第一个班级
</p>
</field>
</record>
<!-- ============ 老师 ============ -->
<record id="view_yuthon_school_teacher_list" model="ir.ui.view">
<field name="name">yuthon.school.teacher.list</field>
<field name="model">yuthon.school.teacher</field>
<field name="arch" type="xml">
<list string="老师">
<field name="name"/>
<field name="school_id"/>
<field name="subject"/>
<field name="phone"/>
</list>
</field>
</record>
<record id="view_yuthon_school_teacher_form" model="ir.ui.view">
<field name="name">yuthon.school.teacher.form</field>
<field name="model">yuthon.school.teacher</field>
<field name="arch" type="xml">
<form string="老师">
<sheet>
<group>
<group>
<field name="name"/>
<field name="school_id"/>
</group>
<group>
<field name="phone"/>
<field name="subject"/>
<field name="active" invisible="1"/>
</group>
</group>
<notebook>
<page string="负责班级" name="class_page">
<field name="class_ids" nolabel="1">
<list>
<field name="name"/>
<field name="grade"/>
<field name="student_count"/>
</list>
</field>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record id="view_yuthon_school_teacher_search" model="ir.ui.view">
<field name="name">yuthon.school.teacher.search</field>
<field name="model">yuthon.school.teacher</field>
<field name="arch" type="xml">
<search string="搜索老师">
<field name="name"/>
<field name="school_id"/>
<field name="subject"/>
<separator/>
<group expand="0" string="分组">
<filter name="groupby_school" string="学校"
context="{'group_by': 'school_id'}"/>
</group>
</search>
</field>
</record>
<record id="action_yuthon_school_teacher" model="ir.actions.act_window">
<field name="name">老师管理</field>
<field name="res_model">yuthon.school.teacher</field>
<field name="view_mode">list,form</field>
<field name="search_view_id" ref="view_yuthon_school_teacher_search"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
添加第一位老师
</p>
</field>
</record>
<!-- ============ 学生 ============ -->
<record id="view_yuthon_school_student_list" model="ir.ui.view">
<field name="name">yuthon.school.student.list</field>
<field name="model">yuthon.school.student</field>
<field name="arch" type="xml">
<list string="学生">
<field name="student_no"/>
<field name="name"/>
<field name="school_id"/>
<field name="class_id"/>
<field name="gender"/>
<field name="phone"/>
<field name="enrollment_date"/>
</list>
</field>
</record>
<record id="view_yuthon_school_student_form" model="ir.ui.view">
<field name="name">yuthon.school.student.form</field>
<field name="model">yuthon.school.student</field>
<field name="arch" type="xml">
<form string="学生">
<sheet>
<group>
<group>
<field name="name"/>
<field name="student_no"/>
<field name="class_id"/>
<field name="school_id"/>
</group>
<group>
<field name="gender"/>
<field name="phone"/>
<field name="enrollment_date"/>
<field name="active" invisible="1"/>
</group>
</group>
<notebook>
<page string="选修课" name="elective_page">
<field name="elective_ids">
<list>
<field name="name"/>
<field name="teacher_id"/>
</list>
</field>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record id="view_yuthon_school_student_search" model="ir.ui.view">
<field name="name">yuthon.school.student.search</field>
<field name="model">yuthon.school.student</field>
<field name="arch" type="xml">
<search string="搜索学生">
<field name="name"/>
<field name="student_no"/>
<field name="class_id"/>
<field name="school_id"/>
<separator/>
<filter name="filter_male" string="男"
domain="[('gender', '=', 'male')]"/>
<filter name="filter_female" string="女"
domain="[('gender', '=', 'female')]"/>
<separator/>
<group expand="0" string="分组">
<filter name="groupby_class" string="班级"
context="{'group_by': 'class_id'}"/>
<filter name="groupby_school" string="学校"
context="{'group_by': 'school_id'}"/>
</group>
</search>
</field>
</record>
<record id="action_yuthon_school_student" model="ir.actions.act_window">
<field name="name">学生管理</field>
<field name="res_model">yuthon.school.student</field>
<field name="view_mode">list,form</field>
<field name="search_view_id" ref="view_yuthon_school_student_search"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
添加第一位学生
</p>
</field>
</record>
</odoo>

View File

@ -1,143 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Kanban View -->
<record id="view_yuthon_school_progress_kanban" model="ir.ui.view">
<field name="name">yuthon.school.progress.kanban</field>
<field name="model">yuthon.school.progress</field>
<field name="arch" type="xml">
<kanban default_group_by="state" class="o_kanban_small_column">
<field name="state"/>
<field name="student_id"/>
<field name="textbook_id"/>
<field name="chapter_id"/>
<field name="video_id"/>
<field name="progress"/>
<field name="completed"/>
<templates>
<t t-name="kanban-box">
<div class="oe_kanban_card oe_kanban_global_click o_kanban_record_has_image_fill">
<div class="o_kanban_record_title">
<field name="student_id"/>
</div>
<div class="o_kanban_record_body">
<div><field name="textbook_id"/></div>
<div><field name="chapter_id"/></div>
</div>
<div class="o_kanban_record_bottom">
<div class="oe_kanban_bottom_left">
<field name="progress" widget="progressbar"/>
</div>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<!-- List View -->
<record id="view_yuthon_school_progress_list" model="ir.ui.view">
<field name="name">yuthon.school.progress.list</field>
<field name="model">yuthon.school.progress</field>
<field name="arch" type="xml">
<list string="学习进度" default_order="student_id, textbook_id">
<field name="student_id"/>
<field name="teacher_id"/>
<field name="textbook_id"/>
<field name="chapter_id"/>
<field name="video_id"/>
<field name="progress" widget="progressbar"/>
<field name="completed" widget="boolean_toggle"/>
<field name="state" widget="badge"
decoration-muted="state == 'not_started'"
decoration-info="state == 'in_progress'"
decoration-success="state == 'completed'"/>
<field name="last_study_date"/>
</list>
</field>
</record>
<!-- Form View -->
<record id="view_yuthon_school_progress_form" model="ir.ui.view">
<field name="name">yuthon.school.progress.form</field>
<field name="model">yuthon.school.progress</field>
<field name="arch" type="xml">
<form string="学习进度">
<sheet>
<group>
<group string="学习内容">
<field name="student_id"/>
<field name="textbook_id"/>
<field name="chapter_id"/>
<field name="video_id"/>
</group>
<group string="进度信息">
<field name="progress" widget="progressbar"/>
<field name="completed" widget="boolean_toggle"/>
<field name="last_study_date"/>
<field name="teacher_id"/>
<field name="active" invisible="1"/>
</group>
</group>
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids"/>
<field name="activity_ids"/>
<field name="message_ids"/>
</div>
</form>
</field>
</record>
<!-- Search View -->
<record id="view_yuthon_school_progress_search" model="ir.ui.view">
<field name="name">yuthon.school.progress.search</field>
<field name="model">yuthon.school.progress</field>
<field name="arch" type="xml">
<search string="搜索学习进度">
<field name="student_id"/>
<field name="textbook_id"/>
<field name="teacher_id"/>
<separator/>
<filter name="filter_not_started" string="未开始"
domain="[('state', '=', 'not_started')]"/>
<filter name="filter_in_progress" string="进行中"
domain="[('state', '=', 'in_progress')]"/>
<filter name="filter_completed" string="已完成"
domain="[('state', '=', 'completed')]"/>
<separator/>
<filter name="filter_low_progress" string="进度不足30%"
domain="[('progress', '&lt;', 30)]"/>
<separator/>
<group expand="0" string="分组">
<filter name="groupby_state" string="学习状态"
context="{'group_by': 'state'}"/>
<filter name="groupby_student" string="学生"
context="{'group_by': 'student_id'}"/>
<filter name="groupby_textbook" string="教材"
context="{'group_by': 'textbook_id'}"/>
<filter name="groupby_teacher" string="教师"
context="{'group_by': 'teacher_id'}"/>
</group>
</search>
</field>
</record>
<!-- Action -->
<record id="action_yuthon_school_progress" model="ir.actions.act_window">
<field name="name">学习进度</field>
<field name="res_model">yuthon.school.progress</field>
<field name="view_mode">kanban,list,form</field>
<field name="search_view_id" ref="view_yuthon_school_progress_search"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
暂无学习进度记录
</p>
<p>
学生的学习进度会在此显示,教师可按学生或教材查看
</p>
</field>
</record>
</odoo>

View File

@ -1,41 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Wizard Form View -->
<record id="view_yuthon_school_progress_wizard_form" model="ir.ui.view">
<field name="name">yuthon.school.progress.wizard.form</field>
<field name="model">yuthon.school.progress.wizard</field>
<field name="arch" type="xml">
<form string="批量更新学习进度">
<group>
<group string="选择范围">
<field name="class_id"/>
<field name="chapter_id"/>
<field name="textbook_id"/>
</group>
<group string="进度设置">
<field name="progress_value"/>
<field name="completed"/>
</group>
</group>
<footer>
<button name="action_apply" type="object"
string="确认更新" class="btn-primary"/>
<button special="cancel" string="取消"
class="btn-secondary"/>
</footer>
</form>
</field>
</record>
<!-- Wizard Action -->
<record id="action_yuthon_school_progress_wizard" model="ir.actions.act_window">
<field name="name">批量更新进度</field>
<field name="res_model">yuthon.school.progress.wizard</field>
<field name="view_mode">form</field>
<field name="target">new</field>
<field name="binding_model_id" ref="model_yuthon_school_progress"/>
<field name="binding_view_types">list,form</field>
</record>
</odoo>

View File

@ -1,174 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- ============ 教材 ============ -->
<!-- List View -->
<record id="view_yuthon_school_textbook_list" model="ir.ui.view">
<field name="name">yuthon.school.textbook.list</field>
<field name="model">yuthon.school.textbook</field>
<field name="arch" type="xml">
<list string="教材" default_order="category_id, sequence">
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="category_id"/>
<field name="difficulty" widget="badge"
decoration-info="difficulty == 'beginner'"
decoration-warning="difficulty == 'intermediate'"
decoration-danger="difficulty == 'advanced'"/>
<field name="total_chapters"/>
<field name="total_videos"/>
<field name="total_hours"/>
<field name="state" widget="badge"
decoration-success="state == 'published'"
decoration-muted="state == 'draft'"/>
</list>
</field>
</record>
<!-- Form View -->
<record id="view_yuthon_school_textbook_form" model="ir.ui.view">
<field name="name">yuthon.school.textbook.form</field>
<field name="model">yuthon.school.textbook</field>
<field name="arch" type="xml">
<form string="教材">
<header>
<button name="action_confirm" string="发布" type="object"
class="oe_highlight" invisible="state != 'draft'"/>
<button name="action_set_draft" string="退回草稿" type="object"
invisible="state != 'published'"/>
<field name="state" widget="statusbar"
statusbar_visible="draft,published"/>
</header>
<sheet>
<group>
<group>
<field name="name"/>
<field name="category_id"/>
<field name="difficulty" widget="radio"/>
<field name="sequence"/>
</group>
<group>
<field name="total_hours"/>
<field name="total_chapters"/>
<field name="total_videos"/>
<field name="active" invisible="1"/>
</group>
</group>
<group string="教材简介">
<field name="description" nolabel="1"
placeholder="教材的内容概要和适用人群..."/>
</group>
<notebook>
<page string="章节" name="chapter_page">
<field name="chapter_ids" nolabel="1">
<list editable="bottom">
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="hours"/>
<field name="video_count"/>
</list>
</field>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<!-- Search View -->
<record id="view_yuthon_school_textbook_search" model="ir.ui.view">
<field name="name">yuthon.school.textbook.search</field>
<field name="model">yuthon.school.textbook</field>
<field name="arch" type="xml">
<search string="搜索教材">
<field name="name"/>
<field name="category_id"/>
<separator/>
<filter name="filter_beginner" string="入门"
domain="[('difficulty', '=', 'beginner')]"/>
<filter name="filter_intermediate" string="进阶"
domain="[('difficulty', '=', 'intermediate')]"/>
<filter name="filter_advanced" string="高级"
domain="[('difficulty', '=', 'advanced')]"/>
<separator/>
<filter name="filter_draft" string="草稿"
domain="[('state', '=', 'draft')]"/>
<filter name="filter_published" string="已发布"
domain="[('state', '=', 'published')]"/>
<separator/>
<group expand="0" string="分组">
<filter name="groupby_category" string="学习分类"
context="{'group_by': 'category_id'}"/>
<filter name="groupby_difficulty" string="难度"
context="{'group_by': 'difficulty'}"/>
</group>
</search>
</field>
</record>
<!-- Action -->
<record id="action_yuthon_school_textbook" model="ir.actions.act_window">
<field name="name">教材管理</field>
<field name="res_model">yuthon.school.textbook</field>
<field name="view_mode">list,form</field>
<field name="search_view_id" ref="view_yuthon_school_textbook_search"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
创建第一本教材
</p>
<p>
教材是完整课程的载体,下设章节和视频,带学时统计
</p>
</field>
</record>
<!-- ============ 章节(独立表单视图,用于弹窗打开) ============ -->
<record id="view_yuthon_school_chapter_form" model="ir.ui.view">
<field name="name">yuthon.school.chapter.form</field>
<field name="model">yuthon.school.chapter</field>
<field name="arch" type="xml">
<form string="章节">
<sheet>
<group>
<group>
<field name="name"/>
<field name="textbook_id"/>
<field name="sequence"/>
<field name="hours"/>
</group>
<group>
<field name="video_count"/>
</group>
</group>
<group string="内容概述">
<field name="summary" nolabel="1"
placeholder="本章涵盖的内容概述..."/>
</group>
<group string="知识要点">
<field name="key_points" nolabel="1"
placeholder="本章需要掌握的关键知识点..."/>
</group>
<notebook>
<page string="视频" name="video_page">
<field name="video_ids" nolabel="1">
<list>
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="difficulty" widget="badge"
decoration-info="difficulty == 'beginner'"
decoration-warning="difficulty == 'intermediate'"
decoration-danger="difficulty == 'advanced'"/>
<field name="duration"/>
<field name="file_size_display"/>
</list>
</field>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
</odoo>

View File

@ -1,108 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- List View -->
<record id="view_yuthon_school_category_list" model="ir.ui.view">
<field name="name">yuthon.school.category.list</field>
<field name="model">yuthon.school.category</field>
<field name="arch" type="xml">
<list string="学习目录">
<field name="complete_name"/>
<field name="level" widget="badge"
decoration-info="level == 'beginner'"
decoration-warning="level == 'intermediate'"
decoration-danger="level == 'advanced'"/>
<field name="video_count"/>
<field name="active" widget="boolean_toggle" invisible="1"/>
</list>
</field>
</record>
<!-- Form View -->
<record id="view_yuthon_school_category_form" model="ir.ui.view">
<field name="name">yuthon.school.category.form</field>
<field name="model">yuthon.school.category</field>
<field name="arch" type="xml">
<form string="学习目录">
<sheet>
<widget name="web_ribbon" title="已归档" bg_color="bg-danger" invisible="active"/>
<group>
<group>
<field name="name"/>
<field name="parent_id"/>
<field name="complete_name" readonly="1"/>
<field name="level" widget="radio"/>
</group>
<group>
<field name="video_count"/>
<field name="active" invisible="1"/>
</group>
</group>
<group string="目录说明">
<field name="description" nolabel="1" placeholder="描述该学习目录涵盖的内容..."/>
</group>
<notebook>
<page string="下级目录" name="child_page">
<field name="child_ids" nolabel="1">
<list editable="bottom">
<field name="name"/>
<field name="level"/>
<field name="video_count"/>
</list>
</field>
</page>
<page string="教学视频" name="video_page">
<field name="video_ids" nolabel="1">
<list>
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="difficulty" widget="badge"
decoration-info="difficulty == 'beginner'"
decoration-warning="difficulty == 'intermediate'"
decoration-danger="difficulty == 'advanced'"/>
<field name="duration"/>
<field name="file_size_display"/>
</list>
</field>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<!-- Search View -->
<record id="view_yuthon_school_category_search" model="ir.ui.view">
<field name="name">yuthon.school.category.search</field>
<field name="model">yuthon.school.category</field>
<field name="arch" type="xml">
<search string="搜索目录">
<field name="name"/>
<field name="complete_name"/>
<separator/>
<filter name="filter_beginner" string="入门" domain="[('level', '=', 'beginner')]"/>
<filter name="filter_intermediate" string="进阶" domain="[('level', '=', 'intermediate')]"/>
<filter name="filter_advanced" string="高级" domain="[('level', '=', 'advanced')]"/>
<separator/>
<filter name="archived" string="已归档" domain="[('active', '=', False)]"/>
</search>
</field>
</record>
<!-- Action -->
<record id="action_yuthon_school_category" model="ir.actions.act_window">
<field name="name">学习目录</field>
<field name="res_model">yuthon.school.category</field>
<field name="view_mode">list,form</field>
<field name="search_view_id" ref="view_yuthon_school_category_search"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
创建第一个学习目录
</p>
<p>
按 Odoo 18 知识体系建立树形目录,如:基础入门 > 模块结构 > 字段类型
</p>
</field>
</record>
</odoo>

View File

@ -1,114 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- List View -->
<record id="view_yuthon_school_video_list" model="ir.ui.view">
<field name="name">yuthon.school.video.list</field>
<field name="model">yuthon.school.video</field>
<field name="arch" type="xml">
<list string="教学视频" default_order="category_id, sequence">
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="category_id"/>
<field name="chapter_id"/>
<field name="difficulty" widget="badge"
decoration-info="difficulty == 'beginner'"
decoration-warning="difficulty == 'intermediate'"
decoration-danger="difficulty == 'advanced'"/>
<field name="duration"/>
<field name="video_filename"/>
<field name="file_size_display"/>
<field name="active" widget="boolean_toggle" invisible="1"/>
</list>
</field>
</record>
<!-- Form View -->
<record id="view_yuthon_school_video_form" model="ir.ui.view">
<field name="name">yuthon.school.video.form</field>
<field name="model">yuthon.school.video</field>
<field name="arch" type="xml">
<form string="教学视频">
<sheet>
<widget name="web_ribbon" title="已归档" bg_color="bg-danger" invisible="active"/>
<group>
<group>
<field name="name"/>
<field name="category_id"/>
<field name="chapter_id"/>
<field name="difficulty" widget="radio"/>
<field name="duration"/>
<field name="sequence"/>
</group>
<group>
<field name="video_filename" invisible="1"/>
<field name="file_size_display"/>
<field name="active" invisible="1"/>
</group>
</group>
<notebook>
<page string="视频文件" name="video_file_page">
<group>
<field name="video_file" filename="video_filename" nolabel="1"
help="上传本地教学视频文件"/>
</group>
</page>
<page string="视频播放" name="video_player_page" invisible="not id">
<group>
<field name="video_player" widget="html" nolabel="1" readonly="1"/>
</group>
</page>
<page string="内容说明" name="description_page">
<group string="视频简介">
<field name="description" nolabel="1" placeholder="该教学视频的内容概要..."/>
</group>
<group string="知识要点">
<field name="key_points" nolabel="1" placeholder="列出该视频涵盖的关键知识点..."/>
</group>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<!-- Search View -->
<record id="view_yuthon_school_video_search" model="ir.ui.view">
<field name="name">yuthon.school.video.search</field>
<field name="model">yuthon.school.video</field>
<field name="arch" type="xml">
<search string="搜索视频">
<field name="name"/>
<field name="category_id"/>
<field name="chapter_id"/>
<field name="video_filename"/>
<separator/>
<filter name="filter_beginner" string="入门" domain="[('difficulty', '=', 'beginner')]"/>
<filter name="filter_intermediate" string="进阶" domain="[('difficulty', '=', 'intermediate')]"/>
<filter name="filter_advanced" string="高级" domain="[('difficulty', '=', 'advanced')]"/>
<separator/>
<group expand="0" string="按目录分组">
<filter name="groupby_category" string="学习目录" context="{'group_by': 'category_id'}"/>
</group>
<filter name="archived" string="已归档" domain="[('active', '=', False)]"/>
</search>
</field>
</record>
<!-- Action -->
<record id="action_yuthon_school_video" model="ir.actions.act_window">
<field name="name">教学视频</field>
<field name="res_model">yuthon.school.video</field>
<field name="view_mode">list,form</field>
<field name="search_view_id" ref="view_yuthon_school_video_search"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
创建第一个教学视频
</p>
<p>
上传 Odoo 18 教学视频,按学习目录分类组织
</p>
</field>
</record>
</odoo>

View File

@ -1 +0,0 @@
from . import progress_wizard

View File

@ -1,69 +0,0 @@
from odoo import fields, models
class ProgressWizard(models.TransientModel):
"""批量更新学习进度向导TransientModel 演示)
TransientModel 的特点
1. 数据存储在临时表中定期自动清理
2. 用于弹出式向导交互不保留持久数据
3. 权限模型与普通 Model 不同通常不需要 unlink 权限
"""
_name = 'yuthon.school.progress.wizard'
_description = '批量更新学习进度'
class_id = fields.Many2one(
'yuthon.school.class',
string='班级',
required=True,
)
chapter_id = fields.Many2one(
'yuthon.school.chapter',
string='章节',
required=True,
)
textbook_id = fields.Many2one(
'yuthon.school.textbook',
string='所属教材',
related='chapter_id.textbook_id',
readonly=True,
)
progress_value = fields.Float(
string='设置进度(%)',
default=100.0,
help='将该班级所有学生此章节的进度设置为该值0-100',
)
completed = fields.Boolean(
string='标记为已完成',
default=True,
)
def action_apply(self):
"""批量更新该班级所有学生的指定章节进度"""
self.ensure_one()
Progress = self.env['yuthon.school.progress']
students = self.class_id.student_ids
now = fields.Datetime.now()
for student in students:
existing = Progress.search([
('student_id', '=', student.id),
('chapter_id', '=', self.chapter_id.id),
], limit=1)
if existing:
existing.write({
'progress': self.progress_value,
'completed': self.completed,
'last_study_date': now,
})
else:
Progress.create({
'student_id': student.id,
'chapter_id': self.chapter_id.id,
'textbook_id': self.chapter_id.textbook_id.id,
'progress': self.progress_value,
'completed': self.completed,
'last_study_date': now,
})
return {'type': 'ir.actions.act_window_close'}

View File

@ -1 +0,0 @@
from . import models

View File

@ -1,33 +0,0 @@
{
'name': 'Yuthon 出勤管理',
'version': '18.0.1.0.0',
'summary': '打卡出勤:签到/签退,自动计算工时、迟到与状态',
'description': """
教学模块②关系层yuthon_attendance
======================================
依赖 yuthon_employee定义出勤记录模型 yuthon.attendance挂在员工下
一对一 N:1并演示计算字段working_hours / status / late_minutes
与模块组合_inherit yuthon.employee 追加反向 attendance_ids
教学要点
- Many2one 关系employee_id 外键 + ondelete='cascade'
- computed field@api.depends 计算工时迟到分钟出勤状态
- 约束_sql_constraints一人一天一条check_in < check_out
- 分析视图graph / pivot 统计出勤
""",
'author': 'Yuthon',
'website': '',
'category': 'Education',
'depends': ['yuthon_employee', 'web'],
'data': [
'security/ir.model.access.csv',
'views/attendance_views.xml',
],
'demo': [
'data/demo.xml',
],
'license': 'LGPL-3',
'installable': True,
'application': True,
'auto_install': False,
}

View File

@ -1,31 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- 演示数据出勤记录。employee_id 通过 ref 引用 yuthon_employee 的 demo。 -->
<!-- 张三9月2日正常、9月3日迟到 -->
<record id="attendance_zhang_0902" model="yuthon.attendance">
<field name="employee_id" ref="yuthon_employee.employee_zhang"/>
<field name="date">2024-09-02</field>
<field name="check_in" eval="datetime.datetime(2024, 9, 2, 8, 30, 0)"/>
<field name="check_out" eval="datetime.datetime(2024, 9, 2, 17, 30, 0)"/>
</record>
<record id="attendance_zhang_0903" model="yuthon.attendance">
<field name="employee_id" ref="yuthon_employee.employee_zhang"/>
<field name="date">2024-09-03</field>
<field name="check_in" eval="datetime.datetime(2024, 9, 3, 9, 35, 0)"/>
<field name="check_out" eval="datetime.datetime(2024, 9, 3, 17, 30, 0)"/>
</record>
<!-- 李四9月2日缺勤无签到 -->
<record id="attendance_li_0902" model="yuthon.attendance">
<field name="employee_id" ref="yuthon_employee.employee_li"/>
<field name="date">2024-09-02</field>
</record>
<!-- 王五9月2日正常 -->
<record id="attendance_wang_0902" model="yuthon.attendance">
<field name="employee_id" ref="yuthon_employee.employee_wang"/>
<field name="date">2024-09-02</field>
<field name="check_in" eval="datetime.datetime(2024, 9, 2, 8, 50, 0)"/>
<field name="check_out" eval="datetime.datetime(2024, 9, 2, 18, 0, 0)"/>
</record>
</odoo>

View File

@ -1 +0,0 @@
from . import attendance

View File

@ -1,114 +0,0 @@
# -*- coding: utf-8 -*-
from odoo import fields, models, api
from datetime import timedelta
# 教学用:规定上班时间为 09:00。
# 注意Odoo 数据库里的 Datetime 以 UTC 存储,这里直接用字段的“小时”做演示比较;
# 生产环境应先用 fields.Datetime.context_timestamp() 把时间转换到用户时区再判断。
WORK_START_HOUR = 9
class Attendance(models.Model):
"""教职工出勤记录。"""
_name = 'yuthon.attendance'
_description = '出勤记录'
_order = 'date desc, employee_id'
employee_id = fields.Many2one(
'yuthon.employee',
string='教职工',
required=True,
ondelete='cascade',
)
date = fields.Date(
string='日期',
required=True,
default=fields.Date.today,
)
check_in = fields.Datetime(string='签到时间')
check_out = fields.Datetime(string='签退时间')
# ---- 计算字段 ----
working_hours = fields.Float(
string='工时(小时)',
compute='_compute_working_hours',
store=True,
)
status = fields.Selection(
[
('normal', '正常'),
('late', '迟到'),
('absent', '缺勤'),
],
string='状态',
compute='_compute_status',
store=True,
)
late_minutes = fields.Integer(
string='迟到分钟',
compute='_compute_status',
store=True,
)
# ---- 约束 ----
_sql_constraints = [
('uniq_employee_date', 'unique(employee_id, date)',
'同一教职工一天只能有一条出勤记录!'),
]
@api.constrains('check_in', 'check_out')
def _check_time_order(self):
for rec in self:
if rec.check_in and rec.check_out and rec.check_out < rec.check_in:
raise models.ValidationError('签退时间不能早于签到时间。')
@api.depends('check_in', 'check_out')
def _compute_working_hours(self):
for rec in self:
if rec.check_in and rec.check_out and rec.check_out >= rec.check_in:
delta = rec.check_out - rec.check_in
rec.working_hours = round(delta.total_seconds() / 3600.0, 2)
else:
rec.working_hours = 0.0
@api.depends('check_in')
def _compute_status(self):
for rec in self:
if not rec.check_in:
rec.status = 'absent'
rec.late_minutes = 0
continue
check_in_hour = rec.check_in.hour + rec.check_in.minute / 60.0
if check_in_hour > WORK_START_HOUR:
rec.status = 'late'
threshold = rec.check_in.replace(
hour=WORK_START_HOUR, minute=0, second=0, microsecond=0)
rec.late_minutes = max(0, int(
(rec.check_in - threshold).total_seconds() // 60))
else:
rec.status = 'normal'
rec.late_minutes = 0
class Employee(models.Model):
"""通过 _inherit 反向扩展 yuthon.employee把出勤记录挂上去。
这是模块组合的关键示范yuthon_attendance 不修改 yuthon_employee 的源码
而是继承它追加 One2many 反向关联让两个模块保持解耦
"""
_inherit = 'yuthon.employee'
attendance_ids = fields.One2many(
'yuthon.attendance',
'employee_id',
string='出勤记录',
)
attendance_count = fields.Integer(
string='出勤记录数',
compute='_compute_attendance_count',
)
@api.depends('attendance_ids')
def _compute_attendance_count(self):
for rec in self:
rec.attendance_count = len(rec.attendance_ids)

View File

@ -1,2 +0,0 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_yuthon_attendance,yuthon.attendance,yuthon_attendance.model_yuthon_attendance,base.group_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_yuthon_attendance yuthon.attendance yuthon_attendance.model_yuthon_attendance base.group_user 1 1 1 1

View File

@ -1,152 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- ============ 列表视图 ============ -->
<record id="view_yuthon_attendance_list" model="ir.ui.view">
<field name="name">yuthon.attendance.list</field>
<field name="model">yuthon.attendance</field>
<field name="arch" type="xml">
<list string="出勤记录" decoration-danger="status == 'absent'"
decoration-warning="status == 'late'">
<field name="employee_id"/>
<field name="date"/>
<field name="check_in"/>
<field name="check_out"/>
<field name="working_hours"/>
<field name="status"/>
<field name="late_minutes"/>
</list>
</field>
</record>
<!-- ============ 表单视图 ============ -->
<record id="view_yuthon_attendance_form" model="ir.ui.view">
<field name="name">yuthon.attendance.form</field>
<field name="model">yuthon.attendance</field>
<field name="arch" type="xml">
<form string="出勤记录">
<sheet>
<group>
<group>
<field name="employee_id"/>
<field name="date"/>
<field name="check_in"/>
<field name="check_out"/>
</group>
<group>
<field name="working_hours"/>
<field name="status"/>
<field name="late_minutes"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<!-- ============ 搜索视图 ============ -->
<record id="view_yuthon_attendance_search" model="ir.ui.view">
<field name="name">yuthon.attendance搜索</field>
<field name="model">yuthon.attendance</field>
<field name="arch" type="xml">
<search string="搜索出勤">
<field name="employee_id"/>
<field name="date"/>
<separator/>
<filter name="filter_late" string="迟到"
domain="[('status', '=', 'late')]"/>
<filter name="filter_absent" string="缺勤"
domain="[('status', '=', 'absent')]"/>
<separator/>
<group expand="0" string="分组">
<filter name="groupby_employee" string="按教职工"
context="{'group_by': 'employee_id'}"/>
<filter name="groupby_status" string="按状态"
context="{'group_by': 'status'}"/>
</group>
</search>
</field>
</record>
<!-- ============ 图形(迟到分钟汇总) ============ -->
<record id="view_yuthon_attendance_graph" model="ir.ui.view">
<field name="name">yuthon.attendance.graph</field>
<field name="model">yuthon.attendance</field>
<field name="arch" type="xml">
<graph string="各教职工迟到分钟数" type="bar">
<field name="employee_id" type="row"/>
<field name="late_minutes" type="measure"/>
</graph>
</field>
</record>
<!-- ============ 透视表 ============ -->
<record id="view_yuthon_attendance_pivot" model="ir.ui.view">
<field name="name">yuthon.attendance.pivot</field>
<field name="model">yuthon.attendance</field>
<field name="arch" type="xml">
<pivot string="出勤透视">
<field name="employee_id" type="row"/>
<field name="status" type="col"/>
<field name="late_minutes" type="measure"/>
</pivot>
</field>
</record>
<!-- ============ 动作 ============ -->
<record id="action_yuthon_attendance" model="ir.actions.act_window">
<field name="name">出勤记录</field>
<field name="res_model">yuthon.attendance</field>
<field name="view_mode">list,form,graph,pivot</field>
<field name="search_view_id" ref="view_yuthon_attendance_search"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
登记第一条出勤记录
</p>
</field>
</record>
<!-- ============ 子菜单(挂载到 yuthon_employee 的根菜单) ============ -->
<menuitem id="menu_yuthon_attendance" name="出勤"
parent="yuthon_employee.menu_yuthon_hr_root"
action="action_yuthon_attendance" sequence="20"/>
<!-- ============ 注入「教职工」视图(模块组合演示) ============ -->
<!-- 列表:追加出勤记录数列 -->
<record id="view_yuthon_employee_list_attendance" model="ir.ui.view">
<field name="name">yuthon.employee.list.inherit.attendance</field>
<field name="model">yuthon.employee</field>
<field name="inherit_id" ref="yuthon_employee.view_yuthon_employee_list"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='base_salary']" position="after">
<field name="attendance_count"/>
</xpath>
</field>
</record>
<!-- 表单:注入出勤记录页 -->
<record id="view_yuthon_employee_form_attendance" model="ir.ui.view">
<field name="name">yuthon.employee.form.inherit.attendance</field>
<field name="model">yuthon.employee</field>
<field name="inherit_id" ref="yuthon_employee.view_yuthon_employee_form"/>
<field name="arch" type="xml">
<xpath expr="//sheet" position="inside">
<notebook>
<page string="出勤记录">
<field name="attendance_ids">
<list>
<field name="date"/>
<field name="check_in"/>
<field name="check_out"/>
<field name="working_hours"/>
<field name="status"/>
<field name="late_minutes"/>
</list>
</field>
</page>
</notebook>
</xpath>
</field>
</record>
</odoo>

View File

@ -1 +0,0 @@
from . import models

View File

@ -1,33 +0,0 @@
{
'name': 'Yuthon 员工管理',
'version': '18.0.1.0.0',
'summary': '大学人事基础:教职工档案(工号/部门/岗位/基本工资)',
'description': """
教学模块①基础层yuthon_employee
======================================
定义教职工员工主数据模型 yuthon.employee是整个出勤-薪资体系的
数据根root后续 yuthon_attendanceyuthon_salary 都依赖本模块
教学要点
- 模型定义_name / _description
- 字段类型Char / Selection / Float / Date / Boolean
- 唯一约束_sql_constraints工号不可重复
- 模块组合被其它模块通过 _inherit 追加反向关联attendance_ids / payslip_ids
- ACLir.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,
}

View File

@ -1,39 +0,0 @@
<?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>

View File

@ -1 +0,0 @@
from . import employee

View File

@ -1,74 +0,0 @@
# -*- 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('基本工资不能为负数。')

View File

@ -1,2 +0,0 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_yuthon_employee,yuthon.employee,yuthon_employee.model_yuthon_employee,base.group_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_yuthon_employee yuthon.employee yuthon_employee.model_yuthon_employee base.group_user 1 1 1 1

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

View File

@ -1,88 +0,0 @@
<?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>

View File

@ -1,23 +0,0 @@
# -*- coding: utf-8 -*-
{
'name': '校园登录主题',
'summary': '把 Odoo 登录页美化成教育网站风格',
'description': '''
纯前端主题模块只改登录页外观不新增任何业务数据
- 整页渐变背景教育紫
- 玻璃质感登录卡片 + 品牌标题
- 登录按钮与主色统一
''',
'category': 'Education',
'version': '1.0',
'depends': ['web'],
'data': [
'views/login_templates.xml',
],
'assets': {
'web.assets_frontend': [
'yuthon_login_theme/static/src/css/login.css',
],
},
'installable': True,
}

View File

@ -1,112 +0,0 @@
/* ===========================================================
校园登录主题 · 教育网站风格
仅作用于登录页body.bg-100 / .o_database_list / .oe_login_form / .edu-*
注意本实例装了第三方主题 udoo_om_ux其登录卡片样式作用在
body.light-dm 作用域下优先级较高此处用同等/更高 specificity
的选择器 + !important 覆盖确保白底与层级生效
=========================================================== */
/* 整页背景图:教育背景图铺满 */
html body.bg-100 {
background-image: url('/yuthon_login_theme/static/src/img/login_bg.jpg') !important;
background-repeat: no-repeat !important;
background-position: center center !important;
background-attachment: fixed !important;
background-size: cover !important;
background-color: transparent !important;
min-height: 100vh;
}
/* 全屏背景层:沉到最底层,避免盖住登录卡片 */
.edu-login-bg {
position: fixed;
inset: 0;
width: 100vw;
height: 100vh;
z-index: -1;
pointer-events: none;
background: url('/yuthon_login_theme/static/src/img/login_bg.jpg') no-repeat center center;
background-size: cover;
}
/* 登录卡片纯白实底 + 圆角 + 阴影覆盖第三方主题
同时覆盖数据库选择页(.o_database_list)登录页(.oe_login_form)两种卡片 */
html body.light-dm main:has(.o_database_list) .o_database_list,
html body.light-dm .oe_login_form,
.edu-login-card {
position: relative !important;
z-index: 10 !important;
max-width: 420px !important;
margin: 0 auto;
border-radius: 22px !important;
border: 1px solid rgba(226, 232, 240, 0.8) !important;
box-shadow: 0 24px 70px rgba(79, 70, 229, 0.22) !important;
background-color: #ffffff !important;
overflow: hidden;
}
/* 数据库选择卡片内部 .card-body 也强制白底,防止透出背景图 */
html body.light-dm main:has(.o_database_list) .o_database_list > .card-body {
background-color: #ffffff !important;
}
/* 品牌标题区(我们注入的内容,不受主题作用域影响) */
.edu-login-head {
text-align: center;
margin-bottom: 20px;
}
.edu-login-brand {
font-size: 13px;
letter-spacing: 3px;
color: #8b5cf6;
font-weight: 600;
}
.edu-login-title {
font-size: 26px;
font-weight: 700;
color: #1e1b4b;
margin: 8px 0 4px;
}
.edu-login-sub {
font-size: 13px;
color: #64748b;
margin: 0;
}
/* 输入框聚焦高亮(提高优先级覆盖主题作用域) */
html body.light-dm .oe_login_form .form-control:focus,
html body.light-dm main:has(.o_database_list) .o_database_list .form-control:focus {
border-color: #8b5cf6 !important;
box-shadow: 0 0 0 0.2rem rgba(139, 92, 246, 0.20) !important;
}
/* 登录按钮:主色 + 通栏 */
html body.light-dm .oe_login_form .btn-primary,
html body.light-dm main:has(.o_database_list) .o_database_list .btn-primary {
background-color: #4f46e5 !important;
border-color: #4f46e5 !important;
width: 100%;
padding: 10px 0;
font-size: 15px;
}
html body.light-dm .oe_login_form .btn-primary:hover,
html body.light-dm main:has(.o_database_list) .o_database_list .btn-primary:hover {
background-color: #4338ca !important;
border-color: #4338ca !important;
}
/* 卡片内分隔线柔和一点 */
html body.light-dm .oe_login_form .border-top,
html body.light-dm .oe_login_form .border-bottom,
html body.light-dm main:has(.o_database_list) .o_database_list .border-top,
html body.light-dm main:has(.o_database_list) .o_database_list .border-bottom {
border-color: rgba(99, 102, 241, 0.12) !important;
}
/* 移动端:卡片占满、留边距 */
@media (max-width: 480px) {
html body.light-dm main:has(.o_database_list) .o_database_list,
html body.light-dm .oe_login_form {
max-width: 92% !important;
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 215 KiB

View File

@ -1,19 +0,0 @@
<odoo>
<template id="yuthon_login_branding" name="校园登录品牌标题" inherit_id="web.login">
<!-- 在表单最顶部插入品牌标题 -->
<xpath expr="//form[@role='form']/input[@name='csrf_token']" position="before">
<div class="edu-login-head">
<div class="edu-login-brand">校园管理系统</div>
<h1 class="edu-login-title">欢迎登录</h1>
<p class="edu-login-sub">教职工 · 课程 · 成绩 · 出勤,一站式管理平台</p>
</div>
</xpath>
</template>
<template id="yuthon_login_background" name="校园登录背景层" inherit_id="web.login">
<!-- 在登录表单之前插入全屏背景层,绕过 body 背景被第三方主题覆盖的问题 -->
<xpath expr="//form[@role='form']" position="before">
<div class="edu-login-bg"/>
</xpath>
</template>
</odoo>

View File

@ -1,2 +0,0 @@
from . import models
from . import wizards

View File

@ -1,35 +0,0 @@
{
'name': 'Yuthon 薪资管理',
'version': '18.0.1.0.0',
'summary': '工资条:聚合出勤迟到/缺勤,自动计算扣款与实发',
'description': """
教学模块③汇总层yuthon_wage
======================================
依赖 yuthon_employee + yuthon_attendance定义工资条模型 yuthon.payslip
在月底把员工基本工资 + 当月出勤聚合结果join 在一起算出实发工资
并提供生成月度工资条向导做批量处理
教学要点
- 跨模型聚合env['yuthon.attendance'].search_count(...) 统计迟到/缺勤
- computed field 多级联动统计 -> 扣款 -> 实发
- related 字段basic_wage 取自员工基本工资
- TransientModel 向导批量生成工资条讲解 wizard 与批量处理
- 状态机draft -> done
""",
'author': 'Yuthon',
'website': '',
'category': 'Education',
'depends': ['yuthon_employee', 'yuthon_attendance', 'web'],
'data': [
'security/ir.model.access.csv',
'views/payslip_views.xml',
'views/payslip_generate_views.xml',
],
'demo': [
'data/demo.xml',
],
'license': 'LGPL-3',
'installable': True,
'application': True,
'auto_install': False,
}

View File

@ -1,26 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- 工资条聚合“2026-07”整月的出勤记录来自 yuthon_attendance 的 demo -->
<record id="payslip_zhang_2026_07" model="yuthon.payslip">
<field name="employee_id" ref="yuthon_employee.employee_zhang"/>
<field name="period_start">2026-07-01</field>
<field name="period_end">2026-07-31</field>
<field name="allowance">500.0</field>
</record>
<record id="payslip_li_2026_07" model="yuthon.payslip">
<field name="employee_id" ref="yuthon_employee.employee_li"/>
<field name="period_start">2026-07-01</field>
<field name="period_end">2026-07-31</field>
<field name="allowance">300.0</field>
</record>
<record id="payslip_wang_2026_07" model="yuthon.payslip">
<field name="employee_id" ref="yuthon_employee.employee_wang"/>
<field name="period_start">2026-07-01</field>
<field name="period_end">2026-07-31</field>
<field name="allowance">800.0</field>
</record>
</odoo>

View File

@ -1 +0,0 @@
from . import payslip

View File

@ -1,106 +0,0 @@
# -*- coding: utf-8 -*-
from odoo import fields, models, api
# 教学用扣款规则(可在 production 改为可配置的 salary.rule 模型)
LATE_PENALTY = 50.0 # 迟到一次扣 50
DAYS_PER_MONTH = 21.75 # 月计薪天数,用于把“缺勤天数”折算成金额
class Payslip(models.Model):
"""工资条:聚合员工当月出勤,自动计算扣款与实发。"""
_name = 'yuthon.payslip'
_description = '工资条'
_order = 'period_end desc, employee_id'
employee_id = fields.Many2one(
'yuthon.employee',
string='教职工',
required=True,
ondelete='cascade',
)
period_start = fields.Date(string='起始日期', required=True)
period_end = fields.Date(string='结束日期', required=True)
# ---- 金额 ----
basic_wage = fields.Float(
string='基本工资',
related='employee_id.base_salary',
store=True,
readonly=True,
)
allowance = fields.Float(string='津贴/补贴', default=0.0)
# ---- 出勤聚合(自动统计)----
late_count = fields.Integer(
string='迟到次数', compute='_compute_attendance_stats', store=True)
absent_days = fields.Integer(
string='缺勤天数', compute='_compute_attendance_stats', store=True)
# ---- 扣款(自动计算)----
late_deduction = fields.Float(
string='迟到扣款', compute='_compute_deductions', store=True)
absent_deduction = fields.Float(
string='缺勤扣款', compute='_compute_deductions', store=True)
attendance_deduction = fields.Float(
string='出勤扣款合计', compute='_compute_deductions', store=True)
# ---- 实发 ----
net_salary = fields.Float(
string='实发工资', compute='_compute_net', store=True)
state = fields.Selection(
[('draft', '草稿'), ('done', '已确认')],
string='状态',
default='draft',
)
# ---- 计算逻辑 ----
@api.depends('employee_id', 'period_start', 'period_end')
def _compute_attendance_stats(self):
Attendance = self.env['yuthon.attendance']
for rec in self:
domain = [('employee_id', '=', rec.employee_id.id)]
if rec.period_start:
domain.append(('date', '>=', rec.period_start))
if rec.period_end:
domain.append(('date', '<=', rec.period_end))
# sudo() 确保在后台重算时不受当前用户 ACL 影响
rec.late_count = Attendance.sudo().search_count(
domain + [('status', '=', 'late')])
rec.absent_days = Attendance.sudo().search_count(
domain + [('status', '=', 'absent')])
@api.depends('late_count', 'absent_days', 'basic_wage')
def _compute_deductions(self):
for rec in self:
rec.late_deduction = rec.late_count * LATE_PENALTY
daily = (rec.basic_wage / DAYS_PER_MONTH) if rec.basic_wage else 0.0
rec.absent_deduction = rec.absent_days * daily
rec.attendance_deduction = rec.late_deduction + rec.absent_deduction
@api.depends('basic_wage', 'allowance', 'attendance_deduction')
def _compute_net(self):
for rec in self:
rec.net_salary = rec.basic_wage + rec.allowance - rec.attendance_deduction
# ---- 业务动作 ----
def action_confirm(self):
self.write({'state': 'done'})
def action_draft(self):
self.write({'state': 'draft'})
class Employee(models.Model):
"""_inherit 反向扩展:把工资条挂到员工上。"""
_inherit = 'yuthon.employee'
payslip_ids = fields.One2many(
'yuthon.payslip', 'employee_id', string='工资条')
payslip_count = fields.Integer(
string='工资条数', compute='_compute_payslip_count')
@api.depends('payslip_ids')
def _compute_payslip_count(self):
for rec in self:
rec.payslip_count = len(rec.payslip_ids)

View File

@ -1,3 +0,0 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_yuthon_payslip,yuthon.payslip,yuthon_wage.model_yuthon_payslip,base.group_user,1,1,1,1
access_yuthon_payslip_generate,yuthon.payslip.yuthon_wage.wizard,yuthon_wage.model_yuthon_payslip_generate_wizard,base.group_user,1,0,1,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_yuthon_payslip yuthon.payslip yuthon_wage.model_yuthon_payslip base.group_user 1 1 1 1
3 access_yuthon_payslip_generate yuthon.payslip.yuthon_wage.wizard yuthon_wage.model_yuthon_payslip_generate_wizard base.group_user 1 0 1 0

View File

@ -1,40 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- ============ 向导表单 ============ -->
<record id="view_yuthon_payslip_generate_form" model="ir.ui.view">
<field name="name">yuthon.payslip.generate.form</field>
<field name="model">yuthon.payslip.generate.wizard</field>
<field name="arch" type="xml">
<form string="生成月度工资条">
<group>
<field name="period_start"/>
<field name="period_end"/>
<field name="allowance"/>
<field name="employee_ids" widget="many2many_tags"/>
</group>
<footer>
<button name="action_generate" type="object"
string="生成" class="btn-primary"/>
<button string="取消" class="btn-secondary"
special="cancel"/>
</footer>
</form>
</field>
</record>
<!-- ============ 向导动作 ============ -->
<record id="action_yuthon_payslip_generate" model="ir.actions.act_window">
<field name="name">生成月度工资条</field>
<field name="res_model">yuthon.payslip.generate.wizard</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
<!-- ============ 子菜单:放在“薪资”下,方便一键批量生成 ============ -->
<menuitem id="menu_yuthon_payslip_generate"
name="生成月度工资条"
parent="yuthon_employee.menu_yuthon_hr_root"
action="action_yuthon_payslip_generate" sequence="31"/>
</odoo>

View File

@ -1,142 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- ============ 列表视图 ============ -->
<record id="view_yuthon_payslip_list" model="ir.ui.view">
<field name="name">yuthon.payslip.list</field>
<field name="model">yuthon.payslip</field>
<field name="arch" type="xml">
<list string="工资条" decoration-info="state == 'draft'">
<field name="employee_id"/>
<field name="period_start"/>
<field name="period_end"/>
<field name="basic_wage"/>
<field name="allowance"/>
<field name="late_count"/>
<field name="absent_days"/>
<field name="attendance_deduction"/>
<field name="net_salary" decoration-bf="1"/>
<field name="state"/>
</list>
</field>
</record>
<!-- ============ 表单视图 ============ -->
<record id="view_yuthon_payslip_form" model="ir.ui.view">
<field name="name">yuthon.payslip.form</field>
<field name="model">yuthon.payslip</field>
<field name="arch" type="xml">
<form string="工资条">
<header>
<button name="action_confirm" type="object"
string="确认" class="btn-primary" invisible="state != 'draft'"/>
<button name="action_draft" type="object"
string="转草稿" invisible="state != 'done'"/>
<field name="state" widget="statusbar"/>
</header>
<sheet>
<group>
<group>
<field name="employee_id"/>
<field name="period_start"/>
<field name="period_end"/>
<field name="state" invisible="1"/>
</group>
<group>
<field name="basic_wage"/>
<field name="allowance"/>
</group>
</group>
<group>
<group string="出勤聚合">
<field name="late_count"/>
<field name="absent_days"/>
</group>
<group string="扣款与实发">
<field name="late_deduction"/>
<field name="absent_deduction"/>
<field name="attendance_deduction"/>
<field name="net_salary" decoration-bf="1"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<!-- ============ 搜索视图 ============ -->
<record id="view_yuthon_payslip_search" model="ir.ui.view">
<field name="name">yuthon.payslip搜索</field>
<field name="model">yuthon.payslip</field>
<field name="arch" type="xml">
<search string="搜索工资条">
<field name="employee_id"/>
<field name="period_start"/>
<field name="period_end"/>
<separator/>
<filter name="filter_draft" string="草稿"
domain="[('state', '=', 'draft')]"/>
<filter name="filter_done" string="已确认"
domain="[('state', '=', 'done')]"/>
<separator/>
<group expand="0" string="分组">
<filter name="groupby_employee" string="按教职工"
context="{'group_by': 'employee_id'}"/>
</group>
</search>
</field>
</record>
<!-- ============ 动作 ============ -->
<record id="action_yuthon_payslip" model="ir.actions.act_window">
<field name="name">工资条</field>
<field name="res_model">yuthon.payslip</field>
<field name="view_mode">list,form</field>
<field name="search_view_id" ref="view_yuthon_payslip_search"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
先用「生成月度工资条」创建第一张工资条
</p>
</field>
</record>
<!-- ============ 子菜单(挂载到员工模块的根菜单) ============ -->
<menuitem id="menu_yuthon_payslip" name="薪资"
parent="yuthon_employee.menu_yuthon_hr_root"
action="action_yuthon_payslip" sequence="30"/>
<!-- ============ 注入「教职工」视图(模块组合演示) ============ -->
<!-- 列表:追加工资条数列 -->
<record id="view_yuthon_employee_list_salary" model="ir.ui.view">
<field name="name">yuthon.employee.list.inherit.salary</field>
<field name="model">yuthon.employee</field>
<field name="inherit_id" ref="yuthon_employee.view_yuthon_employee_list"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='attendance_count']" position="after">
<field name="payslip_count"/>
</xpath>
</field>
</record>
<!-- 表单:注入工资条页 -->
<record id="view_yuthon_employee_form_salary" model="ir.ui.view">
<field name="name">yuthon.employee.form.inherit.salary</field>
<field name="model">yuthon.employee</field>
<field name="inherit_id" ref="yuthon_employee.view_yuthon_employee_form"/>
<field name="arch" type="xml">
<xpath expr="//notebook" position="inside">
<page string="工资条">
<field name="payslip_ids">
<list>
<field name="period_start"/>
<field name="period_end"/>
<field name="net_salary"/>
<field name="state"/>
</list>
</field>
</page>
</xpath>
</field>
</record>
</odoo>

View File

@ -1 +0,0 @@
from . import payslip_generate

View File

@ -1,41 +0,0 @@
# -*- coding: utf-8 -*-
from odoo import fields, models, api
class PayslipGenerateWizard(models.TransientModel):
"""生成月度工资条向导:批量给选定教职工创建 yuthon.payslip。
教学要点TransientModel向导模型数据临时自动清理+ 批量创建
"""
_name = 'yuthon.payslip.generate.wizard'
_description = '生成月度工资条向导'
period_start = fields.Date(string='起始日期', required=True)
period_end = fields.Date(string='结束日期', required=True)
allowance = fields.Float(string='津贴/补贴', default=0.0)
employee_ids = fields.Many2many(
'yuthon.employee',
string='教职工',
)
def action_generate(self):
"""为每位选定教职工创建一条工资条(带默认值,算出迟到/缺勤/实发)。"""
self.ensure_one()
Payslip = self.env['yuthon.payslip']
employees = self.employee_ids or self.env['yuthon.employee'].search([])
created = Payslip
for emp in employees:
created |= Payslip.create({
'employee_id': emp.id,
'period_start': self.period_start,
'period_end': self.period_end,
'allowance': self.allowance,
})
# 返回刚生成的工资条列表视图
return {
'type': 'ir.actions.act_window',
'name': '工资条',
'res_model': 'yuthon.payslip',
'view_mode': 'list,form',
'domain': [('id', 'in', created.ids)],
}