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:
parent
5c007ef408
commit
f609d8064d
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue