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"] = {
filters: [
{
fieldname: "from_date",
label: __("From"),
fieldtype: "Date",
default: frappe.datetime.add_months(frappe.datetime.get_today(), -1),
fieldname: "year",
label: __("Year"),
fieldtype: "Int",
default: new Date().getFullYear(),
reqd: 1,
width: "100px",
width: "80px",
},
{
fieldname: "to_date",
label: __("To"),
fieldtype: "Date",
default: frappe.datetime.get_today(),
fieldname: "quarter",
label: __("Quarter"),
fieldtype: "Select",
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,
width: "100px",
width: "120px",
},
{
fieldname: "company",

View File

@ -1,3 +1,6 @@
from calendar import monthrange
from datetime import date
import frappe
from frappe import _
from frappe.utils import flt
@ -25,100 +28,116 @@ DEDUCTION_TYPES = [
"Gəlir vergisi",
]
TAX_FREE_FIELDS = [
("custom_vergi_indicator", "custom_vergi_amount", "Vergi tutulmayan gəlir"),
("custom_mdss_indicator", "custom_mdss_amount", "MDSS cəlb olunmayan gəlir"),
(
"custom_unemployment_insurance_indicator",
"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",
),
TAX_FREE_AMOUNT_FIELDS = [
("custom_vergi_amount", "Vergi tutulmayan 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_medical_insurance_amount", "İcbari tibbi sığortaya cəlb olunmayan gəlir"),
]
MONTHS = (1, 2, 3)
def execute(filters=None):
if not 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"
company_currency = erpnext.get_company_currency(filters.get("company"))
filters["currency"] = currency
filters["docstatus"] = "Submitted"
salary_slips = get_salary_slips(filters, company_currency)
if not salary_slips:
return get_columns(), []
filters["from_date"] = from_date
filters["to_date"] = to_date
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_ded_map = get_salary_slip_details(salary_slips, currency, company_currency, "deductions")
doj_map = get_employee_doj_map()
data = []
employee_rows = {}
for ss in salary_slips:
row = {
"salary_slip_id": ss.name,
"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,
"start_date": ss.start_date,
"end_date": ss.end_date,
"leave_without_pay": ss.leave_without_pay,
"absent_days": ss.absent_days,
"payment_days": ss.payment_days,
"currency": currency or company_currency,
}
emp = ss.employee
row = employee_rows.get(emp)
if row is None:
row = build_empty_row(ss, doj_map, currency, company_currency)
employee_rows[emp] = row
month_index = ss.start_date.month - start_month + 1
if month_index not in MONTHS:
continue
prefix = f"ay_{month_index}_"
for field in ("leave_without_pay", "absent_days", "payment_days"):
row[prefix + field] += flt(ss.get(field))
for e in EARNING_TYPES:
row[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)
row[prefix + frappe.scrub(e)] += flt(ss_earning_map.get(ss.name, {}).get(e, 0))
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:
row["total_deduction"] = flt(ss.total_deduction) * flt(ss.exchange_rate)
row["net_pay"] = flt(ss.net_pay) * flt(ss.exchange_rate)
rate = 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:
row["total_deduction"] = flt(ss.total_deduction)
row["net_pay"] = flt(ss.net_pay)
row[prefix + "gross_pay"] += flt(ss.gross_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:
row[indicator_field] = ss.get(indicator_field)
for amount_field, _label in TAX_FREE_AMOUNT_FIELDS:
amount = flt(ss.get(amount_field))
if currency == company_currency:
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():
columns = [
{
"label": _("Salary Slip ID"),
"fieldname": "salary_slip_id",
"fieldtype": "Link",
"options": "Salary Slip",
"width": 150,
},
{
"label": _("Employee"),
"fieldname": "employee",
@ -136,7 +155,7 @@ def get_columns():
"label": _("Date of Joining"),
"fieldname": "date_of_joining",
"fieldtype": "Date",
"width": 80,
"width": 100,
},
{
"label": _("Branch"),
@ -167,114 +186,96 @@ def get_columns():
"width": 120,
},
{
"label": _("Start Date"),
"fieldname": "start_date",
"label": _("Currency"),
"fieldtype": "Data",
"width": 80,
},
{
"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,
"fieldname": "currency",
"options": "Currency",
"hidden": 1,
},
]
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(
{
"label": earning,
"fieldname": frappe.scrub(earning),
"label": _("Gross Pay") + suffix,
"fieldname": prefix + "gross_pay",
"fieldtype": "Currency",
"options": "currency",
"width": 120,
"width": 140,
}
)
columns.append(
{
"label": _("Gross Pay"),
"fieldname": "gross_pay",
"fieldtype": "Currency",
"options": "currency",
"width": 120,
}
)
for deduction in DEDUCTION_TYPES:
columns.append(
{
"label": deduction,
"fieldname": frappe.scrub(deduction),
"fieldtype": "Currency",
"options": "currency",
"width": 120,
}
)
columns.extend(
[
{
"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 deduction in DEDUCTION_TYPES:
columns.append(
{
"label": deduction + suffix,
"fieldname": prefix + frappe.scrub(deduction),
"fieldtype": "Currency",
"options": "currency",
"width": 140,
}
)
columns.extend(
[
{
"label": _("Total Deduction") + suffix,
"fieldname": prefix + "total_deduction",
"fieldtype": "Currency",
"options": "currency",
"width": 140,
},
{
"label": _("Net Pay") + suffix,
"fieldname": prefix + "net_pay",
"fieldtype": "Currency",
"options": "currency",
"width": 140,
},
]
)
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