From 9606bb2fc710a718b45163386f632ae1bbbaff5a Mon Sep 17 00:00:00 2001 From: pengyuthon Date: Mon, 18 May 2026 13:43:34 +0800 Subject: [PATCH] =?UTF-8?q?=E5=B7=A5=E4=BD=9C=E5=8F=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common_lib/property_filter.py | 90 +++++++++++ .../common_lib/property_rules.py | 56 +++++++ .../models/ks_dashboard_ninja_items.py | 11 +- .../components/property_filter/constant.js | 49 ++++++ .../property_filter/property_filter.js | 151 ++++++++++++++++++ .../property_filter/property_filter.scss | 19 +++ .../property_filter/property_filter.xml | 42 +++++ .../static/src/js/ks_dashboard_ninja_new.js | 56 ++++++- .../src/xml/ks_dashboard_ninja_templates.xml | 3 +- 9 files changed, 473 insertions(+), 4 deletions(-) create mode 100644 ks_dashboard_ninja/common_lib/property_filter.py create mode 100644 ks_dashboard_ninja/common_lib/property_rules.py create mode 100644 ks_dashboard_ninja/static/src/components/property_filter/constant.js create mode 100644 ks_dashboard_ninja/static/src/components/property_filter/property_filter.js create mode 100644 ks_dashboard_ninja/static/src/components/property_filter/property_filter.scss create mode 100644 ks_dashboard_ninja/static/src/components/property_filter/property_filter.xml diff --git a/ks_dashboard_ninja/common_lib/property_filter.py b/ks_dashboard_ninja/common_lib/property_filter.py new file mode 100644 index 000000000..3da3c6c5e --- /dev/null +++ b/ks_dashboard_ninja/common_lib/property_filter.py @@ -0,0 +1,90 @@ +from datetime import date, datetime + +import pytz + +from odoo.osv import expression +from odoo.tools import ( + date_utils, + DEFAULT_SERVER_DATE_FORMAT as DATE_FORMAT, + DEFAULT_SERVER_DATETIME_FORMAT as DATETIME_FORMAT +) + + +from .property_rules import ( + filter_rules, + DEFAULT_DATE_FIELD, + DEFAULT_ADMIN_FIELD, + DEFAULT_COMPANY_FIELD, + DEFAULT_PRECINCT_FIELD +) + +def get_filter_rule(model_name): + for rule in filter_rules: + if rule['res_model'] == model_name: + return rule + raise ValueError(f'No filter rule for model {model_name}') + + +def convert_to_utc(dt): + """将本地时间转换为 UTC 时间""" + if dt is None: + return None + + tz = pytz.timezone('Asia/Shanghai') + local_dt = tz.localize(dt) if not dt.tzinfo else dt.astimezone(tz) + utc_dt = local_dt.astimezone(pytz.UTC) + return utc_dt.replace(tzinfo=None) + + +def get_property_filter_domain(ctx, model_name): + if not ctx.get('showFilter', False): + return [] + rule = get_filter_rule(model_name) + + date_value = ctx.get('dateValue') + many2x_mapping = ctx.get('many2XMapping') + + domain = [] + + if date_value: + year = date_value.get('year') + month = date_value.get('month') + date_field = rule.get('date_field', DEFAULT_DATE_FIELD) + + if year and not month: + base_date = datetime(year, 1, 1) + start_date_utc = convert_to_utc(base_date).strftime(DATETIME_FORMAT) + end_date = date_utils.end_of(base_date, 'year') + end_date_utc = convert_to_utc(end_date).strftime(DATETIME_FORMAT) + domain = expression.AND([domain, [(date_field, '>=', start_date_utc), (date_field, '<=', end_date_utc)]]) + + elif year and month: + base_date = datetime(year, month, 1) + start_date = date_utils.start_of(base_date, 'month') + end_date = date_utils.end_of(base_date, 'month') + + start_date_utc = convert_to_utc(start_date).strftime(DATETIME_FORMAT) + end_date_utc = convert_to_utc(end_date).strftime(DATETIME_FORMAT) + + domain = expression.AND([domain, [(date_field, '>=', start_date_utc), (date_field, '<=', end_date_utc)]]) + + if many2x_mapping: + many2x_domain = [] + admin = many2x_mapping.get('admin') + precinct = many2x_mapping.get('precinct') + company = many2x_mapping.get('company') + + if admin: + admin_field = rule.get('admin_field', DEFAULT_ADMIN_FIELD) + many2x_domain.append((admin_field, '=', admin)) + + if precinct: + precinct_field = rule.get('precinct_field', DEFAULT_PRECINCT_FIELD) + many2x_domain.append((precinct_field, '=', precinct)) + + if company: + company_field = rule.get('company_field', DEFAULT_COMPANY_FIELD) + many2x_domain.append((company_field, '=', company)) + + domain = expression.AND([domain, many2x_domain]) + return domain diff --git a/ks_dashboard_ninja/common_lib/property_rules.py b/ks_dashboard_ninja/common_lib/property_rules.py new file mode 100644 index 000000000..1970effe2 --- /dev/null +++ b/ks_dashboard_ninja/common_lib/property_rules.py @@ -0,0 +1,56 @@ +DEFAULT_DATE_FIELD = 'create_date' +DEFAULT_ADMIN_FIELD = 'admin_id' +DEFAULT_PRECINCT_FIELD = 'precinct_id' +DEFAULT_COMPANY_FIELD = 'company_id' +# [ +# "yuthon.property", +# "property.lease.contract", +# "property.accounts.receivable", +# "meter.archives.line", +# "property.management.line", +# "receivable.modify.line" +# ] +filter_rules = ( + { + 'res_model': 'yuthon.property', + 'date_field': DEFAULT_DATE_FIELD, + 'admin_field': DEFAULT_ADMIN_FIELD, + 'precinct_field': DEFAULT_PRECINCT_FIELD, + 'company_field': DEFAULT_COMPANY_FIELD, + }, + { + 'res_model': 'property.lease.contract', + 'date_field': DEFAULT_DATE_FIELD, + 'admin_field': DEFAULT_ADMIN_FIELD, + 'precinct_field': DEFAULT_PRECINCT_FIELD, + 'company_field': 'lessor_id', + }, + { + 'res_model': 'property.accounts.receivable', + 'date_field': DEFAULT_DATE_FIELD, + 'admin_field': DEFAULT_ADMIN_FIELD, + 'precinct_field': DEFAULT_PRECINCT_FIELD, + 'company_field': DEFAULT_COMPANY_FIELD, + }, + { + 'res_model': 'meter.archives.line', + 'date_field': DEFAULT_DATE_FIELD, + 'admin_field': DEFAULT_ADMIN_FIELD, + 'precinct_field': DEFAULT_PRECINCT_FIELD, + 'company_field': DEFAULT_COMPANY_FIELD, + }, + { + 'res_model': 'property.management.line', + 'date_field': DEFAULT_DATE_FIELD, + 'admin_field': 'admin_id_all', + 'precinct_field': DEFAULT_PRECINCT_FIELD, + 'company_field': DEFAULT_COMPANY_FIELD, + }, + { + 'res_model': 'receivable.modify.line', + 'date_field': DEFAULT_DATE_FIELD, + 'admin_field': f'receivable_id.{DEFAULT_ADMIN_FIELD}', + 'precinct_field': DEFAULT_PRECINCT_FIELD, + 'company_field': DEFAULT_COMPANY_FIELD, + }, +) diff --git a/ks_dashboard_ninja/models/ks_dashboard_ninja_items.py b/ks_dashboard_ninja/models/ks_dashboard_ninja_items.py index 180da6232..f4b127f2d 100644 --- a/ks_dashboard_ninja/models/ks_dashboard_ninja_items.py +++ b/ks_dashboard_ninja/models/ks_dashboard_ninja_items.py @@ -1927,7 +1927,9 @@ class KsDashboardNinjaItems(models.Model): return 0 return data - def ks_convert_into_proper_domain(self, ks_domain, rec, domain=[]): + def ks_convert_into_proper_domain(self, ks_domain, rec, domain=None): + if domain is None: + domain = [] if ks_domain and "%UID" in ks_domain: ks_domain = ks_domain.replace('"%UID"', str(self.env.user.id)) @@ -2048,6 +2050,13 @@ class KsDashboardNinjaItems(models.Model): if domain: proper_domain.extend(domain) + if self._context.get('showFilter', False): + from odoo.osv import expression + from ..common_lib.property_filter import get_property_filter_domain + + proper_domain = expression.AND( + [proper_domain, get_property_filter_domain(self._context, rec.ks_model_name)] + ) return proper_domain def ks_convert_domain_extension(self, ks_extensiom_domain, rec): diff --git a/ks_dashboard_ninja/static/src/components/property_filter/constant.js b/ks_dashboard_ninja/static/src/components/property_filter/constant.js new file mode 100644 index 000000000..a52b464a3 --- /dev/null +++ b/ks_dashboard_ninja/static/src/components/property_filter/constant.js @@ -0,0 +1,49 @@ +/** @odoo-module */ + +/** + * Dashboard item的模型是否属于物业相关 + */ +export const PROPERTY_MODELS = [ + "yuthon.property", + "property.lease.contract", + "property.accounts.receivable", + "meter.archives.line", + "property.management.line", + "receivable.modify.line" +]; + +/** + * @typedef PropertyFilterField + * @property {string} name 名称 + * @property {string} fieldName 字段名称 + * @property {string} label 字段标签 + * @property {string} resModel 模型名称 + * @property {Array} [domain] domain + */ + +/** + * 物业相关的过滤字段 + * @type {PropertyFilterField[]} + */ +export const PROPERTY_FILTER_FIELDS = [ + { + name: "admin", + fieldName: "admin_id", + label: "片区管理", + resModel: "res.users", + domain: [], + }, + , + { + name: "precinct", + fieldName: "precinct_id", + label: "所属片区", + resModel: "yuthon.precinct.message", + }, + { + name: "company", + fieldName: "company_id", + label: "公司", + resModel: "yuthon.property.company", + }, +]; diff --git a/ks_dashboard_ninja/static/src/components/property_filter/property_filter.js b/ks_dashboard_ninja/static/src/components/property_filter/property_filter.js new file mode 100644 index 000000000..47951542f --- /dev/null +++ b/ks_dashboard_ninja/static/src/components/property_filter/property_filter.js @@ -0,0 +1,151 @@ +/** @odoo-module */ + +import { useService } from "@web/core/utils/hooks"; +import { debounce } from "@web/core/utils/timing"; +import { DateTimeInput } from "@web/core/datetime/datetime_input"; +import { Many2XAutocomplete } from "@web/views/fields/relational_utils"; +import { Component, toRaw, useEffect, useState, useRef } from "@odoo/owl"; +import { PROPERTY_MODELS, PROPERTY_FILTER_FIELDS } from "./constant"; + +export { PROPERTY_MODELS, PROPERTY_FILTER_FIELDS }; + +const { DateTime } = luxon; + +export class PropertyFilter extends Component { + static template = "yuthon.PropertyFilter"; + static components = { DateTimeInput, Many2XAutocomplete }; + static props = { + onApply: { type: Function, optional: true }, + onReset: { type: Function, optional: true }, + }; + static defaultProps = { onApply: () => {}, onReset: () => {} }; + + setup() { + this.filterRef = useRef("property_filter"); + this.notificationService = useService("notification"); + this.state = useState({ + year: null, + month: null, + admin: null, + precinct: null, + company: null, + adminName: "", + precinctName: "", + companyName: "", + }); + + this.dateValue = useState({ + year: null, + month: null, + }); + + this.many2XMapping = useState({ + admin: null, + precinct: null, + company: null, + }); + + useEffect( + (year, month) => { + if (year instanceof DateTime && month instanceof DateTime) { + this.dateValue.year = year.year; + this.dateValue.month = month.month; + } else if (year instanceof DateTime) { + this.dateValue.year = year.year; + } + }, + () => [this.state.year, this.state.month], + ); + + useEffect( + (admin, precinct, company) => { + this.many2XMapping.admin = admin?.id || null; + this.many2XMapping.precinct = precinct?.id || null; + this.many2XMapping.company = company?.id || null; + }, + () => [this.state.admin, this.state.precinct, this.state.company], + ); + + this.onClickApply = debounce(this._onClickApply.bind(this), 300); + this.onClickReset = debounce(this._onClickReset.bind(this), 300); + } + + /** + * @param {'year' | 'month'} type + * @returns {Object} + */ + getDateTimeInputProps(type) { + return { + format: type === "year" ? "yyyy '年'" : "M '月'", + // onChange: (value) => { + // debugger + // this.state[type] = value; + // }, + onApply: (value) => { + this.state[type] = value; + }, + id: `o_yuthon_property_filter__${type}`, + placeholder: type === "year" ? "年" : "月", + minPrecision: `${type}s`, + maxPrecision: `${type}s`, + type: "date", + value: this.state[type], + }; + } + + /** + * + * @param {'admin' | 'precinct' | 'company'} type + */ + getMany2XAutocompleteProps(type) { + const filterField = PROPERTY_FILTER_FIELDS.filter((field) => field.name === type)[0]; + const { resModel, label, fieldName, domain } = filterField; + + return { + id: `o_yuthon_property_filter__${type}`, + placeholder: label, + resModel: resModel, + fieldString: label, + activeActions: {}, + getDomain: () => domain || [], + isToMany: false, + value: this.state[`${type}Name`], + update: (value) => this.onUpdateMany2X(type, fieldName, value), + }; + } + + onUpdateMany2X(type, fieldName, value) { + this.state[`${type}Name`] = value[0]?.display_name; + this.state[type] = value[0]; + } + + async _onClickApply() { + const { year, month } = this.state; + // debugger + if (year === null && month !== null) { + this.notificationService.add("请选择年份", { title: "错误", type: "danger" }); + return; + } + this.props?.onApply?.(toRaw(this.dateValue), toRaw(this.many2XMapping)); + } + + async _onClickReset() { + this.state.year = null; + this.state.month = null; + this.state.admin = null; + this.state.precinct = null; + this.state.company = null; + this.state.adminName = ""; + this.state.precinctName = ""; + this.state.companyName = ""; + + this.dateValue.year = null; + this.dateValue.month = null; + + this.many2XMapping.admin = null; + this.many2XMapping.precinct = null; + this.many2XMapping.company = null; + + this.props?.onReset?.(); + } +} diff --git a/ks_dashboard_ninja/static/src/components/property_filter/property_filter.scss b/ks_dashboard_ninja/static/src/components/property_filter/property_filter.scss new file mode 100644 index 000000000..16c76f353 --- /dev/null +++ b/ks_dashboard_ninja/static/src/components/property_filter/property_filter.scss @@ -0,0 +1,19 @@ +.o_yuthon_property_filter { + display: flex; + align-items: flex-end; + justify-content: flex-start; + gap: 4px; + // border: 1px solid #f7f7f7; + background-color: #f7f7f7; + // border-radius: 4px; + box-shadow: 0 0 2px rgba(0, 0, 0, 0.25); + + &__item { + display: block; + } + &__buttons { + background-color: unset; + display: flex; + gap: 4px; + } +} diff --git a/ks_dashboard_ninja/static/src/components/property_filter/property_filter.xml b/ks_dashboard_ninja/static/src/components/property_filter/property_filter.xml new file mode 100644 index 000000000..fb96c332c --- /dev/null +++ b/ks_dashboard_ninja/static/src/components/property_filter/property_filter.xml @@ -0,0 +1,42 @@ + + + + + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ +
+ + +
+
+ +
+ + + + +
\ No newline at end of file diff --git a/ks_dashboard_ninja/static/src/js/ks_dashboard_ninja_new.js b/ks_dashboard_ninja/static/src/js/ks_dashboard_ninja_new.js index a07831cf2..977a38412 100644 --- a/ks_dashboard_ninja/static/src/js/ks_dashboard_ninja_new.js +++ b/ks_dashboard_ninja/static/src/js/ks_dashboard_ninja_new.js @@ -28,6 +28,7 @@ import { DateTimeInput } from "@web/core/datetime/datetime_input"; const { DateTime } = luxon; import { formatDate, formatDateTime } from "@web/core/l10n/dates"; import { parseDateTime, parseDate, } from "@web/core/l10n/dates"; +import { PropertyFilter, PROPERTY_MODELS } from "../components/property_filter/property_filter"; export class KsDashboardNinja extends Component { @@ -191,8 +192,59 @@ export class KsDashboardNinja extends Component { this._onKsApplyDateFilter(); } }); + + this.propertyFilterState = useState({ + showFilter: false, + dateValue: null, + many2XMapping: null + }) + + useEffect( + (dashboardData) => { + const setter = (val) => (this.propertyFilterState.showFilter = val); + const items = dashboardData?.ks_item_data; + if (!items) { + setter(false); + return; + } + const models = [...new Set(Object.values(items).map((item) => item?.ks_model_name))]; + // console.log(models) + if (!models.length) { + setter(false); + return; + } + for (const model of models) { + if (!PROPERTY_MODELS.includes(model)) { + setter(false); + return; + } + setter(true); + } + }, + () => [this.state?.ks_dashboard_data], + ); } + get propertyFilterProps() { + const update = async () => { + if (this.propertyFilterState.showFilter) { + this.state.ksDateFilterSelection = 'l_none' + } + await this.ks_fetch_items_data(); + }; + return { + onApply: async (dateValue, many2XMapping) => { + this.propertyFilterState.dateValue = dateValue; + this.propertyFilterState.many2XMapping = many2XMapping; + await update(); + }, + onReset: async () => { + this.propertyFilterState.dateValue = null; + this.propertyFilterState.many2XMapping = null; + await update(); + }, + }; + } willStart() { var self = this; @@ -285,7 +337,7 @@ export class KsDashboardNinja extends Component { if (self.dn_state['user_context']['ksDateFilterSelection'] !== undefined && self.ksDateFilterSelection !== 'l_none') { context = self.dn_state['user_context'] } - var ks_new_obj = { ...session.user_context, ...{ allowed_company_ids: this.env.services.company.activeCompanyIds } } + var ks_new_obj = { ...session.user_context, ...{ allowed_company_ids: this.env.services.company.activeCompanyIds, ...this.propertyFilterState } } return Object.assign(context, ks_new_obj); } @@ -1597,7 +1649,7 @@ export class KsDashboardNinja extends Component { } -KsDashboardNinja.components = { Ksdashboardtile, Ksdashboardlistview, Ksdashboardgraph, Ksdashboardkpiview, Ksdashboardtodo, DateTimePicker, DateTimeInput }; +KsDashboardNinja.components = { Ksdashboardtile, Ksdashboardlistview, Ksdashboardgraph, Ksdashboardkpiview, Ksdashboardtodo, DateTimePicker, DateTimeInput, PropertyFilter }; KsDashboardNinja.template = "ks_dashboard_ninja.KsDashboardNinjaHeader" registry.category("actions").add("ks_dashboard_ninja", KsDashboardNinja); diff --git a/ks_dashboard_ninja/static/src/xml/ks_dashboard_ninja_templates.xml b/ks_dashboard_ninja/static/src/xml/ks_dashboard_ninja_templates.xml index 29397704b..8d16d7e19 100644 --- a/ks_dashboard_ninja/static/src/xml/ks_dashboard_ninja_templates.xml +++ b/ks_dashboard_ninja/static/src/xml/ks_dashboard_ninja_templates.xml @@ -132,7 +132,8 @@
-