工作台
This commit is contained in:
@@ -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 ">
|
||||
|
||||
Reference in New Issue
Block a user