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
This commit is contained in:
Oliver 2024-03-27 16:57:59 +11:00 committed by GitHub
parent cd0d35047d
commit 4059d9ffeb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 116 additions and 53 deletions

View File

@ -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.

View File

@ -952,6 +952,8 @@ USE_I18N = True
# 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']

View File

@ -74,7 +74,7 @@ class Build(InvenTree.models.InvenTreeBarcodeMixin, InvenTree.models.InvenTreeNo
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 @@ class Build(InvenTree.models.InvenTreeBarcodeMixin, InvenTree.models.InvenTreeNo
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 @@ class Build(InvenTree.models.InvenTreeBarcodeMixin, InvenTree.models.InvenTreeNo
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

View File

@ -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 @@ from plugin.events import trigger_event
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,

View File

@ -13,7 +13,7 @@ import math
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 NotificationEntry(MetaMixin):
@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)

View File

@ -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 @@ from django.utils import timezone
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

View File

@ -901,7 +901,7 @@ class SupplierPart(
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

View File

@ -1,6 +1,5 @@
"""Label printing models."""
import datetime
import logging
import os
import sys
@ -15,6 +14,7 @@ from django.urls import reverse
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 @@ class LabelTemplate(InvenTree.models.InvenTreeMetadataModel):
# 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

View File

@ -221,7 +221,7 @@ class Order(
"""
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 @@ class Order(
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 @@ class PurchaseOrder(TotalPriceMixin, Order):
"""
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 @@ class PurchaseOrder(TotalPriceMixin, Order):
"""
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 @@ class SalesOrder(TotalPriceMixin, Order):
"""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 @@ class SalesOrder(TotalPriceMixin, Order):
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 @@ class SalesOrderShipment(
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 @@ class ReturnOrder(TotalPriceMixin, Order):
"""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 @@ class ReturnOrder(TotalPriceMixin, Order):
"""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 @@ class ReturnOrder(TotalPriceMixin, Order):
)
# 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)

View File

@ -34,7 +34,13 @@ from company.serializers import (
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 @@ class SalesOrderShipmentCompleteSerializer(serializers.ModelSerializer):
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,

View File

@ -37,6 +37,7 @@ import common.models
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 @@ class Part(
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 @@ class PartPricing(common.models.MetaMixin):
)
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:

View File

@ -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)

View File

@ -18,6 +18,7 @@ from django.utils.translation import gettext_lazy as _
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 @@ class ReportTemplateBase(MetadataMixin, ReportBase):
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

View File

@ -19,6 +19,7 @@ from rest_framework.serializers import ValidationError
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 @@ class StockFilter(rest_filters.FilterSet):
# 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 @@ class StockList(APIDownloadMixin, ListCreateDestroyAPIView):
# 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 @@ class StockList(APIDownloadMixin, ListCreateDestroyAPIView):
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)

View File

@ -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',

View File

@ -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'),
),
]

View File

@ -4,7 +4,7 @@ from __future__ import annotations
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 @@ class StockItem(
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 @@ class StockItem(
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 @@ class StockItem(
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 @@ class StockItem(
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 @@ class StockItem(
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 @@ class StockItemTestResult(InvenTree.models.InvenTreeMetadataModel):
)
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'),
)

View File

@ -345,7 +345,7 @@ class StockItemSerializer(InvenTree.serializers.InvenTreeTagModelSerializer):
# 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 @@ class StockChangeStatusSerializer(serializers.Serializer):
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)

View File

@ -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 @@ class ApiToken(AuthToken, InvenTree.models.MetadataMixin):
@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'))