mirror of
https://github.com/inventree/InvenTree.git
synced 2025-06-17 04:25:42 +00:00
Purchase Order Destination (#8403)
* Add "destination" field to PurchaseOrder * Add 'destination' field to API * Add location to PurchaseOrderDetail page * Display "destination" on PurchaseOrderDetail page * Pre-select location based on selected "destination" * Fix order of reception priority * Auto-expand the per-line destination field * Add "Purchase Order" detail to StockItemDetail page * Bug fix in PurchaseOrderForms * Split playwright tests * Docs updates * Bump API version * Unit test fixes * Fix more tests * Backport to CUI * Use PurchaseOrder destination when scanning items
This commit is contained in:
@ -1,14 +1,17 @@
|
||||
"""InvenTree API version information."""
|
||||
|
||||
# InvenTree API version
|
||||
INVENTREE_API_VERSION = 275
|
||||
INVENTREE_API_VERSION = 276
|
||||
|
||||
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
||||
|
||||
|
||||
INVENTREE_API_TEXT = """
|
||||
|
||||
v274 - 2024-10-31 : https://github.com/inventree/InvenTree/pull/8396
|
||||
v276 - 2024-10-31 : https://github.com/inventree/InvenTree/pull/8403
|
||||
- Adds 'destination' field to the PurchaseOrder model and API endpoints
|
||||
|
||||
v275 - 2024-10-31 : https://github.com/inventree/InvenTree/pull/8396
|
||||
- Adds SKU and MPN fields to the StockItem serializer
|
||||
- Additional export options for the StockItem serializer
|
||||
|
||||
|
@ -0,0 +1,26 @@
|
||||
# Generated by Django 4.2.16 on 2024-10-31 02:52
|
||||
|
||||
from django.db import migrations
|
||||
import django.db.models.deletion
|
||||
import mptt.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('stock', '0113_stockitem_status_custom_key_and_more'),
|
||||
('order', '0101_purchaseorder_status_custom_key_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='purchaseorder',
|
||||
name='destination',
|
||||
field=mptt.fields.TreeForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='purchase_orders', to='stock.stocklocation', verbose_name='Destination', help_text='Destination for received items'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='purchaseorderlineitem',
|
||||
name='destination',
|
||||
field=mptt.fields.TreeForeignKey(blank=True, help_text='Destination for received items', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='po_lines', to='stock.stocklocation', verbose_name='Destination'),
|
||||
),
|
||||
]
|
@ -543,6 +543,16 @@ class PurchaseOrder(TotalPriceMixin, Order):
|
||||
help_text=_('Date order was completed'),
|
||||
)
|
||||
|
||||
destination = TreeForeignKey(
|
||||
'stock.StockLocation',
|
||||
on_delete=models.SET_NULL,
|
||||
related_name='purchase_orders',
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name=_('Destination'),
|
||||
help_text=_('Destination for received items'),
|
||||
)
|
||||
|
||||
@transaction.atomic
|
||||
def add_line_item(
|
||||
self,
|
||||
@ -1544,7 +1554,7 @@ class PurchaseOrderLineItem(OrderLineItem):
|
||||
related_name='po_lines',
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text=_('Where does the Purchaser want this item to be stored?'),
|
||||
help_text=_('Destination for received items'),
|
||||
)
|
||||
|
||||
def get_destination(self):
|
||||
|
@ -318,6 +318,7 @@ class PurchaseOrderSerializer(
|
||||
'supplier_name',
|
||||
'total_price',
|
||||
'order_currency',
|
||||
'destination',
|
||||
])
|
||||
|
||||
read_only_fields = ['issue_date', 'complete_date', 'creation_date']
|
||||
@ -860,6 +861,7 @@ class PurchaseOrderReceiveSerializer(serializers.Serializer):
|
||||
location = serializers.PrimaryKeyRelatedField(
|
||||
queryset=stock.models.StockLocation.objects.all(),
|
||||
many=False,
|
||||
required=False,
|
||||
allow_null=True,
|
||||
label=_('Location'),
|
||||
help_text=_('Select destination location for received items'),
|
||||
@ -873,9 +875,10 @@ class PurchaseOrderReceiveSerializer(serializers.Serializer):
|
||||
"""
|
||||
super().validate(data)
|
||||
|
||||
order = self.context['order']
|
||||
items = data.get('items', [])
|
||||
|
||||
location = data.get('location', None)
|
||||
location = data.get('location', order.destination)
|
||||
|
||||
if len(items) == 0:
|
||||
raise ValidationError(_('Line items must be provided'))
|
||||
@ -919,15 +922,17 @@ class PurchaseOrderReceiveSerializer(serializers.Serializer):
|
||||
order = self.context['order']
|
||||
|
||||
items = data['items']
|
||||
location = data.get('location', None)
|
||||
|
||||
# Location can be provided, or default to the order destination
|
||||
location = data.get('location', order.destination)
|
||||
|
||||
# Now we can actually receive the items into stock
|
||||
with transaction.atomic():
|
||||
for item in items:
|
||||
# Select location (in descending order of priority)
|
||||
loc = (
|
||||
location
|
||||
or item.get('location', None)
|
||||
item.get('location', None)
|
||||
or location
|
||||
or item['line_item'].get_destination()
|
||||
)
|
||||
|
||||
|
@ -129,7 +129,15 @@ src="{% static 'img/blank_image.png' %}"
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{% if order.destination %}
|
||||
<tr>
|
||||
<td><span class='fas fa-sitemap'></span></td>
|
||||
<td>{% trans "Destination" %}</td>
|
||||
<td>
|
||||
<a href='{% url "stock-location-detail" order.destination.id %}'>{{ order.destination.name }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
|
||||
{% endblock details %}
|
||||
|
@ -169,6 +169,9 @@ $('#new-po-line').click(function() {
|
||||
{{ order.id }},
|
||||
items,
|
||||
{
|
||||
{% if order.destination %}
|
||||
destination: {{ order.destination.pk }},
|
||||
{% endif %}
|
||||
success: function() {
|
||||
$("#po-line-table").bootstrapTable('refresh');
|
||||
}
|
||||
|
@ -882,7 +882,6 @@ class PurchaseOrderReceiveTest(OrderTest):
|
||||
data = self.post(self.url, {}, expected_code=400).data
|
||||
|
||||
self.assertIn('This field is required', str(data['items']))
|
||||
self.assertIn('This field is required', str(data['location']))
|
||||
|
||||
# No new stock items have been created
|
||||
self.assertEqual(self.n, StockItem.objects.count())
|
||||
@ -1060,9 +1059,9 @@ class PurchaseOrderReceiveTest(OrderTest):
|
||||
self.assertEqual(stock_1.count(), 1)
|
||||
self.assertEqual(stock_2.count(), 1)
|
||||
|
||||
# Same location for each received item, as overall 'location' field is provided
|
||||
# Check received locations
|
||||
self.assertEqual(stock_1.last().location.pk, 1)
|
||||
self.assertEqual(stock_2.last().location.pk, 1)
|
||||
self.assertEqual(stock_2.last().location.pk, 2)
|
||||
|
||||
# Barcodes should have been assigned to the stock items
|
||||
self.assertTrue(
|
||||
|
@ -500,6 +500,15 @@ class BarcodePOReceive(BarcodeView):
|
||||
purchase_order = kwargs.get('purchase_order')
|
||||
location = kwargs.get('location')
|
||||
|
||||
# Extract location from PurchaseOrder, if available
|
||||
if not location and purchase_order:
|
||||
try:
|
||||
po = order.models.PurchaseOrder.objects.get(pk=purchase_order)
|
||||
if po.destination:
|
||||
location = po.destination.pk
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
plugins = registry.with_mixin('barcode')
|
||||
|
||||
# Look for a barcode plugin which knows how to deal with this barcode
|
||||
|
@ -35,7 +35,7 @@ class PluginDetailAPITest(PluginMixin, InvenTreeAPITestCase):
|
||||
'packagename': 'invalid_package_name-asdads-asfd-asdf-asdf-asdf',
|
||||
},
|
||||
expected_code=400,
|
||||
max_query_time=30,
|
||||
max_query_time=60,
|
||||
)
|
||||
|
||||
# valid - Pypi
|
||||
|
@ -111,6 +111,9 @@ function purchaseOrderFields(options={}) {
|
||||
target_date: {
|
||||
icon: 'fa-calendar-alt',
|
||||
},
|
||||
destination: {
|
||||
icon: 'fa-sitemap'
|
||||
},
|
||||
link: {
|
||||
icon: 'fa-link',
|
||||
},
|
||||
@ -1361,6 +1364,7 @@ function receivePurchaseOrderItems(order_id, line_items, options={}) {
|
||||
method: 'POST',
|
||||
fields: {
|
||||
location: {
|
||||
value: options.destination,
|
||||
filters: {
|
||||
structural: false,
|
||||
},
|
||||
|
Reference in New Issue
Block a user