This commit is contained in:
pengyuthon
2025-12-29 17:48:47 +08:00
parent 962af65ee3
commit 02b78b87ee
12 changed files with 799 additions and 228 deletions
+5 -1
View File
@@ -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',
]
},
}
+88 -32
View File
@@ -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)
+5 -1
View File
@@ -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='名称')
+2 -2
View File
@@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
4 access_yuthon_menu_setting yuthon_menu_setting model_yuthon_menu_setting 1 1 1 1
5 access_yuthon_dishes_setting yuthon_dishes_setting model_yuthon_dishes_setting 1 1 1 1
6 access_catering_reinstated_wizard catering_reinstated_wizard model_catering_reinstated_wizard 1 1 1 1
7
8
@@ -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>
<!-- &lt;!&ndash; 违约合同数 &ndash;&gt;-->
<!-- <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>