├── .dockerignore ├── .editorconfig ├── .env.github_actions ├── .env.sample ├── .gitattributes ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── 1-bug_report.yml │ ├── 2-ideas.yml │ └── config.yml ├── dependabot.yml ├── pull_request_template.md └── workflows │ ├── check_typos.yml │ ├── codeql.yml │ ├── dependency-review.yml │ ├── docs.yml │ ├── publish_to_github_packages.yml │ ├── push_release.yml │ ├── run_black_linter.yml │ ├── run_djlint.yml │ ├── run_mypy.yml │ └── tests.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .pylintrc ├── .typos.toml ├── CODE_OF_CONDUCT.md ├── LICENSE ├── Pulumi.yaml ├── README.md ├── SECURITY.md ├── assets └── scripts │ ├── font_awesome.js │ ├── htmx.js │ ├── init.js │ ├── receipt_downloads.js │ ├── tableify.js │ └── validate_payment_details.js ├── backend ├── __init__.py ├── admin.py ├── apps.py ├── auth_backends.py ├── clients │ ├── __init__.py │ ├── api │ │ ├── __init__.py │ │ ├── delete.py │ │ ├── fetch.py │ │ └── urls.py │ ├── clients.py │ ├── models.py │ └── views │ │ ├── __init__.py │ │ ├── create.py │ │ ├── dashboard.py │ │ ├── detail.py │ │ ├── edit.py │ │ └── urls.py ├── context_processors.py ├── core │ ├── __init__.py │ ├── api │ │ ├── __init__.py │ │ ├── base │ │ │ ├── __init__.py │ │ │ ├── breadcrumbs.py │ │ │ ├── modal.py │ │ │ ├── notifications.py │ │ │ └── urls.py │ │ ├── emails │ │ │ ├── __init__.py │ │ │ ├── fetch.py │ │ │ ├── send.py │ │ │ ├── status.py │ │ │ └── urls.py │ │ ├── healthcheck │ │ │ ├── __init__.py │ │ │ ├── healthcheck.py │ │ │ └── urls.py │ │ ├── landing_page │ │ │ ├── __init__.py │ │ │ ├── email_waitlist.py │ │ │ └── urls.py │ │ ├── maintenance │ │ │ ├── __init__.py │ │ │ ├── now.py │ │ │ └── urls.py │ │ ├── public │ │ │ ├── __init__.py │ │ │ ├── authentication.py │ │ │ ├── decorators.py │ │ │ ├── endpoints │ │ │ │ ├── Invoices │ │ │ │ │ ├── __init__.py │ │ │ │ │ ├── create.py │ │ │ │ │ ├── delete.py │ │ │ │ │ ├── download_pdf.py │ │ │ │ │ ├── edit.py │ │ │ │ │ ├── get.py │ │ │ │ │ ├── list.py │ │ │ │ │ └── urls.py │ │ │ │ ├── __init__.py │ │ │ │ ├── clients │ │ │ │ │ ├── __init__.py │ │ │ │ │ ├── create.py │ │ │ │ │ ├── delete.py │ │ │ │ │ ├── list.py │ │ │ │ │ └── urls.py │ │ │ │ ├── system_health.py │ │ │ │ └── webhooks │ │ │ │ │ ├── __init__.py │ │ │ │ │ ├── urls.py │ │ │ │ │ └── webhook_task_queue_handler.py │ │ │ ├── helpers │ │ │ │ ├── __init__.py │ │ │ │ ├── deprecate.py │ │ │ │ └── response.py │ │ │ ├── middleware.py │ │ │ ├── models.py │ │ │ ├── permissions.py │ │ │ ├── serializers │ │ │ │ ├── __init__.py │ │ │ │ ├── clients.py │ │ │ │ └── invoices.py │ │ │ ├── swagger_ui.py │ │ │ ├── types.py │ │ │ └── urls.py │ │ ├── quotas │ │ │ ├── __init__.py │ │ │ ├── fetch.py │ │ │ ├── requests.py │ │ │ └── urls.py │ │ ├── settings │ │ │ ├── __init__.py │ │ │ ├── api_keys.py │ │ │ ├── change_name.py │ │ │ ├── defaults.py │ │ │ ├── email_templates.py │ │ │ ├── preferences.py │ │ │ ├── profile_picture.py │ │ │ └── urls.py │ │ ├── teams │ │ │ ├── __init__.py │ │ │ ├── create.py │ │ │ ├── create_user.py │ │ │ ├── edit_permissions.py │ │ │ ├── invites.py │ │ │ ├── kick.py │ │ │ ├── leave.py │ │ │ ├── switch_team.py │ │ │ └── urls.py │ │ └── urls.py │ ├── data │ │ ├── __init__.py │ │ ├── default_email_templates.py │ │ ├── default_feature_flags.py │ │ └── default_quota_limits.py │ ├── management │ │ ├── __init__.py │ │ ├── commands │ │ │ ├── __init__.py │ │ │ ├── auto.py │ │ │ ├── contributors.json │ │ │ ├── contributors.py │ │ │ ├── feature_flags.py │ │ │ ├── generate_aws_scheduler_apikey.py │ │ │ ├── lint.py │ │ │ ├── navbar_refresh.py │ │ │ ├── test_urls.py │ │ │ └── test_views.py │ │ └── scheduled_tasks │ │ │ ├── __init__.py │ │ │ └── update_all_schedules.py │ ├── models.py │ ├── service │ │ ├── __init__.py │ │ ├── api_keys │ │ │ ├── __init__.py │ │ │ ├── delete.py │ │ │ ├── generate.py │ │ │ └── get.py │ │ ├── asyn_tasks │ │ │ ├── __init__.py │ │ │ └── tasks.py │ │ ├── base │ │ │ ├── __init__.py │ │ │ └── breadcrumbs.py │ │ ├── boto3 │ │ │ ├── __init__.py │ │ │ ├── handler.py │ │ │ └── scheduler │ │ │ │ ├── __init__.py │ │ │ │ ├── create_schedule.py │ │ │ │ ├── delete_schedule.py │ │ │ │ ├── get.py │ │ │ │ ├── pause.py │ │ │ │ └── update_schedule.py │ │ ├── clients │ │ │ ├── __init__.py │ │ │ ├── create.py │ │ │ ├── delete.py │ │ │ ├── get.py │ │ │ └── validate.py │ │ ├── defaults │ │ │ ├── __init__.py │ │ │ ├── get.py │ │ │ └── update.py │ │ ├── file_storage │ │ │ ├── __init__.py │ │ │ ├── create.py │ │ │ └── utils.py │ │ ├── invoices │ │ │ ├── __init__.py │ │ │ ├── common │ │ │ │ ├── __init__.py │ │ │ │ ├── create │ │ │ │ │ ├── __init__.py │ │ │ │ │ ├── create.py │ │ │ │ │ ├── get_page.py │ │ │ │ │ └── services │ │ │ │ │ │ ├── __init__.py │ │ │ │ │ │ └── add.py │ │ │ │ ├── emails │ │ │ │ │ ├── __init__.py │ │ │ │ │ └── on_create.py │ │ │ │ └── fetch.py │ │ │ ├── handler.py │ │ │ ├── recurring │ │ │ │ ├── __init__.py │ │ │ │ ├── create │ │ │ │ │ ├── __init__.py │ │ │ │ │ ├── get_page.py │ │ │ │ │ └── save.py │ │ │ │ ├── generation │ │ │ │ │ ├── __init__.py │ │ │ │ │ └── next_invoice.py │ │ │ │ ├── get.py │ │ │ │ ├── schedules │ │ │ │ │ ├── __init__.py │ │ │ │ │ └── date_handlers.py │ │ │ │ ├── validate │ │ │ │ │ ├── __init__.py │ │ │ │ │ └── frequencies.py │ │ │ │ └── webhooks │ │ │ │ │ ├── __init__.py │ │ │ │ │ └── webhook_apikey_auth.py │ │ │ └── single │ │ │ │ ├── __init__.py │ │ │ │ ├── create │ │ │ │ ├── __init__.py │ │ │ │ ├── create.py │ │ │ │ └── get_page.py │ │ │ │ ├── create_pdf.py │ │ │ │ ├── create_url.py │ │ │ │ └── get_invoice.py │ │ ├── maintenance │ │ │ ├── __init__.py │ │ │ └── expire │ │ │ │ ├── __init__.py │ │ │ │ └── run.py │ │ ├── permissions │ │ │ ├── __init__.py │ │ │ └── scopes.py │ │ ├── reports │ │ │ ├── __init__.py │ │ │ ├── generate.py │ │ │ └── get.py │ │ ├── settings │ │ │ ├── __init__.py │ │ │ ├── update.py │ │ │ └── view.py │ │ ├── teams │ │ │ ├── __init__.py │ │ │ ├── create_user.py │ │ │ ├── fetch.py │ │ │ └── permissions.py │ │ └── webhooks │ │ │ ├── __init__.py │ │ │ ├── auth.py │ │ │ └── get_url.py │ ├── signals │ │ ├── __init__.py │ │ ├── migrations.py │ │ └── signals.py │ ├── types │ │ ├── __init__.py │ │ ├── emails.py │ │ ├── htmx.py │ │ └── requests.py │ ├── utils │ │ ├── __init__.py │ │ ├── calendar.py │ │ ├── dataclasses.py │ │ ├── feature_flags.py │ │ ├── http_utils.py │ │ ├── quota_limit_ops.py │ │ └── service_retry.py │ ├── views │ │ ├── __init__.py │ │ ├── auth │ │ │ ├── __init__.py │ │ │ ├── create_account.py │ │ │ ├── helpers.py │ │ │ ├── login.py │ │ │ ├── passwords │ │ │ │ ├── __init__.py │ │ │ │ ├── generate.py │ │ │ │ ├── set.py │ │ │ │ └── view.py │ │ │ ├── urls.py │ │ │ └── verify.py │ │ ├── emails │ │ │ ├── __init__.py │ │ │ ├── dashboard.py │ │ │ └── urls.py │ │ ├── other │ │ │ ├── __init__.py │ │ │ ├── errors.py │ │ │ └── index.py │ │ ├── quotas │ │ │ ├── __init__.py │ │ │ └── view.py │ │ ├── settings │ │ │ ├── __init__.py │ │ │ ├── teams.py │ │ │ ├── urls.py │ │ │ └── view.py │ │ └── teams │ │ │ ├── __init__.py │ │ │ └── urls.py │ └── webhooks │ │ ├── __init__.py │ │ ├── invoices │ │ ├── __init__.py │ │ ├── invoice_status.py │ │ └── recurring.py │ │ └── urls.py ├── decorators.py ├── events │ └── __init__.py ├── finance │ ├── __init__.py │ ├── api │ │ ├── __init__.py │ │ ├── invoices │ │ │ ├── __init__.py │ │ │ ├── create │ │ │ │ ├── __init__.py │ │ │ │ ├── services │ │ │ │ │ ├── __init__.py │ │ │ │ │ └── add_service.py │ │ │ │ └── set_destination.py │ │ │ ├── delete.py │ │ │ ├── edit.py │ │ │ ├── fetch.py │ │ │ ├── manage.py │ │ │ ├── recurring │ │ │ │ ├── __init__.py │ │ │ │ ├── delete.py │ │ │ │ ├── edit.py │ │ │ │ ├── fetch.py │ │ │ │ ├── generate_next_invoice_now.py │ │ │ │ ├── poll.py │ │ │ │ └── update_status.py │ │ │ ├── reminders │ │ │ │ ├── __init__.py │ │ │ │ ├── create.py │ │ │ │ ├── delete.py │ │ │ │ ├── fetch.py │ │ │ │ └── urls.py │ │ │ ├── single │ │ │ │ └── __init__.py │ │ │ └── urls.py │ │ ├── products │ │ │ ├── __init__.py │ │ │ ├── create.py │ │ │ ├── fetch.py │ │ │ └── urls.py │ │ ├── receipts │ │ │ ├── __init__.py │ │ │ ├── delete.py │ │ │ ├── download.py │ │ │ ├── edit.py │ │ │ ├── fetch.py │ │ │ ├── new.py │ │ │ └── urls.py │ │ ├── reports │ │ │ ├── __init__.py │ │ │ ├── fetch.py │ │ │ ├── generate.py │ │ │ └── urls.py │ │ └── urls.py │ ├── models.py │ ├── signals │ │ ├── __init__.py │ │ └── schedules.py │ └── views │ │ ├── __init__.py │ │ ├── invoices │ │ ├── __init__.py │ │ ├── handler.py │ │ ├── recurring │ │ │ ├── __init__.py │ │ │ ├── create.py │ │ │ ├── dashboard.py │ │ │ ├── edit.py │ │ │ └── overview.py │ │ ├── single │ │ │ ├── __init__.py │ │ │ ├── create.py │ │ │ ├── dashboard.py │ │ │ ├── edit.py │ │ │ ├── manage_access.py │ │ │ ├── overview.py │ │ │ ├── schedule.py │ │ │ └── view.py │ │ └── urls.py │ │ ├── receipts │ │ ├── __init__.py │ │ ├── dashboard.py │ │ └── urls.py │ │ ├── reports │ │ ├── __init__.py │ │ ├── dashboard.py │ │ ├── urls.py │ │ └── view.py │ │ └── urls.py ├── managers.py ├── middleware.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_alter_receipt_date_uploaded.py │ ├── 0003_client_company_client_is_representative.py │ ├── 0004_invoice_client_is_representative.py │ ├── 0005_invoiceproduct.py │ ├── 0006_receipt_merchant_store_receipt_purchase_category.py │ ├── 0007_alter_receipt_merchant_store_and_more.py │ ├── 0008_receiptdownloadtoken.py │ ├── 0009_alter_invoice_sort_code.py │ ├── 0010_user_logged_in_as_team.py │ ├── 0011_alter_team_leader.py │ ├── 0012_receipt_organization_alter_receipt_user_and_more.py │ ├── 0013_auditlog_organization_client_organization_and_more.py │ ├── 0014_notification_extra_type_notification_extra_value.py │ ├── 0015_alter_notification_user_alter_team_name.py │ ├── 0016_alter_invoice_logo_alter_receipt_image_and_more.py │ ├── 0017_featureflags.py │ ├── 0018_user_role.py │ ├── 0019_alter_featureflags_options_and_more.py │ ├── 0020_alter_verificationcodes_options_and_more.py │ ├── 0021_alter_verificationcodes_expiry_and_more.py │ ├── 0022_loginlog_service_alter_verificationcodes_expiry_and_more.py │ ├── 0023_apikey_invoiceonetimeschedule.py │ ├── 0024_invoiceurl_never_expire_invoiceurl_system_created_and_more.py │ ├── 0025_alter_invoiceonetimeschedule_stored_schedule_arn.py │ ├── 0026_invoice_discount_amount_invoice_discount_percentage.py │ ├── 0027_invoice_currency.py │ ├── 0028_quotalimit_quotaincreaserequest_quotaoverrides_and_more.py │ ├── 0029_alter_invoice_organization_alter_invoice_user_and_more.py │ ├── 0030_alter_invoice_items.py │ ├── 0031_featureflags_description_alter_featureflags_name.py │ ├── 0032_client_email_verified_alter_client_organization_and_more.py │ ├── 0033_alter_auditlog_organization.py │ ├── 0034_invoice_client_email_quotaincreaserequest_reason_and_more.py │ ├── 0035_client_contact_method.py │ ├── 0036_alter_client_address_clientdefaults.py │ ├── 0036_apiauthtoken.py │ ├── 0037_merge_20240619_2223.py │ ├── 0038_alter_apiauthtoken_options.py │ ├── 0039_apiauthtoken_active_apiauthtoken_description_and_more.py │ ├── 0040_apiauthtoken_scopes_apiauthtoken_team_and_more.py │ ├── 0041_alter_apiauthtoken_user.py │ ├── 0042_remove_apiauthtoken_key_apiauthtoken_hashed_key.py │ ├── 0043_rename_team_organization_remove_apiauthtoken_team_and_more.py │ ├── 0044_defaultvalues_delete_clientdefaults_and_more.py │ ├── 0045_usersettings_disabled_features.py │ ├── 0046_rename_status_invoicereminder_boto_schedule_status_and_more.py │ ├── 0047_defaultvalues_default_invoice_logo.py │ ├── 0048_alter_defaultvalues_default_invoice_logo.py │ ├── 0049_filestoragefile.py │ ├── 0050_multifileupload.py │ ├── 0051_planfeaturegroup_subscriptionplan_planfeature_and_more.py │ ├── 0052_filestoragefile_file_uri_path.py │ ├── 0053_usage_instance_id_alter_planfeature_name_and_more.py │ ├── 0054_transferusage_storageusage.py │ ├── 0055_remove_planfeature_group_and_more.py │ ├── 0056_user_stripe_customer_id.py │ ├── 0057_user_entitlements.py │ ├── 0058_organization_entitlements_and_more.py │ ├── 0059_alter_invoicerecurringprofile_managers_and_more.py │ ├── 0060_user_require_change_password.py │ ├── 0061_defaultvalues_invoice_from_address_and_more.py │ ├── 0062_defaultvalues_invoice_account_holder_name_and_more.py │ ├── 0063_defaultvalues_email_template_recurring_invoices_invoice_cancelled_and_more.py │ ├── 0064_remove_invoice_payment_status_invoice_status.py │ ├── 0065_remove_invoiceurl_never_expire_passwordsecret_active_and_more.py │ ├── 0066_delete_apikey_remove_verificationcodes_expiry_and_more.py │ ├── 0067_remove_apiauthtoken_expired_and_more.py │ ├── 0068_invoice_created_at_invoice_status_updated_at_and_more.py │ ├── 0069_alter_auditlog_action.py │ ├── 0070_remove_invoice_invoice_id_and_more.py │ └── __init__.py ├── models.py ├── onboarding │ ├── __init__.py │ ├── api │ │ └── __init__.py │ └── views │ │ └── __init__.py ├── storage │ ├── __init__.py │ ├── api │ │ ├── __init__.py │ │ ├── delete.py │ │ ├── fetch.py │ │ └── urls.py │ ├── file_storage.py │ └── views │ │ ├── __init__.py │ │ ├── dashboard.py │ │ ├── upload.py │ │ └── urls.py ├── templatetags │ ├── __init__.py │ ├── cal_filters.py │ ├── dictfilters.py │ ├── feature_enabled.py │ ├── listfilters.py │ ├── strfilters.py │ └── utils.py └── urls.py ├── billing ├── __init__.py ├── admin.py ├── apps.py ├── billing_settings.py ├── data │ ├── __init__.py │ └── default_usage_plans.py ├── decorators.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ └── stripe.py ├── middleware.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_subscriptionplan_stripe_price_id.py │ ├── 0003_stripewebhookevent_usersubscription_uuid_and_more.py │ ├── 0004_auto_20240830_1655.py │ ├── 0005_auto_20240830_1655.py │ ├── 0006_billingusage.py │ └── __init__.py ├── models.py ├── service │ ├── __init__.py │ ├── checkout_completed.py │ ├── entitlements.py │ ├── get_user.py │ ├── plan_change.py │ ├── price.py │ ├── stripe_customer.py │ ├── subscription_ended.py │ ├── subscription_handler.py │ └── test.py ├── signals │ ├── __init__.py │ ├── migrations.py │ ├── quotas.py │ ├── stripe │ │ ├── __init__.py │ │ └── webhook_handler.py │ └── usage.py ├── templates │ └── pages │ │ └── billing │ │ └── dashboard │ │ ├── all_subscriptions.html │ │ ├── choose_plan_section.html │ │ ├── dashboard.html │ │ ├── growth_usages.html │ │ └── starter_usages.html ├── urls.py ├── views.py └── views │ ├── __init__.py │ ├── change_plan.py │ ├── dashboard.py │ ├── return_urls │ ├── failed.py │ └── success.py │ ├── stripe_misc.py │ └── stripe_webhooks.py ├── components ├── +messages_list.html ├── +modal.html ├── +profile_picture.html └── components.py ├── docs ├── CNAME ├── changelog │ └── index.md ├── contributing │ ├── ask.md │ ├── development-progression.md │ ├── gh-board.md │ ├── gh-issues.md │ ├── index.md │ ├── lead-position.md │ ├── motivation.md │ ├── setup.md │ ├── style-guide.md │ ├── translations.md │ └── writing-documentation.md ├── debugging │ ├── aws │ │ └── iam.md │ ├── problem-solving.md │ ├── python │ │ └── poetry.md │ └── webpack │ │ └── empty-pages.md ├── favicon.png ├── getting-started │ ├── databases │ │ ├── index.md │ │ ├── mysql.md │ │ ├── postgres.md │ │ └── sqlite.md │ ├── index.md │ ├── installation.md │ └── settings │ │ ├── AWS │ │ ├── feature-flags.md │ │ ├── index.md │ │ ├── private-media.md │ │ ├── public-media.md │ │ ├── setup.md │ │ └── static.md │ │ ├── index.md │ │ └── social-login │ │ ├── github.md │ │ ├── google.md │ │ └── index.md ├── index.md ├── js │ └── extra.js ├── overrides │ ├── main.html │ └── partials │ │ └── integrations │ │ └── analytics │ │ └── custom.html └── user-guide │ ├── emails │ └── templates │ │ └── index.md │ ├── index.md │ ├── invoices │ ├── index.md │ └── sending │ │ ├── download-as-pdf.md │ │ ├── preview_dropdown.png │ │ ├── print_dialog.png │ │ ├── print_invoice.png │ │ ├── save_as_pdf_destination.png │ │ └── save_button.png │ └── pricing │ ├── index.md │ └── pricing_table.png ├── frontend ├── static │ ├── favicon.ico │ ├── favicon │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── apple-touch-icon.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ └── favicon.ico │ ├── img │ │ ├── avatar-example.jpg │ │ ├── brainstorming_finance.png │ │ ├── choose.svg │ │ ├── create-account-office-dark.jpeg │ │ ├── create-account-office.jpeg │ │ ├── dashboard_invoices_page.png │ │ ├── default_profile_pic.png │ │ ├── forgot-password-office-dark.jpeg │ │ ├── forgot-password-office.jpeg │ │ ├── forgot_password.svg │ │ ├── github.svg │ │ ├── invoices.svg │ │ ├── login-office-dark.jpeg │ │ ├── login-office.jpeg │ │ ├── login_image.jpg │ │ ├── logo_single.png │ │ ├── logo_with_text.png │ │ ├── mobile_login.svg │ │ ├── receipt.svg │ │ ├── signup.svg │ │ ├── software.svg │ │ ├── team_done.svg │ │ ├── twitter.svg │ │ ├── two_factor_auth.svg │ │ └── verified.svg │ ├── js │ │ ├── base.js │ │ ├── charts-bars.js │ │ ├── charts-lines.js │ │ ├── charts-pie.js │ │ ├── focus-trap.js │ │ └── init-alpine.js │ └── src │ │ ├── input.css │ │ └── invoice.css └── templates │ ├── admin │ └── base_site.html │ ├── base │ ├── +left_drawer.html │ ├── _head.html │ ├── auth.html │ ├── base.html │ ├── breadcrumbs.html │ ├── breadcrumbs_ul.html │ ├── htmx.html │ ├── toast.html │ ├── toasts.html │ └── topbar │ │ ├── +icon_dropdown.html │ │ ├── _notification_count.html │ │ ├── _notification_dropdown_items.html │ │ ├── _organizations_list.html │ │ ├── _topbar.html │ │ └── team_selector │ │ └── selector.html │ ├── components │ ├── +logged_in_for.html │ ├── buttons │ │ ├── brand-button.html │ │ └── save_all.html │ ├── containers │ │ └── dark_main.html │ ├── form │ │ └── toggle.html │ ├── permissions │ │ └── selector.html │ └── table │ │ ├── skeleton_row.html │ │ ├── skeleton_rows.html │ │ └── skeleton_table.html │ ├── modals │ ├── accept_invite.html │ ├── change_profile_picture.html │ ├── create_invoice_product.html │ ├── create_reminder.html │ ├── create_team.html │ ├── edit_team_member_permissions.html │ ├── generate_api_key.html │ ├── generate_report.html │ ├── invite_user.html │ ├── invoices_add_service.html │ ├── invoices_edit_discount.html │ ├── invoices_from_destination.html │ ├── invoices_to_destination.html │ ├── leave_team.html │ ├── receipts_upload.html │ ├── send_bulk_email.html │ ├── send_single_email.html │ ├── team_create_user.html │ └── view_quota_limit_info.html │ ├── pages │ ├── admin │ │ └── api_keys │ │ │ └── generate_response.html │ ├── auth │ │ ├── _magic_link_partial.html │ │ ├── create_account_choose.html │ │ ├── create_account_manual.html │ │ ├── forgot_password.html │ │ ├── login.html │ │ ├── login_initial.html │ │ ├── magic_link_verify.html │ │ └── magic_link_waiting.html │ ├── clients │ │ ├── create │ │ │ └── create.html │ │ ├── dashboard │ │ │ ├── _rows.html │ │ │ ├── _table.html │ │ │ └── dashboard.html │ │ └── detail │ │ │ ├── client_defaults.html │ │ │ └── dashboard.html │ ├── create_account.html │ ├── dashboard.html │ ├── emails │ │ ├── _fetch_body.html │ │ └── dashboard.html │ ├── file_storage │ │ ├── _table_body.html │ │ ├── dashboard.html │ │ └── upload.html │ ├── forgot_password.html │ ├── indtest.html │ ├── invoices │ │ ├── create │ │ │ ├── bank_details │ │ │ │ ├── account_number.html │ │ │ │ ├── holder_name.html │ │ │ │ └── sort_code.html │ │ │ ├── create_recurring.html │ │ │ ├── create_single.html │ │ │ ├── custom_designs │ │ │ │ ├── logo.html │ │ │ │ └── reference.html │ │ │ ├── dates │ │ │ │ ├── due.html │ │ │ │ ├── end.html │ │ │ │ ├── frequency.html │ │ │ │ ├── issue.html │ │ │ │ └── start.html │ │ │ ├── destinations │ │ │ │ ├── _from_destination.html │ │ │ │ ├── _to_destination.html │ │ │ │ └── _view_clients_dropdown.html │ │ │ ├── notes │ │ │ │ └── notes.html │ │ │ └── services │ │ │ │ ├── +services_table.html │ │ │ │ └── _services_table_body.html │ │ ├── dashboard │ │ │ ├── +payment_status_badge.html │ │ │ ├── _fetch_body.html │ │ │ ├── _modify_payment_status.html │ │ │ ├── core │ │ │ │ └── main.html │ │ │ └── manage.html │ │ ├── recurring │ │ │ ├── dashboard │ │ │ │ ├── _fetch_body.html │ │ │ │ ├── _modify_status.html │ │ │ │ ├── _pause_button.html │ │ │ │ ├── _status_badge.html │ │ │ │ ├── core │ │ │ │ │ └── main.html │ │ │ │ ├── dashboard.html │ │ │ │ ├── manage.html │ │ │ │ ├── poll_response.html │ │ │ │ └── poll_update.html │ │ │ ├── edit │ │ │ │ └── edit_recurring.html │ │ │ └── manage │ │ │ │ └── next_invoice_block.html │ │ ├── single │ │ │ ├── dashboard │ │ │ │ └── dashboard.html │ │ │ ├── edit │ │ │ │ ├── edit.html │ │ │ │ ├── edit_from_destination.html │ │ │ │ └── edit_to_destination.html │ │ │ ├── manage_access │ │ │ │ ├── _table_row.html │ │ │ │ └── manage_access.html │ │ │ ├── schedules │ │ │ │ ├── reminders │ │ │ │ │ ├── _table_body.html │ │ │ │ │ ├── _table_row.html │ │ │ │ │ └── container.html │ │ │ │ ├── schedules │ │ │ │ │ ├── +input-email_to_send_to.html │ │ │ │ │ ├── _table_body.html │ │ │ │ │ ├── _table_row.html │ │ │ │ │ ├── onetime_schedule_create.html │ │ │ │ │ └── onetime_schedule_list.html │ │ │ │ └── view.html │ │ │ └── view │ │ │ │ ├── _banner │ │ │ │ ├── _banner.html │ │ │ │ ├── _button_options_dropdown.html │ │ │ │ ├── _button_options_top.html │ │ │ │ └── _invoice_status.html │ │ │ │ ├── _client-details.html │ │ │ │ └── invoice_page.html │ │ └── structure │ │ │ ├── invoices_list.html │ │ │ └── toggler.html │ ├── landing │ │ ├── addons.html │ │ ├── index.html │ │ ├── landing_base.html │ │ ├── pricing.html │ │ ├── pricing_cards.html │ │ └── pricing_feature.html │ ├── login.html │ ├── products │ │ └── fetched_items.html │ ├── quotas │ │ ├── _fetch_body.html │ │ ├── dashboard.html │ │ ├── list.html │ │ └── view_requests.html │ ├── receipts │ │ ├── _search.html │ │ ├── _search_results.html │ │ └── dashboard.html │ ├── reports │ │ ├── _list_rows.html │ │ ├── dashboard.html │ │ └── monthly_report_base.html │ ├── reset_password.html │ └── settings │ │ ├── main.html │ │ ├── pages │ │ ├── account.html │ │ ├── account_defaults.html │ │ ├── account_security.html │ │ ├── api_keys.html │ │ ├── email_templates.html │ │ └── profile.html │ │ ├── settings │ │ ├── _post_profile_pic.html │ │ ├── admin.html │ │ ├── api_key_generated_response.html │ │ ├── api_key_row.html │ │ ├── email_templates │ │ │ └── tabs.html │ │ ├── preferences.html │ │ ├── session.html │ │ └── sessions.html │ │ └── teams │ │ ├── leave.html │ │ ├── login_to_team.html │ │ ├── main.html │ │ └── permissions.html │ └── partials │ └── messages_list.html ├── infrastructure ├── aws │ ├── handler.py │ ├── iam │ │ └── policies │ │ │ └── invoice-schedules.json │ └── pulumi │ │ ├── .gitignore │ │ ├── __init__.py │ │ ├── __main__.py │ │ ├── emails.py │ │ └── requirements.txt ├── backend │ ├── Dockerfile │ ├── docker-compose.ci.yml │ ├── docker-compose.yml │ ├── scripts │ │ ├── entrypoint.sh │ │ └── tests │ │ │ └── views.sh │ └── urls.sh ├── frontend │ └── default.conf └── nginx │ └── default.conf ├── manage.py ├── mkdocs.yml ├── package-lock.json ├── package.json ├── poetry.lock ├── pyproject.toml ├── settings ├── __init__.py ├── asgi.py ├── helpers.py ├── local_settings.py ├── prod_settings.py ├── settings.py └── wsgi.py ├── tailwind.config.js ├── tests ├── __init__.py ├── api │ ├── __init__.py │ ├── test_account_settings.py │ ├── test_clients.py │ ├── test_invoices.py │ └── test_receipts.py ├── handler.py ├── other_tests │ └── __init__.py ├── urls_INACTIVE │ ├── logged_in.json │ ├── unlogged_in.json │ └── verify_urls.py └── views │ ├── __init__.py │ ├── test_change_password.py │ ├── test_clients.py │ ├── test_clone_single_invoice.py │ ├── test_dashboard.py │ ├── test_index.py │ ├── test_invoices.py │ ├── test_login.py │ ├── test_other.py │ ├── test_receipts.py │ ├── test_receipts_download.py │ ├── test_settings_teams.py │ ├── test_usersettings.py │ └── test_usersettings_profile_settings.py ├── webpack.common.js ├── webpack.dev.js └── webpack.prod.js /.dockerignore: -------------------------------------------------------------------------------- 1 | # Git 2 | .git 3 | .gitignore 4 | 5 | # Docker 6 | .docker 7 | 8 | # Python 9 | app/__pycache__/ 10 | app/*/__pycache__/ 11 | app/*/*/__pycache__/ 12 | app/*/*/*/__pycache__/ 13 | .env/ 14 | .venv/ 15 | venv/ 16 | env/ 17 | 18 | # Node 19 | node_modules -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | 4 | # Top-most EditorConfig file 5 | root = true 6 | 7 | # Unix-style newlines with a newline ending every file 8 | [*] 9 | end_of_line = lf 10 | insert_final_newline = true 11 | 12 | # Trim trailing whitespace automatically 13 | trim_trailing_whitespace = true 14 | 15 | # Set default charset 16 | charset = utf-8 17 | 18 | # Use tabs for indentation 19 | [*.{py,html}] 20 | indent_style = space 21 | indent_size = 4 22 | 23 | [*.{js,yml}] 24 | indent_style = space 25 | indent_size = 2 26 | 27 | # Ignore some directories 28 | [**/__pycache__] 29 | [**/migrations] 30 | [**/node_modules] 31 | [docs/*.md] -------------------------------------------------------------------------------- /.env.github_actions: -------------------------------------------------------------------------------- 1 | DEBUG=True 2 | SECRET_KEY=some!random!secret!key!use!online!generator!to!get 3 | URL=127.0.0.1 4 | PROXY_IP=localhost 5 | BRANCH=debug 6 | 7 | DATABASE_TYPE=sqlite3 8 | 9 | SITE_URL=http://myfinances.example.com 10 | SITE_NAME=myfinances-ghactions -------------------------------------------------------------------------------- /.env.sample: -------------------------------------------------------------------------------- 1 | DEBUG=True 2 | SECRET_KEY=some!random!secret!key!use!online!generator!to!get 3 | URL=127.0.0.1 4 | PROXY_IP=localhost 5 | BRANCH=debug 6 | 7 | SITE_URL=https://myfinances.example.com 8 | SITE_NAME=myfinances 9 | 10 | DATABASE_TYPE=sqlite3 11 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.html linguist-detectable=true 2 | *.py linguist-detectable=true 3 | 4 | *.html linguist-vendored=false -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [treyww] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: 🔍 View Issues 4 | url: https://github.com/TreyWW/MyFinances/issues?q=is%3Aissue 5 | about: If you have found an issue (or a bug), try searching our issues tab before reporting it. 6 | - name: ❓ Need some help 7 | url: https://github.com/TreyWW/MyFinances/discussions/categories/q-a 8 | about: If you have a question or are stuck setting up the project, open a Q&A discussion! 9 | - name: 📖 View our documentation 10 | url: https://strelix.link/mfd/?utm_source=github_issue_create 11 | about: Check out our documentation to get started! We have lots of helpful info in there 12 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "pip" 9 | directory: "/" 10 | open-pull-requests-limit: 0 11 | schedule: 12 | interval: "weekly" 13 | - package-ecosystem: "docker" 14 | directory: "/" 15 | schedule: 16 | interval: "weekly" 17 | - package-ecosystem: "npm" 18 | directory: "/" 19 | open-pull-requests-limit: 0 20 | schedule: 21 | interval: "weekly" 22 | -------------------------------------------------------------------------------- /.github/workflows/check_typos.yml: -------------------------------------------------------------------------------- 1 | name: Check Typos 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | 8 | jobs: 9 | check-typos: 10 | strategy: 11 | fail-fast: true 12 | matrix: 13 | os: [ ubuntu-latest ] 14 | python-version: [ "3.*" ] 15 | 16 | runs-on: ${{ matrix.os }} 17 | 18 | steps: 19 | #---------------------------------------------- 20 | # check-out repo and set-up python 21 | #---------------------------------------------- 22 | - name: Checkout code 23 | uses: actions/checkout@v4 24 | 25 | - name: Set up Python ${{ matrix.python-version }} 26 | uses: actions/setup-python@v2 27 | with: 28 | python-version: ${{ matrix.python-version }} 29 | 30 | #---------------------------------------------- 31 | # check grammar with typos 32 | #---------------------------------------------- 33 | - name: Check grammar with typos 34 | run: | 35 | pip3 install typos 36 | typos -------------------------------------------------------------------------------- /.github/workflows/run_black_linter.yml: -------------------------------------------------------------------------------- 1 | name: Black Linter (PY) 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - '**.py' 9 | pull_request: 10 | paths: 11 | - '**.py' 12 | 13 | jobs: 14 | lint: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4.1.6 18 | - uses: psf/black@stable 19 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | disable= 3 | C0114, C0115, C0116 4 | -------------------------------------------------------------------------------- /.typos.toml: -------------------------------------------------------------------------------- 1 | [files] 2 | extend-exclude = ["**/bundle.js", "**/bundle.js.map", "**/cal_filters.py"] 3 | 4 | [default] 5 | extend-ignore-identifiers-re = [ 6 | "zUfuhFKKZCbHTY6aRR46gxiqszMk5tcHjsVFxnUo8VMus4kHGVdIYVbOYYNlKmHV", 7 | "sha384-zUfuhFKKZCbHTY6aRR46gxiqszMk5tcHjsVFxnUo8VMus4kHGVdIYVbOYYNlKmHV" 8 | ] 9 | -------------------------------------------------------------------------------- /Pulumi.yaml: -------------------------------------------------------------------------------- 1 | name: myfinances 2 | main: /infrastructure/aws/pulumi/ 3 | stackConfigDir: /infrastructure/aws/pulumi/ 4 | runtime: 5 | name: python 6 | options: 7 | virtualenv: venv 8 | description: MyFinances infrastructure 9 | config: 10 | pulumi:tags: 11 | value: 12 | pulumi:template: aws-python 13 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | 4 | If you have found a security vulnerability in MyFinances please __**DO NOT**__ share this in Github Issues, Discussions, or any public chat space. Create a [security advisory](https://github.com/TreyWW/MyFinances/security/advisories/new) with as much detail as you are aware of. 5 | 6 | ## Supported Versions 7 | 8 | We are currently still in development and have not reached a public V1 release. We only support the most recent update for feature upgrades, bug reports and security patches. 9 | 10 | | Version | Supported | 11 | | ------- | ------------------ | 12 | | 0.x.x | :white_check_mark: | 13 | -------------------------------------------------------------------------------- /assets/scripts/font_awesome.js: -------------------------------------------------------------------------------- 1 | import '@fortawesome/fontawesome-free/js/fontawesome' 2 | import '@fortawesome/fontawesome-free/js/solid' 3 | import '@fortawesome/fontawesome-free/js/regular' 4 | import '@fortawesome/fontawesome-free/js/brands' 5 | -------------------------------------------------------------------------------- /assets/scripts/receipt_downloads.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Downloads a file from the specified URL. 3 | * 4 | * @param {string} downloadUrl - The URL of the file to be downloaded. 5 | */ 6 | 7 | /*#__NO_RENAME__*/ 8 | window.download_file = function Download_file(downloadUrl) { 9 | var xhr = new XMLHttpRequest(); 10 | xhr.open('GET', downloadUrl, true); 11 | xhr.onload = function () { 12 | if (this.status === 200) { 13 | var response = JSON.parse(this.responseText); 14 | 15 | // Use the download_link and filename from the JSON response 16 | var downloadLink = response.download_link; 17 | var filename = response.filename; 18 | 19 | // Trigger the download using a link element 20 | var a = document.createElement('a'); 21 | a.href = downloadLink; 22 | a.download = filename; 23 | document.body.appendChild(a); 24 | a.click(); 25 | document.body.removeChild(a); 26 | } 27 | }; 28 | xhr.send(); 29 | } 30 | -------------------------------------------------------------------------------- /assets/scripts/validate_payment_details.js: -------------------------------------------------------------------------------- 1 | window.validate_sort_code = function validate_sort_code(value) { 2 | // Remove any non-numeric characters 3 | value = value.replace(/\D/g, ''); 4 | 5 | // Ensure the value is no longer than 9 characters (123-123-123) 6 | if (value.length > 6) { 7 | value = value.slice(0, 6); 8 | } 9 | 10 | // Format the value as "12-34-56" 11 | if (value.length >= 2) { 12 | value = value.slice(0, 2) + "-" + value.slice(2); 13 | } 14 | if (value.length >= 5) { 15 | value = value.slice(0, 5) + "-" + value.slice(5); 16 | } 17 | 18 | return value; 19 | } 20 | 21 | window.validate_account_number = function validate_account_number(value) { 22 | // Remove any non-numeric characters 23 | value = value.replace(/\D/g, ''); 24 | 25 | // Ensure the value is no longer than 16 characters (1234-1234-1234-1234) 26 | if (value.length > 8) { 27 | value = value.slice(0, 8); 28 | } 29 | 30 | return value; 31 | } 32 | -------------------------------------------------------------------------------- /backend/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | __version__ = "0.9.2" 4 | -------------------------------------------------------------------------------- /backend/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class BackendConfig(AppConfig): 5 | name = "backend" 6 | 7 | def ready(self): 8 | from .finance import signals 9 | from .core import signals 10 | 11 | # from .clients import signals 12 | # from .storage import signals 13 | # from .events import signals 14 | pass 15 | -------------------------------------------------------------------------------- /backend/auth_backends.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | from django.contrib.auth.backends import ModelBackend 3 | 4 | 5 | class EmailInsteadOfUsernameBackend(ModelBackend): 6 | def authenticate(self, request, username=None, password=None, **kwargs): 7 | UserModel = get_user_model() 8 | 9 | if username is None or password is None: 10 | return 11 | 12 | try: 13 | user = UserModel.objects.get(email=username) 14 | except UserModel.DoesNotExist: 15 | # Run the default password hasher once to reduce the timing 16 | # difference between an existing and a nonexistent user (#20760). 17 | UserModel().set_password(password) 18 | return None 19 | else: 20 | if user.check_password(password): 21 | return user 22 | return None 23 | -------------------------------------------------------------------------------- /backend/clients/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/clients/__init__.py -------------------------------------------------------------------------------- /backend/clients/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/clients/api/__init__.py -------------------------------------------------------------------------------- /backend/clients/api/delete.py: -------------------------------------------------------------------------------- 1 | from django.contrib import messages 2 | from django.shortcuts import render 3 | from django.views.decorators.http import require_http_methods 4 | 5 | from backend.decorators import web_require_scopes 6 | from backend.core.service.clients.delete import delete_client, DeleteClientServiceResponse 7 | from backend.core.types.requests import WebRequest 8 | 9 | 10 | @require_http_methods(["DELETE"]) 11 | @web_require_scopes("clients:write") 12 | def client_delete(request: WebRequest, id: int): 13 | response: DeleteClientServiceResponse = delete_client(request, id) 14 | 15 | if response.failed: 16 | messages.error(request, response.error) 17 | else: 18 | messages.success(request, f"Successfully deleted client #{id}") 19 | return render(request, "base/toast.html") 20 | -------------------------------------------------------------------------------- /backend/clients/api/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from backend.clients.api import fetch, delete 3 | 4 | urlpatterns = [ 5 | path( 6 | "fetch/", 7 | fetch.fetch_all_clients, 8 | name="fetch", 9 | ), 10 | path( 11 | "fetch/dropdown/", 12 | fetch.fetch_clients_dropdown, 13 | name="fetch dropdown", 14 | ), 15 | path( 16 | "delete//", 17 | delete.client_delete, 18 | name="delete", 19 | ), 20 | ] 21 | app_name = "clients" 22 | -------------------------------------------------------------------------------- /backend/clients/views/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/clients/views/__init__.py -------------------------------------------------------------------------------- /backend/clients/views/create.py: -------------------------------------------------------------------------------- 1 | from django.contrib import messages 2 | from django.shortcuts import render, redirect 3 | 4 | from backend.decorators import web_require_scopes 5 | from backend.core.service.clients.create import create_client, CreateClientServiceResponse 6 | from backend.core.types.requests import WebRequest 7 | 8 | 9 | @web_require_scopes("clients:write", False, False, "clients:dashboard") 10 | def create_client_endpoint(request: WebRequest): 11 | if request.method == "GET": 12 | return render(request, "pages/clients/create/create.html") 13 | 14 | client_response: CreateClientServiceResponse = create_client(request) 15 | 16 | if client_response.failed: 17 | messages.error(request, client_response.error) 18 | return redirect("clients:create") 19 | 20 | messages.success(request, f"Client created successfully (#{client_response.response.id})") 21 | 22 | return redirect("clients:dashboard") 23 | -------------------------------------------------------------------------------- /backend/clients/views/dashboard.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | from backend.decorators import web_require_scopes 4 | from backend.core.types.htmx import HtmxHttpRequest 5 | 6 | 7 | @web_require_scopes("clients:read", False, False, "dashboard") 8 | def clients_dashboard_endpoint(request: HtmxHttpRequest): 9 | return render(request, "pages/clients/dashboard/dashboard.html") 10 | -------------------------------------------------------------------------------- /backend/clients/views/edit.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/clients/views/edit.py -------------------------------------------------------------------------------- /backend/clients/views/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from .dashboard import clients_dashboard_endpoint 4 | from .detail import client_detail_endpoint, delete_client_endpoint 5 | from .create import create_client_endpoint 6 | 7 | urlpatterns = [ 8 | path("", clients_dashboard_endpoint, name="dashboard"), 9 | path("/", client_detail_endpoint, name="detail"), 10 | path( 11 | "create/", 12 | create_client_endpoint, 13 | name="create", 14 | ), 15 | path("/delete/", delete_client_endpoint, name="delete"), 16 | # path("/edit/", client_edit_endpoint, name="edit"), 17 | ] 18 | 19 | app_name = "clients" 20 | -------------------------------------------------------------------------------- /backend/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/core/__init__.py -------------------------------------------------------------------------------- /backend/core/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/core/api/__init__.py -------------------------------------------------------------------------------- /backend/core/api/base/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/core/api/base/__init__.py -------------------------------------------------------------------------------- /backend/core/api/base/breadcrumbs.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | from backend.core.types.requests import WebRequest 4 | from backend.core.service.base.breadcrumbs import get_breadcrumbs 5 | 6 | 7 | def update_breadcrumbs_endpoint(request: WebRequest): 8 | url = request.GET.get("url") 9 | 10 | breadcrumb_dict: dict = get_breadcrumbs(url=url) 11 | return render( 12 | request, 13 | "base/breadcrumbs.html", 14 | { 15 | "breadcrumb": breadcrumb_dict.get("breadcrumb"), 16 | "swapping": True, 17 | # "swap": True 18 | }, 19 | ) 20 | -------------------------------------------------------------------------------- /backend/core/api/base/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from . import modal, notifications, breadcrumbs 3 | 4 | urlpatterns = [ 5 | path( 6 | "modals//retrieve", 7 | modal.open_modal, 8 | name="modal retrieve", 9 | ), 10 | path( 11 | "modals//retrieve//", 12 | modal.open_modal, 13 | name="modal retrieve with context", 14 | ), 15 | path( 16 | "notifications/get", 17 | notifications.get_notification_html, 18 | name="notifications get", 19 | ), 20 | path("notifications/get_count", notifications.get_notification_count_html, name="notifications get count"), 21 | path( 22 | "notifications/delete/", 23 | notifications.delete_notification, 24 | name="notifications delete", 25 | ), 26 | path("breadcrumbs/refetch/", breadcrumbs.update_breadcrumbs_endpoint, name="breadcrumbs refetch"), 27 | ] 28 | 29 | app_name = "base" 30 | -------------------------------------------------------------------------------- /backend/core/api/emails/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/core/api/emails/__init__.py -------------------------------------------------------------------------------- /backend/core/api/emails/urls.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from django.urls import path 4 | 5 | from . import send, fetch, status 6 | 7 | urlpatterns = [ 8 | path( 9 | "send/single/", 10 | send.send_single_email_view, 11 | name="send single", 12 | ), 13 | path( 14 | "send/bulk/", 15 | send.send_bulk_email_view, 16 | name="send bulk", 17 | ), 18 | path("fetch/", fetch.fetch_all_emails, name="fetch"), 19 | path("get_status//", status.get_status_view, name="get_status"), 20 | path("refresh_statuses/", status.refresh_all_statuses_view, name="refresh statuses"), 21 | ] 22 | 23 | app_name = "emails" 24 | -------------------------------------------------------------------------------- /backend/core/api/healthcheck/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/core/api/healthcheck/__init__.py -------------------------------------------------------------------------------- /backend/core/api/healthcheck/healthcheck.py: -------------------------------------------------------------------------------- 1 | from django.db import connection, OperationalError 2 | from django.http import HttpRequest, HttpResponse 3 | from login_required import login_not_required 4 | 5 | 6 | @login_not_required 7 | def ping(request: HttpRequest) -> HttpResponse: 8 | return HttpResponse("pong") 9 | 10 | 11 | @login_not_required 12 | def healthcheck(request: HttpRequest) -> HttpResponse: 13 | try: 14 | connection.ensure_connection() 15 | return HttpResponse(status=200, content="All operations are up and running!") 16 | except OperationalError: 17 | return HttpResponse(status=503, content="Service Unavailable") 18 | -------------------------------------------------------------------------------- /backend/core/api/healthcheck/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from . import healthcheck 4 | 5 | urlpatterns = [ 6 | path( 7 | "ping/", 8 | healthcheck.ping, 9 | name="ping", 10 | ), 11 | path( 12 | "healthcheck/", 13 | healthcheck.healthcheck, 14 | name="healthcheck", 15 | ), 16 | ] 17 | 18 | app_name = "healthcheck" 19 | -------------------------------------------------------------------------------- /backend/core/api/landing_page/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/core/api/landing_page/__init__.py -------------------------------------------------------------------------------- /backend/core/api/landing_page/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from . import email_waitlist 3 | 4 | urlpatterns = [ 5 | path("join_waitlist/", email_waitlist.join_waitlist_endpoint, name="join_waitlist"), 6 | ] 7 | 8 | app_name = "landing_page" 9 | -------------------------------------------------------------------------------- /backend/core/api/maintenance/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/core/api/maintenance/__init__.py -------------------------------------------------------------------------------- /backend/core/api/maintenance/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from . import now 4 | 5 | urlpatterns = [ 6 | path("cleanup/", now.handle_maintenance_now_endpoint, name="cleanup"), 7 | ] 8 | 9 | app_name = "maintenance" 10 | -------------------------------------------------------------------------------- /backend/core/api/public/__init__.py: -------------------------------------------------------------------------------- 1 | from .models import APIAuthToken 2 | -------------------------------------------------------------------------------- /backend/core/api/public/endpoints/Invoices/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/core/api/public/endpoints/Invoices/__init__.py -------------------------------------------------------------------------------- /backend/core/api/public/endpoints/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/core/api/public/endpoints/__init__.py -------------------------------------------------------------------------------- /backend/core/api/public/endpoints/clients/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/core/api/public/endpoints/clients/__init__.py -------------------------------------------------------------------------------- /backend/core/api/public/endpoints/clients/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from . import list, delete 4 | from .create import client_create_endpoint 5 | 6 | urlpatterns = [ 7 | path( 8 | "", 9 | list.list_clients_endpoint, 10 | name="list", 11 | ), 12 | path("/", delete.client_delete_endpoint, name="delete"), 13 | path("create/", client_create_endpoint, name="create"), 14 | ] 15 | 16 | app_name = "clients" 17 | -------------------------------------------------------------------------------- /backend/core/api/public/endpoints/webhooks/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/core/api/public/endpoints/webhooks/__init__.py -------------------------------------------------------------------------------- /backend/core/api/public/endpoints/webhooks/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from .webhook_task_queue_handler import webhook_task_queue_handler_view_endpoint 3 | 4 | urlpatterns = [ 5 | path( 6 | "receive/global/", 7 | webhook_task_queue_handler_view_endpoint, 8 | name="receive_global", 9 | ) 10 | ] 11 | 12 | app_name = "webhooks" 13 | -------------------------------------------------------------------------------- /backend/core/api/public/helpers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/core/api/public/helpers/__init__.py -------------------------------------------------------------------------------- /backend/core/api/public/helpers/response.py: -------------------------------------------------------------------------------- 1 | from rest_framework.response import Response 2 | 3 | 4 | def APIResponse(success: bool = True, data: str | dict | None = None, meta=None, status: int = 0, **kwargs) -> Response: 5 | """ 6 | 7 | Returns a rest_framework Response object, but prefills meta (success etc) aswell as the data with KWARGS. 8 | 9 | """ 10 | meta = meta or {} 11 | if not status and success: 12 | status = 201 13 | elif not status: 14 | status = 400 15 | 16 | if success: 17 | return Response({"meta": {"success": True, **meta}, "data": {**kwargs} | data if isinstance(data, dict) else {}}, status=status) 18 | else: 19 | return Response({"meta": {"success": False}, "error": data}, status=status) 20 | -------------------------------------------------------------------------------- /backend/core/api/public/middleware.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/core/api/public/middleware.py -------------------------------------------------------------------------------- /backend/core/api/public/serializers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/core/api/public/serializers/__init__.py -------------------------------------------------------------------------------- /backend/core/api/public/serializers/clients.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from backend.finance.models import Client 4 | 5 | 6 | class ClientSerializer(serializers.ModelSerializer): 7 | class Meta: 8 | model = Client 9 | exclude = ("organization", "user", "email_verified") 10 | -------------------------------------------------------------------------------- /backend/core/api/public/serializers/invoices.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from backend.finance.models import InvoiceItem, Invoice 4 | 5 | 6 | class InvoiceItemSerializer(serializers.ModelSerializer): 7 | name = serializers.CharField(required=False) 8 | description = serializers.CharField(required=False) 9 | 10 | class Meta: 11 | model = InvoiceItem 12 | fields = "__all__" 13 | 14 | 15 | class InvoiceSerializer(serializers.ModelSerializer): 16 | items = InvoiceItemSerializer(many=True, required=False) 17 | 18 | class Meta: 19 | model = Invoice 20 | exclude = ("user", "organization", "client_to") 21 | # fields = "__all__" 22 | 23 | def create(self, validated_data): 24 | items_data = validated_data.pop("items", []) 25 | invoice = Invoice.objects.create(**validated_data) 26 | 27 | for item_data in items_data: 28 | item = InvoiceItem.objects.create(invoice=invoice, **item_data) 29 | invoice.items.add(item) 30 | 31 | return invoice 32 | -------------------------------------------------------------------------------- /backend/core/api/public/types.py: -------------------------------------------------------------------------------- 1 | from rest_framework.request import Request 2 | 3 | from backend.core.api.public import APIAuthToken 4 | from backend.models import User, Organization 5 | 6 | 7 | class APIRequest(Request): 8 | user: User 9 | auth: APIAuthToken 10 | api_token: APIAuthToken 11 | team: Organization | None 12 | team_id: int | None 13 | -------------------------------------------------------------------------------- /backend/core/api/public/urls.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from django.conf.urls import include 4 | from django.urls import path, re_path 5 | from rest_framework.authentication import TokenAuthentication 6 | 7 | from .endpoints.system_health import system_health_endpoint 8 | 9 | INTERNAL_URLS = [path("health/", system_health_endpoint, name="public-system-health")] 10 | 11 | urlpatterns = [ 12 | path("internal/", include(INTERNAL_URLS)), 13 | path("clients/", include("backend.core.api.public.endpoints.clients.urls")), 14 | path("invoices/", include("backend.core.api.public.endpoints.Invoices.urls")), 15 | path("webhooks/", include("backend.core.api.public.endpoints.webhooks.urls")), 16 | ] 17 | 18 | app_name = "public" 19 | -------------------------------------------------------------------------------- /backend/core/api/quotas/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/core/api/quotas/__init__.py -------------------------------------------------------------------------------- /backend/core/api/quotas/fetch.py: -------------------------------------------------------------------------------- 1 | from django.db.models import Q 2 | from django.shortcuts import render, redirect 3 | 4 | from backend.models import QuotaLimit 5 | from backend.core.types.htmx import HtmxHttpRequest 6 | 7 | 8 | def fetch_all_quotas(request: HtmxHttpRequest, group: str): 9 | context = {} 10 | if not request.htmx: 11 | return redirect("quotas") 12 | 13 | search_text = request.GET.get("search") 14 | 15 | results = QuotaLimit.objects.filter(slug__startswith=group).prefetch_related("quota_overrides", "quota_usage").order_by("-slug") 16 | 17 | if search_text: 18 | results = results.filter(Q(name__icontains=search_text)) 19 | 20 | quotas = [ 21 | { 22 | "quota_limit": ql.get_quota_limit(request.user), 23 | "period_usage": ql.get_period_usage(request.user), 24 | "quota_object": ql, 25 | } 26 | for ql in results 27 | ] 28 | 29 | context.update({"quotas": quotas}) 30 | return render(request, "pages/quotas/_fetch_body.html", context) 31 | -------------------------------------------------------------------------------- /backend/core/api/quotas/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from . import fetch, requests 4 | 5 | urlpatterns = [ 6 | path( 7 | "fetch//", 8 | fetch.fetch_all_quotas, 9 | name="fetch", 10 | ), 11 | path("submit_request//", requests.submit_request, name="submit_request"), 12 | path("request//approve/", requests.approve_request, name="approve request"), 13 | path("request//decline/", requests.decline_request, name="decline request"), 14 | ] 15 | 16 | app_name = "quotas" 17 | -------------------------------------------------------------------------------- /backend/core/api/settings/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/core/api/settings/__init__.py -------------------------------------------------------------------------------- /backend/core/api/teams/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/core/api/teams/__init__.py -------------------------------------------------------------------------------- /backend/core/api/urls.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from django.urls import include 4 | from django.urls import path 5 | 6 | urlpatterns = [ 7 | path("base/", include("backend.core.api.base.urls")), 8 | path("teams/", include("backend.core.api.teams.urls")), 9 | path("settings/", include("backend.core.api.settings.urls")), 10 | path("quotas/", include("backend.core.api.quotas.urls")), 11 | path("clients/", include("backend.clients.api.urls")), 12 | path("emails/", include("backend.core.api.emails.urls")), 13 | path("maintenance/", include("backend.core.api.maintenance.urls")), 14 | path("landing_page/", include("backend.core.api.landing_page.urls")), 15 | path("public/", include("backend.core.api.public.urls")), 16 | path("", include("backend.finance.api.urls")), 17 | ] 18 | 19 | app_name = "api" 20 | -------------------------------------------------------------------------------- /backend/core/data/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/core/data/__init__.py -------------------------------------------------------------------------------- /backend/core/data/default_feature_flags.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from dataclasses import dataclass 4 | 5 | 6 | @dataclass 7 | class FeatureFlag: 8 | name: str 9 | description: str 10 | default: bool 11 | 12 | 13 | default_feature_flags: list[FeatureFlag] = [ 14 | FeatureFlag(name="areSignupsEnabled", description="Are new account creations allowed", default=True), 15 | FeatureFlag( 16 | name="isInvoiceSchedulingEnabled", 17 | description="Invoice Scheduling allows for clients to create invoice schedules that send and invoice at a specific date.", 18 | default=False, 19 | ), 20 | FeatureFlag(name="areUserEmailsAllowed", description="Are users allowed to send emails from YOUR DOMAIN to customers", default=False), 21 | FeatureFlag( 22 | name="areInvoiceRemindersEnabled", 23 | description="Invoice Reminders allow for clients to be reminded to pay an invoice.", 24 | default=False, 25 | ), 26 | ] 27 | -------------------------------------------------------------------------------- /backend/core/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/core/management/__init__.py -------------------------------------------------------------------------------- /backend/core/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/core/management/commands/__init__.py -------------------------------------------------------------------------------- /backend/core/management/commands/auto.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | from backend.core.service.maintenance.expire.run import expire_and_cleanup_objects 3 | 4 | 5 | class Command(BaseCommand): 6 | """ 7 | Runs automation scripts to make sure objects are up to date, expired objects are deleted, etc. 8 | """ 9 | 10 | def handle(self, *args, **kwargs): 11 | self.stdout.write("Running expire + cleanup script...") 12 | self.stdout.write(expire_and_cleanup_objects()) 13 | -------------------------------------------------------------------------------- /backend/core/management/commands/navbar_refresh.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | from django.core.cache import cache 3 | 4 | 5 | class Command(BaseCommand): 6 | """ 7 | Deletes the "navbar_items" cache and prints a message to the standard output. 8 | """ 9 | 10 | def handle(self, *args, **kwargs): 11 | cache.delete("navbar_items") 12 | self.stdout.write("Cleared cache\n") 13 | -------------------------------------------------------------------------------- /backend/core/management/commands/test_urls.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | from django.core.management import call_command 3 | 4 | 5 | class Command(BaseCommand): 6 | help = "Runs URL verification tests." 7 | 8 | def handle(self, *args, **options): 9 | call_command("test", "backend.tests.urls.verify_urls") 10 | -------------------------------------------------------------------------------- /backend/core/management/scheduled_tasks/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/core/management/scheduled_tasks/__init__.py -------------------------------------------------------------------------------- /backend/core/management/scheduled_tasks/update_all_schedules.py: -------------------------------------------------------------------------------- 1 | import threading 2 | from backend.finance.models import InvoiceRecurringProfile 3 | from backend.core.service.boto3.scheduler.update_schedule import update_boto_schedule 4 | 5 | 6 | # thread = threading.Thread(target=self._send_message, args=(func_name, args, kwargs)) 7 | # threads = list() 8 | # thread.start() 9 | 10 | # def fetch_and_update_profile(profile: InvoiceRecurringProfile): 11 | # update_boto_schedule(profile.id) 12 | 13 | 14 | def refresh_all_schedules_statuses(): 15 | print("REFRESHING ALL SCHEDULE STATUSES!!!!!!!!!!!!!!!!!!") 16 | threads: list = [] 17 | 18 | all_recurring_profiles = InvoiceRecurringProfile.objects.filter(active=True).all() 19 | 20 | for profile in all_recurring_profiles: 21 | x = threading.Thread(target=update_boto_schedule, args=(profile.id,)) 22 | threads.append(x) 23 | x.start() 24 | -------------------------------------------------------------------------------- /backend/core/service/__init__.py: -------------------------------------------------------------------------------- 1 | from backend.core.service.boto3.handler import BOTO3_HANDLER 2 | -------------------------------------------------------------------------------- /backend/core/service/api_keys/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/core/service/api_keys/__init__.py -------------------------------------------------------------------------------- /backend/core/service/api_keys/delete.py: -------------------------------------------------------------------------------- 1 | from backend.models import User, Organization 2 | from backend.core.service.api_keys.get import get_api_key_by_name 3 | from backend.core.api.public import APIAuthToken 4 | 5 | 6 | def delete_api_key(request, owner: User | Organization, key: str | None | APIAuthToken) -> bool | str: 7 | if isinstance(owner, Organization) and "api_keys:write" not in owner.permissions.get(user=request.user).scopes: 8 | return "No permission to delete key" 9 | 10 | if not isinstance(key, APIAuthToken): 11 | key: APIAuthToken | None = get_api_key_by_name(owner, key) # type: ignore[no-redef, arg-type] 12 | 13 | if not key: 14 | return "Key not found" 15 | 16 | key.deactivate() # type: ignore[union-attr] 17 | 18 | return True 19 | -------------------------------------------------------------------------------- /backend/core/service/api_keys/get.py: -------------------------------------------------------------------------------- 1 | from backend.core.api.public import APIAuthToken 2 | from backend.models import User, Organization 3 | 4 | 5 | def get_api_key_by_name(owner: User | Organization, key_name: str) -> APIAuthToken | None: 6 | return APIAuthToken.filter_by_owner(owner).filter(name=key_name, active=True).first() 7 | 8 | 9 | def get_api_key_by_id(owner: User | Organization, key_id: str | int) -> APIAuthToken | None: 10 | return APIAuthToken.filter_by_owner(owner).filter(id=key_id, active=True).first() 11 | -------------------------------------------------------------------------------- /backend/core/service/asyn_tasks/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/core/service/asyn_tasks/__init__.py -------------------------------------------------------------------------------- /backend/core/service/base/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/core/service/base/__init__.py -------------------------------------------------------------------------------- /backend/core/service/boto3/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/core/service/boto3/__init__.py -------------------------------------------------------------------------------- /backend/core/service/boto3/scheduler/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/core/service/boto3/scheduler/__init__.py -------------------------------------------------------------------------------- /backend/core/service/clients/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/core/service/clients/__init__.py -------------------------------------------------------------------------------- /backend/core/service/clients/get.py: -------------------------------------------------------------------------------- 1 | from django.db.models import Q, QuerySet 2 | 3 | from backend.models import Client, Organization 4 | from backend.core.utils.dataclasses import BaseServiceResponse 5 | 6 | 7 | class FetchClientServiceResponse(BaseServiceResponse[QuerySet[Client]]): ... 8 | 9 | 10 | def fetch_clients(request, *, search_text: str | None = None, team: Organization | None = None) -> FetchClientServiceResponse: 11 | if team: 12 | clients = Client.objects.filter(organization=team, active=True) 13 | else: 14 | clients = Client.objects.filter(user=request.user, active=True) 15 | 16 | if search_text: 17 | clients = clients.filter(Q(name__icontains=search_text) | Q(email__icontains=search_text) | Q(id__icontains=search_text)) 18 | 19 | return FetchClientServiceResponse(True, clients) 20 | -------------------------------------------------------------------------------- /backend/core/service/defaults/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/core/service/defaults/__init__.py -------------------------------------------------------------------------------- /backend/core/service/defaults/get.py: -------------------------------------------------------------------------------- 1 | from backend.models import User, Organization 2 | from backend.clients.models import DefaultValues, Client 3 | 4 | 5 | def get_account_defaults(actor: User | Organization, client: Client | None = None) -> DefaultValues: 6 | if not client: 7 | account_defaults = DefaultValues.filter_by_owner(owner=actor).filter(client__isnull=True).first() 8 | 9 | if account_defaults: 10 | return account_defaults 11 | return DefaultValues.objects.create(owner=actor, client=None) # type: ignore[misc] 12 | return DefaultValues.filter_by_owner(owner=actor).get(client=client) 13 | -------------------------------------------------------------------------------- /backend/core/service/file_storage/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/core/service/file_storage/__init__.py -------------------------------------------------------------------------------- /backend/core/service/file_storage/utils.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | 4 | def format_file_size(size): 5 | """Convert bytes to a human-readable format.""" 6 | if size == 0: 7 | return "0B" 8 | size_name = ("B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB") 9 | i = int(math.floor(math.log(size, 1024))) 10 | p = math.pow(1024, i) 11 | s = round(size / p, 2) 12 | return f"{s} {size_name[i]}" 13 | -------------------------------------------------------------------------------- /backend/core/service/invoices/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/core/service/invoices/__init__.py -------------------------------------------------------------------------------- /backend/core/service/invoices/common/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/core/service/invoices/common/__init__.py -------------------------------------------------------------------------------- /backend/core/service/invoices/common/create/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/core/service/invoices/common/create/__init__.py -------------------------------------------------------------------------------- /backend/core/service/invoices/common/create/services/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/core/service/invoices/common/create/services/__init__.py -------------------------------------------------------------------------------- /backend/core/service/invoices/common/emails/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/core/service/invoices/common/emails/__init__.py -------------------------------------------------------------------------------- /backend/core/service/invoices/handler.py: -------------------------------------------------------------------------------- 1 | PAGES = ["single", "recurring"] 2 | 3 | SUB_PAGES = ["single/create"] 4 | 5 | 6 | def validate_page(page: str, sub_page: str) -> bool: 7 | if not page: 8 | return True 9 | 10 | if sub_page: 11 | for sub_page_looped in SUB_PAGES: 12 | sub_page_page = sub_page_looped.split("/")[0] 13 | if page == sub_page_page and sub_page == sub_page_looped.split("/")[1]: 14 | return True 15 | else: 16 | if page in PAGES: 17 | return True 18 | return False 19 | -------------------------------------------------------------------------------- /backend/core/service/invoices/recurring/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/core/service/invoices/recurring/__init__.py -------------------------------------------------------------------------------- /backend/core/service/invoices/recurring/create/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/core/service/invoices/recurring/create/__init__.py -------------------------------------------------------------------------------- /backend/core/service/invoices/recurring/generation/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/core/service/invoices/recurring/generation/__init__.py -------------------------------------------------------------------------------- /backend/core/service/invoices/recurring/schedules/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/core/service/invoices/recurring/schedules/__init__.py -------------------------------------------------------------------------------- /backend/core/service/invoices/recurring/validate/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/core/service/invoices/recurring/validate/__init__.py -------------------------------------------------------------------------------- /backend/core/service/invoices/recurring/webhooks/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/core/service/invoices/recurring/webhooks/__init__.py -------------------------------------------------------------------------------- /backend/core/service/invoices/single/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/core/service/invoices/single/__init__.py -------------------------------------------------------------------------------- /backend/core/service/invoices/single/create/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/core/service/invoices/single/create/__init__.py -------------------------------------------------------------------------------- /backend/core/service/invoices/single/create/get_page.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | 3 | from backend.core.service.invoices.common.create.get_page import global_get_invoice_context 4 | from backend.core.types.requests import WebRequest 5 | 6 | 7 | def get_invoice_context(request: WebRequest) -> dict: 8 | global_context_service_response = global_get_invoice_context(request) 9 | 10 | context = global_context_service_response.response.context 11 | defaults = global_context_service_response.response.defaults 12 | 13 | if due_date := request.GET.get("due_date"): 14 | try: 15 | date.fromisoformat(due_date) 16 | context["due_date"] = due_date 17 | except ValueError: 18 | ... 19 | 20 | if not due_date: 21 | context["issue_date"], context["due_date"] = defaults.get_issue_and_due_dates(context["issue_date"]) 22 | return context 23 | -------------------------------------------------------------------------------- /backend/core/service/invoices/single/create_url.py: -------------------------------------------------------------------------------- 1 | from backend.finance.models import InvoiceURL, Invoice 2 | from backend.core.models import User 3 | from backend.core.utils.dataclasses import BaseServiceResponse 4 | 5 | 6 | class CreateInvoiceURLServiceResponse(BaseServiceResponse[InvoiceURL]): ... 7 | 8 | 9 | def create_invoice_url(invoice: Invoice, user: User | None = None) -> CreateInvoiceURLServiceResponse: 10 | return CreateInvoiceURLServiceResponse( 11 | True, 12 | response=InvoiceURL.objects.create( 13 | invoice=invoice, 14 | created_by=user, 15 | ), 16 | ) 17 | -------------------------------------------------------------------------------- /backend/core/service/invoices/single/get_invoice.py: -------------------------------------------------------------------------------- 1 | from backend.finance.models import Invoice, Organization, User 2 | from backend.core.utils.dataclasses import BaseServiceResponse 3 | 4 | 5 | class GetInvoiceServiceResponse(BaseServiceResponse[Invoice]): ... 6 | 7 | 8 | def get_invoice_by_actor(actor: User | Organization, id: str | int, prefetch_related: list[str] | None = None) -> GetInvoiceServiceResponse: 9 | prefetch_related_args: list[str] = prefetch_related or [] 10 | try: 11 | invoice: Invoice = Invoice.filter_by_owner(actor).prefetch_related(*prefetch_related_args).get(id=id) 12 | return GetInvoiceServiceResponse(True, response=invoice) 13 | except Invoice.DoesNotExist: 14 | return GetInvoiceServiceResponse(False, error_message="Invoice not found") 15 | -------------------------------------------------------------------------------- /backend/core/service/maintenance/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/core/service/maintenance/__init__.py -------------------------------------------------------------------------------- /backend/core/service/maintenance/expire/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/core/service/maintenance/expire/__init__.py -------------------------------------------------------------------------------- /backend/core/service/permissions/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/core/service/permissions/__init__.py -------------------------------------------------------------------------------- /backend/core/service/reports/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/core/service/reports/__init__.py -------------------------------------------------------------------------------- /backend/core/service/reports/get.py: -------------------------------------------------------------------------------- 1 | from backend.models import MonthlyReport, User, Organization 2 | from backend.core.utils.dataclasses import BaseServiceResponse 3 | 4 | 5 | class GetReportServiceResponse(BaseServiceResponse[MonthlyReport]): ... 6 | 7 | 8 | def get_report(owner: User | Organization, uuid) -> GetReportServiceResponse: 9 | report = MonthlyReport.filter_by_owner(owner).filter(uuid=uuid).first() 10 | 11 | if report: 12 | return GetReportServiceResponse(True, report) 13 | else: 14 | return GetReportServiceResponse(False, error_message="Report not found") 15 | -------------------------------------------------------------------------------- /backend/core/service/settings/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/core/service/settings/__init__.py -------------------------------------------------------------------------------- /backend/core/service/teams/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/core/service/teams/__init__.py -------------------------------------------------------------------------------- /backend/core/service/teams/fetch.py: -------------------------------------------------------------------------------- 1 | from django.db.models import QuerySet 2 | 3 | from backend.models import Organization 4 | from backend.core.types.requests import WebRequest 5 | 6 | 7 | def get_all_users_teams(request: WebRequest) -> QuerySet[Organization]: 8 | return request.user.teams_joined.all() | request.user.teams_leader_of.all() 9 | -------------------------------------------------------------------------------- /backend/core/service/webhooks/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/core/service/webhooks/__init__.py -------------------------------------------------------------------------------- /backend/core/service/webhooks/auth.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/core/service/webhooks/auth.py -------------------------------------------------------------------------------- /backend/core/service/webhooks/get_url.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from django.urls import reverse 4 | 5 | 6 | def get_global_webhook_response_url(): 7 | return os.environ.get("SITE_URL", default="http://127.0.0.1:8000") + reverse("api:public:webhooks:receive_global") 8 | -------------------------------------------------------------------------------- /backend/core/signals/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from . import migrations 4 | from . import signals 5 | -------------------------------------------------------------------------------- /backend/core/types/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/core/types/__init__.py -------------------------------------------------------------------------------- /backend/core/types/htmx.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import AnonymousUser 2 | from django.core.handlers.wsgi import WSGIRequest 3 | from django.http import HttpRequest 4 | from django_htmx.middleware import HtmxDetails 5 | 6 | from backend.models import User, Organization 7 | 8 | 9 | class HtmxHttpRequest(HttpRequest): 10 | htmx: HtmxDetails 11 | user: User 12 | no_retarget: bool | None 13 | 14 | 15 | class UnauthorizedHttpRequest(HttpRequest): 16 | user: AnonymousUser 17 | htmx: HtmxDetails 18 | no_retarget: bool | None 19 | 20 | 21 | class HtmxAnyHttpRequest(HttpRequest): 22 | user: User | AnonymousUser 23 | htmx: HtmxDetails 24 | no_retarget: bool | None 25 | -------------------------------------------------------------------------------- /backend/core/types/requests.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from django.contrib.auth.models import AnonymousUser 4 | from django.http import HttpRequest 5 | from django_htmx.middleware import HtmxDetails 6 | 7 | from backend.models import User, Organization 8 | 9 | 10 | class WebRequest(HttpRequest): 11 | user: User 12 | team: Organization | None 13 | team_id: int | None 14 | actor: User | Organization 15 | 16 | users_subscription: Any | None 17 | 18 | htmx: HtmxDetails 19 | no_retarget: bool | None 20 | -------------------------------------------------------------------------------- /backend/core/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/core/utils/__init__.py -------------------------------------------------------------------------------- /backend/core/utils/calendar.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from django.utils import timezone 4 | 5 | 6 | def get_months_text() -> list[str]: 7 | return [ 8 | "January", 9 | "February", 10 | "March", 11 | "April", 12 | "May", 13 | "June", 14 | "July", 15 | "August", 16 | "September", 17 | "October", 18 | "November", 19 | "December", 20 | ] 21 | 22 | 23 | def timezone_now() -> datetime.datetime: 24 | return timezone.now() 25 | -------------------------------------------------------------------------------- /backend/core/utils/feature_flags.py: -------------------------------------------------------------------------------- 1 | from backend.models import FeatureFlags 2 | from django.core.cache import cache 3 | from django.core.cache.backends.redis import RedisCacheClient 4 | 5 | cache: RedisCacheClient = cache 6 | 7 | 8 | def get_feature_status(feature, should_use_cache=True): 9 | if should_use_cache: 10 | key = f"myfinances:feature_flag:{feature}" 11 | cached_value = cache.get(key) 12 | if cached_value: 13 | return cached_value 14 | 15 | value = FeatureFlags.objects.filter(name=feature).first() 16 | if value: 17 | if should_use_cache: 18 | cache.set(key, value.value, timeout=300) 19 | return value.value 20 | else: 21 | return False 22 | 23 | 24 | def set_cache(key, value, timeout=300): 25 | cache.set(key, value, timeout=timeout) 26 | 27 | 28 | def get_cache(key): 29 | return cache.get(key) 30 | -------------------------------------------------------------------------------- /backend/core/utils/http_utils.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponseRedirect 2 | 3 | 4 | def redirect_to_last_visited(request, fallback_url="dashboard"): 5 | """ 6 | Redirects user to the last visited URL stored in session. 7 | If no previous URL is found, redirects to the fallback URL. 8 | :param request: HttpRequest object 9 | :param fallback_url: URL to redirect to if no previous URL found 10 | :return: HttpResponseRedirect object 11 | """ 12 | try: 13 | last_visited_url = request.session.get("last_visited", fallback_url) 14 | return HttpResponseRedirect(last_visited_url) 15 | except KeyError: 16 | return HttpResponseRedirect(fallback_url) 17 | -------------------------------------------------------------------------------- /backend/core/utils/service_retry.py: -------------------------------------------------------------------------------- 1 | from typing import Callable, TypeVar 2 | from backend.core.utils.dataclasses import BaseServiceResponse 3 | 4 | T = TypeVar("T", bound=BaseServiceResponse) 5 | 6 | 7 | def retry_handler(function: Callable[..., T], *args, retry_max_attempts: int = 3, **kwargs) -> T: 8 | attempts: int = 0 9 | 10 | while attempts < retry_max_attempts: 11 | response: T = function(*args, **kwargs) 12 | 13 | if response.failed: 14 | attempts += 1 15 | if attempts == retry_max_attempts: 16 | return response 17 | continue 18 | return response 19 | return response 20 | -------------------------------------------------------------------------------- /backend/core/views/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/core/views/__init__.py -------------------------------------------------------------------------------- /backend/core/views/auth/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/core/views/auth/__init__.py -------------------------------------------------------------------------------- /backend/core/views/auth/helpers.py: -------------------------------------------------------------------------------- 1 | def generate_magic_link(user): 2 | 3 | return "" 4 | -------------------------------------------------------------------------------- /backend/core/views/auth/passwords/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/core/views/auth/passwords/__init__.py -------------------------------------------------------------------------------- /backend/core/views/auth/passwords/view.py: -------------------------------------------------------------------------------- 1 | from django.contrib import messages 2 | from django.contrib.auth.hashers import check_password 3 | from django.http import HttpRequest 4 | from django.shortcuts import render, redirect 5 | from django.utils import timezone 6 | 7 | from backend.core.models import PasswordSecret 8 | from backend.decorators import not_authenticated 9 | 10 | 11 | @not_authenticated 12 | def set_password(request: HttpRequest, secret): 13 | SECRET_RETURNED = PasswordSecret.objects.all() 14 | SECRET_RETURNED.filter(expires__lte=timezone.now()).all().delete() 15 | 16 | for SECRET in SECRET_RETURNED: 17 | if check_password(secret, SECRET.secret): 18 | return render(request, "pages/reset_password.html", {"secret": secret}) 19 | 20 | messages.error(request, "Invalid or expired password reset code") 21 | return redirect("dashboard") 22 | -------------------------------------------------------------------------------- /backend/core/views/emails/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/core/views/emails/__init__.py -------------------------------------------------------------------------------- /backend/core/views/emails/dashboard.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from django.http import HttpResponse 4 | from django.shortcuts import render 5 | 6 | from backend.decorators import feature_flag_check, web_require_scopes 7 | from backend.core.types.htmx import HtmxHttpRequest 8 | 9 | 10 | @feature_flag_check("areUserEmailsAllowed", status=True) 11 | @web_require_scopes("emails:read", False, False, "dashboard") 12 | def dashboard(request: HtmxHttpRequest) -> HttpResponse: 13 | return render(request, "pages/emails/dashboard.html", {}) 14 | -------------------------------------------------------------------------------- /backend/core/views/emails/urls.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from django.urls import path 4 | 5 | from . import dashboard 6 | 7 | urlpatterns = [ 8 | path( 9 | "", 10 | dashboard.dashboard, 11 | name="dashboard", 12 | ), 13 | ] 14 | 15 | app_name = "emails" 16 | -------------------------------------------------------------------------------- /backend/core/views/other/__init__.py: -------------------------------------------------------------------------------- 1 | from .index import index 2 | from .errors import universal, e_403 3 | -------------------------------------------------------------------------------- /backend/core/views/other/index.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpRequest 2 | from django.shortcuts import render 3 | from login_required import login_not_required 4 | 5 | 6 | def index(request: HttpRequest): 7 | return render(request, "pages/landing/index.html") 8 | 9 | 10 | @login_not_required 11 | def pricing(request: HttpRequest): 12 | return render(request, "pages/landing/pricing.html") 13 | 14 | 15 | def dashboard(request: HttpRequest): 16 | return render(request, "pages/dashboard.html") 17 | -------------------------------------------------------------------------------- /backend/core/views/quotas/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/core/views/quotas/__init__.py -------------------------------------------------------------------------------- /backend/core/views/settings/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/core/views/settings/__init__.py -------------------------------------------------------------------------------- /backend/core/views/settings/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from backend.core.views.settings.view import change_password, view_settings_page_endpoint 4 | 5 | urlpatterns = [ 6 | path("", view_settings_page_endpoint, name="dashboard"), 7 | path("/", view_settings_page_endpoint, name="dashboard with page"), 8 | path( 9 | "profile/change_password/", 10 | change_password, 11 | name="change_password", 12 | ), 13 | ] 14 | 15 | app_name = "settings" 16 | -------------------------------------------------------------------------------- /backend/core/views/teams/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/core/views/teams/__init__.py -------------------------------------------------------------------------------- /backend/core/views/teams/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from backend.core.views.settings.teams import teams_dashboard_handler 4 | 5 | urlpatterns = [ 6 | path( 7 | "", 8 | teams_dashboard_handler, 9 | name="dashboard", 10 | ), 11 | ] 12 | 13 | app_name = "teams" 14 | -------------------------------------------------------------------------------- /backend/core/webhooks/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/core/webhooks/__init__.py -------------------------------------------------------------------------------- /backend/core/webhooks/invoices/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/core/webhooks/invoices/__init__.py -------------------------------------------------------------------------------- /backend/core/webhooks/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from backend.core.webhooks.invoices.recurring import handle_recurring_invoice_webhook_endpoint 4 | 5 | urlpatterns = [ 6 | path("schedules/receive/recurring_invoices/", handle_recurring_invoice_webhook_endpoint, name="receive_recurring_invoices"), 7 | ] 8 | 9 | app_name = "webhooks" 10 | -------------------------------------------------------------------------------- /backend/events/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/events/__init__.py -------------------------------------------------------------------------------- /backend/finance/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/finance/__init__.py -------------------------------------------------------------------------------- /backend/finance/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/finance/api/__init__.py -------------------------------------------------------------------------------- /backend/finance/api/invoices/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/finance/api/invoices/__init__.py -------------------------------------------------------------------------------- /backend/finance/api/invoices/create/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/finance/api/invoices/create/__init__.py -------------------------------------------------------------------------------- /backend/finance/api/invoices/create/services/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/finance/api/invoices/create/services/__init__.py -------------------------------------------------------------------------------- /backend/finance/api/invoices/create/services/add_service.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | from django.views.decorators.http import require_http_methods 3 | 4 | from backend.core.service.invoices.common.create.services.add import add 5 | from backend.core.types.requests import WebRequest 6 | 7 | 8 | @require_http_methods(["POST"]) 9 | def add_service_endpoint(request: WebRequest): 10 | return render(request, "pages/invoices/create/services/_services_table_body.html", add(request)) 11 | -------------------------------------------------------------------------------- /backend/finance/api/invoices/recurring/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/finance/api/invoices/recurring/__init__.py -------------------------------------------------------------------------------- /backend/finance/api/invoices/reminders/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/finance/api/invoices/reminders/__init__.py -------------------------------------------------------------------------------- /backend/finance/api/invoices/reminders/urls.py: -------------------------------------------------------------------------------- 1 | # from django.urls import path 2 | # 3 | # from . import delete, fetch, create 4 | # 5 | # urlpatterns = [ 6 | # # path("reminders/create/", create.create_reminder_view, name="create"), 7 | # # path("reminders//fetch/", fetch.fetch_reminders, name="fetch"), 8 | # # path("reminder//cancel/", delete.cancel_reminder_view, name="cancel"), 9 | # ] 10 | # 11 | # app_name = "reminders" 12 | -------------------------------------------------------------------------------- /backend/finance/api/invoices/single/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/finance/api/invoices/single/__init__.py -------------------------------------------------------------------------------- /backend/finance/api/products/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/finance/api/products/__init__.py -------------------------------------------------------------------------------- /backend/finance/api/products/fetch.py: -------------------------------------------------------------------------------- 1 | from django.db.models import Q, QuerySet 2 | from django.shortcuts import render 3 | 4 | from backend.decorators import web_require_scopes 5 | from backend.finance.models import InvoiceProduct 6 | from backend.core.types.htmx import HtmxHttpRequest 7 | 8 | 9 | @web_require_scopes("invoices:read", True, True) 10 | def fetch_products(request: HtmxHttpRequest): 11 | results: QuerySet 12 | search_text = request.GET.get("search_existing_service") 13 | if search_text: 14 | results = ( 15 | InvoiceProduct.objects.filter(user=request.user) 16 | .filter(Q(name__icontains=search_text) | Q(description__icontains=search_text)) 17 | .order_by("name") 18 | ) 19 | else: 20 | results = InvoiceProduct.objects.filter(user=request.user).order_by("name") 21 | 22 | return render(request, "pages/products/fetched_items.html", {"products": results[:5]}) 23 | -------------------------------------------------------------------------------- /backend/finance/api/products/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from . import fetch, create 4 | 5 | urlpatterns = [ 6 | path("fetch/", fetch.fetch_products, name="fetch"), 7 | path("create/", create.create_product, name="create"), 8 | ] 9 | 10 | app_name = "products" 11 | -------------------------------------------------------------------------------- /backend/finance/api/receipts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/finance/api/receipts/__init__.py -------------------------------------------------------------------------------- /backend/finance/api/receipts/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from . import delete, new, fetch, download, edit 3 | 4 | urlpatterns = [ 5 | path( 6 | "delete//", 7 | delete.receipt_delete, 8 | name="delete", 9 | ), 10 | path( 11 | "new/", 12 | new.receipt_create, 13 | name="new", 14 | ), 15 | path( 16 | "edit//", 17 | edit.edit_receipt, 18 | name="edit", 19 | ), 20 | path( 21 | "fetch/", 22 | fetch.fetch_all_receipts, 23 | name="fetch", 24 | ), 25 | path( 26 | "download//", 27 | download.generate_download_link, 28 | name="generate_download_link", 29 | ), 30 | path("cdn//", download.download_receipt, name="download_receipt"), 31 | ] 32 | 33 | app_name = "receipts" 34 | -------------------------------------------------------------------------------- /backend/finance/api/reports/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/finance/api/reports/__init__.py -------------------------------------------------------------------------------- /backend/finance/api/reports/fetch.py: -------------------------------------------------------------------------------- 1 | from django.db.models import Q 2 | from django.shortcuts import render 3 | 4 | from backend.models import MonthlyReport 5 | from backend.core.types.requests import WebRequest 6 | 7 | 8 | def fetch_reports_endpoint(request: WebRequest): 9 | search = request.GET.get("search", "") 10 | 11 | reports = MonthlyReport.filter_by_owner(request.actor).all() 12 | 13 | if search: 14 | reports = reports.filter( 15 | Q(name__icontains=search) 16 | | Q(start_date__icontains=search) 17 | | Q(end_date__icontains=search) 18 | | Q(payments_in__icontains=search) 19 | | Q(payments_out__icontains=search) 20 | ) 21 | 22 | return render(request, "pages/reports/_list_rows.html", {"reports": reports}) 23 | -------------------------------------------------------------------------------- /backend/finance/api/reports/generate.py: -------------------------------------------------------------------------------- 1 | from django.contrib import messages 2 | from django.shortcuts import render 3 | 4 | from backend.decorators import web_require_scopes 5 | from backend.core.service.reports.generate import generate_report 6 | from backend.core.types.requests import WebRequest 7 | 8 | 9 | @web_require_scopes("invoices:write", True, True) 10 | def generate_report_endpoint(request: WebRequest): 11 | start_date: str = request.POST.get("start_date", "") 12 | end_date: str = request.POST.get("end_date", "") 13 | name: str = request.POST.get("name", "") 14 | 15 | generated_report = generate_report(request.actor, start_date, end_date, name) 16 | 17 | if generated_report.failed: 18 | messages.error(request, generated_report.error) 19 | return render(request, "base/toast.html") 20 | 21 | messages.success(request, f"Successfully generated report ({str(generated_report.response.uuid)[:4]})") 22 | 23 | resp = render(request, "base/toast.html") 24 | resp["HX-Trigger"] = "refresh_reports_table" 25 | return resp 26 | -------------------------------------------------------------------------------- /backend/finance/api/reports/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from . import generate, fetch 3 | 4 | urlpatterns = [ 5 | path( 6 | "generate/", 7 | generate.generate_report_endpoint, 8 | name="generate", 9 | ), 10 | path("fetch/", fetch.fetch_reports_endpoint, name="fetch"), 11 | ] 12 | 13 | app_name = "reports" 14 | -------------------------------------------------------------------------------- /backend/finance/api/urls.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from django.urls import include 4 | from django.urls import path 5 | 6 | urlpatterns = [ 7 | path("receipts/", include("backend.finance.api.receipts.urls")), 8 | path("invoices/", include("backend.finance.api.invoices.urls")), 9 | path("products/", include("backend.finance.api.products.urls")), 10 | path("reports/", include("backend.finance.api.reports.urls")), 11 | ] 12 | 13 | app_name = "finance" 14 | -------------------------------------------------------------------------------- /backend/finance/signals/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/finance/signals/__init__.py -------------------------------------------------------------------------------- /backend/finance/views/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/finance/views/__init__.py -------------------------------------------------------------------------------- /backend/finance/views/invoices/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/finance/views/invoices/__init__.py -------------------------------------------------------------------------------- /backend/finance/views/invoices/handler.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Any 2 | 3 | from django.http import HttpResponse 4 | from django.shortcuts import render 5 | 6 | from backend.core.types.requests import WebRequest 7 | 8 | 9 | def invoices_core_handler(request: WebRequest, template_name: str, start_context: Dict[str, Any] | None = None, **kwargs) -> HttpResponse: 10 | context: dict[str, Any] = start_context or {} 11 | if not request.GET.get("invoice_structure_main", None) or not request.htmx: 12 | context["page_template"] = template_name 13 | if template_name == "pages/invoices/dashboard/manage.html": 14 | context["notoggler"] = True 15 | return render(request, "pages/invoices/dashboard/core/main.html", context, **kwargs) 16 | 17 | response = render(request, template_name, context, **kwargs) 18 | response.no_retarget = True # type: ignore[attr-defined] 19 | response["HX-Trigger-After-Settle"] = "update_breadcrumbs" 20 | return response 21 | -------------------------------------------------------------------------------- /backend/finance/views/invoices/recurring/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/finance/views/invoices/recurring/__init__.py -------------------------------------------------------------------------------- /backend/finance/views/invoices/recurring/dashboard.py: -------------------------------------------------------------------------------- 1 | from django.views.decorators.http import require_http_methods 2 | 3 | from backend.decorators import web_require_scopes, has_entitlements 4 | from backend.core.types.requests import WebRequest 5 | from backend.finance.views.invoices.handler import invoices_core_handler 6 | 7 | 8 | @require_http_methods(["GET"]) 9 | @has_entitlements("invoice-schedules") 10 | @web_require_scopes("invoices:read", False, False, "dashboard") 11 | def invoices_recurring_dashboard_endpoint(request: WebRequest): 12 | return invoices_core_handler(request, "pages/invoices/recurring/dashboard/dashboard.html") 13 | -------------------------------------------------------------------------------- /backend/finance/views/invoices/single/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/finance/views/invoices/single/__init__.py -------------------------------------------------------------------------------- /backend/finance/views/receipts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/finance/views/receipts/__init__.py -------------------------------------------------------------------------------- /backend/finance/views/receipts/dashboard.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.decorators import login_required 2 | from django.shortcuts import render 3 | 4 | from backend.decorators import web_require_scopes 5 | from backend.core.types.htmx import HtmxHttpRequest 6 | 7 | 8 | @login_required 9 | @web_require_scopes("receipts:read", False, False, "dashboard") 10 | def receipts_dashboard(request: HtmxHttpRequest): 11 | return render(request, "pages/receipts/dashboard.html") 12 | -------------------------------------------------------------------------------- /backend/finance/views/receipts/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from .dashboard import receipts_dashboard 4 | 5 | urlpatterns = [path("", receipts_dashboard, name="dashboard")] 6 | 7 | app_name = "receipts" 8 | -------------------------------------------------------------------------------- /backend/finance/views/reports/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/finance/views/reports/__init__.py -------------------------------------------------------------------------------- /backend/finance/views/reports/dashboard.py: -------------------------------------------------------------------------------- 1 | from backend.core.types.requests import WebRequest 2 | from django.shortcuts import render 3 | 4 | 5 | def view_reports_endpoint(request: WebRequest): 6 | return render(request, "pages/reports/dashboard.html") 7 | -------------------------------------------------------------------------------- /backend/finance/views/reports/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from .dashboard import view_reports_endpoint 4 | from .view import view_report_endpoint 5 | 6 | urlpatterns = [ 7 | path("", view_reports_endpoint, name="dashboard"), 8 | path("view/", view_report_endpoint, name="view"), 9 | ] 10 | 11 | app_name = "reports" 12 | -------------------------------------------------------------------------------- /backend/finance/views/reports/view.py: -------------------------------------------------------------------------------- 1 | from django.contrib import messages 2 | 3 | from backend.core.service.reports.get import get_report 4 | from backend.core.types.requests import WebRequest 5 | from django.shortcuts import render, redirect 6 | 7 | 8 | def view_report_endpoint(request: WebRequest, uuid): 9 | report = get_report(request.actor, uuid) 10 | 11 | if report.failed: 12 | messages.error(request, report.error) 13 | return redirect("reports:dashboard") 14 | 15 | return render(request, "pages/reports/monthly_report_base.html", {"report": report.response}) 16 | -------------------------------------------------------------------------------- /backend/finance/views/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from django.urls.conf import include 3 | 4 | from backend.finance.views.invoices.single.dashboard import invoices_single_dashboard_endpoint 5 | 6 | urlpatterns = [ 7 | path("invoices/", include("backend.finance.views.invoices.urls")), 8 | path("reports/", include("backend.finance.views.reports.urls")), 9 | path("receipts/", include("backend.finance.views.receipts.urls")), 10 | path("", invoices_single_dashboard_endpoint), 11 | ] 12 | 13 | app_name = "finance" 14 | -------------------------------------------------------------------------------- /backend/managers.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class InvoiceRecurringProfile_WithItemsManager(models.Manager): 5 | def get_queryset(self): 6 | return super().get_queryset().select_related("client_to").prefetch_related("generated_invoices__items", "generated_invoices") 7 | -------------------------------------------------------------------------------- /backend/migrations/0002_alter_receipt_date_uploaded.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.5 on 2023-11-25 22:41 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("backend", "0001_initial"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AlterField( 13 | model_name="receipt", 14 | name="date_uploaded", 15 | field=models.DateTimeField(auto_now_add=True), 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /backend/migrations/0003_client_company_client_is_representative.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.7 on 2023-12-12 14:37 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("backend", "0002_alter_receipt_date_uploaded"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AddField( 13 | model_name="client", 14 | name="company", 15 | field=models.CharField(blank=True, max_length=100, null=True), 16 | ), 17 | migrations.AddField( 18 | model_name="client", 19 | name="is_representative", 20 | field=models.BooleanField(default=False), 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /backend/migrations/0004_invoice_client_is_representative.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.7 on 2023-12-15 08:40 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("backend", "0003_client_company_client_is_representative"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AddField( 13 | model_name="invoice", 14 | name="client_is_representative", 15 | field=models.BooleanField(default=False), 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /backend/migrations/0006_receipt_merchant_store_receipt_purchase_category.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.7 on 2024-01-08 20:32 2 | 3 | from django.db import migrations, models 4 | import django.utils.timezone 5 | 6 | 7 | class Migration(migrations.Migration): 8 | dependencies = [ 9 | ("backend", "0005_invoiceproduct"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="receipt", 15 | name="merchant_store", 16 | field=models.CharField(default=None, max_length=200), 17 | preserve_default=False, 18 | ), 19 | migrations.AddField( 20 | model_name="receipt", 21 | name="purchase_category", 22 | field=models.CharField(default=django.utils.timezone.now, max_length=200), 23 | preserve_default=False, 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /backend/migrations/0007_alter_receipt_merchant_store_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.7 on 2024-01-09 08:21 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("backend", "0006_receipt_merchant_store_receipt_purchase_category"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AlterField( 13 | model_name="receipt", 14 | name="merchant_store", 15 | field=models.CharField(blank=True, max_length=255, null=True), 16 | ), 17 | migrations.AlterField( 18 | model_name="receipt", 19 | name="purchase_category", 20 | field=models.CharField(blank=True, max_length=200, null=True), 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /backend/migrations/0009_alter_invoice_sort_code.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.7 on 2024-01-31 08:15 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("backend", "0008_receiptdownloadtoken"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AlterField( 13 | model_name="invoice", 14 | name="sort_code", 15 | field=models.CharField(blank=True, max_length=8, null=True), 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /backend/migrations/0010_user_logged_in_as_team.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.1 on 2024-02-04 19:20 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ("backend", "0009_alter_invoice_sort_code"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name="user", 16 | name="logged_in_as_team", 17 | field=models.ForeignKey( 18 | null=True, 19 | on_delete=django.db.models.deletion.SET_NULL, 20 | to="backend.team", 21 | ), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /backend/migrations/0011_alter_team_leader.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.1 on 2024-02-04 20:36 2 | 3 | import django.db.models.deletion 4 | from django.conf import settings 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ("backend", "0010_user_logged_in_as_team"), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name="team", 17 | name="leader", 18 | field=models.ForeignKey( 19 | on_delete=django.db.models.deletion.CASCADE, 20 | related_name="teams_leader_of", 21 | to=settings.AUTH_USER_MODEL, 22 | ), 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /backend/migrations/0014_notification_extra_type_notification_extra_value.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.1 on 2024-02-06 08:34 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("backend", "0013_auditlog_organization_client_organization_and_more"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="notification", 15 | name="extra_type", 16 | field=models.CharField(blank=True, max_length=100, null=True), 17 | ), 18 | migrations.AddField( 19 | model_name="notification", 20 | name="extra_value", 21 | field=models.CharField(blank=True, max_length=100, null=True), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /backend/migrations/0015_alter_notification_user_alter_team_name.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.1 on 2024-02-07 08:10 2 | 3 | import django.db.models.deletion 4 | from django.conf import settings 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ("backend", "0014_notification_extra_type_notification_extra_value"), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name="notification", 17 | name="user", 18 | field=models.ForeignKey( 19 | on_delete=django.db.models.deletion.CASCADE, 20 | related_name="user_notifications", 21 | to=settings.AUTH_USER_MODEL, 22 | ), 23 | ), 24 | migrations.AlterField( 25 | model_name="team", 26 | name="name", 27 | field=models.CharField(max_length=100, unique=True), 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /backend/migrations/0017_featureflags.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.2 on 2024-02-18 20:17 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("backend", "0016_alter_invoice_logo_alter_receipt_image_and_more"), 10 | ] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name="FeatureFlags", 15 | fields=[ 16 | ( 17 | "id", 18 | models.BigAutoField( 19 | auto_created=True, 20 | primary_key=True, 21 | serialize=False, 22 | verbose_name="ID", 23 | ), 24 | ), 25 | ("name", models.CharField(max_length=100)), 26 | ("value", models.BooleanField(default=False)), 27 | ("updated_at", models.DateTimeField(auto_now=True)), 28 | ], 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /backend/migrations/0018_user_role.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.1 on 2024-02-20 10:18 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("backend", "0017_featureflags"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="user", 15 | name="role", 16 | field=models.CharField( 17 | choices=[ 18 | ("DEV", "Developer"), 19 | ("STAFF", "Staff"), 20 | ("USER", "User"), 21 | ("TESTER", "Tester"), 22 | ], 23 | default="USER", 24 | max_length=10, 25 | ), 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /backend/migrations/0021_alter_verificationcodes_expiry_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.2 on 2024-02-23 19:00 2 | 3 | import datetime 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ("backend", "0020_alter_verificationcodes_options_and_more"), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name="verificationcodes", 17 | name="expiry", 18 | field=models.DateTimeField(default=datetime.datetime(2024, 2, 23, 22, 0, 25, 744643, tzinfo=datetime.timezone.utc)), 19 | ), 20 | migrations.AlterField( 21 | model_name="verificationcodes", 22 | name="token", 23 | field=models.TextField(default="XBNKTM", editable=False), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /backend/migrations/0024_invoiceurl_never_expire_invoiceurl_system_created_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.3 on 2024-03-09 13:50 2 | 3 | import django.db.models.deletion 4 | from django.conf import settings 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ("backend", "0023_apikey_invoiceonetimeschedule"), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name="invoiceurl", 17 | name="never_expire", 18 | field=models.BooleanField(default=False), 19 | ), 20 | migrations.AddField( 21 | model_name="invoiceurl", 22 | name="system_created", 23 | field=models.BooleanField(default=False), 24 | ), 25 | migrations.AlterField( 26 | model_name="invoiceurl", 27 | name="created_by", 28 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /backend/migrations/0025_alter_invoiceonetimeschedule_stored_schedule_arn.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.3 on 2024-03-16 14:17 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("backend", "0024_invoiceurl_never_expire_invoiceurl_system_created_and_more"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="invoiceonetimeschedule", 15 | name="stored_schedule_arn", 16 | field=models.CharField(blank=True, max_length=500, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /backend/migrations/0026_invoice_discount_amount_invoice_discount_percentage.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.3 on 2024-03-29 20:00 2 | 3 | import django.core.validators 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ("backend", "0025_alter_invoiceonetimeschedule_stored_schedule_arn"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name="invoice", 16 | name="discount_amount", 17 | field=models.DecimalField(decimal_places=2, default=0, max_digits=15), 18 | ), 19 | migrations.AddField( 20 | model_name="invoice", 21 | name="discount_percentage", 22 | field=models.DecimalField( 23 | decimal_places=2, default=0, max_digits=5, validators=[django.core.validators.MaxValueValidator(100)] 24 | ), 25 | ), 26 | ] 27 | -------------------------------------------------------------------------------- /backend/migrations/0027_invoice_currency.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.3 on 2024-03-31 23:19 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("backend", "0026_invoice_discount_amount_invoice_discount_percentage"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="invoice", 15 | name="currency", 16 | field=models.CharField( 17 | choices=[ 18 | ("GBP", "British Pound Sterling"), 19 | ("EUR", "Euro"), 20 | ("USD", "United States Dollar"), 21 | ("JPY", "Japanese Yen"), 22 | ("INR", "Indian Rupee"), 23 | ("AUD", "Australian Dollar"), 24 | ("CAD", "Canadian Dollar"), 25 | ], 26 | default="GBP", 27 | max_length=3, 28 | ), 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /backend/migrations/0030_alter_invoice_items.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.3 on 2024-04-04 15:24 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("backend", "0029_alter_invoice_organization_alter_invoice_user_and_more"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="invoice", 15 | name="items", 16 | field=models.ManyToManyField(blank=True, to="backend.invoiceitem"), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /backend/migrations/0031_featureflags_description_alter_featureflags_name.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.4 on 2024-04-05 19:52 2 | from __future__ import annotations 3 | 4 | from django.db import migrations 5 | from django.db import models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ("backend", "0030_alter_invoice_items"), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name="featureflags", 17 | name="description", 18 | field=models.TextField(blank=True, editable=False, max_length=500, null=True), 19 | ), 20 | migrations.AlterField( 21 | model_name="featureflags", 22 | name="name", 23 | field=models.CharField(editable=False, max_length=100, unique=True), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /backend/migrations/0033_alter_auditlog_organization.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.4 on 2024-04-10 16:11 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ("backend", "0032_client_email_verified_alter_client_organization_and_more"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name="auditlog", 16 | name="organization", 17 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to="backend.team"), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /backend/migrations/0035_client_contact_method.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.4 on 2024-05-30 01:58 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("backend", "0034_invoice_client_email_quotaincreaserequest_reason_and_more"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="client", 15 | name="contact_method", 16 | field=models.CharField(blank=True, max_length=100, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /backend/migrations/0036_apiauthtoken.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.4 on 2024-06-10 16:59 2 | 3 | import django.db.models.deletion 4 | from django.conf import settings 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ("backend", "0035_client_contact_method"), 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name="APIAuthToken", 17 | fields=[ 18 | ("key", models.CharField(max_length=40, primary_key=True, serialize=False, verbose_name="Key")), 19 | ("created", models.DateTimeField(auto_now_add=True, verbose_name="Created")), 20 | ("user", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 21 | ], 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /backend/migrations/0037_merge_20240619_2223.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.4 on 2024-06-19 21:23 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("backend", "0036_alter_client_address_clientdefaults"), 10 | ("backend", "0036_apiauthtoken"), 11 | ] 12 | 13 | operations = [] 14 | -------------------------------------------------------------------------------- /backend/migrations/0038_alter_apiauthtoken_options.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.6 on 2024-06-20 16:42 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("backend", "0037_merge_20240619_2223"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name="apiauthtoken", 15 | options={"verbose_name": "API Key", "verbose_name_plural": "API Keys"}, 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /backend/migrations/0041_alter_apiauthtoken_user.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.6 on 2024-06-28 23:57 2 | 3 | import django.db.models.deletion 4 | from django.conf import settings 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ("backend", "0040_apiauthtoken_scopes_apiauthtoken_team_and_more"), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name="apiauthtoken", 17 | name="user", 18 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /backend/migrations/0045_usersettings_disabled_features.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.6 on 2024-07-19 22:44 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("backend", "0044_defaultvalues_delete_clientdefaults_and_more"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="usersettings", 15 | name="disabled_features", 16 | field=models.JSONField(default=list), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /backend/migrations/0047_defaultvalues_default_invoice_logo.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1 on 2024-08-23 11:13 2 | 3 | import settings.settings 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ("backend", "0046_rename_status_invoicereminder_boto_schedule_status_and_more"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name="defaultvalues", 16 | name="default_invoice_logo", 17 | field=models.ImageField( 18 | blank=True, null=True, storage=settings.settings.CustomPublicMediaStorage(), upload_to="invoice_logos/" 19 | ), 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /backend/migrations/0048_alter_defaultvalues_default_invoice_logo.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1 on 2024-08-23 11:54 2 | 3 | import backend.models 4 | from django.db import migrations, models 5 | 6 | from backend.core.models import _private_storage 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ("backend", "0047_defaultvalues_default_invoice_logo"), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterField( 17 | model_name="defaultvalues", 18 | name="default_invoice_logo", 19 | field=models.ImageField(blank=True, null=True, storage=_private_storage, upload_to="invoice_logos/"), 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /backend/migrations/0052_filestoragefile_file_uri_path.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1 on 2024-08-27 08:49 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("backend", "0051_planfeaturegroup_subscriptionplan_planfeature_and_more"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="filestoragefile", 15 | name="file_uri_path", 16 | field=models.CharField(default="/null/", max_length=500), 17 | preserve_default=False, 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /backend/migrations/0056_user_stripe_customer_id.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1 on 2024-08-29 13:44 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("backend", "0055_remove_planfeature_group_and_more"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="user", 15 | name="stripe_customer_id", 16 | field=models.CharField(blank=True, max_length=255, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /backend/migrations/0057_user_entitlements.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1 on 2024-09-01 17:55 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("backend", "0056_user_stripe_customer_id"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="user", 15 | name="entitlements", 16 | field=models.JSONField(blank=True, default=list, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /backend/migrations/0058_organization_entitlements_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1 on 2024-09-02 18:02 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("backend", "0057_user_entitlements"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="organization", 15 | name="entitlements", 16 | field=models.JSONField(blank=True, default=list, null=True), 17 | ), 18 | migrations.AddField( 19 | model_name="organization", 20 | name="stripe_customer_id", 21 | field=models.CharField(blank=True, max_length=255, null=True), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /backend/migrations/0060_user_require_change_password.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1 on 2024-09-14 21:55 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("backend", "0059_alter_invoicerecurringprofile_managers_and_more"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="user", 15 | name="require_change_password", 16 | field=models.BooleanField(default=False), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /backend/migrations/0062_defaultvalues_invoice_account_holder_name_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1 on 2024-09-16 20:49 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("backend", "0061_defaultvalues_invoice_from_address_and_more"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="defaultvalues", 15 | name="invoice_account_holder_name", 16 | field=models.CharField(blank=True, max_length=100, null=True), 17 | ), 18 | migrations.AddField( 19 | model_name="defaultvalues", 20 | name="invoice_account_number", 21 | field=models.CharField(blank=True, max_length=100, null=True), 22 | ), 23 | migrations.AddField( 24 | model_name="defaultvalues", 25 | name="invoice_sort_code", 26 | field=models.CharField(blank=True, max_length=100, null=True), 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /backend/migrations/0064_remove_invoice_payment_status_invoice_status.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1 on 2024-09-28 19:36 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("backend", "0063_defaultvalues_email_template_recurring_invoices_invoice_cancelled_and_more"), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name="invoice", 15 | name="payment_status", 16 | ), 17 | migrations.AddField( 18 | model_name="invoice", 19 | name="status", 20 | field=models.CharField( 21 | choices=[("draft", "Draft"), ("pending", "Pending"), ("paid", "Paid")], default="pending", max_length=10 22 | ), 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /backend/migrations/0066_delete_apikey_remove_verificationcodes_expiry_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.1 on 2024-10-06 07:52 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("backend", "0065_remove_invoiceurl_never_expire_passwordsecret_active_and_more"), 10 | ] 11 | 12 | operations = [ 13 | migrations.DeleteModel( 14 | name="APIKey", 15 | ), 16 | migrations.RemoveField( 17 | model_name="verificationcodes", 18 | name="expiry", 19 | ), 20 | migrations.AddField( 21 | model_name="verificationcodes", 22 | name="active", 23 | field=models.BooleanField(default=True), 24 | ), 25 | migrations.AddField( 26 | model_name="verificationcodes", 27 | name="expires", 28 | field=models.DateTimeField(blank=True, help_text="When the item will expire", null=True, verbose_name="Expires"), 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /backend/migrations/0067_remove_apiauthtoken_expired_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.1 on 2024-10-19 19:03 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("backend", "0066_delete_apikey_remove_verificationcodes_expiry_and_more"), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name="apiauthtoken", 15 | name="expired", 16 | ), 17 | migrations.AlterField( 18 | model_name="apiauthtoken", 19 | name="active", 20 | field=models.BooleanField(default=True), 21 | ), 22 | migrations.AlterField( 23 | model_name="apiauthtoken", 24 | name="expires", 25 | field=models.DateTimeField(blank=True, help_text="When the item will expire", null=True, verbose_name="Expires"), 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /backend/migrations/0069_alter_auditlog_action.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.1 on 2024-11-15 09:10 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("backend", "0068_invoice_created_at_invoice_status_updated_at_and_more"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="auditlog", 15 | name="action", 16 | field=models.CharField(max_length=300), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /backend/migrations/0070_remove_invoice_invoice_id_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.1 on 2024-11-17 15:06 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("backend", "0069_alter_auditlog_action"), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name="invoice", 15 | name="invoice_id", 16 | ), 17 | migrations.RemoveField( 18 | model_name="invoice", 19 | name="invoice_number", 20 | ), 21 | migrations.RemoveField( 22 | model_name="invoicerecurringprofile", 23 | name="invoice_number", 24 | ), 25 | migrations.RemoveField( 26 | model_name="invoicerecurringprofile", 27 | name="reference", 28 | ), 29 | migrations.AlterField( 30 | model_name="invoice", 31 | name="reference", 32 | field=models.CharField(blank=True, max_length=16, null=True), 33 | ), 34 | ] 35 | -------------------------------------------------------------------------------- /backend/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/migrations/__init__.py -------------------------------------------------------------------------------- /backend/models.py: -------------------------------------------------------------------------------- 1 | from backend.core.models import ( 2 | PasswordSecret, 3 | AuditLog, 4 | LoginLog, 5 | Error, 6 | TracebackError, 7 | UserSettings, 8 | Notification, 9 | Organization, 10 | TeamInvitation, 11 | TeamMemberPermission, 12 | User, 13 | FeatureFlags, 14 | VerificationCodes, 15 | QuotaLimit, 16 | QuotaOverrides, 17 | QuotaUsage, 18 | QuotaIncreaseRequest, 19 | EmailSendStatus, 20 | FileStorageFile, 21 | MultiFileUpload, 22 | ) 23 | 24 | from backend.finance.models import ( 25 | Invoice, 26 | InvoiceURL, 27 | InvoiceItem, 28 | InvoiceReminder, 29 | InvoiceRecurringProfile, 30 | InvoiceProduct, 31 | Receipt, 32 | ReceiptDownloadToken, 33 | MonthlyReport, 34 | MonthlyReportRow, 35 | ) 36 | 37 | from backend.clients.models import Client, DefaultValues 38 | -------------------------------------------------------------------------------- /backend/onboarding/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/onboarding/__init__.py -------------------------------------------------------------------------------- /backend/onboarding/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/onboarding/api/__init__.py -------------------------------------------------------------------------------- /backend/onboarding/views/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/onboarding/views/__init__.py -------------------------------------------------------------------------------- /backend/storage/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/storage/__init__.py -------------------------------------------------------------------------------- /backend/storage/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/storage/api/__init__.py -------------------------------------------------------------------------------- /backend/storage/api/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from django.views.generic import RedirectView 3 | 4 | from .delete import recursive_file_delete_endpoint 5 | from .fetch import fetch_table_endpoint 6 | 7 | urlpatterns = [ 8 | # path("delete/", recursive_file_delete_endpoint, name="delete"), 9 | # path("fetch/", fetch_table_endpoint, name="fetch") 10 | path("delete/", RedirectView.as_view(url="/dashboard"), name="delete"), 11 | path("fetch/", RedirectView.as_view(url="/dashboard"), name="fetch"), 12 | ] 13 | 14 | app_name = "file_storage" 15 | -------------------------------------------------------------------------------- /backend/storage/file_storage.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from django.db.models.signals import pre_delete, post_save 4 | from django.dispatch import receiver 5 | 6 | from backend.core.models import _private_storage 7 | from backend.models import FileStorageFile 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | @receiver(pre_delete, sender=FileStorageFile) 13 | def on_delete_remove_media_file(sender, instance: FileStorageFile, **kwargs): 14 | # Check if the file exists in the storage 15 | if instance.file and _private_storage().exists(instance.file.name): 16 | instance.file.delete(save=False) 17 | instance.file = None 18 | instance.save() 19 | -------------------------------------------------------------------------------- /backend/storage/views/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/storage/views/__init__.py -------------------------------------------------------------------------------- /backend/storage/views/dashboard.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | from django.utils.html import escape 3 | 4 | from backend.core.models import FileStorageFile 5 | from backend.core.service.file_storage.utils import format_file_size 6 | from backend.core.types.requests import WebRequest 7 | 8 | 9 | def file_storage_dashboard_endpoint(request: WebRequest): 10 | directory = request.GET.get("dir", "") 11 | 12 | files = FileStorageFile.objects.filter(file__startswith=directory).order_by("file") 13 | 14 | files_with_names = [ 15 | { 16 | "name": file.file.name.split("/")[-1], 17 | "url": file.file.url, 18 | "file_uri_path": file.file_uri_path, 19 | "size": format_file_size(file.file.size), 20 | "last_modified": file.updated_at, 21 | } 22 | for file in files 23 | ] 24 | 25 | return render(request, "pages/file_storage/dashboard.html", {"files": files_with_names, "directory": escape(directory)}) 26 | -------------------------------------------------------------------------------- /backend/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/backend/templatetags/__init__.py -------------------------------------------------------------------------------- /backend/templatetags/cal_filters.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | 3 | register = template.Library() 4 | 5 | 6 | @register.filter 7 | def ordinal(value): 8 | try: 9 | value = int(value) 10 | if 10 <= value % 100 <= 20: 11 | suffix = "th" 12 | else: 13 | suffix = {1: "st", 2: "nd", 3: "rd"}.get(value % 10, "th") 14 | return f"{value}{suffix}" 15 | except (ValueError, TypeError): 16 | return value 17 | -------------------------------------------------------------------------------- /backend/templatetags/dictfilters.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from django import template 4 | 5 | register = template.Library() 6 | 7 | 8 | @register.simple_tag 9 | def dict_get(dictionary: dict, key: Any): 10 | return dictionary.get(key) 11 | 12 | 13 | register.filter("dict_get", dict_get) 14 | -------------------------------------------------------------------------------- /billing/__init__.py: -------------------------------------------------------------------------------- 1 | from . import apps, billing_settings 2 | -------------------------------------------------------------------------------- /billing/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /billing/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class BillingConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "billing" 7 | 8 | def ready(self): 9 | from . import signals 10 | 11 | pass 12 | -------------------------------------------------------------------------------- /billing/data/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/billing/data/__init__.py -------------------------------------------------------------------------------- /billing/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/billing/management/__init__.py -------------------------------------------------------------------------------- /billing/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/billing/management/commands/__init__.py -------------------------------------------------------------------------------- /billing/migrations/0002_subscriptionplan_stripe_price_id.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1 on 2024-08-29 19:20 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("billing", "0001_initial"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="subscriptionplan", 15 | name="stripe_price_id", 16 | field=models.CharField(blank=True, max_length=100, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /billing/migrations/0004_auto_20240830_1655.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1 on 2024-08-30 15:55 2 | import uuid 3 | 4 | from django.db import migrations 5 | 6 | 7 | def gen_uuid(apps, schema_editor): 8 | UserSubscription = apps.get_model("billing", "UserSubscription") 9 | for row in UserSubscription.objects.all(): 10 | row.uuid = uuid.uuid4() 11 | row.save(update_fields=["uuid"]) 12 | 13 | 14 | class Migration(migrations.Migration): 15 | 16 | dependencies = [ 17 | ("billing", "0003_stripewebhookevent_usersubscription_uuid_and_more"), 18 | ] 19 | 20 | operations = [ 21 | migrations.RunPython(gen_uuid, reverse_code=migrations.RunPython.noop), 22 | ] 23 | -------------------------------------------------------------------------------- /billing/migrations/0005_auto_20240830_1655.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1 on 2024-08-30 15:55 2 | import uuid 3 | 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ("billing", "0004_auto_20240830_1655"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name="usersubscription", 16 | name="uuid", 17 | field=models.UUIDField(default=uuid.uuid4, unique=True), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /billing/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/billing/migrations/__init__.py -------------------------------------------------------------------------------- /billing/service/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/billing/service/__init__.py -------------------------------------------------------------------------------- /billing/service/get_user.py: -------------------------------------------------------------------------------- 1 | from backend.models import User, Organization 2 | 3 | 4 | def get_actor_from_stripe_customer(stripe_customer_id: str) -> User | Organization | None: 5 | return ( 6 | User.objects.filter(stripe_customer_id=stripe_customer_id).first() 7 | or Organization.objects.filter(stripe_customer_id=stripe_customer_id).first() 8 | ) 9 | -------------------------------------------------------------------------------- /billing/service/price.py: -------------------------------------------------------------------------------- 1 | import stripe 2 | 3 | 4 | def get_price_id_from_lookup_key(lookup_key: str) -> str: 5 | prices = stripe.Price.list(lookup_keys=[lookup_key]) 6 | if prices.data: 7 | return prices.data[0].id # Assuming the lookup key returns one price 8 | raise ValueError(f"Price with lookup key {lookup_key} not found.") 9 | -------------------------------------------------------------------------------- /billing/service/stripe_customer.py: -------------------------------------------------------------------------------- 1 | from backend.models import User, Organization 2 | import stripe 3 | 4 | 5 | def get_or_create_customer_id(actor: User | Organization) -> str: 6 | if actor.stripe_customer_id: 7 | return actor.stripe_customer_id 8 | 9 | return create_stripe_customer_id(actor) 10 | 11 | 12 | def create_stripe_customer_id(actor: User | Organization) -> str: 13 | if isinstance(actor, User): 14 | customer = stripe.Customer.create( 15 | email=actor.email, 16 | name=actor.get_full_name(), 17 | ) 18 | actor.stripe_customer_id = customer.id 19 | actor.save() 20 | 21 | return customer.id 22 | 23 | else: 24 | customer = stripe.Customer.create( 25 | email=actor.leader.email, 26 | name=actor.name, 27 | ) 28 | 29 | actor.stripe_customer_id = customer.id 30 | actor.save() 31 | 32 | return customer.id 33 | -------------------------------------------------------------------------------- /billing/service/test.py: -------------------------------------------------------------------------------- 1 | # import os 2 | # 3 | # import stripe 4 | # from django.urls import reverse 5 | # 6 | # from backend.models import User 7 | # 8 | # user: User = User.objects.first() 9 | # 10 | # # stripe.billing.MeterEvent.create( 11 | # # event_name="invoices_created", 12 | # # payload={"invoices": "250", "stripe_customer_id": user.stripe_customer_id}, 13 | # # # identifier="id" 14 | # # ) 15 | # # 16 | # # stripe.billing.Meter.list_event_summaries( 17 | # # "" 18 | # # ) 19 | # 20 | # # a = stripe.Customer.create( 21 | # # name=user.get_full_name(), 22 | # # email=user.email 23 | # # ) 24 | # # 25 | # # user.stripe_customer_id = a.id 26 | # # user.save() 27 | # 28 | # # print(a) 29 | # 30 | # # stripe.checkout.Session.create( 31 | # # success_url=os.environ.get("SITE_URL", default="http://127.0.0.1:8000") + reverse("api:public:webhooks:receive_global"), 32 | # # line_items=[ 33 | # # { 34 | # # "price": "price_", 35 | # # "quantity": 1 36 | # # } 37 | # # ] 38 | # # ) 39 | -------------------------------------------------------------------------------- /billing/signals/__init__.py: -------------------------------------------------------------------------------- 1 | from . import migrations, usage, stripe 2 | -------------------------------------------------------------------------------- /billing/signals/quotas.py: -------------------------------------------------------------------------------- 1 | # from django.db.models.signals import post_save 2 | # from django.dispatch import receiver 3 | # 4 | # from backend.finance.models import Invoice, Usage 5 | # 6 | # 7 | # @receiver(post_save, sender=Invoice) 8 | # def created_invoice(sender, instance: Invoice, **kwargs): 9 | # Usage.objects.create( 10 | # owner=instance.owner, 11 | # feature="invoices-created", 12 | # quantity=1, 13 | # unit="invocations", 14 | # instance_id=instance.id, 15 | # ) 16 | -------------------------------------------------------------------------------- /billing/signals/stripe/__init__.py: -------------------------------------------------------------------------------- 1 | from . import webhook_handler 2 | -------------------------------------------------------------------------------- /billing/signals/stripe/webhook_handler.py: -------------------------------------------------------------------------------- 1 | from django.db.models.signals import post_save 2 | from django.dispatch import receiver 3 | from billing.models import StripeWebhookEvent 4 | from billing.service.checkout_completed import checkout_completed 5 | from billing.service.subscription_ended import subscription_ended 6 | from billing.service.entitlements import entitlements_updated_via_stripe_webhook 7 | 8 | 9 | @receiver(post_save, sender=StripeWebhookEvent) 10 | def stripe_webhook_event_created(sender, instance: StripeWebhookEvent, **kwargs): 11 | match instance.event_type: 12 | case "checkout.session.completed": 13 | checkout_completed(instance) 14 | case "customer.subscription.deleted": 15 | subscription_ended(instance) 16 | case "entitlements.active_entitlement_summary.updated": 17 | entitlements_updated_via_stripe_webhook(instance) 18 | case _: 19 | print(f"Unhandled event type: {instance.event_type}") 20 | -------------------------------------------------------------------------------- /billing/templates/pages/billing/dashboard/dashboard.html: -------------------------------------------------------------------------------- 1 | {% extends base|default:"base/base.html" %} 2 | {% load listfilters %} 3 | {% block content %} 4 | {% load get_first_n_items from listfilters %} 5 |
6 |
7 |
8 |

Billing

9 | 11 |
12 |
13 | {% include "pages/billing/dashboard/choose_plan_section.html" %} 14 | {% include "pages/billing/dashboard/all_subscriptions.html" %} 15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /billing/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | -------------------------------------------------------------------------------- /billing/views/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/billing/views/__init__.py -------------------------------------------------------------------------------- /billing/views/return_urls/failed.py: -------------------------------------------------------------------------------- 1 | from django.contrib import messages 2 | from django.shortcuts import redirect 3 | 4 | from backend.core.types.requests import WebRequest 5 | 6 | 7 | def stripe_failed_return_endpoint(request: WebRequest): 8 | messages.warning(request, "FAILED RESPONSE") 9 | return redirect("billing:dashboard") 10 | -------------------------------------------------------------------------------- /billing/views/return_urls/success.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import redirect 2 | 3 | from backend.core.types.requests import WebRequest 4 | 5 | 6 | def stripe_success_return_endpoint(request: WebRequest): 7 | return redirect("billing:dashboard") 8 | -------------------------------------------------------------------------------- /components/+modal.html: -------------------------------------------------------------------------------- 1 | 4 | 12 | 15 | 16 | -------------------------------------------------------------------------------- /components/+profile_picture.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | {% if swap_oob %}
{% endif %} 3 | {% if url %} 4 | 7 | {% else %} 8 | 11 | {% endif %} 12 | {% if swap_oob %}
{% endif %} 13 | -------------------------------------------------------------------------------- /docs/CNAME: -------------------------------------------------------------------------------- 1 | docs.myfinances.cloud 2 | -------------------------------------------------------------------------------- /docs/contributing/ask.md: -------------------------------------------------------------------------------- 1 | # Asking for help 2 | 3 | Use [`Github Discussions`](https://github.com/TreyWW/MyFinances/discussions) to ask any questions, put forward any ideas, or 4 | vote on our polls! 5 | -------------------------------------------------------------------------------- /docs/contributing/development-progression.md: -------------------------------------------------------------------------------- 1 | Coming Soon -------------------------------------------------------------------------------- /docs/contributing/gh-board.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Github Project Board 3 | --- 4 | 5 | We may use the [`Project Board`](https://github.com/users/TreyWW/projects/8) feature on github to track progress on features 6 | or bugs. This is locked to maintainers only and readonly for the public - but it's still good to check up on active/inactive 7 | issues! 8 | -------------------------------------------------------------------------------- /docs/contributing/gh-issues.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Github Issues 3 | --- 4 | 5 | Our main development area for this project is our [Github](https://github.com/TreyWW/MyFinances). On github you can create 6 | `Issues`. These are a sort of discussion that you can open which keeps lots of messages, code snippets and progress in one place. 7 | 8 | When suggesting an idea, we would rather you first start off a 9 | [`discussion`](https://github.com/TreyWW/MyFinances/discussions/new?category=ideas). This allows us to talk about the idea 10 | before any development is made on it. After we'll convert it to an `Issue`. 11 | 12 | ??? bug "Found a bug?" 13 | Use Github Issues for bug reports too! You can fill in the `Bug Report Form` 14 | [here](https://github.com/TreyWW/MyFinances/issues/new?assignees=&labels=bug&projects=&template=1-bug_report.yml&title=bug%3A+) 15 | to report these bugs to help us reproduce them. 16 | -------------------------------------------------------------------------------- /docs/contributing/lead-position.md: -------------------------------------------------------------------------------- 1 | Currently we have no soon to come lead positions in mind. After we start to release the main versions and the hosted version 2 | is out and we have a few customers, we may start to hire for positions. These may include allowing opensource collaborators, 3 | customer support, marketing experts or sponsorships. It all depends on how well the project grows. 4 | -------------------------------------------------------------------------------- /docs/contributing/motivation.md: -------------------------------------------------------------------------------- 1 | ??? note 2 | This is a message from the maintainer and project owner, [`@trey`](https://github.com/TreyWW). 3 | 4 | At the start, I made this project for myself to help me manage my clients. This allows me to store the details of each client, 5 | create invoices to send to them after work is complete, and in general keep everything in one place. But I decided to make it 6 | opensource in hopes to motivate me to work on it more, and allow others to use my creation. 7 | 8 | Now we have lots of contributors helping make this product! I want the product to be a useful tool for freelancers, small 9 | business owners, or in general anyone that needs to manage customers. It isn't a typical "CMS" though, we hope to give you 10 | tools to integrate communication between customers (like invoicing, automatic schedules, and emails) too. 11 | -------------------------------------------------------------------------------- /docs/contributing/style-guide.md: -------------------------------------------------------------------------------- 1 | Coming Soon -------------------------------------------------------------------------------- /docs/contributing/translations.md: -------------------------------------------------------------------------------- 1 | Currently we have no translations or a translation framework. If you have ideas for how we could implement translations feel 2 | free to make an issue on github and suggest it for our docs! 3 | -------------------------------------------------------------------------------- /docs/contributing/writing-documentation.md: -------------------------------------------------------------------------------- 1 | Coming Soon -------------------------------------------------------------------------------- /docs/debugging/aws/iam.md: -------------------------------------------------------------------------------- 1 | # InvalidSignatureException 2 | 3 | ## Error: 4 | ``` 5 | botocore.exceptions.ClientError: An error occurred (InvalidSignatureException) when calling the SendEmail operation: The request signature we calculated does not match the signature you provided. Check your AWS Secret Access Key and signing method. Consult the service documentation for details. 6 | ``` 7 | 8 | ## Solution 9 | 10 | Keep regenerating IAM Access Keys until the SECRET KEY doesn't contain a **slash** (/) or a **plus** (+) 11 | -------------------------------------------------------------------------------- /docs/debugging/problem-solving.md: -------------------------------------------------------------------------------- 1 | ## **Common error messages and their solutions:** 2 | 3 | 1. If you receive this error: **ImportError: cannot import name 'login_not_required' from 'login_required**
You may be missing some django middleware. 4 | 5 | To fix this: 6 | ```shell 7 | pip install django-login-required-middleware 8 | ``` 9 | 10 | 2. To fix several **boto3** "extension name here" **not found errors** 11 | 12 | Do the following: 13 | ```shell 14 | pip install mypy_boto3_iam 15 | 16 | pip install mypy_boto3_scheduler 17 | 18 | pip install mypy_boto3_events 19 | 20 | pip install mypy_boto3_stepfunctions 21 | ``` -------------------------------------------------------------------------------- /docs/debugging/webpack/empty-pages.md: -------------------------------------------------------------------------------- 1 | # Empty Pages on load 2 | 3 | If you have empty pages when you load, no content is shown, or you are getting errors in your browser console, 4 | you are likely missing our javascript dependencies - such as HTMX. Please run `npm run webpack-build`. 5 | For more details, checkout ["Webpack for JS"](../../../getting-started/installation/#webpack-for-js). 6 | -------------------------------------------------------------------------------- /docs/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/docs/favicon.png -------------------------------------------------------------------------------- /docs/getting-started/databases/index.md: -------------------------------------------------------------------------------- 1 | # Setting up a database 2 | 3 | !!! note 4 | We highly suggest that you use SQLite for your database of choice. We may not be able to help with any issues related 5 | to any other database type. 6 | 7 | Our current supported datatypes are: 8 | 9 | - [SQLite3](sqlite.md) 10 | - [MySQL](mysql.md) 11 | - [Postgres](postgres.md) 12 | -------------------------------------------------------------------------------- /docs/getting-started/databases/sqlite.md: -------------------------------------------------------------------------------- 1 | # SQLite 2 | 3 | Sqlite is a very popular option for local development as it has **no installation**, **is fast for small datasets**, and is 4 | easy to use and backup. 5 | 6 | ## Configure MyFinances to use sqlite3 7 | 8 | By default we use sqlite, but if for some reason you have changed this before you'll need to add this to `.env` 9 | 10 | ```dotenv title=".env" 11 | DATABASE_TYPE=sqlite 12 | ``` 13 | 14 | Then you'll need to run `python manage.py migrate` 15 | 16 | And that's it! Your database will be stored under the file `db.sqlite3`, so keep it safe! 17 | -------------------------------------------------------------------------------- /docs/getting-started/index.md: -------------------------------------------------------------------------------- 1 | # Developer Guide 2 | 3 | In this section we cover setting up the project and configuring it to your needs. 4 | 5 | !!! danger "Note" 6 | What we cover in this section **may not be production ready**, and is only suggested to be used to running a development 7 | version for improving the main version. 8 | -------------------------------------------------------------------------------- /docs/getting-started/settings/AWS/index.md: -------------------------------------------------------------------------------- 1 | # AWS Settings 2 | 3 | Coming Soon 4 | -------------------------------------------------------------------------------- /docs/getting-started/settings/AWS/private-media.md: -------------------------------------------------------------------------------- 1 | # Private Media Files 2 | 3 | Coming Soon 4 | -------------------------------------------------------------------------------- /docs/getting-started/settings/AWS/public-media.md: -------------------------------------------------------------------------------- 1 | # Public Media Files 2 | 3 | Coming Soon 4 | -------------------------------------------------------------------------------- /docs/getting-started/settings/index.md: -------------------------------------------------------------------------------- 1 | # Settings 2 | 3 | You can configure parts of MyFinances to fit your needs better! Some production-type configurations may be: 4 | 5 | - Changing logins (allowing social auth) 6 | - [Github Auth](social-login/github/) (DOCS COMING SOON) 7 | - [Google Auth](social-login/google/) (DOCS COMING SOON) 8 | - [Using AWS to host the application as microservices](AWS/setup/) 9 | -------------------------------------------------------------------------------- /docs/getting-started/settings/social-login/github.md: -------------------------------------------------------------------------------- 1 | # Google oAuth 2 | -------------------------------------------------------------------------------- /docs/getting-started/settings/social-login/google.md: -------------------------------------------------------------------------------- 1 | # Google authentication 2 | -------------------------------------------------------------------------------- /docs/getting-started/settings/social-login/index.md: -------------------------------------------------------------------------------- 1 | # Social Logins 2 | 3 | Social logins are highly, highly suggested. This removes the security from your hands to another service like google. 4 | A lot of users would rather use a popular service to handle their details then us, which makes sense! 5 | 6 | We currently only support two options, but feel free to add a contribution to add more! 7 | 8 | - [GitHub oAuth](github.md) 9 | - [Google oAuth](google.md) 10 | -------------------------------------------------------------------------------- /docs/js/extra.js: -------------------------------------------------------------------------------- 1 | document.addEventListener("DOMContentLoaded", function () { 2 | const viewSourceButton = document.querySelector('a[title="View source of this page"]'); 3 | if (viewSourceButton) { 4 | viewSourceButton.style.display = 'none'; 5 | } 6 | }); 7 | -------------------------------------------------------------------------------- /docs/overrides/main.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {#{% block announce %}#} 3 | {# New Announcement: License Change to AGPL v3#} 4 | {#{% endblock %}#} 5 | -------------------------------------------------------------------------------- /docs/overrides/partials/integrations/analytics/custom.html: -------------------------------------------------------------------------------- 1 | {% if config.extra.analytics.property %} 2 | {{ config.extra.analytics.property | safe }} 3 | {% else %} 4 | 5 | {% endif %} 6 | -------------------------------------------------------------------------------- /docs/user-guide/index.md: -------------------------------------------------------------------------------- 1 | # User Guide 2 | 3 | 4 |
5 | 6 | - :material-cursor-pointer:{ .lg .middle } __Set up in 5 minutes__ 7 | 8 | --- 9 | 10 | New to MyFinances? Let's go over the basics. 11 | 12 | [:octicons-arrow-right-24: Getting started](getting-started/) 13 | 14 | - :fontawesome-solid-clock-rotate-left:{ .md .middle } __Release Notes__ 15 | 16 | --- 17 | 18 | View our most recent changes. Make sure to view the changelog before upgrading! 19 | 20 | [:octicons-arrow-right-24: Changelog](../changelog/) 21 |
22 | -------------------------------------------------------------------------------- /docs/user-guide/invoices/sending/download-as-pdf.md: -------------------------------------------------------------------------------- 1 | # Download invoice as a PDF 2 | 3 | Want to download an invoice as a PDF? Sure thing, let's get started. 4 | 5 | First get your invoice created, and head over to the main invoices dashboard. Find your invoice from the list, and click the 6 | three dots on the right - press "Preview" 7 | 8 | ![Preview dropdown](preview_dropdown.png) 9 | 10 | Now you're on the invoice, press "print invoice", or alternatively press 11 | CTRL + P or CMD + P 12 | 13 | ![Print invoice](print_invoice.png) 14 | ![img.png](img.png) 15 | Once the print dialog appears, select "Save as PDF" as the `destination`, and press `save` at the bottom. Now you have a PDF. 16 | This can be sent to clients manually, or kept as a record! 17 | 18 | ![Save as PDF](save_as_pdf_destination.png) 19 | ![Save](save_button.png) 20 | -------------------------------------------------------------------------------- /docs/user-guide/invoices/sending/preview_dropdown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/docs/user-guide/invoices/sending/preview_dropdown.png -------------------------------------------------------------------------------- /docs/user-guide/invoices/sending/print_dialog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/docs/user-guide/invoices/sending/print_dialog.png -------------------------------------------------------------------------------- /docs/user-guide/invoices/sending/print_invoice.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/docs/user-guide/invoices/sending/print_invoice.png -------------------------------------------------------------------------------- /docs/user-guide/invoices/sending/save_as_pdf_destination.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/docs/user-guide/invoices/sending/save_as_pdf_destination.png -------------------------------------------------------------------------------- /docs/user-guide/invoices/sending/save_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/docs/user-guide/invoices/sending/save_button.png -------------------------------------------------------------------------------- /docs/user-guide/pricing/pricing_table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/docs/user-guide/pricing/pricing_table.png -------------------------------------------------------------------------------- /frontend/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/frontend/static/favicon.ico -------------------------------------------------------------------------------- /frontend/static/favicon/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/frontend/static/favicon/android-chrome-192x192.png -------------------------------------------------------------------------------- /frontend/static/favicon/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/frontend/static/favicon/android-chrome-512x512.png -------------------------------------------------------------------------------- /frontend/static/favicon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/frontend/static/favicon/apple-touch-icon.png -------------------------------------------------------------------------------- /frontend/static/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/frontend/static/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /frontend/static/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/frontend/static/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /frontend/static/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/frontend/static/favicon/favicon.ico -------------------------------------------------------------------------------- /frontend/static/img/avatar-example.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/frontend/static/img/avatar-example.jpg -------------------------------------------------------------------------------- /frontend/static/img/brainstorming_finance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/frontend/static/img/brainstorming_finance.png -------------------------------------------------------------------------------- /frontend/static/img/create-account-office-dark.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/frontend/static/img/create-account-office-dark.jpeg -------------------------------------------------------------------------------- /frontend/static/img/create-account-office.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/frontend/static/img/create-account-office.jpeg -------------------------------------------------------------------------------- /frontend/static/img/dashboard_invoices_page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/frontend/static/img/dashboard_invoices_page.png -------------------------------------------------------------------------------- /frontend/static/img/default_profile_pic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/frontend/static/img/default_profile_pic.png -------------------------------------------------------------------------------- /frontend/static/img/forgot-password-office-dark.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/frontend/static/img/forgot-password-office-dark.jpeg -------------------------------------------------------------------------------- /frontend/static/img/forgot-password-office.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/frontend/static/img/forgot-password-office.jpeg -------------------------------------------------------------------------------- /frontend/static/img/github.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/static/img/login-office-dark.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/frontend/static/img/login-office-dark.jpeg -------------------------------------------------------------------------------- /frontend/static/img/login-office.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/frontend/static/img/login-office.jpeg -------------------------------------------------------------------------------- /frontend/static/img/login_image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/frontend/static/img/login_image.jpg -------------------------------------------------------------------------------- /frontend/static/img/logo_single.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/frontend/static/img/logo_single.png -------------------------------------------------------------------------------- /frontend/static/img/logo_with_text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/frontend/static/img/logo_with_text.png -------------------------------------------------------------------------------- /frontend/static/img/twitter.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/static/js/base.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/frontend/static/js/base.js -------------------------------------------------------------------------------- /frontend/static/js/charts-bars.js: -------------------------------------------------------------------------------- 1 | /** 2 | * For usage, visit Chart.js docs https://www.chartjs.org/docs/latest/ 3 | */ 4 | const barConfig = { 5 | type: 'bar', 6 | data: { 7 | labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'], 8 | datasets: [ 9 | { 10 | label: 'Shoes', 11 | backgroundColor: '#0694a2', 12 | // borderColor: window.chartColors.red, 13 | borderWidth: 1, 14 | data: [-3, 14, 52, 74, 33, 90, 70], 15 | }, 16 | { 17 | label: 'Bags', 18 | backgroundColor: '#7e3af2', 19 | // borderColor: window.chartColors.blue, 20 | borderWidth: 1, 21 | data: [66, 33, 43, 12, 54, 62, 84], 22 | }, 23 | ], 24 | }, 25 | options: { 26 | responsive: true, 27 | legend: { 28 | display: false, 29 | }, 30 | }, 31 | } 32 | 33 | const barsCtx = document.getElementById('bars') 34 | window.myBar = new Chart(barsCtx, barConfig) 35 | -------------------------------------------------------------------------------- /frontend/static/js/charts-pie.js: -------------------------------------------------------------------------------- 1 | /** 2 | * For usage, visit Chart.js docs https://www.chartjs.org/docs/latest/ 3 | */ 4 | const pieConfig = { 5 | type: 'doughnut', 6 | data: { 7 | datasets: [ 8 | { 9 | data: [33, 33, 33], 10 | /** 11 | * These colors come from Tailwind CSS palette 12 | * https://tailwindcss.com/docs/customizing-colors/#default-color-palette 13 | */ 14 | backgroundColor: ['#0694a2', '#1c64f2', '#7e3af2'], 15 | label: 'Dataset 1', 16 | }, 17 | ], 18 | labels: ['Shoes', 'Shirts', 'Bags'], 19 | }, 20 | options: { 21 | responsive: true, 22 | cutoutPercentage: 80, 23 | /** 24 | * Default legends are ugly and impossible to style. 25 | * See examples in charts.html to add your own legends 26 | * */ 27 | legend: { 28 | display: false, 29 | }, 30 | }, 31 | } 32 | 33 | // change this to the id of your chart element in HTML 34 | const pieCtx = document.getElementById('pie') 35 | window.myPie = new Chart(pieCtx, pieConfig) 36 | -------------------------------------------------------------------------------- /frontend/templates/admin/base_site.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base.html" %} 2 | {% load static %} 3 | {% block extrahead %} 4 | 7 | {% endblock %} 8 | -------------------------------------------------------------------------------- /frontend/templates/base/breadcrumbs.html: -------------------------------------------------------------------------------- 1 | {% if breadcrumb or swap or breadcrumb_first_load %} 2 | 14 | {% endif %} 15 | -------------------------------------------------------------------------------- /frontend/templates/base/breadcrumbs_ul.html: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /frontend/templates/base/htmx.html: -------------------------------------------------------------------------------- 1 |
2 | {% block content %} 3 | {% endblock content %} 4 |
5 | {# Profile Picture dropdown item #} 6 |
7 | {% component "base:topbar:icon_dropdown" %} 8 |
9 | {% include "base/+left_drawer.html" with swap=True %} 10 | {% include "base/breadcrumbs.html" with swap=True %} 11 | {% include "base/toasts.html" %} 12 | -------------------------------------------------------------------------------- /frontend/templates/base/toast.html: -------------------------------------------------------------------------------- 1 | {% component "messages_list" %} 2 | -------------------------------------------------------------------------------- /frontend/templates/base/toasts.html: -------------------------------------------------------------------------------- 1 | {% component "messages_list" with_js=True %} 2 | -------------------------------------------------------------------------------- /frontend/templates/base/topbar/_notification_count.html: -------------------------------------------------------------------------------- 1 |

{{ notif_count | default:request.user.notification_count }}

7 | -------------------------------------------------------------------------------- /frontend/templates/base/topbar/_organizations_list.html: -------------------------------------------------------------------------------- 1 | {% for team in request.user.teams_joined.all %} 2 |
  • 3 | {{ team.name | title }} 5 |
  • 6 | {% endfor %} 7 | {% for team in request.user.teams_leader_of.all %} 8 |
  • 9 | {{ team.name | title }} 11 |
  • 12 | {% endfor %} 13 |
  • 14 | Personal 15 |
  • 16 | -------------------------------------------------------------------------------- /frontend/templates/components/+logged_in_for.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/frontend/templates/components/+logged_in_for.html -------------------------------------------------------------------------------- /frontend/templates/components/buttons/brand-button.html: -------------------------------------------------------------------------------- 1 | 3 |
    4 | 6 | {{ text | default:brand }} 7 |
    8 |
    9 | -------------------------------------------------------------------------------- /frontend/templates/components/buttons/save_all.html: -------------------------------------------------------------------------------- 1 | 3 | -------------------------------------------------------------------------------- /frontend/templates/components/containers/dark_main.html: -------------------------------------------------------------------------------- 1 |
    2 | {% block container %}{% endblock %} 3 |
    4 | -------------------------------------------------------------------------------- /frontend/templates/components/form/toggle.html: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /frontend/templates/components/table/skeleton_row.html: -------------------------------------------------------------------------------- 1 | {% comment %} 2 | 3 | Brief explanation: 4 | 5 | with "times" --> number of times to repeat the loop default to 4 if none 6 | for _ in --> just a basic python for loop 7 | "n" --> number of times to repeat the loop 8 | "rjust" --> right justify (pretty much doing a range) 9 | :times --> number of times to repeat the loop 10 | 11 | {% endcomment %} 12 | 13 | {% with cols=cols|default:4 %} 14 | {% for _ in "n"|rjust:cols %} 15 | 16 |
    17 | 18 | {% endfor %} 19 | {% endwith %} 20 | 21 | -------------------------------------------------------------------------------- /frontend/templates/components/table/skeleton_rows.html: -------------------------------------------------------------------------------- 1 | {% comment %} 2 | 3 | Brief explanation: 4 | 5 | with "rows" --> number of times to repeat the loop default to 4 if none 6 | for _ in --> just a basic python for loop 7 | "n" --> number of times to repeat the loop 8 | "rjust" --> right justify (pretty much doing a range) 9 | :times --> number of times to repeat the loop 10 | 11 | {% endcomment %} 12 | {% with rows=rows|default:4 %} 13 | {% for _ in "n"|rjust:rows %} 14 | {% include 'components/table/skeleton_row.html' with cols=cols %} 15 | {% endfor %} 16 | {% endwith %} 17 | -------------------------------------------------------------------------------- /frontend/templates/modals/accept_invite.html: -------------------------------------------------------------------------------- 1 | {% component_block "modal" id="modal_accept_invite" start_open="true" title="Accept Invitation" %} 2 | {% fill "content" %} 3 | 21 | {% endfill %} 22 | {% endcomponent_block %} 23 | -------------------------------------------------------------------------------- /frontend/templates/modals/create_team.html: -------------------------------------------------------------------------------- 1 | {% component_block "modal" id="modal_create_team" start_open="true" title="Create Team" %} 2 | {% fill "content" %} 3 | 26 | {% endfill %} 27 | {% endcomponent_block %} 28 | -------------------------------------------------------------------------------- /frontend/templates/modals/leave_team.html: -------------------------------------------------------------------------------- 1 | {% component_block "modal" id="modal_leave_team" start_open="true" title="Leave Team" %} 2 | {% fill "content" %} 3 | 21 | {% endfill %} 22 | {% endcomponent_block %} 23 | -------------------------------------------------------------------------------- /frontend/templates/pages/admin/api_keys/generate_response.html: -------------------------------------------------------------------------------- 1 |
    2 |

    API Key

    3 |
    4 | Make sure to copy your API Key now, you won't be able to view it again. 5 |
    6 | 11 |
    12 | -------------------------------------------------------------------------------- /frontend/templates/pages/auth/_magic_link_partial.html: -------------------------------------------------------------------------------- 1 | {% if declined %} 2 |
    3 |
    4 |

    5 | You can now close this tab. The login request has been declined. 6 |

    7 |
    8 | {% elif accepted %} 9 |
    10 |
    11 |

    12 | You can now close this tab. The login request has been accepted. 13 | The original tab will now be logged in. 14 |

    15 |
    16 | {% endif %} 17 |
    {% component "messages_list" with_js=True %}
    18 | -------------------------------------------------------------------------------- /frontend/templates/pages/auth/create_account_choose.html: -------------------------------------------------------------------------------- 1 | {% extends 'base/auth.html' %} 2 | {% load static %} 3 | {% block image %} 4 | {% static 'img/choose.svg' %} 5 | {% endblock image %} 6 | {% block title %} 7 | Create an account 8 | {% endblock title %} 9 | {% block content %} 10 | Create with manual details 12 | {% if github_enabled %} 13 | 14 | 15 | Create with github 16 | 17 | {% endif %} 18 | {% if google_enabled %} 19 | 21 | 22 | Create with google 23 | 24 | {% endif %} 25 | {% endblock %} 26 | -------------------------------------------------------------------------------- /frontend/templates/pages/clients/dashboard/_table.html: -------------------------------------------------------------------------------- 1 | {% if delete %} 2 | {% component "messages_list" %} 3 | {% endif %} 4 |
    5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | {% include 'pages/clients/dashboard/_rows.html' %} 15 | 16 |
    Client IDClient NameActions
    17 |
    18 | -------------------------------------------------------------------------------- /frontend/templates/pages/dashboard.html: -------------------------------------------------------------------------------- 1 | {% extends base|default:"base/base.html" %} 2 | {% block content %} 3 |
    4 |
    5 |

    Get started with MyFinances

    6 | View documentation 8 |
    9 |
    10 | {% endblock content %} 11 | -------------------------------------------------------------------------------- /frontend/templates/pages/invoices/create/bank_details/account_number.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 | 5 | 13 | 16 |
    17 |
    18 |
    19 | -------------------------------------------------------------------------------- /frontend/templates/pages/invoices/create/bank_details/holder_name.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 | 5 | 11 | 14 |
    15 |
    16 |
    17 | -------------------------------------------------------------------------------- /frontend/templates/pages/invoices/create/custom_designs/logo.html: -------------------------------------------------------------------------------- 1 | {# TODO: show the logo preview on the left #} 2 |
    3 |
    4 |
    5 | 6 | 11 |
    12 |
    13 |
    14 | -------------------------------------------------------------------------------- /frontend/templates/pages/invoices/create/custom_designs/reference.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 | 11 | 16 |
    17 |
    18 |
    19 | -------------------------------------------------------------------------------- /frontend/templates/pages/invoices/create/dates/due.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 | 8 | 15 | 18 |
    19 |
    20 |
    21 | -------------------------------------------------------------------------------- /frontend/templates/pages/invoices/create/dates/end.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 | 8 | 15 | 18 |
    19 |
    20 |
    21 | -------------------------------------------------------------------------------- /frontend/templates/pages/invoices/create/dates/issue.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 | 8 | 15 | 18 |
    19 |
    20 |
    21 | -------------------------------------------------------------------------------- /frontend/templates/pages/invoices/create/dates/start.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 | 8 | 15 | 18 |
    19 |
    20 |
    21 | -------------------------------------------------------------------------------- /frontend/templates/pages/invoices/create/destinations/_view_clients_dropdown.html: -------------------------------------------------------------------------------- 1 | {% if clients|length > 0 %} 2 | 3 | {% for client in clients %} 4 | 8 | {% endfor %} 9 | {% else %} 10 | 11 | {% endif %} 12 | -------------------------------------------------------------------------------- /frontend/templates/pages/invoices/dashboard/_modify_payment_status.html: -------------------------------------------------------------------------------- 1 |
    {% component "messages_list" with_js=True %}
    2 |
    3 | {% component "pages:invoices:dashboard:payment_status_badge" status=status inv_id=invoice_id %} 4 |
    5 |
    6 | {% component "pages:invoices:dashboard:payment_status_badge" status=status inv_id=invoice_id design="default" %} 7 |
    8 | -------------------------------------------------------------------------------- /frontend/templates/pages/invoices/dashboard/core/main.html: -------------------------------------------------------------------------------- 1 | {% extends base|default:"base/base.html" %} 2 | {% block content %} 3 |
    4 | {% if page_template %} 5 | {% include page_template %} 6 | {% endif %} 7 |
    8 | {% endblock content %} 9 | -------------------------------------------------------------------------------- /frontend/templates/pages/invoices/recurring/dashboard/_modify_status.html: -------------------------------------------------------------------------------- 1 |
    {% component "messages_list" with_js=True %}
    2 |
    3 | {% include "pages/invoices/recurring/dashboard/_status_badge.html" with status=status inv_id=invoice_profile_id %} 4 |
    5 |
    6 | {% include "pages/invoices/recurring/dashboard/_status_badge.html" with status="loading" inv_id=invoice_profile_id design="default" %} 7 |
    8 | {% include "pages/invoices/recurring/dashboard/poll_update.html" %} 9 | -------------------------------------------------------------------------------- /frontend/templates/pages/invoices/recurring/dashboard/_pause_button.html: -------------------------------------------------------------------------------- 1 | {% if invoiceProfile.status == "paused" %} 2 | 8 | {% elif invoiceProfile.status == "ongoing" %} 9 | 15 | {% endif %} 16 | -------------------------------------------------------------------------------- /frontend/templates/pages/invoices/recurring/dashboard/core/main.html: -------------------------------------------------------------------------------- 1 | {% extends base|default:"base/base.html" %} 2 | {% block content %} 3 |
    4 | {% if page_template %} 5 | {% include page_template %} 6 | {% endif %} 7 |
    8 | {% endblock content %} 9 | -------------------------------------------------------------------------------- /frontend/templates/pages/invoices/recurring/dashboard/poll_response.html: -------------------------------------------------------------------------------- 1 |
    2 | {% include "pages/invoices/recurring/dashboard/_status_badge.html" with status=status inv_id=invoice_profile_id design="default" %} 3 |
    4 |
    5 | {% include "pages/invoices/recurring/dashboard/_pause_button.html" %} 6 |
    7 | {% component "messages_list" %} 8 | -------------------------------------------------------------------------------- /frontend/templates/pages/invoices/recurring/dashboard/poll_update.html: -------------------------------------------------------------------------------- 1 |
    2 |
    5 |
    6 |
    7 | 8 |
    9 | -------------------------------------------------------------------------------- /frontend/templates/pages/invoices/single/schedules/schedules/+input-email_to_send_to.html: -------------------------------------------------------------------------------- 1 |
    2 | 6 | 17 |
    18 | -------------------------------------------------------------------------------- /frontend/templates/pages/invoices/single/schedules/view.html: -------------------------------------------------------------------------------- 1 | {% extends base|default:"base/base.html" %} 2 | {% block content %} 3 | 4 |
    5 |
    6 |
    7 |
    8 | {% include "pages/invoices/single/schedules/reminders/reminders/container.html" %} 9 |

    One Time

    10 |
    11 | {% include "pages/invoices/single/schedules/schedules/schedules/onetime_schedule_list.html" %} 12 | {% include "pages/invoices/single/schedules/schedules/schedules/onetime_schedule_create.html" %} 13 |
    14 | {% endblock content %} 15 | -------------------------------------------------------------------------------- /frontend/templates/pages/invoices/single/view/_banner/_button_options_dropdown.html: -------------------------------------------------------------------------------- 1 |
  • 2 | 3 | 4 |

    Download

    5 |
    6 |
  • 7 |
  • 8 | 12 |
  • 13 | {% if request.user.is_authenticated %} 14 |
  • 15 | Share Invoice 17 |
  • 18 | {% endif %} 19 | -------------------------------------------------------------------------------- /frontend/templates/pages/invoices/single/view/_banner/_button_options_top.html: -------------------------------------------------------------------------------- 1 |
  • 2 | 3 | 4 |

    Download

    5 |
    6 |
  • 7 |
  • 8 | 12 |
  • 13 | {% if request.user.is_authenticated %} 14 |
  • 15 | 17 |
  • 18 | {% endif %} 19 | -------------------------------------------------------------------------------- /frontend/templates/pages/invoices/structure/toggler.html: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /frontend/templates/pages/landing/pricing_feature.html: -------------------------------------------------------------------------------- 1 |
  • 2 | 3 | {{ feature }} 4 |
  • 5 | -------------------------------------------------------------------------------- /frontend/templates/pages/products/fetched_items.html: -------------------------------------------------------------------------------- 1 | {# This is for the INVOICE CREATION dropdown #} 2 | {% component "messages_list" custom_oob='afterbegin:div[data-card="services_card_body"]' %} 3 | {% for product in products %} 4 |
  • 5 | 17 |
  • 18 | {% endfor %} 19 | -------------------------------------------------------------------------------- /frontend/templates/pages/receipts/_search.html: -------------------------------------------------------------------------------- 1 |
    2 | {% csrf_token %} 3 | 11 | 13 |
    14 |
    15 | -------------------------------------------------------------------------------- /frontend/templates/pages/settings/settings/_post_profile_pic.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | {% component "profile_picture" swap_oob="small" image_type="small" extra_classes="max-w-12 max-h-12 w-12 h-12 avatar-img-ring" url=users_profile_picture %} 3 | {% component "profile_picture" swap_oob="change_modal" image_type="change_modal" extra_classes="max-w-44 max-h-44 w-44 h-44 rounded-full ring ring-primary ring-offset-base-100 ring-offset-2" url=users_profile_picture %} 4 | {% component "messages_list" %} 5 | -------------------------------------------------------------------------------- /frontend/templates/pages/settings/settings/api_key_row.html: -------------------------------------------------------------------------------- 1 | 2 | {{ key.name | default:"No name given" }} 3 | {{ key.last_used | default:"Never used" }} 4 | {{ key.expires | date:"d M, Y" | default:"Never" }} 5 | {{ key.created | date:"d M, Y H:iA" }} 6 | 7 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /frontend/templates/pages/settings/settings/session.html: -------------------------------------------------------------------------------- 1 |
  • 2 |
    3 |
    4 | {% if "12"|random == "1" %} 5 | 6 | {% else %} 7 | 8 | {% endif %} 9 |
    10 |
    11 |

    Country 1.1.1.1

    12 |

    Chrome on windows

    13 |
    14 |
    15 | 16 |
    17 |
    18 |
  • 19 | -------------------------------------------------------------------------------- /frontend/templates/pages/settings/settings/sessions.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | {% if sessions.count > 0 %} 3 |
    4 |
    5 | 6 |

    Coming Soon

    7 |
    8 |
    9 |
    Sessions
    10 |
      11 | {% for session in sessions|slice:":3" %} 12 | {% include 'pages/settings/settings/session.html' with session=session %} 13 | {% endfor %} 14 |
    15 | {% if sessions.count > 3 %}See more{% endif %} 16 |
    17 |
    18 | {% endif %} 19 | -------------------------------------------------------------------------------- /frontend/templates/pages/settings/teams/leave.html: -------------------------------------------------------------------------------- 1 | {% extends base|default:"base/base.html" %} 2 | {% block content %} 3 |

    Leave Team

    4 |

    5 | Are you sure you would like to leave {{ team.name }} team? 6 |

    7 |
    8 | Leave Team 10 | Go back 12 |
    13 | {% endblock content %} 14 | -------------------------------------------------------------------------------- /frontend/templates/partials/messages_list.html: -------------------------------------------------------------------------------- 1 | {% if autohide != False %} 2 | {% component "messages_list" %} 3 | {% else %} 4 | {% component "messages_list" autohide=False %} 5 | {% endif %} 6 | -------------------------------------------------------------------------------- /infrastructure/aws/handler.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | import boto3 5 | from botocore.config import Config 6 | from settings.helpers import get_var 7 | from settings.settings import AWS_TAGS_APP_NAME 8 | 9 | config = Config(connect_timeout=5, retries={"max_attempts": 2}) 10 | 11 | DEBUG_LEVEL = get_var("AWS_PRINT_DEBUG_LEVEL", default="info") 12 | DEBUG_LEVEL = "debug" if DEBUG_LEVEL == "debug" else "info" if DEBUG_LEVEL == "info" else None 13 | 14 | if get_var("LOG_LEVEL", default="info") == "debug": 15 | logging.basicConfig(level=logging.DEBUG) 16 | else: 17 | logging.basicConfig(level=logging.INFO) 18 | 19 | logger = logging.getLogger() 20 | 21 | if DEBUG_LEVEL == "debug": 22 | boto3.set_stream_logger("", level=logging.DEBUG) 23 | else: 24 | boto3.set_stream_logger("", level=logging.INFO) 25 | 26 | APP_TAGS = [{"key": "app", "value": AWS_TAGS_APP_NAME}] 27 | -------------------------------------------------------------------------------- /infrastructure/aws/pulumi/.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | venv/ 3 | -------------------------------------------------------------------------------- /infrastructure/aws/pulumi/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/infrastructure/aws/pulumi/__init__.py -------------------------------------------------------------------------------- /infrastructure/aws/pulumi/requirements.txt: -------------------------------------------------------------------------------- 1 | pulumi>=3.0.0,<4.0.0 2 | pulumi-aws>=6.0.2,<7.0.0 3 | -------------------------------------------------------------------------------- /infrastructure/backend/docker-compose.ci.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | myfinances_django: 5 | restart: always 6 | build: 7 | context: . 8 | dockerfile: ./infrastructure/backend/Dockerfile 9 | ports: 10 | - '9012:9012' 11 | volumes: 12 | - .:/myfinances 13 | environment: 14 | SECRET_KEY: "some!random!secret!key!use!online!generator!to!get" 15 | URL: "127.0.0.1" 16 | PROXY_IP: "localhost" 17 | BRANCH: "debug" 18 | DEBUG: "true" 19 | DATABASE_TYPE: "sqlite3" 20 | SITE_URL: "http://myfinances.example.com" 21 | SITE_NAME: "myfinances" 22 | AWS_SCHEDULES_ACCESS_KEY_ID: "test" 23 | AWS_SCHEDULES_SECRET_ACCESS_KEY: "test" 24 | -------------------------------------------------------------------------------- /infrastructure/backend/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | myfinances_django: 5 | restart: always 6 | build: 7 | context: ../../ 8 | dockerfile: infrastructure/backend/Dockerfile 9 | ports: 10 | - '9012:9012' 11 | volumes: 12 | - ../../:/MyFinances 13 | - myfinances_static_volume:/mnt/static/ 14 | networks: 15 | - myfinances_network 16 | env_file: 17 | - ../../.env 18 | nginx: 19 | restart: always 20 | image: nginx:latest 21 | ports: 22 | - '10012:10012' 23 | depends_on: 24 | - myfinances_django 25 | networks: 26 | - myfinances_network 27 | volumes: 28 | - /var/run/docker.sock:/tmp/docker.sock:ro 29 | - myfinances_static_volume:/srv/http/static/ 30 | - ../nginx/default.conf:/etc/nginx/conf.d/myfinances.conf 31 | command: /bin/bash -c "nginx -g 'daemon off;'" 32 | volumes: 33 | myfinances_static_volume: 34 | networks: 35 | myfinances_network: 36 | driver: bridge 37 | -------------------------------------------------------------------------------- /infrastructure/backend/scripts/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "[SYSTEM] [DJANGO] About to migrate" 4 | python3 manage.py migrate --no-input 5 | 6 | echo "[SYSTEM] [DJANGO] About to collect static" 7 | python3 manage.py collectstatic --no-input 8 | 9 | # Start Gunicorn in the background 10 | echo "[SYSTEM] [DJANGO] Starting Gunicorn" 11 | gunicorn settings.wsgi:application --bind 0.0.0.0:9012 --workers 2 & 12 | 13 | # Start Celery in the background 14 | #echo "[SYSTEM] [CELERY] Starting Celery" 15 | #celery -A backend worker --loglevel=info & 16 | 17 | # Wait for background processes to finish 18 | wait -n 19 | -------------------------------------------------------------------------------- /infrastructure/backend/scripts/tests/views.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | python3 manage.py collectstatic --no-input 4 | python3 manage.py test --parallel 5 | -------------------------------------------------------------------------------- /infrastructure/backend/urls.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | python3 manage.py urls_test -------------------------------------------------------------------------------- /infrastructure/frontend/default.conf: -------------------------------------------------------------------------------- 1 | upstream myfinances_django { 2 | server myfinances_django:9012; 3 | } 4 | 5 | server { 6 | listen 10012; 7 | 8 | location / { 9 | proxy_pass http://myfinances_django; 10 | proxy_set_header Host $host; 11 | proxy_set_header X-Real-IP $remote_addr; 12 | } 13 | 14 | location /static/ { 15 | root /srv/http/; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /infrastructure/nginx/default.conf: -------------------------------------------------------------------------------- 1 | upstream myfinances_django { 2 | server myfinances_django:9012; 3 | } 4 | 5 | server { 6 | listen 10012; 7 | 8 | location / { 9 | proxy_pass http://myfinances_django; 10 | proxy_set_header Host $host; 11 | proxy_set_header X-Real-IP $remote_addr; 12 | } 13 | 14 | location /static/ { 15 | root /srv/http/; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | 6 | def main(): 7 | """Run administrative tasks.""" 8 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings.settings") 9 | try: 10 | from django.core.management import execute_from_command_line 11 | except ImportError as exc: 12 | raise ImportError( 13 | "Couldn't import Django. Are you sure it's installed and " 14 | "available on your PYTHONPATH environment variable? Did you " 15 | "forget to activate a virtual environment?" 16 | ) from exc 17 | execute_from_command_line(sys.argv) 18 | 19 | 20 | if __name__ == "__main__": 21 | main() 22 | -------------------------------------------------------------------------------- /settings/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/settings/__init__.py -------------------------------------------------------------------------------- /settings/asgi.py: -------------------------------------------------------------------------------- 1 | import os 2 | from django.core.asgi import get_asgi_application 3 | 4 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings.settings") 5 | 6 | application = get_asgi_application() 7 | -------------------------------------------------------------------------------- /settings/wsgi.py: -------------------------------------------------------------------------------- 1 | import os 2 | from django.core.wsgi import get_wsgi_application 3 | 4 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings.settings") 5 | 6 | application = get_wsgi_application() 7 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/tests/__init__.py -------------------------------------------------------------------------------- /tests/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/tests/api/__init__.py -------------------------------------------------------------------------------- /tests/other_tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/tests/other_tests/__init__.py -------------------------------------------------------------------------------- /tests/urls_INACTIVE/logged_in.json: -------------------------------------------------------------------------------- 1 | { 2 | "index": [ 3 | 200 4 | ], 5 | "dashboard": [ 6 | 200 7 | ], 8 | "login": [ 9 | 302 10 | ], 11 | "logout": [ 12 | 302 13 | ], 14 | "login create_account": [ 15 | 302 16 | ], 17 | "login forgot_password": [ 18 | 302 19 | ], 20 | "settings:dashboard": [ 21 | 200 22 | ], 23 | "settings:change_password change_password": [ 24 | 200 25 | ], 26 | "finance:invoices:single:dashboard": [ 27 | 200 28 | ], 29 | "receipts dashboard": [ 30 | 200 31 | ], 32 | "api:finance:receipts:new": [ 33 | 405 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /tests/urls_INACTIVE/unlogged_in.json: -------------------------------------------------------------------------------- 1 | { 2 | "index": [ 3 | 200 4 | ], 5 | "dashboard": [ 6 | 302 7 | ], 8 | "login": [ 9 | 200 10 | ], 11 | "logout": [ 12 | 302 13 | ], 14 | "login create_account": [ 15 | 200 16 | ], 17 | "login forgot_password": [ 18 | 200 19 | ], 20 | "settings:dashboard": [ 21 | 302 22 | ], 23 | "settings:change_password": [ 24 | 302 25 | ], 26 | "finance:invoices:single:dashboard": [ 27 | 302 28 | ], 29 | "receipts dashboard": [ 30 | 302 31 | ], 32 | "api:finance:receipts:new": [ 33 | 405 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /tests/views/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TreyWW/MyFinances/c8622e4db26f6ed15abf8afc9295edfd96e45f52/tests/views/__init__.py -------------------------------------------------------------------------------- /tests/views/test_dashboard.py: -------------------------------------------------------------------------------- 1 | from django.urls import reverse, resolve 2 | from tests.handler import ViewTestCase 3 | 4 | 5 | class DashboardViewTestCase(ViewTestCase): 6 | def test_dashboard_view_302_for_non_authenticated_users(self): 7 | response = self.client.get(reverse("dashboard")) 8 | self.assertEqual(response.status_code, 302) 9 | 10 | def test_dashboard_view_200_for_authenticated_users(self): 11 | self.login_user() 12 | response = self.client.get(reverse("dashboard")) 13 | self.assertEqual(response.status_code, 200) 14 | 15 | def test_dashboard_view_matches_with_urls_view(self): 16 | func = resolve("/dashboard/").func 17 | func_name = f"{func.__module__}.{func.__name__}" 18 | self.assertEqual("/dashboard/", reverse("dashboard")) 19 | self.assertEqual("backend.core.views.other.index.dashboard", func_name) 20 | -------------------------------------------------------------------------------- /tests/views/test_index.py: -------------------------------------------------------------------------------- 1 | from django.urls import reverse, resolve 2 | from tests.handler import ViewTestCase 3 | 4 | 5 | class IndexViewTestCase(ViewTestCase): 6 | def test_index_view_200_for_non_authenticated_users(self): 7 | response = self.client.get(reverse("index")) 8 | self.assertEqual(response.status_code, 200) 9 | 10 | def test_index_view_200_for_authenticated_users(self): 11 | self.login_user() 12 | response = self.client.get(reverse("index")) 13 | self.assertEqual(response.status_code, 200) 14 | 15 | def test_clients_view_matches_with_urls_view(self): 16 | func = resolve("/").func 17 | func_name = f"{func.__module__}.{func.__name__}" 18 | self.assertEqual("/", reverse("index")) 19 | self.assertEqual("backend.core.views.other.index.index", func_name) 20 | -------------------------------------------------------------------------------- /tests/views/test_settings_teams.py: -------------------------------------------------------------------------------- 1 | from django.urls import reverse, resolve 2 | 3 | from tests.handler import ViewTestCase 4 | 5 | 6 | class UserSettingsTeamsDashboardViewTestCase(ViewTestCase): 7 | def test_teams_view_302_for_non_authenticated_users(self): 8 | response = self.client.get(reverse("teams:dashboard")) 9 | self.assertEqual(response.status_code, 302) 10 | 11 | def test_teams_view_200_for_authenticated_users(self): 12 | self.login_user() 13 | response = self.client.get(reverse("teams:dashboard")) 14 | self.assertEqual(response.status_code, 200) 15 | 16 | def test_teams_view_matches_with_urls_view(self): 17 | path = "/dashboard/teams/" 18 | func = resolve(path).func 19 | func_name = f"{func.__module__}.{func.__name__}" 20 | self.assertEqual("backend.core.views.settings.teams.teams_dashboard_handler", func_name) 21 | self.assertEqual(path, reverse("teams:dashboard")) 22 | -------------------------------------------------------------------------------- /tests/views/test_usersettings_profile_settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from django.contrib.messages import get_messages 4 | from django.urls import reverse 5 | 6 | from backend.models import UserSettings 7 | from tests.handler import ViewTestCase, create_mock_image 8 | 9 | # class UserSettingsProfileSettingsViewTestCase(ViewTestCase): 10 | # 11 | -------------------------------------------------------------------------------- /webpack.dev.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const common = require('./webpack.common.js'); 3 | const {merge} = require('webpack-merge'); 4 | 5 | module.exports = merge(common, { 6 | mode: 'development', 7 | devtool: 'inline-source-map', 8 | }); -------------------------------------------------------------------------------- /webpack.prod.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const common = require('./webpack.common.js'); 3 | const {merge} = require('webpack-merge'); 4 | 5 | module.exports = merge(common, { 6 | mode: 'production', 7 | }); 8 | --------------------------------------------------------------------------------