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 you’ll find here
Section titled “What you’ll find here”- 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.
I. Two-tier model
Section titled “I. Two-tier model”| Tier | Table | One row per |
|---|---|---|
Payroll batch (vendor_payrolls) | Header — code, period, schedule, totals, status | A store + a period (e.g. “BL000001 — 09/2026”). |
Payslip (vendor_payslips) | One employee’s net pay breakdown for that batch | A 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.
II. Batch lifecycle
Section titled “II. Batch lifecycle”draft ──Chốt lương──▶ locked ──Thanh toán──▶ paid │ └─(void)─▶ cancelled (out of scope v1)| State | What you can do |
|---|---|
draft | Edit payslip numbers, add / remove employees, change note. |
locked | Read-only. Payslips auto-promoted to approved. No more line edits. |
paid | Read-only. Payslips auto-promoted to paid. paid_at is set; paid_total matches total_net. |
cancelled | Read-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.
III. Create a batch
Section titled “III. Create a batch”- Open More → Bảng tính lương on the POS workspace, or HR → Payrolls on the vendor dashboard.
- Tap + New batch.
- Fill in:
| Field | What to enter |
|---|---|
| Period from | First day of the pay period (store timezone, YYYY-MM-DD). |
| Period to | Last day of the pay period. |
| Pay schedule | monthly | weekly | biweekly. Drives the period label. |
| (optional) Initial payslips | Pre-fill the batch with one row per active employee. The wage engine seeds base salary + hours; you can override any number. |
| (optional) Note | Vietnamese copy OK. |
- 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”.
IV. Edit payslips (draft only)
Section titled “IV. Edit payslips (draft only)”Open the batch → tap a payslip row → the editor shows:
| Section | Fields |
|---|---|
| Wage | base_salary, total_hours, overtime_hours. |
| Allowances | Sum + per-line items (JSON). Add: meal, phone, travel, seniority. |
| Deductions | Sum + per-line items (JSON). Add: insurance, advance, loan repayment. Stored as positive numbers — the UI negates. |
| Commissions | Sum + per-line items (JSON). Each item is a (sheet_id, item_id, qty, rate) reference so the report ties back to a commission sheet. |
| Tax | tax_amount (VND withheld). |
| Attendance | work_days, expected_days, absent_days. |
| Net | net_amount = base + hours_wage + allowance + commission − deduction − tax. Stored, not recomputed. |
| History | JSON 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:
- Open the batch → tap Chốt lương (Lock).
- The backend sets
status = locked, setslocked_at, and cascadesapprovedto every child payslip. - 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.
VI. Pay the batch — “Thanh toán”
Section titled “VI. Pay the batch — “Thanh toán””When you’ve disbursed the salaries:
- Open the batch → tap Thanh toán (Pay).
- Fill in:
| Field | What to enter |
|---|---|
| Payment method | cash | bank_transfer | card. |
| (optional) Paid on | Override the paid_at date (defaults to now). |
| (optional) Reference | Bank ref / cheque number. |
| (optional) Note | Vietnamese copy OK. |
- Confirm. The backend sets
status = paid,paid_at,paid_total = total_net, and cascadespaidto every child payslip.
The endpoint is POST /api/v1/vendor/payrolls/{id}/pay.
VII. Where commissions plug in
Section titled “VII. Where commissions plug in”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.
VIII. Endpoints
Section titled “VIII. Endpoints”| Method | Path | Purpose |
|---|---|---|
GET | /api/v1/vendor/payrolls | List 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/payrolls | Create a batch (optional initial payslips). Returns the new batch with code BL00000X. |
POST | /api/v1/vendor/payrolls/{id}/lock | Promote draft → locked; cascades to payslips. |
POST | /api/v1/vendor/payrolls/{id}/pay | Settle 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.
IX. Schema
Section titled “IX. Schema”vendor_payrolls:
| Column | Type | Notes |
|---|---|---|
id | char(13) | Crockford Base32, minted by PublicIdSubscriber. |
vendor_id, store_id | char(13) | Multi-tenant keys. |
code | varchar(16) | BL + 6-digit zero-padded seq per vendor (e.g. BL000001). |
period_label | varchar(32) | "09/2026" shown in list rows. |
period_from, period_to | date | YYYY-MM-DD in the store timezone. |
pay_schedule | varchar(16) | monthly | weekly | biweekly. |
payslip_count | int unsigned | Cache — surface a single SELECT. |
total_net | decimal(15,0) | Sum of payslip net_amount (VND). |
paid_total | decimal(15,0) | Sum of payslip amounts disbursed. remaining = total_net - paid_total. |
status | varchar(16) | draft | locked | paid | cancelled. |
note | text | Free-form Vietnamese. |
locked_at, paid_at | datetime(6) | Stamps for the lock + pay transitions. |
created_by_user_id, locked_by_user_id | char(13) | Actor refs to users. |
vendor_payslips:
| Column | Type | Notes |
|---|---|---|
id | char(13) | Public id. |
vendor_id, store_id, payroll_id, vendor_employee_id | char(13) | FKs. |
employee_name, employee_role_name | varchar(191) | Cached display values. |
base_salary, allowance_total, deduction_total, commission_total, tax_amount, net_amount | decimal(15,0) | VND. |
total_hours, overtime_hours | decimal | Hourly rollup. |
work_days, expected_days, absent_days | decimal(5,1) | Attendance. |
allowance_items, deduction_items, commission_items | json | Per-line breakdowns. |
history | json | Audit trail. |
status | varchar(16) | draft | approved | paid. |
DDL lives in apps/backend/src/database/migrations/2000000000204-CreateVendorPayrollsAndPayslipsTables.ts.
X. Common pitfalls
Section titled “X. Common pitfalls”- 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_amountis 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_netafter partial disbursement. Thepayendpoint expects the full settlement; partial disbursement is not yet modelled — split into multiple batches if needed.BLcode collisions after a vendor data import. Code isBL+ zero-paddedMAX(code) + 1per vendor. If the import re-uses an old one, the next batch will collide — see backendnext-payroll-code.ts.- Migration
Unknown column store_idat runtime. Resolved by keeping thefp(...)legacy prefix as a no-op and aligning the DDL string with the entity decorator (see entity header comment).
Related
Section titled “Related”- Commission sheets — the per-product rules that feed
commission_total. - Employees and stores — employee profile + per-store payroll address.
- Timesheets — the work-day totals that feed
work_days. - Roles — the
payrollmodule grants access to this surface. - API catalog → Vendor / Payroll.
- Migration DDL.