mirror of
https://github.com/inventree/InvenTree.git
synced 2026-09-01 17:51:23 +00:00
[Feature] External build order (#9312)
* Add BuildOrder reference to PurchaseOrderLineItem * Add setting to enable / disable external build orders * Fix for supplier part detail * Update forms * Filter build list by "external" status * Add "external" attribute to BuildOrder * Filter by external build when selecting against purchase order line item * Add frontend elements * Prevent creation of build outputs * Tweak related model field - Send filters when fetching initial data * Fix migrations * Fix some existing typos * Add build info when receiving line items * Logic fix * Bump API version * Updated relationship * Add external orders tab for order * Display table of external purchase orders against a build order * Fix permissions * Tweak field definition * Add unit tests * Tweak api_version.py * Playwright testing * Fix discrepancy in 'building' filter * Add basic documentation ( more work required ) * Tweak docs macros * Migration fix * Adjust build page tabs * Fix imports * Fix broken import * Update playywright tests * Bump API version * Handle DB issues * Improve filter * Cleaner code * Fix column ordering bug * Add filters to build output table * Documentation * Tweak unit test * Add "scheduled_for_production" field * Add helper function to part model * Cleanup
This commit is contained in:
@@ -30,6 +30,7 @@ import order.validators
|
||||
import report.mixins
|
||||
import stock.models
|
||||
import users.models as UserModels
|
||||
from build.status_codes import BuildStatus
|
||||
from common.currency import currency_code_default
|
||||
from common.notifications import InvenTreeNotificationBodies
|
||||
from common.settings import get_global_setting
|
||||
@@ -279,7 +280,7 @@ class Order(
|
||||
|
||||
Instances of this class:
|
||||
|
||||
- PuchaseOrder
|
||||
- PurchaseOrder
|
||||
- SalesOrder
|
||||
|
||||
Attributes:
|
||||
@@ -558,7 +559,7 @@ class PurchaseOrder(TotalPriceMixin, Order):
|
||||
|
||||
@classmethod
|
||||
def get_status_class(cls):
|
||||
"""Return the PurchasOrderStatus class."""
|
||||
"""Return the PurchaseOrderStatus class."""
|
||||
return PurchaseOrderStatusGroups
|
||||
|
||||
@classmethod
|
||||
@@ -923,7 +924,7 @@ class PurchaseOrder(TotalPriceMixin, Order):
|
||||
status: The StockStatus to assign to the item (default: StockStatus.OK)
|
||||
|
||||
Keyword Arguments:
|
||||
barch_code: Optional batch code for the new StockItem
|
||||
batch_code: Optional batch code for the new StockItem
|
||||
serials: Optional list of serial numbers to assign to the new StockItem(s)
|
||||
notes: Optional notes field for the StockItem
|
||||
packaging: Optional packaging field for the StockItem
|
||||
@@ -1005,20 +1006,61 @@ class PurchaseOrder(TotalPriceMixin, Order):
|
||||
serialize = False
|
||||
serials = [None]
|
||||
|
||||
# Construct dataset for receiving items
|
||||
data = {
|
||||
'part': line.part.part,
|
||||
'supplier_part': line.part,
|
||||
'location': location,
|
||||
'quantity': 1 if serialize else stock_quantity,
|
||||
'purchase_order': self,
|
||||
'status': status,
|
||||
'batch': batch_code,
|
||||
'expiry_date': expiry_date,
|
||||
'packaging': packaging,
|
||||
'purchase_price': unit_purchase_price,
|
||||
}
|
||||
|
||||
if build_order := line.build_order:
|
||||
# Receiving items against an "external" build order
|
||||
|
||||
if not build_order.external:
|
||||
raise ValidationError(
|
||||
'Cannot receive items against an internal build order'
|
||||
)
|
||||
|
||||
if build_order.part != data['part']:
|
||||
raise ValidationError(
|
||||
'Cannot receive items against a build order for a different part'
|
||||
)
|
||||
|
||||
if not location and build_order.destination:
|
||||
# Override with the build order destination (if not specified)
|
||||
data['location'] = location = build_order.destination
|
||||
|
||||
if build_order.active:
|
||||
# An 'active' build order marks the items as "in production"
|
||||
data['build'] = build_order
|
||||
data['is_building'] = True
|
||||
elif build_order.status == BuildStatus.COMPLETE:
|
||||
# A 'completed' build order marks the items as "completed"
|
||||
data['build'] = build_order
|
||||
data['is_building'] = False
|
||||
|
||||
# Increase the 'completed' quantity for the build order
|
||||
build_order.completed += stock_quantity
|
||||
build_order.save()
|
||||
|
||||
elif build_order.status == BuildStatus.CANCELLED:
|
||||
# A 'cancelled' build order is ignored
|
||||
pass
|
||||
else:
|
||||
# Un-handled state - raise an error
|
||||
raise ValidationError(
|
||||
"Cannot receive items against a build order in state '{build_order.status}'"
|
||||
)
|
||||
|
||||
for sn in serials:
|
||||
item = stock.models.StockItem(
|
||||
part=line.part.part,
|
||||
supplier_part=line.part,
|
||||
location=location,
|
||||
quantity=1 if serialize else stock_quantity,
|
||||
purchase_order=self,
|
||||
status=status,
|
||||
batch=batch_code,
|
||||
expiry_date=expiry_date,
|
||||
packaging=packaging,
|
||||
serial=sn,
|
||||
purchase_price=unit_purchase_price,
|
||||
)
|
||||
item = stock.models.StockItem(serial=sn, **data)
|
||||
|
||||
# Assign the provided barcode
|
||||
if barcode:
|
||||
@@ -1639,6 +1681,11 @@ class PurchaseOrderLineItem(OrderLineItem):
|
||||
|
||||
Attributes:
|
||||
order: Reference to a PurchaseOrder object
|
||||
part: Reference to a SupplierPart object
|
||||
received: Number of items received
|
||||
purchase_price: Unit purchase price for this line item
|
||||
build_order: Link to an external BuildOrder to be fulfilled by this line item
|
||||
destination: Destination for received items
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
@@ -1670,6 +1717,25 @@ class PurchaseOrderLineItem(OrderLineItem):
|
||||
if self.part.supplier != self.order.supplier:
|
||||
raise ValidationError({'part': _('Supplier part must match supplier')})
|
||||
|
||||
if self.build_order:
|
||||
if not self.build_order.external:
|
||||
raise ValidationError({
|
||||
'build_order': _('Build order must be marked as external')
|
||||
})
|
||||
|
||||
if part := self.part.part:
|
||||
if not part.assembly:
|
||||
raise ValidationError({
|
||||
'build_order': _(
|
||||
'Build orders can only be linked to assembly parts'
|
||||
)
|
||||
})
|
||||
|
||||
if self.build_order.part != self.part.part:
|
||||
raise ValidationError({
|
||||
'build_order': _('Build order part must match line item part')
|
||||
})
|
||||
|
||||
def __str__(self):
|
||||
"""Render a string representation of a PurchaseOrderLineItem instance."""
|
||||
return '{n} x {part} - {po}'.format(
|
||||
@@ -1727,6 +1793,17 @@ class PurchaseOrderLineItem(OrderLineItem):
|
||||
"""Return the 'purchase_price' field as 'price'."""
|
||||
return self.purchase_price
|
||||
|
||||
build_order = models.ForeignKey(
|
||||
'build.Build',
|
||||
on_delete=models.SET_NULL,
|
||||
blank=True,
|
||||
related_name='external_line_items',
|
||||
limit_choices_to={'external': True},
|
||||
null=True,
|
||||
verbose_name=_('Build Order'),
|
||||
help_text=_('External Build Order to be fulfilled by this line item'),
|
||||
)
|
||||
|
||||
destination = TreeForeignKey(
|
||||
'stock.StockLocation',
|
||||
on_delete=models.SET_NULL,
|
||||
@@ -2544,7 +2621,7 @@ class ReturnOrder(TotalPriceMixin, Order):
|
||||
|
||||
@transaction.atomic
|
||||
def hold_order(self):
|
||||
"""Attempt to tranasition to ON_HOLD status."""
|
||||
"""Attempt to transition to ON_HOLD status."""
|
||||
return self.handle_transition(
|
||||
self.status, ReturnOrderStatus.ON_HOLD.value, self, self._action_hold
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user