feat: restructure employee payroll register for quarterly per-month breakdown

Replace from/to date filters with year + quarter selectors and aggregate
one row per employee with numeric columns split per month (ay_1_, ay_2_,
ay_3_) so the formula editor can pick a specific month instead of
dividing the quarter total by 3.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ali 2026-04-23 10:13:41 +00:00
parent 5c007ef408
commit f609d8064d
2 changed files with 172 additions and 165 deletions

View File

@ -4,20 +4,26 @@
frappe.query_reports["Employee Payroll Register"] = { frappe.query_reports["Employee Payroll Register"] = {
filters: [ filters: [
{ {
fieldname: "from_date", fieldname: "year",
label: __("From"), label: __("Year"),
fieldtype: "Date", fieldtype: "Int",
default: frappe.datetime.add_months(frappe.datetime.get_today(), -1), default: new Date().getFullYear(),
reqd: 1, reqd: 1,
width: "100px", width: "80px",
}, },
{ {
fieldname: "to_date", fieldname: "quarter",
label: __("To"), label: __("Quarter"),
fieldtype: "Date", fieldtype: "Select",
default: frappe.datetime.get_today(), options: [
{ value: "1", label: __("Q1 (Jan Mar)") },
{ value: "2", label: __("Q2 (Apr Jun)") },
{ value: "3", label: __("Q3 (Jul Sep)") },
{ value: "4", label: __("Q4 (Oct Dec)") },
],
default: String(Math.ceil((new Date().getMonth() + 1) / 3)),
reqd: 1, reqd: 1,
width: "100px", width: "120px",
}, },
{ {
fieldname: "company", fieldname: "company",

View File

@ -1,3 +1,6 @@
from calendar import monthrange
from datetime import date
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils import flt from frappe.utils import flt
@ -25,100 +28,116 @@ DEDUCTION_TYPES = [
"Gəlir vergisi", "Gəlir vergisi",
] ]
TAX_FREE_FIELDS = [ TAX_FREE_AMOUNT_FIELDS = [
("custom_vergi_indicator", "custom_vergi_amount", "Vergi tutulmayan gəlir"), ("custom_vergi_amount", "Vergi tutulmayan gəlir"),
("custom_mdss_indicator", "custom_mdss_amount", "MDSS cəlb olunmayan gəlir"), ("custom_mdss_amount", "MDSS cəlb olunmayan gəlir"),
( ("custom_unemployment_insurance_amount", "İşsizlik sığortasına cəlb olunmayan gəlir"),
"custom_unemployment_insurance_indicator", ("custom_medical_insurance_amount", "İcbari tibbi sığortaya cəlb olunmayan gəlir"),
"custom_unemployment_insurance_amount",
"İşsizlik sığortasına cəlb olunmayan gəlir",
),
(
"custom_medical_insurance_indicator",
"custom_medical_insurance_amount",
"İcbari tibbi sığortaya cəlb olunmayan gəlir",
),
] ]
MONTHS = (1, 2, 3)
def execute(filters=None): def execute(filters=None):
if not filters: if not filters:
filters = {} filters = {}
today = date.today()
year = int(filters.get("year") or today.year)
quarter = int(filters.get("quarter") or ((today.month - 1) // 3 + 1))
start_month = (quarter - 1) * 3 + 1
end_month = start_month + 2
from_date = date(year, start_month, 1)
to_date = date(year, end_month, monthrange(year, end_month)[1])
currency = "AZN" currency = "AZN"
company_currency = erpnext.get_company_currency(filters.get("company")) company_currency = erpnext.get_company_currency(filters.get("company"))
filters["currency"] = currency filters["currency"] = currency
filters["docstatus"] = "Submitted" filters["docstatus"] = "Submitted"
filters["from_date"] = from_date
salary_slips = get_salary_slips(filters, company_currency) filters["to_date"] = to_date
if not salary_slips:
return get_columns(), []
columns = get_columns() columns = get_columns()
salary_slips = get_salary_slips(filters, company_currency)
if not salary_slips:
return columns, []
ss_earning_map = get_salary_slip_details(salary_slips, currency, company_currency, "earnings") 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") ss_ded_map = get_salary_slip_details(salary_slips, currency, company_currency, "deductions")
doj_map = get_employee_doj_map() doj_map = get_employee_doj_map()
data = [] employee_rows = {}
for ss in salary_slips: for ss in salary_slips:
row = { emp = ss.employee
"salary_slip_id": ss.name, row = employee_rows.get(emp)
"employee": ss.employee, if row is None:
"employee_name": ss.employee_name, row = build_empty_row(ss, doj_map, currency, company_currency)
"date_of_joining": doj_map.get(ss.employee), employee_rows[emp] = row
"branch": ss.branch,
"department": ss.department, month_index = ss.start_date.month - start_month + 1
"designation": ss.designation, if month_index not in MONTHS:
"company": ss.company, continue
"start_date": ss.start_date, prefix = f"ay_{month_index}_"
"end_date": ss.end_date,
"leave_without_pay": ss.leave_without_pay, for field in ("leave_without_pay", "absent_days", "payment_days"):
"absent_days": ss.absent_days, row[prefix + field] += flt(ss.get(field))
"payment_days": ss.payment_days,
"currency": currency or company_currency,
}
for e in EARNING_TYPES: for e in EARNING_TYPES:
row[frappe.scrub(e)] = flt(ss_earning_map.get(ss.name, {}).get(e, 0)) row[prefix + frappe.scrub(e)] += flt(ss_earning_map.get(ss.name, {}).get(e, 0))
if currency == company_currency:
row["gross_pay"] = flt(ss.gross_pay) * flt(ss.exchange_rate)
else:
row["gross_pay"] = flt(ss.gross_pay)
for d in DEDUCTION_TYPES: for d in DEDUCTION_TYPES:
row[frappe.scrub(d)] = flt(ss_ded_map.get(ss.name, {}).get(d, 0)) row[prefix + frappe.scrub(d)] += flt(ss_ded_map.get(ss.name, {}).get(d, 0))
if currency == company_currency: if currency == company_currency:
row["total_deduction"] = flt(ss.total_deduction) * flt(ss.exchange_rate) rate = flt(ss.exchange_rate)
row["net_pay"] = flt(ss.net_pay) * flt(ss.exchange_rate) row[prefix + "gross_pay"] += flt(ss.gross_pay) * rate
row[prefix + "total_deduction"] += flt(ss.total_deduction) * rate
row[prefix + "net_pay"] += flt(ss.net_pay) * rate
else: else:
row["total_deduction"] = flt(ss.total_deduction) row[prefix + "gross_pay"] += flt(ss.gross_pay)
row["net_pay"] = flt(ss.net_pay) row[prefix + "total_deduction"] += flt(ss.total_deduction)
row[prefix + "net_pay"] += flt(ss.net_pay)
for indicator_field, amount_field, _label in TAX_FREE_FIELDS: for amount_field, _label in TAX_FREE_AMOUNT_FIELDS:
row[indicator_field] = ss.get(indicator_field)
amount = flt(ss.get(amount_field)) amount = flt(ss.get(amount_field))
if currency == company_currency: if currency == company_currency:
amount *= flt(ss.exchange_rate) amount *= flt(ss.exchange_rate)
row[amount_field] = amount row[prefix + amount_field] += amount
data.append(row) return columns, list(employee_rows.values())
return columns, data
def build_empty_row(ss, doj_map, currency, company_currency):
row = {
"employee": ss.employee,
"employee_name": ss.employee_name,
"date_of_joining": doj_map.get(ss.employee),
"branch": ss.branch,
"department": ss.department,
"designation": ss.designation,
"company": ss.company,
"currency": currency or company_currency,
}
for i in MONTHS:
prefix = f"ay_{i}_"
row[prefix + "leave_without_pay"] = 0.0
row[prefix + "absent_days"] = 0.0
row[prefix + "payment_days"] = 0.0
row[prefix + "gross_pay"] = 0.0
row[prefix + "total_deduction"] = 0.0
row[prefix + "net_pay"] = 0.0
for e in EARNING_TYPES:
row[prefix + frappe.scrub(e)] = 0.0
for d in DEDUCTION_TYPES:
row[prefix + frappe.scrub(d)] = 0.0
for amount_field, _lbl in TAX_FREE_AMOUNT_FIELDS:
row[prefix + amount_field] = 0.0
return row
def get_columns(): def get_columns():
columns = [ columns = [
{
"label": _("Salary Slip ID"),
"fieldname": "salary_slip_id",
"fieldtype": "Link",
"options": "Salary Slip",
"width": 150,
},
{ {
"label": _("Employee"), "label": _("Employee"),
"fieldname": "employee", "fieldname": "employee",
@ -136,7 +155,7 @@ def get_columns():
"label": _("Date of Joining"), "label": _("Date of Joining"),
"fieldname": "date_of_joining", "fieldname": "date_of_joining",
"fieldtype": "Date", "fieldtype": "Date",
"width": 80, "width": 100,
}, },
{ {
"label": _("Branch"), "label": _("Branch"),
@ -167,114 +186,96 @@ def get_columns():
"width": 120, "width": 120,
}, },
{ {
"label": _("Start Date"), "label": _("Currency"),
"fieldname": "start_date",
"fieldtype": "Data", "fieldtype": "Data",
"width": 80, "fieldname": "currency",
}, "options": "Currency",
{ "hidden": 1,
"label": _("End Date"),
"fieldname": "end_date",
"fieldtype": "Data",
"width": 80,
},
{
"label": _("Leave Without Pay"),
"fieldname": "leave_without_pay",
"fieldtype": "Float",
"width": 50,
},
{
"label": _("Absent Days"),
"fieldname": "absent_days",
"fieldtype": "Float",
"width": 50,
},
{
"label": _("Payment Days"),
"fieldname": "payment_days",
"fieldtype": "Float",
"width": 120,
}, },
] ]
for earning in EARNING_TYPES: for i in MONTHS:
prefix = f"ay_{i}_"
suffix = f" ({i}ci ay)"
columns.extend(
[
{
"label": _("Leave Without Pay") + suffix,
"fieldname": prefix + "leave_without_pay",
"fieldtype": "Float",
"width": 120,
},
{
"label": _("Absent Days") + suffix,
"fieldname": prefix + "absent_days",
"fieldtype": "Float",
"width": 120,
},
{
"label": _("Payment Days") + suffix,
"fieldname": prefix + "payment_days",
"fieldtype": "Float",
"width": 120,
},
]
)
for earning in EARNING_TYPES:
columns.append(
{
"label": earning + suffix,
"fieldname": prefix + frappe.scrub(earning),
"fieldtype": "Currency",
"options": "currency",
"width": 140,
}
)
columns.append( columns.append(
{ {
"label": earning, "label": _("Gross Pay") + suffix,
"fieldname": frappe.scrub(earning), "fieldname": prefix + "gross_pay",
"fieldtype": "Currency", "fieldtype": "Currency",
"options": "currency", "options": "currency",
"width": 120, "width": 140,
} }
) )
for deduction in DEDUCTION_TYPES:
columns.append( columns.append(
{ {
"label": _("Gross Pay"), "label": deduction + suffix,
"fieldname": "gross_pay", "fieldname": prefix + frappe.scrub(deduction),
"fieldtype": "Currency", "fieldtype": "Currency",
"options": "currency", "options": "currency",
"width": 120, "width": 140,
} }
) )
columns.extend(
for deduction in DEDUCTION_TYPES: [
columns.append( {
{ "label": _("Total Deduction") + suffix,
"label": deduction, "fieldname": prefix + "total_deduction",
"fieldname": frappe.scrub(deduction), "fieldtype": "Currency",
"fieldtype": "Currency", "options": "currency",
"options": "currency", "width": 140,
"width": 120, },
} {
) "label": _("Net Pay") + suffix,
"fieldname": prefix + "net_pay",
columns.extend( "fieldtype": "Currency",
[ "options": "currency",
{ "width": 140,
"label": _("Total Deduction"), },
"fieldname": "total_deduction", ]
"fieldtype": "Currency",
"options": "currency",
"width": 120,
},
{
"label": _("Net Pay"),
"fieldname": "net_pay",
"fieldtype": "Currency",
"options": "currency",
"width": 120,
},
{
"label": _("Currency"),
"fieldtype": "Data",
"fieldname": "currency",
"options": "Currency",
"hidden": 1,
},
]
)
for indicator_field, amount_field, label in TAX_FREE_FIELDS:
columns.append(
{
"label": _(label) + " - Göstərici",
"fieldname": indicator_field,
"fieldtype": "Link",
"options": "Tax Free Indicator",
"width": 200,
}
)
columns.append(
{
"label": _(label),
"fieldname": amount_field,
"fieldtype": "Currency",
"options": "currency",
"width": 160,
}
) )
for amount_field, label in TAX_FREE_AMOUNT_FIELDS:
columns.append(
{
"label": _(label) + suffix,
"fieldname": prefix + amount_field,
"fieldtype": "Currency",
"options": "currency",
"width": 180,
}
)
return columns return columns