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:
Oliver 2021-06-17 17:01:05 +10:00 committed by GitHub
commit 93e83d0bf9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 93 additions and 7 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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" %}',

View File

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

View File

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