1
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from . import controllers
|
||||
from . import models
|
||||
@@ -0,0 +1,24 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
{
|
||||
'name': 'Yuthon Catering',
|
||||
'summary': 'Yuthon Catering',
|
||||
'description': 'Yuthon Catering',
|
||||
'author': 'XSK',
|
||||
'website': '',
|
||||
'category': 'Tools',
|
||||
'version': '17.0',
|
||||
'depends': ['base', 'hr'],
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
'views/root_menu.xml',
|
||||
'views/yuthon_catering.xml',
|
||||
'views/yuthon_catering_conf.xml',
|
||||
'templates/templates.xml',
|
||||
],
|
||||
# 'assets': {
|
||||
# 'web.assets_backend': [
|
||||
# 'base_dynamic_columns/static/src/**/*',
|
||||
# ],
|
||||
# },
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from . import controllers
|
||||
@@ -0,0 +1,43 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
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 的区域。
|
||||
"""
|
||||
employee = request.env['hr.employee'].sudo().browse(employee_id)
|
||||
# 渲染 QWeb 模板
|
||||
return request.render('yuthon_catering.yuthon_catering_template', {'employee': employee})
|
||||
|
||||
@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)
|
||||
|
||||
except Exception as e:
|
||||
qcontext['error'] = str(e)
|
||||
return request.render('yuthon_catering.yuthon_catering_template', qcontext)
|
||||
@@ -0,0 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from . import yuthon_catering
|
||||
from . import yuthon_catering_conf
|
||||
@@ -0,0 +1,17 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class YuthonCatering(models.Model):
|
||||
_name = 'yuthon.catering'
|
||||
|
||||
date = fields.Date(string='日期', required=True)
|
||||
employee_id = fields.Many2one(string='员工', comodel_name='hr.employee', required=True)
|
||||
breakfast = fields.Boolean(string='早餐', default=False)
|
||||
lunch = fields.Boolean(string='午餐', default=False)
|
||||
amount = fields.Float(string='结算费用')
|
||||
note = fields.Text(string='备注')
|
||||
|
||||
def cron_send_message(self):
|
||||
url = f'/catering/portal/<int:employee_id>'
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class YuthonCateringConf(models.Model):
|
||||
_name = 'yuthon.catering.conf'
|
||||
|
||||
company_id = fields.Many2one(string='公司', comodel_name='res.company', required=True, default=lambda self: self.env.company)
|
||||
employee_ids = fields.Many2many(string='员工', comodel_name='hr.employee')
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_yuthon_catering,yuthon_catering,model_yuthon_catering,,1,1,1,1
|
||||
access_yuthon_catering_conf,yuthon_catering_conf,model_yuthon_catering_conf,,1,1,1,1
|
||||
|
||||
|
||||
|
||||
|
@@ -0,0 +1,148 @@
|
||||
/** @odoo-module **/
|
||||
import { registry } from "@web/core/registry";
|
||||
import { FileInput } from "@web/core/file_input/file_input";
|
||||
import { useSelectCreate } from "@web/views/fields/relational_utils";
|
||||
import { many2OneField, Many2OneField } from "@web/views/fields/many2one/many2one_field";
|
||||
|
||||
|
||||
export class PreviewMany2OneField extends Many2OneField {
|
||||
static template = "preview_easy.Many2OneField";
|
||||
static components = {
|
||||
...Many2OneField.components,
|
||||
FileInput
|
||||
};
|
||||
static props = {
|
||||
...Many2OneField.props,
|
||||
acceptedFileExtensions: { type: String, optional: true },
|
||||
showSelectBtn: { type: Boolean, optional: true },
|
||||
previewType: { type: String, optional: true },
|
||||
previewString: { type: String, optional: true },
|
||||
className: { type: String, optional: true },
|
||||
};
|
||||
setup() {
|
||||
super.setup();
|
||||
const isToMany = false;
|
||||
this.selectCreate = useSelectCreate({
|
||||
resModel: this.relation,
|
||||
activeActions: {create: false, },
|
||||
onSelected: (resId) => {
|
||||
const resIds = Array.isArray(resId) ? resId : [resId];
|
||||
const values = resIds.map((id) => ({ id }));
|
||||
return this.update(values);
|
||||
},
|
||||
onCreateEdit: ({ context }) => this.openMany2X({ context }),
|
||||
onUnselect: isToMany ? undefined : () => this.update(),
|
||||
});
|
||||
}
|
||||
|
||||
async onFileUploaded(files) {
|
||||
for (const file of files) {
|
||||
if (file.error) {
|
||||
return this.notification.add(file.error, {
|
||||
title: _t("Uploading error"),
|
||||
type: "danger",
|
||||
});
|
||||
}
|
||||
await this.update([{id: file.id, display_name: file.filename}]);
|
||||
}
|
||||
}
|
||||
|
||||
async onFileRemove(deleteId) {
|
||||
await this.update(false);
|
||||
}
|
||||
|
||||
get uploadText() {
|
||||
return this.props.record.fields[this.props.name].string;
|
||||
}
|
||||
|
||||
// 获取文件
|
||||
getFile() {
|
||||
const jsonData = {
|
||||
id: Math.floor(Math.random() * 1000 * 1000 * 1000),
|
||||
jsonrpc: "2.0",
|
||||
method: "call",
|
||||
params: {
|
||||
args: [
|
||||
[this.resId],
|
||||
["id", "name", "mimetype"]
|
||||
],
|
||||
kwargs: {
|
||||
context: this.context,
|
||||
},
|
||||
method: "read",
|
||||
model: this.relation,
|
||||
}
|
||||
};
|
||||
let file;
|
||||
$.ajax({
|
||||
url: "/web/dataset/call_kw/ir.attachment/read",
|
||||
method: "POST",
|
||||
dataType: 'json',
|
||||
async: false, // 设置为同步请求
|
||||
beforeSend: (xhr) => {xhr.setRequestHeader('Content-Type', 'application/json');},
|
||||
data : JSON.stringify(jsonData),
|
||||
success: (result) => {
|
||||
file = result.result[0];
|
||||
}
|
||||
})
|
||||
return file;
|
||||
}
|
||||
|
||||
getUrl(id) {
|
||||
return "/web/content/" + id + "?download=true";
|
||||
}
|
||||
|
||||
preview_Obj(id, name) {
|
||||
var winStr = 'height=600,width=1000,top=' + (screen.height - 600) / 2 + ',left=' + (screen.width - 1000) / 2 + ',toolbar=no,directories=no,status=no,menubar=no,scrollbars=yes,resizable=no,location=no';
|
||||
var newWin = window.open('/web/content/preview/'+ id , name, winStr);
|
||||
newWin.focus();
|
||||
}
|
||||
|
||||
getExtension(file) {
|
||||
return file.name.replace(/^.*\./, "");
|
||||
}
|
||||
|
||||
async onExternalBtnClick() {
|
||||
const domain = this.getDomain();
|
||||
this.selectCreate({
|
||||
domain,
|
||||
context: this.props.record.context,
|
||||
filters: [],
|
||||
title: "Search",
|
||||
});
|
||||
}
|
||||
|
||||
get documentsInstalled() {
|
||||
try {
|
||||
const result = $.ajax({
|
||||
url: "/web/document_module/installed",
|
||||
type: "POST",
|
||||
async: false, // 设置为 false 表示同步请求
|
||||
contentType: "application/json",
|
||||
data: "{}"
|
||||
});
|
||||
return result?.responseJSON?.result?.installed;
|
||||
} catch (error) {
|
||||
console.error('Error fetching document installed status:', error);
|
||||
return false; // 或者其他默认值
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const previewMany2OneField = {
|
||||
...many2OneField,
|
||||
component: PreviewMany2OneField,
|
||||
extractProps({ attrs, context, decorations, options, string }, dynamicInfo) {
|
||||
const result = many2OneField.extractProps.apply(this, arguments);
|
||||
return {
|
||||
...result,
|
||||
acceptedFileExtensions: options.accepted_file_extensions,
|
||||
showSelectBtn: options.show_select_btn === undefined ? true : options.show_select_btn,
|
||||
previewType: options.preview_type,
|
||||
previewString: options.preview_string,
|
||||
className: attrs.class,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
registry.category("fields").add("preview_many2one", previewMany2OneField);
|
||||
@@ -0,0 +1,213 @@
|
||||
/** @odoo-module **/
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import {standardFieldProps} from "@web/views/fields/standard_field_props";
|
||||
import { FileInput } from "@web/core/file_input/file_input";
|
||||
import { useX2ManyCrud, useSelectCreate, useOpenMany2XRecord } from "@web/views/fields/relational_utils";
|
||||
import { Component } from "@odoo/owl";
|
||||
import { TagsList } from "@web/core/tags_list/tags_list";
|
||||
import { Dialog } from "@web/core/dialog/dialog";
|
||||
import {
|
||||
Many2XAutocomplete,
|
||||
useActiveActions,
|
||||
} from "@web/views/fields/relational_utils";
|
||||
import { Many2ManyTagsField, many2ManyTagsField } from "@web/views/fields/many2many_tags/many2many_tags_field";
|
||||
|
||||
export class Many2ManyBinaryField extends Many2ManyTagsField {
|
||||
static template = "web.Many2ManyBinaryField";
|
||||
static components = {
|
||||
FileInput,
|
||||
TagsList,
|
||||
Many2XAutocomplete,
|
||||
};
|
||||
static props = {
|
||||
...standardFieldProps,
|
||||
acceptedFileExtensions: { type: String, optional: true },
|
||||
className: { type: String, optional: true },
|
||||
canCreate: { type: Boolean, optional: true },
|
||||
canQuickCreate: { type: Boolean, optional: true },
|
||||
canCreateEdit: { type: Boolean, optional: true },
|
||||
colorField: { type: String, optional: true },
|
||||
createDomain: { type: [Array, Boolean], optional: true },
|
||||
domain: { type: [Array, Function], optional: true },
|
||||
context: { type: Object, optional: true },
|
||||
placeholder: { type: String, optional: true },
|
||||
nameCreateField: { type: String, optional: true },
|
||||
string: { type: String, optional: true },
|
||||
noSearchMore: { type: Boolean, optional: true },
|
||||
showSelectBtn: { type: Boolean, optional: true },
|
||||
previewType: { type: String, optional: true },
|
||||
previewString: { type: String, optional: true },
|
||||
};
|
||||
|
||||
setup() {
|
||||
super.setup()
|
||||
this.orm = useService("orm");
|
||||
this.notification = useService("notification");
|
||||
this.operations = useX2ManyCrud(() => this.props.record.data[this.props.name], true);
|
||||
const isToMany = true;
|
||||
this.canCreate = false;
|
||||
this.quickCreate = false;
|
||||
this.openMany2X = useOpenMany2XRecord({
|
||||
resModel: this.relation,
|
||||
activeActions: this.activeActions,
|
||||
isToMany: isToMany,
|
||||
onRecordSaved: (record) => {
|
||||
return this.update([{ ...record.data, id: record.resId }]);
|
||||
},
|
||||
onRecordDiscarded: () => {
|
||||
if (!isToMany) {
|
||||
this.update(false);
|
||||
}
|
||||
},
|
||||
fieldString: this.string,
|
||||
onClose: () => {
|
||||
const autoCompleteInput = this.autoCompleteContainer?.el.querySelector("input");
|
||||
|
||||
if (!autoCompleteInput) {
|
||||
return;
|
||||
}
|
||||
// There are two cases:
|
||||
// 1. Value is the same as the input: it means the autocomplete has re-rendered with the right value
|
||||
// This is in case we saved the record, triggering all the interface to update.
|
||||
// 2. Value is different from the input: it means the input has a manually entered value and nothing
|
||||
// happened, that is, we discarded the changes
|
||||
if (this.props.value !== autoCompleteInput.value) {
|
||||
autoCompleteInput.value = "";
|
||||
}
|
||||
autoCompleteInput.focus();
|
||||
},
|
||||
});
|
||||
this.selectCreate = useSelectCreate({
|
||||
resModel: this.relation,
|
||||
activeActions: this.activeActions,
|
||||
onSelected: (resId) => {
|
||||
const resIds = Array.isArray(resId) ? resId : [resId];
|
||||
const values = resIds.map((id) => ({ id }));
|
||||
return this.update(values);
|
||||
},
|
||||
onCreateEdit: ({ context }) => this.openMany2X({ context }),
|
||||
onUnselect: isToMany ? undefined : () => this.update(),
|
||||
});
|
||||
}
|
||||
|
||||
get uploadText() {
|
||||
return this.props.record.fields[this.props.name].string;
|
||||
}
|
||||
get files() {
|
||||
return this.props.record.data[this.props.name].records.map((record) => {
|
||||
return {
|
||||
...record.data,
|
||||
id: record.resId,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
getUrl(id) {
|
||||
return "/web/content/" + id + "?download=true";
|
||||
}
|
||||
|
||||
preview_Obj(id, name) {
|
||||
var winStr = 'height=600,width=1000,top=' + (screen.height - 600) / 2 + ',left=' + (screen.width - 1000) / 2 + ',toolbar=no,directories=no,status=no,menubar=no,scrollbars=yes,resizable=no,location=no';
|
||||
var newWin = window.open('/web/content/preview/'+ id , name, winStr);
|
||||
newWin.focus();
|
||||
}
|
||||
|
||||
getExtension(file) {
|
||||
return file.name.replace(/^.*\./, "");
|
||||
}
|
||||
|
||||
async onFileUploaded(files) {
|
||||
for (const file of files) {
|
||||
if (file.error) {
|
||||
return this.notification.add(file.error, {
|
||||
title: _t("Uploading error"),
|
||||
type: "danger",
|
||||
});
|
||||
}
|
||||
await this.operations.saveRecord([file.id]);
|
||||
}
|
||||
}
|
||||
|
||||
async onFileRemove(deleteId) {
|
||||
const record = this.props.record.data[this.props.name].records.find(
|
||||
(record) => record.resId === deleteId
|
||||
);
|
||||
this.operations.removeRecord(record);
|
||||
}
|
||||
|
||||
async onExternalBtnClick() {
|
||||
|
||||
const domain = this.getDomain();
|
||||
let dynamicFilters = [];
|
||||
const nameGets = await this.orm.call(this.relation, "name_search", [], {
|
||||
name: '',
|
||||
args: domain,
|
||||
operator: "ilike",
|
||||
limit: this.props.searchMoreLimit,
|
||||
context: this.props.context,
|
||||
});
|
||||
|
||||
dynamicFilters = [
|
||||
{
|
||||
description: _t("Quick search"),
|
||||
domain: [["id", "in", nameGets.map((nameGet) => nameGet[0])]],
|
||||
},
|
||||
];
|
||||
|
||||
const title = _t("Search");
|
||||
this.selectCreate({
|
||||
domain,
|
||||
context: this.props.context,
|
||||
filters: dynamicFilters,
|
||||
title,
|
||||
});
|
||||
}
|
||||
get documentsInstalled() {
|
||||
try {
|
||||
const result = $.ajax({
|
||||
url: "/web/document_module/installed",
|
||||
type: "POST",
|
||||
async: false, // 设置为 false 表示同步请求
|
||||
contentType: "application/json",
|
||||
data: "{}"
|
||||
});
|
||||
return result?.responseJSON?.result?.installed;
|
||||
} catch (error) {
|
||||
console.error('Error fetching document installed status:', error);
|
||||
return false; // 或者其他默认值
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const many2ManyBinaryField = {
|
||||
component: Many2ManyBinaryField,
|
||||
supportedOptions: [
|
||||
{
|
||||
label: _t("Accepted file extensions"),
|
||||
name: "accepted_file_extensions",
|
||||
type: "string",
|
||||
},
|
||||
],
|
||||
supportedTypes: ["many2many"],
|
||||
isEmpty: () => false,
|
||||
relatedFields: [
|
||||
{ name: "name", type: "char" },
|
||||
{ name: "mimetype", type: "char" },
|
||||
],
|
||||
extractProps: ({ attrs, options }) => ({
|
||||
acceptedFileExtensions: options.accepted_file_extensions,
|
||||
showSelectBtn: options.show_select_btn === undefined ? true : options.show_select_btn,
|
||||
previewType: options.preview_type,
|
||||
previewString: options.preview_string,
|
||||
className: attrs.class,
|
||||
}),
|
||||
};
|
||||
|
||||
registry.category("fields").add("preview_many2many", many2ManyBinaryField);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
.o_field_preview_many2many {
|
||||
.oe_add {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html;charset=utf-8">
|
||||
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate"/>
|
||||
<meta http-equiv="Pragma" content="no-cache"/>
|
||||
<meta http-equiv="Expires" content="0"/>
|
||||
<title>preview</title>
|
||||
<script>
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
{{html}}
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,91 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="preview_easy.Many2OneField">
|
||||
<t t-if="props.readonly">
|
||||
<t t-call="preview_easy.Many2OneFieldPreview"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<!-- 如果存在文件显示预览按钮,如果不存在文件,显示上传选择按钮 -->
|
||||
<t t-if="resId">
|
||||
<div class="oe_add">
|
||||
<t t-call="preview_easy.Many2OneFieldPreview"/>
|
||||
<FileInput
|
||||
acceptedFileExtensions="props.acceptedFileExtensions"
|
||||
multiUpload="true"
|
||||
onUpload.bind="onFileUploaded"
|
||||
resModel="props.record.resModel"
|
||||
resId="props.record.resId or 0"
|
||||
>
|
||||
<button class="btn btn-secondary o_attach" data-tooltip="Attach">
|
||||
<span class="fa fa-paperclip" aria-label="Attach"/>
|
||||
<t t-esc="uploadText"/>
|
||||
</button>
|
||||
</FileInput>
|
||||
<button t-if="documentsInstalled and props.showSelectBtn" class="btn btn-secondary o_attach" t-on-click="onExternalBtnClick" data-tooltip="Attach">
|
||||
<i class="fa fa-folder-open-o"/>
|
||||
选择文件
|
||||
</button>
|
||||
</div>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<div class="oe_add">
|
||||
<FileInput
|
||||
acceptedFileExtensions="props.acceptedFileExtensions"
|
||||
multiUpload="true"
|
||||
onUpload.bind="onFileUploaded"
|
||||
resModel="props.record.resModel"
|
||||
resId="props.record.resId or 0"
|
||||
>
|
||||
<button class="btn btn-secondary o_attach" data-tooltip="Attach">
|
||||
<span class="fa fa-paperclip" aria-label="Attach"/>
|
||||
<t t-esc="uploadText"/>
|
||||
</button>
|
||||
</FileInput>
|
||||
<button t-if="documentsInstalled and props.showSelectBtn" class="btn btn-secondary o_attach" t-on-click="onExternalBtnClick" data-tooltip="Attach">
|
||||
<i class="fa fa-folder-open-o"/>
|
||||
选择文件
|
||||
</button>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
<t t-name="preview_easy.Many2OneFieldPreview" owl="1">
|
||||
<t t-set="editable" t-value="!props.readonly"/>
|
||||
<t t-set="file" t-value="getFile()"/>
|
||||
<!-- 预览按钮 -->
|
||||
<t t-if="props.previewType and props.previewType === 'button'">
|
||||
<button class="btn btn-secondary o_attach" t-on-click.stop="() => this.preview_Obj()" data-tooltip="Attach">
|
||||
<i class="fa fa-eye"/>
|
||||
<span t-if="props.previewString" t-out="props.previewString"/>
|
||||
<span t-else="" t-out="'预览文件'"/>
|
||||
</button>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<div t-attf-class="o_attachment o_attachment_many2many #{ editable ? 'o_attachment_editable' : '' } #{upload ? 'o_attachment_uploading' : ''}" t-att-title="file.name">
|
||||
<div class="o_attachment_wrap">
|
||||
<t t-set="ext" t-value="getExtension(file)"/>
|
||||
<t t-set="url" t-value="getUrl(file.id)"/>
|
||||
<div class="o_image_box float-start" t-att-data-tooltip="'Preview ' + file.name">
|
||||
<span class="o_image o_hover" t-on-click.stop="() => this.preview_Obj(file.id,file.name)" t-att-data-mimetype="file.mimetype" t-att-data-ext="ext" role="img"/>
|
||||
</div>
|
||||
|
||||
<div class="caption">
|
||||
<a class="ml4" t-att-data-tooltip="'Download ' + file.name" t-att-href="getUrl(file.id)"><t t-esc='file.name'/></a>
|
||||
</div>
|
||||
<div class="caption small">
|
||||
<a class="ml4 small text-uppercase" t-att-href="getUrl(file.id)"><b><t t-esc='ext'/></b></a>
|
||||
<div t-if="editable" class="progress o_attachment_progress_bar">
|
||||
<div class="progress-bar progress-bar-striped active" style="width: 100%">Uploading</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="o_attachment_uploaded"><i class="text-success fa fa-check" role="img" aria-label="Uploaded" title="Uploaded"/></div>
|
||||
<div t-if="editable" class="o_attachment_delete" t-on-click.stop="() => this.onFileRemove(file.id)"><span class="text-white" role="img" aria-label="Delete" title="Delete">×</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -0,0 +1,65 @@
|
||||
<templates xml:space="preserve">
|
||||
<t t-inherit="web.Many2ManyBinaryField" t-inherit-mode="extension" owl="1">
|
||||
<xpath expr="//div[hasclass('o_attachments')]" position="replace">
|
||||
<t t-foreach="files" t-as="file" t-key="file_index">
|
||||
<t t-call="Many2ManyBinaryField.attachment_preview1"/>
|
||||
</t>
|
||||
</xpath>
|
||||
<xpath expr="//div[hasclass('oe_add')]" position="inside">
|
||||
<button t-if="documentsInstalled and props.showSelectBtn" class="btn btn-secondary o_attach" t-on-click="onExternalBtnClick" data-tooltip="Attach">
|
||||
<i class="fa fa-folder-open-o"/>
|
||||
选择文件
|
||||
<!-- <Many2XAutocomplete -->
|
||||
<!-- id="props.id" -->
|
||||
<!-- resModel="relation" -->
|
||||
<!-- autoSelect="true" -->
|
||||
<!-- fieldString="string" -->
|
||||
<!-- activeActions="activeActions" -->
|
||||
<!-- update="update" -->
|
||||
<!-- quickCreate="activeActions.create ? quickCreate : null" -->
|
||||
<!-- context="props.context" -->
|
||||
<!-- getDomain.bind="getDomain" -->
|
||||
<!-- isToMany="true" -->
|
||||
<!-- nameCreateField="props.nameCreateField" -->
|
||||
<!-- noSearchMore="props.noSearchMore" -->
|
||||
<!-- /> -->
|
||||
</button>
|
||||
|
||||
</xpath>
|
||||
</t>
|
||||
<t t-name="Many2ManyBinaryField.attachment_preview1" owl="1">
|
||||
<t t-set="editable" t-value="!props.readonly"/>
|
||||
<t t-if="props.previewType and props.previewType === 'button'">
|
||||
<button class="btn btn-secondary o_attach" t-on-click.stop="() => this.preview_Obj(file.id,file.name)" data-tooltip="Attach">
|
||||
<i class="fa fa-eye"/>
|
||||
<span t-if="props.previewString" t-out="props.previewString"/>
|
||||
<span t-else="" t-out="'预览文件'"/>
|
||||
</button>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<div t-attf-class="o_attachment o_attachment_many2many #{ editable ? 'o_attachment_editable' : '' } #{upload ? 'o_attachment_uploading' : ''}" t-att-title="file.name">
|
||||
<div class="o_attachment_wrap">
|
||||
<t t-set="ext" t-value="getExtension(file)"/>
|
||||
<t t-set="url" t-value="getUrl(file.id)"/>
|
||||
<div class="o_image_box float-start" t-att-data-tooltip="'Preview ' + file.name">
|
||||
<span class="o_image o_hover" t-on-click.stop="() => this.preview_Obj(file.id,file.name)" t-att-data-mimetype="file.mimetype" t-att-data-ext="ext" role="img"/>
|
||||
</div>
|
||||
|
||||
<div class="caption">
|
||||
<a class="ml4" t-att-data-tooltip="'Download ' + file.name" t-att-href="getUrl(file.id)"><t t-esc='file.name'/></a>
|
||||
</div>
|
||||
<div class="caption small">
|
||||
<a class="ml4 small text-uppercase" t-att-href="getUrl(file.id)"><b><t t-esc='ext'/></b></a>
|
||||
<div t-if="editable" class="progress o_attachment_progress_bar">
|
||||
<div class="progress-bar progress-bar-striped active" style="width: 100%">Uploading</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="o_attachment_uploaded"><i class="text-success fa fa-check" role="img" aria-label="Uploaded" title="Uploaded"/></div>
|
||||
<div t-if="editable" class="o_attachment_delete" t-on-click.stop="() => this.onFileRemove(file.id)"><span class="text-white" role="img" aria-label="Delete" title="Delete">×</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -0,0 +1,348 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<template id="yuthon_catering_template" name="Catering Form Page">
|
||||
|
||||
<!-- 自定义CSS样式 -->
|
||||
<style>
|
||||
:root {
|
||||
--primary-color: #4f46e5;
|
||||
--primary-light: #e0e7ff;
|
||||
--success-color: #10b981;
|
||||
--warning-color: #f59e0b;
|
||||
--info-color: #3b82f6;
|
||||
--light-bg: #f8fafc;
|
||||
--card-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1);
|
||||
--card-radius: 5px;
|
||||
--transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.catering-container {
|
||||
max-width: 700px;
|
||||
margin: 2rem auto;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
text-align: center;
|
||||
margin-bottom: 2.5rem;
|
||||
padding: 1.5rem;
|
||||
background: linear-gradient(135deg, var(--primary-color) 0%, #7c3aed 100%);
|
||||
border-radius: var(--card-radius);
|
||||
color: white;
|
||||
box-shadow: var(--card-shadow);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.page-header::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -50%;
|
||||
left: -50%;
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
background: radial-gradient(circle, rgba(255,255,255,0.1) 1px, transparent 1px);
|
||||
background-size: 30px 30px;
|
||||
opacity: 0.3;
|
||||
animation: float 20s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0% { transform: translate(0, 0) rotate(0deg); }
|
||||
100% { transform: translate(-30px, -30px) rotate(360deg); }
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 2.2rem;
|
||||
font-weight: 800;
|
||||
margin-bottom: 0.5rem;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
font-size: 1rem;
|
||||
opacity: 0.9;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.form-card {
|
||||
background: white;
|
||||
border-radius: var(--card-radius);
|
||||
padding: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
box-shadow: var(--card-shadow);
|
||||
border: none;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.form-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 15px 30px -10px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 1.3rem;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
|
||||
/* 开关样式 */
|
||||
.meal-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 1.2rem;
|
||||
border-radius: 16px;
|
||||
background: var(--light-bg);
|
||||
margin-bottom: 1rem;
|
||||
cursor: pointer;
|
||||
transition: var(--transition);
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
|
||||
.meal-toggle:hover {
|
||||
background: #f1f5f9;
|
||||
}
|
||||
|
||||
.meal-toggle.active {
|
||||
background: white;
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 5px 15px rgba(79, 70, 229, 0.1);
|
||||
}
|
||||
|
||||
.toggle-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
/* 自定义单选按钮 */
|
||||
.custom-radio {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.custom-radio:checked + .meal-toggle {
|
||||
background: white;
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 5px 15px rgba(79, 70, 229, 0.1);
|
||||
}
|
||||
|
||||
/* 表单控件 */
|
||||
.form-control-custom {
|
||||
height: 56px;
|
||||
border-radius: 12px;
|
||||
border: 2px solid #e2e8f0;
|
||||
font-size: 1.1rem;
|
||||
padding: 0 1.2rem;
|
||||
transition: var(--transition);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.form-control-custom:focus {
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.1);
|
||||
}
|
||||
|
||||
textarea.form-control-custom {
|
||||
height: auto;
|
||||
padding: 1.2rem;
|
||||
resize: vertical;
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
/* 金额显示区域 */
|
||||
.amount-display {
|
||||
background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
|
||||
border-radius: 16px;
|
||||
padding: 1.5rem;
|
||||
margin-top: 1rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.amount-label {
|
||||
font-size: 1.1rem;
|
||||
color: #475569;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.amount-value {
|
||||
font-size: 2rem;
|
||||
font-weight: 800;
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
/* 提交按钮 */
|
||||
.submit-btn {
|
||||
height: 64px;
|
||||
border-radius: 16px;
|
||||
background: linear-gradient(135deg, var(--primary-color) 0%, #7c3aed 100%);
|
||||
border: none;
|
||||
font-size: 1.3rem;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
width: 100%;
|
||||
margin-top: 1.5rem;
|
||||
transition: var(--transition);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.8rem;
|
||||
box-shadow: 0 10px 20px rgba(79, 70, 229, 0.3);
|
||||
}
|
||||
|
||||
.submit-btn:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 15px 30px rgba(79, 70, 229, 0.4);
|
||||
}
|
||||
|
||||
.submit-btn:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background-color: var(--success-color);
|
||||
}
|
||||
|
||||
.alert-danger {
|
||||
background-color: var(--warning-color);
|
||||
}
|
||||
|
||||
/* 响应式调整 */
|
||||
@media (max-width: 768px) {
|
||||
.catering-container {
|
||||
margin: 1rem auto;
|
||||
padding: 0 0.75rem;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
.form-card {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- 主容器 -->
|
||||
<div class="catering-container">
|
||||
|
||||
<!-- 页面标题 -->
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">
|
||||
员工报餐登记
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<!-- 整个提交 form -->
|
||||
<form id="cateringForm" action="/catering/create" method="post">
|
||||
|
||||
<!-- 当前员工ID -->
|
||||
<input type="hidden" name="employee_id" t-att-value="employee.id"/>
|
||||
|
||||
<!-- 日期选择卡片 -->
|
||||
<div class="form-card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">选择用餐日期</h3>
|
||||
</div>
|
||||
<input type="date" name="date" required="1" class="form-control form-control-custom" t-att-value="date"/>
|
||||
</div>
|
||||
|
||||
<!-- 早餐选择卡片 -->
|
||||
<div class="form-card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">早餐选择</h3>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
<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 class="form-card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">午餐选择</h3>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
|
||||
<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 class="form-card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">结算金额</h3>
|
||||
</div>
|
||||
<input type="number" step="0.01" min="0" name="amount" class="form-control form-control-custom" placeholder="如需手动调整金额,请在此输入"/>
|
||||
</div>
|
||||
|
||||
<!-- 备注卡片 -->
|
||||
<div class="form-card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">备注信息</h3>
|
||||
</div>
|
||||
|
||||
<textarea name="note" class="form-control form-control-custom" rows="3" placeholder="例如:素食、忌口食物、送餐时间要求等"/>
|
||||
</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">
|
||||
<i class="fas fa-paper-plane"></i> 提交报餐登记
|
||||
</button>
|
||||
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
</odoo>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<data>
|
||||
<menuitem id="yuthon_catering_root_menu" name="餐饮" sequence="1">
|
||||
<menuitem id="yuthon_catering_conf_root_menu" name="配置" sequence="99"/>
|
||||
</menuitem>
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<data>
|
||||
<!-- tree视图 -->
|
||||
<record id="yuthon_catering_tree_view" model="ir.ui.view">
|
||||
<field name="name">yuthon.catering.tree</field>
|
||||
<field name="model">yuthon.catering</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="餐饮" editable="top">
|
||||
<field name="date"/>
|
||||
<field name="employee_id"/>
|
||||
<field name="breakfast"/>
|
||||
<field name="lunch"/>
|
||||
<field name="amount"/>
|
||||
<field name="note"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- 跳转菜单 -->
|
||||
<record id="yuthon_catering_action_window" model="ir.actions.act_window">
|
||||
<field name="name">餐饮收集任务</field>
|
||||
<field name="res_model">yuthon.catering</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
</record>
|
||||
|
||||
|
||||
<!-- 二级菜单 -->
|
||||
<menuitem
|
||||
id="yuthon_catering_menu"
|
||||
name="餐饮收集任务"
|
||||
parent="yuthon_catering_root_menu"
|
||||
sequence="1"
|
||||
action="yuthon_catering_action_window"/>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<data>
|
||||
<!-- tree视图 -->
|
||||
<record id="yuthon_catering_conf_tree_view" model="ir.ui.view">
|
||||
<field name="name">yuthon.catering.conf.tree</field>
|
||||
<field name="model">yuthon.catering.conf</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="配置" editable="top">
|
||||
<field name="company_id"/>
|
||||
<field name="employee_ids"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- 跳转菜单 -->
|
||||
<record id="yuthon_catering_conf_action_window" model="ir.actions.act_window">
|
||||
<field name="name">餐饮人员配置</field>
|
||||
<field name="res_model">yuthon.catering.conf</field>
|
||||
<field name="view_mode">tree</field>
|
||||
<field name="search_view_id" ref=""/>
|
||||
</record>
|
||||
|
||||
<!-- 二级菜单 -->
|
||||
<menuitem
|
||||
id="yuthon_catering_conf_menu"
|
||||
name="餐饮人员配置"
|
||||
parent="yuthon_catering_conf_root_menu"
|
||||
sequence="1"
|
||||
action="yuthon_catering_conf_action_window"/>
|
||||
</data>
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user