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:
@@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- [#11383](https://github.com/inventree/InvenTree/pull/11383) adds "exists_for_model_id", "exists_for_related_model", and "exists_for_related_model_id" filters to the ParameterTemplate API endpoint. These filters allow users to check for the existence of parameters associated with specific models or related models, improving the flexibility and usability of the API.
|
- [#11383](https://github.com/inventree/InvenTree/pull/11383) adds "exists_for_model_id", "exists_for_related_model", and "exists_for_related_model_id" filters to the ParameterTemplate API endpoint. These filters allow users to check for the existence of parameters associated with specific models or related models, improving the flexibility and usability of the API.
|
||||||
- [#10887](https://github.com/inventree/InvenTree/pull/10887) adds the ability to auto-allocate tracked items against specific build outputs. Currently, this will only allocate items where the serial number of the tracked item matches the serial number of the build output, but in future this may be extended to allow for more flexible allocation rules.
|
- [#10887](https://github.com/inventree/InvenTree/pull/10887) adds the ability to auto-allocate tracked items against specific build outputs. Currently, this will only allocate items where the serial number of the tracked item matches the serial number of the build output, but in future this may be extended to allow for more flexible allocation rules.
|
||||||
- [#11372](https://github.com/inventree/InvenTree/pull/11372) adds backup metadata setter and restore metadata validator functions to ensure common footguns are harder to trigger when using the backup and restore functionality.
|
- [#11372](https://github.com/inventree/InvenTree/pull/11372) adds backup metadata setter and restore metadata validator functions to ensure common footguns are harder to trigger when using the backup and restore functionality.
|
||||||
|
- [#11374](https://github.com/inventree/InvenTree/pull/11374) adds `updated_at` field on purchase, sales and return orders.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
"""InvenTree API version information."""
|
"""InvenTree API version information."""
|
||||||
|
|
||||||
# InvenTree API version
|
# 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."""
|
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
||||||
|
|
||||||
INVENTREE_API_TEXT = """
|
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
|
v459 -> 2026-02-23 : https://github.com/inventree/InvenTree/pull/11411
|
||||||
- Changed PurchaseOrderLine "auto_pricing" default value from true to false
|
- Changed PurchaseOrderLine "auto_pricing" default value from true to false
|
||||||
|
|
||||||
|
|||||||
@@ -228,6 +228,14 @@ class OrderFilter(FilterSet):
|
|||||||
label=_('Target Date After'), field_name='target_date', lookup_expr='gt'
|
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')
|
min_date = InvenTreeDateFilter(label=_('Min Date'), method='filter_min_date')
|
||||||
|
|
||||||
def filter_min_date(self, queryset, name, value):
|
def filter_min_date(self, queryset, name, value):
|
||||||
@@ -420,6 +428,7 @@ class PurchaseOrderList(
|
|||||||
'responsible',
|
'responsible',
|
||||||
'total_price',
|
'total_price',
|
||||||
'project_code',
|
'project_code',
|
||||||
|
'updated_at',
|
||||||
]
|
]
|
||||||
|
|
||||||
ordering = '-reference'
|
ordering = '-reference'
|
||||||
@@ -882,6 +891,7 @@ class SalesOrderList(
|
|||||||
'shipment_date',
|
'shipment_date',
|
||||||
'total_price',
|
'total_price',
|
||||||
'project_code',
|
'project_code',
|
||||||
|
'updated_at',
|
||||||
]
|
]
|
||||||
|
|
||||||
search_fields = [
|
search_fields = [
|
||||||
@@ -1549,6 +1559,7 @@ class ReturnOrderList(
|
|||||||
'target_date',
|
'target_date',
|
||||||
'complete_date',
|
'complete_date',
|
||||||
'project_code',
|
'project_code',
|
||||||
|
'updated_at',
|
||||||
]
|
]
|
||||||
|
|
||||||
search_fields = [
|
search_fields = [
|
||||||
|
|||||||
@@ -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",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -9,7 +9,7 @@ from django.core.validators import MinValueValidator
|
|||||||
from django.db import models, transaction
|
from django.db import models, transaction
|
||||||
from django.db.models import F, Q, QuerySet, Sum
|
from django.db.models import F, Q, QuerySet, Sum
|
||||||
from django.db.models.functions import Coalesce
|
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.dispatch.dispatcher import receiver
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
@@ -331,6 +331,8 @@ class Order(
|
|||||||
if not self.creation_date:
|
if not self.creation_date:
|
||||||
self.creation_date = InvenTree.helpers.current_date()
|
self.creation_date = InvenTree.helpers.current_date()
|
||||||
|
|
||||||
|
self.updated_at = InvenTree.helpers.current_time()
|
||||||
|
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
def check_locked(self, db: bool = False) -> bool:
|
def check_locked(self, db: bool = False) -> bool:
|
||||||
@@ -498,6 +500,13 @@ class Order(
|
|||||||
help_text=_('Date order was issued'),
|
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(
|
responsible = models.ForeignKey(
|
||||||
UserModels.Owner,
|
UserModels.Owner,
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
@@ -3072,3 +3081,43 @@ class ReturnOrderExtraLine(OrderExtraLine):
|
|||||||
verbose_name=_('Order'),
|
verbose_name=_('Order'),
|
||||||
help_text=_('Return 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)
|
||||||
|
|||||||
@@ -373,8 +373,14 @@ class PurchaseOrderSerializer(
|
|||||||
'total_price',
|
'total_price',
|
||||||
'order_currency',
|
'order_currency',
|
||||||
'destination',
|
'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 = {
|
extra_kwargs = {
|
||||||
'supplier': {'required': True},
|
'supplier': {'required': True},
|
||||||
'order_currency': {'required': False},
|
'order_currency': {'required': False},
|
||||||
@@ -1026,8 +1032,9 @@ class SalesOrderSerializer(
|
|||||||
'shipments_count',
|
'shipments_count',
|
||||||
'completed_shipments_count',
|
'completed_shipments_count',
|
||||||
'allocated_lines',
|
'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}}
|
extra_kwargs = {'order_currency': {'required': False}}
|
||||||
|
|
||||||
def skip_create_fields(self):
|
def skip_create_fields(self):
|
||||||
@@ -1918,8 +1925,9 @@ class ReturnOrderSerializer(
|
|||||||
'customer_reference',
|
'customer_reference',
|
||||||
'order_currency',
|
'order_currency',
|
||||||
'total_price',
|
'total_price',
|
||||||
|
'updated_at',
|
||||||
])
|
])
|
||||||
read_only_fields = ['creation_date']
|
read_only_fields = ['creation_date', 'updated_at']
|
||||||
|
|
||||||
def skip_create_fields(self):
|
def skip_create_fields(self):
|
||||||
"""Skip these fields when instantiating a new object."""
|
"""Skip these fields when instantiating a new object."""
|
||||||
|
|||||||
@@ -25,7 +25,17 @@ from part.models import Part
|
|||||||
from stock.models import StockItem, StockLocation
|
from stock.models import StockItem, StockLocation
|
||||||
from users.models import Owner
|
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):
|
class OrderTest(ExchangeRateMixin, PluginRegistryMixin, TestCase):
|
||||||
@@ -369,7 +379,8 @@ class OrderTest(ExchangeRateMixin, PluginRegistryMixin, TestCase):
|
|||||||
order=po,
|
order=po,
|
||||||
part=sp_1,
|
part=sp_1,
|
||||||
quantity=3,
|
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
|
# 13 x 0.1 = 1.3
|
||||||
@@ -569,3 +580,151 @@ class OrderTest(ExchangeRateMixin, PluginRegistryMixin, TestCase):
|
|||||||
p.set_metadata(k, k)
|
p.set_metadata(k, k)
|
||||||
|
|
||||||
self.assertEqual(len(p.metadata.keys()), 4)
|
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)
|
||||||
|
|||||||
@@ -54,10 +54,16 @@ export type DetailsField = {
|
|||||||
type BadgeType = 'owner' | 'user' | 'group';
|
type BadgeType = 'owner' | 'user' | 'group';
|
||||||
type ValueFormatterReturn = string | number | null | React.ReactNode;
|
type ValueFormatterReturn = string | number | null | React.ReactNode;
|
||||||
|
|
||||||
type StringDetailField = {
|
type StringDetailField =
|
||||||
type: 'string' | 'text' | 'date';
|
| {
|
||||||
|
type: 'string' | 'text';
|
||||||
unit?: boolean;
|
unit?: boolean;
|
||||||
};
|
}
|
||||||
|
| {
|
||||||
|
type: 'date';
|
||||||
|
unit?: boolean;
|
||||||
|
showTime?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
type NumberDetailField = {
|
type NumberDetailField = {
|
||||||
type: 'number';
|
type: 'number';
|
||||||
@@ -260,7 +266,13 @@ function NameBadge({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function DateValue(props: Readonly<FieldProps>) {
|
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
|
// Return a formatted "number" value, with optional unit
|
||||||
|
|||||||
@@ -304,6 +304,15 @@ export default function PurchaseOrderDetail() {
|
|||||||
label: t`Completion Date`,
|
label: t`Completion Date`,
|
||||||
copy: true,
|
copy: true,
|
||||||
hidden: !order.complete_date
|
hidden: !order.complete_date
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'date',
|
||||||
|
name: 'updated_at',
|
||||||
|
label: t`Last Updated`,
|
||||||
|
icon: 'calendar',
|
||||||
|
copy: true,
|
||||||
|
showTime: true,
|
||||||
|
hidden: !order.updated_at
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -282,6 +282,15 @@ export default function ReturnOrderDetail() {
|
|||||||
label: t`Completion Date`,
|
label: t`Completion Date`,
|
||||||
copy: true,
|
copy: true,
|
||||||
hidden: !order.complete_date
|
hidden: !order.complete_date
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'date',
|
||||||
|
name: 'updated_at',
|
||||||
|
label: t`Last Updated`,
|
||||||
|
icon: 'calendar',
|
||||||
|
copy: true,
|
||||||
|
showTime: true,
|
||||||
|
hidden: !order.updated_at
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -273,6 +273,15 @@ export default function SalesOrderDetail() {
|
|||||||
label: t`Completion Date`,
|
label: t`Completion Date`,
|
||||||
hidden: !order.shipment_date,
|
hidden: !order.shipment_date,
|
||||||
copy: true
|
copy: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'date',
|
||||||
|
name: 'updated_at',
|
||||||
|
label: t`Last Updated`,
|
||||||
|
icon: 'calendar',
|
||||||
|
copy: true,
|
||||||
|
showTime: true,
|
||||||
|
hidden: !order.updated_at
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -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({
|
export function CurrencyColumn({
|
||||||
accessor,
|
accessor,
|
||||||
title,
|
title,
|
||||||
|
|||||||
@@ -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 {
|
export function HasProjectCodeFilter(): TableFilter {
|
||||||
const globalSettings = useGlobalSettingsState.getState();
|
const globalSettings = useGlobalSettingsState.getState();
|
||||||
const enabled = globalSettings.isSet('PROJECT_CODES_ENABLED', true);
|
const enabled = globalSettings.isSet('PROJECT_CODES_ENABLED', true);
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ import {
|
|||||||
ResponsibleColumn,
|
ResponsibleColumn,
|
||||||
StartDateColumn,
|
StartDateColumn,
|
||||||
StatusColumn,
|
StatusColumn,
|
||||||
TargetDateColumn
|
TargetDateColumn,
|
||||||
|
UpdatedAtColumn
|
||||||
} from '../ColumnRenderers';
|
} from '../ColumnRenderers';
|
||||||
import {
|
import {
|
||||||
AssignedToMeFilter,
|
AssignedToMeFilter,
|
||||||
@@ -45,7 +46,9 @@ import {
|
|||||||
StartDateAfterFilter,
|
StartDateAfterFilter,
|
||||||
StartDateBeforeFilter,
|
StartDateBeforeFilter,
|
||||||
TargetDateAfterFilter,
|
TargetDateAfterFilter,
|
||||||
TargetDateBeforeFilter
|
TargetDateBeforeFilter,
|
||||||
|
UpdatedAfterFilter,
|
||||||
|
UpdatedBeforeFilter
|
||||||
} from '../Filter';
|
} from '../Filter';
|
||||||
import { InvenTreeTable } from '../InvenTreeTable';
|
import { InvenTreeTable } from '../InvenTreeTable';
|
||||||
|
|
||||||
@@ -99,6 +102,8 @@ export function PurchaseOrderTable({
|
|||||||
},
|
},
|
||||||
CompletedBeforeFilter(),
|
CompletedBeforeFilter(),
|
||||||
CompletedAfterFilter(),
|
CompletedAfterFilter(),
|
||||||
|
UpdatedBeforeFilter(),
|
||||||
|
UpdatedAfterFilter(),
|
||||||
ProjectCodeFilter(),
|
ProjectCodeFilter(),
|
||||||
HasProjectCodeFilter(),
|
HasProjectCodeFilter(),
|
||||||
ResponsibleFilter(),
|
ResponsibleFilter(),
|
||||||
@@ -142,6 +147,9 @@ export function PurchaseOrderTable({
|
|||||||
CompletionDateColumn({
|
CompletionDateColumn({
|
||||||
accessor: 'complete_date'
|
accessor: 'complete_date'
|
||||||
}),
|
}),
|
||||||
|
UpdatedAtColumn({
|
||||||
|
defaultVisible: false
|
||||||
|
}),
|
||||||
{
|
{
|
||||||
accessor: 'total_price',
|
accessor: 'total_price',
|
||||||
title: t`Total Price`,
|
title: t`Total Price`,
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ import {
|
|||||||
ResponsibleColumn,
|
ResponsibleColumn,
|
||||||
StartDateColumn,
|
StartDateColumn,
|
||||||
StatusColumn,
|
StatusColumn,
|
||||||
TargetDateColumn
|
TargetDateColumn,
|
||||||
|
UpdatedAtColumn
|
||||||
} from '../ColumnRenderers';
|
} from '../ColumnRenderers';
|
||||||
import {
|
import {
|
||||||
AssignedToMeFilter,
|
AssignedToMeFilter,
|
||||||
@@ -46,7 +47,9 @@ import {
|
|||||||
StartDateAfterFilter,
|
StartDateAfterFilter,
|
||||||
StartDateBeforeFilter,
|
StartDateBeforeFilter,
|
||||||
TargetDateAfterFilter,
|
TargetDateAfterFilter,
|
||||||
TargetDateBeforeFilter
|
TargetDateBeforeFilter,
|
||||||
|
UpdatedAfterFilter,
|
||||||
|
UpdatedBeforeFilter
|
||||||
} from '../Filter';
|
} from '../Filter';
|
||||||
import { InvenTreeTable } from '../InvenTreeTable';
|
import { InvenTreeTable } from '../InvenTreeTable';
|
||||||
|
|
||||||
@@ -99,6 +102,8 @@ export function ReturnOrderTable({
|
|||||||
},
|
},
|
||||||
CompletedBeforeFilter(),
|
CompletedBeforeFilter(),
|
||||||
CompletedAfterFilter(),
|
CompletedAfterFilter(),
|
||||||
|
UpdatedBeforeFilter(),
|
||||||
|
UpdatedAfterFilter(),
|
||||||
HasProjectCodeFilter(),
|
HasProjectCodeFilter(),
|
||||||
ProjectCodeFilter(),
|
ProjectCodeFilter(),
|
||||||
ResponsibleFilter(),
|
ResponsibleFilter(),
|
||||||
@@ -146,6 +151,9 @@ export function ReturnOrderTable({
|
|||||||
CompletionDateColumn({
|
CompletionDateColumn({
|
||||||
accessor: 'complete_date'
|
accessor: 'complete_date'
|
||||||
}),
|
}),
|
||||||
|
UpdatedAtColumn({
|
||||||
|
defaultVisible: false
|
||||||
|
}),
|
||||||
ResponsibleColumn({}),
|
ResponsibleColumn({}),
|
||||||
{
|
{
|
||||||
accessor: 'total_price',
|
accessor: 'total_price',
|
||||||
|
|||||||
@@ -27,7 +27,8 @@ import {
|
|||||||
ShipmentDateColumn,
|
ShipmentDateColumn,
|
||||||
StartDateColumn,
|
StartDateColumn,
|
||||||
StatusColumn,
|
StatusColumn,
|
||||||
TargetDateColumn
|
TargetDateColumn,
|
||||||
|
UpdatedAtColumn
|
||||||
} from '../ColumnRenderers';
|
} from '../ColumnRenderers';
|
||||||
import {
|
import {
|
||||||
AssignedToMeFilter,
|
AssignedToMeFilter,
|
||||||
@@ -48,7 +49,9 @@ import {
|
|||||||
StartDateAfterFilter,
|
StartDateAfterFilter,
|
||||||
StartDateBeforeFilter,
|
StartDateBeforeFilter,
|
||||||
TargetDateAfterFilter,
|
TargetDateAfterFilter,
|
||||||
TargetDateBeforeFilter
|
TargetDateBeforeFilter,
|
||||||
|
UpdatedAfterFilter,
|
||||||
|
UpdatedBeforeFilter
|
||||||
} from '../Filter';
|
} from '../Filter';
|
||||||
import { InvenTreeTable } from '../InvenTreeTable';
|
import { InvenTreeTable } from '../InvenTreeTable';
|
||||||
|
|
||||||
@@ -97,6 +100,8 @@ export function SalesOrderTable({
|
|||||||
},
|
},
|
||||||
CompletedBeforeFilter(),
|
CompletedBeforeFilter(),
|
||||||
CompletedAfterFilter(),
|
CompletedAfterFilter(),
|
||||||
|
UpdatedBeforeFilter(),
|
||||||
|
UpdatedAfterFilter(),
|
||||||
HasProjectCodeFilter(),
|
HasProjectCodeFilter(),
|
||||||
ProjectCodeFilter(),
|
ProjectCodeFilter(),
|
||||||
ResponsibleFilter(),
|
ResponsibleFilter(),
|
||||||
@@ -182,6 +187,9 @@ export function SalesOrderTable({
|
|||||||
}),
|
}),
|
||||||
TargetDateColumn({}),
|
TargetDateColumn({}),
|
||||||
ShipmentDateColumn({}),
|
ShipmentDateColumn({}),
|
||||||
|
UpdatedAtColumn({
|
||||||
|
defaultVisible: false
|
||||||
|
}),
|
||||||
ResponsibleColumn({}),
|
ResponsibleColumn({}),
|
||||||
{
|
{
|
||||||
accessor: 'total_price',
|
accessor: 'total_price',
|
||||||
|
|||||||
Reference in New Issue
Block a user