|
| 1 | +{ |
| 2 | + "cells": [ |
| 3 | + { |
| 4 | + "cell_type": "markdown", |
| 5 | + "id": "34b777b3-2f88-4b67-ab04-eb8dc126975e", |
| 6 | + "metadata": { |
| 7 | + "tags": [] |
| 8 | + }, |
| 9 | + "source": [ |
| 10 | + "# Invoicing" |
| 11 | + ] |
| 12 | + }, |
| 13 | + { |
| 14 | + "cell_type": "markdown", |
| 15 | + "id": "3335c617-d50d-4e33-a5dc-b56f388081d2", |
| 16 | + "metadata": {}, |
| 17 | + "source": [ |
| 18 | + "Now that we have set up our user info, clients, contracts and projects, as well as a source for time tracking data, we are ready to automatically generate invoices." |
| 19 | + ] |
| 20 | + }, |
| 21 | + { |
| 22 | + "cell_type": "markdown", |
| 23 | + "id": "1d98382c-6c1f-446f-97de-f5b19b97f582", |
| 24 | + "metadata": {}, |
| 25 | + "source": [ |
| 26 | + "## Preamble" |
| 27 | + ] |
| 28 | + }, |
| 29 | + { |
| 30 | + "cell_type": "code", |
| 31 | + "execution_count": 1, |
| 32 | + "id": "aa4ef82b-a4b1-4c07-a2fe-087be2c45bc4", |
| 33 | + "metadata": {}, |
| 34 | + "outputs": [], |
| 35 | + "source": [ |
| 36 | + "from pathlib import Path\n", |
| 37 | + "import datetime\n", |
| 38 | + "from IPython import display" |
| 39 | + ] |
| 40 | + }, |
| 41 | + { |
| 42 | + "cell_type": "code", |
| 43 | + "execution_count": 2, |
| 44 | + "id": "50ec9b6a-ce3d-477c-a4d9-9b1c5db3b4ee", |
| 45 | + "metadata": {}, |
| 46 | + "outputs": [], |
| 47 | + "source": [ |
| 48 | + "import tuttle" |
| 49 | + ] |
| 50 | + }, |
| 51 | + { |
| 52 | + "cell_type": "code", |
| 53 | + "execution_count": 3, |
| 54 | + "id": "d515270c-5a23-49ef-b8cf-dd3c34bdbc00", |
| 55 | + "metadata": {}, |
| 56 | + "outputs": [], |
| 57 | + "source": [ |
| 58 | + "app = tuttle.app.App(verbose=False)" |
| 59 | + ] |
| 60 | + }, |
| 61 | + { |
| 62 | + "cell_type": "markdown", |
| 63 | + "id": "8918a540-ef25-417a-9eae-310d9a63f578", |
| 64 | + "metadata": { |
| 65 | + "tags": [] |
| 66 | + }, |
| 67 | + "source": [ |
| 68 | + "## Workflow" |
| 69 | + ] |
| 70 | + }, |
| 71 | + { |
| 72 | + "cell_type": "markdown", |
| 73 | + "id": "0857141e-106f-4197-ac03-b985f218c20a", |
| 74 | + "metadata": {}, |
| 75 | + "source": [ |
| 76 | + "_1. Select a project_" |
| 77 | + ] |
| 78 | + }, |
| 79 | + { |
| 80 | + "cell_type": "code", |
| 81 | + "execution_count": 4, |
| 82 | + "id": "55f0bf60-d0ec-4b5d-a475-4b541d8799cd", |
| 83 | + "metadata": {}, |
| 84 | + "outputs": [ |
| 85 | + { |
| 86 | + "name": "stderr", |
| 87 | + "output_type": "stream", |
| 88 | + "text": [ |
| 89 | + "/Users/cls/miniforge3/envs/tuttle/lib/python3.9/site-packages/sqlmodel/orm/session.py:60: SAWarning: Class SelectOfScalar will not make use of SQL compilation caching as it does not set the 'inherit_cache' attribute to ``True``. This can have significant performance implications including some performance degradations in comparison to prior SQLAlchemy versions. Set this attribute to True if this object can make use of the cache key generated by the superclass. Alternatively, this attribute may be set to False which will disable this warning. (Background on this error at: https://sqlalche.me/e/14/cprf)\n", |
| 90 | + " results = super().execute(\n" |
| 91 | + ] |
| 92 | + } |
| 93 | + ], |
| 94 | + "source": [ |
| 95 | + "my_project = app.get_project(title=\"Heating Repair\")" |
| 96 | + ] |
| 97 | + }, |
| 98 | + { |
| 99 | + "cell_type": "markdown", |
| 100 | + "id": "c0b2e5f5-7890-46ca-a52f-c6d00197563d", |
| 101 | + "metadata": {}, |
| 102 | + "source": [ |
| 103 | + "2. Select a time tracking data source." |
| 104 | + ] |
| 105 | + }, |
| 106 | + { |
| 107 | + "cell_type": "code", |
| 108 | + "execution_count": 5, |
| 109 | + "id": "b5194e28-e05f-41f0-9f37-066e5c4a4d12", |
| 110 | + "metadata": {}, |
| 111 | + "outputs": [], |
| 112 | + "source": [ |
| 113 | + "from tuttle.calendar import FileCalendar\n", |
| 114 | + "\n", |
| 115 | + "timetracking_calendar_path = Path(\"../../tests/data/TuttleDemo-TimeTracking.ics\")\n", |
| 116 | + "my_calendar = FileCalendar(\n", |
| 117 | + " path=timetracking_calendar_path, \n", |
| 118 | + " name=\"TimeTracking\"\n", |
| 119 | + ")" |
| 120 | + ] |
| 121 | + }, |
| 122 | + { |
| 123 | + "cell_type": "markdown", |
| 124 | + "id": "0142e10a-ebd8-4f7e-84d2-2b8a5f9cd6e4", |
| 125 | + "metadata": {}, |
| 126 | + "source": [ |
| 127 | + "3. Generate one or more timesheets" |
| 128 | + ] |
| 129 | + }, |
| 130 | + { |
| 131 | + "cell_type": "code", |
| 132 | + "execution_count": 6, |
| 133 | + "id": "ecb0e9e5-63a0-4b48-a99d-2d6e2c0905d1", |
| 134 | + "metadata": {}, |
| 135 | + "outputs": [ |
| 136 | + { |
| 137 | + "name": "stderr", |
| 138 | + "output_type": "stream", |
| 139 | + "text": [ |
| 140 | + "/Users/cls/miniforge3/envs/tuttle/lib/python3.9/site-packages/sqlmodel/orm/session.py:101: SAWarning: Dialect sqlite+pysqlite does *not* support Decimal objects natively, and SQLAlchemy must convert from floating point - rounding errors and other issues may occur. Please consider storing Decimal numbers as strings or integers on this platform for lossless storage.\n", |
| 141 | + " return super().execute( # type: ignore\n" |
| 142 | + ] |
| 143 | + } |
| 144 | + ], |
| 145 | + "source": [ |
| 146 | + "my_timesheet = tuttle.timetracking.generate_timesheet(\n", |
| 147 | + " source=my_calendar,\n", |
| 148 | + " project=my_project,\n", |
| 149 | + " period=\"February 2022\",\n", |
| 150 | + " item_description=my_project.title,\n", |
| 151 | + ")" |
| 152 | + ] |
| 153 | + }, |
| 154 | + { |
| 155 | + "cell_type": "markdown", |
| 156 | + "id": "d9a64c75-554b-4e84-92a4-48c6783a1a5d", |
| 157 | + "metadata": {}, |
| 158 | + "source": [ |
| 159 | + "4. Generate an invoice for the timesheet(s)." |
| 160 | + ] |
| 161 | + }, |
| 162 | + { |
| 163 | + "cell_type": "code", |
| 164 | + "execution_count": 7, |
| 165 | + "id": "8a097cc3-5aec-448d-9725-d8f588a02cd4", |
| 166 | + "metadata": {}, |
| 167 | + "outputs": [], |
| 168 | + "source": [ |
| 169 | + "my_invoice = tuttle.invoicing.generate_invoice(\n", |
| 170 | + " timesheets=[\n", |
| 171 | + " my_timesheet,\n", |
| 172 | + " ],\n", |
| 173 | + " contract=my_project.contract,\n", |
| 174 | + " date=datetime.date.today(),\n", |
| 175 | + ")" |
| 176 | + ] |
| 177 | + }, |
| 178 | + { |
| 179 | + "cell_type": "markdown", |
| 180 | + "id": "8f6b4806-a372-48f2-8b9b-0ddae21ce1db", |
| 181 | + "metadata": {}, |
| 182 | + "source": [ |
| 183 | + "5. Render the invoice to a document template:" |
| 184 | + ] |
| 185 | + }, |
| 186 | + { |
| 187 | + "cell_type": "code", |
| 188 | + "execution_count": 8, |
| 189 | + "id": "3da4513d-6523-4268-ab41-647049591267", |
| 190 | + "metadata": {}, |
| 191 | + "outputs": [], |
| 192 | + "source": [ |
| 193 | + "invoice_dir = Path.home() / \"Downloads\"" |
| 194 | + ] |
| 195 | + }, |
| 196 | + { |
| 197 | + "cell_type": "code", |
| 198 | + "execution_count": 9, |
| 199 | + "id": "2a918ca1-9c15-4c56-82e2-396c88116b2d", |
| 200 | + "metadata": {}, |
| 201 | + "outputs": [], |
| 202 | + "source": [ |
| 203 | + "tuttle.rendering.render_invoice(\n", |
| 204 | + " user=app.user, \n", |
| 205 | + " invoice=my_invoice,\n", |
| 206 | + " style=\"anvil\",\n", |
| 207 | + " out_dir=invoice_dir,\n", |
| 208 | + ")" |
| 209 | + ] |
| 210 | + }, |
| 211 | + { |
| 212 | + "cell_type": "code", |
| 213 | + "execution_count": 10, |
| 214 | + "id": "15041ce5-c6b9-4597-86b7-0a5cc5d51fc6", |
| 215 | + "metadata": {}, |
| 216 | + "outputs": [], |
| 217 | + "source": [ |
| 218 | + "invoice_path = str(invoice_dir / f\"Invoice-{my_invoice.number}\" / f\"Invoice-{my_invoice.number}.html\")" |
| 219 | + ] |
| 220 | + }, |
| 221 | + { |
| 222 | + "cell_type": "code", |
| 223 | + "execution_count": 11, |
| 224 | + "id": "8793081f-1f50-4943-8449-cebad19689af", |
| 225 | + "metadata": {}, |
| 226 | + "outputs": [ |
| 227 | + { |
| 228 | + "name": "stdout", |
| 229 | + "output_type": "stream", |
| 230 | + "text": [ |
| 231 | + "Testing Quick Look preview with files:\n", |
| 232 | + "\t/Users/cls/Downloads/Invoice-2022-02-21-01/Invoice-2022-02-21-01.html\n", |
| 233 | + "2022-02-21 21:55:53.852 qlmanage[11525:332330] *** CFMessagePort: bootstrap_register(): failed 1100 (0x44c) 'Permission denied', port = 0x14003, name = 'com.apple.coredrag'\n", |
| 234 | + "See /usr/include/servers/bootstrap_defs.h for the error codes.\n", |
| 235 | + "2022-02-21 21:55:53.883 qlmanage[11525:332330] *** CFMessagePort: bootstrap_register(): failed 1100 (0x44c) 'Permission denied', port = 0xca03, name = 'com.apple.tsm.portname'\n", |
| 236 | + "See /usr/include/servers/bootstrap_defs.h for the error codes.\n", |
| 237 | + "2022-02-21 21:55:59.974 qlmanage[11525:332343] Persistent UI failed to open file file:///Users/cls/Library/Saved%20Application%20State/com.apple.quicklook.qlmanage.savedState/window_1.data: No such file or directory (2)\n" |
| 238 | + ] |
| 239 | + } |
| 240 | + ], |
| 241 | + "source": [ |
| 242 | + "!qlmanage -p {invoice_path}" |
| 243 | + ] |
| 244 | + }, |
| 245 | + { |
| 246 | + "cell_type": "code", |
| 247 | + "execution_count": null, |
| 248 | + "id": "40c8f4b4-2ddb-4d66-b6fa-eccd9ef7d70c", |
| 249 | + "metadata": {}, |
| 250 | + "outputs": [], |
| 251 | + "source": [] |
| 252 | + } |
| 253 | + ], |
| 254 | + "metadata": { |
| 255 | + "kernelspec": { |
| 256 | + "display_name": "tuttle", |
| 257 | + "language": "python", |
| 258 | + "name": "ex" |
| 259 | + }, |
| 260 | + "language_info": { |
| 261 | + "codemirror_mode": { |
| 262 | + "name": "ipython", |
| 263 | + "version": 3 |
| 264 | + }, |
| 265 | + "file_extension": ".py", |
| 266 | + "mimetype": "text/x-python", |
| 267 | + "name": "python", |
| 268 | + "nbconvert_exporter": "python", |
| 269 | + "pygments_lexer": "ipython3", |
| 270 | + "version": "3.9.7" |
| 271 | + } |
| 272 | + }, |
| 273 | + "nbformat": 4, |
| 274 | + "nbformat_minor": 5 |
| 275 | +} |
0 commit comments