2
0
mirror of https://github.com/inventree/InvenTree.git synced 2026-03-04 03:11:46 +00:00

Add "updated_at" field to Orders model (#11374)

* add "updated_at" field to PurchaseOrder model

* change to use abstract po instead

* add api filters

* add show, order and filter by po updated_at date to frontend

* add tests and increment api_version

* change updated_at to null by default

* never trust github conflict resolution

* bump docker image to python 3.14 (#11414)

* chore(deps): bump the dependencies group across 1 directory with 4 updates (#11416)

Bumps the dependencies group with 4 updates in the / directory: [depot/setup-action](https://github.com/depot/setup-action), [depot/build-push-action](https://github.com/depot/build-push-action), [anchore/sbom-action](https://github.com/anchore/sbom-action) and [actions/stale](https://github.com/actions/stale).


Updates `depot/setup-action` from 1.6.0 to 1.7.1
- [Release notes](https://github.com/depot/setup-action/releases)
- [Commits](b0b1ea4f69...15c09a5f77)

Updates `depot/build-push-action` from 1.16.2 to 1.17.0
- [Release notes](https://github.com/depot/build-push-action/releases)
- [Commits](9785b135c3...5f3b3c2e5a)

Updates `anchore/sbom-action` from 0.21.1 to 0.22.2
- [Release notes](https://github.com/anchore/sbom-action/releases)
- [Changelog](https://github.com/anchore/sbom-action/blob/main/RELEASE.md)
- [Commits](0b82b0b1a2...28d71544de)

Updates `actions/stale` from 10.1.1 to 10.2.0
- [Release notes](https://github.com/actions/stale/releases)
- [Changelog](https://github.com/actions/stale/blob/main/CHANGELOG.md)
- [Commits](997185467f...b5d41d4e1d)

---
updated-dependencies:
- dependency-name: depot/setup-action
  dependency-version: 1.7.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: dependencies
- dependency-name: depot/build-push-action
  dependency-version: 1.17.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: dependencies
- dependency-name: anchore/sbom-action
  dependency-version: 0.22.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: dependencies
- dependency-name: actions/stale
  dependency-version: 10.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* [UI] Copy cells expansion (#11410)

* Prevent copy button if copy value is null

* Add "link" columns to order tables

* Support copy for default column types

* Tweak padding to avoid flickering issues

* Refactor IPNColumn

* Adjust visual styling

* Copy for SKU and MPN columns

* Add more copy columns

* More tweaks

* Tweak playwright testing

* Further cleanup

* More copy cols

* Fix auto pricing overwriting manual purchase price #10846 (#11411)

* Fix auto pricing overwriting manual purchase price #10846

* Added entry to api_version.py

---------

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>

* [UI] Default locale (#11412)

* [UI] Support default server language

* Handle faulty theme

* Add option for default language

* Improve language selection

* Brief docs entry

* Fix typo

* Fix yarn build

* Remove debug msg

* Fix calendar locale

* feat(backend): ensure restore of backups only works in correct enviroments (#11372)

* [FR] ensure restore of backups only works in correct enviroments
Fixes #11214

* update PR nbr

* fix wrong ty detection

* fix link

* ensure tracing does not enagage while running backup ops

* fix import

* remove debugging string

* add error codes

* add tests for backup and restore

* complete test for restore

* we do not need e2e on every matrix entry
there is no realy db dep here

* fix changelog format

* add flag to allow bypass

* update CHANGELOG.md

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
Co-authored-by: Matthias Mair <code@mjmair.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: JustusRijke <53965859+JustusRijke@users.noreply.github.com>
This commit is contained in:
Jack
2026-02-28 09:07:02 +11:00
committed by GitHub
parent 23d60bf05f
commit 3d9104e9a5
16 changed files with 385 additions and 18 deletions

View File

@@ -1,11 +1,16 @@
"""InvenTree API version information."""
# InvenTree API version
INVENTREE_API_VERSION = 459
INVENTREE_API_VERSION = 460
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
INVENTREE_API_TEXT = """
v460 -> 2026-02-25 : https://github.com/inventree/InvenTree/pull/11374
- Adds "updated_at" field to PurchaseOrder, SalesOrder and ReturnOrder API endpoints
- Adds "updated_before" and "updated_after" date filters to all three order list endpoints
- Adds "updated_at" ordering option to all three order list endpoints
v459 -> 2026-02-23 : https://github.com/inventree/InvenTree/pull/11411
- Changed PurchaseOrderLine "auto_pricing" default value from true to false

View File

@@ -228,6 +228,14 @@ class OrderFilter(FilterSet):
label=_('Target Date After'), field_name='target_date', lookup_expr='gt'
)
updated_before = InvenTreeDateFilter(
label=_('Updated Before'), field_name='updated_at', lookup_expr='lt'
)
updated_after = InvenTreeDateFilter(
label=_('Updated After'), field_name='updated_at', lookup_expr='gt'
)
min_date = InvenTreeDateFilter(label=_('Min Date'), method='filter_min_date')
def filter_min_date(self, queryset, name, value):
@@ -420,6 +428,7 @@ class PurchaseOrderList(
'responsible',
'total_price',
'project_code',
'updated_at',
]
ordering = '-reference'
@@ -882,6 +891,7 @@ class SalesOrderList(
'shipment_date',
'total_price',
'project_code',
'updated_at',
]
search_fields = [
@@ -1549,6 +1559,7 @@ class ReturnOrderList(
'target_date',
'complete_date',
'project_code',
'updated_at',
]
search_fields = [

View File

@@ -0,0 +1,43 @@
# Generated by Django 5.2.11 on 2026-02-19 22:31
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("order", "0114_purchaseorderextraline_project_code_and_more"),
]
operations = [
migrations.AddField(
model_name="purchaseorder",
name="updated_at",
field=models.DateTimeField(
blank=True,
null=True,
help_text="Timestamp of last update",
verbose_name="Updated At",
),
),
migrations.AddField(
model_name="returnorder",
name="updated_at",
field=models.DateTimeField(
blank=True,
null=True,
help_text="Timestamp of last update",
verbose_name="Updated At",
),
),
migrations.AddField(
model_name="salesorder",
name="updated_at",
field=models.DateTimeField(
blank=True,
null=True,
help_text="Timestamp of last update",
verbose_name="Updated At",
),
),
]

View File

@@ -9,7 +9,7 @@ from django.core.validators import MinValueValidator
from django.db import models, transaction
from django.db.models import F, Q, QuerySet, Sum
from django.db.models.functions import Coalesce
from django.db.models.signals import post_save
from django.db.models.signals import post_delete, post_save
from django.dispatch.dispatcher import receiver
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
@@ -331,6 +331,8 @@ class Order(
if not self.creation_date:
self.creation_date = InvenTree.helpers.current_date()
self.updated_at = InvenTree.helpers.current_time()
super().save(*args, **kwargs)
def check_locked(self, db: bool = False) -> bool:
@@ -498,6 +500,13 @@ class Order(
help_text=_('Date order was issued'),
)
updated_at = models.DateTimeField(
null=True,
blank=True,
verbose_name=_('Updated At'),
help_text=_('Timestamp of last update'),
)
responsible = models.ForeignKey(
UserModels.Owner,
on_delete=models.SET_NULL,
@@ -3072,3 +3081,43 @@ class ReturnOrderExtraLine(OrderExtraLine):
verbose_name=_('Order'),
help_text=_('Return Order'),
)
def _touch_order_updated_at(instance):
"""Bump updated_at on the parent order without triggering a full save."""
if not InvenTree.ready.canAppAccessDatabase(allow_test=True):
return
instance.order.__class__.objects.filter(pk=instance.order_id).update(
updated_at=InvenTree.helpers.current_time()
)
@receiver(post_save, sender=PurchaseOrderLineItem, dispatch_uid='po_lineitem_post_save')
@receiver(
post_delete, sender=PurchaseOrderLineItem, dispatch_uid='po_lineitem_post_delete'
)
@receiver(
post_save, sender=PurchaseOrderExtraLine, dispatch_uid='po_extraline_post_save'
)
@receiver(
post_delete, sender=PurchaseOrderExtraLine, dispatch_uid='po_extraline_post_delete'
)
@receiver(post_save, sender=SalesOrderLineItem, dispatch_uid='so_lineitem_post_save')
@receiver(
post_delete, sender=SalesOrderLineItem, dispatch_uid='so_lineitem_post_delete'
)
@receiver(post_save, sender=SalesOrderExtraLine, dispatch_uid='so_extraline_post_save')
@receiver(
post_delete, sender=SalesOrderExtraLine, dispatch_uid='so_extraline_post_delete'
)
@receiver(post_save, sender=ReturnOrderLineItem, dispatch_uid='ro_lineitem_post_save')
@receiver(
post_delete, sender=ReturnOrderLineItem, dispatch_uid='ro_lineitem_post_delete'
)
@receiver(post_save, sender=ReturnOrderExtraLine, dispatch_uid='ro_extraline_post_save')
@receiver(
post_delete, sender=ReturnOrderExtraLine, dispatch_uid='ro_extraline_post_delete'
)
def update_order_on_lineitem_change(sender, instance, **kwargs):
"""Update parent order updated_at when any line item is saved or deleted."""
_touch_order_updated_at(instance)

View File

@@ -373,8 +373,14 @@ class PurchaseOrderSerializer(
'total_price',
'order_currency',
'destination',
'updated_at',
])
read_only_fields = ['issue_date', 'complete_date', 'creation_date']
read_only_fields = [
'issue_date',
'complete_date',
'creation_date',
'updated_at',
]
extra_kwargs = {
'supplier': {'required': True},
'order_currency': {'required': False},
@@ -1026,8 +1032,9 @@ class SalesOrderSerializer(
'shipments_count',
'completed_shipments_count',
'allocated_lines',
'updated_at',
])
read_only_fields = ['status', 'creation_date', 'shipment_date']
read_only_fields = ['status', 'creation_date', 'shipment_date', 'updated_at']
extra_kwargs = {'order_currency': {'required': False}}
def skip_create_fields(self):
@@ -1918,8 +1925,9 @@ class ReturnOrderSerializer(
'customer_reference',
'order_currency',
'total_price',
'updated_at',
])
read_only_fields = ['creation_date']
read_only_fields = ['creation_date', 'updated_at']
def skip_create_fields(self):
"""Skip these fields when instantiating a new object."""

View File

@@ -25,7 +25,17 @@ from part.models import Part
from stock.models import StockItem, StockLocation
from users.models import Owner
from .models import PurchaseOrder, PurchaseOrderExtraLine, PurchaseOrderLineItem
from .models import (
PurchaseOrder,
PurchaseOrderExtraLine,
PurchaseOrderLineItem,
ReturnOrder,
ReturnOrderExtraLine,
ReturnOrderLineItem,
SalesOrder,
SalesOrderExtraLine,
SalesOrderLineItem,
)
class OrderTest(ExchangeRateMixin, PluginRegistryMixin, TestCase):
@@ -369,7 +379,8 @@ class OrderTest(ExchangeRateMixin, PluginRegistryMixin, TestCase):
order=po,
part=sp_1,
quantity=3,
purchase_price=Money(1000, 'USD'), # "Unit price" should be $100USD
# "Unit price" should be $100USD
purchase_price=Money(1000, 'USD'),
)
# 13 x 0.1 = 1.3
@@ -569,3 +580,151 @@ class OrderTest(ExchangeRateMixin, PluginRegistryMixin, TestCase):
p.set_metadata(k, k)
self.assertEqual(len(p.metadata.keys()), 4)
class OrderUpdatedAtTest(TestCase):
"""Tests to verify that the updated_at field is correctly maintained on all order types."""
def setUp(self):
"""Set up objects for all three order types."""
self.supplier = Company.objects.filter(is_supplier=True).first()
self.customer = Company.objects.filter(is_customer=True).first()
self.po = PurchaseOrder.objects.create(
reference='PO-TEST-001', supplier=self.supplier
)
self.so = SalesOrder.objects.create(
reference='SO-TEST-001', customer=self.customer
)
self.ro = ReturnOrder.objects.create(
reference='RO-TEST-001', customer=self.customer
)
self.part = Part.objects.create(name='Test Part', description='Test Part')
self.stock_item = StockItem.objects.create(part=self.part, quantity=10)
def _refresh(self, instance):
"""Return a fresh copy of the instance from the database."""
return instance.__class__.objects.get(pk=instance.pk)
def test_updated_at_set_on_save(self):
"""updated_at should be populated after the order is saved."""
for instance in [self.po, self.so, self.ro]:
self.assertIsNotNone(self._refresh(instance).updated_at)
def test_updated_at_changes_on_save(self):
"""updated_at should advance when the order is saved again."""
for instance in [self.po, self.so, self.ro]:
original = self._refresh(instance).updated_at
instance.description = 'Updated description'
instance.save()
refreshed = self._refresh(instance)
self.assertGreaterEqual(refreshed.updated_at, original)
def test_updated_at_on_extra_line_add(self):
"""updated_at should advance on the parent order when an extra line is added."""
for instance, ExtraLine in [
(self.po, PurchaseOrderExtraLine),
(self.so, SalesOrderExtraLine),
(self.ro, ReturnOrderExtraLine),
]:
before = self._refresh(instance).updated_at
ExtraLine.objects.create(order=instance, quantity=1)
after = self._refresh(instance).updated_at
self.assertGreaterEqual(after, before)
def test_updated_at_on_extra_line_update(self):
"""updated_at should advance on the parent order when an extra line is updated."""
for instance, ExtraLine in [
(self.po, PurchaseOrderExtraLine),
(self.so, SalesOrderExtraLine),
(self.ro, ReturnOrderExtraLine),
]:
line = ExtraLine.objects.create(order=instance, quantity=1)
before = self._refresh(instance).updated_at
line.quantity = 5
line.save()
after = self._refresh(instance).updated_at
self.assertGreaterEqual(after, before)
def test_updated_at_on_extra_line_delete(self):
"""updated_at should advance on the parent order when an extra line is deleted."""
for instance, ExtraLine in [
(self.po, PurchaseOrderExtraLine),
(self.so, SalesOrderExtraLine),
(self.ro, ReturnOrderExtraLine),
]:
line = ExtraLine.objects.create(order=instance, quantity=1)
before = self._refresh(instance).updated_at
line.delete()
after = self._refresh(instance).updated_at
self.assertGreaterEqual(after, before)
def test_updated_at_on_line_item_add(self):
"""updated_at should advance on the parent order when a regular line item is added."""
before_po = self._refresh(self.po).updated_at
PurchaseOrderLineItem.objects.create(order=self.po, part=None, quantity=1)
self.assertGreaterEqual(self._refresh(self.po).updated_at, before_po)
before_so = self._refresh(self.so).updated_at
SalesOrderLineItem.objects.create(order=self.so, part=None, quantity=1)
self.assertGreaterEqual(self._refresh(self.so).updated_at, before_so)
before_ro = self._refresh(self.ro).updated_at
ReturnOrderLineItem.objects.create(
order=self.ro, item=self.stock_item, quantity=1
)
self.assertGreaterEqual(self._refresh(self.ro).updated_at, before_ro)
def test_updated_at_on_line_item_update(self):
"""updated_at should advance on the parent order when a regular line item is updated."""
po_line = PurchaseOrderLineItem.objects.create(
order=self.po, part=None, quantity=1
)
so_line = SalesOrderLineItem.objects.create(
order=self.so, part=None, quantity=1
)
ro_line = ReturnOrderLineItem.objects.create(
order=self.ro, item=self.stock_item, quantity=1
)
for instance, line in [
(self.po, po_line),
(self.so, so_line),
(self.ro, ro_line),
]:
before = self._refresh(instance).updated_at
line.quantity = 5
line.save()
self.assertGreaterEqual(self._refresh(instance).updated_at, before)
def test_updated_at_on_line_item_delete(self):
"""updated_at should advance on the parent order when a regular line item is deleted."""
po_line = PurchaseOrderLineItem.objects.create(
order=self.po, part=None, quantity=1
)
so_line = SalesOrderLineItem.objects.create(
order=self.so, part=None, quantity=1
)
ro_line = ReturnOrderLineItem.objects.create(
order=self.ro, item=self.stock_item, quantity=1
)
for instance, line in [
(self.po, po_line),
(self.so, so_line),
(self.ro, ro_line),
]:
before = self._refresh(instance).updated_at
line.delete()
self.assertGreaterEqual(self._refresh(instance).updated_at, before)

View File

@@ -54,10 +54,16 @@ export type DetailsField = {
type BadgeType = 'owner' | 'user' | 'group';
type ValueFormatterReturn = string | number | null | React.ReactNode;
type StringDetailField = {
type: 'string' | 'text' | 'date';
unit?: boolean;
};
type StringDetailField =
| {
type: 'string' | 'text';
unit?: boolean;
}
| {
type: 'date';
unit?: boolean;
showTime?: boolean;
};
type NumberDetailField = {
type: 'number';
@@ -260,7 +266,13 @@ function NameBadge({
}
function DateValue(props: Readonly<FieldProps>) {
return <Text size='sm'>{formatDate(props.field_value?.toString())}</Text>;
return (
<Text size='sm'>
{formatDate(props.field_value?.toString(), {
showTime: props.field_data?.showTime
})}
</Text>
);
}
// Return a formatted "number" value, with optional unit

View File

@@ -304,6 +304,15 @@ export default function PurchaseOrderDetail() {
label: t`Completion Date`,
copy: true,
hidden: !order.complete_date
},
{
type: 'date',
name: 'updated_at',
label: t`Last Updated`,
icon: 'calendar',
copy: true,
showTime: true,
hidden: !order.updated_at
}
];

View File

@@ -282,6 +282,15 @@ export default function ReturnOrderDetail() {
label: t`Completion Date`,
copy: true,
hidden: !order.complete_date
},
{
type: 'date',
name: 'updated_at',
label: t`Last Updated`,
icon: 'calendar',
copy: true,
showTime: true,
hidden: !order.updated_at
}
];

View File

@@ -273,6 +273,15 @@ export default function SalesOrderDetail() {
label: t`Completion Date`,
hidden: !order.shipment_date,
copy: true
},
{
type: 'date',
name: 'updated_at',
label: t`Last Updated`,
icon: 'calendar',
copy: true,
showTime: true,
hidden: !order.updated_at
}
];

View File

@@ -725,6 +725,16 @@ export function ShipmentDateColumn(props: TableColumnProps): TableColumn {
});
}
export function UpdatedAtColumn(props: TableColumnProps): TableColumn {
return DateColumn({
accessor: 'updated_at',
title: t`Updated`,
defaultVisible: false,
extra: { showTime: true },
...props
});
}
export function CurrencyColumn({
accessor,
title,

View File

@@ -286,6 +286,24 @@ export function CompletedAfterFilter(): TableFilter {
};
}
export function UpdatedAfterFilter(): TableFilter {
return {
name: 'updated_after',
label: t`Updated After`,
description: t`Show orders updated after this date`,
type: 'date'
};
}
export function UpdatedBeforeFilter(): TableFilter {
return {
name: 'updated_before',
label: t`Updated Before`,
description: t`Show orders updated before this date`,
type: 'date'
};
}
export function HasProjectCodeFilter(): TableFilter {
const globalSettings = useGlobalSettingsState.getState();
const enabled = globalSettings.isSet('PROJECT_CODES_ENABLED', true);

View File

@@ -25,7 +25,8 @@ import {
ResponsibleColumn,
StartDateColumn,
StatusColumn,
TargetDateColumn
TargetDateColumn,
UpdatedAtColumn
} from '../ColumnRenderers';
import {
AssignedToMeFilter,
@@ -45,7 +46,9 @@ import {
StartDateAfterFilter,
StartDateBeforeFilter,
TargetDateAfterFilter,
TargetDateBeforeFilter
TargetDateBeforeFilter,
UpdatedAfterFilter,
UpdatedBeforeFilter
} from '../Filter';
import { InvenTreeTable } from '../InvenTreeTable';
@@ -99,6 +102,8 @@ export function PurchaseOrderTable({
},
CompletedBeforeFilter(),
CompletedAfterFilter(),
UpdatedBeforeFilter(),
UpdatedAfterFilter(),
ProjectCodeFilter(),
HasProjectCodeFilter(),
ResponsibleFilter(),
@@ -142,6 +147,9 @@ export function PurchaseOrderTable({
CompletionDateColumn({
accessor: 'complete_date'
}),
UpdatedAtColumn({
defaultVisible: false
}),
{
accessor: 'total_price',
title: t`Total Price`,

View File

@@ -25,7 +25,8 @@ import {
ResponsibleColumn,
StartDateColumn,
StatusColumn,
TargetDateColumn
TargetDateColumn,
UpdatedAtColumn
} from '../ColumnRenderers';
import {
AssignedToMeFilter,
@@ -46,7 +47,9 @@ import {
StartDateAfterFilter,
StartDateBeforeFilter,
TargetDateAfterFilter,
TargetDateBeforeFilter
TargetDateBeforeFilter,
UpdatedAfterFilter,
UpdatedBeforeFilter
} from '../Filter';
import { InvenTreeTable } from '../InvenTreeTable';
@@ -99,6 +102,8 @@ export function ReturnOrderTable({
},
CompletedBeforeFilter(),
CompletedAfterFilter(),
UpdatedBeforeFilter(),
UpdatedAfterFilter(),
HasProjectCodeFilter(),
ProjectCodeFilter(),
ResponsibleFilter(),
@@ -146,6 +151,9 @@ export function ReturnOrderTable({
CompletionDateColumn({
accessor: 'complete_date'
}),
UpdatedAtColumn({
defaultVisible: false
}),
ResponsibleColumn({}),
{
accessor: 'total_price',

View File

@@ -27,7 +27,8 @@ import {
ShipmentDateColumn,
StartDateColumn,
StatusColumn,
TargetDateColumn
TargetDateColumn,
UpdatedAtColumn
} from '../ColumnRenderers';
import {
AssignedToMeFilter,
@@ -48,7 +49,9 @@ import {
StartDateAfterFilter,
StartDateBeforeFilter,
TargetDateAfterFilter,
TargetDateBeforeFilter
TargetDateBeforeFilter,
UpdatedAfterFilter,
UpdatedBeforeFilter
} from '../Filter';
import { InvenTreeTable } from '../InvenTreeTable';
@@ -97,6 +100,8 @@ export function SalesOrderTable({
},
CompletedBeforeFilter(),
CompletedAfterFilter(),
UpdatedBeforeFilter(),
UpdatedAfterFilter(),
HasProjectCodeFilter(),
ProjectCodeFilter(),
ResponsibleFilter(),
@@ -182,6 +187,9 @@ export function SalesOrderTable({
}),
TargetDateColumn({}),
ShipmentDateColumn({}),
UpdatedAtColumn({
defaultVisible: false
}),
ResponsibleColumn({}),
{
accessor: 'total_price',