crowdtime-cli 0.21.0__tar.gz → 0.21.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/PKG-INFO +1 -1
- {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/pyproject.toml +1 -1
- {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/__init__.py +1 -1
- {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/commands/clients_cmd.py +2 -2
- {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/commands/expense_cmd.py +38 -7
- {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/commands/invoice_cmd.py +5 -5
- {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/commands/org_cmd.py +1 -1
- {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/commands/projects_cmd.py +3 -3
- {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/commands/pto_cmd.py +1 -1
- {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/commands/report_cmd.py +1 -1
- {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/commands/tasks_cmd.py +1 -1
- {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/commands/team_cmd.py +5 -5
- {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/commands/timer_cmd.py +17 -3
- {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/commands/timesheet_cmd.py +4 -4
- {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/formatters.py +4 -4
- {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/main.py +2 -1
- {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/skills/crowdtime/SKILL.md +7 -6
- {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/skills/crowdtime/references/commands.md +10 -7
- {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/skills/crowdtime/references/pto.md +2 -2
- {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/skills/crowdtime/references/workflows.md +6 -4
- {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/.gitignore +0 -0
- {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/LICENSE +0 -0
- {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/README.md +0 -0
- {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/auth.py +0 -0
- {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/client.py +0 -0
- {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/commands/__init__.py +0 -0
- {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/commands/ai_cmd.py +0 -0
- {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/commands/auth_cmd.py +0 -0
- {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/commands/billing_cmd.py +0 -0
- {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/commands/calendar_cmd.py +0 -0
- {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/commands/config_cmd.py +0 -0
- {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/commands/favorites_cmd.py +0 -0
- {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/commands/insights_cmd.py +0 -0
- {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/commands/log_cmd.py +0 -0
- {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/commands/payroll_cmd.py +0 -0
- {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/commands/skill_cmd.py +0 -0
- {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/commands/version_cmd.py +0 -0
- {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/config.py +0 -0
- {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/models.py +0 -0
- {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/oauth.py +0 -0
- {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/resolvers.py +0 -0
- {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/utils.py +0 -0
- {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/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.21.
|
|
3
|
+
Version: 0.21.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
|
|
@@ -75,7 +75,7 @@ def list_clients(
|
|
|
75
75
|
return
|
|
76
76
|
|
|
77
77
|
table = Table(show_header=True, header_style="bold")
|
|
78
|
-
table.add_column("ID", style="dim")
|
|
78
|
+
table.add_column("ID", style="dim", no_wrap=True, min_width=36)
|
|
79
79
|
table.add_column("Name")
|
|
80
80
|
table.add_column("Contact")
|
|
81
81
|
table.add_column("Projects", justify="right")
|
|
@@ -355,7 +355,7 @@ def list_contacts(
|
|
|
355
355
|
return
|
|
356
356
|
|
|
357
357
|
table = Table(show_header=True, header_style="bold")
|
|
358
|
-
table.add_column("ID", style="dim")
|
|
358
|
+
table.add_column("ID", style="dim", no_wrap=True, min_width=36)
|
|
359
359
|
table.add_column("Name", width=20)
|
|
360
360
|
table.add_column("Email", width=25)
|
|
361
361
|
table.add_column("Phone", width=15)
|
|
@@ -21,16 +21,22 @@ console = Console()
|
|
|
21
21
|
# ─── Helpers ──────────────────────────────────────────────────────
|
|
22
22
|
|
|
23
23
|
|
|
24
|
-
def _print_expenses_table(expenses: list[dict]) -> None:
|
|
25
|
-
"""Print expenses in a Rich table."""
|
|
24
|
+
def _print_expenses_table(expenses: list[dict], me: str | None = None) -> None:
|
|
25
|
+
"""Print expenses in a Rich table, with a User column whenever one isn't yours."""
|
|
26
26
|
if not expenses:
|
|
27
27
|
console.print("[dim]No expenses found.[/dim]")
|
|
28
28
|
console.print("Run [bold]ct expense create[/bold] to log an expense.")
|
|
29
29
|
return
|
|
30
30
|
|
|
31
31
|
table = Table(show_header=True, header_style="bold")
|
|
32
|
-
|
|
32
|
+
# Full UUIDs: they are what show/edit/delete take, so never let Rich clip them.
|
|
33
|
+
table.add_column("ID", style="dim", no_wrap=True, min_width=36)
|
|
33
34
|
table.add_column("Date", width=12)
|
|
35
|
+
# The list holds every expense the caller can see — colleagues' too for
|
|
36
|
+
# managers and project leads — so mark whose each one is (CRO-93).
|
|
37
|
+
show_user = any(str(exp.get("user")) != me for exp in expenses)
|
|
38
|
+
if show_user:
|
|
39
|
+
table.add_column("User", width=16)
|
|
34
40
|
table.add_column("Description", width=25)
|
|
35
41
|
table.add_column("Category", width=14)
|
|
36
42
|
table.add_column("Project", style="cyan", width=15)
|
|
@@ -51,9 +57,14 @@ def _print_expenses_table(expenses: list[dict]) -> None:
|
|
|
51
57
|
is_billable = exp.get("is_billable", False)
|
|
52
58
|
is_invoiced = exp.get("is_invoiced", False)
|
|
53
59
|
|
|
60
|
+
user_cell = (
|
|
61
|
+
["You" if str(exp.get("user")) == me else exp.get("user_name", "") or ""]
|
|
62
|
+
if show_user else []
|
|
63
|
+
)
|
|
54
64
|
table.add_row(
|
|
55
65
|
str(exp.get("id", "")),
|
|
56
66
|
exp.get("date", ""),
|
|
67
|
+
*user_cell,
|
|
57
68
|
exp.get("description", ""),
|
|
58
69
|
exp.get("category_name", exp.get("category", "")) or "",
|
|
59
70
|
exp.get("project_name", exp.get("project", "")) or "",
|
|
@@ -65,6 +76,8 @@ def _print_expenses_table(expenses: list[dict]) -> None:
|
|
|
65
76
|
|
|
66
77
|
console.print(table)
|
|
67
78
|
console.print(f"\n[bold]Total:[/bold] {format_currency(total)} ({len(expenses)} expenses)")
|
|
79
|
+
if show_user:
|
|
80
|
+
console.print("[dim]Includes other people's expenses. Use --mine for only yours.[/dim]")
|
|
68
81
|
|
|
69
82
|
|
|
70
83
|
# ─── Commands ─────────────────────────────────────────────────────
|
|
@@ -74,7 +87,12 @@ def _print_expenses_table(expenses: list[dict]) -> None:
|
|
|
74
87
|
def expense_default(ctx: typer.Context) -> None:
|
|
75
88
|
"""Manage expenses."""
|
|
76
89
|
if ctx.invoked_subcommand is None:
|
|
77
|
-
|
|
90
|
+
# Called directly, so every option needs a real value — Typer's
|
|
91
|
+
# defaults are OptionInfo objects outside the CLI parser.
|
|
92
|
+
list_expenses(
|
|
93
|
+
from_date=None, to_date=None, project=None, category=None,
|
|
94
|
+
billable=None, invoiced=None, mine=False, output_json=False,
|
|
95
|
+
)
|
|
78
96
|
|
|
79
97
|
|
|
80
98
|
@app.command("list")
|
|
@@ -85,22 +103,35 @@ def list_expenses(
|
|
|
85
103
|
category: Optional[str] = typer.Option(None, "--category", "-c", help="Filter by category."),
|
|
86
104
|
billable: Optional[bool] = typer.Option(None, "--billable/--no-billable", help="Filter by billable status."),
|
|
87
105
|
invoiced: Optional[bool] = typer.Option(None, "--invoiced/--uninvoiced", help="Filter by invoiced status."),
|
|
106
|
+
mine: bool = typer.Option(
|
|
107
|
+
False, "--mine",
|
|
108
|
+
help="Only your own expenses. Managers and project leads otherwise see colleagues' too.",
|
|
109
|
+
),
|
|
88
110
|
output_json: bool = typer.Option(False, "--json", help="Output as JSON."),
|
|
89
111
|
) -> None:
|
|
90
112
|
"""List expenses with optional filters.
|
|
91
113
|
|
|
92
|
-
By default, shows the current month's expenses
|
|
114
|
+
By default, shows the current month's expenses — every expense you can
|
|
115
|
+
see. The table shows a User column whenever one isn't yours.
|
|
93
116
|
|
|
94
117
|
Examples:
|
|
95
118
|
ct expense list
|
|
96
119
|
ct expense list --from 2026-03-01 --to 2026-03-31
|
|
97
120
|
ct expense list --project myproject --billable
|
|
98
121
|
ct expense list --category travel --uninvoiced
|
|
122
|
+
ct expense list --mine
|
|
99
123
|
"""
|
|
100
124
|
client = CrowdTimeClient(require_auth=True, require_org=True)
|
|
101
125
|
|
|
102
126
|
params: dict = {}
|
|
103
127
|
|
|
128
|
+
me = client.current_user_id()
|
|
129
|
+
if mine:
|
|
130
|
+
if not me:
|
|
131
|
+
format_error("Could not resolve your user id. Run ct auth login again.")
|
|
132
|
+
raise typer.Exit(1)
|
|
133
|
+
params["user"] = me
|
|
134
|
+
|
|
104
135
|
# Default to current month if no date filters provided
|
|
105
136
|
if not from_date and not to_date:
|
|
106
137
|
today = date_type.today()
|
|
@@ -137,7 +168,7 @@ def list_expenses(
|
|
|
137
168
|
if output_json:
|
|
138
169
|
print_json(results)
|
|
139
170
|
else:
|
|
140
|
-
_print_expenses_table(results)
|
|
171
|
+
_print_expenses_table(results, me)
|
|
141
172
|
except APIError as e:
|
|
142
173
|
format_error(e.message)
|
|
143
174
|
raise typer.Exit(1)
|
|
@@ -385,7 +416,7 @@ def list_categories(
|
|
|
385
416
|
return
|
|
386
417
|
|
|
387
418
|
table = Table(show_header=True, header_style="bold")
|
|
388
|
-
table.add_column("ID", style="dim")
|
|
419
|
+
table.add_column("ID", style="dim", no_wrap=True, min_width=36)
|
|
389
420
|
table.add_column("Name", width=25)
|
|
390
421
|
|
|
391
422
|
for cat in results:
|
|
@@ -1305,7 +1305,7 @@ def list_recurring(
|
|
|
1305
1305
|
return
|
|
1306
1306
|
|
|
1307
1307
|
table = Table(show_header=True, header_style="bold")
|
|
1308
|
-
table.add_column("ID", style="dim")
|
|
1308
|
+
table.add_column("ID", style="dim", no_wrap=True, min_width=36)
|
|
1309
1309
|
table.add_column("Name", width=20)
|
|
1310
1310
|
table.add_column("Client", width=18)
|
|
1311
1311
|
table.add_column("Schedule", width=14)
|
|
@@ -1674,7 +1674,7 @@ def list_retainers(
|
|
|
1674
1674
|
return
|
|
1675
1675
|
|
|
1676
1676
|
table = Table(show_header=True, header_style="bold")
|
|
1677
|
-
table.add_column("ID", style="dim")
|
|
1677
|
+
table.add_column("ID", style="dim", no_wrap=True, min_width=36)
|
|
1678
1678
|
table.add_column("Name", width=20)
|
|
1679
1679
|
table.add_column("Client", width=18)
|
|
1680
1680
|
table.add_column("Monthly", justify="right", width=12)
|
|
@@ -1878,7 +1878,7 @@ def list_withdrawals(
|
|
|
1878
1878
|
return
|
|
1879
1879
|
|
|
1880
1880
|
table = Table(show_header=True, header_style="bold")
|
|
1881
|
-
table.add_column("ID", style="dim")
|
|
1881
|
+
table.add_column("ID", style="dim", no_wrap=True, min_width=36)
|
|
1882
1882
|
table.add_column("Invoice", width=14)
|
|
1883
1883
|
table.add_column("Amount", justify="right", width=12)
|
|
1884
1884
|
table.add_column("Date", width=12)
|
|
@@ -1981,7 +1981,7 @@ def tax_rates_list(
|
|
|
1981
1981
|
return
|
|
1982
1982
|
|
|
1983
1983
|
table = Table(title="Tax Rates")
|
|
1984
|
-
table.add_column("ID", style="dim")
|
|
1984
|
+
table.add_column("ID", style="dim", no_wrap=True, min_width=36)
|
|
1985
1985
|
table.add_column("Name", style="bold")
|
|
1986
1986
|
table.add_column("Rate %", justify="right")
|
|
1987
1987
|
table.add_column("Default")
|
|
@@ -2140,7 +2140,7 @@ def uninvoiced_entries(
|
|
|
2140
2140
|
table.add_column("Entries", justify="right")
|
|
2141
2141
|
table.add_column("Hours", justify="right")
|
|
2142
2142
|
table.add_column("Amount", justify="right", style="green")
|
|
2143
|
-
table.add_column("Client ID", style="dim")
|
|
2143
|
+
table.add_column("Client ID", style="dim", no_wrap=True, min_width=36)
|
|
2144
2144
|
|
|
2145
2145
|
total_entries = 0
|
|
2146
2146
|
total_hours = Decimal("0")
|
|
@@ -383,7 +383,7 @@ def invitations(
|
|
|
383
383
|
return
|
|
384
384
|
|
|
385
385
|
table = Table(show_header=True, header_style="bold")
|
|
386
|
-
table.add_column("ID", style="dim")
|
|
386
|
+
table.add_column("ID", style="dim", no_wrap=True, min_width=36)
|
|
387
387
|
table.add_column("Email", width=25)
|
|
388
388
|
table.add_column("Role", width=18)
|
|
389
389
|
table.add_column("Status", width=10)
|
|
@@ -618,7 +618,7 @@ def list_members(
|
|
|
618
618
|
table.add_column("Project Role", width=16)
|
|
619
619
|
table.add_column("Org Role", width=16)
|
|
620
620
|
table.add_column("Rate", justify="right", width=10)
|
|
621
|
-
table.add_column("User ID", style="dim")
|
|
621
|
+
table.add_column("User ID", style="dim", no_wrap=True, min_width=36)
|
|
622
622
|
|
|
623
623
|
for m in members_list:
|
|
624
624
|
proj_role = m.get("project_role") or "[dim]-[/dim]"
|
|
@@ -677,7 +677,7 @@ def list_tasks(
|
|
|
677
677
|
table.add_column("Task Rate", justify="right", width=12)
|
|
678
678
|
table.add_column("Project Rate", justify="right", width=14)
|
|
679
679
|
table.add_column("Active", width=8)
|
|
680
|
-
table.add_column("Task ID", style="dim")
|
|
680
|
+
table.add_column("Task ID", style="dim", no_wrap=True, min_width=36)
|
|
681
681
|
|
|
682
682
|
for t in tasks_list:
|
|
683
683
|
billable = "[green]Yes[/green]" if t.get("task_is_billable") else "[dim]No[/dim]"
|
|
@@ -784,7 +784,7 @@ def assignable_users(
|
|
|
784
784
|
return
|
|
785
785
|
|
|
786
786
|
table = Table(title="Assignable users")
|
|
787
|
-
table.add_column("ID", style="dim")
|
|
787
|
+
table.add_column("ID", style="dim", no_wrap=True, min_width=36)
|
|
788
788
|
table.add_column("Name")
|
|
789
789
|
table.add_column("Email")
|
|
790
790
|
table.add_column("Org role")
|
|
@@ -778,7 +778,7 @@ def allowance_list_cmd(
|
|
|
778
778
|
from rich.table import Table
|
|
779
779
|
|
|
780
780
|
table = Table(show_header=True, header_style="bold")
|
|
781
|
-
table.add_column("User ID", style="dim")
|
|
781
|
+
table.add_column("User ID", style="dim", no_wrap=True, min_width=36)
|
|
782
782
|
table.add_column("Name")
|
|
783
783
|
table.add_column("Days/year", justify="right")
|
|
784
784
|
table.add_column("Counting from")
|
|
@@ -562,7 +562,7 @@ def saved_list(
|
|
|
562
562
|
return
|
|
563
563
|
|
|
564
564
|
table = Table(show_header=True, header_style="bold", title="Saved Reports")
|
|
565
|
-
table.add_column("ID",
|
|
565
|
+
table.add_column("ID", no_wrap=True, min_width=36)
|
|
566
566
|
table.add_column("Name", style="bold", width=25)
|
|
567
567
|
table.add_column("Type", width=18)
|
|
568
568
|
table.add_column("Shared", justify="center", width=8)
|
|
@@ -195,7 +195,7 @@ def list_tasks(
|
|
|
195
195
|
return
|
|
196
196
|
|
|
197
197
|
table = Table(show_header=True, header_style="bold")
|
|
198
|
-
table.add_column("ID", style="dim")
|
|
198
|
+
table.add_column("ID", style="dim", no_wrap=True, min_width=36)
|
|
199
199
|
table.add_column("Name", width=25)
|
|
200
200
|
table.add_column("Project", style="cyan", width=15)
|
|
201
201
|
table.add_column("Billable", justify="center", width=8)
|
|
@@ -343,7 +343,7 @@ def list_assignments(
|
|
|
343
343
|
return
|
|
344
344
|
|
|
345
345
|
table = Table(show_header=True, header_style="bold")
|
|
346
|
-
table.add_column("Assignment ID", style="dim")
|
|
346
|
+
table.add_column("Assignment ID", style="dim", no_wrap=True, min_width=36)
|
|
347
347
|
table.add_column("Manager")
|
|
348
348
|
table.add_column("Manager Email", style="dim")
|
|
349
349
|
table.add_column("Direct Report")
|
|
@@ -402,8 +402,8 @@ def reports(
|
|
|
402
402
|
table = Table(show_header=True, header_style="bold")
|
|
403
403
|
table.add_column("Direct Report")
|
|
404
404
|
table.add_column("Email", style="dim")
|
|
405
|
-
table.add_column("User ID", style="dim")
|
|
406
|
-
table.add_column("Assignment ID", style="dim")
|
|
405
|
+
table.add_column("User ID", style="dim", no_wrap=True, min_width=36)
|
|
406
|
+
table.add_column("Assignment ID", style="dim", no_wrap=True, min_width=36)
|
|
407
407
|
|
|
408
408
|
for row in rows:
|
|
409
409
|
table.add_row(
|
|
@@ -496,7 +496,7 @@ def show(
|
|
|
496
496
|
projects = profile.get("projects") or []
|
|
497
497
|
if projects:
|
|
498
498
|
table = Table(show_header=True, header_style="bold", title="Assigned projects")
|
|
499
|
-
table.add_column("Project ID", style="dim")
|
|
499
|
+
table.add_column("Project ID", style="dim", no_wrap=True, min_width=36)
|
|
500
500
|
table.add_column("Project")
|
|
501
501
|
table.add_column("Client", style="dim")
|
|
502
502
|
table.add_column("Role on project")
|
|
@@ -532,7 +532,7 @@ def show(
|
|
|
532
532
|
by_project = overview.get("by_project") or []
|
|
533
533
|
if by_project:
|
|
534
534
|
table = Table(show_header=True, header_style="bold", title="By project")
|
|
535
|
-
table.add_column("Project ID", style="dim")
|
|
535
|
+
table.add_column("Project ID", style="dim", no_wrap=True, min_width=36)
|
|
536
536
|
table.add_column("Project")
|
|
537
537
|
table.add_column("Client", style="dim")
|
|
538
538
|
table.add_column("Hours", justify="right")
|
|
@@ -281,11 +281,25 @@ def continue_timer(
|
|
|
281
281
|
"""
|
|
282
282
|
client = CrowdTimeClient(require_auth=True, require_org=True)
|
|
283
283
|
|
|
284
|
-
#
|
|
284
|
+
# The time list holds everything the caller can see — colleagues' entries
|
|
285
|
+
# too for project leads and managers — so ask for your own (CRO-93).
|
|
286
|
+
me = client.current_user_id()
|
|
287
|
+
if not me:
|
|
288
|
+
format_error("Could not resolve your user id. Run ct auth login again.")
|
|
289
|
+
raise typer.Exit(1)
|
|
290
|
+
|
|
291
|
+
# Fetch recent entries to find the last non-running one. Server ordering is
|
|
292
|
+
# newest date first; future-dated entries and paid-leave rows aren't
|
|
293
|
+
# something you'd resume.
|
|
285
294
|
try:
|
|
286
|
-
|
|
295
|
+
from datetime import date as date_type
|
|
296
|
+
data = client.get("/time/", params={
|
|
297
|
+
"user": me,
|
|
298
|
+
"date_to": date_type.today().isoformat(),
|
|
299
|
+
"page_size": "10",
|
|
300
|
+
})
|
|
287
301
|
from ..formatters import extract_results
|
|
288
|
-
entries = extract_results(data)
|
|
302
|
+
entries = [e for e in extract_results(data) if e.get("source") != "time_off"]
|
|
289
303
|
if not entries:
|
|
290
304
|
format_error("No recent entries to continue from.")
|
|
291
305
|
raise typer.Exit(1)
|
|
@@ -41,7 +41,7 @@ def _print_timesheets_table(timesheets: list[dict], show_user: bool = False) ->
|
|
|
41
41
|
return
|
|
42
42
|
|
|
43
43
|
table = Table(show_header=True, header_style="bold")
|
|
44
|
-
table.add_column("ID", style="dim")
|
|
44
|
+
table.add_column("ID", style="dim", no_wrap=True, min_width=36)
|
|
45
45
|
if show_user:
|
|
46
46
|
table.add_column("User", width=20)
|
|
47
47
|
table.add_column("Period", width=25)
|
|
@@ -684,7 +684,7 @@ def viewable_users(
|
|
|
684
684
|
return
|
|
685
685
|
|
|
686
686
|
table = Table(show_header=True, header_style="bold")
|
|
687
|
-
table.add_column("ID", style="dim")
|
|
687
|
+
table.add_column("ID", style="dim", no_wrap=True, min_width=36)
|
|
688
688
|
table.add_column("Name", width=20)
|
|
689
689
|
table.add_column("Email", width=30)
|
|
690
690
|
|
|
@@ -1121,7 +1121,7 @@ def compliance_report(
|
|
|
1121
1121
|
title=f"[blue]Pending Approval ({len(pending)})[/blue]",
|
|
1122
1122
|
)
|
|
1123
1123
|
table.add_column("Member", width=22)
|
|
1124
|
-
table.add_column("Timesheet ID", style="dim")
|
|
1124
|
+
table.add_column("Timesheet ID", style="dim", no_wrap=True, min_width=36)
|
|
1125
1125
|
table.add_column("Hours", justify="right", width=8)
|
|
1126
1126
|
table.add_column("Submitted", width=12)
|
|
1127
1127
|
table.add_column("Days Pending", justify="right", width=13)
|
|
@@ -1147,7 +1147,7 @@ def compliance_report(
|
|
|
1147
1147
|
title=f"[red]Rejected ({len(rejected_list)})[/red]",
|
|
1148
1148
|
)
|
|
1149
1149
|
table.add_column("Member", width=22)
|
|
1150
|
-
table.add_column("Timesheet ID", style="dim")
|
|
1150
|
+
table.add_column("Timesheet ID", style="dim", no_wrap=True, min_width=36)
|
|
1151
1151
|
table.add_column("Hours", justify="right", width=8)
|
|
1152
1152
|
table.add_column("Reviewer Notes", width=35)
|
|
1153
1153
|
|
|
@@ -208,7 +208,7 @@ def format_entries_table(
|
|
|
208
208
|
return
|
|
209
209
|
|
|
210
210
|
table = Table(show_header=True, header_style="bold")
|
|
211
|
-
table.add_column("ID", style="dim")
|
|
211
|
+
table.add_column("ID", style="dim", no_wrap=True, min_width=36)
|
|
212
212
|
if show_date:
|
|
213
213
|
table.add_column("Date", width=12)
|
|
214
214
|
if show_user:
|
|
@@ -352,7 +352,7 @@ def format_projects_table(projects: list[Project]) -> None:
|
|
|
352
352
|
return
|
|
353
353
|
|
|
354
354
|
table = Table(show_header=True, header_style="bold")
|
|
355
|
-
table.add_column("ID", style="dim")
|
|
355
|
+
table.add_column("ID", style="dim", no_wrap=True, min_width=36)
|
|
356
356
|
table.add_column("Name", style="bold", width=20)
|
|
357
357
|
table.add_column("Code", width=8)
|
|
358
358
|
table.add_column("Client", width=15)
|
|
@@ -438,7 +438,7 @@ def format_favorites_table(favorites: list[FavoriteEntry]) -> None:
|
|
|
438
438
|
return
|
|
439
439
|
|
|
440
440
|
table = Table(show_header=True, header_style="bold")
|
|
441
|
-
table.add_column("ID", style="dim")
|
|
441
|
+
table.add_column("ID", style="dim", no_wrap=True, min_width=36)
|
|
442
442
|
table.add_column("Project", style="cyan", width=15)
|
|
443
443
|
table.add_column("Task", width=15)
|
|
444
444
|
table.add_column("Description", width=30)
|
|
@@ -952,7 +952,7 @@ def format_timeoff_summary_panel(summary: TimeOffSummary) -> Panel:
|
|
|
952
952
|
padding=(0, 2),
|
|
953
953
|
)
|
|
954
954
|
by_user_table.add_column("User", width=24)
|
|
955
|
-
by_user_table.add_column("User ID", style="dim",
|
|
955
|
+
by_user_table.add_column("User ID", style="dim", no_wrap=True, min_width=36)
|
|
956
956
|
by_user_table.add_column("Hours", justify="right", width=10)
|
|
957
957
|
by_user_table.add_column("Days", justify="right", width=6)
|
|
958
958
|
if summary.by_user:
|
|
@@ -428,13 +428,14 @@ def expense_list_alias(
|
|
|
428
428
|
category: Optional[str] = typer.Option(None, "--category", "-c"),
|
|
429
429
|
billable: Optional[bool] = typer.Option(None, "--billable/--no-billable"),
|
|
430
430
|
invoiced: Optional[bool] = typer.Option(None, "--invoiced/--uninvoiced"),
|
|
431
|
+
mine: bool = typer.Option(False, "--mine"),
|
|
431
432
|
output_json: bool = typer.Option(False, "--json"),
|
|
432
433
|
) -> None:
|
|
433
434
|
"""Alias for 'expense list'."""
|
|
434
435
|
expense_cmd.list_expenses(
|
|
435
436
|
from_date=from_date, to_date=to_date, project=project,
|
|
436
437
|
category=category, billable=billable, invoiced=invoiced,
|
|
437
|
-
output_json=output_json,
|
|
438
|
+
mine=mine, output_json=output_json,
|
|
438
439
|
)
|
|
439
440
|
|
|
440
441
|
|
|
@@ -140,7 +140,8 @@ ct clients remove-contact <id> <contact-id>
|
|
|
140
140
|
|
|
141
141
|
### Expenses
|
|
142
142
|
```bash
|
|
143
|
-
ct expense list # List expenses
|
|
143
|
+
ct expense list # List expenses you can see (colleagues' too if you're a manager or run a project)
|
|
144
|
+
ct expense list --mine # Only your own — use this for "my expenses" and totals
|
|
144
145
|
ct expense list --from 2026-03-01 --to 2026-03-31 --billable
|
|
145
146
|
ct expense create --amount 49.99 "Domain renewal" --project acme-website --category "Software" --vendor "Namecheap" --billable
|
|
146
147
|
ct expense edit <id> --amount 59.99 --description "Domain + SSL renewal"
|
|
@@ -148,9 +149,8 @@ ct expense delete <id>
|
|
|
148
149
|
ct expense categories # List expense categories
|
|
149
150
|
ct expense add-category "Travel" # Create a new category
|
|
150
151
|
|
|
151
|
-
# Short alias
|
|
152
|
-
ct ex
|
|
153
|
-
ct ex create --amount 25 "Lunch with client" --category "Meals" --billable
|
|
152
|
+
# Short alias (list only — takes the same options as expense list)
|
|
153
|
+
ct ex --mine # expense list --mine
|
|
154
154
|
```
|
|
155
155
|
|
|
156
156
|
**Who sees which expenses**: members and viewers see only their own; project managers also see expenses on projects they manage; account managers and above see all. Only the owner of an expense (or an account manager+) can edit or delete it, and not once it has been invoiced. `--project` must be a project the user is assigned to. The invoiced flag is set by invoicing and cannot be edited.
|
|
@@ -296,7 +296,8 @@ A favorite can only point at a project the user is assigned to (and a task on th
|
|
|
296
296
|
|
|
297
297
|
### Timesheets
|
|
298
298
|
```bash
|
|
299
|
-
ct timesheet list
|
|
299
|
+
ct timesheet list --mine # List your own timesheets
|
|
300
|
+
ct timesheet list # Everyone's you can see (a User column marks rows that aren't yours)
|
|
300
301
|
ct timesheet list --status submitted # Filter by status
|
|
301
302
|
ct timesheet submit --from 2026-03-10 --to 2026-03-16 # Submit full week
|
|
302
303
|
ct timesheet submit --from 2026-03-30 --to 2026-03-31 # Partial week (end-of-month split)
|
|
@@ -437,7 +438,7 @@ ct pto add <user> <dates> # Add PTO on behalf of user, auto-approved
|
|
|
437
438
|
ct pto list [--mine|--team] # List PTO entries with optional filters
|
|
438
439
|
ct pto show <id> # Detail view of a single PTO entry (NEVER truncate the ID)
|
|
439
440
|
ct pto edit <id> [fields] # Edit date range, kind, notes, hours/day (your own: pending only)
|
|
440
|
-
ct pto cancel <id> [--force] # Cancel a PTO entry (own pending; or project manager+
|
|
441
|
+
ct pto cancel <id> [--force] # Cancel a PTO entry (own pending; or project manager+ someone else's; your own decided leave needs another approver, admins excepted)
|
|
441
442
|
ct pto approve <id> [--notes "..."] # Approve a pending entry (project manager+; never your own unless admin)
|
|
442
443
|
ct pto reject <id> --notes "..." # Reject a pending entry — notes required (project manager+; never your own unless admin)
|
|
443
444
|
ct pto calendar [--from --to --user --status] # PTO slice for a date window (approved only unless --status)
|
|
@@ -233,9 +233,9 @@ ct timer continue [--note/-n NOTE] [--billable/-b] [--no-billable/-B] [--json]
|
|
|
233
233
|
| `--no-billable`, `-B` | flag | Override billable status (mark non-billable) |
|
|
234
234
|
| `--json` | flag | Output as JSON |
|
|
235
235
|
|
|
236
|
-
Starts a new timer with the same project, task, and description as
|
|
236
|
+
Starts a new timer with the same project, task, and description as your most recent entry (up to today; paid-leave rows are skipped). Great for resuming after a break. Use `--billable`/`--no-billable` to override the original entry's billable status.
|
|
237
237
|
|
|
238
|
-
- **Endpoint:** `GET /time
|
|
238
|
+
- **Endpoint:** `GET /time/?user=<you>` (fetch your last entry) + `POST /time/start/`
|
|
239
239
|
- **Alias:** `ct c` (e.g. `ct c -b` to continue as billable)
|
|
240
240
|
|
|
241
241
|
### ct timer discard
|
|
@@ -563,10 +563,12 @@ ct expense list [options]
|
|
|
563
563
|
| `--category` | string | Filter by category |
|
|
564
564
|
| `--billable` | flag | Only billable expenses |
|
|
565
565
|
| `--invoiced` | flag | Only invoiced expenses |
|
|
566
|
+
| `--mine` | flag | Only your own expenses |
|
|
566
567
|
| `--json` | flag | JSON output |
|
|
567
568
|
|
|
568
|
-
Shows: ID, Date, Description, Amount, Category, Project, Billable, Invoiced.
|
|
569
|
-
Scope: members and viewers see their own expenses; project
|
|
569
|
+
Shows: ID, Date, User (only when a row isn't yours), Description, Amount, Category, Project, Billable, Invoiced. The total adds up every row shown.
|
|
570
|
+
Scope: members and viewers see their own expenses; anyone running a project also sees those logged on it; account managers and above see all. Pass `--mine` whenever the question is about the user's own expenses — without it, a manager's or project lead's list and total include colleagues'. In `--json` output compare each row's `user` with yours.
|
|
571
|
+
Alias: `ct ex [options]` (list only).
|
|
570
572
|
Endpoint: `GET /expenses/`
|
|
571
573
|
|
|
572
574
|
### ct expense show
|
|
@@ -602,8 +604,8 @@ ct expense create --amount AMOUNT DESCRIPTION [options]
|
|
|
602
604
|
**Usage examples:**
|
|
603
605
|
```bash
|
|
604
606
|
ct expense create --amount 49.99 "Domain renewal" --project acme-website --category "Software" --billable
|
|
605
|
-
ct
|
|
606
|
-
ct
|
|
607
|
+
ct expense create --amount 125 "Client lunch" --category "Meals" --vendor "La Trattoria" --billable --notes "Q1 planning lunch"
|
|
608
|
+
ct expense create --amount 29.99 "Adobe subscription" -p design-project --category "Software" -d yesterday -b
|
|
607
609
|
```
|
|
608
610
|
|
|
609
611
|
Endpoint: `POST /expenses/`
|
|
@@ -2860,7 +2862,7 @@ routing from the list payload.
|
|
|
2860
2862
|
ct pto list [--mine] [--team] [--status/-s STATUS] [--kind/-k KIND] [--from DATE] [--to DATE] [--user/-u USER] [--json]
|
|
2861
2863
|
```
|
|
2862
2864
|
|
|
2863
|
-
List PTO entries.
|
|
2865
|
+
List PTO entries. Shows your own entries by default (same as `--mine`); `--team` shows every entry you can see.
|
|
2864
2866
|
|
|
2865
2867
|
**Who can call it:**
|
|
2866
2868
|
- Any member — sees own entries only (scoped by server regardless of flags).
|
|
@@ -2988,6 +2990,7 @@ Cancel a PTO entry. Transitions status to `canceled`.
|
|
|
2988
2990
|
**Authorization:**
|
|
2989
2991
|
- Own entry + `status=pending` → any member.
|
|
2990
2992
|
- Manager+ → any non-canceled entry for users in their visibility scope (including approved entries).
|
|
2993
|
+
- Your own entry once it is approved or rejected → admins and owners only; a manager or project manager gets a `403` and must ask another approver (same rule as editing).
|
|
2991
2994
|
|
|
2992
2995
|
`decided_by` / `decided_at` are NOT stamped by cancel (only by approve/reject).
|
|
2993
2996
|
|
{crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/skills/crowdtime/references/pto.md
RENAMED
|
@@ -45,7 +45,7 @@ If the user doesn't specify a kind, default to `vacation` but **confirm before s
|
|
|
45
45
|
| Status | Meaning | Who can act next |
|
|
46
46
|
|--------|---------|-----------------|
|
|
47
47
|
| `pending` | Submitted, awaiting decision | Manager+ can approve or reject; owner of the entry can cancel or edit |
|
|
48
|
-
| `approved` | Approved and active | Manager+ can cancel (removes from active capacity)
|
|
48
|
+
| `approved` | Approved and active | Manager+ can cancel (removes from active capacity) or edit dates/hours (audit-logged) — but not their own entry unless they are admin/owner |
|
|
49
49
|
| `rejected` | Rejected by manager | Entry owner can submit a new request with corrected dates |
|
|
50
50
|
| `canceled` | Canceled (no longer active) | Terminal state — no further transitions |
|
|
51
51
|
|
|
@@ -93,7 +93,7 @@ Project managers and account managers using `ct pto list --team` or `ct pto add
|
|
|
93
93
|
| `ct pto add <user>` (on-behalf) | No | Within scope → approved (400 outside it) | Any user → approved |
|
|
94
94
|
| `ct pto list / show` | Own only | Within scope | Any |
|
|
95
95
|
| `ct pto edit` | Own + pending only | Within scope (any status); **their own** entry only while pending | Any |
|
|
96
|
-
| `ct pto cancel` | Own + pending only | Within scope (any non-canceled) | Any |
|
|
96
|
+
| `ct pto cancel` | Own + pending only | Within scope (any non-canceled); **their own** entry only while pending | Any |
|
|
97
97
|
| `ct pto approve` | No | Within scope + pending, **never their own** (403) | Any pending, including their own |
|
|
98
98
|
| `ct pto reject` | No | Within scope + pending, **never their own** (403) | Any pending, including their own |
|
|
99
99
|
| `ct pto calendar / summary` | Own | Within scope | Any |
|
|
@@ -577,8 +577,8 @@ ct timesheet submit --from 2026-03-09 --to 2026-03-15 --force
|
|
|
577
577
|
ct timesheet submit --from monday --to wednesday --force # Close out March
|
|
578
578
|
ct timesheet submit --from thursday --to friday --force # Start April
|
|
579
579
|
|
|
580
|
-
# Check submission status
|
|
581
|
-
ct timesheet list
|
|
580
|
+
# Check submission status (--mine: project leads and managers also see colleagues' timesheets)
|
|
581
|
+
ct timesheet list --mine
|
|
582
582
|
|
|
583
583
|
# Made a mistake? Recall it back to draft (before approval) — confirms before executing
|
|
584
584
|
ct timesheet recall <timesheet-id> --notes "Need to fix Wednesday entries"
|
|
@@ -714,7 +714,7 @@ ct expense create --amount 49.99 "Domain renewal" --project acme-website --categ
|
|
|
714
714
|
ct expense create --amount 29.99 "Team Slack subscription" --category "Software"
|
|
715
715
|
|
|
716
716
|
# Log a past expense
|
|
717
|
-
ct
|
|
717
|
+
ct expense create --amount 85 "Client dinner" --category "Meals" --vendor "La Trattoria" -d yesterday --billable -p acme-project
|
|
718
718
|
```
|
|
719
719
|
|
|
720
720
|
### Setting Up Expense Categories
|
|
@@ -734,8 +734,10 @@ ct expense add-category "Hardware"
|
|
|
734
734
|
### Reviewing Expenses
|
|
735
735
|
|
|
736
736
|
```bash
|
|
737
|
-
# List expenses for a period
|
|
737
|
+
# List expenses for a period. Managers and project leads also see colleagues'
|
|
738
|
+
# expenses here; add --mine when the user means their own
|
|
738
739
|
ct expense list --from 2026-03-01 --to 2026-03-31
|
|
740
|
+
ct expense list --mine --from 2026-03-01 --to 2026-03-31
|
|
739
741
|
|
|
740
742
|
# Filter by project and billable status
|
|
741
743
|
ct expense list --project acme-website --billable
|
|
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
|