Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"""Add piece_count and piece_size fields to BomItem model.

These fields support cut-to-length parts (cables, tubing, profiles) where
a BOM line requires multiple pieces of a specific size, rather than a
single total quantity.
"""

import django.core.validators
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('part', '0152_alter_partpricing_currency'),
]

operations = [
migrations.AddField(
model_name='bomitem',
name='piece_count',
field=models.PositiveIntegerField(
default=1,
help_text='Number of pieces required (for cut-to-length items)',
validators=[django.core.validators.MinValueValidator(1)],
verbose_name='Piece Count',
),
),
migrations.AddField(
model_name='bomitem',
name='piece_size',
field=models.CharField(
blank=True,
help_text='Size of each piece (e.g. "250 mm"). When specified, total quantity = piece_count × piece_size.',
max_length=25,
verbose_name='Piece Size',
),
),
]
55 changes: 54 additions & 1 deletion src/backend/InvenTree/part/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3651,6 +3651,8 @@ class BomItem(InvenTree.models.MetadataMixin, InvenTree.models.InvenTreeModel):
setup_quantity: Extra required quantity for a build, to account for setup losses
attrition: Estimated losses for a Build, expressed as a percentage (e.g. '2%')
rounding_multiple: Rounding quantity when calculating the required quantity for a build
piece_count: Number of pieces required (for cut-to-length items like cables, tubing)
piece_size: Size of each piece (e.g. '250 mm'); when set, quantity = piece_count * piece_size
note: Note field for this BOM item
checksum: Validation checksum for the particular BOM line item
validated: Boolean field indicating if this BOM item is valid (checksum matches)
Expand Down Expand Up @@ -3752,7 +3754,39 @@ def set_quantity(self, quantity: Decimal | str | float):
self.recalculate_quantity()

def recalculate_quantity(self):
"""Recalculate the 'quantity' field based on the 'raw_amount' field."""
"""Recalculate the 'quantity' field based on the 'raw_amount' field.

If piece_size is specified, the effective raw_amount is calculated as
piece_count * piece_size (for cut-to-length parts like cables, tubing,
or profiles). Otherwise, raw_amount is used directly.
"""
# If piece_size is provided, compute total amount from piece_count * piece_size
if self.piece_size and self.piece_size.strip():
try:
piece_qty = InvenTree.conversion.convert_physical_value(
self.piece_size, self.sub_part.units, strip_units=False
)

if float(piece_qty.magnitude) <= 0:
raise ValidationError({
'piece_size': _('Piece size must be greater than zero')
})

total_magnitude = Decimal(piece_qty.magnitude) * Decimal(self.piece_count)

# Store the computed total as raw_amount for display consistency
if self.sub_part.units:
self.raw_amount = f'{total_magnitude} {self.sub_part.units}'
else:
self.raw_amount = str(total_magnitude)

except ValidationError:
raise
except Exception:
raise ValidationError({
'piece_size': _('Invalid piece size value')
})

if self.raw_amount is None or self.raw_amount == '':
self.raw_amount = self.quantity

Expand Down Expand Up @@ -3971,6 +4005,23 @@ def check_part_lock(self, assembly):
),
)

piece_count = models.PositiveIntegerField(
default=1,
validators=[MinValueValidator(1)],
verbose_name=_('Piece Count'),
help_text=_('Number of pieces required (for cut-to-length items)'),
)

piece_size = models.CharField(
max_length=25,
blank=True,
verbose_name=_('Piece Size'),
help_text=_(
'Size of each piece (e.g. "250 mm"). '
'When specified, total quantity = piece_count × piece_size.'
),
)

reference = models.CharField(
max_length=5000,
blank=True,
Expand Down Expand Up @@ -4025,6 +4076,8 @@ def hash_fields(self) -> list[str]:
'setup_quantity',
'attrition',
'rounding_multiple',
'piece_count',
'piece_size',
'reference',
'optional',
'inherited',
Expand Down
17 changes: 17 additions & 0 deletions src/backend/InvenTree/part/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1666,6 +1666,8 @@ class Meta:
'reference',
'raw_amount',
'quantity',
'piece_count',
'piece_size',
'allow_variants',
'inherited',
'optional',
Expand Down Expand Up @@ -1715,6 +1717,21 @@ class Meta:
required=False, allow_null=True
)

piece_count = serializers.IntegerField(
required=False,
default=1,
label=_('Piece Count'),
help_text=_('Number of pieces required (for cut-to-length items)'),
)

piece_size = serializers.CharField(
required=False,
allow_blank=True,
default='',
label=_('Piece Size'),
help_text=_('Size of each piece (e.g. "250 mm")'),
)

part = serializers.PrimaryKeyRelatedField(
queryset=Part.objects.filter(assembly=True),
label=_('Assembly'),
Expand Down
8 changes: 8 additions & 0 deletions src/frontend/src/forms/BomForms.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,14 @@ export function bomItemFields({
label: t`Quantity`,
description: t`Required component quantity`
},
piece_count: {
label: t`Piece Count`,
description: t`Number of pieces required (for cut-to-length items)`
},
piece_size: {
label: t`Piece Size`,
description: t`Size of each piece (e.g. "250 mm")`
},
reference: {},
setup_quantity: {},
attrition: {},
Expand Down
26 changes: 26 additions & 0 deletions src/frontend/src/tables/bom/BomTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,32 @@ export function BomTable({
}
}
},
{
accessor: 'piece_count',
defaultVisible: false,
sortable: true,
render: (record: any) => {
const piece_count = record.piece_count;
if (piece_count == null || piece_count <= 1) {
return '-';
} else {
return <Text size='xs'>{piece_count}</Text>;
}
}
},
{
accessor: 'piece_size',
defaultVisible: false,
sortable: false,
render: (record: any) => {
const piece_size = record.piece_size;
if (!piece_size) {
return '-';
} else {
return <Text size='xs'>{piece_size}</Text>;
}
}
},
{
accessor: 'substitutes',
defaultVisible: false,
Expand Down