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
170
README.md
170
README.md
@ -1,120 +1,150 @@
|
||||
# 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.
|
||||
- **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.
|
||||
1. **Install dependencies:**
|
||||
|
||||
## 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
|
||||
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)
|
||||
|
||||
Run the interactive wizard. The tool will guide you through selecting a sender, client, and one or more products.
|
||||
### Interactive Mode (Recommended)
|
||||
|
||||
```bash
|
||||
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
|
||||
|
||||
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:**
|
||||
### Non-Interactive Mode
|
||||
|
||||
```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.
|
||||
|
||||
### Running Tests
|
||||
|
||||
To run the automated tests for the project:
|
||||
### Test Mode
|
||||
|
||||
```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
|
||||
{
|
||||
"company1": { "company": "Your Company", "address": ... },
|
||||
"company2": { "company": "Another Profile", "address": ... }
|
||||
"levkin": {
|
||||
"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`
|
||||
|
||||
Stores your client profiles. The `invoiceFileName` key is optional; if provided, it will be used for the generated PDF's filename.
|
||||
### `data/client.json` - Client Information
|
||||
|
||||
```json
|
||||
{
|
||||
"company3": { "company": "First Client", "address": ... },
|
||||
"company4": { "company": "Second Client", "invoiceFileName": "second_client_invoices", "address": ... }
|
||||
"levitin": {
|
||||
"company": "LEVITIN EMPLOYMENT LAWYERS",
|
||||
"address": "#2809 - 330 Richmond Street West",
|
||||
"zip": "M5V 0M4",
|
||||
"city": "Toronto, ON",
|
||||
"country": "Canada"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `products.json`
|
||||
|
||||
A catalog of the services or products you offer.
|
||||
### `data/products.json` - Services/Products Catalog
|
||||
|
||||
```json
|
||||
{
|
||||
"linkedin_scraper": {
|
||||
"description": "LinkedIn Scraper Bot",
|
||||
"price": 250,
|
||||
"taxRate": 0
|
||||
"taxRate": 13
|
||||
},
|
||||
"web_design": { "description": "Website Design", "price": 1200, "taxRate": 0 }
|
||||
}
|
||||
```
|
||||
|
||||
### `invoices.json`
|
||||
|
||||
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.
|
||||
|
||||
```json
|
||||
{
|
||||
"invoices": [
|
||||
{
|
||||
"number": "2024-0001",
|
||||
"date": "2024-07-31",
|
||||
...
|
||||
"web_design": {
|
||||
"description": "Website Design",
|
||||
"price": 1200,
|
||||
"taxRate": 13
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
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
|
||||
|
||||
```
|
||||
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",
|
||||
"city": "Toronto, ON",
|
||||
"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,
|
||||
"taxTotal": 32.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",
|
||||
"price": 1200,
|
||||
"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": {
|
||||
"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": "<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": {
|
||||
"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;
|
||||
}, 0);
|
||||
|
||||
invoicesData.invoices.push({
|
||||
const ledgerRow = {
|
||||
number: invoiceData.information.number,
|
||||
date: invoiceData.information.date,
|
||||
sender: invoiceData.sender.company,
|
||||
@ -92,7 +92,11 @@ function saveInvoiceRecord(invoiceData, invoiceProducts) {
|
||||
subtotal: parseFloat(subtotal.toFixed(2)),
|
||||
taxTotal: parseFloat(taxTotal.toFixed(2)),
|
||||
total: parseFloat((subtotal + taxTotal).toFixed(2)),
|
||||
});
|
||||
};
|
||||
if (invoiceData.information.dueDate) {
|
||||
ledgerRow.dueDate = invoiceData.information.dueDate;
|
||||
}
|
||||
invoicesData.invoices.push(ledgerRow);
|
||||
try {
|
||||
fs.writeFileSync(FILES.INVOICES, JSON.stringify(invoicesData, null, 2));
|
||||
console.log("✅ Invoice record saved to invoices.json.");
|
||||
@ -108,6 +112,11 @@ async function main() {
|
||||
type: "boolean",
|
||||
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", {
|
||||
type: "string",
|
||||
describe: "Sender key from sender.json",
|
||||
@ -132,6 +141,97 @@ async function main() {
|
||||
const clients = loadJson(FILES.CLIENTS);
|
||||
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
|
||||
if (argv.test) {
|
||||
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