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.
Files changed (36) hide show
  1. {crowdtime_cli-0.7.0 → crowdtime_cli-0.8.1}/PKG-INFO +1 -1
  2. {crowdtime_cli-0.7.0 → crowdtime_cli-0.8.1}/pyproject.toml +1 -1
  3. {crowdtime_cli-0.7.0 → crowdtime_cli-0.8.1}/src/crowdtime_cli/__init__.py +1 -1
  4. {crowdtime_cli-0.7.0 → crowdtime_cli-0.8.1}/src/crowdtime_cli/client.py +12 -10
  5. {crowdtime_cli-0.7.0 → crowdtime_cli-0.8.1}/src/crowdtime_cli/commands/billing_cmd.py +3 -3
  6. {crowdtime_cli-0.7.0 → crowdtime_cli-0.8.1}/src/crowdtime_cli/commands/invoice_cmd.py +155 -10
  7. {crowdtime_cli-0.7.0 → crowdtime_cli-0.8.1}/src/crowdtime_cli/commands/timesheet_cmd.py +24 -0
  8. {crowdtime_cli-0.7.0 → crowdtime_cli-0.8.1}/src/crowdtime_cli/skills/crowdtime/SKILL.md +8 -7
  9. {crowdtime_cli-0.7.0 → crowdtime_cli-0.8.1}/src/crowdtime_cli/skills/crowdtime/references/commands.md +46 -25
  10. {crowdtime_cli-0.7.0 → crowdtime_cli-0.8.1}/src/crowdtime_cli/skills/crowdtime/references/workflows.md +17 -10
  11. {crowdtime_cli-0.7.0 → crowdtime_cli-0.8.1}/.gitignore +0 -0
  12. {crowdtime_cli-0.7.0 → crowdtime_cli-0.8.1}/LICENSE +0 -0
  13. {crowdtime_cli-0.7.0 → crowdtime_cli-0.8.1}/README.md +0 -0
  14. {crowdtime_cli-0.7.0 → crowdtime_cli-0.8.1}/src/crowdtime_cli/auth.py +0 -0
  15. {crowdtime_cli-0.7.0 → crowdtime_cli-0.8.1}/src/crowdtime_cli/commands/__init__.py +0 -0
  16. {crowdtime_cli-0.7.0 → crowdtime_cli-0.8.1}/src/crowdtime_cli/commands/ai_cmd.py +0 -0
  17. {crowdtime_cli-0.7.0 → crowdtime_cli-0.8.1}/src/crowdtime_cli/commands/auth_cmd.py +0 -0
  18. {crowdtime_cli-0.7.0 → crowdtime_cli-0.8.1}/src/crowdtime_cli/commands/clients_cmd.py +0 -0
  19. {crowdtime_cli-0.7.0 → crowdtime_cli-0.8.1}/src/crowdtime_cli/commands/config_cmd.py +0 -0
  20. {crowdtime_cli-0.7.0 → crowdtime_cli-0.8.1}/src/crowdtime_cli/commands/expense_cmd.py +0 -0
  21. {crowdtime_cli-0.7.0 → crowdtime_cli-0.8.1}/src/crowdtime_cli/commands/favorites_cmd.py +0 -0
  22. {crowdtime_cli-0.7.0 → crowdtime_cli-0.8.1}/src/crowdtime_cli/commands/log_cmd.py +0 -0
  23. {crowdtime_cli-0.7.0 → crowdtime_cli-0.8.1}/src/crowdtime_cli/commands/org_cmd.py +0 -0
  24. {crowdtime_cli-0.7.0 → crowdtime_cli-0.8.1}/src/crowdtime_cli/commands/payroll_cmd.py +0 -0
  25. {crowdtime_cli-0.7.0 → crowdtime_cli-0.8.1}/src/crowdtime_cli/commands/projects_cmd.py +0 -0
  26. {crowdtime_cli-0.7.0 → crowdtime_cli-0.8.1}/src/crowdtime_cli/commands/report_cmd.py +0 -0
  27. {crowdtime_cli-0.7.0 → crowdtime_cli-0.8.1}/src/crowdtime_cli/commands/skill_cmd.py +0 -0
  28. {crowdtime_cli-0.7.0 → crowdtime_cli-0.8.1}/src/crowdtime_cli/commands/tasks_cmd.py +0 -0
  29. {crowdtime_cli-0.7.0 → crowdtime_cli-0.8.1}/src/crowdtime_cli/commands/timer_cmd.py +0 -0
  30. {crowdtime_cli-0.7.0 → crowdtime_cli-0.8.1}/src/crowdtime_cli/config.py +0 -0
  31. {crowdtime_cli-0.7.0 → crowdtime_cli-0.8.1}/src/crowdtime_cli/formatters.py +0 -0
  32. {crowdtime_cli-0.7.0 → crowdtime_cli-0.8.1}/src/crowdtime_cli/main.py +0 -0
  33. {crowdtime_cli-0.7.0 → crowdtime_cli-0.8.1}/src/crowdtime_cli/models.py +0 -0
  34. {crowdtime_cli-0.7.0 → crowdtime_cli-0.8.1}/src/crowdtime_cli/oauth.py +0 -0
  35. {crowdtime_cli-0.7.0 → crowdtime_cli-0.8.1}/src/crowdtime_cli/resolvers.py +0 -0
  36. {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.7.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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "crowdtime-cli"
3
- version = "0.7.0"
3
+ version = "0.8.1"
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.7.0"
3
+ __version__ = "0.8.1"
@@ -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)
@@ -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 "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
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 mark-paid <id> # Record payment
198
- ct invoice void <id> # Void an invoice
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 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
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 [--due DATE] [--period PERIOD] [--payment-terms TERMS] [--type TYPE] [--currency CUR] [--notes NOTES] [--json]
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 name or ID |
1206
- | `--due` | string | Due date (default: 30 days from now) |
1207
- | `--period` | string | Period preset: `last-week`, `last-2-weeks`, `last-month`, `this-month` |
1208
- | `--payment-terms` | string | Payment terms: `receipt`, `net15`, `net30`, or a number of days |
1209
- | `--type` | string | `standard` (default), `retainer`, `credit_note` |
1210
- | `--currency` | string | Currency code |
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
- 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.
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 "Acme Corp" --due 2026-04-15
1221
- ct invoice create --client "Acme Corp" --period last-month --payment-terms net30
1222
- ct invoice create --client "Beta Inc" --period this-month --payment-terms net15
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 add-line
1247
+ ### ct invoice update
1229
1248
 
1230
1249
  ```
1231
- ct invoice add-line INVOICE_ID --description DESC [--quantity QTY] [--rate RATE] [--hours HOURS] [--tax TAX_RATE_NAME] [--json]
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
- Adds a line item. `--hours` is shorthand for `--quantity` in hours context. Endpoint: `POST /invoices/{id}/line-items/`
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 import-time
1257
+ ### ct invoice add-item
1237
1258
 
1238
1259
  ```
1239
- ct invoice import-time INVOICE_ID --from DATE --to DATE [--project/-p PROJECT] [--json]
1260
+ ct invoice add-item INVOICE_ID --description DESC --qty QTY --rate RATE [--taxable/--no-taxable] [--json]
1240
1261
  ```
1241
1262
 
1242
- Imports billable time entries as line items. Endpoint: `POST /invoices/{id}/import-time/`
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 mark-paid
1273
+ ### ct invoice pay
1253
1274
 
1254
1275
  ```
1255
- ct invoice mark-paid INVOICE_ID [--date DATE] [--method METHOD] [--reference REF] [--json]
1276
+ ct invoice pay INVOICE_ID --amount AMOUNT [--date DATE] [--method METHOD] [--reference REF] [--notes NOTES] [--json]
1256
1277
  ```
1257
1278
 
1258
- Records full payment. Endpoint: `POST /invoices/{id}/mark-paid/`
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 [--force/-f] [--json]
1284
+ ct invoice void INVOICE_ID --reason/-r REASON [--json]
1264
1285
  ```
1265
1286
 
1266
- Voids an invoice. Requires confirmation unless `--force`. Endpoint: `POST /invoices/{id}/void/`
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 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
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. Add manual line items if needed
463
- ct invoice add-line <invoice-id> --description "Setup fee" --quantity 1 --rate 500
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
- # 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
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 for the project are included
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 mark-paid <invoice-id> --method "bank_transfer" --reference "TXN-12345"
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