crowdtime-cli 0.7.0__tar.gz → 0.8.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {crowdtime_cli-0.7.0 → crowdtime_cli-0.8.1}/PKG-INFO +1 -1
- {crowdtime_cli-0.7.0 → crowdtime_cli-0.8.1}/pyproject.toml +1 -1
- {crowdtime_cli-0.7.0 → crowdtime_cli-0.8.1}/src/crowdtime_cli/__init__.py +1 -1
- {crowdtime_cli-0.7.0 → crowdtime_cli-0.8.1}/src/crowdtime_cli/client.py +12 -10
- {crowdtime_cli-0.7.0 → crowdtime_cli-0.8.1}/src/crowdtime_cli/commands/billing_cmd.py +3 -3
- {crowdtime_cli-0.7.0 → crowdtime_cli-0.8.1}/src/crowdtime_cli/commands/invoice_cmd.py +155 -10
- {crowdtime_cli-0.7.0 → crowdtime_cli-0.8.1}/src/crowdtime_cli/commands/timesheet_cmd.py +24 -0
- {crowdtime_cli-0.7.0 → crowdtime_cli-0.8.1}/src/crowdtime_cli/skills/crowdtime/SKILL.md +8 -7
- {crowdtime_cli-0.7.0 → crowdtime_cli-0.8.1}/src/crowdtime_cli/skills/crowdtime/references/commands.md +46 -25
- {crowdtime_cli-0.7.0 → crowdtime_cli-0.8.1}/src/crowdtime_cli/skills/crowdtime/references/workflows.md +17 -10
- {crowdtime_cli-0.7.0 → crowdtime_cli-0.8.1}/.gitignore +0 -0
- {crowdtime_cli-0.7.0 → crowdtime_cli-0.8.1}/LICENSE +0 -0
- {crowdtime_cli-0.7.0 → crowdtime_cli-0.8.1}/README.md +0 -0
- {crowdtime_cli-0.7.0 → crowdtime_cli-0.8.1}/src/crowdtime_cli/auth.py +0 -0
- {crowdtime_cli-0.7.0 → crowdtime_cli-0.8.1}/src/crowdtime_cli/commands/__init__.py +0 -0
- {crowdtime_cli-0.7.0 → crowdtime_cli-0.8.1}/src/crowdtime_cli/commands/ai_cmd.py +0 -0
- {crowdtime_cli-0.7.0 → crowdtime_cli-0.8.1}/src/crowdtime_cli/commands/auth_cmd.py +0 -0
- {crowdtime_cli-0.7.0 → crowdtime_cli-0.8.1}/src/crowdtime_cli/commands/clients_cmd.py +0 -0
- {crowdtime_cli-0.7.0 → crowdtime_cli-0.8.1}/src/crowdtime_cli/commands/config_cmd.py +0 -0
- {crowdtime_cli-0.7.0 → crowdtime_cli-0.8.1}/src/crowdtime_cli/commands/expense_cmd.py +0 -0
- {crowdtime_cli-0.7.0 → crowdtime_cli-0.8.1}/src/crowdtime_cli/commands/favorites_cmd.py +0 -0
- {crowdtime_cli-0.7.0 → crowdtime_cli-0.8.1}/src/crowdtime_cli/commands/log_cmd.py +0 -0
- {crowdtime_cli-0.7.0 → crowdtime_cli-0.8.1}/src/crowdtime_cli/commands/org_cmd.py +0 -0
- {crowdtime_cli-0.7.0 → crowdtime_cli-0.8.1}/src/crowdtime_cli/commands/payroll_cmd.py +0 -0
- {crowdtime_cli-0.7.0 → crowdtime_cli-0.8.1}/src/crowdtime_cli/commands/projects_cmd.py +0 -0
- {crowdtime_cli-0.7.0 → crowdtime_cli-0.8.1}/src/crowdtime_cli/commands/report_cmd.py +0 -0
- {crowdtime_cli-0.7.0 → crowdtime_cli-0.8.1}/src/crowdtime_cli/commands/skill_cmd.py +0 -0
- {crowdtime_cli-0.7.0 → crowdtime_cli-0.8.1}/src/crowdtime_cli/commands/tasks_cmd.py +0 -0
- {crowdtime_cli-0.7.0 → crowdtime_cli-0.8.1}/src/crowdtime_cli/commands/timer_cmd.py +0 -0
- {crowdtime_cli-0.7.0 → crowdtime_cli-0.8.1}/src/crowdtime_cli/config.py +0 -0
- {crowdtime_cli-0.7.0 → crowdtime_cli-0.8.1}/src/crowdtime_cli/formatters.py +0 -0
- {crowdtime_cli-0.7.0 → crowdtime_cli-0.8.1}/src/crowdtime_cli/main.py +0 -0
- {crowdtime_cli-0.7.0 → crowdtime_cli-0.8.1}/src/crowdtime_cli/models.py +0 -0
- {crowdtime_cli-0.7.0 → crowdtime_cli-0.8.1}/src/crowdtime_cli/oauth.py +0 -0
- {crowdtime_cli-0.7.0 → crowdtime_cli-0.8.1}/src/crowdtime_cli/resolvers.py +0 -0
- {crowdtime_cli-0.7.0 → crowdtime_cli-0.8.1}/src/crowdtime_cli/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: crowdtime-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.1
|
|
4
4
|
Summary: AI-powered time tracking CLI — a modern, developer-friendly alternative to Harvest
|
|
5
5
|
Project-URL: Homepage, https://crowdtime.lat
|
|
6
6
|
Project-URL: Documentation, https://crowdtime.lat/docs
|
|
@@ -140,16 +140,18 @@ class CrowdTimeClient:
|
|
|
140
140
|
detail = response.json()
|
|
141
141
|
except Exception:
|
|
142
142
|
detail = {}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
143
|
+
code = detail.get("code", "") if isinstance(detail, dict) else ""
|
|
144
|
+
if code == "subscription_required":
|
|
145
|
+
console.print(
|
|
146
|
+
"\n[yellow]Your organization does not have an active subscription.[/yellow]\n"
|
|
147
|
+
"\n Start your free trial: [bold]ct billing checkout[/bold]\n"
|
|
148
|
+
)
|
|
149
|
+
else:
|
|
150
|
+
console.print(
|
|
151
|
+
"\n[yellow]Your organization's subscription is inactive.[/yellow]\n"
|
|
152
|
+
"\n Manage your billing: [bold]ct billing portal[/bold]\n"
|
|
153
|
+
)
|
|
154
|
+
raise SystemExit(1)
|
|
153
155
|
|
|
154
156
|
if response.status_code == 404:
|
|
155
157
|
raise APIError("Not found", status_code=404)
|
|
@@ -205,9 +205,9 @@ def portal(
|
|
|
205
205
|
)
|
|
206
206
|
raise typer.Exit(1)
|
|
207
207
|
if e.status_code == 404:
|
|
208
|
-
|
|
209
|
-
"No subscription found
|
|
210
|
-
"
|
|
208
|
+
console.print(
|
|
209
|
+
"[yellow]No subscription found for this organization.[/yellow]\n"
|
|
210
|
+
"\n Start your free trial: [bold]ct billing checkout[/bold]"
|
|
211
211
|
)
|
|
212
212
|
raise typer.Exit(1)
|
|
213
213
|
format_error(e.message)
|
|
@@ -397,9 +397,9 @@ def create_invoice(
|
|
|
397
397
|
client = CrowdTimeClient(require_auth=True, require_org=True)
|
|
398
398
|
|
|
399
399
|
payload: dict = {
|
|
400
|
-
"
|
|
401
|
-
"
|
|
402
|
-
"
|
|
400
|
+
"client_id": client_id,
|
|
401
|
+
"date_from": start,
|
|
402
|
+
"date_to": end,
|
|
403
403
|
}
|
|
404
404
|
if group_by:
|
|
405
405
|
payload["group_by"] = group_by
|
|
@@ -453,23 +453,44 @@ def create_invoice(
|
|
|
453
453
|
def create_blank_invoice(
|
|
454
454
|
client_id: str = typer.Option(..., "--client", "-c", help="Client ID."),
|
|
455
455
|
issue_date: str = typer.Option(..., "--issue-date", help="Issue date."),
|
|
456
|
-
due_date: Optional[str] = typer.Option(None, "--due-date", help="Due date."),
|
|
456
|
+
due_date: Optional[str] = typer.Option(None, "--due-date", help="Due date (auto-calculated from --payment-terms if omitted)."),
|
|
457
|
+
subject: Optional[str] = typer.Option(None, "--subject", "-s", help="Invoice subject line."),
|
|
458
|
+
payment_terms: Optional[str] = typer.Option(
|
|
459
|
+
None, "--payment-terms",
|
|
460
|
+
help="Payment terms: receipt, net15, net30, or number of days. Auto-calculates due date.",
|
|
461
|
+
),
|
|
457
462
|
currency: Optional[str] = typer.Option(None, "--currency", help="Currency code (e.g. USD, EUR)."),
|
|
458
463
|
notes: Optional[str] = typer.Option(None, "--notes", "-n", help="Invoice notes."),
|
|
464
|
+
terms: Optional[str] = typer.Option(None, "--terms", help="Payment terms text (e.g. bank account details)."),
|
|
459
465
|
output_json: bool = typer.Option(False, "--json", help="Output as JSON."),
|
|
460
466
|
) -> None:
|
|
461
467
|
"""Create an empty draft invoice for manual line item addition.
|
|
462
468
|
|
|
463
469
|
Examples:
|
|
464
|
-
ct invoice create-blank --client <id> --issue-date
|
|
470
|
+
ct invoice create-blank --client <id> --issue-date today
|
|
471
|
+
ct invoice create-blank --client <id> --issue-date 2026-03-15 --payment-terms net30
|
|
465
472
|
ct invoice create-blank --client <id> --issue-date today --due-date 2026-04-15 --currency EUR
|
|
473
|
+
ct invoice create-blank --client <id> --issue-date today --terms "Wire to IBAN XX" --notes "March work"
|
|
466
474
|
"""
|
|
467
475
|
try:
|
|
468
|
-
|
|
476
|
+
parsed_issue_date = parse_date(issue_date)
|
|
477
|
+
parsed_issue = format_date(parsed_issue_date)
|
|
469
478
|
except ValueError as e:
|
|
470
479
|
format_error(str(e))
|
|
471
480
|
raise typer.Exit(1)
|
|
472
481
|
|
|
482
|
+
# Resolve payment terms days
|
|
483
|
+
payment_terms_days: int | None = None
|
|
484
|
+
if payment_terms is not None:
|
|
485
|
+
payment_terms_days = _resolve_payment_terms(payment_terms)
|
|
486
|
+
if payment_terms_days is None:
|
|
487
|
+
format_error(
|
|
488
|
+
f"Invalid --payment-terms: '{payment_terms}'. "
|
|
489
|
+
"Use: receipt, net15, net30, or a number of days."
|
|
490
|
+
)
|
|
491
|
+
raise typer.Exit(1)
|
|
492
|
+
|
|
493
|
+
# Resolve due date: explicit --due-date > calculated from --payment-terms > default net30
|
|
473
494
|
parsed_due = None
|
|
474
495
|
if due_date:
|
|
475
496
|
try:
|
|
@@ -477,19 +498,30 @@ def create_blank_invoice(
|
|
|
477
498
|
except ValueError as e:
|
|
478
499
|
format_error(str(e))
|
|
479
500
|
raise typer.Exit(1)
|
|
501
|
+
elif payment_terms_days is not None:
|
|
502
|
+
parsed_due = format_date(parsed_issue_date + timedelta(days=payment_terms_days))
|
|
503
|
+
else:
|
|
504
|
+
# Default to net30
|
|
505
|
+
payment_terms_days = 30
|
|
506
|
+
parsed_due = format_date(parsed_issue_date + timedelta(days=30))
|
|
480
507
|
|
|
481
508
|
api_client = CrowdTimeClient(require_auth=True, require_org=True)
|
|
482
509
|
|
|
483
510
|
payload: dict = {
|
|
484
511
|
"client": client_id,
|
|
485
512
|
"issue_date": parsed_issue,
|
|
513
|
+
"due_date": parsed_due,
|
|
486
514
|
}
|
|
487
|
-
if
|
|
488
|
-
payload["
|
|
515
|
+
if subject:
|
|
516
|
+
payload["subject"] = subject
|
|
489
517
|
if currency:
|
|
490
518
|
payload["currency"] = currency
|
|
491
519
|
if notes:
|
|
492
520
|
payload["notes"] = notes
|
|
521
|
+
if terms:
|
|
522
|
+
payload["terms"] = terms
|
|
523
|
+
if payment_terms_days is not None:
|
|
524
|
+
payload["payment_terms_days"] = payment_terms_days
|
|
493
525
|
|
|
494
526
|
try:
|
|
495
527
|
data = api_client.post("/invoices/", data=payload)
|
|
@@ -506,6 +538,119 @@ def create_blank_invoice(
|
|
|
506
538
|
raise typer.Exit(1)
|
|
507
539
|
|
|
508
540
|
|
|
541
|
+
@app.command("update")
|
|
542
|
+
def update_invoice(
|
|
543
|
+
invoice_id: str = typer.Argument(..., help="Invoice ID to update (must be draft)."),
|
|
544
|
+
invoice_number: Optional[str] = typer.Option(None, "--number", help="Invoice number (e.g. INV-0078)."),
|
|
545
|
+
subject: Optional[str] = typer.Option(None, "--subject", "-s", help="Invoice subject line."),
|
|
546
|
+
issue_date: Optional[str] = typer.Option(None, "--issue-date", help="Issue date."),
|
|
547
|
+
due_date: Optional[str] = typer.Option(None, "--due-date", help="Due date."),
|
|
548
|
+
notes: Optional[str] = typer.Option(None, "--notes", "-n", help="Invoice notes."),
|
|
549
|
+
terms: Optional[str] = typer.Option(None, "--terms", help="Payment terms text (e.g. bank account details)."),
|
|
550
|
+
footer: Optional[str] = typer.Option(None, "--footer", help="Invoice footer text."),
|
|
551
|
+
currency: Optional[str] = typer.Option(None, "--currency", help="Currency code (e.g. USD, EUR)."),
|
|
552
|
+
tax_rate: Optional[float] = typer.Option(None, "--tax-rate", help="Tax rate percentage (e.g. 21 for 21%%)."),
|
|
553
|
+
payment_terms: Optional[str] = typer.Option(
|
|
554
|
+
None, "--payment-terms",
|
|
555
|
+
help="Payment terms: receipt, net15, net30, or number of days.",
|
|
556
|
+
),
|
|
557
|
+
from_name: Optional[str] = typer.Option(None, "--from-name", help="Issuer/sender name."),
|
|
558
|
+
from_email: Optional[str] = typer.Option(None, "--from-email", help="Issuer/sender email."),
|
|
559
|
+
from_address: Optional[str] = typer.Option(None, "--from-address", help="Issuer/sender address."),
|
|
560
|
+
client_name: Optional[str] = typer.Option(None, "--client-name", help="Bill-to client name."),
|
|
561
|
+
client_email: Optional[str] = typer.Option(None, "--client-email", help="Bill-to client email."),
|
|
562
|
+
client_address: Optional[str] = typer.Option(None, "--client-address", help="Bill-to client address."),
|
|
563
|
+
output_json: bool = typer.Option(False, "--json", help="Output as JSON."),
|
|
564
|
+
) -> None:
|
|
565
|
+
"""Update a draft invoice's fields.
|
|
566
|
+
|
|
567
|
+
Only draft invoices can be edited. Once sent, use void + recreate.
|
|
568
|
+
|
|
569
|
+
Examples:
|
|
570
|
+
ct invoice update <id> --subject "March 2026 Development"
|
|
571
|
+
ct invoice update <id> --terms "Wire to IBAN XX" --notes "Net 30"
|
|
572
|
+
ct invoice update <id> --tax-rate 21 --due-date 2026-04-15
|
|
573
|
+
ct invoice update <id> --number "INV-0078"
|
|
574
|
+
ct invoice update <id> --from-name "Acme Inc" --from-email "billing@acme.com"
|
|
575
|
+
ct invoice update <id> --client-name "Beta Corp" --client-email "ap@beta.com"
|
|
576
|
+
"""
|
|
577
|
+
# Build payload from provided options
|
|
578
|
+
payload: dict = {}
|
|
579
|
+
|
|
580
|
+
if invoice_number is not None:
|
|
581
|
+
payload["invoice_number"] = invoice_number
|
|
582
|
+
if subject is not None:
|
|
583
|
+
payload["subject"] = subject
|
|
584
|
+
if notes is not None:
|
|
585
|
+
payload["notes"] = notes
|
|
586
|
+
if terms is not None:
|
|
587
|
+
payload["terms"] = terms
|
|
588
|
+
if footer is not None:
|
|
589
|
+
payload["footer"] = footer
|
|
590
|
+
if currency is not None:
|
|
591
|
+
payload["currency"] = currency
|
|
592
|
+
if tax_rate is not None:
|
|
593
|
+
payload["tax_rate"] = str(tax_rate)
|
|
594
|
+
if from_name is not None:
|
|
595
|
+
payload["from_name"] = from_name
|
|
596
|
+
if from_email is not None:
|
|
597
|
+
payload["from_email"] = from_email
|
|
598
|
+
if from_address is not None:
|
|
599
|
+
payload["from_address"] = from_address
|
|
600
|
+
if client_name is not None:
|
|
601
|
+
payload["client_name"] = client_name
|
|
602
|
+
if client_email is not None:
|
|
603
|
+
payload["client_email"] = client_email
|
|
604
|
+
if client_address is not None:
|
|
605
|
+
payload["client_address"] = client_address
|
|
606
|
+
|
|
607
|
+
if issue_date is not None:
|
|
608
|
+
try:
|
|
609
|
+
payload["issue_date"] = format_date(parse_date(issue_date))
|
|
610
|
+
except ValueError as e:
|
|
611
|
+
format_error(str(e))
|
|
612
|
+
raise typer.Exit(1)
|
|
613
|
+
|
|
614
|
+
if due_date is not None:
|
|
615
|
+
try:
|
|
616
|
+
payload["due_date"] = format_date(parse_date(due_date))
|
|
617
|
+
except ValueError as e:
|
|
618
|
+
format_error(str(e))
|
|
619
|
+
raise typer.Exit(1)
|
|
620
|
+
|
|
621
|
+
if payment_terms is not None:
|
|
622
|
+
days = _resolve_payment_terms(payment_terms)
|
|
623
|
+
if days is None:
|
|
624
|
+
format_error(
|
|
625
|
+
f"Invalid --payment-terms: '{payment_terms}'. "
|
|
626
|
+
"Use: receipt, net15, net30, or a number of days."
|
|
627
|
+
)
|
|
628
|
+
raise typer.Exit(1)
|
|
629
|
+
payload["payment_terms_days"] = days
|
|
630
|
+
|
|
631
|
+
if not payload:
|
|
632
|
+
format_error("No fields to update. Provide at least one option.")
|
|
633
|
+
raise typer.Exit(1)
|
|
634
|
+
|
|
635
|
+
client = CrowdTimeClient(require_auth=True, require_org=True)
|
|
636
|
+
|
|
637
|
+
try:
|
|
638
|
+
data = client.patch(f"/invoices/{invoice_id}/", data=payload)
|
|
639
|
+
|
|
640
|
+
if output_json:
|
|
641
|
+
print_json(data)
|
|
642
|
+
else:
|
|
643
|
+
inv_number = data.get("invoice_number", data.get("number", invoice_id))
|
|
644
|
+
fields = ", ".join(payload.keys())
|
|
645
|
+
format_success(f"Invoice {inv_number} updated ({fields}).")
|
|
646
|
+
except APIError as e:
|
|
647
|
+
if e.status_code == 404:
|
|
648
|
+
format_error(f"Invoice '{invoice_id}' not found.")
|
|
649
|
+
else:
|
|
650
|
+
format_error(e.message)
|
|
651
|
+
raise typer.Exit(1)
|
|
652
|
+
|
|
653
|
+
|
|
509
654
|
@app.command("send")
|
|
510
655
|
def send_invoice(
|
|
511
656
|
invoice_id: str = typer.Argument(..., help="Invoice ID to send."),
|
|
@@ -562,7 +707,7 @@ def void_invoice(
|
|
|
562
707
|
client = CrowdTimeClient(require_auth=True, require_org=True)
|
|
563
708
|
|
|
564
709
|
try:
|
|
565
|
-
data = client.post(f"/invoices/{invoice_id}/void/", data={"
|
|
710
|
+
data = client.post(f"/invoices/{invoice_id}/void/", data={"void_reason": reason})
|
|
566
711
|
|
|
567
712
|
if output_json:
|
|
568
713
|
print_json(data)
|
|
@@ -754,7 +899,7 @@ def add_line_item(
|
|
|
754
899
|
"unit_price": str(parsed_rate),
|
|
755
900
|
}
|
|
756
901
|
if taxable is not None:
|
|
757
|
-
payload["
|
|
902
|
+
payload["is_taxable"] = taxable
|
|
758
903
|
|
|
759
904
|
try:
|
|
760
905
|
data = client.post(f"/invoices/{invoice_id}/line-items/", data=payload)
|
|
@@ -260,6 +260,30 @@ def reject_timesheet(
|
|
|
260
260
|
raise typer.Exit(1)
|
|
261
261
|
|
|
262
262
|
|
|
263
|
+
@app.command("recall")
|
|
264
|
+
def recall_timesheet(
|
|
265
|
+
timesheet_id: str = typer.Argument(..., help="Timesheet ID to recall."),
|
|
266
|
+
output_json: bool = typer.Option(False, "--json", help="Output as JSON."),
|
|
267
|
+
) -> None:
|
|
268
|
+
"""Recall a submitted timesheet back to draft (owner only, before approval).
|
|
269
|
+
|
|
270
|
+
Examples:
|
|
271
|
+
ct timesheet recall <timesheet-id>
|
|
272
|
+
"""
|
|
273
|
+
client = CrowdTimeClient(require_auth=True, require_org=True)
|
|
274
|
+
|
|
275
|
+
try:
|
|
276
|
+
result = client.post(f"/timesheets/{timesheet_id}/recall/")
|
|
277
|
+
|
|
278
|
+
if output_json:
|
|
279
|
+
print_json(result)
|
|
280
|
+
else:
|
|
281
|
+
format_success("Timesheet recalled to draft. You can now edit and resubmit.")
|
|
282
|
+
except APIError as e:
|
|
283
|
+
format_error(e.message)
|
|
284
|
+
raise typer.Exit(1)
|
|
285
|
+
|
|
286
|
+
|
|
263
287
|
@app.command("team")
|
|
264
288
|
def team_overview(
|
|
265
289
|
period_start: Optional[str] = typer.Option(
|
|
@@ -177,6 +177,7 @@ ct favorites delete <id>
|
|
|
177
177
|
ct timesheet list # List your timesheets
|
|
178
178
|
ct timesheet list --status submitted # Filter by status
|
|
179
179
|
ct timesheet submit --from 2026-03-10 --to 2026-03-16 # Submit for a period
|
|
180
|
+
ct timesheet recall <id> # Recall submitted timesheet back to draft
|
|
180
181
|
ct timesheet approve <id> # Approve a submitted timesheet (manager+)
|
|
181
182
|
ct timesheet reject <id> --notes "Missing entries for Wednesday" # Reject with notes
|
|
182
183
|
ct timesheet team # Team overview for this week (manager+)
|
|
@@ -189,13 +190,13 @@ ct timesheet team --last-week # Last week's overview
|
|
|
189
190
|
ct invoice list # List all invoices
|
|
190
191
|
ct invoice list --status draft # Filter by status
|
|
191
192
|
ct invoice show <id> # Invoice details with line items
|
|
192
|
-
ct invoice create --client
|
|
193
|
-
ct invoice create --client
|
|
194
|
-
ct invoice
|
|
195
|
-
ct invoice
|
|
193
|
+
ct invoice create --client <id> --period last-month --payment-terms net30 # From time entries
|
|
194
|
+
ct invoice create-blank --client <id> --issue-date today --payment-terms net30 # Empty draft
|
|
195
|
+
ct invoice update <id> --terms "Wire to IBAN XX" --notes "March" # Edit draft fields
|
|
196
|
+
ct invoice add-item <id> --desc "Development" --qty 40 --rate 150 # Add line item
|
|
196
197
|
ct invoice send <id> # Mark as sent
|
|
197
|
-
ct invoice
|
|
198
|
-
ct invoice void <id>
|
|
198
|
+
ct invoice pay <id> --amount 1500 # Record payment
|
|
199
|
+
ct invoice void <id> -r "Duplicate" # Void an invoice
|
|
199
200
|
ct invoice duplicate <id> # Duplicate an existing invoice
|
|
200
201
|
```
|
|
201
202
|
|
|
@@ -319,6 +320,6 @@ Read `references/commands.md` for the full reference of every command, subcomman
|
|
|
319
320
|
12. **Output formats** — `ct report` supports `table`, `json`, `csv`, and `markdown` via `--format`; suggest `csv` for spreadsheet export, `markdown` for docs
|
|
320
321
|
13. **Ask for details when vague** — if the user's request is ambiguous (e.g. "log 2 hours" with no description or project), ask them for the missing context rather than guessing. It's better to ask one clarifying question than to log something wrong that needs to be edited or deleted
|
|
321
322
|
14. **Expenses** — when the user mentions expenses, receipts, or purchases, use the `ct expense` commands. Always suggest a category and ask if the expense is billable. If recording a billable expense for a client project, always set `--project` and `--billable` so it gets included automatically when the client is invoiced
|
|
322
|
-
15. **Invoices include expenses** — when creating invoices with `ct invoice
|
|
323
|
+
15. **Invoices include expenses** — when creating invoices with `ct invoice create --period`, billable expenses from the same period are automatically included as line items. Mention this to users so they know their expenses will appear on the invoice
|
|
323
324
|
16. **Client contacts** — use `ct clients contacts` and `ct clients add-contact` to manage multiple contacts per client. Set `--primary` for the main point of contact
|
|
324
325
|
17. **Payroll** — payroll data is strictly confidential. Only HR managers, admins, and owners can access it. When the user asks about payroll, always use the `ct payroll` commands. Compensation types are `hourly` (rate × hours) or `salary` (fixed monthly). Rate changes are always effective from the 1st of a month. The payroll workflow is: configure compensation → run liquidation → approve → mark paid
|
|
@@ -1151,6 +1151,14 @@ ct timesheet reject TIMESHEET_ID --notes NOTES [--json]
|
|
|
1151
1151
|
|
|
1152
1152
|
Rejects a submitted timesheet with notes (requires manager+ role). Endpoint: `POST /timesheets/{id}/reject/`
|
|
1153
1153
|
|
|
1154
|
+
### ct timesheet recall
|
|
1155
|
+
|
|
1156
|
+
```
|
|
1157
|
+
ct timesheet recall TIMESHEET_ID [--json]
|
|
1158
|
+
```
|
|
1159
|
+
|
|
1160
|
+
Recalls a submitted timesheet back to draft so you can edit and resubmit. Only the timesheet owner can recall, and only before it's been approved or rejected. Endpoint: `POST /timesheets/{id}/recall/`
|
|
1161
|
+
|
|
1154
1162
|
### ct timesheet team
|
|
1155
1163
|
|
|
1156
1164
|
```
|
|
@@ -1197,49 +1205,62 @@ Shows invoice details including line items, payments, and totals. Endpoint: `GET
|
|
|
1197
1205
|
### ct invoice create
|
|
1198
1206
|
|
|
1199
1207
|
```
|
|
1200
|
-
ct invoice create --client CLIENT
|
|
1208
|
+
ct invoice create --client CLIENT --from DATE --to DATE [--period PERIOD] [--group-by GROUP] [--tax-rate RATE] [--payment-terms TERMS] [--notes NOTES] [--terms TEXT] [--json]
|
|
1201
1209
|
```
|
|
1202
1210
|
|
|
1211
|
+
Creates an invoice from tracked time entries. Requires either `--from`/`--to` or `--period`.
|
|
1212
|
+
|
|
1203
1213
|
| Option | Type | Description |
|
|
1204
1214
|
|--------|------|-------------|
|
|
1205
|
-
| `--client` | string (required) | Client
|
|
1206
|
-
| `--
|
|
1207
|
-
| `--
|
|
1208
|
-
| `--
|
|
1209
|
-
| `--
|
|
1210
|
-
| `--
|
|
1215
|
+
| `--client` | string (required) | Client ID |
|
|
1216
|
+
| `--from` | string | Period start date |
|
|
1217
|
+
| `--to` | string | Period end date |
|
|
1218
|
+
| `--period` | string | Preset: `last-week`, `last-2-weeks`, `last-month`, `this-month` |
|
|
1219
|
+
| `--group-by` | string | Group line items by: `project`, `task`, `date`, `entry` |
|
|
1220
|
+
| `--tax-rate` | float | Tax rate percentage (e.g. 21 for 21%) |
|
|
1221
|
+
| `--payment-terms` | string | Payment terms: `receipt`, `net15`, `net30`, or number of days |
|
|
1211
1222
|
| `--notes` | string | Invoice notes |
|
|
1223
|
+
| `--terms` | string | Payment terms text |
|
|
1212
1224
|
| `--json` | flag | JSON output |
|
|
1213
1225
|
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
When `--payment-terms` is specified, the due date is calculated automatically from the issue date (e.g. `net30` sets the due date to 30 days from now). This overrides `--due` if both are provided.
|
|
1226
|
+
Billable time entries and expenses from the period are automatically imported as line items.
|
|
1217
1227
|
|
|
1218
1228
|
**Usage examples:**
|
|
1219
1229
|
```bash
|
|
1220
|
-
ct invoice create --client
|
|
1221
|
-
ct invoice create --client
|
|
1222
|
-
ct invoice create --client
|
|
1223
|
-
ct invoice create --client "Gamma LLC" --period last-2-weeks --payment-terms receipt
|
|
1230
|
+
ct invoice create --client <id> --period last-month --payment-terms net30
|
|
1231
|
+
ct invoice create --client <id> --from 2026-03-01 --to 2026-03-31
|
|
1232
|
+
ct invoice create --client <id> --period last-2-weeks --group-by project
|
|
1224
1233
|
```
|
|
1225
1234
|
|
|
1235
|
+
Endpoint: `POST /invoices/from-time-entries/`
|
|
1236
|
+
|
|
1237
|
+
### ct invoice create-blank
|
|
1238
|
+
|
|
1239
|
+
```
|
|
1240
|
+
ct invoice create-blank --client CLIENT --issue-date DATE [--due-date DATE] [--payment-terms TERMS] [--subject SUBJECT] [--terms TEXT] [--notes NOTES] [--currency CUR] [--json]
|
|
1241
|
+
```
|
|
1242
|
+
|
|
1243
|
+
Creates an empty draft invoice for manual line item addition. If `--due-date` is omitted, it's auto-calculated from `--payment-terms` (default: net30). `--terms` sets the payment terms text (e.g. bank account details).
|
|
1244
|
+
|
|
1226
1245
|
Endpoint: `POST /invoices/`
|
|
1227
1246
|
|
|
1228
|
-
### ct invoice
|
|
1247
|
+
### ct invoice update
|
|
1229
1248
|
|
|
1230
1249
|
```
|
|
1231
|
-
ct invoice
|
|
1250
|
+
ct invoice update INVOICE_ID [--number NUM] [--subject SUBJECT] [--issue-date DATE] [--due-date DATE] [--notes NOTES] [--terms TEXT] [--footer TEXT] [--currency CUR] [--tax-rate RATE] [--payment-terms TERMS] [--from-name NAME] [--from-email EMAIL] [--from-address ADDR] [--client-name NAME] [--client-email EMAIL] [--client-address ADDR] [--json]
|
|
1232
1251
|
```
|
|
1233
1252
|
|
|
1234
|
-
|
|
1253
|
+
Updates fields on a draft invoice. All fields are editable: invoice number, from/bill-to details, dates, currency, payment terms, notes, terms, footer. Only draft invoices can be edited — once sent, use void + recreate.
|
|
1254
|
+
|
|
1255
|
+
Endpoint: `PATCH /invoices/{id}/`
|
|
1235
1256
|
|
|
1236
|
-
### ct invoice
|
|
1257
|
+
### ct invoice add-item
|
|
1237
1258
|
|
|
1238
1259
|
```
|
|
1239
|
-
ct invoice
|
|
1260
|
+
ct invoice add-item INVOICE_ID --description DESC --qty QTY --rate RATE [--taxable/--no-taxable] [--json]
|
|
1240
1261
|
```
|
|
1241
1262
|
|
|
1242
|
-
|
|
1263
|
+
Adds a line item to a draft invoice. Endpoint: `POST /invoices/{id}/line-items/`
|
|
1243
1264
|
|
|
1244
1265
|
### ct invoice send
|
|
1245
1266
|
|
|
@@ -1249,21 +1270,21 @@ ct invoice send INVOICE_ID [--json]
|
|
|
1249
1270
|
|
|
1250
1271
|
Marks invoice as sent. Endpoint: `POST /invoices/{id}/send/`
|
|
1251
1272
|
|
|
1252
|
-
### ct invoice
|
|
1273
|
+
### ct invoice pay
|
|
1253
1274
|
|
|
1254
1275
|
```
|
|
1255
|
-
ct invoice
|
|
1276
|
+
ct invoice pay INVOICE_ID --amount AMOUNT [--date DATE] [--method METHOD] [--reference REF] [--notes NOTES] [--json]
|
|
1256
1277
|
```
|
|
1257
1278
|
|
|
1258
|
-
Records
|
|
1279
|
+
Records a payment against an invoice. Method choices: `bank_transfer`, `credit_card`, `check`, `cash`, `paypal`, `other`. Endpoint: `POST /invoices/{id}/payments/`
|
|
1259
1280
|
|
|
1260
1281
|
### ct invoice void
|
|
1261
1282
|
|
|
1262
1283
|
```
|
|
1263
|
-
ct invoice void INVOICE_ID
|
|
1284
|
+
ct invoice void INVOICE_ID --reason/-r REASON [--json]
|
|
1264
1285
|
```
|
|
1265
1286
|
|
|
1266
|
-
Voids an invoice
|
|
1287
|
+
Voids an invoice with a required reason. Endpoint: `POST /invoices/{id}/void/`
|
|
1267
1288
|
|
|
1268
1289
|
### ct invoice duplicate
|
|
1269
1290
|
|
|
@@ -286,6 +286,13 @@ ct timesheet submit --from 2026-03-09 --to 2026-03-15
|
|
|
286
286
|
|
|
287
287
|
# Check submission status
|
|
288
288
|
ct timesheet list
|
|
289
|
+
|
|
290
|
+
# Made a mistake? Recall it back to draft (before approval)
|
|
291
|
+
ct timesheet recall <timesheet-id>
|
|
292
|
+
|
|
293
|
+
# Fix and resubmit
|
|
294
|
+
ct l -p project-alpha -d friday 2h "missed entry"
|
|
295
|
+
ct timesheet submit --from 2026-03-09 --to 2026-03-15
|
|
289
296
|
```
|
|
290
297
|
|
|
291
298
|
### Manager: Review Team Timesheets
|
|
@@ -455,12 +462,13 @@ ct invoice create --client "Acme Corp" --period last-month --payment-terms net30
|
|
|
455
462
|
# The output shows how many time entries and expenses were included.
|
|
456
463
|
# Billable expenses from the period are automatically added as line items.
|
|
457
464
|
|
|
458
|
-
# 2. Or create
|
|
459
|
-
ct invoice create --client
|
|
460
|
-
ct invoice
|
|
465
|
+
# 2. Or create a blank draft and add items manually
|
|
466
|
+
ct invoice create-blank --client <client-id> --issue-date today --payment-terms net30
|
|
467
|
+
ct invoice add-item <invoice-id> --desc "Development" --qty 40 --rate 150
|
|
468
|
+
ct invoice add-item <invoice-id> --desc "Setup fee" --qty 1 --rate 500
|
|
461
469
|
|
|
462
|
-
# 3.
|
|
463
|
-
ct invoice
|
|
470
|
+
# 3. Update draft fields (terms, notes, etc.)
|
|
471
|
+
ct invoice update <invoice-id> --terms "Wire to IBAN XX" --notes "March 2026"
|
|
464
472
|
|
|
465
473
|
# 4. Review the invoice (includes time entries + expenses as line items)
|
|
466
474
|
ct invoice show <invoice-id>
|
|
@@ -488,11 +496,10 @@ ct invoice create --client "Acme Corp" --period this-month --payment-terms 45
|
|
|
488
496
|
### Invoice for a Specific Project
|
|
489
497
|
|
|
490
498
|
```bash
|
|
491
|
-
#
|
|
492
|
-
ct invoice create --client
|
|
493
|
-
ct invoice import-time <invoice-id> --from 2026-03-01 --to 2026-03-31 -p acme-website
|
|
499
|
+
# Create invoice from time entries for a specific date range
|
|
500
|
+
ct invoice create --client <client-id> --from 2026-03-01 --to 2026-03-31
|
|
494
501
|
|
|
495
|
-
# Review and send — note: billable expenses
|
|
502
|
+
# Review and send — note: billable expenses from the period are included
|
|
496
503
|
ct invoice show <invoice-id>
|
|
497
504
|
ct invoice send <invoice-id>
|
|
498
505
|
```
|
|
@@ -517,7 +524,7 @@ ct invoice send <invoice-id>
|
|
|
517
524
|
|
|
518
525
|
```bash
|
|
519
526
|
# Full payment
|
|
520
|
-
ct invoice
|
|
527
|
+
ct invoice pay <invoice-id> --amount 1500 --method bank_transfer --reference "TXN-12345"
|
|
521
528
|
|
|
522
529
|
# Check invoice status
|
|
523
530
|
ct invoice show <invoice-id>
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|