Document ledger reprints, optional dueDate, Limmud FSU invoice data
Add --print to regenerate PDFs from invoices.json without ledger writes. Support optional dueDate on ledger rows (default remains invoice date + 14 days) and persist dueDate when saving new production rows. Data: limmud_fsu_canada client with invoiceFileName, cruise_event_av_it product, 2026-LFSU01 sample invoice, Levkin contact fields in sender.json. README and project.md describe CLI, schema, and May 2026 changelog. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
0bec2d8e6f
commit
0cb8ad3d11
164
README.md
164
README.md
@ -1,120 +1,150 @@
|
|||||||
# Invoice Generator
|
# Invoice Generator
|
||||||
|
|
||||||
A powerful and flexible command-line tool for generating PDF invoices from simple JSON data files. It features an interactive mode for ease of use and a robust, non-interactive mode for scripting and automation.
|
A command-line tool for generating professional PDF invoices from JSON data files. Features interactive and non-interactive modes with preview-first workflow.
|
||||||
|
|
||||||
## Features
|
## Quick Start
|
||||||
|
|
||||||
- **Interactive & Non-Interactive Modes**: Use the simple interactive wizard (`npm start`) or pass arguments directly for scripted use.
|
1. **Install dependencies:**
|
||||||
- **Safe Preview-First Workflow**: Always generates a development preview first. You must confirm before the official invoice is created and recorded, preventing accidental invoice generation.
|
|
||||||
- **Data-Driven**: Senders, clients, and products are managed in simple `.json` files, making them easy to update without changing code.
|
|
||||||
- **Automatic Invoice Numbering**: Invoice numbers are automatically incremented based on the year and existing records. The sequence resets annually.
|
|
||||||
- **Organized Output**: Generated PDFs are neatly stored in folders organized by sender (`invoice/<sender_name>/`).
|
|
||||||
- **Unit Tested**: Core logic is verified with unit tests using Jest.
|
|
||||||
|
|
||||||
## Prerequisites
|
|
||||||
|
|
||||||
- [Node.js](https://nodejs.org/) (v16 or higher recommended)
|
|
||||||
- [npm](https://www.npmjs.com/) (comes with Node.js)
|
|
||||||
|
|
||||||
## Setup
|
|
||||||
|
|
||||||
1. Clone the repository or download the source code.
|
|
||||||
2. Open your terminal in the project directory.
|
|
||||||
3. Install the dependencies:
|
|
||||||
```bash
|
```bash
|
||||||
npm install
|
npm install
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage
|
2. **Generate an invoice:**
|
||||||
|
```bash
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
There are two primary ways to generate an invoice:
|
## Usage Options
|
||||||
|
|
||||||
### 1. Interactive Mode (Recommended for most users)
|
### Interactive Mode (Recommended)
|
||||||
|
|
||||||
Run the interactive wizard. The tool will guide you through selecting a sender, client, and one or more products.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm start
|
npm start
|
||||||
```
|
```
|
||||||
|
|
||||||
The script will first generate a **development preview** PDF. After you review it, you will be prompted to confirm whether you want to create the final **production** invoice.
|
Guided wizard to select sender, client, and products.
|
||||||
|
|
||||||
### 2. Non-Interactive / Scripted Mode
|
### Non-Interactive Mode
|
||||||
|
|
||||||
You can also generate an invoice by passing command-line arguments. This is useful for automation and scripting.
|
|
||||||
|
|
||||||
The basic command is:
|
|
||||||
`npm run generate -- --sender <sender_key> --client <client_key> --products <product_key_1> <product_key_2>`
|
|
||||||
|
|
||||||
**Example:**
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run generate -- --sender company1 --client company4 --products linkedin_scraper web_design
|
npm run generate -- --sender levkin --client levitin --products linkedin_scraper web_design
|
||||||
```
|
```
|
||||||
|
|
||||||
Like the interactive mode, this will also generate a preview and prompt for confirmation before creating the production invoice.
|
### Test Mode
|
||||||
|
|
||||||
### Running Tests
|
|
||||||
|
|
||||||
To run the automated tests for the project:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm test
|
npm run test-invoice
|
||||||
```
|
```
|
||||||
|
|
||||||
## How the Data Files Work
|
Uses first available options for testing.
|
||||||
|
|
||||||
All data is managed in the `data/` directory.
|
### Reprint an existing invoice (from ledger)
|
||||||
|
|
||||||
### `sender.json`
|
Generates a PDF from `data/invoices.json` only — **does not** change the ledger or run the preview prompt:
|
||||||
|
|
||||||
Stores your company or sender profiles. Each key represents a unique sender.
|
```bash
|
||||||
|
node index.js --print 2026-LFSU01
|
||||||
|
```
|
||||||
|
|
||||||
|
Output path: `invoice/<senderKey>/<filenamePrefix>_<number>.pdf`. The filename prefix is the client’s optional `invoiceFileName` in `client.json`, otherwise the client key.
|
||||||
|
|
||||||
|
**Due date on the PDF:** Each ledger row can include optional `"dueDate": "YYYY-MM-DD"`. If omitted, the tool uses invoice `date` plus 14 days.
|
||||||
|
|
||||||
|
All data is stored in the `data/` directory:
|
||||||
|
|
||||||
|
### `data/sender.json` - Your Company Profiles
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"company1": { "company": "Your Company", "address": ... },
|
"levkin": {
|
||||||
"company2": { "company": "Another Profile", "address": ... }
|
"company": "Levkin Inc.",
|
||||||
|
"contactName": "Ilia Dobkin",
|
||||||
|
"phone": "647 987 2792",
|
||||||
|
"email": "idobkin@gmail.com",
|
||||||
|
"address": "6 Keefer Court",
|
||||||
|
"zip": "L4J 5Y4",
|
||||||
|
"city": "Thornhill, ON",
|
||||||
|
"country": "Canada",
|
||||||
|
"taxName": "HST",
|
||||||
|
"taxNumber": "79438 2895 RT0001",
|
||||||
|
"paymentInfo": "Bank transfer details..."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### `client.json`
|
### `data/client.json` - Client Information
|
||||||
|
|
||||||
Stores your client profiles. The `invoiceFileName` key is optional; if provided, it will be used for the generated PDF's filename.
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"company3": { "company": "First Client", "address": ... },
|
"levitin": {
|
||||||
"company4": { "company": "Second Client", "invoiceFileName": "second_client_invoices", "address": ... }
|
"company": "LEVITIN EMPLOYMENT LAWYERS",
|
||||||
|
"address": "#2809 - 330 Richmond Street West",
|
||||||
|
"zip": "M5V 0M4",
|
||||||
|
"city": "Toronto, ON",
|
||||||
|
"country": "Canada"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### `products.json`
|
### `data/products.json` - Services/Products Catalog
|
||||||
|
|
||||||
A catalog of the services or products you offer.
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"linkedin_scraper": {
|
"linkedin_scraper": {
|
||||||
"description": "LinkedIn Scraper Bot",
|
"description": "LinkedIn Scraper Bot",
|
||||||
"price": 250,
|
"price": 250,
|
||||||
"taxRate": 0
|
"taxRate": 13
|
||||||
},
|
},
|
||||||
"web_design": { "description": "Website Design", "price": 1200, "taxRate": 0 }
|
"web_design": {
|
||||||
|
"description": "Website Design",
|
||||||
|
"price": 1200,
|
||||||
|
"taxRate": 13
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### `invoices.json`
|
## Workflow
|
||||||
|
|
||||||
This file is your ledger. It's **automatically updated** when you create a production invoice. It tracks all generated invoices to determine the next sequential number. You should not need to edit this file manually.
|
1. **Preview Generation**: Creates a development preview PDF first
|
||||||
|
2. **Review**: Check the preview in `invoice/<sender>/<client>_<year>-DEV-PREVIEW.pdf`
|
||||||
|
3. **Confirmation**: Choose to create the production invoice or keep only the preview
|
||||||
|
4. **Production**: Generates final invoice with sequential numbering and saves to ledger
|
||||||
|
|
||||||
|
## Output Structure
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"invoices": [
|
|
||||||
{
|
|
||||||
"number": "2024-0001",
|
|
||||||
"date": "2024-07-31",
|
|
||||||
...
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
invoice/
|
||||||
|
├── levkin/
|
||||||
|
│ ├── levitin_2025-0007.pdf # Production invoice
|
||||||
|
│ └── levitin_2025-DEV-PREVIEW.pdf # Preview
|
||||||
|
└── dobit/
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm test
|
||||||
|
```
|
||||||
|
|
||||||
|
Tests cover invoice numbering logic including year resets and sequential ordering.
|
||||||
|
|
||||||
|
## Key Features
|
||||||
|
|
||||||
|
- **Safe Preview-First**: Always generates preview before production
|
||||||
|
- **Automatic Numbering**: Sequential invoice numbers (YYYY-0001, YYYY-0002...)
|
||||||
|
- **Year Reset**: Invoice numbers reset annually
|
||||||
|
- **Organized Output**: PDFs stored by sender in separate folders
|
||||||
|
- **Tax Support**: Configurable tax rates per product
|
||||||
|
- **Payment Info**: Custom payment instructions in footer
|
||||||
|
- **Ledger Tracking**: All invoices automatically recorded in `data/invoices.json`
|
||||||
|
- **Reprint mode** (`--print`): PDF from ledger without editing it
|
||||||
|
- **Optional `dueDate`** on ledger rows for custom payment deadlines when reprinting
|
||||||
|
|
||||||
|
See [project.md](project.md) for full schema, CLI details, and changelog.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Node.js v16+
|
||||||
|
- npm
|
||||||
|
|||||||
@ -12,5 +12,27 @@
|
|||||||
"zip": "M5V 0M4",
|
"zip": "M5V 0M4",
|
||||||
"city": "Toronto, ON",
|
"city": "Toronto, ON",
|
||||||
"country": "Canada"
|
"country": "Canada"
|
||||||
|
},
|
||||||
|
"jrcc": {
|
||||||
|
"company": "JRCC - Jewish Russian Community Centre",
|
||||||
|
"address": "5987 Bathurst St #3",
|
||||||
|
"zip": "M2R 1Z3",
|
||||||
|
"city": "North York, ON",
|
||||||
|
"country": "Canada"
|
||||||
|
},
|
||||||
|
"innosphere": {
|
||||||
|
"company": "Innosphere SDG Ltd.",
|
||||||
|
"address": "49 Norfolk Street - Suite 300",
|
||||||
|
"zip": "N1H 4J1",
|
||||||
|
"city": "Guelph, Ontario",
|
||||||
|
"country": "Canada"
|
||||||
|
},
|
||||||
|
"limmud_fsu_canada": {
|
||||||
|
"company": "Limmud FSU Canada",
|
||||||
|
"address": "9600 Bathurst St",
|
||||||
|
"zip": "L6A 3Z8",
|
||||||
|
"city": "Maple, Ontario",
|
||||||
|
"country": "Canada",
|
||||||
|
"invoiceFileName": "limmud_fsuc"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -118,6 +118,75 @@
|
|||||||
"subtotal": 250,
|
"subtotal": 250,
|
||||||
"taxTotal": 32.5,
|
"taxTotal": 32.5,
|
||||||
"total": 282.5
|
"total": 282.5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"number": "2026-JRCC10",
|
||||||
|
"date": "2026-04-30",
|
||||||
|
"sender": "Levkin Inc.",
|
||||||
|
"client": "JRCC - Jewish Russian Community Centre",
|
||||||
|
"products": [
|
||||||
|
{
|
||||||
|
"description": "Design and development of PunimTag Web: full-stack facial recognition photo management platform (FastAPI backend, React frontend) with DeepFace-based face detection and identification, PostgreSQL/SQLite data storage, Redis/RQ background processing, advanced search and tagging, and deployment-ready configuration and documentation.",
|
||||||
|
"price": 10000,
|
||||||
|
"quantity": 1,
|
||||||
|
"taxRate": 13
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"subtotal": 10000,
|
||||||
|
"taxTotal": 1300,
|
||||||
|
"total": 11300
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"number": "2026-INNO16",
|
||||||
|
"date": "2026-03-31",
|
||||||
|
"sender": "Levkin Inc.",
|
||||||
|
"client": "Innosphere SDG Ltd.",
|
||||||
|
"products": [
|
||||||
|
{
|
||||||
|
"description": "Javascript Developer - Mar 1 - Mar 31, 2026",
|
||||||
|
"price": 85,
|
||||||
|
"quantity": 136,
|
||||||
|
"taxRate": 13
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"subtotal": 11560,
|
||||||
|
"taxTotal": 1502.8,
|
||||||
|
"total": 13062.8
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"number": "2026-INNO17",
|
||||||
|
"date": "2026-04-30",
|
||||||
|
"sender": "Levkin Inc.",
|
||||||
|
"client": "Innosphere SDG Ltd.",
|
||||||
|
"products": [
|
||||||
|
{
|
||||||
|
"description": "Javascript Developer - Apr 1 - Apr 30, 2026",
|
||||||
|
"price": 85,
|
||||||
|
"quantity": 176,
|
||||||
|
"taxRate": 13
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"subtotal": 14960,
|
||||||
|
"taxTotal": 1944.8,
|
||||||
|
"total": 16904.8
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"number": "2026-LFSU01",
|
||||||
|
"date": "2026-03-22",
|
||||||
|
"dueDate": "2026-05-31",
|
||||||
|
"sender": "Levkin Inc.",
|
||||||
|
"client": "Limmud FSU Canada",
|
||||||
|
"products": [
|
||||||
|
{
|
||||||
|
"description": "March 15–22, 2026 — Cruise event IT — audio/video and stage tech: microphones, speakers, video projection and signage, presenter feeds, cabling and rack setup, sound checks and on-site troubleshooting for onboard Limmud FSU programming.",
|
||||||
|
"price": 500,
|
||||||
|
"quantity": 1,
|
||||||
|
"taxRate": 13
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"subtotal": 500,
|
||||||
|
"taxTotal": 65,
|
||||||
|
"total": 565
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -8,5 +8,10 @@
|
|||||||
"description": "Website Design",
|
"description": "Website Design",
|
||||||
"price": 1200,
|
"price": 1200,
|
||||||
"taxRate": 13
|
"taxRate": 13
|
||||||
|
},
|
||||||
|
"cruise_event_av_it": {
|
||||||
|
"description": "March 15–22, 2026 — Cruise event IT — audio/video and stage tech: microphones, mixing, amplifiers and speakers, video projection and signage, presenter feeds, cabling and rack setup, sound checks and on-site troubleshooting for onboard Limmud FSU programming.",
|
||||||
|
"price": 500,
|
||||||
|
"taxRate": 13
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,13 +1,16 @@
|
|||||||
{
|
{
|
||||||
"levkin": {
|
"levkin": {
|
||||||
"company": "Levkin Inc.",
|
"company": "Levkin Inc.",
|
||||||
|
"contactName": "Ilia Dobkin",
|
||||||
|
"phone": "647 987 2792",
|
||||||
|
"email": "idobkin@gmail.com",
|
||||||
"address": "6 Keefer Court",
|
"address": "6 Keefer Court",
|
||||||
"zip": "L4J 5Y4",
|
"zip": "L4J 5Y4",
|
||||||
"city": "Thornhill, ON",
|
"city": "Thornhill, ON",
|
||||||
"country": "Canada",
|
"country": "Canada",
|
||||||
"taxName": "HST",
|
"taxName": "HST",
|
||||||
"taxNumber": "79438 2895 RT0001",
|
"taxNumber": "79438 2895 RT0001",
|
||||||
"paymentInfo": "<div style=\"text-align:left;\">All payments to be made by bank transfer to the following account:<br><br>- Levkin Inc. - Ilia Dobkin<br>- 6 Keefer Court, ON, L4J 5Y4<br>- Account: 5004295<br>- Transit: 31822<br>- 2600 Simcoe St. N. Unit 1, Oshawa, ON, L1L 0R1 - Inst: 004<br>- SWIFT Code: TDOMCATTTOR</div>"
|
"paymentInfo": "<div style=\"text-align:left;\">All payments to be made by bank transfer to the following account:<br><br>- Levkin Inc. - Ilia Dobkin<br>- 6 Keefer Court, ON, L4J 5Y4<br>- Account: 5004295<br>- Transit: 31822<br>- Inst: 004<br>- 2600 Simcoe St. N. Unit 1, Oshawa, ON, L1L 0R1<br>- SWIFT Code: TDOMCATTTOR</div>"
|
||||||
},
|
},
|
||||||
"dobit": {
|
"dobit": {
|
||||||
"company": "DOBIT Consulting",
|
"company": "DOBIT Consulting",
|
||||||
|
|||||||
104
index.js
104
index.js
@ -78,7 +78,7 @@ function saveInvoiceRecord(invoiceData, invoiceProducts) {
|
|||||||
return sum + (p.price * p.quantity * taxRate) / 100;
|
return sum + (p.price * p.quantity * taxRate) / 100;
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
invoicesData.invoices.push({
|
const ledgerRow = {
|
||||||
number: invoiceData.information.number,
|
number: invoiceData.information.number,
|
||||||
date: invoiceData.information.date,
|
date: invoiceData.information.date,
|
||||||
sender: invoiceData.sender.company,
|
sender: invoiceData.sender.company,
|
||||||
@ -92,7 +92,11 @@ function saveInvoiceRecord(invoiceData, invoiceProducts) {
|
|||||||
subtotal: parseFloat(subtotal.toFixed(2)),
|
subtotal: parseFloat(subtotal.toFixed(2)),
|
||||||
taxTotal: parseFloat(taxTotal.toFixed(2)),
|
taxTotal: parseFloat(taxTotal.toFixed(2)),
|
||||||
total: parseFloat((subtotal + taxTotal).toFixed(2)),
|
total: parseFloat((subtotal + taxTotal).toFixed(2)),
|
||||||
});
|
};
|
||||||
|
if (invoiceData.information.dueDate) {
|
||||||
|
ledgerRow.dueDate = invoiceData.information.dueDate;
|
||||||
|
}
|
||||||
|
invoicesData.invoices.push(ledgerRow);
|
||||||
try {
|
try {
|
||||||
fs.writeFileSync(FILES.INVOICES, JSON.stringify(invoicesData, null, 2));
|
fs.writeFileSync(FILES.INVOICES, JSON.stringify(invoicesData, null, 2));
|
||||||
console.log("✅ Invoice record saved to invoices.json.");
|
console.log("✅ Invoice record saved to invoices.json.");
|
||||||
@ -108,6 +112,11 @@ async function main() {
|
|||||||
type: "boolean",
|
type: "boolean",
|
||||||
describe: "Run in interactive mode",
|
describe: "Run in interactive mode",
|
||||||
})
|
})
|
||||||
|
.option("print", {
|
||||||
|
type: "string",
|
||||||
|
describe:
|
||||||
|
"Generate a PDF for an existing invoice number (from data/invoices.json) without modifying the ledger",
|
||||||
|
})
|
||||||
.option("sender", {
|
.option("sender", {
|
||||||
type: "string",
|
type: "string",
|
||||||
describe: "Sender key from sender.json",
|
describe: "Sender key from sender.json",
|
||||||
@ -132,6 +141,97 @@ async function main() {
|
|||||||
const clients = loadJson(FILES.CLIENTS);
|
const clients = loadJson(FILES.CLIENTS);
|
||||||
const productsData = loadJson(FILES.PRODUCTS);
|
const productsData = loadJson(FILES.PRODUCTS);
|
||||||
|
|
||||||
|
// --- Print existing invoice (no prompts, no ledger update) ---
|
||||||
|
if (argv.print) {
|
||||||
|
const invoiceNumber = argv.print;
|
||||||
|
const invoicesData = loadJson(FILES.INVOICES);
|
||||||
|
const record = invoicesData.invoices.find((inv) => inv.number === invoiceNumber);
|
||||||
|
if (!record) {
|
||||||
|
console.error(`Invoice not found in invoices.json: ${invoiceNumber}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolvedSenderKey =
|
||||||
|
Object.keys(senders).find((k) => senders[k]?.company === record.sender) || null;
|
||||||
|
const resolvedClientKey =
|
||||||
|
Object.keys(clients).find((k) => clients[k]?.company === record.client) || null;
|
||||||
|
|
||||||
|
if (!resolvedSenderKey) {
|
||||||
|
console.error(
|
||||||
|
`Could not match sender company "${record.sender}" to a key in sender.json.`
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
if (!resolvedClientKey) {
|
||||||
|
console.error(
|
||||||
|
`Could not match client company "${record.client}" to a key in client.json.`
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sender = senders[resolvedSenderKey];
|
||||||
|
const client = clients[resolvedClientKey];
|
||||||
|
|
||||||
|
const invoiceProducts = (record.products || []).map((p) => ({
|
||||||
|
quantity: p.quantity ?? 1,
|
||||||
|
description: p.description,
|
||||||
|
"tax-rate": p.taxRate ?? 0,
|
||||||
|
price: p.price,
|
||||||
|
}));
|
||||||
|
if (invoiceProducts.length === 0) {
|
||||||
|
console.error(`Invoice ${invoiceNumber} has no products.`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const senderForInvoice = { ...sender };
|
||||||
|
if (sender.taxNumber) {
|
||||||
|
senderForInvoice.custom1 = `${sender.taxName || "Tax"} Number: ${
|
||||||
|
sender.taxNumber
|
||||||
|
}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let dueDateStr;
|
||||||
|
if (record.dueDate) {
|
||||||
|
dueDateStr = record.dueDate;
|
||||||
|
} else {
|
||||||
|
const due = new Date(record.date);
|
||||||
|
due.setDate(due.getDate() + 14);
|
||||||
|
dueDateStr = due.toISOString().split("T")[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
const commonData = {
|
||||||
|
sender: senderForInvoice,
|
||||||
|
client,
|
||||||
|
products: invoiceProducts,
|
||||||
|
bottomNotice: sender.paymentInfo
|
||||||
|
? `Thank you for your business!<br /><br /><br />${sender.paymentInfo}`
|
||||||
|
: "Thank you for your business!",
|
||||||
|
settings: { currency: "CAD" },
|
||||||
|
translate: {
|
||||||
|
taxNotation: sender.taxName || "VAT",
|
||||||
|
number: "Invoice",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const printData = {
|
||||||
|
...commonData,
|
||||||
|
information: {
|
||||||
|
number: record.number,
|
||||||
|
date: record.date,
|
||||||
|
dueDate: dueDateStr,
|
||||||
|
},
|
||||||
|
mode: "production",
|
||||||
|
};
|
||||||
|
|
||||||
|
const fileNameKey = client.invoiceFileName || resolvedClientKey;
|
||||||
|
const outFileName = `${fileNameKey}_${record.number}.pdf`;
|
||||||
|
const outFilePath = path.join(FOLDERS.INVOICE, resolvedSenderKey, outFileName);
|
||||||
|
|
||||||
|
console.log(`\nPrinting invoice ${record.number}...`);
|
||||||
|
const ok = await createInvoice(printData, outFilePath);
|
||||||
|
process.exit(ok ? 0 : 1);
|
||||||
|
}
|
||||||
|
|
||||||
// Test mode: use first available options
|
// Test mode: use first available options
|
||||||
if (argv.test) {
|
if (argv.test) {
|
||||||
senderKey = Object.keys(senders)[0];
|
senderKey = Object.keys(senders)[0];
|
||||||
|
|||||||
324
project.md
Normal file
324
project.md
Normal file
@ -0,0 +1,324 @@
|
|||||||
|
# Invoice Generator - Project Documentation
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
A Node.js command-line application for generating professional PDF invoices. The system uses a data-driven approach where all business information (senders, clients, products) is stored in JSON files, making it easy to maintain without code changes.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Core Components
|
||||||
|
|
||||||
|
```
|
||||||
|
invoice/
|
||||||
|
├── index.js # Main application entry point
|
||||||
|
├── data/ # JSON data files
|
||||||
|
│ ├── sender.json # Company/sender profiles
|
||||||
|
│ ├── client.json # Client information
|
||||||
|
│ ├── products.json # Product/service catalog
|
||||||
|
│ └── invoices.json # Invoice ledger (auto-generated)
|
||||||
|
├── invoice/ # Generated PDF output
|
||||||
|
│ └── <sender_name>/ # Organized by sender
|
||||||
|
└── test/
|
||||||
|
└── invoice.test.js # Unit tests
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
|
||||||
|
**Production:**
|
||||||
|
|
||||||
|
- `easyinvoice` (v3.0.47) - PDF generation library
|
||||||
|
- `inquirer` (v8.2.4) - Interactive command-line prompts
|
||||||
|
- `yargs` (v17.7.2) - Command-line argument parsing
|
||||||
|
|
||||||
|
**Development:**
|
||||||
|
|
||||||
|
- `jest` (v29.7.0) - Testing framework
|
||||||
|
|
||||||
|
## Data Flow
|
||||||
|
|
||||||
|
### 1. Data Loading
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Loads all JSON files from data/ directory
|
||||||
|
const senders = loadJson(FILES.SENDERS);
|
||||||
|
const clients = loadJson(FILES.CLIENTS);
|
||||||
|
const productsData = loadJson(FILES.PRODUCTS);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. User Input Processing
|
||||||
|
|
||||||
|
- **Interactive Mode**: Uses `inquirer` prompts for sender, client, and product selection
|
||||||
|
- **Non-Interactive Mode**: Processes command-line arguments via `yargs`
|
||||||
|
- **Test Mode**: Automatically selects first available options
|
||||||
|
|
||||||
|
### 3. Invoice Generation Process
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[Load Data] --> B[Validate Inputs]
|
||||||
|
B --> C[Generate Preview PDF]
|
||||||
|
C --> D[User Review]
|
||||||
|
D --> E{Proceed?}
|
||||||
|
E -->|Yes| F[Generate Production PDF]
|
||||||
|
E -->|No| G[Keep Preview Only]
|
||||||
|
F --> H[Save to Ledger]
|
||||||
|
H --> I[Update invoices.json]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. File Output
|
||||||
|
|
||||||
|
- **Preview**: `invoice/<sender>/<client>_<year>-DEV-PREVIEW.pdf`
|
||||||
|
- **Production**: `invoice/<sender>/<client>_<year>-<sequence>.pdf`
|
||||||
|
|
||||||
|
## Key Functions
|
||||||
|
|
||||||
|
### `getNextInvoiceNumber(year, invoicesData)`
|
||||||
|
|
||||||
|
- Generates sequential invoice numbers (YYYY-0001, YYYY-0002...)
|
||||||
|
- Resets numbering annually
|
||||||
|
- Handles unordered invoice numbers correctly
|
||||||
|
|
||||||
|
### `createInvoice(invoiceData, invoiceFilePath)`
|
||||||
|
|
||||||
|
- Uses `easyinvoice` library to generate PDF
|
||||||
|
- Creates directory structure if needed
|
||||||
|
- Returns success/failure status
|
||||||
|
|
||||||
|
### `saveInvoiceRecord(invoiceData, invoiceProducts)`
|
||||||
|
|
||||||
|
- Calculates totals (subtotal, tax, grand total)
|
||||||
|
- Appends invoice record to `invoices.json`
|
||||||
|
- Persists `dueDate` from `invoiceData.information.dueDate` when set, for accurate future `--print` output
|
||||||
|
- Maintains complete invoice history
|
||||||
|
|
||||||
|
## Data Schema
|
||||||
|
|
||||||
|
### Sender Schema
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"company": "string",
|
||||||
|
"contactName": "string (optional)",
|
||||||
|
"phone": "string (optional)",
|
||||||
|
"email": "string (optional)",
|
||||||
|
"address": "string",
|
||||||
|
"zip": "string",
|
||||||
|
"city": "string",
|
||||||
|
"country": "string",
|
||||||
|
"taxName": "string (optional)",
|
||||||
|
"taxNumber": "string (optional)",
|
||||||
|
"paymentInfo": "string (optional)"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Client Schema
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"company": "string",
|
||||||
|
"address": "string",
|
||||||
|
"zip": "string",
|
||||||
|
"city": "string",
|
||||||
|
"country": "string",
|
||||||
|
"invoiceFileName": "string (optional)"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Product Schema
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"description": "string",
|
||||||
|
"price": "number",
|
||||||
|
"taxRate": "number (optional, default 0)"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Invoice Record Schema
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"number": "string (YYYY-0001 or custom e.g. 2026-LFSU01)",
|
||||||
|
"date": "string (YYYY-MM-DD)",
|
||||||
|
"dueDate": "string (YYYY-MM-DD, optional — see Due dates below)",
|
||||||
|
"sender": "string (must match sender.json company name)",
|
||||||
|
"client": "string (must match client.json company name)",
|
||||||
|
"products": [
|
||||||
|
{
|
||||||
|
"description": "string",
|
||||||
|
"price": "number",
|
||||||
|
"quantity": "number",
|
||||||
|
"taxRate": "number"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"subtotal": "number",
|
||||||
|
"taxTotal": "number",
|
||||||
|
"total": "number"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Due dates:** When you run `node index.js --print <number>`, the PDF due date is taken from `dueDate` if present. Otherwise it defaults to `date` plus 14 days. New production invoices still compute due as “today + 14” in the UI flow; `saveInvoiceRecord` stores that value on the ledger row as `dueDate` when present so reprints stay consistent. You can edit `dueDate` in `invoices.json` for any row (for example, payment terms through end of month).
|
||||||
|
|
||||||
|
## Command-Line Interface
|
||||||
|
|
||||||
|
### Available Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm start # Interactive mode
|
||||||
|
npm run generate # Non-interactive mode
|
||||||
|
npm test # Run unit tests
|
||||||
|
npm run test-invoice # Test mode (dev only)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Reprinting from the ledger (no prompts, no ledger changes)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node index.js --print <invoice-number>
|
||||||
|
```
|
||||||
|
|
||||||
|
Looks up `invoice-number` in `data/invoices.json`, resolves sender and client by matching `company` strings to `sender.json` / `client.json`, and writes `invoice/<senderKey>/<filename>_<number>.pdf`. The client’s optional `invoiceFileName` is used as the filename prefix when set (for example `limmud_fsuc_2026-LFSU01.pdf`).
|
||||||
|
|
||||||
|
### Command-Line Options
|
||||||
|
|
||||||
|
```bash
|
||||||
|
--interactive # Force interactive mode
|
||||||
|
--print <invoice-number> # PDF only from invoices.json; does not append ledger
|
||||||
|
--sender <key> # Sender key from sender.json
|
||||||
|
--client <key> # Client key from client.json
|
||||||
|
--products <key1> <key2> # Product keys from products.json
|
||||||
|
--test # Test mode (uses first available options)
|
||||||
|
--help # Show help
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Strategy
|
||||||
|
|
||||||
|
### Unit Tests (`test/invoice.test.js`)
|
||||||
|
|
||||||
|
- **Invoice Numbering**: Tests sequential numbering logic
|
||||||
|
- **Year Reset**: Verifies numbering resets annually
|
||||||
|
- **Unordered Numbers**: Handles gaps in invoice sequence
|
||||||
|
|
||||||
|
### Test Coverage
|
||||||
|
|
||||||
|
- `getNextInvoiceNumber()` function
|
||||||
|
- Edge cases for invoice numbering
|
||||||
|
- Year boundary scenarios
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
### File System Errors
|
||||||
|
|
||||||
|
- Missing JSON files trigger helpful error messages
|
||||||
|
- Invalid JSON syntax is caught and reported
|
||||||
|
- Directory creation failures are handled gracefully
|
||||||
|
|
||||||
|
### Data Validation
|
||||||
|
|
||||||
|
- Invalid sender/client/product keys are rejected
|
||||||
|
- Missing required fields trigger validation errors
|
||||||
|
- Tax rate calculations handle missing values
|
||||||
|
|
||||||
|
### PDF Generation Errors
|
||||||
|
|
||||||
|
- `easyinvoice` failures are caught and logged
|
||||||
|
- File write errors are handled with user feedback
|
||||||
|
|
||||||
|
## Configuration Options
|
||||||
|
|
||||||
|
### Currency
|
||||||
|
|
||||||
|
- Default: CAD (Canadian Dollar)
|
||||||
|
- Set in `commonData.settings.currency`
|
||||||
|
|
||||||
|
### Tax Configuration
|
||||||
|
|
||||||
|
- Per-product tax rates in `products.json`
|
||||||
|
- Tax name customization in `sender.json`
|
||||||
|
- Automatic tax calculations
|
||||||
|
|
||||||
|
### Payment Information
|
||||||
|
|
||||||
|
- Custom payment instructions in `sender.paymentInfo`
|
||||||
|
- Rendered in invoice footer
|
||||||
|
- Supports HTML formatting
|
||||||
|
|
||||||
|
## File Organization
|
||||||
|
|
||||||
|
### Input Files (`data/`)
|
||||||
|
|
||||||
|
- **sender.json**: Company profiles and tax information
|
||||||
|
- **client.json**: Client contact details
|
||||||
|
- **products.json**: Service/product catalog with pricing
|
||||||
|
- **invoices.json**: Auto-generated invoice ledger
|
||||||
|
|
||||||
|
### Output Files (`invoice/`)
|
||||||
|
|
||||||
|
- Organized by sender name
|
||||||
|
- Preview files marked with `-DEV-PREVIEW`
|
||||||
|
- Production files with sequential numbering
|
||||||
|
- Automatic directory creation
|
||||||
|
|
||||||
|
## Development Workflow
|
||||||
|
|
||||||
|
### Adding New Data
|
||||||
|
|
||||||
|
1. Edit appropriate JSON file in `data/`
|
||||||
|
2. Use interactive mode to test: `npm start`
|
||||||
|
3. Verify preview generation
|
||||||
|
4. Create production invoice if satisfied
|
||||||
|
|
||||||
|
### Testing Changes
|
||||||
|
|
||||||
|
1. Run unit tests: `npm test`
|
||||||
|
2. Test invoice generation: `npm run test-invoice`
|
||||||
|
3. Verify file output structure
|
||||||
|
4. Check ledger updates
|
||||||
|
|
||||||
|
### Code Modifications
|
||||||
|
|
||||||
|
- Main logic in `index.js`
|
||||||
|
- Test cases in `test/invoice.test.js`
|
||||||
|
- Data validation in main function
|
||||||
|
- Error handling throughout
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
- No external API calls
|
||||||
|
- Local file system only
|
||||||
|
- No sensitive data in code
|
||||||
|
- JSON validation for data integrity
|
||||||
|
- Error boundaries prevent crashes
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
- Minimal dependencies
|
||||||
|
- Synchronous file operations
|
||||||
|
- No database overhead
|
||||||
|
- Fast PDF generation
|
||||||
|
- Efficient JSON parsing
|
||||||
|
|
||||||
|
## Changelog (May 2026)
|
||||||
|
|
||||||
|
- **`--print`**: Regenerate PDFs from `data/invoices.json` without modifying the ledger.
|
||||||
|
- **Optional ledger `dueDate`**: Overrides the default “invoice date + 14 days” when printing; stored on new rows when production creates `information.dueDate`.
|
||||||
|
- **Data**: Client `limmud_fsu_canada` (Limmud FSU Canada) with `invoiceFileName` for PDF naming; product `cruise_event_av_it` ($500 + 13% HST); sample invoice `2026-LFSU01` (Levkin Inc. → Limmud FSU Canada).
|
||||||
|
- **Sender**: Optional `contactName`, `phone`, and `email` on `levkin` in `sender.json` (and schema support for other senders).
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
### Potential Features
|
||||||
|
|
||||||
|
- Multiple currency support
|
||||||
|
- Invoice templates
|
||||||
|
- Email integration
|
||||||
|
- Export to accounting software
|
||||||
|
- Invoice status tracking
|
||||||
|
- Payment reminders
|
||||||
|
|
||||||
|
### Technical Improvements
|
||||||
|
|
||||||
|
- TypeScript migration
|
||||||
|
- Database integration
|
||||||
|
- Web interface
|
||||||
|
- API endpoints
|
||||||
|
- Docker containerization
|
||||||
Loading…
x
Reference in New Issue
Block a user