crowdtime-cli 0.6.0__tar.gz → 0.8.0__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.6.0 → crowdtime_cli-0.8.0}/PKG-INFO +1 -1
- {crowdtime_cli-0.6.0 → crowdtime_cli-0.8.0}/pyproject.toml +1 -1
- {crowdtime_cli-0.6.0 → crowdtime_cli-0.8.0}/src/crowdtime_cli/__init__.py +1 -1
- {crowdtime_cli-0.6.0 → crowdtime_cli-0.8.0}/src/crowdtime_cli/client.py +12 -10
- {crowdtime_cli-0.6.0 → crowdtime_cli-0.8.0}/src/crowdtime_cli/commands/billing_cmd.py +3 -3
- {crowdtime_cli-0.6.0 → crowdtime_cli-0.8.0}/src/crowdtime_cli/commands/invoice_cmd.py +155 -10
- {crowdtime_cli-0.6.0 → crowdtime_cli-0.8.0}/src/crowdtime_cli/commands/org_cmd.py +27 -0
- {crowdtime_cli-0.6.0 → crowdtime_cli-0.8.0}/src/crowdtime_cli/skills/crowdtime/SKILL.md +10 -9
- {crowdtime_cli-0.6.0 → crowdtime_cli-0.8.0}/src/crowdtime_cli/skills/crowdtime/references/commands.md +49 -27
- {crowdtime_cli-0.6.0 → crowdtime_cli-0.8.0}/src/crowdtime_cli/skills/crowdtime/references/workflows.md +11 -13
- {crowdtime_cli-0.6.0 → crowdtime_cli-0.8.0}/.gitignore +0 -0
- {crowdtime_cli-0.6.0 → crowdtime_cli-0.8.0}/LICENSE +0 -0
- {crowdtime_cli-0.6.0 → crowdtime_cli-0.8.0}/README.md +0 -0
- {crowdtime_cli-0.6.0 → crowdtime_cli-0.8.0}/src/crowdtime_cli/auth.py +0 -0
- {crowdtime_cli-0.6.0 → crowdtime_cli-0.8.0}/src/crowdtime_cli/commands/__init__.py +0 -0
- {crowdtime_cli-0.6.0 → crowdtime_cli-0.8.0}/src/crowdtime_cli/commands/ai_cmd.py +0 -0
- {crowdtime_cli-0.6.0 → crowdtime_cli-0.8.0}/src/crowdtime_cli/commands/auth_cmd.py +0 -0
- {crowdtime_cli-0.6.0 → crowdtime_cli-0.8.0}/src/crowdtime_cli/commands/clients_cmd.py +0 -0
- {crowdtime_cli-0.6.0 → crowdtime_cli-0.8.0}/src/crowdtime_cli/commands/config_cmd.py +0 -0
- {crowdtime_cli-0.6.0 → crowdtime_cli-0.8.0}/src/crowdtime_cli/commands/expense_cmd.py +0 -0
- {crowdtime_cli-0.6.0 → crowdtime_cli-0.8.0}/src/crowdtime_cli/commands/favorites_cmd.py +0 -0
- {crowdtime_cli-0.6.0 → crowdtime_cli-0.8.0}/src/crowdtime_cli/commands/log_cmd.py +0 -0
- {crowdtime_cli-0.6.0 → crowdtime_cli-0.8.0}/src/crowdtime_cli/commands/payroll_cmd.py +0 -0
- {crowdtime_cli-0.6.0 → crowdtime_cli-0.8.0}/src/crowdtime_cli/commands/projects_cmd.py +0 -0
- {crowdtime_cli-0.6.0 → crowdtime_cli-0.8.0}/src/crowdtime_cli/commands/report_cmd.py +0 -0
- {crowdtime_cli-0.6.0 → crowdtime_cli-0.8.0}/src/crowdtime_cli/commands/skill_cmd.py +0 -0
- {crowdtime_cli-0.6.0 → crowdtime_cli-0.8.0}/src/crowdtime_cli/commands/tasks_cmd.py +0 -0
- {crowdtime_cli-0.6.0 → crowdtime_cli-0.8.0}/src/crowdtime_cli/commands/timer_cmd.py +0 -0
- {crowdtime_cli-0.6.0 → crowdtime_cli-0.8.0}/src/crowdtime_cli/commands/timesheet_cmd.py +0 -0
- {crowdtime_cli-0.6.0 → crowdtime_cli-0.8.0}/src/crowdtime_cli/config.py +0 -0
- {crowdtime_cli-0.6.0 → crowdtime_cli-0.8.0}/src/crowdtime_cli/formatters.py +0 -0
- {crowdtime_cli-0.6.0 → crowdtime_cli-0.8.0}/src/crowdtime_cli/main.py +0 -0
- {crowdtime_cli-0.6.0 → crowdtime_cli-0.8.0}/src/crowdtime_cli/models.py +0 -0
- {crowdtime_cli-0.6.0 → crowdtime_cli-0.8.0}/src/crowdtime_cli/oauth.py +0 -0
- {crowdtime_cli-0.6.0 → crowdtime_cli-0.8.0}/src/crowdtime_cli/resolvers.py +0 -0
- {crowdtime_cli-0.6.0 → crowdtime_cli-0.8.0}/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.0
|
|
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)
|
|
@@ -64,6 +64,33 @@ def list_orgs(
|
|
|
64
64
|
raise typer.Exit(1)
|
|
65
65
|
|
|
66
66
|
|
|
67
|
+
@app.command("create")
|
|
68
|
+
def create_org(
|
|
69
|
+
name: str = typer.Argument(..., help="Name for the new organization."),
|
|
70
|
+
output_json: bool = typer.Option(False, "--json", help="Output as JSON."),
|
|
71
|
+
) -> None:
|
|
72
|
+
"""Create a new organization."""
|
|
73
|
+
client = CrowdTimeClient(require_auth=True, require_org=False)
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
data = client.post(
|
|
77
|
+
"/api/v1/organizations/create-personal/",
|
|
78
|
+
data={"name": name},
|
|
79
|
+
org_scoped=False,
|
|
80
|
+
)
|
|
81
|
+
org = data.get("organization", {})
|
|
82
|
+
|
|
83
|
+
if output_json:
|
|
84
|
+
print_json(data)
|
|
85
|
+
else:
|
|
86
|
+
slug = org.get("slug", "")
|
|
87
|
+
format_success(f"Organization '{org.get('name', name)}' created (slug: {slug})")
|
|
88
|
+
console.print(f"[dim]Switch to it with: ct org switch {slug}[/dim]")
|
|
89
|
+
except APIError as e:
|
|
90
|
+
format_error(e.message)
|
|
91
|
+
raise typer.Exit(1)
|
|
92
|
+
|
|
93
|
+
|
|
67
94
|
@app.command("switch")
|
|
68
95
|
def switch_org(
|
|
69
96
|
slug: str = typer.Argument(..., help="Organization slug to switch to."),
|
|
@@ -189,13 +189,13 @@ ct timesheet team --last-week # Last week's overview
|
|
|
189
189
|
ct invoice list # List all invoices
|
|
190
190
|
ct invoice list --status draft # Filter by status
|
|
191
191
|
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
|
|
192
|
+
ct invoice create --client <id> --period last-month --payment-terms net30 # From time entries
|
|
193
|
+
ct invoice create-blank --client <id> --issue-date today --payment-terms net30 # Empty draft
|
|
194
|
+
ct invoice update <id> --terms "Wire to IBAN XX" --notes "March" # Edit draft fields
|
|
195
|
+
ct invoice add-item <id> --desc "Development" --qty 40 --rate 150 # Add line item
|
|
196
196
|
ct invoice send <id> # Mark as sent
|
|
197
|
-
ct invoice
|
|
198
|
-
ct invoice void <id>
|
|
197
|
+
ct invoice pay <id> --amount 1500 # Record payment
|
|
198
|
+
ct invoice void <id> -r "Duplicate" # Void an invoice
|
|
199
199
|
ct invoice duplicate <id> # Duplicate an existing invoice
|
|
200
200
|
```
|
|
201
201
|
|
|
@@ -246,7 +246,7 @@ ct payroll payments # List all payments
|
|
|
246
246
|
ct payroll payments --period 2026-03 --status paid
|
|
247
247
|
```
|
|
248
248
|
|
|
249
|
-
**Access**: Only
|
|
249
|
+
**Access**: Only admins and owners can access payroll. Regular members cannot see any payroll data.
|
|
250
250
|
|
|
251
251
|
**Compensation types**: `hourly` (gross = hours × rate) or `salary` (fixed monthly amount, no proration).
|
|
252
252
|
|
|
@@ -255,8 +255,9 @@ ct payroll payments --period 2026-03 --status paid
|
|
|
255
255
|
### Organizations
|
|
256
256
|
```bash
|
|
257
257
|
ct org list # List your organizations
|
|
258
|
+
ct org create "My Company" # Create a new organization
|
|
258
259
|
ct org switch <slug> # Switch active organization
|
|
259
|
-
ct org members # List members
|
|
260
|
+
ct org members # List members (manager+ only)
|
|
260
261
|
ct org invitations # List pending invitations
|
|
261
262
|
ct org invite user@example.com -r member
|
|
262
263
|
ct org invite client@acme.com -r viewer # External client stakeholder (read-only)
|
|
@@ -318,6 +319,6 @@ Read `references/commands.md` for the full reference of every command, subcomman
|
|
|
318
319
|
12. **Output formats** — `ct report` supports `table`, `json`, `csv`, and `markdown` via `--format`; suggest `csv` for spreadsheet export, `markdown` for docs
|
|
319
320
|
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
|
|
320
321
|
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
|
|
321
|
-
15. **Invoices include expenses** — when creating invoices with `ct invoice
|
|
322
|
+
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
|
|
322
323
|
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
|
|
323
324
|
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
|
|
@@ -580,7 +580,7 @@ Endpoint: `GET /billing/events/`
|
|
|
580
580
|
|
|
581
581
|
## ct payroll
|
|
582
582
|
|
|
583
|
-
Access restricted to
|
|
583
|
+
Access restricted to admins and owners.
|
|
584
584
|
|
|
585
585
|
### ct payroll (bare / summary)
|
|
586
586
|
|
|
@@ -984,6 +984,15 @@ ct org list [--json]
|
|
|
984
984
|
Lists organizations you belong to. Marks current with `*`. Non-org-scoped.
|
|
985
985
|
Endpoint: `GET /api/v1/organizations/`
|
|
986
986
|
|
|
987
|
+
### ct org create
|
|
988
|
+
|
|
989
|
+
```
|
|
990
|
+
ct org create NAME [--json]
|
|
991
|
+
```
|
|
992
|
+
|
|
993
|
+
Creates a new organization with the given name. Max 10 organizations per user. Non-org-scoped.
|
|
994
|
+
Endpoint: `POST /api/v1/organizations/create-personal/`
|
|
995
|
+
|
|
987
996
|
### ct org switch
|
|
988
997
|
|
|
989
998
|
```
|
|
@@ -998,7 +1007,7 @@ Switches active organization. Saves to config `defaults.organization`.
|
|
|
998
1007
|
ct org members [--json]
|
|
999
1008
|
```
|
|
1000
1009
|
|
|
1001
|
-
Shows: Name, Email, Role, Status. Endpoint: `GET /members/`
|
|
1010
|
+
Shows: Name, Email, Role, Status. Requires manager+ role. Endpoint: `GET /members/`
|
|
1002
1011
|
|
|
1003
1012
|
### ct org invite
|
|
1004
1013
|
|
|
@@ -1188,49 +1197,62 @@ Shows invoice details including line items, payments, and totals. Endpoint: `GET
|
|
|
1188
1197
|
### ct invoice create
|
|
1189
1198
|
|
|
1190
1199
|
```
|
|
1191
|
-
ct invoice create --client CLIENT
|
|
1200
|
+
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]
|
|
1192
1201
|
```
|
|
1193
1202
|
|
|
1203
|
+
Creates an invoice from tracked time entries. Requires either `--from`/`--to` or `--period`.
|
|
1204
|
+
|
|
1194
1205
|
| Option | Type | Description |
|
|
1195
1206
|
|--------|------|-------------|
|
|
1196
|
-
| `--client` | string (required) | Client
|
|
1197
|
-
| `--
|
|
1198
|
-
| `--
|
|
1199
|
-
| `--
|
|
1200
|
-
| `--
|
|
1201
|
-
| `--
|
|
1207
|
+
| `--client` | string (required) | Client ID |
|
|
1208
|
+
| `--from` | string | Period start date |
|
|
1209
|
+
| `--to` | string | Period end date |
|
|
1210
|
+
| `--period` | string | Preset: `last-week`, `last-2-weeks`, `last-month`, `this-month` |
|
|
1211
|
+
| `--group-by` | string | Group line items by: `project`, `task`, `date`, `entry` |
|
|
1212
|
+
| `--tax-rate` | float | Tax rate percentage (e.g. 21 for 21%) |
|
|
1213
|
+
| `--payment-terms` | string | Payment terms: `receipt`, `net15`, `net30`, or number of days |
|
|
1202
1214
|
| `--notes` | string | Invoice notes |
|
|
1215
|
+
| `--terms` | string | Payment terms text |
|
|
1203
1216
|
| `--json` | flag | JSON output |
|
|
1204
1217
|
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
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.
|
|
1218
|
+
Billable time entries and expenses from the period are automatically imported as line items.
|
|
1208
1219
|
|
|
1209
1220
|
**Usage examples:**
|
|
1210
1221
|
```bash
|
|
1211
|
-
ct invoice create --client
|
|
1212
|
-
ct invoice create --client
|
|
1213
|
-
ct invoice create --client
|
|
1214
|
-
ct invoice create --client "Gamma LLC" --period last-2-weeks --payment-terms receipt
|
|
1222
|
+
ct invoice create --client <id> --period last-month --payment-terms net30
|
|
1223
|
+
ct invoice create --client <id> --from 2026-03-01 --to 2026-03-31
|
|
1224
|
+
ct invoice create --client <id> --period last-2-weeks --group-by project
|
|
1215
1225
|
```
|
|
1216
1226
|
|
|
1227
|
+
Endpoint: `POST /invoices/from-time-entries/`
|
|
1228
|
+
|
|
1229
|
+
### ct invoice create-blank
|
|
1230
|
+
|
|
1231
|
+
```
|
|
1232
|
+
ct invoice create-blank --client CLIENT --issue-date DATE [--due-date DATE] [--payment-terms TERMS] [--subject SUBJECT] [--terms TEXT] [--notes NOTES] [--currency CUR] [--json]
|
|
1233
|
+
```
|
|
1234
|
+
|
|
1235
|
+
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).
|
|
1236
|
+
|
|
1217
1237
|
Endpoint: `POST /invoices/`
|
|
1218
1238
|
|
|
1219
|
-
### ct invoice
|
|
1239
|
+
### ct invoice update
|
|
1220
1240
|
|
|
1221
1241
|
```
|
|
1222
|
-
ct invoice
|
|
1242
|
+
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]
|
|
1223
1243
|
```
|
|
1224
1244
|
|
|
1225
|
-
|
|
1245
|
+
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.
|
|
1246
|
+
|
|
1247
|
+
Endpoint: `PATCH /invoices/{id}/`
|
|
1226
1248
|
|
|
1227
|
-
### ct invoice
|
|
1249
|
+
### ct invoice add-item
|
|
1228
1250
|
|
|
1229
1251
|
```
|
|
1230
|
-
ct invoice
|
|
1252
|
+
ct invoice add-item INVOICE_ID --description DESC --qty QTY --rate RATE [--taxable/--no-taxable] [--json]
|
|
1231
1253
|
```
|
|
1232
1254
|
|
|
1233
|
-
|
|
1255
|
+
Adds a line item to a draft invoice. Endpoint: `POST /invoices/{id}/line-items/`
|
|
1234
1256
|
|
|
1235
1257
|
### ct invoice send
|
|
1236
1258
|
|
|
@@ -1240,21 +1262,21 @@ ct invoice send INVOICE_ID [--json]
|
|
|
1240
1262
|
|
|
1241
1263
|
Marks invoice as sent. Endpoint: `POST /invoices/{id}/send/`
|
|
1242
1264
|
|
|
1243
|
-
### ct invoice
|
|
1265
|
+
### ct invoice pay
|
|
1244
1266
|
|
|
1245
1267
|
```
|
|
1246
|
-
ct invoice
|
|
1268
|
+
ct invoice pay INVOICE_ID --amount AMOUNT [--date DATE] [--method METHOD] [--reference REF] [--notes NOTES] [--json]
|
|
1247
1269
|
```
|
|
1248
1270
|
|
|
1249
|
-
Records
|
|
1271
|
+
Records a payment against an invoice. Method choices: `bank_transfer`, `credit_card`, `check`, `cash`, `paypal`, `other`. Endpoint: `POST /invoices/{id}/payments/`
|
|
1250
1272
|
|
|
1251
1273
|
### ct invoice void
|
|
1252
1274
|
|
|
1253
1275
|
```
|
|
1254
|
-
ct invoice void INVOICE_ID
|
|
1276
|
+
ct invoice void INVOICE_ID --reason/-r REASON [--json]
|
|
1255
1277
|
```
|
|
1256
1278
|
|
|
1257
|
-
Voids an invoice
|
|
1279
|
+
Voids an invoice with a required reason. Endpoint: `POST /invoices/{id}/void/`
|
|
1258
1280
|
|
|
1259
1281
|
### ct invoice duplicate
|
|
1260
1282
|
|
|
@@ -455,12 +455,13 @@ ct invoice create --client "Acme Corp" --period last-month --payment-terms net30
|
|
|
455
455
|
# The output shows how many time entries and expenses were included.
|
|
456
456
|
# Billable expenses from the period are automatically added as line items.
|
|
457
457
|
|
|
458
|
-
# 2. Or create
|
|
459
|
-
ct invoice create --client
|
|
460
|
-
ct invoice
|
|
458
|
+
# 2. Or create a blank draft and add items manually
|
|
459
|
+
ct invoice create-blank --client <client-id> --issue-date today --payment-terms net30
|
|
460
|
+
ct invoice add-item <invoice-id> --desc "Development" --qty 40 --rate 150
|
|
461
|
+
ct invoice add-item <invoice-id> --desc "Setup fee" --qty 1 --rate 500
|
|
461
462
|
|
|
462
|
-
# 3.
|
|
463
|
-
ct invoice
|
|
463
|
+
# 3. Update draft fields (terms, notes, etc.)
|
|
464
|
+
ct invoice update <invoice-id> --terms "Wire to IBAN XX" --notes "March 2026"
|
|
464
465
|
|
|
465
466
|
# 4. Review the invoice (includes time entries + expenses as line items)
|
|
466
467
|
ct invoice show <invoice-id>
|
|
@@ -488,11 +489,10 @@ ct invoice create --client "Acme Corp" --period this-month --payment-terms 45
|
|
|
488
489
|
### Invoice for a Specific Project
|
|
489
490
|
|
|
490
491
|
```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
|
|
492
|
+
# Create invoice from time entries for a specific date range
|
|
493
|
+
ct invoice create --client <client-id> --from 2026-03-01 --to 2026-03-31
|
|
494
494
|
|
|
495
|
-
# Review and send — note: billable expenses
|
|
495
|
+
# Review and send — note: billable expenses from the period are included
|
|
496
496
|
ct invoice show <invoice-id>
|
|
497
497
|
ct invoice send <invoice-id>
|
|
498
498
|
```
|
|
@@ -517,7 +517,7 @@ ct invoice send <invoice-id>
|
|
|
517
517
|
|
|
518
518
|
```bash
|
|
519
519
|
# Full payment
|
|
520
|
-
ct invoice
|
|
520
|
+
ct invoice pay <invoice-id> --amount 1500 --method bank_transfer --reference "TXN-12345"
|
|
521
521
|
|
|
522
522
|
# Check invoice status
|
|
523
523
|
ct invoice show <invoice-id>
|
|
@@ -700,9 +700,7 @@ ct payroll payments --member alice@company.com
|
|
|
700
700
|
|
|
701
701
|
### Payroll Access Control
|
|
702
702
|
|
|
703
|
-
Only
|
|
704
|
-
- Use the web admin or API to set `is_hr_manager=True` on a member's membership
|
|
705
|
-
- Admins and owners can do this via `PATCH /api/v1/organizations/<slug>/members/<id>/`
|
|
703
|
+
Only admins and owners can access payroll data.
|
|
706
704
|
|
|
707
705
|
---
|
|
708
706
|
|
|
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
|