auto-populate hissə_1 from Employee Payroll Register on year/quarter change

Redesign Single Declaration-Elave1_Hisse1 child table with all report columns
(26 fields), add server-side aggregation per employee with insurance formula
(işçi + şirkət) × 3, and client-side triggers on il/hesabatdövrü fields.
Also fix backtick escaping for Unicode field names in frappe.db.get_value.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ali 2026-03-30 19:53:55 +04:00
parent 4addeb8a05
commit 63e9ec5ee4
3 changed files with 452 additions and 54 deletions

View File

@ -6,60 +6,194 @@
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"soyadadataad\u0131",
"fin",
"g\u0259lirl\u0259rc\u0259mi",
"vergim\u0259bl\u0259\u011fi",
"employee",
"employeename",
"dateofjoining",
"branch",
"department",
"designation",
"leavewithoutpay",
"absentdays",
"paymentdays",
"\u0259sas\u0259m\u0259khaqq\u0131",
"m\u0259zuniyy\u0259tkompensasiyas\u0131",
"dsmfx\u0259rc\u015firk\u0259t",
"icbaritibbis\u0131\u011fortax\u0259rc\u015firk\u0259t2",
"i\u015fsizlikd\u0259ns\u0131\u011fortahaqq\u0131x\u0259rc\u015firk\u0259t05",
"grosspay",
"dsmfi\u015f\u00e7i",
"dsmf\u015firk\u0259t",
"icbaritibbis\u0131\u011fortai\u015f\u00e7i2",
"icbaritibbis\u0131\u011forta\u015firk\u0259t2",
"i\u015fsizlikd\u0259ns\u0131\u011fortahaqq\u0131i\u015f\u00e7i05",
"i\u015fsizlikd\u0259ns\u0131\u011fortahaqq\u0131\u015firk\u0259t05",
"totaldeduction",
"netpay",
"hesablanm\u0131\u015fmdsshaqq\u0131",
"hesablanm\u0131\u015fi\u015fsizlikd\u0259ns\u0131\u011fortahaqq\u0131",
"hesablanm\u0131\u015ficbaritibbis\u0131\u011fortahaqq\u0131"
],
"fields": [
{
"fieldname": "soyadadataad\u0131",
"fieldtype": "Data",
"fieldname": "employee",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Soyad, ad, ata ad\u0131",
"label": "Employee",
"options": "Employee",
"read_only": 1
},
{
"fieldname": "fin",
"fieldname": "employeename",
"fieldtype": "Data",
"in_list_view": 1,
"label": "F\u0130N",
"label": "Employee Name",
"read_only": 1
},
{
"fieldname": "g\u0259lirl\u0259rc\u0259mi",
"fieldtype": "Data",
"in_list_view": 1,
"label": "G\u0259lirl\u0259r c\u0259mi",
"fieldname": "dateofjoining",
"fieldtype": "Date",
"label": "Date of Joining",
"read_only": 1
},
{
"fieldname": "vergim\u0259bl\u0259\u011fi",
"fieldname": "branch",
"fieldtype": "Data",
"label": "Branch",
"read_only": 1
},
{
"fieldname": "department",
"fieldtype": "Data",
"label": "Department",
"read_only": 1
},
{
"fieldname": "designation",
"fieldtype": "Data",
"label": "Designation",
"read_only": 1
},
{
"fieldname": "leavewithoutpay",
"fieldtype": "Float",
"label": "Leave Without Pay",
"read_only": 1
},
{
"fieldname": "absentdays",
"fieldtype": "Float",
"label": "Absent Days",
"read_only": 1
},
{
"fieldname": "paymentdays",
"fieldtype": "Float",
"label": "Payment Days",
"read_only": 1
},
{
"fieldname": "\u0259sas\u0259m\u0259khaqq\u0131",
"fieldtype": "Currency",
"label": "\u018esas \u0259m\u0259k haqq\u0131",
"read_only": 1
},
{
"fieldname": "m\u0259zuniyy\u0259tkompensasiyas\u0131",
"fieldtype": "Currency",
"label": "M\u0259zuniyy\u0259t Kompensasiyas\u0131",
"read_only": 1
},
{
"fieldname": "dsmfx\u0259rc\u015firk\u0259t",
"fieldtype": "Currency",
"label": "DSMF (x\u0259rc \u015firk\u0259t)",
"read_only": 1
},
{
"fieldname": "icbaritibbis\u0131\u011fortax\u0259rc\u015firk\u0259t2",
"fieldtype": "Currency",
"label": "\u0130cbari tibbi s\u0131\u011forta (x\u0259rc \u015firk\u0259t 2%)",
"read_only": 1
},
{
"fieldname": "i\u015fsizlikd\u0259ns\u0131\u011fortahaqq\u0131x\u0259rc\u015firk\u0259t05",
"fieldtype": "Currency",
"label": "\u0130\u015fsizlikd\u0259n s\u0131\u011forta haqq\u0131 (x\u0259rc \u015firk\u0259t 0,5%)",
"read_only": 1
},
{
"fieldname": "grosspay",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Vergi m\u0259bl\u0259\u011fi",
"label": "Gross Pay",
"read_only": 1
},
{
"fieldname": "dsmfi\u015f\u00e7i",
"fieldtype": "Currency",
"label": "DSMF (i\u015f\u00e7i)",
"read_only": 1
},
{
"fieldname": "dsmf\u015firk\u0259t",
"fieldtype": "Currency",
"label": "DSMF (\u015firk\u0259t)",
"read_only": 1
},
{
"fieldname": "icbaritibbis\u0131\u011fortai\u015f\u00e7i2",
"fieldtype": "Currency",
"label": "\u0130cbari tibbi s\u0131\u011forta (i\u015f\u00e7i 2%)",
"read_only": 1
},
{
"fieldname": "icbaritibbis\u0131\u011forta\u015firk\u0259t2",
"fieldtype": "Currency",
"label": "\u0130cbari tibbi s\u0131\u011forta (\u015firk\u0259t 2%)",
"read_only": 1
},
{
"fieldname": "i\u015fsizlikd\u0259ns\u0131\u011fortahaqq\u0131i\u015f\u00e7i05",
"fieldtype": "Currency",
"label": "\u0130\u015fsizlikd\u0259n s\u0131\u011forta haqq\u0131 (i\u015f\u00e7i 0,5%)",
"read_only": 1
},
{
"fieldname": "i\u015fsizlikd\u0259ns\u0131\u011fortahaqq\u0131\u015firk\u0259t05",
"fieldtype": "Currency",
"label": "\u0130\u015fsizlikd\u0259n s\u0131\u011forta haqq\u0131 (\u015firk\u0259t 0,5%)",
"read_only": 1
},
{
"fieldname": "totaldeduction",
"fieldtype": "Currency",
"label": "Total Deduction",
"read_only": 1
},
{
"fieldname": "netpay",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Net Pay",
"read_only": 1
},
{
"fieldname": "hesablanm\u0131\u015fmdsshaqq\u0131",
"fieldtype": "Data",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Hesablanm\u0131\u015f MDSS haqq\u0131",
"read_only": 1
},
{
"fieldname": "hesablanm\u0131\u015fi\u015fsizlikd\u0259ns\u0131\u011fortahaqq\u0131",
"fieldtype": "Data",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Hesablanm\u0131\u015f i\u015fsizlikd\u0259n s\u0131\u011forta haqq\u0131",
"read_only": 1
},
{
"fieldname": "hesablanm\u0131\u015ficbaritibbis\u0131\u011fortahaqq\u0131",
"fieldtype": "Data",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Hesablanm\u0131\u015f icbari tibbi s\u0131\u011forta haqq\u0131",
"read_only": 1
@ -68,8 +202,8 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2026-03-26 18:02:10.219887",
"modified_by": "nihad_m@jeysoft.az",
"modified": "2026-03-30 12:00:00.000000",
"modified_by": "Administrator",
"module": "Taxes Az",
"name": "Single Declaration-Elave1_Hisse1",
"owner": "Administrator",

View File

@ -6,7 +6,7 @@ function fillFromCompanyDefaults(frm) {
frappe.db.get_value(
'Company',
company,
['mdssüzrə', 'icbaritibbisığortaüzrə', 'işsizlikdənsığortaüzrə', 'main_activity']
['`mdssüzrə`', '`icbaritibbisığortaüzrə`', '`işsizlikdənsığortaüzrə`', '`main_activity`']
).then(r => {
if (!r || !r.message) return;
const v = r.message;
@ -25,39 +25,106 @@ frappe.ui.form.on('Single declaration related to salaried and non-salaried work'
il: function(frm) {
fillFromCompanyDefaults(frm);
reload_hisse_1_data(frm);
},
hesabatdövrü: function(frm) {
reload_hisse_1_data(frm);
},
refresh: function(frm) {
frm.add_custom_button(__('Export XML'), function() {
frappe.call({
method: 'taxes_az.taxes_az.doctype.xml_mapping_tool.xml_mapping_tool.generate_xml',
args: {
mapping_tool: 'Single declaration related to salaried and non-salaried work',
doctype: 'Single declaration related to salaried and non-salaried work',
docname: frm.docname
},
callback: function(r) {
if (r.message) {
var element = document.createElement('a');
element.setAttribute('href', 'data:text/xml;charset=utf-8,' + encodeURIComponent(r.message));
element.setAttribute('download', frm.docname + '.xml');
element.style.display = 'none';
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
frappe.show_alert({
message: __('XML file generated and downloaded'),
indicator: 'green'
});
}
}
});
}, __('Actions'));
}
});
// Код для экспорта XML
frappe.ui.form.on('Single declaration related to salaried and non-salaried work', {
refresh: function(frm) {
frm.add_custom_button(__('Export XML'), function() {
frappe.call({
method: 'taxes_az.taxes_az.doctype.xml_mapping_tool.xml_mapping_tool.generate_xml',
args: {
mapping_tool: 'Single declaration related to salaried and non-salaried work', // Замените на имя вашего документа XML Mapping Tool
doctype: 'Single declaration related to salaried and non-salaried work', // Замените на имя DocType
docname: frm.docname
},
callback: function(r) {
if (r.message) {
// Создаем временный элемент для скачивания
var element = document.createElement('a');
element.setAttribute('href', 'data:text/xml;charset=utf-8,' + encodeURIComponent(r.message));
element.setAttribute('download', frm.docname + '.xml');
element.style.display = 'none';
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
frappe.show_alert({
message: __('XML file generated and downloaded'),
indicator: 'green'
});
}
}
});
}, __('Actions'));
}
function reload_hisse_1_data(frm) {
if (!frm.doc.il || !frm.doc.hesabatdövrü) {
return;
}
);
const company = frappe.defaults.get_user_default('Company')
|| frappe.defaults.get_default('Company');
if (!company) {
frappe.msgprint(__('Company not found in user defaults'));
return;
}
frappe.dom.freeze(__('Loading payroll data...'));
frappe.call({
method: 'taxes_az.taxes_az.doctype.single_declaration_related_to_salaried_and_non_salaried_work.single_declaration_related_to_salaried_and_non_salaried_work.populate_declaration_tables',
args: {
company: company,
year: frm.doc.il,
quarter: frm.doc.hesabatdövrü
},
callback: function(r) {
frappe.dom.unfreeze();
if (r.message && r.message.success) {
populate_hisse_1(frm, r.message.hisse_1_data || []);
frm.dirty();
frm.refresh_fields();
frappe.show_alert({
message: __('Payroll data loaded successfully'),
indicator: 'green'
});
} else {
frappe.msgprint({
title: __('Error'),
message: r.message ? r.message.error : __('Failed to load data'),
indicator: 'red'
});
}
},
error: function() {
frappe.dom.unfreeze();
frappe.msgprint({
title: __('Error'),
message: __('Server error while loading payroll data'),
indicator: 'red'
});
}
});
}
function populate_hisse_1(frm, data) {
frm.clear_table('hissə_1');
data.forEach(function(emp_data) {
let row = frm.add_child('hissə_1');
Object.keys(emp_data).forEach(function(field) {
if (emp_data[field] !== null && emp_data[field] !== undefined) {
row[field] = emp_data[field];
}
});
});
frm.refresh_field('hissə_1');
}

View File

@ -1,9 +1,206 @@
# Copyright (c) 2025, Jey Soft and contributors
# For license information, please see license.txt
# import frappe
import frappe
from frappe.model.document import Document
from frappe.utils import flt
from taxes_az.taxes_az.report.employee_payroll_register.employee_payroll_register import (
DEDUCTION_TYPES,
EARNING_TYPES,
get_employee_doj_map,
get_salary_slip_details,
get_salary_slips,
)
QUARTER_DATE_RANGES = {
"1. Rüb": ("01-01", "03-31"),
"2. Rüb": ("04-01", "06-30"),
"3. Rüb": ("07-01", "09-30"),
"4. Rüb": ("10-01", "12-31"),
}
# Mapping from frappe.scrub(salary_component_name) to child table fieldnames.
# Uses frappe.scrub() at module level to handle İ→i̇ Unicode correctly.
REPORT_KEY_TO_FIELDNAME = {
# Earnings
frappe.scrub("DSMF (xərc şirkət)"): "dsmfxərcşirkət",
frappe.scrub("Məzuniyyət Kompensasiyası"): "məzuniyyətkompensasiyası",
frappe.scrub("İcbari tibbi sığorta (xərc şirkət 2%)"): "icbaritibbisığortaxərcşirkət2",
frappe.scrub("İşsizlikdən sığorta haqqı (xərc şirkət 0,5%)"): "işsizlikdənsığortahaqqıxərcşirkət05",
frappe.scrub("Əsas əmək haqqı"): "əsasəməkhaqqı",
# Deductions
frappe.scrub("DSMF (işçi)"): "dsmfişçi",
frappe.scrub("DSMF (şirkət)"): "dsmfşirkət",
frappe.scrub("İcbari tibbi sığorta (işçi 2%)"): "icbaritibbisığortaişçi2",
frappe.scrub("İcbari tibbi sığorta (şirkət 2%)"): "icbaritibbisığortaşirkət2",
frappe.scrub("İşsizlikdən sığorta haqqı (işçi 0,5%)"): "işsizlikdənsığortahaqqıişçi05",
frappe.scrub("İşsizlikdən sığorta haqqı (şirkət 0,5%)"): "işsizlikdənsığortahaqqışirkət05",
}
# Scrubbed keys for insurance formula calculation
_DSMF_ISCI = frappe.scrub("DSMF (işçi)")
_DSMF_SIRKET = frappe.scrub("DSMF (şirkət)")
_ISSIZLIK_ISCI = frappe.scrub("İşsizlikdən sığorta haqqı (işçi 0,5%)")
_ISSIZLIK_SIRKET = frappe.scrub("İşsizlikdən sığorta haqqı (şirkət 0,5%)")
_ITS_ISCI = frappe.scrub("İcbari tibbi sığorta (işçi 2%)")
_ITS_SIRKET = frappe.scrub("İcbari tibbi sığorta (şirkət 2%)")
class Singledeclarationrelatedtosalariedandnonsalariedwork(Document):
pass
@frappe.whitelist()
def populate_declaration_tables(company, year, quarter):
"""Populate child tables from Employee Payroll Register data.
Returns dict with data for each child table.
Designed for extensibility: future tables add their key to the result dict.
"""
try:
year = int(year)
if quarter not in QUARTER_DATE_RANGES:
return {"success": False, "error": f"Invalid quarter: {quarter}"}
start_suffix, end_suffix = QUARTER_DATE_RANGES[quarter]
from_date = f"{year}-{start_suffix}"
to_date = f"{year}-{end_suffix}"
hisse_1_data = get_aggregated_employee_data(company, from_date, to_date)
return {
"success": True,
"hisse_1_data": hisse_1_data,
}
except Exception as e:
frappe.log_error(
f"Error in populate_declaration_tables: {str(e)}",
"Single Declaration",
)
return {"success": False, "error": str(e)}
def get_aggregated_employee_data(company, from_date, to_date):
"""Fetch salary slip data and aggregate per employee for hissə_1."""
import erpnext
company_currency = erpnext.get_company_currency(company)
currency = "AZN"
filters = {
"company": company,
"from_date": from_date,
"to_date": to_date,
"currency": currency,
"docstatus": "Submitted",
}
salary_slips = get_salary_slips(filters, company_currency)
if not salary_slips:
return []
ss_earning_map = get_salary_slip_details(salary_slips, currency, company_currency, "earnings")
ss_ded_map = get_salary_slip_details(salary_slips, currency, company_currency, "deductions")
doj_map = get_employee_doj_map()
# Build per-slip rows (same logic as report's execute())
all_scrubbed_keys = []
for e in EARNING_TYPES:
all_scrubbed_keys.append(frappe.scrub(e))
for d in DEDUCTION_TYPES:
all_scrubbed_keys.append(frappe.scrub(d))
sum_fields = [
"leave_without_pay",
"absent_days",
"payment_days",
"gross_pay",
"total_deduction",
"net_pay",
] + all_scrubbed_keys
employee_map = {}
for ss in salary_slips:
emp = ss.employee
if emp not in employee_map:
employee_map[emp] = {
"employee": emp,
"employee_name": ss.employee_name,
"date_of_joining": doj_map.get(emp),
"branch": ss.branch,
"department": ss.department,
"designation": ss.designation,
}
for field in sum_fields:
employee_map[emp][field] = 0.0
agg = employee_map[emp]
# Earnings
for e in EARNING_TYPES:
key = frappe.scrub(e)
agg[key] += flt(ss_earning_map.get(ss.name, {}).get(e, 0))
# Gross pay
if currency == company_currency:
agg["gross_pay"] += flt(ss.gross_pay) * flt(ss.exchange_rate)
else:
agg["gross_pay"] += flt(ss.gross_pay)
# Deductions
for d in DEDUCTION_TYPES:
key = frappe.scrub(d)
agg[key] += flt(ss_ded_map.get(ss.name, {}).get(d, 0))
# Totals
if currency == company_currency:
agg["total_deduction"] += flt(ss.total_deduction) * flt(ss.exchange_rate)
agg["net_pay"] += flt(ss.net_pay) * flt(ss.exchange_rate)
else:
agg["total_deduction"] += flt(ss.total_deduction)
agg["net_pay"] += flt(ss.net_pay)
# Days
agg["leave_without_pay"] += flt(ss.leave_without_pay)
agg["absent_days"] += flt(ss.absent_days)
agg["payment_days"] += flt(ss.payment_days)
# Convert to child table format with clean fieldnames
result = []
for agg in employee_map.values():
child_row = {
"employee": agg["employee"],
"employeename": agg["employee_name"],
"dateofjoining": str(agg["date_of_joining"]) if agg["date_of_joining"] else None,
"branch": agg.get("branch"),
"department": agg.get("department"),
"designation": agg.get("designation"),
"leavewithoutpay": agg["leave_without_pay"],
"absentdays": agg["absent_days"],
"paymentdays": agg["payment_days"],
"grosspay": agg["gross_pay"],
"totaldeduction": agg["total_deduction"],
"netpay": agg["net_pay"],
}
# Map scrubbed report keys to clean child table fieldnames
for report_key, child_fieldname in REPORT_KEY_TO_FIELDNAME.items():
child_row[child_fieldname] = agg.get(report_key, 0.0)
# Calculate insurance fields: (işçi + şirkət) × 3
child_row["hesablanmışmdsshaqqı"] = (flt(agg.get(_DSMF_ISCI, 0)) + flt(agg.get(_DSMF_SIRKET, 0))) * 3
child_row["hesablanmışişsizlikdənsığortahaqqı"] = (
flt(agg.get(_ISSIZLIK_ISCI, 0)) + flt(agg.get(_ISSIZLIK_SIRKET, 0))
) * 3
child_row["hesablanmışicbaritibbisığortahaqqı"] = (
flt(agg.get(_ITS_ISCI, 0)) + flt(agg.get(_ITS_SIRKET, 0))
) * 3
result.append(child_row)
result.sort(key=lambda x: x.get("employeename", ""))
return result