Browse Source

:Dec 15 [ADD] Bug Fixed 'dynamic_accounts_report'

18.0
Risvana Cybro 4 days ago
parent
commit
48d6d66f98
  1. 2
      dynamic_accounts_report/__manifest__.py
  2. 5
      dynamic_accounts_report/doc/RELEASE_NOTES.md
  3. 44
      dynamic_accounts_report/models/account_partner_ledger.py
  4. 499
      dynamic_accounts_report/static/src/js/partner_ledger.js
  5. 254
      dynamic_accounts_report/static/src/xml/partner_ledger_view.xml

2
dynamic_accounts_report/__manifest__.py

@ -21,7 +21,7 @@
################################################################################ ################################################################################
{ {
'name': 'Odoo18 Dynamic Accounting Reports', 'name': 'Odoo18 Dynamic Accounting Reports',
'version': '18.0.1.2.6', 'version': '18.0.1.3.0',
'category': 'Accounting', 'category': 'Accounting',
'summary': "Odoo 18 Accounting Financial Reports,Dynamic Accounting Reports, Dynamic Financial Reports,Dynamic Report Odoo18, Odoo18,Financial Reports, Odoo18 Accounting,Accounting, Odoo Apps", 'summary': "Odoo 18 Accounting Financial Reports,Dynamic Accounting Reports, Dynamic Financial Reports,Dynamic Report Odoo18, Odoo18,Financial Reports, Odoo18 Accounting,Accounting, Odoo Apps",
'description': "This module creates dynamic Accounting General Ledger, Trial" 'description': "This module creates dynamic Accounting General Ledger, Trial"

5
dynamic_accounts_report/doc/RELEASE_NOTES.md

@ -34,3 +34,8 @@
#### Version 18.0.1.2.6 #### Version 18.0.1.2.6
#### BUG FIX #### BUG FIX
- Removed the controller for xlsx report generation which was already present in the dependent module base_accounting_kit. - Removed the controller for xlsx report generation which was already present in the dependent module base_accounting_kit.
#### 12.12.2025
#### Version 18.0.1.3.0
#### UPDT
- Commit for adding partner, partner tag, and account filter in the Partner Ledger report.

44
dynamic_accounts_report/models/account_partner_ledger.py

@ -98,7 +98,7 @@ class AccountPartnerLedger(models.TransientModel):
return partner_dict return partner_dict
@api.model @api.model
def get_filter_values(self, partner_id, data_range, account, options): def get_filter_values(self, partner_id, data_range, account, options, tag_ids=None, account_ids=None):
""" """
Retrieve filtered partner-related data for generating a report. Retrieve filtered partner-related data for generating a report.
@ -140,6 +140,18 @@ class AccountPartnerLedger(models.TransientModel):
quarter_start, quarter_end = date_utils.get_quarter(today) quarter_start, quarter_end = date_utils.get_quarter(today)
previous_quarter_start = quarter_start - relativedelta(months=3) previous_quarter_start = quarter_start - relativedelta(months=3)
previous_quarter_end = quarter_start - relativedelta(days=1) previous_quarter_end = quarter_start - relativedelta(days=1)
# Handle partner tag filter
if tag_ids:
partners_with_tags = self.env['res.partner'].search([
('category_id', 'in', tag_ids)
])
if partner_id:
# Intersection of selected partners and partners with tags
partner_id = list(set(partner_id) & set(partners_with_tags.ids))
else:
partner_id = partners_with_tags.ids
if not partner_id: if not partner_id:
partner_id = self.env['account.move.line'].search([( partner_id = self.env['account.move.line'].search([(
'account_type', 'in', account_type_domain), 'account_type', 'in', account_type_domain),
@ -148,13 +160,24 @@ class AccountPartnerLedger(models.TransientModel):
balance_move_line_ids = [] balance_move_line_ids = []
for partners in partner_id: for partners in partner_id:
partner = self.env['res.partner'].browse(partners).name partner = self.env['res.partner'].browse(partners).name
# Base domain for move lines
base_domain = [
('partner_id', '=', partners),
('account_type', 'in', account_type_domain),
('parent_state', 'in', option_domain)
]
# NEW: Add account filter to base domain
if account_ids:
base_domain.append(('account_id', 'in', account_ids))
if data_range: if data_range:
if data_range == 'month': if data_range == 'month':
domain = base_domain + [
('date', '>=', fields.Date.today().replace(day=1)),
('date', '<=', fields.Date.today())
]
move_line_ids = self.env['account.move.line'].search( move_line_ids = self.env['account.move.line'].search(
[('partner_id', '=', partners), ( domain).filtered(
'account_type', 'in',
account_type_domain),
('parent_state', 'in', option_domain)]).filtered(
lambda x: x.date.month == fields.Date.today().month) lambda x: x.date.month == fields.Date.today().month)
date_start = fields.Date.today().replace(day=1) date_start = fields.Date.today().replace(day=1)
balance_move_line_ids = self.env[ balance_move_line_ids = self.env[
@ -164,6 +187,7 @@ class AccountPartnerLedger(models.TransientModel):
account_type_domain), account_type_domain),
('parent_state', 'in', option_domain), ('parent_state', 'in', option_domain),
('invoice_date', '<', date_start)]) ('invoice_date', '<', date_start)])
elif data_range == 'year': elif data_range == 'year':
move_line_ids = self.env['account.move.line'].search( move_line_ids = self.env['account.move.line'].search(
[('partner_id', '=', partners), ( [('partner_id', '=', partners), (
@ -299,11 +323,7 @@ class AccountPartnerLedger(models.TransientModel):
('parent_state', 'in', option_domain), ('parent_state', 'in', option_domain),
('invoice_date', '<', date_start)]) ('invoice_date', '<', date_start)])
else: else:
move_line_ids = self.env['account.move.line'].search( move_line_ids = self.env['account.move.line'].search(base_domain)
[('partner_id', '=', partners), (
'account_type', 'in',
account_type_domain),
('parent_state', 'in', option_domain)])
total_debit_balance = 0 total_debit_balance = 0
total_credit_balance = 0 total_credit_balance = 0
balance = 0 balance = 0
@ -498,4 +518,6 @@ class AccountPartnerLedger(models.TransientModel):
workbook.close() workbook.close()
output.seek(0) output.seek(0)
response.stream.write(output.read()) response.stream.write(output.read())
output.close() output.close()

499
dynamic_accounts_report/static/src/js/partner_ledger.js

@ -6,7 +6,6 @@ import { useRef, useState } from "@odoo/owl";
import { BlockUI } from "@web/core/ui/block_ui"; import { BlockUI } from "@web/core/ui/block_ui";
import { download } from "@web/core/network/download"; import { download } from "@web/core/network/download";
const actionRegistry = registry.category("actions"); const actionRegistry = registry.category("actions");
class PartnerLedger extends owl.Component { class PartnerLedger extends owl.Component {
setup() { setup() {
super.setup(...arguments); super.setup(...arguments);
@ -25,6 +24,8 @@ class PartnerLedger extends owl.Component {
filter_applied: null, filter_applied: null,
selected_partner: [], selected_partner: [],
selected_partner_rec: [], selected_partner_rec: [],
all_partners: [],
filtered_partners: [],
total_debit: null, total_debit: null,
total_debit_display:null, total_debit_display:null,
total_credit: null, total_credit: null,
@ -34,10 +35,281 @@ class PartnerLedger extends owl.Component {
account: null, account: null,
options: null, options: null,
message_list : [], message_list : [],
selected_tags: [],
selected_tag_ids: [],
all_tags: [],
filtered_tags: [],
selected_accounts: [],
selected_account_ids: [],
all_accounts: [],
filtered_accounts: [],
}); });
this.load_data(self.initial_render = true); this.load_data(self.initial_render = true);
this.loadPartners();
this.loadTags();
this.loadAccounts();
}
async loadPartners() {
/**
* Loads all available partners from the database
*/
try {
const partners = await this.orm.searchRead(
'res.partner',
[],
['id', 'name', 'display_name'],
{ limit: 100 }
);
this.state.all_partners = partners;
this.state.filtered_partners = partners;
} catch (error) {
console.error('Error loading partners:', error);
}
}
searchPartners(ev) {
/**
* Filters the partner list based on search input
* @param {Event} ev - The input event
*/
const searchTerm = ev.target.value.toLowerCase();
if (!searchTerm) {
this.state.filtered_partners = this.state.all_partners;
} else {
this.state.filtered_partners = this.state.all_partners.filter(partner => {
const name = (partner.display_name || partner.name || '').toLowerCase();
return name.includes(searchTerm);
});
}
}
selectPartner(ev) {
/**
* Adds a partner to the selected list
*/
ev.preventDefault();
ev.stopPropagation();
const partnerId = parseInt(ev.target.dataset.partnerId, 10);
if (this.state.selected_partner.includes(partnerId)) {
return;
}
const partner = this.state.all_partners.find(p => p.id === partnerId);
if (partner) {
this.state.selected_partner.push(partnerId);
this.state.selected_partner_rec.push(partner);
this.applyAllFilters();
}
}
removePartner(ev) {
/**
* Removes a partner from the selected list
*/
ev.preventDefault();
ev.stopPropagation();
const partnerId = parseInt(ev.target.dataset.partnerId, 10);
const partnerIndex = this.state.selected_partner.indexOf(partnerId);
if (partnerIndex > -1) {
this.state.selected_partner.splice(partnerIndex, 1);
this.state.selected_partner_rec.splice(partnerIndex, 1);
this.applyAllFilters();
}
}
clearAllPartners(ev) {
/**
* Clears all selected partners
*/
ev.preventDefault();
ev.stopPropagation();
this.state.selected_partner = [];
this.state.selected_partner_rec = [];
this.applyAllFilters();
}
// ==================== PARTNER TAGS METHODS ====================
async loadTags() {
/**
* Loads all partner tags (categories) from the database
*/
try {
const tags = await this.orm.searchRead(
'res.partner.category',
[],
['id', 'name'],
{ limit: 200, order: 'name' }
);
this.state.all_tags = tags;
this.state.filtered_tags = tags;
} catch (error) {
console.error('Error loading tags:', error);
}
}
searchTags(ev) {
/**
* Filters the tag list based on search input
*/
const searchTerm = ev.target.value.toLowerCase();
if (!searchTerm) {
this.state.filtered_tags = this.state.all_tags;
} else {
this.state.filtered_tags = this.state.all_tags.filter(tag => {
const name = (tag.name || '').toLowerCase();
return name.includes(searchTerm);
});
}
}
selectTag(ev) {
/**
* Adds a tag to the selected list
*/
ev.preventDefault();
ev.stopPropagation();
const tagId = parseInt(ev.target.dataset.tagId, 10);
if (this.state.selected_tag_ids.includes(tagId)) {
return;
}
const tag = this.state.all_tags.find(t => t.id === tagId);
if (tag) {
this.state.selected_tag_ids.push(tagId);
this.state.selected_tags.push(tag);
this.applyAllFilters();
}
}
removeTag(ev) {
/**
* Removes a tag from the selected list
*/
ev.preventDefault();
ev.stopPropagation();
const tagId = parseInt(ev.target.dataset.tagId, 10);
const tagIndex = this.state.selected_tag_ids.indexOf(tagId);
if (tagIndex > -1) {
this.state.selected_tag_ids.splice(tagIndex, 1);
this.state.selected_tags.splice(tagIndex, 1);
this.applyAllFilters();
}
}
clearAllTags(ev) {
/**
* Clears all selected tags
*/
ev.preventDefault();
ev.stopPropagation();
this.state.selected_tag_ids = [];
this.state.selected_tags = [];
this.applyAllFilters();
}
// ==================== ACCOUNTS METHODS ====================
async loadAccounts() {
/**
* Loads all accounts from the database
*/
try {
const accounts = await this.orm.searchRead(
'account.account',
[],
['id', 'code', 'name'],
{ limit: 500, order: 'code' }
);
this.state.all_accounts = accounts;
this.state.filtered_accounts = accounts;
} catch (error) {
console.error('Error loading accounts:', error);
}
}
searchAccounts(ev) {
/**
* Filters the account list based on search input
*/
const searchTerm = ev.target.value.toLowerCase();
if (!searchTerm) {
this.state.filtered_accounts = this.state.all_accounts;
} else {
this.state.filtered_accounts = this.state.all_accounts.filter(account => {
const code = (account.code || '').toLowerCase();
const name = (account.name || '').toLowerCase();
return code.includes(searchTerm) || name.includes(searchTerm);
});
}
}
selectAccount(ev) {
/**
* Adds an account to the selected list
*/
ev.preventDefault();
ev.stopPropagation();
const accountId = parseInt(ev.target.dataset.accountId, 10);
if (this.state.selected_account_ids.includes(accountId)) {
return;
}
const account = this.state.all_accounts.find(a => a.id === accountId);
if (account) {
this.state.selected_account_ids.push(accountId);
this.state.selected_accounts.push(account);
this.applyAllFilters();
}
}
removeAccount(ev) {
/**
* Removes an account from the selected list
*/
ev.preventDefault();
ev.stopPropagation();
const accountId = parseInt(ev.target.dataset.accountId, 10);
const accountIndex = this.state.selected_account_ids.indexOf(accountId);
if (accountIndex > -1) {
this.state.selected_account_ids.splice(accountIndex, 1);
this.state.selected_accounts.splice(accountIndex, 1);
this.applyAllFilters();
}
}
clearAllAccounts(ev) {
/**
* Clears all selected accounts
*/
ev.preventDefault();
ev.stopPropagation();
this.state.selected_account_ids = [];
this.state.selected_accounts = [];
this.applyAllFilters();
} }
formatNumberWithSeparators(number) { formatNumberWithSeparators(number) {
const parsedNumber = parseFloat(number); const parsedNumber = parseFloat(number);
if (isNaN(parsedNumber)) { if (isNaN(parsedNumber)) {
@ -274,14 +546,19 @@ class PartnerLedger extends owl.Component {
target: "current", target: "current",
}); });
} }
async applyFilter(val, ev, is_delete = false) { async applyFilter(val, ev, is_delete = false) {
/** /**
* Applies filters to the partner ledger report based on the provided values. * Applies filters to the partner ledger report based on the provided values.
* * This method handles date ranges, account types, and options filters.
* @param {any} val - The value of the filter. * Partner, tag, and account filters are handled by their respective methods.
* @param {Event} ev - The event object triggered by the action. *
* @param {boolean} is_delete - Indicates whether the filter value is being deleted. * @param {any} val - The value of the filter.
*/ * @param {Event} ev - The event object triggered by the action (can be null).
* @param {boolean} is_delete - Indicates whether the filter value is being deleted.
*/
// Handle date range and other button-based filters
let partner_list = [] let partner_list = []
let partner_value = [] let partner_value = []
let partner_totals = '' let partner_totals = ''
@ -292,109 +569,147 @@ class PartnerLedger extends owl.Component {
this.state.filter_applied = true; this.state.filter_applied = true;
let totalDebitSum = 0; let totalDebitSum = 0;
let totalCreditSum = 0; let totalCreditSum = 0;
if (ev) { if (val && val.target) {
if (ev.input && ev.input.attributes.placeholder.value == 'Partner' && !is_delete) { const target = val.target;
this.state.selected_partner.push(val[0].id)
this.state.selected_partner_rec.push(val[0]) if (target.name === 'start_date') {
} else if (is_delete) {
let index = this.state.selected_partner_rec.indexOf(val)
this.state.selected_partner_rec.splice(index, 1)
this.state.selected_partner = this.state.selected_partner_rec.map((rec) => rec.id)
}
}
else {
if (val.target.name === 'start_date') {
this.state.date_range = { this.state.date_range = {
...this.state.date_range, ...this.state.date_range,
start_date: val.target.value start_date: target.value
}; };
} else if (val.target.name === 'end_date') { } else if (target.name === 'end_date') {
this.state.date_range = { this.state.date_range = {
...this.state.date_range, ...this.state.date_range,
end_date: val.target.value end_date: target.value
}; };
} else if (val.target.attributes["data-value"].value == 'month') { } else if (target.attributes["data-value"]) {
this.state.date_range = val.target.attributes["data-value"].value const dataValue = target.attributes["data-value"].value;
} else if (val.target.attributes["data-value"].value == 'year') {
this.state.date_range = val.target.attributes["data-value"].value // Handle date range presets
} else if (val.target.attributes["data-value"].value == 'quarter') { if (['month', 'year', 'quarter', 'last-month', 'last-year', 'last-quarter'].includes(dataValue)) {
this.state.date_range = val.target.attributes["data-value"].value this.state.date_range = dataValue;
} else if (val.target.attributes["data-value"].value == 'last-month') {
this.state.date_range = val.target.attributes["data-value"].value
} else if (val.target.attributes["data-value"].value == 'last-year') {
this.state.date_range = val.target.attributes["data-value"].value
} else if (val.target.attributes["data-value"].value == 'last-quarter') {
this.state.date_range = val.target.attributes["data-value"].value
} else if (val.target.attributes["data-value"].value === 'receivable') {
// Check if the target has 'selected-filter' class
if (val.target.classList.contains("selected-filter")) {
// Remove 'receivable' key from account
const { Receivable, ...updatedAccount } = this.state.account;
this.state.account = updatedAccount;
val.target.classList.remove("selected-filter");
} else {
// Update receivable property in account
this.state.account = {
...this.state.account,
'Receivable': true
};
val.target.classList.add("selected-filter"); // Add class "selected-filter"
} }
} else if (val.target.attributes["data-value"].value === 'payable') { // Handle account types (Receivable/Payable)
// Check if the target has 'selected-filter' class else if (dataValue === 'receivable') {
if (val.target.classList.contains("selected-filter")) { if (target.classList.contains("selected-filter")) {
// Remove 'receivable' key from account const { Receivable, ...updatedAccount } = this.state.account || {};
const { Payable, ...updatedAccount } = this.state.account; this.state.account = updatedAccount;
this.state.account = updatedAccount; target.classList.remove("selected-filter");
val.target.classList.remove("selected-filter"); } else {
} else { this.state.account = {
// Update receivable property in account ...this.state.account,
this.state.account = { 'Receivable': true
...this.state.account, };
'Payable': true target.classList.add("selected-filter");
}; }
val.target.classList.add("selected-filter"); // Add class "selected-filter" } else if (dataValue === 'payable') {
if (target.classList.contains("selected-filter")) {
const { Payable, ...updatedAccount } = this.state.account || {};
this.state.account = updatedAccount;
target.classList.remove("selected-filter");
} else {
this.state.account = {
...this.state.account,
'Payable': true
};
target.classList.add("selected-filter");
}
} }
} else if (val.target.attributes["data-value"].value === 'draft') { // Handle options (Draft entries, etc.)
// Check if the target has 'selected-filter' class else if (dataValue === 'draft') {
if (val.target.classList.contains("selected-filter")) { if (target.classList.contains("selected-filter")) {
// Remove 'receivable' key from account const { draft, ...updatedOptions } = this.state.options || {};
const { draft, ...updatedAccount } = this.state.options; this.state.options = updatedOptions;
this.state.options = updatedAccount; target.classList.remove("selected-filter");
val.target.classList.remove("selected-filter"); } else {
} else { this.state.options = {
// Update receivable property in account ...this.state.options,
this.state.options = { 'draft': true
...this.state.options, };
'draft': true target.classList.add("selected-filter");
}; }
val.target.classList.add("selected-filter"); // Add class "selected-filter"
} }
} }
// Apply all filters after any change
await this.applyAllFilters();
} }
let filtered_data = await this.orm.call("account.partner.ledger", "get_filter_values", [this.state.selected_partner, this.state.date_range, this.state.account, this.state.options,]); }
async applyAllFilters() {
/**
* Applies all selected filters (partners, tags, accounts, date range, etc.)
*/
this.state.partners = null;
this.state.data = null;
this.state.total = null;
this.state.filter_applied = true;
let totalDebitSum = 0;
let totalCreditSum = 0;
let partner_list = [];
let partner_totals = '';
try {
// Call backend with all filter parameters
let filtered_data = await this.orm.call(
"account.partner.ledger",
"get_filter_values",
[
this.state.selected_partner, // partner IDs
this.state.date_range, // date range
this.state.account, // account type (receivable/payable)
this.state.options, // options (draft entries, etc.)
this.state.selected_tag_ids, // partner tag IDs
this.state.selected_account_ids // account IDs
]
);
// Process filtered data
for (let index in filtered_data) { for (let index in filtered_data) {
const value = filtered_data[index]; const value = filtered_data[index];
if (index !== 'partner_totals') { if (index !== 'partner_totals') {
partner_list.push(index) partner_list.push(index);
} } else {
else { partner_totals = value;
partner_totals = value Object.values(partner_totals).forEach(partner_data => {
Object.values(partner_totals).forEach(partner_list => { totalDebitSum += partner_data.total_debit || 0;
totalDebitSum += partner_list.total_debit || 0; totalCreditSum += partner_data.total_credit || 0;
totalCreditSum += partner_list.total_credit || 0; partner_data.total_debit_display = this.formatNumberWithSeparators(partner_data.total_debit || 0);
}); partner_data.total_credit_display = this.formatNumberWithSeparators(partner_data.total_credit || 0);
});
} }
} }
this.state.partners = partner_list
this.state.data = filtered_data // Format entries
this.state.total = partner_totals Object.entries(filtered_data).forEach(([key, value]) => {
this.state.total_debit = totalDebitSum if (key !== 'partner_totals') {
this.state.total_credit = totalCreditSum value.forEach(entry => {
if (this.unfoldButton.el.classList.contains("selected-filter")) { entry[0].debit_display = this.formatNumberWithSeparators(entry[0].debit || 0);
entry[0].credit_display = this.formatNumberWithSeparators(entry[0].credit || 0);
entry[0].amount_currency_display = this.formatNumberWithSeparators(entry[0].amount_currency || 0);
});
}
});
// Update state
this.state.partners = partner_list;
this.state.data = filtered_data;
this.state.total = partner_totals;
this.state.total_debit = totalDebitSum;
this.state.total_debit_display = this.formatNumberWithSeparators(totalDebitSum);
this.state.total_credit = totalCreditSum;
this.state.total_credit_display = this.formatNumberWithSeparators(totalCreditSum);
if (this.unfoldButton.el && this.unfoldButton.el.classList.contains("selected-filter")) {
this.unfoldButton.el.classList.remove("selected-filter"); this.unfoldButton.el.classList.remove("selected-filter");
} }
} catch (error) {
console.error('Error applying filters:', error);
} }
}
getDomain() { getDomain() {
return []; return [];
} }

254
dynamic_accounts_report/static/src/xml/partner_ledger_view.xml

@ -138,7 +138,7 @@
<span class="fa fa-user" <span class="fa fa-user"
title="Accounts" title="Accounts"
role="img" aria-label="Dates"/> role="img" aria-label="Dates"/>
Account Account Type
<t t-if="state.account">: <t t-if="state.account">:
<t t-foreach="state.account" <t t-foreach="state.account"
t-as="account_key" t-as="account_key"
@ -170,6 +170,252 @@
</div> </div>
</div> </div>
</div> </div>
<!--Partner filter-->
<div class="px-2">
<div class="partner_filter" style="">
<a type="button" class="dropdown-toggle"
data-bs-toggle="dropdown">
<span class="fa fa-users"
title="Partners"
role="img" aria-label="Partners"/>
Partners
<t t-if="state.selected_partner_rec and state.selected_partner_rec.length > 0">
:
<t t-foreach="state.selected_partner_rec" t-as="partner_rec"
t-key="partner_rec_index">
<t t-esc="partner_rec.display_name || partner_rec.name"/>
<t t-if="partner_rec_index != state.selected_partner_rec.length - 1">,
</t>
</t>
</t>
</a>
<div class="dropdown-menu partner-dropdown" role="menu"
style="width: 300px; max-height: 350px; overflow-y: auto; padding: 8px;;">
<div class="list-group">
<!-- Search Input -->
<div class="mb-2">
<input type="text"
class="form-control form-control-sm"
placeholder="Search partners..."
t-ref="partnerSearch"
t-on-input="searchPartners"
style="border: 1px solid #ccc; padding: 5px; font-size: 13px;"/>
</div>
<!-- &lt;!&ndash; Selected Partners Display &ndash;&gt;-->
<t t-if="state.selected_partner_rec and state.selected_partner_rec.length > 0">
<div class="mb-2" style="max-height: 80px; overflow-y: auto;">
<small class="text-muted">
<strong>Selected:</strong>
</small>
<div class="mt-1">
<t t-foreach="state.selected_partner_rec" t-as="selected"
t-key="selected_index">
<span class="badge bg-primary me-1 mb-1"
style="display: inline-flex; align-items: center; font-size: 11px;">
<t t-esc="selected.display_name || selected.name"/>
<i class="fa fa-times ms-1"
style="cursor: pointer; font-size: 10px;"
t-att-data-partner-id="selected.id"
t-on-click="removePartner"/>
</span>
</t>
</div>
</div>
<div role="separator" class="dropdown-divider"/>
</t>
<!-- Partner List -->
<div class="partner-list" style="max-height: 180px; overflow-y: auto;">
<t t-if="state.filtered_partners and state.filtered_partners.length > 0">
<t t-foreach="state.filtered_partners" t-as="partner"
t-key="partner_index">
<button class="dropdown-item partner-item"
type="button"
t-att-data-partner-id="partner.id"
t-att-data-partner-name="partner.display_name || partner.name"
t-on-click="selectPartner"
style="width: 100%; text-align: left; padding: 6px 10px; font-size: 13px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
<t t-esc="partner.display_name || partner.name"/>
</button>
</t>
</t>
<t t-else="">
<div class="text-muted p-2" style="font-size: 12px;">No partners
found
</div>
</t>
</div>
<t t-if="state.selected_partner_rec and state.selected_partner_rec.length > 0">
<div role="separator" class="dropdown-divider"/>
<button class="btn btn-sm btn-danger w-100 mt-2"
type="button"
t-on-click="clearAllPartners"
style="font-size: 12px; padding: 4px;">
Clear All
</button>
</t>
</div>
</div>
</div>
</div>
<!-- Partner Tags Filter - Fixed Size -->
<div class="px-2">
<div class="category_filter">
<a type="button" class="dropdown-toggle" data-bs-toggle="dropdown">
<span class="fa fa-filter" title="Partner Tags" role="img" aria-label="Partner Tags"/>
Partner Tags
<t t-if="state.selected_tags and state.selected_tags.length > 0">
(<t t-esc="state.selected_tags.length"/>)
</t>
</a>
<div class="dropdown-menu category-dropdown" role="menu"
style="width: 280px; max-height: 350px; overflow-y: auto; padding: 8px;">
<div class="list-group">
<!-- Search Input -->
<div class="mb-2">
<input type="text"
class="form-control form-control-sm"
placeholder="Search tags..."
t-ref="tagSearch"
t-on-input="searchTags"
style="border: 1px solid #ccc; padding: 5px; font-size: 13px;"/>
</div>
<!-- Selected Tags Display -->
<t t-if="state.selected_tags and state.selected_tags.length > 0">
<div class="mb-2" style="max-height: 80px; overflow-y: auto;">
<small class="text-muted"><strong>Selected:</strong></small>
<div class="mt-1">
<t t-foreach="state.selected_tags" t-as="tag" t-key="tag_index">
<span class="badge bg-info me-1 mb-1"
style="display: inline-flex; align-items: center; font-size: 11px;">
<t t-esc="tag.name"/>
<i class="fa fa-times ms-1"
style="cursor: pointer; font-size: 10px;"
t-att-data-tag-id="tag.id"
t-on-click="removeTag"/>
</span>
</t>
</div>
</div>
<div role="separator" class="dropdown-divider"/>
</t>
<!-- Tags List -->
<div class="tag-list" style="max-height: 180px; overflow-y: auto;">
<t t-if="state.filtered_tags and state.filtered_tags.length > 0">
<t t-foreach="state.filtered_tags" t-as="tag" t-key="tag_index">
<button class="dropdown-item tag-item"
type="button"
t-att-data-tag-id="tag.id"
t-att-data-tag-name="tag.name"
t-on-click="selectTag"
style="width: 100%; text-align: left; padding: 6px 10px; font-size: 13px;">
<t t-esc="tag.name"/>
</button>
</t>
</t>
<t t-else="">
<div class="text-muted p-2" style="font-size: 12px;">No tags found</div>
</t>
</div>
<!-- Clear All Button -->
<t t-if="state.selected_tags and state.selected_tags.length > 0">
<div role="separator" class="dropdown-divider"/>
<button class="btn btn-sm btn-danger w-100 mt-2"
type="button"
t-on-click="clearAllTags"
style="font-size: 12px; padding: 4px;">
Clear All
</button>
</t>
</div>
</div>
</div>
</div>
<!-- Accounts Filter - Fixed Size -->
<div class="px-2">
<div class="acc_filter">
<a type="button" class="dropdown-toggle" data-bs-toggle="dropdown">
<span class="fa fa-book" title="Accounts" role="img" aria-label="Accounts"/>
Accounts
<t t-if="state.selected_accounts and state.selected_accounts.length > 0">
(<t t-esc="state.selected_accounts.length"/>)
</t>
</a>
<div class="dropdown-menu account-dropdown" role="menu"
style="width: 320px; max-height: 350px; overflow-y: auto; padding: 8px;">
<div class="list-group">
<!-- Search Input -->
<div class="mb-2">
<input type="text"
class="form-control form-control-sm"
placeholder="Search accounts..."
t-ref="accountSearch"
t-on-input="searchAccounts"
style="border: 1px solid #ccc; padding: 5px; font-size: 13px;"/>
</div>
<!-- Selected Accounts Display -->
<t t-if="state.selected_accounts and state.selected_accounts.length > 0">
<div class="mb-2" style="max-height: 80px; overflow-y: auto;">
<small class="text-muted"><strong>Selected:</strong></small>
<div class="mt-1">
<t t-foreach="state.selected_accounts" t-as="acc" t-key="acc_index">
<span class="badge bg-success me-1 mb-1"
style="display: inline-flex; align-items: center; font-size: 11px;">
<t t-esc="acc.code"/> - <t t-esc="acc.name"/>
<i class="fa fa-times ms-1"
style="cursor: pointer; font-size: 10px;"
t-att-data-account-id="acc.id"
t-on-click="removeAccount"/>
</span>
</t>
</div>
</div>
<div role="separator" class="dropdown-divider"/>
</t>
<!-- Accounts List -->
<div class="account-list" style="max-height: 180px; overflow-y: auto;">
<t t-if="state.filtered_accounts and state.filtered_accounts.length > 0">
<t t-foreach="state.filtered_accounts" t-as="acc" t-key="acc_index">
<button class="dropdown-item account-item"
type="button"
t-att-data-account-id="acc.id"
t-att-data-account-code="acc.code"
t-att-data-account-name="acc.name"
t-on-click="selectAccount"
style="width: 100%; text-align: left; padding: 6px 10px; font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
<strong><t t-esc="acc.code"/></strong> - <t t-esc="acc.name"/>
</button>
</t>
</t>
<t t-else="">
<div class="text-muted p-2" style="font-size: 12px;">No accounts found</div>
</t>
</div>
<!-- Clear All Button -->
<t t-if="state.selected_accounts and state.selected_accounts.length > 0">
<div role="separator" class="dropdown-divider"/>
<button class="btn btn-sm btn-danger w-100 mt-2"
type="button"
t-on-click="clearAllAccounts"
style="font-size: 12px; padding: 4px;">
Clear All
</button>
</t>
</div>
</div>
</div>
</div>
<!-- Options Dropdown --> <!-- Options Dropdown -->
<div class="px-2"> <div class="px-2">
<div class="option" style=""> <div class="option" style="">
@ -236,9 +482,10 @@
t-as="partner" t-as="partner"
t-key="partner_index"> t-key="partner_index">
<t t-set="i" t-value="i + 1"/> <t t-set="i" t-value="i + 1"/>
<tr class="border-bottom border-dark border-gainsboro"> <t t-if="state.total[partner]['total_debit']">
<tr class="border-bottom border-dark border-gainsboro">
<th> <th>
<div data-bs-toggle="collapse" <div data-bs-toggle="collapse"
t-attf-href="#partner-{{i}}" t-attf-href="#partner-{{i}}"
aria-expanded="false" aria-expanded="false"
t-attf-aria-controls="partner-{{i}}" t-attf-aria-controls="partner-{{i}}"
@ -303,6 +550,7 @@
</span> </span>
</th> </th>
</tr> </tr>
</t>
<!-- Iterate over partner's initial balance --> <!-- Iterate over partner's initial balance -->
<t t-set="j" t-value="0"/> <t t-set="j" t-value="0"/>
<t t-foreach="state.partners" <t t-foreach="state.partners"

Loading…
Cancel
Save