mirror of
https://github.com/inventree/InvenTree.git
synced 2025-06-17 12:35:46 +00:00
Merge branch 'inventree:master' into part-import
This commit is contained in:
@ -68,6 +68,7 @@
|
||||
order: 1
|
||||
part: 1
|
||||
quantity: 100
|
||||
destination: 5 # Desk/Drawer_1
|
||||
|
||||
# 250 x ACME0002 (M2x4 LPHS)
|
||||
# Partially received (50)
|
||||
@ -95,3 +96,10 @@
|
||||
part: 3
|
||||
quantity: 100
|
||||
|
||||
# 1 x R_4K7_0603
|
||||
- model: order.purchaseorderlineitem
|
||||
pk: 23
|
||||
fields:
|
||||
order: 1
|
||||
part: 5
|
||||
quantity: 1
|
||||
|
@ -86,12 +86,17 @@ class ShipSalesOrderForm(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:
|
||||
model = PurchaseOrder
|
||||
fields = [
|
||||
'location',
|
||||
"location",
|
||||
]
|
||||
|
||||
|
||||
@ -202,6 +207,7 @@ class EditPurchaseOrderLineItemForm(HelperForm):
|
||||
'quantity',
|
||||
'reference',
|
||||
'purchase_price',
|
||||
'destination',
|
||||
'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 markdownx.models import MarkdownxField
|
||||
from mptt.models import TreeForeignKey
|
||||
|
||||
from djmoney.models.fields import MoneyField
|
||||
|
||||
@ -672,6 +673,29 @@ class PurchaseOrderLineItem(OrderLineItem):
|
||||
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):
|
||||
""" Calculate the number of items remaining to be received """
|
||||
r = self.quantity - self.received
|
||||
|
@ -17,6 +17,7 @@ from InvenTree.serializers import InvenTreeAttachmentSerializerField
|
||||
|
||||
from company.serializers import CompanyBriefSerializer, SupplierPartSerializer
|
||||
from part.serializers import PartBriefSerializer
|
||||
from stock.serializers import LocationBriefSerializer
|
||||
|
||||
from .models import PurchaseOrder, PurchaseOrderLineItem
|
||||
from .models import PurchaseOrderAttachment, SalesOrderAttachment
|
||||
@ -116,6 +117,8 @@ class POLineItemSerializer(InvenTreeModelSerializer):
|
||||
|
||||
purchase_price_string = serializers.CharField(source='purchase_price', read_only=True)
|
||||
|
||||
destination = LocationBriefSerializer(source='get_destination', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = PurchaseOrderLineItem
|
||||
|
||||
@ -132,6 +135,7 @@ class POLineItemSerializer(InvenTreeModelSerializer):
|
||||
'purchase_price',
|
||||
'purchase_price_currency',
|
||||
'purchase_price_string',
|
||||
'destination',
|
||||
]
|
||||
|
||||
|
||||
|
@ -117,6 +117,7 @@ $("#po-table").inventreeTable({
|
||||
part_detail: true,
|
||||
},
|
||||
url: "{% url 'api-po-line-list' %}",
|
||||
showFooter: true,
|
||||
columns: [
|
||||
{
|
||||
field: 'pk',
|
||||
@ -137,6 +138,9 @@ $("#po-table").inventreeTable({
|
||||
return '-';
|
||||
}
|
||||
},
|
||||
footerFormatter: function() {
|
||||
return '{% trans "Total" %}'
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'part_detail.description',
|
||||
@ -172,7 +176,14 @@ $("#po-table").inventreeTable({
|
||||
{
|
||||
sortable: true,
|
||||
field: 'quantity',
|
||||
title: '{% trans "Quantity" %}'
|
||||
title: '{% trans "Quantity" %}',
|
||||
footerFormatter: function(data) {
|
||||
return data.map(function (row) {
|
||||
return +row['quantity']
|
||||
}).reduce(function (sum, i) {
|
||||
return sum + i
|
||||
}, 0)
|
||||
}
|
||||
},
|
||||
{
|
||||
sortable: true,
|
||||
@ -182,6 +193,25 @@ $("#po-table").inventreeTable({
|
||||
return row.purchase_price_string || row.purchase_price;
|
||||
}
|
||||
},
|
||||
{
|
||||
sortable: true,
|
||||
title: '{% trans "Total price" %}',
|
||||
formatter: function(value, row) {
|
||||
var total = row.purchase_price * row.quantity;
|
||||
var formatter = new Intl.NumberFormat('en-US', {style: 'currency', currency: row.purchase_price_currency});
|
||||
return formatter.format(total)
|
||||
},
|
||||
footerFormatter: function(data) {
|
||||
var total = data.map(function (row) {
|
||||
return +row['purchase_price']*row['quantity']
|
||||
}).reduce(function (sum, i) {
|
||||
return sum + i
|
||||
}, 0)
|
||||
var currency = (data.slice(-1)[0] && data.slice(-1)[0].purchase_price_currency) || 'USD';
|
||||
var formatter = new Intl.NumberFormat('en-US', {style: 'currency', currency: currency});
|
||||
return formatter.format(total)
|
||||
}
|
||||
},
|
||||
{
|
||||
sortable: true,
|
||||
field: 'received',
|
||||
@ -204,6 +234,10 @@ $("#po-table").inventreeTable({
|
||||
return (progressA < progressB) ? 1 : -1;
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'destination.pathstring',
|
||||
title: '{% trans "Destination" %}',
|
||||
},
|
||||
{
|
||||
field: 'notes',
|
||||
title: '{% trans "Notes" %}',
|
||||
|
@ -22,6 +22,7 @@
|
||||
<th>{% trans "Received" %}</th>
|
||||
<th>{% trans "Receive" %}</th>
|
||||
<th>{% trans "Status" %}</th>
|
||||
<th>{% trans "Destination" %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
{% for line in lines %}
|
||||
@ -53,6 +54,9 @@
|
||||
</select>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
{{ line.get_destination }}
|
||||
</td>
|
||||
<td>
|
||||
<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>
|
||||
|
@ -199,6 +199,7 @@ $("#so-lines-table").inventreeTable({
|
||||
detailFormatter: showFulfilledSubTable,
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
showFooter: true,
|
||||
columns: [
|
||||
{
|
||||
field: 'pk',
|
||||
@ -217,7 +218,10 @@ $("#so-lines-table").inventreeTable({
|
||||
} else {
|
||||
return '-';
|
||||
}
|
||||
}
|
||||
},
|
||||
footerFormatter: function() {
|
||||
return '{% trans "Total" %}'
|
||||
},
|
||||
},
|
||||
{
|
||||
sortable: true,
|
||||
@ -228,6 +232,13 @@ $("#so-lines-table").inventreeTable({
|
||||
sortable: true,
|
||||
field: 'quantity',
|
||||
title: '{% trans "Quantity" %}',
|
||||
footerFormatter: function(data) {
|
||||
return data.map(function (row) {
|
||||
return +row['quantity']
|
||||
}).reduce(function (sum, i) {
|
||||
return sum + i
|
||||
}, 0)
|
||||
},
|
||||
},
|
||||
{
|
||||
sortable: true,
|
||||
@ -237,6 +248,26 @@ $("#so-lines-table").inventreeTable({
|
||||
return row.sale_price_string || row.sale_price;
|
||||
}
|
||||
},
|
||||
{
|
||||
sortable: true,
|
||||
title: '{% trans "Total price" %}',
|
||||
formatter: function(value, row) {
|
||||
var total = row.sale_price * row.quantity;
|
||||
var formatter = new Intl.NumberFormat('en-US', {style: 'currency', currency: row.sale_price_currency});
|
||||
return formatter.format(total)
|
||||
},
|
||||
footerFormatter: function(data) {
|
||||
var total = data.map(function (row) {
|
||||
return +row['sale_price']*row['quantity']
|
||||
}).reduce(function (sum, i) {
|
||||
return sum + i
|
||||
}, 0)
|
||||
var currency = (data.slice(-1)[0] && data.slice(-1)[0].sale_price_currency) || 'USD';
|
||||
var formatter = new Intl.NumberFormat('en-US', {style: 'currency', currency: currency});
|
||||
return formatter.format(total)
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
field: 'allocated',
|
||||
{% if order.status == SalesOrderStatus.PENDING %}
|
||||
|
@ -87,7 +87,7 @@ class OrderTest(TestCase):
|
||||
order = PurchaseOrder.objects.get(pk=1)
|
||||
|
||||
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')
|
||||
part = sku.part
|
||||
@ -105,11 +105,11 @@ class OrderTest(TestCase):
|
||||
order.add_line_item(sku, 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.add_line_item(sku, 50)
|
||||
self.assertEqual(order.lines.count(), 4)
|
||||
self.assertEqual(order.lines.count(), 5)
|
||||
self.assertEqual(part.on_order, 150)
|
||||
|
||||
# Try to order a supplier part from the wrong supplier
|
||||
@ -163,7 +163,7 @@ class OrderTest(TestCase):
|
||||
loc = StockLocation.objects.get(id=1)
|
||||
|
||||
# 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"
|
||||
self.assertEqual(order.status, PurchaseOrderStatus.PENDING)
|
||||
|
Reference in New Issue
Block a user