diff --git a/dynamic_accounts_report/__manifest__.py b/dynamic_accounts_report/__manifest__.py index d39edd231..f292e45a1 100644 --- a/dynamic_accounts_report/__manifest__.py +++ b/dynamic_accounts_report/__manifest__.py @@ -21,7 +21,7 @@ ################################################################################ { 'name': 'Odoo18 Dynamic Accounting Reports', - 'version': '18.0.1.2.6', + 'version': '18.0.1.3.0', '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", 'description': "This module creates dynamic Accounting General Ledger, Trial" diff --git a/dynamic_accounts_report/doc/RELEASE_NOTES.md b/dynamic_accounts_report/doc/RELEASE_NOTES.md index a7edf3c99..2c07ae52f 100644 --- a/dynamic_accounts_report/doc/RELEASE_NOTES.md +++ b/dynamic_accounts_report/doc/RELEASE_NOTES.md @@ -34,3 +34,8 @@ #### Version 18.0.1.2.6 #### BUG FIX - 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. diff --git a/dynamic_accounts_report/models/account_partner_ledger.py b/dynamic_accounts_report/models/account_partner_ledger.py index e7276cd6e..bea78f478 100644 --- a/dynamic_accounts_report/models/account_partner_ledger.py +++ b/dynamic_accounts_report/models/account_partner_ledger.py @@ -98,7 +98,7 @@ class AccountPartnerLedger(models.TransientModel): return partner_dict @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. @@ -140,6 +140,18 @@ class AccountPartnerLedger(models.TransientModel): quarter_start, quarter_end = date_utils.get_quarter(today) previous_quarter_start = quarter_start - relativedelta(months=3) 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: partner_id = self.env['account.move.line'].search([( 'account_type', 'in', account_type_domain), @@ -148,13 +160,24 @@ class AccountPartnerLedger(models.TransientModel): balance_move_line_ids = [] for partners in partner_id: 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 == 'month': + domain = base_domain + [ + ('date', '>=', fields.Date.today().replace(day=1)), + ('date', '<=', fields.Date.today()) + ] move_line_ids = self.env['account.move.line'].search( - [('partner_id', '=', partners), ( - 'account_type', 'in', - account_type_domain), - ('parent_state', 'in', option_domain)]).filtered( + domain).filtered( lambda x: x.date.month == fields.Date.today().month) date_start = fields.Date.today().replace(day=1) balance_move_line_ids = self.env[ @@ -164,6 +187,7 @@ class AccountPartnerLedger(models.TransientModel): account_type_domain), ('parent_state', 'in', option_domain), ('invoice_date', '<', date_start)]) + elif data_range == 'year': move_line_ids = self.env['account.move.line'].search( [('partner_id', '=', partners), ( @@ -299,11 +323,7 @@ class AccountPartnerLedger(models.TransientModel): ('parent_state', 'in', option_domain), ('invoice_date', '<', date_start)]) else: - move_line_ids = self.env['account.move.line'].search( - [('partner_id', '=', partners), ( - 'account_type', 'in', - account_type_domain), - ('parent_state', 'in', option_domain)]) + move_line_ids = self.env['account.move.line'].search(base_domain) total_debit_balance = 0 total_credit_balance = 0 balance = 0 @@ -498,4 +518,6 @@ class AccountPartnerLedger(models.TransientModel): workbook.close() output.seek(0) response.stream.write(output.read()) - output.close() \ No newline at end of file + output.close() + + diff --git a/dynamic_accounts_report/static/src/js/partner_ledger.js b/dynamic_accounts_report/static/src/js/partner_ledger.js index 1a4e2583f..906079805 100644 --- a/dynamic_accounts_report/static/src/js/partner_ledger.js +++ b/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 { download } from "@web/core/network/download"; const actionRegistry = registry.category("actions"); - class PartnerLedger extends owl.Component { setup() { super.setup(...arguments); @@ -25,6 +24,8 @@ class PartnerLedger extends owl.Component { filter_applied: null, selected_partner: [], selected_partner_rec: [], + all_partners: [], + filtered_partners: [], total_debit: null, total_debit_display:null, total_credit: null, @@ -34,10 +35,281 @@ class PartnerLedger extends owl.Component { account: null, options: null, 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.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) { const parsedNumber = parseFloat(number); if (isNaN(parsedNumber)) { @@ -274,14 +546,19 @@ class PartnerLedger extends owl.Component { target: "current", }); } + async applyFilter(val, ev, is_delete = false) { - /** - * Applies filters to the partner ledger report based on the provided values. - * - * @param {any} val - The value of the filter. - * @param {Event} ev - The event object triggered by the action. - * @param {boolean} is_delete - Indicates whether the filter value is being deleted. - */ + /** + * Applies filters to the partner ledger report based on the provided values. + * This method handles date ranges, account types, and options filters. + * Partner, tag, and account filters are handled by their respective methods. + * + * @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_value = [] let partner_totals = '' @@ -292,109 +569,147 @@ class PartnerLedger extends owl.Component { this.state.filter_applied = true; let totalDebitSum = 0; let totalCreditSum = 0; - if (ev) { - if (ev.input && ev.input.attributes.placeholder.value == 'Partner' && !is_delete) { - this.state.selected_partner.push(val[0].id) - this.state.selected_partner_rec.push(val[0]) - } 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') { + if (val && val.target) { + const target = val.target; + + if (target.name === 'start_date') { 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, - end_date: val.target.value + end_date: target.value }; - } else if (val.target.attributes["data-value"].value == 'month') { - this.state.date_range = val.target.attributes["data-value"].value - } else if (val.target.attributes["data-value"].value == 'year') { - this.state.date_range = val.target.attributes["data-value"].value - } else if (val.target.attributes["data-value"].value == 'quarter') { - this.state.date_range = val.target.attributes["data-value"].value - } 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 (target.attributes["data-value"]) { + const dataValue = target.attributes["data-value"].value; + + // Handle date range presets + if (['month', 'year', 'quarter', 'last-month', 'last-year', 'last-quarter'].includes(dataValue)) { + this.state.date_range = dataValue; } - } else if (val.target.attributes["data-value"].value === 'payable') { - // Check if the target has 'selected-filter' class - if (val.target.classList.contains("selected-filter")) { - // Remove 'receivable' key from account - const { Payable, ...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, - 'Payable': true - }; - val.target.classList.add("selected-filter"); // Add class "selected-filter" + // Handle account types (Receivable/Payable) + else if (dataValue === 'receivable') { + if (target.classList.contains("selected-filter")) { + const { Receivable, ...updatedAccount } = this.state.account || {}; + this.state.account = updatedAccount; + target.classList.remove("selected-filter"); + } else { + this.state.account = { + ...this.state.account, + 'Receivable': true + }; + target.classList.add("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') { - // Check if the target has 'selected-filter' class - if (val.target.classList.contains("selected-filter")) { - // Remove 'receivable' key from account - const { draft, ...updatedAccount } = this.state.options; - this.state.options = updatedAccount; - val.target.classList.remove("selected-filter"); - } else { - // Update receivable property in account - this.state.options = { - ...this.state.options, - 'draft': true - }; - val.target.classList.add("selected-filter"); // Add class "selected-filter" + // Handle options (Draft entries, etc.) + else if (dataValue === 'draft') { + if (target.classList.contains("selected-filter")) { + const { draft, ...updatedOptions } = this.state.options || {}; + this.state.options = updatedOptions; + target.classList.remove("selected-filter"); + } else { + this.state.options = { + ...this.state.options, + 'draft': true + }; + target.classList.add("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) { const value = filtered_data[index]; if (index !== 'partner_totals') { - partner_list.push(index) - } - else { - partner_totals = value - Object.values(partner_totals).forEach(partner_list => { - totalDebitSum += partner_list.total_debit || 0; - totalCreditSum += partner_list.total_credit || 0; - }); + partner_list.push(index); + } else { + partner_totals = value; + Object.values(partner_totals).forEach(partner_data => { + totalDebitSum += partner_data.total_debit || 0; + totalCreditSum += partner_data.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 - this.state.total = partner_totals - this.state.total_debit = totalDebitSum - this.state.total_credit = totalCreditSum - if (this.unfoldButton.el.classList.contains("selected-filter")) { + + // Format entries + Object.entries(filtered_data).forEach(([key, value]) => { + if (key !== 'partner_totals') { + value.forEach(entry => { + 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"); } + } catch (error) { + console.error('Error applying filters:', error); } + } + + + getDomain() { return []; } diff --git a/dynamic_accounts_report/static/src/xml/partner_ledger_view.xml b/dynamic_accounts_report/static/src/xml/partner_ledger_view.xml index 2ff785a5f..9fe2c8d2d 100644 --- a/dynamic_accounts_report/static/src/xml/partner_ledger_view.xml +++ b/dynamic_accounts_report/static/src/xml/partner_ledger_view.xml @@ -138,7 +138,7 @@ - Account + Account Type : + + +
+
+ + + Partners + + : + + + , + + + + + + + +
+
+ + + Partner Tags + + () + + + + + +
+
+ + + Accounts + + () + + + +
@@ -236,9 +482,10 @@ t-as="partner" t-key="partner_index"> - + + -