crowdtime-cli 0.22.0__tar.gz → 0.23.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 (43) hide show
  1. {crowdtime_cli-0.22.0 → crowdtime_cli-0.23.0}/PKG-INFO +1 -1
  2. {crowdtime_cli-0.22.0 → crowdtime_cli-0.23.0}/pyproject.toml +1 -1
  3. {crowdtime_cli-0.22.0 → crowdtime_cli-0.23.0}/src/crowdtime_cli/__init__.py +1 -1
  4. {crowdtime_cli-0.22.0 → crowdtime_cli-0.23.0}/src/crowdtime_cli/commands/invoice_cmd.py +61 -11
  5. {crowdtime_cli-0.22.0 → crowdtime_cli-0.23.0}/src/crowdtime_cli/skills/crowdtime/SKILL.md +3 -2
  6. {crowdtime_cli-0.22.0 → crowdtime_cli-0.23.0}/src/crowdtime_cli/skills/crowdtime/references/commands.md +9 -5
  7. {crowdtime_cli-0.22.0 → crowdtime_cli-0.23.0}/src/crowdtime_cli/skills/crowdtime/references/workflows.md +1 -1
  8. {crowdtime_cli-0.22.0 → crowdtime_cli-0.23.0}/.gitignore +0 -0
  9. {crowdtime_cli-0.22.0 → crowdtime_cli-0.23.0}/LICENSE +0 -0
  10. {crowdtime_cli-0.22.0 → crowdtime_cli-0.23.0}/README.md +0 -0
  11. {crowdtime_cli-0.22.0 → crowdtime_cli-0.23.0}/src/crowdtime_cli/auth.py +0 -0
  12. {crowdtime_cli-0.22.0 → crowdtime_cli-0.23.0}/src/crowdtime_cli/client.py +0 -0
  13. {crowdtime_cli-0.22.0 → crowdtime_cli-0.23.0}/src/crowdtime_cli/commands/__init__.py +0 -0
  14. {crowdtime_cli-0.22.0 → crowdtime_cli-0.23.0}/src/crowdtime_cli/commands/ai_cmd.py +0 -0
  15. {crowdtime_cli-0.22.0 → crowdtime_cli-0.23.0}/src/crowdtime_cli/commands/auth_cmd.py +0 -0
  16. {crowdtime_cli-0.22.0 → crowdtime_cli-0.23.0}/src/crowdtime_cli/commands/billing_cmd.py +0 -0
  17. {crowdtime_cli-0.22.0 → crowdtime_cli-0.23.0}/src/crowdtime_cli/commands/calendar_cmd.py +0 -0
  18. {crowdtime_cli-0.22.0 → crowdtime_cli-0.23.0}/src/crowdtime_cli/commands/clients_cmd.py +0 -0
  19. {crowdtime_cli-0.22.0 → crowdtime_cli-0.23.0}/src/crowdtime_cli/commands/config_cmd.py +0 -0
  20. {crowdtime_cli-0.22.0 → crowdtime_cli-0.23.0}/src/crowdtime_cli/commands/expense_cmd.py +0 -0
  21. {crowdtime_cli-0.22.0 → crowdtime_cli-0.23.0}/src/crowdtime_cli/commands/favorites_cmd.py +0 -0
  22. {crowdtime_cli-0.22.0 → crowdtime_cli-0.23.0}/src/crowdtime_cli/commands/insights_cmd.py +0 -0
  23. {crowdtime_cli-0.22.0 → crowdtime_cli-0.23.0}/src/crowdtime_cli/commands/log_cmd.py +0 -0
  24. {crowdtime_cli-0.22.0 → crowdtime_cli-0.23.0}/src/crowdtime_cli/commands/org_cmd.py +0 -0
  25. {crowdtime_cli-0.22.0 → crowdtime_cli-0.23.0}/src/crowdtime_cli/commands/payroll_cmd.py +0 -0
  26. {crowdtime_cli-0.22.0 → crowdtime_cli-0.23.0}/src/crowdtime_cli/commands/projects_cmd.py +0 -0
  27. {crowdtime_cli-0.22.0 → crowdtime_cli-0.23.0}/src/crowdtime_cli/commands/pto_cmd.py +0 -0
  28. {crowdtime_cli-0.22.0 → crowdtime_cli-0.23.0}/src/crowdtime_cli/commands/report_cmd.py +0 -0
  29. {crowdtime_cli-0.22.0 → crowdtime_cli-0.23.0}/src/crowdtime_cli/commands/skill_cmd.py +0 -0
  30. {crowdtime_cli-0.22.0 → crowdtime_cli-0.23.0}/src/crowdtime_cli/commands/tasks_cmd.py +0 -0
  31. {crowdtime_cli-0.22.0 → crowdtime_cli-0.23.0}/src/crowdtime_cli/commands/team_cmd.py +0 -0
  32. {crowdtime_cli-0.22.0 → crowdtime_cli-0.23.0}/src/crowdtime_cli/commands/timer_cmd.py +0 -0
  33. {crowdtime_cli-0.22.0 → crowdtime_cli-0.23.0}/src/crowdtime_cli/commands/timesheet_cmd.py +0 -0
  34. {crowdtime_cli-0.22.0 → crowdtime_cli-0.23.0}/src/crowdtime_cli/commands/version_cmd.py +0 -0
  35. {crowdtime_cli-0.22.0 → crowdtime_cli-0.23.0}/src/crowdtime_cli/config.py +0 -0
  36. {crowdtime_cli-0.22.0 → crowdtime_cli-0.23.0}/src/crowdtime_cli/formatters.py +0 -0
  37. {crowdtime_cli-0.22.0 → crowdtime_cli-0.23.0}/src/crowdtime_cli/main.py +0 -0
  38. {crowdtime_cli-0.22.0 → crowdtime_cli-0.23.0}/src/crowdtime_cli/models.py +0 -0
  39. {crowdtime_cli-0.22.0 → crowdtime_cli-0.23.0}/src/crowdtime_cli/oauth.py +0 -0
  40. {crowdtime_cli-0.22.0 → crowdtime_cli-0.23.0}/src/crowdtime_cli/resolvers.py +0 -0
  41. {crowdtime_cli-0.22.0 → crowdtime_cli-0.23.0}/src/crowdtime_cli/skills/crowdtime/references/pto.md +0 -0
  42. {crowdtime_cli-0.22.0 → crowdtime_cli-0.23.0}/src/crowdtime_cli/utils.py +0 -0
  43. {crowdtime_cli-0.22.0 → crowdtime_cli-0.23.0}/src/crowdtime_cli/version_check.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: crowdtime-cli
3
- Version: 0.22.0
3
+ Version: 0.23.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.22.0"
3
+ version = "0.23.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.22.0"
3
+ __version__ = "0.23.0"
@@ -572,6 +572,41 @@ def show_invoice(
572
572
  raise typer.Exit(1)
573
573
 
574
574
 
575
+ # How hours are shown on an invoice (CRO-94). "person" is accepted for "user".
576
+ HOURS_DISPLAYS = {
577
+ "project": "project", "task": "task", "user": "user", "person": "user",
578
+ "date": "date", "none": "none", "detailed": "detailed",
579
+ }
580
+ LINE_DETAILS = ["date", "person", "project", "task", "notes"]
581
+ GROUP_BY_HELP = (
582
+ "How to display hours: project, task, person (one line each, per project), "
583
+ "detailed (one line per time entry), date, or none (everything on one line)."
584
+ )
585
+ DETAILS_HELP = (
586
+ "What each detailed line shows (implies --group-by detailed), comma-separated "
587
+ "from date, person, project, task, notes. Default: person,date,notes."
588
+ )
589
+
590
+
591
+ def _display_payload(group_by: Optional[str], details: Optional[str]) -> dict:
592
+ """Validate --group-by / --details into API fields, or raise ValueError."""
593
+ payload: dict = {}
594
+ if group_by is not None:
595
+ resolved = HOURS_DISPLAYS.get(group_by.strip().lower())
596
+ if resolved is None:
597
+ raise ValueError(f"Invalid --group-by. Choose from: {', '.join(HOURS_DISPLAYS)}.")
598
+ payload["group_by"] = resolved
599
+ if details is not None:
600
+ items = [d.strip().lower() for d in details.split(",") if d.strip()]
601
+ bad = [d for d in items if d not in LINE_DETAILS]
602
+ if bad or not items:
603
+ raise ValueError(f"Invalid --details. Choose from: {', '.join(LINE_DETAILS)}.")
604
+ if payload.setdefault("group_by", "detailed") != "detailed":
605
+ raise ValueError("--details only applies to --group-by detailed.")
606
+ payload["line_details"] = items
607
+ return payload
608
+
609
+
575
610
  @app.command("create")
576
611
  def create_invoice(
577
612
  client_id: str = typer.Option(..., "--client", "-c", help="Client ID."),
@@ -597,9 +632,8 @@ def create_invoice(
597
632
  "its next unbilled payment; each payment is billed only once."
598
633
  ),
599
634
  ),
600
- group_by: Optional[str] = typer.Option(
601
- None, "--group-by", "-g", help="Group line items by: project, task, user, date, none."
602
- ),
635
+ group_by: Optional[str] = typer.Option(None, "--group-by", "-g", help=GROUP_BY_HELP),
636
+ details: Optional[str] = typer.Option(None, "--details", help=DETAILS_HELP),
603
637
  tax_rate: Optional[float] = typer.Option(
604
638
  None, "--tax-rate", help="Tax rate percentage (e.g. 21 for 21%)."
605
639
  ),
@@ -634,6 +668,8 @@ def create_invoice(
634
668
  Examples:
635
669
  ct invoice create --client <id> --from 2026-03-01 --to 2026-03-31
636
670
  ct invoice create --client <id> --period last-month --group-by project
671
+ ct invoice create --client <id> --period last-month --group-by person
672
+ ct invoice create --client <id> --period last-month --group-by detailed --details date,person,notes
637
673
  ct invoice create --client <id> --period last-2-weeks --payment-terms net30
638
674
  ct invoice create --client <id> --from monday --to friday --payment-terms 15
639
675
  ct invoice create --client <id> --period last-month --due-date 2026-09-15
@@ -704,8 +740,11 @@ def create_invoice(
704
740
  format_error("--payment needs the fixed-fee project selected with --project.")
705
741
  raise typer.Exit(1)
706
742
  payload["milestone_ids"] = list(payments)
707
- if group_by:
708
- payload["group_by"] = group_by
743
+ try:
744
+ payload.update(_display_payload(group_by, details))
745
+ except ValueError as e:
746
+ format_error(str(e))
747
+ raise typer.Exit(1)
709
748
  if tax_rate is not None:
710
749
  payload["tax_rate"] = str(tax_rate)
711
750
  if notes:
@@ -1384,6 +1423,9 @@ def show_recurring(
1384
1423
  console.print(f" Auto-send: {'Yes' if data.get('auto_send') else 'No'}")
1385
1424
  console.print(f" Pull Entries: {'Yes' if data.get('pull_time_entries') else 'No'}")
1386
1425
  console.print(f" Group By: {data.get('group_by', 'project')}")
1426
+ if data.get("group_by") == "detailed":
1427
+ shown = ", ".join(data.get("line_details") or ["person", "date", "notes"])
1428
+ console.print(f" Line Details: {shown}")
1387
1429
 
1388
1430
  line_items = data.get("line_items", [])
1389
1431
  if line_items:
@@ -1416,7 +1458,8 @@ def create_recurring(
1416
1458
  payment_terms: Optional[int] = typer.Option(None, "--payment-terms", help="Payment terms in days."),
1417
1459
  auto_send: bool = typer.Option(False, "--auto-send", help="Automatically send generated invoices."),
1418
1460
  pull_entries: bool = typer.Option(True, "--pull-entries/--no-pull-entries", help="Pull time entries into invoice."),
1419
- group_by: Optional[str] = typer.Option(None, "--group-by", "-g", help="Group by: project, task, user, date, none."),
1461
+ group_by: Optional[str] = typer.Option(None, "--group-by", "-g", help=GROUP_BY_HELP),
1462
+ details: Optional[str] = typer.Option(None, "--details", help=DETAILS_HELP),
1420
1463
  notes: Optional[str] = typer.Option(None, "--notes", help="Invoice notes."),
1421
1464
  terms: Optional[str] = typer.Option(None, "--terms", help="Payment terms text."),
1422
1465
  output_json: bool = typer.Option(False, "--json", help="Output as JSON."),
@@ -1442,8 +1485,11 @@ def create_recurring(
1442
1485
  payload["payment_terms_days"] = payment_terms
1443
1486
  if auto_send:
1444
1487
  payload["auto_send"] = True
1445
- if group_by:
1446
- payload["group_by"] = group_by
1488
+ try:
1489
+ payload.update(_display_payload(group_by, details))
1490
+ except ValueError as e:
1491
+ format_error(str(e))
1492
+ raise typer.Exit(1)
1447
1493
  if notes:
1448
1494
  payload["notes"] = notes
1449
1495
  if terms:
@@ -1474,7 +1520,8 @@ def update_recurring(
1474
1520
  active: Optional[bool] = typer.Option(None, "--active/--inactive", help="Activate or deactivate."),
1475
1521
  auto_send: Optional[bool] = typer.Option(None, "--auto-send/--no-auto-send", help="Auto-send invoices."),
1476
1522
  payment_terms: Optional[int] = typer.Option(None, "--payment-terms", help="Payment terms in days."),
1477
- group_by: Optional[str] = typer.Option(None, "--group-by", "-g", help="Group by: project, task, user, date, none."),
1523
+ group_by: Optional[str] = typer.Option(None, "--group-by", "-g", help=GROUP_BY_HELP),
1524
+ details: Optional[str] = typer.Option(None, "--details", help=DETAILS_HELP),
1478
1525
  notes: Optional[str] = typer.Option(None, "--notes", help="Invoice notes."),
1479
1526
  output_json: bool = typer.Option(False, "--json", help="Output as JSON."),
1480
1527
  ) -> None:
@@ -1500,8 +1547,11 @@ def update_recurring(
1500
1547
  payload["auto_send"] = auto_send
1501
1548
  if payment_terms is not None:
1502
1549
  payload["payment_terms_days"] = payment_terms
1503
- if group_by is not None:
1504
- payload["group_by"] = group_by
1550
+ try:
1551
+ payload.update(_display_payload(group_by, details))
1552
+ except ValueError as e:
1553
+ format_error(str(e))
1554
+ raise typer.Exit(1)
1505
1555
  if notes is not None:
1506
1556
  payload["notes"] = notes
1507
1557
 
@@ -343,7 +343,8 @@ ct invoice aging # Overdue aging report: buckets 1-30d, 3
343
343
  ct invoice aging --client <id> # Aging for a specific client
344
344
  ct invoice show <id> # Invoice details with line items
345
345
  ct invoice create --client <id> --period last-month --payment-terms net30 # From time entries
346
- ct invoice create --client <id> --period last-month --group-by user # Group by team member
346
+ ct invoice create --client <id> --period last-month --group-by person # One line per person on each project
347
+ ct invoice create --client <id> --period last-month --group-by detailed --details date,person,notes # One line per entry
347
348
  ct invoice create-blank --client <id> --issue-date today --payment-terms net30 # Empty draft
348
349
  ct invoice update <id> --terms "Wire to IBAN XX" --notes "March" # Edit draft fields
349
350
  ct invoice update <id> --subject "April Dev" --currency EUR --tax-rate <id> # More fields
@@ -601,7 +602,7 @@ Read `references/commands.md` for the full reference of every command, subcomman
601
602
  12. **Output formats** — `ct report` supports `table`, `json`, `csv`, and `markdown` via `--format`; suggest `csv` for spreadsheet export, `markdown` for docs
602
603
  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
603
604
  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
604
- 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. Invoices can be grouped by `project`, `task`, `user`, `date`, or `none` via `--group-by`. Line items support discounts (`--discount-type fixed|percent|hours --discount-value N`)
605
+ 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. Hours are shown per `--group-by`: `project`, `task` or `person` (one line each, per project, labelled with the period), `detailed` (one line per time entry; `--details` picks what each line shows from date, person, project, task, notes), `date`, or `none` (all on one line). Recurring templates take the same options. Line items support discounts (`--discount-type fixed|percent|hours --discount-value N`)
605
606
  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. Use `--receives-invoices` to flag contacts who should receive invoice emails
606
607
  17. **Permission errors are answers, not glitches** — the CLI now prints the server's reason on `403` (not your entry, already invoiced, no approval authority, read-only token, role too low) and on `400` validation errors (not assigned to the project, task not on the project). Relay the reason and the suggested alternative; do not retry the same command or look for another route to the same data. What each role can see is summarised under Team → Visibility rule and Organizations → Role hierarchy
607
608
  18. **Rate confidentiality** — rates, budgets, amounts, invoices and retainers are manager+ only (see Projects). If a member, viewer or project manager asks about rates or billed amounts, tell them it's restricted to account managers instead of trying workarounds
@@ -2029,7 +2029,7 @@ Shows invoice details including line items, payments, and totals. Endpoint: `GET
2029
2029
  ### ct invoice create
2030
2030
 
2031
2031
  ```
2032
- ct invoice create --client CLIENT --from DATE --to DATE [--period PERIOD] [--group-by GROUP] [--tax-rate RATE] [--payment-terms TERMS] [--issue-date DATE] [--due-date DATE] [--notes NOTES] [--terms TEXT] [--json]
2032
+ ct invoice create --client CLIENT --from DATE --to DATE [--period PERIOD] [--group-by GROUP] [--details LIST] [--tax-rate RATE] [--payment-terms TERMS] [--issue-date DATE] [--due-date DATE] [--notes NOTES] [--terms TEXT] [--json]
2033
2033
  ```
2034
2034
 
2035
2035
  Creates an invoice from tracked time entries. Requires either `--from`/`--to` or `--period`.
@@ -2042,7 +2042,8 @@ Creates an invoice from tracked time entries. Requires either `--from`/`--to` or
2042
2042
  | `--period` | string | Preset: `last-week`, `last-2-weeks`, `last-month`, `this-month` |
2043
2043
  | `--project`, `-p` | string (repeatable) | Limit to specific project ID(s). **Required to bill a fixed-fee project** — those are never included in a client-wide invoice |
2044
2044
  | `--payment` | string (repeatable) | One-off fixed-fee payment ID(s) to bill (from `ct projects show`). Without it, each selected one-off project bills its next unbilled payment |
2045
- | `--group-by` | string | Group line items by: `project`, `task`, `user`, `date`, `none` |
2045
+ | `--group-by` | string | How to display hours: `project`, `task`, `person` (alias `user`) — one line each, per project, e.g. "Website: Ana Pérez (Oct 1 – Oct 31, 2026)"; `detailed` — one line per time entry; `date`; `none` — all on one line. Omitted: by project, or the project's billing mode when one project is billed |
2046
+ | `--details` | string | With `detailed`: comma-separated `date`, `person`, `project`, `task`, `notes` (default `person,date,notes`), e.g. "10/23/2026 – Ana Pérez: Signup form". Implies `--group-by detailed` |
2046
2047
  | `--tax-rate` | float | Tax rate percentage (e.g. 21 for 21%) |
2047
2048
  | `--payment-terms` | string | Payment terms: `receipt` (0 days), `net7`, `net15`, `net30`, or number of days. **Omit** to resolve from the waterfall below |
2048
2049
  | `--issue-date` | string | Invoice date. Default: today |
@@ -2062,7 +2063,8 @@ Billable time entries and expenses from the period are automatically imported as
2062
2063
  ct invoice create --client <id> --period last-month --payment-terms net30
2063
2064
  ct invoice create --client <id> --from 2026-03-01 --to 2026-03-31
2064
2065
  ct invoice create --client <id> --period last-2-weeks --group-by project
2065
- ct invoice create --client <id> --period last-month --group-by user # One line item per team member
2066
+ ct invoice create --client <id> --period last-month --group-by person # One line per person on each project
2067
+ ct invoice create --client <id> --period last-month --group-by detailed --details date,task,notes # One line per entry
2066
2068
  ct invoice create --client <id> --period last-month # Uses the client's own terms
2067
2069
  ct invoice create --client <id> --period last-month --due-date 2026-09-15
2068
2070
  ct invoice create --client <id> --period last-month --project <fixed-project-id> # Bills the next payment / one period
@@ -2203,7 +2205,8 @@ ct invoice recurring create --name NAME --client CLIENT_ID --schedule SCHEDULE -
2203
2205
  | `--payment-terms` | integer | Payment terms in days |
2204
2206
  | `--auto-send` | flag | Auto-send generated invoices |
2205
2207
  | `--pull-entries/--no-pull-entries` | flag | Pull time entries (default: yes) |
2206
- | `--group-by`, `-g` | string | Group by: `project`, `task`, `user`, `date`, `none` |
2208
+ | `--group-by`, `-g` | string | How to display hours: `project`, `task`, `person`, `detailed`, `date`, `none` (see `ct invoice create`) |
2209
+ | `--details` | string | With `detailed`: what each line shows (`date,person,project,task,notes`) |
2207
2210
  | `--notes` | string | Invoice notes |
2208
2211
  | `--terms` | string | Payment terms text |
2209
2212
  | `--json` | flag | JSON output |
@@ -2224,7 +2227,8 @@ ct invoice recurring update TEMPLATE_ID [options]
2224
2227
  | `--active/--inactive` | flag | Activate or deactivate |
2225
2228
  | `--auto-send/--no-auto-send` | flag | Auto-send setting |
2226
2229
  | `--payment-terms` | integer | Payment terms in days |
2227
- | `--group-by`, `-g` | string | Group by setting |
2230
+ | `--group-by`, `-g` | string | How to display hours (see `ct invoice create`) |
2231
+ | `--details` | string | With `detailed`: what each line shows |
2228
2232
  | `--notes` | string | Invoice notes |
2229
2233
  | `--json` | flag | JSON output |
2230
2234
 
@@ -330,7 +330,7 @@ When a project uses per-person billing, grouping the invoice by user gives clien
330
330
 
331
331
  ```bash
332
332
  # Create invoice grouped by team member
333
- ct invoice create --client "Acme Corp" --period last-month --group-by user --payment-terms net30
333
+ ct invoice create --client "Acme Corp" --period last-month --group-by person --payment-terms net30
334
334
 
335
335
  # Add a line item with a discount (e.g. 2 hours courtesy)
336
336
  ct invoice add-item <invoice-id> --desc "Extra support" --qty 10 --rate 150 --discount-type hours --discount-value 2
File without changes
File without changes