Repository: inventree/InvenTree

Issue:
Purchase order clarification
Since there is a lack of documentation about PO I would like to ask some (maybe silly) questions.

Prerequisites:
- Parts with filled manufacturer MPN and/or supplier SKU.

What is a suggested manual PO workflow?
- Create PO
- Use "Order Parts" dialog from Part to add a line to PO.
- (optional) Add order lines manually.
- Click "Place order"
- Make real order on supplier(s) website. Wait for shipping.
- Click "Receive Parts", fill actual received amount and status
- Click "Complete Order"

Is it a correct Upload File for Purchase Order UseCase?
- Create PO
- Make order on supplier website
- Receive parts and accounting + inventory docs
- Upload inventory sheet to PO. Automatically fill order item lines, quantities, prices.
- Click "Place order"
- Click "Receive Parts", fill actual received amount and status
- Click "Complete Order"

Questions:
- Currently it looks like I have to fill part price even before making real supplier order, is it correct?
- What if price has changed during making real order? But as I already clicked "Place order" I can't change item price.
- What is the purpose of "destination" field in order item line, if I have to choose "Destination" each time I click "Receive Parts"?
- Does it make sense to keep "Pricing ranges" in inventree because supplier prices could change pretty often.
- Does it make sense to fill order item price when parts are received (not before they are ordered)?



Discussion:
Both workflows look correct to me.

> * Currently it looks like I have to fill part price even before making real supplier order, is it correct?

You don't have to do it before, after is fine too at the moment because "placing the order" is just updating your database locally, it does not interact with your supplier.

> * What if price has changed during making real order? But as I already clicked "Place order" I can't change item price.

That's a use-case not covered yet. You can however alter the status of the order in the Admin section :smiley: 

> * What is the purpose of "destination" field in order item line, if I have to choose "Destination" each time I click "Receive Parts"?

The destination is where all the received items will be stored as stock items when you mark the full order or some items as "received".

> * Does it make sense to keep "Pricing ranges" in inventree because supplier prices could change pretty often.

Still makes sense to have local data for reference even if not accurate / obsolete. But yes you're right supplier prices vary a lot, especially this year with the component shortage!

> * Does it make sense to fill order item price when parts are received (not before they are ordered)?

It could make sense to do it **after your supplier confirmed your order**, after that they shouldn't be able to change the prices. But it could happen that the quantities they send to you are lower than what you expected.
Thank you @eeintech it is more clear now.

> The destination is where all the received items will be stored as stock items when you mark the full order or some items as "received".

But why then it is asking destination explicitly each time I do receive parts?
Also what does it mean "Select parts to receive against this order", how do I select?
![изображение](https://user-images.githubusercontent.com/10057327/126291923-874610f0-3f06-404f-adf3-8db71346cfec.png)

@sintech If you have multiple parts in the order, it lets you the option to select a different destination (eg. a stock location) for each of them. You can also select a single destination for all of them if you click on the global receive button.

The "Select parts to receive against this order" in your case is a little confusing because you have only one part in your order. If you had a list of parts, that instruction means to fill out the "Receive" column with the number of parts received.
Ok, I have a feeling of issue here.
Screenshot above is from a click to "Receive line item". and "Destination" is required field, I can't submit form without selecting destination.
If I click global "Receive items" button the behavior is the same, I forced to choose destination. 
And in this case I expect that only `None` destinations will be rewritten by value from "Destination" field. While in reality they all got rewritten.
![изображение](https://user-images.githubusercontent.com/10057327/126316808-b36eb73b-134f-486e-9297-61859a4848b0.png)

@sintech Yes you're right, if destination field is set on all items in the list, it should be okay to submit the form without selecting a choice in the "Destination" dropdown field. And it shouldn't be mandatory to select "Destination" if one or more items in the list don't have a destination set because stock item location is not mandatory. However, if the destination is selected in the form, it **should** overwrite any previous destination set as a shortcut to reset the storing location for all items. If left blank, the form should use the information from the destination column. Does it makes sense?
@eeintech sure it does. 
You described exactly what I expected from this form, except for blank destination, but it's ok since item location is optional.
Actually it would be great to have destination selector on every line of the table for further flexibility. It will be a step closer to bulk part operations.
@sintech You can already select the destination for each line but it has to be done in the order item table, not in the receive item form.
Sure I can. 
But for 20 lines in order I have to click at least 60 times to change destination (not counting scrolling).
And it looks more logical to set part location on receive phase not before order is placed.
Anyway I will be happy if "Receive parts" is fixed according to https://github.com/inventree/InvenTree/issues/1832#issuecomment-883441266
Shall I make a new issue?
No that's fine, should have time to fix the behavior soon :+1: 

Resolving patch:
diff --git a/InvenTree/order/forms.py b/InvenTree/order/forms.py
--- a/InvenTree/order/forms.py
+++ b/InvenTree/order/forms.py
@@ -84,9 +84,9 @@ class ReceivePurchaseOrderForm(HelperForm):
 
     location = TreeNodeChoiceField(
         queryset=StockLocation.objects.all(),
-        required=True,
+        required=False,
         label=_("Destination"),
-        help_text=_("Receive parts to this location"),
+        help_text=_("Set all received parts listed above to this location (if left blank, use \"Destination\" column value in above table)"),
     )
 
     class Meta:
diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py
--- a/InvenTree/order/views.py
+++ b/InvenTree/order/views.py
@@ -491,6 +491,7 @@ def get_context_data(self):
         ctx = {
             'order': self.order,
             'lines': self.lines,
+            'stock_locations': StockLocation.objects.all(),
         }
 
         return ctx
@@ -543,6 +544,7 @@ def post(self, request, *args, **kwargs):
 
         self.request = request
         self.order = get_object_or_404(PurchaseOrder, pk=self.kwargs['pk'])
+        errors = False
 
         self.lines = []
         self.destination = None
@@ -557,12 +559,6 @@ def post(self, request, *args, **kwargs):
             except (StockLocation.DoesNotExist, ValueError):
                 pass
 
-        errors = False
-
-        if self.destination is None:
-            errors = True
-            msg = _("No destination set")
-
         # Extract information on all submitted line items
         for item in request.POST:
             if item.startswith('line-'):
@@ -587,6 +583,21 @@ def post(self, request, *args, **kwargs):
                 else:
                     line.status_code = StockStatus.OK
 
+                # Check the destination field
+                line.destination = None
+                if self.destination:
+                    # If global destination is set, overwrite line value
+                    line.destination = self.destination
+                else:
+                    destination_key = f'destination-{pk}'
+                    destination = request.POST.get(destination_key, None)
+
+                    if destination:
+                        try:
+                            line.destination = StockLocation.objects.get(pk=destination)
+                        except (StockLocation.DoesNotExist, ValueError):
+                            pass
+
                 # Check that line matches the order
                 if not line.order == self.order:
                     # TODO - Display a non-field error?
@@ -645,7 +656,7 @@ def receive_parts(self):
 
             self.order.receive_line_item(
                 line,
-                self.destination,
+                line.destination,
                 line.receive_quantity,
                 self.request.user,
                 status=line.status_code,


Test patch: