mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge pull request #1587 from nwns/feature/see_default_location
feat(purchase orders): show the preferred location for each PO Line
This commit is contained in:
commit
93e83d0bf9
@ -52,3 +52,10 @@
|
|||||||
part: 2
|
part: 2
|
||||||
supplier: 2
|
supplier: 2
|
||||||
SKU: 'ZERGM312'
|
SKU: 'ZERGM312'
|
||||||
|
|
||||||
|
- model: company.supplierpart
|
||||||
|
pk: 5
|
||||||
|
fields:
|
||||||
|
part: 4
|
||||||
|
supplier: 2
|
||||||
|
SKU: 'R_4K7_0603'
|
||||||
|
@ -65,7 +65,7 @@ class CompanySimpleTest(TestCase):
|
|||||||
self.assertEqual(acme.supplied_part_count, 4)
|
self.assertEqual(acme.supplied_part_count, 4)
|
||||||
|
|
||||||
self.assertTrue(appel.has_parts)
|
self.assertTrue(appel.has_parts)
|
||||||
self.assertEqual(appel.supplied_part_count, 3)
|
self.assertEqual(appel.supplied_part_count, 4)
|
||||||
|
|
||||||
self.assertTrue(zerg.has_parts)
|
self.assertTrue(zerg.has_parts)
|
||||||
self.assertEqual(zerg.supplied_part_count, 2)
|
self.assertEqual(zerg.supplied_part_count, 2)
|
||||||
|
@ -68,6 +68,7 @@
|
|||||||
order: 1
|
order: 1
|
||||||
part: 1
|
part: 1
|
||||||
quantity: 100
|
quantity: 100
|
||||||
|
destination: 5 # Desk/Drawer_1
|
||||||
|
|
||||||
# 250 x ACME0002 (M2x4 LPHS)
|
# 250 x ACME0002 (M2x4 LPHS)
|
||||||
# Partially received (50)
|
# Partially received (50)
|
||||||
@ -95,3 +96,10 @@
|
|||||||
part: 3
|
part: 3
|
||||||
quantity: 100
|
quantity: 100
|
||||||
|
|
||||||
|
# 1 x R_4K7_0603
|
||||||
|
- model: order.purchaseorderlineitem
|
||||||
|
pk: 23
|
||||||
|
fields:
|
||||||
|
order: 1
|
||||||
|
part: 5
|
||||||
|
quantity: 1
|
||||||
|
@ -79,12 +79,17 @@ class ShipSalesOrderForm(HelperForm):
|
|||||||
|
|
||||||
class ReceivePurchaseOrderForm(HelperForm):
|
class ReceivePurchaseOrderForm(HelperForm):
|
||||||
|
|
||||||
location = TreeNodeChoiceField(queryset=StockLocation.objects.all(), required=True, label=_('Location'), help_text=_('Receive parts to this location'))
|
location = TreeNodeChoiceField(
|
||||||
|
queryset=StockLocation.objects.all(),
|
||||||
|
required=True,
|
||||||
|
label=_("Destination"),
|
||||||
|
help_text=_("Receive parts to this location"),
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = PurchaseOrder
|
model = PurchaseOrder
|
||||||
fields = [
|
fields = [
|
||||||
'location',
|
"location",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -195,6 +200,7 @@ class EditPurchaseOrderLineItemForm(HelperForm):
|
|||||||
'quantity',
|
'quantity',
|
||||||
'reference',
|
'reference',
|
||||||
'purchase_price',
|
'purchase_price',
|
||||||
|
'destination',
|
||||||
'notes',
|
'notes',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -0,0 +1,29 @@
|
|||||||
|
# Generated by Django 3.2 on 2021-05-13 22:38
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
import django.db.models.deletion
|
||||||
|
import mptt.fields
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("stock", "0063_auto_20210511_2343"),
|
||||||
|
("order", "0045_auto_20210504_1946"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="purchaseorderlineitem",
|
||||||
|
name="destination",
|
||||||
|
field=mptt.fields.TreeForeignKey(
|
||||||
|
blank=True,
|
||||||
|
help_text="Where does the Purchaser want this item to be stored?",
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||||
|
related_name="po_lines",
|
||||||
|
to="stock.stocklocation",
|
||||||
|
verbose_name="Destination",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
@ -20,6 +20,7 @@ from django.utils.translation import ugettext_lazy as _
|
|||||||
from common.settings import currency_code_default
|
from common.settings import currency_code_default
|
||||||
|
|
||||||
from markdownx.models import MarkdownxField
|
from markdownx.models import MarkdownxField
|
||||||
|
from mptt.models import TreeForeignKey
|
||||||
|
|
||||||
from djmoney.models.fields import MoneyField
|
from djmoney.models.fields import MoneyField
|
||||||
|
|
||||||
@ -672,6 +673,29 @@ class PurchaseOrderLineItem(OrderLineItem):
|
|||||||
help_text=_('Unit purchase price'),
|
help_text=_('Unit purchase price'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
destination = TreeForeignKey(
|
||||||
|
'stock.StockLocation', on_delete=models.DO_NOTHING,
|
||||||
|
verbose_name=_('Destination'),
|
||||||
|
related_name='po_lines',
|
||||||
|
blank=True, null=True,
|
||||||
|
help_text=_('Where does the Purchaser want this item to be stored?')
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_destination(self):
|
||||||
|
"""Show where the line item is or should be placed"""
|
||||||
|
# NOTE: If a line item gets split when recieved, only an arbitrary
|
||||||
|
# stock items location will be reported as the location for the
|
||||||
|
# entire line.
|
||||||
|
for stock in stock_models.StockItem.objects.filter(
|
||||||
|
supplier_part=self.part, purchase_order=self.order
|
||||||
|
):
|
||||||
|
if stock.location:
|
||||||
|
return stock.location
|
||||||
|
if self.destination:
|
||||||
|
return self.destination
|
||||||
|
if self.part and self.part.part and self.part.part.default_location:
|
||||||
|
return self.part.part.default_location
|
||||||
|
|
||||||
def remaining(self):
|
def remaining(self):
|
||||||
""" Calculate the number of items remaining to be received """
|
""" Calculate the number of items remaining to be received """
|
||||||
r = self.quantity - self.received
|
r = self.quantity - self.received
|
||||||
|
@ -17,6 +17,7 @@ from InvenTree.serializers import InvenTreeAttachmentSerializerField
|
|||||||
|
|
||||||
from company.serializers import CompanyBriefSerializer, SupplierPartSerializer
|
from company.serializers import CompanyBriefSerializer, SupplierPartSerializer
|
||||||
from part.serializers import PartBriefSerializer
|
from part.serializers import PartBriefSerializer
|
||||||
|
from stock.serializers import LocationBriefSerializer
|
||||||
|
|
||||||
from .models import PurchaseOrder, PurchaseOrderLineItem
|
from .models import PurchaseOrder, PurchaseOrderLineItem
|
||||||
from .models import PurchaseOrderAttachment, SalesOrderAttachment
|
from .models import PurchaseOrderAttachment, SalesOrderAttachment
|
||||||
@ -116,6 +117,8 @@ class POLineItemSerializer(InvenTreeModelSerializer):
|
|||||||
|
|
||||||
purchase_price_string = serializers.CharField(source='purchase_price', read_only=True)
|
purchase_price_string = serializers.CharField(source='purchase_price', read_only=True)
|
||||||
|
|
||||||
|
destination = LocationBriefSerializer(source='get_destination', read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = PurchaseOrderLineItem
|
model = PurchaseOrderLineItem
|
||||||
|
|
||||||
@ -132,6 +135,7 @@ class POLineItemSerializer(InvenTreeModelSerializer):
|
|||||||
'purchase_price',
|
'purchase_price',
|
||||||
'purchase_price_currency',
|
'purchase_price_currency',
|
||||||
'purchase_price_string',
|
'purchase_price_string',
|
||||||
|
'destination',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -234,6 +234,10 @@ $("#po-table").inventreeTable({
|
|||||||
return (progressA < progressB) ? 1 : -1;
|
return (progressA < progressB) ? 1 : -1;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
field: 'destination.pathstring',
|
||||||
|
title: '{% trans "Destination" %}',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
field: 'notes',
|
field: 'notes',
|
||||||
title: '{% trans "Notes" %}',
|
title: '{% trans "Notes" %}',
|
||||||
|
@ -22,6 +22,7 @@
|
|||||||
<th>{% trans "Received" %}</th>
|
<th>{% trans "Received" %}</th>
|
||||||
<th>{% trans "Receive" %}</th>
|
<th>{% trans "Receive" %}</th>
|
||||||
<th>{% trans "Status" %}</th>
|
<th>{% trans "Status" %}</th>
|
||||||
|
<th>{% trans "Destination" %}</th>
|
||||||
<th></th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
{% for line in lines %}
|
{% for line in lines %}
|
||||||
@ -53,6 +54,9 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ line.get_destination }}
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<button class='btn btn-default btn-remove' onClick="removeOrderRowFromOrderWizard()" id='del_item_{{ line.id }}' title='{% trans "Remove line" %}' type='button'>
|
<button class='btn btn-default btn-remove' onClick="removeOrderRowFromOrderWizard()" id='del_item_{{ line.id }}' title='{% trans "Remove line" %}' type='button'>
|
||||||
<span row='line_row_{{ line.id }}' class='fas fa-times-circle icon-red'></span>
|
<span row='line_row_{{ line.id }}' class='fas fa-times-circle icon-red'></span>
|
||||||
|
@ -87,7 +87,7 @@ class OrderTest(TestCase):
|
|||||||
order = PurchaseOrder.objects.get(pk=1)
|
order = PurchaseOrder.objects.get(pk=1)
|
||||||
|
|
||||||
self.assertEqual(order.status, PurchaseOrderStatus.PENDING)
|
self.assertEqual(order.status, PurchaseOrderStatus.PENDING)
|
||||||
self.assertEqual(order.lines.count(), 3)
|
self.assertEqual(order.lines.count(), 4)
|
||||||
|
|
||||||
sku = SupplierPart.objects.get(SKU='ACME-WIDGET')
|
sku = SupplierPart.objects.get(SKU='ACME-WIDGET')
|
||||||
part = sku.part
|
part = sku.part
|
||||||
@ -105,11 +105,11 @@ class OrderTest(TestCase):
|
|||||||
order.add_line_item(sku, 100)
|
order.add_line_item(sku, 100)
|
||||||
|
|
||||||
self.assertEqual(part.on_order, 100)
|
self.assertEqual(part.on_order, 100)
|
||||||
self.assertEqual(order.lines.count(), 4)
|
self.assertEqual(order.lines.count(), 5)
|
||||||
|
|
||||||
# Order the same part again (it should be merged)
|
# Order the same part again (it should be merged)
|
||||||
order.add_line_item(sku, 50)
|
order.add_line_item(sku, 50)
|
||||||
self.assertEqual(order.lines.count(), 4)
|
self.assertEqual(order.lines.count(), 5)
|
||||||
self.assertEqual(part.on_order, 150)
|
self.assertEqual(part.on_order, 150)
|
||||||
|
|
||||||
# Try to order a supplier part from the wrong supplier
|
# Try to order a supplier part from the wrong supplier
|
||||||
@ -163,7 +163,7 @@ class OrderTest(TestCase):
|
|||||||
loc = StockLocation.objects.get(id=1)
|
loc = StockLocation.objects.get(id=1)
|
||||||
|
|
||||||
# There should be two lines against this order
|
# There should be two lines against this order
|
||||||
self.assertEqual(len(order.pending_line_items()), 3)
|
self.assertEqual(len(order.pending_line_items()), 4)
|
||||||
|
|
||||||
# Should fail, as order is 'PENDING' not 'PLACED"
|
# Should fail, as order is 'PENDING' not 'PLACED"
|
||||||
self.assertEqual(order.status, PurchaseOrderStatus.PENDING)
|
self.assertEqual(order.status, PurchaseOrderStatus.PENDING)
|
||||||
|
Loading…
Reference in New Issue
Block a user