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 @@