1
This commit is contained in:
@@ -14,14 +14,18 @@
|
||||
'views/yuthon_catering.xml',
|
||||
'views/yuthon_catering_conf.xml',
|
||||
'views/yuthon_catering_setting_views.xml',
|
||||
'views/yuthon_catering_templates.xml',
|
||||
'wizard/catering_reinstated_wizard_views.xml',
|
||||
'templates/templates.xml',
|
||||
],
|
||||
'assets': {
|
||||
'web.assets_backend': [
|
||||
'yuthon_catering/static/src/js/catering_create_button.js',
|
||||
'yuthon_catering/static/src/xml/catering_create_button_views.xml',
|
||||
],
|
||||
# 自定义一个样式文件,只在某些页面生效
|
||||
'yuthon_catering_templates.scss': [
|
||||
'yuthon_catering/static/src/scss/yuthon_catering_template.scss',
|
||||
]
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -1,43 +1,99 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import calendar
|
||||
from collections import defaultdict
|
||||
from datetime import date, datetime
|
||||
|
||||
from odoo import http
|
||||
from odoo.http import request
|
||||
|
||||
|
||||
class CateringController(http.Controller):
|
||||
|
||||
@http.route(['/catering/portal/<int:employee_id>'], type='http', auth='public', website=True)
|
||||
def catering_portal(self, employee_id, **kw):
|
||||
"""
|
||||
渲染 catering_portal_page 这个 QWeb 模板。
|
||||
页面内部包含一个挂载 OWL 的区域。
|
||||
"""
|
||||
# 登记页面
|
||||
@http.route(['/catering/registration/<int:employee_id>'], type='http', auth='public', methods=['GET', 'POST'], csrf=False)
|
||||
def catering_portal(self, employee_id, **kwargs):
|
||||
employee = request.env['hr.employee'].sudo().browse(employee_id)
|
||||
today = datetime.today().strftime('%Y-%m-%d')
|
||||
# 今日菜单
|
||||
menu_setting = request.env['yuthon.menu.setting'].search([('menu_date', '=', today)])
|
||||
menu_map = defaultdict(list)
|
||||
for menu in menu_setting:
|
||||
menu_map['breakfast'].extend(menu.breakfast_ids.mapped('name'))
|
||||
menu_map['lunch'].extend(menu.lunch_ids.mapped('name'))
|
||||
# 构建qweb上下文
|
||||
qcontext = {
|
||||
'employee': employee, # 员工
|
||||
'today': today, # 日期
|
||||
'breakfast_menu': list(set(menu_map['breakfast'])), # 早餐
|
||||
'lunch_menu': list(set(menu_map['lunch'])), # 午餐
|
||||
**kwargs # post请求的数据
|
||||
}
|
||||
if request.httprequest.method == 'GET':
|
||||
# 渲染 QWeb 模板
|
||||
return request.render('yuthon_catering.catering_registration_template', qcontext)
|
||||
else:
|
||||
# 创建数据
|
||||
# [XSK] TODO 可以添加判断条件,如果今天已经登记则修改q_context['error']为错误信息 -2025/12/6 16:10
|
||||
request.env['yuthon.catering'].create({
|
||||
'date': kwargs.get('date'),
|
||||
'employee_id': employee.id,
|
||||
'breakfast': kwargs.get('breakfast') == 'true',
|
||||
'lunch': kwargs.get('lunch') == 'true',
|
||||
})
|
||||
qcontext.update({
|
||||
'tips': '提交成功',
|
||||
})
|
||||
# 渲染 QWeb 模板
|
||||
return request.render('yuthon_catering.yuthon_catering_template', {'employee': employee})
|
||||
return request.render('yuthon_catering.catering_registration_template', qcontext)
|
||||
|
||||
@http.route(['/catering/create'], type='http', auth='public', methods=['POST'], csrf=False)
|
||||
def catering_create(self, **post):
|
||||
"""
|
||||
前端 OWL 调用此接口创建 yuthon.catering 记录。
|
||||
接受字段:date, employee_id, breakfast, lunch, amount, note
|
||||
"""
|
||||
qcontext = post
|
||||
try:
|
||||
employee_id = int(post.get('employee_id'))
|
||||
vals = {
|
||||
'date': post.get('date'),
|
||||
'employee_id': employee_id,
|
||||
'breakfast': post.get('breakfast') == 'true',
|
||||
'lunch': post.get('lunch') == 'true',
|
||||
'amount': float(post.get('amount') or 0),
|
||||
'note': post.get('note') or '',
|
||||
}
|
||||
request.env['yuthon.catering'].sudo().create(vals)
|
||||
employee = request.env['hr.employee'].sudo().browse(employee_id)
|
||||
qcontext['employee'] = employee
|
||||
qcontext['tips'] = '提交成功!'
|
||||
return request.render('yuthon_catering.yuthon_catering_template', qcontext)
|
||||
# 统计页面
|
||||
@http.route(['/catering/statistics/<int:employee_id>'], type='http', auth='public', methods=['GET'], csrf=False)
|
||||
def catering_create(self, employee_id, **kwargs):
|
||||
# 公司信息
|
||||
company = request.env.company
|
||||
# 当前员工
|
||||
employee = request.env['hr.employee'].sudo().browse(employee_id)
|
||||
today = datetime.today().strftime('%Y-%m-%d')
|
||||
# 获取筛选模式 默认筛选模式为天
|
||||
filter_mode = kwargs.get('filter_mode', 'day')
|
||||
# 筛选数据
|
||||
filter_date = kwargs.get('filter_date')
|
||||
filter_month = kwargs.get('filter_month')
|
||||
department_id = kwargs.get('department_id')
|
||||
|
||||
if filter_mode == 'day' and not filter_date:
|
||||
filter_date = today
|
||||
if filter_mode == 'month' and not filter_month:
|
||||
filter_month = datetime.today().strftime('%Y-%m')
|
||||
# 获取所有部门
|
||||
departments = request.env['hr.department'].sudo().search([])
|
||||
# 构建查询条件
|
||||
domain = []
|
||||
# 根据筛选模式添加条件
|
||||
if filter_mode == 'day' and filter_date:
|
||||
domain.append(('date', '=', filter_date))
|
||||
elif filter_mode == 'month' and filter_month:
|
||||
# 解析月份
|
||||
year, month = map(int, filter_month.split('-'))
|
||||
from_date = date(year, month, 1)
|
||||
to_date = date(year, month, calendar.monthrange(year, month)[1])
|
||||
domain.append(('date', '>=', from_date))
|
||||
domain.append(('date', '<=', to_date))
|
||||
else:
|
||||
if department_id:
|
||||
domain.append(('dept_id', '=', department_id))
|
||||
result = request.env['yuthon.catering'].sudo().search(domain)
|
||||
qcontext = {
|
||||
**kwargs,
|
||||
'employee': employee,
|
||||
'departments': departments,
|
||||
'today': datetime.today().strftime('%Y-%m-%d'),
|
||||
'filter_month': filter_month,
|
||||
'filter_mode': kwargs.get('filter_mode', 'day'),
|
||||
'breakfast': len(result.filtered(lambda x: x.breakfast)),
|
||||
'lunch': len(result.filtered(lambda x: x.lunch)),
|
||||
'total': sum(result.mapped('amount')),
|
||||
}
|
||||
|
||||
return request.render('yuthon_catering.catering_statistics_template', qcontext)
|
||||
|
||||
except Exception as e:
|
||||
qcontext['error'] = str(e)
|
||||
return request.render('yuthon_catering.yuthon_catering_template', qcontext)
|
||||
|
||||
@@ -7,6 +7,7 @@ class YuthonCatering(models.Model):
|
||||
_name = 'yuthon.catering'
|
||||
|
||||
date = fields.Date(string='日期', required=True)
|
||||
dept_id = fields.Many2one(string='部门', comodel_name='hr.department', related='employee_id.department_id', store=True)
|
||||
employee_id = fields.Many2one(string='员工', comodel_name='hr.employee', required=True)
|
||||
breakfast = fields.Boolean(string='早餐', default=False)
|
||||
lunch = fields.Boolean(string='午餐', default=False)
|
||||
@@ -79,5 +80,8 @@ class YuthonCatering(models.Model):
|
||||
return f'{month}月{day}日'
|
||||
|
||||
def cron_send_message(self):
|
||||
url = f'/catering/portal/<int:employee_id>'
|
||||
# 登记页面
|
||||
url = f'/catering/registration/<int:employee_id>'
|
||||
# 统计页面
|
||||
url = f'/catering/statistics/<int:employee_id>'
|
||||
|
||||
|
||||
@@ -28,35 +28,6 @@ class YuthonDishesSetting(models.Model):
|
||||
_description = '菜品配置'
|
||||
|
||||
name = fields.Char(string='菜品名称')
|
||||
dishes_class = fields.Selection([('hun', '荤'), ('su', '素'), ('tang', '汤'), ('fruit', '水果'), ('drink', '饮料'),('coarse', '粗粮') ,('bread', '面包'), ('pastry', '糕点')], string='菜品分类')
|
||||
dishes_class = fields.Selection([('hun', '荤'), ('su', '素'), ('tang', '汤')], string='类别')
|
||||
dishes_type = fields.Selection([('breakfast', '早餐'), ('lunch', '午餐')], string='菜品类型', default='lunch')
|
||||
ingredient_id1 = fields.Many2one('yuthon.ingredient.setting', string='食材1')
|
||||
ingredient_id2 = fields.Many2one('yuthon.ingredient.setting', string='食材2')
|
||||
ingredient_id3 = fields.Many2one('yuthon.ingredient.setting', string='食材3')
|
||||
ingredient_id4 = fields.Many2one('yuthon.ingredient.setting', string='食材4')
|
||||
|
||||
single_quantity1 = fields.Float(string='单量1')
|
||||
single_quantity2 = fields.Float(string='单量2')
|
||||
single_quantity3 = fields.Float(string='单量3')
|
||||
single_quantity4 = fields.Float(string='单量4')
|
||||
|
||||
unit1_id = fields.Many2one('yuthon.unit.setting', string='单位1')
|
||||
unit2_id = fields.Many2one('yuthon.unit.setting', string='单位2')
|
||||
unit3_id = fields.Many2one('yuthon.unit.setting', string='单位3')
|
||||
unit4_id = fields.Many2one('yuthon.unit.setting', string='单位4')
|
||||
|
||||
|
||||
class YuthonIngredientSetting(models.Model):
|
||||
_name = 'yuthon.ingredient.setting'
|
||||
_description = '食材配置'
|
||||
|
||||
name = fields.Char(string='食材名称')
|
||||
|
||||
|
||||
class YuthonUnitSetting(models.Model):
|
||||
_name = 'yuthon.unit.setting'
|
||||
_description = '单位配置'
|
||||
|
||||
name = fields.Char(string='名称')
|
||||
|
||||
|
||||
|
||||
@@ -4,5 +4,5 @@ access_yuthon_catering_conf,yuthon_catering_conf,model_yuthon_catering_conf,,1,1
|
||||
access_yuthon_menu_setting,yuthon_menu_setting,model_yuthon_menu_setting,,1,1,1,1
|
||||
access_yuthon_dishes_setting,yuthon_dishes_setting,model_yuthon_dishes_setting,,1,1,1,1
|
||||
access_catering_reinstated_wizard,catering_reinstated_wizard,model_catering_reinstated_wizard,,1,1,1,1
|
||||
access_yuthon_ingredient_setting,yuthon_ingredient_setting,model_yuthon_ingredient_setting,,1,1,1,1
|
||||
access_yuthon_unit_setting,yuthon_unit_setting,model_yuthon_unit_setting,,1,1,1,1
|
||||
|
||||
|
||||
|
||||
|
@@ -0,0 +1,403 @@
|
||||
:root {
|
||||
--primary-color: #4f46e5;
|
||||
--primary-light: #eef2ff;
|
||||
--primary-dark: #4338ca;
|
||||
|
||||
--success-color: #10b981;
|
||||
--warning-color: #f59e0b;
|
||||
--danger-color: #ef4444;
|
||||
|
||||
--info-color: #3b82f6;
|
||||
--text-dark: #333;
|
||||
--text-light: #777;
|
||||
|
||||
--light-bg: #f8fafc;
|
||||
--card-bg: #ffffff;
|
||||
|
||||
--card-shadow: 0 8px 20px rgba(0,0,0,0.06);
|
||||
--card-shadow-hover: 0 12px 28px rgba(0,0,0,0.12);
|
||||
|
||||
--card-radius: 14px;
|
||||
--transition: 0.25s ease;
|
||||
}
|
||||
|
||||
// 导航栏样式
|
||||
.tab-navigation {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.tab-list {
|
||||
display: flex;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: var(--card-radius);
|
||||
backdrop-filter: blur(10px);
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.tab-button {
|
||||
flex: 1;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border: none;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
border-radius: calc(var(--card-radius) - 2px);
|
||||
transition: var(--transition);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
background-color: var(--primary-color);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
|
||||
// 选择器样式
|
||||
.custom-radio {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.custom-radio:checked + .radio-custom::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background-color: #1976d2;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.date-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.datepicker-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
}
|
||||
/* ------------------------------------
|
||||
页面布局容器
|
||||
------------------------------------ */
|
||||
/* 页面最外层容器 */
|
||||
.catering-container {
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
padding: 0 1rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
|
||||
/* ------------------------------------
|
||||
卡片
|
||||
------------------------------------ */
|
||||
.form-card {
|
||||
background-color: var(--card-bg);
|
||||
border-radius: var(--card-radius);
|
||||
padding: 25px;
|
||||
margin-bottom: 10px;
|
||||
box-shadow: var(--card-shadow);
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
/* ------------------------------------
|
||||
员工信息卡片
|
||||
------------------------------------ */
|
||||
.employee-info {
|
||||
background-color: #f5f6fa;
|
||||
padding: 20px;
|
||||
border-radius: var(--card-radius);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-light);
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-size: 1.2rem;
|
||||
color: var(--text-dark);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ------------------------------------
|
||||
提醒条
|
||||
------------------------------------ */
|
||||
.reminder {
|
||||
background-color: #fff7e1;
|
||||
border-radius: var(--card-radius);
|
||||
font-size: 0.95rem;
|
||||
color: #8a5a00;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.reminder strong {
|
||||
color: var(--warning-color);
|
||||
}
|
||||
|
||||
/* ------------------------------------
|
||||
菜单展示区
|
||||
------------------------------------ */
|
||||
.meal-details {
|
||||
background-color: #f3f4f6;
|
||||
padding: 20px;
|
||||
border-radius: var(--card-radius);
|
||||
}
|
||||
|
||||
.meal-details span {
|
||||
color: #555;
|
||||
display: flex;
|
||||
font-weight: 600;
|
||||
font-size: 1.05rem;
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.meal-items {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.meal-item {
|
||||
background: white;
|
||||
padding: 8px 16px;
|
||||
border-radius: 30px;
|
||||
font-size: 0.85rem;
|
||||
box-shadow: 0 2px 6px rgba(0,0,0,0.06);
|
||||
}
|
||||
|
||||
/* ------------------------------------
|
||||
用餐选项(YES / NO)
|
||||
------------------------------------ */
|
||||
.meal-title {
|
||||
font-size: 1.1rem;
|
||||
font-weight: bold;
|
||||
margin-bottom: 18px;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
|
||||
/* 容器 */
|
||||
.meal-options {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.meal-option {
|
||||
display: flex;
|
||||
|
||||
/* 默认按钮样式 */
|
||||
.meal-toggle {
|
||||
padding: 10px 18px;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
background-color: #f3f4f6;
|
||||
color: #374151;
|
||||
transition: all 0.2s ease;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
/* 高亮状态:radio 选中时高亮 label */
|
||||
.custom-radio:checked + .meal-toggle {
|
||||
background-color: #6366f1; /* 高亮底色 */
|
||||
border-color: #4f46e5; /* 高亮边框 */
|
||||
color: white; /* 文本颜色 */
|
||||
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.35);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* hover 效果 */
|
||||
.meal-option:hover {
|
||||
background-color: #f2f4ff;
|
||||
border-color: #c7d2fe;
|
||||
}
|
||||
|
||||
/* ------------------------------------
|
||||
表单提交按钮
|
||||
------------------------------------ */
|
||||
.submit-btn {
|
||||
width: 100%;
|
||||
padding: 16px 0;
|
||||
margin-top: 8px;
|
||||
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
border: none;
|
||||
border-radius: var(--card-radius);
|
||||
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
transition: var(--transition);
|
||||
box-shadow: 0 4px 12px rgba(79,70,229,0.35);
|
||||
}
|
||||
|
||||
.submit-btn:hover {
|
||||
background: var(--primary-dark);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(79,70,229,0.45);
|
||||
}
|
||||
|
||||
/* ------------------------------------
|
||||
成功/错误提示
|
||||
------------------------------------ */
|
||||
.alert {
|
||||
padding: 18px;
|
||||
font-size: 1rem;
|
||||
border-radius: var(--card-radius);
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background: #ecfdf5;
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
.alert-danger {
|
||||
background: #fee2e2;
|
||||
color: #7f1d1d;
|
||||
}
|
||||
|
||||
/* ================================
|
||||
顶部筛选模式(按日/按月/按部门)
|
||||
================================ */
|
||||
.filter-mode-container {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.filter-mode {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
|
||||
.custom-radio {
|
||||
display: none; /* 隐藏原生 radio */
|
||||
}
|
||||
|
||||
.mode-toggle {
|
||||
width: 100%;
|
||||
padding: 10px 16px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #d1d5db;
|
||||
background-color: #f3f4f6;
|
||||
color: #374151;
|
||||
cursor: pointer;
|
||||
transition: all .2s;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* radio 选中后高亮 */
|
||||
.custom-radio:checked + .mode-toggle {
|
||||
background-color: #6366f1;
|
||||
border-color: #4f46e5;
|
||||
color: #ffffff;
|
||||
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.35);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
}
|
||||
|
||||
/* ================================
|
||||
筛选控件区域
|
||||
================================ */
|
||||
.filter-conditions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
.filter-label {
|
||||
font-size: 14px;
|
||||
color: #374151;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 输入框、下拉框样式统一 */
|
||||
input[type="date"],
|
||||
input[type="month"],
|
||||
select {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
font-size: 14px;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 8px;
|
||||
background: #f9fafb;
|
||||
transition: border-color .2s, box-shadow .2s;
|
||||
|
||||
&:focus {
|
||||
border-color: #6366f1;
|
||||
box-shadow: 0 0 0 3px rgba(99,102,241,0.2);
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ================================
|
||||
结果展示卡片(早餐 / 午餐 / 总计)
|
||||
================================ */
|
||||
.filter-result {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.filter-result-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.filter-result-content {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
/* ================================
|
||||
不同卡片颜色(可选)
|
||||
================================ */
|
||||
.filter {
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
.breakfast .filter-result-title {
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.lunch .filter-result-title {
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.total .filter-result-title {
|
||||
color: #3b82f6;
|
||||
}
|
||||
@@ -8,6 +8,7 @@
|
||||
<field name="arch" type="xml">
|
||||
<tree string="餐饮" editable="top">
|
||||
<field name="date"/>
|
||||
<field name="dept_id"/>
|
||||
<field name="employee_id"/>
|
||||
<field name="breakfast"/>
|
||||
<field name="lunch"/>
|
||||
|
||||
@@ -25,18 +25,6 @@
|
||||
<field name="name"/>
|
||||
<field name="dishes_class"/>
|
||||
<field name="dishes_type"/>
|
||||
<field name="ingredient_id1"/>
|
||||
<field name="single_quantity1"/>
|
||||
<field name="unit1_id"/>
|
||||
<field name="ingredient_id2"/>
|
||||
<field name="single_quantity2"/>
|
||||
<field name="unit2_id"/>
|
||||
<field name="ingredient_id3"/>
|
||||
<field name="single_quantity3"/>
|
||||
<field name="unit3_id"/>
|
||||
<field name="ingredient_id4"/>
|
||||
<field name="single_quantity4"/>
|
||||
<field name="unit4_id"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
@@ -46,36 +34,6 @@
|
||||
<field name="view_mode">tree</field>
|
||||
</record>
|
||||
|
||||
<record id="yuthon_ingredient_setting_tree" model="ir.ui.view">
|
||||
<field name="name">yuthon_ingredient_setting_tree</field>
|
||||
<field name="model">yuthon.ingredient.setting</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="食材配置" editable="bottom">
|
||||
<field name="name"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
<record id="yuthon_ingredient_setting_action" model="ir.actions.act_window">
|
||||
<field name="name">食材配置</field>
|
||||
<field name="res_model">yuthon.ingredient.setting</field>
|
||||
<field name="view_mode">tree</field>
|
||||
</record>
|
||||
|
||||
<record id="yuthon_unit_setting_tree" model="ir.ui.view">
|
||||
<field name="name">yuthon_unit_setting_tree</field>
|
||||
<field name="model">yuthon.unit.setting</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="单位配置" editable="bottom">
|
||||
<field name="name"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
<record id="yuthon_unit_setting_action" model="ir.actions.act_window">
|
||||
<field name="name">单位配置</field>
|
||||
<field name="res_model">yuthon.unit.setting</field>
|
||||
<field name="view_mode">tree</field>
|
||||
</record>
|
||||
|
||||
<menuitem
|
||||
id="yuthon_menu_setting_menu"
|
||||
name="菜单配置"
|
||||
@@ -89,16 +47,4 @@
|
||||
parent="yuthon_catering_conf_root_menu"
|
||||
sequence="10"
|
||||
action="yuthon_dishes_setting_action"/>
|
||||
<menuitem
|
||||
id="yuthon_ingredient_setting_menu"
|
||||
name="食材配置"
|
||||
parent="yuthon_catering_conf_root_menu"
|
||||
sequence="15"
|
||||
action="yuthon_ingredient_setting_action"/>
|
||||
<menuitem
|
||||
id="yuthon_unit_setting_menu"
|
||||
name="单位配置"
|
||||
parent="yuthon_catering_conf_root_menu"
|
||||
sequence="20"
|
||||
action="yuthon_unit_setting_action"/>
|
||||
</odoo>
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<template id="yuthon_catering.layout" name="Catering Layout">
|
||||
<t t-call-assets="yuthon_catering_templates.scss" t-js="false"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"/>
|
||||
<div class="catering-container">
|
||||
<!-- 导航 -->
|
||||
<div class="tab-navigation">
|
||||
<div class="tab-list">
|
||||
<a class="tab-button" t-att-href="'/catering/registration/%s' % employee.id">登记</a>
|
||||
<a class="tab-button" t-att-href="'/catering/statistics/%s' % employee.id">统计</a>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 正文内容 -->
|
||||
<t t-out="content or '未设置内容'"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 登记页面 -->
|
||||
<template id="yuthon_catering.catering_registration_template" name="Catering Registration Template">
|
||||
<t t-call="yuthon_catering.layout">
|
||||
<!-- 默认内容 -->
|
||||
<t t-set="content">
|
||||
<!-- 整个提交 form -->
|
||||
<form id="cateringRegistrationForm" t-att-action="'/catering/registration/%s' % employee.id" method="post">
|
||||
<!-- 当前员工ID -->
|
||||
<input type="hidden" name="employee_id" t-att-value="employee.id"/>
|
||||
|
||||
<!-- 员工信息 -->
|
||||
<div class="form-card employee-info">
|
||||
<div class="info-item">
|
||||
<div class="info-label">当前日期</div>
|
||||
<div class="info-value">
|
||||
<t t-esc="today"/>
|
||||
<input type="hidden" name="date" required="1" t-att-value="today" readonly="1" class="info-value" style="border: none;font-size:1rem;"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">当前员工</div>
|
||||
<div class="info-value">
|
||||
<t t-esc="employee.name"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">所属部门</div>
|
||||
<div class="info-value">
|
||||
<t t-esc="employee.department_id.name"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 备注提醒 -->
|
||||
<div class="form-card reminder">
|
||||
<strong>
|
||||
<i class="fas fa-exclamation-circle"/>提醒:
|
||||
</strong>
|
||||
请在 17:30 前登记次日用餐,逾期未填系统将自动视为就餐。
|
||||
</div>
|
||||
<!-- 今日菜单 -->
|
||||
<div class="meal-details">
|
||||
<span><i class="fas fa-coffee"/>早餐菜单</span>
|
||||
<div class="meal-items">
|
||||
<t t-foreach="breakfast_menu" t-as="menu">
|
||||
<div class="meal-item">
|
||||
<t t-esc="menu"/>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
<span><i class="fas fa-utensils"/> 午餐菜单</span>
|
||||
<div class="meal-items">
|
||||
<t t-foreach="lunch_menu" t-as="menu">
|
||||
<div class="meal-item">
|
||||
<t t-esc="menu"/>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 早餐选择卡片 -->
|
||||
<div class="form-card meal-section">
|
||||
<!-- 标题 -->
|
||||
<div class="meal-title">
|
||||
早餐选择
|
||||
</div>
|
||||
<!-- 选择 -->
|
||||
<div class="meal-options">
|
||||
<div class="meal-option">
|
||||
<input type="radio" id="breakfast-true" name="breakfast" value="true" class="custom-radio" t-att-checked="'checked' if breakfast else None"/>
|
||||
<label for="breakfast-true" class="meal-toggle">
|
||||
<span class="toggle-label">是</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="meal-option">
|
||||
<input type="radio" id="breakfast-false" name="breakfast" value="false" class="custom-radio" t-att-checked="'checked' if not breakfast else None"/>
|
||||
<label for="breakfast-false" class="meal-toggle">
|
||||
<span class="toggle-label">否</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 午餐选择卡片 -->
|
||||
<div class="form-card">
|
||||
<div class="meal-title">
|
||||
午餐选择
|
||||
</div>
|
||||
<div class="meal-options">
|
||||
<div class="meal-option">
|
||||
<input type="radio" id="lunch-true" name="lunch" value="true" class="custom-radio" t-att-checked="'checked' if lunch else None"/>
|
||||
<label for="lunch-true" class="meal-toggle">
|
||||
<span class="toggle-label">是</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="meal-option">
|
||||
<input type="radio" id="lunch-false" name="lunch" value="false" class="custom-radio" t-att-checked="'checked' if not lunch else None"/>
|
||||
<label for="lunch-false" class="meal-toggle">
|
||||
<span class="toggle-label">
|
||||
否
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 提交成功 -->
|
||||
<div class="form-card alert alert-success" t-if="tips">
|
||||
<t t-esc="tips"/>
|
||||
</div>
|
||||
<!-- 提交失败 -->
|
||||
<div class="form-card alert alert-danger" t-if="error">
|
||||
<t t-esc="error"/>
|
||||
</div>
|
||||
<!-- 提交按钮 -->
|
||||
<button type="submit" class="submit-btn">提交</button>
|
||||
</form>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<!-- 统计页面 -->
|
||||
<template id="yuthon_catering.catering_statistics_template" name="Catering Statistics Template">
|
||||
<t t-call="yuthon_catering.layout">
|
||||
<t t-set="content">
|
||||
<form id="cateringStatisticsForm" t-att-action="'/catering/statistics/%s' % employee.id" method="get">
|
||||
<!-- 筛选模式切换 -->
|
||||
<div class="filter-mode-container">
|
||||
<div class="filter-mode">
|
||||
<input type="radio" id="mode-day" name="filter_mode" value="day" class="custom-radio"
|
||||
onchange="this.form.submit()"
|
||||
t-att-checked="'checked' if filter_mode == 'day' else None"/>
|
||||
<label for="mode-day" class="mode-toggle">
|
||||
<span class="toggle-label">按日统计</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="filter-mode">
|
||||
<input type="radio" id="mode-month" name="filter_mode" value="month" class="custom-radio"
|
||||
onchange="this.form.submit()"
|
||||
t-att-checked="'checked' if filter_mode == 'month' else None"/>
|
||||
<label for="mode-month" class="mode-toggle">
|
||||
<span class="toggle-label">按月统计</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="filter-mode">
|
||||
<input type="radio" id="mode-department" name="filter_mode" value="department" class="custom-radio"
|
||||
onchange="this.form.submit()"
|
||||
t-att-checked="'checked' if filter_mode == 'department' else None"/>
|
||||
<label for="mode-department" class="mode-toggle">
|
||||
<span class="toggle-label">按部门统计</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 筛选 -->
|
||||
<div class="form-card filter">
|
||||
<div class="filter-conditions">
|
||||
<!-- 按日筛选 -->
|
||||
<div class="filter-group" t-if="filter_mode == 'day'">
|
||||
<div class="filter-label">选择日期</div>
|
||||
<div class="date-wrapper">
|
||||
<input type="date" id="filter-day" name="filter_date" onchange="this.form.submit()" t-att-value="filter_date or today"/>
|
||||
<div class="datepicker-overlay" onclick="document.getElementById('filter-day').showPicker()"/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 按月筛选 -->
|
||||
<div class="filter-group" t-if="filter_mode == 'month'">
|
||||
<div class="filter-label">选择月份</div>
|
||||
<div class="date-wrapper">
|
||||
<input type="month" id="filter-month" name="filter_month" onchange="this.form.submit()" t-att-value="filter_month"/>
|
||||
<div class="datepicker-overlay" onclick="document.getElementById('filter-month').showPicker()"/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 按部门筛选 -->
|
||||
<div class="filter-group" t-if="filter_mode == 'department'">
|
||||
<div class="filter-label">选择部门</div>
|
||||
<select name="department_id" id="filter-department" onchange="this.form.submit()">
|
||||
<option value="0">所有部门</option>
|
||||
<t t-foreach="departments" t-as="dept">
|
||||
<option t-att-value="dept.id"
|
||||
t-att-selected="'selected' if department_id == dept.id else None">
|
||||
<t t-esc="dept.name"/>
|
||||
</option>
|
||||
</t>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 早餐 -->
|
||||
<div class="form-card breakfast">
|
||||
<div class="filter-result">
|
||||
<div class="filter-result-title">早餐</div>
|
||||
<div class="filter-result-content"><t t-out="breakfast"/></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 午餐 -->
|
||||
<div class="form-card lunch">
|
||||
<div class="filter-result">
|
||||
<div class="filter-result-title">午餐</div>
|
||||
<div class="filter-result-content"><t t-out="lunch"/></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 总计 -->
|
||||
<div class="form-card total">
|
||||
<div class="filter-result">
|
||||
<div class="filter-result-title">总计</div>
|
||||
<div class="filter-result-content"><t t-out="total"/></div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
</odoo>
|
||||
@@ -8,18 +8,6 @@ class CateringReinstatedWizard(models.TransientModel):
|
||||
_description = '复用弹窗'
|
||||
|
||||
def confirm(self):
|
||||
today = fields.Date.today()
|
||||
date_list = [today + timedelta(days=i) for i in range(7)]
|
||||
for target_date in date_list:
|
||||
if not self._process_shift_days(target_date):
|
||||
continue
|
||||
existing = self.env['yuthon.menu.setting'].search([('menu_date', '=', target_date)], limit=1)
|
||||
if existing:
|
||||
continue
|
||||
self.env['yuthon.menu.setting'].create({
|
||||
'menu_date': target_date,
|
||||
})
|
||||
|
||||
menu_ids = self.env['yuthon.menu.setting'].search([])
|
||||
for menu in menu_ids:
|
||||
new_date = menu.menu_date + timedelta(days=7)
|
||||
@@ -32,35 +20,4 @@ class CateringReinstatedWizard(models.TransientModel):
|
||||
'lunch_ids': [(6, 0, menu.lunch_ids.ids)],
|
||||
})
|
||||
|
||||
def _process_shift_days(self, current_data):
|
||||
"""
|
||||
判断是否是工作日 调休日 双休
|
||||
"""
|
||||
calendar_leaves = self.env['resource.calendar.leaves'].search([("calendar_id", "=", "行政考勤工作日")])
|
||||
holidays_list = []
|
||||
shift_days_list = []
|
||||
# 提取节假日和调休日
|
||||
for leave in calendar_leaves:
|
||||
if leave.holidays:
|
||||
holidays_list.extend(leave.holidays.split(','))
|
||||
if leave.alternate_rest_day:
|
||||
holidays_list.extend(leave.alternate_rest_day.split(','))
|
||||
if leave.shift_replacement_day:
|
||||
shift_days_list.extend(leave.shift_replacement_day.split(','))
|
||||
|
||||
holidays_list = [self.remove_leading_zero(date) for date in holidays_list]
|
||||
shift_days_list = [self.remove_leading_zero(date) for date in shift_days_list]
|
||||
result = f"{current_data.month}月{current_data.day}日"
|
||||
if result in shift_days_list:
|
||||
return True
|
||||
elif (result in holidays_list) or (current_data.weekday() in [5, 6]):
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def remove_leading_zero(self, date_str):
|
||||
month, day = date_str.split('月')
|
||||
month = month.lstrip('0')
|
||||
day = day.rstrip('日')
|
||||
day = day.lstrip('0')
|
||||
return f'{month}月{day}日'
|
||||
|
||||
@@ -80,7 +80,7 @@
|
||||
<attribute name="invisible">1</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='department_id']" position="after">
|
||||
<field name="allowed_user_ids" invisible="1"/>
|
||||
<!-- <field name="allowed_user_ids" invisible="1"/>-->
|
||||
<field name="number"/>
|
||||
<field name="establishment_quotas"/>
|
||||
<field name="no_of_employee" string="在编人数"/>
|
||||
|
||||
@@ -3,19 +3,19 @@
|
||||
<templates id="template" xml:space="preserve">
|
||||
<t t-name="youthon_report.YouthonReport">
|
||||
<div class="container-fluid d-flex flex-column h-100 mt-2 pb-3 ">
|
||||
<!-- <div class="card mb-2">-->
|
||||
<!-- <div class="row g-4">-->
|
||||
<div class="card mb-2">
|
||||
<div class="row g-4">
|
||||
|
||||
<!-- 未处理违约金 -->
|
||||
<div class="col-md-4">
|
||||
<div class="stat-card danger bg-white">
|
||||
<div class="stat-label mb-2">未处理违约金</div>
|
||||
<div class="stat-value text-dark">¥245,890.00</div>
|
||||
<div class="stat-change text-danger mt-1">
|
||||
▲ 12.5% <small class="text-muted">较上月</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 未处理违约金 -->
|
||||
<div class="col-md-4">
|
||||
<div class="stat-card danger bg-white">
|
||||
<div class="stat-label mb-2">未处理违约金</div>
|
||||
<div class="stat-value text-dark">¥245,890.00</div>
|
||||
<div class="stat-change text-danger mt-1">
|
||||
▲ 12.5% <small class="text-muted">较上月</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 已收回违约金 -->
|
||||
<div class="col-md-4">
|
||||
@@ -28,64 +28,64 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <!– 违约合同数 –>-->
|
||||
<!-- <div class="col-md-4">-->
|
||||
<!-- <div class="stat-card info bg-white">-->
|
||||
<!-- <div class="stat-label mb-2">违约合同数</div>-->
|
||||
<!-- <div class="stat-value text-dark">148</div>-->
|
||||
<!-- <div class="stat-change text-danger mt-1">-->
|
||||
<!-- ▲ 3.1% <small class="text-muted">较上月</small>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<!-- 违约合同数 -->
|
||||
<div class="col-md-4">
|
||||
<div class="stat-card info bg-white">
|
||||
<div class="stat-label mb-2">违约合同数</div>
|
||||
<div class="stat-value text-dark">148</div>
|
||||
<div class="stat-change text-danger mt-1">
|
||||
▲ 3.1% <small class="text-muted">较上月</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="card mb-2">
|
||||
<div class="card mb-2">
|
||||
<div class="card-body">
|
||||
<!-- 搜索区域 -->
|
||||
<div class="row mb-2" t-ref="search_area">
|
||||
<div class="row mb-2" t-ref="search_area">
|
||||
|
||||
<!-- 应收日期范围 -->
|
||||
<div class="col-3">
|
||||
<div class="col-3">
|
||||
<label>日期范围</label>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<DateTimeInput
|
||||
range="true"
|
||||
value="state.startDate"
|
||||
type="'date'"
|
||||
placeholder="fromPlaceholder"
|
||||
onChange="(date) => this.onChangeStartDate(date)"
|
||||
/>
|
||||
<i class="oi oi-arrow-right"></i>
|
||||
<DateTimeInput
|
||||
range="true"
|
||||
value="state.endDate"
|
||||
type="'date'"
|
||||
placeholder="toPlaceholder"
|
||||
onChange="(date) => this.onChangeEndDate(date)"
|
||||
value="state.startDate"
|
||||
type="'date'"
|
||||
placeholder="fromPlaceholder"
|
||||
onChange="(date) => this.onChangeStartDate(date)"
|
||||
/>
|
||||
<i class="oi oi-arrow-right"></i>
|
||||
<DateTimeInput
|
||||
range="true"
|
||||
value="state.endDate"
|
||||
type="'date'"
|
||||
placeholder="toPlaceholder"
|
||||
onChange="(date) => this.onChangeEndDate(date)"
|
||||
/>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 所属公司 -->
|
||||
<div class="col-3">
|
||||
<!-- 所属公司 -->
|
||||
<div class="col-3">
|
||||
<label>所属公司</label>
|
||||
<Many2XAutocomplete
|
||||
value="state.selectCompany?.display_name"
|
||||
getDomain="getDomainCompany"
|
||||
resModel="'yuthon.property.company'"
|
||||
fieldString="'所属公司'"
|
||||
activeActions="{}"
|
||||
update.bind="onUpdateCompany"
|
||||
/>
|
||||
<Many2XAutocomplete
|
||||
value="state.selectCompany?.display_name"
|
||||
getDomain="getDomainCompany"
|
||||
resModel="'yuthon.property.company'"
|
||||
fieldString="'所属公司'"
|
||||
activeActions="{}"
|
||||
update.bind="onUpdateCompany"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<div class="col-3">
|
||||
<label>权属类型</label>
|
||||
<select id="formview_default_conditions" class="o_input" t-model="state.condition">
|
||||
<select id="formview_default_conditions" class="o_input" t-model="state.condition">
|
||||
<option t-foreach="state.conditions" t-as="cond"
|
||||
t-att-value="cond.value"
|
||||
t-key="cond.name">
|
||||
@@ -93,22 +93,22 @@
|
||||
</option>
|
||||
</select>
|
||||
</div><!-- 片区管理员 -->
|
||||
<div class="col-3">
|
||||
<div class="col-3">
|
||||
<label>片区管理员</label>
|
||||
<Many2XAutocomplete
|
||||
value="state.selectPartner?.display_name"
|
||||
getDomain="getDomain"
|
||||
resModel="'res.users'"
|
||||
fieldString="'片区管理员'"
|
||||
activeActions="{}"
|
||||
update.bind="onUpdatePartner"
|
||||
/>
|
||||
<Many2XAutocomplete
|
||||
value="state.selectPartner?.display_name"
|
||||
getDomain="getDomain"
|
||||
resModel="'res.users'"
|
||||
fieldString="'片区管理员'"
|
||||
activeActions="{}"
|
||||
update.bind="onUpdatePartner"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="col-12 mt-3 d-flex justify-content-end gap-2">
|
||||
<!-- 操作按钮 -->
|
||||
<div class="col-12 mt-3 d-flex justify-content-end gap-2">
|
||||
<button class="btn btn-primary" t-on-click="onSearch">搜索</button>
|
||||
<button class="btn btn-success" t-on-click="onExportExcel">导出</button>
|
||||
<button class="btn btn-success" t-on-click="onExportExcel">导出</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user