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"] = {
|
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",
|
||||||
|
|
|
||||||
|
|
@ -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,46 +28,88 @@ 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:
|
||||||
|
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[prefix + frappe.scrub(e)] += flt(ss_earning_map.get(ss.name, {}).get(e, 0))
|
||||||
|
|
||||||
|
for d in DEDUCTION_TYPES:
|
||||||
|
row[prefix + frappe.scrub(d)] += flt(ss_ded_map.get(ss.name, {}).get(d, 0))
|
||||||
|
|
||||||
|
if currency == company_currency:
|
||||||
|
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[prefix + "gross_pay"] += flt(ss.gross_pay)
|
||||||
|
row[prefix + "total_deduction"] += flt(ss.total_deduction)
|
||||||
|
row[prefix + "net_pay"] += flt(ss.net_pay)
|
||||||
|
|
||||||
|
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[prefix + amount_field] += amount
|
||||||
|
|
||||||
|
return columns, list(employee_rows.values())
|
||||||
|
|
||||||
|
|
||||||
|
def build_empty_row(ss, doj_map, currency, company_currency):
|
||||||
row = {
|
row = {
|
||||||
"salary_slip_id": ss.name,
|
|
||||||
"employee": ss.employee,
|
"employee": ss.employee,
|
||||||
"employee_name": ss.employee_name,
|
"employee_name": ss.employee_name,
|
||||||
"date_of_joining": doj_map.get(ss.employee),
|
"date_of_joining": doj_map.get(ss.employee),
|
||||||
|
|
@ -72,53 +117,27 @@ def execute(filters=None):
|
||||||
"department": ss.department,
|
"department": ss.department,
|
||||||
"designation": ss.designation,
|
"designation": ss.designation,
|
||||||
"company": ss.company,
|
"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,
|
"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:
|
for e in EARNING_TYPES:
|
||||||
row[frappe.scrub(e)] = flt(ss_earning_map.get(ss.name, {}).get(e, 0))
|
row[prefix + frappe.scrub(e)] = 0.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)] = 0.0
|
||||||
|
for amount_field, _lbl in TAX_FREE_AMOUNT_FIELDS:
|
||||||
if currency == company_currency:
|
row[prefix + amount_field] = 0.0
|
||||||
row["total_deduction"] = flt(ss.total_deduction) * flt(ss.exchange_rate)
|
return row
|
||||||
row["net_pay"] = flt(ss.net_pay) * flt(ss.exchange_rate)
|
|
||||||
else:
|
|
||||||
row["total_deduction"] = flt(ss.total_deduction)
|
|
||||||
row["net_pay"] = flt(ss.net_pay)
|
|
||||||
|
|
||||||
for indicator_field, amount_field, _label in TAX_FREE_FIELDS:
|
|
||||||
row[indicator_field] = ss.get(indicator_field)
|
|
||||||
amount = flt(ss.get(amount_field))
|
|
||||||
if currency == company_currency:
|
|
||||||
amount *= flt(ss.exchange_rate)
|
|
||||||
row[amount_field] = amount
|
|
||||||
|
|
||||||
data.append(row)
|
|
||||||
|
|
||||||
return columns, data
|
|
||||||
|
|
||||||
|
|
||||||
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"),
|
||||||
|
|
@ -166,86 +185,6 @@ def get_columns():
|
||||||
"options": "Company",
|
"options": "Company",
|
||||||
"width": 120,
|
"width": 120,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"label": _("Start Date"),
|
|
||||||
"fieldname": "start_date",
|
|
||||||
"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,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
for earning in EARNING_TYPES:
|
|
||||||
columns.append(
|
|
||||||
{
|
|
||||||
"label": earning,
|
|
||||||
"fieldname": frappe.scrub(earning),
|
|
||||||
"fieldtype": "Currency",
|
|
||||||
"options": "currency",
|
|
||||||
"width": 120,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
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"),
|
"label": _("Currency"),
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
|
|
@ -254,25 +193,87 @@ def get_columns():
|
||||||
"hidden": 1,
|
"hidden": 1,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
)
|
|
||||||
|
|
||||||
for indicator_field, amount_field, label in TAX_FREE_FIELDS:
|
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(
|
columns.append(
|
||||||
{
|
{
|
||||||
"label": _(label) + " - Göstərici",
|
"label": earning + suffix,
|
||||||
"fieldname": indicator_field,
|
"fieldname": prefix + frappe.scrub(earning),
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Currency",
|
||||||
"options": "Tax Free Indicator",
|
"options": "currency",
|
||||||
"width": 200,
|
"width": 140,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
columns.append(
|
columns.append(
|
||||||
{
|
{
|
||||||
"label": _(label),
|
"label": _("Gross Pay") + suffix,
|
||||||
"fieldname": amount_field,
|
"fieldname": prefix + "gross_pay",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"options": "currency",
|
"options": "currency",
|
||||||
"width": 160,
|
"width": 140,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
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,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue