Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Purchase Order Destination #8403

Merged
merged 19 commits into from
Nov 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/docs/order/purchase_order.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,14 @@ There are two options to mark items as "received":
!!! note "Permissions"
Marking line items as received requires the "Purchase order" ADD permission.

### Item Location

When receiving items from a purchase order, the location of the items must be specified. There are multiple ways to specify the location:

* **Order Destination**: The *destination* field of the purchase order can be set to a specific location. When receiving items, the location will default to the destination location.

* **Line Item Location**: Each line item can have a specific location set. When receiving items, the location will default to the line item location. *Note: A destination specified at the line item level will override the destination specified at the order level.*

### Received Items

Each item marked as "received" is automatically converted into a stock item.
Expand Down
7 changes: 5 additions & 2 deletions src/backend/InvenTree/InvenTree/api_version.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down
Original file line number Diff line number Diff line change
@@ -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'),
),
]
12 changes: 11 additions & 1 deletion src/backend/InvenTree/order/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,16 @@ def company(self):
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,
Expand Down Expand Up @@ -1544,7 +1554,7 @@ def price(self):
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):
Expand Down
13 changes: 9 additions & 4 deletions src/backend/InvenTree/order/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,7 @@ class Meta:
'supplier_name',
'total_price',
'order_currency',
'destination',
])

read_only_fields = ['issue_date', 'complete_date', 'creation_date']
Expand Down Expand Up @@ -860,6 +861,7 @@ class Meta:
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'),
Expand All @@ -873,9 +875,10 @@ def validate(self, data):
"""
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'))
Expand Down Expand Up @@ -919,15 +922,17 @@ def save(self):
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()
)

Expand Down
10 changes: 9 additions & 1 deletion src/backend/InvenTree/order/templates/order/order_base.html
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,15 @@
{% 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 %}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,9 @@ <h4>{% trans "Order Notes" %}</h4>
{{ order.id }},
items,
{
{% if order.destination %}
destination: {{ order.destination.pk }},
{% endif %}
success: function() {
$("#po-line-table").bootstrapTable('refresh');
}
Expand Down
5 changes: 2 additions & 3 deletions src/backend/InvenTree/order/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -882,7 +882,6 @@ def test_empty(self):
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())
Expand Down Expand Up @@ -1060,9 +1059,9 @@ def test_valid(self):
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(
Expand Down
9 changes: 9 additions & 0 deletions src/backend/InvenTree/plugin/base/barcodes/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,15 @@ def handle_barcode(self, barcode: str, request, **kwargs):
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
Expand Down
2 changes: 1 addition & 1 deletion src/backend/InvenTree/plugin/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def test_plugin_install(self):
'packagename': 'invalid_package_name-asdads-asfd-asdf-asdf-asdf',
},
expected_code=400,
max_query_time=30,
max_query_time=60,
)

# valid - Pypi
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@ function purchaseOrderFields(options={}) {
target_date: {
icon: 'fa-calendar-alt',
},
destination: {
icon: 'fa-sitemap'
},
link: {
icon: 'fa-link',
},
Expand Down Expand Up @@ -1361,6 +1364,7 @@ function receivePurchaseOrderItems(order_id, line_items, options={}) {
method: 'POST',
fields: {
location: {
value: options.destination,
filters: {
structural: false,
},
Expand Down
19 changes: 16 additions & 3 deletions src/frontend/src/forms/PurchaseOrderForms.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,11 @@ export function usePurchaseOrderFields({
target_date: {
icon: <IconCalendar />
},
destination: {
filters: {
structural: false
}
},
link: {},
contact: {
icon: <IconUser />,
Expand Down Expand Up @@ -232,14 +237,21 @@ function LineItemFormRow({
onClose: () => props.changeFn(props.idx, 'location', undefined)
});

useEffect(() => {
if (!!record.destination) {
props.changeFn(props.idx, 'location', record.destination);
locationHandlers.open();
}
}, [record.destination]);

// Batch code generator
const batchCodeGenerator = useBatchCodeGenerator((value: any) => {
if (value) {
props.changeFn(props.idx, 'batch_code', value);
}
});

// Serial numbebr generator
// Serial number generator
const serialNumberGenerator = useSerialNumberGenerator((value: any) => {
if (value) {
props.changeFn(props.idx, 'serial_numbers', value);
Expand Down Expand Up @@ -475,7 +487,7 @@ function LineItemFormRow({
props.changeFn(props.idx, 'location', value);
},
description: locationDescription,
value: location,
value: props.item.location,
label: t`Location`,
icon: <InvenTreeIcon icon="location" />
}}
Expand Down Expand Up @@ -599,6 +611,7 @@ type LineFormHandlers = {
type LineItemsForm = {
items: any[];
orderPk: number;
destinationPk?: number;
formProps?: LineFormHandlers;
};

Expand Down Expand Up @@ -674,7 +687,7 @@ export function useReceiveLineItems(props: LineItemsForm) {
title: t`Receive Line Items`,
fields: fields,
initialData: {
location: null
location: props.destinationPk
},
size: '80%'
});
Expand Down
8 changes: 8 additions & 0 deletions src/frontend/src/pages/build/BuildDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,14 @@ export default function BuildDetail() {
label: t`Completed`,
icon: 'calendar',
hidden: !build.completion_date
},
{
type: 'text',
name: 'project_code_label',
label: t`Project Code`,
icon: 'reference',
copy: true,
hidden: !build.project_code
}
];

Expand Down
22 changes: 18 additions & 4 deletions src/frontend/src/pages/purchasing/PurchaseOrderDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -153,12 +153,19 @@ export default function PurchaseOrderDetail() {
total: order.line_items,
progress: order.completed_lines
},
{
type: 'link',
model: ModelType.stocklocation,
link: true,
name: 'destination',
label: t`Destination`,
hidden: !order.destination
},
{
type: 'text',
name: 'currency',
label: t`Order Currency`,
value_formatter: () =>
order?.order_currency ?? order?.supplier_detail?.currency
value_formatter: () => orderCurrency
},
{
type: 'text',
Expand Down Expand Up @@ -190,8 +197,15 @@ export default function PurchaseOrderDetail() {
icon: 'user',
copy: true,
hidden: !order.contact
},
{
type: 'text',
name: 'project_code_label',
label: t`Project Code`,
icon: 'reference',
copy: true,
hidden: !order.project_code
}
// TODO: Project code
];

let br: DetailsField[] = [
Expand Down Expand Up @@ -253,7 +267,7 @@ export default function PurchaseOrderDetail() {
<DetailsTable fields={br} item={order} />
</ItemDetailsGrid>
);
}, [order, instanceQuery]);
}, [order, orderCurrency, instanceQuery]);

const orderPanels: PanelType[] = useMemo(() => {
return [
Expand Down
9 changes: 8 additions & 1 deletion src/frontend/src/pages/sales/ReturnOrderDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -167,8 +167,15 @@ export default function ReturnOrderDetail() {
icon: 'user',
copy: true,
hidden: !order.contact
},
{
type: 'text',
name: 'project_code_label',
label: t`Project Code`,
icon: 'reference',
copy: true,
hidden: !order.project_code
}
// TODO: Project code
];

let br: DetailsField[] = [
Expand Down
9 changes: 8 additions & 1 deletion src/frontend/src/pages/sales/SalesOrderDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -179,8 +179,15 @@ export default function SalesOrderDetail() {
icon: 'user',
copy: true,
hidden: !order.contact
},
{
type: 'text',
name: 'project_code_label',
label: t`Project Code`,
icon: 'reference',
copy: true,
hidden: !order.project_code
}
// TODO: Project code
];

let br: DetailsField[] = [
Expand Down
Loading
Loading