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.
Files changed (36) hide show
  1. {crowdtime_cli-0.6.0 → crowdtime_cli-0.8.0}/PKG-INFO +1 -1
  2. {crowdtime_cli-0.6.0 → crowdtime_cli-0.8.0}/pyproject.toml +1 -1
  3. {crowdtime_cli-0.6.0 → crowdtime_cli-0.8.0}/src/crowdtime_cli/__init__.py +1 -1
  4. {crowdtime_cli-0.6.0 → crowdtime_cli-0.8.0}/src/crowdtime_cli/client.py +12 -10
  5. {crowdtime_cli-0.6.0 → crowdtime_cli-0.8.0}/src/crowdtime_cli/commands/billing_cmd.py +3 -3
  6. {crowdtime_cli-0.6.0 → crowdtime_cli-0.8.0}/src/crowdtime_cli/commands/invoice_cmd.py +155 -10
  7. {crowdtime_cli-0.6.0 → crowdtime_cli-0.8.0}/src/crowdtime_cli/commands/org_cmd.py +27 -0
  8. {crowdtime_cli-0.6.0 → crowdtime_cli-0.8.0}/src/crowdtime_cli/skills/crowdtime/SKILL.md +10 -9
  9. {crowdtime_cli-0.6.0 → crowdtime_cli-0.8.0}/src/crowdtime_cli/skills/crowdtime/references/commands.md +49 -27
  10. {crowdtime_cli-0.6.0 → crowdtime_cli-0.8.0}/src/crowdtime_cli/skills/crowdtime/references/workflows.md +11 -13
  11. {crowdtime_cli-0.6.0 → crowdtime_cli-0.8.0}/.gitignore +0 -0
  12. {crowdtime_cli-0.6.0 → crowdtime_cli-0.8.0}/LICENSE +0 -0
  13. {crowdtime_cli-0.6.0 → crowdtime_cli-0.8.0}/README.md +0 -0
  14. {crowdtime_cli-0.6.0 → crowdtime_cli-0.8.0}/src/crowdtime_cli/auth.py +0 -0
  15. {crowdtime_cli-0.6.0 → crowdtime_cli-0.8.0}/src/crowdtime_cli/commands/__init__.py +0 -0
  16. {crowdtime_cli-0.6.0 → crowdtime_cli-0.8.0}/src/crowdtime_cli/commands/ai_cmd.py +0 -0
  17. {crowdtime_cli-0.6.0 → crowdtime_cli-0.8.0}/src/crowdtime_cli/commands/auth_cmd.py +0 -0
  18. {crowdtime_cli-0.6.0 → crowdtime_cli-0.8.0}/src/crowdtime_cli/commands/clients_cmd.py +0 -0
  19. {crowdtime_cli-0.6.0 → crowdtime_cli-0.8.0}/src/crowdtime_cli/commands/config_cmd.py +0 -0
  20. {crowdtime_cli-0.6.0 → crowdtime_cli-0.8.0}/src/crowdtime_cli/commands/expense_cmd.py +0 -0
  21. {crowdtime_cli-0.6.0 → crowdtime_cli-0.8.0}/src/crowdtime_cli/commands/favorites_cmd.py +0 -0
  22. {crowdtime_cli-0.6.0 → crowdtime_cli-0.8.0}/src/crowdtime_cli/commands/log_cmd.py +0 -0
  23. {crowdtime_cli-0.6.0 → crowdtime_cli-0.8.0}/src/crowdtime_cli/commands/payroll_cmd.py +0 -0
  24. {crowdtime_cli-0.6.0 → crowdtime_cli-0.8.0}/src/crowdtime_cli/commands/projects_cmd.py +0 -0
  25. {crowdtime_cli-0.6.0 → crowdtime_cli-0.8.0}/src/crowdtime_cli/commands/report_cmd.py +0 -0
  26. {crowdtime_cli-0.6.0 → crowdtime_cli-0.8.0}/src/crowdtime_cli/commands/skill_cmd.py +0 -0
  27. {crowdtime_cli-0.6.0 → crowdtime_cli-0.8.0}/src/crowdtime_cli/commands/tasks_cmd.py +0 -0
  28. {crowdtime_cli-0.6.0 → crowdtime_cli-0.8.0}/src/crowdtime_cli/commands/timer_cmd.py +0 -0
  29. {crowdtime_cli-0.6.0 → crowdtime_cli-0.8.0}/src/crowdtime_cli/commands/timesheet_cmd.py +0 -0
  30. {crowdtime_cli-0.6.0 → crowdtime_cli-0.8.0}/src/crowdtime_cli/config.py +0 -0
  31. {crowdtime_cli-0.6.0 → crowdtime_cli-0.8.0}/src/crowdtime_cli/formatters.py +0 -0
  32. {crowdtime_cli-0.6.0 → crowdtime_cli-0.8.0}/src/crowdtime_cli/main.py +0 -0
  33. {crowdtime_cli-0.6.0 → crowdtime_cli-0.8.0}/src/crowdtime_cli/models.py +0 -0
  34. {crowdtime_cli-0.6.0 → crowdtime_cli-0.8.0}/src/crowdtime_cli/oauth.py +0 -0
  35. {crowdtime_cli-0.6.0 → crowdtime_cli-0.8.0}/src/crowdtime_cli/resolvers.py +0 -0
  36. {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.6.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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "crowdtime-cli"
3
- version = "0.6.0"
3
+ version = "0.8.0"
4
4
  description = "AI-powered time tracking CLI — a modern, developer-friendly alternative to Harvest"
5
5
  readme = "README.md"
6
6
  license = {text = "Proprietary"}
@@ -1,3 +1,3 @@
1
1
  """CrowdTime CLI - AI-powered time tracking from the command line."""
2
2
 
3
- __version__ = "0.6.0"
3
+ __version__ = "0.8.0"
@@ -140,16 +140,18 @@ class CrowdTimeClient:
140
140
  detail = response.json()
141
141
  except Exception:
142
142
  detail = {}
143
- msg = (
144
- detail.get("detail", "")
145
- if isinstance(detail, dict)
146
- else str(detail)
147
- ) or "Your organization's subscription is inactive."
148
- console.print(
149
- f"[yellow]{msg}[/yellow]\n"
150
- "Run [bold]ct billing portal[/bold] to update your billing."
151
- )
152
- raise APIError(msg, status_code=402, detail=detail)
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
- format_error(
209
- "No subscription found. An organization owner must set up "
210
- "billing from the web dashboard first."
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
- "client": client_id,
401
- "from_date": start,
402
- "to_date": end,
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 2026-03-15
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
- parsed_issue = format_date(parse_date(issue_date))
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 parsed_due:
488
- payload["due_date"] = parsed_due
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={"reason": reason})
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["taxable"] = taxable
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 "Acme" --due 2026-04-15 # Create a draft invoice
193
- ct invoice create --client "Acme" --period last-month --payment-terms net30 # Period preset + payment terms
194
- ct invoice add-line <id> --description "Development" --hours 40 --rate 150
195
- ct invoice import-time <id> --from 2026-03-01 --to 2026-03-31 -p project-slug
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 mark-paid <id> # Record payment
198
- ct invoice void <id> # Void an invoice
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 HR managers (`is_hr_manager=True`), admins, and owners can access payroll. Regular members cannot see any payroll data.
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 import-time`, 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
+ 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 HR managers (`is_hr_manager=True`), admins, and owners.
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 [--due DATE] [--period PERIOD] [--payment-terms TERMS] [--type TYPE] [--currency CUR] [--notes NOTES] [--json]
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 name or ID |
1197
- | `--due` | string | Due date (default: 30 days from now) |
1198
- | `--period` | string | Period preset: `last-week`, `last-2-weeks`, `last-month`, `this-month` |
1199
- | `--payment-terms` | string | Payment terms: `receipt`, `net15`, `net30`, or a number of days |
1200
- | `--type` | string | `standard` (default), `retainer`, `credit_note` |
1201
- | `--currency` | string | Currency code |
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
- Creates a draft invoice. When `--period` is specified, billable time entries and billable expenses from that period are automatically imported as line items. The output shows how many time entries and expenses were included.
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 "Acme Corp" --due 2026-04-15
1212
- ct invoice create --client "Acme Corp" --period last-month --payment-terms net30
1213
- ct invoice create --client "Beta Inc" --period this-month --payment-terms net15
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 add-line
1239
+ ### ct invoice update
1220
1240
 
1221
1241
  ```
1222
- ct invoice add-line INVOICE_ID --description DESC [--quantity QTY] [--rate RATE] [--hours HOURS] [--tax TAX_RATE_NAME] [--json]
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
- Adds a line item. `--hours` is shorthand for `--quantity` in hours context. Endpoint: `POST /invoices/{id}/line-items/`
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 import-time
1249
+ ### ct invoice add-item
1228
1250
 
1229
1251
  ```
1230
- ct invoice import-time INVOICE_ID --from DATE --to DATE [--project/-p PROJECT] [--json]
1252
+ ct invoice add-item INVOICE_ID --description DESC --qty QTY --rate RATE [--taxable/--no-taxable] [--json]
1231
1253
  ```
1232
1254
 
1233
- Imports billable time entries as line items. Endpoint: `POST /invoices/{id}/import-time/`
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 mark-paid
1265
+ ### ct invoice pay
1244
1266
 
1245
1267
  ```
1246
- ct invoice mark-paid INVOICE_ID [--date DATE] [--method METHOD] [--reference REF] [--json]
1268
+ ct invoice pay INVOICE_ID --amount AMOUNT [--date DATE] [--method METHOD] [--reference REF] [--notes NOTES] [--json]
1247
1269
  ```
1248
1270
 
1249
- Records full payment. Endpoint: `POST /invoices/{id}/mark-paid/`
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 [--force/-f] [--json]
1276
+ ct invoice void INVOICE_ID --reason/-r REASON [--json]
1255
1277
  ```
1256
1278
 
1257
- Voids an invoice. Requires confirmation unless `--force`. Endpoint: `POST /invoices/{id}/void/`
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 manually and import separately
459
- ct invoice create --client "Acme Corp" --due 2026-04-15
460
- ct invoice import-time <invoice-id> --from 2026-03-01 --to 2026-03-31
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. Add manual line items if needed
463
- ct invoice add-line <invoice-id> --description "Setup fee" --quantity 1 --rate 500
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
- # Import time for just one project
492
- ct invoice create --client "Acme Corp" --due 2026-04-15
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 for the project are included
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 mark-paid <invoice-id> --method "bank_transfer" --reference "TXN-12345"
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 HR managers, admins, and owners can access payroll data. To grant HR access:
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