added vat_allocation
This commit is contained in:
parent
7fff108480
commit
f121789432
|
|
@ -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 = `
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Sales Invoice</th>
|
||||
<th>Date</th>
|
||||
<th>Customer</th>
|
||||
<th>Item Code</th>
|
||||
<th>Item Name</th>
|
||||
<th>Qty</th>
|
||||
<th>Rate</th>
|
||||
<th>Amount</th>
|
||||
<th>Tax Article</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
`;
|
||||
|
||||
let total_amount = 0;
|
||||
|
||||
data.forEach(function(row) {
|
||||
html += `
|
||||
<tr>
|
||||
<td><a href="/app/sales-invoice/${row.parent}" target="_blank">${row.parent}</a></td>
|
||||
<td>${frappe.datetime.str_to_user(row.posting_date)}</td>
|
||||
<td>${row.customer || ''}</td>
|
||||
<td>${row.item_code || ''}</td>
|
||||
<td>${row.item_name || ''}</td>
|
||||
<td>${row.qty || 0}</td>
|
||||
<td>${format_currency(row.rate || 0)}</td>
|
||||
<td>${format_currency(row.amount || 0)}</td>
|
||||
<td>${row.tax_article || ''}</td>
|
||||
</tr>
|
||||
`;
|
||||
total_amount += (row.amount || 0);
|
||||
});
|
||||
|
||||
html += `
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="text-bold">
|
||||
<td colspan="7">Total</td>
|
||||
<td>${format_currency(total_amount)}</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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('<div class="text-muted">Select Tax Article to see results</div>');
|
||||
frm.set_value('total_items_found', 0);
|
||||
}
|
||||
|
|
@ -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": "<div id='tax-article-items-container'>Select Tax Article and click Refresh to see results</div>"
|
||||
}
|
||||
],
|
||||
"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
|
||||
}
|
||||
|
|
@ -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 {}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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 = `
|
||||
<div style="padding: 10px; background-color: #f8f9fa; border-radius: 5px; margin-bottom: 10px;">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<strong>Total VAT Free:</strong> ${format_currency(data.payment_row.vat_free_amount)}<br>
|
||||
<strong>VAT Free Spent:</strong> ${format_currency(data.payment_row.vat_free_allocated || 0)}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<strong>Total VAT Allocated:</strong> ${format_currency(data.payment_row.vat_allocated_amount)}<br>
|
||||
<strong>VAT Allocated Spent:</strong> ${format_currency(data.payment_row.vat_allocated_allocated || 0)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
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 = '<div class="allocation-container" style="max-height: 400px; overflow-y: auto;">';
|
||||
|
||||
// VAT Free items section
|
||||
if (vat_free_items.length > 0) {
|
||||
html += '<h5 style="margin-top: 10px; color: #2490ef;">VAT Free Items (ƏDV 0%, ƏDV-dən azadolma)</h5>';
|
||||
html += '<table class="table table-bordered" style="margin-bottom: 20px;">';
|
||||
html += '<thead><tr>';
|
||||
html += '<th>Item Tax Template</th>';
|
||||
html += '<th>Tax Article</th>';
|
||||
html += '<th>Total Amount</th>';
|
||||
html += '<th>Unallocated</th>';
|
||||
html += '<th>Current Alloc</th>';
|
||||
html += '<th style="width: 150px;">New Allocation</th>';
|
||||
html += '</tr></thead><tbody>';
|
||||
|
||||
vat_free_items.forEach(function(item) {
|
||||
allocation_data[item.name] = { value: item.current_allocation || 0, type: 'vat_free' };
|
||||
html += '<tr>';
|
||||
html += '<td>' + (item.item_tax_template || '-') + '</td>';
|
||||
html += '<td>' + (item.tax_article || '-') + '</td>';
|
||||
html += '<td>' + format_currency(item.amount) + '</td>';
|
||||
html += '<td>' + format_currency(item.unallocated_amount) + '</td>';
|
||||
html += '<td style="background-color: #fff3cd;">' + format_currency(item.current_allocation || 0) + '</td>';
|
||||
html += '<td><input type="number" class="form-control allocation-input" data-name="' + item.name + '" data-type="vat_free" data-max="' + (item.unallocated_amount + (item.current_allocation || 0)) + '" value="' + (item.current_allocation || 0) + '" min="0" step="0.01"></td>';
|
||||
html += '</tr>';
|
||||
});
|
||||
|
||||
html += '</tbody></table>';
|
||||
}
|
||||
|
||||
// VAT 18% items section
|
||||
if (vat_items.length > 0) {
|
||||
html += '<h5 style="margin-top: 10px; color: #f56b00;">VAT 18% Items (ƏDV 18%, ƏDV daxil 18%)</h5>';
|
||||
html += '<table class="table table-bordered">';
|
||||
html += '<thead><tr>';
|
||||
html += '<th>Item Tax Template</th>';
|
||||
html += '<th>Tax Article</th>';
|
||||
html += '<th>Total Amount</th>';
|
||||
html += '<th>Unallocated</th>';
|
||||
html += '<th>Current Alloc</th>';
|
||||
html += '<th style="width: 150px;">New Allocation</th>';
|
||||
html += '</tr></thead><tbody>';
|
||||
|
||||
vat_items.forEach(function(item) {
|
||||
allocation_data[item.name] = { value: item.current_allocation || 0, type: 'vat_allocated' };
|
||||
html += '<tr>';
|
||||
html += '<td>' + (item.item_tax_template || '-') + '</td>';
|
||||
html += '<td>' + (item.tax_article || '-') + '</td>';
|
||||
html += '<td>' + format_currency(item.amount) + '</td>';
|
||||
html += '<td>' + format_currency(item.unallocated_amount) + '</td>';
|
||||
html += '<td style="background-color: #fff3cd;">' + format_currency(item.current_allocation || 0) + '</td>';
|
||||
html += '<td><input type="number" class="form-control allocation-input" data-name="' + item.name + '" data-type="vat_allocated" data-max="' + (item.unallocated_amount + (item.current_allocation || 0)) + '" value="' + (item.current_allocation || 0) + '" min="0" step="0.01"></td>';
|
||||
html += '</tr>';
|
||||
});
|
||||
|
||||
html += '</tbody></table>';
|
||||
}
|
||||
|
||||
// Summary section
|
||||
let formatted_zero = $(frappe.format(0, {fieldtype: 'Currency'})).text();
|
||||
|
||||
html += '<div class="allocation-summary" style="margin-top: 20px; padding: 10px; background-color: #f8f9fa; border-radius: 5px;">';
|
||||
html += '<div class="row">';
|
||||
html += '<div class="col-md-6"><strong>Total VAT Free to Allocate:</strong> <span id="total_vat_free" class="text-primary">' + formatted_zero + '</span></div>';
|
||||
html += '<div class="col-md-6"><strong>Total VAT to Allocate:</strong> <span id="total_vat_allocated" class="text-warning">' + formatted_zero + '</span></div>';
|
||||
html += '</div>';
|
||||
html += '<div class="row" style="margin-top: 10px;">';
|
||||
html += '<div class="col-md-12"><strong>Total:</strong> <span id="total_allocated" class="text-success">' + formatted_zero + '</span></div>';
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
|
||||
html += '</div>';
|
||||
|
||||
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 + '<br>Total: ' + format_currency(r.message.total_allocated) +
|
||||
'<br>VAT Free: ' + format_currency(r.message.vat_free_spent) +
|
||||
'<br>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'});
|
||||
}
|
||||
|
|
@ -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": []
|
||||
}
|
||||
|
|
@ -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"""
|
||||
<b>Allocation Updated</b><br>
|
||||
Payment Row: {payment_entry_row_name}<br>
|
||||
VAT Free: {total_vat_free}<br>
|
||||
VAT 18%: {total_vat_allocated}<br>
|
||||
Total: {total_vat_free + total_vat_allocated}<br>
|
||||
Items: {len([a for a in allocations if a['allocated'] > 0])}<br>
|
||||
User: {frappe.session.user}<br>
|
||||
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
|
||||
|
|
@ -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": []
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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": []
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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": []
|
||||
}
|
||||
|
|
@ -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
|
||||
Loading…
Reference in New Issue