0

Mô hình hoá proforma invoice: đánh số riêng, không vào sổ, chuyển đổi một chiều

Nếu bạn đang viết một module lập hoá đơn, sớm muộn gì cũng sẽ có người yêu cầu: "Cho em xuất một cái proforma invoice để khách chuyển tiền trước." Cách làm nhanh nhất thường là thêm một checkbox isProforma vào bảng invoices rồi đổi tiêu đề trên PDF. Cách này có thể chạy ổn được vài tuần, nhưng thường sẽ bắt đầu gây lỗi: số hoá đơn bị nhảy cóc, báo cáo công nợ phải thu (AR) phình ra những khoản khách chưa hề nợ, và kế toán hỏi vì sao có hai chứng từ cho cùng một giao dịch.

Bài này ghi lại cách chúng tôi (InvoiceWorkshop) nghĩ về proforma khi xây công cụ lập chứng từ của mình, dựa trên cách HMRC (cơ quan thuế Vương quốc Anh) mô tả loại chứng từ này. Chúng tôi dùng nguồn của Anh vì hướng dẫn của họ viết rất rõ và công khai. Quy định ở Việt Nam hoặc ở nước khác có thể khác, nên hãy xem đây là tham khảo khi thiết kế phần mềm, không phải tư vấn pháp lý, thuế hay kế toán.

1. HMRC nói gì về proforma invoice

Trong sổ tay nội bộ VAT Traders' Records Manual, HMRC có một nhóm mục riêng về proforma. Có thể tóm lại như sau:

  • Định nghĩa (VATREC9010): proforma là chứng từ thương mại chứa một phần hoặc toàn bộ thông tin thường có trên hoá đơn, nhưng không làm chức năng của hoá đơn. Nó liệt kê hàng hoá, dịch vụ sẽ được cung cấp nếu khách thanh toán, và là chứng từ để khách căn cứ vào đó mà trả tiền.
  • Không thuộc sổ sách kế toán (VATREC9010): trong điều kiện bình thường, proforma "không thể được coi là chứng từ kế toán và không có chỗ trong sổ sách". HMRC cũng khuyên ghi rõ đây là proforma, tốt nhất kèm dòng "This is not a VAT invoice".
  • Không dùng để khấu trừ thuế đầu vào (VATREC9020): proforma không phải bằng chứng hợp lệ để người mua khấu trừ VAT đầu vào. Khi đã nhận tiền hoặc đã cung cấp hàng, người bán phải xuất hoá đơn VAT thật.
  • Rủi ro (VATREC9030): người mua khấu trừ thuế dựa trên proforma, người mua khấu trừ hai lần (một lần theo proforma, một lần theo hoá đơn VAT), người bán không bao giờ xuất hoá đơn VAT mà chỉ dựa vào proforma, và người bán xác định sai thời điểm tính thuế.

Thêm một chi tiết quan trọng từ VAT Notice 700 (mục 14.2 và 14.3): thời điểm tính thuế cơ bản là lúc hoàn thành việc cung cấp hàng hoá, dịch vụ. Tuy vậy, nếu người bán nhận tiền trước thời điểm đó và trước khi xuất hoá đơn VAT thì thời điểm tính thuế thực tế là ngày nhận tiền. Mục 8.13.1 cũng nói VAT phát sinh trên các khoản đặt cọc, phí là thanh toán một phần hay toàn bộ cho việc cung cấp.

Với dev, bốn rủi ro ở VATREC9030 gần như là một danh sách yêu cầu có sẵn: mỗi rủi ro tương ứng với một ràng buộc mà hệ thống có thể ép buộc.

2. Proforma là một loại chứng từ riêng, không phải một cờ trên hoá đơn

Ràng buộc đầu tiên là proforma không nằm trong sổ. Cách đơn giản nhất để đảm bảo điều đó là đừng lưu nó như một hoá đơn. Hãy tách loại chứng từ thành một kiểu riêng (các đoạn code trong bài là ví dụ minh hoạ viết cho bài này, không phải code chép nguyên từ công cụ của chúng tôi):

type DocumentKind = 'quotation' | 'proforma' | 'invoice' | 'receipt' | 'creditNote';

interface DocumentRecord {
  id: string;
  kind: DocumentKind;
  number: string;               // ví dụ "PRO-1001", "INV-1001"
  status: 'draft' | 'completed' | 'paid';
  issueDate: string;
  validUntil?: string;          // proforma nên có hạn hiệu lực giá
  convertedFrom?: { id: string; kind: DocumentKind; number: string };
  lines: LineItem[];
}

// Chỉ những loại này mới được ghi vào công nợ phải thu
const POSTS_TO_LEDGER: ReadonlySet<DocumentKind> = new Set(['invoice', 'creditNote']);

Mọi báo cáo AR, doanh thu hay tuổi nợ chỉ lọc theo POSTS_TO_LEDGER. Như vậy một proforma dù đang ở trạng thái nào cũng không thể lọt vào sổ vì ai đó quên bỏ một checkbox.

3. Mỗi loại có dãy số riêng

Nếu proforma lấy số từ dãy hoá đơn, mỗi proforma không bao giờ được thanh toán sẽ để lại một lỗ hổng trong dãy số hoá đơn. Đến khi đối chiếu, bạn sẽ phải giải thích từng lỗ hổng một. Công cụ của chúng tôi cho mỗi loại chứng từ một tiền tố và một bộ đếm độc lập, bắt đầu từ 1001. Dưới đây là phiên bản đơn giản hoá cho năm loại trong bài (công cụ thật có nhiều loại chứng từ hơn):

// Đơn giản hoá từ công cụ của chúng tôi, không phải code nguyên văn
const numbering = {
  prefixes:    { invoice: 'INV-', proforma: 'PRO-', quotation: 'QUO-', receipt: 'REC-', creditNote: 'CN-' },
  nextNumbers: { invoice: 1001,   proforma: 1001,   quotation: 1001,   receipt: 1001,   creditNote: 1001 },
};

Dãy PRO- thủng lỗ cũng không sao, vì proforma bị bỏ là chuyện bình thường. Dãy INV- chỉ tăng khi thực sự có hoá đơn.

4. Chuyển đổi một chiều, tạo bản ghi mới

Khi khách đồng ý hoặc đã chuyển tiền, đừng sửa proforma thành hoá đơn bằng cách đổi kind. Hãy tạo một bản ghi mới với số hoá đơn mới và giữ tham chiếu ngược về proforma. Proforma cũ vẫn còn nguyên, đúng như những gì khách đã nhận.

const allowedConversions: Partial<Record<DocumentKind, DocumentKind[]>> = {
  quotation: ['invoice'],
  proforma:  ['invoice'],
  invoice:   ['receipt', 'creditNote'],
};

function convert(source: DocumentRecord, target: DocumentKind, numbering: Numbering): DocumentRecord {
  if (!allowedConversions[source.kind]?.includes(target)) {
    throw new Error(`${source.kind} không thể chuyển thành ${target}`);
  }
  const now = new Date().toISOString();
  return {
    ...structuredClone(source),
    id: crypto.randomUUID(),
    kind: target,
    status: 'draft',
    number: `${numbering.prefixes[target]}${numbering.nextNumbers[target]}`,
    convertedFrom: { id: source.id, kind: source.kind, number: source.number },
    issueDate: now.slice(0, 10),
  };
}

Có ba điểm cần chú ý:

  1. Không có đường ngược lại. Bảng allowedConversions không cho invoice → proforma. Muốn huỷ một hoá đơn đã gửi thì dùng credit note, không "hạ cấp" nó.
  2. Hoá đơn mới bắt đầu ở trạng thái draft. Số lượng thực giao, cước vận chuyển thực tế, giá có thay đổi hay không đều cần người dùng xem lại, không nên chép nguyên từ proforma.
  3. convertedFrom giúp chống khấu trừ hai lần ở phía người mua. Khi hoá đơn in ra dòng "Thay thế proforma PRO-1007", kế toán bên mua biết ngay hai chứng từ này là một giao dịch.

5. Tiền về trước hoá đơn: thêm một truy vấn cảnh báo

Rủi ro thứ ba và thứ tư trong VATREC9030 (không xuất hoá đơn VAT, sai thời điểm tính thuế) thường xảy ra theo một kịch bản rất đời thường: khách chuyển khoản theo proforma, mọi người vui vẻ giao hàng, và không ai nhớ xuất hoá đơn. Theo VAT Notice 700 mục 14.3, ở Anh, khi tiền về trước thời điểm tính thuế cơ bản và trước khi có hoá đơn VAT (đúng như kịch bản này), ngày nhận tiền trở thành thời điểm tính thuế thực tế. Vì vậy hệ thống nên nhắc người dùng.

Bạn có thể ghi nhận khoản thanh toán như một khoản ứng trước chưa phân bổ, gắn với proforma, rồi chạy một truy vấn đơn giản:

function paidButNotInvoiced(docs: DocumentRecord[], payments: Payment[]): DocumentRecord[] {
  const invoicedFrom = new Set(
    docs.filter(d => d.kind === 'invoice' && d.convertedFrom?.kind === 'proforma')
        .map(d => d.convertedFrom!.id),
  );
  const paidProformas = new Set(payments.map(p => p.proformaId).filter(Boolean));
  return docs.filter(d => d.kind === 'proforma' && paidProformas.has(d.id) && !invoicedFrom.has(d.id));
}

Hiện danh sách này ở dashboard kèm số ngày kể từ lúc nhận tiền. Hệ thống không cần biết luật từng nước; nó chỉ cần làm cho trạng thái "đã nhận tiền, chưa có hoá đơn" không thể bị quên.

6. PDF: tiêu đề và dòng chú thích do loại chứng từ quyết định

Tiêu đề trên PDF nên được suy ra từ kind, không cho gõ tự do, để không ai vô tình in chữ "TAX INVOICE" lên một proforma:

const headings: Record<DocumentKind, string> = {
  quotation: 'QUOTATION',
  proforma: 'PROFORMA INVOICE',
  invoice: 'INVOICE',
  receipt: 'RECEIPT',
  creditNote: 'CREDIT NOTE',
};

const footerNote = (kind: DocumentKind) =>
  kind === 'proforma' ? 'This is not a VAT invoice.' : undefined;

Nếu ứng dụng hỗ trợ định dạng hoá đơn GST của Ấn Độ, cũng nên áp dụng cùng nguyên tắc cho mặc định. Trong công cụ của mình, theo mặc định chúng tôi không in dấu bản sao "ORIGINAL FOR RECIPIENT" lên proforma; dấu đó được tự động thêm khi proforma được chuyển thành hoá đơn thuế (người dùng vẫn có thể tự chọn nhãn bản sao nếu muốn).

7. Các test nên có

  • Tạo và xoá 5 proforma, sau đó tạo 1 hoá đơn: số hoá đơn phải là INV-1001.
  • Tổng AR không đổi sau khi tạo một proforma ở bất kỳ trạng thái nào.
  • convert(invoice, 'proforma') phải ném lỗi.
  • Chuyển đổi xong, proforma gốc giữ nguyên số, ngày và các dòng hàng.
  • Hoá đơn tạo từ proforma có convertedFrom.number đúng và in ra tham chiếu đó.
  • Một proforma đã có thanh toán nhưng chưa chuyển đổi phải xuất hiện trong paidButNotInvoiced, và biến mất sau khi chuyển đổi.

Kết

Proforma trông giống hoá đơn đến mức rất dễ cài đặt nó thành "hoá đơn có đổi tiêu đề". Theo cách HMRC mô tả thì nó là một chứng từ khác hẳn: nằm ngoài sổ sách, không dùng để khấu trừ thuế, và phải được thay bằng hoá đơn thật khi giao dịch diễn ra. Nếu đưa những điều đó vào kiểu dữ liệu (loại riêng, dãy số riêng, chuyển đổi một chiều, cảnh báo tiền về trước hoá đơn), phần lớn lỗi sẽ bị chặn từ trước khi có người dùng.

Nếu muốn xem một cách triển khai chạy hoàn toàn trên trình duyệt (dữ liệu lưu trên máy người dùng, có nút chuyển proforma thành hoá đơn), bạn có thể thử công cụ tạo proforma invoice của InvoiceWorkshop.

Bài viết chỉ mang tính thông tin chung, không phải tư vấn pháp lý, thuế hay kế toán. Hãy kiểm tra quy định hiện hành ở nơi bạn kinh doanh.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí