From 4059d9ffeb6e642fdfd44f390b3426bbc52353ef Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 27 Mar 2024 16:57:59 +1100 Subject: [PATCH] Timestamp issues (#6867) * Adjust default values for test result fields * Add helper functions: - current_time() - current_date() Handles timezone "awareness" * Use new helper function widely * Update defaults - do not use None * Allow null field values --- InvenTree/InvenTree/helpers.py | 23 ++++++++++++++++ InvenTree/InvenTree/settings.py | 2 ++ InvenTree/build/models.py | 6 ++--- InvenTree/build/tasks.py | 5 ++-- InvenTree/common/models.py | 4 +-- InvenTree/common/tasks.py | 5 ++-- InvenTree/company/models.py | 2 +- InvenTree/label/models.py | 6 ++--- InvenTree/order/models.py | 26 ++++++++++--------- InvenTree/order/serializers.py | 13 +++++++--- InvenTree/part/models.py | 5 ++-- InvenTree/part/stocktake.py | 2 +- InvenTree/report/models.py | 5 ++-- InvenTree/stock/api.py | 7 ++--- .../0109_add_additional_test_fields.py | 5 ++-- ...emtestresult_finished_datetime_and_more.py | 23 ++++++++++++++++ InvenTree/stock/models.py | 20 +++++++------- InvenTree/stock/serializers.py | 4 +-- InvenTree/users/models.py | 6 +++-- 19 files changed, 116 insertions(+), 53 deletions(-) create mode 100644 InvenTree/stock/migrations/0110_alter_stockitemtestresult_finished_datetime_and_more.py diff --git a/InvenTree/InvenTree/helpers.py b/InvenTree/InvenTree/helpers.py index c4cbe28f486..82116c26f6a 100644 --- a/InvenTree/InvenTree/helpers.py +++ b/InvenTree/InvenTree/helpers.py @@ -872,6 +872,29 @@ def hash_file(filename: Union[str, Path], storage: Union[Storage, None] = None): return hashlib.md5(content).hexdigest() +def current_time(local=True): + """Return the current date and time as a datetime object. + + - If timezone support is active, returns a timezone aware time + - If timezone support is not active, returns a timezone naive time + + Arguments: + local: Return the time in the local timezone, otherwise UTC (default = True) + + """ + if settings.USE_TZ: + now = timezone.now() + now = to_local_time(now, target_tz=server_timezone() if local else 'UTC') + return now + else: + return datetime.datetime.now() + + +def current_date(local=True): + """Return the current date.""" + return current_time(local=local).date() + + def server_timezone() -> str: """Return the timezone of the server as a string. diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index 5269427df06..fd660451ba8 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -952,6 +952,8 @@ # It generates a *lot* of cruft in the logs if not TESTING: USE_TZ = True # pragma: no cover +else: + USE_TZ = False DATE_INPUT_FORMATS = ['%Y-%m-%d'] diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index f2cffe4b34e..9890c1dd22c 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -74,7 +74,7 @@ class Meta: verbose_name = _("Build Order") verbose_name_plural = _("Build Orders") - OVERDUE_FILTER = Q(status__in=BuildStatusGroups.ACTIVE_CODES) & ~Q(target_date=None) & Q(target_date__lte=datetime.now().date()) + OVERDUE_FILTER = Q(status__in=BuildStatusGroups.ACTIVE_CODES) & ~Q(target_date=None) & Q(target_date__lte=InvenTree.helpers.current_date()) # Global setting for specifying reference pattern REFERENCE_PATTERN_SETTING = 'BUILDORDER_REFERENCE_PATTERN' @@ -546,7 +546,7 @@ def complete_build(self, user): if self.incomplete_count > 0: return - self.completion_date = datetime.now().date() + self.completion_date = InvenTree.helpers.current_date() self.completed_by = user self.status = BuildStatus.COMPLETE.value self.save() @@ -628,7 +628,7 @@ def cancel_build(self, user, **kwargs): output.delete() # Date of 'completion' is the date the build was cancelled - self.completion_date = datetime.now().date() + self.completion_date = InvenTree.helpers.current_date() self.completed_by = user self.status = BuildStatus.CANCELLED.value diff --git a/InvenTree/build/tasks.py b/InvenTree/build/tasks.py index 472b8c5166f..9464930ccd9 100644 --- a/InvenTree/build/tasks.py +++ b/InvenTree/build/tasks.py @@ -1,6 +1,6 @@ """Background task definitions for the BuildOrder app""" -from datetime import datetime, timedelta +from datetime import timedelta from decimal import Decimal import logging @@ -14,6 +14,7 @@ import common.notifications import build.models import InvenTree.email +import InvenTree.helpers import InvenTree.helpers_model import InvenTree.tasks from InvenTree.status_codes import BuildStatusGroups @@ -222,7 +223,7 @@ def check_overdue_build_orders(): - Look at the 'target_date' of any outstanding BuildOrder objects - If the 'target_date' expired *yesterday* then the order is just out of date """ - yesterday = datetime.now().date() - timedelta(days=1) + yesterday = InvenTree.helpers.current_date() - timedelta(days=1) overdue_orders = build.models.Build.objects.filter( target_date=yesterday, diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index 8d5ac1a341a..405427c16db 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -13,7 +13,7 @@ import os import re import uuid -from datetime import datetime, timedelta, timezone +from datetime import timedelta, timezone from enum import Enum from secrets import compare_digest from typing import Any, Callable, TypedDict, Union @@ -2915,7 +2915,7 @@ class Meta: @classmethod def check_recent(cls, key: str, uid: int, delta: timedelta): """Test if a particular notification has been sent in the specified time period.""" - since = datetime.now().date() - delta + since = InvenTree.helpers.current_date() - delta entries = cls.objects.filter(key=key, uid=uid, updated__gte=since) diff --git a/InvenTree/common/tasks.py b/InvenTree/common/tasks.py index 04353ec6134..92a666cfe7a 100644 --- a/InvenTree/common/tasks.py +++ b/InvenTree/common/tasks.py @@ -2,7 +2,7 @@ import logging import os -from datetime import datetime, timedelta +from datetime import timedelta from django.conf import settings from django.core.exceptions import AppRegistryNotReady @@ -12,6 +12,7 @@ import feedparser import requests +import InvenTree.helpers from InvenTree.helpers_model import getModelsWithMixin from InvenTree.models import InvenTreeNotesMixin from InvenTree.tasks import ScheduledTask, scheduled_task @@ -107,7 +108,7 @@ def delete_old_notes_images(): note.delete() note_classes = getModelsWithMixin(InvenTreeNotesMixin) - before = datetime.now() - timedelta(days=90) + before = InvenTree.helpers.current_date() - timedelta(days=90) for note in NotesImage.objects.filter(date__lte=before): # Find any images which are no longer referenced by a note diff --git a/InvenTree/company/models.py b/InvenTree/company/models.py index f8cdedcc8b2..dc21dc62b1b 100644 --- a/InvenTree/company/models.py +++ b/InvenTree/company/models.py @@ -901,7 +901,7 @@ def base_quantity(self, quantity=1) -> Decimal: def update_available_quantity(self, quantity): """Update the available quantity for this SupplierPart.""" self.available = quantity - self.availability_updated = datetime.now() + self.availability_updated = InvenTree.helpers.current_time() self.save() @property diff --git a/InvenTree/label/models.py b/InvenTree/label/models.py index a472bedd7b4..17cc252afb2 100644 --- a/InvenTree/label/models.py +++ b/InvenTree/label/models.py @@ -1,6 +1,5 @@ """Label printing models.""" -import datetime import logging import os import sys @@ -15,6 +14,7 @@ from django.utils.translation import gettext_lazy as _ import build.models +import InvenTree.helpers import InvenTree.models import part.models import stock.models @@ -228,8 +228,8 @@ def context(self, request, **kwargs): # Add "basic" context data which gets passed to every label context['base_url'] = get_base_url(request=request) - context['date'] = datetime.datetime.now().date() - context['datetime'] = datetime.datetime.now() + context['date'] = InvenTree.helpers.current_date() + context['datetime'] = InvenTree.helpers.current_time() context['request'] = request context['user'] = request.user context['width'] = self.width diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index e990163c25e..4dd6584f7c1 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -221,7 +221,7 @@ def save(self, *args, **kwargs): """ self.reference_int = self.rebuild_reference_field(self.reference) if not self.creation_date: - self.creation_date = datetime.now().date() + self.creation_date = InvenTree.helpers.current_date() super().save(*args, **kwargs) @@ -252,7 +252,7 @@ def overdue_filter(cls): It requires any subclasses to implement the get_status_class() class method """ - today = datetime.now().date() + today = InvenTree.helpers.current_date() return ( Q(status__in=cls.get_status_class().OPEN) & ~Q(target_date=None) @@ -584,7 +584,7 @@ def _action_place(self, *args, **kwargs): """ if self.is_pending: self.status = PurchaseOrderStatus.PLACED.value - self.issue_date = datetime.now().date() + self.issue_date = InvenTree.helpers.current_date() self.save() trigger_event('purchaseorder.placed', id=self.pk) @@ -604,7 +604,7 @@ def _action_complete(self, *args, **kwargs): """ if self.status == PurchaseOrderStatus.PLACED: self.status = PurchaseOrderStatus.COMPLETE.value - self.complete_date = datetime.now().date() + self.complete_date = InvenTree.helpers.current_date() self.save() @@ -1030,7 +1030,7 @@ def _action_place(self, *args, **kwargs): """Change this order from 'PENDING' to 'IN_PROGRESS'.""" if self.status == SalesOrderStatus.PENDING: self.status = SalesOrderStatus.IN_PROGRESS.value - self.issue_date = datetime.now().date() + self.issue_date = InvenTree.helpers.current_date() self.save() trigger_event('salesorder.issued', id=self.pk) @@ -1044,7 +1044,7 @@ def _action_complete(self, *args, **kwargs): self.status = SalesOrderStatus.SHIPPED.value self.shipped_by = user - self.shipment_date = datetime.now() + self.shipment_date = InvenTree.helpers.current_date() self.save() @@ -1346,7 +1346,7 @@ class PurchaseOrderLineItem(OrderLineItem): OVERDUE_FILTER = ( Q(received__lt=F('quantity')) & ~Q(target_date=None) - & Q(target_date__lt=datetime.now().date()) + & Q(target_date__lt=InvenTree.helpers.current_date()) ) @staticmethod @@ -1505,7 +1505,7 @@ class SalesOrderLineItem(OrderLineItem): OVERDUE_FILTER = ( Q(shipped__lt=F('quantity')) & ~Q(target_date=None) - & Q(target_date__lt=datetime.now().date()) + & Q(target_date__lt=InvenTree.helpers.current_date()) ) @staticmethod @@ -1748,7 +1748,9 @@ def complete_shipment(self, user, **kwargs): allocation.complete_allocation(user) # Update the "shipment" date - self.shipment_date = kwargs.get('shipment_date', datetime.now()) + self.shipment_date = kwargs.get( + 'shipment_date', InvenTree.helpers.current_date() + ) self.shipped_by = user # Was a tracking number provided? @@ -2076,7 +2078,7 @@ def _action_complete(self, *args, **kwargs): """Complete this ReturnOrder (if not already completed).""" if self.status == ReturnOrderStatus.IN_PROGRESS: self.status = ReturnOrderStatus.COMPLETE.value - self.complete_date = datetime.now().date() + self.complete_date = InvenTree.helpers.current_date() self.save() trigger_event('returnorder.completed', id=self.pk) @@ -2089,7 +2091,7 @@ def _action_place(self, *args, **kwargs): """Issue this ReturnOrder (if currently pending).""" if self.status == ReturnOrderStatus.PENDING: self.status = ReturnOrderStatus.IN_PROGRESS.value - self.issue_date = datetime.now().date() + self.issue_date = InvenTree.helpers.current_date() self.save() trigger_event('returnorder.issued', id=self.pk) @@ -2162,7 +2164,7 @@ def receive_line_item(self, line, location, user, note=''): ) # Update the LineItem - line.received_date = datetime.now().date() + line.received_date = InvenTree.helpers.current_date() line.save() trigger_event('returnorder.received', id=self.pk) diff --git a/InvenTree/order/serializers.py b/InvenTree/order/serializers.py index 90f9aa0a0d0..42f6a3c69e7 100644 --- a/InvenTree/order/serializers.py +++ b/InvenTree/order/serializers.py @@ -34,7 +34,13 @@ ContactSerializer, SupplierPartSerializer, ) -from InvenTree.helpers import extract_serial_numbers, hash_barcode, normalize, str2bool +from InvenTree.helpers import ( + current_date, + extract_serial_numbers, + hash_barcode, + normalize, + str2bool, +) from InvenTree.serializers import ( InvenTreeAttachmentSerializer, InvenTreeCurrencySerializer, @@ -1140,11 +1146,12 @@ def save(self): user = request.user # Extract shipping date (defaults to today's date) - shipment_date = data.get('shipment_date', datetime.now()) + now = current_date() + shipment_date = data.get('shipment_date', now) if shipment_date is None: # Shipment date should not be None - check above only # checks if shipment_date exists in data - shipment_date = datetime.now() + shipment_date = now shipment.complete_shipment( user, diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 2f2b34a409a..9c4e92d3d5f 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -37,6 +37,7 @@ import common.settings import InvenTree.conversion import InvenTree.fields +import InvenTree.helpers import InvenTree.models import InvenTree.ready import InvenTree.tasks @@ -1728,7 +1729,7 @@ def validate_bom(self, user): self.bom_checksum = self.get_bom_hash() self.bom_checked_by = user - self.bom_checked_date = datetime.now().date() + self.bom_checked_date = InvenTree.helpers.current_date() self.save() @@ -2715,7 +2716,7 @@ def update_purchase_cost(self, save=True): ) if days > 0: - date_threshold = datetime.now().date() - timedelta(days=days) + date_threshold = InvenTree.helpers.current_date() - timedelta(days=days) items = items.filter(updated__gte=date_threshold) for item in items: diff --git a/InvenTree/part/stocktake.py b/InvenTree/part/stocktake.py index 31de40bbfb8..1bdd41f9774 100644 --- a/InvenTree/part/stocktake.py +++ b/InvenTree/part/stocktake.py @@ -266,7 +266,7 @@ def generate_stocktake_report(**kwargs): buffer = io.StringIO() buffer.write(dataset.export('csv')) - today = datetime.now().date().isoformat() + today = InvenTree.helpers.current_date().isoformat() filename = f'InvenTree_Stocktake_{today}.csv' report_file = ContentFile(buffer.getvalue(), name=filename) diff --git a/InvenTree/report/models.py b/InvenTree/report/models.py index 7958ffb08d6..6575b494cb1 100644 --- a/InvenTree/report/models.py +++ b/InvenTree/report/models.py @@ -18,6 +18,7 @@ import build.models import common.models import InvenTree.exceptions +import InvenTree.helpers import InvenTree.models import order.models import part.models @@ -250,8 +251,8 @@ def context(self, request): context = self.get_context_data(request) context['base_url'] = get_base_url(request=request) - context['date'] = datetime.datetime.now().date() - context['datetime'] = datetime.datetime.now() + context['date'] = InvenTree.helpers.current_date() + context['datetime'] = InvenTree.helpers.current_time() context['page_size'] = self.get_report_size() context['report_template'] = self context['report_description'] = self.description diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index 80ca2efedea..f2b26743bcc 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -19,6 +19,7 @@ import common.models import common.settings +import InvenTree.helpers import stock.serializers as StockSerializers from build.models import Build from build.serializers import BuildSerializer @@ -810,7 +811,7 @@ def filter_stale(self, queryset, name, value): # No filtering, does not make sense return queryset - stale_date = datetime.now().date() + timedelta(days=stale_days) + stale_date = InvenTree.helpers.current_date() + timedelta(days=stale_days) stale_filter = ( StockItem.IN_STOCK_FILTER & ~Q(expiry_date=None) @@ -906,7 +907,7 @@ def create(self, request, *args, **kwargs): # An expiry date was *not* specified - try to infer it! if expiry_date is None and part.default_expiry > 0: - data['expiry_date'] = datetime.now().date() + timedelta( + data['expiry_date'] = InvenTree.helpers.current_date() + timedelta( days=part.default_expiry ) @@ -1050,7 +1051,7 @@ def download_queryset(self, queryset, export_format): filedata = dataset.export(export_format) - filename = f'InvenTree_StockItems_{datetime.now().strftime("%d-%b-%Y")}.{export_format}' + filename = f'InvenTree_StockItems_{InvenTree.helpers.current_date().strftime("%d-%b-%Y")}.{export_format}' return DownloadFile(filedata, filename) diff --git a/InvenTree/stock/migrations/0109_add_additional_test_fields.py b/InvenTree/stock/migrations/0109_add_additional_test_fields.py index 7366b1136e2..aa2c12f4890 100644 --- a/InvenTree/stock/migrations/0109_add_additional_test_fields.py +++ b/InvenTree/stock/migrations/0109_add_additional_test_fields.py @@ -1,6 +1,5 @@ # Generated by Django 3.2.23 on 2023-12-18 18:52 -import datetime from django.db import migrations, models @@ -14,12 +13,12 @@ class Migration(migrations.Migration): migrations.AddField( model_name='stockitemtestresult', name='finished_datetime', - field=models.DateTimeField(blank=True, default=datetime.datetime.now, help_text='The timestamp of the test finish', verbose_name='Finished'), + field=models.DateTimeField(blank=True, help_text='The timestamp of the test finish', verbose_name='Finished'), ), migrations.AddField( model_name='stockitemtestresult', name='started_datetime', - field=models.DateTimeField(blank=True, default=datetime.datetime.now, help_text='The timestamp of the test start', verbose_name='Started'), + field=models.DateTimeField(blank=True, help_text='The timestamp of the test start', verbose_name='Started'), ), migrations.AddField( model_name='stockitemtestresult', diff --git a/InvenTree/stock/migrations/0110_alter_stockitemtestresult_finished_datetime_and_more.py b/InvenTree/stock/migrations/0110_alter_stockitemtestresult_finished_datetime_and_more.py new file mode 100644 index 00000000000..5ded82c134c --- /dev/null +++ b/InvenTree/stock/migrations/0110_alter_stockitemtestresult_finished_datetime_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.11 on 2024-03-27 04:25 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('stock', '0109_add_additional_test_fields'), + ] + + operations = [ + migrations.AlterField( + model_name='stockitemtestresult', + name='finished_datetime', + field=models.DateTimeField(blank=True, help_text='The timestamp of the test finish', null=True, verbose_name='Finished'), + ), + migrations.AlterField( + model_name='stockitemtestresult', + name='started_datetime', + field=models.DateTimeField(blank=True, help_text='The timestamp of the test start', null=True, verbose_name='Started'), + ), + ] diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 749a2ca6fd0..3e7e95504bb 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -4,7 +4,7 @@ import logging import os -from datetime import datetime, timedelta +from datetime import timedelta from decimal import Decimal, InvalidOperation from django.conf import settings @@ -319,9 +319,9 @@ def generate_batch_code(): 'STOCK_BATCH_CODE_TEMPLATE', '' ) - now = datetime.now() + now = InvenTree.helpers.current_time() - # Pass context data through to the template randering. + # Pass context data through to the template rendering. # The following context variables are available for custom batch code generation context = { 'date': now, @@ -412,7 +412,7 @@ def api_instance_filters(self): EXPIRED_FILTER = ( IN_STOCK_FILTER & ~Q(expiry_date=None) - & Q(expiry_date__lt=datetime.now().date()) + & Q(expiry_date__lt=InvenTree.helpers.current_date()) ) def update_serial_number(self): @@ -1011,7 +1011,7 @@ def is_stale(self): if not self.in_stock: return False - today = datetime.now().date() + today = InvenTree.helpers.current_date() stale_days = common.models.InvenTreeSetting.get_setting('STOCK_STALE_DAYS') @@ -1036,7 +1036,7 @@ def is_expired(self): if not self.in_stock: return False - today = datetime.now().date() + today = InvenTree.helpers.current_date() return self.expiry_date < today @@ -1438,7 +1438,7 @@ def add_tracking_entry( item=self, tracking_type=entry_type.value, user=user, - date=datetime.now(), + date=InvenTree.helpers.current_time(), notes=notes, deltas=deltas, ) @@ -1962,7 +1962,7 @@ def stocktake(self, count, user, notes=''): if count < 0: return False - self.stocktake_date = datetime.now().date() + self.stocktake_date = InvenTree.helpers.current_date() self.stocktake_user = user if self.updateQuantity(count): @@ -2464,15 +2464,15 @@ def test_name(self): ) started_datetime = models.DateTimeField( - default=datetime.now, blank=True, + null=True, verbose_name=_('Started'), help_text=_('The timestamp of the test start'), ) finished_datetime = models.DateTimeField( - default=datetime.now, blank=True, + null=True, verbose_name=_('Finished'), help_text=_('The timestamp of the test finish'), ) diff --git a/InvenTree/stock/serializers.py b/InvenTree/stock/serializers.py index c0fc6769456..a820de9ccc6 100644 --- a/InvenTree/stock/serializers.py +++ b/InvenTree/stock/serializers.py @@ -345,7 +345,7 @@ def annotate_queryset(queryset): # Add flag to indicate if the StockItem is stale stale_days = common.models.InvenTreeSetting.get_setting('STOCK_STALE_DAYS') - stale_date = datetime.now().date() + timedelta(days=stale_days) + stale_date = InvenTree.helpers.current_date() + timedelta(days=stale_days) stale_filter = ( StockItem.IN_STOCK_FILTER & ~Q(expiry_date=None) @@ -826,7 +826,7 @@ def save(self): deltas = {'status': status} - now = datetime.now() + now = InvenTree.helpers.current_time() # Instead of performing database updates for each item, # perform bulk database updates (much more efficient) diff --git a/InvenTree/users/models.py b/InvenTree/users/models.py index 701b7e8105b..e2ddcd34179 100644 --- a/InvenTree/users/models.py +++ b/InvenTree/users/models.py @@ -53,7 +53,7 @@ def default_token_expiry(): """Generate an expiry date for a newly created token.""" # TODO: Custom value for default expiry timeout # TODO: For now, tokens last for 1 year - return datetime.datetime.now().date() + datetime.timedelta(days=365) + return InvenTree.helpers.current_date() + datetime.timedelta(days=365) class ApiToken(AuthToken, InvenTree.models.MetadataMixin): @@ -163,7 +163,9 @@ def token(self): @admin.display(boolean=True, description=_('Expired')) def expired(self): """Test if this token has expired.""" - return self.expiry is not None and self.expiry < datetime.datetime.now().date() + return ( + self.expiry is not None and self.expiry < InvenTree.helpers.current_date() + ) @property @admin.display(boolean=True, description=_('Active'))