Compare commits
No commits in common. "master" and "dev_haoran" have entirely different histories.
master
...
dev_haoran
@ -1,3 +0,0 @@
|
|||||||
from . import models
|
|
||||||
from . import controllers
|
|
||||||
from . import wizards
|
|
||||||
@ -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,
|
|
||||||
}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
from . import main
|
|
||||||
@ -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''
|
|
||||||
@ -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>
|
|
||||||
@ -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
|
|
||||||
@ -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)
|
|
||||||
@ -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,
|
|
||||||
)
|
|
||||||
@ -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 之间"
|
|
||||||
)
|
|
||||||
@ -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
|
|
||||||
@ -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)
|
|
||||||
@ -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}'
|
|
||||||
)
|
|
||||||
|
|
||||||
@ -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'})
|
|
||||||
@ -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'})
|
|
||||||
@ -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)
|
|
||||||
@ -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,
|
|
||||||
)
|
|
||||||
@ -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
|
|
||||||
|
Binary file not shown.
|
Before Width: | Height: | Size: 5.6 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 2.8 KiB |
@ -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>
|
|
||||||
@ -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>
|
|
||||||
@ -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>
|
|
||||||
@ -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>
|
|
||||||
@ -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>
|
|
||||||
@ -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>
|
|
||||||
@ -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', '<', 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>
|
|
||||||
@ -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>
|
|
||||||
@ -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>
|
|
||||||
@ -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>
|
|
||||||
@ -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>
|
|
||||||
@ -1 +0,0 @@
|
|||||||
from . import progress_wizard
|
|
||||||
@ -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'}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
from . import models
|
|
||||||
@ -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,
|
|
||||||
}
|
|
||||||
@ -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>
|
|
||||||
@ -1 +0,0 @@
|
|||||||
from . import attendance
|
|
||||||
@ -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)
|
|
||||||
@ -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,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>
|
|
||||||
@ -1 +0,0 @@
|
|||||||
from . import models
|
|
||||||
@ -1,33 +0,0 @@
|
|||||||
{
|
|
||||||
'name': 'Yuthon 员工管理',
|
|
||||||
'version': '18.0.1.0.0',
|
|
||||||
'summary': '大学人事基础:教职工档案(工号/部门/岗位/基本工资)',
|
|
||||||
'description': """
|
|
||||||
教学模块①(基础层):yuthon_employee
|
|
||||||
======================================
|
|
||||||
定义教职工(员工)主数据模型 yuthon.employee,是整个「出勤-薪资」体系的
|
|
||||||
数据根(root)。后续 yuthon_attendance、yuthon_salary 都依赖本模块。
|
|
||||||
|
|
||||||
教学要点:
|
|
||||||
- 模型定义:_name / _description
|
|
||||||
- 字段类型:Char / Selection / Float / Date / Boolean
|
|
||||||
- 唯一约束:_sql_constraints(工号不可重复)
|
|
||||||
- 模块组合:被其它模块通过 _inherit 追加反向关联(attendance_ids / payslip_ids)
|
|
||||||
- ACL:ir.model.access.csv 授权
|
|
||||||
""",
|
|
||||||
'author': 'Yuthon',
|
|
||||||
'website': '',
|
|
||||||
'category': 'Education',
|
|
||||||
'depends': ['base', 'web'],
|
|
||||||
'data': [
|
|
||||||
'security/ir.model.access.csv',
|
|
||||||
'views/employee_views.xml',
|
|
||||||
],
|
|
||||||
'demo': [
|
|
||||||
'data/demo.xml',
|
|
||||||
],
|
|
||||||
'license': 'LGPL-3',
|
|
||||||
'installable': True,
|
|
||||||
'application': True,
|
|
||||||
'auto_install': False,
|
|
||||||
}
|
|
||||||
@ -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>
|
|
||||||
@ -1 +0,0 @@
|
|||||||
from . import employee
|
|
||||||
@ -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('基本工资不能为负数。')
|
|
||||||
@ -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
|
|
||||||
|
Binary file not shown.
|
Before Width: | Height: | Size: 7.3 KiB |
@ -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>
|
|
||||||
@ -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,
|
|
||||||
}
|
|
||||||
@ -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 |
@ -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>
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
from . import models
|
|
||||||
from . import wizards
|
|
||||||
@ -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,
|
|
||||||
}
|
|
||||||
@ -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>
|
|
||||||
@ -1 +0,0 @@
|
|||||||
from . import payslip
|
|
||||||
@ -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)
|
|
||||||
@ -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,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>
|
|
||||||
@ -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>
|
|
||||||
@ -1 +0,0 @@
|
|||||||
from . import payslip_generate
|
|
||||||
@ -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)],
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue
Block a user