2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-06-17 12:35:46 +00:00

Specify order currency (#4698)

* Add 'order_currency' to the various external order models

- By default will use the currency specified for the supplier (or customer)
- Can be specified per order, also

* Display order currency on order pgae

* Add 'order_currency' field

* Enable "blank" currency option (to default to the currency specified by the referenced company

* Fix default currency code when adding line items

* Remove 'total_price_currency' serializer field

- Now replaced with 'order_currency' for greater flexibility

* Bump api_version.py

* Update default order report templates

* Updated docs

* More docs updaes

* Adjust unit tests

* Use 'order_currency' in order tables

* Update purchase order api unit tests
This commit is contained in:
Oliver
2023-04-26 17:35:15 +10:00
committed by GitHub
parent 36d17c082b
commit 5fcab2aec3
22 changed files with 199 additions and 35 deletions

View File

@ -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

View File

@ -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')

View File

@ -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'),
),
]

View File

@ -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()

View File

@ -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)

View File

@ -215,7 +215,7 @@ src="{% static 'img/blank_image.png' %}"
<td>{{ order.responsible }}</td>
</tr>
{% endif %}
{% include "currency_data.html" with instance=order %}
<tr>
<td><span class='fas fa-dollar-sign'></span></td>
<td>{% trans "Total cost" %}</td>
@ -224,7 +224,7 @@ src="{% static 'img/blank_image.png' %}"
{% if tp == None %}
<span class='badge bg-warning'>{% trans "Total cost could not be calculated" %}</span>
{% else %}
{% render_currency tp currency=order.supplier.currency %}
{% render_currency tp currency=order.currency %}
{% endif %}
{% endwith %}
</td>

View File

@ -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 }}',
});
});

View File

@ -183,7 +183,7 @@ src="{% static 'img/blank_image.png' %}"
<td>{{ order.responsible }}</td>
</tr>
{% endif %}
{% include "currency_data.html" with instance=order %}
<tr>
<td><span class='fas fa-dollar-sign'></span></td>
<td>{% trans "Total Cost" %}</td>
@ -192,7 +192,7 @@ src="{% static 'img/blank_image.png' %}"
{% if tp == None %}
<span class='badge bg-warning'>{% trans "Total cost could not be calculated" %}</span>
{% else %}
{% render_currency tp currency=order.customer.currency %}
{% render_currency tp currency=order.currency %}
{% endif %}
{% endwith %}
</td>

View File

@ -223,7 +223,7 @@ src="{% static 'img/blank_image.png' %}"
<td>{{ order.responsible }}</td>
</tr>
{% endif %}
{% include "currency_data.html" with instance=order %}
<tr>
<td><span class='fas fa-dollar-sign'></span></td>
<td>{% trans "Total Cost" %}</td>
@ -232,7 +232,7 @@ src="{% static 'img/blank_image.png' %}"
{% if tp == None %}
<span class='badge bg-warning'>{% trans "Total cost could not be calculated" %}</span>
{% else %}
{% render_currency tp currency=order.customer.currency %}
{% render_currency tp currency=order.currency %}
{% endif %}
{% endwith %}
</td>

View File

@ -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 }}',
});
});

View File

@ -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."""

View File

@ -70,7 +70,7 @@
<td></td>
<td></td>
<th>{% trans "Total" %}</th>
<td>{% render_currency order.total_price decimal_places=2 currency=order.supplier.currency %}</td>
<td>{% render_currency order.total_price decimal_places=2 currency=order.currency %}</td>
<td></td>
</tr>

View File

@ -70,7 +70,7 @@
<td></td>
<td></td>
<th>{% trans "Total" %}</th>
<td>{% render_currency order.total_price currency=order.customer.currency %}</td>
<td>{% render_currency order.total_price currency=order.currency %}</td>
<td></td>
</tr>
</tbody>

View File

@ -0,0 +1,12 @@
{% load i18n %}
{% if instance and instance.currency %}
<tr>
<td><span class='fas fa-dollar-sign'></span></td>
<td>{% trans "Currency" %}</td>
{% if instance.order_currency %}
<td>{{ instance.currency }}</td>
{% else %}
<td><em>{{ instance.currency }}</em></td>
{% endif %}
</tr>
{% endif %}

View File

@ -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,
});
},
},

View File

@ -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
});
}
}

View File

@ -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,
});
}
}