From 02b78b87ee41dfdae81aff1aa5f9384b22cb9143 Mon Sep 17 00:00:00 2001 From: pengyuthon Date: Mon, 29 Dec 2025 17:48:47 +0800 Subject: [PATCH] 1 --- yuthon_catering/__manifest__.py | 6 +- yuthon_catering/controllers/controllers.py | 120 ++++-- yuthon_catering/models/yuthon_catering.py | 6 +- .../models/yuthon_catering_setting.py | 31 +- yuthon_catering/security/ir.model.access.csv | 4 +- .../src/scss/yuthon_catering_template.scss | 403 ++++++++++++++++++ yuthon_catering/views/yuthon_catering.xml | 1 + .../views/yuthon_catering_setting_views.xml | 54 --- .../views/yuthon_catering_templates.xml | 229 ++++++++++ .../wizard/catering_reinstated_wizard.py | 43 -- .../views/inherit_hr_job_views.xml | 2 +- .../yuthon_report/yuthon_report.xml | 128 +++--- 12 files changed, 799 insertions(+), 228 deletions(-) create mode 100644 yuthon_catering/static/src/scss/yuthon_catering_template.scss create mode 100644 yuthon_catering/views/yuthon_catering_templates.xml diff --git a/yuthon_catering/__manifest__.py b/yuthon_catering/__manifest__.py index dced2d36e..c7c33564c 100644 --- a/yuthon_catering/__manifest__.py +++ b/yuthon_catering/__manifest__.py @@ -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', + ] }, } diff --git a/yuthon_catering/controllers/controllers.py b/yuthon_catering/controllers/controllers.py index 212685ea1..5d8aa2ef0 100644 --- a/yuthon_catering/controllers/controllers.py +++ b/yuthon_catering/controllers/controllers.py @@ -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/'], type='http', auth='public', website=True) - def catering_portal(self, employee_id, **kw): - """ - 渲染 catering_portal_page 这个 QWeb 模板。 - 页面内部包含一个挂载 OWL 的区域。 - """ + # 登记页面 + @http.route(['/catering/registration/'], 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/'], 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) diff --git a/yuthon_catering/models/yuthon_catering.py b/yuthon_catering/models/yuthon_catering.py index 499a6259f..7c28c1015 100644 --- a/yuthon_catering/models/yuthon_catering.py +++ b/yuthon_catering/models/yuthon_catering.py @@ -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/' + # 登记页面 + url = f'/catering/registration/' + # 统计页面 + url = f'/catering/statistics/' diff --git a/yuthon_catering/models/yuthon_catering_setting.py b/yuthon_catering/models/yuthon_catering_setting.py index ba09af4b9..59ea8e5cb 100644 --- a/yuthon_catering/models/yuthon_catering_setting.py +++ b/yuthon_catering/models/yuthon_catering_setting.py @@ -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='名称') - diff --git a/yuthon_catering/security/ir.model.access.csv b/yuthon_catering/security/ir.model.access.csv index ff30aa956..1d3056d3a 100644 --- a/yuthon_catering/security/ir.model.access.csv +++ b/yuthon_catering/security/ir.model.access.csv @@ -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 \ No newline at end of file + + diff --git a/yuthon_catering/static/src/scss/yuthon_catering_template.scss b/yuthon_catering/static/src/scss/yuthon_catering_template.scss new file mode 100644 index 000000000..38d1cec08 --- /dev/null +++ b/yuthon_catering/static/src/scss/yuthon_catering_template.scss @@ -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; +} \ No newline at end of file diff --git a/yuthon_catering/views/yuthon_catering.xml b/yuthon_catering/views/yuthon_catering.xml index 379cb1081..1cfe07b3a 100644 --- a/yuthon_catering/views/yuthon_catering.xml +++ b/yuthon_catering/views/yuthon_catering.xml @@ -8,6 +8,7 @@ + diff --git a/yuthon_catering/views/yuthon_catering_setting_views.xml b/yuthon_catering/views/yuthon_catering_setting_views.xml index 787422a20..55e873235 100644 --- a/yuthon_catering/views/yuthon_catering_setting_views.xml +++ b/yuthon_catering/views/yuthon_catering_setting_views.xml @@ -25,18 +25,6 @@ - - - - - - - - - - - - @@ -46,36 +34,6 @@ tree - - yuthon_ingredient_setting_tree - yuthon.ingredient.setting - - - - - - - - 食材配置 - yuthon.ingredient.setting - tree - - - - yuthon_unit_setting_tree - yuthon.unit.setting - - - - - - - - 单位配置 - yuthon.unit.setting - tree - - - - diff --git a/yuthon_catering/views/yuthon_catering_templates.xml b/yuthon_catering/views/yuthon_catering_templates.xml new file mode 100644 index 000000000..93d70367e --- /dev/null +++ b/yuthon_catering/views/yuthon_catering_templates.xml @@ -0,0 +1,229 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/yuthon_catering/wizard/catering_reinstated_wizard.py b/yuthon_catering/wizard/catering_reinstated_wizard.py index 231942b04..5f505ab2a 100644 --- a/yuthon_catering/wizard/catering_reinstated_wizard.py +++ b/yuthon_catering/wizard/catering_reinstated_wizard.py @@ -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}日' diff --git a/yuthon_hr_employee/views/inherit_hr_job_views.xml b/yuthon_hr_employee/views/inherit_hr_job_views.xml index 24747189d..01f17d6cd 100644 --- a/yuthon_hr_employee/views/inherit_hr_job_views.xml +++ b/yuthon_hr_employee/views/inherit_hr_job_views.xml @@ -80,7 +80,7 @@ 1 - + diff --git a/yuthon_report/static/src/components/yuthon_report/yuthon_report.xml b/yuthon_report/static/src/components/yuthon_report/yuthon_report.xml index 563c68418..f80b10267 100644 --- a/yuthon_report/static/src/components/yuthon_report/yuthon_report.xml +++ b/yuthon_report/static/src/components/yuthon_report/yuthon_report.xml @@ -3,19 +3,19 @@
- - +
+
- -
-
-
未处理违约金
-
¥245,890.00
-
- ▲ 12.5% 较上月 -
-
-
+ +
+
+
未处理违约金
+
¥245,890.00
+
+ ▲ 12.5% 较上月 +
+
+
@@ -28,64 +28,64 @@
- - - - - - - - - - + +
+
+
违约合同数
+
148
+
+ ▲ 3.1% 较上月 +
+
+
- - +
+
-
+
-
+
-
+
-
+
- - + +
- -
+ +
- +
-
+
-
-
+
- +
- -
+ +
- +