diff --git a/InvenTree/InvenTree/api_version.py b/InvenTree/InvenTree/api_version.py
index a9fa115293..b39d3f1e43 100644
--- a/InvenTree/InvenTree/api_version.py
+++ b/InvenTree/InvenTree/api_version.py
@@ -2,11 +2,14 @@
# InvenTree API version
-INVENTREE_API_VERSION = 109
+INVENTREE_API_VERSION = 110
"""
Increment this API version number whenever there is a significant change to the API that any clients need to know about
+v110 -> 2023-04-26 : https://github.com/inventree/InvenTree/pull/4698
+ - Adds 'order_currency' field for PurchaseOrder / SalesOrder endpoints
+
v109 -> 2023-04-19 : https://github.com/inventree/InvenTree/pull/4636
- Adds API endpoints for the "ProjectCode" model
diff --git a/InvenTree/InvenTree/serializers.py b/InvenTree/InvenTree/serializers.py
index 018f2332f7..405d2c28bf 100644
--- a/InvenTree/InvenTree/serializers.py
+++ b/InvenTree/InvenTree/serializers.py
@@ -73,10 +73,17 @@ class InvenTreeCurrencySerializer(serializers.ChoiceField):
def __init__(self, *args, **kwargs):
"""Initialize the currency serializer"""
- kwargs['choices'] = currency_code_mappings()
+ choices = currency_code_mappings()
+
+ allow_blank = kwargs.get('allow_blank', False) or kwargs.get('allow_null', False)
+
+ if allow_blank:
+ choices = [('', '---------')] + choices
+
+ kwargs['choices'] = choices
if 'default' not in kwargs and 'required' not in kwargs:
- kwargs['default'] = currency_code_default
+ kwargs['default'] = '' if allow_blank else currency_code_default
if 'label' not in kwargs:
kwargs['label'] = _('Currency')
diff --git a/InvenTree/order/migrations/0093_auto_20230426_0248.py b/InvenTree/order/migrations/0093_auto_20230426_0248.py
new file mode 100644
index 0000000000..cefb7ed724
--- /dev/null
+++ b/InvenTree/order/migrations/0093_auto_20230426_0248.py
@@ -0,0 +1,29 @@
+# Generated by Django 3.2.18 on 2023-04-26 02:48
+
+import InvenTree.validators
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('order', '0092_auto_20230419_0250'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='purchaseorder',
+ name='order_currency',
+ field=models.CharField(blank=True, help_text='Currency for this order (leave blank to use company default)', max_length=3, null=True, validators=[InvenTree.validators.validate_currency_code], verbose_name='Order Currency'),
+ ),
+ migrations.AddField(
+ model_name='returnorder',
+ name='order_currency',
+ field=models.CharField(blank=True, help_text='Currency for this order (leave blank to use company default)', max_length=3, null=True, validators=[InvenTree.validators.validate_currency_code], verbose_name='Order Currency'),
+ ),
+ migrations.AddField(
+ model_name='salesorder',
+ name='order_currency',
+ field=models.CharField(blank=True, help_text='Currency for this order (leave blank to use company default)', max_length=3, null=True, validators=[InvenTree.validators.validate_currency_code], verbose_name='Order Currency'),
+ ),
+ ]
diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py
index 6146305aef..94b3e22fb7 100644
--- a/InvenTree/order/models.py
+++ b/InvenTree/order/models.py
@@ -25,6 +25,7 @@ from mptt.models import TreeForeignKey
import InvenTree.helpers
import InvenTree.ready
import InvenTree.tasks
+import InvenTree.validators
import order.validators
import stock.models
import users.models as UserModels
@@ -69,10 +70,36 @@ class TotalPriceMixin(models.Model):
help_text=_('Total price for this order')
)
+ order_currency = models.CharField(
+ max_length=3,
+ verbose_name=_('Order Currency'),
+ blank=True, null=True,
+ help_text=_('Currency for this order (leave blank to use company default)'),
+ validators=[InvenTree.validators.validate_currency_code]
+ )
+
+ @property
+ def currency(self):
+ """Return the currency associated with this order instance:
+
+ - If the order_currency field is set, return that
+ - Otherwise, return the currency associated with the company
+ - Finally, return the default currency code
+ """
+
+ if self.order_currency:
+ return self.order_currency
+
+ if self.company:
+ return self.company.currency_code
+
+ # Return default currency code
+ return currency_code_default()
+
def update_total_price(self, commit=True):
"""Recalculate and save the total_price for this order"""
- self.total_price = self.calculate_total_price()
+ self.total_price = self.calculate_total_price(target_currency=self.currency)
if commit:
self.save()
diff --git a/InvenTree/order/serializers.py b/InvenTree/order/serializers.py
index bc993fb8a4..fa315149ba 100644
--- a/InvenTree/order/serializers.py
+++ b/InvenTree/order/serializers.py
@@ -41,7 +41,13 @@ class TotalPriceMixin(serializers.Serializer):
read_only=True,
)
- total_price_currency = InvenTreeCurrencySerializer(read_only=True)
+ order_currency = InvenTreeCurrencySerializer(
+ allow_blank=True,
+ allow_null=True,
+ required=False,
+ label=_('Order Currency'),
+ help_text=_('Currency for this order (leave blank to use company default)'),
+ )
class AbstractOrderSerializer(serializers.Serializer):
@@ -168,7 +174,7 @@ class PurchaseOrderSerializer(TotalPriceMixin, AbstractOrderSerializer, InvenTre
'supplier_detail',
'supplier_reference',
'total_price',
- 'total_price_currency',
+ 'order_currency',
])
read_only_fields = [
@@ -178,7 +184,8 @@ class PurchaseOrderSerializer(TotalPriceMixin, AbstractOrderSerializer, InvenTre
]
extra_kwargs = {
- 'supplier': {'required': True}
+ 'supplier': {'required': True},
+ 'order_currency': {'required': False},
}
def __init__(self, *args, **kwargs):
@@ -707,7 +714,7 @@ class SalesOrderSerializer(TotalPriceMixin, AbstractOrderSerializer, InvenTreeMo
'customer_reference',
'shipment_date',
'total_price',
- 'total_price_currency',
+ 'order_currency',
])
read_only_fields = [
@@ -716,6 +723,10 @@ class SalesOrderSerializer(TotalPriceMixin, AbstractOrderSerializer, InvenTreeMo
'shipment_date',
]
+ extra_kwargs = {
+ 'order_currency': {'required': False},
+ }
+
def __init__(self, *args, **kwargs):
"""Initialization routine for the serializer"""
customer_detail = kwargs.pop('customer_detail', False)
diff --git a/InvenTree/order/templates/order/order_base.html b/InvenTree/order/templates/order/order_base.html
index 3594703afb..bce90acfa2 100644
--- a/InvenTree/order/templates/order/order_base.html
+++ b/InvenTree/order/templates/order/order_base.html
@@ -215,7 +215,7 @@ src="{% static 'img/blank_image.png' %}"
{{ order.responsible }} |
{% endif %}
-
+ {% include "currency_data.html" with instance=order %}
|
{% trans "Total cost" %} |
@@ -224,7 +224,7 @@ src="{% static 'img/blank_image.png' %}"
{% if tp == None %}
{% trans "Total cost could not be calculated" %}
{% else %}
- {% render_currency tp currency=order.supplier.currency %}
+ {% render_currency tp currency=order.currency %}
{% endif %}
{% endwith %}
diff --git a/InvenTree/order/templates/order/purchase_order_detail.html b/InvenTree/order/templates/order/purchase_order_detail.html
index 4a2a095aa0..0e5839c6b5 100644
--- a/InvenTree/order/templates/order/purchase_order_detail.html
+++ b/InvenTree/order/templates/order/purchase_order_detail.html
@@ -180,9 +180,7 @@ $('#new-po-line').click(function() {
createPurchaseOrderLineItem({{ order.pk }}, {
{% if order.supplier %}
supplier: {{ order.supplier.pk }},
- {% if order.supplier.currency %}
- currency: '{{ order.supplier.currency }}',
- {% endif %}
+ currency: '{{ order.currency }}',
{% endif %}
onSuccess: function() {
$('#po-line-table').bootstrapTable('refresh');
@@ -235,9 +233,7 @@ onPanelLoad('order-items', function() {
order: {{ order.pk }},
table: '#po-extra-lines-table',
url: '{% url "api-po-extra-line-list" %}',
- {% if order.supplier.currency %}
- currency: '{{ order.supplier.currency }}',
- {% endif %}
+ currency: '{{ order.currency }}',
});
});
diff --git a/InvenTree/order/templates/order/return_order_base.html b/InvenTree/order/templates/order/return_order_base.html
index 0499cfa232..178ea71b46 100644
--- a/InvenTree/order/templates/order/return_order_base.html
+++ b/InvenTree/order/templates/order/return_order_base.html
@@ -183,7 +183,7 @@ src="{% static 'img/blank_image.png' %}"
{{ order.responsible }} |
{% endif %}
-
+ {% include "currency_data.html" with instance=order %}
|
{% trans "Total Cost" %} |
@@ -192,7 +192,7 @@ src="{% static 'img/blank_image.png' %}"
{% if tp == None %}
{% trans "Total cost could not be calculated" %}
{% else %}
- {% render_currency tp currency=order.customer.currency %}
+ {% render_currency tp currency=order.currency %}
{% endif %}
{% endwith %}
diff --git a/InvenTree/order/templates/order/sales_order_base.html b/InvenTree/order/templates/order/sales_order_base.html
index 15615c4fee..f6810553b1 100644
--- a/InvenTree/order/templates/order/sales_order_base.html
+++ b/InvenTree/order/templates/order/sales_order_base.html
@@ -223,7 +223,7 @@ src="{% static 'img/blank_image.png' %}"
{{ order.responsible }} |
{% endif %}
-
+ {% include "currency_data.html" with instance=order %}
|
{% trans "Total Cost" %} |
@@ -232,7 +232,7 @@ src="{% static 'img/blank_image.png' %}"
{% if tp == None %}
{% trans "Total cost could not be calculated" %}
{% else %}
- {% render_currency tp currency=order.customer.currency %}
+ {% render_currency tp currency=order.currency %}
{% endif %}
{% endwith %}
diff --git a/InvenTree/order/templates/order/sales_order_detail.html b/InvenTree/order/templates/order/sales_order_detail.html
index c25b1862d7..055f85f046 100644
--- a/InvenTree/order/templates/order/sales_order_detail.html
+++ b/InvenTree/order/templates/order/sales_order_detail.html
@@ -275,9 +275,7 @@
order: {{ order.pk }},
table: '#so-extra-lines-table',
url: '{% url "api-so-extra-line-list" %}',
- {% if order.customer.currency %}
- currency: '{{ order.customer.currency }}',
- {% endif %}
+ currency: '{{ order.currency }}',
});
});
diff --git a/InvenTree/order/test_api.py b/InvenTree/order/test_api.py
index 7b990c9639..f8e6846a5c 100644
--- a/InvenTree/order/test_api.py
+++ b/InvenTree/order/test_api.py
@@ -60,6 +60,50 @@ class PurchaseOrderTest(OrderTest):
LIST_URL = reverse('api-po-list')
+ def test_options(self):
+ """Test the PurchaseOrder OPTIONS endpoint."""
+
+ self.assignRole('purchase_order.add')
+
+ response = self.options(self.LIST_URL, expected_code=200)
+
+ data = response.data
+ self.assertEqual(data['name'], 'Purchase Order List')
+
+ post = data['actions']['POST']
+
+ def check_options(data, field_name, spec):
+ """Helper function to check that the options are configured correctly."""
+ field_data = data[field_name]
+
+ for k, v in spec.items():
+ self.assertIn(k, field_data)
+ self.assertEqual(field_data[k], v)
+
+ # Checks for the 'order_currency' field
+ check_options(post, 'order_currency', {
+ 'type': 'choice',
+ 'required': False,
+ 'read_only': False,
+ 'label': 'Order Currency',
+ 'help_text': 'Currency for this order (leave blank to use company default)',
+ })
+
+ # Checks for the 'reference' field
+ check_options(post, 'reference', {
+ 'type': 'string',
+ 'required': True,
+ 'read_only': False,
+ 'label': 'Reference',
+ })
+
+ # Checks for the 'supplier' field
+ check_options(post, 'supplier', {
+ 'type': 'related field',
+ 'required': True,
+ 'api_url': '/api/company/',
+ })
+
def test_po_list(self):
"""Test the PurchaseOrder list API endpoint"""
# List *ALL* PurchaseOrder items
@@ -155,7 +199,7 @@ class PurchaseOrderTest(OrderTest):
for result in response.data['results']:
self.assertIn('total_price', result)
- self.assertIn('total_price_currency', result)
+ self.assertIn('order_currency', result)
def test_overdue(self):
"""Test "overdue" status."""
@@ -323,13 +367,18 @@ class PurchaseOrderTest(OrderTest):
self.assertTrue(po.lines.count() > 0)
+ lines = []
+
# Add some extra line items to this order
for idx in range(5):
- models.PurchaseOrderExtraLine.objects.create(
+ lines.append(models.PurchaseOrderExtraLine(
order=po,
quantity=idx + 10,
reference='some reference',
- )
+ ))
+
+ # bulk create orders
+ models.PurchaseOrderExtraLine.objects.bulk_create(lines)
data = self.get(reverse('api-po-detail', kwargs={'pk': 1})).data
@@ -1157,7 +1206,7 @@ class SalesOrderTest(OrderTest):
for result in response.data['results']:
self.assertIn('total_price', result)
- self.assertIn('total_price_currency', result)
+ self.assertIn('order_currency', result)
def test_overdue(self):
"""Test "overdue" status."""
diff --git a/InvenTree/report/templates/report/inventree_po_report_base.html b/InvenTree/report/templates/report/inventree_po_report_base.html
index d6c5ee3ee3..88eb5cb2c4 100644
--- a/InvenTree/report/templates/report/inventree_po_report_base.html
+++ b/InvenTree/report/templates/report/inventree_po_report_base.html
@@ -70,7 +70,7 @@
|
|
{% trans "Total" %} |
- {% render_currency order.total_price decimal_places=2 currency=order.supplier.currency %} |
+ {% render_currency order.total_price decimal_places=2 currency=order.currency %} |
|
diff --git a/InvenTree/report/templates/report/inventree_so_report_base.html b/InvenTree/report/templates/report/inventree_so_report_base.html
index 7ee4ca0edd..daeace7b1d 100644
--- a/InvenTree/report/templates/report/inventree_so_report_base.html
+++ b/InvenTree/report/templates/report/inventree_so_report_base.html
@@ -70,7 +70,7 @@
|
|
{% trans "Total" %} |
- {% render_currency order.total_price currency=order.customer.currency %} |
+ {% render_currency order.total_price currency=order.currency %} |
|
diff --git a/InvenTree/templates/currency_data.html b/InvenTree/templates/currency_data.html
new file mode 100644
index 0000000000..ef9eb39531
--- /dev/null
+++ b/InvenTree/templates/currency_data.html
@@ -0,0 +1,12 @@
+{% load i18n %}
+{% if instance and instance.currency %}
+
+ |
+ {% trans "Currency" %} |
+ {% if instance.order_currency %}
+ {{ instance.currency }} |
+ {% else %}
+ {{ instance.currency }} |
+ {% endif %}
+
+{% endif %}
diff --git a/InvenTree/templates/js/translated/purchase_order.js b/InvenTree/templates/js/translated/purchase_order.js
index dfe049a0a9..4eccbdf602 100644
--- a/InvenTree/templates/js/translated/purchase_order.js
+++ b/InvenTree/templates/js/translated/purchase_order.js
@@ -65,6 +65,9 @@ function purchaseOrderFields(options={}) {
project_code: {
icon: 'fa-list',
},
+ order_currency: {
+ icon: 'fa-coins',
+ },
target_date: {
icon: 'fa-calendar-alt',
},
@@ -1670,7 +1673,7 @@ function loadPurchaseOrderTable(table, options) {
sortable: true,
formatter: function(value, row) {
return formatCurrency(value, {
- currency: row.total_price_currency,
+ currency: row.order_currency,
});
},
},
diff --git a/InvenTree/templates/js/translated/return_order.js b/InvenTree/templates/js/translated/return_order.js
index 31855438ce..13e34e721b 100644
--- a/InvenTree/templates/js/translated/return_order.js
+++ b/InvenTree/templates/js/translated/return_order.js
@@ -49,6 +49,9 @@ function returnOrderFields(options={}) {
project_code: {
icon: 'fa-list',
},
+ order_currency: {
+ icon: 'fa-coins',
+ },
target_date: {
icon: 'fa-calendar-alt',
},
@@ -349,7 +352,7 @@ function loadReturnOrderTable(table, options={}) {
visible: false,
formatter: function(value, row) {
return formatCurrency(value, {
- currency: row.total_price_currency
+ currency: row.order_currency
});
}
}
diff --git a/InvenTree/templates/js/translated/sales_order.js b/InvenTree/templates/js/translated/sales_order.js
index 095160726b..fd10a94923 100644
--- a/InvenTree/templates/js/translated/sales_order.js
+++ b/InvenTree/templates/js/translated/sales_order.js
@@ -62,6 +62,9 @@ function salesOrderFields(options={}) {
project_code: {
icon: 'fa-list',
},
+ order_currency: {
+ icon: 'fa-coins',
+ },
target_date: {
icon: 'fa-calendar-alt',
},
@@ -802,7 +805,7 @@ function loadSalesOrderTable(table, options) {
sortable: true,
formatter: function(value, row) {
return formatCurrency(value, {
- currency: row.total_price_currency,
+ currency: row.order_currency,
});
}
}
diff --git a/docs/README.md b/docs/README.md
index 23f590d6fd..b5aef7a538 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -21,7 +21,7 @@ $ pip install -r requirements.txt
To serve the pages locally, run the following command (from the top-level project directory):
```
-$ mkdocs serve -a localhost:8080
+$ mkdocs serve -f docs/mkdocs.yml -a localhost:8080
```
## Edit Documentation Files
diff --git a/docs/docs/buy/po.md b/docs/docs/buy/po.md
index 914360622e..3ebe0a795d 100644
--- a/docs/docs/buy/po.md
+++ b/docs/docs/buy/po.md
@@ -23,6 +23,10 @@ Each Purchase Order has a specific status code which indicates the current state
| Complete | The purchase order has been completed, and is now closed |
| Cancelled | The purchase order was cancelled, and is now closed |
+### Purchase Order Currency
+
+The currency code can be specified for an individual purchase order. If not specified, the default currency specified against the [supplier](./supplier.md) will be used.
+
## Create Purchase Order
Once the purchase order page is loaded, click on New Purchase Order which opens the "Create Purchase Order" form.
diff --git a/docs/docs/report/purchase_order.md b/docs/docs/report/purchase_order.md
index 8729e9ff1c..e80a68087f 100644
--- a/docs/docs/report/purchase_order.md
+++ b/docs/docs/report/purchase_order.md
@@ -27,9 +27,11 @@ In addition to the default report context variables, the following variables are
| order.creation_date | The date when the order was created |
| order.target_date | The date when the order should arrive |
| order.if_overdue | Boolean value that tells if the target date has passed |
+| order.currency | The currency code associated with this order, e.g. 'AUD' |
#### Lines
-The lines have sub variables.
+
+Each line item have sub variables, as follows:
| Variable | Description |
| --- | --- |
@@ -39,9 +41,20 @@ The lines have sub variables.
| reference | The reference given in the part of the order |
| notes | The notes given in the part of the order |
| target_date | The date when the part should arrive. Each part can have an individual date |
-| price | The price the part supplierpart |
+| price | The unit price the line item |
+| total_line_price | The total price for this line item, calculated from the unit price and quantity |
| destination | The stock location where the part will be stored |
+A simple example below shows how to use the context variables for line items:
+
+```html
+{% raw %}
+{% for line in order.lines %}
+Price: {% render_currency line.total_line_price %}
+{% endfor %}
+{% endraw %}
+```
+
### Default Report Template
diff --git a/docs/docs/report/sales_order.md b/docs/docs/report/sales_order.md
index 54fcd808e0..e195dbddf7 100644
--- a/docs/docs/report/sales_order.md
+++ b/docs/docs/report/sales_order.md
@@ -22,6 +22,7 @@ In addition to the default report context variables, the following variables are
| customer | The [customer](../sell/customer.md) associated with the particular sales order |
| lines | A list of available line items for this order |
| extra_lines | A list of available *extra* line items for this order |
+| order.currency | The currency code associated with this order, e.g. 'CAD' |
### Default Report Template
diff --git a/docs/docs/sell/so.md b/docs/docs/sell/so.md
index bea918e9ca..fe15b597bc 100644
--- a/docs/docs/sell/so.md
+++ b/docs/docs/sell/so.md
@@ -23,6 +23,11 @@ Each Sales Order has a specific status code, which represents the state of the o
| Complete | The sales order has been completed, and is now closed |
| Cancelled | The sales order was cancelled, and is now closed |
+### Sales Order Currency
+
+The currency code can be specified for an individual sales order. If not specified, the default currency specified against the [customer](./customer.md) will be used.
+
+
## Create a Sales Order
Once the sales order page is loaded, click on New Sales Order which opens the "Create Sales Order" form.