mirror of
https://github.com/eitchtee/WYGIWYH.git
synced 2026-02-26 09:24:51 +01:00
Compare commits
96 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fee5b93cea | ||
|
|
a7d8f94412 | ||
|
|
44b87da423 | ||
|
|
85794f5c01 | ||
|
|
f246d115e2 | ||
|
|
aae85ecf94 | ||
|
|
ec911c0085 | ||
|
|
7b77f6f363 | ||
|
|
239e9c4b2a | ||
|
|
5abd0b8d3c | ||
|
|
320217f64a | ||
|
|
2735906d5e | ||
|
|
1f03edcc2e | ||
|
|
1405976292 | ||
|
|
6a06d0ee88 | ||
|
|
49c17f75b4 | ||
|
|
2ff6d69fac | ||
|
|
3023f33d3d | ||
|
|
b5671fcd0e | ||
|
|
48408cead8 | ||
|
|
cd7ecd42ea | ||
|
|
0b83ad6b3e | ||
|
|
d0ef08252e | ||
|
|
1140d9c896 | ||
|
|
b2843a1ec1 | ||
|
|
d25aba7be9 | ||
|
|
c3eaca3e9a | ||
|
|
5677706452 | ||
|
|
5bf7f9f272 | ||
|
|
448841dadc | ||
|
|
1b6934694e | ||
|
|
d4d00ba02f | ||
|
|
19a65ac45f | ||
|
|
b72e7bd707 | ||
|
|
190be3e813 | ||
|
|
88300b314c | ||
|
|
fab77c8d9f | ||
|
|
1ae7158d7e | ||
|
|
05f0356288 | ||
|
|
b3cea17b8d | ||
|
|
0b66b23f16 | ||
|
|
80fdf70f7d | ||
|
|
fa931b0db2 | ||
|
|
cab79b4203 | ||
|
|
ddab3db6b5 | ||
|
|
9fa704811c | ||
|
|
4c0d14def0 | ||
|
|
43382d2ffe | ||
|
|
65ad51c273 | ||
|
|
27d448afd6 | ||
|
|
1dd90974bd | ||
|
|
31cc8db3ac | ||
|
|
3d85a15ec9 | ||
|
|
90f98c2d15 | ||
|
|
643855e60e | ||
|
|
e0f7b532f8 | ||
|
|
b4d3e4b42f | ||
|
|
9a7ccb0973 | ||
|
|
a9b67ff272 | ||
|
|
233b9629a2 | ||
|
|
4180c177f1 | ||
|
|
f1bc04756f | ||
|
|
13795c797f | ||
|
|
331a7d5b18 | ||
|
|
81b8da30d6 | ||
|
|
80bad240e7 | ||
|
|
187c56c96c | ||
|
|
3796112d77 | ||
|
|
958940089a | ||
|
|
a08548bb13 | ||
|
|
7fe446e510 | ||
|
|
eccb0d15ee | ||
|
|
7ebd329706 | ||
|
|
d3fcd5fe7e | ||
|
|
b0a3acbdde | ||
|
|
33ce38d74c | ||
|
|
fa51a7fef9 | ||
|
|
d7c072a35c | ||
|
|
c88a6dcf3a | ||
|
|
fcb54a0af2 | ||
|
|
eec2ced481 | ||
|
|
58a6048857 | ||
|
|
93774cca64 | ||
|
|
679f49badc | ||
|
|
b535a12014 | ||
|
|
72876bff43 | ||
|
|
4411022027 | ||
|
|
086210b39d | ||
|
|
73cb2d861b | ||
|
|
1c479ef85a | ||
|
|
51b2b11825 | ||
|
|
414a9bb88a | ||
|
|
f6d1a42b35 | ||
|
|
eb25f8aeb3 | ||
|
|
2ee64a534e | ||
|
|
14073d3555 |
@@ -1,6 +1,5 @@
|
||||
SERVER_NAME=wygiwyh_server
|
||||
DB_NAME=wygiwyh_pg
|
||||
PROCRASTINATE_NAME=wygiwyh_procrastinate
|
||||
|
||||
DEBUG=false
|
||||
URL = https://...
|
||||
@@ -9,7 +8,6 @@ SECRET_KEY=<GENERATE A SAFE SECRET KEY AND PLACE IT HERE>
|
||||
DJANGO_ALLOWED_HOSTS=localhost 127.0.0.1 [::1]
|
||||
OUTBOUND_PORT=9005
|
||||
|
||||
SQL_ENGINE=django.db.backends.postgresql
|
||||
SQL_DATABASE=wygiwyh
|
||||
SQL_USER=wygiwyh
|
||||
SQL_PASSWORD=<INSERT A SAFE PASSWORD HERE>
|
||||
@@ -24,3 +22,5 @@ WEB_CONCURRENCY=4
|
||||
ENABLE_SOFT_DELETE=false
|
||||
# If ENABLE_SOFT_DELETE is true, transactions deleted for more than KEEP_DELETED_TRANSACTIONS_FOR days will be truly deleted. Set to 0 to keep all.
|
||||
KEEP_DELETED_TRANSACTIONS_FOR=365
|
||||
|
||||
TASK_WORKERS=1 # This only work if you're using the single container option. Increase to have more open queues via procrastinate, you probably don't need to increase this.
|
||||
|
||||
57
.github/workflows/release.yml
vendored
57
.github/workflows/release.yml
vendored
@@ -2,7 +2,20 @@ name: Release Pipeline
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [created]
|
||||
types: [ created ]
|
||||
push:
|
||||
branches: [ main ]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Custom tag name for the image'
|
||||
required: true
|
||||
type: string
|
||||
ref:
|
||||
description: 'Git ref to checkout (branch, tag, or SHA)'
|
||||
required: true
|
||||
default: 'main'
|
||||
type: string
|
||||
|
||||
env:
|
||||
IMAGE_NAME: wygiwyh
|
||||
@@ -16,6 +29,13 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.inputs.ref }}
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
|
||||
- name: Checkout code (non-manual)
|
||||
uses: actions/checkout@v4
|
||||
if: github.event_name != 'workflow_dispatch'
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
|
||||
@@ -29,16 +49,49 @@ jobs:
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build and push image
|
||||
- name: Build and push nightly image
|
||||
if: github.event_name == 'push'
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/prod/django/Dockerfile
|
||||
push: true
|
||||
provenance: false
|
||||
build-args: |
|
||||
VERSION=nightly
|
||||
tags: ${{ secrets.DOCKERHUB_USERNAME }}/${{ env.IMAGE_NAME }}:nightly
|
||||
platforms: linux/amd64,linux/arm64
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
- name: Build and push release image
|
||||
if: github.event_name == 'release'
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/prod/django/Dockerfile
|
||||
push: true
|
||||
provenance: false
|
||||
build-args: |
|
||||
VERSION=${{ github.event.release.tag_name }}
|
||||
tags: |
|
||||
${{ secrets.DOCKERHUB_USERNAME }}/${{ env.IMAGE_NAME }}:latest
|
||||
${{ secrets.DOCKERHUB_USERNAME }}/${{ env.IMAGE_NAME }}:${{ github.event.release.tag_name }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
- name: Build and push custom image
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/prod/django/Dockerfile
|
||||
push: true
|
||||
provenance: false
|
||||
build-args: |
|
||||
VERSION=${{ github.event.inputs.tag }}
|
||||
tags: ${{ secrets.DOCKERHUB_USERNAME }}/${{ env.IMAGE_NAME }}:${{ github.event.inputs.tag }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
479
README.md
479
README.md
@@ -90,471 +90,44 @@ If you want to run WYGIWYH locally, on your env file:
|
||||
You can now access localhost:OUTBOUND_PORT
|
||||
|
||||
> [!NOTE]
|
||||
> If you're planning on running this behind Tailscale or other similar service also add your machine given IP to `DJANGO_ALLOWED_HOSTS`
|
||||
|
||||
> [!NOTE]
|
||||
> If you're going to use another IP that isn't localhost, add it to `DJANGO_ALLOWED_HOSTS`, without `http://`
|
||||
> - If you're planning on running this behind Tailscale or other similar service also add your machine given IP to `DJANGO_ALLOWED_HOSTS`
|
||||
> - If you're going to use another IP that isn't localhost, add it to `DJANGO_ALLOWED_HOSTS`, without `http://`
|
||||
|
||||
|
||||
## Building from source
|
||||
## Latest changes
|
||||
Features are only added to `main` when ready, if you want to run the latest version, you must build from source or use the `:nightly` tag on docker. Keep in mind that there can be undocumented breaking changes.
|
||||
|
||||
Features are only added to `main` when ready, if you want to run the latest version, you must build from source.
|
||||
All the required Dockerfiles are [here](https://github.com/eitchtee/WYGIWYH/tree/main/docker/prod).
|
||||
|
||||
```bash
|
||||
# Create a folder for WYGIWYH (optional)
|
||||
$ mkdir WYGIWYH
|
||||
## Unraid
|
||||
|
||||
# Go into the folder
|
||||
$ cd WYGIWYH
|
||||
[nwithan8](https://github.com/nwithan8) has kindly provided a Unraid template for WYGIWYH, have a look at the [unraid_templates](https://github.com/nwithan8/unraid_templates) repo.
|
||||
|
||||
# Clone this repository
|
||||
$ git clone https://github.com/eitchtee/WYGIWYH.git .
|
||||
WYGIWYH is available on the Unraid Store. You'll need to provision your own postgres (version 15 or up) database.
|
||||
|
||||
$ cp docker-compose.prod.yml docker-compose.yml
|
||||
$ cp .env.example .env
|
||||
# Now edit both files as you see fit
|
||||
## Enviroment Variables
|
||||
|
||||
# Run the app
|
||||
$ docker compose up -d --build
|
||||
|
||||
# Create the first admin account
|
||||
$ docker compose exec -it web python manage.py createsuperuser
|
||||
```
|
||||
| variable | type | default | explanation |
|
||||
|-------------------------------|-------------|-----------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| DJANGO_ALLOWED_HOSTS | string | localhost 127.0.0.1 | A list of space separated domains and IPs representing the host/domain names that WYGIWYH site can serve. [Click here](https://docs.djangoproject.com/en/5.1/ref/settings/#allowed-hosts) for more details |
|
||||
| HTTPS_ENABLED | true\|false | false | Whether to use secure cookies. If this is set to true, the cookie will be marked as “secure”, which means browsers may ensure that the cookie is only sent under an HTTPS connection |
|
||||
| URL | string | http://localhost http://127.0.0.1 | A list of space separated domains and IPs (with the protocol) representing the trusted origins for unsafe requests (e.g. POST). [Click here](https://docs.djangoproject.com/en/5.1/ref/settings/#csrf-trusted-origins ) for more details |
|
||||
| SECRET_KEY | string | "" | This is used to provide cryptographic signing, and should be set to a unique, unpredictable value. |
|
||||
| DEBUG | true\|false | false | Turns DEBUG mode on or off, this is useful to gather more data about possible errors you're having. Don't use in production. |
|
||||
| SQL_DATABASE | string | None *required | The name of your postgres database |
|
||||
| SQL_USER | string | user | The username used to connect to your postgres database |
|
||||
| SQL_PASSWORD | string | password | The password used to connect to your postgres database |
|
||||
| SQL_HOST | string | localhost | The adress used to connect to your postgres database |
|
||||
| SQL_PORT | string | 5432 | The port used to connect to your postgres database |
|
||||
| SESSION_EXPIRY_TIME | int | 2678400 (31 days) | The age of session cookies, in seconds. E.g. how long you will stay logged in |
|
||||
| ENABLE_SOFT_DELETE | true\|false | false | Whether to enable transactions soft delete, if enabled, deleted transactions will remain in the database. Useful for imports and avoiding duplicate entries. |
|
||||
| KEEP_DELETED_TRANSACTIONS_FOR | int | 365 | Time in days to keep soft deleted transactions for. If 0, will keep all transactions indefinitely. Only works if ENABLE_SOFT_DELETE is true. |
|
||||
| TASK_WORKERS | int | 1 | How many workers to have for async tasks. One should be enough for most use cases |
|
||||
|
||||
# How it works
|
||||
|
||||
## Models
|
||||
Check out our [Wiki](https://github.com/eitchtee/WYGIWYH/wiki) for more information.
|
||||
|
||||
### Transactions
|
||||
|
||||
Transactions are the core feature of WYGIWYH, representing expenses or income in your accounts. Each transaction consists of the following fields:
|
||||
|
||||
#### Type
|
||||
|
||||
- **Income**: A positive amount entering your account
|
||||
- **Expense**: A negative amount exiting your account
|
||||
|
||||
#### Paid Status
|
||||
|
||||
A transaction can be either:
|
||||
|
||||
- **Current**: When marked as paid
|
||||
- **Projected**: When marked as unpaid
|
||||
|
||||
#### Account
|
||||
|
||||
The account associated with the transaction. Required, limited to one account per transaction.
|
||||
|
||||
#### Entity
|
||||
|
||||
The party involved in the transaction:
|
||||
|
||||
- For **Income**: The paying entity
|
||||
- For **Expense**: The receiving entity
|
||||
|
||||
Optional field.
|
||||
|
||||
#### Date
|
||||
|
||||
The date when the transaction occurred. Required.
|
||||
|
||||
#### Reference Date
|
||||
|
||||
One of **WYGIWYH**'s key features. The reference date determines which month a transaction should count towards. For example, you can have a transaction that occurred on January 26th count towards February's finances.
|
||||
|
||||
Optional - defaults to the transaction date's month if not specified.
|
||||
|
||||
> [!CAUTION]
|
||||
> While designed primarily for credit card closing dates, this feature allows for debt rolling across months. Use responsibly to maintain accurate financial tracking.
|
||||
|
||||
#### Type
|
||||
|
||||
- Income, meaning a positive amount (usually) entering your account
|
||||
- Expense, meaning a negative amount exiting your account
|
||||
|
||||
#### Description
|
||||
|
||||
The name or purpose of the transaction. Required.
|
||||
|
||||
#### Amount
|
||||
|
||||
The monetary value of the transaction. Required.
|
||||
|
||||
#### Category
|
||||
|
||||
The primary classification of the transaction. Optional.
|
||||
|
||||
#### Tags
|
||||
|
||||
Additional labels for transaction categorization. Optional.
|
||||
|
||||
#### Notes
|
||||
|
||||
Additional information about the transaction. Optional.
|
||||
|
||||

|
||||
|
||||
### Installment Plan
|
||||
|
||||
An Installment Plan is a helper model that generates a series of recurring transactions over a fixed period.
|
||||
|
||||
#### Core Fields
|
||||
|
||||
- **Account**: The account for all transactions in the plan. Required.
|
||||
- **Entity**: The paying or receiving party for all transactions. Optional.
|
||||
- **Description**: The name of the installment plan, used for all transactions. Required.
|
||||
- **Notes**: Additional information applied to all transactions. Optional.
|
||||
|
||||
#### Installment Configuration
|
||||
|
||||
- **Number of Installments**: Total number of transactions to create (e.g., 1/10, 2/10)
|
||||
- **Installment Start**: Initial counting point
|
||||
- **Start Date**: Date of the first transaction
|
||||
- **Reference Date**: Reference date for the first transaction
|
||||
- **Recurrence**: Frequency of transactions (e.g., Monthly)
|
||||
|
||||

|
||||
|
||||
### Transaction Details
|
||||
|
||||
- **Amount**: Value for each transaction. Required.
|
||||
- **Category**: Primary classification for all transactions. Optional.
|
||||
- **Tags**: Labels applied to all transactions. Optional.
|
||||
|
||||
### Recurring Transaction
|
||||
A Recurring Transaction is a helper model that generates recurring transactions indefinitely or until a certain date.
|
||||
|
||||
#### Core Fields
|
||||
|
||||
- **Account**: The account for all transactions in the plan. Required.
|
||||
- **Entity**: The paying or receiving party for all transactions. Optional.
|
||||
- **Description**: The name of the recurring transaction, used for all transactions. Required.
|
||||
- **Notes**: Additional information applied to all transactions. Optional.
|
||||
|
||||
#### Recurring Transaction Configuration
|
||||
|
||||
- **Start Date**: Date of the first transaction. Required.
|
||||
- **Reference Date**: Reference date for the first transaction. Optional.
|
||||
- **Recurrence Type**: Frequency of transactions (e.g., Monthly). Required.
|
||||
- **Recurrence Interval**: The interval between transactions (e.g. every 1 month, every 2 weeks, etc.). Required.
|
||||
- **End date**: When new transactions should stop being created. Optional.
|
||||
|
||||
#### Transaction Details
|
||||
|
||||
- **Amount**: Value for each transaction. Required.
|
||||
- **Category**: Primary classification for all transactions. Optional.
|
||||
- **Tags**: Labels applied to all transactions. Optional.
|
||||
|
||||
#### Other information
|
||||
|
||||
- Recurring transactions are checked and created every midnight using Procrastinate.
|
||||
- **WYGIWYH** tries to keep at most **6** future transactions created at any time.
|
||||
- If you delete a recurring transaction it will not be recreated.
|
||||
- You can stop or pause a recurring transaction at any time on the config page (/recurring-trasanctions/)
|
||||
|
||||

|
||||
|
||||
### Account
|
||||
|
||||
Accounts represent different financial entities where transactions occur. They have the following attributes:
|
||||
|
||||
- **Name**: A unique identifier for the account.
|
||||
- **Group**: An optional [account group](#account-groups) the account belongs to for organizational purposes.
|
||||
- **Currency**: The primary [currency](#currency) of the account.
|
||||
- **Exchange Currency**: An optional currency used for exchange rate calculations.
|
||||
- **Is Asset**: A boolean indicating if the account is considered an asset (counts towards net worth).
|
||||
- **Is Archived**: A boolean indicating if the account is archived (doesn't show up in active lists or count towards net worth).
|
||||
|
||||
### Account Groups
|
||||
|
||||
Account Groups are used to organize accounts into logical categories. They consist of:
|
||||
|
||||
- **Name**: A unique identifier for the group.
|
||||
|
||||
### Currency
|
||||
|
||||
Currencies represent different monetary units. They include:
|
||||
|
||||
* **Code**: A unique identifier for the currency (e.g., USD, EUR).
|
||||
* **Name**: The full name of the currency.
|
||||
* **Decimal Place**: The number of decimal places used for the currency.
|
||||
* **Prefix**: An optional symbol or text that comes before the amount.
|
||||
* **Suffix**: An optional symbol or text that comes after the amount.
|
||||
|
||||
### Exchange Rate
|
||||
|
||||
Exchange Rates store conversion rates between currencies:
|
||||
|
||||
* **From Currency**: The source currency.
|
||||
* **To Currency**: The target currency.
|
||||
* **Rate**: The conversion rate.
|
||||
* **Date**: The date the rate was recorded or is valid for.
|
||||
|
||||
### Category
|
||||
|
||||
Categories are used to classify transactions:
|
||||
|
||||
* **Name**: A unique identifier for the category.
|
||||
* **Muted**: Muted categories won't count towards your monthly total.
|
||||
* **Active**: A boolean indicating if the category is currently in use. This will disable its use on new transactions.
|
||||
|
||||
### Tag
|
||||
|
||||
Tags provide additional labeling for transactions:
|
||||
|
||||
* **Name**: A unique identifier for the tag.
|
||||
* **Active**: A boolean indicating if the category is currently in use. This will disable its use on new transactions.
|
||||
|
||||
### Entity
|
||||
|
||||
Entities represent parties involved in transactions:
|
||||
|
||||
* **Name**: A unique identifier for the entity.
|
||||
* **Active**: A boolean indicating if the entity is currently in use. This will disable its use on new transactions.
|
||||
|
||||
---
|
||||
|
||||
## Helper actions
|
||||
|
||||
### Transfer
|
||||
|
||||
A transfer happens when you move a monetary value from one account to another. This will create two transactions, one expense and one income with the values set by the user.
|
||||
|
||||
Contrary to other finance trackers, due to our multi-currency support, **WYGIWYH**'s transfer system allows for non-zero transfers.
|
||||
|
||||

|
||||
|
||||
### Balance (Account Reconciliation)
|
||||
|
||||
A balance is a easy way of updating your accounts balance. It creates a transaction with the difference between the balance currently in **WYGIWYH** and the new balance informed by you.
|
||||
|
||||
This can be useful for savings accounts or other interest accruing investments.
|
||||
|
||||
---
|
||||
|
||||
## Views
|
||||
|
||||
### Monthly
|
||||
|
||||
The Monthly view provides an overview of your financial activity for a specific month. It includes:
|
||||
|
||||
* Total income and expenses for the month
|
||||
* Daily spending allowance calculation
|
||||
* List of transactions for the month
|
||||
|
||||
> [!NOTE]
|
||||
> Reference dates are taken into account here.
|
||||
|
||||
### Yearly by currency
|
||||
|
||||
This view gives you a yearly summary of your finances grouped by currency. It shows:
|
||||
|
||||
* Total income and expenses for each currency
|
||||
* Monthly breakdown of income and expenses
|
||||
|
||||
### Yearly by account
|
||||
|
||||
Similar to the [yearly by currency](#yearly-by-currency) view, but groups the data by account instead.
|
||||
|
||||
### Calendar
|
||||
|
||||
The Calendar view presents your transactions in a monthly calendar format, allowing you to see your financial activity day by day. It includes:
|
||||
|
||||
* Visual representation of daily transaction totals
|
||||
* Ability to view details of transactions for each day
|
||||
|
||||
> [!NOTE]
|
||||
> Reference dates are **not** taken into account here.
|
||||
|
||||
### Networh
|
||||
|
||||
#### Current
|
||||
|
||||
The Current Net Worth view shows your present financial standing, including:
|
||||
|
||||
* Total value of all asset accounts
|
||||
* Breakdown of assets by account and currency
|
||||
* Historical net worth trend
|
||||
|
||||
#### Projected
|
||||
|
||||
The Projected Net Worth view estimates your future financial position based on current data and recurring transactions. It includes:
|
||||
|
||||
* Your total net worth with projected and current transactions
|
||||
* Breakdown of assets by account and currency
|
||||
* Historical and future net worth trend
|
||||
|
||||
### All Transactions
|
||||
|
||||
This view provides a comprehensive list of all transactions across all accounts. Features include:
|
||||
|
||||
* Advanced filtering and sorting options
|
||||
* Detailed information
|
||||
|
||||
You can use this to see how much you spent on a given category, or a given day, etc..
|
||||
|
||||
### Configuration and Management
|
||||
|
||||
#### Management
|
||||
The Management section in the navbar allows you to add and edit most elements of WYGIWYH, including:
|
||||
|
||||
* Accounts and Groups
|
||||
* Currencies and Exchange Rates
|
||||
* Categories, Tags and Entities
|
||||
* Rules
|
||||
|
||||
#### User Settings
|
||||
|
||||
WYGIWYH allows users to personalize their experience through customizable settings. Each user can configure:
|
||||
|
||||
* **Language**: Choose your preferred interface language.
|
||||
* **Timezone**: Set your local timezone for accurate date and time display.
|
||||
* **Start Page**: Select which page you want to see first when you log in.
|
||||
* **Sound Preferences**: Toggle sound effects on or off.
|
||||
* **Amount Display**: Choose to show or hide monetary amounts by default.
|
||||
|
||||
To access and modify these settings:
|
||||
|
||||
1. Click on your username in the top-right corner of the page.
|
||||
2. Select "Settings" from the dropdown menu.
|
||||
3. Adjust your preferences as desired.
|
||||
4. Click "Save" to apply your changes.
|
||||
|
||||
These settings ensure that WYGIWYH adapts to your personal preferences and working style.
|
||||
|
||||
#### Django Admin
|
||||
From here you can also access Django's own admin site.
|
||||
|
||||
> [!WARNING]
|
||||
> Most side effects aren't triggered from the admin.
|
||||
> Only use it if you know what you're doing or were told by a developer to do so.
|
||||
|
||||
---
|
||||
|
||||
## Tools
|
||||
|
||||
### Calculator
|
||||
|
||||
The calculator is a floating widget that can be toggled by clicking the calculator icon on the navbar or by pressing <kbd>Alt</kbd> + <kbd>C</kbd> on any page.
|
||||
|
||||
It allows for any math expression supported by [math.js](https://mathjs.org).
|
||||
|
||||

|
||||
|
||||
### Dollar Cost Average Tracker
|
||||
|
||||
The DCA Tracker can be accessed from the navbar's **Tools** menu.
|
||||
|
||||
It allows for tracking DCA strategies and getting helpful information and insights.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Currently DCA exists separately from your main transactions. You will need to add your entries manually.
|
||||
|
||||
<img src=".github/img/readme_dca_1.png" width="45%"></img> <img src=".github/img/readme_dca_2.png" width="45%"></img>
|
||||
|
||||
### Unit Price Calculator
|
||||
|
||||
The Unit Price Calculator can be accessed from the navbar's **Tools** menu.
|
||||
|
||||
This is a self-contained tool for comparing and finding the most cost-efficient item quickly and easily.
|
||||
|
||||
Input the price and the amount of each item, the cheapeast will be highlighted in green, and the most expensive in red.
|
||||
|
||||
You can add additional items by clicking the _Add_ button at the end of the page.
|
||||
|
||||
> [!NOTE]
|
||||
> This doesn't do unit convertion. The amount of all items needs to be on the same the unit for proper functioning.
|
||||
|
||||

|
||||
|
||||
### Currency Converter
|
||||
|
||||
The Currency Converter is a tool that allows you to quickly convert amounts between different currencies.
|
||||
|
||||
> [!NOTE]
|
||||
> There's no external Exchange Rate fetching. This uses the Exchange Rates configured in the [Management](#configuration-and-management) page for [Exchange Rates](#exchange-rate)
|
||||
|
||||
## Automation
|
||||
|
||||
### API
|
||||
|
||||
WYGIWYH has a comprehensive API, it's documentation can be accessed on `<your-wygiwyh-url>/api/docs/`
|
||||
|
||||
> [!NOTE]
|
||||
> While the API works, there's still much to be added to it to equipare functionality with the main web app.
|
||||
|
||||
### Transaction Rules
|
||||
|
||||
Transaction Rules are a powerful feature in WYGIWYH that allow for automatic modification of transactions based on specified criteria. This can save time and ensure consistency in your financial tracking.
|
||||
|
||||
Key Aspects of Transaction Rules:
|
||||
|
||||
* **Conditions**: Set specific criteria that a transaction must meet for the rule to apply. This can include attributes like description, amount, account, etc.
|
||||
* **Actions**: Define what changes should be made to a transaction when the conditions are met. This can include setting categories, tags, or modifying other fields.
|
||||
* **Activation Options**: Rules can be set to apply when transactions are created, updated, or both.
|
||||
|
||||
#### Actions and Conditions
|
||||
|
||||
When creating a new rule, you will need to add a Condition and, later, Actions.
|
||||
|
||||
Both use a limited subset of Python, via [SimpleEval](https://github.com/danthedeckie/simpleeval).
|
||||
|
||||
The Condition must evaluate to True or False, and the Action must evaluate to a value that will be set on the selected field.
|
||||
|
||||
You may use any of the available [variables](#available-variables) and [functions](#available-functions).
|
||||
|
||||
#### Available variables
|
||||
|
||||
* `account_name`
|
||||
* `account_id`
|
||||
* `account_group_name`
|
||||
* `account_group_id`
|
||||
* `is_asset_account`
|
||||
* `is_archived_account`
|
||||
* `category_name`
|
||||
* `category_id`
|
||||
* `tag_names`
|
||||
* `tag_ids`
|
||||
* `entities_names`
|
||||
* `entities_ids`
|
||||
* `is_expense`
|
||||
* `is_income`
|
||||
* `is_paid`
|
||||
* `description`
|
||||
* `amount`
|
||||
* `notes`
|
||||
* `date`
|
||||
* `reference_date`
|
||||
|
||||
#### Available functions
|
||||
|
||||
* `relativedelta`
|
||||
|
||||
#### Examples
|
||||
|
||||
Add a tag to an income transaction if it happens in a specific account
|
||||
|
||||
```
|
||||
If...
|
||||
account_name == "My Investing Account" and is_income
|
||||
|
||||
Then...
|
||||
Set Tags to
|
||||
tag_names + ["Yield"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Move credit card transactions to next month when they happen at a cutoff date
|
||||
|
||||
```
|
||||
If...
|
||||
account_name == "My credit card" and date.day >= 26 and reference_date.month == date.month
|
||||
|
||||
Then...
|
||||
Set Reference Date to
|
||||
reference_date + relativedelta(months=1)).replace(day=1)
|
||||
```
|
||||
# Caveats and Warnings
|
||||
|
||||
- I'm not an accountant, some terms and even calculations might be wrong. Make sure to open an issue if you see anything that could be improved.
|
||||
|
||||
@@ -31,10 +31,8 @@ SECRET_KEY = os.getenv("SECRET_KEY", "")
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = os.getenv("DEBUG", "false").lower() == "true"
|
||||
|
||||
ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS", "localhost 127.0.0.1").split(" ")
|
||||
CSRF_TRUSTED_ORIGINS = os.environ.get("URL", "http://localhost http://127.0.0.1").split(
|
||||
" "
|
||||
)
|
||||
ALLOWED_HOSTS = os.getenv("DJANGO_ALLOWED_HOSTS", "localhost 127.0.0.1").split(" ")
|
||||
CSRF_TRUSTED_ORIGINS = os.getenv("URL", "http://localhost http://127.0.0.1").split(" ")
|
||||
|
||||
# Application definition
|
||||
|
||||
@@ -77,6 +75,7 @@ INSTALLED_APPS = [
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
"apps.common.middleware.thread_local.ThreadLocalMiddleware",
|
||||
"debug_toolbar.middleware.DebugToolbarMiddleware",
|
||||
"django.middleware.security.SecurityMiddleware",
|
||||
"whitenoise.middleware.WhiteNoiseMiddleware",
|
||||
@@ -126,12 +125,12 @@ WSGI_APPLICATION = "WYGIWYH.wsgi.application"
|
||||
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"ENGINE": os.environ.get("SQL_ENGINE", "django.db.backends.sqlite3"),
|
||||
"NAME": os.environ.get("SQL_DATABASE", BASE_DIR / "db.sqlite3"),
|
||||
"USER": os.environ.get("SQL_USER", "user"),
|
||||
"PASSWORD": os.environ.get("SQL_PASSWORD", "password"),
|
||||
"HOST": os.environ.get("SQL_HOST", "localhost"),
|
||||
"PORT": "5432",
|
||||
"ENGINE": "django.db.backends.postgresql",
|
||||
"NAME": os.getenv("SQL_DATABASE"),
|
||||
"USER": os.getenv("SQL_USER", "user"),
|
||||
"PASSWORD": os.getenv("SQL_PASSWORD", "password"),
|
||||
"HOST": os.getenv("SQL_HOST", "localhost"),
|
||||
"PORT": os.getenv("SQL_PORT", "5432"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,7 +162,7 @@ AUTH_USER_MODEL = "users.User"
|
||||
LANGUAGE_CODE = "en"
|
||||
LANGUAGES = (
|
||||
("en", "English"),
|
||||
# ("nl", "Nederlands"),
|
||||
("nl", "Nederlands"),
|
||||
("pt-br", "Português (Brasil)"),
|
||||
)
|
||||
|
||||
@@ -221,7 +220,7 @@ SESSION_COOKIE_SECURE = os.getenv("HTTPS_ENABLED", "false").lower() == "true"
|
||||
|
||||
DEBUG_TOOLBAR_CONFIG = {
|
||||
"ROOT_TAG_EXTRA_ATTRS": "hx-preserve",
|
||||
"SHOW_TOOLBAR_CALLBACK": lambda r: False, # disables it}
|
||||
# "SHOW_TOOLBAR_CALLBACK": lambda r: False, # disables it
|
||||
}
|
||||
DEBUG_TOOLBAR_PANELS = [
|
||||
"debug_toolbar.panels.history.HistoryPanel",
|
||||
@@ -387,3 +386,4 @@ PWA_SERVICE_WORKER_PATH = BASE_DIR / "templates" / "pwa" / "serviceworker.js"
|
||||
|
||||
ENABLE_SOFT_DELETE = os.getenv("ENABLE_SOFT_DELETE", "false").lower() == "true"
|
||||
KEEP_DELETED_TRANSACTIONS_FOR = int(os.getenv("KEEP_DELETED_ENTRIES_FOR", "365"))
|
||||
APP_VERSION = os.getenv("APP_VERSION", "unknown")
|
||||
|
||||
31
app/apps/common/functions/format.py
Normal file
31
app/apps/common/functions/format.py
Normal file
@@ -0,0 +1,31 @@
|
||||
from apps.common.middleware.thread_local import get_current_user
|
||||
from django.utils.formats import get_format as original_get_format
|
||||
|
||||
|
||||
def get_format(format_type=None, lang=None, use_l10n=None):
|
||||
user = get_current_user()
|
||||
|
||||
if user and user.is_authenticated and hasattr(user, "settings"):
|
||||
user_settings = user.settings
|
||||
if format_type == "THOUSAND_SEPARATOR":
|
||||
number_format = getattr(user_settings, "number_format", None)
|
||||
if number_format == "DC":
|
||||
return "."
|
||||
elif number_format == "CD":
|
||||
return ","
|
||||
elif format_type == "DECIMAL_SEPARATOR":
|
||||
number_format = getattr(user_settings, "number_format", None)
|
||||
if number_format == "DC":
|
||||
return ","
|
||||
elif number_format == "CD":
|
||||
return "."
|
||||
elif format_type == "SHORT_DATE_FORMAT":
|
||||
date_format = getattr(user_settings, "date_format", None)
|
||||
if date_format and date_format != "SHORT_DATE_FORMAT":
|
||||
return date_format
|
||||
elif format_type == "SHORT_DATETIME_FORMAT":
|
||||
datetime_format = getattr(user_settings, "datetime_format", None)
|
||||
if datetime_format and datetime_format != "SHORT_DATETIME_FORMAT":
|
||||
return datetime_format
|
||||
|
||||
return original_get_format(format_type, lang, use_l10n)
|
||||
@@ -1,14 +1,17 @@
|
||||
import zoneinfo
|
||||
|
||||
from django.utils import formats
|
||||
from django.utils import timezone, translation
|
||||
from django.utils.translation import activate
|
||||
from django.utils.functional import lazy
|
||||
|
||||
from apps.common.functions.format import get_format as custom_get_format
|
||||
from apps.users.models import UserSettings
|
||||
|
||||
|
||||
class LocalizationMiddleware:
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
self.patch_get_format()
|
||||
|
||||
def __call__(self, request):
|
||||
tz = request.COOKIES.get("mytz")
|
||||
@@ -33,9 +36,14 @@ class LocalizationMiddleware:
|
||||
timezone.activate(zoneinfo.ZoneInfo("UTC"))
|
||||
|
||||
if user_language and user_language != "auto":
|
||||
activate(user_language)
|
||||
translation.activate(user_language)
|
||||
else:
|
||||
detected_language = translation.get_language_from_request(request)
|
||||
activate(detected_language)
|
||||
translation.activate(detected_language)
|
||||
|
||||
return self.get_response(request)
|
||||
|
||||
@staticmethod
|
||||
def patch_get_format():
|
||||
formats.get_format = custom_get_format
|
||||
formats.get_format_lazy = lazy(custom_get_format, str, list, tuple)
|
||||
|
||||
73
app/apps/common/middleware/thread_local.py
Normal file
73
app/apps/common/middleware/thread_local.py
Normal file
@@ -0,0 +1,73 @@
|
||||
"""
|
||||
threadlocals middleware
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
make the request object everywhere available (e.g. in model instance).
|
||||
|
||||
based on: http://code.djangoproject.com/wiki/CookBookThreadlocalsAndUser
|
||||
|
||||
Put this into your settings:
|
||||
--------------------------------------------------------------------------
|
||||
MIDDLEWARE_CLASSES = (
|
||||
...
|
||||
'django_tools.middlewares.ThreadLocal.ThreadLocalMiddleware',
|
||||
...
|
||||
)
|
||||
--------------------------------------------------------------------------
|
||||
|
||||
|
||||
Usage:
|
||||
--------------------------------------------------------------------------
|
||||
from django_tools.middlewares import ThreadLocal
|
||||
|
||||
# Get the current request object:
|
||||
request = ThreadLocal.get_current_request()
|
||||
|
||||
# You can get the current user directly with:
|
||||
user = ThreadLocal.get_current_user()
|
||||
--------------------------------------------------------------------------
|
||||
|
||||
:copyleft: 2009-2017 by the django-tools team, see AUTHORS for more details.
|
||||
:license: GNU GPL v3 or above, see LICENSE for more details.
|
||||
"""
|
||||
|
||||
try:
|
||||
from threading import local
|
||||
except ImportError:
|
||||
from django.utils._threading_local import local
|
||||
|
||||
try:
|
||||
from django.utils.deprecation import MiddlewareMixin
|
||||
except ImportError:
|
||||
MiddlewareMixin = object # fallback for Django < 1.10
|
||||
|
||||
|
||||
_thread_locals = local()
|
||||
|
||||
|
||||
def get_current_request():
|
||||
"""returns the request object for this thread"""
|
||||
return getattr(_thread_locals, "request", None)
|
||||
|
||||
|
||||
def get_current_user():
|
||||
"""returns the current user, if exist, otherwise returns None"""
|
||||
request = get_current_request()
|
||||
if request:
|
||||
return getattr(request, "user", None)
|
||||
|
||||
|
||||
class ThreadLocalMiddleware(MiddlewareMixin):
|
||||
"""Simple middleware that adds the request object in thread local storage."""
|
||||
|
||||
def process_request(self, request):
|
||||
_thread_locals.request = request
|
||||
|
||||
def process_response(self, request, response):
|
||||
if hasattr(_thread_locals, "request"):
|
||||
del _thread_locals.request
|
||||
return response
|
||||
|
||||
def process_exception(self, request, exception):
|
||||
if hasattr(_thread_locals, "request"):
|
||||
del _thread_locals.request
|
||||
@@ -1,5 +1,8 @@
|
||||
import logging
|
||||
|
||||
from asgiref.sync import sync_to_async
|
||||
from django.core import management
|
||||
|
||||
from procrastinate import builtin_tasks
|
||||
from procrastinate.contrib.django import app
|
||||
|
||||
@@ -24,3 +27,16 @@ async def remove_old_jobs(context, timestamp):
|
||||
exc_info=True,
|
||||
)
|
||||
raise e
|
||||
|
||||
|
||||
@app.periodic(cron="0 6 1 * *")
|
||||
@app.task(queueing_lock="remove_expired_sessions")
|
||||
async def remove_expired_sessions(timestamp=None):
|
||||
"""Cleanup expired sessions by using Django management command."""
|
||||
try:
|
||||
await sync_to_async(management.call_command)("clearsessions", verbosity=0)
|
||||
except Exception:
|
||||
logger.error(
|
||||
"Error while executing 'remove_expired_sessions' task",
|
||||
exc_info=True,
|
||||
)
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
from django import template
|
||||
from django.template.defaultfilters import date as date_filter
|
||||
from django.utils import formats, timezone
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.filter
|
||||
def custom_date(value, user=None):
|
||||
if not value:
|
||||
return ""
|
||||
|
||||
# Determine if the value is a datetime or just a date
|
||||
is_datetime = hasattr(value, "hour")
|
||||
|
||||
# Convert to current timezone if it's a datetime
|
||||
if is_datetime and timezone.is_aware(value):
|
||||
value = timezone.localtime(value)
|
||||
|
||||
if user and user.is_authenticated:
|
||||
user_settings = user.settings
|
||||
|
||||
if is_datetime:
|
||||
format_setting = user_settings.datetime_format
|
||||
else:
|
||||
format_setting = user_settings.date_format
|
||||
|
||||
return formats.date_format(value, format_setting, use_l10n=True)
|
||||
|
||||
return date_filter(
|
||||
value, "SHORT_DATE_FORMAT" if not is_datetime else "SHORT_DATETIME_FORMAT"
|
||||
)
|
||||
@@ -1,6 +1,6 @@
|
||||
from django import template
|
||||
from django.utils.formats import get_format
|
||||
|
||||
from apps.common.functions.format import get_format
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
52
app/apps/common/templatetags/markdown.py
Normal file
52
app/apps/common/templatetags/markdown.py
Normal file
@@ -0,0 +1,52 @@
|
||||
from typing import Optional
|
||||
|
||||
import mistune
|
||||
from django import template
|
||||
from django.utils.safestring import mark_safe
|
||||
from mistune import HTMLRenderer, Markdown, BlockParser, InlineParser, safe_entity
|
||||
from mistune.plugins.formatting import strikethrough as plugin_strikethrough
|
||||
from mistune.plugins.url import url as plugin_url
|
||||
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
class CustomRenderer(HTMLRenderer):
|
||||
def link(self, text: str, url: str, title: Optional[str] = None) -> str:
|
||||
s = '<a rel="nofollow" target="_blank" href="' + self.safe_url(url) + '"'
|
||||
if title:
|
||||
s += ' title="' + safe_entity(title) + '"'
|
||||
return s + ">" + text + "</a>"
|
||||
|
||||
def paragraph(self, text: str) -> str:
|
||||
return text + "\n"
|
||||
|
||||
def softbreak(self) -> str:
|
||||
return "\n"
|
||||
|
||||
def blank_line(self) -> str:
|
||||
return "\n"
|
||||
|
||||
|
||||
block = BlockParser()
|
||||
block.rules = ["blank_line"]
|
||||
inline = InlineParser(hard_wrap=False)
|
||||
inline.rules = [
|
||||
"emphasis",
|
||||
"link",
|
||||
"auto_link",
|
||||
"auto_email",
|
||||
"linebreak",
|
||||
"softbreak",
|
||||
]
|
||||
markdown = Markdown(
|
||||
renderer=CustomRenderer(escape=False),
|
||||
block=block,
|
||||
inline=inline,
|
||||
plugins=[plugin_strikethrough, plugin_url],
|
||||
)
|
||||
|
||||
|
||||
@register.filter(name="limited_markdown")
|
||||
def limited_markdown(value):
|
||||
return mark_safe(markdown(value))
|
||||
9
app/apps/common/templatetags/settings.py
Normal file
9
app/apps/common/templatetags/settings.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from django import template
|
||||
from django.conf import settings
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.simple_tag(name="settings")
|
||||
def settings_value(name):
|
||||
return getattr(settings, name, "")
|
||||
@@ -13,4 +13,9 @@ urlpatterns = [
|
||||
views.month_year_picker,
|
||||
name="month_year_picker",
|
||||
),
|
||||
path(
|
||||
"cache/invalidate/",
|
||||
views.invalidate_cache,
|
||||
name="invalidate_cache",
|
||||
),
|
||||
]
|
||||
|
||||
@@ -35,7 +35,7 @@ def django_to_python_datetime(django_format):
|
||||
def django_to_airdatepicker_datetime(django_format):
|
||||
format_map = {
|
||||
# Time
|
||||
"h": "h", # Hour (12-hour)
|
||||
"h": "hh", # Hour (12-hour)
|
||||
"H": "H", # Hour (24-hour)
|
||||
"i": "m", # Minutes
|
||||
"A": "AA", # AM/PM uppercase
|
||||
@@ -76,7 +76,7 @@ def django_to_airdatepicker_datetime(django_format):
|
||||
def django_to_airdatepicker_datetime_separated(django_format):
|
||||
format_map = {
|
||||
# Time formats
|
||||
"h": "hH", # Hour (12-hour)
|
||||
"h": "hh", # Hour (12-hour)
|
||||
"H": "HH", # Hour (24-hour)
|
||||
"i": "mm", # Minutes
|
||||
"A": "AA", # AM/PM uppercase
|
||||
|
||||
@@ -1,17 +1,32 @@
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.db.models import Count
|
||||
from django.db.models.functions import ExtractYear, ExtractMonth
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import render
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.views.decorators.http import require_http_methods
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from cachalot.api import invalidate
|
||||
|
||||
from apps.common.decorators.htmx import only_htmx
|
||||
from apps.transactions.models import Transaction
|
||||
|
||||
|
||||
@only_htmx
|
||||
@login_required
|
||||
@require_http_methods(["GET"])
|
||||
def toasts(request):
|
||||
return render(request, "common/fragments/toasts.html")
|
||||
|
||||
|
||||
@only_htmx
|
||||
@login_required
|
||||
@require_http_methods(["GET"])
|
||||
def month_year_picker(request):
|
||||
field = request.GET.get("field", "reference_date")
|
||||
for_ = request.GET.get("for", None)
|
||||
@@ -84,3 +99,19 @@ def month_year_picker(request):
|
||||
"current_year": current_year,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@only_htmx
|
||||
@login_required
|
||||
@require_http_methods(["GET"])
|
||||
def invalidate_cache(request):
|
||||
invalidate()
|
||||
|
||||
messages.success(request, _("Cache cleared successfully"))
|
||||
|
||||
return HttpResponse(
|
||||
status=204,
|
||||
headers={
|
||||
"HX-Trigger": "updated",
|
||||
},
|
||||
)
|
||||
|
||||
@@ -2,7 +2,6 @@ import datetime
|
||||
|
||||
from django.forms import widgets
|
||||
from django.utils import formats, translation, dates
|
||||
from django.utils.formats import get_format
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from apps.common.utils.django import (
|
||||
@@ -10,6 +9,7 @@ from apps.common.utils.django import (
|
||||
django_to_airdatepicker_datetime,
|
||||
django_to_airdatepicker_datetime_separated,
|
||||
)
|
||||
from apps.common.functions.format import get_format
|
||||
|
||||
|
||||
class AirDatePickerInput(widgets.DateInput):
|
||||
@@ -19,12 +19,10 @@ class AirDatePickerInput(widgets.DateInput):
|
||||
format=None,
|
||||
clear_button=True,
|
||||
auto_close=True,
|
||||
user=None,
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
attrs = attrs or {}
|
||||
self.user = user
|
||||
super().__init__(attrs=attrs, format=format, *args, **kwargs)
|
||||
self.clear_button = clear_button
|
||||
self.auto_close = auto_close
|
||||
@@ -41,12 +39,6 @@ class AirDatePickerInput(widgets.DateInput):
|
||||
if self.format:
|
||||
return self.format
|
||||
|
||||
if self.user and hasattr(self.user, "settings"):
|
||||
user_format = self.user.settings.date_format
|
||||
if user_format == "SHORT_DATE_FORMAT":
|
||||
return get_format("SHORT_DATE_FORMAT", use_l10n=True)
|
||||
return user_format
|
||||
|
||||
return get_format("SHORT_DATE_FORMAT", use_l10n=True)
|
||||
|
||||
def build_attrs(self, base_attrs, extra_attrs=None):
|
||||
@@ -97,12 +89,10 @@ class AirDateTimePickerInput(widgets.DateTimeInput):
|
||||
timepicker=True,
|
||||
clear_button=True,
|
||||
auto_close=True,
|
||||
user=None,
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
attrs = attrs or {}
|
||||
self.user = user
|
||||
super().__init__(attrs=attrs, format=format, *args, **kwargs)
|
||||
self.timepicker = timepicker
|
||||
self.clear_button = clear_button
|
||||
@@ -120,12 +110,6 @@ class AirDateTimePickerInput(widgets.DateTimeInput):
|
||||
if self.format:
|
||||
return self.format
|
||||
|
||||
if self.user and hasattr(self.user, "settings"):
|
||||
user_format = self.user.settings.datetime_format
|
||||
if user_format == "SHORT_DATETIME_FORMAT":
|
||||
return get_format("SHORT_DATETIME_FORMAT", use_l10n=True)
|
||||
return user_format
|
||||
|
||||
return get_format("SHORT_DATETIME_FORMAT", use_l10n=True)
|
||||
|
||||
def build_attrs(self, base_attrs, extra_attrs=None):
|
||||
@@ -148,9 +132,14 @@ class AirDateTimePickerInput(widgets.DateTimeInput):
|
||||
|
||||
def format_value(self, value):
|
||||
"""Format the value for display in the widget."""
|
||||
if value:
|
||||
if value and isinstance(value, (datetime.date, datetime.datetime)):
|
||||
self.attrs["data-value"] = datetime.datetime.strftime(
|
||||
value, "%Y-%m-%d %H:%M:00"
|
||||
value, "%Y-%m-%dT%H:%M:00"
|
||||
)
|
||||
elif value and isinstance(value, str):
|
||||
value = datetime.datetime.strptime(value, "%Y-%m-%d %H:%M:00")
|
||||
self.attrs["data-value"] = datetime.datetime.strftime(
|
||||
value, "%Y-%m-%dT%H:%M:00"
|
||||
)
|
||||
|
||||
if value is None:
|
||||
@@ -195,6 +184,7 @@ class AirMonthYearPickerInput(AirDatePickerInput):
|
||||
|
||||
# Add data attributes for AirDatepicker configuration
|
||||
attrs["data-now-button-txt"] = _("Today")
|
||||
attrs["data-date-format"] = "MMMM yyyy"
|
||||
|
||||
return attrs
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
from decimal import Decimal, InvalidOperation
|
||||
|
||||
from django import forms
|
||||
from django.utils.formats import get_format, number_format
|
||||
from django.utils.formats import number_format
|
||||
|
||||
from apps.common.functions.format import get_format
|
||||
|
||||
|
||||
def convert_to_decimal(value: str):
|
||||
|
||||
@@ -72,7 +72,7 @@ class ExchangeRateForm(forms.ModelForm):
|
||||
model = ExchangeRate
|
||||
fields = ["from_currency", "to_currency", "rate", "date"]
|
||||
|
||||
def __init__(self, *args, user=None, **kwargs):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.helper = FormHelper()
|
||||
@@ -81,9 +81,7 @@ class ExchangeRateForm(forms.ModelForm):
|
||||
self.helper.layout = Layout("date", "from_currency", "to_currency", "rate")
|
||||
|
||||
self.fields["rate"].widget = ArbitraryDecimalDisplayNumberInput()
|
||||
self.fields["date"].widget = AirDateTimePickerInput(
|
||||
clear_button=False, user=user
|
||||
)
|
||||
self.fields["date"].widget = AirDateTimePickerInput(clear_button=False)
|
||||
|
||||
if self.instance and self.instance.pk:
|
||||
self.helper.layout.append(
|
||||
|
||||
@@ -72,7 +72,9 @@ class ExchangeRate(models.Model):
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
if self.from_currency == self.to_currency:
|
||||
raise ValidationError(
|
||||
{"to_currency": _("From and To currencies cannot be the same.")}
|
||||
)
|
||||
# Check if the attributes exist before comparing them
|
||||
if hasattr(self, "from_currency") and hasattr(self, "to_currency"):
|
||||
if self.from_currency == self.to_currency:
|
||||
raise ValidationError(
|
||||
{"to_currency": _("From and To currencies cannot be the same.")}
|
||||
)
|
||||
|
||||
@@ -83,7 +83,7 @@ def exchange_rates_list_pair(request):
|
||||
@require_http_methods(["GET", "POST"])
|
||||
def exchange_rate_add(request):
|
||||
if request.method == "POST":
|
||||
form = ExchangeRateForm(request.POST, user=request.user)
|
||||
form = ExchangeRateForm(request.POST)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
messages.success(request, _("Exchange rate added successfully"))
|
||||
@@ -95,7 +95,7 @@ def exchange_rate_add(request):
|
||||
},
|
||||
)
|
||||
else:
|
||||
form = ExchangeRateForm(user=request.user)
|
||||
form = ExchangeRateForm()
|
||||
|
||||
return render(
|
||||
request,
|
||||
@@ -111,7 +111,7 @@ def exchange_rate_edit(request, pk):
|
||||
exchange_rate = get_object_or_404(ExchangeRate, id=pk)
|
||||
|
||||
if request.method == "POST":
|
||||
form = ExchangeRateForm(request.POST, instance=exchange_rate, user=request.user)
|
||||
form = ExchangeRateForm(request.POST, instance=exchange_rate)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
messages.success(request, _("Exchange rate updated successfully"))
|
||||
@@ -123,7 +123,7 @@ def exchange_rate_edit(request, pk):
|
||||
},
|
||||
)
|
||||
else:
|
||||
form = ExchangeRateForm(instance=exchange_rate, user=request.user)
|
||||
form = ExchangeRateForm(instance=exchange_rate)
|
||||
|
||||
return render(
|
||||
request,
|
||||
|
||||
@@ -65,7 +65,7 @@ class DCAEntryForm(forms.ModelForm):
|
||||
"notes": forms.Textarea(attrs={"rows": 3}),
|
||||
}
|
||||
|
||||
def __init__(self, *args, user=None, **kwargs):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.helper = FormHelper()
|
||||
self.helper.form_tag = False
|
||||
@@ -106,4 +106,4 @@ class DCAEntryForm(forms.ModelForm):
|
||||
|
||||
self.fields["amount_paid"].widget = ArbitraryDecimalDisplayNumberInput()
|
||||
self.fields["amount_received"].widget = ArbitraryDecimalDisplayNumberInput()
|
||||
self.fields["date"].widget = AirDatePickerInput(clear_button=False, user=user)
|
||||
self.fields["date"].widget = AirDatePickerInput(clear_button=False)
|
||||
|
||||
@@ -155,7 +155,7 @@ def strategy_detail(request, strategy_id):
|
||||
def strategy_entry_add(request, strategy_id):
|
||||
strategy = get_object_or_404(DCAStrategy, id=strategy_id)
|
||||
if request.method == "POST":
|
||||
form = DCAEntryForm(request.POST, user=request.user)
|
||||
form = DCAEntryForm(request.POST)
|
||||
if form.is_valid():
|
||||
entry = form.save(commit=False)
|
||||
entry.strategy = strategy
|
||||
@@ -169,7 +169,7 @@ def strategy_entry_add(request, strategy_id):
|
||||
},
|
||||
)
|
||||
else:
|
||||
form = DCAEntryForm(user=request.user)
|
||||
form = DCAEntryForm()
|
||||
|
||||
return render(
|
||||
request,
|
||||
@@ -184,7 +184,7 @@ def strategy_entry_edit(request, strategy_id, entry_id):
|
||||
dca_entry = get_object_or_404(DCAEntry, id=entry_id, strategy__id=strategy_id)
|
||||
|
||||
if request.method == "POST":
|
||||
form = DCAEntryForm(request.POST, instance=dca_entry, user=request.user)
|
||||
form = DCAEntryForm(request.POST, instance=dca_entry)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
messages.success(request, _("Entry updated successfully"))
|
||||
@@ -196,7 +196,7 @@ def strategy_entry_edit(request, strategy_id, entry_id):
|
||||
},
|
||||
)
|
||||
else:
|
||||
form = DCAEntryForm(instance=dca_entry, user=request.user)
|
||||
form = DCAEntryForm(instance=dca_entry)
|
||||
|
||||
return render(
|
||||
request,
|
||||
|
||||
@@ -65,7 +65,7 @@ class CSVImportSettings(BaseModel):
|
||||
|
||||
|
||||
class ColumnMapping(BaseModel):
|
||||
source: Optional[str] = Field(
|
||||
source: Optional[str] | Optional[list[str]] = Field(
|
||||
default=None,
|
||||
description="CSV column header. If None, the field will be generated from transformations",
|
||||
)
|
||||
|
||||
@@ -486,11 +486,21 @@ class ImportService:
|
||||
mapped_data = {}
|
||||
|
||||
for field, mapping in self.mapping.items():
|
||||
# If source is None, use None as the initial value
|
||||
value = row.get(mapping.source) if mapping.source else None
|
||||
value = None
|
||||
|
||||
if isinstance(mapping.source, str):
|
||||
value = row.get(mapping.source, None)
|
||||
elif isinstance(mapping.source, list):
|
||||
for source in mapping.source:
|
||||
value = row.get(source, None)
|
||||
if value:
|
||||
break
|
||||
else:
|
||||
# If source is None, use None as the initial value
|
||||
value = None
|
||||
|
||||
# Use default_value if value is None
|
||||
if value is None:
|
||||
if not value:
|
||||
value = mapping.default
|
||||
|
||||
# Apply transformations
|
||||
|
||||
@@ -30,6 +30,8 @@ def index(request):
|
||||
@login_required
|
||||
@require_http_methods(["GET"])
|
||||
def monthly_overview(request, month: int, year: int):
|
||||
order = request.session.get("monthly_transactions_order", "default")
|
||||
|
||||
if month < 1 or month > 12:
|
||||
from django.http import Http404
|
||||
|
||||
@@ -41,7 +43,7 @@ def monthly_overview(request, month: int, year: int):
|
||||
previous_month = 12 if month == 1 else month - 1
|
||||
previous_year = year - 1 if previous_month == 12 and month == 1 else year
|
||||
|
||||
f = TransactionsFilter(request.GET, user=request.user)
|
||||
f = TransactionsFilter(request.GET)
|
||||
|
||||
return render(
|
||||
request,
|
||||
@@ -54,6 +56,7 @@ def monthly_overview(request, month: int, year: int):
|
||||
"previous_month": previous_month,
|
||||
"previous_year": previous_year,
|
||||
"filter": f,
|
||||
"order": order,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -62,9 +65,14 @@ def monthly_overview(request, month: int, year: int):
|
||||
@login_required
|
||||
@require_http_methods(["GET"])
|
||||
def transactions_list(request, month: int, year: int):
|
||||
order = request.GET.get("order")
|
||||
order = request.session.get("monthly_transactions_order", "default")
|
||||
|
||||
f = TransactionsFilter(request.GET, user=request.user)
|
||||
if "order" in request.GET:
|
||||
order = request.GET["order"]
|
||||
if order != request.session.get("monthly_transactions_order", "default"):
|
||||
request.session["monthly_transactions_order"] = order
|
||||
|
||||
f = TransactionsFilter(request.GET)
|
||||
transactions_filtered = (
|
||||
f.qs.filter()
|
||||
.filter(
|
||||
@@ -79,6 +87,7 @@ def transactions_list(request, month: int, year: int):
|
||||
"account__exchange_currency",
|
||||
"account__currency",
|
||||
"installment_plan",
|
||||
"entities",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -133,7 +133,7 @@ class TransactionsFilter(django_filters.FilterSet):
|
||||
"to_amount",
|
||||
]
|
||||
|
||||
def __init__(self, data=None, user=None, *args, **kwargs):
|
||||
def __init__(self, data=None, *args, **kwargs):
|
||||
# if filterset is bound, use initial values as defaults
|
||||
if data is not None:
|
||||
# get a mutable copy of the QueryDict
|
||||
@@ -182,5 +182,5 @@ class TransactionsFilter(django_filters.FilterSet):
|
||||
|
||||
self.form.fields["to_amount"].widget = ArbitraryDecimalDisplayNumberInput()
|
||||
self.form.fields["from_amount"].widget = ArbitraryDecimalDisplayNumberInput()
|
||||
self.form.fields["date_start"].widget = AirDatePickerInput(user=user)
|
||||
self.form.fields["date_end"].widget = AirDatePickerInput(user=user)
|
||||
self.form.fields["date_start"].widget = AirDatePickerInput()
|
||||
self.form.fields["date_end"].widget = AirDatePickerInput()
|
||||
|
||||
@@ -86,7 +86,7 @@ class TransactionForm(forms.ModelForm):
|
||||
"account": TomSelect(clear_button=False, group_by="group"),
|
||||
}
|
||||
|
||||
def __init__(self, *args, user=None, **kwargs):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# if editing a transaction display non-archived items and it's own item even if it's archived
|
||||
@@ -177,7 +177,7 @@ class TransactionForm(forms.ModelForm):
|
||||
)
|
||||
|
||||
self.fields["reference_date"].required = False
|
||||
self.fields["date"].widget = AirDatePickerInput(clear_button=False, user=user)
|
||||
self.fields["date"].widget = AirDatePickerInput(clear_button=False)
|
||||
|
||||
if self.instance and self.instance.pk:
|
||||
decimal_places = self.instance.account.currency.decimal_places
|
||||
@@ -333,7 +333,7 @@ class TransferForm(forms.Form):
|
||||
label=_("Notes"),
|
||||
)
|
||||
|
||||
def __init__(self, *args, user=None, **kwargs):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.helper = FormHelper()
|
||||
@@ -402,7 +402,7 @@ class TransferForm(forms.Form):
|
||||
|
||||
self.fields["from_amount"].widget = ArbitraryDecimalDisplayNumberInput()
|
||||
self.fields["to_amount"].widget = ArbitraryDecimalDisplayNumberInput()
|
||||
self.fields["date"].widget = AirDatePickerInput(clear_button=False, user=user)
|
||||
self.fields["date"].widget = AirDatePickerInput(clear_button=False)
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
@@ -515,7 +515,7 @@ class InstallmentPlanForm(forms.ModelForm):
|
||||
"notes": forms.Textarea(attrs={"rows": 3}),
|
||||
}
|
||||
|
||||
def __init__(self, *args, user=None, **kwargs):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# if editing display non-archived items and it's own item even if it's archived
|
||||
@@ -572,9 +572,7 @@ class InstallmentPlanForm(forms.ModelForm):
|
||||
)
|
||||
|
||||
self.fields["installment_amount"].widget = ArbitraryDecimalDisplayNumberInput()
|
||||
self.fields["start_date"].widget = AirDatePickerInput(
|
||||
clear_button=False, user=user
|
||||
)
|
||||
self.fields["start_date"].widget = AirDatePickerInput(clear_button=False)
|
||||
|
||||
if self.instance and self.instance.pk:
|
||||
self.helper.layout.append(
|
||||
@@ -762,7 +760,7 @@ class RecurringTransactionForm(forms.ModelForm):
|
||||
),
|
||||
}
|
||||
|
||||
def __init__(self, *args, user=None, **kwargs):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# if editing display non-archived items and it's own item even if it's archived
|
||||
@@ -819,10 +817,8 @@ class RecurringTransactionForm(forms.ModelForm):
|
||||
)
|
||||
|
||||
self.fields["amount"].widget = ArbitraryDecimalDisplayNumberInput()
|
||||
self.fields["start_date"].widget = AirDatePickerInput(
|
||||
clear_button=False, user=user
|
||||
)
|
||||
self.fields["end_date"].widget = AirDatePickerInput(user=user)
|
||||
self.fields["start_date"].widget = AirDatePickerInput(clear_button=False)
|
||||
self.fields["end_date"].widget = AirDatePickerInput()
|
||||
|
||||
if self.instance and self.instance.pk:
|
||||
self.helper.layout.append(
|
||||
|
||||
@@ -49,7 +49,7 @@ class SoftDeleteQuerySet(models.QuerySet):
|
||||
class SoftDeleteManager(models.Manager):
|
||||
def get_queryset(self):
|
||||
qs = SoftDeleteQuerySet(self.model, using=self._db)
|
||||
return qs if not settings.ENABLE_SOFT_DELETE else qs.filter(deleted=False)
|
||||
return qs.filter(deleted=False)
|
||||
|
||||
|
||||
class AllObjectsManager(models.Manager):
|
||||
@@ -60,7 +60,7 @@ class AllObjectsManager(models.Manager):
|
||||
class DeletedObjectsManager(models.Manager):
|
||||
def get_queryset(self):
|
||||
qs = SoftDeleteQuerySet(self.model, using=self._db)
|
||||
return qs if not settings.ENABLE_SOFT_DELETE else qs.filter(deleted=True)
|
||||
return qs.filter(deleted=True)
|
||||
|
||||
|
||||
class TransactionCategory(models.Model):
|
||||
|
||||
@@ -27,7 +27,7 @@ def generate_recurring_transactions(timestamp=None):
|
||||
|
||||
@app.periodic(cron="10 1 * * *")
|
||||
@app.task
|
||||
def cleanup_deleted_transactions():
|
||||
def cleanup_deleted_transactions(timestamp=None):
|
||||
with cachalot_disabled():
|
||||
if settings.ENABLE_SOFT_DELETE and settings.KEEP_DELETED_TRANSACTIONS_FOR == 0:
|
||||
return "KEEP_DELETED_TRANSACTIONS_FOR is 0, no cleanup performed."
|
||||
@@ -44,7 +44,7 @@ def cleanup_deleted_transactions():
|
||||
days=settings.KEEP_DELETED_TRANSACTIONS_FOR
|
||||
)
|
||||
|
||||
invalidate("transactions.Transaction")
|
||||
invalidate()
|
||||
|
||||
# Hard delete soft-deleted transactions older than the cutoff date
|
||||
old_transactions = Transaction.deleted_objects.filter(deleted_at__lt=cutoff_date)
|
||||
|
||||
@@ -81,7 +81,7 @@ def installment_plan_transactions(request, installment_plan_id):
|
||||
@require_http_methods(["GET", "POST"])
|
||||
def installment_plan_add(request):
|
||||
if request.method == "POST":
|
||||
form = InstallmentPlanForm(request.POST, user=request.user)
|
||||
form = InstallmentPlanForm(request.POST)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
messages.success(request, _("Installment Plan added successfully"))
|
||||
@@ -93,7 +93,7 @@ def installment_plan_add(request):
|
||||
},
|
||||
)
|
||||
else:
|
||||
form = InstallmentPlanForm(user=request.user)
|
||||
form = InstallmentPlanForm()
|
||||
|
||||
return render(
|
||||
request,
|
||||
@@ -109,9 +109,7 @@ def installment_plan_edit(request, installment_plan_id):
|
||||
installment_plan = get_object_or_404(InstallmentPlan, id=installment_plan_id)
|
||||
|
||||
if request.method == "POST":
|
||||
form = InstallmentPlanForm(
|
||||
request.POST, instance=installment_plan, user=request.user
|
||||
)
|
||||
form = InstallmentPlanForm(request.POST, instance=installment_plan)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
messages.success(request, _("Installment Plan updated successfully"))
|
||||
@@ -123,7 +121,7 @@ def installment_plan_edit(request, installment_plan_id):
|
||||
},
|
||||
)
|
||||
else:
|
||||
form = InstallmentPlanForm(instance=installment_plan, user=request.user)
|
||||
form = InstallmentPlanForm(instance=installment_plan)
|
||||
|
||||
return render(
|
||||
request,
|
||||
|
||||
@@ -106,7 +106,7 @@ def recurring_transaction_transactions(request, recurring_transaction_id):
|
||||
@require_http_methods(["GET", "POST"])
|
||||
def recurring_transaction_add(request):
|
||||
if request.method == "POST":
|
||||
form = RecurringTransactionForm(request.POST, user=request.user)
|
||||
form = RecurringTransactionForm(request.POST)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
messages.success(request, _("Recurring Transaction added successfully"))
|
||||
@@ -118,7 +118,7 @@ def recurring_transaction_add(request):
|
||||
},
|
||||
)
|
||||
else:
|
||||
form = RecurringTransactionForm(user=request.user)
|
||||
form = RecurringTransactionForm()
|
||||
|
||||
return render(
|
||||
request,
|
||||
@@ -136,9 +136,7 @@ def recurring_transaction_edit(request, recurring_transaction_id):
|
||||
)
|
||||
|
||||
if request.method == "POST":
|
||||
form = RecurringTransactionForm(
|
||||
request.POST, instance=recurring_transaction, user=request.user
|
||||
)
|
||||
form = RecurringTransactionForm(request.POST, instance=recurring_transaction)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
messages.success(request, _("Recurring Transaction updated successfully"))
|
||||
@@ -150,9 +148,7 @@ def recurring_transaction_edit(request, recurring_transaction_id):
|
||||
},
|
||||
)
|
||||
else:
|
||||
form = RecurringTransactionForm(
|
||||
instance=recurring_transaction, user=request.user
|
||||
)
|
||||
form = RecurringTransactionForm(instance=recurring_transaction)
|
||||
|
||||
return render(
|
||||
request,
|
||||
|
||||
@@ -44,7 +44,7 @@ def transaction_add(request):
|
||||
).date()
|
||||
|
||||
if request.method == "POST":
|
||||
form = TransactionForm(request.POST, user=request.user)
|
||||
form = TransactionForm(request.POST)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
messages.success(request, _("Transaction added successfully"))
|
||||
@@ -55,7 +55,6 @@ def transaction_add(request):
|
||||
)
|
||||
else:
|
||||
form = TransactionForm(
|
||||
user=request.user,
|
||||
initial={
|
||||
"date": expected_date,
|
||||
"type": transaction_type,
|
||||
@@ -84,13 +83,12 @@ def transaction_simple_add(request):
|
||||
).date()
|
||||
|
||||
if request.method == "POST":
|
||||
form = TransactionForm(request.POST, user=request.user)
|
||||
form = TransactionForm(request.POST)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
messages.success(request, _("Transaction added successfully"))
|
||||
|
||||
form = TransactionForm(
|
||||
user=request.user,
|
||||
initial={
|
||||
"date": expected_date,
|
||||
"type": transaction_type,
|
||||
@@ -99,7 +97,6 @@ def transaction_simple_add(request):
|
||||
|
||||
else:
|
||||
form = TransactionForm(
|
||||
user=request.user,
|
||||
initial={
|
||||
"date": expected_date,
|
||||
"type": transaction_type,
|
||||
@@ -120,7 +117,7 @@ def transaction_edit(request, transaction_id, **kwargs):
|
||||
transaction = get_object_or_404(Transaction, id=transaction_id)
|
||||
|
||||
if request.method == "POST":
|
||||
form = TransactionForm(request.POST, user=request.user, instance=transaction)
|
||||
form = TransactionForm(request.POST, instance=transaction)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
messages.success(request, _("Transaction updated successfully"))
|
||||
@@ -130,7 +127,7 @@ def transaction_edit(request, transaction_id, **kwargs):
|
||||
headers={"HX-Trigger": "updated, hide_offcanvas"},
|
||||
)
|
||||
else:
|
||||
form = TransactionForm(instance=transaction, user=request.user)
|
||||
form = TransactionForm(instance=transaction)
|
||||
|
||||
return render(
|
||||
request,
|
||||
@@ -152,7 +149,7 @@ def transactions_bulk_edit(request):
|
||||
count = transactions.count()
|
||||
|
||||
if request.method == "POST":
|
||||
form = BulkEditTransactionForm(request.POST, user=request.user)
|
||||
form = BulkEditTransactionForm(request.POST)
|
||||
if form.is_valid():
|
||||
# Apply changes from the form to all selected transactions
|
||||
for transaction in transactions:
|
||||
@@ -184,9 +181,7 @@ def transactions_bulk_edit(request):
|
||||
headers={"HX-Trigger": "updated, hide_offcanvas"},
|
||||
)
|
||||
else:
|
||||
form = BulkEditTransactionForm(
|
||||
initial={"is_paid": None, "type": None}, user=request.user
|
||||
)
|
||||
form = BulkEditTransactionForm(initial={"is_paid": None, "type": None})
|
||||
|
||||
context = {
|
||||
"form": form,
|
||||
@@ -276,7 +271,7 @@ def transactions_transfer(request):
|
||||
).date()
|
||||
|
||||
if request.method == "POST":
|
||||
form = TransferForm(request.POST, user=request.user)
|
||||
form = TransferForm(request.POST)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
messages.success(request, _("Transfer added successfully"))
|
||||
@@ -290,7 +285,6 @@ def transactions_transfer(request):
|
||||
"reference_date": expected_date,
|
||||
"date": expected_date,
|
||||
},
|
||||
user=request.user,
|
||||
)
|
||||
|
||||
return render(request, "transactions/fragments/transfer.html", {"form": form})
|
||||
@@ -319,15 +313,23 @@ def transaction_pay(request, transaction_id):
|
||||
@login_required
|
||||
@require_http_methods(["GET"])
|
||||
def transaction_all_index(request):
|
||||
f = TransactionsFilter(request.GET, user=request.user)
|
||||
return render(request, "transactions/pages/transactions.html", {"filter": f})
|
||||
order = request.session.get("all_transactions_order", "default")
|
||||
f = TransactionsFilter(request.GET)
|
||||
return render(
|
||||
request, "transactions/pages/transactions.html", {"filter": f, "order": order}
|
||||
)
|
||||
|
||||
|
||||
@only_htmx
|
||||
@login_required
|
||||
@require_http_methods(["GET"])
|
||||
def transaction_all_list(request):
|
||||
order = request.GET.get("order")
|
||||
order = request.session.get("all_transactions_order", "default")
|
||||
|
||||
if "order" in request.GET:
|
||||
order = request.GET["order"]
|
||||
if order != request.session.get("all_transactions_order", "default"):
|
||||
request.session["all_transactions_order"] = order
|
||||
|
||||
transactions = Transaction.objects.prefetch_related(
|
||||
"account",
|
||||
@@ -337,11 +339,12 @@ def transaction_all_list(request):
|
||||
"account__exchange_currency",
|
||||
"account__currency",
|
||||
"installment_plan",
|
||||
"entities",
|
||||
).all()
|
||||
|
||||
transactions = default_order(transactions, order=order)
|
||||
|
||||
f = TransactionsFilter(request.GET, user=request.user, queryset=transactions)
|
||||
f = TransactionsFilter(request.GET, queryset=transactions)
|
||||
|
||||
page_number = request.GET.get("page", 1)
|
||||
paginator = Paginator(f.qs, 100)
|
||||
@@ -371,7 +374,7 @@ def transaction_all_summary(request):
|
||||
"installment_plan",
|
||||
).all()
|
||||
|
||||
f = TransactionsFilter(request.GET, user=request.user, queryset=transactions)
|
||||
f = TransactionsFilter(request.GET, queryset=transactions)
|
||||
|
||||
currency_data = calculate_currency_totals(f.qs.all(), ignore_empty=True)
|
||||
currency_percentages = calculate_percentage_distribution(currency_data)
|
||||
|
||||
@@ -81,6 +81,12 @@ class UserSettingsForm(forms.ModelForm):
|
||||
("Y.m.d h:i A", "2025.01.20 03:30 PM"),
|
||||
]
|
||||
|
||||
NUMBER_FORMAT_CHOICES = [
|
||||
("AA", _("Default")),
|
||||
("DC", "1.234,50"),
|
||||
("CD", "1,234.50"),
|
||||
]
|
||||
|
||||
date_format = forms.ChoiceField(
|
||||
choices=DATE_FORMAT_CHOICES, initial="SHORT_DATE_FORMAT", label=_("Date Format")
|
||||
)
|
||||
@@ -90,6 +96,12 @@ class UserSettingsForm(forms.ModelForm):
|
||||
label=_("Datetime Format"),
|
||||
)
|
||||
|
||||
number_format = forms.ChoiceField(
|
||||
choices=NUMBER_FORMAT_CHOICES,
|
||||
initial="AA",
|
||||
label=_("Number Format"),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = UserSettings
|
||||
fields = [
|
||||
@@ -98,6 +110,7 @@ class UserSettingsForm(forms.ModelForm):
|
||||
"start_page",
|
||||
"date_format",
|
||||
"datetime_format",
|
||||
"number_format",
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -111,6 +124,7 @@ class UserSettingsForm(forms.ModelForm):
|
||||
"timezone",
|
||||
"date_format",
|
||||
"datetime_format",
|
||||
"number_format",
|
||||
"start_page",
|
||||
FormActions(
|
||||
NoClassSubmit(
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.1.5 on 2025-01-25 18:55
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('users', '0015_alter_usersettings_language'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='usersettings',
|
||||
name='language',
|
||||
field=models.CharField(choices=[('auto', 'Auto'), ('en', 'English'), ('nl', 'Nederlands'), ('pt-br', 'Português (Brasil)')], default='auto', max_length=10, verbose_name='Language'),
|
||||
),
|
||||
]
|
||||
18
app/apps/users/migrations/0017_usersettings_number_format.py
Normal file
18
app/apps/users/migrations/0017_usersettings_number_format.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.1.5 on 2025-01-27 12:32
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('users', '0016_alter_usersettings_language'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='usersettings',
|
||||
name='number_format',
|
||||
field=models.CharField(default='AA', max_length=2, verbose_name='Number Format'),
|
||||
),
|
||||
]
|
||||
@@ -1,8 +1,8 @@
|
||||
import pytz
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import AbstractUser, Group
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from apps.users.managers import UserManager
|
||||
@@ -44,6 +44,9 @@ class UserSettings(models.Model):
|
||||
default="SHORT_DATETIME_FORMAT",
|
||||
verbose_name=_("Datetime Format"),
|
||||
)
|
||||
number_format = models.CharField(
|
||||
max_length=2, default="AA", verbose_name=_("Number Format")
|
||||
)
|
||||
|
||||
language = models.CharField(
|
||||
max_length=10,
|
||||
@@ -66,3 +69,6 @@ class UserSettings(models.Model):
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.user.email}'s settings"
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
2319
app/locale/de/LC_MESSAGES/django.po
Normal file
2319
app/locale/de/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -8,9 +8,9 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-01-25 16:51+0000\n"
|
||||
"PO-Revision-Date: 2025-01-25 13:53-0300\n"
|
||||
"Last-Translator: \n"
|
||||
"POT-Creation-Date: 2025-01-28 00:49+0000\n"
|
||||
"PO-Revision-Date: 2025-01-27 21:49-0300\n"
|
||||
"Last-Translator: Herculino Trotta\n"
|
||||
"Language-Team: \n"
|
||||
"Language: pt_BR\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
@@ -24,28 +24,28 @@ msgid "Group name"
|
||||
msgstr "Nome do grupo"
|
||||
|
||||
#: apps/accounts/forms.py:40 apps/accounts/forms.py:96
|
||||
#: apps/currencies/forms.py:52 apps/currencies/forms.py:92 apps/dca/forms.py:41
|
||||
#: apps/currencies/forms.py:52 apps/currencies/forms.py:90 apps/dca/forms.py:41
|
||||
#: apps/dca/forms.py:93 apps/import_app/forms.py:34 apps/rules/forms.py:45
|
||||
#: apps/rules/forms.py:87 apps/transactions/forms.py:190
|
||||
#: apps/transactions/forms.py:257 apps/transactions/forms.py:583
|
||||
#: apps/transactions/forms.py:626 apps/transactions/forms.py:658
|
||||
#: apps/transactions/forms.py:693 apps/transactions/forms.py:831
|
||||
#: apps/transactions/forms.py:257 apps/transactions/forms.py:581
|
||||
#: apps/transactions/forms.py:624 apps/transactions/forms.py:656
|
||||
#: apps/transactions/forms.py:691 apps/transactions/forms.py:827
|
||||
msgid "Update"
|
||||
msgstr "Atualizar"
|
||||
|
||||
#: apps/accounts/forms.py:48 apps/accounts/forms.py:104
|
||||
#: apps/common/widgets/tom_select.py:12 apps/currencies/forms.py:60
|
||||
#: apps/currencies/forms.py:100 apps/dca/forms.py:49 apps/dca/forms.py:102
|
||||
#: apps/currencies/forms.py:98 apps/dca/forms.py:49 apps/dca/forms.py:102
|
||||
#: apps/import_app/forms.py:42 apps/rules/forms.py:53 apps/rules/forms.py:95
|
||||
#: apps/transactions/forms.py:174 apps/transactions/forms.py:199
|
||||
#: apps/transactions/forms.py:591 apps/transactions/forms.py:634
|
||||
#: apps/transactions/forms.py:666 apps/transactions/forms.py:701
|
||||
#: apps/transactions/forms.py:839
|
||||
#: apps/transactions/forms.py:589 apps/transactions/forms.py:632
|
||||
#: apps/transactions/forms.py:664 apps/transactions/forms.py:699
|
||||
#: apps/transactions/forms.py:835
|
||||
#: templates/account_groups/fragments/list.html:9
|
||||
#: templates/accounts/fragments/list.html:9
|
||||
#: templates/categories/fragments/list.html:9
|
||||
#: templates/currencies/fragments/list.html:9
|
||||
#: templates/dca/fragments/strategy/details.html:38
|
||||
#: templates/dca/fragments/strategy/details.html:37
|
||||
#: templates/dca/fragments/strategy/list.html:9
|
||||
#: templates/entities/fragments/list.html:9
|
||||
#: templates/exchange_rates/fragments/list.html:10
|
||||
@@ -69,7 +69,7 @@ msgstr "Novo saldo"
|
||||
#: apps/accounts/forms.py:119 apps/rules/models.py:27
|
||||
#: apps/transactions/forms.py:39 apps/transactions/forms.py:291
|
||||
#: apps/transactions/forms.py:298 apps/transactions/forms.py:478
|
||||
#: apps/transactions/forms.py:725 apps/transactions/models.py:159
|
||||
#: apps/transactions/forms.py:723 apps/transactions/models.py:159
|
||||
#: apps/transactions/models.py:311 apps/transactions/models.py:491
|
||||
msgid "Category"
|
||||
msgstr "Categoria"
|
||||
@@ -77,7 +77,7 @@ msgstr "Categoria"
|
||||
#: apps/accounts/forms.py:126 apps/rules/models.py:28
|
||||
#: apps/transactions/filters.py:74 apps/transactions/forms.py:47
|
||||
#: apps/transactions/forms.py:307 apps/transactions/forms.py:315
|
||||
#: apps/transactions/forms.py:471 apps/transactions/forms.py:718
|
||||
#: apps/transactions/forms.py:471 apps/transactions/forms.py:716
|
||||
#: apps/transactions/models.py:165 apps/transactions/models.py:313
|
||||
#: apps/transactions/models.py:495 templates/includes/navbar.html:98
|
||||
#: templates/tags/fragments/list.html:5 templates/tags/pages/index.html:4
|
||||
@@ -150,7 +150,7 @@ msgstr ""
|
||||
|
||||
#: apps/accounts/models.py:59 apps/rules/models.py:19
|
||||
#: apps/transactions/forms.py:59 apps/transactions/forms.py:463
|
||||
#: apps/transactions/forms.py:710 apps/transactions/models.py:132
|
||||
#: apps/transactions/forms.py:708 apps/transactions/models.py:132
|
||||
#: apps/transactions/models.py:271 apps/transactions/models.py:473
|
||||
msgid "Account"
|
||||
msgstr "Conta"
|
||||
@@ -320,11 +320,15 @@ msgstr "Erro"
|
||||
msgid "Info"
|
||||
msgstr "Informação"
|
||||
|
||||
#: apps/common/widgets/datepicker.py:55 apps/common/widgets/datepicker.py:197
|
||||
#: apps/common/views.py:110
|
||||
msgid "Cache cleared successfully"
|
||||
msgstr "Cache limpo com sucesso"
|
||||
|
||||
#: apps/common/widgets/datepicker.py:47 apps/common/widgets/datepicker.py:186
|
||||
msgid "Today"
|
||||
msgstr "Hoje"
|
||||
|
||||
#: apps/common/widgets/datepicker.py:139
|
||||
#: apps/common/widgets/datepicker.py:123
|
||||
msgid "Now"
|
||||
msgstr "Agora"
|
||||
|
||||
@@ -354,8 +358,8 @@ msgstr "Sufixo"
|
||||
#: apps/currencies/forms.py:68 apps/dca/models.py:156 apps/rules/models.py:22
|
||||
#: apps/transactions/forms.py:63 apps/transactions/forms.py:319
|
||||
#: apps/transactions/models.py:142
|
||||
#: templates/dca/fragments/strategy/details.html:53
|
||||
#: templates/exchange_rates/fragments/table.html:11
|
||||
#: templates/dca/fragments/strategy/details.html:52
|
||||
#: templates/exchange_rates/fragments/table.html:10
|
||||
msgid "Date"
|
||||
msgstr "Data"
|
||||
|
||||
@@ -405,7 +409,7 @@ msgstr "Data e Tempo"
|
||||
msgid "Exchange Rates"
|
||||
msgstr "Taxas de Câmbio"
|
||||
|
||||
#: apps/currencies/models.py:77
|
||||
#: apps/currencies/models.py:79
|
||||
msgid "From and To currencies cannot be the same."
|
||||
msgstr "As moedas De e Para não podem ser as mesmas."
|
||||
|
||||
@@ -459,11 +463,11 @@ msgstr "Estratégias CMP"
|
||||
msgid "Strategy"
|
||||
msgstr "Estratégia"
|
||||
|
||||
#: apps/dca/models.py:158 templates/dca/fragments/strategy/details.html:55
|
||||
#: apps/dca/models.py:158 templates/dca/fragments/strategy/details.html:54
|
||||
msgid "Amount Paid"
|
||||
msgstr "Quantia paga"
|
||||
|
||||
#: apps/dca/models.py:161 templates/dca/fragments/strategy/details.html:54
|
||||
#: apps/dca/models.py:161 templates/dca/fragments/strategy/details.html:53
|
||||
msgid "Amount Received"
|
||||
msgstr "Quantia recebida"
|
||||
|
||||
@@ -641,7 +645,7 @@ msgstr "Quantia"
|
||||
|
||||
#: apps/rules/models.py:29 apps/transactions/filters.py:81
|
||||
#: apps/transactions/forms.py:55 apps/transactions/forms.py:486
|
||||
#: apps/transactions/forms.py:733 apps/transactions/models.py:117
|
||||
#: apps/transactions/forms.py:731 apps/transactions/models.py:117
|
||||
#: apps/transactions/models.py:170 apps/transactions/models.py:316
|
||||
#: apps/transactions/models.py:498 templates/entities/fragments/list.html:5
|
||||
#: templates/entities/pages/index.html:4 templates/includes/navbar.html:100
|
||||
@@ -759,23 +763,23 @@ msgstr "Transferir"
|
||||
msgid "From and To accounts must be different."
|
||||
msgstr "As contas De e Para devem ser diferentes."
|
||||
|
||||
#: apps/transactions/forms.py:612
|
||||
#: apps/transactions/forms.py:610
|
||||
msgid "Tag name"
|
||||
msgstr "Nome da Tag"
|
||||
|
||||
#: apps/transactions/forms.py:644
|
||||
#: apps/transactions/forms.py:642
|
||||
msgid "Entity name"
|
||||
msgstr "Nome da entidade"
|
||||
|
||||
#: apps/transactions/forms.py:676
|
||||
#: apps/transactions/forms.py:674
|
||||
msgid "Category name"
|
||||
msgstr "Nome da Categoria"
|
||||
|
||||
#: apps/transactions/forms.py:678
|
||||
#: apps/transactions/forms.py:676
|
||||
msgid "Muted categories won't count towards your monthly total"
|
||||
msgstr "As categorias silenciadas não serão contabilizadas em seu total mensal"
|
||||
|
||||
#: apps/transactions/forms.py:850
|
||||
#: apps/transactions/forms.py:846
|
||||
msgid "End date should be after the start date"
|
||||
msgstr "Data final deve ser após data inicial"
|
||||
|
||||
@@ -1043,15 +1047,15 @@ msgstr "Entidade apagada com sucesso"
|
||||
msgid "Installment Plan added successfully"
|
||||
msgstr "Parcelamento adicionado com sucesso"
|
||||
|
||||
#: apps/transactions/views/installment_plans.py:117
|
||||
#: apps/transactions/views/installment_plans.py:115
|
||||
msgid "Installment Plan updated successfully"
|
||||
msgstr "Parcelamento atualizado com sucesso"
|
||||
|
||||
#: apps/transactions/views/installment_plans.py:142
|
||||
#: apps/transactions/views/installment_plans.py:140
|
||||
msgid "Installment Plan refreshed successfully"
|
||||
msgstr "Parcelamento atualizado com sucesso"
|
||||
|
||||
#: apps/transactions/views/installment_plans.py:160
|
||||
#: apps/transactions/views/installment_plans.py:158
|
||||
msgid "Installment Plan deleted successfully"
|
||||
msgstr "Parcelamento apagado com sucesso"
|
||||
|
||||
@@ -1059,23 +1063,23 @@ msgstr "Parcelamento apagado com sucesso"
|
||||
msgid "Recurring Transaction added successfully"
|
||||
msgstr "Transação Recorrente adicionada com sucesso"
|
||||
|
||||
#: apps/transactions/views/recurring_transactions.py:144
|
||||
#: apps/transactions/views/recurring_transactions.py:142
|
||||
msgid "Recurring Transaction updated successfully"
|
||||
msgstr "Transação Recorrente atualizada com sucesso"
|
||||
|
||||
#: apps/transactions/views/recurring_transactions.py:190
|
||||
#: apps/transactions/views/recurring_transactions.py:186
|
||||
msgid "Recurring transaction unpaused successfully"
|
||||
msgstr "Transação Recorrente despausada com sucesso"
|
||||
|
||||
#: apps/transactions/views/recurring_transactions.py:193
|
||||
#: apps/transactions/views/recurring_transactions.py:189
|
||||
msgid "Recurring transaction paused successfully"
|
||||
msgstr "Transação Recorrente pausada com sucesso"
|
||||
|
||||
#: apps/transactions/views/recurring_transactions.py:219
|
||||
#: apps/transactions/views/recurring_transactions.py:215
|
||||
msgid "Recurring transaction finished successfully"
|
||||
msgstr "Transação Recorrente finalizada com sucesso"
|
||||
|
||||
#: apps/transactions/views/recurring_transactions.py:239
|
||||
#: apps/transactions/views/recurring_transactions.py:235
|
||||
msgid "Recurring Transaction deleted successfully"
|
||||
msgstr "Transação Recorrente apagada com sucesso"
|
||||
|
||||
@@ -1092,30 +1096,30 @@ msgid "Tag deleted successfully"
|
||||
msgstr "Tag apagada com sucesso"
|
||||
|
||||
#: apps/transactions/views/transactions.py:50
|
||||
#: apps/transactions/views/transactions.py:90
|
||||
#: apps/transactions/views/transactions.py:89
|
||||
msgid "Transaction added successfully"
|
||||
msgstr "Transação adicionada com sucesso"
|
||||
|
||||
#: apps/transactions/views/transactions.py:126
|
||||
#: apps/transactions/views/transactions.py:123
|
||||
msgid "Transaction updated successfully"
|
||||
msgstr "Transação atualizada com sucesso"
|
||||
|
||||
#: apps/transactions/views/transactions.py:176
|
||||
#: apps/transactions/views/transactions.py:173
|
||||
#, python-format
|
||||
msgid "%(count)s transaction updated successfully"
|
||||
msgid_plural "%(count)s transactions updated successfully"
|
||||
msgstr[0] "%(count)s transação atualizada com sucesso"
|
||||
msgstr[1] "%(count)s transações atualizadas com sucesso"
|
||||
|
||||
#: apps/transactions/views/transactions.py:214
|
||||
#: apps/transactions/views/transactions.py:209
|
||||
msgid "Transaction duplicated successfully"
|
||||
msgstr "Transação duplicada com sucesso"
|
||||
|
||||
#: apps/transactions/views/transactions.py:256
|
||||
#: apps/transactions/views/transactions.py:251
|
||||
msgid "Transaction deleted successfully"
|
||||
msgstr "Transação apagada com sucesso"
|
||||
|
||||
#: apps/transactions/views/transactions.py:282
|
||||
#: apps/transactions/views/transactions.py:277
|
||||
msgid "Transfer added successfully"
|
||||
msgstr "Transferência adicionada com sucesso"
|
||||
|
||||
@@ -1155,21 +1159,25 @@ msgstr "E-mail ou senha inválidos"
|
||||
msgid "This account is deactivated"
|
||||
msgstr "Essa conta está desativada"
|
||||
|
||||
#: apps/users/forms.py:50 apps/users/forms.py:63
|
||||
#: apps/users/forms.py:50 apps/users/forms.py:63 apps/users/forms.py:85
|
||||
#: templates/monthly_overview/pages/overview.html:116
|
||||
#: templates/transactions/pages/transactions.html:35
|
||||
msgid "Default"
|
||||
msgstr "Padrão"
|
||||
|
||||
#: apps/users/forms.py:85 apps/users/models.py:40
|
||||
#: apps/users/forms.py:91 apps/users/models.py:40
|
||||
msgid "Date Format"
|
||||
msgstr "Formato de Data"
|
||||
|
||||
#: apps/users/forms.py:90 apps/users/models.py:45
|
||||
#: apps/users/forms.py:96 apps/users/models.py:45
|
||||
msgid "Datetime Format"
|
||||
msgstr "Formato de Data e Hora"
|
||||
|
||||
#: apps/users/forms.py:117
|
||||
#: apps/users/forms.py:102 apps/users/models.py:48
|
||||
msgid "Number Format"
|
||||
msgstr "Formato de Número"
|
||||
|
||||
#: apps/users/forms.py:131
|
||||
msgid "Save"
|
||||
msgstr "Salvar"
|
||||
|
||||
@@ -1193,19 +1201,19 @@ msgstr "Todas as transações"
|
||||
msgid "Calendar"
|
||||
msgstr "Calendário"
|
||||
|
||||
#: apps/users/models.py:50 apps/users/models.py:56
|
||||
#: apps/users/models.py:53 apps/users/models.py:59
|
||||
msgid "Auto"
|
||||
msgstr "Automático"
|
||||
|
||||
#: apps/users/models.py:52
|
||||
#: apps/users/models.py:55
|
||||
msgid "Language"
|
||||
msgstr "Linguagem"
|
||||
|
||||
#: apps/users/models.py:58
|
||||
#: apps/users/models.py:61
|
||||
msgid "Time Zone"
|
||||
msgstr "Fuso horário"
|
||||
|
||||
#: apps/users/models.py:64
|
||||
#: apps/users/models.py:67
|
||||
msgid "Start page"
|
||||
msgstr "Página inicial"
|
||||
|
||||
@@ -1241,9 +1249,9 @@ msgstr "Editar grupo de conta"
|
||||
#: templates/accounts/fragments/list.html:37
|
||||
#: templates/categories/fragments/table.html:24
|
||||
#: templates/currencies/fragments/list.html:33
|
||||
#: templates/dca/fragments/strategy/details.html:64
|
||||
#: templates/dca/fragments/strategy/details.html:63
|
||||
#: templates/entities/fragments/table.html:23
|
||||
#: templates/exchange_rates/fragments/table.html:20
|
||||
#: templates/exchange_rates/fragments/table.html:19
|
||||
#: templates/import_app/fragments/profiles/list.html:44
|
||||
#: templates/installment_plans/fragments/table.html:23
|
||||
#: templates/recurring_transactions/fragments/table.html:25
|
||||
@@ -1255,13 +1263,13 @@ msgstr "Ações"
|
||||
#: templates/account_groups/fragments/list.html:36
|
||||
#: templates/accounts/fragments/list.html:41
|
||||
#: templates/categories/fragments/table.html:29
|
||||
#: templates/cotton/transaction/item.html:110
|
||||
#: templates/cotton/ui/transactions_action_bar.html:43
|
||||
#: templates/cotton/transaction/item.html:109
|
||||
#: templates/cotton/ui/transactions_action_bar.html:47
|
||||
#: templates/currencies/fragments/list.html:37
|
||||
#: templates/dca/fragments/strategy/details.html:68
|
||||
#: templates/dca/fragments/strategy/details.html:67
|
||||
#: templates/dca/fragments/strategy/list.html:34
|
||||
#: templates/entities/fragments/table.html:28
|
||||
#: templates/exchange_rates/fragments/table.html:24
|
||||
#: templates/exchange_rates/fragments/table.html:23
|
||||
#: templates/import_app/fragments/profiles/list.html:48
|
||||
#: templates/installment_plans/fragments/table.html:27
|
||||
#: templates/recurring_transactions/fragments/table.html:29
|
||||
@@ -1274,13 +1282,13 @@ msgstr "Editar"
|
||||
#: templates/account_groups/fragments/list.html:43
|
||||
#: templates/accounts/fragments/list.html:48
|
||||
#: templates/categories/fragments/table.html:36
|
||||
#: templates/cotton/transaction/item.html:125
|
||||
#: templates/cotton/ui/transactions_action_bar.html:80
|
||||
#: templates/cotton/transaction/item.html:124
|
||||
#: templates/cotton/ui/transactions_action_bar.html:84
|
||||
#: templates/currencies/fragments/list.html:44
|
||||
#: templates/dca/fragments/strategy/details.html:76
|
||||
#: templates/dca/fragments/strategy/details.html:75
|
||||
#: templates/dca/fragments/strategy/list.html:42
|
||||
#: templates/entities/fragments/table.html:36
|
||||
#: templates/exchange_rates/fragments/table.html:32
|
||||
#: templates/exchange_rates/fragments/table.html:31
|
||||
#: templates/import_app/fragments/profiles/list.html:69
|
||||
#: templates/import_app/fragments/runs/list.html:102
|
||||
#: templates/installment_plans/fragments/table.html:56
|
||||
@@ -1295,13 +1303,13 @@ msgstr "Apagar"
|
||||
#: templates/account_groups/fragments/list.html:47
|
||||
#: templates/accounts/fragments/list.html:52
|
||||
#: templates/categories/fragments/table.html:41
|
||||
#: templates/cotton/transaction/item.html:129
|
||||
#: templates/cotton/ui/transactions_action_bar.html:82
|
||||
#: templates/cotton/transaction/item.html:128
|
||||
#: templates/cotton/ui/transactions_action_bar.html:86
|
||||
#: templates/currencies/fragments/list.html:48
|
||||
#: templates/dca/fragments/strategy/details.html:81
|
||||
#: templates/dca/fragments/strategy/details.html:80
|
||||
#: templates/dca/fragments/strategy/list.html:46
|
||||
#: templates/entities/fragments/table.html:40
|
||||
#: templates/exchange_rates/fragments/table.html:37
|
||||
#: templates/exchange_rates/fragments/table.html:36
|
||||
#: templates/import_app/fragments/profiles/list.html:73
|
||||
#: templates/import_app/fragments/runs/list.html:106
|
||||
#: templates/installment_plans/fragments/table.html:48
|
||||
@@ -1319,13 +1327,13 @@ msgstr "Tem certeza?"
|
||||
#: templates/account_groups/fragments/list.html:48
|
||||
#: templates/accounts/fragments/list.html:53
|
||||
#: templates/categories/fragments/table.html:42
|
||||
#: templates/cotton/transaction/item.html:130
|
||||
#: templates/cotton/ui/transactions_action_bar.html:83
|
||||
#: templates/cotton/transaction/item.html:129
|
||||
#: templates/cotton/ui/transactions_action_bar.html:87
|
||||
#: templates/currencies/fragments/list.html:49
|
||||
#: templates/dca/fragments/strategy/details.html:82
|
||||
#: templates/dca/fragments/strategy/details.html:81
|
||||
#: templates/dca/fragments/strategy/list.html:47
|
||||
#: templates/entities/fragments/table.html:41
|
||||
#: templates/exchange_rates/fragments/table.html:38
|
||||
#: templates/exchange_rates/fragments/table.html:37
|
||||
#: templates/import_app/fragments/profiles/list.html:74
|
||||
#: templates/rules/fragments/list.html:49
|
||||
#: templates/rules/fragments/transaction_rule/view.html:61
|
||||
@@ -1336,12 +1344,12 @@ msgstr "Você não será capaz de reverter isso!"
|
||||
#: templates/account_groups/fragments/list.html:49
|
||||
#: templates/accounts/fragments/list.html:54
|
||||
#: templates/categories/fragments/table.html:43
|
||||
#: templates/cotton/transaction/item.html:131
|
||||
#: templates/cotton/transaction/item.html:130
|
||||
#: templates/currencies/fragments/list.html:50
|
||||
#: templates/dca/fragments/strategy/details.html:83
|
||||
#: templates/dca/fragments/strategy/details.html:82
|
||||
#: templates/dca/fragments/strategy/list.html:48
|
||||
#: templates/entities/fragments/table.html:42
|
||||
#: templates/exchange_rates/fragments/table.html:39
|
||||
#: templates/exchange_rates/fragments/table.html:38
|
||||
#: templates/import_app/fragments/profiles/list.html:75
|
||||
#: templates/import_app/fragments/runs/list.html:108
|
||||
#: templates/installment_plans/fragments/table.html:62
|
||||
@@ -1416,11 +1424,11 @@ msgstr "SÁB"
|
||||
msgid "SUN"
|
||||
msgstr "DOM"
|
||||
|
||||
#: templates/calendar_view/fragments/list_transactions.html:6
|
||||
#: templates/calendar_view/fragments/list_transactions.html:5
|
||||
msgid "Transactions on"
|
||||
msgstr "Transações em"
|
||||
|
||||
#: templates/calendar_view/fragments/list_transactions.html:16
|
||||
#: templates/calendar_view/fragments/list_transactions.html:15
|
||||
msgid "No transactions on this date"
|
||||
msgstr "Nenhuma transação nesta data"
|
||||
|
||||
@@ -1479,12 +1487,12 @@ msgstr "Fechar"
|
||||
msgid "Search"
|
||||
msgstr "Buscar"
|
||||
|
||||
#: templates/cotton/transaction/item.html:6
|
||||
#: templates/cotton/transaction/item.html:5
|
||||
msgid "Select"
|
||||
msgstr "Selecionar"
|
||||
|
||||
#: templates/cotton/transaction/item.html:117
|
||||
#: templates/cotton/ui/transactions_action_bar.html:72
|
||||
#: templates/cotton/transaction/item.html:116
|
||||
#: templates/cotton/ui/transactions_action_bar.html:76
|
||||
msgid "Duplicate"
|
||||
msgstr "Duplicar"
|
||||
|
||||
@@ -1508,62 +1516,62 @@ msgstr "Despesas Previstas"
|
||||
msgid "Current Expenses"
|
||||
msgstr "Despesas Atuais"
|
||||
|
||||
#: templates/cotton/ui/transactions_action_bar.html:25
|
||||
#: templates/cotton/ui/transactions_action_bar.html:29
|
||||
msgid "Select All"
|
||||
msgstr "Selecionar todos"
|
||||
|
||||
#: templates/cotton/ui/transactions_action_bar.html:31
|
||||
#: templates/cotton/ui/transactions_action_bar.html:35
|
||||
msgid "Unselect All"
|
||||
msgstr "Desmarcar todos"
|
||||
|
||||
#: templates/cotton/ui/transactions_action_bar.html:48
|
||||
#: templates/cotton/ui/transactions_action_bar.html:139
|
||||
#: templates/cotton/ui/transactions_action_bar.html:52
|
||||
#: templates/cotton/ui/transactions_action_bar.html:143
|
||||
msgid "Toggle Dropdown"
|
||||
msgstr "Alternar menu suspenso"
|
||||
|
||||
#: templates/cotton/ui/transactions_action_bar.html:56
|
||||
#: templates/cotton/ui/transactions_action_bar.html:60
|
||||
msgid "Mark as unpaid"
|
||||
msgstr "Marcar como não pago"
|
||||
|
||||
#: templates/cotton/ui/transactions_action_bar.html:63
|
||||
#: templates/cotton/ui/transactions_action_bar.html:67
|
||||
msgid "Mark as paid"
|
||||
msgstr "Marcar como pago"
|
||||
|
||||
#: templates/cotton/ui/transactions_action_bar.html:84
|
||||
#: templates/cotton/ui/transactions_action_bar.html:88
|
||||
msgid "Yes, delete them!"
|
||||
msgstr "Sim, apague!"
|
||||
|
||||
#: templates/cotton/ui/transactions_action_bar.html:130
|
||||
#: templates/cotton/ui/transactions_action_bar.html:154
|
||||
#: templates/cotton/ui/transactions_action_bar.html:174
|
||||
#: templates/cotton/ui/transactions_action_bar.html:194
|
||||
#: templates/cotton/ui/transactions_action_bar.html:214
|
||||
#: templates/cotton/ui/transactions_action_bar.html:234
|
||||
#: templates/cotton/ui/transactions_action_bar.html:254
|
||||
#: templates/cotton/ui/transactions_action_bar.html:134
|
||||
#: templates/cotton/ui/transactions_action_bar.html:158
|
||||
#: templates/cotton/ui/transactions_action_bar.html:178
|
||||
#: templates/cotton/ui/transactions_action_bar.html:198
|
||||
#: templates/cotton/ui/transactions_action_bar.html:218
|
||||
#: templates/cotton/ui/transactions_action_bar.html:238
|
||||
#: templates/cotton/ui/transactions_action_bar.html:258
|
||||
msgid "copied!"
|
||||
msgstr "copiado!"
|
||||
|
||||
#: templates/cotton/ui/transactions_action_bar.html:147
|
||||
#: templates/cotton/ui/transactions_action_bar.html:151
|
||||
msgid "Flat Total"
|
||||
msgstr "Total Fixo"
|
||||
|
||||
#: templates/cotton/ui/transactions_action_bar.html:167
|
||||
#: templates/cotton/ui/transactions_action_bar.html:171
|
||||
msgid "Real Total"
|
||||
msgstr "Total Real"
|
||||
|
||||
#: templates/cotton/ui/transactions_action_bar.html:187
|
||||
#: templates/cotton/ui/transactions_action_bar.html:191
|
||||
msgid "Mean"
|
||||
msgstr "Média"
|
||||
|
||||
#: templates/cotton/ui/transactions_action_bar.html:207
|
||||
#: templates/cotton/ui/transactions_action_bar.html:211
|
||||
msgid "Max"
|
||||
msgstr "Máximo"
|
||||
|
||||
#: templates/cotton/ui/transactions_action_bar.html:227
|
||||
#: templates/cotton/ui/transactions_action_bar.html:231
|
||||
msgid "Min"
|
||||
msgstr "Minímo"
|
||||
|
||||
#: templates/cotton/ui/transactions_action_bar.html:247
|
||||
#: templates/cotton/ui/transactions_action_bar.html:251
|
||||
msgid "Count"
|
||||
msgstr "Contagem"
|
||||
|
||||
@@ -1595,91 +1603,91 @@ msgstr "Editar entrada CMP"
|
||||
msgid "Add DCA strategy"
|
||||
msgstr "Adicionar estratégia CMP"
|
||||
|
||||
#: templates/dca/fragments/strategy/details.html:23
|
||||
#: templates/dca/fragments/strategy/details.html:22
|
||||
msgid "No exchange rate available"
|
||||
msgstr "Nenhuma taxa de câmbio disponível"
|
||||
|
||||
#: templates/dca/fragments/strategy/details.html:34
|
||||
#: templates/dca/fragments/strategy/details.html:33
|
||||
msgid "Entries"
|
||||
msgstr "Entradas"
|
||||
|
||||
#: templates/dca/fragments/strategy/details.html:56
|
||||
#: templates/dca/fragments/strategy/details.html:55
|
||||
msgid "Current Value"
|
||||
msgstr "Valor atual"
|
||||
|
||||
#: templates/dca/fragments/strategy/details.html:57
|
||||
#: templates/dca/fragments/strategy/details.html:56
|
||||
msgid "P/L"
|
||||
msgstr "P/L"
|
||||
|
||||
#: templates/dca/fragments/strategy/details.html:125
|
||||
#: templates/dca/fragments/strategy/details.html:124
|
||||
msgid "No entries for this DCA"
|
||||
msgstr "Nenhuma entrada neste CMP"
|
||||
|
||||
#: templates/dca/fragments/strategy/details.html:126
|
||||
#: templates/dca/fragments/strategy/details.html:125
|
||||
#: templates/monthly_overview/fragments/list.html:41
|
||||
#: templates/transactions/fragments/list_all.html:40
|
||||
msgid "Try adding one"
|
||||
msgstr "Tente adicionar uma"
|
||||
|
||||
#: templates/dca/fragments/strategy/details.html:136
|
||||
#: templates/dca/fragments/strategy/details.html:135
|
||||
msgid "Total Invested"
|
||||
msgstr "Total investido"
|
||||
|
||||
#: templates/dca/fragments/strategy/details.html:150
|
||||
#: templates/dca/fragments/strategy/details.html:149
|
||||
msgid "Total Received"
|
||||
msgstr "Total recebido"
|
||||
|
||||
#: templates/dca/fragments/strategy/details.html:164
|
||||
#: templates/dca/fragments/strategy/details.html:163
|
||||
msgid "Current Total Value"
|
||||
msgstr "Valor total atual"
|
||||
|
||||
#: templates/dca/fragments/strategy/details.html:178
|
||||
#: templates/dca/fragments/strategy/details.html:177
|
||||
msgid "Average Entry Price"
|
||||
msgstr "Preço médio de entrada"
|
||||
|
||||
#: templates/dca/fragments/strategy/details.html:192
|
||||
#: templates/dca/fragments/strategy/details.html:191
|
||||
msgid "Total P/L"
|
||||
msgstr "P/L total"
|
||||
|
||||
#: templates/dca/fragments/strategy/details.html:208
|
||||
#: templates/dca/fragments/strategy/details.html:207
|
||||
#, python-format
|
||||
msgid "Total %% P/L"
|
||||
msgstr "P/L%% Total"
|
||||
|
||||
#: templates/dca/fragments/strategy/details.html:227
|
||||
#: templates/dca/fragments/strategy/details.html:226
|
||||
#, python-format
|
||||
msgid "P/L %%"
|
||||
msgstr "P/L %%"
|
||||
|
||||
#: templates/dca/fragments/strategy/details.html:289
|
||||
#: templates/dca/fragments/strategy/details.html:288
|
||||
msgid "Performance Over Time"
|
||||
msgstr "Desempenho ao longo do tempo"
|
||||
|
||||
#: templates/dca/fragments/strategy/details.html:307
|
||||
#: templates/dca/fragments/strategy/details.html:306
|
||||
msgid "Entry Price"
|
||||
msgstr "Preço de Entrada"
|
||||
|
||||
#: templates/dca/fragments/strategy/details.html:315
|
||||
#: templates/dca/fragments/strategy/details.html:314
|
||||
msgid "Current Price"
|
||||
msgstr "Preço atual"
|
||||
|
||||
#: templates/dca/fragments/strategy/details.html:323
|
||||
#: templates/dca/fragments/strategy/details.html:322
|
||||
msgid "Amount Bought"
|
||||
msgstr "Quantia comprada"
|
||||
|
||||
#: templates/dca/fragments/strategy/details.html:391
|
||||
#: templates/dca/fragments/strategy/details.html:390
|
||||
msgid "Entry Price vs Current Price"
|
||||
msgstr "Preço de Entrada vs Preço Atual"
|
||||
|
||||
#: templates/dca/fragments/strategy/details.html:407
|
||||
#: templates/dca/fragments/strategy/details.html:406
|
||||
msgid "Days Between Investments"
|
||||
msgstr "Dias entre investimentos"
|
||||
|
||||
#: templates/dca/fragments/strategy/details.html:454
|
||||
#: templates/dca/fragments/strategy/details.html:453
|
||||
msgid "Investment Frequency"
|
||||
msgstr "Frequência de Investimento"
|
||||
|
||||
#: templates/dca/fragments/strategy/details.html:456
|
||||
#: templates/dca/fragments/strategy/details.html:455
|
||||
msgid "The straighter the blue line, the more consistent your DCA strategy is."
|
||||
msgstr ""
|
||||
"Quanto mais reta for a linha azul, mais consistente é sua estratégia de CMP."
|
||||
@@ -1725,19 +1733,19 @@ msgstr "Editar taxa de câmbio"
|
||||
msgid "All"
|
||||
msgstr "Todas"
|
||||
|
||||
#: templates/exchange_rates/fragments/table.html:12
|
||||
#: templates/exchange_rates/fragments/table.html:11
|
||||
msgid "Pairing"
|
||||
msgstr "Pares"
|
||||
|
||||
#: templates/exchange_rates/fragments/table.html:13
|
||||
#: templates/exchange_rates/fragments/table.html:12
|
||||
msgid "Rate"
|
||||
msgstr "Taxa de Câmbio"
|
||||
|
||||
#: templates/exchange_rates/fragments/table.html:52
|
||||
#: templates/exchange_rates/fragments/table.html:51
|
||||
msgid "No exchange rates"
|
||||
msgstr "Nenhuma taxa de câmbio"
|
||||
|
||||
#: templates/exchange_rates/fragments/table.html:59
|
||||
#: templates/exchange_rates/fragments/table.html:58
|
||||
#: templates/transactions/fragments/list_all.html:47
|
||||
msgid "Page navigation"
|
||||
msgstr "Navegação por página"
|
||||
@@ -1894,7 +1902,11 @@ msgstr "Calculadora"
|
||||
msgid "Settings"
|
||||
msgstr "Configurações"
|
||||
|
||||
#: templates/includes/navbar/user_menu.html:37
|
||||
#: templates/includes/navbar/user_menu.html:38
|
||||
msgid "Clear cache"
|
||||
msgstr "Limpar cache"
|
||||
|
||||
#: templates/includes/navbar/user_menu.html:42
|
||||
msgid "Logout"
|
||||
msgstr "Sair"
|
||||
|
||||
|
||||
@@ -39,23 +39,23 @@
|
||||
{% for transaction in date.transactions %}
|
||||
{% if transaction.is_paid %}
|
||||
{% if transaction.type == "IN" and not transaction.account.is_asset %}
|
||||
<i class="fa-solid fa-circle-check tw-text-green-400" data-bs-toggle="tooltip" data-bs-title="{{ transaction.description }}"></i>
|
||||
<i class="fa-solid fa-circle-check tw-text-green-400" data-bs-toggle="tooltip" data-bs-title="{% if transaction.description %}{{ transaction.description }}{% else %}{% trans 'Income' %}{% endif %}"></i>
|
||||
{% elif transaction.type == "IN" and transaction.account.is_asset %}
|
||||
<i class="fa-solid fa-circle-check tw-text-green-300" data-bs-toggle="tooltip" data-bs-title="{{ transaction.description }}"></i>
|
||||
<i class="fa-solid fa-circle-check tw-text-green-300" data-bs-toggle="tooltip" data-bs-title="{% if transaction.description %}{{ transaction.description }}{% else %}{% trans 'Income' %}{% endif %}"></i>
|
||||
{% elif transaction.type == "EX" and not transaction.account.is_asset %}
|
||||
<i class="fa-solid fa-circle-check tw-text-red-400" data-bs-toggle="tooltip" data-bs-title="{{ transaction.description }}"></i>
|
||||
<i class="fa-solid fa-circle-check tw-text-red-400" data-bs-toggle="tooltip" data-bs-title="{% if transaction.description %}{{ transaction.description }}{% else %}{% trans 'Expense' %}{% endif %}"></i>
|
||||
{% elif transaction.type == "EX" and transaction.account.is_asset %}
|
||||
<i class="fa-solid fa-circle-check tw-text-red-300" data-bs-toggle="tooltip" data-bs-title="{{ transaction.description }}"></i>
|
||||
<i class="fa-solid fa-circle-check tw-text-red-300" data-bs-toggle="tooltip" data-bs-title="{% if transaction.description %}{{ transaction.description }}{% else %}{% trans 'Expense' %}{% endif %}"></i>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% if transaction.type == "IN" and not transaction.account.is_asset %}
|
||||
<i class="fa-regular fa-circle tw-text-green-400" data-bs-toggle="tooltip" data-bs-title="{{ transaction.description }}"></i>
|
||||
<i class="fa-regular fa-circle tw-text-green-400" data-bs-toggle="tooltip" data-bs-title="{% if transaction.description %}{{ transaction.description }}{% else %}{% trans 'Income' %}{% endif %}"></i>
|
||||
{% elif transaction.type == "IN" and transaction.account.is_asset %}
|
||||
<i class="fa-regular fa-circle tw-text-green-300" data-bs-toggle="tooltip" data-bs-title="{{ transaction.description }}"></i>
|
||||
<i class="fa-regular fa-circle tw-text-green-300" data-bs-toggle="tooltip" data-bs-title="{% if transaction.description %}{{ transaction.description }}{% else %}{% trans 'Income' %}{% endif %}"></i>
|
||||
{% elif transaction.type == "EX" and not transaction.account.is_asset %}
|
||||
<i class="fa-regular fa-circle tw-text-red-400" data-bs-toggle="tooltip" data-bs-title="{{ transaction.description }}"></i>
|
||||
<i class="fa-regular fa-circle tw-text-red-400" data-bs-toggle="tooltip" data-bs-title="{% if transaction.description %}{{ transaction.description }}{% else %}{% trans 'Expense' %}{% endif %}"></i>
|
||||
{% elif transaction.type == "EX" and transaction.account.is_asset %}
|
||||
<i class="fa-regular fa-circle tw-text-green-300" data-bs-toggle="tooltip" data-bs-title="{{ transaction.description }}"></i>
|
||||
<i class="fa-regular fa-circle tw-text-red-300" data-bs-toggle="tooltip" data-bs-title="{% if transaction.description %}{{ transaction.description }}{% else %}{% trans 'Expense' %}{% endif %}"></i>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
{% extends 'extends/offcanvas.html' %}
|
||||
{% load date %}
|
||||
{% load i18n %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
{% block title %}{% translate 'Transactions on' %} {{ date|custom_date:request.user }}{% endblock %}
|
||||
{% block title %}{% translate 'Transactions on' %} {{ date|date:"SHORT_DATE_FORMAT" }}{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div hx-get="{% url 'calendar_transactions_list' day=date.day month=date.month year=date.year %}" hx-trigger="updated from:window" hx-vals='{"disable_selection": true}' hx-target="closest .offcanvas" class="show-loading">
|
||||
{% for transaction in transactions %}
|
||||
<c-transaction.item
|
||||
:transaction="transaction"
|
||||
:disable-selection="True"></c-transaction.item>
|
||||
{% empty %}
|
||||
<c-msg.empty
|
||||
title="{% translate 'No transactions on this date' %}"></c-msg.empty>
|
||||
{% endfor %}
|
||||
<div hx-get="{% url 'calendar_transactions_list' day=date.day month=date.month year=date.year %}" hx-trigger="updated from:window" hx-vals='{"disable_selection": true}' hx-target="closest .offcanvas" class="show-loading" id="transactions-list">
|
||||
{% for transaction in transactions %}
|
||||
<c-transaction.item :transaction="transaction"></c-transaction.item>
|
||||
{% empty %}
|
||||
<c-msg.empty
|
||||
title="{% translate 'No transactions on this date' %}"></c-msg.empty>
|
||||
{% endfor %}
|
||||
{# Floating bar #}
|
||||
<c-ui.transactions-action-bar></c-ui.transactions-action-bar>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -44,53 +44,10 @@
|
||||
</div>
|
||||
{# Action buttons#}
|
||||
<div class="col-12 col-xl-8">
|
||||
<div class="d-grid gap-2 d-xl-flex justify-content-xl-end">
|
||||
<button class="btn btn-sm btn-outline-success"
|
||||
hx-get="{% url 'transaction_add' %}"
|
||||
hx-target="#generic-offcanvas"
|
||||
hx-trigger="click, add_income from:window"
|
||||
hx-vals='{"year": {{ year }}, "month": {{ month }}, "type": "IN"}'>
|
||||
<i class="fa-solid fa-arrow-right-to-bracket me-2"></i>
|
||||
{% translate "Income" %}
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-danger"
|
||||
hx-get="{% url 'transaction_add' %}"
|
||||
hx-target="#generic-offcanvas"
|
||||
hx-trigger="click, add_expense from:window"
|
||||
hx-vals='{"year": {{ year }}, "month": {{ month }}, "type": "EX"}'>
|
||||
<i class="fa-solid fa-arrow-right-from-bracket me-2"></i>
|
||||
{% translate "Expense" %}
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-warning"
|
||||
hx-get="{% url 'installment_plan_add' %}"
|
||||
hx-trigger="click, installment from:window"
|
||||
hx-target="#generic-offcanvas">
|
||||
<i class="fa-solid fa-divide me-2"></i>
|
||||
{% translate "Installment" %}
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-warning"
|
||||
hx-get="{% url 'recurring_transaction_add' %}"
|
||||
hx-trigger="click, balance from:window"
|
||||
hx-target="#generic-offcanvas">
|
||||
<i class="fa-solid fa-repeat me-2"></i>
|
||||
{% translate "Recurring" %}
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-info"
|
||||
hx-get="{% url 'transactions_transfer' %}"
|
||||
hx-target="#generic-offcanvas"
|
||||
hx-trigger="click, add_transfer from:window"
|
||||
hx-vals='{"year": {{ year }}, "month": {{ month }}}'>
|
||||
<i class="fa-solid fa-money-bill-transfer me-2"></i>
|
||||
{% translate "Transfer" %}
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-info"
|
||||
hx-get="{% url 'account_reconciliation' %}"
|
||||
hx-trigger="click, balance from:window"
|
||||
hx-target="#generic-offcanvas">
|
||||
<i class="fa-solid fa-scale-balanced me-2"></i>
|
||||
{% translate "Balance" %}
|
||||
</button>
|
||||
</div>
|
||||
<c-ui.quick-transactions-buttons
|
||||
:year="year"
|
||||
:month="month"
|
||||
></c-ui.quick-transactions-buttons>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{% load date %}
|
||||
{% load markdown %}
|
||||
{% load i18n %}
|
||||
<div class="transaction d-flex my-1 {% if transaction.type == "EX" %}expense{% else %}income{% endif %}">
|
||||
{% if not disable_selection %}
|
||||
@@ -27,7 +27,7 @@
|
||||
{# Date#}
|
||||
<div class="row mb-2 mb-lg-1 tw-text-gray-400">
|
||||
<div class="col-auto pe-1"><i class="fa-solid fa-calendar fa-fw me-1 fa-xs"></i></div>
|
||||
<div class="col ps-0">{{ transaction.date|custom_date:request.user }} • {{ transaction.reference_date|date:"b/Y" }}</div>
|
||||
<div class="col ps-0">{{ transaction.date|date:"SHORT_DATE_FORMAT" }} • {{ transaction.reference_date|date:"b/Y" }}</div>
|
||||
</div>
|
||||
{# Description#}
|
||||
<div class="mb-2 mb-lg-1 text-white tw-text-base">
|
||||
@@ -55,7 +55,7 @@
|
||||
{% if transaction.notes %}
|
||||
<div class="row mb-2 mb-lg-1 tw-text-gray-400">
|
||||
<div class="col-auto pe-1"><i class="fa-solid fa-align-left fa-fw me-1 fa-xs"></i></div>
|
||||
<div class="col ps-0">{{ transaction.notes | linebreaksbr }}</div>
|
||||
<div class="col ps-0">{{ transaction.notes | limited_markdown | linebreaksbr }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{# Category#}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<i class="{{ icon }}"></i>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h5 class="tw-text-{{ color }}-400 fw-bold" {{ attrs }}>{{ title }}{% if help_text %}{% include 'includes/help_icon.html' with content=help_text %}{% endif %}</h5>
|
||||
<h5 class="tw-text-{{ color }}-400 fw-bold tw-mr-[50px]" {{ attrs }}>{{ title }}{% if help_text %}{% include 'includes/help_icon.html' with content=help_text %}{% endif %}</h5>
|
||||
{{ slot }}
|
||||
</div>
|
||||
</div>
|
||||
50
app/templates/cotton/ui/quick_transactions_buttons.html
Normal file
50
app/templates/cotton/ui/quick_transactions_buttons.html
Normal file
@@ -0,0 +1,50 @@
|
||||
{% load i18n %}
|
||||
<div class="d-grid gap-2 d-xl-flex justify-content-xl-end">
|
||||
<div class="d-grid gap-2 d-xl-flex flex-wrap justify-content-xl-center">
|
||||
<button class="btn btn-sm btn-outline-success"
|
||||
hx-get="{% url 'transaction_add' %}"
|
||||
hx-target="#generic-offcanvas"
|
||||
hx-trigger="click, add_income from:window"
|
||||
hx-vals='{"year": {{ year }}, {% if month %}"month": {{ month }},{% endif %} "type": "IN"}'>
|
||||
<i class="fa-solid fa-arrow-right-to-bracket me-2"></i>
|
||||
{% translate "Income" %}
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-danger"
|
||||
hx-get="{% url 'transaction_add' %}"
|
||||
hx-target="#generic-offcanvas"
|
||||
hx-trigger="click, add_expense from:window"
|
||||
hx-vals='{"year": {{ year }}, {% if month %}"month": {{ month }},{% endif %} "type": "EX"}'>
|
||||
<i class="fa-solid fa-arrow-right-from-bracket me-2"></i>
|
||||
{% translate "Expense" %}
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-warning"
|
||||
hx-get="{% url 'installment_plan_add' %}"
|
||||
hx-trigger="click, installment from:window"
|
||||
hx-target="#generic-offcanvas">
|
||||
<i class="fa-solid fa-divide me-2"></i>
|
||||
{% translate "Installment" %}
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-warning"
|
||||
hx-get="{% url 'recurring_transaction_add' %}"
|
||||
hx-trigger="click, balance from:window"
|
||||
hx-target="#generic-offcanvas">
|
||||
<i class="fa-solid fa-repeat me-2"></i>
|
||||
{% translate "Recurring" %}
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-info"
|
||||
hx-get="{% url 'transactions_transfer' %}"
|
||||
hx-target="#generic-offcanvas"
|
||||
hx-trigger="click, add_transfer from:window"
|
||||
hx-vals='{"year": {{ year }} {% if month %}, "month": {{ month }}{% endif %}}'>
|
||||
<i class="fa-solid fa-money-bill-transfer me-2"></i>
|
||||
{% translate "Transfer" %}
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-info"
|
||||
hx-get="{% url 'account_reconciliation' %}"
|
||||
hx-trigger="click, balance from:window"
|
||||
hx-target="#generic-offcanvas">
|
||||
<i class="fa-solid fa-scale-balanced me-2"></i>
|
||||
{% translate "Balance" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2,12 +2,16 @@
|
||||
<div class="tw-sticky tw-bottom-4 tw-left-0 tw-right-0 tw-z-50 tw-hidden mx-auto tw-w-fit" id="actions-bar"
|
||||
_="on change from #transactions-list or htmx:afterSettle from window
|
||||
if no <input[type='checkbox']:checked/> in #transactions-list
|
||||
add .slide-in-bottom-reverse then settle
|
||||
then add .tw-hidden to #actions-bar
|
||||
then remove .slide-in-bottom-reverse
|
||||
if #actions-bar
|
||||
add .slide-in-bottom-reverse then settle
|
||||
then add .tw-hidden to #actions-bar
|
||||
then remove .slide-in-bottom-reverse
|
||||
end
|
||||
else
|
||||
remove .tw-hidden from #actions-bar
|
||||
then trigger selected_transactions_updated
|
||||
if #actions-bar
|
||||
remove .tw-hidden from #actions-bar
|
||||
then trigger selected_transactions_updated
|
||||
end
|
||||
end
|
||||
end">
|
||||
<div class="card slide-in-bottom">
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
{% load date %}
|
||||
{% load currency_display %}
|
||||
{% load i18n %}
|
||||
<div class="container-fluid px-md-3 py-3 column-gap-5">
|
||||
@@ -17,7 +16,7 @@
|
||||
:prefix="strategy.payment_currency.prefix"
|
||||
:suffix="strategy.payment_currency.suffix"
|
||||
:decimal_places="strategy.payment_currency.decimal_places">
|
||||
• {{ strategy.current_price.1|custom_date:request.user }}
|
||||
• {{ strategy.current_price.1|date:"SHORT_DATETIME_FORMAT" }}
|
||||
</c-amount.display>
|
||||
{% else %}
|
||||
<div class="tw-text-red-400">{% trans "No exchange rate available" %}</div>
|
||||
@@ -84,7 +83,7 @@
|
||||
_="install prompt_swal"><i class="fa-solid fa-trash fa-fw"></i></a>
|
||||
</div>
|
||||
</td>
|
||||
<td>{{ entry.date|custom_date:request.user }}</td>
|
||||
<td>{{ entry.date|date:"SHORT_DATE_FORMAT" }}</td>
|
||||
<td>
|
||||
<c-amount.display
|
||||
:amount="entry.amount_received"
|
||||
@@ -222,7 +221,7 @@
|
||||
new Chart(perfomancectx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: [{% for entry in entries_data %}'{{ entry.entry.date|custom_date:request.user }}'{% if not forloop.last %}, {% endif %}{% endfor %}],
|
||||
labels: [{% for entry in entries_data %}'{{ entry.entry.date|date:"SHORT_DATE_FORMAT" }}'{% if not forloop.last %}, {% endif %}{% endfor %}],
|
||||
datasets: [{
|
||||
label: '{% trans "P/L %" %}',
|
||||
data: [{% for entry in entries_data %}{{ entry.profit_loss_percentage|floatformat:"-40u" }}{% if not forloop.last %}, {% endif %}{% endfor %}],
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
{% load date %}
|
||||
{% load currency_display %}
|
||||
{% load i18n %}
|
||||
<div class="card-body show-loading" hx-get="{% url 'exchange_rates_list_pair' %}" hx-trigger="updated from:window" hx-swap="outerHTML" hx-vals='{"page": "{{ page_obj.number }}", "from": "{{ from_currency|default_if_none:"" }}", "to": "{{ to_currency|default_if_none:"" }}"}'>
|
||||
@@ -40,7 +39,7 @@
|
||||
_="install prompt_swal"><i class="fa-solid fa-trash fa-fw"></i></a>
|
||||
</div>
|
||||
</td>
|
||||
<td class="col-3">{{ exchange_rate.date|custom_date:request.user }}</td>
|
||||
<td class="col-3">{{ exchange_rate.date|date:"SHORT_DATETIME_FORMAT" }}</td>
|
||||
<td class="col-3"><span class="badge rounded-pill text-bg-secondary">{{ exchange_rate.from_currency.code }}</span> x <span class="badge rounded-pill text-bg-secondary">{{ exchange_rate.to_currency.code }}</span></td>
|
||||
<td class="col-3">1 {{ exchange_rate.from_currency.code }} ≅ {% currency_display amount=exchange_rate.rate prefix=exchange_rate.to_currency.prefix suffix=exchange_rate.to_currency.suffix decimal_places=exchange_rate.to_currency.decimal_places%}</td>
|
||||
</tr>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{% load settings %}
|
||||
{% load i18n %}
|
||||
<div class="dropdown">
|
||||
<a class="tw-text-2xl" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
@@ -33,7 +34,14 @@
|
||||
</li>
|
||||
{% endspaceless %}
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li>
|
||||
<a class="dropdown-item" hx-get="{% url 'invalidate_cache' %}" role="button">
|
||||
<i class="fa-solid fa-broom me-2 fa-fw"></i>{% translate 'Clear cache' %}
|
||||
</a>
|
||||
</li>
|
||||
<li><a class="dropdown-item" href="{% url 'logout' %}"><i class="fa-solid fa-door-open me-2 fa-fw"></i
|
||||
>{% translate 'Logout' %}</a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><a class="dropdown-item" href="https://github.com/eitchtee/WYGIWYH/releases" target="_blank" rel="nofollow">v. {% settings "APP_VERSION" %}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
{% block title %}{% translate 'Installments' %}{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div hx-get="{% url 'installment_plan_transactions' installment_plan_id=installment_plan.id %}" hx-trigger="updated from:window" hx-vals='{"disable_selection": true}' hx-target="closest .offcanvas" class="show-loading">
|
||||
{% for transaction in transactions %}
|
||||
<c-transaction.item
|
||||
:transaction="transaction"
|
||||
:disable-selection="True"></c-transaction.item>
|
||||
{% endfor %}
|
||||
<div hx-get="{% url 'installment_plan_transactions' installment_plan_id=installment_plan.id %}" hx-trigger="updated from:window" hx-vals='{"disable_selection": true}' hx-target="closest .offcanvas" class="show-loading" id="transactions-list">
|
||||
{% for transaction in transactions %}
|
||||
<c-transaction.item :transaction="transaction"></c-transaction.item>
|
||||
{% endfor %}
|
||||
{# Floating bar #}
|
||||
<c-ui.transactions-action-bar></c-ui.transactions-action-bar>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -23,8 +23,6 @@
|
||||
<div id="content">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
|
||||
{% include 'includes/toasts.html' %}
|
||||
|
||||
{% include 'includes/scripts.html' %}
|
||||
{% block extra_js %}{% endblock %}
|
||||
|
||||
@@ -44,53 +44,10 @@
|
||||
</div>
|
||||
{# Action buttons#}
|
||||
<div class="col-12 col-xl-8">
|
||||
<div class="d-grid gap-2 d-xl-flex justify-content-xl-end">
|
||||
<button class="btn btn-sm btn-outline-success"
|
||||
hx-get="{% url 'transaction_add' %}"
|
||||
hx-target="#generic-offcanvas"
|
||||
hx-trigger="click, add_income from:window"
|
||||
hx-vals='{"year": {{ year }}, "month": {{ month }}, "type": "IN"}'>
|
||||
<i class="fa-solid fa-arrow-right-to-bracket me-2"></i>
|
||||
{% translate "Income" %}
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-danger"
|
||||
hx-get="{% url 'transaction_add' %}"
|
||||
hx-target="#generic-offcanvas"
|
||||
hx-trigger="click, add_expense from:window"
|
||||
hx-vals='{"year": {{ year }}, "month": {{ month }}, "type": "EX"}'>
|
||||
<i class="fa-solid fa-arrow-right-from-bracket me-2"></i>
|
||||
{% translate "Expense" %}
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-warning"
|
||||
hx-get="{% url 'installment_plan_add' %}"
|
||||
hx-trigger="click, installment from:window"
|
||||
hx-target="#generic-offcanvas">
|
||||
<i class="fa-solid fa-divide me-2"></i>
|
||||
{% translate "Installment" %}
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-warning"
|
||||
hx-get="{% url 'recurring_transaction_add' %}"
|
||||
hx-trigger="click, balance from:window"
|
||||
hx-target="#generic-offcanvas">
|
||||
<i class="fa-solid fa-repeat me-2"></i>
|
||||
{% translate "Recurring" %}
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-info"
|
||||
hx-get="{% url 'transactions_transfer' %}"
|
||||
hx-target="#generic-offcanvas"
|
||||
hx-trigger="click, add_transfer from:window"
|
||||
hx-vals='{"year": {{ year }}, "month": {{ month }}}'>
|
||||
<i class="fa-solid fa-money-bill-transfer me-2"></i>
|
||||
{% translate "Transfer" %}
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-info"
|
||||
hx-get="{% url 'account_reconciliation' %}"
|
||||
hx-trigger="click, balance from:window"
|
||||
hx-target="#generic-offcanvas">
|
||||
<i class="fa-solid fa-scale-balanced me-2"></i>
|
||||
{% translate "Balance" %}
|
||||
</button>
|
||||
</div>
|
||||
<c-ui.quick-transactions-buttons
|
||||
:year="year"
|
||||
:month="month"
|
||||
></c-ui.quick-transactions-buttons>
|
||||
</div>
|
||||
</div>
|
||||
{# Monthly summary#}
|
||||
@@ -113,9 +70,9 @@
|
||||
<div class="text-sm-end" _="on change trigger updated on window">
|
||||
<label for="order">{% translate "Order by" %}</label>
|
||||
<select class="tw-border-0 focus-visible:tw-outline-0 w-full pe-2 tw-leading-normal text-bg-tertiary tw-font-medium rounded" name="order" id="order">
|
||||
<option value="default">{% translate 'Default' %}</option>
|
||||
<option value="older">{% translate 'Oldest first' %}</option>
|
||||
<option value="newer">{% translate 'Newest first' %}</option>
|
||||
<option value="default" {% if order == 'default' %}selected{% endif %}>{% translate 'Default' %}</option>
|
||||
<option value="older" {% if order == 'older' %}selected{% endif %}>{% translate 'Oldest first' %}</option>
|
||||
<option value="newer" {% if order == 'newer' %}selected{% endif %}>{% translate 'Newest first' %}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
{% block title %}{% translate 'Transactions' %}{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div hx-get="{% url 'recurring_transaction_transactions' recurring_transaction_id=recurring_transaction.id %}" hx-trigger="updated from:window" hx-vals='{"disable_selection": true}' hx-target="closest .offcanvas" class="show-loading">
|
||||
{% for transaction in transactions %}
|
||||
<c-transaction.item
|
||||
:transaction="transaction"
|
||||
:disable-selection="True"></c-transaction.item>
|
||||
{% endfor %}
|
||||
<div hx-get="{% url 'recurring_transaction_transactions' recurring_transaction_id=recurring_transaction.id %}" hx-trigger="updated from:window" hx-vals='{"disable_selection": true}' hx-target="closest .offcanvas" class="show-loading" id="transactions-list">
|
||||
{% for transaction in transactions %}
|
||||
<c-transaction.item :transaction="transaction"></c-transaction.item>
|
||||
{% endfor %}
|
||||
{# Floating bar #}
|
||||
<c-ui.transactions-action-bar></c-ui.transactions-action-bar>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -32,9 +32,9 @@
|
||||
<div class="tw-content-center" _="on change trigger updated on window">
|
||||
<label for="order">{% translate "Order by" %}</label>
|
||||
<select class="tw-border-0 focus-visible:tw-outline-0 w-full pe-2 tw-leading-normal text-bg-tertiary tw-font-medium rounded" name="order" id="order">
|
||||
<option value="default">{% translate 'Default' %}</option>
|
||||
<option value="older">{% translate 'Oldest first' %}</option>
|
||||
<option value="newer">{% translate 'Newest first' %}</option>
|
||||
<option value="default" {% if order == 'default' %}selected{% endif %}>{% translate 'Default' %}</option>
|
||||
<option value="older" {% if order == 'older' %}selected{% endif %}>{% translate 'Oldest first' %}</option>
|
||||
<option value="newer" {% if order == 'newer' %}selected{% endif %}>{% translate 'Newest first' %}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -39,53 +39,10 @@
|
||||
</div>
|
||||
{# Action buttons#}
|
||||
<div class="col-12 col-xl-10">
|
||||
<div class="d-grid gap-2 d-xl-flex justify-content-xl-end">
|
||||
<button class="btn btn-sm btn-outline-success"
|
||||
hx-get="{% url 'transaction_add' %}"
|
||||
hx-target="#generic-offcanvas"
|
||||
hx-trigger="click, add_income from:window"
|
||||
hx-vals='{"year": {{ year }}, "type": "IN"}'>
|
||||
<i class="fa-solid fa-arrow-right-to-bracket me-2"></i>
|
||||
{% translate "Income" %}
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-danger"
|
||||
hx-get="{% url 'transaction_add' %}"
|
||||
hx-target="#generic-offcanvas"
|
||||
hx-trigger="click, add_expense from:window"
|
||||
hx-vals='{"year": {{ year }}, "type": "EX"}'>
|
||||
<i class="fa-solid fa-arrow-right-from-bracket me-2"></i>
|
||||
{% translate "Expense" %}
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-warning"
|
||||
hx-get="{% url 'installment_plan_add' %}"
|
||||
hx-trigger="click, installment from:window"
|
||||
hx-target="#generic-offcanvas">
|
||||
<i class="fa-solid fa-divide me-2"></i>
|
||||
{% translate "Installment" %}
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-warning"
|
||||
hx-get="{% url 'recurring_transaction_add' %}"
|
||||
hx-trigger="click, balance from:window"
|
||||
hx-target="#generic-offcanvas">
|
||||
<i class="fa-solid fa-repeat me-2"></i>
|
||||
{% translate "Recurring" %}
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-info"
|
||||
hx-get="{% url 'transactions_transfer' %}"
|
||||
hx-target="#generic-offcanvas"
|
||||
hx-trigger="click, add_transfer from:window"
|
||||
hx-vals='{"year": {{ year }}}'>
|
||||
<i class="fa-solid fa-money-bill-transfer me-2"></i>
|
||||
{% translate "Transfer" %}
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-info"
|
||||
hx-get="{% url 'account_reconciliation' %}"
|
||||
hx-trigger="click, balance from:window"
|
||||
hx-target="#generic-offcanvas">
|
||||
<i class="fa-solid fa-scale-balanced me-2"></i>
|
||||
{% translate "Balance" %}
|
||||
</button>
|
||||
</div>
|
||||
<c-ui.quick-transactions-buttons
|
||||
:year="year"
|
||||
:month="month"
|
||||
></c-ui.quick-transactions-buttons>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
|
||||
@@ -41,53 +41,10 @@
|
||||
</div>
|
||||
{# Action buttons#}
|
||||
<div class="col-12 col-xl-10">
|
||||
<div class="d-grid gap-2 d-xl-flex justify-content-xl-end">
|
||||
<button class="btn btn-sm btn-outline-success"
|
||||
hx-get="{% url 'transaction_add' %}"
|
||||
hx-target="#generic-offcanvas"
|
||||
hx-trigger="click, add_income from:window"
|
||||
hx-vals='{"year": {{ year }}, "type": "IN"}'>
|
||||
<i class="fa-solid fa-arrow-right-to-bracket me-2"></i>
|
||||
{% translate "Income" %}
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-danger"
|
||||
hx-get="{% url 'transaction_add' %}"
|
||||
hx-target="#generic-offcanvas"
|
||||
hx-trigger="click, add_expense from:window"
|
||||
hx-vals='{"year": {{ year }}, "type": "EX"}'>
|
||||
<i class="fa-solid fa-arrow-right-from-bracket me-2"></i>
|
||||
{% translate "Expense" %}
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-warning"
|
||||
hx-get="{% url 'installment_plan_add' %}"
|
||||
hx-trigger="click, installment from:window"
|
||||
hx-target="#generic-offcanvas">
|
||||
<i class="fa-solid fa-divide me-2"></i>
|
||||
{% translate "Installment" %}
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-warning"
|
||||
hx-get="{% url 'recurring_transaction_add' %}"
|
||||
hx-trigger="click, balance from:window"
|
||||
hx-target="#generic-offcanvas">
|
||||
<i class="fa-solid fa-repeat me-2"></i>
|
||||
{% translate "Recurring" %}
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-info"
|
||||
hx-get="{% url 'transactions_transfer' %}"
|
||||
hx-target="#generic-offcanvas"
|
||||
hx-trigger="click, add_transfer from:window"
|
||||
hx-vals='{"year": {{ year }}}'>
|
||||
<i class="fa-solid fa-money-bill-transfer me-2"></i>
|
||||
{% translate "Transfer" %}
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-info"
|
||||
hx-get="{% url 'account_reconciliation' %}"
|
||||
hx-trigger="click, balance from:window"
|
||||
hx-target="#generic-offcanvas">
|
||||
<i class="fa-solid fa-scale-balanced me-2"></i>
|
||||
{% translate "Balance" %}
|
||||
</button>
|
||||
</div>
|
||||
<c-ui.quick-transactions-buttons
|
||||
:year="year"
|
||||
:month="month"
|
||||
></c-ui.quick-transactions-buttons>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
|
||||
@@ -3,13 +3,13 @@ volumes:
|
||||
wygiwyh_temp:
|
||||
|
||||
services:
|
||||
web: &django
|
||||
web:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./docker/dev/django/Dockerfile
|
||||
image: wygiwyh_dev_server
|
||||
container_name: wygiwyh_dev_server
|
||||
command: /start
|
||||
command: /start-supervisor
|
||||
volumes:
|
||||
- ./app/:/usr/src/app/:z
|
||||
- ./frontend/:/usr/src/frontend:z
|
||||
@@ -54,12 +54,12 @@ services:
|
||||
- '${SQL_PORT}:5432'
|
||||
restart: unless-stopped
|
||||
|
||||
procrastinate:
|
||||
<<: *django
|
||||
image: wygiwyh_dev_procrastinate
|
||||
container_name: wygiwyh_dev_procrastinate
|
||||
depends_on:
|
||||
- db
|
||||
ports: [ ]
|
||||
command: /start-procrastinate
|
||||
restart: unless-stopped
|
||||
# procrastinate:
|
||||
# <<: *django
|
||||
# image: wygiwyh_dev_procrastinate
|
||||
# container_name: wygiwyh_dev_procrastinate
|
||||
# depends_on:
|
||||
# - db
|
||||
# ports: [ ]
|
||||
# command: /start-procrastinate
|
||||
# restart: unless-stopped
|
||||
|
||||
@@ -2,15 +2,13 @@ services:
|
||||
web:
|
||||
image: eitchtee/wygiwyh:latest
|
||||
container_name: ${SERVER_NAME}
|
||||
command: /start
|
||||
command: /start-single
|
||||
ports:
|
||||
- "${OUTBOUND_PORT}:8000"
|
||||
env_file:
|
||||
- .env
|
||||
depends_on:
|
||||
- db
|
||||
volumes:
|
||||
- wygiwyh_temp:/usr/src/app/temp/
|
||||
restart: unless-stopped
|
||||
|
||||
db:
|
||||
@@ -23,18 +21,3 @@ services:
|
||||
- POSTGRES_USER=${SQL_USER}
|
||||
- POSTGRES_PASSWORD=${SQL_PASSWORD}
|
||||
- POSTGRES_DB=${SQL_DATABASE}
|
||||
|
||||
procrastinate:
|
||||
image: eitchtee/wygiwyh:latest
|
||||
container_name: ${PROCRASTINATE_NAME}
|
||||
depends_on:
|
||||
- db
|
||||
env_file:
|
||||
- .env
|
||||
volumes:
|
||||
- wygiwyh_temp:/usr/src/app/temp/
|
||||
command: /start-procrastinate
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
wygiwyh_temp:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM python:3.11-slim-buster AS python-build-stage
|
||||
FROM python:3.11-slim-bookworm AS python-build-stage
|
||||
|
||||
RUN apt-get update && apt-get install --no-install-recommends -y \
|
||||
build-essential \
|
||||
@@ -8,7 +8,10 @@ RUN apt-get update && apt-get install --no-install-recommends -y \
|
||||
COPY ../requirements.txt .
|
||||
RUN pip wheel --wheel-dir /usr/src/app/wheels -r requirements.txt
|
||||
|
||||
FROM python:3.11-slim-buster AS python-run-stage
|
||||
FROM python:3.11-slim-bookworm AS python-run-stage
|
||||
|
||||
ARG VERSION=dev
|
||||
ENV APP_VERSION=$VERSION
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
@@ -18,7 +21,7 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
COPY --from=python-build-stage /usr/src/app/wheels /wheels/
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install --no-install-recommends -y gettext && \
|
||||
apt-get install --no-install-recommends -y gettext supervisor && \
|
||||
rm -rf /var/lib/apt/lists/* && \
|
||||
pip install --upgrade pip && \
|
||||
pip install --no-cache-dir --no-index --find-links=/wheels/ /wheels/* && \
|
||||
@@ -26,9 +29,15 @@ RUN apt-get update && \
|
||||
|
||||
COPY ./docker/dev/django/start /start
|
||||
COPY ./docker/dev/procrastinate/start /start-procrastinate
|
||||
COPY ./docker/dev/supervisord/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
|
||||
COPY ./docker/dev/supervisord/supervisord.conf /etc/supervisord.conf
|
||||
COPY ./docker/dev/supervisord/start /start-supervisor
|
||||
|
||||
RUN sed -i 's/\r$//g' /start && \
|
||||
chmod +x /start && \
|
||||
sed -i 's/\r$//g' /start-procrastinate && \
|
||||
chmod +x /start-procrastinate
|
||||
chmod +x /start-procrastinate && \
|
||||
sed -i 's/\r$//g' /start-supervisor && \
|
||||
chmod +x /start-supervisor
|
||||
|
||||
COPY ./app .
|
||||
|
||||
9
docker/dev/supervisord/start
Normal file
9
docker/dev/supervisord/start
Normal file
@@ -0,0 +1,9 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
set -o nounset
|
||||
|
||||
export TASK_WORKERS=${TASK_WORKERS:=1}
|
||||
|
||||
exec supervisord -c /etc/supervisor/conf.d/supervisord.conf
|
||||
39
docker/dev/supervisord/supervisord.conf
Normal file
39
docker/dev/supervisord/supervisord.conf
Normal file
@@ -0,0 +1,39 @@
|
||||
[supervisord]
|
||||
nodaemon=true
|
||||
logfile=/dev/null
|
||||
logfile_maxbytes=0
|
||||
pidfile=/tmp/supervisord.pid
|
||||
user=root
|
||||
|
||||
[supervisorctl]
|
||||
serverurl=unix:///run/supervisord.sock
|
||||
|
||||
[rpcinterface:supervisor]
|
||||
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
|
||||
|
||||
[unix_http_server]
|
||||
file=/run/supervisord.sock
|
||||
chmod=0700
|
||||
|
||||
[program:web]
|
||||
directory=/usr/src/app
|
||||
command=/bin/bash /start
|
||||
stdout_logfile=/dev/fd/1
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/fd/2
|
||||
stderr_logfile_maxbytes=0
|
||||
autorestart=true
|
||||
startretries=5
|
||||
|
||||
[program:procrastinate]
|
||||
directory=/usr/src/app
|
||||
command=/bin/bash /start-procrastinate
|
||||
process_name=%(program_name)s_%(process_num)02d
|
||||
numprocs=%(ENV_TASK_WORKERS)s
|
||||
numprocs_start=1
|
||||
stdout_logfile=/dev/fd/1
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/fd/2
|
||||
stderr_logfile_maxbytes=0
|
||||
autorestart=true
|
||||
startretries=5
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM python:3.11-slim-buster AS python-build-stage
|
||||
FROM python:3.11-slim-bookworm AS python-build-stage
|
||||
|
||||
RUN apt-get update && apt-get install --no-install-recommends -y \
|
||||
build-essential \
|
||||
@@ -17,7 +17,11 @@ RUN --mount=type=cache,target=/root/.npm \
|
||||
npm install --verbose && \
|
||||
npm run build
|
||||
|
||||
FROM python:3.11-slim-buster AS python-run-stage
|
||||
FROM python:3.11-slim-bookworm AS python-run-stage
|
||||
|
||||
ARG VERSION=dev
|
||||
ENV APP_VERSION=$VERSION
|
||||
|
||||
COPY --from=webpack_build /usr/src/frontend/build /usr/src/frontend/build
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
@@ -31,7 +35,7 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
COPY --from=python-build-stage /usr/src/app/wheels /wheels/
|
||||
RUN --mount=type=cache,target=/root/.cache/apt \
|
||||
apt-get update && \
|
||||
apt-get install --no-install-recommends -y gettext && \
|
||||
apt-get install --no-install-recommends -y gettext supervisor && \
|
||||
rm -rf /var/lib/apt/lists/* && \
|
||||
pip install --upgrade pip && \
|
||||
pip install --no-cache-dir --no-index --find-links=/wheels/ /wheels/* && \
|
||||
@@ -39,10 +43,15 @@ RUN --mount=type=cache,target=/root/.cache/apt \
|
||||
|
||||
COPY --chown=app:app ./docker/prod/django/start /start
|
||||
COPY --chown=app:app ./docker/prod/procrastinate/start /start-procrastinate
|
||||
COPY --chown=app:app ./docker/prod/supervisord/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
|
||||
COPY --chown=app:app ./docker/prod/supervisord/supervisord.conf /etc/supervisord.conf
|
||||
COPY --chown=app:app ./docker/prod/supervisord/start /start-single
|
||||
RUN sed -i 's/\r$//g' /start && \
|
||||
chmod +x /start && \
|
||||
sed -i 's/\r$//g' /start-procrastinate && \
|
||||
chmod +x /start-procrastinate
|
||||
chmod +x /start-procrastinate && \
|
||||
sed -i 's/\r$//g' /start-single && \
|
||||
chmod +x /start-single
|
||||
|
||||
COPY --chown=app:app ./app .
|
||||
|
||||
|
||||
9
docker/prod/supervisord/start
Normal file
9
docker/prod/supervisord/start
Normal file
@@ -0,0 +1,9 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
set -o nounset
|
||||
|
||||
export TASK_WORKERS=${TASK_WORKERS:=1}
|
||||
|
||||
exec supervisord -c /etc/supervisor/conf.d/supervisord.conf
|
||||
37
docker/prod/supervisord/supervisord.conf
Normal file
37
docker/prod/supervisord/supervisord.conf
Normal file
@@ -0,0 +1,37 @@
|
||||
[supervisord]
|
||||
nodaemon=true
|
||||
logfile=/dev/null
|
||||
logfile_maxbytes=0
|
||||
pidfile=/tmp/supervisord.pid
|
||||
|
||||
[supervisorctl]
|
||||
serverurl=unix:///tmp/supervisord.sock
|
||||
|
||||
[unix_http_server]
|
||||
file=/tmp/supervisord.sock
|
||||
chmod=0700
|
||||
|
||||
[program:web]
|
||||
user=app
|
||||
directory=/usr/src/app
|
||||
command=/bin/bash /start
|
||||
stdout_logfile=/dev/fd/1
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/fd/2
|
||||
stderr_logfile_maxbytes=0
|
||||
autorestart=true
|
||||
startretries=5
|
||||
|
||||
[program:procrastinate]
|
||||
user=app
|
||||
directory=/usr/src/app
|
||||
command=/bin/bash /start-procrastinate
|
||||
process_name=%(program_name)s_%(process_num)02d
|
||||
numprocs=%(ENV_TASK_WORKERS)s
|
||||
numprocs_start=1
|
||||
stdout_logfile=/dev/fd/1
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/fd/2
|
||||
stderr_logfile_maxbytes=0
|
||||
autorestart=true
|
||||
startretries=5
|
||||
@@ -1,11 +1,13 @@
|
||||
import AirDatepicker from 'air-datepicker';
|
||||
import en from 'air-datepicker/locale/en';
|
||||
import ptBr from 'air-datepicker/locale/pt-BR';
|
||||
import nl from 'air-datepicker/locale/nl';
|
||||
import {createPopper} from '@popperjs/core';
|
||||
|
||||
const locales = {
|
||||
'pt': ptBr,
|
||||
'en': en
|
||||
'en': en,
|
||||
'nl': nl
|
||||
};
|
||||
|
||||
function isMobileDevice() {
|
||||
@@ -161,8 +163,8 @@ window.MonthYearPicker = function createDynamicDatePicker(element) {
|
||||
let opts = {...baseOpts, ...positionConfig};
|
||||
|
||||
if (element.dataset.value) {
|
||||
opts["selectedDates"] = [element.dataset.value];
|
||||
opts["startDate"] = [element.dataset.value];
|
||||
opts["selectedDates"] = [new Date(element.dataset.value + "T00:00:00")];
|
||||
opts["startDate"] = [new Date(element.dataset.value + "T00:00:00")];
|
||||
}
|
||||
return new AirDatepicker(element, opts);
|
||||
};
|
||||
|
||||
@@ -26,3 +26,4 @@ python-dateutil~=2.9.0.post0
|
||||
simpleeval~=1.0.0
|
||||
pydantic~=2.10.5
|
||||
PyYAML~=6.0.2
|
||||
mistune~=3.1.1
|
||||
|
||||
Reference in New Issue
Block a user