模块迁移
This commit is contained in:
parent
36b6cb1e0e
commit
26c311d913
3
yuthon_school/__init__.py
Normal file
3
yuthon_school/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
from . import models
|
||||
from . import controllers
|
||||
from . import wizards
|
||||
35
yuthon_school/__manifest__.py
Normal file
35
yuthon_school/__manifest__.py
Normal file
@ -0,0 +1,35 @@
|
||||
{
|
||||
'name': 'Yuthon School',
|
||||
'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',
|
||||
'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/menus.xml',
|
||||
],
|
||||
'demo': [
|
||||
'data/demo.xml',
|
||||
],
|
||||
'license': 'LGPL-3',
|
||||
'installable': True,
|
||||
'application': True,
|
||||
'auto_install': False,
|
||||
}
|
||||
1
yuthon_school/controllers/__init__.py
Normal file
1
yuthon_school/controllers/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from . import main
|
||||
77
yuthon_school/controllers/main.py
Normal file
77
yuthon_school/controllers/main.py
Normal file
@ -0,0 +1,77 @@
|
||||
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''
|
||||
307
yuthon_school/data/demo.xml
Normal file
307
yuthon_school/data/demo.xml
Normal file
@ -0,0 +1,307 @@
|
||||
<?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>
|
||||
|
||||
</odoo>
|
||||
6
yuthon_school/models/__init__.py
Normal file
6
yuthon_school/models/__init__.py
Normal file
@ -0,0 +1,6 @@
|
||||
from . import video_playback
|
||||
from . import video_category
|
||||
from . import textbook
|
||||
from . import organization
|
||||
from . import progress
|
||||
from . import elective
|
||||
41
yuthon_school/models/elective.py
Normal file
41
yuthon_school/models/elective.py
Normal file
@ -0,0 +1,41 @@
|
||||
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)
|
||||
167
yuthon_school/models/organization.py
Normal file
167
yuthon_school/models/organization.py
Normal file
@ -0,0 +1,167 @@
|
||||
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,
|
||||
)
|
||||
84
yuthon_school/models/progress.py
Normal file
84
yuthon_school/models/progress.py
Normal file
@ -0,0 +1,84 @@
|
||||
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 之间"
|
||||
)
|
||||
147
yuthon_school/models/textbook.py
Normal file
147
yuthon_school/models/textbook.py
Normal file
@ -0,0 +1,147 @@
|
||||
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
|
||||
74
yuthon_school/models/video_category.py
Normal file
74
yuthon_school/models/video_category.py
Normal file
@ -0,0 +1,74 @@
|
||||
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)
|
||||
147
yuthon_school/models/video_playback.py
Normal file
147
yuthon_school/models/video_playback.py
Normal file
@ -0,0 +1,147 @@
|
||||
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}'
|
||||
)
|
||||
|
||||
12
yuthon_school/security/ir.model.access.csv
Normal file
12
yuthon_school/security/ir.model.access.csv
Normal file
@ -0,0 +1,12 @@
|
||||
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
|
||||
|
90
yuthon_school/views/elective_views.xml
Normal file
90
yuthon_school/views/elective_views.xml
Normal file
@ -0,0 +1,90 @@
|
||||
<?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>
|
||||
76
yuthon_school/views/menus.xml
Normal file
76
yuthon_school/views/menus.xml
Normal file
@ -0,0 +1,76 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<!-- Root menu -->
|
||||
<menuitem id="menu_yuthon_school_root"
|
||||
name="Yuthon School"
|
||||
web_icon="yuthon_school,static/description/icon.png"
|
||||
sequence="10"/>
|
||||
|
||||
<!-- ============ 体系教材 ============ -->
|
||||
<menuitem id="menu_yuthon_school_curriculum"
|
||||
name="体系教材"
|
||||
parent="menu_yuthon_school_root"
|
||||
sequence="10"/>
|
||||
|
||||
<menuitem id="menu_yuthon_school_category"
|
||||
name="学习分类"
|
||||
parent="menu_yuthon_school_curriculum"
|
||||
action="action_yuthon_school_category"
|
||||
sequence="10"/>
|
||||
|
||||
<menuitem id="menu_yuthon_school_textbook"
|
||||
name="教材管理"
|
||||
parent="menu_yuthon_school_curriculum"
|
||||
action="action_yuthon_school_textbook"
|
||||
sequence="20"/>
|
||||
|
||||
<menuitem id="menu_yuthon_school_video"
|
||||
name="视频管理"
|
||||
parent="menu_yuthon_school_curriculum"
|
||||
action="action_yuthon_school_video"
|
||||
sequence="30"/>
|
||||
|
||||
<menuitem id="menu_yuthon_school_progress"
|
||||
name="学习进度"
|
||||
parent="menu_yuthon_school_curriculum"
|
||||
action="action_yuthon_school_progress"
|
||||
sequence="40"/>
|
||||
|
||||
<!-- ============ 组织架构 ============ -->
|
||||
<menuitem id="menu_yuthon_school_organization"
|
||||
name="组织架构"
|
||||
parent="menu_yuthon_school_root"
|
||||
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>
|
||||
336
yuthon_school/views/organization_views.xml
Normal file
336
yuthon_school/views/organization_views.xml
Normal file
@ -0,0 +1,336 @@
|
||||
<?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>
|
||||
143
yuthon_school/views/progress_views.xml
Normal file
143
yuthon_school/views/progress_views.xml
Normal file
@ -0,0 +1,143 @@
|
||||
<?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', '<', 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>
|
||||
41
yuthon_school/views/progress_wizard_views.xml
Normal file
41
yuthon_school/views/progress_wizard_views.xml
Normal file
@ -0,0 +1,41 @@
|
||||
<?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>
|
||||
174
yuthon_school/views/textbook_views.xml
Normal file
174
yuthon_school/views/textbook_views.xml
Normal file
@ -0,0 +1,174 @@
|
||||
<?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>
|
||||
108
yuthon_school/views/video_category_views.xml
Normal file
108
yuthon_school/views/video_category_views.xml
Normal file
@ -0,0 +1,108 @@
|
||||
<?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>
|
||||
114
yuthon_school/views/video_playback_views.xml
Normal file
114
yuthon_school/views/video_playback_views.xml
Normal file
@ -0,0 +1,114 @@
|
||||
<?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>
|
||||
1
yuthon_school/wizards/__init__.py
Normal file
1
yuthon_school/wizards/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from . import progress_wizard
|
||||
69
yuthon_school/wizards/progress_wizard.py
Normal file
69
yuthon_school/wizards/progress_wizard.py
Normal file
@ -0,0 +1,69 @@
|
||||
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'}
|
||||
Loading…
Reference in New Issue
Block a user