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:
parent
7478d4d66c
commit
129a87711f
|
|
@ -23,6 +23,9 @@ frappe.ui.form.on('VAT allocation', {
|
|||
// Setup filter for Payment Entry
|
||||
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 fill_sales_invoice_items(frm, filters) {
|
||||
// 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');
|
||||
row.customer = item.customer;
|
||||
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.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');
|
||||
row.customer = item.customer;
|
||||
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.unallocated_amount = item.unallocated_amount;
|
||||
row.allocated_amount = item.allocated_amount;
|
||||
|
|
@ -707,7 +806,7 @@ function show_allocation_dialog(frm, payment_row_idx, customer) {
|
|||
let item_data = {
|
||||
name: item.name,
|
||||
item_tax_template: item.item_tax_template || '',
|
||||
tax_article: item.tax_article || '',
|
||||
tax_articles: item.tax_articles || '',
|
||||
amount: item.amount || 0,
|
||||
unallocated_amount: item.unallocated_amount || 0,
|
||||
allocated_amount: item.allocated_amount || 0,
|
||||
|
|
@ -742,7 +841,7 @@ function show_allocation_dialog(frm, payment_row_idx, customer) {
|
|||
let item_data = {
|
||||
name: item.name,
|
||||
item_tax_template: item.item_tax_template || '',
|
||||
tax_article: item.tax_article || '',
|
||||
tax_articles: item.tax_articles || '',
|
||||
amount: item.amount || 0,
|
||||
unallocated_amount: item.unallocated_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 += '<thead><tr>';
|
||||
html += '<th>Item Tax Template</th>';
|
||||
html += '<th>Tax Article</th>';
|
||||
html += '<th>Tax Articles</th>';
|
||||
html += '<th>Total Amount</th>';
|
||||
html += '<th>Unallocated</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' };
|
||||
html += '<tr>';
|
||||
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.unallocated_amount) + '</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 += '<thead><tr>';
|
||||
html += '<th>Item Tax Template</th>';
|
||||
html += '<th>Tax Article</th>';
|
||||
html += '<th>Tax Articles</th>';
|
||||
html += '<th>Total Amount</th>';
|
||||
html += '<th>Unallocated</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' };
|
||||
html += '<tr>';
|
||||
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.unallocated_amount) + '</td>';
|
||||
html += '<td style="background-color: #fff3cd;">' + format_currency(item.current_allocation || 0) + '</td>';
|
||||
|
|
|
|||
|
|
@ -7,6 +7,26 @@ import json
|
|||
# Константа для tolerance округления
|
||||
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):
|
||||
def autoname(self):
|
||||
"""
|
||||
|
|
@ -430,8 +450,9 @@ def get_journal_entry_query(doctype, txt, searchfield, start, page_len, filters)
|
|||
@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
|
||||
Get Sales Invoice items for specified month and year, grouped by customer and
|
||||
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
|
||||
"""
|
||||
# Convert month name to number
|
||||
|
|
@ -484,12 +505,15 @@ def get_sales_invoice_items(year, month, customers=None, min_amount=None, max_am
|
|||
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
|
||||
# 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"""
|
||||
SELECT
|
||||
si.customer,
|
||||
sii.item_tax_template,
|
||||
sita.tax_article,
|
||||
SUM(sii.amount) as amount,
|
||||
SUM(sii.net_amount) as net_amount,
|
||||
SUM(sii.qty) as total_qty
|
||||
|
|
@ -497,24 +521,56 @@ def get_sales_invoice_items(year, month, customers=None, min_amount=None, max_am
|
|||
`tabSales Invoice` si
|
||||
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
|
||||
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
|
||||
WHERE
|
||||
{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)
|
||||
|
||||
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
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_sales_invoice_items_previous_months(year, month, customers=None, min_amount=None, max_amount=None):
|
||||
"""
|
||||
Get Sales Invoice items from previous VAT Allocation documents where unallocated_amount > 0
|
||||
Grouped by customer, item_tax_template, and tax_article
|
||||
Grouped by customer and item_tax_template, unioning their tax_articles lists
|
||||
"""
|
||||
# Convert month name to number
|
||||
months = {
|
||||
|
|
@ -583,7 +639,7 @@ def get_sales_invoice_items_previous_months(year, month, customers=None, min_amo
|
|||
SELECT
|
||||
customer,
|
||||
item_tax_template,
|
||||
tax_article,
|
||||
tax_articles,
|
||||
unallocated_amount
|
||||
FROM `tabVAT allocation sales invoice`
|
||||
WHERE parent = %(parent)s
|
||||
|
|
@ -594,7 +650,8 @@ def get_sales_invoice_items_previous_months(year, month, customers=None, min_amo
|
|||
|
||||
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 = {}
|
||||
|
||||
for item in all_items:
|
||||
|
|
@ -602,17 +659,18 @@ def get_sales_invoice_items_previous_months(year, month, customers=None, min_amo
|
|||
if customer_list and item.customer not in customer_list:
|
||||
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:
|
||||
grouped_items[key] = {
|
||||
'customer': item.customer,
|
||||
'item_tax_template': item.item_tax_template,
|
||||
'tax_article': item.tax_article,
|
||||
'tax_articles': set(),
|
||||
'amount': Decimal('0')
|
||||
}
|
||||
|
||||
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
|
||||
result = []
|
||||
|
|
@ -628,14 +686,14 @@ def get_sales_invoice_items_previous_months(year, month, customers=None, min_amo
|
|||
result.append({
|
||||
'customer': item['customer'],
|
||||
'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,
|
||||
'unallocated_amount': amount, # Initially all is unallocated
|
||||
'allocated_amount': 0
|
||||
})
|
||||
|
||||
# Sort by customer, item_tax_template, tax_article
|
||||
result.sort(key=lambda x: (x['customer'], x['item_tax_template'] or '', x['tax_article'] or ''))
|
||||
# Sort by customer, item_tax_template
|
||||
result.sort(key=lambda x: (x['customer'], x['item_tax_template'] or ''))
|
||||
|
||||
return result
|
||||
|
||||
|
|
@ -705,7 +763,7 @@ def get_allocation_data(vat_allocation_name, payment_entry_row_name, customer):
|
|||
item_data = {
|
||||
'name': item.name,
|
||||
'item_tax_template': item.item_tax_template or '',
|
||||
'tax_article': item.tax_article or '',
|
||||
'tax_articles': item.tax_articles or '',
|
||||
'amount': item.amount,
|
||||
'unallocated_amount': item.unallocated_amount,
|
||||
'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 = {
|
||||
'name': item.name,
|
||||
'item_tax_template': item.item_tax_template or '',
|
||||
'tax_article': item.tax_article or '',
|
||||
'tax_articles': item.tax_articles or '',
|
||||
'amount': item.amount,
|
||||
'unallocated_amount': item.unallocated_amount,
|
||||
'allocated_amount': item.allocated_amount or 0,
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@
|
|||
"customer",
|
||||
"payment_entry",
|
||||
"item_tax_template",
|
||||
"tax_article",
|
||||
"tax_articles_display",
|
||||
"tax_articles",
|
||||
"amount",
|
||||
"allocated_amount",
|
||||
"unallocated_amount"
|
||||
|
|
@ -57,12 +58,17 @@
|
|||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "tax_article",
|
||||
"fieldtype": "Link",
|
||||
"fieldname": "tax_articles_display",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Tax Article",
|
||||
"options": "Tax Article",
|
||||
"precision": "2",
|
||||
"label": "Tax Articles",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "tax_articles",
|
||||
"fieldtype": "Small Text",
|
||||
"hidden": 1,
|
||||
"label": "Tax Articles (raw)",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
# Copyright (c) 2026, Your Company and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import flt
|
||||
|
|
@ -11,6 +13,19 @@ def execute(filters=None):
|
|||
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):
|
||||
"""Получаем данные по VAT Allocation с распределением по налоговым статьям"""
|
||||
|
||||
|
|
@ -45,9 +60,13 @@ def get_data(filters):
|
|||
allocations_data[allocation_name][field_key] = 0
|
||||
allocations_data[allocation_name][f"{field_key}_odenis"] = 0
|
||||
|
||||
# Получаем полное название статьи по сокращенному name
|
||||
tax_article_name = item['tax_article']
|
||||
if tax_article_name and tax_article_name in tax_article_names_mapping:
|
||||
# Каждая строка несёт JSON-список статей; одна и та же сумма строки
|
||||
# попадает в КАЖДУЮ статью из списка (как на ФНО, где один оборот может
|
||||
# стоять под несколькими 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]
|
||||
|
||||
# Проверяем есть ли маппинг для полного названия
|
||||
|
|
@ -100,8 +119,8 @@ def get_vat_allocation_items(filters):
|
|||
# Только строки с amount > 0 или allocated_amount > 0
|
||||
conditions.append("(vasi.amount > 0 OR vasi.allocated_amount > 0)")
|
||||
|
||||
# Только строки с tax_article
|
||||
conditions.append("vasi.tax_article IS NOT NULL AND vasi.tax_article != ''")
|
||||
# Только строки с tax_articles (непустой JSON-список)
|
||||
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 ""
|
||||
|
||||
|
|
@ -112,7 +131,7 @@ def get_vat_allocation_items(filters):
|
|||
va.month,
|
||||
STR_TO_DATE(CONCAT('01 ', va.month, ' ', va.year), '%%d %%M %%Y') as posting_date,
|
||||
vasi.customer,
|
||||
vasi.tax_article,
|
||||
vasi.tax_articles,
|
||||
vasi.amount,
|
||||
vasi.allocated_amount
|
||||
FROM
|
||||
|
|
|
|||
Loading…
Reference in New Issue