diff --git a/.idea/workspace.xml b/.idea/workspace.xml
index 44b4c3d..0cd9f48 100644
--- a/.idea/workspace.xml
+++ b/.idea/workspace.xml
@@ -8,48 +8,55 @@
-
+
-
-
-
-
+
+
-
-
+
+
+
+
+
+
+
+
-
+
+
-
-
+
+
+
+
+
-
+
+
+
-
-
-
+
+
+
+
+
+
-
-
-
-
+
-
-
-
-
+
-
+
@@ -63,8 +70,8 @@
- {
+ "keyToString": {
+ "DefaultHtmlFileTemplate": "HTML File",
+ "Django Server.DepoT.executor": "Debug",
+ "RunOnceActivity.OpenDjangoStructureViewOnStart": "true",
+ "RunOnceActivity.ShowReadmeOnStart": "true",
+ "RunOnceActivity.git.unshallow": "true",
+ "RunOnceActivity.pycharm.django.structure.promotion.once.per.project": "true",
+ "django.template.preview.state": "SHOW_EDITOR_AND_PREVIEW",
+ "git-widget-placeholder": "master",
+ "ignore.virus.scanning.warn.message": "true",
+ "last_opened_file_path": "C:/dev_projects/python/Django/DepoT/templates/employee",
+ "list.type.of.created.stylesheet": "CSS",
+ "node.js.detected.package.eslint": "true",
+ "node.js.detected.package.tslint": "true",
+ "node.js.selected.package.eslint": "(autodetect)",
+ "node.js.selected.package.tslint": "(autodetect)",
+ "nodejs_package_manager_path": "npm",
+ "vue.rearranger.settings.migration": "true"
},
- "keyToStringList": {
- "DatabaseDriversLRU": [
- "postgresql"
+ "keyToStringList": {
+ "DatabaseDriversLRU": [
+ "postgresql"
]
}
-}]]>
+}
@@ -116,11 +123,11 @@
+
-
@@ -168,7 +175,11 @@
1750784740296
-
+
+
+
+
+
@@ -293,16 +304,6 @@
118
-
- file://$PROJECT_DIR$/damages_api/views.py
- 15
-
-
-
- file://$PROJECT_DIR$/damages_api/views.py
- 39
-
-
file://$PROJECT_DIR$/common/utils/owncloud_utls.py
29
@@ -310,8 +311,18 @@
file://$PROJECT_DIR$/payments/views.py
- 40
-
+ 41
+
+
+
+ file://$PROJECT_DIR$/payments/views.py
+ 56
+
+
+
+ file://$PROJECT_DIR$/static/js/container_validation.js
+ 4
+
diff --git a/DepoT/settings.py b/DepoT/settings.py
index aa83382..fa93ef1 100644
--- a/DepoT/settings.py
+++ b/DepoT/settings.py
@@ -11,25 +11,21 @@ https://docs.djangoproject.com/en/5.2/ref/settings/
"""
from pathlib import Path
+import os
+import environ
-# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
+env = environ.Env()
+environ.Env.read_env(os.path.join(BASE_DIR, '.env'))
-# Quick-start development settings - unsuitable for production
-# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
-
-# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = "django-insecure-g%187p84o9^rr)3#9@r3n^o2v1i%@6=+puxm7hlodg+kbsk%n#"
-# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = ['192.168.24.43', '127.0.0.1', 'localhost', ]
-# Application definition
-
PROJECT_APPS = [
'accounts',
"booking",
@@ -95,11 +91,11 @@ WSGI_APPLICATION = "DepoT.wsgi.application"
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
- "NAME": "depot",
- "USER": "postgres",
- "PASSWORD": "admin",
- "HOST": "127.0.0.1",
- "PORT": "5432",
+ "NAME": os.environ.get("DB_NAME", env("DB_NAME")),
+ "USER": os.environ.get("DB_USER", env("DB_USER")),
+ "PASSWORD": os.environ.get("DB_PASSWORD", env("DB_PASSWORD")),
+ "HOST": os.environ.get("DB_HOST", env("DB_HOST")),
+ "PORT": os.environ.get("DB_PORT", env("DB_PORT")),
}
}
@@ -157,12 +153,27 @@ TEMP_FILE_FOLDER = "/tmp/damages_photos"
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
-import environ
-env = environ.Env()
-import os
-environ.Env.read_env(os.path.join(BASE_DIR, '.env'))
OWNCLOUD_URL = env('OWNCLOUD_URL')
OWNCLOUD_USER = env('OWNCLOUD_USER')
OWNCLOUD_PASSWORD = env('OWNCLOUD_PASSWORD')
-OWNCLOUD_DAMAGES_FOLDER = env('OWNCLOUD_DAMAGES_FOLDER')
\ No newline at end of file
+OWNCLOUD_DAMAGES_FOLDER = env('OWNCLOUD_DAMAGES_FOLDER')
+
+EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
+EMAIL_HOST = env("EMAIL_HOST", cast=str, default=None)
+EMAIL_PORT = env("EMAIL_PORT", cast=str, default='587') # Recommended
+EMAIL_HOST_USER = env("EMAIL_HOST_USER", cast=str, default=None)
+EMAIL_HOST_PASSWORD = env("EMAIL_HOST_PASSWORD", cast=str, default=None)
+EMAIL_USE_TLS = env("EMAIL_USE_TLS", cast=bool, default=True) # Use EMAIL_PORT 587 for TLS
+EMAIL_USE_SSL = env("EMAIL_USE_SSL", cast=bool, default=False) # EUse MAIL_PORT 465 for SSL
+
+ADMIN_USER_NAME=env("ADMIN_USER_NAME", default="Admin user")
+ADMIN_USER_EMAIL=env("ADMIN_USER_EMAIL", default=None)
+
+MANAGERS=[]
+ADMINS=[]
+if all([ADMIN_USER_NAME, ADMIN_USER_EMAIL]):
+ ADMINS +=[
+ (f'{ADMIN_USER_NAME}', f'{ADMIN_USER_EMAIL}')
+ ]
+ MANAGERS=ADMINS
\ No newline at end of file
diff --git a/DepoT/urls.py b/DepoT/urls.py
index 42e5a70..da8d7bb 100644
--- a/DepoT/urls.py
+++ b/DepoT/urls.py
@@ -26,5 +26,6 @@ urlpatterns = [
path('container/', include('containers.urls')),
path('booking/', include('booking.urls')),
path('payment/', include('payments.urls')),
- path('damages/', include('damages_api.urls')),
+ path('api/damages/', include('damages_api.urls')),
+ path('api/common/', include('common_api.urls')),
]
diff --git a/accounts/forms.py b/accounts/forms.py
index f2d127f..0121fa6 100644
--- a/accounts/forms.py
+++ b/accounts/forms.py
@@ -27,7 +27,7 @@ class RegisterForm(ModelForm):
super().__init__(*args, **kwargs)
self.fields['password1'].widget.attrs.update({'autocomplete': 'new-password'})
self.fields['password2'].widget.attrs.update({'autocomplete': 'new-password'})
- self.fields['user_type'].widget.attrs['disabled'] = True
+ self.fields['user_type'].widget.attrs['readonly'] = True
self.fields['company_permissions'].widget.attrs['disabled'] = True
self.fields['employee_permissions'].widget.attrs['disabled'] = True
diff --git a/accounts/views.py b/accounts/views.py
index 7ce928c..1eae994 100644
--- a/accounts/views.py
+++ b/accounts/views.py
@@ -97,6 +97,17 @@ class UserListView(ListView):
context['base_template'] = self.base_template
return context
+ def get_queryset(self):
+ queryset = super().get_queryset()
+ user = self.request.user
+
+ # Filter users based on permissions
+ if user.is_superuser:
+ return queryset.all()
+ elif user.user_type == DepotUser.UserType.COMPANY_ADMIN:
+ return queryset.filter(company=user.company)
+ else:
+ return queryset.none()
class UserUpdateView(UpdateView):
template_name = 'registration/register.html'
diff --git a/booking/forms.py b/booking/forms.py
index 32f12ed..c84fdb7 100644
--- a/booking/forms.py
+++ b/booking/forms.py
@@ -17,9 +17,20 @@ class BookingCreateForm(BookingBaseForm):
class Meta(BookingBaseForm.Meta):
fields = ['number', 'vehicles', 'container_type', 'container_count', 'carrier', 'line', 'container_number']
-
+ widgets = {
+ 'container_number': forms.TextInput(attrs={
+ 'oninput': 'validateContainerNumber(this)',
+ 'class': 'form-control'
+ })
+ }
class BookingUpdateForm(BookingBaseForm):
class Meta(BookingBaseForm.Meta):
fields = ['number', 'vehicles', 'container_type', 'container_count', 'carrier', 'line', 'container_number']
+ widgets = {
+ 'container_number': forms.TextInput(attrs={
+ 'oninput': 'validateContainerNumber(this)',
+ 'class': 'form-control'
+ })
+ }
\ No newline at end of file
diff --git a/common/templatetags/filters.py b/common/templatetags/filters.py
index e69de29..cfd1dd4 100644
--- a/common/templatetags/filters.py
+++ b/common/templatetags/filters.py
@@ -0,0 +1,20 @@
+from django import template
+from django.template.defaultfilters import date
+
+from common.utils.utils import check_container_number_validity
+
+register = template.Library()
+
+@register.filter
+def bg_date(value):
+ if value:
+ return date(value, "y.m.d h:m")
+ return ""
+
+
+@register.simple_tag
+def container_validity_class(container_number):
+ if not check_container_number_validity(container_number):
+ return 'invalid-container'
+ return ''
+
diff --git a/common/utils/owncloud_utls.py b/common/utils/owncloud_utls.py
new file mode 100644
index 0000000..5c9db30
--- /dev/null
+++ b/common/utils/owncloud_utls.py
@@ -0,0 +1,40 @@
+import os
+import uuid
+from datetime import datetime
+from django.conf import settings
+from rest_framework import status
+from rest_framework.response import Response
+import owncloud
+from owncloud import (HTTPResponseError)
+
+
+class Owncloud:
+ @staticmethod
+ def upload_damage_photo(filename, depot_id):
+ _, ext = os.path.splitext(filename)
+ oc = owncloud.Client(settings.OWNCLOUD_URL)
+ oc.login(settings.OWNCLOUD_USER, settings.OWNCLOUD_PASSWORD)
+ path = f"{settings.OWNCLOUD_DAMAGES_FOLDER}{str(depot_id)}"
+ try:
+ oc.file_info(path)
+ except HTTPResponseError:
+ _ = oc.mkdir(path)
+ owncloud_filename = f"{datetime.today().strftime('%Y%m%d')}_{uuid.uuid4()}{ext}"
+ owncloud_fullpath = path + "/" + owncloud_filename
+ oc.put_file(owncloud_fullpath, filename)
+ file_info = oc.share_file_with_link(owncloud_fullpath)
+ return file_info.get_link()
+
+ @staticmethod
+ def get_damages(depot_id):
+ oc = owncloud.Client(settings.OWNCLOUD_URL)
+ oc.login(settings.OWNCLOUD_USER, settings.OWNCLOUD_PASSWORD)
+ files = oc.list(f"{settings.OWNCLOUD_DAMAGES_FOLDER}{str(depot_id)}")
+ damages = []
+ for file_info in files:
+ if file_info.is_dir():
+ continue # Пропускаме поддиректории
+ # Създаване на публичен линк за всеки файл
+ link_info = oc.share_file_with_link(file_info.path)
+ damages.append(link_info.get_link())
+ return Response(damages, status=status.HTTP_200_OK)
diff --git a/common/utils/utils.py b/common/utils/utils.py
index 3af4f9f..a3a2a37 100644
--- a/common/utils/utils.py
+++ b/common/utils/utils.py
@@ -1,5 +1,12 @@
+import re
+
+from django.template.loader import render_to_string
+from django.utils.html import strip_tags
+
from containers.models import Container
from preinfo.models import Preinfo
+from django.conf import settings
+from django.core.mail import send_mail, EmailMessage
def filter_queryset_by_user(queryset, user):
@@ -40,4 +47,43 @@ def get_container_by_number(number):
try:
return Container.objects.get(number=number, expedited=False)
except Container.DoesNotExist:
- return None
\ No newline at end of file
+ return None
+
+
+CALC_STRING = "0123456789A.BCDEFGHIJK.LMNOPQRSTU.VWXYZ"
+
+def check_container_number_validity(container_number):
+ if not re.search(r"[A-Z]{4}[0-9]{7}", container_number):
+ return False
+ subtotal = 0
+ delta = 1
+ for i in range(0, 10):
+ subtotal = subtotal + CALC_STRING.find(container_number[i]) * delta
+ delta = delta * 2
+ subtotal = (subtotal % 11) % 10
+ if not subtotal == CALC_STRING.find(container_number[10]):
+ return False
+ return True
+
+
+ADMIN_USER_EMAIL= settings.ADMIN_USER_EMAIL
+EMAIL_HOST_USER = settings.EMAIL_HOST_USER
+
+def send_test_email(user_email, urls):
+ subject='Welcome to Our Platform'
+ html_message = render_to_string('email.html', {
+ 'site_name': 'Our Platform',
+ 'user_email': user_email,
+ 'link1': urls['pay_epay'],
+ 'link2': urls['pay_credit_card'],
+ })
+ plain_message = strip_tags(html_message)
+
+ email = EmailMessage(
+ subject=subject,
+ body=html_message,
+ from_email=EMAIL_HOST_USER,
+ to=[user_email],
+ )
+ email.content_subtype = 'html' # This tells Django to send HTML email
+ email.send()
\ No newline at end of file
diff --git a/common/views/barrier_views.py b/common/views/barrier_views.py
index dd4694c..960246c 100644
--- a/common/views/barrier_views.py
+++ b/common/views/barrier_views.py
@@ -1,14 +1,17 @@
from django.shortcuts import render
from django.views.generic import TemplateView
+from containers.models import Container
+
class BarrierDashboardView(TemplateView):
template_name = 'barrier/barrier-dashboard.html'
- extra_context = {
- 'title': 'Client Dashboard',
- 'description': 'This is the client dashboard page.',
- }
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+ recent_containers = Container.objects.select_related('line', 'booking').order_by('-expedited_on', '-received_on')[:10]
+ context['recent_containers'] = recent_containers
+ return context
def get(self, request, *args, **kwargs):
- return render(request, self.template_name, self.extra_context)
\ No newline at end of file
+ return render(request, self.template_name, self.get_context_data())
\ No newline at end of file
diff --git a/common_api/urls.py b/common_api/urls.py
index a5810ac..06aa38c 100644
--- a/common_api/urls.py
+++ b/common_api/urls.py
@@ -3,5 +3,5 @@ from .views import validate_container
urlpatterns = [
# ... your other urls
- path('api/validate-container//', validate_container, name='validate_container'),
+ path('validate-container//', validate_container, name='validate_container'),
]
\ No newline at end of file
diff --git a/containers/forms.py b/containers/forms.py
index 8fb1f60..88b607c 100644
--- a/containers/forms.py
+++ b/containers/forms.py
@@ -1,4 +1,4 @@
-from django.forms import ModelForm
+from django.forms import ModelForm, TextInput
from containers.models import Container
@@ -7,7 +7,12 @@ class ContainerBaseForm(ModelForm):
class Meta:
model = Container
fields = '__all__'
-
+ widgets = {
+ 'number': TextInput(attrs={
+ 'oninput': 'validateContainerNumber(this)',
+ 'class': 'form-control'
+ })
+ }
class ContainerReceiveForm(ContainerBaseForm):
"""
Form for creating a new Container instance.
@@ -16,14 +21,28 @@ class ContainerReceiveForm(ContainerBaseForm):
class Meta(ContainerBaseForm.Meta):
fields = ['number', 'receive_vehicle', 'damages', 'heavy_damaged', 'position',]
+ def __init__(self, *args, preinfo=None, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ # Add form-control class to all fields
+ for field in self.fields.values():
+ field.widget.attrs.update({'class': 'form-control'})
+
+ # Checkbox specific styling
+ self.fields['heavy_damaged'].widget.attrs.update({'class': 'form-check-input'})
+ # If preinfo is provided, set initial values and make fields readonly
+ if preinfo:
+ ...
+ # self.fields['preinfo'].initial = preinfo.container_number
+ # self.fields['number'].initial = preinfo.number
+ # self.fields['container_number'].widget.attrs['readonly'] = True
+ # self.fields['number'].widget.attrs['readonly'] = True
+ # self.fields['booking'].disabled = True
+ # self.fields['number'].disabled = True
class ContainerExpeditionForm(ContainerBaseForm):
- """
- Form for updating an existing Container instance.
- Inherits from ContainerBaseForm.
- """
class Meta(ContainerBaseForm.Meta):
fields = ['number', 'expedition_vehicle', 'position', 'line', 'container_type', 'damages', 'heavy_damaged']
@@ -32,4 +51,4 @@ class ContainerExpeditionForm(ContainerBaseForm):
readonly_fields = ['number', 'position', 'line', 'container_type', 'damages', 'heavy_damaged']
for field in readonly_fields:
self.fields[field].widget.attrs['readonly'] = True
- self.fields[field].disabled = True
\ No newline at end of file
+ # self.fields[field].disabled = True
\ No newline at end of file
diff --git a/containers/models.py b/containers/models.py
index 1973fd9..d9ec771 100644
--- a/containers/models.py
+++ b/containers/models.py
@@ -1,5 +1,7 @@
+
from django.db import models
from common.models import LinesModel, ContainerTypeModel
+# from payments.models import ContainerTariffPeriod, AdditionalFees
# Create your models here.
class Container(models.Model):
@@ -42,6 +44,11 @@ class Container(models.Model):
blank=True,
null=True
)
+ preinfo = models.ForeignKey(
+ 'preinfo.Preinfo',
+ on_delete=models.CASCADE,
+ related_name='container_preinfo',
+ )
booking = models.ForeignKey(
'booking.Booking',
on_delete=models.CASCADE,
@@ -61,6 +68,7 @@ class Container(models.Model):
expedition_vehicle = models.CharField(max_length=100, blank=True, null=True)
+
class ContainerHistory(Container):
operation = models.ForeignKey(
'common.OperationModel',
diff --git a/containers/services.py b/containers/services.py
index ca643b1..7212e6d 100644
--- a/containers/services.py
+++ b/containers/services.py
@@ -1,11 +1,11 @@
from booking.models import Booking
from containers.models import Container
-def get_container_for_booking(booking_number):
+def get_container_for_booking(booking):
filters = {
'expedited': False,
}
- booking = Booking.objects.get(number=booking_number)
+ # booking = Booking.objects.get(number=booking_number)
if not booking:
return None
if booking.container_number:
diff --git a/containers/urls.py b/containers/urls.py
index 9105965..f3366f6 100644
--- a/containers/urls.py
+++ b/containers/urls.py
@@ -1,14 +1,18 @@
from django.urls import include, path
from containers.views.employee_views import ContainersListView, ReportContainersUnpaidListView
-from containers.views.barrier_views import ContainerReceive, ContainerExpedition, ContainerSearchView
+from containers.views.barrier_views import ContainerReceiveView, ContainerExpedition, ContainerSearchView, \
+ ContainerPhotosView
urlpatterns = [
- path('search/', ContainerSearchView.as_view(), name='container_search'),
+ path('container-search/', ContainerSearchView.as_view(), name='container_search'),
+ path('container-search/', ContainerSearchView.as_view(), name='barrier_photos'),
+ # path('search/', ContainerSearchView.as_view(), name='container_search'),
path('employee/', ContainersListView.as_view(), name='employee_containers'),
path('not-paid', ReportContainersUnpaidListView.as_view(), name='not_paid'),
+ path('barrier/receive/', ContainerReceiveView.as_view(), name='container_receive'),
+ path('barrier/expedition/', ContainerExpedition.as_view(), name='container_expedition'),
path('barrier//', include([
- path('receive/', ContainerReceive.as_view(), name='container_receive'),
- path('expedition/', ContainerExpedition.as_view(), name='container_expedition'),
+ path('photos/', ContainerPhotosView.as_view(), name='container_photos'),
])),
]
\ No newline at end of file
diff --git a/containers/views/barrier_views.py b/containers/views/barrier_views.py
index e2cba26..62321ea 100644
--- a/containers/views/barrier_views.py
+++ b/containers/views/barrier_views.py
@@ -7,53 +7,69 @@ from django.views.generic import CreateView, UpdateView, FormView, ListView
from booking.models import Booking
from common.utils.utils import get_container_by_number
from containers.forms import ContainerReceiveForm, ContainerExpeditionForm
-from containers.models import Container
+from containers.models import Container, ContainerPhotos
from containers.services import get_container_for_booking
from preinfo.models import Preinfo
# Create your views here.
-class ContainerReceive(CreateView):
+class ContainerReceiveView(FormView):
template_name = 'container-receive.html'
- model = Container
form_class = ContainerReceiveForm
- success_url = reverse_lazy('dashboard')
+ success_url = reverse_lazy('container_photos')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
- pk = self.kwargs.get('pk')
- try:
- preinfo =Preinfo.objects.filter(pk=pk, received=False).first()
- except Preinfo.DoesNotExist:
- preinfo = None
+ context['show_search'] = True
- if preinfo:
- context['preinfo'] = preinfo
- context['containers'] = Container.objects.order_by('-received_on').all()[:10] # Fetch the last 10 containers
- return context
- return redirect(reverse_lazy('container_search'))
+ search_number = self.request.GET.get('number')
+ if search_number:
+ preinfo = Preinfo.objects.filter(container_number=search_number).first()
+ if preinfo:
+ form = self.form_class(initial={'number': preinfo.container_number})
+ context.update({
+ 'form': form,
+ 'preinfo': preinfo,
+ 'search_number': search_number,
+ 'show_search': False
+ })
+ else:
+ context.update({
+ 'error': 'Preinfo for this container not found',
+ 'search_number': search_number
+ })
+ return context
def form_valid(self, form):
+ container = form.save(commit=False)
+ preinfo = Preinfo.objects.filter(container_number=self.request.POST.get('number')).first()
+ preinfo.received = True
+ preinfo.save()
+ # Set required fields from preinfo
+ container.container_type = preinfo.container_type
+ container.line = preinfo.line
+ container.received_by = self.request.user
+ container.preinfo = preinfo
+ container.save()
- # Get the preinfo_id from the POST data
- preinfo_id = self.request.POST.get('preinfo_id')
- try:
- preinfo = Preinfo.objects.get(id=preinfo_id)
- except Preinfo.DoesNotExist:
- preinfo = None
-
- # validate if data is correct, comparing user data with preinfo data
- if preinfo and preinfo.container_number == form.cleaned_data.get('number') and not preinfo.received:
- preinfo.received = True
- preinfo.save()
-
- form.instance.received_by = self.request.user
- form.instance.line = preinfo.line
- form.instance.container_type = preinfo.container_type
- else:
- form.add_error('number', 'Invalid data')
+ return redirect('container_photos', pk=container.pk)
- return super().form_valid(form)
+ def get_form(self, form_class=None):
+ form = super().get_form(form_class)
+ search_number = self.request.GET.get('number') # Changed from search_number
+ if search_number:
+ preinfo = Preinfo.objects.filter(container_number=search_number).first()
+ if preinfo:
+ form.initial['number'] = preinfo.container_number
+ form.fields['number'].widget.readonly = True
+ return form
+
+ def form_invalid(self, form):
+ return self.render_to_response(self.get_context_data(
+ form=form,
+ search_number=self.request.POST.get('number'),
+ show_search=False
+ ))
class ContainerSearchView(View):
@@ -83,30 +99,131 @@ class ContainerSearchView(View):
return redirect(next_url, pk=booking.pk)
-class ContainerExpedition(UpdateView):
+# class ContainerExpedition(UpdateView):
+# template_name = 'container-expedition.html'
+# model = Container
+# form_class = ContainerExpeditionForm
+# success_url = reverse_lazy('dashboard')
+#
+# def get_context_data(self, **kwargs):
+# context = super().get_context_data(**kwargs)
+# pk = self.kwargs.get('pk')
+# booking = Booking.objects.get(pk=pk)
+# filters = {
+# 'expedited': False,
+# }
+# if booking.container_number:
+# filters['container_number'] = booking.container_number
+# else:
+# filters['container_type'] = booking.container_type
+# filters['line'] = booking.line
+#
+# container = Container.objects.filter(**filters).order_by('received_on').first()
+# context['container'] = container
+# context['booking'] = booking
+# return context
+#
+# def form_valid(self, form):
+# booking_id = self.request.POST.get('booking_id')
+# container_id = self.request.POST.get('container_id')
+#
+# try:
+# booking = Booking.objects.get(id=booking_id)
+# container = Container.objects.get(id=container_id)
+# vehicle = form.cleaned_data['expedition_vehicle']
+# # Update container
+# container.expedited = True
+# container.expedited_on = timezone.now()
+# container.expedited_by = self.request.user
+# container.expedition_vehicle = vehicle
+# container.booking = booking
+# container.save()
+#
+# # Update booking status if needed
+# booking.container_expedited_count += 1
+# if booking.vehicles:
+# vehicles_list = booking.vehicles.split(',')
+# if vehicle in vehicles_list:
+# vehicles_list.remove(vehicle)
+# booking.vehicles = ','.join(vehicles_list)
+# if booking.container_expedited_count >= booking.container_count:
+# booking.status = 'completed' # or any other status you want to set
+# booking.save()
+#
+# return redirect(self.success_url)
+#
+# except (Booking.DoesNotExist, Container.DoesNotExist):
+# form.add_error(None, 'Invalid booking or container')
+# return self.form_invalid(form)
+#
+#
+# # try:
+# # booking = Booking.objects.get(id=booking_id)
+# # except Booking.DoesNotExist:
+# # booking = None
+# # # validate if data is correct, comparing user data with booking data
+# # if booking and booking.container_number == form.cleaned_data.get('container_number') and not booking.expedited:
+# # booking.expedited = True
+# # booking.save()
+# #
+# # form.instance.expedited_by = self.request.user
+# # form.instance.line = booking.line
+# # form.instance.container_type = booking.container_type
+# # else:
+# # form.add_error('container_number', 'Invalid data')
+#
+# # if preinfo:
+# # context['preinfo'] = preinfo
+# # context['containers'] = Container.objects.order_by('-received_on').all()[:10] # Fetch the last 10 containers
+# # return context
+# # return redirect(reverse_lazy('container_search'))
+
+
+class ContainerExpedition(FormView):
template_name = 'container-expedition.html'
- model = Container
form_class = ContainerExpeditionForm
- success_url = reverse_lazy('dashboard')
+ success_url = reverse_lazy('barrier_dashboard')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
- pk = self.kwargs.get('pk')
- booking = Booking.objects.get(pk=pk)
- filters = {
- 'expedited': False,
- }
- if booking.container_number:
- filters['container_number'] = booking.container_number
- else:
- filters['container_type'] = booking.container_type
- filters['line'] = booking.line
+ context['show_search'] = True
- container = Container.objects.filter(**filters).order_by('received_on').first()
- context['container'] = container
- context['booking'] = booking
+ search_number = self.request.GET.get('number')
+ if search_number:
+ booking = Booking.objects.filter(number=search_number).first()
+ if booking:
+ container = get_container_for_booking(booking)
+ if container:
+ # Initialize form with container data
+ form = self.form_class(initial={
+ 'number': container.number,
+ 'position': container.position,
+ 'line': container.line.id,
+ 'container_type': container.container_type.id,
+ 'damages': container.damages,
+ 'heavy_damaged': container.heavy_damaged,
+ 'expedition_vehicle': container.expedition_vehicle
+ })
+ context.update({
+ 'form': form,
+ 'booking': booking,
+ 'container': container,
+ 'search_number': search_number,
+ 'show_search': False
+ })
+ else:
+ context.update({
+ 'error': 'No available container found for this booking',
+ 'search_number': search_number
+ })
+ else:
+ context.update({
+ 'error': 'Booking not found',
+ 'search_number': search_number
+ })
return context
+
def form_valid(self, form):
booking_id = self.request.POST.get('booking_id')
container_id = self.request.POST.get('container_id')
@@ -114,51 +231,74 @@ class ContainerExpedition(UpdateView):
try:
booking = Booking.objects.get(id=booking_id)
container = Container.objects.get(id=container_id)
- vehicle = form.cleaned_data['expedition_vehicle']
+
# Update container
container.expedited = True
container.expedited_on = timezone.now()
container.expedited_by = self.request.user
- container.expedition_vehicle = vehicle
+ container.expedition_vehicle = form.cleaned_data['expedition_vehicle']
container.booking = booking
container.save()
- # Update booking status if needed
+ # Update booking
booking.container_expedited_count += 1
if booking.vehicles:
vehicles_list = booking.vehicles.split(',')
- if vehicle in vehicles_list:
- vehicles_list.remove(vehicle)
+ if container.expedition_vehicle in vehicles_list:
+ vehicles_list.remove(container.expedition_vehicle)
booking.vehicles = ','.join(vehicles_list)
if booking.container_expedited_count >= booking.container_count:
- booking.status = 'completed' # or any other status you want to set
+ booking.status = 'completed'
booking.save()
return redirect(self.success_url)
-
except (Booking.DoesNotExist, Container.DoesNotExist):
form.add_error(None, 'Invalid booking or container')
return self.form_invalid(form)
+ def form_invalid(self, form):
+ print("Form errors:", form.errors) # Debug print
+ print("Form data:", form.data) # Debug print
+ return self.render_to_response(self.get_context_data(
+ form=form,
+ search_number=self.request.POST.get('number'),
+ show_search=False
+ ))
+
+
+ def get_form(self, form_class=None):
+ form_class = self.get_form_class()
+
+ if self.request.method == 'POST':
+ search_number = self.request.POST.get('search_number')
+ booking = Booking.objects.filter(number=search_number).first()
+ if booking:
+ container = get_container_for_booking(booking)
+ if container:
+ data = self.request.POST.copy()
+ data.update({
+ 'number': container.number,
+ 'position': container.position,
+ 'line': container.line,
+ 'container_type': container.container_type,
+ 'damages': container.damages,
+ 'heavy_damaged': container.heavy_damaged,
+ })
+ return form_class(data)
+ return form_class()
+
+
+
- # try:
- # booking = Booking.objects.get(id=booking_id)
- # except Booking.DoesNotExist:
- # booking = None
- # # validate if data is correct, comparing user data with booking data
- # if booking and booking.container_number == form.cleaned_data.get('container_number') and not booking.expedited:
- # booking.expedited = True
- # booking.save()
- #
- # form.instance.expedited_by = self.request.user
- # form.instance.line = booking.line
- # form.instance.container_type = booking.container_type
- # else:
- # form.add_error('container_number', 'Invalid data')
-
- # if preinfo:
- # context['preinfo'] = preinfo
- # context['containers'] = Container.objects.order_by('-received_on').all()[:10] # Fetch the last 10 containers
- # return context
- # return redirect(reverse_lazy('container_search'))
+# containers/views/barrier_views.py
+class ContainerPhotosView(View):
+ template_name = 'container-photos.html'
+ def get(self, request, pk):
+ container = Container.objects.get(pk=pk)
+ photos = ContainerPhotos.objects.filter(container=container)
+ return render(request, self.template_name, {
+ 'container': container,
+ 'photos': photos,
+ 'photos_count': photos.count()
+ })
\ No newline at end of file
diff --git a/containers/views/employee_views.py b/containers/views/employee_views.py
index f49cb4b..8178ac0 100644
--- a/containers/views/employee_views.py
+++ b/containers/views/employee_views.py
@@ -1,6 +1,9 @@
+from django.shortcuts import render, redirect
+from django.views import View
from django.views.generic import ListView
from common.models import CompanyModel
+from common.utils.utils import get_preinfo_by_number, get_container_by_number
from containers.models import Container
@@ -49,4 +52,30 @@ class ReportContainersUnpaidListView(ListView):
# Add payment filter to show only unpaid containers
queryset = queryset.filter(payment_containers__isnull=True)
- return queryset.order_by('-expedited_on')
\ No newline at end of file
+ return queryset.order_by('-expedited_on')
+
+
+class ContainerSearchView(View):
+ template_name = 'barrier/container-search.html' # Single template for all searches
+
+ def get(self, request):
+ search_type = request.GET.get('param') # container_receive or container_expedition
+ return render(request, self.template_name, {'search_type': search_type})
+
+ def post(self, request):
+ number = request.POST.get('number')
+ search_type = request.POST.get('search_type')
+
+ if search_type == 'container_receive':
+ preinfo = get_preinfo_by_number(number)
+ if preinfo:
+ return redirect('receive_container', pk=preinfo.pk)
+ else: # container_expedition
+ container = get_container_by_number(number)
+ if container:
+ return redirect('expedite_container', pk=container.pk)
+
+ return render(request, self.template_name, {
+ 'error': 'Not found',
+ 'search_type': search_type
+ })
\ No newline at end of file
diff --git a/damages_api/urls.py b/damages_api/urls.py
index 8bbacbc..f062a47 100644
--- a/damages_api/urls.py
+++ b/damages_api/urls.py
@@ -3,5 +3,5 @@ from django.urls import path
from damages_api.views import Damages
urlpatterns = [
- path('', Damages.as_view(), name='damages_list'),
+ path('/', Damages.as_view(), name='damages_list'),
]
\ No newline at end of file
diff --git a/damages_api/views.py b/damages_api/views.py
index b1c93df..4b97286 100644
--- a/damages_api/views.py
+++ b/damages_api/views.py
@@ -9,34 +9,43 @@ from rest_framework.response import Response
from rest_framework.views import APIView
from common.utils.owncloud_utls import Owncloud
-from containers.models import ContainerPhotos
+from containers.models import ContainerPhotos, Container
class Damages(APIView):
+
def post(self, request, depot_id):
- data = request.get_json()
- photo = data.pop("photo")
- extension = data.pop("photo_extension")
+ photo = request.data.get("photo")
+ extension = request.data.get("photo_extension")
with tempfile.NamedTemporaryFile(suffix=f'.{extension}', delete=False) as temp_file:
try:
temp_file.write(base64.b64decode(photo.encode("utf-8")))
temp_file.flush()
+ temp_file.close() # Close the file before uploading
own_filename = Owncloud.upload_damage_photo(temp_file.name, depot_id)
container_photo = ContainerPhotos()
- container_photo.container.pk = depot_id
+ container_photo.container = Container.objects.get(pk=depot_id)
container_photo.photo = own_filename
container_photo.uploaded_on = datetime.now()
container_photo.uploaded_by = request.user
container_photo.save()
+ return Response(status=status.HTTP_201_CREATED)
+
except Exception as ex:
- raise BadRequest("Invalid photo encoding")
- finally:
- os.unlink(temp_file.name)
- return Response(status=status.HTTP_201_CREATED)
+ return Response(
+ {"error": "Failed to process photo"},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+ finally:
+ if temp_file:
+ try:
+ os.unlink(temp_file.name)
+ except OSError:
+ pass # Ignore deletion errors
def get(self, request, depot_id):
return Owncloud.get_damages(depot_id)
diff --git a/docker-compose.yml b/docker-compose.yml
index e69de29..64264d6 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -0,0 +1,11 @@
+version: '3.8'
+
+services:
+ web:
+ build: .
+ ports:
+ - "8000:8000"
+ env_file:
+ - development.env
+ volumes:
+ - .:/DepoT
\ No newline at end of file
diff --git a/dockerfile b/dockerfile
index e69de29..ed8d0a6 100644
--- a/dockerfile
+++ b/dockerfile
@@ -0,0 +1,7 @@
+FROM python:3.11-slim
+ENV PYTHONUNBUFFERED=1
+WORKDIR /DepoT
+COPY requirements.txt .
+RUN pip install -r requirements.txt
+COPY . .
+CMD python manage.py migrate && python manage.py runserver 0.0.0.0:8000
\ No newline at end of file
diff --git a/payments/forms.py b/payments/forms.py
index 54eaa5b..32bc423 100644
--- a/payments/forms.py
+++ b/payments/forms.py
@@ -7,7 +7,7 @@ from payments.models import Payment
class PaymentCreateForm(ModelForm):
class Meta:
model = Payment
- fields = ['total_amount', 'company', 'description']
+ fields = ['company', 'total_amount', 'description']
widgets = {
'containers': CheckboxSelectMultiple(),
}
diff --git a/payments/services.py b/payments/services.py
index e69de29..6ea1990 100644
--- a/payments/services.py
+++ b/payments/services.py
@@ -0,0 +1,94 @@
+import base64
+import hashlib
+import hmac
+import urllib.parse
+from datetime import datetime, timedelta
+
+import requests
+
+from DepoT.settings import env
+
+
+class EPay:
+ payment_types = {
+ "pay_credit_card": "credit_paydirect",
+ "pay_epay": "paylogin",
+ }
+
+ @staticmethod
+ def encode_message(message):
+ encoded_data = base64.b64encode(message.encode("utf-8")).decode("utf-8")
+ secret_key = env("EPAY_SECURITY_KEY")
+ checksum = hmac.new(
+ secret_key.encode("utf-8"), encoded_data.encode("utf-8"), hashlib.sha1
+ ).hexdigest()
+ return encoded_data, checksum
+
+ @classmethod
+ def generate_urls(self, invoice, amount):
+ client_id = env("EPAY_CLIENT_ID")
+ description = f"Invoice number: {invoice} with amount of {amount}"
+ currency = "BGN"
+ expiry = (
+ datetime.today() + timedelta(days=int(env("INVOICE_EXPIRE_PERIOD")))
+ ).strftime("%d.%m.%Y")
+
+ if amount == 0:
+ amount = 1000
+
+ message = f"MIN={client_id}\nINVOICE={invoice}\nAMOUNT={amount}\nCURRENCY={currency}\nEXP_TIME={expiry}\nDESCR={description}"
+ encoded_data, checksum = EPay.encode_message(message)
+ result = {}
+ for pay_type, keyword in EPay.payment_types.items():
+ url = f"https://demo.epay.bg/?PAGE={keyword}&ENCODED={urllib.parse.quote(encoded_data)}&CHECKSUM={urllib.parse.quote(checksum)}"
+ result[pay_type] = url
+ return result
+
+ @staticmethod
+ def payment_result(encoded_data, checksum_received):
+ secret_key = env("EPAY_SECURITY_KEY")
+ decoded_data = base64.b64decode(encoded_data).decode("utf-8")
+ checksum_calculated = hmac.new(
+ secret_key.encode("utf-8"), encoded_data.encode("utf-8"), hashlib.sha1
+ ).hexdigest()
+ checksum_result = checksum_calculated == checksum_received
+
+ fields = decoded_data.split(":")
+ fields_dict = {}
+ for field in fields:
+ try:
+ name, value = field.split("=")
+ except ValueError:
+ print(fields)
+ print(field)
+
+ fields_dict[name] = value
+
+ return fields_dict, checksum_result
+
+ @staticmethod
+ def send_response(encoded_data, checksum):
+ url = f"https://demo.epay.bg/?ENCODED={urllib.parse.quote(encoded_data)}&CHECKSUM={urllib.parse.quote(checksum)}"
+ print(url)
+ res = requests.post(url)
+
+ @staticmethod
+ def send_ok(invoice):
+ message = f"INVOICE={invoice}:STATUS=OK"
+ encoded_data, checksum = EPay.encode_message(message)
+ print(f"OK {invoice}")
+ EPay.send_response(encoded_data, checksum)
+
+ @staticmethod
+ def send_no(invoice):
+ message = f"INVOICE={invoice}:STATUS=NO"
+ encoded_data, checksum = EPay.encode_message(message)
+ print(f"NO {invoice}")
+ EPay.send_response(encoded_data, checksum)
+
+ @staticmethod
+ def send_err(invoice):
+ message = f"INVOICE={invoice}:STATUS=ERR:ERR=BAD_CHECKSUM"
+ encoded_data, checksum = EPay.encode_message(message)
+ print(f"ERR {invoice}")
+ EPay.send_response(encoded_data, checksum)
diff --git a/payments/urls.py b/payments/urls.py
index 247476a..1b7c2c9 100644
--- a/payments/urls.py
+++ b/payments/urls.py
@@ -1,7 +1,8 @@
from django.urls import path
-from payments.views import PaymentCreateView
+from payments.views import PaymentCreateView, some_view
urlpatterns = [
path("create/", PaymentCreateView.as_view(), name="payments_create"),
+ path('invoice/', some_view, name='payments_invoice'),
]
diff --git a/payments/utils.py b/payments/utils.py
index e69de29..d787498 100644
--- a/payments/utils.py
+++ b/payments/utils.py
@@ -0,0 +1,36 @@
+from django.db.models import Q
+
+from payments.models import ContainerTariffPeriod, AdditionalFees
+
+
+def calculate_charges(container):
+ days = (container.expedited_on.date() - container.received_on.date()).days + 1
+ total = 0
+
+ size = '20' if container.container_type.length == 20 else '40'
+
+ tariff = ContainerTariffPeriod.objects.get(
+ Q(to_days__gt=days) | Q(to_days__isnull=True),
+ container_size=size,
+ from_days__lte=days,
+ )
+
+ fees = AdditionalFees.objects.first()
+ storage_charge = days * float(tariff.daily_rate)
+
+ if container.container_type.container_type == 'reefer':
+ storage_charge += days * float(fees.reefer_daily_supplement)
+
+ if container.swept:
+ total += float(fees.sweeping_fee)
+ if container.washed:
+ total += float(fees.fumigation_fee)
+
+ total += storage_charge
+
+ return {
+ 'days': days,
+ 'storage_charge': storage_charge,
+ 'services_charge': total - storage_charge,
+ 'total': total,
+ }
diff --git a/payments/views.py b/payments/views.py
index 7378e18..1dfcb3e 100644
--- a/payments/views.py
+++ b/payments/views.py
@@ -1,25 +1,72 @@
+from datetime import datetime
+
+from django.db.models import Sum
from django.shortcuts import render
from django.urls import reverse_lazy
from django.views.generic import ListView, CreateView
+from reportlab.lib import colors
+from reportlab.lib.pagesizes import A4
+from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
+from reportlab.platypus import Table, TableStyle, Spacer, Paragraph, SimpleDocTemplate
from common.models import CompanyModel
+from common.utils.utils import send_test_email
from containers.models import Container
from payments.forms import PaymentCreateForm
-from payments.models import Payment
+from payments.models import Payment, PaymentItem
+from payments.services import EPay
+from payments.utils import calculate_charges
+import io
+from django.http import FileResponse, HttpResponse, response
+from reportlab.pdfgen import canvas
# Create your views here.
class PaymentCreateView(CreateView):
model = Payment
form_class = PaymentCreateForm
template_name = 'employee/payment-create.html'
- success_url = reverse_lazy('payment-list')
+ success_url = reverse_lazy('not_paid')
def form_valid(self, form):
- container_ids = self.request.POST.getlist('containers')
+
+ last_payment = Payment.objects.order_by('-invoice_number').first()
+ today = datetime.now().strftime('%Y%m%d')
+
+ if last_payment and last_payment.invoice_number.startswith(f'INV-{today}'):
+ last_number = int(last_payment.invoice_number.split('-')[-1])
+ new_number = str(last_number + 1).zfill(4)
+ else:
+ new_number = '0001'
+
+ invoice_number = f'INV-{today}-{new_number}'
+
payment = form.save(commit=False)
+ payment.invoice_number = invoice_number
+ payment.created_by = self.request.user
+ payment.updated_by = self.request.user
+ payment.total_amount = 0
+ payment.save()
+
+ container_ids = self.request.POST.getlist('containers')
+ containers = Container.objects.filter(id__in=container_ids)
+ for container in containers:
+ charges = calculate_charges(container)
+ payment.total_amount += charges['total']
+ PaymentItem.objects.create(
+ payment=payment,
+ container=container,
+ amount=charges['total'] # Using total charge for the container
+ )
+
+ # 0000000000000000000000000000000000#
+ urls = EPay.generate_urls(payment.invoice_number, payment.total_amount)
+ print("Generated URLs:", urls)
payment.save()
- payment.containers.set(container_ids)
+ send_test_email('kikimor@gmail.com', urls) # invoice and
+ # 0000000000000000000000000000000000#
+
+
return super().form_valid(form)
def get_context_data(self, **kwargs):
@@ -42,6 +89,145 @@ class PaymentCreateView(CreateView):
company_pk = self.request.GET.get('company', '')
form.fields['company'].initial = company_pk
- # container_ids = [int(id) for id in container_ids if id.strip().isdigit()]
- # form.fields['containers'].initial = container_ids
- return form
\ No newline at end of file
+ container_ids = self.request.GET.get('containers', '').split(',')
+ container_ids = [int(_id) for _id in container_ids if _id.strip().isdigit()]
+
+ total_charges = {}
+ if container_ids:
+ containers = Container.objects.filter(
+ id__in=container_ids,
+ expedited=True,
+ payment_containers__isnull=True
+ )
+ for container in containers:
+ charges = calculate_charges(container)
+ total_charges['days'] = total_charges.get('days', 0) + charges.get('days', 0)
+ total_charges['storage_charge'] = total_charges.get('storage_charge', 0) + charges.get('storage_charge', 0)
+ total_charges['services_charge'] = total_charges.get('services_charge', 0) + charges.get('services_charge', 0)
+ total_charges['total'] = total_charges.get('total', 0) + charges.get('total', 0)
+ form.fields['total_amount'].initial = total_charges['total']
+ return form
+
+def some_view(request):
+ buffer = io.BytesIO()
+ doc = SimpleDocTemplate(buffer, pagesize=A4)
+ story = []
+ styles = getSampleStyleSheet()
+
+ # Custom styles
+ styles.add(ParagraphStyle(
+ name='RightAlign',
+ parent=styles['Normal'],
+ alignment=2 # right alignment
+ ))
+
+ # Header data
+ depot_data = [
+ ['ABC Depot Ltd.'],
+ ['123 Depot Street'],
+ ['Sofia, Bulgaria'],
+ ['VAT: BG123456789'],
+ ['Phone: +359 2 123 4567']
+ ]
+
+ customer_data = [
+ ['Customer Company Ltd.'],
+ ['456 Customer Ave.'],
+ ['London, UK'],
+ ['VAT: GB987654321'],
+ ['Contact: John Doe']
+ ]
+
+ # Create header table
+ header_data = [[
+ Table(depot_data, colWidths=[200]),
+ Table(customer_data, colWidths=[200])
+ ]]
+ header_table = Table(header_data, colWidths=[250, 250])
+ header_table.setStyle(TableStyle([
+ ('ALIGN', (0, 0), (-1, -1), 'LEFT'),
+ ('VALIGN', (0, 0), (-1, -1), 'TOP'),
+ ]))
+ story.append(header_table)
+ story.append(Spacer(1, 30))
+
+ # Invoice title and number
+ story.append(Paragraph('INVOICE', styles['Heading1']))
+ story.append(Paragraph('Invoice No: INV-20240220-0001', styles['Normal']))
+ story.append(Paragraph('Date: 20.02.2024', styles['Normal']))
+ story.append(Spacer(1, 20))
+
+ # Invoice table data
+ table_data = [
+ ['Container Type', 'Days', 'Swept', 'Washed', 'Amount'],
+ ['20\' Container', '15', 'Yes', 'No', '€450.00'],
+ ['40\' Container', '10', 'Yes', 'Yes', '€750.00'],
+ ]
+
+ # Create invoice table
+ invoice_table = Table(table_data, colWidths=[120, 80, 80, 80, 100])
+ invoice_table.setStyle(TableStyle([
+ ('BACKGROUND', (0, 0), (-1, 0), colors.grey),
+ ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
+ ('ALIGN', (0, 0), (-1, -1), 'CENTER'),
+ ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
+ ('FONTSIZE', (0, 0), (-1, 0), 12),
+ ('BOTTOMPADDING', (0, 0), (-1, 0), 12),
+ ('GRID', (0, 0), (-1, -1), 1, colors.black),
+ ('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
+ ]))
+ story.append(invoice_table)
+ story.append(Spacer(1, 20))
+
+ # Total amount
+ total_text = f'Total Amount: €1,200.00'
+ story.append(Paragraph(total_text, styles['RightAlign']))
+
+ # Build PDF
+ doc.build(story, onFirstPage=add_watermark)
+ pdf = buffer.getvalue()
+ buffer.close()
+
+ # Create response
+ response = HttpResponse(content_type='application/pdf')
+ response['Content-Disposition'] = 'inline; filename="invoice.pdf"'
+ response.write(pdf)
+ return response
+
+
+def add_watermark(canvas, doc):
+ canvas.saveState()
+ canvas.setFillColorRGB(0.9, 0.9, 0.9)
+ canvas.setFont('Helvetica', 80)
+ canvas.rotate(45)
+ canvas.drawString(200, 000, 'ORIGINAL')
+ canvas.restoreState()
+
+ return response
+
+
+def on_payment_result(encoded, checksum):
+ fields_dict, checksum_result = EPay.payment_result(encoded, checksum)
+ status = fields_dict["STATUS"]
+ invoice = fields_dict["INVOICE"]
+ if not checksum_result:
+ return f"INVOICE={invoice}:STATUS=ERR"
+
+ payment = Payment.objects.get(invoice_number=invoice)
+ if not payment:
+ return f"INVOICE={invoice}:STATUS=NO"
+
+ match status:
+ case "PAID":
+ payment.status = "paid"
+ payment.paid_on = datetime.now()
+ payment.pay_time = fields_dict["PAY_TIME"]
+ payment.pay_stan = fields_dict["STAN"]
+ payment.pay_bcode = fields_dict["BCODE"]
+
+ case "DENIED":
+ payment.status = "denied"
+ case "EXPIRED":
+ payment.status = "expired"
+ payment.save()
+ return f"INVOICE={invoice}:STATUS=OK"
\ No newline at end of file
diff --git a/populate_database.txt b/populate_database.txt
index da97378..78fa1c4 100644
--- a/populate_database.txt
+++ b/populate_database.txt
@@ -37,4 +37,24 @@ insert into accounts_clientpermission (id, codename, name) values (5, 'can_view_
insert into accounts_clientpermission (id, codename, name) values (6, 'can_manage_payment', 'Can manage payment');
insert into accounts_clientpermission (id, codename, name) values (7, 'can_manage_company_users', 'Can manage company users');
+INSERT INTO payments_containertariffperiod
+ (container_size, from_days, to_days, daily_rate)
+VALUES
+ ('20', 1, 3, 5.00),
+ ('20', 4, 7, 10.00),
+ ('20', 8, 15, 15.00),
+ ('20', 16, NULL, 20.00);
+-- For 40/45 feet containers
+INSERT INTO payments_containertariffperiod
+ (container_size, from_days, to_days, daily_rate)
+VALUES
+ ('40', 1, 3, 10.00),
+ ('40', 4, 7, 15.00),
+ ('40', 8, 15, 20.00),
+ ('40', 16, NULL, 25.00);
+
+INSERT INTO payments_additionalfees
+ (reefer_daily_supplement, sweeping_fee, fumigation_fee)
+VALUES
+ (3.00, 35.00, 75.00);
diff --git a/preinfo/forms.py b/preinfo/forms.py
index 77eadc1..50a3c76 100644
--- a/preinfo/forms.py
+++ b/preinfo/forms.py
@@ -1,4 +1,4 @@
-from django.forms import ModelForm
+from django.forms import ModelForm, TextInput
from django.urls import reverse_lazy
from preinfo.models import Preinfo
@@ -8,7 +8,12 @@ class PreinfoBaseForm(ModelForm):
class Meta:
model = Preinfo
fields = '__all__'
-
+ widgets = {
+ 'container_number': TextInput(attrs={
+ 'oninput': 'validateContainerNumber(this)',
+ 'class': 'form-control'
+ })
+ }
# success_url = reverse_lazy('client_preinfo')
class PreinfoCreateForm(PreinfoBaseForm):
"""
@@ -24,7 +29,6 @@ class PreinfoCreateForm(PreinfoBaseForm):
'received'] # Exclude fields that should not be set by the user
-class PreinfoEditForm(ModelForm):
+class PreinfoEditForm(PreinfoBaseForm):
class Meta:
- model = Preinfo
fields = ['container_number', 'container_type', 'line']
\ No newline at end of file
diff --git a/preinfo/views/client_views.py b/preinfo/views/client_views.py
index ce3d599..11e2411 100644
--- a/preinfo/views/client_views.py
+++ b/preinfo/views/client_views.py
@@ -6,7 +6,8 @@ from django.views import View
from django.views.generic import CreateView, ListView, UpdateView
from DepoT.mixins.LineFiltweFormMixin import LineFilterFormMixin
-from common.utils.utils import filter_queryset_by_user, get_preinfo_by_number
+from common.utils.utils import filter_queryset_by_user, get_preinfo_by_number, send_test_email
+from payments.services import EPay
from preinfo.forms import PreinfoCreateForm, PreinfoEditForm
from preinfo.models import Preinfo
diff --git a/static/images/k-depot-logo.svg b/static/images/k-depot-logo.svg
index a20bee1..460f169 100644
--- a/static/images/k-depot-logo.svg
+++ b/static/images/k-depot-logo.svg
@@ -44,5 +44,5 @@
K-DepoT
- SAFE AND READY
+ SAFE AND READY
\ No newline at end of file
diff --git a/static/js/container_validation.js b/static/js/container_validation.js
index e69de29..d834d71 100644
--- a/static/js/container_validation.js
+++ b/static/js/container_validation.js
@@ -0,0 +1,35 @@
+function validateContainerNumber(input) {
+ const containerNumber = input.value.trim();
+ const feedbackElement = input.nextElementSibling;
+
+ if (!containerNumber) {
+ input.classList.remove('invalid-container', 'incomplete-container');
+ if (feedbackElement) feedbackElement.remove();
+ return;
+ }
+
+ if (containerNumber.length > 0 && containerNumber.length < 11) {
+ input.classList.remove('invalid-container');
+ input.classList.add('incomplete-container');
+ if (feedbackElement) feedbackElement.remove();
+ return;
+ }
+
+ fetch(`${validateContainerUrl}${containerNumber}/`)
+ .then(response => response.json())
+ .then(data => {
+ input.classList.remove('incomplete-container');
+ if (!data.valid) {
+ input.classList.add('invalid-container');
+ if (!feedbackElement) {
+ const feedback = document.createElement('div');
+ feedback.className = 'validation-feedback';
+ feedback.textContent = 'Invalid container number';
+ input.parentNode.insertBefore(feedback, input.nextSibling);
+ }
+ } else {
+ input.classList.remove('invalid-container');
+ if (feedbackElement) feedbackElement.remove();
+ }
+ });
+}
\ No newline at end of file
diff --git a/static/js/crud-list.js b/static/js/crud-list.js
index 229f91e..f38620c 100644
--- a/static/js/crud-list.js
+++ b/static/js/crud-list.js
@@ -12,12 +12,10 @@ document.addEventListener('DOMContentLoaded', function() {
table.addEventListener('click', function(e) {
const row = e.target.closest('.selectable-row');
if (!row) return;
-
const checkbox = row.querySelector('input[type="checkbox"]');
if (!checkbox) return;
if (selectionMode === 'single') {
- // Deselect all other rows
table.querySelectorAll('.selected-row').forEach(selectedRow => {
if (selectedRow !== row) {
selectedRow.classList.remove('selected-row');
@@ -25,22 +23,15 @@ document.addEventListener('DOMContentLoaded', function() {
}
});
}
-
- // Toggle current row
row.classList.toggle('selected-row');
checkbox.checked = !checkbox.checked;
-
- // Update buttons state
const selectedRows = table.querySelectorAll('.selected-row');
document.querySelectorAll('[data-requires-selection]').forEach(button => {
button.disabled = selectedRows.length === 0;
});
-
- // Handle edit/delete buttons from original crud-list.js
const objectId = row.dataset.id;
const editBtn = document.getElementById('editBtn');
const deleteBtn = document.getElementById('deleteBtn');
-
if (editBtn) {
editBtn.removeAttribute('disabled');
editBtn.href = editBtn.dataset.url?.replace('0', objectId);
@@ -54,19 +45,14 @@ document.addEventListener('DOMContentLoaded', function() {
toggleSelectAllBtn.addEventListener('click', function() {
const rows = table.querySelectorAll('.selectable-row');
allSelected = !allSelected;
-
rows.forEach(row => {
const checkbox = row.querySelector('input[type="checkbox"]');
checkbox.checked = allSelected;
row.classList.toggle('selected-row', allSelected);
});
-
- // Update other buttons state
document.querySelectorAll('[data-requires-selection]').forEach(button => {
button.disabled = !allSelected;
});
-
- // Update button text
this.textContent = allSelected ? 'Unselect All' : 'Select All';
});
diff --git a/static/styles/forms.css b/static/styles/forms.css
index d2fbbc4..0f87909 100644
--- a/static/styles/forms.css
+++ b/static/styles/forms.css
@@ -36,7 +36,23 @@ input[type="email"],
input[type="password"],
input[type="date"],
input[type="number"],
-select,
+select {
+ width: 100%;
+ padding: 8px;
+ margin-bottom: 0px;
+ border: 1px solid #a57d52;
+ border-radius: 4px;
+ box-sizing: border-box;
+
+ font-family: "Noto Sans", sans-serif;
+ font-optical-sizing: auto;
+ font-weight: 500;
+ font-style: normal;
+ font-size: 1.1rem;
+ font-variation-settings:
+ "wdth" 100;
+}
+
textarea {
width: 100%;
padding: 8px;
@@ -52,21 +68,28 @@ textarea {
font-size: 1.1rem;
font-variation-settings:
"wdth" 100;
+ height: 100px; /* Adjust height as needed */
+ min-height: 100px; /* Minimum height */
+ resize: vertical; /* Allow vertical resizing */
}
+
+
/* Submit button styling */
-button[type="submit"] {
- background-color: #a57d52;
- color: white;
+button[type="submit"],
+button.btn-primary,
+button.btn-secondary {
+ background-color: #F2E8DB;
padding: 10px 20px;
- border: none;
+ border: 1px solid #a57d52;
border-radius: 4px;
cursor: pointer;
- /*margin-top: 20px;*/
}
-button[type="submit"]:hover {
- background-color: #a67744;
+button[type="submit"]:hover,
+button.btn-primary:hover,
+button.btn-secondary:hover{
+ background-color: #EDDECB;
}
/* Error messages */
@@ -89,7 +112,7 @@ button[type="submit"]:hover {
.card {
margin-top: 24px;
- background-color: white;
+ background-color: #EDDECB;
border-radius: 8px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
}
@@ -278,4 +301,34 @@ button[type="submit"]:hover {
display: flex;
align-items: center;
width: 100%;
+}
+
+@media print {
+ /* Hide sidebar */
+ .sidebar {
+ display: none !important;
+ }
+
+ /* Make main content full width */
+ .main-content {
+ margin-left: 0 !important;
+ width: 100% !important;
+ }
+
+ /* Hide other non-printable elements if needed */
+ .no-print {
+ display: none !important;
+ }
+
+ /* Ensure white background and black text */
+ body {
+ background: white !important;
+ color: black !important;
+ }
+
+ /* Remove shadows and borders */
+ * {
+ box-shadow: none !important;
+ border: none !important;
+ }
}
\ No newline at end of file
diff --git a/static/styles/styles.css b/static/styles/styles.css
index e810ba8..a3d45d8 100644
--- a/static/styles/styles.css
+++ b/static/styles/styles.css
@@ -7,6 +7,7 @@ body {
align-items: center;
justify-content: center;
padding: 1rem;
+ background-color: #ead9cb;
}
.wave {
@@ -22,7 +23,7 @@ body {
}
.login-container {
- background-color: white;
+ background-color: #ead9cb;
border-radius: 0.75rem;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
overflow: hidden;
@@ -31,6 +32,8 @@ body {
display: flex;
flex-direction: column;
animation: fadeIn 0.8s ease-in-out;
+ margin: 2rem; /* Add margin to see the background */
+
}
@media (min-width: 768px) {
diff --git a/static/styles/tables.css b/static/styles/tables.css
index 77355c5..40f8a30 100644
--- a/static/styles/tables.css
+++ b/static/styles/tables.css
@@ -1,76 +1,3 @@
-/*.table {*/
-/* width: 100%;*/
-/* border-collapse: separate;*/
-/* border-spacing: 0;*/
-/* margin: 10px auto;*/
-/* border: 1px solid #a57d52;*/
-/* border-radius: 10px;*/
-/* overflow: hidden;*/
-/*}*/
-
-/*.table th {*/
-/* text-align: left;*/
-/* padding: 0.75rem;*/
-/* font-size: 0.75rem;*/
-/* text-transform: uppercase;*/
-/* font-weight: 600;*/
-/* border-bottom: 1px solid #a57d52;*/
-/* background-color: #F2E8DB;*/
-/*}*/
-
-/*.table td {*/
-/* padding: 0.75rem;*/
-/* font-size: 0.875rem;*/
-/* border-bottom: 1px solid #a57d52;*/
-/*}*/
-
-/*!* Last row shouldn't have bottom border *!*/
-/*.table tr:last-child td {*/
-/* border-bottom: none;*/
-/*}*/
-
-/*!* Selection styles *!*/
-/*.selectable-row {*/
-/* cursor: pointer;*/
-/*}*/
-
-/*.selected-row {*/
-/* background-color: rgba(165, 125, 82, 0.1);*/
-/*}*/
-
-/*.status-received {*/
-/* background-color: #e8f5e9;*/
-/* color: #2e7d32;*/
-/*}*/
-
-/*.status-pending {*/
-/* background-color: #fff3e0;*/
-/* color: #ef6c00;*/
-/*}*/
-
-/*.status-overdue {*/
-/* background-color: #ffebee;*/
-/* color: #c62828;*/
-/*}*/
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
table {
width: 100%;
border-collapse: collapse;
@@ -208,4 +135,33 @@ table tr:last-child td {
color: #990000;
}
+.status-ok {
+ color: #afddc0; /* green */
+ font-weight: 600;
+}
+.status-deleted {
+ color: #e49486; /* green */
+ font-weight: 600;
+}
+.status-inactive {
+ color: #b2b5b3; /* green */
+ font-weight: 600;
+}
+.invalid-container {
+ color: #df6161; /* red */
+ font-weight: 600;
+}
+.validation-feedback {
+ color: #dc2626;
+ font-size: 0.875rem;
+ margin-top: 0.25rem;
+}
+input.invalid-container {
+ border-color: #dc2626;
+ background-color: #fee2e2;
+}
+input.incomplete-container {
+ background-color: #fbf3a2; /* red */
+ border-color: #dc2626;
+}
diff --git a/templates/barrier/barrier-base.html b/templates/barrier/barrier-base.html
index 5cb7d30..7a4afa5 100644
--- a/templates/barrier/barrier-base.html
+++ b/templates/barrier/barrier-base.html
@@ -5,19 +5,66 @@
- Line Operator Dashboard | Container Depot
-
-
-
-
+ Barrier Dashboard | Container Depot
+{# #}
+{# #}
+{# #}
+
+
+
+
{% block content %}
-
{% endblock content %}
+
+ {% block extra_js %}
+
+
+
+ {% endblock %}