From 85e4274e8444d0282728fad547d61b42d32d8564 Mon Sep 17 00:00:00 2001 From: pengyuthon Date: Thu, 4 Dec 2025 00:12:38 +0800 Subject: [PATCH] 1 --- yuthon_catering/__init__.py | 3 + yuthon_catering/__manifest__.py | 24 ++ yuthon_catering/controllers/__init__.py | 2 + yuthon_catering/controllers/controllers.py | 43 +++ yuthon_catering/models/__init__.py | 3 + yuthon_catering/models/yuthon_catering.py | 17 + .../models/yuthon_catering_conf.py | 16 + yuthon_catering/security/ir.model.access.csv | 6 + .../static/src/js/preview_many2one.js | 148 ++++++++ yuthon_catering/static/src/js/qweb.js | 213 +++++++++++ yuthon_catering/static/src/scss/qweb.scss | 5 + yuthon_catering/static/src/xml/1.html | 17 + .../static/src/xml/preview_many2one.xml | 91 +++++ yuthon_catering/static/src/xml/qweb.xml | 65 ++++ yuthon_catering/templates/templates.xml | 348 ++++++++++++++++++ yuthon_catering/views/root_menu.xml | 8 + yuthon_catering/views/yuthon_catering.xml | 37 ++ .../views/yuthon_catering_conf.xml | 32 ++ 18 files changed, 1078 insertions(+) create mode 100644 yuthon_catering/__init__.py create mode 100644 yuthon_catering/__manifest__.py create mode 100644 yuthon_catering/controllers/__init__.py create mode 100644 yuthon_catering/controllers/controllers.py create mode 100644 yuthon_catering/models/__init__.py create mode 100644 yuthon_catering/models/yuthon_catering.py create mode 100644 yuthon_catering/models/yuthon_catering_conf.py create mode 100644 yuthon_catering/security/ir.model.access.csv create mode 100644 yuthon_catering/static/src/js/preview_many2one.js create mode 100644 yuthon_catering/static/src/js/qweb.js create mode 100644 yuthon_catering/static/src/scss/qweb.scss create mode 100644 yuthon_catering/static/src/xml/1.html create mode 100644 yuthon_catering/static/src/xml/preview_many2one.xml create mode 100644 yuthon_catering/static/src/xml/qweb.xml create mode 100644 yuthon_catering/templates/templates.xml create mode 100644 yuthon_catering/views/root_menu.xml create mode 100644 yuthon_catering/views/yuthon_catering.xml create mode 100644 yuthon_catering/views/yuthon_catering_conf.xml diff --git a/yuthon_catering/__init__.py b/yuthon_catering/__init__.py new file mode 100644 index 000000000..9e5827f90 --- /dev/null +++ b/yuthon_catering/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +from . import controllers +from . import models diff --git a/yuthon_catering/__manifest__.py b/yuthon_catering/__manifest__.py new file mode 100644 index 000000000..7b4de9300 --- /dev/null +++ b/yuthon_catering/__manifest__.py @@ -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/**/*', + # ], + # }, +} + diff --git a/yuthon_catering/controllers/__init__.py b/yuthon_catering/controllers/__init__.py new file mode 100644 index 000000000..153a9e31e --- /dev/null +++ b/yuthon_catering/controllers/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from . import controllers diff --git a/yuthon_catering/controllers/controllers.py b/yuthon_catering/controllers/controllers.py new file mode 100644 index 000000000..212685ea1 --- /dev/null +++ b/yuthon_catering/controllers/controllers.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +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 的区域。 + """ + 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) diff --git a/yuthon_catering/models/__init__.py b/yuthon_catering/models/__init__.py new file mode 100644 index 000000000..5b6692f79 --- /dev/null +++ b/yuthon_catering/models/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +from . import yuthon_catering +from . import yuthon_catering_conf diff --git a/yuthon_catering/models/yuthon_catering.py b/yuthon_catering/models/yuthon_catering.py new file mode 100644 index 000000000..3da727d34 --- /dev/null +++ b/yuthon_catering/models/yuthon_catering.py @@ -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/' + diff --git a/yuthon_catering/models/yuthon_catering_conf.py b/yuthon_catering/models/yuthon_catering_conf.py new file mode 100644 index 000000000..957435183 --- /dev/null +++ b/yuthon_catering/models/yuthon_catering_conf.py @@ -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') + + + + + + + diff --git a/yuthon_catering/security/ir.model.access.csv b/yuthon_catering/security/ir.model.access.csv new file mode 100644 index 000000000..70698e012 --- /dev/null +++ b/yuthon_catering/security/ir.model.access.csv @@ -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 + + + diff --git a/yuthon_catering/static/src/js/preview_many2one.js b/yuthon_catering/static/src/js/preview_many2one.js new file mode 100644 index 000000000..daedd2c7f --- /dev/null +++ b/yuthon_catering/static/src/js/preview_many2one.js @@ -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); diff --git a/yuthon_catering/static/src/js/qweb.js b/yuthon_catering/static/src/js/qweb.js new file mode 100644 index 000000000..cfee1c391 --- /dev/null +++ b/yuthon_catering/static/src/js/qweb.js @@ -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); + + + + + diff --git a/yuthon_catering/static/src/scss/qweb.scss b/yuthon_catering/static/src/scss/qweb.scss new file mode 100644 index 000000000..9e8f9832b --- /dev/null +++ b/yuthon_catering/static/src/scss/qweb.scss @@ -0,0 +1,5 @@ +.o_field_preview_many2many { + .oe_add { + + } +} \ No newline at end of file diff --git a/yuthon_catering/static/src/xml/1.html b/yuthon_catering/static/src/xml/1.html new file mode 100644 index 000000000..2012695ee --- /dev/null +++ b/yuthon_catering/static/src/xml/1.html @@ -0,0 +1,17 @@ + + + + + + + + preview + + + + +{{html}} + + + diff --git a/yuthon_catering/static/src/xml/preview_many2one.xml b/yuthon_catering/static/src/xml/preview_many2one.xml new file mode 100644 index 000000000..e8c2bf88f --- /dev/null +++ b/yuthon_catering/static/src/xml/preview_many2one.xml @@ -0,0 +1,91 @@ + + + + + + + + + + +
+ + + + + +
+
+ +
+ + + + +
+
+
+
+ + + + + + + + + +
+
+ + +
+ +
+ +
+ +
+
+ +
+
Uploading
+
+
+ +
+
×
+
+
+
+
+ +
diff --git a/yuthon_catering/static/src/xml/qweb.xml b/yuthon_catering/static/src/xml/qweb.xml new file mode 100644 index 000000000..20a3e9133 --- /dev/null +++ b/yuthon_catering/static/src/xml/qweb.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + +
+
+ + +
+ +
+ +
+ +
+
+ +
+
Uploading
+
+
+ +
+
×
+
+
+
+
+ +
diff --git a/yuthon_catering/templates/templates.xml b/yuthon_catering/templates/templates.xml new file mode 100644 index 000000000..581afd9fc --- /dev/null +++ b/yuthon_catering/templates/templates.xml @@ -0,0 +1,348 @@ + + +