Bỏ qua để đến nội dung

Payrolls (Bảng tính lương)

Nội dung này hiện chưa có sẵn bằng ngôn ngữ của bạn.

  • What a payroll batch is and how it differs from a per-employee payslip.
  • How to create → lock → pay a batch in the Bảng tính lương surface.
  • Where commissions and wage engine numbers feed into the payslip.
  • Endpoints, schema, and common pitfalls.

Reading order: this page covers the manager surface (mobile POS workspace + vendor-web HR editor). For the rules-engine behind commission numbers, see Commission sheets.

TierTableOne row per
Payroll batch (vendor_payrolls)Header — code, period, schedule, totals, statusA store + a period (e.g. “BL000001 — 09/2026”).
Payslip (vendor_payslips)One employee’s net pay breakdown for that batchA payroll_id × vendor_employee_id pair.

A batch is the container. Each payslip inside the batch holds the per-employee breakdown: base salary, hours, allowances, deductions, tax, net, attendance, history.

draft ──Chốt lương──▶ locked ──Thanh toán──▶ paid
│
└─(void)─▶ cancelled (out of scope v1)
StateWhat you can do
draftEdit payslip numbers, add / remove employees, change note.
lockedRead-only. Payslips auto-promoted to approved. No more line edits.
paidRead-only. Payslips auto-promoted to paid. paid_at is set; paid_total matches total_net.
cancelledRead-only. Out of scope for v1 mobile.

The wage engine itself lives upstream in vendor-web (“timesheet → payroll” flow). The backend stores user-edited numbers — never re-computes them server-side once saved.

  1. Open More → Bảng tính lương on the POS workspace, or HR → Payrolls on the vendor dashboard.
  2. Tap + New batch.
  3. Fill in:
FieldWhat to enter
Period fromFirst day of the pay period (store timezone, YYYY-MM-DD).
Period toLast day of the pay period.
Pay schedulemonthly | weekly | biweekly. Drives the period label.
(optional) Initial payslipsPre-fill the batch with one row per active employee. The wage engine seeds base salary + hours; you can override any number.
(optional) NoteVietnamese copy OK.
  1. Save. The new batch gets a BL000001-style code (zero-padded, monotonic per vendor) and appears at the top of the list.

The backend endpoint is POST /api/v1/vendor/payrolls — see API catalog → “Vendor / Payroll”.

Open the batch → tap a payslip row → the editor shows:

SectionFields
Wagebase_salary, total_hours, overtime_hours.
AllowancesSum + per-line items (JSON). Add: meal, phone, travel, seniority.
DeductionsSum + per-line items (JSON). Add: insurance, advance, loan repayment. Stored as positive numbers — the UI negates.
CommissionsSum + per-line items (JSON). Each item is a (sheet_id, item_id, qty, rate) reference so the report ties back to a commission sheet.
Taxtax_amount (VND withheld).
Attendancework_days, expected_days, absent_days.
Netnet_amount = base + hours_wage + allowance + commission − deduction − tax. Stored, not recomputed.
HistoryJSON array — created, edited, approved, paid entries with timestamps and actor.

net_amount is stored, not derived — so editing a line item does not auto-recompute the net. This matches the wage engine upstream where the operator owns the final number.

V. Lock the batch — “Chốt lương”

Section titled “V. Lock the batch — “Chốt lương””

When you’re done editing:

  1. Open the batch → tap Chốt lương (Lock).
  2. The backend sets status = locked, sets locked_at, and cascades approved to every child payslip.
  3. The batch becomes read-only.

Re-opening a locked batch is not supported in v1 — instead create a new batch with the same period and copy the numbers you want to revise.

When you’ve disbursed the salaries:

  1. Open the batch → tap Thanh toán (Pay).
  2. Fill in:
FieldWhat to enter
Payment methodcash | bank_transfer | card.
(optional) Paid onOverride the paid_at date (defaults to now).
(optional) ReferenceBank ref / cheque number.
(optional) NoteVietnamese copy OK.
  1. Confirm. The backend sets status = paid, paid_at, paid_total = total_net, and cascades paid to every child payslip.

The endpoint is POST /api/v1/vendor/payrolls/{id}/pay.

A payslip’s commission_items JSON references the commission sheet rules that fired for this employee during the period:

[
{
"sheet_id": "01HF...0ABC",
"item_id": "01HF...ITEM1",
"product_id": "01HF...PROD1",
"qty": 12,
"rate_vnd": 2000,
"total": 24000
}
]

The wage engine sums (qty × rate) for every rule whose sheet is anchored to the employee’s store (origin store OR any sheet whose scope === 'branches' includes the store). The total becomes commission_total on the payslip.

See Commission sheets → “How commissions reach payroll” for the full flow.

MethodPathPurpose
GET/api/v1/vendor/payrollsList batches for the active store (filter by status, free-text q, paginated page / limit). Scope: vendor JWT + store_id.
GET/api/v1/vendor/payrolls/{id}One batch with nested payslips[].
POST/api/v1/vendor/payrollsCreate a batch (optional initial payslips). Returns the new batch with code BL00000X.
POST/api/v1/vendor/payrolls/{id}/lockPromote draft → locked; cascades to payslips.
POST/api/v1/vendor/payrolls/{id}/paySettle the locked batch (payment_method, optional paid_on, reference, note).

All routes require the vendor JWT; store_id is enforced server-side; a cross-store id resolves to 404 to avoid leaking ids.

vendor_payrolls:

ColumnTypeNotes
idchar(13)Crockford Base32, minted by PublicIdSubscriber.
vendor_id, store_idchar(13)Multi-tenant keys.
codevarchar(16)BL + 6-digit zero-padded seq per vendor (e.g. BL000001).
period_labelvarchar(32)"09/2026" shown in list rows.
period_from, period_todateYYYY-MM-DD in the store timezone.
pay_schedulevarchar(16)monthly | weekly | biweekly.
payslip_countint unsignedCache — surface a single SELECT.
total_netdecimal(15,0)Sum of payslip net_amount (VND).
paid_totaldecimal(15,0)Sum of payslip amounts disbursed. remaining = total_net - paid_total.
statusvarchar(16)draft | locked | paid | cancelled.
notetextFree-form Vietnamese.
locked_at, paid_atdatetime(6)Stamps for the lock + pay transitions.
created_by_user_id, locked_by_user_idchar(13)Actor refs to users.

vendor_payslips:

ColumnTypeNotes
idchar(13)Public id.
vendor_id, store_id, payroll_id, vendor_employee_idchar(13)FKs.
employee_name, employee_role_namevarchar(191)Cached display values.
base_salary, allowance_total, deduction_total, commission_total, tax_amount, net_amountdecimal(15,0)VND.
total_hours, overtime_hoursdecimalHourly rollup.
work_days, expected_days, absent_daysdecimal(5,1)Attendance.
allowance_items, deduction_items, commission_itemsjsonPer-line breakdowns.
historyjsonAudit trail.
statusvarchar(16)draft | approved | paid.

DDL lives in apps/backend/src/database/migrations/2000000000204-CreateVendorPayrollsAndPayslipsTables.ts.

  • Locked batch won’t accept line edits. That’s intentional — re-lock with a new batch instead of trying to “unlock”.
  • Net pay doesn’t recompute when you change an allowance. net_amount is stored, not derived. Recompute by hand and re-save the payslip.
  • Cross-store batch URL. Resolves to 404 — the backend never leaks ids across vendors. Open the batch from the right store in the top bar.
  • paid_total < total_net after partial disbursement. The pay endpoint expects the full settlement; partial disbursement is not yet modelled — split into multiple batches if needed.
  • BL code collisions after a vendor data import. Code is BL + zero-padded MAX(code) + 1 per vendor. If the import re-uses an old one, the next batch will collide — see backend next-payroll-code.ts.
  • Migration Unknown column store_id at runtime. Resolved by keeping the fp(...) legacy prefix as a no-op and aligning the DDL string with the entity decorator (see entity header comment).