diff --git a/taxes_az/taxes_az/doctype/tax_article_items_report/__init__.py b/taxes_az/taxes_az/doctype/tax_article_items_report/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/taxes_az/taxes_az/doctype/tax_article_items_report/tax_article_items_report.js b/taxes_az/taxes_az/doctype/tax_article_items_report/tax_article_items_report.js
new file mode 100644
index 0000000..3517dd0
--- /dev/null
+++ b/taxes_az/taxes_az/doctype/tax_article_items_report/tax_article_items_report.js
@@ -0,0 +1,113 @@
+frappe.ui.form.on('Tax Article Items Report', {
+ tax_article: function(frm) {
+ if (frm.doc.tax_article) {
+ refresh_items_list(frm);
+ } else {
+ clear_results(frm);
+ }
+ },
+
+ from_date: function(frm) {
+ if (frm.doc.tax_article) {
+ refresh_items_list(frm);
+ }
+ },
+
+ to_date: function(frm) {
+ if (frm.doc.tax_article) {
+ refresh_items_list(frm);
+ }
+ },
+
+ company: function(frm) {
+ if (frm.doc.tax_article) {
+ refresh_items_list(frm);
+ }
+ },
+
+ refresh_button: function(frm) {
+ if (frm.doc.tax_article) {
+ refresh_items_list(frm);
+ } else {
+ frappe.msgprint(__('Please select a Tax Article first'));
+ }
+ }
+});
+
+function refresh_items_list(frm) {
+ frappe.call({
+ method: 'taxes_az.taxes_az.doctype.tax_article_items_report.tax_article_items_report.get_items_by_tax_article',
+ args: {
+ tax_article: frm.doc.tax_article,
+ from_date: frm.doc.from_date,
+ to_date: frm.doc.to_date,
+ company: frm.doc.company
+ },
+ callback: function(r) {
+ if (r.message) {
+ display_items_list(frm, r.message);
+ }
+ }
+ });
+}
+
+function display_items_list(frm, data) {
+ let html = `
+
+
+
+
+ | Sales Invoice |
+ Date |
+ Customer |
+ Item Code |
+ Item Name |
+ Qty |
+ Rate |
+ Amount |
+ Tax Article |
+
+
+
+ `;
+
+ let total_amount = 0;
+
+ data.forEach(function(row) {
+ html += `
+
+ | ${row.parent} |
+ ${frappe.datetime.str_to_user(row.posting_date)} |
+ ${row.customer || ''} |
+ ${row.item_code || ''} |
+ ${row.item_name || ''} |
+ ${row.qty || 0} |
+ ${format_currency(row.rate || 0)} |
+ ${format_currency(row.amount || 0)} |
+ ${row.tax_article || ''} |
+
+ `;
+ total_amount += (row.amount || 0);
+ });
+
+ html += `
+
+
+
+ | Total |
+ ${format_currency(total_amount)} |
+ |
+
+
+
+
+ `;
+
+ frm.fields_dict.items_html.$wrapper.html(html);
+ frm.set_value('total_items_found', data.length);
+}
+
+function clear_results(frm) {
+ frm.fields_dict.items_html.$wrapper.html('Select Tax Article to see results
');
+ frm.set_value('total_items_found', 0);
+}
\ No newline at end of file
diff --git a/taxes_az/taxes_az/doctype/tax_article_items_report/tax_article_items_report.json b/taxes_az/taxes_az/doctype/tax_article_items_report/tax_article_items_report.json
new file mode 100644
index 0000000..6983cfc
--- /dev/null
+++ b/taxes_az/taxes_az/doctype/tax_article_items_report/tax_article_items_report.json
@@ -0,0 +1,129 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "autoname": "naming_series:",
+ "creation": "2025-01-25 12:00:00",
+ "doctype": "DocType",
+ "engine": "InnoDB",
+ "field_order": [
+ "main_section",
+ "naming_series",
+ "tax_article",
+ "date_range_section",
+ "from_date",
+ "to_date",
+ "column_break_1",
+ "company",
+ "refresh_button",
+ "results_section",
+ "total_items_found",
+ "items_html"
+ ],
+ "fields": [
+ {
+ "fieldname": "main_section",
+ "fieldtype": "Section Break",
+ "label": "Filter Settings"
+ },
+ {
+ "fieldname": "naming_series",
+ "fieldtype": "Select",
+ "label": "Series",
+ "options": "TAX-REP-.YYYY.-",
+ "default": "TAX-REP-.YYYY.-",
+ "reqd": 1
+ },
+ {
+ "fieldname": "tax_article",
+ "fieldtype": "Link",
+ "label": "Tax Article",
+ "options": "Tax Article",
+ "reqd": 1,
+ "in_list_view": 1
+ },
+ {
+ "fieldname": "date_range_section",
+ "fieldtype": "Section Break",
+ "label": "Date Range (Optional)"
+ },
+ {
+ "fieldname": "from_date",
+ "fieldtype": "Date",
+ "label": "From Date"
+ },
+ {
+ "fieldname": "to_date",
+ "fieldtype": "Date",
+ "label": "To Date"
+ },
+ {
+ "fieldname": "column_break_1",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "label": "Company",
+ "options": "Company"
+ },
+ {
+ "fieldname": "refresh_button",
+ "fieldtype": "Button",
+ "label": "Refresh Results"
+ },
+ {
+ "fieldname": "results_section",
+ "fieldtype": "Section Break",
+ "label": "Results"
+ },
+ {
+ "fieldname": "total_items_found",
+ "fieldtype": "Int",
+ "label": "Total Items Found",
+ "read_only": 1
+ },
+ {
+ "fieldname": "items_html",
+ "fieldtype": "HTML",
+ "label": "Items List",
+ "options": "Select Tax Article and click Refresh to see results
"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2025-01-25 12:00:00",
+ "modified_by": "Administrator",
+ "module": "Taxes Az",
+ "name": "Tax Article Items Report",
+ "naming_rule": "By Naming Series",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Accounts Manager",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": [],
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/taxes_az/taxes_az/doctype/tax_article_items_report/tax_article_items_report.py b/taxes_az/taxes_az/doctype/tax_article_items_report/tax_article_items_report.py
new file mode 100644
index 0000000..3590ac9
--- /dev/null
+++ b/taxes_az/taxes_az/doctype/tax_article_items_report/tax_article_items_report.py
@@ -0,0 +1,115 @@
+# Copyright (c) 2025, Jey Soft and contributors
+# For license information, please see license.txt
+
+from frappe.model.document import Document
+import frappe
+from frappe.model.document import Document
+
+class TaxArticleItemsReport(Document):
+ pass
+
+@frappe.whitelist()
+def get_items_by_tax_article(tax_article, from_date=None, to_date=None, company=None):
+ """
+ Get all Sales Invoice Items that have the specified tax_article
+ """
+
+ conditions = ["sii.tax_article = %s"]
+ values = [tax_article]
+
+ # Add date filters
+ if from_date:
+ conditions.append("si.posting_date >= %s")
+ values.append(from_date)
+
+ if to_date:
+ conditions.append("si.posting_date <= %s")
+ values.append(to_date)
+
+ # Add company filter
+ if company:
+ conditions.append("si.company = %s")
+ values.append(company)
+
+ # Add docstatus condition (only submitted invoices)
+ conditions.append("si.docstatus = 1")
+
+ where_clause = " AND ".join(conditions)
+
+ query = f"""
+ SELECT
+ sii.parent,
+ si.posting_date,
+ si.customer,
+ si.customer_name,
+ sii.item_code,
+ sii.item_name,
+ sii.qty,
+ sii.rate,
+ sii.amount,
+ sii.tax_article,
+ si.company,
+ si.currency
+ FROM
+ `tabSales Invoice Item` sii
+ INNER JOIN
+ `tabSales Invoice` si ON sii.parent = si.name
+ WHERE
+ {where_clause}
+ ORDER BY
+ si.posting_date DESC, sii.parent, sii.idx
+ """
+
+ try:
+ results = frappe.db.sql(query, values, as_dict=True)
+ return results
+ except Exception as e:
+ frappe.log_error(f"Error in get_items_by_tax_article: {str(e)}")
+ frappe.throw(f"Error retrieving data: {str(e)}")
+
+@frappe.whitelist()
+def get_tax_article_summary(tax_article, from_date=None, to_date=None, company=None):
+ """
+ Get summary statistics for a tax article
+ """
+
+ conditions = ["sii.tax_article = %s"]
+ values = [tax_article]
+
+ if from_date:
+ conditions.append("si.posting_date >= %s")
+ values.append(from_date)
+
+ if to_date:
+ conditions.append("si.posting_date <= %s")
+ values.append(to_date)
+
+ if company:
+ conditions.append("si.company = %s")
+ values.append(company)
+
+ conditions.append("si.docstatus = 1")
+
+ where_clause = " AND ".join(conditions)
+
+ query = f"""
+ SELECT
+ COUNT(DISTINCT sii.parent) as total_invoices,
+ COUNT(sii.name) as total_items,
+ SUM(sii.qty) as total_qty,
+ SUM(sii.amount) as total_amount,
+ AVG(sii.rate) as avg_rate
+ FROM
+ `tabSales Invoice Item` sii
+ INNER JOIN
+ `tabSales Invoice` si ON sii.parent = si.name
+ WHERE
+ {where_clause}
+ """
+
+ try:
+ result = frappe.db.sql(query, values, as_dict=True)
+ return result[0] if result else {}
+ except Exception as e:
+ frappe.log_error(f"Error in get_tax_article_summary: {str(e)}")
+ return {}
\ No newline at end of file
diff --git a/taxes_az/taxes_az/doctype/tax_article_items_report/test_tax_article_items_report.py b/taxes_az/taxes_az/doctype/tax_article_items_report/test_tax_article_items_report.py
new file mode 100644
index 0000000..1e8929d
--- /dev/null
+++ b/taxes_az/taxes_az/doctype/tax_article_items_report/test_tax_article_items_report.py
@@ -0,0 +1,30 @@
+# Copyright (c) 2025, Jey Soft and Contributors
+# See license.txt
+
+# import frappe
+from frappe.tests import IntegrationTestCase, UnitTestCase
+
+
+# On IntegrationTestCase, the doctype test records and all
+# link-field test record depdendencies are recursively loaded
+# Use these module variables to add/remove to/from that list
+EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
+IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
+
+
+class UnitTestTaxArticleItemsReport(UnitTestCase):
+ """
+ Unit tests for TaxArticleItemsReport.
+ Use this class for testing individual functions and methods.
+ """
+
+ pass
+
+
+class IntegrationTestTaxArticleItemsReport(IntegrationTestCase):
+ """
+ Integration tests for TaxArticleItemsReport.
+ Use this class for testing interactions between multiple components.
+ """
+
+ pass
diff --git a/taxes_az/taxes_az/doctype/vat_allocation/__init__.py b/taxes_az/taxes_az/doctype/vat_allocation/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/taxes_az/taxes_az/doctype/vat_allocation/test_vat_allocation.py b/taxes_az/taxes_az/doctype/vat_allocation/test_vat_allocation.py
new file mode 100644
index 0000000..22991c2
--- /dev/null
+++ b/taxes_az/taxes_az/doctype/vat_allocation/test_vat_allocation.py
@@ -0,0 +1,30 @@
+# Copyright (c) 2025, Jey Soft and Contributors
+# See license.txt
+
+# import frappe
+from frappe.tests import IntegrationTestCase, UnitTestCase
+
+
+# On IntegrationTestCase, the doctype test records and all
+# link-field test record depdendencies are recursively loaded
+# Use these module variables to add/remove to/from that list
+EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
+IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
+
+
+class UnitTestVATallocation(UnitTestCase):
+ """
+ Unit tests for VATallocation.
+ Use this class for testing individual functions and methods.
+ """
+
+ pass
+
+
+class IntegrationTestVATallocation(IntegrationTestCase):
+ """
+ Integration tests for VATallocation.
+ Use this class for testing interactions between multiple components.
+ """
+
+ pass
diff --git a/taxes_az/taxes_az/doctype/vat_allocation/vat_allocation.js b/taxes_az/taxes_az/doctype/vat_allocation/vat_allocation.js
new file mode 100644
index 0000000..9211bed
--- /dev/null
+++ b/taxes_az/taxes_az/doctype/vat_allocation/vat_allocation.js
@@ -0,0 +1,706 @@
+frappe.ui.form.on('VAT allocation', {
+ refresh: function(frm) {
+ // Add button for filling Payment Entries with filters
+ if (!frm.is_new()) {
+ frm.add_custom_button(__('Fill Payment Entries'), function() {
+ show_payment_entry_filters(frm);
+ });
+
+ // Add button for filling Sales Invoice Items
+ frm.add_custom_button(__('Fill Sales Invoice Items'), function() {
+ show_sales_invoice_filters(frm);
+ });
+ }
+
+ // Setup filter for Journal Entry
+ setup_journal_entry_query(frm);
+ }
+});
+
+// Function to show filter dialog for Payment Entries
+function show_payment_entry_filters(frm) {
+ // Check that year and month are selected
+ if (!frm.doc.year || !frm.doc.month) {
+ frappe.msgprint(__('Please select Year and Month first'));
+ return;
+ }
+
+ // Create dialog with filters
+ let dialog = new frappe.ui.Dialog({
+ title: __('Payment Entry Filters'),
+ fields: [
+ {
+ fieldname: 'customers_section',
+ fieldtype: 'Section Break',
+ label: __('Select Customers (Optional)')
+ },
+ {
+ fieldname: 'customers',
+ fieldtype: 'Table',
+ label: __('Customers'),
+ description: __('Leave empty to get all customers'),
+ fields: [
+ {
+ fieldname: 'customer',
+ fieldtype: 'Link',
+ label: __('Customer'),
+ options: 'Customer',
+ in_list_view: 1
+ }
+ ]
+ },
+ {
+ fieldname: 'amount_section',
+ fieldtype: 'Section Break',
+ label: __('Amount Range (Optional)')
+ },
+ {
+ fieldname: 'min_amount',
+ fieldtype: 'Currency',
+ label: __('Minimum Amount'),
+ description: __('Only payments >= this amount')
+ },
+ {
+ fieldname: 'column_break_1',
+ fieldtype: 'Column Break'
+ },
+ {
+ fieldname: 'max_amount',
+ fieldtype: 'Currency',
+ label: __('Maximum Amount'),
+ description: __('Only payments <= this amount')
+ }
+ ],
+ primary_action_label: __('Fill Table'),
+ primary_action: function(values) {
+ fill_payment_entries(frm, values);
+ dialog.hide();
+ }
+ });
+
+ dialog.show();
+
+ // Hide scrollbar
+ setTimeout(function() {
+ dialog.$wrapper.find('.frappe-control[data-fieldname="customers"] .grid-scroll-bar').css({
+ 'visibility': 'hidden',
+ 'height': '0px'
+ });
+ }, 100);
+}
+
+// Function to show filter dialog for Sales Invoice Items
+function show_sales_invoice_filters(frm) {
+ // Check that year and month are selected
+ if (!frm.doc.year || !frm.doc.month) {
+ frappe.msgprint(__('Please select Year and Month first'));
+ return;
+ }
+
+ // Create dialog with filters
+ let dialog = new frappe.ui.Dialog({
+ title: __('Sales Invoice Items Filters'),
+ fields: [
+ {
+ fieldname: 'customers_section',
+ fieldtype: 'Section Break',
+ label: __('Select Customers (Optional)')
+ },
+ {
+ fieldname: 'customers',
+ fieldtype: 'Table',
+ label: __('Customers'),
+ description: __('Leave empty to get all customers'),
+ fields: [
+ {
+ fieldname: 'customer',
+ fieldtype: 'Link',
+ label: __('Customer'),
+ options: 'Customer',
+ in_list_view: 1
+ }
+ ]
+ },
+ {
+ fieldname: 'amount_section',
+ fieldtype: 'Section Break',
+ label: __('Amount Range (Optional)')
+ },
+ {
+ fieldname: 'min_amount',
+ fieldtype: 'Currency',
+ label: __('Minimum Amount'),
+ description: __('Only grouped amounts >= this amount')
+ },
+ {
+ fieldname: 'column_break_1',
+ fieldtype: 'Column Break'
+ },
+ {
+ fieldname: 'max_amount',
+ fieldtype: 'Currency',
+ label: __('Maximum Amount'),
+ description: __('Only grouped amounts <= this amount')
+ }
+ ],
+ primary_action_label: __('Fill Table'),
+ primary_action: function(values) {
+ fill_sales_invoice_items(frm, values);
+ dialog.hide();
+ }
+ });
+
+ dialog.show();
+
+ // Hide scrollbar
+ setTimeout(function() {
+ dialog.$wrapper.find('.frappe-control[data-fieldname="customers"] .grid-scroll-bar').css({
+ 'visibility': 'hidden',
+ 'height': '0px'
+ });
+ }, 100);
+}
+
+// Function to fill Payment Entries with filters
+function fill_payment_entries(frm, filters) {
+ // Call server method
+ frappe.call({
+ method: 'taxes_az.taxes_az.doctype.vat_allocation.vat_allocation.get_payment_entries',
+ 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) {
+ if (r.message && r.message.length > 0) {
+ // Clear existing table
+ frm.clear_table('payment_entry_table');
+
+ // Add new rows
+ r.message.forEach(function(payment) {
+ let row = frm.add_child('payment_entry_table');
+ 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;
+ });
+
+ frm.refresh_field('payment_entry_table');
+ frappe.msgprint(__('Payment Entries loaded successfully: {0} records', [r.message.length]));
+ } else {
+ frappe.msgprint(__('No Payment Entries found for selected period'));
+ }
+ }
+ });
+}
+
+// Function to fill Sales Invoice Items with filters
+function fill_sales_invoice_items(frm, filters) {
+ // Call server method
+ frappe.call({
+ method: 'taxes_az.taxes_az.doctype.vat_allocation.vat_allocation.get_sales_invoice_items',
+ 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) {
+ if (r.message && r.message.length > 0) {
+ // Clear existing table
+ frm.clear_table('sales_invoice_table');
+
+ // Add new rows
+ r.message.forEach(function(item) {
+ let row = frm.add_child('sales_invoice_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.amount; // Default: all amount is unallocated
+ });
+
+ frm.refresh_field('sales_invoice_table');
+ frappe.msgprint(__('Sales Invoice Items loaded successfully: {0} records', [r.message.length]));
+ } else {
+ frappe.msgprint(__('No Sales Invoice Items found for selected period'));
+ }
+ }
+ });
+}
+
+// Setup query filter for Journal Entry
+function setup_journal_entry_query(frm) {
+ frm.fields_dict.payment_entry_table.grid.get_field('vat_journal_entry').get_query = function(doc, cdt, cdn) {
+ let child = locals[cdt][cdn];
+
+ return {
+ query: 'taxes_az.taxes_az.doctype.vat_allocation.vat_allocation.get_journal_entry_query',
+ filters: {
+ customer: child.customer,
+ year: doc.year,
+ month: doc.month
+ }
+ };
+ };
+}
+
+// Setup for VAT Journal Entry
+frappe.ui.form.on('VAT allocation payment entry', {
+ vat_journal_entry: function(frm, cdt, cdn) {
+ let row = locals[cdt][cdn];
+
+ if (row.vat_journal_entry) {
+ // Get Total Debit from Journal Entry
+ frappe.call({
+ method: 'frappe.client.get_value',
+ args: {
+ doctype: 'Journal Entry',
+ filters: { name: row.vat_journal_entry },
+ fieldname: ['total_debit', 'docstatus']
+ },
+ callback: function(r) {
+ if (r.message) {
+ // Check if Journal Entry is submitted
+ if (r.message.docstatus !== 1) {
+ frappe.msgprint(__('Warning: Selected Journal Entry is not submitted'));
+ frappe.model.set_value(cdt, cdn, 'vat_journal_entry', '');
+ return;
+ }
+
+ // Fill vat_amount
+ frappe.model.set_value(cdt, cdn, 'vat_amount', r.message.total_debit);
+
+ // Calculate vat_allocated_amount
+ let vat_allocated = r.message.total_debit / 0.18;
+ frappe.model.set_value(cdt, cdn, 'vat_allocated_amount', vat_allocated);
+
+ // Calculate vat_free_amount
+ let vat_free = row.payment_entry_amount - vat_allocated;
+ frappe.model.set_value(cdt, cdn, 'vat_free_amount', vat_free);
+
+ // allocated_amount remains 0 for now (will be calculated from links)
+ frappe.model.set_value(cdt, cdn, 'allocated_amount', 0);
+ }
+ }
+ });
+ } else {
+ // If journal entry is cleared or not selected
+ frappe.model.set_value(cdt, cdn, 'vat_amount', 0);
+ frappe.model.set_value(cdt, cdn, 'vat_allocated_amount', 0);
+ frappe.model.set_value(cdt, cdn, 'vat_free_amount', row.payment_entry_amount);
+ frappe.model.set_value(cdt, cdn, 'allocated_amount', 0);
+ }
+ },
+
+ // Setup query filter for vat_journal_entry when form loads
+ payment_entry_table_add: function(frm, cdt, cdn) {
+ setup_journal_entry_query(frm);
+ },
+
+ // If payment_entry_amount changes and no journal entry selected
+ payment_entry_amount: function(frm, cdt, cdn) {
+ let row = locals[cdt][cdn];
+
+ if (!row.vat_journal_entry) {
+ frappe.model.set_value(cdt, cdn, 'vat_free_amount', row.payment_entry_amount);
+ frappe.model.set_value(cdt, cdn, 'unallocated_amount', row.payment_entry_amount);
+ }
+ },
+
+ // Handle Allocate button click
+ allocate_button: function(frm, cdt, cdn) {
+ let row = locals[cdt][cdn];
+
+ // Validate that customer is selected
+ if (!row.customer) {
+ frappe.msgprint(__('Please select a customer first'));
+ return;
+ }
+
+ // Check if customer is active
+ frappe.call({
+ method: 'frappe.client.get_value',
+ args: {
+ doctype: 'Customer',
+ filters: { name: row.customer },
+ fieldname: ['disabled']
+ },
+ callback: function(r) {
+ if (r.message && r.message.disabled) {
+ frappe.msgprint(__('Cannot allocate to disabled customer'));
+ return;
+ }
+ // Open allocation dialog
+ show_allocation_dialog(frm, row.name, row.customer);
+ }
+ });
+ }
+});
+
+// Show allocation dialog
+function show_allocation_dialog(frm, payment_row_name, customer) {
+ // Get allocation data from server
+ frappe.call({
+ method: 'taxes_az.taxes_az.doctype.vat_allocation.vat_allocation.get_allocation_data',
+ args: {
+ vat_allocation_name: frm.doc.name,
+ payment_entry_row_name: payment_row_name,
+ customer: customer
+ },
+ callback: function(r) {
+ if (!r.message) {
+ frappe.msgprint(__('No data available for allocation'));
+ return;
+ }
+
+ let data = r.message;
+ let vat_free_items = data.vat_free_items;
+ let vat_items = data.vat_items;
+
+ // Check if there are items to allocate to
+ if (vat_free_items.length === 0 && vat_items.length === 0) {
+ frappe.msgprint(__('No sales invoice items available for this customer'));
+ return;
+ }
+
+ // Create allocation dialog
+ create_allocation_dialog(frm, payment_row_name, data, vat_free_items, vat_items);
+ }
+ });
+}
+
+function create_allocation_dialog(frm, payment_row_name, data, vat_free_items, vat_items) {
+ let allocation_data = {};
+
+ let dialog = new frappe.ui.Dialog({
+ title: __('Allocate Amount'),
+ size: 'large',
+ fields: [
+ {
+ fieldname: 'info_section',
+ fieldtype: 'Section Break',
+ label: __('Payment Entry Information')
+ },
+ {
+ fieldname: 'payment_info_html',
+ fieldtype: 'HTML'
+ },
+ {
+ fieldname: 'available_section',
+ fieldtype: 'Section Break',
+ label: __('Available for Allocation')
+ },
+ {
+ fieldname: 'vat_free_available',
+ fieldtype: 'Currency',
+ label: __('VAT Free Available (ƏDV 0%, ƏDV-dən azadolma)'),
+ read_only: 1,
+ default: data.vat_free_available
+ },
+ {
+ fieldname: 'column_break_avail',
+ fieldtype: 'Column Break'
+ },
+ {
+ fieldname: 'vat_allocated_available',
+ fieldtype: 'Currency',
+ label: __('VAT Allocated Available (ƏDV 18%, ƏDV daxil 18%)'),
+ read_only: 1,
+ default: data.vat_allocated_available
+ },
+ {
+ fieldname: 'items_section',
+ fieldtype: 'Section Break',
+ label: __('Sales Invoice Items - Edit Allocations')
+ },
+ {
+ fieldname: 'allocation_html',
+ fieldtype: 'HTML'
+ }
+ ],
+ primary_action_label: __('Save Allocation'),
+ primary_action: function() {
+ save_allocation_data(frm, payment_row_name, allocation_data, dialog);
+ },
+ secondary_action_label: __('Auto Allocate'),
+ secondary_action: function() {
+ auto_allocate(frm, payment_row_name, data, vat_free_items, vat_items, allocation_data, dialog);
+ }
+ });
+
+ // Build payment info HTML
+ let info_html = `
+
+
+
+ Total VAT Free: ${format_currency(data.payment_row.vat_free_amount)}
+ VAT Free Spent: ${format_currency(data.payment_row.vat_free_allocated || 0)}
+
+
+ Total VAT Allocated: ${format_currency(data.payment_row.vat_allocated_amount)}
+ VAT Allocated Spent: ${format_currency(data.payment_row.vat_allocated_allocated || 0)}
+
+
+
+ `;
+ 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);
+
+ // Setup input handlers
+ setup_allocation_inputs(dialog, allocation_data, data.vat_free_available, data.vat_allocated_available);
+
+ dialog.show();
+}
+
+function build_allocation_html(vat_free_items, vat_items, allocation_data) {
+ let html = '';
+
+ // VAT Free items section
+ if (vat_free_items.length > 0) {
+ html += '
VAT Free Items (ƏDV 0%, ƏDV-dən azadolma)
';
+ html += '
';
+ }
+
+ // VAT 18% items section
+ if (vat_items.length > 0) {
+ html += '
VAT 18% Items (ƏDV 18%, ƏDV daxil 18%)
';
+ html += '
';
+ }
+
+ // Summary section
+ let formatted_zero = $(frappe.format(0, {fieldtype: 'Currency'})).text();
+
+ html += '
';
+ html += '
';
+ html += '
Total VAT Free to Allocate: ' + formatted_zero + '
';
+ html += '
Total VAT to Allocate: ' + formatted_zero + '
';
+ html += '
';
+ html += '
';
+ html += '
Total: ' + formatted_zero + '
';
+ html += '
';
+ html += '
';
+
+ html += '
';
+
+ return html;
+}
+
+function setup_allocation_inputs(dialog, allocation_data, max_vat_free, max_vat_allocated) {
+ let $wrapper = dialog.fields_dict.allocation_html.$wrapper;
+
+ // Initial calculation
+ setTimeout(function() {
+ $wrapper.find('.allocation-input').first().trigger('input');
+ }, 100);
+
+ $wrapper.find('.allocation-input').on('input', function() {
+ let $input = $(this);
+ let name = $input.data('name');
+ let type = $input.data('type');
+ let max_value = parseFloat($input.data('max'));
+ let value = parseFloat($input.val()) || 0;
+
+ // Validate against item's max
+ if (value > max_value) {
+ value = max_value;
+ $input.val(value.toFixed(2));
+ }
+
+ if (value < 0) {
+ value = 0;
+ $input.val(value.toFixed(2));
+ }
+
+ allocation_data[name] = {
+ value: value,
+ type: type
+ };
+
+ // Calculate totals
+ let total_vat_free = 0;
+ let total_vat_allocated = 0;
+
+ $wrapper.find('.allocation-input').each(function() {
+ let item_type = $(this).data('type');
+ let item_value = parseFloat($(this).val()) || 0;
+
+ if (item_type === 'vat_free') {
+ total_vat_free += item_value;
+ } else {
+ total_vat_allocated += item_value;
+ }
+ });
+
+ // Validate totals against available amounts (with tolerance)
+ const TOLERANCE = 0.01;
+
+ if (total_vat_free > max_vat_free + TOLERANCE) {
+ frappe.show_alert({
+ message: __('Total VAT Free allocation exceeds available amount'),
+ indicator: 'red'
+ });
+ // Reset this input
+ allocation_data[name] = { value: 0, type: type };
+ $input.val(0);
+ 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());
+ });
+}
+
+function auto_allocate(frm, payment_row_name, data, vat_free_items, vat_items, allocation_data, dialog) {
+ // Call server method to calculate auto allocation
+ 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_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;
+
+ // Reset all inputs
+ $wrapper.find('.allocation-input').val(0);
+
+ // Update allocation data
+ for (let key in allocation_data) {
+ allocation_data[key].value = 0;
+ }
+
+ // 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));
+ });
+
+ // Trigger input event to update summary
+ $wrapper.find('.allocation-input').first().trigger('input');
+
+ frappe.show_alert({
+ message: __('Auto allocation completed'),
+ indicator: 'green'
+ });
+ }
+ }
+ });
+}
+
+function save_allocation_data(frm, payment_row_name, allocation_data, dialog) {
+ // Prepare allocations array
+ let allocations = [];
+
+ for (let name in allocation_data) {
+ let data = allocation_data[name];
+ allocations.push({
+ name: name,
+ allocated: data.value,
+ type: data.type
+ });
+ }
+
+ // Save to server with loading indicator
+ frappe.dom.freeze(__('Saving allocation...'));
+
+ frappe.call({
+ method: 'taxes_az.taxes_az.doctype.vat_allocation.vat_allocation.save_allocation',
+ args: {
+ vat_allocation_name: frm.doc.name,
+ payment_entry_row_name: payment_row_name,
+ allocations: JSON.stringify(allocations)
+ },
+ callback: function(r) {
+ frappe.dom.unfreeze();
+
+ if (r.message && r.message.success) {
+ frappe.show_alert({
+ message: r.message.message + '
Total: ' + format_currency(r.message.total_allocated) +
+ '
VAT Free: ' + format_currency(r.message.vat_free_spent) +
+ '
VAT 18%: ' + format_currency(r.message.vat_allocated_spent),
+ indicator: 'green'
+ }, 7);
+
+ dialog.hide();
+ frm.reload_doc();
+ } else if (r.message && !r.message.success) {
+ frappe.msgprint({
+ title: __('Error'),
+ message: r.message.message,
+ indicator: 'red'
+ });
+ }
+ },
+ error: function(r) {
+ frappe.dom.unfreeze();
+ frappe.msgprint({
+ title: __('Error'),
+ message: __('Failed to save allocation. Please try again.'),
+ indicator: 'red'
+ });
+ }
+ });
+}
+
+function format_currency(value) {
+ return frappe.format(value, {fieldtype: 'Currency'});
+}
\ No newline at end of file
diff --git a/taxes_az/taxes_az/doctype/vat_allocation/vat_allocation.json b/taxes_az/taxes_az/doctype/vat_allocation/vat_allocation.json
new file mode 100644
index 0000000..824c528
--- /dev/null
+++ b/taxes_az/taxes_az/doctype/vat_allocation/vat_allocation.json
@@ -0,0 +1,97 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "creation": "2025-09-29 19:03:22.767217",
+ "doctype": "DocType",
+ "engine": "InnoDB",
+ "field_order": [
+ "date_section",
+ "year",
+ "column_break_ruqk",
+ "month",
+ "payment_entry_section",
+ "payment_entry_table",
+ "sales_invoice_section",
+ "sales_invoice_table",
+ "allocation_links_section",
+ "allocation_links"
+ ],
+ "fields": [
+ {
+ "fieldname": "date_section",
+ "fieldtype": "Section Break",
+ "label": "Date"
+ },
+ {
+ "fieldname": "year",
+ "fieldtype": "Int",
+ "label": "Year"
+ },
+ {
+ "fieldname": "column_break_ruqk",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "month",
+ "fieldtype": "Select",
+ "label": "Month",
+ "options": "January\nFebruary\nMarch\nApril\nMay\nJune\nJuly\nAugust\nSeptember\nOctober\nNovember\nDecember"
+ },
+ {
+ "fieldname": "payment_entry_section",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "payment_entry_table",
+ "fieldtype": "Table",
+ "label": "Payment Entry",
+ "options": "VAT allocation payment entry"
+ },
+ {
+ "fieldname": "sales_invoice_section",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "sales_invoice_table",
+ "fieldtype": "Table",
+ "label": "Sales Invoice",
+ "options": "VAT allocation sales invoice"
+ },
+ {
+ "fieldname": "allocation_links_section",
+ "fieldtype": "Section Break",
+ "label": "Allocation Links"
+ },
+ {
+ "fieldname": "allocation_links",
+ "fieldtype": "Table",
+ "hidden": 1,
+ "label": "Allocation Links",
+ "options": "VAT Allocation Link"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2025-10-01 21:26:07.923819",
+ "modified_by": "Administrator",
+ "module": "Taxes Az",
+ "name": "VAT allocation",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "creation",
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
diff --git a/taxes_az/taxes_az/doctype/vat_allocation/vat_allocation.py b/taxes_az/taxes_az/doctype/vat_allocation/vat_allocation.py
new file mode 100644
index 0000000..6a8faac
--- /dev/null
+++ b/taxes_az/taxes_az/doctype/vat_allocation/vat_allocation.py
@@ -0,0 +1,704 @@
+import frappe
+from frappe import _
+from frappe.model.document import Document
+from decimal import Decimal, ROUND_HALF_UP
+import json
+
+# Константа для tolerance округления
+ROUNDING_TOLERANCE = Decimal('0.01')
+
+class VATallocation(Document):
+ def validate(self):
+ # Check if document with same year and month already exists
+ self.validate_unique_year_month()
+ # Recalculate all totals from links
+ self.recalculate_totals_from_links()
+ # Validate all allocations
+ self.validate_allocations()
+
+ def validate_unique_year_month(self):
+ """
+ Ensure only one VAT allocation document exists per year and month
+ """
+ if not self.year or not self.month:
+ return
+
+ existing = frappe.db.exists({
+ 'doctype': 'VAT allocation',
+ 'year': self.year,
+ 'month': self.month,
+ 'name': ['!=', self.name]
+ })
+
+ if existing:
+ frappe.throw(_(f"VAT allocation for {self.month} {self.year} already exists: {existing}"))
+
+ def recalculate_totals_from_links(self):
+ """
+ Recalculate all allocated/unallocated amounts from allocation links
+ """
+ # Reset all allocated amounts
+ for pe_row in self.payment_entry_table:
+ pe_row.vat_free_allocated = 0
+ pe_row.vat_allocated_allocated = 0
+ pe_row.allocated_amount = 0
+
+ for si_row in self.sales_invoice_table:
+ si_row.allocated_amount = 0
+
+ # Calculate from links
+ for link in self.allocation_links:
+ allocated = Decimal(str(link.allocated_amount or 0))
+ if allocated <= 0:
+ continue
+
+ # Update payment entry row
+ for pe_row in self.payment_entry_table:
+ if pe_row.name == link.payment_entry_row:
+ current_allocated = Decimal(str(pe_row.allocated_amount or 0))
+ pe_row.allocated_amount = float(current_allocated + allocated)
+
+ if link.allocation_type == 'vat_free':
+ current_vat_free = Decimal(str(pe_row.vat_free_allocated or 0))
+ pe_row.vat_free_allocated = float(current_vat_free + allocated)
+ else:
+ current_vat_allocated = Decimal(str(pe_row.vat_allocated_allocated or 0))
+ pe_row.vat_allocated_allocated = float(current_vat_allocated + allocated)
+ break
+
+ # Update sales invoice row
+ 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)
+ break
+
+ # Calculate unallocated amounts
+ for pe_row in self.payment_entry_table:
+ total = Decimal(str(pe_row.payment_entry_amount or 0))
+ allocated = Decimal(str(pe_row.allocated_amount or 0))
+ pe_row.unallocated_amount = float(max(Decimal('0'), total - allocated))
+
+ for si_row in self.sales_invoice_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):
+ """
+ Validate that all allocations are mathematically correct
+ """
+ # Validate payment entries
+ for pe_row in self.payment_entry_table:
+ vat_free_total = Decimal(str(pe_row.vat_free_amount or 0))
+ vat_free_spent = Decimal(str(pe_row.vat_free_allocated or 0))
+ vat_allocated_total = Decimal(str(pe_row.vat_allocated_amount or 0))
+ vat_allocated_spent = Decimal(str(pe_row.vat_allocated_allocated or 0))
+
+ # Check non-negative
+ if vat_free_spent < 0 or vat_allocated_spent < 0:
+ frappe.throw(_(f"Spent amounts cannot be negative for payment entry {pe_row.payment_entry}"))
+
+ # Check not exceeding totals (with tolerance)
+ if vat_free_spent > vat_free_total + ROUNDING_TOLERANCE:
+ frappe.throw(_(f"VAT Free spent exceeds total for payment entry {pe_row.payment_entry}"))
+
+ 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
+ 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))
+ 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 {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 {si_row.item_tax_template}"))
+
+@frappe.whitelist()
+def validate_all_allocations(vat_allocation_name):
+ """
+ Manual validation function that can be called from UI
+ """
+ try:
+ doc = frappe.get_doc('VAT allocation', vat_allocation_name)
+ doc.validate_allocations()
+ return {
+ 'success': True,
+ 'message': _('All allocations are valid')
+ }
+ except Exception as e:
+ return {
+ 'success': False,
+ 'message': str(e)
+ }
+
+@frappe.whitelist()
+def get_payment_entries(year, month, customers=None, min_amount=None, max_amount=None):
+ """
+ Get Payment Entries for specified month and year with optional filters
+ customers can be a JSON string of customer list
+ """
+ # 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"))
+
+ # Build WHERE conditions
+ conditions = """
+ pe.docstatus = 1
+ AND YEAR(pe.posting_date) = %(year)s
+ AND MONTH(pe.posting_date) = %(month)s
+ AND pe.payment_type = 'Receive'
+ AND pe.party_type = 'Customer'
+ """
+
+ params = {
+ 'year': year,
+ 'month': month_number
+ }
+
+ # Add optional filters
+ if customers:
+ # Parse customers JSON string if it's a string
+ 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')]
+ if customer_list:
+ conditions += " AND pe.party IN %(customers)s"
+ params['customers'] = customer_list
+
+ if min_amount:
+ conditions += " AND pe.paid_amount >= %(min_amount)s"
+ params['min_amount'] = min_amount
+
+ if max_amount:
+ conditions += " AND pe.paid_amount <= %(max_amount)s"
+ params['max_amount'] = max_amount
+
+ # SQL query to get Payment Entries
+ payment_entries = frappe.db.sql(f"""
+ SELECT
+ pe.name as payment_entry,
+ pe.party as customer,
+ pe.paid_amount as payment_entry_amount,
+ pe.posting_date
+ FROM
+ `tabPayment Entry` pe
+ WHERE
+ {conditions}
+ ORDER BY
+ pe.posting_date
+ """, params, as_dict=1)
+
+ return payment_entries
+
+@frappe.whitelist()
+@frappe.validate_and_sanitize_search_inputs
+def get_journal_entry_query(doctype, txt, searchfield, start, page_len, filters):
+ """
+ Custom query for filtering Journal Entries
+ """
+ customer = filters.get('customer')
+ year = filters.get('year')
+ month = filters.get('month')
+
+ if not all([customer, 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 []
+
+ # SQL query to get matching Journal Entries
+ # txt is already sanitized by @frappe.validate_and_sanitize_search_inputs decorator
+ return frappe.db.sql("""
+ SELECT DISTINCT
+ je.name, je.posting_date, je.total_debit
+ FROM
+ `tabJournal Entry` je
+ WHERE
+ je.docstatus = 1
+ AND YEAR(je.posting_date) = %(year)s
+ AND MONTH(je.posting_date) = %(month)s
+ AND je.name LIKE %(txt)s
+ AND EXISTS (
+ SELECT 1
+ FROM `tabJournal Entry Account` jea_credit
+ WHERE jea_credit.parent = je.name
+ AND jea_credit.account LIKE '211%%'
+ AND jea_credit.credit_in_account_currency > 0
+ AND jea_credit.party = %(customer)s
+ )
+ AND EXISTS (
+ SELECT 1
+ FROM `tabJournal Entry Account` jea_debit
+ WHERE jea_debit.parent = je.name
+ AND jea_debit.account LIKE '226%%'
+ AND jea_debit.debit_in_account_currency > 0
+ )
+ ORDER BY
+ je.posting_date DESC
+ LIMIT %(start)s, %(page_len)s
+ """, {
+ 'year': year,
+ 'month': month_number,
+ 'customer': customer,
+ 'txt': f"%{txt}%",
+ 'start': start,
+ 'page_len': page_len
+ })
+
+@frappe.whitelist()
+def get_sales_invoice_items(year, month, customers=None, min_amount=None, max_amount=None):
+ """
+ Get Sales Invoice items for specified month and year, grouped by customer, item_tax_template, and tax_article
+ Includes all items, even those without tax_article
+ customers can be a JSON string of customer list
+ """
+ # 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"))
+
+ # Build WHERE conditions
+ conditions = """
+ si.docstatus = 1
+ AND YEAR(si.posting_date) = %(year)s
+ AND MONTH(si.posting_date) = %(month)s
+ """
+
+ params = {
+ 'year': year,
+ 'month': month_number
+ }
+
+ # Add optional customer filter
+ if customers:
+ # Parse customers JSON string if it's a string
+ 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')]
+ if customer_list:
+ conditions += " AND si.customer IN %(customers)s"
+ params['customers'] = customer_list
+
+ # SQL query with HAVING clause for amount filters after grouping
+ having_conditions = []
+
+ if min_amount:
+ having_conditions.append("SUM(sii.amount) >= %(min_amount)s")
+ params['min_amount'] = min_amount
+
+ if max_amount:
+ having_conditions.append("SUM(sii.amount) <= %(max_amount)s")
+ params['max_amount'] = max_amount
+
+ having_clause = ""
+ if having_conditions:
+ having_clause = " HAVING " + " AND ".join(having_conditions)
+
+ # SQL query to get Sales Invoice items grouped by customer, item_tax_template, and tax_article
+ invoice_items = frappe.db.sql(f"""
+ SELECT
+ si.customer,
+ sii.item_tax_template,
+ sii.tax_article,
+ SUM(sii.amount) as amount,
+ SUM(sii.net_amount) as net_amount,
+ SUM(sii.qty) as total_qty
+ FROM
+ `tabSales Invoice` si
+ INNER JOIN
+ `tabSales Invoice Item` sii ON si.name = sii.parent
+ WHERE
+ {conditions}
+ GROUP BY
+ si.customer, sii.item_tax_template, sii.tax_article
+ {having_clause}
+ ORDER BY
+ si.customer, sii.item_tax_template, sii.tax_article
+ """, params, as_dict=1)
+
+ return invoice_items
+
+@frappe.whitelist()
+def get_allocation_data(vat_allocation_name, payment_entry_row_name, customer):
+ """
+ Get data needed for allocation dialog with existing allocations
+ """
+ # Validate customer is active
+ customer_doc = frappe.get_cached_value('Customer', customer, ['disabled'], as_dict=True)
+ if customer_doc and customer_doc.get('disabled'):
+ frappe.throw(_("Customer {0} is disabled").format(customer))
+
+ # Get the payment entry row
+ payment_row = frappe.db.get_value(
+ 'VAT allocation payment entry',
+ payment_entry_row_name,
+ [
+ 'vat_free_amount',
+ 'vat_allocated_amount',
+ 'vat_free_allocated',
+ 'vat_allocated_allocated',
+ 'allocated_amount',
+ 'unallocated_amount'
+ ],
+ as_dict=1
+ )
+
+ if not payment_row:
+ frappe.throw(_("Payment entry row not found"))
+
+ # Calculate available amounts using Decimal for precision
+ vat_free_amount = Decimal(str(payment_row.vat_free_amount or 0))
+ vat_free_allocated = Decimal(str(payment_row.vat_free_allocated or 0))
+ vat_allocated_amount = Decimal(str(payment_row.vat_allocated_amount or 0))
+ vat_allocated_allocated = Decimal(str(payment_row.vat_allocated_allocated or 0))
+
+ vat_free_available = float(vat_free_amount - vat_free_allocated)
+ vat_allocated_available = float(vat_allocated_amount - vat_allocated_allocated)
+
+ # Get sales invoice items for this customer from the same VAT allocation document
+ doc = frappe.get_doc('VAT allocation', vat_allocation_name)
+
+ # Get existing allocations from this payment row
+ existing_allocations = {}
+ for link in doc.allocation_links:
+ if link.payment_entry_row == payment_entry_row_name:
+ existing_allocations[link.sales_invoice_row] = {
+ 'amount': link.allocated_amount,
+ 'type': link.allocation_type
+ }
+
+ # VAT Free templates (ƏDV 0% and ƏDV-dən azadolma)
+ vat_free_items = []
+ # VAT templates (ƏDV 18% and ƏDV daxil 18%)
+ vat_items = []
+
+ for item in doc.sales_invoice_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.append(item_data)
+ # Check if it's VAT 18% template
+ elif 'ədv 18%' in template or 'ədv daxil 18%' in template:
+ vat_items.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
+ }
+
+def round_decimal(value, precision=2):
+ """
+ Round decimal value to specified precision
+ """
+ if isinstance(value, (int, float)):
+ value = Decimal(str(value))
+ quantize_value = Decimal('0.1') ** precision
+ return float(value.quantize(quantize_value, rounding=ROUND_HALF_UP))
+
+@frappe.whitelist()
+def save_allocation(vat_allocation_name, payment_entry_row_name, allocations):
+ """
+ Save allocation data using links table
+ allocations is a JSON string with format:
+ [
+ {'name': 'si_row_name', 'allocated': amount, 'type': 'vat_free' or 'vat_allocated'},
+ ...
+ ]
+ """
+ try:
+ # Parse allocations
+ if isinstance(allocations, str):
+ allocations = json.loads(allocations)
+
+ # Validate input
+ if not isinstance(allocations, list):
+ frappe.throw(_("Invalid allocations data"))
+
+ # Get document
+ doc = frappe.get_doc('VAT allocation', vat_allocation_name)
+
+ # Check if document is submitted or cancelled
+ if doc.docstatus == 1:
+ frappe.throw(_("Cannot modify submitted document"))
+ if doc.docstatus == 2:
+ frappe.throw(_("Cannot modify cancelled document"))
+
+ # Find payment entry row
+ payment_row = None
+ for pe_row in doc.payment_entry_table:
+ if pe_row.name == payment_entry_row_name:
+ payment_row = pe_row
+ break
+
+ if not payment_row:
+ frappe.throw(_("Payment entry row not found"))
+
+ # Calculate what we're trying to allocate
+ total_vat_free_new = Decimal('0')
+ total_vat_allocated_new = Decimal('0')
+
+ for allocation in allocations:
+ allocated_amount = Decimal(str(allocation['allocated']))
+ if allocated_amount < 0:
+ frappe.throw(_("Allocated amount cannot be negative"))
+
+ if allocation['type'] == 'vat_free':
+ total_vat_free_new += allocated_amount
+ else:
+ total_vat_allocated_new += allocated_amount
+
+ # Check available amounts
+ vat_free_amount = Decimal(str(payment_row.vat_free_amount or 0))
+ vat_allocated_amount = Decimal(str(payment_row.vat_allocated_amount or 0))
+
+ # Validate totals with tolerance
+ if total_vat_free_new > vat_free_amount + ROUNDING_TOLERANCE:
+ frappe.throw(_(
+ "Total VAT Free allocation ({0}) exceeds total amount ({1})"
+ ).format(total_vat_free_new, vat_free_amount))
+
+ if total_vat_allocated_new > vat_allocated_amount + ROUNDING_TOLERANCE:
+ frappe.throw(_(
+ "Total VAT Allocated ({0}) exceeds total amount ({1})"
+ ).format(total_vat_allocated_new, vat_allocated_amount))
+
+ # Validate each SI item
+ si_items_map = {item.name: item for item in doc.sales_invoice_table}
+
+ for allocation in allocations:
+ si_row_name = allocation['name']
+ allocated_amount = Decimal(str(allocation['allocated']))
+
+ item = si_items_map.get(si_row_name)
+ if not item:
+ frappe.throw(_(f"Sales invoice item {si_row_name} not found"))
+
+ # Calculate how much is already allocated to this SI from OTHER payment rows
+ other_allocations = Decimal('0')
+ for link in doc.allocation_links:
+ if link.sales_invoice_row == si_row_name and link.payment_entry_row != payment_entry_row_name:
+ other_allocations += Decimal(str(link.allocated_amount or 0))
+
+ # Check if total (new + other) exceeds SI amount
+ total_for_si = allocated_amount + other_allocations
+ si_amount = Decimal(str(item.amount))
+
+ if total_for_si > si_amount + ROUNDING_TOLERANCE:
+ frappe.throw(_(
+ "Cannot allocate {0} to {1}. Total allocation would be {2}, but amount is only {3}."
+ ).format(allocated_amount, item.item_tax_template, total_for_si, si_amount))
+
+ # Remove existing links for this payment row
+ links_to_remove = []
+ for link in doc.allocation_links:
+ if link.payment_entry_row == payment_entry_row_name:
+ links_to_remove.append(link)
+
+ for link in links_to_remove:
+ doc.allocation_links.remove(link)
+
+ # Create new links
+ for allocation in allocations:
+ allocated_amount = Decimal(str(allocation['allocated']))
+
+ # Skip if amount is 0
+ if allocated_amount <= 0:
+ continue
+
+ link_row = doc.append('allocation_links', {})
+ link_row.payment_entry_row = payment_entry_row_name
+ link_row.sales_invoice_row = allocation['name']
+ link_row.allocated_amount = float(allocated_amount)
+ link_row.allocation_type = allocation['type']
+
+ # Save document (this will trigger recalculate_totals_from_links in validate)
+ doc.save()
+
+ # Create audit log
+ create_allocation_log(
+ vat_allocation_name=vat_allocation_name,
+ payment_entry_row_name=payment_entry_row_name,
+ allocations=allocations,
+ total_vat_free=float(total_vat_free_new),
+ total_vat_allocated=float(total_vat_allocated_new)
+ )
+
+ frappe.db.commit()
+
+ return {
+ 'success': True,
+ 'message': _('Allocation saved successfully'),
+ 'vat_free_spent': round_decimal(total_vat_free_new),
+ 'vat_allocated_spent': round_decimal(total_vat_allocated_new),
+ 'total_allocated': round_decimal(total_vat_free_new + total_vat_allocated_new)
+ }
+
+ except Exception as e:
+ frappe.db.rollback()
+ frappe.log_error(f"Allocation Save Error: {str(e)}", "VAT Allocation Error")
+ return {
+ 'success': False,
+ 'message': str(e)
+ }
+
+def create_allocation_log(vat_allocation_name, payment_entry_row_name, allocations,
+ total_vat_free, total_vat_allocated):
+ """
+ Create audit log for allocation
+ """
+ try:
+ log_doc = frappe.get_doc({
+ 'doctype': 'Comment',
+ 'comment_type': 'Info',
+ 'reference_doctype': 'VAT allocation',
+ 'reference_name': vat_allocation_name,
+ 'content': f"""
+ Allocation Updated
+ Payment Row: {payment_entry_row_name}
+ VAT Free: {total_vat_free}
+ VAT 18%: {total_vat_allocated}
+ Total: {total_vat_free + total_vat_allocated}
+ Items: {len([a for a in allocations if a['allocated'] > 0])}
+ User: {frappe.session.user}
+ Time: {frappe.utils.now_datetime()}
+ """
+ })
+ log_doc.insert(ignore_permissions=True)
+ except Exception as e:
+ frappe.log_error(f"Failed to create allocation log: {str(e)}", "Allocation Log Error")
+
+@frappe.whitelist()
+def calculate_auto_allocation(vat_free_available, vat_allocated_available, vat_free_items, vat_items):
+ """
+ Calculate smart auto allocation with improved rounding
+ """
+ if isinstance(vat_free_items, str):
+ vat_free_items = json.loads(vat_free_items)
+ if isinstance(vat_items, str):
+ vat_items = json.loads(vat_items)
+
+ vat_free_available = Decimal(str(vat_free_available))
+ vat_allocated_available = Decimal(str(vat_allocated_available))
+
+ allocations = []
+
+ # Allocate VAT free amount proportionally to VAT free items
+ if vat_free_available > 0 and len(vat_free_items) > 0:
+ # Calculate available for each item (total unallocated minus current allocation from this PE)
+ items_with_available = []
+ for item in vat_free_items:
+ available = Decimal(str(item['unallocated_amount'])) + Decimal(str(item.get('current_allocation', 0)))
+ if available > 0:
+ items_with_available.append({
+ 'name': item['name'],
+ 'available': available
+ })
+
+ if items_with_available:
+ total_available = sum(item['available'] for item in items_with_available)
+ remaining = vat_free_available
+
+ for i, item in enumerate(items_with_available):
+ if i == len(items_with_available) - 1:
+ # Last item gets the remainder
+ allocated = min(remaining, item['available'])
+ else:
+ # Proportional allocation
+ proportion = item['available'] / total_available
+ allocated = min(vat_free_available * proportion, item['available'])
+ allocated = allocated.quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
+
+ if allocated > 0:
+ allocations.append({
+ 'name': item['name'],
+ 'allocated': float(allocated),
+ 'type': 'vat_free'
+ })
+ remaining -= allocated
+
+ # Allocate VAT amount proportionally to VAT items
+ if vat_allocated_available > 0 and len(vat_items) > 0:
+ # Calculate available for each item
+ items_with_available = []
+ for item in vat_items:
+ available = Decimal(str(item['unallocated_amount'])) + Decimal(str(item.get('current_allocation', 0)))
+ if available > 0:
+ items_with_available.append({
+ 'name': item['name'],
+ 'available': available
+ })
+
+ if items_with_available:
+ total_available = sum(item['available'] for item in items_with_available)
+ remaining = vat_allocated_available
+
+ for i, item in enumerate(items_with_available):
+ if i == len(items_with_available) - 1:
+ # Last item gets the remainder
+ allocated = min(remaining, item['available'])
+ else:
+ # Proportional allocation
+ proportion = item['available'] / total_available
+ allocated = min(vat_allocated_available * proportion, item['available'])
+ allocated = allocated.quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
+
+ if allocated > 0:
+ allocations.append({
+ 'name': item['name'],
+ 'allocated': float(allocated),
+ 'type': 'vat_allocated'
+ })
+ remaining -= allocated
+
+ return allocations
\ No newline at end of file
diff --git a/taxes_az/taxes_az/doctype/vat_allocation_link/__init__.py b/taxes_az/taxes_az/doctype/vat_allocation_link/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/taxes_az/taxes_az/doctype/vat_allocation_link/vat_allocation_link.json b/taxes_az/taxes_az/doctype/vat_allocation_link/vat_allocation_link.json
new file mode 100644
index 0000000..d90e056
--- /dev/null
+++ b/taxes_az/taxes_az/doctype/vat_allocation_link/vat_allocation_link.json
@@ -0,0 +1,56 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "creation": "2025-10-01 20:00:00.000000",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "payment_entry_row",
+ "sales_invoice_row",
+ "allocated_amount",
+ "allocation_type"
+ ],
+ "fields": [
+ {
+ "fieldname": "payment_entry_row",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Payment Entry Row",
+ "read_only": 1
+ },
+ {
+ "fieldname": "sales_invoice_row",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Sales Invoice Row",
+ "read_only": 1
+ },
+ {
+ "fieldname": "allocated_amount",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Allocated Amount",
+ "precision": "2"
+ },
+ {
+ "fieldname": "allocation_type",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "label": "Type",
+ "options": "vat_free\nvat_allocated"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2025-10-01 20:00:00.000000",
+ "modified_by": "Administrator",
+ "module": "Taxes Az",
+ "name": "VAT Allocation Link",
+ "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_link/vat_allocation_link.py b/taxes_az/taxes_az/doctype/vat_allocation_link/vat_allocation_link.py
new file mode 100644
index 0000000..986bfa8
--- /dev/null
+++ b/taxes_az/taxes_az/doctype/vat_allocation_link/vat_allocation_link.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 VATAllocationLink(Document):
+ pass
diff --git a/taxes_az/taxes_az/doctype/vat_allocation_payment_entry/__init__.py b/taxes_az/taxes_az/doctype/vat_allocation_payment_entry/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/taxes_az/taxes_az/doctype/vat_allocation_payment_entry/vat_allocation_payment_entry.json b/taxes_az/taxes_az/doctype/vat_allocation_payment_entry/vat_allocation_payment_entry.json
new file mode 100644
index 0000000..3a721ba
--- /dev/null
+++ b/taxes_az/taxes_az/doctype/vat_allocation_payment_entry/vat_allocation_payment_entry.json
@@ -0,0 +1,122 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "creation": "2025-09-29 19:05:02.860158",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "customer",
+ "payment_entry",
+ "payment_entry_amount",
+ "vat_journal_entry",
+ "vat_amount",
+ "vat_allocated_amount",
+ "vat_allocated_allocated",
+ "vat_free_amount",
+ "vat_free_allocated",
+ "allocated_amount",
+ "unallocated_amount",
+ "allocate_button"
+ ],
+ "fields": [
+ {
+ "fieldname": "customer",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Customer",
+ "options": "Customer"
+ },
+ {
+ "fieldname": "payment_entry",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Payment Entry",
+ "options": "Payment Entry"
+ },
+ {
+ "fieldname": "payment_entry_amount",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Payment Entry Amount",
+ "precision": "2"
+ },
+ {
+ "fieldname": "vat_amount",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "VAT Amount",
+ "precision": "2"
+ },
+ {
+ "fieldname": "vat_journal_entry",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "VAT Journal Entry",
+ "options": "Journal Entry",
+ "precision": "2"
+ },
+ {
+ "fieldname": "vat_allocated_amount",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "VAT Allocated Amount",
+ "precision": "2",
+ "read_only": 1
+ },
+ {
+ "fieldname": "vat_allocated_allocated",
+ "fieldtype": "Currency",
+ "label": "VAT Allocated (Spent)",
+ "precision": "2",
+ "read_only": 1
+ },
+ {
+ "fieldname": "vat_free_amount",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "VAT Free Amount",
+ "precision": "2",
+ "read_only": 1
+ },
+ {
+ "fieldname": "vat_free_allocated",
+ "fieldtype": "Currency",
+ "label": "VAT Free (Spent)",
+ "precision": "2",
+ "read_only": 1
+ },
+ {
+ "fieldname": "allocated_amount",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Allocated amount",
+ "precision": "2"
+ },
+ {
+ "fieldname": "unallocated_amount",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Unallocated amount",
+ "precision": "2"
+ },
+ {
+ "fieldname": "allocate_button",
+ "fieldtype": "Button",
+ "in_list_view": 1,
+ "label": "Allocate"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2025-10-01 19:51:09.389265",
+ "modified_by": "Administrator",
+ "module": "Taxes Az",
+ "name": "VAT allocation payment entry",
+ "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_payment_entry/vat_allocation_payment_entry.py b/taxes_az/taxes_az/doctype/vat_allocation_payment_entry/vat_allocation_payment_entry.py
new file mode 100644
index 0000000..9968863
--- /dev/null
+++ b/taxes_az/taxes_az/doctype/vat_allocation_payment_entry/vat_allocation_payment_entry.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 VATallocationpaymententry(Document):
+ pass
diff --git a/taxes_az/taxes_az/doctype/vat_allocation_sales_invoice/__init__.py b/taxes_az/taxes_az/doctype/vat_allocation_sales_invoice/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/taxes_az/taxes_az/doctype/vat_allocation_sales_invoice/vat_allocation_sales_invoice.json b/taxes_az/taxes_az/doctype/vat_allocation_sales_invoice/vat_allocation_sales_invoice.json
new file mode 100644
index 0000000..5d07bb8
--- /dev/null
+++ b/taxes_az/taxes_az/doctype/vat_allocation_sales_invoice/vat_allocation_sales_invoice.json
@@ -0,0 +1,82 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "creation": "2025-09-29 20:34:31.450432",
+ "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"
+ },
+ {
+ "fieldname": "payment_entry",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Payment Entry",
+ "options": "Payment Entry"
+ },
+ {
+ "fieldname": "allocated_amount",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Allocated amount",
+ "precision": "2"
+ },
+ {
+ "fieldname": "unallocated_amount",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Unallocated amount",
+ "precision": "2"
+ },
+ {
+ "fieldname": "item_tax_template",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Item tax template",
+ "options": "Item Tax Template",
+ "precision": "2"
+ },
+ {
+ "fieldname": "tax_article",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Tax Article",
+ "options": "Tax Article",
+ "precision": "2"
+ },
+ {
+ "fieldname": "amount",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Amount",
+ "precision": "2"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2025-09-29 20:36:21.708944",
+ "modified_by": "Administrator",
+ "module": "Taxes Az",
+ "name": "VAT allocation sales invoice",
+ "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/vat_allocation_sales_invoice.py b/taxes_az/taxes_az/doctype/vat_allocation_sales_invoice/vat_allocation_sales_invoice.py
new file mode 100644
index 0000000..184efad
--- /dev/null
+++ b/taxes_az/taxes_az/doctype/vat_allocation_sales_invoice/vat_allocation_sales_invoice.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 VATallocationsalesinvoice(Document):
+ pass