Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Pricing] Add option to convert received items currency #8970

Merged
merged 42 commits into from
Feb 3, 2025
Merged
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
778407f
Add new global setting
SchrodingersGat Jan 28, 2025
82e4270
Convert to base currency on receipt
SchrodingersGat Jan 28, 2025
7c6b3d4
Fix total price rendering in PO table
SchrodingersGat Jan 28, 2025
4e9e528
Fix for tasks.py
SchrodingersGat Jan 28, 2025
8cfdc3c
Update .gitignore
SchrodingersGat Jan 28, 2025
7ce8de7
Update docs
SchrodingersGat Jan 28, 2025
44f2a63
Updates
SchrodingersGat Jan 28, 2025
0ad42f5
Fix caching for default currency
SchrodingersGat Jan 28, 2025
7f86faa
Add unit test for new feature
SchrodingersGat Jan 28, 2025
044a763
Merge branch 'master' into po-currency
SchrodingersGat Jan 29, 2025
7e6daae
Playwright test fixes
SchrodingersGat Jan 29, 2025
de0c10a
Validate copying of media files
SchrodingersGat Jan 29, 2025
da4ea4c
Merge branch 'master' into po-currency
SchrodingersGat Jan 29, 2025
2fe7cb6
Validate media files
SchrodingersGat Jan 29, 2025
dc8a318
Merge branch 'master' into po-currency
SchrodingersGat Jan 29, 2025
7246fc7
Adjust playwright setup
SchrodingersGat Jan 29, 2025
e922592
Merge remote-tracking branch 'origin/master' into po-currency
SchrodingersGat Jan 29, 2025
c238fc1
Merge branch 'master' into po-currency
matmair Jan 29, 2025
be87657
Merge branch 'master' into po-currency
SchrodingersGat Jan 30, 2025
f3cf257
Merge branch 'master' into po-currency
SchrodingersGat Jan 30, 2025
6cb3254
Merge branch 'master' into po-currency
SchrodingersGat Jan 31, 2025
79d40f0
Merge branch 'master' into po-currency
SchrodingersGat Feb 1, 2025
b7a9acc
Allow multiple attempts to fetch release information
SchrodingersGat Feb 1, 2025
8388d68
Merge remote-tracking branch 'origin/master' into po-currency
SchrodingersGat Feb 1, 2025
26fa658
Merge branch 'master' into po-currency
SchrodingersGat Feb 1, 2025
c6b724a
Merge branch 'master' into po-currency
SchrodingersGat Feb 1, 2025
3d60878
Tweak unit tests
SchrodingersGat Feb 1, 2025
008a04d
Merge branch 'master' into po-currency
SchrodingersGat Feb 1, 2025
93b76e7
Revert changes to .gitignore file
SchrodingersGat Feb 1, 2025
b76b9c0
Add debug msg
SchrodingersGat Feb 1, 2025
3dbe58d
Try hard-coded paths
SchrodingersGat Feb 1, 2025
f07c977
Merge branch 'master' into po-currency
SchrodingersGat Feb 2, 2025
709853e
Remove debug prints
SchrodingersGat Feb 2, 2025
4432249
Abs path for database
SchrodingersGat Feb 2, 2025
3980a4e
More debug
SchrodingersGat Feb 2, 2025
be9e3bd
Fix typos
SchrodingersGat Feb 2, 2025
453025e
Revert change to db name
SchrodingersGat Feb 2, 2025
b5865a8
Remove debug statements (again)
SchrodingersGat Feb 2, 2025
2e874ee
Merge remote-tracking branch 'origin/master' into po-currency
SchrodingersGat Feb 2, 2025
6df1a52
Cleanup playwright tests
SchrodingersGat Feb 3, 2025
3e83c39
More test tweaks
SchrodingersGat Feb 3, 2025
d9f723a
Merge branch 'master' into po-currency
SchrodingersGat Feb 3, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions .github/workflows/qc_checks.yaml
Original file line number Diff line number Diff line change
@@ -17,10 +17,11 @@ env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
INVENTREE_DB_ENGINE: sqlite3
INVENTREE_DB_NAME: inventree
INVENTREE_MEDIA_ROOT: ../test_inventree_media
INVENTREE_STATIC_ROOT: ../test_inventree_static
INVENTREE_BACKUP_DIR: ../test_inventree_backup
INVENTREE_MEDIA_ROOT: /home/runner/work/InvenTree/test_inventree_media
INVENTREE_STATIC_ROOT: /home/runner/work/InvenTree/test_inventree_static
INVENTREE_BACKUP_DIR: /home/runner/work/InvenTree/test_inventree_backup
INVENTREE_SITE_URL: http://localhost:8000
INVENTREE_DEBUG: true

permissions:
contents: read
@@ -570,7 +571,7 @@ jobs:
install: true
update: true
- name: Set up test data
run: invoke dev.setup-test -i
run: invoke dev.setup-test -iv
- name: Rebuild thumbnails
run: invoke int.rebuild-thumbnails
- name: Install dependencies
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -112,7 +112,7 @@ api.yaml
src/backend/InvenTree/web/static
InvenTree/web/static

# Generated docs files
# Exported interim files
docs/schema.yml
docs/docs/api/*.yml
docs/docs/api/schema/*.yml
65 changes: 65 additions & 0 deletions docs/docs/concepts/pricing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
---
title: Pricing Support
---

## Pricing

Pricing is an inherently complex topic, often subject to the particular requirements of the user. InvenTree attempts to provide a comprehensive pricing architecture which is useful without being proscriptive.

InvenTree provides support for multiple currencies, allowing pricing information to be stored with base currency rates.

!!! warning "Raw Data Only"
InvenTree stores raw pricing data, as provided by the user. Any calculations or decisions based on this data must take into consideration the context in which the data are entered.

InvenTree uses the [django-money](https://github.com/django-money/django-money) library, which in turn uses the [py-moneyed library](https://py-moneyed.readthedocs.io/en/latest/index.html). `py-moneyed` supports any currency which is defined in the [ISO 3166 standard](https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes) standard.


### Terminology

Throughout this documentation (and within InvenTree) the concepts of *cost* and *price* are separated as follows:

| Term | Description |
| --- | --- |
| Price | The theoretical amount of money required to pay for something. |
| Cost | The actual amount of money paid. |


## Currency Support

InvenTree supports pricing data in multiple currencies, allowing integration with suppliers and customers using different currency systems.

### Default Currency

Many of the pricing operations are performed in reference to a *Default Currency* (which can be selected for the particular InvenTree installation).

The default currency is user configurable in the InvenTree settings.

!!! warning "Setting Default Currency"
Changing the default currency once the system in use may have unintended consequences. It is recommended to set the default currency during the initial setup of the InvenTree instance.

## Conversion Rates

To facilitate conversion between different currencies, exchange rate information is stored in the InvenTree database.

### Currency Codes

The list of support currency codes is user configurable in the InvenTree settings. It is recommended to select only the currencies which are relevant to the user.

While InvenTree can support any of the currencies defined in the ISO 3166 standard, the list of supported currencies can be limited to only those which are relevant to the user. The supported currencies are used to populate the currency selection dropdowns throughout the InvenTree interface.


### Exchange Rate Data

The exchange rate data is provided by a [currency plugin](../extend/plugins/currency.md) which fetches exchange rate data from an external source.

InvenTree includes a default currency plugin which fetches exchange rate data from the [frankfurter](https://frankfurter.dev/) API, which is an open source currency API made freely available.

However, the user can configure a custom currency plugin to fetch exchange rate data from a different source. If a different currency exchange backend is needed, or a custom implementation is desired, the currency exchange framework can be extended [via plugins](../extend/plugins/currency.md). Plugins which implement custom currency exchange frameworks can be easily integrated into the InvenTree framework.

### Exchange Rate Updates

Currency exchange rates are updated periodically, using the configured currency plugin. The update frequency can be configured in the InvenTree settings.

## Pricing Settings

Refer to the [global settings](../settings/global.md#pricing-and-currency) documentation for more information on available currency settings.
14 changes: 12 additions & 2 deletions docs/docs/hooks.py
Original file line number Diff line number Diff line change
@@ -113,8 +113,18 @@ def get_release_data():
while 1:
url = f'https://api.github.com/repos/inventree/inventree/releases?page={page}&per_page=150'

response = requests.get(url, timeout=30)
assert response.status_code == 200
attempts = 5

while attempts > 0:
attempts -= 1

response = requests.get(url, timeout=30)
if response.status_code == 200:
break

assert response.status_code == 200, (
f'Failed to fetch release data: {response.status_code} - {url}'
)

data = json.loads(response.text)

20 changes: 17 additions & 3 deletions docs/docs/order/purchase_order.md
Original file line number Diff line number Diff line change
@@ -42,7 +42,14 @@ Purchase Order Status supports [custom states](../concepts/custom_states.md).

### Purchase Order Currency

The currency code can be specified for an individual purchase order. If not specified, the default currency specified against the [supplier](./company.md#suppliers) will be used.
The currency code can be specified for an individual purchase order. If not specified, the default currency specified against the [supplier](./company.md#suppliers) will be used. Additionally, the currency can be specified separately for each line item.

So, when determining the cost of each line item in the purchase order, the following order of precedence is used:

1. Line item currency
2. Purchase order currency
3. Supplier currency
4. Default (base) currency

## Create Purchase Order

@@ -77,11 +84,11 @@ It is possible to upload an exported purchase order from the supplier instead of
!!! info "Supported Formats"
This process only supports tabular data and the following formats are supported: CSV, TSV, XLS, XLSX, JSON and YAML

### Issue Order
## Issue Order

Once all the line items were added, click on the <span class='fas fa-paper-plane'></span> button on the main purchase order detail panel and confirm the order has been submitted.

### Receive Line Items
## Receive Line Items

After receiving all the items from the order, the purchase order will convert the line items into stock items / inventory.

@@ -107,6 +114,12 @@ Each item marked as "received" is automatically converted into a stock item.

To see the list of stock items created from the purchase order, click on the <span class="badge inventree nav side"><span class='fas fa-sign-in-alt'></span> Received Items</span> tab.

### Item Value Currency

The unit cost of the purchase order line item is transferred across to the created stock item. By default, the same currency is used for the stock item as was used for the purchase order line item.

However, if the [Convert Currency](#purchase-order-settings) setting is enabled, the currency of the stock item will be converted to the [default currency](../concepts/pricing.md#default-currency) of the system. This may be useful when ordering stock in a different currency, to ensure that the unit cost of the stock item is converted to the base currency at the time of receipt.

## Complete Order

Once the quantity of all __received__ items is equal or above the quantity of all line items, the order will be automatically marked as __complete__.
@@ -174,5 +187,6 @@ The following [global settings](../settings/global.md) are available for purchas
| ---- | ----------- | ------- | ----- |
{{ globalsetting("PURCHASEORDER_REFERENCE_PATTERN") }}
{{ globalsetting("PURCHASEORDER_REQUIRE_RESPONSIBLE") }}
{{ globalsetting("PURCHASEORDER_CONVERT_CURRENCY") }}
{{ globalsetting("PURCHASEORDER_EDIT_COMPLETED_ORDERS") }}
{{ globalsetting("PURCHASEORDER_AUTO_COMPLETE") }}
43 changes: 5 additions & 38 deletions docs/docs/part/pricing.md
Original file line number Diff line number Diff line change
@@ -2,23 +2,10 @@
title: Pricing
---

## Pricing
## Part Pricing

Pricing is an inherently complex topic, often subject to the particular requirements of the user. InvenTree attempts to provide a comprehensive pricing architecture which is useful without being proscriptive.

!!! warning "Raw Data Only"
InvenTree stores raw pricing data, as provided by the user. Any calculations or decisions based on this data must take into consideration the context in which the data are entered.

### Terminology

Throughout this documentation (and within InvenTree) the concepts of *cost* and *price* are separated as follows:

| Term | Description |
| --- | --- |
| Price | The theoretical amount of money required to pay for something. |
| Cost | The actual amount of money paid. |

### Pricing Sources
!!! info "Pricing Support"
Refer to the [Pricing Support](../concepts/pricing.md) documentation for more information on pricing support in InvenTree.

Pricing information can be determined from multiple sources:

@@ -42,26 +29,6 @@ Additionally, the following information is stored for each part, in relation to
| Sale Price | How much a salable item is sold for (with price-breaks) | [Part](../part/part.md) |
| Sale Cost | How much an item was sold for | [Sales Order](../order/sales_order.md) |

### Currency Support

InvenTree supports pricing data in multiple currencies, allowing integration with suppliers and customers using different currency systems.

Supported currencies can be configured in the [InvenTree settings](../settings/currency.md).

!!! info "Currency Support"
InvenTree provides multi-currency pricing support via the [django-money](https://django-money.readthedocs.io/en/latest/) library.

#### Default Currency

Many of the pricing operations are performed in reference to a *Default Currency* (which can be selected for the particular InvenTree installation).

#### Conversion Rates

To facilitate conversion between different currencies, exchange rate data is provided via the [exchangerate.host](https://exchangerate.host/#/) API. Currency exchange rates are updated once per day.

!!! tip "Custom Exchange Rates"
Custom exchange rates or databases can be used if desired.

## Pricing Tab

The pricing tab for a given Part provides all available pricing information for that part. It shows all price ranges and provides tools to calculate them.
@@ -81,7 +48,7 @@ At the top of the pricing tab, an *Overview* section shows a synopsis of the ava

This overview tab provides information on the *range* of pricing data available within each category. If pricing data is not available for a given category, it is marked as *No data*.

Each price range is calculated in the [Default Currency](#default-currency), independent of the currency in which the original pricing information is stored. This is necessary for operations such as data sorting, price comparison, etc. Note that while the *overview* information is calculated in a single currency, the original pricing information is still available in the original currency.
Each price range is calculated in the [Default Currency](../concepts/pricing.md#default-currency), independent of the currency in which the original pricing information is stored. This is necessary for operations such as data sorting, price comparison, etc. Note that while the *overview* information is calculated in a single currency, the original pricing information is still available in the original currency.

Price range data is [cached in the database](#price-data-caching) when underlying pricing information changes.

@@ -167,7 +134,7 @@ Pricing calculations (and conversions) can be expensive to perform. This can mak

For this reason, all information displayed in the [pricing overview](#pricing-overview) section is pre-calculated and *cached* in the database. This ensures that when it needs to be retrieved (e.g. viewing pricing for an entire BOM) it can be accessed immediately.

Pricing data is cached in the [default currency](#default-currency), which ensures that pricing can be compared across multiple parts in a consistent format.
Pricing data is cached in the [default currency](../concepts/pricing.md/#default-currency), which ensures that pricing can be compared across multiple parts in a consistent format.

#### Data Updates

31 changes: 0 additions & 31 deletions docs/docs/settings/currency.md

This file was deleted.

11 changes: 9 additions & 2 deletions docs/docs/settings/global.md
Original file line number Diff line number Diff line change
@@ -101,11 +101,18 @@ Configuration of pricing data and currency support:
| ---- | ----------- | ------- | ----- |
{{ globalsetting("INVENTREE_DEFAULT_CURRENCY") }}
{{ globalsetting("CURRENCY_CODES") }}
{{ globalsetting("PART_INTERNAL_PRICE") }}
{{ globalsetting("PART_BOM_USE_INTERNAL_PRICE") }}
{{ globalsetting("PRICING_DECIMAL_PLACES_MIN") }}
{{ globalsetting("PRICING_DECIMAL_PLACES") }}
{{ globalsetting("PRICING_UPDATE_DAYS") }}

#### Part Pricing

Configuration of part pricing:

| Name | Description | Default | Units |
| ---- | ----------- | ------- | ----- |
{{ globalsetting("PART_INTERNAL_PRICE") }}
{{ globalsetting("PART_BOM_USE_INTERNAL_PRICE") }}
{{ globalsetting("PRICING_USE_SUPPLIER_PRICING") }}
{{ globalsetting("PRICING_PURCHASE_HISTORY_OVERRIDES_SUPPLIER") }}
{{ globalsetting("PRICING_USE_STOCK_PRICING") }}
2 changes: 1 addition & 1 deletion docs/mkdocs.yml
Original file line number Diff line number Diff line change
@@ -78,6 +78,7 @@ nav:
- Terminology: concepts/terminology.md
- Physical Units: concepts/units.md
- Custom States: concepts/custom_states.md
- Pricing: concepts/pricing.md
- Development:
- Contributing: develop/contributing.md
- Devcontainer: develop/devcontainer.md
@@ -157,7 +158,6 @@ nav:
- Single Sign on: settings/SSO.md
- Multi Factor Authentication: settings/MFA.md
- Email: settings/email.md
- Currency Support: settings/currency.md
- Export Data: settings/export.md
- Import Data: settings/import.md
- Operations:
2 changes: 1 addition & 1 deletion src/backend/InvenTree/InvenTree/config.py
Original file line number Diff line number Diff line change
@@ -150,7 +150,7 @@ def do_typecast(value, type, var_name=None):
var_name: Name that should be logged e.g. 'INVENTREE_STATIC_ROOT'. Set if logging is required.

Returns:
Typecasted value or original value if typecasting failed.
Typecast value or original value if typecasting failed.
"""
# Force 'list' of strings
if type is list:
3 changes: 2 additions & 1 deletion src/backend/InvenTree/InvenTree/exceptions.py
Original file line number Diff line number Diff line change
@@ -9,7 +9,6 @@
from django.core.exceptions import ValidationError as DjangoValidationError
from django.utils.translation import gettext_lazy as _

import rest_framework.views as drfviews
import structlog
from rest_framework import serializers
from rest_framework.exceptions import ValidationError as DRFValidationError
@@ -77,6 +76,8 @@ def exception_handler(exc, context):

If sentry error reporting is enabled, we will also provide the original exception to sentry.io
"""
import rest_framework.views as drfviews

import InvenTree.sentry

response = None
2 changes: 1 addition & 1 deletion src/backend/InvenTree/InvenTree/fields.py
Original file line number Diff line number Diff line change
@@ -33,7 +33,7 @@ def __init__(self, **kwargs):
self.validators[-1].schemes = allowable_url_schemes()

def run_validation(self, data=empty):
"""Override default validation behaviour for this field type."""
"""Override default validation behavior for this field type."""
strict_urls = get_global_setting('INVENTREE_STRICT_URLS', cache=False)

if not strict_urls and data is not empty and '://' not in data:
2 changes: 1 addition & 1 deletion src/backend/InvenTree/InvenTree/filters.py
Original file line number Diff line number Diff line change
@@ -92,7 +92,7 @@ class InvenTreeOrderingFilter(filters.OrderingFilter):

Then, specify a ordering_field_aliases attribute:

ordering_field_alises = {
ordering_field_aliases = {
'name': 'part__part__name',
'SKU': 'part__SKU',
}
2 changes: 1 addition & 1 deletion src/backend/InvenTree/InvenTree/middleware.py
Original file line number Diff line number Diff line change
@@ -157,7 +157,7 @@ class CustomAllauthTwoFactorMiddleware(AllauthTwoFactorMiddleware):
"""This function ensures only frontend code triggers the MFA auth cycle."""

def process_request(self, request):
"""Check if requested url is forntend and enforce MFA check."""
"""Check if requested url is frontend and enforce MFA check."""
try:
if not url_matcher.resolve(request.path[1:]):
super().process_request(request)
2 changes: 1 addition & 1 deletion src/backend/InvenTree/InvenTree/mixins.py
Original file line number Diff line number Diff line change
@@ -96,7 +96,7 @@ def clean_string(self, field: str, data: str) -> str:
def clean_data(self, data: dict) -> dict:
"""Clean / sanitize data.

This uses mozillas bleach under the hood to disable certain html tags by
This uses Mozilla's bleach under the hood to disable certain html tags by
encoding them - this leads to script tags etc. to not work.
The results can be longer then the input; might make some character combinations
`ugly`. Prevents XSS on the server-level.
2 changes: 1 addition & 1 deletion src/backend/InvenTree/InvenTree/permissions.py
Original file line number Diff line number Diff line change
@@ -16,7 +16,7 @@ def get_model_for_view(view):
return view.serializer_class.Meta.model

if hasattr(view, 'get_serializer_class'):
return view.get_serializr_class().Meta.model
return view.get_serializer_class().Meta.model

raise AttributeError(f'Serializer class not specified for {view.__class__}')

Loading

Unchanged files with check annotations Beta

await page.waitForURL('**/platform/home');
await page.getByLabel('navigation-menu').waitFor();
await page.getByText(/InvenTree Demo Server -/).waitFor();

Check failure on line 39 in src/frontend/tests/login.ts

GitHub Actions / Tests - Platform UI

[chromium] › pages/pui_part.spec.ts:249:1 › Parts - Pricing (Variant)

3) [chromium] › pages/pui_part.spec.ts:249:1 › Parts - Pricing (Variant) ───────────────────────── Error: locator.waitFor: Test timeout of 90000ms exceeded. Call log: - waiting for getByText(/InvenTree Demo Server -/) to be visible at login.ts:39 37 | 38 | await page.getByLabel('navigation-menu').waitFor(); > 39 | await page.getByText(/InvenTree Demo Server -/).waitFor(); | ^ 40 | 41 | // Wait for the dashboard to load 42 | await page.getByText('No widgets selected').waitFor(); at doQuickLogin (/home/runner/work/InvenTree/InvenTree/src/frontend/tests/login.ts:39:51) at /home/runner/work/InvenTree/InvenTree/src/frontend/tests/pages/pui_part.spec.ts:250:3

Check failure on line 39 in src/frontend/tests/login.ts

GitHub Actions / Tests - Platform UI

[chromium] › pages/pui_part.spec.ts:344:1 › Parts - Parameters

4) [chromium] › pages/pui_part.spec.ts:344:1 › Parts - Parameters ──────────────────────────────── Error: locator.waitFor: Test timeout of 90000ms exceeded. Call log: - waiting for getByText(/InvenTree Demo Server -/) to be visible at login.ts:39 37 | 38 | await page.getByLabel('navigation-menu').waitFor(); > 39 | await page.getByText(/InvenTree Demo Server -/).waitFor(); | ^ 40 | 41 | // Wait for the dashboard to load 42 | await page.getByText('No widgets selected').waitFor(); at doQuickLogin (/home/runner/work/InvenTree/InvenTree/src/frontend/tests/login.ts:39:51) at /home/runner/work/InvenTree/InvenTree/src/frontend/tests/pages/pui_part.spec.ts:345:3
// Wait for the dashboard to load
await page.getByText('No widgets selected').waitFor();