feat(vat-allocation): group SI rows by customer+template with multi tax articles

Sales Invoice moved its single tax_article (Link) to a multi-select child table,
which inflated VAT Allocation: one line citing N articles produced N rows of the
full amount (e.g. 1500 -> 6000), so a 1500 payment only covered one article.

Group sales-invoice rows by (customer, item_tax_template) and union the cited
articles into a JSON tax_articles list, summing the amount once (the article
aggregation is computed separately so the JOIN can no longer multiply amounts).
The tax-articles report parses the list and replicates each row's amount/odenis
into every cited maddE column (same turnover under several articles, as on e-taxes).

The grid shows a translatable "N articles" cell that opens a read-only pill popup.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ali 2026-06-05 11:15:15 +00:00
parent 7478d4d66c
commit 129a87711f
4 changed files with 236 additions and 54 deletions

View File

@ -23,6 +23,9 @@ frappe.ui.form.on('VAT allocation', {
// Setup filter for Payment Entry // Setup filter for Payment Entry
setup_payment_entry_query(frm); setup_payment_entry_query(frm);
// Tax Articles: grid labels + click-to-view popup
setup_tax_articles_viewer(frm);
} }
}); });
@ -225,6 +228,100 @@ function setup_payment_entry_query(frm) {
}; };
} }
// ---- Tax Articles helpers (stored as a JSON list of Tax Article names) ----
// Parse the stored tax_articles value into an array of names.
function parse_tax_articles_value(value) {
if (!value) return [];
if (Array.isArray(value)) return value.filter(Boolean);
if (typeof value === 'string') {
try {
let parsed = JSON.parse(value);
if (Array.isArray(parsed)) return parsed.filter(Boolean);
return parsed ? [parsed] : [];
} catch (e) {
return [value];
}
}
return [String(value)];
}
// Comma-separated label (used inside the allocation dialog).
function format_tax_articles(value) {
let articles = parse_tax_articles_value(value);
return articles.length ? frappe.utils.escape_html(articles.join(', ')) : '-';
}
// Short label shown in the grid cell, e.g. "3 articles" (translatable).
function format_tax_articles_count(value) {
let n = parse_tax_articles_value(value).length;
return n ? __('{0} articles', [n]) : '';
}
// Read-only popup showing the articles as pills (same look as the Sales Invoice
// "Tax Articles" control). Rendered as plain styled chips so it does not depend on
// the Sales Invoice Tax Article meta being loaded on this form, and cannot be edited.
function show_tax_articles_view(value) {
let items = parse_tax_articles_value(value);
let pills = items.length
? items.map(function(a) {
return '<span class="tax-article-pill">' + frappe.utils.escape_html(a) + '</span>';
}).join('')
: '<div class="text-muted">' + __('No tax articles') + '</div>';
let d = new frappe.ui.Dialog({
title: __('Tax Articles'),
fields: [{
fieldtype: 'HTML',
fieldname: 'body',
options: '<div class="tax-article-pills">' + pills + '</div>'
}],
primary_action_label: __('Close'),
primary_action() { d.hide(); }
});
d.show();
}
// Recompute grid labels for loaded rows and bind the click-to-view handler.
function setup_tax_articles_viewer(frm) {
['sales_invoice_table', 'sales_invoice_previous_table'].forEach(function(tbl) {
(frm.doc[tbl] || []).forEach(function(row) {
let label = format_tax_articles_count(row.tax_articles);
if (row.tax_articles_display !== label) {
row.tax_articles_display = label; // direct set: cosmetic, must not dirty the form
}
});
frm.refresh_field(tbl);
});
if (frm.__tax_articles_viewer_bound) return;
frm.__tax_articles_viewer_bound = true;
if (!document.getElementById('vat-tax-articles-style')) {
let style = document.createElement('style');
style.id = 'vat-tax-articles-style';
style.textContent =
'.grid-row [data-fieldname="tax_articles_display"]{cursor:pointer;}' +
'.grid-row [data-fieldname="tax_articles_display"] input,' +
'.grid-row [data-fieldname="tax_articles_display"] .like-disabled-input{pointer-events:none;}' +
// read-only pill chips (same look as the SI Tax Articles control)
'.tax-article-pills{display:flex;flex-wrap:wrap;gap:6px;padding:4px 0;}' +
'.tax-article-pill{display:inline-block;background:var(--control-bg,#f4f5f6);' +
'border:1px solid var(--border-color,#d1d8dd);border-radius:10px;' +
'padding:4px 10px;font-size:12px;line-height:1.5;color:var(--text-color,#1f272e);}';
document.head.appendChild(style);
}
$(document)
.off('mousedown.vataa')
.on('mousedown.vataa', '[data-fieldname="tax_articles_display"]', function(ev) {
let grid_row = $(this).closest('.grid-row').data('grid_row');
if (grid_row && grid_row.doc) {
ev.stopPropagation();
show_tax_articles_view(grid_row.doc.tax_articles);
}
});
}
// Function to fill Sales Invoice Items with filters - UPDATED to fill both tables // Function to fill Sales Invoice Items with filters - UPDATED to fill both tables
function fill_sales_invoice_items(frm, filters) { function fill_sales_invoice_items(frm, filters) {
// Call both server methods - current month and previous months // Call both server methods - current month and previous months
@ -247,7 +344,8 @@ function fill_sales_invoice_items(frm, filters) {
let row = frm.add_child('sales_invoice_table'); let row = frm.add_child('sales_invoice_table');
row.customer = item.customer; row.customer = item.customer;
row.item_tax_template = item.item_tax_template; row.item_tax_template = item.item_tax_template;
row.tax_article = item.tax_article; row.tax_articles = item.tax_articles;
row.tax_articles_display = format_tax_articles_count(item.tax_articles);
row.amount = item.amount; row.amount = item.amount;
row.unallocated_amount = item.amount; // Default: all amount is unallocated row.unallocated_amount = item.amount; // Default: all amount is unallocated
}); });
@ -285,7 +383,8 @@ function load_previous_months_items(frm, filters) {
let row = frm.add_child('sales_invoice_previous_table'); let row = frm.add_child('sales_invoice_previous_table');
row.customer = item.customer; row.customer = item.customer;
row.item_tax_template = item.item_tax_template; row.item_tax_template = item.item_tax_template;
row.tax_article = item.tax_article; row.tax_articles = item.tax_articles;
row.tax_articles_display = format_tax_articles_count(item.tax_articles);
row.amount = item.amount; row.amount = item.amount;
row.unallocated_amount = item.unallocated_amount; row.unallocated_amount = item.unallocated_amount;
row.allocated_amount = item.allocated_amount; row.allocated_amount = item.allocated_amount;
@ -707,7 +806,7 @@ function show_allocation_dialog(frm, payment_row_idx, customer) {
let item_data = { let item_data = {
name: item.name, name: item.name,
item_tax_template: item.item_tax_template || '', item_tax_template: item.item_tax_template || '',
tax_article: item.tax_article || '', tax_articles: item.tax_articles || '',
amount: item.amount || 0, amount: item.amount || 0,
unallocated_amount: item.unallocated_amount || 0, unallocated_amount: item.unallocated_amount || 0,
allocated_amount: item.allocated_amount || 0, allocated_amount: item.allocated_amount || 0,
@ -742,7 +841,7 @@ function show_allocation_dialog(frm, payment_row_idx, customer) {
let item_data = { let item_data = {
name: item.name, name: item.name,
item_tax_template: item.item_tax_template || '', item_tax_template: item.item_tax_template || '',
tax_article: item.tax_article || '', tax_articles: item.tax_articles || '',
amount: item.amount || 0, amount: item.amount || 0,
unallocated_amount: item.unallocated_amount || 0, unallocated_amount: item.unallocated_amount || 0,
allocated_amount: item.allocated_amount || 0, allocated_amount: item.allocated_amount || 0,
@ -1099,7 +1198,7 @@ function build_allocation_html(vat_free_items, vat_items, allocation_data, tab_t
html += '<table class="table table-bordered" style="margin-bottom: 20px;">'; html += '<table class="table table-bordered" style="margin-bottom: 20px;">';
html += '<thead><tr>'; html += '<thead><tr>';
html += '<th>Item Tax Template</th>'; html += '<th>Item Tax Template</th>';
html += '<th>Tax Article</th>'; html += '<th>Tax Articles</th>';
html += '<th>Total Amount</th>'; html += '<th>Total Amount</th>';
html += '<th>Unallocated</th>'; html += '<th>Unallocated</th>';
html += '<th>Current Alloc</th>'; html += '<th>Current Alloc</th>';
@ -1110,7 +1209,7 @@ function build_allocation_html(vat_free_items, vat_items, allocation_data, tab_t
allocation_data[item.name] = { value: item.current_allocation || 0, type: 'vat_free' }; allocation_data[item.name] = { value: item.current_allocation || 0, type: 'vat_free' };
html += '<tr>'; html += '<tr>';
html += '<td>' + (item.item_tax_template || '-') + '</td>'; html += '<td>' + (item.item_tax_template || '-') + '</td>';
html += '<td>' + (item.tax_article || '-') + '</td>'; html += '<td>' + format_tax_articles(item.tax_articles) + '</td>';
html += '<td>' + format_currency(item.amount) + '</td>'; html += '<td>' + format_currency(item.amount) + '</td>';
html += '<td>' + format_currency(item.unallocated_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 style="background-color: #fff3cd;">' + format_currency(item.current_allocation || 0) + '</td>';
@ -1127,7 +1226,7 @@ function build_allocation_html(vat_free_items, vat_items, allocation_data, tab_t
html += '<table class="table table-bordered">'; html += '<table class="table table-bordered">';
html += '<thead><tr>'; html += '<thead><tr>';
html += '<th>Item Tax Template</th>'; html += '<th>Item Tax Template</th>';
html += '<th>Tax Article</th>'; html += '<th>Tax Articles</th>';
html += '<th>Total Amount</th>'; html += '<th>Total Amount</th>';
html += '<th>Unallocated</th>'; html += '<th>Unallocated</th>';
html += '<th>Current Alloc</th>'; html += '<th>Current Alloc</th>';
@ -1138,7 +1237,7 @@ function build_allocation_html(vat_free_items, vat_items, allocation_data, tab_t
allocation_data[item.name] = { value: item.current_allocation || 0, type: 'vat_allocated' }; allocation_data[item.name] = { value: item.current_allocation || 0, type: 'vat_allocated' };
html += '<tr>'; html += '<tr>';
html += '<td>' + (item.item_tax_template || '-') + '</td>'; html += '<td>' + (item.item_tax_template || '-') + '</td>';
html += '<td>' + (item.tax_article || '-') + '</td>'; html += '<td>' + format_tax_articles(item.tax_articles) + '</td>';
html += '<td>' + format_currency(item.amount) + '</td>'; html += '<td>' + format_currency(item.amount) + '</td>';
html += '<td>' + format_currency(item.unallocated_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 style="background-color: #fff3cd;">' + format_currency(item.current_allocation || 0) + '</td>';

View File

@ -7,6 +7,26 @@ import json
# Константа для tolerance округления # Константа для tolerance округления
ROUNDING_TOLERANCE = Decimal('0.01') ROUNDING_TOLERANCE = Decimal('0.01')
def parse_tax_articles(value):
"""Parse the stored ``tax_articles`` value into a list of Tax Article names.
Rows store the set of articles as a JSON list (e.g. ``["A", "B"]``). This
tolerates empty values and a legacy single-article string.
"""
if not value:
return []
if isinstance(value, (list, tuple, set)):
return [a for a in value if a]
try:
parsed = json.loads(value)
except (ValueError, TypeError):
return [value]
if isinstance(parsed, list):
return [a for a in parsed if a]
return [parsed] if parsed else []
class VATallocation(Document): class VATallocation(Document):
def autoname(self): def autoname(self):
""" """
@ -430,8 +450,9 @@ def get_journal_entry_query(doctype, txt, searchfield, start, page_len, filters)
@frappe.whitelist() @frappe.whitelist()
def get_sales_invoice_items(year, month, customers=None, min_amount=None, max_amount=None): 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 Get Sales Invoice items for specified month and year, grouped by customer and
Includes all items, even those without tax_article item_tax_template. Only lines that carry at least one tax article are included;
their articles are unioned into a JSON ``tax_articles`` list per group.
customers can be a JSON string of customer list customers can be a JSON string of customer list
""" """
# Convert month name to number # Convert month name to number
@ -484,37 +505,72 @@ def get_sales_invoice_items(year, month, customers=None, min_amount=None, max_am
if having_conditions: if having_conditions:
having_clause = " HAVING " + " AND ".join(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 # Amounts grouped by (customer, item_tax_template). We deliberately do NOT
# join the tax-article child table here: a line carrying N articles would be
# multiplied into N joined rows and SUM(amount) would inflate N-fold. Only
# lines that carry at least one tax article participate (EXISTS), matching the
# previous behaviour where article-less lines were excluded.
invoice_items = frappe.db.sql(f""" invoice_items = frappe.db.sql(f"""
SELECT SELECT
si.customer, si.customer,
sii.item_tax_template, sii.item_tax_template,
sita.tax_article,
SUM(sii.amount) as amount, SUM(sii.amount) as amount,
SUM(sii.net_amount) as net_amount, SUM(sii.net_amount) as net_amount,
SUM(sii.qty) as total_qty SUM(sii.qty) as total_qty
FROM FROM
`tabSales Invoice` si `tabSales Invoice` si
INNER JOIN INNER JOIN
`tabSales Invoice Item` sii ON si.name = sii.parent
WHERE
{conditions}
AND EXISTS (
SELECT 1 FROM `tabSales Invoice Tax Article` sita
WHERE sita.parent = sii.parent
AND sita.parentfield = 'custom_item_tax_articles'
AND sita.parent_row = sii.custom_si_row_key
)
GROUP BY
si.customer, sii.item_tax_template
{having_clause}
ORDER BY
si.customer, sii.item_tax_template
""", params, as_dict=1)
# Distinct tax articles per (customer, item_tax_template), aggregated
# separately so the JOIN cannot inflate amounts, then unioned in Python.
article_rows = frappe.db.sql(f"""
SELECT DISTINCT
si.customer,
sii.item_tax_template,
sita.tax_article
FROM
`tabSales Invoice` si
INNER JOIN
`tabSales Invoice Item` sii ON si.name = sii.parent `tabSales Invoice Item` sii ON si.name = sii.parent
INNER JOIN INNER JOIN
`tabSales Invoice Tax Article` sita ON sita.parent = sii.parent AND sita.parentfield = 'custom_item_tax_articles' AND sita.parent_row = sii.custom_si_row_key `tabSales Invoice Tax Article` sita ON sita.parent = sii.parent AND sita.parentfield = 'custom_item_tax_articles' AND sita.parent_row = sii.custom_si_row_key
WHERE WHERE
{conditions} {conditions}
GROUP BY
si.customer, sii.item_tax_template, sita.tax_article
{having_clause}
ORDER BY
si.customer, sii.item_tax_template, sita.tax_article
""", params, as_dict=1) """, params, as_dict=1)
articles_by_group = {}
for row in article_rows:
if not row.tax_article:
continue
key = (row.customer, row.item_tax_template or '')
articles_by_group.setdefault(key, set()).add(row.tax_article)
for item in invoice_items:
key = (item.customer, item.item_tax_template or '')
item['tax_articles'] = json.dumps(sorted(articles_by_group.get(key, set())), ensure_ascii=False)
return invoice_items return invoice_items
@frappe.whitelist() @frappe.whitelist()
def get_sales_invoice_items_previous_months(year, month, customers=None, min_amount=None, max_amount=None): def get_sales_invoice_items_previous_months(year, month, customers=None, min_amount=None, max_amount=None):
""" """
Get Sales Invoice items from previous VAT Allocation documents where unallocated_amount > 0 Get Sales Invoice items from previous VAT Allocation documents where unallocated_amount > 0
Grouped by customer, item_tax_template, and tax_article Grouped by customer and item_tax_template, unioning their tax_articles lists
""" """
# Convert month name to number # Convert month name to number
months = { months = {
@ -580,10 +636,10 @@ def get_sales_invoice_items_previous_months(year, month, customers=None, min_amo
for vat_alloc in vat_allocations: for vat_alloc in vat_allocations:
# Get sales invoice items from this VAT Allocation # Get sales invoice items from this VAT Allocation
items = frappe.db.sql(""" items = frappe.db.sql("""
SELECT SELECT
customer, customer,
item_tax_template, item_tax_template,
tax_article, tax_articles,
unallocated_amount unallocated_amount
FROM `tabVAT allocation sales invoice` FROM `tabVAT allocation sales invoice`
WHERE parent = %(parent)s WHERE parent = %(parent)s
@ -591,51 +647,53 @@ def get_sales_invoice_items_previous_months(year, month, customers=None, min_amo
""", { """, {
'parent': vat_alloc.name 'parent': vat_alloc.name
}, as_dict=1) }, as_dict=1)
all_items.extend(items) all_items.extend(items)
# Group by customer, item_tax_template, tax_article and sum unallocated amounts # Group by customer, item_tax_template and sum unallocated amounts,
# unioning the tax-article sets across rows.
grouped_items = {} grouped_items = {}
for item in all_items: for item in all_items:
# Apply customer filter # Apply customer filter
if customer_list and item.customer not in customer_list: if customer_list and item.customer not in customer_list:
continue continue
key = (item.customer, item.item_tax_template or '', item.tax_article or '') key = (item.customer, item.item_tax_template or '')
if key not in grouped_items: if key not in grouped_items:
grouped_items[key] = { grouped_items[key] = {
'customer': item.customer, 'customer': item.customer,
'item_tax_template': item.item_tax_template, 'item_tax_template': item.item_tax_template,
'tax_article': item.tax_article, 'tax_articles': set(),
'amount': Decimal('0') 'amount': Decimal('0')
} }
grouped_items[key]['amount'] += Decimal(str(item.unallocated_amount)) grouped_items[key]['amount'] += Decimal(str(item.unallocated_amount))
grouped_items[key]['tax_articles'].update(parse_tax_articles(item.get('tax_articles')))
# Convert to list and apply amount filters # Convert to list and apply amount filters
result = [] result = []
for key, item in grouped_items.items(): for key, item in grouped_items.items():
amount = float(item['amount']) amount = float(item['amount'])
# Apply min/max amount filters # Apply min/max amount filters
if min_amount and amount < float(min_amount): if min_amount and amount < float(min_amount):
continue continue
if max_amount and amount > float(max_amount): if max_amount and amount > float(max_amount):
continue continue
result.append({ result.append({
'customer': item['customer'], 'customer': item['customer'],
'item_tax_template': item['item_tax_template'], 'item_tax_template': item['item_tax_template'],
'tax_article': item['tax_article'], 'tax_articles': json.dumps(sorted(item['tax_articles']), ensure_ascii=False),
'amount': amount, 'amount': amount,
'unallocated_amount': amount, # Initially all is unallocated 'unallocated_amount': amount, # Initially all is unallocated
'allocated_amount': 0 'allocated_amount': 0
}) })
# Sort by customer, item_tax_template, tax_article # Sort by customer, item_tax_template
result.sort(key=lambda x: (x['customer'], x['item_tax_template'] or '', x['tax_article'] or '')) result.sort(key=lambda x: (x['customer'], x['item_tax_template'] or ''))
return result return result
@ -705,7 +763,7 @@ def get_allocation_data(vat_allocation_name, payment_entry_row_name, customer):
item_data = { item_data = {
'name': item.name, 'name': item.name,
'item_tax_template': item.item_tax_template or '', 'item_tax_template': item.item_tax_template or '',
'tax_article': item.tax_article or '', 'tax_articles': item.tax_articles or '',
'amount': item.amount, 'amount': item.amount,
'unallocated_amount': item.unallocated_amount, 'unallocated_amount': item.unallocated_amount,
'allocated_amount': item.allocated_amount or 0, 'allocated_amount': item.allocated_amount or 0,
@ -730,7 +788,7 @@ def get_allocation_data(vat_allocation_name, payment_entry_row_name, customer):
item_data = { item_data = {
'name': item.name, 'name': item.name,
'item_tax_template': item.item_tax_template or '', 'item_tax_template': item.item_tax_template or '',
'tax_article': item.tax_article or '', 'tax_articles': item.tax_articles or '',
'amount': item.amount, 'amount': item.amount,
'unallocated_amount': item.unallocated_amount, 'unallocated_amount': item.unallocated_amount,
'allocated_amount': item.allocated_amount or 0, 'allocated_amount': item.allocated_amount or 0,

View File

@ -9,7 +9,8 @@
"customer", "customer",
"payment_entry", "payment_entry",
"item_tax_template", "item_tax_template",
"tax_article", "tax_articles_display",
"tax_articles",
"amount", "amount",
"allocated_amount", "allocated_amount",
"unallocated_amount" "unallocated_amount"
@ -57,12 +58,17 @@
"read_only": 1 "read_only": 1
}, },
{ {
"fieldname": "tax_article", "fieldname": "tax_articles_display",
"fieldtype": "Link", "fieldtype": "Data",
"in_list_view": 1, "in_list_view": 1,
"label": "Tax Article", "label": "Tax Articles",
"options": "Tax Article", "read_only": 1
"precision": "2", },
{
"fieldname": "tax_articles",
"fieldtype": "Small Text",
"hidden": 1,
"label": "Tax Articles (raw)",
"read_only": 1 "read_only": 1
}, },
{ {

View File

@ -1,6 +1,8 @@
# Copyright (c) 2026, Your Company and contributors # Copyright (c) 2026, Your Company and contributors
# For license information, please see license.txt # For license information, please see license.txt
import json
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils import flt from frappe.utils import flt
@ -11,6 +13,19 @@ def execute(filters=None):
return columns, data return columns, data
def _parse_tax_articles(value):
"""Parse the stored ``tax_articles`` JSON list; tolerate empty/legacy values."""
if not value:
return []
try:
parsed = json.loads(value)
except (ValueError, TypeError):
return [value]
if isinstance(parsed, list):
return [a for a in parsed if a]
return [parsed] if parsed else []
def get_data(filters): def get_data(filters):
"""Получаем данные по VAT Allocation с распределением по налоговым статьям""" """Получаем данные по VAT Allocation с распределением по налоговым статьям"""
@ -45,9 +60,13 @@ def get_data(filters):
allocations_data[allocation_name][field_key] = 0 allocations_data[allocation_name][field_key] = 0
allocations_data[allocation_name][f"{field_key}_odenis"] = 0 allocations_data[allocation_name][f"{field_key}_odenis"] = 0
# Получаем полное название статьи по сокращенному name # Каждая строка несёт JSON-список статей; одна и та же сумма строки
tax_article_name = item['tax_article'] # попадает в КАЖДУЮ статью из списка (как на ФНО, где один оборот может
if tax_article_name and tax_article_name in tax_article_names_mapping: # стоять под несколькими maddə).
for tax_article_name in _parse_tax_articles(item.get('tax_articles')):
if tax_article_name not in tax_article_names_mapping:
continue
full_article_name = tax_article_names_mapping[tax_article_name] full_article_name = tax_article_names_mapping[tax_article_name]
# Проверяем есть ли маппинг для полного названия # Проверяем есть ли маппинг для полного названия
@ -100,8 +119,8 @@ def get_vat_allocation_items(filters):
# Только строки с amount > 0 или allocated_amount > 0 # Только строки с amount > 0 или allocated_amount > 0
conditions.append("(vasi.amount > 0 OR vasi.allocated_amount > 0)") conditions.append("(vasi.amount > 0 OR vasi.allocated_amount > 0)")
# Только строки с tax_article # Только строки с tax_articles (непустой JSON-список)
conditions.append("vasi.tax_article IS NOT NULL AND vasi.tax_article != ''") conditions.append("vasi.tax_articles IS NOT NULL AND vasi.tax_articles != '' AND vasi.tax_articles != '[]'")
where_clause = "WHERE " + " AND ".join(conditions) if conditions else "" where_clause = "WHERE " + " AND ".join(conditions) if conditions else ""
@ -112,7 +131,7 @@ def get_vat_allocation_items(filters):
va.month, va.month,
STR_TO_DATE(CONCAT('01 ', va.month, ' ', va.year), '%%d %%M %%Y') as posting_date, STR_TO_DATE(CONCAT('01 ', va.month, ' ', va.year), '%%d %%M %%Y') as posting_date,
vasi.customer, vasi.customer,
vasi.tax_article, vasi.tax_articles,
vasi.amount, vasi.amount,
vasi.allocated_amount vasi.allocated_amount
FROM FROM