Add credits, projects, tasks, vendors, and purchase orders
Five more resources following clients.rs's template exactly, same as the previous batch. credits.rs is otherwise identical to quotes.rs since Credit is also a type alias for Invoice in the SDK (same table, distinguished by entity_type upstream). Verified against a live local server: full create/list/get/update/ delete round trip for all five. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+9
-8
@@ -12,14 +12,15 @@ command line.
|
||||
|
||||
### Added
|
||||
|
||||
- Six resources, each with `list` (with `--all` for every page, `--status`,
|
||||
and `--param key=value` for anything `ListParams` doesn't have a typed
|
||||
setter for), `get`, `create`, `update`, and `delete`: `clients`,
|
||||
`invoices`, `quotes`, `payments`, `products`, `expenses`. `quotes` is
|
||||
`invoices` under a different endpoint — Invoice Ninja stores both in the
|
||||
same table, distinguished by `entity_type`, so `Quote` is a type alias for
|
||||
`Invoice` in the SDK and `commands/quotes.rs` is otherwise identical to
|
||||
`commands/invoices.rs`.
|
||||
- Eleven resources, each with `list` (with `--all` for every page,
|
||||
`--status`, and `--param key=value` for anything `ListParams` doesn't have
|
||||
a typed setter for), `get`, `create`, `update`, and `delete`: `clients`,
|
||||
`invoices`, `quotes`, `credits`, `payments`, `products`, `expenses`,
|
||||
`projects`, `tasks`, `vendors`, `purchase-orders`. `quotes` and `credits`
|
||||
are `invoices` under different endpoints — Invoice Ninja stores all three
|
||||
in the same table, distinguished by `entity_type`, so `Quote` and `Credit`
|
||||
are type aliases for `Invoice` in the SDK and `commands/quotes.rs` /
|
||||
`commands/credits.rs` are otherwise identical to `commands/invoices.rs`.
|
||||
- `update` fetches the current record, shallow-merges the caller's
|
||||
`--data`/`--file` JSON on top, and writes the merged result back, rather
|
||||
than sending the patch alone — Invoice Ninja's `PUT` is a full replace.
|
||||
|
||||
@@ -20,9 +20,10 @@ design call, the same way its counterpart in the SDK works. If a change
|
||||
would contradict a stated goal or settle an open question, update
|
||||
`GOALS.md` in the same change.
|
||||
|
||||
**Early scaffold.** Six resources are wired up (`clients`, `invoices`,
|
||||
`quotes`, `payments`, `products`, `expenses`) out of ~34; `clients` was the
|
||||
template the other five followed. See "Adding a resource" below before
|
||||
**Early scaffold.** Eleven resources are wired up (`clients`, `invoices`,
|
||||
`quotes`, `credits`, `payments`, `products`, `expenses`, `projects`,
|
||||
`tasks`, `vendors`, `purchase_orders`) out of ~34; `clients` was the
|
||||
template the other ten followed. See "Adding a resource" below before
|
||||
writing the next one.
|
||||
|
||||
## Development environment
|
||||
|
||||
@@ -3,10 +3,11 @@
|
||||
A command-line client for the [Invoice Ninja](https://www.invoiceninja.com/)
|
||||
v5 API, built on [`invoiceninja-sdk`](https://crates.io/crates/invoiceninja-sdk).
|
||||
|
||||
> **Early scaffold.** `clients`, `invoices`, `quotes`, `payments`, `products`,
|
||||
> and `expenses` are wired up so far, out of ~34 resources — see `CLAUDE.md`
|
||||
> for the pattern the rest follow. [`GOALS.md`](GOALS.md) covers what the
|
||||
> crate is for and what it is not.
|
||||
> **Early scaffold.** `clients`, `invoices`, `quotes`, `credits`, `payments`,
|
||||
> `products`, `expenses`, `projects`, `tasks`, `vendors`, and
|
||||
> `purchase-orders` are wired up so far, out of ~34 resources — see
|
||||
> `CLAUDE.md` for the pattern the rest follow. [`GOALS.md`](GOALS.md) covers
|
||||
> what the crate is for and what it is not.
|
||||
|
||||
## Development
|
||||
|
||||
@@ -48,7 +49,8 @@ table view's few key columns:
|
||||
invoiceninja-cli --output json clients get <id>
|
||||
```
|
||||
|
||||
Every other wired-up resource (`invoices`, `quotes`, `payments`, `products`,
|
||||
`expenses`) follows the same `list`/`get`/`create`/`update`/`delete` shape,
|
||||
e.g. `invoiceninja-cli invoices list --status active` or
|
||||
`invoiceninja-cli payments get <id>`.
|
||||
Every other wired-up resource (`invoices`, `quotes`, `credits`, `payments`,
|
||||
`products`, `expenses`, `projects`, `tasks`, `vendors`,
|
||||
`purchase-orders`) follows the same `list`/`get`/`create`/`update`/`delete`
|
||||
shape, e.g. `invoiceninja-cli invoices list --status active` or
|
||||
`invoiceninja-cli purchase-orders get <id>`.
|
||||
|
||||
+12
-2
@@ -45,9 +45,9 @@ pub enum OutputFormat {
|
||||
Json,
|
||||
}
|
||||
|
||||
/// One variant per resource. Six are wired up so far (`clients` was the
|
||||
/// One variant per resource. Eleven are wired up so far (`clients` was the
|
||||
/// template for the rest); see CLAUDE.md's "Adding a resource" for the
|
||||
/// remaining ~28.
|
||||
/// remaining ~23.
|
||||
#[derive(Subcommand)]
|
||||
pub enum Command {
|
||||
/// Manage clients
|
||||
@@ -56,12 +56,22 @@ pub enum Command {
|
||||
Invoices(ResourceArgs),
|
||||
/// Manage quotes
|
||||
Quotes(ResourceArgs),
|
||||
/// Manage credits
|
||||
Credits(ResourceArgs),
|
||||
/// Manage payments
|
||||
Payments(ResourceArgs),
|
||||
/// Manage products
|
||||
Products(ResourceArgs),
|
||||
/// Manage expenses
|
||||
Expenses(ResourceArgs),
|
||||
/// Manage projects
|
||||
Projects(ResourceArgs),
|
||||
/// Manage tasks
|
||||
Tasks(ResourceArgs),
|
||||
/// Manage vendors
|
||||
Vendors(ResourceArgs),
|
||||
/// Manage purchase orders
|
||||
PurchaseOrders(ResourceArgs),
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
//! `credits` subcommand. Same shape as `clients.rs` — see the "Adding a
|
||||
//! resource" section in CLAUDE.md. `Credit` is a type alias for `Invoice`
|
||||
//! (Invoice Ninja stores credits in the same table/schema, distinguished by
|
||||
//! an `entity_type`), same as `quotes.rs`.
|
||||
|
||||
use invoiceninja_sdk::models::{Credit, ListParams};
|
||||
use invoiceninja_sdk::InvoiceNinjaClient;
|
||||
|
||||
use crate::body;
|
||||
use crate::cli::{Action, BodyArgs, ListArgs, OutputFormat, ResourceArgs};
|
||||
use crate::output;
|
||||
|
||||
pub async fn run(
|
||||
client: &InvoiceNinjaClient,
|
||||
args: ResourceArgs,
|
||||
format: OutputFormat,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
match args.action {
|
||||
Action::List(list_args) => list(client, list_args, format).await,
|
||||
Action::Get { id } => get(client, &id, format).await,
|
||||
Action::Create(body_args) => create(client, body_args, format).await,
|
||||
Action::Update { id, body } => update(client, &id, body, format).await,
|
||||
Action::Delete { id } => delete(client, &id).await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn list(
|
||||
client: &InvoiceNinjaClient,
|
||||
args: ListArgs,
|
||||
format: OutputFormat,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut params = ListParams::new();
|
||||
if let Some(page) = args.page {
|
||||
params = params.page(page);
|
||||
}
|
||||
if let Some(per_page) = args.per_page {
|
||||
params = params.per_page(per_page);
|
||||
}
|
||||
if let Some(sort) = args.sort {
|
||||
params = params.sort(sort);
|
||||
}
|
||||
if let Some(filter) = args.filter {
|
||||
params = params.filter(filter);
|
||||
}
|
||||
if let Some(status) = args.status {
|
||||
params = params.status(status);
|
||||
}
|
||||
if let Some(include) = args.include {
|
||||
params = params.include(include);
|
||||
}
|
||||
for (k, v) in args.params {
|
||||
params = params.param(k, v);
|
||||
}
|
||||
|
||||
let credits = if args.all {
|
||||
client.list_all_credits(¶ms).await?
|
||||
} else {
|
||||
client.list_credits(¶ms).await?
|
||||
};
|
||||
|
||||
render(&credits, format)
|
||||
}
|
||||
|
||||
async fn get(
|
||||
client: &InvoiceNinjaClient,
|
||||
id: &str,
|
||||
format: OutputFormat,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let found = client.get_credit(id).await?;
|
||||
render(std::slice::from_ref(&found), format)
|
||||
}
|
||||
|
||||
async fn create(
|
||||
client: &InvoiceNinjaClient,
|
||||
body_args: BodyArgs,
|
||||
format: OutputFormat,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let value = body::load(&body_args)?;
|
||||
let model: Credit = serde_json::from_value(value)?;
|
||||
let created = client.create_credit(&model).await?;
|
||||
render(std::slice::from_ref(&created), format)
|
||||
}
|
||||
|
||||
async fn update(
|
||||
client: &InvoiceNinjaClient,
|
||||
id: &str,
|
||||
body_args: BodyArgs,
|
||||
format: OutputFormat,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let patch = body::load(&body_args)?;
|
||||
let current = client.get_credit(id).await?;
|
||||
let merged = body::merge_shallow(serde_json::to_value(¤t)?, patch);
|
||||
let model: Credit = serde_json::from_value(merged)?;
|
||||
let updated = client.update_credit(id, &model).await?;
|
||||
render(std::slice::from_ref(&updated), format)
|
||||
}
|
||||
|
||||
async fn delete(client: &InvoiceNinjaClient, id: &str) -> Result<(), Box<dyn std::error::Error>> {
|
||||
client.delete_credit(id).await?;
|
||||
println!("deleted {id}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render(credits: &[Credit], format: OutputFormat) -> Result<(), Box<dyn std::error::Error>> {
|
||||
match format {
|
||||
OutputFormat::Json => output::print_json(credits)?,
|
||||
OutputFormat::Table => print_table(credits),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_table(credits: &[Credit]) {
|
||||
if credits.is_empty() {
|
||||
println!("(no records)");
|
||||
return;
|
||||
}
|
||||
println!(
|
||||
"{:<20} {:<15} {:<20} {:<8} {:>12} {:>12}",
|
||||
"id", "number", "client_id", "status", "amount", "balance"
|
||||
);
|
||||
for c in credits {
|
||||
println!(
|
||||
"{:<20} {:<15} {:<20} {:<8} {:>12.2} {:>12.2}",
|
||||
c.id.as_deref().unwrap_or(""),
|
||||
truncate(&c.number, 15),
|
||||
truncate(&c.client_id, 20),
|
||||
c.status_id,
|
||||
c.amount,
|
||||
c.balance
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn truncate(s: &str, max: usize) -> String {
|
||||
if s.chars().count() <= max {
|
||||
s.to_string()
|
||||
} else {
|
||||
s.chars().take(max.saturating_sub(1)).collect::<String>() + "…"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,11 @@
|
||||
pub mod clients;
|
||||
pub mod credits;
|
||||
pub mod expenses;
|
||||
pub mod invoices;
|
||||
pub mod payments;
|
||||
pub mod products;
|
||||
pub mod projects;
|
||||
pub mod purchase_orders;
|
||||
pub mod quotes;
|
||||
pub mod tasks;
|
||||
pub mod vendors;
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
//! `projects` subcommand. Same shape as `clients.rs` — see the "Adding a
|
||||
//! resource" section in CLAUDE.md.
|
||||
|
||||
use invoiceninja_sdk::models::{ListParams, Project};
|
||||
use invoiceninja_sdk::InvoiceNinjaClient;
|
||||
|
||||
use crate::body;
|
||||
use crate::cli::{Action, BodyArgs, ListArgs, OutputFormat, ResourceArgs};
|
||||
use crate::output;
|
||||
|
||||
pub async fn run(
|
||||
client: &InvoiceNinjaClient,
|
||||
args: ResourceArgs,
|
||||
format: OutputFormat,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
match args.action {
|
||||
Action::List(list_args) => list(client, list_args, format).await,
|
||||
Action::Get { id } => get(client, &id, format).await,
|
||||
Action::Create(body_args) => create(client, body_args, format).await,
|
||||
Action::Update { id, body } => update(client, &id, body, format).await,
|
||||
Action::Delete { id } => delete(client, &id).await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn list(
|
||||
client: &InvoiceNinjaClient,
|
||||
args: ListArgs,
|
||||
format: OutputFormat,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut params = ListParams::new();
|
||||
if let Some(page) = args.page {
|
||||
params = params.page(page);
|
||||
}
|
||||
if let Some(per_page) = args.per_page {
|
||||
params = params.per_page(per_page);
|
||||
}
|
||||
if let Some(sort) = args.sort {
|
||||
params = params.sort(sort);
|
||||
}
|
||||
if let Some(filter) = args.filter {
|
||||
params = params.filter(filter);
|
||||
}
|
||||
if let Some(status) = args.status {
|
||||
params = params.status(status);
|
||||
}
|
||||
if let Some(include) = args.include {
|
||||
params = params.include(include);
|
||||
}
|
||||
for (k, v) in args.params {
|
||||
params = params.param(k, v);
|
||||
}
|
||||
|
||||
let projects = if args.all {
|
||||
client.list_all_projects(¶ms).await?
|
||||
} else {
|
||||
client.list_projects(¶ms).await?
|
||||
};
|
||||
|
||||
render(&projects, format)
|
||||
}
|
||||
|
||||
async fn get(
|
||||
client: &InvoiceNinjaClient,
|
||||
id: &str,
|
||||
format: OutputFormat,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let found = client.get_project(id).await?;
|
||||
render(std::slice::from_ref(&found), format)
|
||||
}
|
||||
|
||||
async fn create(
|
||||
client: &InvoiceNinjaClient,
|
||||
body_args: BodyArgs,
|
||||
format: OutputFormat,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let value = body::load(&body_args)?;
|
||||
let model: Project = serde_json::from_value(value)?;
|
||||
let created = client.create_project(&model).await?;
|
||||
render(std::slice::from_ref(&created), format)
|
||||
}
|
||||
|
||||
async fn update(
|
||||
client: &InvoiceNinjaClient,
|
||||
id: &str,
|
||||
body_args: BodyArgs,
|
||||
format: OutputFormat,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let patch = body::load(&body_args)?;
|
||||
let current = client.get_project(id).await?;
|
||||
let merged = body::merge_shallow(serde_json::to_value(¤t)?, patch);
|
||||
let model: Project = serde_json::from_value(merged)?;
|
||||
let updated = client.update_project(id, &model).await?;
|
||||
render(std::slice::from_ref(&updated), format)
|
||||
}
|
||||
|
||||
async fn delete(client: &InvoiceNinjaClient, id: &str) -> Result<(), Box<dyn std::error::Error>> {
|
||||
client.delete_project(id).await?;
|
||||
println!("deleted {id}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render(projects: &[Project], format: OutputFormat) -> Result<(), Box<dyn std::error::Error>> {
|
||||
match format {
|
||||
OutputFormat::Json => output::print_json(projects)?,
|
||||
OutputFormat::Table => print_table(projects),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_table(projects: &[Project]) {
|
||||
if projects.is_empty() {
|
||||
println!("(no records)");
|
||||
return;
|
||||
}
|
||||
println!(
|
||||
"{:<20} {:<30} {:<20} {:<10} {:>14}",
|
||||
"id", "name", "client_id", "number", "budgeted_hours"
|
||||
);
|
||||
for p in projects {
|
||||
println!(
|
||||
"{:<20} {:<30} {:<20} {:<10} {:>14.2}",
|
||||
p.id.as_deref().unwrap_or(""),
|
||||
truncate(&p.name, 30),
|
||||
truncate(&p.client_id, 20),
|
||||
p.number,
|
||||
p.budgeted_hours
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn truncate(s: &str, max: usize) -> String {
|
||||
if s.chars().count() <= max {
|
||||
s.to_string()
|
||||
} else {
|
||||
s.chars().take(max.saturating_sub(1)).collect::<String>() + "…"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
//! `purchase-orders` subcommand. Same shape as `clients.rs` — see the
|
||||
//! "Adding a resource" section in CLAUDE.md.
|
||||
|
||||
use invoiceninja_sdk::models::{ListParams, PurchaseOrder};
|
||||
use invoiceninja_sdk::InvoiceNinjaClient;
|
||||
|
||||
use crate::body;
|
||||
use crate::cli::{Action, BodyArgs, ListArgs, OutputFormat, ResourceArgs};
|
||||
use crate::output;
|
||||
|
||||
pub async fn run(
|
||||
client: &InvoiceNinjaClient,
|
||||
args: ResourceArgs,
|
||||
format: OutputFormat,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
match args.action {
|
||||
Action::List(list_args) => list(client, list_args, format).await,
|
||||
Action::Get { id } => get(client, &id, format).await,
|
||||
Action::Create(body_args) => create(client, body_args, format).await,
|
||||
Action::Update { id, body } => update(client, &id, body, format).await,
|
||||
Action::Delete { id } => delete(client, &id).await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn list(
|
||||
client: &InvoiceNinjaClient,
|
||||
args: ListArgs,
|
||||
format: OutputFormat,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut params = ListParams::new();
|
||||
if let Some(page) = args.page {
|
||||
params = params.page(page);
|
||||
}
|
||||
if let Some(per_page) = args.per_page {
|
||||
params = params.per_page(per_page);
|
||||
}
|
||||
if let Some(sort) = args.sort {
|
||||
params = params.sort(sort);
|
||||
}
|
||||
if let Some(filter) = args.filter {
|
||||
params = params.filter(filter);
|
||||
}
|
||||
if let Some(status) = args.status {
|
||||
params = params.status(status);
|
||||
}
|
||||
if let Some(include) = args.include {
|
||||
params = params.include(include);
|
||||
}
|
||||
for (k, v) in args.params {
|
||||
params = params.param(k, v);
|
||||
}
|
||||
|
||||
let purchase_orders = if args.all {
|
||||
client.list_all_purchase_orders(¶ms).await?
|
||||
} else {
|
||||
client.list_purchase_orders(¶ms).await?
|
||||
};
|
||||
|
||||
render(&purchase_orders, format)
|
||||
}
|
||||
|
||||
async fn get(
|
||||
client: &InvoiceNinjaClient,
|
||||
id: &str,
|
||||
format: OutputFormat,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let found = client.get_purchase_order(id).await?;
|
||||
render(std::slice::from_ref(&found), format)
|
||||
}
|
||||
|
||||
async fn create(
|
||||
client: &InvoiceNinjaClient,
|
||||
body_args: BodyArgs,
|
||||
format: OutputFormat,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let value = body::load(&body_args)?;
|
||||
let model: PurchaseOrder = serde_json::from_value(value)?;
|
||||
let created = client.create_purchase_order(&model).await?;
|
||||
render(std::slice::from_ref(&created), format)
|
||||
}
|
||||
|
||||
async fn update(
|
||||
client: &InvoiceNinjaClient,
|
||||
id: &str,
|
||||
body_args: BodyArgs,
|
||||
format: OutputFormat,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let patch = body::load(&body_args)?;
|
||||
let current = client.get_purchase_order(id).await?;
|
||||
let merged = body::merge_shallow(serde_json::to_value(¤t)?, patch);
|
||||
let model: PurchaseOrder = serde_json::from_value(merged)?;
|
||||
let updated = client.update_purchase_order(id, &model).await?;
|
||||
render(std::slice::from_ref(&updated), format)
|
||||
}
|
||||
|
||||
async fn delete(client: &InvoiceNinjaClient, id: &str) -> Result<(), Box<dyn std::error::Error>> {
|
||||
client.delete_purchase_order(id).await?;
|
||||
println!("deleted {id}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render(
|
||||
purchase_orders: &[PurchaseOrder],
|
||||
format: OutputFormat,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
match format {
|
||||
OutputFormat::Json => output::print_json(purchase_orders)?,
|
||||
OutputFormat::Table => print_table(purchase_orders),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_table(purchase_orders: &[PurchaseOrder]) {
|
||||
if purchase_orders.is_empty() {
|
||||
println!("(no records)");
|
||||
return;
|
||||
}
|
||||
println!(
|
||||
"{:<20} {:<15} {:<20} {:<8} {:>12} {:>12}",
|
||||
"id", "number", "vendor_id", "status", "amount", "balance"
|
||||
);
|
||||
for po in purchase_orders {
|
||||
println!(
|
||||
"{:<20} {:<15} {:<20} {:<8} {:>12.2} {:>12.2}",
|
||||
po.id.as_deref().unwrap_or(""),
|
||||
truncate(&po.number, 15),
|
||||
truncate(&po.vendor_id, 20),
|
||||
po.status_id,
|
||||
po.amount,
|
||||
po.balance
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn truncate(s: &str, max: usize) -> String {
|
||||
if s.chars().count() <= max {
|
||||
s.to_string()
|
||||
} else {
|
||||
s.chars().take(max.saturating_sub(1)).collect::<String>() + "…"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
//! `tasks` subcommand. Same shape as `clients.rs` — see the "Adding a
|
||||
//! resource" section in CLAUDE.md.
|
||||
|
||||
use invoiceninja_sdk::models::{ListParams, Task};
|
||||
use invoiceninja_sdk::InvoiceNinjaClient;
|
||||
|
||||
use crate::body;
|
||||
use crate::cli::{Action, BodyArgs, ListArgs, OutputFormat, ResourceArgs};
|
||||
use crate::output;
|
||||
|
||||
pub async fn run(
|
||||
client: &InvoiceNinjaClient,
|
||||
args: ResourceArgs,
|
||||
format: OutputFormat,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
match args.action {
|
||||
Action::List(list_args) => list(client, list_args, format).await,
|
||||
Action::Get { id } => get(client, &id, format).await,
|
||||
Action::Create(body_args) => create(client, body_args, format).await,
|
||||
Action::Update { id, body } => update(client, &id, body, format).await,
|
||||
Action::Delete { id } => delete(client, &id).await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn list(
|
||||
client: &InvoiceNinjaClient,
|
||||
args: ListArgs,
|
||||
format: OutputFormat,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut params = ListParams::new();
|
||||
if let Some(page) = args.page {
|
||||
params = params.page(page);
|
||||
}
|
||||
if let Some(per_page) = args.per_page {
|
||||
params = params.per_page(per_page);
|
||||
}
|
||||
if let Some(sort) = args.sort {
|
||||
params = params.sort(sort);
|
||||
}
|
||||
if let Some(filter) = args.filter {
|
||||
params = params.filter(filter);
|
||||
}
|
||||
if let Some(status) = args.status {
|
||||
params = params.status(status);
|
||||
}
|
||||
if let Some(include) = args.include {
|
||||
params = params.include(include);
|
||||
}
|
||||
for (k, v) in args.params {
|
||||
params = params.param(k, v);
|
||||
}
|
||||
|
||||
let tasks = if args.all {
|
||||
client.list_all_tasks(¶ms).await?
|
||||
} else {
|
||||
client.list_tasks(¶ms).await?
|
||||
};
|
||||
|
||||
render(&tasks, format)
|
||||
}
|
||||
|
||||
async fn get(
|
||||
client: &InvoiceNinjaClient,
|
||||
id: &str,
|
||||
format: OutputFormat,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let found = client.get_task(id).await?;
|
||||
render(std::slice::from_ref(&found), format)
|
||||
}
|
||||
|
||||
async fn create(
|
||||
client: &InvoiceNinjaClient,
|
||||
body_args: BodyArgs,
|
||||
format: OutputFormat,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let value = body::load(&body_args)?;
|
||||
let model: Task = serde_json::from_value(value)?;
|
||||
let created = client.create_task(&model).await?;
|
||||
render(std::slice::from_ref(&created), format)
|
||||
}
|
||||
|
||||
async fn update(
|
||||
client: &InvoiceNinjaClient,
|
||||
id: &str,
|
||||
body_args: BodyArgs,
|
||||
format: OutputFormat,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let patch = body::load(&body_args)?;
|
||||
let current = client.get_task(id).await?;
|
||||
let merged = body::merge_shallow(serde_json::to_value(¤t)?, patch);
|
||||
let model: Task = serde_json::from_value(merged)?;
|
||||
let updated = client.update_task(id, &model).await?;
|
||||
render(std::slice::from_ref(&updated), format)
|
||||
}
|
||||
|
||||
async fn delete(client: &InvoiceNinjaClient, id: &str) -> Result<(), Box<dyn std::error::Error>> {
|
||||
client.delete_task(id).await?;
|
||||
println!("deleted {id}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render(tasks: &[Task], format: OutputFormat) -> Result<(), Box<dyn std::error::Error>> {
|
||||
match format {
|
||||
OutputFormat::Json => output::print_json(tasks)?,
|
||||
OutputFormat::Table => print_table(tasks),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_table(tasks: &[Task]) {
|
||||
if tasks.is_empty() {
|
||||
println!("(no records)");
|
||||
return;
|
||||
}
|
||||
println!(
|
||||
"{:<20} {:<30} {:<20} {:<20} {:<8}",
|
||||
"id", "description", "client_id", "project_id", "status"
|
||||
);
|
||||
for t in tasks {
|
||||
println!(
|
||||
"{:<20} {:<30} {:<20} {:<20} {:<8}",
|
||||
t.id.as_deref().unwrap_or(""),
|
||||
truncate(&t.description, 30),
|
||||
truncate(&t.client_id, 20),
|
||||
truncate(&t.project_id, 20),
|
||||
t.status_id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn truncate(s: &str, max: usize) -> String {
|
||||
if s.chars().count() <= max {
|
||||
s.to_string()
|
||||
} else {
|
||||
s.chars().take(max.saturating_sub(1)).collect::<String>() + "…"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
//! `vendors` subcommand. Same shape as `clients.rs` — see the "Adding a
|
||||
//! resource" section in CLAUDE.md.
|
||||
|
||||
use invoiceninja_sdk::models::{ListParams, Vendor};
|
||||
use invoiceninja_sdk::InvoiceNinjaClient;
|
||||
|
||||
use crate::body;
|
||||
use crate::cli::{Action, BodyArgs, ListArgs, OutputFormat, ResourceArgs};
|
||||
use crate::output;
|
||||
|
||||
pub async fn run(
|
||||
client: &InvoiceNinjaClient,
|
||||
args: ResourceArgs,
|
||||
format: OutputFormat,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
match args.action {
|
||||
Action::List(list_args) => list(client, list_args, format).await,
|
||||
Action::Get { id } => get(client, &id, format).await,
|
||||
Action::Create(body_args) => create(client, body_args, format).await,
|
||||
Action::Update { id, body } => update(client, &id, body, format).await,
|
||||
Action::Delete { id } => delete(client, &id).await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn list(
|
||||
client: &InvoiceNinjaClient,
|
||||
args: ListArgs,
|
||||
format: OutputFormat,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut params = ListParams::new();
|
||||
if let Some(page) = args.page {
|
||||
params = params.page(page);
|
||||
}
|
||||
if let Some(per_page) = args.per_page {
|
||||
params = params.per_page(per_page);
|
||||
}
|
||||
if let Some(sort) = args.sort {
|
||||
params = params.sort(sort);
|
||||
}
|
||||
if let Some(filter) = args.filter {
|
||||
params = params.filter(filter);
|
||||
}
|
||||
if let Some(status) = args.status {
|
||||
params = params.status(status);
|
||||
}
|
||||
if let Some(include) = args.include {
|
||||
params = params.include(include);
|
||||
}
|
||||
for (k, v) in args.params {
|
||||
params = params.param(k, v);
|
||||
}
|
||||
|
||||
let vendors = if args.all {
|
||||
client.list_all_vendors(¶ms).await?
|
||||
} else {
|
||||
client.list_vendors(¶ms).await?
|
||||
};
|
||||
|
||||
render(&vendors, format)
|
||||
}
|
||||
|
||||
async fn get(
|
||||
client: &InvoiceNinjaClient,
|
||||
id: &str,
|
||||
format: OutputFormat,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let found = client.get_vendor(id).await?;
|
||||
render(std::slice::from_ref(&found), format)
|
||||
}
|
||||
|
||||
async fn create(
|
||||
client: &InvoiceNinjaClient,
|
||||
body_args: BodyArgs,
|
||||
format: OutputFormat,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let value = body::load(&body_args)?;
|
||||
let model: Vendor = serde_json::from_value(value)?;
|
||||
let created = client.create_vendor(&model).await?;
|
||||
render(std::slice::from_ref(&created), format)
|
||||
}
|
||||
|
||||
async fn update(
|
||||
client: &InvoiceNinjaClient,
|
||||
id: &str,
|
||||
body_args: BodyArgs,
|
||||
format: OutputFormat,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let patch = body::load(&body_args)?;
|
||||
let current = client.get_vendor(id).await?;
|
||||
let merged = body::merge_shallow(serde_json::to_value(¤t)?, patch);
|
||||
let model: Vendor = serde_json::from_value(merged)?;
|
||||
let updated = client.update_vendor(id, &model).await?;
|
||||
render(std::slice::from_ref(&updated), format)
|
||||
}
|
||||
|
||||
async fn delete(client: &InvoiceNinjaClient, id: &str) -> Result<(), Box<dyn std::error::Error>> {
|
||||
client.delete_vendor(id).await?;
|
||||
println!("deleted {id}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render(vendors: &[Vendor], format: OutputFormat) -> Result<(), Box<dyn std::error::Error>> {
|
||||
match format {
|
||||
OutputFormat::Json => output::print_json(vendors)?,
|
||||
OutputFormat::Table => print_table(vendors),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_table(vendors: &[Vendor]) {
|
||||
if vendors.is_empty() {
|
||||
println!("(no records)");
|
||||
return;
|
||||
}
|
||||
println!(
|
||||
"{:<20} {:<30} {:<20} {:<10}",
|
||||
"id", "name", "city", "country_id"
|
||||
);
|
||||
for v in vendors {
|
||||
println!(
|
||||
"{:<20} {:<30} {:<20} {:<10}",
|
||||
v.id.as_deref().unwrap_or(""),
|
||||
truncate(&v.name, 30),
|
||||
truncate(&v.city, 20),
|
||||
v.country_id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn truncate(s: &str, max: usize) -> String {
|
||||
if s.chars().count() <= max {
|
||||
s.to_string()
|
||||
} else {
|
||||
s.chars().take(max.saturating_sub(1)).collect::<String>() + "…"
|
||||
}
|
||||
}
|
||||
@@ -24,8 +24,15 @@ async fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
Command::Clients(args) => commands::clients::run(&client, args, cli.output).await,
|
||||
Command::Invoices(args) => commands::invoices::run(&client, args, cli.output).await,
|
||||
Command::Quotes(args) => commands::quotes::run(&client, args, cli.output).await,
|
||||
Command::Credits(args) => commands::credits::run(&client, args, cli.output).await,
|
||||
Command::Payments(args) => commands::payments::run(&client, args, cli.output).await,
|
||||
Command::Products(args) => commands::products::run(&client, args, cli.output).await,
|
||||
Command::Expenses(args) => commands::expenses::run(&client, args, cli.output).await,
|
||||
Command::Projects(args) => commands::projects::run(&client, args, cli.output).await,
|
||||
Command::Tasks(args) => commands::tasks::run(&client, args, cli.output).await,
|
||||
Command::Vendors(args) => commands::vendors::run(&client, args, cli.output).await,
|
||||
Command::PurchaseOrders(args) => {
|
||||
commands::purchase_orders::run(&client, args, cli.output).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user