diff --git a/college/__manifest__.py b/college/__manifest__.py
index 9081f3c..c5ac616 100644
--- a/college/__manifest__.py
+++ b/college/__manifest__.py
@@ -41,6 +41,7 @@
'views/import_wizard_views.xml',
'views/homework_views.xml',
'views/training_views.xml',
+ 'views/course_import_wizard_views.xml',
'data/about_pages.xml',
'views/about_page_views.xml',
'views/menus.xml',
diff --git a/college/models/__init__.py b/college/models/__init__.py
index 2a892f2..6d33cbb 100644
--- a/college/models/__init__.py
+++ b/college/models/__init__.py
@@ -9,3 +9,4 @@ from . import assignment_batch_wizard
from . import import_wizard
from . import about_page
from . import training
+from . import course_import_wizard
diff --git a/college/models/course_import_wizard.py b/college/models/course_import_wizard.py
new file mode 100644
index 0000000..268716c
--- /dev/null
+++ b/college/models/course_import_wizard.py
@@ -0,0 +1,228 @@
+"""课程批量导入向导 —— CSV 模板下载 + 批量创建课程(导入大纲即可)。
+
+模板列:课程名称、课程编号、所属学院、学期、课程类型、课时、课程大纲。
+导入时直接创建 yuthon.school.course 记录。
+"""
+
+import base64
+import csv
+import io
+import logging
+
+from odoo import _, api, fields, models
+from odoo.exceptions import UserError
+
+_logger = logging.getLogger(__name__)
+
+COURSE_TYPE_MAP = {
+ '理论': 'theory',
+ '实践': 'practice',
+ 'theory': 'theory',
+ 'practice': 'practice',
+}
+
+COLLEGE_NAME_MAP = {
+ '计算机学院': 'computer',
+ '软件学院': 'software',
+ '商学院': 'business',
+ '外国语学院': 'foreign',
+ '机械工程学院': 'mechanic',
+ '电子信息学院': 'electric',
+ '数学学院': 'math',
+ '其他': 'other',
+ 'computer': 'computer',
+ 'software': 'software',
+ 'business': 'business',
+ 'foreign': 'foreign',
+ 'mechanic': 'mechanic',
+ 'electric': 'electric',
+ 'math': 'math',
+ 'other': 'other',
+}
+
+
+class CourseImportWizard(models.TransientModel):
+ """课程批量导入向导。"""
+
+ _name = 'yuthon.school.course.import.wizard'
+ _description = '课程批量导入'
+
+ data_file = fields.Binary(string='上传文件')
+ file_name = fields.Char(string='文件名')
+ note = fields.Html(
+ string='导入说明',
+ readonly=True,
+ default=lambda self: _(
+ '
模板说明:
'
+ ''
+ '- 点击下方「下载模板」按钮下载 CSV 模板;
'
+ '- 用 Excel / WPS 打开模板,按示例填写数据:
'
+ '课程名称、课程编号 为必填;
'
+ '所属学院、学期、课程类型、课时、课程大纲 为选填; '
+ '- 课程类型可选:理论 / 实践
'
+ '- 保存为 CSV 后上传并点击「开始导入」;
'
+ '- 若课程编号已存在则跳过,不会重复创建。
'
+ '
'
+ ),
+ )
+
+ # ─────────────────── 下载模板 ───────────────────
+
+ def action_download_template(self):
+ """生成 CSV 模板并提供下载。"""
+ header = ['课程名称', '课程编号', '所属学院', '学期',
+ '课程类型(理论/实践)', '课时', '课程大纲']
+ sample = ['Python 编程基础', 'PY101', '计算机学院',
+ '2025-2026第1学期', '理论', '48',
+ 'Python基础语法、数据结构、面向对象编程']
+
+ buf = io.StringIO()
+ writer = csv.writer(buf)
+ writer.writerow(header)
+ writer.writerow(sample)
+
+ attachment = self.env['ir.attachment'].sudo().create({
+ 'name': '课程导入模板.csv',
+ 'datas': base64.b64encode(
+ buf.getvalue().encode('utf-8-sig')
+ ),
+ 'res_model': self._name,
+ 'res_id': self.id,
+ 'type': 'binary',
+ 'mimetype': 'text/csv',
+ })
+ return {
+ 'type': 'ir.actions.act_url',
+ 'url': f'/web/content/{attachment.id}?download=true',
+ 'target': 'self',
+ }
+
+ # ─────────────────── 导入 ───────────────────
+
+ def action_import(self):
+ """解析上传的 CSV 并批量创建课程。"""
+ self.ensure_one()
+ if not self.data_file:
+ raise UserError(_('请先上传文件。'))
+
+ content = base64.b64decode(self.data_file)
+ for encoding in ('utf-8-sig', 'utf-8', 'gbk', 'gb2312'):
+ try:
+ text = content.decode(encoding)
+ break
+ except UnicodeDecodeError:
+ continue
+ else:
+ raise UserError(_('无法识别文件编码,请使用 UTF-8 或 GBK 编码的 CSV。'))
+
+ reader = csv.DictReader(io.StringIO(text))
+ if not reader.fieldnames:
+ raise UserError(_('CSV 文件为空或格式不正确。'))
+
+ col_map = self._normalize_columns(reader.fieldnames)
+
+ created = 0
+ skipped = 0
+ errors = []
+
+ for row_idx, raw_row in enumerate(reader, start=2):
+ row = {col_map.get(k, k): v.strip() if v else '' for k, v in raw_row.items()}
+
+ name = row.get('name', '').strip()
+ code = row.get('code', '').strip()
+ if not name:
+ skipped += 1
+ errors.append(_('第 %d 行:课程名称为必填,跳过。') % row_idx)
+ continue
+
+ # 编号去重
+ if code:
+ existing = self.env['yuthon.school.course'].sudo().search(
+ [('code', '=', code)], limit=1)
+ if existing:
+ skipped += 1
+ continue
+
+ try:
+ vals = {
+ 'name': name,
+ 'code': code,
+ 'state': 'draft',
+ }
+
+ college_raw = row.get('college', '')
+ if college_raw:
+ vals['college'] = COLLEGE_NAME_MAP.get(college_raw, 'other')
+
+ semester = row.get('semester', '')
+ if semester:
+ vals['semester'] = semester
+
+ course_type_raw = row.get('course_type', '')
+ if course_type_raw:
+ vals['course_type'] = COURSE_TYPE_MAP.get(course_type_raw, 'theory')
+
+ hours_raw = row.get('hours', '')
+ if hours_raw:
+ try:
+ vals['hours'] = float(hours_raw)
+ except ValueError:
+ pass
+
+ syllabus = row.get('syllabus', '')
+ if syllabus:
+ vals['syllabus'] = syllabus
+
+ self.env['yuthon.school.course'].sudo().create(vals)
+ created += 1
+
+ except Exception as exc:
+ skipped += 1
+ errors.append(
+ _('第 %d 行「%s」导入失败:%s') % (row_idx, name, str(exc)))
+
+ summary = _(
+ '导入完成!\n'
+ '新建课程:%d\n'
+ '跳过:%d'
+ ) % (created, skipped)
+
+ if errors:
+ summary += '\n' + _('注意事项:\n') + '\n'.join(errors[:10])
+ if len(errors) > 10:
+ summary += _('\n... 还有 %d 条,详见日志。') % (len(errors) - 10)
+
+ return {
+ 'type': 'ir.actions.client',
+ 'tag': 'display_notification',
+ 'params': {
+ 'title': _('导入结果'),
+ 'message': summary,
+ 'type': 'info' if not errors else 'warning',
+ 'sticky': True,
+ 'next': {'type': 'ir.actions.act_window_close'},
+ },
+ }
+
+ def _normalize_columns(self, fieldnames):
+ """列名 → 内部 key 映射。"""
+ mapping = {}
+ for f in fieldnames:
+ f_lower = f.strip().lower()
+ if f_lower in ('课程名称', 'name', '课程名'):
+ mapping[f] = 'name'
+ elif f_lower in ('课程编号', 'code', '课程号', '编号'):
+ mapping[f] = 'code'
+ elif f_lower in ('所属学院', 'college', '学院'):
+ mapping[f] = 'college'
+ elif f_lower in ('学期', 'semester'):
+ mapping[f] = 'semester'
+ elif f_lower in ('课程类型', '课程类型(理论/实践)', 'course_type', '类型'):
+ mapping[f] = 'course_type'
+ elif f_lower in ('课时', 'hours'):
+ mapping[f] = 'hours'
+ elif f_lower in ('课程大纲', 'syllabus', '大纲'):
+ mapping[f] = 'syllabus'
+ else:
+ mapping[f] = f # 保留原名
+ return mapping
diff --git a/college/security/ir.model.access.csv b/college/security/ir.model.access.csv
index f435d03..862bffa 100644
--- a/college/security/ir.model.access.csv
+++ b/college/security/ir.model.access.csv
@@ -33,6 +33,8 @@ access_yuthon_school_course_chapter_attachment_student,yuthon.school.course.chap
access_yuthon_school_course_chapter_exercise_admin,yuthon.school.course.chapter.exercise.admin,model_yuthon_school_course_chapter_exercise,base.group_system,1,1,1,1
access_yuthon_school_course_chapter_exercise_teacher,yuthon.school.course.chapter.exercise.teacher,model_yuthon_school_course_chapter_exercise,college.group_teacher,1,1,1,1
access_yuthon_school_course_chapter_exercise_student,yuthon.school.course.chapter.exercise.student,model_yuthon_school_course_chapter_exercise,college.group_student,1,0,0,0
+access_yuthon_school_course_import_wizard_admin,yuthon.school.course.import.wizard.admin,model_yuthon_school_course_import_wizard,base.group_system,1,1,1,1
+access_yuthon_school_course_import_wizard_teacher,yuthon.school.course.import.wizard.teacher,model_yuthon_school_course_import_wizard,college.group_teacher,1,1,1,1
access_yuthon_school_course_chapter_exercise_answer_admin,yuthon.school.course.chapter.exercise.answer.admin,model_yuthon_school_course_chapter_exercise_answer,base.group_system,1,1,1,1
access_yuthon_school_course_chapter_exercise_answer_teacher,yuthon.school.course.chapter.exercise.answer.teacher,model_yuthon_school_course_chapter_exercise_answer,college.group_teacher,1,0,0,0
access_yuthon_school_course_chapter_exercise_answer_student,yuthon.school.course.chapter.exercise.answer.student,model_yuthon_school_course_chapter_exercise_answer,college.group_student,1,1,1,0
diff --git a/college/views/course_import_wizard_views.xml b/college/views/course_import_wizard_views.xml
new file mode 100644
index 0000000..bb1f343
--- /dev/null
+++ b/college/views/course_import_wizard_views.xml
@@ -0,0 +1,25 @@
+
+
+
+ yuthon.school.course.import.wizard.form
+ yuthon.school.course.import.wizard
+
+
+
+
+
diff --git a/college/views/menus.xml b/college/views/menus.xml
index 5de7054..98d3a9e 100644
--- a/college/views/menus.xml
+++ b/college/views/menus.xml
@@ -1,12 +1,36 @@
-
+
+
+
+
+
+
+ 我的课程(按角色路由)
+
+ code
+
+
+
+
+
+
-
+
+
+ 课程导入
+ yuthon.school.course.import.wizard
+ form
+ new
+
+
+
+
+