工作台

This commit is contained in:
pengyuthon
2026-05-18 13:43:34 +08:00
parent 72c5fe398d
commit 9606bb2fc7
9 changed files with 473 additions and 4 deletions
@@ -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
@@ -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,
},
)
@@ -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):
@@ -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",
},
];
@@ -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?.();
}
}
@@ -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;
}
}
@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="yuthon.PropertyFilter">
<div class="o_yuthon_property_filter" t-ref="property_filter">
<div class="o_yuthon_property_filter__item">
<DateTimeInput t-props="this.getDateTimeInputProps('year')"/>
</div>
<div class="o_yuthon_property_filter__item">
<DateTimeInput t-props="this.getDateTimeInputProps('month')"/>
</div>
<div class="o_yuthon_property_filter__item">
<Many2XAutocomplete t-props="this.getMany2XAutocompleteProps('admin')"/>
</div>
<div class="o_yuthon_property_filter__item">
<Many2XAutocomplete t-props="this.getMany2XAutocompleteProps('precinct')"/>
</div>
<div class="o_yuthon_property_filter__item">
<Many2XAutocomplete t-props="this.getMany2XAutocompleteProps('company')"/>
</div>
<div class="o_yuthon_property_filter__buttons">
<button class="btn btn-primary" t-on-click.stop.prevent="this.onClickApply">查询</button>
<button class="btn btn-secondary" t-on-click.stop.prevent="this.onClickReset">重置</button>
</div>
</div>
</t>
<t t-name="yuthon.PropertyFilterDateTimeInput">
<input
type="text"
t-ref="start-date"
t-att-id="props.id"
class="o_datetime_input o_input cursor-pointer o_yuthon_property_filter__datetime_input"
autocomplete="off"
t-att-placeholder="props.placeholder"
t-on-change.stop.prevent="testChange"
/>
</t>
</templates>
@@ -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);
@@ -132,7 +132,8 @@
<t t-if="!this.ks_dashboard_data.ks_ai_explain_dash">
<div class="flex_column_datetimerow ks_dashboard_top_menu ks_select_none">
<!-- Date Filter Selection Options (Hide default for no content view)-->
<div class="ks_dashboard_link ks_am_content_element ks_custom_date_filter ks_hide mr-4">
<PropertyFilter t-if="this.propertyFilterState.showFilter" t-props="this.propertyFilterProps"/>
<div t-if="!this.propertyFilterState.showFilter" class="ks_dashboard_link ks_am_content_element ks_custom_date_filter ks_hide mr-4">
<div class="ks_date_filter_selection_input">
<div class="ks_date_selection_box">
<div class="btn-group ">