mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-28 11:36:44 +00:00
Added various PO fixes (#6483)
* Added various PO fixes * Add auto-pricing and merge items functionality to PurchaseOrderLineItem * Bump api version to v173 * Add po line item create/update tests
This commit is contained in:
parent
55c64b546f
commit
7694092935
@ -1,11 +1,15 @@
|
|||||||
"""InvenTree API version information."""
|
"""InvenTree API version information."""
|
||||||
|
|
||||||
# InvenTree API version
|
# InvenTree API version
|
||||||
INVENTREE_API_VERSION = 172
|
INVENTREE_API_VERSION = 173
|
||||||
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
||||||
|
|
||||||
INVENTREE_API_TEXT = """
|
INVENTREE_API_TEXT = """
|
||||||
|
|
||||||
|
v173 - 2024-02-20 : https://github.com/inventree/InvenTree/pull/6483
|
||||||
|
- Adds "merge_items" to the PurchaseOrderLine create API endpoint
|
||||||
|
- Adds "auto_pricing" to the PurchaseOrderLine create/update API endpoint
|
||||||
|
|
||||||
v172 - 2024-02-20 : https://github.com/inventree/InvenTree/pull/6526
|
v172 - 2024-02-20 : https://github.com/inventree/InvenTree/pull/6526
|
||||||
- Adds "enabled" field to the PartTestTemplate API endpoint
|
- Adds "enabled" field to the PartTestTemplate API endpoint
|
||||||
- Adds "enabled" filter to the PartTestTemplate list
|
- Adds "enabled" filter to the PartTestTemplate list
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
"""JSON API for the Order app."""
|
"""JSON API for the Order app."""
|
||||||
|
|
||||||
|
from decimal import Decimal
|
||||||
|
from typing import cast
|
||||||
|
|
||||||
from django.contrib.auth import authenticate, login
|
from django.contrib.auth import authenticate, login
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.db.models import F, Q
|
from django.db.models import F, Q
|
||||||
@ -481,6 +484,14 @@ class PurchaseOrderLineItemMixin:
|
|||||||
|
|
||||||
return self.serializer_class(*args, **kwargs)
|
return self.serializer_class(*args, **kwargs)
|
||||||
|
|
||||||
|
def perform_update(self, serializer):
|
||||||
|
"""Override the perform_update method to auto-update pricing if required."""
|
||||||
|
super().perform_update(serializer)
|
||||||
|
|
||||||
|
# possibly auto-update pricing based on the supplier part pricing data
|
||||||
|
if serializer.validated_data.get('auto_pricing', True):
|
||||||
|
serializer.instance.update_pricing()
|
||||||
|
|
||||||
|
|
||||||
class PurchaseOrderLineItemList(
|
class PurchaseOrderLineItemList(
|
||||||
PurchaseOrderLineItemMixin, APIDownloadMixin, ListCreateDestroyAPIView
|
PurchaseOrderLineItemMixin, APIDownloadMixin, ListCreateDestroyAPIView
|
||||||
@ -493,6 +504,44 @@ class PurchaseOrderLineItemList(
|
|||||||
|
|
||||||
filterset_class = PurchaseOrderLineItemFilter
|
filterset_class = PurchaseOrderLineItemFilter
|
||||||
|
|
||||||
|
def create(self, request, *args, **kwargs):
|
||||||
|
"""Create or update a new PurchaseOrderLineItem object."""
|
||||||
|
serializer = self.get_serializer(data=request.data)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
data = cast(dict, serializer.validated_data)
|
||||||
|
|
||||||
|
# possibly merge duplicate items
|
||||||
|
line_item = None
|
||||||
|
if data.get('merge_items', True):
|
||||||
|
other_line = models.PurchaseOrderLineItem.objects.filter(
|
||||||
|
part=data.get('part'),
|
||||||
|
order=data.get('order'),
|
||||||
|
target_date=data.get('target_date'),
|
||||||
|
destination=data.get('destination'),
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if other_line is not None:
|
||||||
|
other_line.quantity += Decimal(data.get('quantity', 0))
|
||||||
|
other_line.save()
|
||||||
|
|
||||||
|
line_item = other_line
|
||||||
|
|
||||||
|
# otherwise create a new line item
|
||||||
|
if line_item is None:
|
||||||
|
line_item = serializer.save()
|
||||||
|
|
||||||
|
# possibly auto-update pricing based on the supplier part pricing data
|
||||||
|
if data.get('auto_pricing', True) and isinstance(
|
||||||
|
line_item, models.PurchaseOrderLineItem
|
||||||
|
):
|
||||||
|
line_item.update_pricing()
|
||||||
|
|
||||||
|
serializer = serializers.PurchaseOrderLineItemSerializer(line_item)
|
||||||
|
headers = self.get_success_headers(serializer.data)
|
||||||
|
return Response(
|
||||||
|
serializer.data, status=status.HTTP_201_CREATED, headers=headers
|
||||||
|
)
|
||||||
|
|
||||||
def filter_queryset(self, queryset):
|
def filter_queryset(self, queryset):
|
||||||
"""Additional filtering options."""
|
"""Additional filtering options."""
|
||||||
params = self.request.query_params
|
params = self.request.query_params
|
||||||
|
@ -1439,6 +1439,17 @@ class PurchaseOrderLineItem(OrderLineItem):
|
|||||||
r = self.quantity - self.received
|
r = self.quantity - self.received
|
||||||
return max(r, 0)
|
return max(r, 0)
|
||||||
|
|
||||||
|
def update_pricing(self):
|
||||||
|
"""Update pricing information based on the supplier part data."""
|
||||||
|
if self.part:
|
||||||
|
price = self.part.get_price(self.quantity)
|
||||||
|
|
||||||
|
if price is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.purchase_price = Decimal(price) / Decimal(self.quantity)
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
|
||||||
class PurchaseOrderExtraLine(OrderExtraLine):
|
class PurchaseOrderExtraLine(OrderExtraLine):
|
||||||
"""Model for a single ExtraLine in a PurchaseOrder.
|
"""Model for a single ExtraLine in a PurchaseOrder.
|
||||||
|
@ -340,11 +340,13 @@ class PurchaseOrderLineItemSerializer(InvenTreeModelSerializer):
|
|||||||
'received',
|
'received',
|
||||||
'purchase_price',
|
'purchase_price',
|
||||||
'purchase_price_currency',
|
'purchase_price_currency',
|
||||||
|
'auto_pricing',
|
||||||
'destination',
|
'destination',
|
||||||
'destination_detail',
|
'destination_detail',
|
||||||
'target_date',
|
'target_date',
|
||||||
'total_price',
|
'total_price',
|
||||||
'link',
|
'link',
|
||||||
|
'merge_items',
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@ -362,6 +364,10 @@ class PurchaseOrderLineItemSerializer(InvenTreeModelSerializer):
|
|||||||
if order_detail is not True:
|
if order_detail is not True:
|
||||||
self.fields.pop('order_detail')
|
self.fields.pop('order_detail')
|
||||||
|
|
||||||
|
def skip_create_fields(self):
|
||||||
|
"""Return a list of fields to skip when creating a new object."""
|
||||||
|
return ['auto_pricing', 'merge_items'] + super().skip_create_fields()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def annotate_queryset(queryset):
|
def annotate_queryset(queryset):
|
||||||
"""Add some extra annotations to this queryset.
|
"""Add some extra annotations to this queryset.
|
||||||
@ -419,6 +425,14 @@ class PurchaseOrderLineItemSerializer(InvenTreeModelSerializer):
|
|||||||
|
|
||||||
purchase_price = InvenTreeMoneySerializer(allow_null=True)
|
purchase_price = InvenTreeMoneySerializer(allow_null=True)
|
||||||
|
|
||||||
|
auto_pricing = serializers.BooleanField(
|
||||||
|
label=_('Auto Pricing'),
|
||||||
|
help_text=_(
|
||||||
|
'Automatically calculate purchase price based on supplier part data'
|
||||||
|
),
|
||||||
|
default=True,
|
||||||
|
)
|
||||||
|
|
||||||
destination_detail = stock.serializers.LocationBriefSerializer(
|
destination_detail = stock.serializers.LocationBriefSerializer(
|
||||||
source='get_destination', read_only=True
|
source='get_destination', read_only=True
|
||||||
)
|
)
|
||||||
@ -429,6 +443,14 @@ class PurchaseOrderLineItemSerializer(InvenTreeModelSerializer):
|
|||||||
|
|
||||||
order_detail = PurchaseOrderSerializer(source='order', read_only=True, many=False)
|
order_detail = PurchaseOrderSerializer(source='order', read_only=True, many=False)
|
||||||
|
|
||||||
|
merge_items = serializers.BooleanField(
|
||||||
|
label=_('Merge Items'),
|
||||||
|
help_text=_(
|
||||||
|
'Merge items with the same part, destination and target date into one line item'
|
||||||
|
),
|
||||||
|
default=True,
|
||||||
|
)
|
||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
"""Custom validation for the serializer.
|
"""Custom validation for the serializer.
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ from icalendar import Calendar
|
|||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
|
||||||
from common.settings import currency_codes
|
from common.settings import currency_codes
|
||||||
from company.models import Company
|
from company.models import Company, SupplierPart, SupplierPriceBreak
|
||||||
from InvenTree.status_codes import (
|
from InvenTree.status_codes import (
|
||||||
PurchaseOrderStatus,
|
PurchaseOrderStatus,
|
||||||
ReturnOrderLineStatus,
|
ReturnOrderLineStatus,
|
||||||
@ -675,6 +675,94 @@ class PurchaseOrderLineItemTest(OrderTest):
|
|||||||
# We should have 2 less PurchaseOrderLineItems after deletign them
|
# We should have 2 less PurchaseOrderLineItems after deletign them
|
||||||
self.assertEqual(models.PurchaseOrderLineItem.objects.count(), n - 2)
|
self.assertEqual(models.PurchaseOrderLineItem.objects.count(), n - 2)
|
||||||
|
|
||||||
|
def test_po_line_merge_pricing(self):
|
||||||
|
"""Test that we can create a new PurchaseOrderLineItem via the API."""
|
||||||
|
self.assignRole('purchase_order.add')
|
||||||
|
self.generate_exchange_rates()
|
||||||
|
|
||||||
|
su = Company.objects.get(pk=1)
|
||||||
|
sp = SupplierPart.objects.get(pk=1)
|
||||||
|
po = models.PurchaseOrder.objects.create(supplier=su, reference='PO-1234567890')
|
||||||
|
SupplierPriceBreak.objects.create(part=sp, quantity=1, price=Money(1, 'USD'))
|
||||||
|
SupplierPriceBreak.objects.create(part=sp, quantity=10, price=Money(0.5, 'USD'))
|
||||||
|
|
||||||
|
li1 = self.post(
|
||||||
|
reverse('api-po-line-list'),
|
||||||
|
{
|
||||||
|
'order': po.pk,
|
||||||
|
'part': sp.pk,
|
||||||
|
'quantity': 1,
|
||||||
|
'auto_pricing': True,
|
||||||
|
'merge_items': False,
|
||||||
|
},
|
||||||
|
expected_code=201,
|
||||||
|
).json()
|
||||||
|
self.assertEqual(float(li1['purchase_price']), 1)
|
||||||
|
|
||||||
|
li2 = self.post(
|
||||||
|
reverse('api-po-line-list'),
|
||||||
|
{
|
||||||
|
'order': po.pk,
|
||||||
|
'part': sp.pk,
|
||||||
|
'quantity': 10,
|
||||||
|
'auto_pricing': True,
|
||||||
|
'merge_items': False,
|
||||||
|
},
|
||||||
|
expected_code=201,
|
||||||
|
).json()
|
||||||
|
self.assertEqual(float(li2['purchase_price']), 0.5)
|
||||||
|
|
||||||
|
# test that items where not merged
|
||||||
|
self.assertNotEqual(li1['pk'], li2['pk'])
|
||||||
|
|
||||||
|
li3 = self.post(
|
||||||
|
reverse('api-po-line-list'),
|
||||||
|
{
|
||||||
|
'order': po.pk,
|
||||||
|
'part': sp.pk,
|
||||||
|
'quantity': 9,
|
||||||
|
'auto_pricing': True,
|
||||||
|
'merge_items': True,
|
||||||
|
},
|
||||||
|
expected_code=201,
|
||||||
|
).json()
|
||||||
|
|
||||||
|
# test that items where merged
|
||||||
|
self.assertEqual(li1['pk'], li3['pk'])
|
||||||
|
|
||||||
|
# test that price was recalculated
|
||||||
|
self.assertEqual(float(li3['purchase_price']), 0.5)
|
||||||
|
|
||||||
|
# test that pricing will be not recalculated if auto_pricing is False
|
||||||
|
li4 = self.post(
|
||||||
|
reverse('api-po-line-list'),
|
||||||
|
{
|
||||||
|
'order': po.pk,
|
||||||
|
'part': sp.pk,
|
||||||
|
'quantity': 1,
|
||||||
|
'auto_pricing': False,
|
||||||
|
'purchase_price': 0.5,
|
||||||
|
'merge_items': False,
|
||||||
|
},
|
||||||
|
expected_code=201,
|
||||||
|
).json()
|
||||||
|
self.assertEqual(float(li4['purchase_price']), 0.5)
|
||||||
|
|
||||||
|
# test that pricing is correctly recalculated if auto_pricing is True for update
|
||||||
|
li5 = self.patch(
|
||||||
|
reverse('api-po-line-detail', kwargs={'pk': li4['pk']}),
|
||||||
|
{**li4, 'quantity': 5, 'auto_pricing': False},
|
||||||
|
expected_code=200,
|
||||||
|
).json()
|
||||||
|
self.assertEqual(float(li5['purchase_price']), 0.5)
|
||||||
|
|
||||||
|
li5 = self.patch(
|
||||||
|
reverse('api-po-line-detail', kwargs={'pk': li4['pk']}),
|
||||||
|
{**li4, 'quantity': 5, 'auto_pricing': True},
|
||||||
|
expected_code=200,
|
||||||
|
).json()
|
||||||
|
self.assertEqual(float(li5['purchase_price']), 1)
|
||||||
|
|
||||||
|
|
||||||
class PurchaseOrderDownloadTest(OrderTest):
|
class PurchaseOrderDownloadTest(OrderTest):
|
||||||
"""Unit tests for downloading PurchaseOrder data via the API endpoint."""
|
"""Unit tests for downloading PurchaseOrder data via the API endpoint."""
|
||||||
|
@ -225,7 +225,7 @@ function createPurchaseOrder(options={}) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
constructForm('{% url "api-po-list" %}', {
|
constructForm('{% url "api-po-list" %}?supplier_detail=true', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
fields: fields,
|
fields: fields,
|
||||||
groups: groups,
|
groups: groups,
|
||||||
@ -268,7 +268,6 @@ function duplicatePurchaseOrder(order_id, options={}) {
|
|||||||
|
|
||||||
/* Construct a set of fields for the PurchaseOrderLineItem form */
|
/* Construct a set of fields for the PurchaseOrderLineItem form */
|
||||||
function poLineItemFields(options={}) {
|
function poLineItemFields(options={}) {
|
||||||
|
|
||||||
var fields = {
|
var fields = {
|
||||||
order: {
|
order: {
|
||||||
filters: {
|
filters: {
|
||||||
@ -286,8 +285,6 @@ function poLineItemFields(options={}) {
|
|||||||
// If the pack_quantity != 1, add a note to the field
|
// If the pack_quantity != 1, add a note to the field
|
||||||
var pack_quantity = 1;
|
var pack_quantity = 1;
|
||||||
var units = '';
|
var units = '';
|
||||||
var supplier_part_id = value;
|
|
||||||
var quantity = getFormFieldValue('quantity', {}, opts);
|
|
||||||
|
|
||||||
// Remove any existing note fields
|
// Remove any existing note fields
|
||||||
$(opts.modal).find('#info-pack-size').remove();
|
$(opts.modal).find('#info-pack-size').remove();
|
||||||
@ -314,37 +311,6 @@ function poLineItemFields(options={}) {
|
|||||||
var txt = `<span class='fas fa-info-circle icon-blue'></span> {% trans "Pack Quantity" %}: ${formatDecimal(pack_quantity)} ${units}`;
|
var txt = `<span class='fas fa-info-circle icon-blue'></span> {% trans "Pack Quantity" %}: ${formatDecimal(pack_quantity)} ${units}`;
|
||||||
$(opts.modal).find('#hint_id_quantity').after(`<div class='form-info-message' id='info-pack-size'>${txt}</div>`);
|
$(opts.modal).find('#hint_id_quantity').after(`<div class='form-info-message' id='info-pack-size'>${txt}</div>`);
|
||||||
}
|
}
|
||||||
}).then(function() {
|
|
||||||
// Update pricing data (if available)
|
|
||||||
if (options.update_pricing) {
|
|
||||||
inventreeGet(
|
|
||||||
'{% url "api-part-supplier-price-list" %}',
|
|
||||||
{
|
|
||||||
part: supplier_part_id,
|
|
||||||
ordering: 'quantity',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
success: function(response) {
|
|
||||||
// Returned prices are in increasing order of quantity
|
|
||||||
if (response.length > 0) {
|
|
||||||
let index = 0;
|
|
||||||
|
|
||||||
for (var idx = 0; idx < response.length; idx++) {
|
|
||||||
if (response[idx].quantity > quantity) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
index = idx;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update price and currency data in the form
|
|
||||||
updateFieldValue('purchase_price', response[index].price, {}, opts);
|
|
||||||
updateFieldValue('purchase_price_currency', response[index].price_currency, {}, opts);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
secondary: {
|
secondary: {
|
||||||
@ -377,10 +343,20 @@ function poLineItemFields(options={}) {
|
|||||||
reference: {},
|
reference: {},
|
||||||
purchase_price: {
|
purchase_price: {
|
||||||
icon: 'fa-dollar-sign',
|
icon: 'fa-dollar-sign',
|
||||||
|
onEdit: function(value, name, field, opts) {
|
||||||
|
updateFieldValue('auto_pricing', value === '', {}, opts);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
purchase_price_currency: {
|
purchase_price_currency: {
|
||||||
icon: 'fa-coins',
|
icon: 'fa-coins',
|
||||||
},
|
},
|
||||||
|
auto_pricing: {
|
||||||
|
onEdit: function(value, name, field, opts) {
|
||||||
|
if (value) {
|
||||||
|
updateFieldValue('purchase_price', '', {}, opts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
target_date: {
|
target_date: {
|
||||||
icon: 'fa-calendar-alt',
|
icon: 'fa-calendar-alt',
|
||||||
},
|
},
|
||||||
@ -411,6 +387,10 @@ function poLineItemFields(options={}) {
|
|||||||
fields.target_date.value = options.target_date;
|
fields.target_date.value = options.target_date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options.create) {
|
||||||
|
fields.merge_items = {};
|
||||||
|
}
|
||||||
|
|
||||||
return fields;
|
return fields;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -425,6 +405,7 @@ function createPurchaseOrderLineItem(order, options={}) {
|
|||||||
currency: options.currency,
|
currency: options.currency,
|
||||||
target_date: options.target_date,
|
target_date: options.target_date,
|
||||||
update_pricing: true,
|
update_pricing: true,
|
||||||
|
create: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
constructForm('{% url "api-po-line-list" %}', {
|
constructForm('{% url "api-po-line-list" %}', {
|
||||||
@ -697,6 +678,15 @@ function orderParts(parts_list, options={}) {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const merge_item_input = constructField(
|
||||||
|
`merge_item_${pk}`,
|
||||||
|
{
|
||||||
|
type: 'boolean',
|
||||||
|
value: true,
|
||||||
|
},
|
||||||
|
{ hideLabels: true },
|
||||||
|
);
|
||||||
|
|
||||||
let buttons = '';
|
let buttons = '';
|
||||||
|
|
||||||
if (parts.length > 1) {
|
if (parts.length > 1) {
|
||||||
@ -723,6 +713,7 @@ function orderParts(parts_list, options={}) {
|
|||||||
<td id='td_supplier_part_${pk}'>${supplier_part_input}</td>
|
<td id='td_supplier_part_${pk}'>${supplier_part_input}</td>
|
||||||
<td id='td_order_${pk}'>${purchase_order_input}</td>
|
<td id='td_order_${pk}'>${purchase_order_input}</td>
|
||||||
<td id='td_quantity_${pk}'>${quantity_input}</td>
|
<td id='td_quantity_${pk}'>${quantity_input}</td>
|
||||||
|
<td id='td_merge_item_${pk}'>${merge_item_input}</td>
|
||||||
<td id='td_actions_${pk}'>${buttons}</td>
|
<td id='td_actions_${pk}'>${buttons}</td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
|
|
||||||
@ -761,6 +752,7 @@ function orderParts(parts_list, options={}) {
|
|||||||
<th style='min-width: 300px;'>{% trans "Supplier Part" %}</th>
|
<th style='min-width: 300px;'>{% trans "Supplier Part" %}</th>
|
||||||
<th style='min-width: 300px;'>{% trans "Purchase Order" %}</th>
|
<th style='min-width: 300px;'>{% trans "Purchase Order" %}</th>
|
||||||
<th style='min-width: 50px;'>{% trans "Quantity" %}</th>
|
<th style='min-width: 50px;'>{% trans "Quantity" %}</th>
|
||||||
|
<th style='min-width: 50px;'>{% trans "Merge" %}</th>
|
||||||
<th><!-- Actions --></th>
|
<th><!-- Actions --></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@ -838,6 +830,10 @@ function orderParts(parts_list, options={}) {
|
|||||||
success: function(response) {
|
success: function(response) {
|
||||||
pack_quantity = response.pack_quantity_native || 1;
|
pack_quantity = response.pack_quantity_native || 1;
|
||||||
units = response.part_detail.units || '';
|
units = response.part_detail.units || '';
|
||||||
|
if(response.supplier) {
|
||||||
|
order_filters.supplier = response.supplier;
|
||||||
|
options.supplier = response.supplier;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
).then(function() {
|
).then(function() {
|
||||||
@ -926,6 +922,7 @@ function orderParts(parts_list, options={}) {
|
|||||||
quantity: getFormFieldValue(`quantity_${pk}`, {type: 'decimal'}, opts),
|
quantity: getFormFieldValue(`quantity_${pk}`, {type: 'decimal'}, opts),
|
||||||
part: getFormFieldValue(`part_${pk}`, {}, opts),
|
part: getFormFieldValue(`part_${pk}`, {}, opts),
|
||||||
order: getFormFieldValue(`order_${pk}`, {}, opts),
|
order: getFormFieldValue(`order_${pk}`, {}, opts),
|
||||||
|
merge_items: getFormFieldValue(`merge_item_${pk}`, {type: 'boolean'}, opts),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Duplicate the form options, to prevent 'field_suffix' override
|
// Duplicate the form options, to prevent 'field_suffix' override
|
||||||
@ -984,7 +981,7 @@ function orderParts(parts_list, options={}) {
|
|||||||
var pk = $(this).attr('pk');
|
var pk = $(this).attr('pk');
|
||||||
|
|
||||||
// Launch dialog to create new purchase order
|
// Launch dialog to create new purchase order
|
||||||
createPurchaseOrder({
|
const poOptions = {
|
||||||
onSuccess: function(response) {
|
onSuccess: function(response) {
|
||||||
setRelatedFieldData(
|
setRelatedFieldData(
|
||||||
`order_${pk}`,
|
`order_${pk}`,
|
||||||
@ -992,7 +989,14 @@ function orderParts(parts_list, options={}) {
|
|||||||
opts
|
opts
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
|
if(options.supplier) {
|
||||||
|
poOptions.supplier = options.supplier;
|
||||||
|
poOptions.hide_supplier = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
createPurchaseOrder(poOptions);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -144,24 +144,24 @@ export function ApiFormField({
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Coerce the value to a numerical value
|
// Coerce the value to a numerical value
|
||||||
const numericalValue: number | undefined = useMemo(() => {
|
const numericalValue: number | '' = useMemo(() => {
|
||||||
let val = 0;
|
let val: number | '' = 0;
|
||||||
|
|
||||||
switch (definition.field_type) {
|
switch (definition.field_type) {
|
||||||
case 'integer':
|
case 'integer':
|
||||||
val = parseInt(value) ?? 0;
|
val = parseInt(value) ?? '';
|
||||||
break;
|
break;
|
||||||
case 'decimal':
|
case 'decimal':
|
||||||
case 'float':
|
case 'float':
|
||||||
case 'number':
|
case 'number':
|
||||||
val = parseFloat(value) ?? 0;
|
val = parseFloat(value) ?? '';
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isNaN(val) || !isFinite(val)) {
|
if (isNaN(val) || !isFinite(val)) {
|
||||||
val = 0;
|
val = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
return val;
|
return val;
|
||||||
|
@ -11,6 +11,7 @@ import {
|
|||||||
IconUser,
|
IconUser,
|
||||||
IconUsers
|
IconUsers
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ApiFormAdjustFilterType,
|
ApiFormAdjustFilterType,
|
||||||
@ -20,45 +21,76 @@ import {
|
|||||||
/*
|
/*
|
||||||
* Construct a set of fields for creating / editing a PurchaseOrderLineItem instance
|
* Construct a set of fields for creating / editing a PurchaseOrderLineItem instance
|
||||||
*/
|
*/
|
||||||
export function purchaseOrderLineItemFields() {
|
export function usePurchaseOrderLineItemFields({
|
||||||
let fields: ApiFormFieldSet = {
|
create
|
||||||
order: {
|
}: {
|
||||||
filters: {
|
create?: boolean;
|
||||||
supplier_detail: true
|
}) {
|
||||||
},
|
const [purchasePrice, setPurchasePrice] = useState<string>('');
|
||||||
hidden: true
|
const [autoPricing, setAutoPricing] = useState(true);
|
||||||
},
|
|
||||||
part: {
|
useEffect(() => {
|
||||||
filters: {
|
if (autoPricing) {
|
||||||
part_detail: true,
|
setPurchasePrice('');
|
||||||
supplier_detail: true
|
|
||||||
},
|
|
||||||
adjustFilters: (value: ApiFormAdjustFilterType) => {
|
|
||||||
// TODO: Adjust part based on the supplier associated with the supplier
|
|
||||||
return value.filters;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
quantity: {},
|
|
||||||
reference: {},
|
|
||||||
purchase_price: {
|
|
||||||
icon: <IconCurrencyDollar />
|
|
||||||
},
|
|
||||||
purchase_price_currency: {
|
|
||||||
icon: <IconCoins />
|
|
||||||
},
|
|
||||||
target_date: {
|
|
||||||
icon: <IconCalendar />
|
|
||||||
},
|
|
||||||
destination: {
|
|
||||||
icon: <IconSitemap />
|
|
||||||
},
|
|
||||||
notes: {
|
|
||||||
icon: <IconNotes />
|
|
||||||
},
|
|
||||||
link: {
|
|
||||||
icon: <IconLink />
|
|
||||||
}
|
}
|
||||||
};
|
}, [autoPricing]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setAutoPricing(purchasePrice === '');
|
||||||
|
}, [purchasePrice]);
|
||||||
|
|
||||||
|
const fields = useMemo(() => {
|
||||||
|
const fields: ApiFormFieldSet = {
|
||||||
|
order: {
|
||||||
|
filters: {
|
||||||
|
supplier_detail: true
|
||||||
|
},
|
||||||
|
hidden: true
|
||||||
|
},
|
||||||
|
part: {
|
||||||
|
filters: {
|
||||||
|
part_detail: true,
|
||||||
|
supplier_detail: true
|
||||||
|
},
|
||||||
|
adjustFilters: (value: ApiFormAdjustFilterType) => {
|
||||||
|
// TODO: Adjust part based on the supplier associated with the supplier
|
||||||
|
return value.filters;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
quantity: {},
|
||||||
|
reference: {},
|
||||||
|
purchase_price: {
|
||||||
|
icon: <IconCurrencyDollar />,
|
||||||
|
value: purchasePrice,
|
||||||
|
onValueChange: setPurchasePrice
|
||||||
|
},
|
||||||
|
purchase_price_currency: {
|
||||||
|
icon: <IconCoins />
|
||||||
|
},
|
||||||
|
auto_pricing: {
|
||||||
|
value: autoPricing,
|
||||||
|
onValueChange: setAutoPricing
|
||||||
|
},
|
||||||
|
target_date: {
|
||||||
|
icon: <IconCalendar />
|
||||||
|
},
|
||||||
|
destination: {
|
||||||
|
icon: <IconSitemap />
|
||||||
|
},
|
||||||
|
notes: {
|
||||||
|
icon: <IconNotes />
|
||||||
|
},
|
||||||
|
link: {
|
||||||
|
icon: <IconLink />
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (create) {
|
||||||
|
fields['merge_items'] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return fields;
|
||||||
|
}, [create, autoPricing, purchasePrice]);
|
||||||
|
|
||||||
return fields;
|
return fields;
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@ import { RenderStockLocation } from '../../components/render/Stock';
|
|||||||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||||
import { ModelType } from '../../enums/ModelType';
|
import { ModelType } from '../../enums/ModelType';
|
||||||
import { UserRoles } from '../../enums/Roles';
|
import { UserRoles } from '../../enums/Roles';
|
||||||
import { purchaseOrderLineItemFields } from '../../forms/PurchaseOrderForms';
|
import { usePurchaseOrderLineItemFields } from '../../forms/PurchaseOrderForms';
|
||||||
import { getDetailUrl } from '../../functions/urls';
|
import { getDetailUrl } from '../../functions/urls';
|
||||||
import {
|
import {
|
||||||
useCreateApiFormModal,
|
useCreateApiFormModal,
|
||||||
@ -178,7 +178,7 @@ export function PurchaseOrderLineItemTable({
|
|||||||
const newLine = useCreateApiFormModal({
|
const newLine = useCreateApiFormModal({
|
||||||
url: ApiEndpoints.purchase_order_line_list,
|
url: ApiEndpoints.purchase_order_line_list,
|
||||||
title: t`Add Line Item`,
|
title: t`Add Line Item`,
|
||||||
fields: purchaseOrderLineItemFields(),
|
fields: usePurchaseOrderLineItemFields({ create: true }),
|
||||||
initialData: {
|
initialData: {
|
||||||
order: orderId
|
order: orderId
|
||||||
},
|
},
|
||||||
@ -193,7 +193,7 @@ export function PurchaseOrderLineItemTable({
|
|||||||
url: ApiEndpoints.purchase_order_line_list,
|
url: ApiEndpoints.purchase_order_line_list,
|
||||||
pk: selectedLine,
|
pk: selectedLine,
|
||||||
title: t`Edit Line Item`,
|
title: t`Edit Line Item`,
|
||||||
fields: purchaseOrderLineItemFields(),
|
fields: usePurchaseOrderLineItemFields({}),
|
||||||
onFormSuccess: table.refreshTable
|
onFormSuccess: table.refreshTable
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user