diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index 37b2d96b7b..e1e82d97fe 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -501,6 +501,11 @@ class PurchaseOrder(Order): # Take the 'pack_size' of the SupplierPart into account pack_quantity = Decimal(quantity) * Decimal(line.part.pack_size) + if line.purchase_price: + unit_purchase_price = line.purchase_price / line.part.pack_size + else: + unit_purchase_price = None + # Determine if we should individually serialize the items, or not if type(serials) is list and len(serials) > 0: serialize = True @@ -519,7 +524,7 @@ class PurchaseOrder(Order): status=status, batch=batch_code, serial=sn, - purchase_price=line.purchase_price, + purchase_price=unit_purchase_price, barcode_hash=barcode_hash ) diff --git a/InvenTree/order/tests.py b/InvenTree/order/tests.py index 4a758c3d6e..c2557a0707 100644 --- a/InvenTree/order/tests.py +++ b/InvenTree/order/tests.py @@ -8,12 +8,14 @@ from django.contrib.auth import get_user_model from django.contrib.auth.models import Group from django.test import TestCase +from djmoney.money import Money + import common.models import order.tasks from company.models import Company, SupplierPart from InvenTree.status_codes import PurchaseOrderStatus from part.models import Part -from stock.models import StockLocation +from stock.models import StockItem, StockLocation from users.models import Owner from .models import PurchaseOrder, PurchaseOrderLineItem @@ -255,7 +257,8 @@ class OrderTest(TestCase): line_1 = PurchaseOrderLineItem.objects.create( order=po, part=sp_1, - quantity=3 + quantity=3, + purchase_price=Money(1000, 'USD'), # "Unit price" should be $100USD ) # 13 x 0.1 = 1.3 @@ -263,6 +266,7 @@ class OrderTest(TestCase): order=po, part=sp_2, quantity=13, + purchase_price=Money(10, 'USD'), # "Unit price" should be $100USD ) po.place_order() @@ -299,6 +303,23 @@ class OrderTest(TestCase): round(in_stock + Decimal(10.5), 1) ) + # Check that the unit purchase price value has been updated correctly + si = StockItem.objects.filter(supplier_part=sp_1) + self.assertEqual(si.count(), 1) + + # Ensure that received quantity and unit purchase price data are correct + si = si.first() + self.assertEqual(si.quantity, 10) + self.assertEqual(si.purchase_price, Money(100, 'USD')) + + si = StockItem.objects.filter(supplier_part=sp_2) + self.assertEqual(si.count(), 1) + + # Ensure that received quantity and unit purchase price data are correct + si = si.first() + self.assertEqual(si.quantity, 0.5) + self.assertEqual(si.purchase_price, Money(100, 'USD')) + def test_overdue_notification(self): """Test overdue purchase order notification diff --git a/InvenTree/part/templates/part/part_base.html b/InvenTree/part/templates/part/part_base.html index 891fc0fdd2..e5c9d6f07d 100644 --- a/InvenTree/part/templates/part/part_base.html +++ b/InvenTree/part/templates/part/part_base.html @@ -334,6 +334,7 @@ {% else %} {% render_currency pricing.overall_min %} - {% render_currency pricing.overall_max %} {% endif %} + {% if part.units %}  / {{ part.units }}{% endif %} {% endif %} diff --git a/InvenTree/stock/migrations/0094_auto_20230220_0025.py b/InvenTree/stock/migrations/0094_auto_20230220_0025.py new file mode 100644 index 0000000000..f83a21b5e9 --- /dev/null +++ b/InvenTree/stock/migrations/0094_auto_20230220_0025.py @@ -0,0 +1,79 @@ +# Generated by Django 3.2.18 on 2023-02-20 00:25 + +import logging + +from django.db import migrations + +logger = logging.getLogger('inventree') + + +def fix_purchase_price(apps, schema_editor): + """Data migration for fixing historical issue with StockItem.purchase_price field. + + Ref: https://github.com/inventree/InvenTree/pull/4373 + + Due to an existing bug, if a PurchaseOrderLineItem was received, + which had: + + a) A SupplierPart with a non-unity pack size + b) A defined purchase_price + + then the StockItem.purchase_price was not calculated correctly! + + Specifically, the purchase_price was not divided through by the pack_size attribute. + + This migration fixes this by looking through all stock items which: + + - Is linked to a purchase order + - Have a purchase_price field + - Are linked to a supplier_part + - We can determine correctly that the calculation was misapplied + """ + + StockItem = apps.get_model('stock', 'stockitem') + + items = StockItem.objects.exclude( + purchase_order=None + ).exclude( + supplier_part=None + ).exclude( + purchase_price=None + ).exclude( + supplier_part__pack_size=1 + ) + + n_updated = 0 + + for item in items: + # Grab a reference to the associated PurchaseOrder + # Trying to find an absolute match between this StockItem and an associated PurchaseOrderLineItem + po = item.purchase_order + for line in po.lines.all(): + # SupplierPart match + if line.part == item.supplier_part: + # Unit price matches original PurchaseOrder (and is thus incorrect) + if item.purchase_price == line.purchase_price: + item.purchase_price /= item.supplier_part.pack_size + item.save() + + n_updated += 1 + + if n_updated > 0: + logger.info(f"Corrected purchase_price field for {n_updated} stock items.") + + +def reverse(apps, schema_editor): # pragmae: no cover + pass + +class Migration(migrations.Migration): + + dependencies = [ + ('stock', '0093_auto_20230217_2140'), + ] + + operations = [ + migrations.RunPython( + fix_purchase_price, + reverse_code=reverse, + ) + ] diff --git a/InvenTree/stock/templates/stock/item_base.html b/InvenTree/stock/templates/stock/item_base.html index 5dc0b97762..ffe49237f1 100644 --- a/InvenTree/stock/templates/stock/item_base.html +++ b/InvenTree/stock/templates/stock/item_base.html @@ -187,7 +187,10 @@ {% trans "Purchase Price" %} - {% include "price_data.html" with price=item.purchase_price %} + + {% include "price_data.html" with price=item.purchase_price %} + {% if item.part.units %} / {{ item.part.units }}{% endif %} + {% endif %} {% if item.parent %}