feat(cli): implement Bigcapital CLI with full module support
- Add CLI package with commander.js for interacting with Bigcapital API - Implement listing commands for all modules: items, invoices, customers, vendors, bills, accounts, expenses, credit-notes, vendor-credits, payments, estimates, receipts, journals, inventory, tax-rates, warehouses, and users - Add comprehensive financial reports: balance-sheet, profit-loss, cashflow, trial-balance, general-ledger, journal, receivable-aging, payable-aging, customer-balance, vendor-balance, sales-by-items, purchases-by-items, inventory-valuation, and sales-tax-liability - Support configuration management for API key, base URL, and org ID - Add professional table formatting with chalk and cli-table3 - Include loading spinners and error handling Usage: bigcapital config set api-key <key> bigcapital items list bigcapital reports balance-sheet --from 2024-01-01 --to 2024-12-31 Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,9 @@
|
||||
"build:webapp": "lerna run build --scope \"@bigcapital/webapp\" --scope \"@bigcapital/utils\" --scope \"@bigcapital/pdf-templates\"",
|
||||
"dev:server": "lerna run dev --scope \"@bigcapital/server\" --scope \"@bigcapital/utils\" --scope \"@bigcapital/pdf-templates\" --scope \"@bigcapital/email-components\"",
|
||||
"build:server": "lerna run build --scope \"@bigcapital/server\" --scope \"@bigcapital/utils\" --scope \"@bigcapital/pdf-templates\" --scope \"@bigcapital/email-components\"",
|
||||
"dev:cli": "lerna run dev --scope \"@bigcapital/cli\" --scope \"@bigcapital/sdk-ts\"",
|
||||
"build:cli": "lerna run build --scope \"@bigcapital/cli\" --scope \"@bigcapital/sdk-ts\"",
|
||||
"cli": "node packages/cli/dist/index.js",
|
||||
"serve:server": "lerna run serve --scope \"@bigcapital/server\" --scope \"@bigcapital/utils\"",
|
||||
"server:start": "lerna run start:dev --scope \"@bigcapital/server\"",
|
||||
"test:watch": "lerna run test:watch",
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
# Bigcapital CLI
|
||||
|
||||
A command-line interface for interacting with the Bigcapital API.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install -g @bigcapital/cli
|
||||
```
|
||||
|
||||
Or use directly with `npx`:
|
||||
|
||||
```bash
|
||||
npx @bigcapital/cli --help
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Before using the CLI, you need to configure your API credentials:
|
||||
|
||||
```bash
|
||||
# Set your API key
|
||||
bigcapital config set api-key your-api-key-here
|
||||
|
||||
# Set the base URL of your Bigcapital instance
|
||||
bigcapital config set base-url https://api.bigcapital.ly
|
||||
|
||||
# Optionally set a default organization ID
|
||||
bigcapital config set organization-id your-org-id
|
||||
|
||||
# Verify your configuration
|
||||
bigcapital config get
|
||||
```
|
||||
|
||||
Configuration is stored in `~/.config/bigcapital-nodejs/config.json`.
|
||||
|
||||
## Usage
|
||||
|
||||
### Items
|
||||
|
||||
List all items/products:
|
||||
|
||||
```bash
|
||||
# List all items
|
||||
bigcapital items list
|
||||
|
||||
# Limit results
|
||||
bigcapital items list --limit 10
|
||||
|
||||
# Paginate
|
||||
bigcapital items list --page 2 --limit 25
|
||||
|
||||
# Filter by type (inventory, service, product)
|
||||
bigcapital items list --type inventory
|
||||
|
||||
# Show only active items
|
||||
bigcapital items list --active-only
|
||||
```
|
||||
|
||||
### Invoices
|
||||
|
||||
List all sale invoices:
|
||||
|
||||
```bash
|
||||
# List all invoices
|
||||
bigcapital invoices list
|
||||
|
||||
# Filter by customer
|
||||
bigcapital invoices list --customer 123
|
||||
|
||||
# Filter by status
|
||||
bigcapital invoices list --status overdue
|
||||
|
||||
# Paginate results
|
||||
bigcapital invoices list --page 1 --limit 20
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `config set <key> <value>` | Set configuration value (api-key, base-url, organization-id) |
|
||||
| `config get` | Display current configuration |
|
||||
| `items list` | List all items/products |
|
||||
| `invoices list` | List all sale invoices |
|
||||
|
||||
## Global Options
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `-V, --version` | Output the version number |
|
||||
| `-h, --help` | Display help for command |
|
||||
|
||||
## Environment Variables
|
||||
|
||||
The CLI will also read from environment variables if set:
|
||||
|
||||
- `BIGCAPITAL_API_KEY` - Your API key
|
||||
- `BIGCAPITAL_BASE_URL` - The base URL of your Bigcapital instance
|
||||
- `BIGCAPITAL_ORGANIZATION_ID` - Default organization ID
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
# Build the CLI
|
||||
pnpm run build
|
||||
|
||||
# Watch mode for development
|
||||
pnpm run dev
|
||||
|
||||
# Type check
|
||||
pnpm run typecheck
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
ISC
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
import { Command } from 'commander';
|
||||
export declare function createAccountsCommand(): Command;
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.createAccountsCommand = createAccountsCommand;
|
||||
const commander_1 = require("commander");
|
||||
const chalk_1 = __importDefault(require("chalk"));
|
||||
const ora_1 = __importDefault(require("ora"));
|
||||
const sdk_ts_1 = require("@bigcapital/sdk-ts");
|
||||
const config_1 = require("../config");
|
||||
const errors_1 = require("../utils/errors");
|
||||
const table_1 = require("../utils/table");
|
||||
function createAccountsCommand() {
|
||||
const command = new commander_1.Command('accounts')
|
||||
.description('Manage chart of accounts');
|
||||
command
|
||||
.command('list')
|
||||
.description('List all accounts')
|
||||
.option('-t, --type <type>', 'Filter by account type')
|
||||
.option('--active-only', 'Show only active accounts', false)
|
||||
.action(async (options) => {
|
||||
const spinner = (0, ora_1.default)('Loading accounts...').start();
|
||||
try {
|
||||
const fetcher = (0, config_1.createAuthenticatedFetcher)();
|
||||
const query = {
|
||||
...(options.type && { type: options.type }),
|
||||
...(options.activeOnly && { active: true }),
|
||||
};
|
||||
const accounts = await (0, sdk_ts_1.fetchAccounts)(fetcher, query);
|
||||
spinner.stop();
|
||||
if (!accounts || accounts.length === 0) {
|
||||
console.log(chalk_1.default.yellow('No accounts found.'));
|
||||
return;
|
||||
}
|
||||
const table = (0, table_1.createTable)(['ID', 'Code', 'Name', 'Type', 'Balance', 'Status']);
|
||||
accounts.forEach((account) => {
|
||||
table.push([
|
||||
account.id,
|
||||
account.code || '-',
|
||||
(0, table_1.truncate)(account.name, 30),
|
||||
account.accountType || '-',
|
||||
(0, table_1.formatCurrency)(account.amount),
|
||||
(0, table_1.formatStatus)(account.active ? 'active' : 'inactive'),
|
||||
]);
|
||||
});
|
||||
console.log(table.toString());
|
||||
console.log(chalk_1.default.gray(`\nTotal accounts: ${accounts.length}`));
|
||||
}
|
||||
catch (error) {
|
||||
spinner.stop();
|
||||
(0, errors_1.handleError)(error);
|
||||
}
|
||||
});
|
||||
return command;
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
import { Command } from 'commander';
|
||||
export declare function createBillsCommand(): Command;
|
||||
Vendored
+75
@@ -0,0 +1,75 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.createBillsCommand = createBillsCommand;
|
||||
const commander_1 = require("commander");
|
||||
const chalk_1 = __importDefault(require("chalk"));
|
||||
const ora_1 = __importDefault(require("ora"));
|
||||
const sdk_ts_1 = require("@bigcapital/sdk-ts");
|
||||
const config_1 = require("../config");
|
||||
const errors_1 = require("../utils/errors");
|
||||
const table_1 = require("../utils/table");
|
||||
function createBillsCommand() {
|
||||
const command = new commander_1.Command('bills')
|
||||
.description('Manage bills');
|
||||
command
|
||||
.command('list')
|
||||
.description('List all bills')
|
||||
.option('-l, --limit <number>', 'Limit number of results per page', '50')
|
||||
.option('-p, --page <number>', 'Page number', '1')
|
||||
.option('-v, --vendor <id>', 'Filter by vendor ID')
|
||||
.option('-s, --status <status>', 'Filter by status (draft, published, paid, partial)')
|
||||
.action(async (options) => {
|
||||
const spinner = (0, ora_1.default)('Loading bills...').start();
|
||||
try {
|
||||
const fetcher = (0, config_1.createAuthenticatedFetcher)();
|
||||
const query = {
|
||||
page: parseInt(options.page, 10),
|
||||
pageSize: Math.min(parseInt(options.limit, 10), 100),
|
||||
...(options.vendor && { vendorId: parseInt(options.vendor, 10) }),
|
||||
...(options.status && { status: options.status }),
|
||||
};
|
||||
const response = await (0, sdk_ts_1.fetchBills)(fetcher, query);
|
||||
spinner.stop();
|
||||
const bills = response.bills;
|
||||
if (!bills || bills.length === 0) {
|
||||
console.log(chalk_1.default.yellow('No bills found.'));
|
||||
return;
|
||||
}
|
||||
const pagination = response.pagination;
|
||||
if (pagination) {
|
||||
const pageSize = pagination.pageSize || pagination.page_size || 10;
|
||||
const totalPages = Math.ceil(pagination.total / pageSize);
|
||||
console.log(chalk_1.default.gray(`\nPage ${pagination.page} of ${totalPages} (${pagination.total} total bills)\n`));
|
||||
}
|
||||
const table = (0, table_1.createTable)(['ID', 'Bill #', 'Vendor', 'Date', 'Due Date', 'Total', 'Balance', 'Status']);
|
||||
bills.forEach((bill) => {
|
||||
table.push([
|
||||
bill.id,
|
||||
bill.billNumber || '-',
|
||||
(0, table_1.truncate)(bill.vendor?.displayName, 20),
|
||||
(0, table_1.formatDate)(bill.billDate),
|
||||
(0, table_1.formatDate)(bill.dueDate),
|
||||
(0, table_1.formatCurrency)(bill.total),
|
||||
(0, table_1.formatCurrency)(bill.balance),
|
||||
(0, table_1.formatStatus)(bill.status),
|
||||
]);
|
||||
});
|
||||
console.log(table.toString());
|
||||
if (pagination) {
|
||||
const pageSize = pagination.pageSize || pagination.page_size || 10;
|
||||
const totalPages = Math.ceil(pagination.total / pageSize);
|
||||
if (pagination.page < totalPages) {
|
||||
console.log(chalk_1.default.gray(`\nUse --page ${pagination.page + 1} to see more results`));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
spinner.stop();
|
||||
(0, errors_1.handleError)(error);
|
||||
}
|
||||
});
|
||||
return command;
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
import { Command } from 'commander';
|
||||
export declare function createConfigCommand(): Command;
|
||||
Vendored
+68
@@ -0,0 +1,68 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.createConfigCommand = createConfigCommand;
|
||||
const commander_1 = require("commander");
|
||||
const chalk_1 = __importDefault(require("chalk"));
|
||||
const config_1 = require("../config");
|
||||
function createConfigCommand() {
|
||||
const command = new commander_1.Command('config')
|
||||
.description('Manage CLI configuration');
|
||||
command
|
||||
.command('set')
|
||||
.description('Set a configuration value')
|
||||
.argument('<key>', 'Configuration key (api-key, base-url, organization-id)')
|
||||
.argument('<value>', 'Configuration value')
|
||||
.action((key, value) => {
|
||||
switch (key.toLowerCase()) {
|
||||
case 'api-key':
|
||||
(0, config_1.setApiKey)(value);
|
||||
console.log(chalk_1.default.green('✓ API key configured successfully'));
|
||||
break;
|
||||
case 'base-url':
|
||||
(0, config_1.setBaseUrl)(value);
|
||||
console.log(chalk_1.default.green('✓ Base URL configured successfully'));
|
||||
break;
|
||||
case 'organization-id':
|
||||
(0, config_1.setOrganizationId)(value);
|
||||
console.log(chalk_1.default.green('✓ Organization ID configured successfully'));
|
||||
break;
|
||||
default:
|
||||
console.error(chalk_1.default.red(`Error: Unknown configuration key "${key}"`));
|
||||
console.log(chalk_1.default.yellow('Valid keys: api-key, base-url, organization-id'));
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
command
|
||||
.command('get')
|
||||
.description('Show current configuration')
|
||||
.action(() => {
|
||||
const config = (0, config_1.getConfig)();
|
||||
console.log(chalk_1.default.bold('\nBigcapital CLI Configuration:'));
|
||||
console.log(chalk_1.default.gray('─'.repeat(50)));
|
||||
if (config.apiKey) {
|
||||
const maskedKey = config.apiKey.substring(0, 4) + '...' + config.apiKey.substring(config.apiKey.length - 4);
|
||||
console.log(`API Key: ${chalk_1.default.green(maskedKey)}`);
|
||||
}
|
||||
else {
|
||||
console.log(`API Key: ${chalk_1.default.yellow('Not set')}`);
|
||||
}
|
||||
if (config.baseUrl) {
|
||||
console.log(`Base URL: ${chalk_1.default.green(config.baseUrl)}`);
|
||||
}
|
||||
else {
|
||||
console.log(`Base URL: ${chalk_1.default.yellow('Not set')}`);
|
||||
}
|
||||
if (config.organizationId) {
|
||||
console.log(`Organization: ${chalk_1.default.green(config.organizationId)}`);
|
||||
}
|
||||
else {
|
||||
console.log(`Organization: ${chalk_1.default.yellow('Not set')} (optional)`);
|
||||
}
|
||||
console.log(chalk_1.default.gray('─'.repeat(50)));
|
||||
console.log(chalk_1.default.gray('\nConfig file location: ~/.config/bigcapital-nodejs/config.json\n'));
|
||||
});
|
||||
return command;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
import { Command } from 'commander';
|
||||
export declare function createCreditNotesCommand(): Command;
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.createCreditNotesCommand = createCreditNotesCommand;
|
||||
const commander_1 = require("commander");
|
||||
const chalk_1 = __importDefault(require("chalk"));
|
||||
const ora_1 = __importDefault(require("ora"));
|
||||
const sdk_ts_1 = require("@bigcapital/sdk-ts");
|
||||
const config_1 = require("../config");
|
||||
const errors_1 = require("../utils/errors");
|
||||
const table_1 = require("../utils/table");
|
||||
function createCreditNotesCommand() {
|
||||
const command = new commander_1.Command('credit-notes')
|
||||
.description('Manage credit notes');
|
||||
command
|
||||
.command('list')
|
||||
.description('List all credit notes')
|
||||
.option('-l, --limit <number>', 'Limit number of results per page', '50')
|
||||
.option('-p, --page <number>', 'Page number', '1')
|
||||
.option('-c, --customer <id>', 'Filter by customer ID')
|
||||
.action(async (options) => {
|
||||
const spinner = (0, ora_1.default)('Loading credit notes...').start();
|
||||
try {
|
||||
const fetcher = (0, config_1.createAuthenticatedFetcher)();
|
||||
const query = {
|
||||
page: parseInt(options.page, 10),
|
||||
pageSize: Math.min(parseInt(options.limit, 10), 100),
|
||||
...(options.customer && { customerId: parseInt(options.customer, 10) }),
|
||||
};
|
||||
const response = await (0, sdk_ts_1.fetchCreditNotes)(fetcher, query);
|
||||
spinner.stop();
|
||||
const creditNotes = response.creditNotes;
|
||||
if (!creditNotes || creditNotes.length === 0) {
|
||||
console.log(chalk_1.default.yellow('No credit notes found.'));
|
||||
return;
|
||||
}
|
||||
const pagination = response.pagination;
|
||||
if (pagination) {
|
||||
const pageSize = pagination.pageSize || pagination.page_size || 10;
|
||||
const totalPages = Math.ceil(pagination.total / pageSize);
|
||||
console.log(chalk_1.default.gray(`\nPage ${pagination.page} of ${totalPages} (${pagination.total} total credit notes)\n`));
|
||||
}
|
||||
const table = (0, table_1.createTable)(['ID', 'CN #', 'Customer', 'Date', 'Total', 'Balance', 'Status']);
|
||||
creditNotes.forEach((cn) => {
|
||||
table.push([
|
||||
cn.id,
|
||||
cn.creditNoteNumber || '-',
|
||||
(0, table_1.truncate)(cn.customer?.displayName, 20),
|
||||
(0, table_1.formatDate)(cn.creditNoteDate),
|
||||
(0, table_1.formatCurrency)(cn.total),
|
||||
(0, table_1.formatCurrency)(cn.balance),
|
||||
(0, table_1.formatStatus)(cn.status),
|
||||
]);
|
||||
});
|
||||
console.log(table.toString());
|
||||
if (pagination) {
|
||||
const pageSize = pagination.pageSize || pagination.page_size || 10;
|
||||
const totalPages = Math.ceil(pagination.total / pageSize);
|
||||
if (pagination.page < totalPages) {
|
||||
console.log(chalk_1.default.gray(`\nUse --page ${pagination.page + 1} to see more results`));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
spinner.stop();
|
||||
(0, errors_1.handleError)(error);
|
||||
}
|
||||
});
|
||||
return command;
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
import { Command } from 'commander';
|
||||
export declare function createCustomersCommand(): Command;
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.createCustomersCommand = createCustomersCommand;
|
||||
const commander_1 = require("commander");
|
||||
const chalk_1 = __importDefault(require("chalk"));
|
||||
const ora_1 = __importDefault(require("ora"));
|
||||
const sdk_ts_1 = require("@bigcapital/sdk-ts");
|
||||
const config_1 = require("../config");
|
||||
const errors_1 = require("../utils/errors");
|
||||
const table_1 = require("../utils/table");
|
||||
function createCustomersCommand() {
|
||||
const command = new commander_1.Command('customers')
|
||||
.description('Manage customers');
|
||||
command
|
||||
.command('list')
|
||||
.description('List all customers')
|
||||
.option('-l, --limit <number>', 'Limit number of results per page', '50')
|
||||
.option('-p, --page <number>', 'Page number', '1')
|
||||
.option('--active-only', 'Show only active customers', false)
|
||||
.action(async (options) => {
|
||||
const spinner = (0, ora_1.default)('Loading customers...').start();
|
||||
try {
|
||||
const fetcher = (0, config_1.createAuthenticatedFetcher)();
|
||||
const query = {
|
||||
page: parseInt(options.page, 10),
|
||||
pageSize: Math.min(parseInt(options.limit, 10), 100),
|
||||
...(options.activeOnly && { active: true }),
|
||||
};
|
||||
const response = await (0, sdk_ts_1.fetchCustomers)(fetcher, query);
|
||||
spinner.stop();
|
||||
const customers = response.customers;
|
||||
if (!customers || customers.length === 0) {
|
||||
console.log(chalk_1.default.yellow('No customers found.'));
|
||||
return;
|
||||
}
|
||||
const pagination = response.pagination;
|
||||
if (pagination) {
|
||||
const pageSize = pagination.pageSize || pagination.page_size || 10;
|
||||
const totalPages = Math.ceil(pagination.total / pageSize);
|
||||
console.log(chalk_1.default.gray(`\nPage ${pagination.page} of ${totalPages} (${pagination.total} total customers)\n`));
|
||||
}
|
||||
const table = (0, table_1.createTable)(['ID', 'Name', 'Email', 'Work Phone', 'Personal Phone', 'Balance', 'Status']);
|
||||
customers.forEach((customer) => {
|
||||
table.push([
|
||||
customer.id,
|
||||
(0, table_1.truncate)(customer.displayName, 25),
|
||||
(0, table_1.truncate)(customer.email, 25) || '-',
|
||||
customer.workPhone || '-',
|
||||
customer.personalPhone || '-',
|
||||
(0, table_1.formatCurrency)(customer.balance),
|
||||
(0, table_1.formatStatus)(customer.active ? 'active' : 'inactive'),
|
||||
]);
|
||||
});
|
||||
console.log(table.toString());
|
||||
if (pagination) {
|
||||
const pageSize = pagination.pageSize || pagination.page_size || 10;
|
||||
const totalPages = Math.ceil(pagination.total / pageSize);
|
||||
if (pagination.page < totalPages) {
|
||||
console.log(chalk_1.default.gray(`\nUse --page ${pagination.page + 1} to see more results`));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
spinner.stop();
|
||||
(0, errors_1.handleError)(error);
|
||||
}
|
||||
});
|
||||
return command;
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
import { Command } from 'commander';
|
||||
export declare function createEstimatesCommand(): Command;
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.createEstimatesCommand = createEstimatesCommand;
|
||||
const commander_1 = require("commander");
|
||||
const chalk_1 = __importDefault(require("chalk"));
|
||||
const ora_1 = __importDefault(require("ora"));
|
||||
const sdk_ts_1 = require("@bigcapital/sdk-ts");
|
||||
const config_1 = require("../config");
|
||||
const errors_1 = require("../utils/errors");
|
||||
const table_1 = require("../utils/table");
|
||||
function createEstimatesCommand() {
|
||||
const command = new commander_1.Command('estimates')
|
||||
.description('Manage sale estimates/quotes');
|
||||
command
|
||||
.command('list')
|
||||
.description('List all sale estimates')
|
||||
.option('-l, --limit <number>', 'Limit number of results per page', '50')
|
||||
.option('-p, --page <number>', 'Page number', '1')
|
||||
.option('-c, --customer <id>', 'Filter by customer ID')
|
||||
.option('-s, --status <status>', 'Filter by status (draft, published)')
|
||||
.action(async (options) => {
|
||||
const spinner = (0, ora_1.default)('Loading estimates...').start();
|
||||
try {
|
||||
const fetcher = (0, config_1.createAuthenticatedFetcher)();
|
||||
const query = {
|
||||
page: parseInt(options.page, 10),
|
||||
pageSize: Math.min(parseInt(options.limit, 10), 100),
|
||||
...(options.customer && { customerId: parseInt(options.customer, 10) }),
|
||||
...(options.status && { status: options.status }),
|
||||
};
|
||||
const response = await (0, sdk_ts_1.fetchSaleEstimates)(fetcher, query);
|
||||
spinner.stop();
|
||||
const estimates = response.saleEstimates;
|
||||
if (!estimates || estimates.length === 0) {
|
||||
console.log(chalk_1.default.yellow('No estimates found.'));
|
||||
return;
|
||||
}
|
||||
const pagination = response.pagination;
|
||||
if (pagination) {
|
||||
const pageSize = pagination.pageSize || pagination.page_size || 10;
|
||||
const totalPages = Math.ceil(pagination.total / pageSize);
|
||||
console.log(chalk_1.default.gray(`\nPage ${pagination.page} of ${totalPages} (${pagination.total} total estimates)\n`));
|
||||
}
|
||||
const table = (0, table_1.createTable)(['ID', 'Estimate #', 'Customer', 'Date', 'Expires', 'Total', 'Status']);
|
||||
estimates.forEach((estimate) => {
|
||||
table.push([
|
||||
estimate.id,
|
||||
estimate.estimateNumber || '-',
|
||||
(0, table_1.truncate)(estimate.customer?.displayName, 20),
|
||||
(0, table_1.formatDate)(estimate.estimateDate),
|
||||
(0, table_1.formatDate)(estimate.expirationDate),
|
||||
(0, table_1.formatCurrency)(estimate.total),
|
||||
(0, table_1.formatStatus)(estimate.status),
|
||||
]);
|
||||
});
|
||||
console.log(table.toString());
|
||||
if (pagination) {
|
||||
const pageSize = pagination.pageSize || pagination.page_size || 10;
|
||||
const totalPages = Math.ceil(pagination.total / pageSize);
|
||||
if (pagination.page < totalPages) {
|
||||
console.log(chalk_1.default.gray(`\nUse --page ${pagination.page + 1} to see more results`));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
spinner.stop();
|
||||
(0, errors_1.handleError)(error);
|
||||
}
|
||||
});
|
||||
return command;
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
import { Command } from 'commander';
|
||||
export declare function createExpensesCommand(): Command;
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.createExpensesCommand = createExpensesCommand;
|
||||
const commander_1 = require("commander");
|
||||
const chalk_1 = __importDefault(require("chalk"));
|
||||
const ora_1 = __importDefault(require("ora"));
|
||||
const sdk_ts_1 = require("@bigcapital/sdk-ts");
|
||||
const config_1 = require("../config");
|
||||
const errors_1 = require("../utils/errors");
|
||||
const table_1 = require("../utils/table");
|
||||
function createExpensesCommand() {
|
||||
const command = new commander_1.Command('expenses')
|
||||
.description('Manage expenses');
|
||||
command
|
||||
.command('list')
|
||||
.description('List all expenses')
|
||||
.option('-l, --limit <number>', 'Limit number of results per page', '50')
|
||||
.option('-p, --page <number>', 'Page number', '1')
|
||||
.option('--active-only', 'Show only active expenses', false)
|
||||
.action(async (options) => {
|
||||
const spinner = (0, ora_1.default)('Loading expenses...').start();
|
||||
try {
|
||||
const fetcher = (0, config_1.createAuthenticatedFetcher)();
|
||||
const query = {
|
||||
page: parseInt(options.page, 10),
|
||||
pageSize: Math.min(parseInt(options.limit, 10), 100),
|
||||
...(options.activeOnly && { active: true }),
|
||||
};
|
||||
const response = await (0, sdk_ts_1.fetchExpenses)(fetcher, query);
|
||||
spinner.stop();
|
||||
const expenses = response.expenses;
|
||||
if (!expenses || expenses.length === 0) {
|
||||
console.log(chalk_1.default.yellow('No expenses found.'));
|
||||
return;
|
||||
}
|
||||
const pagination = response.pagination;
|
||||
if (pagination) {
|
||||
const pageSize = pagination.pageSize || pagination.page_size || 10;
|
||||
const totalPages = Math.ceil(pagination.total / pageSize);
|
||||
console.log(chalk_1.default.gray(`\nPage ${pagination.page} of ${totalPages} (${pagination.total} total expenses)\n`));
|
||||
}
|
||||
const table = (0, table_1.createTable)(['ID', 'Date', 'Description', 'Total', 'Status']);
|
||||
expenses.forEach((expense) => {
|
||||
table.push([
|
||||
expense.id,
|
||||
(0, table_1.formatDate)(expense.paymentDate),
|
||||
(0, table_1.truncate)(expense.description, 35),
|
||||
(0, table_1.formatCurrency)(expense.total),
|
||||
(0, table_1.formatStatus)(expense.status),
|
||||
]);
|
||||
});
|
||||
console.log(table.toString());
|
||||
if (pagination) {
|
||||
const pageSize = pagination.pageSize || pagination.page_size || 10;
|
||||
const totalPages = Math.ceil(pagination.total / pageSize);
|
||||
if (pagination.page < totalPages) {
|
||||
console.log(chalk_1.default.gray(`\nUse --page ${pagination.page + 1} to see more results`));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
spinner.stop();
|
||||
(0, errors_1.handleError)(error);
|
||||
}
|
||||
});
|
||||
return command;
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
import { Command } from 'commander';
|
||||
export declare function createInventoryCommand(): Command;
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.createInventoryCommand = createInventoryCommand;
|
||||
const commander_1 = require("commander");
|
||||
const chalk_1 = __importDefault(require("chalk"));
|
||||
const ora_1 = __importDefault(require("ora"));
|
||||
const sdk_ts_1 = require("@bigcapital/sdk-ts");
|
||||
const config_1 = require("../config");
|
||||
const errors_1 = require("../utils/errors");
|
||||
const table_1 = require("../utils/table");
|
||||
function createInventoryCommand() {
|
||||
const command = new commander_1.Command('inventory')
|
||||
.description('Manage inventory');
|
||||
command
|
||||
.command('adjustments')
|
||||
.description('List inventory adjustments')
|
||||
.option('-l, --limit <number>', 'Limit number of results per page', '50')
|
||||
.option('-p, --page <number>', 'Page number', '1')
|
||||
.action(async (options) => {
|
||||
const spinner = (0, ora_1.default)('Loading inventory adjustments...').start();
|
||||
try {
|
||||
const fetcher = (0, config_1.createAuthenticatedFetcher)();
|
||||
const query = {
|
||||
page: parseInt(options.page, 10),
|
||||
pageSize: Math.min(parseInt(options.limit, 10), 100),
|
||||
};
|
||||
const response = await (0, sdk_ts_1.fetchInventoryAdjustments)(fetcher, query);
|
||||
spinner.stop();
|
||||
const adjustments = response.inventoryAdjustments;
|
||||
if (!adjustments || adjustments.length === 0) {
|
||||
console.log(chalk_1.default.yellow('No inventory adjustments found.'));
|
||||
return;
|
||||
}
|
||||
const pagination = response.pagination;
|
||||
if (pagination) {
|
||||
const pageSize = pagination.pageSize || pagination.page_size || 10;
|
||||
const totalPages = Math.ceil(pagination.total / pageSize);
|
||||
console.log(chalk_1.default.gray(`\nPage ${pagination.page} of ${totalPages} (${pagination.total} total adjustments)\n`));
|
||||
}
|
||||
const table = (0, table_1.createTable)(['ID', 'Date', 'Reference', 'Reason', 'Status']);
|
||||
adjustments.forEach((adj) => {
|
||||
table.push([
|
||||
adj.id,
|
||||
(0, table_1.formatDate)(adj.adjustmentDate),
|
||||
adj.referenceNo || '-',
|
||||
(0, table_1.truncate)(adj.reason, 30),
|
||||
(0, table_1.formatStatus)(adj.status),
|
||||
]);
|
||||
});
|
||||
console.log(table.toString());
|
||||
if (pagination) {
|
||||
const pageSize = pagination.pageSize || pagination.page_size || 10;
|
||||
const totalPages = Math.ceil(pagination.total / pageSize);
|
||||
if (pagination.page < totalPages) {
|
||||
console.log(chalk_1.default.gray(`\nUse --page ${pagination.page + 1} to see more results`));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
spinner.stop();
|
||||
(0, errors_1.handleError)(error);
|
||||
}
|
||||
});
|
||||
command
|
||||
.command('transfers')
|
||||
.description('List warehouse transfers')
|
||||
.option('-l, --limit <number>', 'Limit number of results per page', '50')
|
||||
.option('-p, --page <number>', 'Page number', '1')
|
||||
.action(async (options) => {
|
||||
const spinner = (0, ora_1.default)('Loading warehouse transfers...').start();
|
||||
try {
|
||||
const fetcher = (0, config_1.createAuthenticatedFetcher)();
|
||||
const query = {
|
||||
page: parseInt(options.page, 10),
|
||||
pageSize: Math.min(parseInt(options.limit, 10), 100),
|
||||
};
|
||||
const response = await (0, sdk_ts_1.fetchWarehouseTransfers)(fetcher, query);
|
||||
spinner.stop();
|
||||
const transfers = response.warehouseTransfers;
|
||||
if (!transfers || transfers.length === 0) {
|
||||
console.log(chalk_1.default.yellow('No warehouse transfers found.'));
|
||||
return;
|
||||
}
|
||||
const pagination = response.pagination;
|
||||
if (pagination) {
|
||||
const pageSize = pagination.pageSize || pagination.page_size || 10;
|
||||
const totalPages = Math.ceil(pagination.total / pageSize);
|
||||
console.log(chalk_1.default.gray(`\nPage ${pagination.page} of ${totalPages} (${pagination.total} total transfers)\n`));
|
||||
}
|
||||
const table = (0, table_1.createTable)(['ID', 'Date', 'Reference', 'From', 'To', 'Status']);
|
||||
transfers.forEach((transfer) => {
|
||||
table.push([
|
||||
transfer.id,
|
||||
(0, table_1.formatDate)(transfer.date),
|
||||
transfer.referenceNo || '-',
|
||||
(0, table_1.truncate)(transfer.fromWarehouse?.name, 15),
|
||||
(0, table_1.truncate)(transfer.toWarehouse?.name, 15),
|
||||
(0, table_1.formatStatus)(transfer.status),
|
||||
]);
|
||||
});
|
||||
console.log(table.toString());
|
||||
if (pagination) {
|
||||
const pageSize = pagination.pageSize || pagination.page_size || 10;
|
||||
const totalPages = Math.ceil(pagination.total / pageSize);
|
||||
if (pagination.page < totalPages) {
|
||||
console.log(chalk_1.default.gray(`\nUse --page ${pagination.page + 1} to see more results`));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
spinner.stop();
|
||||
(0, errors_1.handleError)(error);
|
||||
}
|
||||
});
|
||||
return command;
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
import { Command } from 'commander';
|
||||
export declare function createInvoicesCommand(): Command;
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.createInvoicesCommand = createInvoicesCommand;
|
||||
const commander_1 = require("commander");
|
||||
const chalk_1 = __importDefault(require("chalk"));
|
||||
const ora_1 = __importDefault(require("ora"));
|
||||
const sdk_ts_1 = require("@bigcapital/sdk-ts");
|
||||
const config_1 = require("../config");
|
||||
const errors_1 = require("../utils/errors");
|
||||
const table_1 = require("../utils/table");
|
||||
function createInvoicesCommand() {
|
||||
const command = new commander_1.Command('invoices')
|
||||
.description('Manage sale invoices');
|
||||
command
|
||||
.command('list')
|
||||
.description('List all sale invoices')
|
||||
.option('-l, --limit <number>', 'Limit number of results per page', '50')
|
||||
.option('-p, --page <number>', 'Page number', '1')
|
||||
.option('-c, --customer <id>', 'Filter by customer ID')
|
||||
.option('-s, --status <status>', 'Filter by status (draft, published, paid, partial, overdue)')
|
||||
.action(async (options) => {
|
||||
const spinner = (0, ora_1.default)('Loading invoices...').start();
|
||||
try {
|
||||
const fetcher = (0, config_1.createAuthenticatedFetcher)();
|
||||
const query = {
|
||||
page: parseInt(options.page, 10),
|
||||
pageSize: Math.min(parseInt(options.limit, 10), 100),
|
||||
...(options.customer && { customerId: parseInt(options.customer, 10) }),
|
||||
...(options.status && { status: options.status }),
|
||||
};
|
||||
const response = await (0, sdk_ts_1.fetchSaleInvoices)(fetcher, query);
|
||||
spinner.stop();
|
||||
// The response has 'salesInvoices' array (camelCase from middleware), not 'data'
|
||||
const invoices = response.salesInvoices;
|
||||
if (!invoices || invoices.length === 0) {
|
||||
console.log(chalk_1.default.yellow('No invoices found.'));
|
||||
return;
|
||||
}
|
||||
// Pagination info
|
||||
const pagination = response.pagination;
|
||||
if (pagination) {
|
||||
const pageSize = pagination.pageSize || pagination.page_size || 10;
|
||||
const totalPages = Math.ceil(pagination.total / pageSize);
|
||||
console.log(chalk_1.default.gray(`\nPage ${pagination.page} of ${totalPages} (${pagination.total} total invoices)\n`));
|
||||
}
|
||||
// Create table
|
||||
const table = (0, table_1.createTable)(['ID', 'Invoice #', 'Customer', 'Date', 'Total', 'Balance', 'Status']);
|
||||
invoices.forEach((invoice) => {
|
||||
table.push([
|
||||
invoice.id,
|
||||
invoice.invoiceNo || '-',
|
||||
(0, table_1.truncate)(invoice.customer?.displayName, 25),
|
||||
(0, table_1.formatDate)(invoice.invoiceDate),
|
||||
(0, table_1.formatCurrency)(invoice.total),
|
||||
(0, table_1.formatCurrency)(invoice.balance),
|
||||
(0, table_1.formatStatus)(invoice.status),
|
||||
]);
|
||||
});
|
||||
console.log(table.toString());
|
||||
if (pagination) {
|
||||
const pageSize = pagination.pageSize || pagination.page_size || 10;
|
||||
const totalPages = Math.ceil(pagination.total / pageSize);
|
||||
if (pagination.page < totalPages) {
|
||||
console.log(chalk_1.default.gray(`\nUse --page ${pagination.page + 1} to see more results`));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
spinner.stop();
|
||||
(0, errors_1.handleError)(error);
|
||||
}
|
||||
});
|
||||
return command;
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
import { Command } from 'commander';
|
||||
export declare function createItemsCommand(): Command;
|
||||
Vendored
+77
@@ -0,0 +1,77 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.createItemsCommand = createItemsCommand;
|
||||
const commander_1 = require("commander");
|
||||
const chalk_1 = __importDefault(require("chalk"));
|
||||
const ora_1 = __importDefault(require("ora"));
|
||||
const sdk_ts_1 = require("@bigcapital/sdk-ts");
|
||||
const config_1 = require("../config");
|
||||
const errors_1 = require("../utils/errors");
|
||||
const table_1 = require("../utils/table");
|
||||
function createItemsCommand() {
|
||||
const command = new commander_1.Command('items')
|
||||
.description('Manage items and products');
|
||||
command
|
||||
.command('list')
|
||||
.description('List all items/products')
|
||||
.option('-l, --limit <number>', 'Limit number of results per page', '50')
|
||||
.option('-p, --page <number>', 'Page number', '1')
|
||||
.option('-t, --type <type>', 'Filter by item type (inventory, service, product)')
|
||||
.option('--active-only', 'Show only active items', false)
|
||||
.action(async (options) => {
|
||||
const spinner = (0, ora_1.default)('Loading items...').start();
|
||||
try {
|
||||
const fetcher = (0, config_1.createAuthenticatedFetcher)();
|
||||
const query = {
|
||||
page: parseInt(options.page, 10),
|
||||
pageSize: Math.min(parseInt(options.limit, 10), 100),
|
||||
...(options.type && { type: options.type }),
|
||||
...(options.activeOnly && { active: true }),
|
||||
};
|
||||
const response = await (0, sdk_ts_1.fetchItems)(fetcher, query);
|
||||
spinner.stop();
|
||||
// The response has 'items' array, not 'data'
|
||||
const items = response.items;
|
||||
if (!items || items.length === 0) {
|
||||
console.log(chalk_1.default.yellow('No items found.'));
|
||||
return;
|
||||
}
|
||||
// Pagination info
|
||||
const pagination = response.pagination;
|
||||
if (pagination) {
|
||||
const pageSize = pagination.pageSize || pagination.page_size || 10;
|
||||
const totalPages = Math.ceil(pagination.total / pageSize);
|
||||
console.log(chalk_1.default.gray(`\nPage ${pagination.page} of ${totalPages} (${pagination.total} total items)\n`));
|
||||
}
|
||||
// Create table
|
||||
const table = (0, table_1.createTable)(['ID', 'Name', 'Type', 'Sell Price', 'Cost Price', 'Qty', 'Status']);
|
||||
items.forEach((item) => {
|
||||
table.push([
|
||||
item.id,
|
||||
(0, table_1.truncate)(item.name, 30),
|
||||
item.type || '-',
|
||||
(0, table_1.formatCurrency)(item.sellPrice),
|
||||
(0, table_1.formatCurrency)(item.costPrice),
|
||||
item.quantityOnHand ?? '-',
|
||||
(0, table_1.formatStatus)(item.active ? 'active' : 'inactive'),
|
||||
]);
|
||||
});
|
||||
console.log(table.toString());
|
||||
if (pagination) {
|
||||
const pageSize = pagination.pageSize || pagination.page_size || 10;
|
||||
const totalPages = Math.ceil(pagination.total / pageSize);
|
||||
if (pagination.page < totalPages) {
|
||||
console.log(chalk_1.default.gray(`\nUse --page ${pagination.page + 1} to see more results`));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
spinner.stop();
|
||||
(0, errors_1.handleError)(error);
|
||||
}
|
||||
});
|
||||
return command;
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
import { Command } from 'commander';
|
||||
export declare function createJournalsCommand(): Command;
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.createJournalsCommand = createJournalsCommand;
|
||||
const commander_1 = require("commander");
|
||||
const chalk_1 = __importDefault(require("chalk"));
|
||||
const ora_1 = __importDefault(require("ora"));
|
||||
const sdk_ts_1 = require("@bigcapital/sdk-ts");
|
||||
const config_1 = require("../config");
|
||||
const errors_1 = require("../utils/errors");
|
||||
const table_1 = require("../utils/table");
|
||||
function createJournalsCommand() {
|
||||
const command = new commander_1.Command('journals')
|
||||
.description('Manage manual journals');
|
||||
command
|
||||
.command('list')
|
||||
.description('List all manual journals')
|
||||
.option('-l, --limit <number>', 'Limit number of results per page', '50')
|
||||
.option('-p, --page <number>', 'Page number', '1')
|
||||
.action(async (options) => {
|
||||
const spinner = (0, ora_1.default)('Loading journals...').start();
|
||||
try {
|
||||
const fetcher = (0, config_1.createAuthenticatedFetcher)();
|
||||
const response = await (0, sdk_ts_1.fetchManualJournals)(fetcher, {});
|
||||
spinner.stop();
|
||||
const journals = response.manualJournals;
|
||||
if (!journals || journals.length === 0) {
|
||||
console.log(chalk_1.default.yellow('No manual journals found.'));
|
||||
return;
|
||||
}
|
||||
const pagination = response.pagination;
|
||||
if (pagination) {
|
||||
const pageSize = pagination.pageSize || pagination.page_size || 10;
|
||||
const totalPages = Math.ceil(pagination.total / pageSize);
|
||||
console.log(chalk_1.default.gray(`\nPage ${pagination.page} of ${totalPages} (${pagination.total} total journals)\n`));
|
||||
}
|
||||
const table = (0, table_1.createTable)(['ID', 'Journal #', 'Date', 'Reference', 'Description', 'Amount', 'Status']);
|
||||
journals.forEach((journal) => {
|
||||
table.push([
|
||||
journal.id,
|
||||
journal.journalNumber || '-',
|
||||
(0, table_1.formatDate)(journal.date),
|
||||
journal.reference || '-',
|
||||
(0, table_1.truncate)(journal.description, 25),
|
||||
journal.amount?.toFixed(2) || '-',
|
||||
(0, table_1.formatStatus)(journal.status),
|
||||
]);
|
||||
});
|
||||
console.log(table.toString());
|
||||
if (pagination) {
|
||||
const pageSize = pagination.pageSize || pagination.page_size || 10;
|
||||
const totalPages = Math.ceil(pagination.total / pageSize);
|
||||
if (pagination.page < totalPages) {
|
||||
console.log(chalk_1.default.gray(`\nUse --page ${pagination.page + 1} to see more results`));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
spinner.stop();
|
||||
(0, errors_1.handleError)(error);
|
||||
}
|
||||
});
|
||||
return command;
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
import { Command } from 'commander';
|
||||
export declare function createPaymentsCommand(): Command;
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.createPaymentsCommand = createPaymentsCommand;
|
||||
const commander_1 = require("commander");
|
||||
const chalk_1 = __importDefault(require("chalk"));
|
||||
const ora_1 = __importDefault(require("ora"));
|
||||
const sdk_ts_1 = require("@bigcapital/sdk-ts");
|
||||
const config_1 = require("../config");
|
||||
const errors_1 = require("../utils/errors");
|
||||
const table_1 = require("../utils/table");
|
||||
function createPaymentsCommand() {
|
||||
const command = new commander_1.Command('payments')
|
||||
.description('Manage payments');
|
||||
command
|
||||
.command('received')
|
||||
.description('List payments received from customers')
|
||||
.action(async () => {
|
||||
const spinner = (0, ora_1.default)('Loading payments received...').start();
|
||||
try {
|
||||
const fetcher = (0, config_1.createAuthenticatedFetcher)();
|
||||
const response = await (0, sdk_ts_1.fetchPaymentsReceived)(fetcher);
|
||||
spinner.stop();
|
||||
const payments = response.paymentsReceived;
|
||||
if (!payments || payments.length === 0) {
|
||||
console.log(chalk_1.default.yellow('No payments received found.'));
|
||||
return;
|
||||
}
|
||||
const table = (0, table_1.createTable)(['ID', 'Customer', 'Date', 'Amount', 'Reference']);
|
||||
payments.forEach((payment) => {
|
||||
table.push([
|
||||
payment.id,
|
||||
(0, table_1.truncate)(payment.customer?.displayName, 25),
|
||||
(0, table_1.formatDate)(payment.paymentDate),
|
||||
(0, table_1.formatCurrency)(payment.amount),
|
||||
payment.referenceNo || '-',
|
||||
]);
|
||||
});
|
||||
console.log(table.toString());
|
||||
}
|
||||
catch (error) {
|
||||
spinner.stop();
|
||||
(0, errors_1.handleError)(error);
|
||||
}
|
||||
});
|
||||
command
|
||||
.command('made')
|
||||
.description('List bill payments made to vendors')
|
||||
.action(async () => {
|
||||
const spinner = (0, ora_1.default)('Loading payments made...').start();
|
||||
try {
|
||||
const fetcher = (0, config_1.createAuthenticatedFetcher)();
|
||||
const response = await (0, sdk_ts_1.fetchBillPayments)(fetcher);
|
||||
spinner.stop();
|
||||
const payments = response.billPayments;
|
||||
if (!payments || payments.length === 0) {
|
||||
console.log(chalk_1.default.yellow('No payments made found.'));
|
||||
return;
|
||||
}
|
||||
const table = (0, table_1.createTable)(['ID', 'Vendor', 'Date', 'Amount', 'Reference']);
|
||||
payments.forEach((payment) => {
|
||||
table.push([
|
||||
payment.id,
|
||||
(0, table_1.truncate)(payment.vendor?.displayName, 25),
|
||||
(0, table_1.formatDate)(payment.paymentDate),
|
||||
(0, table_1.formatCurrency)(payment.amount),
|
||||
payment.referenceNo || '-',
|
||||
]);
|
||||
});
|
||||
console.log(table.toString());
|
||||
}
|
||||
catch (error) {
|
||||
spinner.stop();
|
||||
(0, errors_1.handleError)(error);
|
||||
}
|
||||
});
|
||||
return command;
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
import { Command } from 'commander';
|
||||
export declare function createReceiptsCommand(): Command;
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.createReceiptsCommand = createReceiptsCommand;
|
||||
const commander_1 = require("commander");
|
||||
const chalk_1 = __importDefault(require("chalk"));
|
||||
const ora_1 = __importDefault(require("ora"));
|
||||
const sdk_ts_1 = require("@bigcapital/sdk-ts");
|
||||
const config_1 = require("../config");
|
||||
const errors_1 = require("../utils/errors");
|
||||
const table_1 = require("../utils/table");
|
||||
function createReceiptsCommand() {
|
||||
const command = new commander_1.Command('receipts')
|
||||
.description('Manage sale receipts');
|
||||
command
|
||||
.command('list')
|
||||
.description('List all sale receipts')
|
||||
.option('-l, --limit <number>', 'Limit number of results per page', '50')
|
||||
.option('-p, --page <number>', 'Page number', '1')
|
||||
.option('-c, --customer <id>', 'Filter by customer ID')
|
||||
.action(async (options) => {
|
||||
const spinner = (0, ora_1.default)('Loading receipts...').start();
|
||||
try {
|
||||
const fetcher = (0, config_1.createAuthenticatedFetcher)();
|
||||
const query = {
|
||||
page: parseInt(options.page, 10),
|
||||
pageSize: Math.min(parseInt(options.limit, 10), 100),
|
||||
...(options.customer && { customerId: parseInt(options.customer, 10) }),
|
||||
};
|
||||
const response = await (0, sdk_ts_1.fetchSaleReceipts)(fetcher, query);
|
||||
spinner.stop();
|
||||
const receipts = response.saleReceipts;
|
||||
if (!receipts || receipts.length === 0) {
|
||||
console.log(chalk_1.default.yellow('No receipts found.'));
|
||||
return;
|
||||
}
|
||||
const pagination = response.pagination;
|
||||
if (pagination) {
|
||||
const pageSize = pagination.pageSize || pagination.page_size || 10;
|
||||
const totalPages = Math.ceil(pagination.total / pageSize);
|
||||
console.log(chalk_1.default.gray(`\nPage ${pagination.page} of ${totalPages} (${pagination.total} total receipts)\n`));
|
||||
}
|
||||
const table = (0, table_1.createTable)(['ID', 'Receipt #', 'Customer', 'Date', 'Total', 'Status']);
|
||||
receipts.forEach((receipt) => {
|
||||
table.push([
|
||||
receipt.id,
|
||||
receipt.receiptNumber || '-',
|
||||
(0, table_1.truncate)(receipt.customer?.displayName, 20),
|
||||
(0, table_1.formatDate)(receipt.receiptDate),
|
||||
(0, table_1.formatCurrency)(receipt.total),
|
||||
(0, table_1.formatStatus)(receipt.status),
|
||||
]);
|
||||
});
|
||||
console.log(table.toString());
|
||||
if (pagination) {
|
||||
const pageSize = pagination.pageSize || pagination.page_size || 10;
|
||||
const totalPages = Math.ceil(pagination.total / pageSize);
|
||||
if (pagination.page < totalPages) {
|
||||
console.log(chalk_1.default.gray(`\nUse --page ${pagination.page + 1} to see more results`));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
spinner.stop();
|
||||
(0, errors_1.handleError)(error);
|
||||
}
|
||||
});
|
||||
return command;
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
import { Command } from 'commander';
|
||||
export declare function createReportsCommand(): Command;
|
||||
+448
@@ -0,0 +1,448 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.createReportsCommand = createReportsCommand;
|
||||
const commander_1 = require("commander");
|
||||
const chalk_1 = __importDefault(require("chalk"));
|
||||
const ora_1 = __importDefault(require("ora"));
|
||||
const sdk_ts_1 = require("@bigcapital/sdk-ts");
|
||||
const config_1 = require("../config");
|
||||
const errors_1 = require("../utils/errors");
|
||||
const table_1 = require("../utils/table");
|
||||
function getDateRange(options) {
|
||||
const toDate = options.to || new Date().toISOString().split('T')[0];
|
||||
const fromDate = options.from || new Date(Date.now() - 365 * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
|
||||
return { fromDate, toDate };
|
||||
}
|
||||
function createReportsCommand() {
|
||||
const command = new commander_1.Command('reports')
|
||||
.description('Financial reports');
|
||||
// Balance Sheet
|
||||
command
|
||||
.command('balance-sheet')
|
||||
.description('Show balance sheet')
|
||||
.option('--from <date>', 'Start date (YYYY-MM-DD)')
|
||||
.option('--to <date>', 'End date (YYYY-MM-DD)')
|
||||
.action(async (options) => {
|
||||
const spinner = (0, ora_1.default)('Loading balance sheet...').start();
|
||||
try {
|
||||
const fetcher = (0, config_1.createAuthenticatedFetcher)();
|
||||
const { fromDate, toDate } = getDateRange(options);
|
||||
const report = await (0, sdk_ts_1.fetchBalanceSheetJson)(fetcher, { fromDate, toDate });
|
||||
spinner.stop();
|
||||
console.log(chalk_1.default.bold('\n📊 Balance Sheet'));
|
||||
console.log(chalk_1.default.gray(`${fromDate} to ${toDate}\n`));
|
||||
// Assets
|
||||
console.log(chalk_1.default.green.bold('\nASSETS'));
|
||||
const assetsTable = (0, table_1.createTable)(['Account', 'Amount']);
|
||||
let totalAssets = 0;
|
||||
report.assets?.forEach((item) => {
|
||||
assetsTable.push([item.accountName, (0, table_1.formatCurrency)(item.total)]);
|
||||
totalAssets += item.total;
|
||||
});
|
||||
assetsTable.push(['', '']);
|
||||
assetsTable.push([chalk_1.default.bold('Total Assets'), chalk_1.default.bold((0, table_1.formatCurrency)(totalAssets))]);
|
||||
console.log(assetsTable.toString());
|
||||
// Liabilities
|
||||
console.log(chalk_1.default.red.bold('\nLIABILITIES'));
|
||||
const liabilitiesTable = (0, table_1.createTable)(['Account', 'Amount']);
|
||||
let totalLiabilities = 0;
|
||||
report.liabilities?.forEach((item) => {
|
||||
liabilitiesTable.push([item.accountName, (0, table_1.formatCurrency)(item.total)]);
|
||||
totalLiabilities += item.total;
|
||||
});
|
||||
liabilitiesTable.push(['', '']);
|
||||
liabilitiesTable.push([chalk_1.default.bold('Total Liabilities'), chalk_1.default.bold((0, table_1.formatCurrency)(totalLiabilities))]);
|
||||
console.log(liabilitiesTable.toString());
|
||||
// Equity
|
||||
console.log(chalk_1.default.blue.bold('\nEQUITY'));
|
||||
const equityTable = (0, table_1.createTable)(['Account', 'Amount']);
|
||||
let totalEquity = 0;
|
||||
report.equity?.forEach((item) => {
|
||||
equityTable.push([item.accountName, (0, table_1.formatCurrency)(item.total)]);
|
||||
totalEquity += item.total;
|
||||
});
|
||||
equityTable.push(['', '']);
|
||||
equityTable.push([chalk_1.default.bold('Total Equity'), chalk_1.default.bold((0, table_1.formatCurrency)(totalEquity))]);
|
||||
console.log(equityTable.toString());
|
||||
// Summary
|
||||
console.log(chalk_1.default.bold('\n' + '─'.repeat(40)));
|
||||
console.log(`${chalk_1.default.bold('Total Liabilities + Equity:')} ${(0, table_1.formatCurrency)(totalLiabilities + totalEquity)}`);
|
||||
console.log(`${chalk_1.default.bold('Balance:')} ${totalAssets === (totalLiabilities + totalEquity) ? chalk_1.default.green('✓ Balanced') : chalk_1.default.red('✗ Unbalanced')}\n`);
|
||||
}
|
||||
catch (error) {
|
||||
spinner.stop();
|
||||
(0, errors_1.handleError)(error);
|
||||
}
|
||||
});
|
||||
// Profit & Loss
|
||||
command
|
||||
.command('profit-loss')
|
||||
.alias('pl')
|
||||
.description('Show profit and loss statement')
|
||||
.option('--from <date>', 'Start date (YYYY-MM-DD)')
|
||||
.option('--to <date>', 'End date (YYYY-MM-DD)')
|
||||
.action(async (options) => {
|
||||
const spinner = (0, ora_1.default)('Loading profit & loss...').start();
|
||||
try {
|
||||
const fetcher = (0, config_1.createAuthenticatedFetcher)();
|
||||
const { fromDate, toDate } = getDateRange(options);
|
||||
const report = await (0, sdk_ts_1.fetchProfitLossJson)(fetcher, { fromDate, toDate });
|
||||
spinner.stop();
|
||||
console.log(chalk_1.default.bold('\n📈 Profit & Loss Statement'));
|
||||
console.log(chalk_1.default.gray(`${fromDate} to ${toDate}\n`));
|
||||
const income = report.income?.total || 0;
|
||||
const expenses = report.expenses?.total || 0;
|
||||
const netProfit = income - expenses;
|
||||
const table = (0, table_1.createTable)(['', 'Amount']);
|
||||
table.push([chalk_1.default.green('Total Income'), (0, table_1.formatCurrency)(income)]);
|
||||
table.push([chalk_1.default.red('Total Expenses'), (0, table_1.formatCurrency)(expenses)]);
|
||||
table.push(['', '']);
|
||||
table.push([netProfit >= 0 ? chalk_1.default.green.bold('Net Profit') : chalk_1.default.red.bold('Net Loss'), (0, table_1.formatCurrency)(Math.abs(netProfit))]);
|
||||
console.log(table.toString() + '\n');
|
||||
}
|
||||
catch (error) {
|
||||
spinner.stop();
|
||||
(0, errors_1.handleError)(error);
|
||||
}
|
||||
});
|
||||
// Cashflow
|
||||
command
|
||||
.command('cashflow')
|
||||
.description('Show cashflow statement')
|
||||
.option('--from <date>', 'Start date (YYYY-MM-DD)')
|
||||
.option('--to <date>', 'End date (YYYY-MM-DD)')
|
||||
.action(async (options) => {
|
||||
const spinner = (0, ora_1.default)('Loading cashflow statement...').start();
|
||||
try {
|
||||
const fetcher = (0, config_1.createAuthenticatedFetcher)();
|
||||
const { fromDate, toDate } = getDateRange(options);
|
||||
const report = await (0, sdk_ts_1.fetchCashflowStatementJson)(fetcher, { fromDate, toDate });
|
||||
spinner.stop();
|
||||
console.log(chalk_1.default.bold('\n💰 Cashflow Statement'));
|
||||
console.log(chalk_1.default.gray(`${fromDate} to ${toDate}\n`));
|
||||
const operating = report.operating?.total || 0;
|
||||
const investing = report.investing?.total || 0;
|
||||
const financing = report.financing?.total || 0;
|
||||
const netCash = operating + investing + financing;
|
||||
const table = (0, table_1.createTable)(['Activity', 'Amount']);
|
||||
table.push(['Operating Activities', (0, table_1.formatCurrency)(operating)]);
|
||||
table.push(['Investing Activities', (0, table_1.formatCurrency)(investing)]);
|
||||
table.push(['Financing Activities', (0, table_1.formatCurrency)(financing)]);
|
||||
table.push(['', '']);
|
||||
table.push([chalk_1.default.bold('Net Cash Change'), chalk_1.default.bold((0, table_1.formatCurrency)(netCash))]);
|
||||
console.log(table.toString() + '\n');
|
||||
}
|
||||
catch (error) {
|
||||
spinner.stop();
|
||||
(0, errors_1.handleError)(error);
|
||||
}
|
||||
});
|
||||
// Trial Balance
|
||||
command
|
||||
.command('trial-balance')
|
||||
.description('Show trial balance')
|
||||
.option('--from <date>', 'Start date (YYYY-MM-DD)')
|
||||
.option('--to <date>', 'End date (YYYY-MM-DD)')
|
||||
.action(async (options) => {
|
||||
const spinner = (0, ora_1.default)('Loading trial balance...').start();
|
||||
try {
|
||||
const fetcher = (0, config_1.createAuthenticatedFetcher)();
|
||||
const { fromDate, toDate } = getDateRange(options);
|
||||
const report = await (0, sdk_ts_1.fetchTrialBalanceJson)(fetcher, {});
|
||||
spinner.stop();
|
||||
console.log(chalk_1.default.bold('\n⚖️ Trial Balance'));
|
||||
console.log(chalk_1.default.gray(`${fromDate} to ${toDate}\n`));
|
||||
const table = (0, table_1.createTable)(['Account', 'Debit', 'Credit']);
|
||||
let totalDebit = 0;
|
||||
let totalCredit = 0;
|
||||
report.data?.forEach((item) => {
|
||||
table.push([item.accountName, (0, table_1.formatCurrency)(item.debit), (0, table_1.formatCurrency)(item.credit)]);
|
||||
totalDebit += item.debit;
|
||||
totalCredit += item.credit;
|
||||
});
|
||||
table.push(['', '', '']);
|
||||
table.push([chalk_1.default.bold('Total'), chalk_1.default.bold((0, table_1.formatCurrency)(totalDebit)), chalk_1.default.bold((0, table_1.formatCurrency)(totalCredit))]);
|
||||
console.log(table.toString());
|
||||
console.log(`\n${chalk_1.default.bold('Balance:')} ${Math.abs(totalDebit - totalCredit) < 0.01 ? chalk_1.default.green('✓ Balanced') : chalk_1.default.red('✗ Unbalanced')}\n`);
|
||||
}
|
||||
catch (error) {
|
||||
spinner.stop();
|
||||
(0, errors_1.handleError)(error);
|
||||
}
|
||||
});
|
||||
// General Ledger
|
||||
command
|
||||
.command('general-ledger')
|
||||
.description('Show general ledger')
|
||||
.option('--from <date>', 'Start date (YYYY-MM-DD)')
|
||||
.option('--to <date>', 'End date (YYYY-MM-DD)')
|
||||
.action(async (options) => {
|
||||
const spinner = (0, ora_1.default)('Loading general ledger...').start();
|
||||
try {
|
||||
const fetcher = (0, config_1.createAuthenticatedFetcher)();
|
||||
const { fromDate, toDate } = getDateRange(options);
|
||||
const report = await (0, sdk_ts_1.fetchGeneralLedgerJson)(fetcher, {});
|
||||
spinner.stop();
|
||||
console.log(chalk_1.default.bold('\n📒 General Ledger'));
|
||||
console.log(chalk_1.default.gray(`${fromDate} to ${toDate}\n`));
|
||||
console.log(JSON.stringify(report, null, 2));
|
||||
}
|
||||
catch (error) {
|
||||
spinner.stop();
|
||||
(0, errors_1.handleError)(error);
|
||||
}
|
||||
});
|
||||
// Journal
|
||||
command
|
||||
.command('journal')
|
||||
.description('Show journal report')
|
||||
.option('--from <date>', 'Start date (YYYY-MM-DD)')
|
||||
.option('--to <date>', 'End date (YYYY-MM-DD)')
|
||||
.action(async (options) => {
|
||||
const spinner = (0, ora_1.default)('Loading journal...').start();
|
||||
try {
|
||||
const fetcher = (0, config_1.createAuthenticatedFetcher)();
|
||||
const { fromDate, toDate } = getDateRange(options);
|
||||
const report = await (0, sdk_ts_1.fetchJournalJson)(fetcher, {});
|
||||
spinner.stop();
|
||||
console.log(chalk_1.default.bold('\n📝 Journal Report'));
|
||||
console.log(chalk_1.default.gray(`${fromDate} to ${toDate}\n`));
|
||||
const table = (0, table_1.createTable)(['Date', 'Reference', 'Account', 'Debit', 'Credit']);
|
||||
report.data?.forEach((item) => {
|
||||
table.push([item.date, item.reference || '-', item.accountName, (0, table_1.formatCurrency)(item.debit), (0, table_1.formatCurrency)(item.credit)]);
|
||||
});
|
||||
console.log(table.toString() + '\n');
|
||||
}
|
||||
catch (error) {
|
||||
spinner.stop();
|
||||
(0, errors_1.handleError)(error);
|
||||
}
|
||||
});
|
||||
// Receivable Aging
|
||||
command
|
||||
.command('receivable-aging')
|
||||
.description('Show accounts receivable aging')
|
||||
.action(async () => {
|
||||
const spinner = (0, ora_1.default)('Loading receivable aging...').start();
|
||||
try {
|
||||
const fetcher = (0, config_1.createAuthenticatedFetcher)();
|
||||
const report = await (0, sdk_ts_1.fetchReceivableAgingJson)(fetcher, {});
|
||||
spinner.stop();
|
||||
console.log(chalk_1.default.bold('\n📋 Accounts Receivable Aging\n'));
|
||||
const table = (0, table_1.createTable)(['Customer', 'Current', '1-30', '31-60', '61-90', '90+', 'Total']);
|
||||
report.data?.forEach((item) => {
|
||||
table.push([
|
||||
item.customerName,
|
||||
(0, table_1.formatCurrency)(item.current),
|
||||
(0, table_1.formatCurrency)(item.days1to30),
|
||||
(0, table_1.formatCurrency)(item.days31to60),
|
||||
(0, table_1.formatCurrency)(item.days61to90),
|
||||
(0, table_1.formatCurrency)(item.over90),
|
||||
chalk_1.default.bold((0, table_1.formatCurrency)(item.total)),
|
||||
]);
|
||||
});
|
||||
console.log(table.toString() + '\n');
|
||||
}
|
||||
catch (error) {
|
||||
spinner.stop();
|
||||
(0, errors_1.handleError)(error);
|
||||
}
|
||||
});
|
||||
// Payable Aging
|
||||
command
|
||||
.command('payable-aging')
|
||||
.description('Show accounts payable aging')
|
||||
.action(async () => {
|
||||
const spinner = (0, ora_1.default)('Loading payable aging...').start();
|
||||
try {
|
||||
const fetcher = (0, config_1.createAuthenticatedFetcher)();
|
||||
const report = await (0, sdk_ts_1.fetchPayableAgingJson)(fetcher, {});
|
||||
spinner.stop();
|
||||
console.log(chalk_1.default.bold('\n📋 Accounts Payable Aging\n'));
|
||||
const table = (0, table_1.createTable)(['Vendor', 'Current', '1-30', '31-60', '61-90', '90+', 'Total']);
|
||||
report.data?.forEach((item) => {
|
||||
table.push([
|
||||
item.vendorName,
|
||||
(0, table_1.formatCurrency)(item.current),
|
||||
(0, table_1.formatCurrency)(item.days1to30),
|
||||
(0, table_1.formatCurrency)(item.days31to60),
|
||||
(0, table_1.formatCurrency)(item.days61to90),
|
||||
(0, table_1.formatCurrency)(item.over90),
|
||||
chalk_1.default.bold((0, table_1.formatCurrency)(item.total)),
|
||||
]);
|
||||
});
|
||||
console.log(table.toString() + '\n');
|
||||
}
|
||||
catch (error) {
|
||||
spinner.stop();
|
||||
(0, errors_1.handleError)(error);
|
||||
}
|
||||
});
|
||||
// Customer Balance
|
||||
command
|
||||
.command('customer-balance')
|
||||
.description('Show customer balance summary')
|
||||
.action(async () => {
|
||||
const spinner = (0, ora_1.default)('Loading customer balances...').start();
|
||||
try {
|
||||
const fetcher = (0, config_1.createAuthenticatedFetcher)();
|
||||
const report = await (0, sdk_ts_1.fetchCustomerBalanceJson)(fetcher, {});
|
||||
spinner.stop();
|
||||
console.log(chalk_1.default.bold('\n👤 Customer Balance Summary\n'));
|
||||
const table = (0, table_1.createTable)(['Customer', 'Total']);
|
||||
report.data?.forEach((item) => {
|
||||
table.push([item.customerName, (0, table_1.formatCurrency)(item.total)]);
|
||||
});
|
||||
console.log(table.toString() + '\n');
|
||||
}
|
||||
catch (error) {
|
||||
spinner.stop();
|
||||
(0, errors_1.handleError)(error);
|
||||
}
|
||||
});
|
||||
// Vendor Balance
|
||||
command
|
||||
.command('vendor-balance')
|
||||
.description('Show vendor balance summary')
|
||||
.action(async () => {
|
||||
const spinner = (0, ora_1.default)('Loading vendor balances...').start();
|
||||
try {
|
||||
const fetcher = (0, config_1.createAuthenticatedFetcher)();
|
||||
const report = await (0, sdk_ts_1.fetchVendorBalanceJson)(fetcher, {});
|
||||
spinner.stop();
|
||||
console.log(chalk_1.default.bold('\n🏢 Vendor Balance Summary\n'));
|
||||
const table = (0, table_1.createTable)(['Vendor', 'Total']);
|
||||
report.data?.forEach((item) => {
|
||||
table.push([item.vendorName, (0, table_1.formatCurrency)(item.total)]);
|
||||
});
|
||||
console.log(table.toString() + '\n');
|
||||
}
|
||||
catch (error) {
|
||||
spinner.stop();
|
||||
(0, errors_1.handleError)(error);
|
||||
}
|
||||
});
|
||||
// Sales by Items
|
||||
command
|
||||
.command('sales-by-items')
|
||||
.description('Show sales by items report')
|
||||
.option('--from <date>', 'Start date (YYYY-MM-DD)')
|
||||
.option('--to <date>', 'End date (YYYY-MM-DD)')
|
||||
.action(async (options) => {
|
||||
const spinner = (0, ora_1.default)('Loading sales by items...').start();
|
||||
try {
|
||||
const fetcher = (0, config_1.createAuthenticatedFetcher)();
|
||||
const { fromDate, toDate } = getDateRange(options);
|
||||
const report = await (0, sdk_ts_1.fetchSalesByItemsJson)(fetcher, { fromDate, toDate });
|
||||
spinner.stop();
|
||||
console.log(chalk_1.default.bold('\n🛍️ Sales by Items'));
|
||||
console.log(chalk_1.default.gray(`${fromDate} to ${toDate}\n`));
|
||||
const table = (0, table_1.createTable)(['Item', 'Quantity', 'Amount']);
|
||||
report.data?.forEach((item) => {
|
||||
table.push([item.itemName, item.quantity.toString(), (0, table_1.formatCurrency)(item.amount)]);
|
||||
});
|
||||
console.log(table.toString() + '\n');
|
||||
}
|
||||
catch (error) {
|
||||
spinner.stop();
|
||||
(0, errors_1.handleError)(error);
|
||||
}
|
||||
});
|
||||
// Purchases by Items
|
||||
command
|
||||
.command('purchases-by-items')
|
||||
.description('Show purchases by items report')
|
||||
.option('--from <date>', 'Start date (YYYY-MM-DD)')
|
||||
.option('--to <date>', 'End date (YYYY-MM-DD)')
|
||||
.action(async (options) => {
|
||||
const spinner = (0, ora_1.default)('Loading purchases by items...').start();
|
||||
try {
|
||||
const fetcher = (0, config_1.createAuthenticatedFetcher)();
|
||||
const { fromDate, toDate } = getDateRange(options);
|
||||
const report = await (0, sdk_ts_1.fetchPurchasesByItemsJson)(fetcher, { fromDate, toDate });
|
||||
spinner.stop();
|
||||
console.log(chalk_1.default.bold('\n🛒 Purchases by Items'));
|
||||
console.log(chalk_1.default.gray(`${fromDate} to ${toDate}\n`));
|
||||
const table = (0, table_1.createTable)(['Item', 'Quantity', 'Amount']);
|
||||
report.data?.forEach((item) => {
|
||||
table.push([item.itemName, item.quantity.toString(), (0, table_1.formatCurrency)(item.amount)]);
|
||||
});
|
||||
console.log(table.toString() + '\n');
|
||||
}
|
||||
catch (error) {
|
||||
spinner.stop();
|
||||
(0, errors_1.handleError)(error);
|
||||
}
|
||||
});
|
||||
// Inventory Valuation
|
||||
command
|
||||
.command('inventory-valuation')
|
||||
.description('Show inventory valuation summary')
|
||||
.action(async () => {
|
||||
const spinner = (0, ora_1.default)('Loading inventory valuation...').start();
|
||||
try {
|
||||
const fetcher = (0, config_1.createAuthenticatedFetcher)();
|
||||
const report = await (0, sdk_ts_1.fetchInventoryValuationJson)(fetcher, {});
|
||||
spinner.stop();
|
||||
console.log(chalk_1.default.bold('\n📦 Inventory Valuation\n'));
|
||||
const table = (0, table_1.createTable)(['Item', 'Qty on Hand', 'Avg Cost', 'Total Value']);
|
||||
report.data?.forEach((item) => {
|
||||
table.push([item.itemName, item.quantityOnHand.toString(), (0, table_1.formatCurrency)(item.averageCost), (0, table_1.formatCurrency)(item.totalValue)]);
|
||||
});
|
||||
console.log(table.toString() + '\n');
|
||||
}
|
||||
catch (error) {
|
||||
spinner.stop();
|
||||
(0, errors_1.handleError)(error);
|
||||
}
|
||||
});
|
||||
// Inventory Details
|
||||
command
|
||||
.command('inventory-details')
|
||||
.description('Show inventory item details')
|
||||
.option('--item <id>', 'Filter by item ID')
|
||||
.action(async (options) => {
|
||||
const spinner = (0, ora_1.default)('Loading inventory details...').start();
|
||||
try {
|
||||
const fetcher = (0, config_1.createAuthenticatedFetcher)();
|
||||
const report = await (0, sdk_ts_1.fetchInventoryItemDetailsJson)(fetcher, {});
|
||||
spinner.stop();
|
||||
console.log(chalk_1.default.bold('\n📦 Inventory Item Details\n'));
|
||||
console.log(JSON.stringify(report, null, 2));
|
||||
}
|
||||
catch (error) {
|
||||
spinner.stop();
|
||||
(0, errors_1.handleError)(error);
|
||||
}
|
||||
});
|
||||
// Sales Tax Liability
|
||||
command
|
||||
.command('sales-tax-liability')
|
||||
.description('Show sales tax liability report')
|
||||
.option('--from <date>', 'Start date (YYYY-MM-DD)')
|
||||
.option('--to <date>', 'End date (YYYY-MM-DD)')
|
||||
.action(async (options) => {
|
||||
const spinner = (0, ora_1.default)('Loading sales tax liability...').start();
|
||||
try {
|
||||
const fetcher = (0, config_1.createAuthenticatedFetcher)();
|
||||
const { fromDate, toDate } = getDateRange(options);
|
||||
const report = await (0, sdk_ts_1.fetchSalesTaxLiabilityJson)(fetcher, { fromDate, toDate });
|
||||
spinner.stop();
|
||||
console.log(chalk_1.default.bold('\n💵 Sales Tax Liability'));
|
||||
console.log(chalk_1.default.gray(`${fromDate} to ${toDate}\n`));
|
||||
const table = (0, table_1.createTable)(['Tax Rate', 'Taxable Amount', 'Tax Amount']);
|
||||
report.data?.forEach((item) => {
|
||||
table.push([item.taxRateName, (0, table_1.formatCurrency)(item.taxableAmount), (0, table_1.formatCurrency)(item.taxAmount)]);
|
||||
});
|
||||
console.log(table.toString() + '\n');
|
||||
}
|
||||
catch (error) {
|
||||
spinner.stop();
|
||||
(0, errors_1.handleError)(error);
|
||||
}
|
||||
});
|
||||
return command;
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
import { Command } from 'commander';
|
||||
export declare function createTaxRatesCommand(): Command;
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.createTaxRatesCommand = createTaxRatesCommand;
|
||||
const commander_1 = require("commander");
|
||||
const chalk_1 = __importDefault(require("chalk"));
|
||||
const ora_1 = __importDefault(require("ora"));
|
||||
const sdk_ts_1 = require("@bigcapital/sdk-ts");
|
||||
const config_1 = require("../config");
|
||||
const errors_1 = require("../utils/errors");
|
||||
const table_1 = require("../utils/table");
|
||||
function createTaxRatesCommand() {
|
||||
const command = new commander_1.Command('tax-rates')
|
||||
.description('Manage tax rates');
|
||||
command
|
||||
.command('list')
|
||||
.description('List all tax rates')
|
||||
.action(async () => {
|
||||
const spinner = (0, ora_1.default)('Loading tax rates...').start();
|
||||
try {
|
||||
const fetcher = (0, config_1.createAuthenticatedFetcher)();
|
||||
const response = await (0, sdk_ts_1.fetchTaxRates)(fetcher);
|
||||
spinner.stop();
|
||||
const taxRates = response.taxRates;
|
||||
if (!taxRates || taxRates.length === 0) {
|
||||
console.log(chalk_1.default.yellow('No tax rates found.'));
|
||||
return;
|
||||
}
|
||||
const table = (0, table_1.createTable)(['ID', 'Name', 'Code', 'Rate', 'Status']);
|
||||
taxRates.forEach((tax) => {
|
||||
table.push([
|
||||
tax.id,
|
||||
tax.name || '-',
|
||||
tax.code || '-',
|
||||
`${tax.rate || 0}%`,
|
||||
(0, table_1.formatStatus)(tax.active ? 'active' : 'inactive'),
|
||||
]);
|
||||
});
|
||||
console.log(table.toString());
|
||||
}
|
||||
catch (error) {
|
||||
spinner.stop();
|
||||
(0, errors_1.handleError)(error);
|
||||
}
|
||||
});
|
||||
return command;
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
import { Command } from 'commander';
|
||||
export declare function createUsersCommand(): Command;
|
||||
Vendored
+80
@@ -0,0 +1,80 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.createUsersCommand = createUsersCommand;
|
||||
const commander_1 = require("commander");
|
||||
const chalk_1 = __importDefault(require("chalk"));
|
||||
const ora_1 = __importDefault(require("ora"));
|
||||
const sdk_ts_1 = require("@bigcapital/sdk-ts");
|
||||
const config_1 = require("../config");
|
||||
const errors_1 = require("../utils/errors");
|
||||
const table_1 = require("../utils/table");
|
||||
function createUsersCommand() {
|
||||
const command = new commander_1.Command('users')
|
||||
.description('Manage users and roles');
|
||||
command
|
||||
.command('list')
|
||||
.description('List all users')
|
||||
.action(async () => {
|
||||
const spinner = (0, ora_1.default)('Loading users...').start();
|
||||
try {
|
||||
const fetcher = (0, config_1.createAuthenticatedFetcher)();
|
||||
const response = await (0, sdk_ts_1.fetchUsers)(fetcher);
|
||||
spinner.stop();
|
||||
const users = response.users;
|
||||
if (!users || users.length === 0) {
|
||||
console.log(chalk_1.default.yellow('No users found.'));
|
||||
return;
|
||||
}
|
||||
const table = (0, table_1.createTable)(['ID', 'Name', 'Email', 'Role', 'Status']);
|
||||
users.forEach((user) => {
|
||||
const name = `${user.firstName || ''} ${user.lastName || ''}`.trim() || '-';
|
||||
table.push([
|
||||
user.id,
|
||||
name,
|
||||
user.email || '-',
|
||||
user.role?.name || '-',
|
||||
(0, table_1.formatStatus)(user.active ? 'active' : 'inactive'),
|
||||
]);
|
||||
});
|
||||
console.log(table.toString());
|
||||
}
|
||||
catch (error) {
|
||||
spinner.stop();
|
||||
(0, errors_1.handleError)(error);
|
||||
}
|
||||
});
|
||||
command
|
||||
.command('roles')
|
||||
.description('List all roles')
|
||||
.action(async () => {
|
||||
const spinner = (0, ora_1.default)('Loading roles...').start();
|
||||
try {
|
||||
const fetcher = (0, config_1.createAuthenticatedFetcher)();
|
||||
const response = await (0, sdk_ts_1.fetchRoles)(fetcher);
|
||||
spinner.stop();
|
||||
const roles = response.roles;
|
||||
if (!roles || roles.length === 0) {
|
||||
console.log(chalk_1.default.yellow('No roles found.'));
|
||||
return;
|
||||
}
|
||||
const table = (0, table_1.createTable)(['ID', 'Name', 'Slug', 'Description']);
|
||||
roles.forEach((role) => {
|
||||
table.push([
|
||||
role.id,
|
||||
role.name || '-',
|
||||
role.slug || '-',
|
||||
role.description || '-',
|
||||
]);
|
||||
});
|
||||
console.log(table.toString());
|
||||
}
|
||||
catch (error) {
|
||||
spinner.stop();
|
||||
(0, errors_1.handleError)(error);
|
||||
}
|
||||
});
|
||||
return command;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
import { Command } from 'commander';
|
||||
export declare function createVendorCreditsCommand(): Command;
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.createVendorCreditsCommand = createVendorCreditsCommand;
|
||||
const commander_1 = require("commander");
|
||||
const chalk_1 = __importDefault(require("chalk"));
|
||||
const ora_1 = __importDefault(require("ora"));
|
||||
const sdk_ts_1 = require("@bigcapital/sdk-ts");
|
||||
const config_1 = require("../config");
|
||||
const errors_1 = require("../utils/errors");
|
||||
const table_1 = require("../utils/table");
|
||||
function createVendorCreditsCommand() {
|
||||
const command = new commander_1.Command('vendor-credits')
|
||||
.description('Manage vendor credits');
|
||||
command
|
||||
.command('list')
|
||||
.description('List all vendor credits')
|
||||
.option('-l, --limit <number>', 'Limit number of results per page', '50')
|
||||
.option('-p, --page <number>', 'Page number', '1')
|
||||
.option('-v, --vendor <id>', 'Filter by vendor ID')
|
||||
.action(async (options) => {
|
||||
const spinner = (0, ora_1.default)('Loading vendor credits...').start();
|
||||
try {
|
||||
const fetcher = (0, config_1.createAuthenticatedFetcher)();
|
||||
const query = {
|
||||
page: parseInt(options.page, 10),
|
||||
pageSize: Math.min(parseInt(options.limit, 10), 100),
|
||||
...(options.vendor && { vendorId: parseInt(options.vendor, 10) }),
|
||||
};
|
||||
const response = await (0, sdk_ts_1.fetchVendorCredits)(fetcher, query);
|
||||
spinner.stop();
|
||||
const vendorCredits = response.vendorCredits;
|
||||
if (!vendorCredits || vendorCredits.length === 0) {
|
||||
console.log(chalk_1.default.yellow('No vendor credits found.'));
|
||||
return;
|
||||
}
|
||||
const pagination = response.pagination;
|
||||
if (pagination) {
|
||||
const pageSize = pagination.pageSize || pagination.page_size || 10;
|
||||
const totalPages = Math.ceil(pagination.total / pageSize);
|
||||
console.log(chalk_1.default.gray(`\nPage ${pagination.page} of ${totalPages} (${pagination.total} total vendor credits)\n`));
|
||||
}
|
||||
const table = (0, table_1.createTable)(['ID', 'VC #', 'Vendor', 'Date', 'Total', 'Balance', 'Status']);
|
||||
vendorCredits.forEach((vc) => {
|
||||
table.push([
|
||||
vc.id,
|
||||
vc.vendorCreditNumber || '-',
|
||||
(0, table_1.truncate)(vc.vendor?.displayName, 20),
|
||||
(0, table_1.formatDate)(vc.vendorCreditDate),
|
||||
(0, table_1.formatCurrency)(vc.total),
|
||||
(0, table_1.formatCurrency)(vc.balance),
|
||||
(0, table_1.formatStatus)(vc.status),
|
||||
]);
|
||||
});
|
||||
console.log(table.toString());
|
||||
if (pagination) {
|
||||
const pageSize = pagination.pageSize || pagination.page_size || 10;
|
||||
const totalPages = Math.ceil(pagination.total / pageSize);
|
||||
if (pagination.page < totalPages) {
|
||||
console.log(chalk_1.default.gray(`\nUse --page ${pagination.page + 1} to see more results`));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
spinner.stop();
|
||||
(0, errors_1.handleError)(error);
|
||||
}
|
||||
});
|
||||
return command;
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
import { Command } from 'commander';
|
||||
export declare function createVendorsCommand(): Command;
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.createVendorsCommand = createVendorsCommand;
|
||||
const commander_1 = require("commander");
|
||||
const chalk_1 = __importDefault(require("chalk"));
|
||||
const ora_1 = __importDefault(require("ora"));
|
||||
const sdk_ts_1 = require("@bigcapital/sdk-ts");
|
||||
const config_1 = require("../config");
|
||||
const errors_1 = require("../utils/errors");
|
||||
const table_1 = require("../utils/table");
|
||||
function createVendorsCommand() {
|
||||
const command = new commander_1.Command('vendors')
|
||||
.description('Manage vendors');
|
||||
command
|
||||
.command('list')
|
||||
.description('List all vendors')
|
||||
.option('-l, --limit <number>', 'Limit number of results per page', '50')
|
||||
.option('-p, --page <number>', 'Page number', '1')
|
||||
.option('--active-only', 'Show only active vendors', false)
|
||||
.action(async (options) => {
|
||||
const spinner = (0, ora_1.default)('Loading vendors...').start();
|
||||
try {
|
||||
const fetcher = (0, config_1.createAuthenticatedFetcher)();
|
||||
const query = {
|
||||
page: parseInt(options.page, 10),
|
||||
pageSize: Math.min(parseInt(options.limit, 10), 100),
|
||||
...(options.activeOnly && { active: true }),
|
||||
};
|
||||
const response = await (0, sdk_ts_1.fetchVendors)(fetcher, query);
|
||||
spinner.stop();
|
||||
const vendors = response.vendors;
|
||||
if (!vendors || vendors.length === 0) {
|
||||
console.log(chalk_1.default.yellow('No vendors found.'));
|
||||
return;
|
||||
}
|
||||
const pagination = response.pagination;
|
||||
if (pagination) {
|
||||
const pageSize = pagination.pageSize || pagination.page_size || 10;
|
||||
const totalPages = Math.ceil(pagination.total / pageSize);
|
||||
console.log(chalk_1.default.gray(`\nPage ${pagination.page} of ${totalPages} (${pagination.total} total vendors)\n`));
|
||||
}
|
||||
const table = (0, table_1.createTable)(['ID', 'Name', 'Email', 'Work Phone', 'Personal Phone', 'Balance', 'Status']);
|
||||
vendors.forEach((vendor) => {
|
||||
table.push([
|
||||
vendor.id,
|
||||
(0, table_1.truncate)(vendor.displayName, 25),
|
||||
(0, table_1.truncate)(vendor.email, 25) || '-',
|
||||
vendor.workPhone || '-',
|
||||
vendor.personalPhone || '-',
|
||||
(0, table_1.formatCurrency)(vendor.balance),
|
||||
(0, table_1.formatStatus)(vendor.active ? 'active' : 'inactive'),
|
||||
]);
|
||||
});
|
||||
console.log(table.toString());
|
||||
if (pagination) {
|
||||
const pageSize = pagination.pageSize || pagination.page_size || 10;
|
||||
const totalPages = Math.ceil(pagination.total / pageSize);
|
||||
if (pagination.page < totalPages) {
|
||||
console.log(chalk_1.default.gray(`\nUse --page ${pagination.page + 1} to see more results`));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
spinner.stop();
|
||||
(0, errors_1.handleError)(error);
|
||||
}
|
||||
});
|
||||
return command;
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
import { Command } from 'commander';
|
||||
export declare function createWarehousesCommand(): Command;
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.createWarehousesCommand = createWarehousesCommand;
|
||||
const commander_1 = require("commander");
|
||||
const chalk_1 = __importDefault(require("chalk"));
|
||||
const ora_1 = __importDefault(require("ora"));
|
||||
const sdk_ts_1 = require("@bigcapital/sdk-ts");
|
||||
const config_1 = require("../config");
|
||||
const errors_1 = require("../utils/errors");
|
||||
const table_1 = require("../utils/table");
|
||||
function createWarehousesCommand() {
|
||||
const command = new commander_1.Command('warehouses')
|
||||
.description('Manage warehouses');
|
||||
command
|
||||
.command('list')
|
||||
.description('List all warehouses')
|
||||
.action(async () => {
|
||||
const spinner = (0, ora_1.default)('Loading warehouses...').start();
|
||||
try {
|
||||
const fetcher = (0, config_1.createAuthenticatedFetcher)();
|
||||
const response = await (0, sdk_ts_1.fetchWarehouses)(fetcher);
|
||||
spinner.stop();
|
||||
const warehouses = response.warehouses;
|
||||
if (!warehouses || warehouses.length === 0) {
|
||||
console.log(chalk_1.default.yellow('No warehouses found.'));
|
||||
return;
|
||||
}
|
||||
const table = (0, table_1.createTable)(['ID', 'Code', 'Name', 'City', 'Status']);
|
||||
warehouses.forEach((warehouse) => {
|
||||
table.push([
|
||||
warehouse.id,
|
||||
warehouse.code || '-',
|
||||
warehouse.name || '-',
|
||||
warehouse.city || '-',
|
||||
(0, table_1.formatStatus)(warehouse.active ? 'active' : 'inactive'),
|
||||
]);
|
||||
});
|
||||
console.log(table.toString());
|
||||
}
|
||||
catch (error) {
|
||||
spinner.stop();
|
||||
(0, errors_1.handleError)(error);
|
||||
}
|
||||
});
|
||||
return command;
|
||||
}
|
||||
Vendored
+13
@@ -0,0 +1,13 @@
|
||||
import { ApiFetcher } from '@bigcapital/sdk-ts';
|
||||
interface ConfigSchema {
|
||||
apiKey: string;
|
||||
baseUrl: string;
|
||||
organizationId?: string;
|
||||
}
|
||||
export declare function getConfig(): ConfigSchema;
|
||||
export declare function setApiKey(apiKey: string): void;
|
||||
export declare function setBaseUrl(baseUrl: string): void;
|
||||
export declare function setOrganizationId(organizationId: string): void;
|
||||
export declare function validateConfig(): ConfigSchema;
|
||||
export declare function createAuthenticatedFetcher(): ApiFetcher;
|
||||
export {};
|
||||
Vendored
+75
@@ -0,0 +1,75 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.getConfig = getConfig;
|
||||
exports.setApiKey = setApiKey;
|
||||
exports.setBaseUrl = setBaseUrl;
|
||||
exports.setOrganizationId = setOrganizationId;
|
||||
exports.validateConfig = validateConfig;
|
||||
exports.createAuthenticatedFetcher = createAuthenticatedFetcher;
|
||||
const conf_1 = __importDefault(require("conf"));
|
||||
const chalk_1 = __importDefault(require("chalk"));
|
||||
const sdk_ts_1 = require("@bigcapital/sdk-ts");
|
||||
const config = new conf_1.default({
|
||||
projectName: 'bigcapital',
|
||||
schema: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
},
|
||||
baseUrl: {
|
||||
type: 'string',
|
||||
},
|
||||
organizationId: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
});
|
||||
function getConfig() {
|
||||
return {
|
||||
apiKey: config.get('apiKey', ''),
|
||||
baseUrl: config.get('baseUrl', ''),
|
||||
organizationId: config.get('organizationId'),
|
||||
};
|
||||
}
|
||||
function setApiKey(apiKey) {
|
||||
config.set('apiKey', apiKey);
|
||||
}
|
||||
function setBaseUrl(baseUrl) {
|
||||
// Remove trailing slash if present
|
||||
const normalizedUrl = baseUrl.replace(/\/$/, '');
|
||||
config.set('baseUrl', normalizedUrl);
|
||||
}
|
||||
function setOrganizationId(organizationId) {
|
||||
config.set('organizationId', organizationId);
|
||||
}
|
||||
function validateConfig() {
|
||||
const currentConfig = getConfig();
|
||||
if (!currentConfig.apiKey) {
|
||||
console.error(chalk_1.default.red('Error: API key is not configured.'));
|
||||
console.error(chalk_1.default.yellow('Run: bigcapital config set api-key <your-api-key>'));
|
||||
process.exit(1);
|
||||
}
|
||||
if (!currentConfig.baseUrl) {
|
||||
console.error(chalk_1.default.red('Error: Base URL is not configured.'));
|
||||
console.error(chalk_1.default.yellow('Run: bigcapital config set base-url <https://your-api-url>'));
|
||||
process.exit(1);
|
||||
}
|
||||
return currentConfig;
|
||||
}
|
||||
function createAuthenticatedFetcher() {
|
||||
const currentConfig = validateConfig();
|
||||
const headers = {
|
||||
'Authorization': `Bearer ${currentConfig.apiKey}`,
|
||||
};
|
||||
if (currentConfig.organizationId) {
|
||||
headers['organization-id'] = currentConfig.organizationId;
|
||||
}
|
||||
return (0, sdk_ts_1.createApiFetcher)({
|
||||
baseUrl: currentConfig.baseUrl,
|
||||
init: {
|
||||
headers,
|
||||
},
|
||||
});
|
||||
}
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
#!/usr/bin/env node
|
||||
export {};
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env node
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const commander_1 = require("commander");
|
||||
const items_1 = require("./commands/items");
|
||||
const invoices_1 = require("./commands/invoices");
|
||||
const config_1 = require("./commands/config");
|
||||
const customers_1 = require("./commands/customers");
|
||||
const vendors_1 = require("./commands/vendors");
|
||||
const bills_1 = require("./commands/bills");
|
||||
const accounts_1 = require("./commands/accounts");
|
||||
const expenses_1 = require("./commands/expenses");
|
||||
const credit_notes_1 = require("./commands/credit-notes");
|
||||
const vendor_credits_1 = require("./commands/vendor-credits");
|
||||
const payments_1 = require("./commands/payments");
|
||||
const estimates_1 = require("./commands/estimates");
|
||||
const receipts_1 = require("./commands/receipts");
|
||||
const journals_1 = require("./commands/journals");
|
||||
const inventory_1 = require("./commands/inventory");
|
||||
const tax_rates_1 = require("./commands/tax-rates");
|
||||
const warehouses_1 = require("./commands/warehouses");
|
||||
const users_1 = require("./commands/users");
|
||||
const reports_1 = require("./commands/reports");
|
||||
const chalk_1 = __importDefault(require("chalk"));
|
||||
const program = new commander_1.Command();
|
||||
program
|
||||
.name('bigcapital')
|
||||
.description('Bigcapital CLI - Interact with Bigcapital API')
|
||||
.version('1.0.0')
|
||||
.configureOutput({
|
||||
writeErr: (str) => process.stderr.write(chalk_1.default.red(str)),
|
||||
outputError: (str, write) => write(chalk_1.default.red(str)),
|
||||
});
|
||||
// Core modules
|
||||
program.addCommand((0, config_1.createConfigCommand)());
|
||||
program.addCommand((0, items_1.createItemsCommand)());
|
||||
program.addCommand((0, invoices_1.createInvoicesCommand)());
|
||||
program.addCommand((0, customers_1.createCustomersCommand)());
|
||||
program.addCommand((0, vendors_1.createVendorsCommand)());
|
||||
program.addCommand((0, bills_1.createBillsCommand)());
|
||||
// Additional transactional modules
|
||||
program.addCommand((0, accounts_1.createAccountsCommand)());
|
||||
program.addCommand((0, expenses_1.createExpensesCommand)());
|
||||
program.addCommand((0, credit_notes_1.createCreditNotesCommand)());
|
||||
program.addCommand((0, vendor_credits_1.createVendorCreditsCommand)());
|
||||
program.addCommand((0, payments_1.createPaymentsCommand)());
|
||||
program.addCommand((0, estimates_1.createEstimatesCommand)());
|
||||
program.addCommand((0, receipts_1.createReceiptsCommand)());
|
||||
program.addCommand((0, journals_1.createJournalsCommand)());
|
||||
program.addCommand((0, inventory_1.createInventoryCommand)());
|
||||
program.addCommand((0, tax_rates_1.createTaxRatesCommand)());
|
||||
program.addCommand((0, warehouses_1.createWarehousesCommand)());
|
||||
program.addCommand((0, users_1.createUsersCommand)());
|
||||
// Financial reports
|
||||
program.addCommand((0, reports_1.createReportsCommand)());
|
||||
// Global error handling
|
||||
program.hook('preAction', () => {
|
||||
process.on('unhandledRejection', (error) => {
|
||||
console.error(chalk_1.default.red('\nUnhandled error:'), error);
|
||||
process.exit(1);
|
||||
});
|
||||
process.on('uncaughtException', (error) => {
|
||||
console.error(chalk_1.default.red('\nUncaught exception:'), error);
|
||||
process.exit(1);
|
||||
});
|
||||
});
|
||||
// Show help if no command provided
|
||||
if (process.argv.length <= 2) {
|
||||
program.help();
|
||||
}
|
||||
program.parse();
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
export declare function handleError(error: unknown): never;
|
||||
export declare function assertDefined<T>(value: T | undefined | null, name: string): T;
|
||||
Vendored
+45
@@ -0,0 +1,45 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.handleError = handleError;
|
||||
exports.assertDefined = assertDefined;
|
||||
const chalk_1 = __importDefault(require("chalk"));
|
||||
function handleError(error) {
|
||||
if (error instanceof Error) {
|
||||
// Check if it's an HTTP error from the SDK
|
||||
const httpError = error;
|
||||
if (httpError.status) {
|
||||
console.error(chalk_1.default.red(`HTTP Error ${httpError.status}: ${httpError.statusText || error.message}`));
|
||||
if (httpError.status === 401) {
|
||||
console.error(chalk_1.default.yellow('Your API key may be invalid or expired.'));
|
||||
console.error(chalk_1.default.yellow('Run: bigcapital config set api-key <your-new-api-key>'));
|
||||
}
|
||||
else if (httpError.status === 403) {
|
||||
console.error(chalk_1.default.yellow('You don\'t have permission to access this resource.'));
|
||||
}
|
||||
else if (httpError.status === 404) {
|
||||
console.error(chalk_1.default.yellow('The requested resource was not found.'));
|
||||
}
|
||||
}
|
||||
else {
|
||||
console.error(chalk_1.default.red(`Error: ${error.message}`));
|
||||
}
|
||||
if (process.env.DEBUG) {
|
||||
console.error(chalk_1.default.gray(error.stack));
|
||||
}
|
||||
}
|
||||
else {
|
||||
console.error(chalk_1.default.red('An unknown error occurred'));
|
||||
console.error(error);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
function assertDefined(value, name) {
|
||||
if (value === undefined || value === null) {
|
||||
console.error(chalk_1.default.red(`Error: ${name} is required but was not provided.`));
|
||||
process.exit(1);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
import Table from 'cli-table3';
|
||||
export declare function createTable(headers: string[]): Table.Table;
|
||||
export declare function formatCurrency(amount: number | string | undefined, currency?: string): string;
|
||||
export declare function formatDate(dateStr: string | undefined): string;
|
||||
export declare function truncate(str: string | undefined, maxLength: number): string;
|
||||
export declare function formatStatus(status: string | undefined): string;
|
||||
Vendored
+84
@@ -0,0 +1,84 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.createTable = createTable;
|
||||
exports.formatCurrency = formatCurrency;
|
||||
exports.formatDate = formatDate;
|
||||
exports.truncate = truncate;
|
||||
exports.formatStatus = formatStatus;
|
||||
const cli_table3_1 = __importDefault(require("cli-table3"));
|
||||
const chalk_1 = __importDefault(require("chalk"));
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function createTable(headers) {
|
||||
return new cli_table3_1.default({
|
||||
head: headers.map(h => chalk_1.default.cyan.bold(h)),
|
||||
style: {
|
||||
head: [],
|
||||
border: [],
|
||||
},
|
||||
chars: {
|
||||
top: '─',
|
||||
'top-mid': '┬',
|
||||
'top-left': '┌',
|
||||
'top-right': '┐',
|
||||
bottom: '─',
|
||||
'bottom-mid': '┴',
|
||||
'bottom-left': '└',
|
||||
'bottom-right': '┘',
|
||||
left: '│',
|
||||
'left-mid': '├',
|
||||
mid: '─',
|
||||
'mid-mid': '┼',
|
||||
right: '│',
|
||||
'right-mid': '┤',
|
||||
middle: '│',
|
||||
},
|
||||
});
|
||||
}
|
||||
function formatCurrency(amount, currency = '$') {
|
||||
if (amount === undefined || amount === null)
|
||||
return '-';
|
||||
const num = typeof amount === 'string' ? parseFloat(amount) : amount;
|
||||
if (isNaN(num))
|
||||
return '-';
|
||||
return `${currency}${num.toFixed(2)}`;
|
||||
}
|
||||
function formatDate(dateStr) {
|
||||
if (!dateStr)
|
||||
return '-';
|
||||
const date = new Date(dateStr);
|
||||
if (isNaN(date.getTime()))
|
||||
return dateStr;
|
||||
return date.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
});
|
||||
}
|
||||
function truncate(str, maxLength) {
|
||||
if (!str)
|
||||
return '-';
|
||||
if (str.length <= maxLength)
|
||||
return str;
|
||||
return str.substring(0, maxLength - 3) + '...';
|
||||
}
|
||||
function formatStatus(status) {
|
||||
if (!status)
|
||||
return '-';
|
||||
const statusColors = {
|
||||
published: chalk_1.default.green,
|
||||
draft: chalk_1.default.gray,
|
||||
delivered: chalk_1.default.blue,
|
||||
partial: chalk_1.default.yellow,
|
||||
paid: chalk_1.default.green.bold,
|
||||
unpaid: chalk_1.default.red,
|
||||
overdue: chalk_1.default.red.bold,
|
||||
active: chalk_1.default.green,
|
||||
inactive: chalk_1.default.gray,
|
||||
};
|
||||
const lowerStatus = status.toLowerCase();
|
||||
const colorFn = statusColors[lowerStatus] || chalk_1.default.white;
|
||||
return colorFn(status);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "@bigcapital/cli",
|
||||
"version": "1.0.0",
|
||||
"description": "Bigcapital CLI - Interact with Bigcapital API",
|
||||
"main": "./dist/index.js",
|
||||
"bin": {
|
||||
"bigcapital": "./dist/index.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "tsc --watch",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"keywords": [
|
||||
"bigcapital",
|
||||
"cli",
|
||||
"accounting",
|
||||
"api"
|
||||
],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@bigcapital/sdk-ts": "workspace:*",
|
||||
"chalk": "^4.1.2",
|
||||
"cli-table3": "^0.6.3",
|
||||
"commander": "^11.1.0",
|
||||
"conf": "^10.2.0",
|
||||
"ora": "^5.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.0.0",
|
||||
"typescript": "^5.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { Command } from 'commander';
|
||||
import chalk from 'chalk';
|
||||
import ora from 'ora';
|
||||
import { fetchAccounts } from '@bigcapital/sdk-ts';
|
||||
import { createAuthenticatedFetcher } from '../config';
|
||||
import { handleError } from '../utils/errors';
|
||||
import { createTable, formatCurrency, truncate, formatStatus } from '../utils/table';
|
||||
|
||||
export function createAccountsCommand(): Command {
|
||||
const command = new Command('accounts')
|
||||
.description('Manage chart of accounts');
|
||||
|
||||
command
|
||||
.command('list')
|
||||
.description('List all accounts')
|
||||
.option('-t, --type <type>', 'Filter by account type')
|
||||
.option('--active-only', 'Show only active accounts', false)
|
||||
.action(async (options) => {
|
||||
const spinner = ora('Loading accounts...').start();
|
||||
|
||||
try {
|
||||
const fetcher = createAuthenticatedFetcher();
|
||||
|
||||
const query = {
|
||||
...(options.type && { type: options.type }),
|
||||
...(options.activeOnly && { active: true }),
|
||||
};
|
||||
|
||||
const accounts = await fetchAccounts(fetcher, query);
|
||||
|
||||
spinner.stop();
|
||||
|
||||
if (!accounts || accounts.length === 0) {
|
||||
console.log(chalk.yellow('No accounts found.'));
|
||||
return;
|
||||
}
|
||||
|
||||
const table = createTable(['ID', 'Code', 'Name', 'Type', 'Balance', 'Status']);
|
||||
|
||||
accounts.forEach((account: {
|
||||
id: number;
|
||||
code?: string;
|
||||
name?: string;
|
||||
accountType?: string;
|
||||
amount?: number;
|
||||
active?: boolean;
|
||||
}) => {
|
||||
table.push([
|
||||
account.id,
|
||||
account.code || '-',
|
||||
truncate(account.name, 30),
|
||||
account.accountType || '-',
|
||||
formatCurrency(account.amount),
|
||||
formatStatus(account.active ? 'active' : 'inactive'),
|
||||
]);
|
||||
});
|
||||
|
||||
console.log(table.toString());
|
||||
console.log(chalk.gray(`\nTotal accounts: ${accounts.length}`));
|
||||
} catch (error) {
|
||||
spinner.stop();
|
||||
handleError(error);
|
||||
}
|
||||
});
|
||||
|
||||
return command;
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import { Command } from 'commander';
|
||||
import chalk from 'chalk';
|
||||
import ora from 'ora';
|
||||
import { fetchBills } from '@bigcapital/sdk-ts';
|
||||
import { createAuthenticatedFetcher } from '../config';
|
||||
import { handleError } from '../utils/errors';
|
||||
import { createTable, formatCurrency, formatDate, truncate, formatStatus } from '../utils/table';
|
||||
|
||||
export function createBillsCommand(): Command {
|
||||
const command = new Command('bills')
|
||||
.description('Manage bills');
|
||||
|
||||
command
|
||||
.command('list')
|
||||
.description('List all bills')
|
||||
.option('-l, --limit <number>', 'Limit number of results per page', '50')
|
||||
.option('-p, --page <number>', 'Page number', '1')
|
||||
.option('-v, --vendor <id>', 'Filter by vendor ID')
|
||||
.option('-s, --status <status>', 'Filter by status (draft, published, paid, partial)')
|
||||
.action(async (options) => {
|
||||
const spinner = ora('Loading bills...').start();
|
||||
|
||||
try {
|
||||
const fetcher = createAuthenticatedFetcher();
|
||||
|
||||
const query = {
|
||||
page: parseInt(options.page, 10),
|
||||
pageSize: Math.min(parseInt(options.limit, 10), 100),
|
||||
...(options.vendor && { vendorId: parseInt(options.vendor, 10) }),
|
||||
...(options.status && { status: options.status }),
|
||||
};
|
||||
|
||||
const response = await fetchBills(fetcher, query);
|
||||
|
||||
spinner.stop();
|
||||
|
||||
const bills = (response as unknown as { bills: Array<{
|
||||
id: number;
|
||||
billNumber?: string;
|
||||
vendor?: { displayName?: string };
|
||||
billDate?: string;
|
||||
dueDate?: string;
|
||||
total?: number;
|
||||
balance?: number;
|
||||
status?: string;
|
||||
}> }).bills;
|
||||
|
||||
if (!bills || bills.length === 0) {
|
||||
console.log(chalk.yellow('No bills found.'));
|
||||
return;
|
||||
}
|
||||
|
||||
const pagination = (response as unknown as {
|
||||
pagination?: { total: number; page: number; pageSize?: number; page_size?: number }
|
||||
}).pagination;
|
||||
if (pagination) {
|
||||
const pageSize = pagination.pageSize || pagination.page_size || 10;
|
||||
const totalPages = Math.ceil(pagination.total / pageSize);
|
||||
console.log(chalk.gray(`\nPage ${pagination.page} of ${totalPages} (${pagination.total} total bills)\n`));
|
||||
}
|
||||
|
||||
const table = createTable(['ID', 'Bill #', 'Vendor', 'Date', 'Due Date', 'Total', 'Balance', 'Status']);
|
||||
|
||||
bills.forEach((bill) => {
|
||||
table.push([
|
||||
bill.id,
|
||||
bill.billNumber || '-',
|
||||
truncate(bill.vendor?.displayName, 20),
|
||||
formatDate(bill.billDate),
|
||||
formatDate(bill.dueDate),
|
||||
formatCurrency(bill.total),
|
||||
formatCurrency(bill.balance),
|
||||
formatStatus(bill.status),
|
||||
]);
|
||||
});
|
||||
|
||||
console.log(table.toString());
|
||||
|
||||
if (pagination) {
|
||||
const pageSize = pagination.pageSize || pagination.page_size || 10;
|
||||
const totalPages = Math.ceil(pagination.total / pageSize);
|
||||
if (pagination.page < totalPages) {
|
||||
console.log(chalk.gray(`\nUse --page ${pagination.page + 1} to see more results`));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
spinner.stop();
|
||||
handleError(error);
|
||||
}
|
||||
});
|
||||
|
||||
return command;
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { Command } from 'commander';
|
||||
import chalk from 'chalk';
|
||||
import { getConfig, setApiKey, setBaseUrl, setOrganizationId } from '../config';
|
||||
|
||||
export function createConfigCommand(): Command {
|
||||
const command = new Command('config')
|
||||
.description('Manage CLI configuration');
|
||||
|
||||
command
|
||||
.command('set')
|
||||
.description('Set a configuration value')
|
||||
.argument('<key>', 'Configuration key (api-key, base-url, organization-id)')
|
||||
.argument('<value>', 'Configuration value')
|
||||
.action((key: string, value: string) => {
|
||||
switch (key.toLowerCase()) {
|
||||
case 'api-key':
|
||||
setApiKey(value);
|
||||
console.log(chalk.green('✓ API key configured successfully'));
|
||||
break;
|
||||
case 'base-url':
|
||||
setBaseUrl(value);
|
||||
console.log(chalk.green('✓ Base URL configured successfully'));
|
||||
break;
|
||||
case 'organization-id':
|
||||
setOrganizationId(value);
|
||||
console.log(chalk.green('✓ Organization ID configured successfully'));
|
||||
break;
|
||||
default:
|
||||
console.error(chalk.red(`Error: Unknown configuration key "${key}"`));
|
||||
console.log(chalk.yellow('Valid keys: api-key, base-url, organization-id'));
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
command
|
||||
.command('get')
|
||||
.description('Show current configuration')
|
||||
.action(() => {
|
||||
const config = getConfig();
|
||||
|
||||
console.log(chalk.bold('\nBigcapital CLI Configuration:'));
|
||||
console.log(chalk.gray('─'.repeat(50)));
|
||||
|
||||
if (config.apiKey) {
|
||||
const maskedKey = config.apiKey.substring(0, 4) + '...' + config.apiKey.substring(config.apiKey.length - 4);
|
||||
console.log(`API Key: ${chalk.green(maskedKey)}`);
|
||||
} else {
|
||||
console.log(`API Key: ${chalk.yellow('Not set')}`);
|
||||
}
|
||||
|
||||
if (config.baseUrl) {
|
||||
console.log(`Base URL: ${chalk.green(config.baseUrl)}`);
|
||||
} else {
|
||||
console.log(`Base URL: ${chalk.yellow('Not set')}`);
|
||||
}
|
||||
|
||||
if (config.organizationId) {
|
||||
console.log(`Organization: ${chalk.green(config.organizationId)}`);
|
||||
} else {
|
||||
console.log(`Organization: ${chalk.yellow('Not set')} (optional)`);
|
||||
}
|
||||
|
||||
console.log(chalk.gray('─'.repeat(50)));
|
||||
console.log(chalk.gray('\nConfig file location: ~/.config/bigcapital-nodejs/config.json\n'));
|
||||
});
|
||||
|
||||
return command;
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import { Command } from 'commander';
|
||||
import chalk from 'chalk';
|
||||
import ora from 'ora';
|
||||
import { fetchCreditNotes } from '@bigcapital/sdk-ts';
|
||||
import { createAuthenticatedFetcher } from '../config';
|
||||
import { handleError } from '../utils/errors';
|
||||
import { createTable, formatCurrency, formatDate, truncate, formatStatus } from '../utils/table';
|
||||
|
||||
export function createCreditNotesCommand(): Command {
|
||||
const command = new Command('credit-notes')
|
||||
.description('Manage credit notes');
|
||||
|
||||
command
|
||||
.command('list')
|
||||
.description('List all credit notes')
|
||||
.option('-l, --limit <number>', 'Limit number of results per page', '50')
|
||||
.option('-p, --page <number>', 'Page number', '1')
|
||||
.option('-c, --customer <id>', 'Filter by customer ID')
|
||||
.action(async (options) => {
|
||||
const spinner = ora('Loading credit notes...').start();
|
||||
|
||||
try {
|
||||
const fetcher = createAuthenticatedFetcher();
|
||||
|
||||
const query = {
|
||||
page: parseInt(options.page, 10),
|
||||
pageSize: Math.min(parseInt(options.limit, 10), 100),
|
||||
...(options.customer && { customerId: parseInt(options.customer, 10) }),
|
||||
};
|
||||
|
||||
const response = await fetchCreditNotes(fetcher, query);
|
||||
|
||||
spinner.stop();
|
||||
|
||||
const creditNotes = (response as unknown as { creditNotes: Array<{
|
||||
id: number;
|
||||
creditNoteNumber?: string;
|
||||
customer?: { displayName?: string };
|
||||
creditNoteDate?: string;
|
||||
total?: number;
|
||||
balance?: number;
|
||||
status?: string;
|
||||
}> }).creditNotes;
|
||||
|
||||
if (!creditNotes || creditNotes.length === 0) {
|
||||
console.log(chalk.yellow('No credit notes found.'));
|
||||
return;
|
||||
}
|
||||
|
||||
const pagination = (response as unknown as {
|
||||
pagination?: { total: number; page: number; pageSize?: number; page_size?: number }
|
||||
}).pagination;
|
||||
if (pagination) {
|
||||
const pageSize = pagination.pageSize || pagination.page_size || 10;
|
||||
const totalPages = Math.ceil(pagination.total / pageSize);
|
||||
console.log(chalk.gray(`\nPage ${pagination.page} of ${totalPages} (${pagination.total} total credit notes)\n`));
|
||||
}
|
||||
|
||||
const table = createTable(['ID', 'CN #', 'Customer', 'Date', 'Total', 'Balance', 'Status']);
|
||||
|
||||
creditNotes.forEach((cn) => {
|
||||
table.push([
|
||||
cn.id,
|
||||
cn.creditNoteNumber || '-',
|
||||
truncate(cn.customer?.displayName, 20),
|
||||
formatDate(cn.creditNoteDate),
|
||||
formatCurrency(cn.total),
|
||||
formatCurrency(cn.balance),
|
||||
formatStatus(cn.status),
|
||||
]);
|
||||
});
|
||||
|
||||
console.log(table.toString());
|
||||
|
||||
if (pagination) {
|
||||
const pageSize = pagination.pageSize || pagination.page_size || 10;
|
||||
const totalPages = Math.ceil(pagination.total / pageSize);
|
||||
if (pagination.page < totalPages) {
|
||||
console.log(chalk.gray(`\nUse --page ${pagination.page + 1} to see more results`));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
spinner.stop();
|
||||
handleError(error);
|
||||
}
|
||||
});
|
||||
|
||||
return command;
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import { Command } from 'commander';
|
||||
import chalk from 'chalk';
|
||||
import ora from 'ora';
|
||||
import { fetchCustomers } from '@bigcapital/sdk-ts';
|
||||
import { createAuthenticatedFetcher } from '../config';
|
||||
import { handleError } from '../utils/errors';
|
||||
import { createTable, formatCurrency, truncate, formatStatus } from '../utils/table';
|
||||
|
||||
export function createCustomersCommand(): Command {
|
||||
const command = new Command('customers')
|
||||
.description('Manage customers');
|
||||
|
||||
command
|
||||
.command('list')
|
||||
.description('List all customers')
|
||||
.option('-l, --limit <number>', 'Limit number of results per page', '50')
|
||||
.option('-p, --page <number>', 'Page number', '1')
|
||||
.option('--active-only', 'Show only active customers', false)
|
||||
.action(async (options) => {
|
||||
const spinner = ora('Loading customers...').start();
|
||||
|
||||
try {
|
||||
const fetcher = createAuthenticatedFetcher();
|
||||
|
||||
const query = {
|
||||
page: parseInt(options.page, 10),
|
||||
pageSize: Math.min(parseInt(options.limit, 10), 100),
|
||||
...(options.activeOnly && { active: true }),
|
||||
};
|
||||
|
||||
const response = await fetchCustomers(fetcher, query);
|
||||
|
||||
spinner.stop();
|
||||
|
||||
const customers = (response as unknown as { customers: Array<{
|
||||
id: number;
|
||||
displayName?: string;
|
||||
email?: string;
|
||||
workPhone?: string;
|
||||
personalPhone?: string;
|
||||
balance?: number;
|
||||
active?: boolean;
|
||||
}> }).customers;
|
||||
|
||||
if (!customers || customers.length === 0) {
|
||||
console.log(chalk.yellow('No customers found.'));
|
||||
return;
|
||||
}
|
||||
|
||||
const pagination = (response as unknown as {
|
||||
pagination?: { total: number; page: number; pageSize?: number; page_size?: number }
|
||||
}).pagination;
|
||||
if (pagination) {
|
||||
const pageSize = pagination.pageSize || pagination.page_size || 10;
|
||||
const totalPages = Math.ceil(pagination.total / pageSize);
|
||||
console.log(chalk.gray(`\nPage ${pagination.page} of ${totalPages} (${pagination.total} total customers)\n`));
|
||||
}
|
||||
|
||||
const table = createTable(['ID', 'Name', 'Email', 'Work Phone', 'Personal Phone', 'Balance', 'Status']);
|
||||
|
||||
customers.forEach((customer) => {
|
||||
table.push([
|
||||
customer.id,
|
||||
truncate(customer.displayName, 25),
|
||||
truncate(customer.email, 25) || '-',
|
||||
customer.workPhone || '-',
|
||||
customer.personalPhone || '-',
|
||||
formatCurrency(customer.balance),
|
||||
formatStatus(customer.active ? 'active' : 'inactive'),
|
||||
]);
|
||||
});
|
||||
|
||||
console.log(table.toString());
|
||||
|
||||
if (pagination) {
|
||||
const pageSize = pagination.pageSize || pagination.page_size || 10;
|
||||
const totalPages = Math.ceil(pagination.total / pageSize);
|
||||
if (pagination.page < totalPages) {
|
||||
console.log(chalk.gray(`\nUse --page ${pagination.page + 1} to see more results`));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
spinner.stop();
|
||||
handleError(error);
|
||||
}
|
||||
});
|
||||
|
||||
return command;
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import { Command } from 'commander';
|
||||
import chalk from 'chalk';
|
||||
import ora from 'ora';
|
||||
import { fetchSaleEstimates } from '@bigcapital/sdk-ts';
|
||||
import { createAuthenticatedFetcher } from '../config';
|
||||
import { handleError } from '../utils/errors';
|
||||
import { createTable, formatCurrency, formatDate, truncate, formatStatus } from '../utils/table';
|
||||
|
||||
export function createEstimatesCommand(): Command {
|
||||
const command = new Command('estimates')
|
||||
.description('Manage sale estimates/quotes');
|
||||
|
||||
command
|
||||
.command('list')
|
||||
.description('List all sale estimates')
|
||||
.option('-l, --limit <number>', 'Limit number of results per page', '50')
|
||||
.option('-p, --page <number>', 'Page number', '1')
|
||||
.option('-c, --customer <id>', 'Filter by customer ID')
|
||||
.option('-s, --status <status>', 'Filter by status (draft, published)')
|
||||
.action(async (options) => {
|
||||
const spinner = ora('Loading estimates...').start();
|
||||
|
||||
try {
|
||||
const fetcher = createAuthenticatedFetcher();
|
||||
|
||||
const query = {
|
||||
page: parseInt(options.page, 10),
|
||||
pageSize: Math.min(parseInt(options.limit, 10), 100),
|
||||
...(options.customer && { customerId: parseInt(options.customer, 10) }),
|
||||
...(options.status && { status: options.status }),
|
||||
};
|
||||
|
||||
const response = await fetchSaleEstimates(fetcher, query);
|
||||
|
||||
spinner.stop();
|
||||
|
||||
const estimates = (response as unknown as { saleEstimates: Array<{
|
||||
id: number;
|
||||
estimateNumber?: string;
|
||||
customer?: { displayName?: string };
|
||||
estimateDate?: string;
|
||||
expirationDate?: string;
|
||||
total?: number;
|
||||
status?: string;
|
||||
}> }).saleEstimates;
|
||||
|
||||
if (!estimates || estimates.length === 0) {
|
||||
console.log(chalk.yellow('No estimates found.'));
|
||||
return;
|
||||
}
|
||||
|
||||
const pagination = (response as unknown as {
|
||||
pagination?: { total: number; page: number; pageSize?: number; page_size?: number }
|
||||
}).pagination;
|
||||
if (pagination) {
|
||||
const pageSize = pagination.pageSize || pagination.page_size || 10;
|
||||
const totalPages = Math.ceil(pagination.total / pageSize);
|
||||
console.log(chalk.gray(`\nPage ${pagination.page} of ${totalPages} (${pagination.total} total estimates)\n`));
|
||||
}
|
||||
|
||||
const table = createTable(['ID', 'Estimate #', 'Customer', 'Date', 'Expires', 'Total', 'Status']);
|
||||
|
||||
estimates.forEach((estimate) => {
|
||||
table.push([
|
||||
estimate.id,
|
||||
estimate.estimateNumber || '-',
|
||||
truncate(estimate.customer?.displayName, 20),
|
||||
formatDate(estimate.estimateDate),
|
||||
formatDate(estimate.expirationDate),
|
||||
formatCurrency(estimate.total),
|
||||
formatStatus(estimate.status),
|
||||
]);
|
||||
});
|
||||
|
||||
console.log(table.toString());
|
||||
|
||||
if (pagination) {
|
||||
const pageSize = pagination.pageSize || pagination.page_size || 10;
|
||||
const totalPages = Math.ceil(pagination.total / pageSize);
|
||||
if (pagination.page < totalPages) {
|
||||
console.log(chalk.gray(`\nUse --page ${pagination.page + 1} to see more results`));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
spinner.stop();
|
||||
handleError(error);
|
||||
}
|
||||
});
|
||||
|
||||
return command;
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import { Command } from 'commander';
|
||||
import chalk from 'chalk';
|
||||
import ora from 'ora';
|
||||
import { fetchExpenses } from '@bigcapital/sdk-ts';
|
||||
import { createAuthenticatedFetcher } from '../config';
|
||||
import { handleError } from '../utils/errors';
|
||||
import { createTable, formatCurrency, formatDate, truncate, formatStatus } from '../utils/table';
|
||||
|
||||
export function createExpensesCommand(): Command {
|
||||
const command = new Command('expenses')
|
||||
.description('Manage expenses');
|
||||
|
||||
command
|
||||
.command('list')
|
||||
.description('List all expenses')
|
||||
.option('-l, --limit <number>', 'Limit number of results per page', '50')
|
||||
.option('-p, --page <number>', 'Page number', '1')
|
||||
.option('--active-only', 'Show only active expenses', false)
|
||||
.action(async (options) => {
|
||||
const spinner = ora('Loading expenses...').start();
|
||||
|
||||
try {
|
||||
const fetcher = createAuthenticatedFetcher();
|
||||
|
||||
const query = {
|
||||
page: parseInt(options.page, 10),
|
||||
pageSize: Math.min(parseInt(options.limit, 10), 100),
|
||||
...(options.activeOnly && { active: true }),
|
||||
};
|
||||
|
||||
const response = await fetchExpenses(fetcher, query);
|
||||
|
||||
spinner.stop();
|
||||
|
||||
const expenses = (response as unknown as { expenses: Array<{
|
||||
id: number;
|
||||
paymentDate?: string;
|
||||
description?: string;
|
||||
total?: number;
|
||||
status?: string;
|
||||
active?: boolean;
|
||||
}> }).expenses;
|
||||
|
||||
if (!expenses || expenses.length === 0) {
|
||||
console.log(chalk.yellow('No expenses found.'));
|
||||
return;
|
||||
}
|
||||
|
||||
const pagination = (response as unknown as {
|
||||
pagination?: { total: number; page: number; pageSize?: number; page_size?: number }
|
||||
}).pagination;
|
||||
if (pagination) {
|
||||
const pageSize = pagination.pageSize || pagination.page_size || 10;
|
||||
const totalPages = Math.ceil(pagination.total / pageSize);
|
||||
console.log(chalk.gray(`\nPage ${pagination.page} of ${totalPages} (${pagination.total} total expenses)\n`));
|
||||
}
|
||||
|
||||
const table = createTable(['ID', 'Date', 'Description', 'Total', 'Status']);
|
||||
|
||||
expenses.forEach((expense) => {
|
||||
table.push([
|
||||
expense.id,
|
||||
formatDate(expense.paymentDate),
|
||||
truncate(expense.description, 35),
|
||||
formatCurrency(expense.total),
|
||||
formatStatus(expense.status),
|
||||
]);
|
||||
});
|
||||
|
||||
console.log(table.toString());
|
||||
|
||||
if (pagination) {
|
||||
const pageSize = pagination.pageSize || pagination.page_size || 10;
|
||||
const totalPages = Math.ceil(pagination.total / pageSize);
|
||||
if (pagination.page < totalPages) {
|
||||
console.log(chalk.gray(`\nUse --page ${pagination.page + 1} to see more results`));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
spinner.stop();
|
||||
handleError(error);
|
||||
}
|
||||
});
|
||||
|
||||
return command;
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
import { Command } from 'commander';
|
||||
import chalk from 'chalk';
|
||||
import ora from 'ora';
|
||||
import { fetchInventoryAdjustments, fetchWarehouseTransfers } from '@bigcapital/sdk-ts';
|
||||
import { createAuthenticatedFetcher } from '../config';
|
||||
import { handleError } from '../utils/errors';
|
||||
import { createTable, formatDate, formatStatus, truncate } from '../utils/table';
|
||||
|
||||
export function createInventoryCommand(): Command {
|
||||
const command = new Command('inventory')
|
||||
.description('Manage inventory');
|
||||
|
||||
command
|
||||
.command('adjustments')
|
||||
.description('List inventory adjustments')
|
||||
.option('-l, --limit <number>', 'Limit number of results per page', '50')
|
||||
.option('-p, --page <number>', 'Page number', '1')
|
||||
.action(async (options) => {
|
||||
const spinner = ora('Loading inventory adjustments...').start();
|
||||
|
||||
try {
|
||||
const fetcher = createAuthenticatedFetcher();
|
||||
|
||||
const query = {
|
||||
page: parseInt(options.page, 10),
|
||||
pageSize: Math.min(parseInt(options.limit, 10), 100),
|
||||
};
|
||||
|
||||
const response = await fetchInventoryAdjustments(fetcher, query);
|
||||
|
||||
spinner.stop();
|
||||
|
||||
const adjustments = (response as unknown as { inventoryAdjustments: Array<{
|
||||
id: number;
|
||||
adjustmentDate?: string;
|
||||
referenceNo?: string;
|
||||
reason?: string;
|
||||
status?: string;
|
||||
}> }).inventoryAdjustments;
|
||||
|
||||
if (!adjustments || adjustments.length === 0) {
|
||||
console.log(chalk.yellow('No inventory adjustments found.'));
|
||||
return;
|
||||
}
|
||||
|
||||
const pagination = (response as unknown as {
|
||||
pagination?: { total: number; page: number; pageSize?: number; page_size?: number }
|
||||
}).pagination;
|
||||
if (pagination) {
|
||||
const pageSize = pagination.pageSize || pagination.page_size || 10;
|
||||
const totalPages = Math.ceil(pagination.total / pageSize);
|
||||
console.log(chalk.gray(`\nPage ${pagination.page} of ${totalPages} (${pagination.total} total adjustments)\n`));
|
||||
}
|
||||
|
||||
const table = createTable(['ID', 'Date', 'Reference', 'Reason', 'Status']);
|
||||
|
||||
adjustments.forEach((adj) => {
|
||||
table.push([
|
||||
adj.id,
|
||||
formatDate(adj.adjustmentDate),
|
||||
adj.referenceNo || '-',
|
||||
truncate(adj.reason, 30),
|
||||
formatStatus(adj.status),
|
||||
]);
|
||||
});
|
||||
|
||||
console.log(table.toString());
|
||||
|
||||
if (pagination) {
|
||||
const pageSize = pagination.pageSize || pagination.page_size || 10;
|
||||
const totalPages = Math.ceil(pagination.total / pageSize);
|
||||
if (pagination.page < totalPages) {
|
||||
console.log(chalk.gray(`\nUse --page ${pagination.page + 1} to see more results`));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
spinner.stop();
|
||||
handleError(error);
|
||||
}
|
||||
});
|
||||
|
||||
command
|
||||
.command('transfers')
|
||||
.description('List warehouse transfers')
|
||||
.option('-l, --limit <number>', 'Limit number of results per page', '50')
|
||||
.option('-p, --page <number>', 'Page number', '1')
|
||||
.action(async (options) => {
|
||||
const spinner = ora('Loading warehouse transfers...').start();
|
||||
|
||||
try {
|
||||
const fetcher = createAuthenticatedFetcher();
|
||||
|
||||
const query = {
|
||||
page: parseInt(options.page, 10),
|
||||
pageSize: Math.min(parseInt(options.limit, 10), 100),
|
||||
};
|
||||
|
||||
const response = await fetchWarehouseTransfers(fetcher, query);
|
||||
|
||||
spinner.stop();
|
||||
|
||||
const transfers = (response as unknown as { warehouseTransfers: Array<{
|
||||
id: number;
|
||||
date?: string;
|
||||
referenceNo?: string;
|
||||
fromWarehouse?: { name?: string };
|
||||
toWarehouse?: { name?: string };
|
||||
status?: string;
|
||||
}> }).warehouseTransfers;
|
||||
|
||||
if (!transfers || transfers.length === 0) {
|
||||
console.log(chalk.yellow('No warehouse transfers found.'));
|
||||
return;
|
||||
}
|
||||
|
||||
const pagination = (response as unknown as {
|
||||
pagination?: { total: number; page: number; pageSize?: number; page_size?: number }
|
||||
}).pagination;
|
||||
if (pagination) {
|
||||
const pageSize = pagination.pageSize || pagination.page_size || 10;
|
||||
const totalPages = Math.ceil(pagination.total / pageSize);
|
||||
console.log(chalk.gray(`\nPage ${pagination.page} of ${totalPages} (${pagination.total} total transfers)\n`));
|
||||
}
|
||||
|
||||
const table = createTable(['ID', 'Date', 'Reference', 'From', 'To', 'Status']);
|
||||
|
||||
transfers.forEach((transfer) => {
|
||||
table.push([
|
||||
transfer.id,
|
||||
formatDate(transfer.date),
|
||||
transfer.referenceNo || '-',
|
||||
truncate(transfer.fromWarehouse?.name, 15),
|
||||
truncate(transfer.toWarehouse?.name, 15),
|
||||
formatStatus(transfer.status),
|
||||
]);
|
||||
});
|
||||
|
||||
console.log(table.toString());
|
||||
|
||||
if (pagination) {
|
||||
const pageSize = pagination.pageSize || pagination.page_size || 10;
|
||||
const totalPages = Math.ceil(pagination.total / pageSize);
|
||||
if (pagination.page < totalPages) {
|
||||
console.log(chalk.gray(`\nUse --page ${pagination.page + 1} to see more results`));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
spinner.stop();
|
||||
handleError(error);
|
||||
}
|
||||
});
|
||||
|
||||
return command;
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import { Command } from 'commander';
|
||||
import chalk from 'chalk';
|
||||
import ora from 'ora';
|
||||
import { fetchSaleInvoices } from '@bigcapital/sdk-ts';
|
||||
import { createAuthenticatedFetcher } from '../config';
|
||||
import { handleError } from '../utils/errors';
|
||||
import { createTable, formatCurrency, formatDate, truncate, formatStatus } from '../utils/table';
|
||||
|
||||
export function createInvoicesCommand(): Command {
|
||||
const command = new Command('invoices')
|
||||
.description('Manage sale invoices');
|
||||
|
||||
command
|
||||
.command('list')
|
||||
.description('List all sale invoices')
|
||||
.option('-l, --limit <number>', 'Limit number of results per page', '50')
|
||||
.option('-p, --page <number>', 'Page number', '1')
|
||||
.option('-c, --customer <id>', 'Filter by customer ID')
|
||||
.option('-s, --status <status>', 'Filter by status (draft, published, paid, partial, overdue)')
|
||||
.action(async (options) => {
|
||||
const spinner = ora('Loading invoices...').start();
|
||||
|
||||
try {
|
||||
const fetcher = createAuthenticatedFetcher();
|
||||
|
||||
const query = {
|
||||
page: parseInt(options.page, 10),
|
||||
pageSize: Math.min(parseInt(options.limit, 10), 100),
|
||||
...(options.customer && { customerId: parseInt(options.customer, 10) }),
|
||||
...(options.status && { status: options.status }),
|
||||
};
|
||||
|
||||
const response = await fetchSaleInvoices(fetcher, query);
|
||||
|
||||
spinner.stop();
|
||||
|
||||
// The response has 'salesInvoices' array (camelCase from middleware), not 'data'
|
||||
const invoices = (response as unknown as { salesInvoices: Array<{
|
||||
id: number;
|
||||
invoiceNo?: string;
|
||||
customer?: { displayName?: string };
|
||||
invoiceDate?: string;
|
||||
total?: number;
|
||||
balance?: number;
|
||||
status?: string;
|
||||
}> }).salesInvoices;
|
||||
|
||||
if (!invoices || invoices.length === 0) {
|
||||
console.log(chalk.yellow('No invoices found.'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Pagination info
|
||||
const pagination = (response as unknown as {
|
||||
pagination?: { total: number; page: number; pageSize?: number; page_size?: number }
|
||||
}).pagination;
|
||||
if (pagination) {
|
||||
const pageSize = pagination.pageSize || pagination.page_size || 10;
|
||||
const totalPages = Math.ceil(pagination.total / pageSize);
|
||||
console.log(chalk.gray(`\nPage ${pagination.page} of ${totalPages} (${pagination.total} total invoices)\n`));
|
||||
}
|
||||
|
||||
// Create table
|
||||
const table = createTable(['ID', 'Invoice #', 'Customer', 'Date', 'Total', 'Balance', 'Status']);
|
||||
|
||||
invoices.forEach((invoice) => {
|
||||
table.push([
|
||||
invoice.id,
|
||||
invoice.invoiceNo || '-',
|
||||
truncate(invoice.customer?.displayName, 25),
|
||||
formatDate(invoice.invoiceDate),
|
||||
formatCurrency(invoice.total),
|
||||
formatCurrency(invoice.balance),
|
||||
formatStatus(invoice.status),
|
||||
]);
|
||||
});
|
||||
|
||||
console.log(table.toString());
|
||||
|
||||
if (pagination) {
|
||||
const pageSize = pagination.pageSize || pagination.page_size || 10;
|
||||
const totalPages = Math.ceil(pagination.total / pageSize);
|
||||
if (pagination.page < totalPages) {
|
||||
console.log(chalk.gray(`\nUse --page ${pagination.page + 1} to see more results`));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
spinner.stop();
|
||||
handleError(error);
|
||||
}
|
||||
});
|
||||
|
||||
return command;
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import { Command } from 'commander';
|
||||
import chalk from 'chalk';
|
||||
import ora from 'ora';
|
||||
import { fetchItems } from '@bigcapital/sdk-ts';
|
||||
import { createAuthenticatedFetcher } from '../config';
|
||||
import { handleError } from '../utils/errors';
|
||||
import { createTable, formatCurrency, truncate, formatStatus } from '../utils/table';
|
||||
|
||||
export function createItemsCommand(): Command {
|
||||
const command = new Command('items')
|
||||
.description('Manage items and products');
|
||||
|
||||
command
|
||||
.command('list')
|
||||
.description('List all items/products')
|
||||
.option('-l, --limit <number>', 'Limit number of results per page', '50')
|
||||
.option('-p, --page <number>', 'Page number', '1')
|
||||
.option('-t, --type <type>', 'Filter by item type (inventory, service, product)')
|
||||
.option('--active-only', 'Show only active items', false)
|
||||
.action(async (options) => {
|
||||
const spinner = ora('Loading items...').start();
|
||||
|
||||
try {
|
||||
const fetcher = createAuthenticatedFetcher();
|
||||
|
||||
const query = {
|
||||
page: parseInt(options.page, 10),
|
||||
pageSize: Math.min(parseInt(options.limit, 10), 100),
|
||||
...(options.type && { type: options.type }),
|
||||
...(options.activeOnly && { active: true }),
|
||||
};
|
||||
|
||||
const response = await fetchItems(fetcher, query);
|
||||
|
||||
spinner.stop();
|
||||
|
||||
// The response has 'items' array, not 'data'
|
||||
const items = (response as unknown as { items: Array<{
|
||||
id: number;
|
||||
name?: string;
|
||||
type?: string;
|
||||
sellPrice?: number;
|
||||
costPrice?: number;
|
||||
quantityOnHand?: number;
|
||||
active?: boolean;
|
||||
}> }).items;
|
||||
|
||||
if (!items || items.length === 0) {
|
||||
console.log(chalk.yellow('No items found.'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Pagination info
|
||||
const pagination = (response as unknown as {
|
||||
pagination?: { total: number; page: number; pageSize?: number; page_size?: number }
|
||||
}).pagination;
|
||||
if (pagination) {
|
||||
const pageSize = pagination.pageSize || pagination.page_size || 10;
|
||||
const totalPages = Math.ceil(pagination.total / pageSize);
|
||||
console.log(chalk.gray(`\nPage ${pagination.page} of ${totalPages} (${pagination.total} total items)\n`));
|
||||
}
|
||||
|
||||
// Create table
|
||||
const table = createTable(['ID', 'Name', 'Type', 'Sell Price', 'Cost Price', 'Qty', 'Status']);
|
||||
|
||||
items.forEach((item) => {
|
||||
table.push([
|
||||
item.id,
|
||||
truncate(item.name, 30),
|
||||
item.type || '-',
|
||||
formatCurrency(item.sellPrice),
|
||||
formatCurrency(item.costPrice),
|
||||
item.quantityOnHand ?? '-',
|
||||
formatStatus(item.active ? 'active' : 'inactive'),
|
||||
]);
|
||||
});
|
||||
|
||||
console.log(table.toString());
|
||||
|
||||
if (pagination) {
|
||||
const pageSize = pagination.pageSize || pagination.page_size || 10;
|
||||
const totalPages = Math.ceil(pagination.total / pageSize);
|
||||
if (pagination.page < totalPages) {
|
||||
console.log(chalk.gray(`\nUse --page ${pagination.page + 1} to see more results`));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
spinner.stop();
|
||||
handleError(error);
|
||||
}
|
||||
});
|
||||
|
||||
return command;
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { Command } from 'commander';
|
||||
import chalk from 'chalk';
|
||||
import ora from 'ora';
|
||||
import { fetchManualJournals } from '@bigcapital/sdk-ts';
|
||||
import { createAuthenticatedFetcher } from '../config';
|
||||
import { handleError } from '../utils/errors';
|
||||
import { createTable, formatDate, truncate, formatStatus } from '../utils/table';
|
||||
|
||||
export function createJournalsCommand(): Command {
|
||||
const command = new Command('journals')
|
||||
.description('Manage manual journals');
|
||||
|
||||
command
|
||||
.command('list')
|
||||
.description('List all manual journals')
|
||||
.option('-l, --limit <number>', 'Limit number of results per page', '50')
|
||||
.option('-p, --page <number>', 'Page number', '1')
|
||||
.action(async (options) => {
|
||||
const spinner = ora('Loading journals...').start();
|
||||
|
||||
try {
|
||||
const fetcher = createAuthenticatedFetcher();
|
||||
|
||||
const response = await fetchManualJournals(fetcher, {} as never);
|
||||
|
||||
spinner.stop();
|
||||
|
||||
const journals = (response as unknown as { manualJournals: Array<{
|
||||
id: number;
|
||||
journalNumber?: string;
|
||||
date?: string;
|
||||
reference?: string;
|
||||
description?: string;
|
||||
status?: string;
|
||||
amount?: number;
|
||||
}> }).manualJournals;
|
||||
|
||||
if (!journals || journals.length === 0) {
|
||||
console.log(chalk.yellow('No manual journals found.'));
|
||||
return;
|
||||
}
|
||||
|
||||
const pagination = (response as unknown as {
|
||||
pagination?: { total: number; page: number; pageSize?: number; page_size?: number }
|
||||
}).pagination;
|
||||
if (pagination) {
|
||||
const pageSize = pagination.pageSize || pagination.page_size || 10;
|
||||
const totalPages = Math.ceil(pagination.total / pageSize);
|
||||
console.log(chalk.gray(`\nPage ${pagination.page} of ${totalPages} (${pagination.total} total journals)\n`));
|
||||
}
|
||||
|
||||
const table = createTable(['ID', 'Journal #', 'Date', 'Reference', 'Description', 'Amount', 'Status']);
|
||||
|
||||
journals.forEach((journal) => {
|
||||
table.push([
|
||||
journal.id,
|
||||
journal.journalNumber || '-',
|
||||
formatDate(journal.date),
|
||||
journal.reference || '-',
|
||||
truncate(journal.description, 25),
|
||||
journal.amount?.toFixed(2) || '-',
|
||||
formatStatus(journal.status),
|
||||
]);
|
||||
});
|
||||
|
||||
console.log(table.toString());
|
||||
|
||||
if (pagination) {
|
||||
const pageSize = pagination.pageSize || pagination.page_size || 10;
|
||||
const totalPages = Math.ceil(pagination.total / pageSize);
|
||||
if (pagination.page < totalPages) {
|
||||
console.log(chalk.gray(`\nUse --page ${pagination.page + 1} to see more results`));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
spinner.stop();
|
||||
handleError(error);
|
||||
}
|
||||
});
|
||||
|
||||
return command;
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import { Command } from 'commander';
|
||||
import chalk from 'chalk';
|
||||
import ora from 'ora';
|
||||
import { fetchPaymentsReceived, fetchBillPayments } from '@bigcapital/sdk-ts';
|
||||
import { createAuthenticatedFetcher } from '../config';
|
||||
import { handleError } from '../utils/errors';
|
||||
import { createTable, formatCurrency, formatDate, truncate } from '../utils/table';
|
||||
|
||||
export function createPaymentsCommand(): Command {
|
||||
const command = new Command('payments')
|
||||
.description('Manage payments');
|
||||
|
||||
command
|
||||
.command('received')
|
||||
.description('List payments received from customers')
|
||||
.action(async () => {
|
||||
const spinner = ora('Loading payments received...').start();
|
||||
|
||||
try {
|
||||
const fetcher = createAuthenticatedFetcher();
|
||||
const response = await fetchPaymentsReceived(fetcher);
|
||||
|
||||
spinner.stop();
|
||||
|
||||
const payments = (response as unknown as { paymentsReceived: Array<{
|
||||
id: number;
|
||||
customer?: { displayName?: string };
|
||||
paymentDate?: string;
|
||||
amount?: number;
|
||||
referenceNo?: string;
|
||||
}> }).paymentsReceived;
|
||||
|
||||
if (!payments || payments.length === 0) {
|
||||
console.log(chalk.yellow('No payments received found.'));
|
||||
return;
|
||||
}
|
||||
|
||||
const table = createTable(['ID', 'Customer', 'Date', 'Amount', 'Reference']);
|
||||
|
||||
payments.forEach((payment) => {
|
||||
table.push([
|
||||
payment.id,
|
||||
truncate(payment.customer?.displayName, 25),
|
||||
formatDate(payment.paymentDate),
|
||||
formatCurrency(payment.amount),
|
||||
payment.referenceNo || '-',
|
||||
]);
|
||||
});
|
||||
|
||||
console.log(table.toString());
|
||||
} catch (error) {
|
||||
spinner.stop();
|
||||
handleError(error);
|
||||
}
|
||||
});
|
||||
|
||||
command
|
||||
.command('made')
|
||||
.description('List bill payments made to vendors')
|
||||
.action(async () => {
|
||||
const spinner = ora('Loading payments made...').start();
|
||||
|
||||
try {
|
||||
const fetcher = createAuthenticatedFetcher();
|
||||
const response = await fetchBillPayments(fetcher);
|
||||
|
||||
spinner.stop();
|
||||
|
||||
const payments = (response as unknown as { billPayments: Array<{
|
||||
id: number;
|
||||
vendor?: { displayName?: string };
|
||||
paymentDate?: string;
|
||||
amount?: number;
|
||||
referenceNo?: string;
|
||||
}> }).billPayments;
|
||||
|
||||
if (!payments || payments.length === 0) {
|
||||
console.log(chalk.yellow('No payments made found.'));
|
||||
return;
|
||||
}
|
||||
|
||||
const table = createTable(['ID', 'Vendor', 'Date', 'Amount', 'Reference']);
|
||||
|
||||
payments.forEach((payment) => {
|
||||
table.push([
|
||||
payment.id,
|
||||
truncate(payment.vendor?.displayName, 25),
|
||||
formatDate(payment.paymentDate),
|
||||
formatCurrency(payment.amount),
|
||||
payment.referenceNo || '-',
|
||||
]);
|
||||
});
|
||||
|
||||
console.log(table.toString());
|
||||
} catch (error) {
|
||||
spinner.stop();
|
||||
handleError(error);
|
||||
}
|
||||
});
|
||||
|
||||
return command;
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import { Command } from 'commander';
|
||||
import chalk from 'chalk';
|
||||
import ora from 'ora';
|
||||
import { fetchSaleReceipts } from '@bigcapital/sdk-ts';
|
||||
import { createAuthenticatedFetcher } from '../config';
|
||||
import { handleError } from '../utils/errors';
|
||||
import { createTable, formatCurrency, formatDate, truncate, formatStatus } from '../utils/table';
|
||||
|
||||
export function createReceiptsCommand(): Command {
|
||||
const command = new Command('receipts')
|
||||
.description('Manage sale receipts');
|
||||
|
||||
command
|
||||
.command('list')
|
||||
.description('List all sale receipts')
|
||||
.option('-l, --limit <number>', 'Limit number of results per page', '50')
|
||||
.option('-p, --page <number>', 'Page number', '1')
|
||||
.option('-c, --customer <id>', 'Filter by customer ID')
|
||||
.action(async (options) => {
|
||||
const spinner = ora('Loading receipts...').start();
|
||||
|
||||
try {
|
||||
const fetcher = createAuthenticatedFetcher();
|
||||
|
||||
const query = {
|
||||
page: parseInt(options.page, 10),
|
||||
pageSize: Math.min(parseInt(options.limit, 10), 100),
|
||||
...(options.customer && { customerId: parseInt(options.customer, 10) }),
|
||||
};
|
||||
|
||||
const response = await fetchSaleReceipts(fetcher, query);
|
||||
|
||||
spinner.stop();
|
||||
|
||||
const receipts = (response as unknown as { saleReceipts: Array<{
|
||||
id: number;
|
||||
receiptNumber?: string;
|
||||
customer?: { displayName?: string };
|
||||
receiptDate?: string;
|
||||
total?: number;
|
||||
status?: string;
|
||||
}> }).saleReceipts;
|
||||
|
||||
if (!receipts || receipts.length === 0) {
|
||||
console.log(chalk.yellow('No receipts found.'));
|
||||
return;
|
||||
}
|
||||
|
||||
const pagination = (response as unknown as {
|
||||
pagination?: { total: number; page: number; pageSize?: number; page_size?: number }
|
||||
}).pagination;
|
||||
if (pagination) {
|
||||
const pageSize = pagination.pageSize || pagination.page_size || 10;
|
||||
const totalPages = Math.ceil(pagination.total / pageSize);
|
||||
console.log(chalk.gray(`\nPage ${pagination.page} of ${totalPages} (${pagination.total} total receipts)\n`));
|
||||
}
|
||||
|
||||
const table = createTable(['ID', 'Receipt #', 'Customer', 'Date', 'Total', 'Status']);
|
||||
|
||||
receipts.forEach((receipt) => {
|
||||
table.push([
|
||||
receipt.id,
|
||||
receipt.receiptNumber || '-',
|
||||
truncate(receipt.customer?.displayName, 20),
|
||||
formatDate(receipt.receiptDate),
|
||||
formatCurrency(receipt.total),
|
||||
formatStatus(receipt.status),
|
||||
]);
|
||||
});
|
||||
|
||||
console.log(table.toString());
|
||||
|
||||
if (pagination) {
|
||||
const pageSize = pagination.pageSize || pagination.page_size || 10;
|
||||
const totalPages = Math.ceil(pagination.total / pageSize);
|
||||
if (pagination.page < totalPages) {
|
||||
console.log(chalk.gray(`\nUse --page ${pagination.page + 1} to see more results`));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
spinner.stop();
|
||||
handleError(error);
|
||||
}
|
||||
});
|
||||
|
||||
return command;
|
||||
}
|
||||
@@ -0,0 +1,513 @@
|
||||
import { Command } from 'commander';
|
||||
import chalk from 'chalk';
|
||||
import ora from 'ora';
|
||||
import {
|
||||
fetchBalanceSheetJson,
|
||||
fetchProfitLossJson,
|
||||
fetchCashflowStatementJson,
|
||||
fetchTrialBalanceJson,
|
||||
fetchGeneralLedgerJson,
|
||||
fetchJournalJson,
|
||||
fetchReceivableAgingJson,
|
||||
fetchPayableAgingJson,
|
||||
fetchCustomerBalanceJson,
|
||||
fetchVendorBalanceJson,
|
||||
fetchSalesByItemsJson,
|
||||
fetchPurchasesByItemsJson,
|
||||
fetchInventoryValuationJson,
|
||||
fetchInventoryItemDetailsJson,
|
||||
fetchSalesTaxLiabilityJson,
|
||||
} from '@bigcapital/sdk-ts';
|
||||
import { createAuthenticatedFetcher } from '../config';
|
||||
import { handleError } from '../utils/errors';
|
||||
import { createTable, formatCurrency } from '../utils/table';
|
||||
|
||||
interface DateRangeOptions {
|
||||
from?: string;
|
||||
to?: string;
|
||||
}
|
||||
|
||||
function getDateRange(options: DateRangeOptions): { fromDate: string; toDate: string } {
|
||||
const toDate = options.to || new Date().toISOString().split('T')[0];
|
||||
const fromDate = options.from || new Date(Date.now() - 365 * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
|
||||
return { fromDate, toDate };
|
||||
}
|
||||
|
||||
export function createReportsCommand(): Command {
|
||||
const command = new Command('reports')
|
||||
.description('Financial reports');
|
||||
|
||||
// Balance Sheet
|
||||
command
|
||||
.command('balance-sheet')
|
||||
.description('Show balance sheet')
|
||||
.option('--from <date>', 'Start date (YYYY-MM-DD)')
|
||||
.option('--to <date>', 'End date (YYYY-MM-DD)')
|
||||
.action(async (options: DateRangeOptions) => {
|
||||
const spinner = ora('Loading balance sheet...').start();
|
||||
try {
|
||||
const fetcher = createAuthenticatedFetcher();
|
||||
const { fromDate, toDate } = getDateRange(options);
|
||||
const report = await fetchBalanceSheetJson(fetcher, { fromDate, toDate } as never);
|
||||
spinner.stop();
|
||||
|
||||
console.log(chalk.bold('\n📊 Balance Sheet'));
|
||||
console.log(chalk.gray(`${fromDate} to ${toDate}\n`));
|
||||
|
||||
// Assets
|
||||
console.log(chalk.green.bold('\nASSETS'));
|
||||
const assetsTable = createTable(['Account', 'Amount']);
|
||||
let totalAssets = 0;
|
||||
(report as unknown as { assets?: Array<{ accountName: string; total: number }> }).assets?.forEach((item) => {
|
||||
assetsTable.push([item.accountName, formatCurrency(item.total)]);
|
||||
totalAssets += item.total;
|
||||
});
|
||||
assetsTable.push(['', '']);
|
||||
assetsTable.push([chalk.bold('Total Assets'), chalk.bold(formatCurrency(totalAssets))]);
|
||||
console.log(assetsTable.toString());
|
||||
|
||||
// Liabilities
|
||||
console.log(chalk.red.bold('\nLIABILITIES'));
|
||||
const liabilitiesTable = createTable(['Account', 'Amount']);
|
||||
let totalLiabilities = 0;
|
||||
(report as unknown as { liabilities?: Array<{ accountName: string; total: number }> }).liabilities?.forEach((item) => {
|
||||
liabilitiesTable.push([item.accountName, formatCurrency(item.total)]);
|
||||
totalLiabilities += item.total;
|
||||
});
|
||||
liabilitiesTable.push(['', '']);
|
||||
liabilitiesTable.push([chalk.bold('Total Liabilities'), chalk.bold(formatCurrency(totalLiabilities))]);
|
||||
console.log(liabilitiesTable.toString());
|
||||
|
||||
// Equity
|
||||
console.log(chalk.blue.bold('\nEQUITY'));
|
||||
const equityTable = createTable(['Account', 'Amount']);
|
||||
let totalEquity = 0;
|
||||
(report as unknown as { equity?: Array<{ accountName: string; total: number }> }).equity?.forEach((item) => {
|
||||
equityTable.push([item.accountName, formatCurrency(item.total)]);
|
||||
totalEquity += item.total;
|
||||
});
|
||||
equityTable.push(['', '']);
|
||||
equityTable.push([chalk.bold('Total Equity'), chalk.bold(formatCurrency(totalEquity))]);
|
||||
console.log(equityTable.toString());
|
||||
|
||||
// Summary
|
||||
console.log(chalk.bold('\n' + '─'.repeat(40)));
|
||||
console.log(`${chalk.bold('Total Liabilities + Equity:')} ${formatCurrency(totalLiabilities + totalEquity)}`);
|
||||
console.log(`${chalk.bold('Balance:')} ${totalAssets === (totalLiabilities + totalEquity) ? chalk.green('✓ Balanced') : chalk.red('✗ Unbalanced')}\n`);
|
||||
} catch (error) {
|
||||
spinner.stop();
|
||||
handleError(error);
|
||||
}
|
||||
});
|
||||
|
||||
// Profit & Loss
|
||||
command
|
||||
.command('profit-loss')
|
||||
.alias('pl')
|
||||
.description('Show profit and loss statement')
|
||||
.option('--from <date>', 'Start date (YYYY-MM-DD)')
|
||||
.option('--to <date>', 'End date (YYYY-MM-DD)')
|
||||
.action(async (options: DateRangeOptions) => {
|
||||
const spinner = ora('Loading profit & loss...').start();
|
||||
try {
|
||||
const fetcher = createAuthenticatedFetcher();
|
||||
const { fromDate, toDate } = getDateRange(options);
|
||||
const report = await fetchProfitLossJson(fetcher, { fromDate, toDate } as never);
|
||||
spinner.stop();
|
||||
|
||||
console.log(chalk.bold('\n📈 Profit & Loss Statement'));
|
||||
console.log(chalk.gray(`${fromDate} to ${toDate}\n`));
|
||||
|
||||
const income = (report as unknown as { income?: { total: number } }).income?.total || 0;
|
||||
const expenses = (report as unknown as { expenses?: { total: number } }).expenses?.total || 0;
|
||||
const netProfit = income - expenses;
|
||||
|
||||
const table = createTable(['', 'Amount']);
|
||||
table.push([chalk.green('Total Income'), formatCurrency(income)]);
|
||||
table.push([chalk.red('Total Expenses'), formatCurrency(expenses)]);
|
||||
table.push(['', '']);
|
||||
table.push([netProfit >= 0 ? chalk.green.bold('Net Profit') : chalk.red.bold('Net Loss'), formatCurrency(Math.abs(netProfit))]);
|
||||
|
||||
console.log(table.toString() + '\n');
|
||||
} catch (error) {
|
||||
spinner.stop();
|
||||
handleError(error);
|
||||
}
|
||||
});
|
||||
|
||||
// Cashflow
|
||||
command
|
||||
.command('cashflow')
|
||||
.description('Show cashflow statement')
|
||||
.option('--from <date>', 'Start date (YYYY-MM-DD)')
|
||||
.option('--to <date>', 'End date (YYYY-MM-DD)')
|
||||
.action(async (options: DateRangeOptions) => {
|
||||
const spinner = ora('Loading cashflow statement...').start();
|
||||
try {
|
||||
const fetcher = createAuthenticatedFetcher();
|
||||
const { fromDate, toDate } = getDateRange(options);
|
||||
const report = await fetchCashflowStatementJson(fetcher, { fromDate, toDate } as never);
|
||||
spinner.stop();
|
||||
|
||||
console.log(chalk.bold('\n💰 Cashflow Statement'));
|
||||
console.log(chalk.gray(`${fromDate} to ${toDate}\n`));
|
||||
|
||||
const operating = (report as unknown as { operating?: { total: number } }).operating?.total || 0;
|
||||
const investing = (report as unknown as { investing?: { total: number } }).investing?.total || 0;
|
||||
const financing = (report as unknown as { financing?: { total: number } }).financing?.total || 0;
|
||||
const netCash = operating + investing + financing;
|
||||
|
||||
const table = createTable(['Activity', 'Amount']);
|
||||
table.push(['Operating Activities', formatCurrency(operating)]);
|
||||
table.push(['Investing Activities', formatCurrency(investing)]);
|
||||
table.push(['Financing Activities', formatCurrency(financing)]);
|
||||
table.push(['', '']);
|
||||
table.push([chalk.bold('Net Cash Change'), chalk.bold(formatCurrency(netCash))]);
|
||||
|
||||
console.log(table.toString() + '\n');
|
||||
} catch (error) {
|
||||
spinner.stop();
|
||||
handleError(error);
|
||||
}
|
||||
});
|
||||
|
||||
// Trial Balance
|
||||
command
|
||||
.command('trial-balance')
|
||||
.description('Show trial balance')
|
||||
.option('--from <date>', 'Start date (YYYY-MM-DD)')
|
||||
.option('--to <date>', 'End date (YYYY-MM-DD)')
|
||||
.action(async (options: DateRangeOptions) => {
|
||||
const spinner = ora('Loading trial balance...').start();
|
||||
try {
|
||||
const fetcher = createAuthenticatedFetcher();
|
||||
const { fromDate, toDate } = getDateRange(options);
|
||||
const report = await fetchTrialBalanceJson(fetcher, {} as never);
|
||||
spinner.stop();
|
||||
|
||||
console.log(chalk.bold('\n⚖️ Trial Balance'));
|
||||
console.log(chalk.gray(`${fromDate} to ${toDate}\n`));
|
||||
|
||||
const table = createTable(['Account', 'Debit', 'Credit']);
|
||||
let totalDebit = 0;
|
||||
let totalCredit = 0;
|
||||
|
||||
(report as unknown as { data?: Array<{ accountName: string; debit: number; credit: number }> }).data?.forEach((item) => {
|
||||
table.push([item.accountName, formatCurrency(item.debit), formatCurrency(item.credit)]);
|
||||
totalDebit += item.debit;
|
||||
totalCredit += item.credit;
|
||||
});
|
||||
|
||||
table.push(['', '', '']);
|
||||
table.push([chalk.bold('Total'), chalk.bold(formatCurrency(totalDebit)), chalk.bold(formatCurrency(totalCredit))]);
|
||||
|
||||
console.log(table.toString());
|
||||
console.log(`\n${chalk.bold('Balance:')} ${Math.abs(totalDebit - totalCredit) < 0.01 ? chalk.green('✓ Balanced') : chalk.red('✗ Unbalanced')}\n`);
|
||||
} catch (error) {
|
||||
spinner.stop();
|
||||
handleError(error);
|
||||
}
|
||||
});
|
||||
|
||||
// General Ledger
|
||||
command
|
||||
.command('general-ledger')
|
||||
.description('Show general ledger')
|
||||
.option('--from <date>', 'Start date (YYYY-MM-DD)')
|
||||
.option('--to <date>', 'End date (YYYY-MM-DD)')
|
||||
.action(async (options: DateRangeOptions) => {
|
||||
const spinner = ora('Loading general ledger...').start();
|
||||
try {
|
||||
const fetcher = createAuthenticatedFetcher();
|
||||
const { fromDate, toDate } = getDateRange(options);
|
||||
const report = await fetchGeneralLedgerJson(fetcher, {} as never);
|
||||
spinner.stop();
|
||||
|
||||
console.log(chalk.bold('\n📒 General Ledger'));
|
||||
console.log(chalk.gray(`${fromDate} to ${toDate}\n`));
|
||||
console.log(JSON.stringify(report, null, 2));
|
||||
} catch (error) {
|
||||
spinner.stop();
|
||||
handleError(error);
|
||||
}
|
||||
});
|
||||
|
||||
// Journal
|
||||
command
|
||||
.command('journal')
|
||||
.description('Show journal report')
|
||||
.option('--from <date>', 'Start date (YYYY-MM-DD)')
|
||||
.option('--to <date>', 'End date (YYYY-MM-DD)')
|
||||
.action(async (options: DateRangeOptions) => {
|
||||
const spinner = ora('Loading journal...').start();
|
||||
try {
|
||||
const fetcher = createAuthenticatedFetcher();
|
||||
const { fromDate, toDate } = getDateRange(options);
|
||||
const report = await fetchJournalJson(fetcher, {} as never);
|
||||
spinner.stop();
|
||||
|
||||
console.log(chalk.bold('\n📝 Journal Report'));
|
||||
console.log(chalk.gray(`${fromDate} to ${toDate}\n`));
|
||||
|
||||
const table = createTable(['Date', 'Reference', 'Account', 'Debit', 'Credit']);
|
||||
(report as unknown as { data?: Array<{ date: string; reference: string; accountName: string; debit: number; credit: number }> }).data?.forEach((item) => {
|
||||
table.push([item.date, item.reference || '-', item.accountName, formatCurrency(item.debit), formatCurrency(item.credit)]);
|
||||
});
|
||||
|
||||
console.log(table.toString() + '\n');
|
||||
} catch (error) {
|
||||
spinner.stop();
|
||||
handleError(error);
|
||||
}
|
||||
});
|
||||
|
||||
// Receivable Aging
|
||||
command
|
||||
.command('receivable-aging')
|
||||
.description('Show accounts receivable aging')
|
||||
.action(async () => {
|
||||
const spinner = ora('Loading receivable aging...').start();
|
||||
try {
|
||||
const fetcher = createAuthenticatedFetcher();
|
||||
const report = await fetchReceivableAgingJson(fetcher, {} as never);
|
||||
spinner.stop();
|
||||
|
||||
console.log(chalk.bold('\n📋 Accounts Receivable Aging\n'));
|
||||
|
||||
const table = createTable(['Customer', 'Current', '1-30', '31-60', '61-90', '90+', 'Total']);
|
||||
(report as unknown as { data?: Array<{ customerName: string; current: number; days1to30: number; days31to60: number; days61to90: number; over90: number; total: number }> }).data?.forEach((item) => {
|
||||
table.push([
|
||||
item.customerName,
|
||||
formatCurrency(item.current),
|
||||
formatCurrency(item.days1to30),
|
||||
formatCurrency(item.days31to60),
|
||||
formatCurrency(item.days61to90),
|
||||
formatCurrency(item.over90),
|
||||
chalk.bold(formatCurrency(item.total)),
|
||||
]);
|
||||
});
|
||||
|
||||
console.log(table.toString() + '\n');
|
||||
} catch (error) {
|
||||
spinner.stop();
|
||||
handleError(error);
|
||||
}
|
||||
});
|
||||
|
||||
// Payable Aging
|
||||
command
|
||||
.command('payable-aging')
|
||||
.description('Show accounts payable aging')
|
||||
.action(async () => {
|
||||
const spinner = ora('Loading payable aging...').start();
|
||||
try {
|
||||
const fetcher = createAuthenticatedFetcher();
|
||||
const report = await fetchPayableAgingJson(fetcher, {} as never);
|
||||
spinner.stop();
|
||||
|
||||
console.log(chalk.bold('\n📋 Accounts Payable Aging\n'));
|
||||
|
||||
const table = createTable(['Vendor', 'Current', '1-30', '31-60', '61-90', '90+', 'Total']);
|
||||
(report as unknown as { data?: Array<{ vendorName: string; current: number; days1to30: number; days31to60: number; days61to90: number; over90: number; total: number }> }).data?.forEach((item) => {
|
||||
table.push([
|
||||
item.vendorName,
|
||||
formatCurrency(item.current),
|
||||
formatCurrency(item.days1to30),
|
||||
formatCurrency(item.days31to60),
|
||||
formatCurrency(item.days61to90),
|
||||
formatCurrency(item.over90),
|
||||
chalk.bold(formatCurrency(item.total)),
|
||||
]);
|
||||
});
|
||||
|
||||
console.log(table.toString() + '\n');
|
||||
} catch (error) {
|
||||
spinner.stop();
|
||||
handleError(error);
|
||||
}
|
||||
});
|
||||
|
||||
// Customer Balance
|
||||
command
|
||||
.command('customer-balance')
|
||||
.description('Show customer balance summary')
|
||||
.action(async () => {
|
||||
const spinner = ora('Loading customer balances...').start();
|
||||
try {
|
||||
const fetcher = createAuthenticatedFetcher();
|
||||
const report = await fetchCustomerBalanceJson(fetcher, {} as never);
|
||||
spinner.stop();
|
||||
|
||||
console.log(chalk.bold('\n👤 Customer Balance Summary\n'));
|
||||
|
||||
const table = createTable(['Customer', 'Total']);
|
||||
(report as unknown as { data?: Array<{ customerName: string; total: number }> }).data?.forEach((item) => {
|
||||
table.push([item.customerName, formatCurrency(item.total)]);
|
||||
});
|
||||
|
||||
console.log(table.toString() + '\n');
|
||||
} catch (error) {
|
||||
spinner.stop();
|
||||
handleError(error);
|
||||
}
|
||||
});
|
||||
|
||||
// Vendor Balance
|
||||
command
|
||||
.command('vendor-balance')
|
||||
.description('Show vendor balance summary')
|
||||
.action(async () => {
|
||||
const spinner = ora('Loading vendor balances...').start();
|
||||
try {
|
||||
const fetcher = createAuthenticatedFetcher();
|
||||
const report = await fetchVendorBalanceJson(fetcher, {} as never);
|
||||
spinner.stop();
|
||||
|
||||
console.log(chalk.bold('\n🏢 Vendor Balance Summary\n'));
|
||||
|
||||
const table = createTable(['Vendor', 'Total']);
|
||||
(report as unknown as { data?: Array<{ vendorName: string; total: number }> }).data?.forEach((item) => {
|
||||
table.push([item.vendorName, formatCurrency(item.total)]);
|
||||
});
|
||||
|
||||
console.log(table.toString() + '\n');
|
||||
} catch (error) {
|
||||
spinner.stop();
|
||||
handleError(error);
|
||||
}
|
||||
});
|
||||
|
||||
// Sales by Items
|
||||
command
|
||||
.command('sales-by-items')
|
||||
.description('Show sales by items report')
|
||||
.option('--from <date>', 'Start date (YYYY-MM-DD)')
|
||||
.option('--to <date>', 'End date (YYYY-MM-DD)')
|
||||
.action(async (options: DateRangeOptions) => {
|
||||
const spinner = ora('Loading sales by items...').start();
|
||||
try {
|
||||
const fetcher = createAuthenticatedFetcher();
|
||||
const { fromDate, toDate } = getDateRange(options);
|
||||
const report = await fetchSalesByItemsJson(fetcher, { fromDate, toDate } as never);
|
||||
spinner.stop();
|
||||
|
||||
console.log(chalk.bold('\n🛍️ Sales by Items'));
|
||||
console.log(chalk.gray(`${fromDate} to ${toDate}\n`));
|
||||
|
||||
const table = createTable(['Item', 'Quantity', 'Amount']);
|
||||
(report as unknown as { data?: Array<{ itemName: string; quantity: number; amount: number }> }).data?.forEach((item) => {
|
||||
table.push([item.itemName, item.quantity.toString(), formatCurrency(item.amount)]);
|
||||
});
|
||||
|
||||
console.log(table.toString() + '\n');
|
||||
} catch (error) {
|
||||
spinner.stop();
|
||||
handleError(error);
|
||||
}
|
||||
});
|
||||
|
||||
// Purchases by Items
|
||||
command
|
||||
.command('purchases-by-items')
|
||||
.description('Show purchases by items report')
|
||||
.option('--from <date>', 'Start date (YYYY-MM-DD)')
|
||||
.option('--to <date>', 'End date (YYYY-MM-DD)')
|
||||
.action(async (options: DateRangeOptions) => {
|
||||
const spinner = ora('Loading purchases by items...').start();
|
||||
try {
|
||||
const fetcher = createAuthenticatedFetcher();
|
||||
const { fromDate, toDate } = getDateRange(options);
|
||||
const report = await fetchPurchasesByItemsJson(fetcher, { fromDate, toDate } as never);
|
||||
spinner.stop();
|
||||
|
||||
console.log(chalk.bold('\n🛒 Purchases by Items'));
|
||||
console.log(chalk.gray(`${fromDate} to ${toDate}\n`));
|
||||
|
||||
const table = createTable(['Item', 'Quantity', 'Amount']);
|
||||
(report as unknown as { data?: Array<{ itemName: string; quantity: number; amount: number }> }).data?.forEach((item) => {
|
||||
table.push([item.itemName, item.quantity.toString(), formatCurrency(item.amount)]);
|
||||
});
|
||||
|
||||
console.log(table.toString() + '\n');
|
||||
} catch (error) {
|
||||
spinner.stop();
|
||||
handleError(error);
|
||||
}
|
||||
});
|
||||
|
||||
// Inventory Valuation
|
||||
command
|
||||
.command('inventory-valuation')
|
||||
.description('Show inventory valuation summary')
|
||||
.action(async () => {
|
||||
const spinner = ora('Loading inventory valuation...').start();
|
||||
try {
|
||||
const fetcher = createAuthenticatedFetcher();
|
||||
const report = await fetchInventoryValuationJson(fetcher, {} as never);
|
||||
spinner.stop();
|
||||
|
||||
console.log(chalk.bold('\n📦 Inventory Valuation\n'));
|
||||
|
||||
const table = createTable(['Item', 'Qty on Hand', 'Avg Cost', 'Total Value']);
|
||||
(report as unknown as { data?: Array<{ itemName: string; quantityOnHand: number; averageCost: number; totalValue: number }> }).data?.forEach((item) => {
|
||||
table.push([item.itemName, item.quantityOnHand.toString(), formatCurrency(item.averageCost), formatCurrency(item.totalValue)]);
|
||||
});
|
||||
|
||||
console.log(table.toString() + '\n');
|
||||
} catch (error) {
|
||||
spinner.stop();
|
||||
handleError(error);
|
||||
}
|
||||
});
|
||||
|
||||
// Inventory Details
|
||||
command
|
||||
.command('inventory-details')
|
||||
.description('Show inventory item details')
|
||||
.option('--item <id>', 'Filter by item ID')
|
||||
.action(async (options: { item?: string }) => {
|
||||
const spinner = ora('Loading inventory details...').start();
|
||||
try {
|
||||
const fetcher = createAuthenticatedFetcher();
|
||||
const report = await fetchInventoryItemDetailsJson(fetcher, {} as never);
|
||||
spinner.stop();
|
||||
|
||||
console.log(chalk.bold('\n📦 Inventory Item Details\n'));
|
||||
console.log(JSON.stringify(report, null, 2));
|
||||
} catch (error) {
|
||||
spinner.stop();
|
||||
handleError(error);
|
||||
}
|
||||
});
|
||||
|
||||
// Sales Tax Liability
|
||||
command
|
||||
.command('sales-tax-liability')
|
||||
.description('Show sales tax liability report')
|
||||
.option('--from <date>', 'Start date (YYYY-MM-DD)')
|
||||
.option('--to <date>', 'End date (YYYY-MM-DD)')
|
||||
.action(async (options: DateRangeOptions) => {
|
||||
const spinner = ora('Loading sales tax liability...').start();
|
||||
try {
|
||||
const fetcher = createAuthenticatedFetcher();
|
||||
const { fromDate, toDate } = getDateRange(options);
|
||||
const report = await fetchSalesTaxLiabilityJson(fetcher, { fromDate, toDate } as never);
|
||||
spinner.stop();
|
||||
|
||||
console.log(chalk.bold('\n💵 Sales Tax Liability'));
|
||||
console.log(chalk.gray(`${fromDate} to ${toDate}\n`));
|
||||
|
||||
const table = createTable(['Tax Rate', 'Taxable Amount', 'Tax Amount']);
|
||||
(report as unknown as { data?: Array<{ taxRateName: string; taxableAmount: number; taxAmount: number }> }).data?.forEach((item) => {
|
||||
table.push([item.taxRateName, formatCurrency(item.taxableAmount), formatCurrency(item.taxAmount)]);
|
||||
});
|
||||
|
||||
console.log(table.toString() + '\n');
|
||||
} catch (error) {
|
||||
spinner.stop();
|
||||
handleError(error);
|
||||
}
|
||||
});
|
||||
|
||||
return command;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { Command } from 'commander';
|
||||
import chalk from 'chalk';
|
||||
import ora from 'ora';
|
||||
import { fetchTaxRates } from '@bigcapital/sdk-ts';
|
||||
import { createAuthenticatedFetcher } from '../config';
|
||||
import { handleError } from '../utils/errors';
|
||||
import { createTable, formatStatus } from '../utils/table';
|
||||
|
||||
export function createTaxRatesCommand(): Command {
|
||||
const command = new Command('tax-rates')
|
||||
.description('Manage tax rates');
|
||||
|
||||
command
|
||||
.command('list')
|
||||
.description('List all tax rates')
|
||||
.action(async () => {
|
||||
const spinner = ora('Loading tax rates...').start();
|
||||
|
||||
try {
|
||||
const fetcher = createAuthenticatedFetcher();
|
||||
const response = await fetchTaxRates(fetcher);
|
||||
|
||||
spinner.stop();
|
||||
|
||||
const taxRates = (response as unknown as { taxRates: Array<{
|
||||
id: number;
|
||||
name?: string;
|
||||
rate?: number;
|
||||
code?: string;
|
||||
active?: boolean;
|
||||
}> }).taxRates;
|
||||
|
||||
if (!taxRates || taxRates.length === 0) {
|
||||
console.log(chalk.yellow('No tax rates found.'));
|
||||
return;
|
||||
}
|
||||
|
||||
const table = createTable(['ID', 'Name', 'Code', 'Rate', 'Status']);
|
||||
|
||||
taxRates.forEach((tax) => {
|
||||
table.push([
|
||||
tax.id,
|
||||
tax.name || '-',
|
||||
tax.code || '-',
|
||||
`${tax.rate || 0}%`,
|
||||
formatStatus(tax.active ? 'active' : 'inactive'),
|
||||
]);
|
||||
});
|
||||
|
||||
console.log(table.toString());
|
||||
} catch (error) {
|
||||
spinner.stop();
|
||||
handleError(error);
|
||||
}
|
||||
});
|
||||
|
||||
return command;
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import { Command } from 'commander';
|
||||
import chalk from 'chalk';
|
||||
import ora from 'ora';
|
||||
import { fetchUsers, fetchRoles } from '@bigcapital/sdk-ts';
|
||||
import { createAuthenticatedFetcher } from '../config';
|
||||
import { handleError } from '../utils/errors';
|
||||
import { createTable, formatStatus } from '../utils/table';
|
||||
|
||||
export function createUsersCommand(): Command {
|
||||
const command = new Command('users')
|
||||
.description('Manage users and roles');
|
||||
|
||||
command
|
||||
.command('list')
|
||||
.description('List all users')
|
||||
.action(async () => {
|
||||
const spinner = ora('Loading users...').start();
|
||||
|
||||
try {
|
||||
const fetcher = createAuthenticatedFetcher();
|
||||
const response = await fetchUsers(fetcher);
|
||||
|
||||
spinner.stop();
|
||||
|
||||
const users = (response as unknown as { users: Array<{
|
||||
id: number;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
email?: string;
|
||||
role?: { name?: string };
|
||||
active?: boolean;
|
||||
}> }).users;
|
||||
|
||||
if (!users || users.length === 0) {
|
||||
console.log(chalk.yellow('No users found.'));
|
||||
return;
|
||||
}
|
||||
|
||||
const table = createTable(['ID', 'Name', 'Email', 'Role', 'Status']);
|
||||
|
||||
users.forEach((user) => {
|
||||
const name = `${user.firstName || ''} ${user.lastName || ''}`.trim() || '-';
|
||||
table.push([
|
||||
user.id,
|
||||
name,
|
||||
user.email || '-',
|
||||
user.role?.name || '-',
|
||||
formatStatus(user.active ? 'active' : 'inactive'),
|
||||
]);
|
||||
});
|
||||
|
||||
console.log(table.toString());
|
||||
} catch (error) {
|
||||
spinner.stop();
|
||||
handleError(error);
|
||||
}
|
||||
});
|
||||
|
||||
command
|
||||
.command('roles')
|
||||
.description('List all roles')
|
||||
.action(async () => {
|
||||
const spinner = ora('Loading roles...').start();
|
||||
|
||||
try {
|
||||
const fetcher = createAuthenticatedFetcher();
|
||||
const response = await fetchRoles(fetcher);
|
||||
|
||||
spinner.stop();
|
||||
|
||||
const roles = (response as unknown as { roles: Array<{
|
||||
id: number;
|
||||
name?: string;
|
||||
description?: string;
|
||||
slug?: string;
|
||||
}> }).roles;
|
||||
|
||||
if (!roles || roles.length === 0) {
|
||||
console.log(chalk.yellow('No roles found.'));
|
||||
return;
|
||||
}
|
||||
|
||||
const table = createTable(['ID', 'Name', 'Slug', 'Description']);
|
||||
|
||||
roles.forEach((role) => {
|
||||
table.push([
|
||||
role.id,
|
||||
role.name || '-',
|
||||
role.slug || '-',
|
||||
role.description || '-',
|
||||
]);
|
||||
});
|
||||
|
||||
console.log(table.toString());
|
||||
} catch (error) {
|
||||
spinner.stop();
|
||||
handleError(error);
|
||||
}
|
||||
});
|
||||
|
||||
return command;
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import { Command } from 'commander';
|
||||
import chalk from 'chalk';
|
||||
import ora from 'ora';
|
||||
import { fetchVendorCredits } from '@bigcapital/sdk-ts';
|
||||
import { createAuthenticatedFetcher } from '../config';
|
||||
import { handleError } from '../utils/errors';
|
||||
import { createTable, formatCurrency, formatDate, truncate, formatStatus } from '../utils/table';
|
||||
|
||||
export function createVendorCreditsCommand(): Command {
|
||||
const command = new Command('vendor-credits')
|
||||
.description('Manage vendor credits');
|
||||
|
||||
command
|
||||
.command('list')
|
||||
.description('List all vendor credits')
|
||||
.option('-l, --limit <number>', 'Limit number of results per page', '50')
|
||||
.option('-p, --page <number>', 'Page number', '1')
|
||||
.option('-v, --vendor <id>', 'Filter by vendor ID')
|
||||
.action(async (options) => {
|
||||
const spinner = ora('Loading vendor credits...').start();
|
||||
|
||||
try {
|
||||
const fetcher = createAuthenticatedFetcher();
|
||||
|
||||
const query = {
|
||||
page: parseInt(options.page, 10),
|
||||
pageSize: Math.min(parseInt(options.limit, 10), 100),
|
||||
...(options.vendor && { vendorId: parseInt(options.vendor, 10) }),
|
||||
};
|
||||
|
||||
const response = await fetchVendorCredits(fetcher, query);
|
||||
|
||||
spinner.stop();
|
||||
|
||||
const vendorCredits = (response as unknown as { vendorCredits: Array<{
|
||||
id: number;
|
||||
vendorCreditNumber?: string;
|
||||
vendor?: { displayName?: string };
|
||||
vendorCreditDate?: string;
|
||||
total?: number;
|
||||
balance?: number;
|
||||
status?: string;
|
||||
}> }).vendorCredits;
|
||||
|
||||
if (!vendorCredits || vendorCredits.length === 0) {
|
||||
console.log(chalk.yellow('No vendor credits found.'));
|
||||
return;
|
||||
}
|
||||
|
||||
const pagination = (response as unknown as {
|
||||
pagination?: { total: number; page: number; pageSize?: number; page_size?: number }
|
||||
}).pagination;
|
||||
if (pagination) {
|
||||
const pageSize = pagination.pageSize || pagination.page_size || 10;
|
||||
const totalPages = Math.ceil(pagination.total / pageSize);
|
||||
console.log(chalk.gray(`\nPage ${pagination.page} of ${totalPages} (${pagination.total} total vendor credits)\n`));
|
||||
}
|
||||
|
||||
const table = createTable(['ID', 'VC #', 'Vendor', 'Date', 'Total', 'Balance', 'Status']);
|
||||
|
||||
vendorCredits.forEach((vc) => {
|
||||
table.push([
|
||||
vc.id,
|
||||
vc.vendorCreditNumber || '-',
|
||||
truncate(vc.vendor?.displayName, 20),
|
||||
formatDate(vc.vendorCreditDate),
|
||||
formatCurrency(vc.total),
|
||||
formatCurrency(vc.balance),
|
||||
formatStatus(vc.status),
|
||||
]);
|
||||
});
|
||||
|
||||
console.log(table.toString());
|
||||
|
||||
if (pagination) {
|
||||
const pageSize = pagination.pageSize || pagination.page_size || 10;
|
||||
const totalPages = Math.ceil(pagination.total / pageSize);
|
||||
if (pagination.page < totalPages) {
|
||||
console.log(chalk.gray(`\nUse --page ${pagination.page + 1} to see more results`));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
spinner.stop();
|
||||
handleError(error);
|
||||
}
|
||||
});
|
||||
|
||||
return command;
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import { Command } from 'commander';
|
||||
import chalk from 'chalk';
|
||||
import ora from 'ora';
|
||||
import { fetchVendors } from '@bigcapital/sdk-ts';
|
||||
import { createAuthenticatedFetcher } from '../config';
|
||||
import { handleError } from '../utils/errors';
|
||||
import { createTable, formatCurrency, truncate, formatStatus } from '../utils/table';
|
||||
|
||||
export function createVendorsCommand(): Command {
|
||||
const command = new Command('vendors')
|
||||
.description('Manage vendors');
|
||||
|
||||
command
|
||||
.command('list')
|
||||
.description('List all vendors')
|
||||
.option('-l, --limit <number>', 'Limit number of results per page', '50')
|
||||
.option('-p, --page <number>', 'Page number', '1')
|
||||
.option('--active-only', 'Show only active vendors', false)
|
||||
.action(async (options) => {
|
||||
const spinner = ora('Loading vendors...').start();
|
||||
|
||||
try {
|
||||
const fetcher = createAuthenticatedFetcher();
|
||||
|
||||
const query = {
|
||||
page: parseInt(options.page, 10),
|
||||
pageSize: Math.min(parseInt(options.limit, 10), 100),
|
||||
...(options.activeOnly && { active: true }),
|
||||
};
|
||||
|
||||
const response = await fetchVendors(fetcher, query);
|
||||
|
||||
spinner.stop();
|
||||
|
||||
const vendors = (response as unknown as { vendors: Array<{
|
||||
id: number;
|
||||
displayName?: string;
|
||||
email?: string;
|
||||
workPhone?: string;
|
||||
personalPhone?: string;
|
||||
balance?: number;
|
||||
active?: boolean;
|
||||
}> }).vendors;
|
||||
|
||||
if (!vendors || vendors.length === 0) {
|
||||
console.log(chalk.yellow('No vendors found.'));
|
||||
return;
|
||||
}
|
||||
|
||||
const pagination = (response as unknown as {
|
||||
pagination?: { total: number; page: number; pageSize?: number; page_size?: number }
|
||||
}).pagination;
|
||||
if (pagination) {
|
||||
const pageSize = pagination.pageSize || pagination.page_size || 10;
|
||||
const totalPages = Math.ceil(pagination.total / pageSize);
|
||||
console.log(chalk.gray(`\nPage ${pagination.page} of ${totalPages} (${pagination.total} total vendors)\n`));
|
||||
}
|
||||
|
||||
const table = createTable(['ID', 'Name', 'Email', 'Work Phone', 'Personal Phone', 'Balance', 'Status']);
|
||||
|
||||
vendors.forEach((vendor) => {
|
||||
table.push([
|
||||
vendor.id,
|
||||
truncate(vendor.displayName, 25),
|
||||
truncate(vendor.email, 25) || '-',
|
||||
vendor.workPhone || '-',
|
||||
vendor.personalPhone || '-',
|
||||
formatCurrency(vendor.balance),
|
||||
formatStatus(vendor.active ? 'active' : 'inactive'),
|
||||
]);
|
||||
});
|
||||
|
||||
console.log(table.toString());
|
||||
|
||||
if (pagination) {
|
||||
const pageSize = pagination.pageSize || pagination.page_size || 10;
|
||||
const totalPages = Math.ceil(pagination.total / pageSize);
|
||||
if (pagination.page < totalPages) {
|
||||
console.log(chalk.gray(`\nUse --page ${pagination.page + 1} to see more results`));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
spinner.stop();
|
||||
handleError(error);
|
||||
}
|
||||
});
|
||||
|
||||
return command;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { Command } from 'commander';
|
||||
import chalk from 'chalk';
|
||||
import ora from 'ora';
|
||||
import { fetchWarehouses } from '@bigcapital/sdk-ts';
|
||||
import { createAuthenticatedFetcher } from '../config';
|
||||
import { handleError } from '../utils/errors';
|
||||
import { createTable, formatStatus } from '../utils/table';
|
||||
|
||||
export function createWarehousesCommand(): Command {
|
||||
const command = new Command('warehouses')
|
||||
.description('Manage warehouses');
|
||||
|
||||
command
|
||||
.command('list')
|
||||
.description('List all warehouses')
|
||||
.action(async () => {
|
||||
const spinner = ora('Loading warehouses...').start();
|
||||
|
||||
try {
|
||||
const fetcher = createAuthenticatedFetcher();
|
||||
const response = await fetchWarehouses(fetcher);
|
||||
|
||||
spinner.stop();
|
||||
|
||||
const warehouses = (response as unknown as { warehouses: Array<{
|
||||
id: string;
|
||||
name?: string;
|
||||
code?: string;
|
||||
address?: string;
|
||||
city?: string;
|
||||
active?: boolean;
|
||||
}> }).warehouses;
|
||||
|
||||
if (!warehouses || warehouses.length === 0) {
|
||||
console.log(chalk.yellow('No warehouses found.'));
|
||||
return;
|
||||
}
|
||||
|
||||
const table = createTable(['ID', 'Code', 'Name', 'City', 'Status']);
|
||||
|
||||
warehouses.forEach((warehouse) => {
|
||||
table.push([
|
||||
warehouse.id,
|
||||
warehouse.code || '-',
|
||||
warehouse.name || '-',
|
||||
warehouse.city || '-',
|
||||
formatStatus(warehouse.active ? 'active' : 'inactive'),
|
||||
]);
|
||||
});
|
||||
|
||||
console.log(table.toString());
|
||||
} catch (error) {
|
||||
spinner.stop();
|
||||
handleError(error);
|
||||
}
|
||||
});
|
||||
|
||||
return command;
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import Conf from 'conf';
|
||||
import chalk from 'chalk';
|
||||
import { createApiFetcher, ApiFetcher } from '@bigcapital/sdk-ts';
|
||||
|
||||
interface ConfigSchema {
|
||||
apiKey: string;
|
||||
baseUrl: string;
|
||||
organizationId?: string;
|
||||
}
|
||||
|
||||
const config = new Conf<ConfigSchema>({
|
||||
projectName: 'bigcapital',
|
||||
schema: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
},
|
||||
baseUrl: {
|
||||
type: 'string',
|
||||
},
|
||||
organizationId: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export function getConfig(): ConfigSchema {
|
||||
return {
|
||||
apiKey: config.get('apiKey', ''),
|
||||
baseUrl: config.get('baseUrl', ''),
|
||||
organizationId: config.get('organizationId'),
|
||||
};
|
||||
}
|
||||
|
||||
export function setApiKey(apiKey: string): void {
|
||||
config.set('apiKey', apiKey);
|
||||
}
|
||||
|
||||
export function setBaseUrl(baseUrl: string): void {
|
||||
// Remove trailing slash if present
|
||||
const normalizedUrl = baseUrl.replace(/\/$/, '');
|
||||
config.set('baseUrl', normalizedUrl);
|
||||
}
|
||||
|
||||
export function setOrganizationId(organizationId: string): void {
|
||||
config.set('organizationId', organizationId);
|
||||
}
|
||||
|
||||
export function validateConfig(): ConfigSchema {
|
||||
const currentConfig = getConfig();
|
||||
|
||||
if (!currentConfig.apiKey) {
|
||||
console.error(chalk.red('Error: API key is not configured.'));
|
||||
console.error(chalk.yellow('Run: bigcapital config set api-key <your-api-key>'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!currentConfig.baseUrl) {
|
||||
console.error(chalk.red('Error: Base URL is not configured.'));
|
||||
console.error(chalk.yellow('Run: bigcapital config set base-url <https://your-api-url>'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
return currentConfig;
|
||||
}
|
||||
|
||||
export function createAuthenticatedFetcher(): ApiFetcher {
|
||||
const currentConfig = validateConfig();
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
'Authorization': `Bearer ${currentConfig.apiKey}`,
|
||||
};
|
||||
|
||||
if (currentConfig.organizationId) {
|
||||
headers['organization-id'] = currentConfig.organizationId;
|
||||
}
|
||||
|
||||
return createApiFetcher({
|
||||
baseUrl: currentConfig.baseUrl,
|
||||
init: {
|
||||
headers,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { Command } from 'commander';
|
||||
import { createItemsCommand } from './commands/items';
|
||||
import { createInvoicesCommand } from './commands/invoices';
|
||||
import { createConfigCommand } from './commands/config';
|
||||
import { createCustomersCommand } from './commands/customers';
|
||||
import { createVendorsCommand } from './commands/vendors';
|
||||
import { createBillsCommand } from './commands/bills';
|
||||
import { createAccountsCommand } from './commands/accounts';
|
||||
import { createExpensesCommand } from './commands/expenses';
|
||||
import { createCreditNotesCommand } from './commands/credit-notes';
|
||||
import { createVendorCreditsCommand } from './commands/vendor-credits';
|
||||
import { createPaymentsCommand } from './commands/payments';
|
||||
import { createEstimatesCommand } from './commands/estimates';
|
||||
import { createReceiptsCommand } from './commands/receipts';
|
||||
import { createJournalsCommand } from './commands/journals';
|
||||
import { createInventoryCommand } from './commands/inventory';
|
||||
import { createTaxRatesCommand } from './commands/tax-rates';
|
||||
import { createWarehousesCommand } from './commands/warehouses';
|
||||
import { createUsersCommand } from './commands/users';
|
||||
import { createReportsCommand } from './commands/reports';
|
||||
import chalk from 'chalk';
|
||||
|
||||
const program = new Command();
|
||||
|
||||
program
|
||||
.name('bigcapital')
|
||||
.description('Bigcapital CLI - Interact with Bigcapital API')
|
||||
.version('1.0.0')
|
||||
.configureOutput({
|
||||
writeErr: (str) => process.stderr.write(chalk.red(str)),
|
||||
outputError: (str, write) => write(chalk.red(str)),
|
||||
});
|
||||
|
||||
// Core modules
|
||||
program.addCommand(createConfigCommand());
|
||||
program.addCommand(createItemsCommand());
|
||||
program.addCommand(createInvoicesCommand());
|
||||
program.addCommand(createCustomersCommand());
|
||||
program.addCommand(createVendorsCommand());
|
||||
program.addCommand(createBillsCommand());
|
||||
|
||||
// Additional transactional modules
|
||||
program.addCommand(createAccountsCommand());
|
||||
program.addCommand(createExpensesCommand());
|
||||
program.addCommand(createCreditNotesCommand());
|
||||
program.addCommand(createVendorCreditsCommand());
|
||||
program.addCommand(createPaymentsCommand());
|
||||
program.addCommand(createEstimatesCommand());
|
||||
program.addCommand(createReceiptsCommand());
|
||||
program.addCommand(createJournalsCommand());
|
||||
program.addCommand(createInventoryCommand());
|
||||
program.addCommand(createTaxRatesCommand());
|
||||
program.addCommand(createWarehousesCommand());
|
||||
program.addCommand(createUsersCommand());
|
||||
|
||||
// Financial reports
|
||||
program.addCommand(createReportsCommand());
|
||||
|
||||
// Global error handling
|
||||
program.hook('preAction', () => {
|
||||
process.on('unhandledRejection', (error) => {
|
||||
console.error(chalk.red('\nUnhandled error:'), error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
process.on('uncaughtException', (error) => {
|
||||
console.error(chalk.red('\nUncaught exception:'), error);
|
||||
process.exit(1);
|
||||
});
|
||||
});
|
||||
|
||||
// Show help if no command provided
|
||||
if (process.argv.length <= 2) {
|
||||
program.help();
|
||||
}
|
||||
|
||||
program.parse();
|
||||
@@ -0,0 +1,40 @@
|
||||
import chalk from 'chalk';
|
||||
|
||||
export function handleError(error: unknown): never {
|
||||
if (error instanceof Error) {
|
||||
// Check if it's an HTTP error from the SDK
|
||||
const httpError = error as { status?: number; statusText?: string };
|
||||
|
||||
if (httpError.status) {
|
||||
console.error(chalk.red(`HTTP Error ${httpError.status}: ${httpError.statusText || error.message}`));
|
||||
|
||||
if (httpError.status === 401) {
|
||||
console.error(chalk.yellow('Your API key may be invalid or expired.'));
|
||||
console.error(chalk.yellow('Run: bigcapital config set api-key <your-new-api-key>'));
|
||||
} else if (httpError.status === 403) {
|
||||
console.error(chalk.yellow('You don\'t have permission to access this resource.'));
|
||||
} else if (httpError.status === 404) {
|
||||
console.error(chalk.yellow('The requested resource was not found.'));
|
||||
}
|
||||
} else {
|
||||
console.error(chalk.red(`Error: ${error.message}`));
|
||||
}
|
||||
|
||||
if (process.env.DEBUG) {
|
||||
console.error(chalk.gray(error.stack));
|
||||
}
|
||||
} else {
|
||||
console.error(chalk.red('An unknown error occurred'));
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
export function assertDefined<T>(value: T | undefined | null, name: string): T {
|
||||
if (value === undefined || value === null) {
|
||||
console.error(chalk.red(`Error: ${name} is required but was not provided.`));
|
||||
process.exit(1);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import Table from 'cli-table3';
|
||||
import chalk from 'chalk';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function createTable(headers: string[]): Table.Table {
|
||||
return new Table({
|
||||
head: headers.map(h => chalk.cyan.bold(h)),
|
||||
style: {
|
||||
head: [],
|
||||
border: [],
|
||||
},
|
||||
chars: {
|
||||
top: '─',
|
||||
'top-mid': '┬',
|
||||
'top-left': '┌',
|
||||
'top-right': '┐',
|
||||
bottom: '─',
|
||||
'bottom-mid': '┴',
|
||||
'bottom-left': '└',
|
||||
'bottom-right': '┘',
|
||||
left: '│',
|
||||
'left-mid': '├',
|
||||
mid: '─',
|
||||
'mid-mid': '┼',
|
||||
right: '│',
|
||||
'right-mid': '┤',
|
||||
middle: '│',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function formatCurrency(amount: number | string | undefined, currency = '$'): string {
|
||||
if (amount === undefined || amount === null) return '-';
|
||||
const num = typeof amount === 'string' ? parseFloat(amount) : amount;
|
||||
if (isNaN(num)) return '-';
|
||||
return `${currency}${num.toFixed(2)}`;
|
||||
}
|
||||
|
||||
export function formatDate(dateStr: string | undefined): string {
|
||||
if (!dateStr) return '-';
|
||||
const date = new Date(dateStr);
|
||||
if (isNaN(date.getTime())) return dateStr;
|
||||
return date.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
});
|
||||
}
|
||||
|
||||
export function truncate(str: string | undefined, maxLength: number): string {
|
||||
if (!str) return '-';
|
||||
if (str.length <= maxLength) return str;
|
||||
return str.substring(0, maxLength - 3) + '...';
|
||||
}
|
||||
|
||||
export function formatStatus(status: string | undefined): string {
|
||||
if (!status) return '-';
|
||||
|
||||
const statusColors: Record<string, (s: string) => string> = {
|
||||
published: chalk.green,
|
||||
draft: chalk.gray,
|
||||
delivered: chalk.blue,
|
||||
partial: chalk.yellow,
|
||||
paid: chalk.green.bold,
|
||||
unpaid: chalk.red,
|
||||
overdue: chalk.red.bold,
|
||||
active: chalk.green,
|
||||
inactive: chalk.gray,
|
||||
};
|
||||
|
||||
const lowerStatus = status.toLowerCase();
|
||||
const colorFn = statusColors[lowerStatus] || chalk.white;
|
||||
return colorFn(status);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "CommonJS",
|
||||
"lib": ["ES2020"],
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"declaration": true,
|
||||
"moduleResolution": "node"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Generated
+80
-2
@@ -37,6 +37,34 @@ importers:
|
||||
specifier: ^9.0.5
|
||||
version: 9.1.2
|
||||
|
||||
packages/cli:
|
||||
dependencies:
|
||||
'@bigcapital/sdk-ts':
|
||||
specifier: workspace:*
|
||||
version: link:../../shared/sdk-ts
|
||||
chalk:
|
||||
specifier: ^4.1.2
|
||||
version: 4.1.2
|
||||
cli-table3:
|
||||
specifier: ^0.6.3
|
||||
version: 0.6.5
|
||||
commander:
|
||||
specifier: ^11.1.0
|
||||
version: 11.1.0
|
||||
conf:
|
||||
specifier: ^10.2.0
|
||||
version: 10.2.0
|
||||
ora:
|
||||
specifier: ^5.4.1
|
||||
version: 5.4.1
|
||||
devDependencies:
|
||||
'@types/node':
|
||||
specifier: ^20.0.0
|
||||
version: 20.19.25
|
||||
typescript:
|
||||
specifier: ^5.1.3
|
||||
version: 5.6.3
|
||||
|
||||
packages/server:
|
||||
dependencies:
|
||||
'@aws-sdk/client-s3':
|
||||
@@ -6354,6 +6382,10 @@ packages:
|
||||
asynckit@0.4.0:
|
||||
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
||||
|
||||
atomically@1.7.0:
|
||||
resolution: {integrity: sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w==}
|
||||
engines: {node: '>=10.12.0'}
|
||||
|
||||
attr-accept@2.2.2:
|
||||
resolution: {integrity: sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -7020,6 +7052,10 @@ packages:
|
||||
resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==}
|
||||
engines: {'0': node >= 6.0}
|
||||
|
||||
conf@10.2.0:
|
||||
resolution: {integrity: sha512-8fLl9F04EJqjSqH+QjITQfJF8BrOVaYr1jewVgSRAEWePfxT0sku4w2hrGQ60BC/TNLGQ2pgxNlTbWQmMPFvXg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
confbox@0.1.8:
|
||||
resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==}
|
||||
|
||||
@@ -7299,6 +7335,10 @@ packages:
|
||||
de-indent@1.0.2:
|
||||
resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==}
|
||||
|
||||
debounce-fn@4.0.0:
|
||||
resolution: {integrity: sha512-8pYCQiL9Xdcg0UPSD3d+0KMlOjp+KGU5EPwYddgzQ7DATsg4fuUDjQtsYLmWjnk2obnNHgV3vE2Y4jejSOJVBQ==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
debug@2.6.9:
|
||||
resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
|
||||
peerDependencies:
|
||||
@@ -9406,6 +9446,9 @@ packages:
|
||||
json-schema-traverse@1.0.0:
|
||||
resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
|
||||
|
||||
json-schema-typed@7.0.3:
|
||||
resolution: {integrity: sha512-7DE8mpG+/fVw+dTpjbxnx47TaMnDfOI1jwft9g1VybltZCduyRQPJPvc+zzKY9WPHxhPWczyFuYa6I8Mw4iU5A==}
|
||||
|
||||
json-stable-stringify-without-jsonify@1.0.1:
|
||||
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
|
||||
|
||||
@@ -9910,6 +9953,10 @@ packages:
|
||||
resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
mimic-fn@3.1.0:
|
||||
resolution: {integrity: sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
mimic-fn@4.0.0:
|
||||
resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -10747,6 +10794,10 @@ packages:
|
||||
pkg-types@1.2.1:
|
||||
resolution: {integrity: sha512-sQoqa8alT3nHjGuTjuKgOnvjo4cljkufdtLMnO2LBP/wRwuDlo1tkaEdMxCRhyGRPacv/ztlZgDPm2b7FAmEvw==}
|
||||
|
||||
pkg-up@3.1.0:
|
||||
resolution: {integrity: sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
plaid-threads@11.5.0:
|
||||
resolution: {integrity: sha512-KS3w5Ydv+aC7wS1XxiWeDUhQHHFQ/5dQOoAQwdg+3DxFHbh5nSPH1L4Zy9qHru5FVDmC5DQ6/9XhJwzZ9BhpTA==}
|
||||
peerDependencies:
|
||||
@@ -20181,7 +20232,7 @@ snapshots:
|
||||
|
||||
'@types/nodemailer@6.4.17':
|
||||
dependencies:
|
||||
'@types/node': 20.5.1
|
||||
'@types/node': 20.19.25
|
||||
|
||||
'@types/normalize-package-data@2.4.4': {}
|
||||
|
||||
@@ -21238,6 +21289,8 @@ snapshots:
|
||||
|
||||
asynckit@0.4.0: {}
|
||||
|
||||
atomically@1.7.0: {}
|
||||
|
||||
attr-accept@2.2.2: {}
|
||||
|
||||
available-typed-arrays@1.0.7:
|
||||
@@ -22095,6 +22148,19 @@ snapshots:
|
||||
readable-stream: 3.6.2
|
||||
typedarray: 0.0.6
|
||||
|
||||
conf@10.2.0:
|
||||
dependencies:
|
||||
ajv: 8.17.1
|
||||
ajv-formats: 2.1.1(ajv@8.17.1)
|
||||
atomically: 1.7.0
|
||||
debounce-fn: 4.0.0
|
||||
dot-prop: 6.0.1
|
||||
env-paths: 2.2.1
|
||||
json-schema-typed: 7.0.3
|
||||
onetime: 5.1.2
|
||||
pkg-up: 3.1.0
|
||||
semver: 7.6.3
|
||||
|
||||
confbox@0.1.8: {}
|
||||
|
||||
config-chain@1.1.13:
|
||||
@@ -22405,6 +22471,10 @@ snapshots:
|
||||
|
||||
de-indent@1.0.2: {}
|
||||
|
||||
debounce-fn@4.0.0:
|
||||
dependencies:
|
||||
mimic-fn: 3.1.0
|
||||
|
||||
debug@2.6.9:
|
||||
dependencies:
|
||||
ms: 2.0.0
|
||||
@@ -25124,6 +25194,8 @@ snapshots:
|
||||
|
||||
json-schema-traverse@1.0.0: {}
|
||||
|
||||
json-schema-typed@7.0.3: {}
|
||||
|
||||
json-stable-stringify-without-jsonify@1.0.1: {}
|
||||
|
||||
json-stringify-safe@5.0.1: {}
|
||||
@@ -25718,6 +25790,8 @@ snapshots:
|
||||
|
||||
mimic-fn@2.1.0: {}
|
||||
|
||||
mimic-fn@3.1.0: {}
|
||||
|
||||
mimic-fn@4.0.0: {}
|
||||
|
||||
min-indent@1.0.1: {}
|
||||
@@ -26700,6 +26774,10 @@ snapshots:
|
||||
mlly: 1.7.2
|
||||
pathe: 1.1.2
|
||||
|
||||
pkg-up@3.1.0:
|
||||
dependencies:
|
||||
find-up: 3.0.0
|
||||
|
||||
plaid-threads@11.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||
dependencies:
|
||||
'@popperjs/core': 2.11.8
|
||||
@@ -28541,7 +28619,7 @@ snapshots:
|
||||
|
||||
stripe@16.10.0:
|
||||
dependencies:
|
||||
'@types/node': 20.5.1
|
||||
'@types/node': 20.19.25
|
||||
qs: 6.14.0
|
||||
|
||||
strnum@1.0.5: {}
|
||||
|
||||
Reference in New Issue
Block a user