diff --git a/taxes_az/taxes_az/doctype/vat_allocation/vat_allocation.js b/taxes_az/taxes_az/doctype/vat_allocation/vat_allocation.js index 690099e..35dc667 100644 --- a/taxes_az/taxes_az/doctype/vat_allocation/vat_allocation.js +++ b/taxes_az/taxes_az/doctype/vat_allocation/vat_allocation.js @@ -14,6 +14,10 @@ frappe.ui.form.on('VAT allocation', { // Setup filter for Journal Entry setup_journal_entry_query(frm); + + // Setup filter for Payment Entry + setup_payment_entry_query(frm); + } }); @@ -161,11 +165,11 @@ function show_sales_invoice_filters(frm) { }, 100); } -// Function to fill Payment Entries with filters +// Function to fill Payment Entries with filters - UPDATED to include previous months function fill_payment_entries(frm, filters) { - // Call server method + // Call server method to get both current and previous payment entries frappe.call({ - method: 'taxes_az.taxes_az.doctype.vat_allocation.vat_allocation.get_payment_entries', + method: 'taxes_az.taxes_az.doctype.vat_allocation.vat_allocation.get_payment_entries_with_previous', args: { year: frm.doc.year, month: frm.doc.month, @@ -184,8 +188,14 @@ function fill_payment_entries(frm, filters) { row.customer = payment.customer; row.payment_entry = payment.payment_entry; row.payment_entry_amount = payment.payment_entry_amount; - row.vat_free_amount = payment.payment_entry_amount; // Default: all amount is VAT free - row.unallocated_amount = payment.payment_entry_amount; + row.vat_journal_entry = payment.vat_journal_entry || ''; + row.vat_amount = payment.vat_amount || 0; + row.vat_allocated_amount = payment.vat_allocated_amount || 0; + row.vat_free_amount = payment.vat_free_amount || payment.payment_entry_amount; + row.unallocated_amount = payment.unallocated_amount || 0; + row.allocated_amount = payment.allocated_amount || 0; + row.vat_allocated_allocated = payment.vat_allocated_allocated || 0; + row.vat_free_allocated = payment.vat_free_allocated || 0; }); frm.refresh_field('payment_entry_table'); @@ -197,9 +207,22 @@ function fill_payment_entries(frm, filters) { }); } -// Function to fill Sales Invoice Items with filters +// Setup query filter for Payment Entry - UPDATED +function setup_payment_entry_query(frm) { + frm.fields_dict.payment_entry_table.grid.get_field('payment_entry').get_query = function(doc, cdt, cdn) { + return { + query: 'taxes_az.taxes_az.doctype.vat_allocation.vat_allocation.get_payment_entry_query', + filters: { + year: doc.year, + month: doc.month + } + }; + }; +} + +// Function to fill Sales Invoice Items with filters - UPDATED to fill both tables function fill_sales_invoice_items(frm, filters) { - // Call server method + // Call both server methods - current month and previous months frappe.call({ method: 'taxes_az.taxes_az.doctype.vat_allocation.vat_allocation.get_sales_invoice_items', args: { @@ -229,6 +252,52 @@ function fill_sales_invoice_items(frm, filters) { } else { frappe.msgprint(__('No Sales Invoice Items found for selected period')); } + + // Now load previous months items + load_previous_months_items(frm, filters); + } + }); +} + +// Function to load previous months items +function load_previous_months_items(frm, filters) { + frappe.call({ + method: 'taxes_az.taxes_az.doctype.vat_allocation.vat_allocation.get_sales_invoice_items_previous_months', + args: { + year: frm.doc.year, + month: frm.doc.month, + customers: filters.customers ? JSON.stringify(filters.customers) : null, + min_amount: filters.min_amount || null, + max_amount: filters.max_amount || null + }, + callback: function(r) { + // Clear existing table + frm.clear_table('sales_invoice_previous_table'); + + if (r.message && r.message.length > 0) { + // Add new rows + r.message.forEach(function(item) { + let row = frm.add_child('sales_invoice_previous_table'); + row.customer = item.customer; + row.item_tax_template = item.item_tax_template; + row.tax_article = item.tax_article; + row.amount = item.amount; + row.unallocated_amount = item.unallocated_amount; + row.allocated_amount = item.allocated_amount; + }); + + frm.refresh_field('sales_invoice_previous_table'); + frappe.show_alert({ + message: __('Previous months items loaded: {0} records', [r.message.length]), + indicator: 'blue' + }); + } else { + frm.refresh_field('sales_invoice_previous_table'); + frappe.show_alert({ + message: __('No unpaid items from previous months'), + indicator: 'blue' + }); + } } }); } @@ -279,6 +348,7 @@ frappe.ui.form.on('VAT allocation payment entry', { recalculate_totals_manually(frm); frm.refresh_field('payment_entry_table'); frm.refresh_field('sales_invoice_table'); + frm.refresh_field('sales_invoice_previous_table'); }, // При изменении Payment Entry - обновить суммы @@ -305,6 +375,7 @@ frappe.ui.form.on('VAT allocation payment entry', { recalculate_totals_manually(frm); frm.refresh_field('payment_entry_table'); frm.refresh_field('sales_invoice_table'); + frm.refresh_field('sales_invoice_previous_table'); return; } @@ -365,6 +436,7 @@ frappe.ui.form.on('VAT allocation payment entry', { recalculate_totals_manually(frm); frm.refresh_field('payment_entry_table'); frm.refresh_field('sales_invoice_table'); + frm.refresh_field('sales_invoice_previous_table'); } } }); @@ -389,6 +461,7 @@ frappe.ui.form.on('VAT allocation payment entry', { recalculate_totals_manually(frm); frm.refresh_field('payment_entry_table'); frm.refresh_field('sales_invoice_table'); + frm.refresh_field('sales_invoice_previous_table'); }, vat_journal_entry: function(frm, cdt, cdn) { @@ -501,7 +574,10 @@ function show_allocation_dialog(frm, payment_row_idx, customer) { // Получаем sales invoice items для этого клиента из формы let vat_free_items = []; let vat_items = []; + let vat_free_items_previous = []; + let vat_items_previous = []; + // Current month items for (let item of frm.doc.sales_invoice_table || []) { if (item.customer !== customer) { continue; @@ -529,7 +605,6 @@ function show_allocation_dialog(frm, payment_row_idx, customer) { let template = (item.item_tax_template || '').toLowerCase(); - // ИСПРАВЛЕНО: используем .includes() вместо 'in' if (template.includes('ədv 0%') || template.includes('ədv-dən azadolma')) { vat_free_items.push(item_data); } else if (template.includes('ədv 18%') || template.includes('ədv daxil 18%')) { @@ -537,8 +612,44 @@ function show_allocation_dialog(frm, payment_row_idx, customer) { } } + // Previous months items + for (let item of frm.doc.sales_invoice_previous_table || []) { + if (item.customer !== customer) { + continue; + } + + // Получаем существующие аллокации для этого item от текущего payment row + let current_allocation = 0; + for (let link of frm.doc.allocation_links || []) { + if (link.payment_entry_row === payment_row.name && + link.sales_invoice_row === item.name) { + current_allocation = link.allocated_amount || 0; + break; + } + } + + let item_data = { + name: item.name, + item_tax_template: item.item_tax_template || '', + tax_article: item.tax_article || '', + amount: item.amount || 0, + unallocated_amount: item.unallocated_amount || 0, + allocated_amount: item.allocated_amount || 0, + current_allocation: current_allocation + }; + + let template = (item.item_tax_template || '').toLowerCase(); + + if (template.includes('ədv 0%') || template.includes('ədv-dən azadolma')) { + vat_free_items_previous.push(item_data); + } else if (template.includes('ədv 18%') || template.includes('ədv daxil 18%')) { + vat_items_previous.push(item_data); + } + } + // Check if there are items to allocate to - if (vat_free_items.length === 0 && vat_items.length === 0) { + if (vat_free_items.length === 0 && vat_items.length === 0 && + vat_free_items_previous.length === 0 && vat_items_previous.length === 0) { frappe.msgprint(__('No sales invoice items available for this customer')); return; } @@ -554,19 +665,22 @@ function show_allocation_dialog(frm, payment_row_idx, customer) { vat_free_available: vat_free_amount - vat_free_allocated, vat_allocated_available: vat_allocated_amount - vat_allocated_allocated, vat_free_items: vat_free_items, - vat_items: vat_items + vat_items: vat_items, + vat_free_items_previous: vat_free_items_previous, + vat_items_previous: vat_items_previous }; // Create allocation dialog - create_allocation_dialog(frm, payment_row_idx, data, vat_free_items, vat_items); + create_allocation_dialog(frm, payment_row_idx, data); } -function create_allocation_dialog(frm, payment_row_idx, data, vat_free_items, vat_items) { - let allocation_data = {}; +function create_allocation_dialog(frm, payment_row_idx, data) { + let allocation_data_current = {}; + let allocation_data_previous = {}; let dialog = new frappe.ui.Dialog({ title: __('Allocate Amount'), - size: 'large', + size: 'extra-large', fields: [ { fieldname: 'info_section', @@ -601,22 +715,39 @@ function create_allocation_dialog(frm, payment_row_idx, data, vat_free_items, va default: data.vat_allocated_available }, { - fieldname: 'items_section', - fieldtype: 'Section Break', - label: __('Sales Invoice Items - Edit Allocations') + fieldname: 'tabs_section', + fieldtype: 'Section Break' }, { - fieldname: 'allocation_html', + fieldname: 'tabs_html', fieldtype: 'HTML' } ], primary_action_label: __('Save Allocation'), primary_action: function() { - save_allocation_data(frm, payment_row_idx, allocation_data, dialog); + save_allocation_data(frm, payment_row_idx, allocation_data_current, allocation_data_previous, dialog); }, secondary_action_label: __('Auto Allocate'), secondary_action: function() { - auto_allocate(frm, payment_row_idx, data, vat_free_items, vat_items, allocation_data, dialog); + // Determine which tab is active + let active_tab = dialog.$wrapper.find('.nav-tabs .nav-link.active').data('tab'); + + // Calculate remaining available amounts + let available_amounts = calculate_available_amounts( + data, + allocation_data_current, + allocation_data_previous + ); + + if (active_tab === 'current') { + auto_allocate(frm, payment_row_idx, data, data.vat_free_items, data.vat_items, + allocation_data_current, dialog, 'current', available_amounts, + allocation_data_previous); + } else { + auto_allocate(frm, payment_row_idx, data, data.vat_free_items_previous, data.vat_items_previous, + allocation_data_previous, dialog, 'previous', available_amounts, + allocation_data_current); + } } }); @@ -637,17 +768,121 @@ function create_allocation_dialog(frm, payment_row_idx, data, vat_free_items, va `; dialog.fields_dict.payment_info_html.$wrapper.html(info_html); - // Build allocation HTML - let html = build_allocation_html(vat_free_items, vat_items, allocation_data); - dialog.fields_dict.allocation_html.$wrapper.html(html); + // Build tabs HTML with Bootstrap + let html_current = build_allocation_html(data.vat_free_items, data.vat_items, allocation_data_current, 'current'); + let html_previous = build_allocation_html(data.vat_free_items_previous, data.vat_items_previous, allocation_data_previous, 'previous'); - // Setup input handlers - setup_allocation_inputs(dialog, allocation_data, data.vat_free_available, data.vat_allocated_available); + let tabs_html = ` + + +
+
+ ${html_current} +
+
+ ${html_previous} +
+
+ `; + + dialog.fields_dict.tabs_html.$wrapper.html(tabs_html); + + // Setup custom tab switching (without Bootstrap's data-toggle) + dialog.$wrapper.find('.nav-link').on('click', function(e) { + e.preventDefault(); + e.stopPropagation(); + + let $this = $(this); + let target = $this.attr('href'); + + // Update nav links + dialog.$wrapper.find('.nav-link').removeClass('active'); + $this.addClass('active'); + + // Update tab panes + dialog.$wrapper.find('.tab-pane').removeClass('show active'); + dialog.$wrapper.find(target).addClass('show active'); + }); + + // Setup input handlers for both tabs + setup_allocation_inputs(dialog, allocation_data_current, data.vat_free_available, data.vat_allocated_available, 'current'); + setup_allocation_inputs(dialog, allocation_data_previous, data.vat_free_available, data.vat_allocated_available, 'previous'); dialog.show(); } -function build_allocation_html(vat_free_items, vat_items, allocation_data) { +function calculate_available_amounts(data, allocation_data_current, allocation_data_previous) { + let total_vat_free_current = 0; + let total_vat_allocated_current = 0; + let total_vat_free_previous = 0; + let total_vat_allocated_previous = 0; + + // Calculate totals from current month + for (let name in allocation_data_current) { + let alloc = allocation_data_current[name]; + if (alloc.type === 'vat_free') { + total_vat_free_current += alloc.value || 0; + } else { + total_vat_allocated_current += alloc.value || 0; + } + } + + // Calculate totals from previous months + for (let name in allocation_data_previous) { + let alloc = allocation_data_previous[name]; + if (alloc.type === 'vat_free') { + total_vat_free_previous += alloc.value || 0; + } else { + total_vat_allocated_previous += alloc.value || 0; + } + } + + // Calculate remaining available + let vat_free_available = data.vat_free_available - total_vat_free_current - total_vat_free_previous; + let vat_allocated_available = data.vat_allocated_available - total_vat_allocated_current - total_vat_allocated_previous; + + return { + vat_free_available: Math.max(0, vat_free_available), + vat_allocated_available: Math.max(0, vat_allocated_available) + }; +} + +function build_allocation_html(vat_free_items, vat_items, allocation_data, tab_type) { let html = '
'; // VAT Free items section @@ -671,7 +906,7 @@ function build_allocation_html(vat_free_items, vat_items, allocation_data) { html += '' + format_currency(item.amount) + ''; html += '' + format_currency(item.unallocated_amount) + ''; html += '' + format_currency(item.current_allocation || 0) + ''; - html += ''; + html += ''; html += ''; }); @@ -699,7 +934,7 @@ function build_allocation_html(vat_free_items, vat_items, allocation_data) { html += '' + format_currency(item.amount) + ''; html += '' + format_currency(item.unallocated_amount) + ''; html += '' + format_currency(item.current_allocation || 0) + ''; - html += ''; + html += ''; html += ''; }); @@ -709,13 +944,13 @@ function build_allocation_html(vat_free_items, vat_items, allocation_data) { // Summary section let formatted_zero = $(frappe.format(0, {fieldtype: 'Currency'})).text(); - html += '
'; + html += '
'; html += '
'; - html += '
Total VAT Free to Allocate: ' + formatted_zero + '
'; - html += '
Total VAT to Allocate: ' + formatted_zero + '
'; + html += '
Total VAT Free to Allocate: ' + formatted_zero + '
'; + html += '
Total VAT to Allocate: ' + formatted_zero + '
'; html += '
'; html += '
'; - html += '
Total: ' + formatted_zero + '
'; + html += '
Total: ' + formatted_zero + '
'; html += '
'; html += '
'; @@ -724,15 +959,16 @@ function build_allocation_html(vat_free_items, vat_items, allocation_data) { return html; } -function setup_allocation_inputs(dialog, allocation_data, max_vat_free, max_vat_allocated) { - let $wrapper = dialog.fields_dict.allocation_html.$wrapper; +function setup_allocation_inputs(dialog, allocation_data, max_vat_free, max_vat_allocated, tab_type) { + // Ищем wrapper по ID вкладки в общем wrapper диалога + let $wrapper = dialog.$wrapper.find('#tab-' + tab_type); // Initial calculation setTimeout(function() { - $wrapper.find('.allocation-input').first().trigger('input'); + $wrapper.find('.allocation-input-' + tab_type).first().trigger('input'); }, 100); - $wrapper.find('.allocation-input').on('input', function() { + $wrapper.find('.allocation-input-' + tab_type).on('input', function() { let $input = $(this); let name = $input.data('name'); let type = $input.data('type'); @@ -755,11 +991,11 @@ function setup_allocation_inputs(dialog, allocation_data, max_vat_free, max_vat_ type: type }; - // Calculate totals + // Calculate totals for this tab only let total_vat_free = 0; let total_vat_allocated = 0; - $wrapper.find('.allocation-input').each(function() { + $wrapper.find('.allocation-input-' + tab_type).each(function() { let item_type = $(this).data('type'); let item_value = parseFloat($(this).val()) || 0; @@ -784,31 +1020,30 @@ function setup_allocation_inputs(dialog, allocation_data, max_vat_free, max_vat_ total_vat_free -= value; } - - // Update summary - $wrapper.find('#total_vat_free').text($(frappe.format(total_vat_free, {fieldtype: 'Currency'})).text()); - $wrapper.find('#total_vat_allocated').text($(frappe.format(total_vat_allocated, {fieldtype: 'Currency'})).text()); - $wrapper.find('#total_allocated').text($(frappe.format(total_vat_free + total_vat_allocated, {fieldtype: 'Currency'})).text()); + // Update summary for this tab + $wrapper.find('#total_vat_free_' + tab_type).text($(frappe.format(total_vat_free, {fieldtype: 'Currency'})).text()); + $wrapper.find('#total_vat_allocated_' + tab_type).text($(frappe.format(total_vat_allocated, {fieldtype: 'Currency'})).text()); + $wrapper.find('#total_allocated_' + tab_type).text($(frappe.format(total_vat_free + total_vat_allocated, {fieldtype: 'Currency'})).text()); }); } -function auto_allocate(frm, payment_row_idx, data, vat_free_items, vat_items, allocation_data, dialog) { - // Call server method to calculate auto allocation +function auto_allocate(frm, payment_row_idx, data, vat_free_items, vat_items, allocation_data, dialog, tab_type, available_amounts, other_allocation_data) { + // Use calculated available amounts frappe.call({ method: 'taxes_az.taxes_az.doctype.vat_allocation.vat_allocation.calculate_auto_allocation', args: { - vat_free_available: data.vat_free_available, - vat_allocated_available: data.vat_allocated_available, + vat_free_available: available_amounts.vat_free_available, + vat_allocated_available: available_amounts.vat_allocated_available, vat_free_items: JSON.stringify(vat_free_items), vat_items: JSON.stringify(vat_items) }, callback: function(r) { if (r.message) { let allocations = r.message; - let $wrapper = dialog.fields_dict.allocation_html.$wrapper; + let $wrapper = dialog.$wrapper.find('#tab-' + tab_type); - // Reset all inputs - $wrapper.find('.allocation-input').val(0); + // Reset all inputs for this tab + $wrapper.find('.allocation-input-' + tab_type).val(0); // Update allocation data for (let key in allocation_data) { @@ -818,11 +1053,20 @@ function auto_allocate(frm, payment_row_idx, data, vat_free_items, vat_items, al // Apply auto allocations allocations.forEach(function(alloc) { allocation_data[alloc.name].value = alloc.allocated; - $wrapper.find('.allocation-input[data-name="' + alloc.name + '"]').val(alloc.allocated.toFixed(2)); + $wrapper.find('.allocation-input-' + tab_type + '[data-name="' + alloc.name + '"]').val(alloc.allocated.toFixed(2)); }); // Trigger input event to update summary - $wrapper.find('.allocation-input').first().trigger('input'); + $wrapper.find('.allocation-input-' + tab_type).first().trigger('input'); + + // Update available amounts in dialog fields + let new_available = calculate_available_amounts(data, + tab_type === 'current' ? allocation_data : other_allocation_data, + tab_type === 'previous' ? allocation_data : other_allocation_data + ); + + dialog.set_value('vat_free_available', new_available.vat_free_available); + dialog.set_value('vat_allocated_available', new_available.vat_allocated_available); frappe.show_alert({ message: __('Auto allocation completed'), @@ -834,12 +1078,23 @@ function auto_allocate(frm, payment_row_idx, data, vat_free_items, vat_items, al } // Сохранение allocation - работает с несохраненным документом -function save_allocation_data(frm, payment_row_idx, allocation_data, dialog) { - // Prepare allocations array +function save_allocation_data(frm, payment_row_idx, allocation_data_current, allocation_data_previous, dialog) { + // Prepare allocations array - combine both tabs let allocations = []; - for (let name in allocation_data) { - let data = allocation_data[name]; + // Add allocations from current month tab + for (let name in allocation_data_current) { + let data = allocation_data_current[name]; + allocations.push({ + name: name, + allocated: data.value, + type: data.type + }); + } + + // Add allocations from previous months tab + for (let name in allocation_data_previous) { + let data = allocation_data_previous[name]; allocations.push({ name: name, allocated: data.value, @@ -892,6 +1147,7 @@ function save_allocation_data(frm, payment_row_idx, allocation_data, dialog) { frm.refresh_field('allocation_links'); frm.refresh_field('payment_entry_table'); frm.refresh_field('sales_invoice_table'); + frm.refresh_field('sales_invoice_previous_table'); // Помечаем документ как измененный frm.dirty(); @@ -899,7 +1155,7 @@ function save_allocation_data(frm, payment_row_idx, allocation_data, dialog) { dialog.hide(); } -// Функция для пересчета totals без сохранения в БД +// Функция для пересчета totals без сохранения в БД - UPDATED function recalculate_totals_manually(frm) { // Reset all allocated amounts for (let pe_row of frm.doc.payment_entry_table || []) { @@ -912,6 +1168,10 @@ function recalculate_totals_manually(frm) { si_row.allocated_amount = 0; } + for (let si_row of frm.doc.sales_invoice_previous_table || []) { + si_row.allocated_amount = 0; + } + // Calculate from links for (let link of frm.doc.allocation_links || []) { let allocated = link.allocated_amount || 0; @@ -931,13 +1191,24 @@ function recalculate_totals_manually(frm) { } } - // Update sales invoice row + // Update sales invoice row - check both tables + let found = false; for (let si_row of frm.doc.sales_invoice_table || []) { if (si_row.name === link.sales_invoice_row) { si_row.allocated_amount = (si_row.allocated_amount || 0) + allocated; + found = true; break; } } + + if (!found) { + for (let si_row of frm.doc.sales_invoice_previous_table || []) { + if (si_row.name === link.sales_invoice_row) { + si_row.allocated_amount = (si_row.allocated_amount || 0) + allocated; + break; + } + } + } } // Calculate unallocated amounts @@ -953,6 +1224,12 @@ function recalculate_totals_manually(frm) { si_row.unallocated_amount = Math.max(0, total - allocated); } + for (let si_row of frm.doc.sales_invoice_previous_table || []) { + let total = si_row.amount || 0; + let allocated = si_row.allocated_amount || 0; + si_row.unallocated_amount = Math.max(0, total - allocated); + } + // Calculate tax template totals calculate_tax_template_totals(frm); } @@ -966,7 +1243,7 @@ function calculate_tax_template_totals(frm) { frm.doc.total_edv_18 = 0; frm.doc.total_allocated_sum = 0; - // Calculate totals from sales invoice table + // Calculate totals from sales invoice table (only current month) for (let si_row of frm.doc.sales_invoice_table || []) { let allocated = si_row.allocated_amount || 0; if (allocated <= 0) continue; diff --git a/taxes_az/taxes_az/doctype/vat_allocation/vat_allocation.json b/taxes_az/taxes_az/doctype/vat_allocation/vat_allocation.json index f7f35cc..b65c43c 100644 --- a/taxes_az/taxes_az/doctype/vat_allocation/vat_allocation.json +++ b/taxes_az/taxes_az/doctype/vat_allocation/vat_allocation.json @@ -13,6 +13,8 @@ "payment_entry_table", "sales_invoice_section", "sales_invoice_table", + "section_break_fhfs", + "sales_invoice_previous_table", "allocation_links_section", "allocation_links", "total_edv_18", @@ -146,12 +148,22 @@ "print_hide": 1, "read_only": 1, "search_index": 1 + }, + { + "fieldname": "section_break_fhfs", + "fieldtype": "Section Break" + }, + { + "fieldname": "sales_invoice_previous_table", + "fieldtype": "Table", + "label": "Sales Invoice Previous Months", + "options": "VAT allocation sales invoice" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2025-10-06 16:52:07.347647", + "modified": "2025-10-07 14:10:17.385408", "modified_by": "Administrator", "module": "Taxes Az", "name": "VAT allocation", diff --git a/taxes_az/taxes_az/doctype/vat_allocation/vat_allocation.py b/taxes_az/taxes_az/doctype/vat_allocation/vat_allocation.py index 67ad2c3..31dcd00 100644 --- a/taxes_az/taxes_az/doctype/vat_allocation/vat_allocation.py +++ b/taxes_az/taxes_az/doctype/vat_allocation/vat_allocation.py @@ -46,6 +46,9 @@ class VATallocation(Document): for si_row in self.sales_invoice_table: si_row.allocated_amount = 0 + for si_row in self.sales_invoice_previous_table: + si_row.allocated_amount = 0 + # Calculate from links for link in self.allocation_links: allocated = Decimal(str(link.allocated_amount or 0)) @@ -66,12 +69,21 @@ class VATallocation(Document): pe_row.vat_allocated_allocated = float(current_vat_allocated + allocated) break - # Update sales invoice row + # Update sales invoice row - check both tables + found = False for si_row in self.sales_invoice_table: if si_row.name == link.sales_invoice_row: current_allocated = Decimal(str(si_row.allocated_amount or 0)) si_row.allocated_amount = float(current_allocated + allocated) + found = True break + + if not found: + for si_row in self.sales_invoice_previous_table: + if si_row.name == link.sales_invoice_row: + current_allocated = Decimal(str(si_row.allocated_amount or 0)) + si_row.allocated_amount = float(current_allocated + allocated) + break # Calculate unallocated amounts for pe_row in self.payment_entry_table: @@ -83,6 +95,11 @@ class VATallocation(Document): total = Decimal(str(si_row.amount or 0)) allocated = Decimal(str(si_row.allocated_amount or 0)) si_row.unallocated_amount = float(max(Decimal('0'), total - allocated)) + + for si_row in self.sales_invoice_previous_table: + total = Decimal(str(si_row.amount or 0)) + allocated = Decimal(str(si_row.allocated_amount or 0)) + si_row.unallocated_amount = float(max(Decimal('0'), total - allocated)) def validate_allocations(self): """ @@ -106,7 +123,7 @@ class VATallocation(Document): if vat_allocated_spent > vat_allocated_total + ROUNDING_TOLERANCE: frappe.throw(_(f"VAT Allocated spent exceeds total for payment entry {pe_row.payment_entry}")) - # Validate sales invoice items + # Validate sales invoice items - both tables for si_row in self.sales_invoice_table: amount = Decimal(str(si_row.amount or 0)) allocated = Decimal(str(si_row.allocated_amount or 0)) @@ -120,6 +137,20 @@ class VATallocation(Document): total_check = allocated + unallocated if abs(total_check - amount) > ROUNDING_TOLERANCE: frappe.throw(_(f"Allocated + Unallocated != Amount for {si_row.item_tax_template}")) + + for si_row in self.sales_invoice_previous_table: + amount = Decimal(str(si_row.amount or 0)) + allocated = Decimal(str(si_row.allocated_amount or 0)) + unallocated = Decimal(str(si_row.unallocated_amount or 0)) + + # Check non-negative + if allocated < 0 or unallocated < -ROUNDING_TOLERANCE: + frappe.throw(_(f"Allocated amounts cannot be negative for previous month {si_row.item_tax_template}")) + + # Check sum equals total (with tolerance) + total_check = allocated + unallocated + if abs(total_check - amount) > ROUNDING_TOLERANCE: + frappe.throw(_(f"Allocated + Unallocated != Amount for previous month {si_row.item_tax_template}")) @frappe.whitelist() def validate_all_allocations(vat_allocation_name): @@ -351,6 +382,115 @@ def get_sales_invoice_items(year, month, customers=None, min_amount=None, max_am return invoice_items +@frappe.whitelist() +def get_sales_invoice_items_previous_months(year, month, customers=None, min_amount=None, max_amount=None): + """ + Get Sales Invoice items from previous VAT Allocation documents where unallocated_amount > 0 + Grouped by customer, item_tax_template, and tax_article + """ + # Convert month name to number + months = { + "January": 1, "February": 2, "March": 3, "April": 4, + "May": 5, "June": 6, "July": 7, "August": 8, + "September": 9, "October": 10, "November": 11, "December": 12 + } + + month_number = months.get(month) + if not month_number: + frappe.throw(_("Invalid month")) + + # Parse customers filter + customer_list = None + if customers: + if isinstance(customers, str): + customers = json.loads(customers) + if customers and len(customers) > 0: + customer_list = [c.get('customer') for c in customers if c.get('customer')] + + # Find all previous VAT Allocation documents + # We need to find documents where (year < current_year) OR (year == current_year AND month < current_month) + vat_allocations = frappe.db.sql(""" + SELECT name, year, month + FROM `tabVAT allocation` + WHERE docstatus = 1 + AND ( + year < %(year)s + OR (year = %(year)s AND month < %(month)s) + ) + ORDER BY year DESC, month DESC + """, { + 'year': year, + 'month': month_number + }, as_dict=1) + + if not vat_allocations: + return [] + + # Collect all items with unallocated_amount > 0 from previous VAT Allocations + all_items = [] + + for vat_alloc in vat_allocations: + # Get sales invoice items from this VAT Allocation + items = frappe.db.sql(""" + SELECT + customer, + item_tax_template, + tax_article, + unallocated_amount + FROM `tabVAT allocation sales invoice` + WHERE parent = %(parent)s + AND unallocated_amount > 0 + """, { + 'parent': vat_alloc.name + }, as_dict=1) + + all_items.extend(items) + + # Group by customer, item_tax_template, tax_article and sum unallocated amounts + grouped_items = {} + + for item in all_items: + # Apply customer filter + if customer_list and item.customer not in customer_list: + continue + + key = (item.customer, item.item_tax_template or '', item.tax_article or '') + + if key not in grouped_items: + grouped_items[key] = { + 'customer': item.customer, + 'item_tax_template': item.item_tax_template, + 'tax_article': item.tax_article, + 'amount': Decimal('0') + } + + grouped_items[key]['amount'] += Decimal(str(item.unallocated_amount)) + + # Convert to list and apply amount filters + result = [] + for key, item in grouped_items.items(): + amount = float(item['amount']) + + # Apply min/max amount filters + if min_amount and amount < float(min_amount): + continue + if max_amount and amount > float(max_amount): + continue + + result.append({ + 'customer': item['customer'], + 'item_tax_template': item['item_tax_template'], + 'tax_article': item['tax_article'], + 'amount': amount, + 'unallocated_amount': amount, # Initially all is unallocated + 'allocated_amount': 0 + }) + + # Sort by customer, item_tax_template, tax_article + result.sort(key=lambda x: (x['customer'], x['item_tax_template'] or '', x['tax_article'] or '')) + + return result + @frappe.whitelist() def get_allocation_data(vat_allocation_name, payment_entry_row_name, customer): """ @@ -405,6 +545,10 @@ def get_allocation_data(vat_allocation_name, payment_entry_row_name, customer): # VAT templates (ƏDV 18% and ƏDV daxil 18%) vat_items = [] + vat_free_items_previous = [] + vat_items_previous = [] + + # Current month items for item in doc.sales_invoice_table: if item.customer == customer: # Get existing allocation for this item @@ -429,12 +573,39 @@ def get_allocation_data(vat_allocation_name, payment_entry_row_name, customer): elif 'ədv 18%' in template or 'ədv daxil 18%' in template: vat_items.append(item_data) + # Previous months items + for item in doc.sales_invoice_previous_table: + if item.customer == customer: + # Get existing allocation for this item + existing_alloc = existing_allocations.get(item.name, {'amount': 0, 'type': None}) + + item_data = { + 'name': item.name, + 'item_tax_template': item.item_tax_template or '', + 'tax_article': item.tax_article or '', + 'amount': item.amount, + 'unallocated_amount': item.unallocated_amount, + 'allocated_amount': item.allocated_amount or 0, + 'current_allocation': existing_alloc['amount'] # Allocation from THIS payment row + } + + template = (item.item_tax_template or '').lower() + + # Check if it's VAT free template + if 'ədv 0%' in template or 'ədv-dən azadolma' in template: + vat_free_items_previous.append(item_data) + # Check if it's VAT 18% template + elif 'ədv 18%' in template or 'ədv daxil 18%' in template: + vat_items_previous.append(item_data) + return { 'payment_row': payment_row, 'vat_free_available': vat_free_available, 'vat_allocated_available': vat_allocated_available, 'vat_free_items': vat_free_items, - 'vat_items': vat_items + 'vat_items': vat_items, + 'vat_free_items_previous': vat_free_items_previous, + 'vat_items_previous': vat_items_previous } def round_decimal(value, precision=2): @@ -513,8 +684,9 @@ def save_allocation(vat_allocation_name, payment_entry_row_name, allocations): "Total VAT Allocated ({0}) exceeds total amount ({1})" ).format(total_vat_allocated_new, vat_allocated_amount)) - # Validate each SI item + # Validate each SI item - check both tables si_items_map = {item.name: item for item in doc.sales_invoice_table} + si_items_map.update({item.name: item for item in doc.sales_invoice_previous_table}) for allocation in allocations: si_row_name = allocation['name'] @@ -716,6 +888,9 @@ def recalculate_totals_from_links(self): for si_row in self.sales_invoice_table: si_row.allocated_amount = 0 + for si_row in self.sales_invoice_previous_table: + si_row.allocated_amount = 0 + # Calculate from links for link in self.allocation_links: allocated = Decimal(str(link.allocated_amount or 0)) @@ -736,12 +911,21 @@ def recalculate_totals_from_links(self): pe_row.vat_allocated_allocated = float(current_vat_allocated + allocated) break - # Update sales invoice row + # Update sales invoice row - check both tables + found = False for si_row in self.sales_invoice_table: if si_row.name == link.sales_invoice_row: current_allocated = Decimal(str(si_row.allocated_amount or 0)) si_row.allocated_amount = float(current_allocated + allocated) + found = True break + + if not found: + for si_row in self.sales_invoice_previous_table: + if si_row.name == link.sales_invoice_row: + current_allocated = Decimal(str(si_row.allocated_amount or 0)) + si_row.allocated_amount = float(current_allocated + allocated) + break # Calculate unallocated amounts for pe_row in self.payment_entry_table: @@ -754,6 +938,11 @@ def recalculate_totals_from_links(self): allocated = Decimal(str(si_row.allocated_amount or 0)) si_row.unallocated_amount = float(max(Decimal('0'), total - allocated)) + for si_row in self.sales_invoice_previous_table: + total = Decimal(str(si_row.amount or 0)) + allocated = Decimal(str(si_row.allocated_amount or 0)) + si_row.unallocated_amount = float(max(Decimal('0'), total - allocated)) + # Calculate totals by tax template self.calculate_tax_template_totals() @@ -768,7 +957,7 @@ def calculate_tax_template_totals(self): self.total_edv_18 = 0 self.total_allocated_sum = 0 - # Calculate totals + # Calculate totals - only from current month table for si_row in self.sales_invoice_table: allocated = Decimal(str(si_row.allocated_amount or 0)) if allocated <= 0: @@ -787,4 +976,188 @@ def calculate_tax_template_totals(self): self.total_edv_18 = float(Decimal(str(self.total_edv_18 or 0)) + allocated) # Add to total sum - self.total_allocated_sum = float(Decimal(str(self.total_allocated_sum or 0)) + allocated) \ No newline at end of file + self.total_allocated_sum = float(Decimal(str(self.total_allocated_sum or 0)) + allocated) + +@frappe.whitelist() +def get_payment_entries_previous_months(year, month, customers=None, min_amount=None, max_amount=None): + """ + Get Payment Entries from previous VAT Allocation documents where unallocated_amount > 0 + Returns the most recent state of each Payment Entry + """ + # Convert month name to number + months = { + "January": 1, "February": 2, "March": 3, "April": 4, + "May": 5, "June": 6, "July": 7, "August": 8, + "September": 9, "October": 10, "November": 11, "December": 12 + } + + month_number = months.get(month) + if not month_number: + frappe.throw(_("Invalid month")) + + # Parse customers filter + customer_list = None + if customers: + if isinstance(customers, str): + customers = json.loads(customers) + if customers and len(customers) > 0: + customer_list = [c.get('customer') for c in customers if c.get('customer')] + + # Find all previous VAT Allocation documents + vat_allocations = frappe.db.sql(""" + SELECT name, year, month + FROM `tabVAT allocation` + WHERE docstatus = 1 + AND ( + year < %(year)s + OR (year = %(year)s AND month < %(month)s) + ) + ORDER BY year DESC, month DESC + """, { + 'year': year, + 'month': month_number + }, as_dict=1) + + if not vat_allocations: + return [] + + # Collect all payment entries with unallocated_amount > 0 + # Keep only the most recent entry for each payment_entry + payment_entries_map = {} + + for vat_alloc in vat_allocations: + # Get payment entries from this VAT Allocation + entries = frappe.db.sql(""" + SELECT + payment_entry, + customer, + payment_entry_amount, + vat_journal_entry, + vat_amount, + vat_allocated_amount, + vat_free_amount, + vat_allocated_allocated, + vat_free_allocated, + unallocated_amount + FROM `tabVAT allocation payment entry` + WHERE parent = %(parent)s + AND unallocated_amount > 0 + """, { + 'parent': vat_alloc.name + }, as_dict=1) + + for entry in entries: + # Apply customer filter + if customer_list and entry.customer not in customer_list: + continue + + # Apply min/max amount filters to payment_entry_amount + if min_amount and (entry.payment_entry_amount or 0) < float(min_amount): + continue + if max_amount and (entry.payment_entry_amount or 0) > float(max_amount): + continue + + # Keep only the most recent (first encountered due to ORDER BY DESC) + if entry.payment_entry not in payment_entries_map: + # Calculate remaining amounts + vat_allocated_remaining = Decimal(str(entry.vat_allocated_amount or 0)) - Decimal(str(entry.vat_allocated_allocated or 0)) + vat_free_remaining = Decimal(str(entry.vat_free_amount or 0)) - Decimal(str(entry.vat_free_allocated or 0)) + + payment_entries_map[entry.payment_entry] = { + 'payment_entry': entry.payment_entry, + 'customer': entry.customer, + 'payment_entry_amount': entry.payment_entry_amount, + 'vat_journal_entry': entry.vat_journal_entry, + 'vat_amount': entry.vat_amount, + 'vat_allocated_amount': float(vat_allocated_remaining), + 'vat_free_amount': float(vat_free_remaining), + 'unallocated_amount': 0, # Will be calculated + 'allocated_amount': 0, + 'vat_allocated_allocated': 0, + 'vat_free_allocated': 0 + } + + # Convert to list + result = list(payment_entries_map.values()) + + # Sort by payment_entry + result.sort(key=lambda x: x['payment_entry']) + + return result + +@frappe.whitelist() +def get_payment_entries_with_previous(year, month, customers=None, min_amount=None, max_amount=None): + """ + Get Payment Entries - both current month and previous months with unallocated amounts + """ + # Get current month payment entries + current_entries = get_payment_entries(year, month, customers, min_amount, max_amount) + + # Get previous months payment entries + previous_entries = get_payment_entries_previous_months(year, month, customers, min_amount, max_amount) + + # Create a set of payment_entry names from current month + current_pe_names = {entry['payment_entry'] for entry in current_entries} + + # Filter out previous entries that are already in current month + filtered_previous = [entry for entry in previous_entries if entry['payment_entry'] not in current_pe_names] + + # Combine and return + return current_entries + filtered_previous + +@frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs +def get_payment_entry_query(doctype, txt, searchfield, start, page_len, filters): + """ + Custom query for filtering Payment Entries - includes both current month and those with remaining balance + """ + year = filters.get('year') + month = filters.get('month') + + if not all([year, month]): + return [] + + # Convert month name to number + months = { + "January": 1, "February": 2, "March": 3, "April": 4, + "May": 5, "June": 6, "July": 7, "August": 8, + "September": 9, "October": 10, "November": 11, "December": 12 + } + + month_number = months.get(month) + if not month_number: + return [] + + # Get Payment Entries from current month OR from previous VAT Allocations with balance + return frappe.db.sql(""" + SELECT DISTINCT pe.name, pe.posting_date, pe.paid_amount + FROM `tabPayment Entry` pe + WHERE pe.docstatus = 1 + AND pe.payment_type = 'Receive' + AND pe.party_type = 'Customer' + AND pe.name LIKE %(txt)s + AND ( + (YEAR(pe.posting_date) = %(year)s AND MONTH(pe.posting_date) = %(month)s) + OR + EXISTS ( + SELECT 1 + FROM `tabVAT allocation payment entry` vape + INNER JOIN `tabVAT allocation` va ON va.name = vape.parent + WHERE vape.payment_entry = pe.name + AND va.docstatus = 1 + AND vape.unallocated_amount > 0 + AND ( + va.year < %(year)s + OR (va.year = %(year)s AND va.month < %(month)s) + ) + ) + ) + ORDER BY pe.posting_date DESC + LIMIT %(start)s, %(page_len)s + """, { + 'year': year, + 'month': month_number, + 'txt': f"%{txt}%", + 'start': start, + 'page_len': page_len + }) \ No newline at end of file diff --git a/taxes_az/taxes_az/doctype/vat_allocation_sales_invoice_previous_months/__init__.py b/taxes_az/taxes_az/doctype/vat_allocation_sales_invoice_previous_months/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/taxes_az/taxes_az/doctype/vat_allocation_sales_invoice_previous_months/vat_allocation_sales_invoice_previous_months.json b/taxes_az/taxes_az/doctype/vat_allocation_sales_invoice_previous_months/vat_allocation_sales_invoice_previous_months.json new file mode 100644 index 0000000..599da76 --- /dev/null +++ b/taxes_az/taxes_az/doctype/vat_allocation_sales_invoice_previous_months/vat_allocation_sales_invoice_previous_months.json @@ -0,0 +1,89 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2025-10-07 14:07:17.398766", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "customer", + "payment_entry", + "item_tax_template", + "tax_article", + "amount", + "allocated_amount", + "unallocated_amount" + ], + "fields": [ + { + "fieldname": "customer", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Customer", + "options": "Customer", + "read_only": 1 + }, + { + "fieldname": "payment_entry", + "fieldtype": "Link", + "hidden": 1, + "in_list_view": 1, + "label": "Payment Entry", + "options": "Payment Entry" + }, + { + "fieldname": "item_tax_template", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Item tax template", + "options": "Item Tax Template", + "precision": "2", + "read_only": 1 + }, + { + "fieldname": "tax_article", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Tax Article", + "options": "Tax Article", + "precision": "2", + "read_only": 1 + }, + { + "fieldname": "amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Amount", + "precision": "2", + "read_only": 1 + }, + { + "fieldname": "allocated_amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Allocated amount", + "precision": "2", + "read_only": 1 + }, + { + "fieldname": "unallocated_amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Unallocated amount", + "precision": "2", + "read_only": 1 + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2025-10-07 14:07:17.398766", + "modified_by": "Administrator", + "module": "Taxes Az", + "name": "VAT allocation sales invoice previous months", + "owner": "Administrator", + "permissions": [], + "sort_field": "creation", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/taxes_az/taxes_az/doctype/vat_allocation_sales_invoice_previous_months/vat_allocation_sales_invoice_previous_months.py b/taxes_az/taxes_az/doctype/vat_allocation_sales_invoice_previous_months/vat_allocation_sales_invoice_previous_months.py new file mode 100644 index 0000000..2223ae9 --- /dev/null +++ b/taxes_az/taxes_az/doctype/vat_allocation_sales_invoice_previous_months/vat_allocation_sales_invoice_previous_months.py @@ -0,0 +1,9 @@ +# Copyright (c) 2025, Jey Soft and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class VATallocationsalesinvoicepreviousmonths(Document): + pass