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

[WIP] Build order consume #8191

Draft
wants to merge 17 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 11 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 src/backend/InvenTree/build/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,14 @@ def filter_allocated(self, queryset, name, value):
return queryset.filter(allocated__gte=F('quantity'))
return queryset.filter(allocated__lt=F('quantity'))

consumed = rest_filters.BooleanFilter(label=_('Consumed'), method='filter_consumed')

def filter_consumed(self, queryset, name, value):
"""Filter by whether each BuildLine is fully consumed"""
if str2bool(value):
return queryset.filter(consumed__gte=F('quantity'))
return queryset.filter(consumed__lt=F('quantity'))

available = rest_filters.BooleanFilter(label=_('Available'), method='filter_available')

def filter_available(self, queryset, name, value):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Generated by Django 4.2.15 on 2024-09-26 10:11

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


class Migration(migrations.Migration):

dependencies = [
('build', '0052_build_status_custom_key_alter_build_status'),
]

operations = [
migrations.AddField(
model_name='buildline',
name='consumed',
field=models.DecimalField(decimal_places=5, default=0, help_text='Quantity of consumed stock', max_digits=15, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Consumed'),
),
]
25 changes: 25 additions & 0 deletions src/backend/InvenTree/build/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1471,6 +1471,7 @@ class BuildLine(report.mixins.InvenTreeReportMixin, InvenTree.models.InvenTreeMo
build: Link to a Build object
bom_item: Link to a BomItem object
quantity: Number of units required for the Build
consumed: Number of units which have been consumed against this line item
"""

class Meta:
Expand Down Expand Up @@ -1518,6 +1519,15 @@ def report_context(self):
help_text=_('Required quantity for build order'),
)

consumed = models.DecimalField(
decimal_places=5,
max_digits=15,
default=0,
validators=[MinValueValidator(0)],
verbose_name=_('Consumed'),
help_text=_('Quantity of consumed stock'),
)

@property
def part(self):
"""Return the sub_part reference from the link bom_item"""
Expand Down Expand Up @@ -1549,6 +1559,10 @@ def is_overallocated(self):
"""Return True if this BuildLine is over-allocated"""
return self.allocated_quantity() > self.quantity

def is_fully_consumed(self):
"""Return True if this BuildLine is fully consumed"""
return self.consumed >= self.quantity


class BuildItem(InvenTree.models.InvenTreeMetadataModel):
"""A BuildItem links multiple StockItem objects to a Build.
Expand Down Expand Up @@ -1713,10 +1727,17 @@ def complete_allocation(self, user, notes=''):
- If the referenced part is *not* trackable, the stock item will be *consumed* by the build order

TODO: This is quite expensive (in terms of number of database hits) - and requires some thought
TODO: Revisit, and refactor!

"""
item = self.stock_item

# Ensure we are not allocating more than available
if self.quantity > item.quantity:
raise ValidationError({
'quantity': _('Allocated quantity exceeds available stock quantity')
})

# Split the allocated stock if there are more available than allocated
if item.quantity > self.quantity:
item = item.splitStock(
Expand Down Expand Up @@ -1757,6 +1778,10 @@ def complete_allocation(self, user, notes=''):
}
)

# Increase the "consumed" count for the associated BuildLine
self.build_line.consumed += self.quantity
self.build_line.save()

build_line = models.ForeignKey(
BuildLine,
on_delete=models.CASCADE, null=True,
Expand Down
4 changes: 3 additions & 1 deletion src/backend/InvenTree/build/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1279,6 +1279,7 @@ class Meta:
'bom_item_detail',
'part_detail',
'quantity',
'consumed',
'allocations',

# BOM item detail fields
Expand Down Expand Up @@ -1336,6 +1337,7 @@ class Meta:
allow_variants = serializers.BooleanField(source='bom_item.allow_variants', label=_('Allow Variants'), read_only=True)

quantity = serializers.FloatField(label=_('Quantity'))
consumed = serializers.FloatField(label=_('Consumed'))

bom_item = serializers.PrimaryKeyRelatedField(label=_('BOM Item'), read_only=True)

Expand All @@ -1346,7 +1348,7 @@ class Meta:

# Annotated (calculated) fields
allocated = serializers.FloatField(
label=_('Allocated Stock'),
label=_('Allocated'),
read_only=True
)

Expand Down
22 changes: 18 additions & 4 deletions src/frontend/src/components/buttons/YesNoButton.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,31 @@
import { t } from '@lingui/macro';
import { Badge, Skeleton } from '@mantine/core';
import { Badge, MantineColor, Skeleton } from '@mantine/core';

import { isTrue } from '../../functions/conversion';

export function PassFailButton({
value,
passText,
failText
failText,
passColor,
failColor
}: Readonly<{
value: any;
passText?: string;
failText?: string;
passColor?: MantineColor;
failColor?: MantineColor;
}>) {
const v = isTrue(value);
const pass = passText ?? t`Pass`;
const fail = failText ?? t`Fail`;

const pColor = passColor ?? 'lime.5';
const fColor = failColor ?? 'red.6';

return (
<Badge
color={v ? 'lime.5' : 'red.6'}
color={v ? pColor : fColor}
variant="filled"
radius="lg"
size="sm"
Expand All @@ -30,7 +37,14 @@ export function PassFailButton({
}

export function YesNoButton({ value }: Readonly<{ value: any }>) {
return <PassFailButton value={value} passText={t`Yes`} failText={t`No`} />;
return (
<PassFailButton
value={value}
passText={t`Yes`}
failText={t`No`}
failColor={'orange.6'}
/>
);
}

export function YesNoUndefinedButton({ value }: Readonly<{ value?: boolean }>) {
Expand Down
6 changes: 4 additions & 2 deletions src/frontend/src/tables/ColumnRenderers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* Common rendering functions for table column data.
*/
import { t } from '@lingui/macro';
import { Anchor, Group, Skeleton, Text, Tooltip } from '@mantine/core';
import { Anchor, Center, Group, Skeleton, Text, Tooltip } from '@mantine/core';
import { IconBell, IconExclamationCircle, IconLock } from '@tabler/icons-react';

import { YesNoButton } from '../components/buttons/YesNoButton';
Expand Down Expand Up @@ -80,7 +80,9 @@ export function BooleanColumn(props: TableColumn): TableColumn {
sortable: true,
switchable: true,
render: (record: any) => (
<YesNoButton value={resolveItem(record, props.accessor ?? '')} />
<Center>
<YesNoButton value={resolveItem(record, props.accessor ?? '')} />
</Center>
),
...props
};
Expand Down
23 changes: 22 additions & 1 deletion src/frontend/src/tables/build/BuildLineTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,12 @@ export default function BuildLineTable({
{
name: 'allocated',
label: t`Allocated`,
description: t`Show allocated lines`
description: t`Show fully allocated lines`
},
{
name: 'consumed',
label: t`Consumed`,
description: t`Show fully consumed lines`
},
{
name: 'available',
Expand Down Expand Up @@ -268,12 +273,28 @@ export default function BuildLineTable({
switchable: false,
hidden: !isActive,
render: (record: any) => {
const required = Math.max(0, record.quantity - record.consumed);

return record?.bom_item_detail?.consumable ? (
<Text style={{ fontStyle: 'italic' }}>{t`Consumable item`}</Text>
) : (
<ProgressBar
progressLabel={true}
value={record.allocated}
maximum={required}
/>
);
}
},
{
accessor: 'consumed',
render: (record: any) => {
return record?.bom_item_detail?.consumable ? (
<Text style={{ fontStyle: 'italic' }}>{t`Consumable Item`}</Text>
) : (
<ProgressBar
progressLabel={true}
value={record.consumed}
maximum={record.quantity}
/>
);
Expand Down
Loading