pacioli 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +340 -0
- package/config/freelancer.example.json +16 -0
- package/customers/acme-corp.json +7 -0
- package/customers/demo-company.json +7 -0
- package/customers/test-customer.json +7 -0
- package/examples/invoice-auto.json +34 -0
- package/examples/invoice.json +34 -0
- package/examples/quotation-auto.json +34 -0
- package/examples/quotation.json +34 -0
- package/examples/receipt-auto.json +37 -0
- package/examples/receipt.json +37 -0
- package/package.json +58 -0
- package/src/cli.ts +111 -0
- package/src/commands/generate.ts +286 -0
- package/src/commands/init.ts +372 -0
- package/src/generator.ts +275 -0
- package/src/index.ts +250 -0
- package/src/metadata.ts +174 -0
- package/src/utils.ts +110 -0
- package/src/validator.ts +322 -0
- package/templates/invoice.html +389 -0
- package/templates/quotation.html +389 -0
- package/templates/receipt.html +397 -0
package/src/cli.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Pacioli CLI
|
|
5
|
+
* Main entry point for command routing
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { join, dirname } from "path";
|
|
9
|
+
import { fileURLToPath } from "url";
|
|
10
|
+
import { initCommand } from "./commands/init";
|
|
11
|
+
import { generateCommand } from "./commands/generate";
|
|
12
|
+
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
+
const __dirname = dirname(__filename);
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Get version from package.json
|
|
18
|
+
*/
|
|
19
|
+
function getVersion(): string {
|
|
20
|
+
try {
|
|
21
|
+
const packageRoot = join(__dirname, "..");
|
|
22
|
+
const packageJsonPath = join(packageRoot, "package.json");
|
|
23
|
+
const packageJson = require(packageJsonPath);
|
|
24
|
+
return packageJson.version || "1.0.0";
|
|
25
|
+
} catch {
|
|
26
|
+
return "1.0.0";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Print main help message
|
|
32
|
+
*/
|
|
33
|
+
function printHelp() {
|
|
34
|
+
console.log(`
|
|
35
|
+
Pacioli - Financial Document Generator for Freelancers
|
|
36
|
+
Named after Luca Pacioli, father of accounting (1494)
|
|
37
|
+
|
|
38
|
+
Usage:
|
|
39
|
+
pacioli <command> [options]
|
|
40
|
+
|
|
41
|
+
Commands:
|
|
42
|
+
init Initialize a new project with templates and examples
|
|
43
|
+
generate Generate a financial document (invoice, quotation, receipt)
|
|
44
|
+
help Show this help message
|
|
45
|
+
version Show version number
|
|
46
|
+
|
|
47
|
+
Examples:
|
|
48
|
+
# Initialize new project
|
|
49
|
+
mkdir my-invoices && cd my-invoices
|
|
50
|
+
pacioli init
|
|
51
|
+
|
|
52
|
+
# Generate documents
|
|
53
|
+
pacioli generate invoice examples/invoice.json --customer customers/acme-corp.json
|
|
54
|
+
pacioli generate quotation examples/quotation.json --customer customers/demo.json
|
|
55
|
+
pacioli generate receipt examples/receipt.json --customer customers/test.json
|
|
56
|
+
|
|
57
|
+
For command-specific help:
|
|
58
|
+
pacioli generate --help
|
|
59
|
+
|
|
60
|
+
Learn more: https://github.com/peerasak-u/pacioli
|
|
61
|
+
`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Main CLI router
|
|
66
|
+
*/
|
|
67
|
+
async function main() {
|
|
68
|
+
const args = process.argv.slice(2);
|
|
69
|
+
|
|
70
|
+
// No arguments - show help
|
|
71
|
+
if (args.length === 0) {
|
|
72
|
+
printHelp();
|
|
73
|
+
process.exit(0);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const command = args[0];
|
|
77
|
+
|
|
78
|
+
// Route commands
|
|
79
|
+
switch (command) {
|
|
80
|
+
case "init":
|
|
81
|
+
await initCommand(args.slice(1));
|
|
82
|
+
break;
|
|
83
|
+
|
|
84
|
+
case "generate":
|
|
85
|
+
await generateCommand(args.slice(1));
|
|
86
|
+
break;
|
|
87
|
+
|
|
88
|
+
case "help":
|
|
89
|
+
case "--help":
|
|
90
|
+
case "-h":
|
|
91
|
+
printHelp();
|
|
92
|
+
break;
|
|
93
|
+
|
|
94
|
+
case "version":
|
|
95
|
+
case "--version":
|
|
96
|
+
case "-v":
|
|
97
|
+
console.log(`v${getVersion()}`);
|
|
98
|
+
break;
|
|
99
|
+
|
|
100
|
+
default:
|
|
101
|
+
console.error(`Unknown command: ${command}`);
|
|
102
|
+
console.error('Run "pacioli help" for usage information');
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Run CLI
|
|
108
|
+
main().catch((error) => {
|
|
109
|
+
console.error("Fatal error:", error instanceof Error ? error.message : error);
|
|
110
|
+
process.exit(1);
|
|
111
|
+
});
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pacioli Generate Command
|
|
3
|
+
* Generates financial documents (invoice, quotation, receipt) as PDFs
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { join, dirname } from "path";
|
|
7
|
+
import { fileURLToPath } from "url";
|
|
8
|
+
import { fileExists, readJSON, getOutputPath } from "../utils";
|
|
9
|
+
import {
|
|
10
|
+
validateInvoice,
|
|
11
|
+
validateQuotation,
|
|
12
|
+
validateReceipt,
|
|
13
|
+
validateFreelancerConfig,
|
|
14
|
+
validateCustomer,
|
|
15
|
+
type DocumentData,
|
|
16
|
+
type FreelancerConfig,
|
|
17
|
+
type Customer,
|
|
18
|
+
} from "../validator";
|
|
19
|
+
import { generatePDF } from "../generator";
|
|
20
|
+
import {
|
|
21
|
+
getNextDocumentNumber,
|
|
22
|
+
incrementDocumentCounter,
|
|
23
|
+
} from "../metadata";
|
|
24
|
+
|
|
25
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
26
|
+
const __dirname = dirname(__filename);
|
|
27
|
+
|
|
28
|
+
const VALID_TYPES = ["invoice", "quotation", "receipt"] as const;
|
|
29
|
+
type DocumentType = (typeof VALID_TYPES)[number];
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Get the package root directory
|
|
33
|
+
*/
|
|
34
|
+
function getPackageRoot(): string {
|
|
35
|
+
return join(__dirname, "../..");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Print generate command help
|
|
40
|
+
*/
|
|
41
|
+
function printGenerateHelp() {
|
|
42
|
+
console.log(`
|
|
43
|
+
Generate a financial document as PDF
|
|
44
|
+
|
|
45
|
+
Usage:
|
|
46
|
+
pacioli generate <type> <input-json> --customer <customer-json> [options]
|
|
47
|
+
|
|
48
|
+
Arguments:
|
|
49
|
+
<type> Document type: invoice, quotation, or receipt
|
|
50
|
+
<input-json> Path to document data JSON file (relative to current directory)
|
|
51
|
+
|
|
52
|
+
Required Options:
|
|
53
|
+
--customer <path> Path to customer JSON file (required)
|
|
54
|
+
|
|
55
|
+
Optional Settings:
|
|
56
|
+
--output <path> Custom output PDF path (default: output/{type}-{number}.pdf)
|
|
57
|
+
--profile <path> Path to freelancer profile (default: config/freelancer.json)
|
|
58
|
+
--help Show this help message
|
|
59
|
+
|
|
60
|
+
Examples:
|
|
61
|
+
# Generate invoice with auto-numbering
|
|
62
|
+
pacioli generate invoice examples/invoice-auto.json --customer customers/acme-corp.json
|
|
63
|
+
|
|
64
|
+
# Generate quotation with custom output
|
|
65
|
+
pacioli generate quotation examples/quotation.json --customer customers/demo.json --output custom/quote.pdf
|
|
66
|
+
|
|
67
|
+
# Generate receipt with specific profile
|
|
68
|
+
pacioli generate receipt examples/receipt.json --customer customers/test.json --profile config/freelancer-alt.json
|
|
69
|
+
|
|
70
|
+
Document Types:
|
|
71
|
+
invoice - Bill for completed work (includes due date, payment terms)
|
|
72
|
+
quotation - Price estimate before work begins (includes validity period)
|
|
73
|
+
receipt - Payment confirmation (includes payment date, method, reference)
|
|
74
|
+
|
|
75
|
+
Auto-numbering:
|
|
76
|
+
Set "documentNumber": "auto" in your JSON file to use sequential numbering.
|
|
77
|
+
Format: PREFIX-YYYYMM-NUMBER (e.g., INV-202410-001)
|
|
78
|
+
Counters automatically reset each month.
|
|
79
|
+
`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Parse generate command arguments
|
|
84
|
+
*/
|
|
85
|
+
function parseGenerateArgs(args: string[]) {
|
|
86
|
+
const options: {
|
|
87
|
+
type?: DocumentType;
|
|
88
|
+
inputPath?: string;
|
|
89
|
+
customerPath?: string;
|
|
90
|
+
outputPath?: string;
|
|
91
|
+
configPath: string;
|
|
92
|
+
help: boolean;
|
|
93
|
+
} = {
|
|
94
|
+
configPath: "config/freelancer.json",
|
|
95
|
+
help: false,
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// Check for help flag
|
|
99
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
100
|
+
options.help = true;
|
|
101
|
+
return options;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Get positional arguments
|
|
105
|
+
const positionalArgs = args.filter((arg) => !arg.startsWith("--"));
|
|
106
|
+
|
|
107
|
+
if (positionalArgs.length >= 2) {
|
|
108
|
+
const type = positionalArgs[0];
|
|
109
|
+
if (VALID_TYPES.includes(type as DocumentType)) {
|
|
110
|
+
options.type = type as DocumentType;
|
|
111
|
+
}
|
|
112
|
+
options.inputPath = positionalArgs[1];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Parse options
|
|
116
|
+
for (let i = 0; i < args.length; i++) {
|
|
117
|
+
const arg = args[i];
|
|
118
|
+
const nextArg = args[i + 1];
|
|
119
|
+
|
|
120
|
+
if (arg === "--customer" && nextArg) {
|
|
121
|
+
options.customerPath = nextArg;
|
|
122
|
+
i++;
|
|
123
|
+
} else if (arg === "--output" && nextArg) {
|
|
124
|
+
options.outputPath = nextArg;
|
|
125
|
+
i++;
|
|
126
|
+
} else if (arg === "--profile" && nextArg) {
|
|
127
|
+
options.configPath = nextArg;
|
|
128
|
+
i++;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return options;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Resolve paths relative to current working directory
|
|
137
|
+
*/
|
|
138
|
+
function resolvePaths(options: ReturnType<typeof parseGenerateArgs>) {
|
|
139
|
+
const cwd = process.cwd();
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
inputPath: options.inputPath ? join(cwd, options.inputPath) : undefined,
|
|
143
|
+
customerPath: options.customerPath ? join(cwd, options.customerPath) : undefined,
|
|
144
|
+
outputPath: options.outputPath ? join(cwd, options.outputPath) : undefined,
|
|
145
|
+
configPath: join(cwd, options.configPath),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Generate command handler
|
|
151
|
+
*/
|
|
152
|
+
export async function generateCommand(args: string[]) {
|
|
153
|
+
const options = parseGenerateArgs(args);
|
|
154
|
+
|
|
155
|
+
if (options.help) {
|
|
156
|
+
printGenerateHelp();
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Validate arguments
|
|
161
|
+
if (!options.type) {
|
|
162
|
+
console.error("❌ Error: Invalid or missing document type");
|
|
163
|
+
console.error(`Valid types: ${VALID_TYPES.join(", ")}`);
|
|
164
|
+
console.error('\nRun "pacioli generate --help" for usage information');
|
|
165
|
+
process.exit(1);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (!options.inputPath) {
|
|
169
|
+
console.error("❌ Error: Input JSON file path is required");
|
|
170
|
+
console.error('\nRun "pacioli generate --help" for usage information');
|
|
171
|
+
process.exit(1);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (!options.customerPath) {
|
|
175
|
+
console.error("❌ Error: Customer JSON file path is required (use --customer)");
|
|
176
|
+
console.error('\nRun "pacioli generate --help" for usage information');
|
|
177
|
+
process.exit(1);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Resolve all paths relative to current directory
|
|
181
|
+
const paths = resolvePaths(options);
|
|
182
|
+
|
|
183
|
+
// Check if input file exists
|
|
184
|
+
if (!(await fileExists(paths.inputPath!))) {
|
|
185
|
+
console.error(`❌ Error: Input file not found: ${options.inputPath}`);
|
|
186
|
+
console.error(` Looking in: ${paths.inputPath}`);
|
|
187
|
+
process.exit(1);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Check if customer file exists
|
|
191
|
+
if (!(await fileExists(paths.customerPath!))) {
|
|
192
|
+
console.error(`❌ Error: Customer file not found: ${options.customerPath}`);
|
|
193
|
+
console.error(` Looking in: ${paths.customerPath}`);
|
|
194
|
+
process.exit(1);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Check if profile file exists
|
|
198
|
+
if (!(await fileExists(paths.configPath))) {
|
|
199
|
+
console.error(`❌ Error: Profile file not found: ${options.configPath}`);
|
|
200
|
+
console.error(` Looking in: ${paths.configPath}`);
|
|
201
|
+
console.error("\n💡 Tip: Did you forget to run 'pacioli init'?");
|
|
202
|
+
console.error(" Or copy config/freelancer.example.json to config/freelancer.json");
|
|
203
|
+
process.exit(1);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
console.log(`\n📄 Generating ${options.type}...`);
|
|
208
|
+
|
|
209
|
+
// Load freelancer profile
|
|
210
|
+
console.log(`📋 Loading profile from ${options.configPath}...`);
|
|
211
|
+
const config = await readJSON<FreelancerConfig>(paths.configPath);
|
|
212
|
+
|
|
213
|
+
// Validate profile
|
|
214
|
+
const configValidation = validateFreelancerConfig(config);
|
|
215
|
+
if (!configValidation.valid) {
|
|
216
|
+
console.error("❌ Error: Invalid freelancer profile:");
|
|
217
|
+
configValidation.errors.forEach((err) => console.error(` - ${err}`));
|
|
218
|
+
process.exit(1);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Load customer data
|
|
222
|
+
console.log(`👤 Loading customer from ${options.customerPath}...`);
|
|
223
|
+
const customer = await readJSON<Customer>(paths.customerPath!);
|
|
224
|
+
|
|
225
|
+
// Validate customer
|
|
226
|
+
const customerValidation = validateCustomer(customer);
|
|
227
|
+
if (!customerValidation.valid) {
|
|
228
|
+
console.error("❌ Error: Invalid customer data:");
|
|
229
|
+
customerValidation.errors.forEach((err) => console.error(` - ${err}`));
|
|
230
|
+
process.exit(1);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Load document data
|
|
234
|
+
console.log(`📋 Loading data from ${options.inputPath}...`);
|
|
235
|
+
const data = await readJSON<DocumentData>(paths.inputPath!);
|
|
236
|
+
|
|
237
|
+
// Validate document data based on type
|
|
238
|
+
let validation;
|
|
239
|
+
switch (options.type) {
|
|
240
|
+
case "invoice":
|
|
241
|
+
validation = validateInvoice(data);
|
|
242
|
+
break;
|
|
243
|
+
case "quotation":
|
|
244
|
+
validation = validateQuotation(data);
|
|
245
|
+
break;
|
|
246
|
+
case "receipt":
|
|
247
|
+
validation = validateReceipt(data);
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (!validation.valid) {
|
|
252
|
+
console.error(`❌ Error: Invalid ${options.type} data:`);
|
|
253
|
+
validation.errors.forEach((err) => console.error(` - ${err}`));
|
|
254
|
+
process.exit(1);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Handle auto-numbering
|
|
258
|
+
let resolvedDocumentNumber = data.documentNumber;
|
|
259
|
+
if (data.documentNumber === "auto") {
|
|
260
|
+
console.log(`🔢 Generating next document number...`);
|
|
261
|
+
resolvedDocumentNumber = await getNextDocumentNumber(options.type);
|
|
262
|
+
data.documentNumber = resolvedDocumentNumber;
|
|
263
|
+
console.log(` ✓ Document number: ${resolvedDocumentNumber}`);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Determine output path
|
|
267
|
+
const outputPath = getOutputPath(
|
|
268
|
+
options.type,
|
|
269
|
+
resolvedDocumentNumber,
|
|
270
|
+
paths.outputPath
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
// Generate PDF
|
|
274
|
+
console.log(`🔨 Generating PDF...`);
|
|
275
|
+
await generatePDF(options.type, data, customer, config, outputPath);
|
|
276
|
+
|
|
277
|
+
// Update metadata counter after successful generation
|
|
278
|
+
await incrementDocumentCounter(options.type, resolvedDocumentNumber);
|
|
279
|
+
|
|
280
|
+
console.log(`\n✅ Success! PDF saved to: ${outputPath}`);
|
|
281
|
+
} catch (error) {
|
|
282
|
+
console.error("\n❌ Generation failed:");
|
|
283
|
+
console.error(error instanceof Error ? error.message : error);
|
|
284
|
+
process.exit(1);
|
|
285
|
+
}
|
|
286
|
+
}
|