This commit is contained in:
ismail 2025-06-11 15:07:00 +03:00
parent 914b98a66b
commit 2a397c3a2b
28 changed files with 1865 additions and 7 deletions

View File

@ -14,8 +14,9 @@ from .mixins import AddClassMixin
from django_ledger.forms.invoice import (
InvoiceModelCreateForm as InvoiceModelCreateFormBase,
)
from django.forms.models import inlineformset_factory
from django_ledger.forms.bill import BillModelCreateForm as BillModelCreateFormBase
from django_ledger.forms.journal_entry import (
JournalEntryModelCreateForm as JournalEntryModelCreateFormBase,
)
@ -42,6 +43,8 @@ from .models import (
Activity,
Notes,
CarModel,
CarSerie,
CarTrim,
SaleOrder,
CarMake,
Customer,
@ -1899,3 +1902,29 @@ class StaffTaskForm(forms.ModelForm):
widgets = {
"due_date": forms.DateTimeInput(attrs={"type": "date"}),
}
#############################################################
class ItemInventoryForm(forms.Form):
make = forms.ModelChoiceField(
queryset=CarMake.objects.all(),
widget=forms.Select(attrs={"class": "form-control", "id": "make"}),
label=_("Make"),
)
model = forms.ModelChoiceField(
queryset=CarModel.objects.none(),
widget=forms.Select(attrs={"class": "form-control", "id": "model"}),
label=_("Model"),
)
serie = forms.ModelChoiceField(
queryset=CarSerie.objects.none(),
widget=forms.Select(attrs={"class": "form-control", "id": "serie"}),
label=_("Serie"),
)
trim = forms.ModelChoiceField(
queryset=CarTrim.objects.none(),
widget=forms.Select(attrs={"class": "form-control", "id": "trim"}),
label=_("Trim"),
)

View File

@ -0,0 +1,63 @@
# Generated by Django 5.2.1 on 2025-06-03 11:03
import django.db.models.deletion
import django.utils.timezone
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0014_alter_opportunity_amount'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='IntendedVehicle',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('year', models.PositiveIntegerField()),
('color', models.CharField(max_length=30)),
('engine', models.CharField(blank=True, max_length=50, null=True)),
('condition', models.CharField(choices=[('new', 'New'), ('used', 'Used'), ('certified', 'Certified Pre-Owned')], max_length=20)),
('expected_cost', models.DecimalField(decimal_places=2, max_digits=12)),
('make', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.carmake')),
('model', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.carmodel')),
('serie', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.carserie')),
('trim', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.cartrim')),
],
),
migrations.CreateModel(
name='PurchaseOrder',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('po_number', models.CharField(editable=False, max_length=50, unique=True)),
('status', models.CharField(choices=[('pending', 'Pending'), ('approved', 'Approved'), ('rejected', 'Rejected'), ('completed', 'Completed'), ('canceled', 'Canceled')], default='pending', max_length=20)),
('quantity', models.PositiveIntegerField(default=1)),
('total_cost', models.DecimalField(decimal_places=2, max_digits=12)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('expected_delivery_date', models.DateField(blank=True, null=True)),
('notes', models.TextField(blank=True, null=True)),
('approved_at', models.DateTimeField(blank=True, null=True)),
('approved_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='approved_orders', to=settings.AUTH_USER_MODEL)),
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_orders', to=settings.AUTH_USER_MODEL)),
('intended_vehicle', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.intendedvehicle')),
('supplier', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.vendor')),
],
),
migrations.CreateModel(
name='DeliveryReceipt',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('receipt_number', models.CharField(editable=False, max_length=50, unique=True)),
('received_at', models.DateTimeField(default=django.utils.timezone.now)),
('notes', models.TextField(blank=True, null=True)),
('car', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='inventory.car')),
('received_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
('purchase_order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.purchaseorder')),
],
),
]

View File

@ -0,0 +1,35 @@
# Generated by Django 5.2.1 on 2025-06-03 11:21
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0015_intendedvehicle_purchaseorder_deliveryreceipt'),
]
operations = [
migrations.CreateModel(
name='PurchaseOrderItem',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('quantity', models.PositiveIntegerField(default=1)),
('price', models.DecimalField(decimal_places=2, max_digits=10)),
('purchase_order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.purchaseorder')),
('vehicle', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.intendedvehicle')),
],
),
migrations.CreateModel(
name='Sale',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('sale_date', models.DateTimeField(auto_now_add=True)),
('selling_price', models.DecimalField(decimal_places=2, max_digits=12)),
('car', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.car')),
('customer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.customer')),
('salesperson', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='inventory.staff')),
],
),
]

View File

@ -0,0 +1,20 @@
# Generated by Django 5.2.1 on 2025-06-03 11:36
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0016_purchaseorderitem_sale'),
]
operations = [
migrations.AddField(
model_name='intendedvehicle',
name='purchase_order',
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='intended_vehicles', to='inventory.purchaseorder'),
preserve_default=False,
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 5.2.1 on 2025-06-03 13:56
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0017_intendedvehicle_purchase_order'),
]
operations = [
migrations.AddField(
model_name='intendedvehicle',
name='quantity',
field=models.PositiveIntegerField(default=1),
),
]

View File

@ -0,0 +1,33 @@
# Generated by Django 5.2.1 on 2025-06-03 17:42
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0018_intendedvehicle_quantity'),
]
operations = [
migrations.AlterField(
model_name='intendedvehicle',
name='make',
field=models.CharField(max_length=30),
),
migrations.AlterField(
model_name='intendedvehicle',
name='model',
field=models.CharField(max_length=30),
),
migrations.AlterField(
model_name='intendedvehicle',
name='serie',
field=models.CharField(blank=True, max_length=30, null=True),
),
migrations.AlterField(
model_name='intendedvehicle',
name='trim',
field=models.CharField(blank=True, max_length=30, null=True),
),
]

View File

@ -0,0 +1,68 @@
# Generated by Django 5.2.1 on 2025-06-04 13:33
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('inventory', '0019_alter_intendedvehicle_make_and_more'),
]
operations = [
migrations.RemoveField(
model_name='intendedvehicle',
name='purchase_order',
),
migrations.RemoveField(
model_name='purchaseorder',
name='intended_vehicle',
),
migrations.RemoveField(
model_name='purchaseorderitem',
name='vehicle',
),
migrations.RemoveField(
model_name='purchaseorder',
name='approved_by',
),
migrations.RemoveField(
model_name='purchaseorder',
name='created_by',
),
migrations.RemoveField(
model_name='purchaseorder',
name='supplier',
),
migrations.RemoveField(
model_name='purchaseorderitem',
name='purchase_order',
),
migrations.RemoveField(
model_name='sale',
name='car',
),
migrations.RemoveField(
model_name='sale',
name='customer',
),
migrations.RemoveField(
model_name='sale',
name='salesperson',
),
migrations.DeleteModel(
name='DeliveryReceipt',
),
migrations.DeleteModel(
name='IntendedVehicle',
),
migrations.DeleteModel(
name='PurchaseOrder',
),
migrations.DeleteModel(
name='PurchaseOrderItem',
),
migrations.DeleteModel(
name='Sale',
),
]

View File

@ -29,7 +29,7 @@ from django_ledger.models import (
EstimateModel,
InvoiceModel,
AccountModel,
EntityManagementModel,
EntityManagementModel
)
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
@ -2855,3 +2855,9 @@ class PaymentHistory(models.Model):
def is_successful(self):
return self.status == self.COMPLETED
######################################################################################################
######################################################################################################
######################################################################################################

View File

@ -363,4 +363,20 @@ def currency_format(value):
@register.filter
def filter_by_role(accounts, role_prefix):
return [account for account in accounts if account.role.startswith(role_prefix)]
return [account for account in accounts if account.role.startswith(role_prefix)]
@register.inclusion_tag('purchase_orders/tags/po_item_table.html', takes_context=True)
def po_item_table1(context, queryset):
return {
'entity_slug': context['entity_slug'],
'po_model': context['po_model'],
'po_item_list': queryset
}
@register.inclusion_tag('purchase_orders/includes/po_item_formset.html', takes_context=True)
def po_item_formset_table(context, po_model, itemtxs_formset):
return {
'entity_slug': context['view'].kwargs['entity_slug'],
'po_model': po_model,
'itemtxs_formset': itemtxs_formset,
}

View File

@ -693,7 +693,8 @@ path(
),
# Bills
path("items/bills/", views.BillListView.as_view(), name="bill_list"),
path("items/bills/create/", views.bill_create, name="bill_create"),
path("items/bills/create/", views.BillModelCreateViewView.as_view(), name="bill_create"),
# path("items/bills/create/", views.bill_create, name="bill_create"),
path(
"items/bills/<uuid:pk>/bill_detail/",
views.BillDetailView.as_view(),
@ -796,7 +797,7 @@ path(
#dashboard api
path('entity/<slug:entity_slug>/data/net-payables/',
views.PayableNetAPIView.as_view(),
name='entity-json-net-payables'),
name='entity-json-net-payables'),
path('entity/<slug:entity_slug>/data/net-receivables/',
views.ReceivableNetAPIView.as_view(),
name='entity-json-net-receivables'),
@ -808,8 +809,41 @@ path(
path('management/user_management/', views.user_management, name='user_management'),
path('management/<str:content_type>/<slug:slug>/activate_account/', views.activate_account, name='activate_account'),
path('management/<str:content_type>/<slug:slug>/permenant_delete_account/', views.permenant_delete_account, name='permenant_delete_account'),
]
#########
# Purchase Order
path('purchase_orders/', views.PurchaseOrderListView.as_view(), name='purchase_order_list'),
path('purchase_orders/new/', views.PurchaseOrderCreateView, name='purchase_order_create'),
path('purchase_orders/<uuid:pk>/detail/', views.PurchaseOrderDetailView.as_view(), name='purchase_order_detail'),
path('purchase_orders/<slug:entity_slug>/<uuid:po_pk>/update/', views.PurchaseOrderUpdateView.as_view(), name='purchase_order_update'),
path('purchase_orders/<slug:entity_slug>/update/<uuid:po_pk>/update-items/',
views.PurchaseOrderUpdateView.as_view(action_update_items=True),
name='purchase_order_update_items'),
path('purchase_orders/inventory_item/<uuid:pk>/create/', views.InventoryItemCreateView, name='inventory_item_create'),
path('purchase_orders/<uuid:po_pk>/inventory_items_filter/', views.inventory_items_filter, name='inventory_items_filter'),
path('purchase_orders/<slug:entity_slug>/delete/<uuid:po_pk>/',
views.PurchaseOrderModelDeleteView.as_view(),
name='po-delete'),
# Actions....
path('<slug:entity_slug>/action/<uuid:po_pk>/mark-as-draft/',
views.PurchaseOrderMarkAsDraftView.as_view(),
name='po-action-mark-as-draft'),
path('<slug:entity_slug>/action/<uuid:po_pk>/mark-as-review/',
views.PurchaseOrderMarkAsReviewView.as_view(),
name='po-action-mark-as-review'),
path('<slug:entity_slug>/action/<uuid:po_pk>/mark-as-approved/',
views.PurchaseOrderMarkAsApprovedView.as_view(),
name='po-action-mark-as-approved'),
path('<slug:entity_slug>/action/<uuid:po_pk>/mark-as-fulfilled/',
views.PurchaseOrderMarkAsFulfilledView.as_view(),
name='po-action-mark-as-fulfilled'),
path('<slug:entity_slug>/action/<uuid:po_pk>/mark-as-canceled/',
views.PurchaseOrderMarkAsCanceledView.as_view(),
name='po-action-mark-as-canceled'),
path('<slug:entity_slug>/action/<uuid:po_pk>/mark-as-void/',
views.PurchaseOrderMarkAsVoidView.as_view(),
name='po-action-mark-as-void'),
]
handler404 = "inventory.views.custom_page_not_found_view"
handler500 = "inventory.views.custom_error_view"

View File

@ -5,7 +5,6 @@ import logging
from datetime import datetime
from time import sleep
import numpy as np
# from rich import print
from random import randint
from decimal import Decimal
@ -20,6 +19,7 @@ from django.db import IntegrityError
from background_task.models import Task
from django.db.models.deletion import RestrictedError
from django.http.response import StreamingHttpResponse
from django.core.exceptions import ImproperlyConfigured, ValidationError
# Django
from django.db.models import Q
@ -83,6 +83,10 @@ from django_ledger.forms.bank_account import (
BankAccountCreateForm,
BankAccountUpdateForm,
)
from django_ledger.views.bill import (
BillModelCreateView
# BillModelUpdateView as BillModelUpdateViewBase
)
from django_ledger.forms.bill import (
ApprovedBillModelUpdateForm,
InReviewBillModelUpdateForm,
@ -92,6 +96,19 @@ from django_ledger.forms.invoice import (
ApprovedInvoiceModelUpdateForm,
PaidInvoiceModelUpdateForm,
)
from django_ledger.forms.item import (
InventoryItemCreateForm,
)
from django_ledger.forms.purchase_order import (PurchaseOrderModelCreateForm, BasePurchaseOrderModelUpdateForm,
DraftPurchaseOrderModelUpdateForm, ReviewPurchaseOrderModelUpdateForm,
ApprovedPurchaseOrderModelUpdateForm,
get_po_itemtxs_formset_class)
from django_ledger.views.purchase_order import (
PurchaseOrderModelDetailView as PurchaseOrderModelDetailViewBase,
PurchaseOrderModelUpdateView as PurchaseOrderModelUpdateViewBase,
BasePurchaseOrderActionActionView as BasePurchaseOrderActionActionViewBase,
PurchaseOrderModelDeleteView as PurchaseOrderModelDeleteViewBase
)
from django_ledger.models import (
ItemTransactionModel,
EntityModel,
@ -105,6 +122,7 @@ from django_ledger.models import (
ItemModel,
BillModel,
LedgerModel,
PurchaseOrderModel
)
from django_ledger.views.financial_statement import (
FiscalYearBalanceSheetView,
@ -6263,6 +6281,7 @@ def bill_mark_as_paid(request, pk):
return redirect("bill_detail", pk=bill.pk)
@login_required
@permission_required("django_ledger.add_billmodel", raise_exception=True)
def bill_create(request):
@ -8275,3 +8294,318 @@ def permenant_delete_account(request, content_type, slug):
return render(
request, "admin_management/permenant_delete_account.html", {"obj": obj}
)
#####################################################################
def PurchaseOrderCreateView(request):
dealer = get_user_type(request)
entity = dealer.entity
if(request.method == "POST"):
po = entity.create_purchase_order(po_title=request.POST.get("po_title"))
po.entity = entity
po.save()
messages.success(request, _("Purchase order created successfully"))
return redirect('purchase_order_detail', pk=po.pk)
form = PurchaseOrderModelCreateForm(entity_slug=entity.slug, user_model=entity.admin)
return render(request, "purchase_orders/po_form.html", {"form": form})
def InventoryItemCreateView(request,pk):
po = get_object_or_404(PurchaseOrderModel, pk=pk)
dealer = get_user_type(request)
entity = dealer.entity
coa = entity.get_default_coa()
inventory_accounts = entity.get_coa_all().get(name='ASSET_CA_INVENTORY')
if(request.method == "POST"):
make = request.POST.get("make")
model = request.POST.get("model")
serie = request.POST.get("serie")
trim = request.POST.get("trim")
make_name = models.CarMake.objects.get(pk=make)
model_name = models.CarModel.objects.get(pk=model)
serie_name = models.CarSerie.objects.get(pk=serie)
trim_name = models.CarTrim.objects.get(pk=trim)
inventory_name = f"{make_name.name} - {model_name.name} - {serie_name.name} - {trim_name.name}"
uom = entity.get_uom_all().get(name='Unit')
entity.create_item_inventory(
name=inventory_name,
uom_model=uom,
item_type=ItemModel.ITEM_TYPE_MATERIAL
)
messages.success(request, _("Inventory item created successfully"))
return redirect('purchase_order_detail', pk=po.pk)
class PurchaseOrderDetailView(PurchaseOrderModelDetailViewBase):
template_name = 'purchase_orders/po_detail.html'
context_object_name = 'po_model'
def get_queryset(self):
dealer = get_user_type(self.request)
self.queryset = PurchaseOrderModel.objects.for_entity(
entity_slug=dealer.entity.slug,
user_model=dealer.entity.admin
).select_related('entity', 'ce_model')
return super().get_queryset()
def get_context_data(self, **kwargs):
dealer = get_user_type(self.request)
context = super().get_context_data(**kwargs)
context['entity_slug'] = dealer.entity.slug
return context
def inventory_items_filter(request,po_pk):
dealer = get_user_type(request)
make = request.GET.get('make')
model = request.GET.get('model')
serie = request.GET.get('serie')
make_data = models.CarMake.objects.all()
model_data = models.CarModel.objects.none()
serie_data = models.CarSerie.objects.none()
trim_data = models.CarTrim.objects.none()
if make:
make = models.CarMake.objects.get(pk=make)
model_data = make.carmodel_set.all()
elif model:
model = models.CarModel.objects.get(pk=model)
serie_data = model.carserie_set.all()
elif serie:
serie = models.CarSerie.objects.get(pk=serie)
trim_data = serie.cartrim_set.all()
context = {
'make_data': make_data,
'model_data': model_data,
'serie_data': serie_data,
'trim_data': trim_data,
'inventory_accounts': dealer.entity.get_coa_accounts().filter(role="asset_ca_inv"),
'inventory_items': dealer.entity.get_items_inventory(),
'entity_slug': dealer.entity.slug,
'po_model': get_object_or_404(PurchaseOrderModel, pk=po_pk)
}
return render(request, "purchase_orders/po_detail.html", context)
# def PurchaseOrderDetailView(request, pk):
# po = get_object_or_404(PurchaseOrderModel, pk=pk)
# dealer = get_user_type(request)
# make = request.GET.get('make')
# model = request.GET.get('model')
# serie = request.GET.get('serie')
# make_data = models.CarMake.objects.all()
# model_data = models.CarModel.objects.none()
# serie_data = models.CarSerie.objects.none()
# trim_data = models.CarTrim.objects.none()
# if make:
# make = models.CarMake.objects.get(pk=make)
# model_data = make.carmodel_set.all()
# elif model:
# model = models.CarModel.objects.get(pk=model)
# serie_data = model.carserie_set.all()
# elif serie:
# serie = models.CarSerie.objects.get(pk=serie)
# trim_data = serie.cartrim_set.all()
# context = {
# 'make_data': make_data,
# 'model_data': model_data,
# 'serie_data': serie_data,
# 'trim_data': trim_data,
# 'po': po,
# 'inventory_accounts': dealer.entity.get_coa_accounts().filter(role="asset_ca_inv"),
# 'inventory_items': dealer.entity.get_items_inventory(),
# }
# return render(request, "purchase_orders/po_detail.html", context)
class PurchaseOrderListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
model = PurchaseOrderModel
context_object_name = "purchase_orders"
paginate_by = 30
template_name = "purchase_orders/po_list.html"
permission_required = ["inventory.view_carfinance"]
def get_queryset(self):
dealer = get_user_type(self.request)
entity = dealer.entity
return self.model.objects.filter(entity=entity)
def get_context_data(self, **kwargs):
dealer = get_user_type(self.request)
context = super().get_context_data(**kwargs)
context['entity_slug'] = dealer.entity.slug
return context
class PurchaseOrderUpdateView(PurchaseOrderModelUpdateViewBase):
template_name = 'purchase_orders/po_update.html'
context_object_name = 'po_model'
def get_context_data(self, **kwargs):
dealer = get_user_type(self.request)
context = super().get_context_data(**kwargs)
context['entity_slug'] = dealer.entity.slug
context['make_data'] = models.CarMake.objects.all()
context['model_data'] = models.CarModel.objects.none()
context['serie_data'] = models.CarSerie.objects.none()
context['trim_data'] = models.CarTrim.objects.none()
return context
def get_success_url(self):
return reverse('purchase_order_update',
kwargs={
'entity_slug': self.kwargs['entity_slug'],
'po_pk': self.kwargs['po_pk']
})
def get(self, request, entity_slug, po_pk, *args, **kwargs):
if self.action_update_items:
return HttpResponseRedirect(
redirect_to=reverse('purchase_order_update',
kwargs={
'entity_slug': entity_slug,
'po_pk': po_pk
})
)
return super(PurchaseOrderModelUpdateViewBase, self).get(request, entity_slug, po_pk, *args, **kwargs)
def post(self, request, entity_slug, *args, **kwargs):
dealer = get_user_type(self.request)
if self.action_update_items:
if not request.user.is_authenticated:
return HttpResponseForbidden()
queryset = self.get_queryset()
po_model: PurchaseOrderModel = self.get_object(queryset=queryset)
self.object = po_model
po_itemtxs_formset_class = get_po_itemtxs_formset_class(po_model)
itemtxs_formset = po_itemtxs_formset_class(request.POST,
user_model=dealer.entity.admin,
po_model=po_model,
entity_slug=entity_slug)
if itemtxs_formset.has_changed():
if itemtxs_formset.is_valid():
itemtxs_list = itemtxs_formset.save(commit=False)
create_bill_uuids = [
str(i['uuid'].uuid) for i in itemtxs_formset.cleaned_data if i and i['create_bill'] is True
]
if create_bill_uuids:
item_uuids = ','.join(create_bill_uuids)
redirect_url = reverse(
'django_ledger:bill-create-po',
kwargs={
'entity_slug': self.kwargs['entity_slug'],
'po_pk': po_model.uuid,
}
)
redirect_url += f'?item_uuids={item_uuids}'
return HttpResponseRedirect(redirect_url)
for itemtxs in itemtxs_list:
if not itemtxs.po_model_id:
itemtxs.po_model_id = po_model.uuid
itemtxs.clean()
itemtxs_list = itemtxs_formset.save()
po_model.update_state()
po_model.clean()
po_model.save(update_fields=['po_amount',
'po_amount_received',
'updated'])
# if valid get saved formset from DB
messages.add_message(request, messages.SUCCESS, 'PO items updated successfully.')
return self.render_to_response(context=self.get_context_data())
# if not valid, return formset with errors...
return self.render_to_response(context=self.get_context_data(itemtxs_formset=itemtxs_formset))
return super(PurchaseOrderUpdateView, self).post(request,entity_slug, *args, **kwargs)
def get_form(self, form_class=None):
po_model: PurchaseOrderModel = self.object
if po_model.is_draft():
return DraftPurchaseOrderModelUpdateForm(
entity_slug=self.kwargs['entity_slug'],
user_model=self.request.user,
**self.get_form_kwargs()
)
elif po_model.is_review():
return ReviewPurchaseOrderModelUpdateForm(
entity_slug=self.kwargs['entity_slug'],
user_model=self.request.user,
**self.get_form_kwargs()
)
elif po_model.is_approved():
return ApprovedPurchaseOrderModelUpdateForm(
entity_slug=self.kwargs['entity_slug'],
user_model=self.request.user,
**self.get_form_kwargs()
)
return BasePurchaseOrderModelUpdateForm(
entity_slug=self.kwargs['entity_slug'],
user_model=self.request.user,
**self.get_form_kwargs()
)
class BasePurchaseOrderActionActionView(BasePurchaseOrderActionActionViewBase):
def get_redirect_url(self, entity_slug, po_pk, *args, **kwargs):
return reverse('purchase_order_update',
kwargs={
'entity_slug': entity_slug,
'po_pk': po_pk
})
def get(self, request, *args, **kwargs):
kwargs['user_model'] = self.request.user
if not self.action_name:
raise ImproperlyConfigured('View attribute action_name is required.')
response = super(BasePurchaseOrderActionActionView, self).get(request, *args, **kwargs)
po_model: PurchaseOrderModel = self.get_object()
try:
getattr(po_model, self.action_name)(commit=self.commit, **kwargs)
except ValidationError as e:
messages.add_message(request,
message=e.message,
level=messages.ERROR,
)
return response
class PurchaseOrderModelDeleteView(PurchaseOrderModelDeleteViewBase):
template_name = 'purchase_orders/po_delete.html'
def get_success_url(self):
return reverse('purchase_order_list')
class PurchaseOrderMarkAsDraftView(BasePurchaseOrderActionActionView):
action_name = 'mark_as_draft'
class PurchaseOrderMarkAsReviewView(BasePurchaseOrderActionActionView):
action_name = 'mark_as_review'
class PurchaseOrderMarkAsApprovedView(BasePurchaseOrderActionActionView):
action_name = 'mark_as_approved'
class PurchaseOrderMarkAsFulfilledView(BasePurchaseOrderActionActionView):
action_name = 'mark_as_fulfilled'
class PurchaseOrderMarkAsCanceledView(BasePurchaseOrderActionActionView):
action_name = 'mark_as_canceled'
class PurchaseOrderMarkAsVoidView(BasePurchaseOrderActionActionView):
action_name = 'mark_as_void'
##############################bil
class BillModelCreateViewView(BillModelCreateView):
template_name = 'ledger/bills/bill_form.html'

View File

@ -0,0 +1,49 @@
{% extends 'base.html' %}
{% load i18n %}
{% load static %}
{% load django_ledger %}
{% block content %}
<div class="row justify-content-center">
<div class="col-lg-6">
<div class="card shadow-sm">
<div class="card-header bg-light py-3">
<h2 class="h4 text-center mb-0">{% trans 'Create Bill' %}</h2>
</div>
<form action="{{ form_action_url }}" method="post" id="djl-bill-model-create-form-id">
<div class="card-body">
{% csrf_token %}
{% if po_model %}
<div class="text-center mb-4">
<h3 class="h5">{% trans 'Bill for' %} {{ po_model.po_number }}</h3>
<p class="text-muted mb-3">{% trans 'Bill for' %} {{ po_model.po_title }}</p>
<div class="d-flex flex-column gap-2">
{% for itemtxs in po_itemtxs_qs %}
<span class="badge bg-secondary">{{ itemtxs }}</span>
{% endfor %}
</div>
</div>
{% endif %}
<div class="mb-4">
{{ form|add_class:"form-control" }}
</div>
</div>
<div class="card-footer bg-transparent">
<div class="d-grid gap-2">
<button type="submit"
id="djl-bill-create-button"
class="btn btn-primary btn-lg">{% trans 'Create' %}
</button>
<a href="{% url 'django_ledger:bill-list' entity_slug=view.kwargs.entity_slug %}"
id="djl-bill-create-back-button"
class="btn btn-outline-secondary">{% trans 'Cancel' %}</a>
</div>
</div>
</form>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,135 @@
{% extends "base.html" %}
{% block content %}
<div class="container mt-4">
<h2>{{ title }}</h2>
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<hr>
<h4>Items</h4>
{{ formset.management_form }}
<div id="formset-container">
{% for form in formset %}
<div class="item-form row g-3 mb-3 align-items-end">
{{ form.id }}
<div class="col-md-2">
{{ form.make.label_tag }}
{{ form.make }}
</div>
<div class="col-md-2">
{{ form.model.label_tag }}
{{ form.model }}
</div>
<div class="col-md-2">
{{ form.serie.label_tag }}
{{ form.serie }}
</div>
<div class="col-md-2">
{{ form.trim.label_tag }}
{{ form.trim }}
</div>
<div class="col-md-1">
{{ form.year.label_tag }}
{{ form.year }}
</div>
<div class="col-md-2">
{{ form.color.label_tag }}
{{ form.color }}
</div>
<div class="col-md-2">
{{ form.expected_cost.label_tag }}
{{ form.expected_cost }}
</div>
<div class="col-md-1">
<button type="button" class="btn btn-danger btn-sm remove-row">Remove</button>
</div>
</div>
{% endfor %}
</div>
<button type="button" id="add-item" class="btn btn-secondary btn-sm mb-3">+ Add Item</button>
<div class="mt-3">
<button type="submit" class="btn btn-primary">Save</button>
<a href="{% url 'purchase_order_list' %}" class="btn btn-secondary">Cancel</a>
</div>
</form>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
const container = document.getElementById('formset-container');
const addBtn = document.getElementById('add-item');
const totalForms = document.querySelector('#id_form-TOTAL_FORMS');
const emptyForm = document.querySelector('.empty-form').cloneNode(true);
function updateFormIndex(formRow, index) {
formRow.querySelectorAll('input, select').forEach(input => {
input.name = input.name.replace('__prefix__', index);
input.id = input.id.replace('__prefix__', index);
});
}
addBtn.addEventListener('click', function () {
const currentCount = parseInt(totalForms.value);
const newForm = emptyForm.cloneNode(true);
updateFormIndex(newForm, currentCount);
newForm.classList.remove('empty-form');
// Attach remove handler
const removeBtn = newForm.querySelector('.remove-row');
removeBtn.addEventListener('click', function () {
newForm.remove();
totalForms.value = parseInt(totalForms.value) - 1;
});
container.appendChild(newForm);
totalForms.value = currentCount + 1;
});
document.querySelectorAll('.remove-row').forEach(btn => {
btn.addEventListener('click', function () {
const row = this.closest('.item-form');
row.remove();
totalForms.value = parseInt(totalForms.value) - 1;
});
});
});
</script>
<!-- Empty form template -->
<div class="empty-form d-none">
<div class="item-form row g-3 mb-3 align-items-end">
<div class="col-md-2">
<select name="form-__prefix__-make" class="form-select make-select"></select>
</div>
<div class="col-md-2">
<select name="form-__prefix__-model" class="form-select model-select"></select>
</div>
<div class="col-md-2">
<select name="form-__prefix__-serie" class="form-select serie-select"></select>
</div>
<div class="col-md-2">
<select name="form-__prefix__-trim" class="form-select trim-select"></select>
</div>
<div class="col-md-1">
<input type="number" name="form-__prefix__-year" class="form-control">
</div>
<div class="col-md-2">
<input type="text" name="form-__prefix__-color" class="form-control">
</div>
<div class="col-md-2">
<input type="number" step="0.01" name="form-__prefix__-expected_cost" class="form-control">
</div>
<div class="col-md-1">
<button type="button" class="btn btn-danger btn-sm remove-row">Remove</button>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,237 @@
{% load i18n %}
{% load django_ledger %}
{% load widget_tweaks %}
<script>
document.addEventListener('DOMContentLoaded', function() {
window.showPOModal = function(title, actionUrl, buttonText) {
const modalEl = document.getElementById('POModal');
if (!modalEl) {
console.error('Modal element not found');
return;
}
const modal = bootstrap.Modal.getOrCreateInstance(modalEl);
document.getElementById('POModalTitle').textContent = title;
document.getElementById('POModalBody').innerHTML = `
<div class="d-flex justify-content-center gap-3 py-3">
<a class="btn btn-primary px-4" href="${actionUrl}">
<i class="fas fa-check-circle me-2"></i>${buttonText}
</a>
<button class="btn btn-outline-secondary" data-bs-dismiss="modal">
<i class="fas fa-times me-2"></i>Cancel
</button>
</div>
`;
modal.show();
};
});
</script>
{% if not create_po %}
{% if style == 'po-detail' %}
<div class="card shadow-sm border-0 mb-4">
<div class="card-header bg-light">
<div class="d-flex align-items-center">
<span class="me-3 text-primary">
{% icon 'uil:bill' 36 %}
</span>
<h2 class="h3 mb-0">
{{ po_model.po_number }}
</h2>
</div>
</div>
<div class="card-body">
<h3 class="h4 mb-4">
<span class="badge bg-{% if po_model.is_draft %}secondary{% elif po_model.is_approved %}success{% elif po_model.is_fulfilled %}primary{% else %}warning{% endif %}">
{{ po_model.get_po_status_display }}
</span>
</h3>
{# Display PO Contract Information #}
{% if po_model.is_contract_bound %}
<div class="alert alert-info d-flex align-items-center mb-4">
<i class="fas fa-file-contract me-3 fs-4"></i>
<div>
<h4 class="h6 mb-1">{% trans 'Contract' %}</h4>
<p class="mb-0">{{ po_model.ce_model.estimate_number }}</p>
</div>
<a href="{% url 'django_ledger:customer-estimate-detail' entity_slug=view.kwargs.entity_slug ce_pk=po_model.ce_model_id %}"
class="btn btn-sm btn-outline-info ms-auto">
{% trans 'View Contract' %}
</a>
</div>
{% endif %}
<div class="row g-3 mb-4">
{% if po_model.is_draft %}
<div class="col-md-6">
<div class="card border-light h-100">
<div class="card-body">
<h5 class="h6 text-muted mb-2">{% trans 'Draft Date' %}</h5>
<p class="h5">{{ po_model.date_draft|date }}</p>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card border-light h-100">
<div class="card-body">
<h5 class="h6 text-muted mb-2">{% trans 'Purchase Order Amount' %}</h5>
<p class="h5">{% currency_symbol %}{{ po_model.po_amount|currency_format }}</p>
</div>
</div>
</div>
{% endif %}
{% if po_model.is_review %}
<div class="col-md-6">
<div class="card border-light h-100">
<div class="card-body">
<h5 class="h6 text-muted mb-2">{% trans 'Review Date' %}</h5>
<p class="h5">{{ po_model.date_in_review|date }}</p>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card border-light h-100">
<div class="card-body">
<h5 class="h6 text-muted mb-2">{% trans 'Purchase Order Amount' %}</h5>
<p class="h5">{% currency_symbol %}{{ po_model.po_amount|currency_format }}</p>
</div>
</div>
</div>
{% endif %}
{% if po_model.is_approved %}
<div class="col-md-4">
<div class="card border-light h-100">
<div class="card-body">
<h5 class="h6 text-muted mb-2">{% trans 'Approved Date' %}</h5>
<p class="h5">{{ po_model.date_approved|date }}</p>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card border-light h-100">
<div class="card-body">
<h5 class="h6 text-muted mb-2">{% trans 'PO Amount' %}</h5>
<p class="h5">{% currency_symbol %}{{ po_model.po_amount|currency_format }}</p>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card border-light h-100">
<div class="card-body">
<h5 class="h6 text-muted mb-2">{% trans 'Received Amount' %}</h5>
<p class="h5 text-success">{% currency_symbol %}{{ po_model.po_amount_received|currency_format }}</p>
</div>
</div>
</div>
{% endif %}
{% if po_model.is_fulfilled %}
<div class="col-md-6">
<div class="card border-light h-100">
<div class="card-body">
<h5 class="h6 text-muted mb-2">{% trans 'Fulfilled Date' %}</h5>
<p class="h5">{{ po_model.date_fulfilled|date }}</p>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card border-light h-100">
<div class="card-body">
<h5 class="h6 text-muted mb-2">{% trans 'PO Amount' %}</h5>
<div class="d-flex align-items-center">
<p class="h5 mb-0 me-2">{% currency_symbol %}{{ po_model.po_amount|currency_format }}</p>
<span class="badge bg-success">
<i class="fas fa-check-circle me-1"></i>{% trans 'Fulfilled' %}
</span>
</div>
</div>
</div>
</div>
{% endif %}
</div>
</div>
<div class="card-footer bg-light">
<div class="d-flex flex-wrap gap-2 justify-content-between">
<a href="{% url 'purchase_order_update' entity_slug=entity_slug po_pk=po_model.pk %}"
class="btn btn-outline-primary">
<i class="fas fa-edit me-2"></i>{% trans 'Update' %}
</a>
<div class="d-flex flex-wrap gap-2">
{# Status Action Buttons #}
{% if po_model.can_draft %}
<button class="btn btn-outline-secondary"
onclick="showPOModal('Draft PO', '{% url 'po-action-mark-as-draft' entity_slug po_model.pk %}', 'Mark As Draft')">
<i class="fas fa-file me-2"></i>{% trans 'Mark as Draft' %}
</button>
{% endif %}
{% if po_model.can_review %}
<button class="btn btn-outline-warning"
onclick="showPOModal('Review PO', '{% url 'po-action-mark-as-review' entity_slug po_model.pk %}', 'Mark As Review')">
<i class="fas fa-search me-2"></i>{% trans 'Mark as Review' %}
</button>
{% endif %}
{% if po_model.can_approve %}
<button class="btn btn-outline-success"
onclick="showPOModal('Approve PO', '{% url 'po-action-mark-as-approved' entity_slug po_model.pk %}', 'Mark As Approved')">
<i class="fas fa-check-circle me-2"></i>{% trans 'Mark as Approved' %}
</button>
{% endif %}
{% if po_model.can_fulfill %}
<button class="btn btn-outline-primary"
onclick="showPOModal('Fulfill PO', '{% url 'po-action-mark-as-fulfilled' entity_slug po_model.pk %}', 'Mark As Fulfilled')">
<i class="fas fa-truck me-2"></i>{% trans 'Mark as Fulfilled' %}
</button>
<button class="btn btn-outline-danger"
onclick="showPOModal('Cancel PO', '{% url 'po-action-mark-as-canceled' entity_slug po_model.pk %}', 'Mark As Cancelled')">
<i class="fas fa-ban me-2"></i>{% trans 'Cancel' %}
</button>
{% endif %}
{# Danger Action Buttons #}
{% if po_model.can_delete %}
<a class="btn btn-outline-danger" href="{% url 'po-delete' entity_slug po_model.pk %}">
<i class="fas fa-ban me-2"></i>{% trans 'Delete' %}
</a>
{% endif %}
{% if po_model.can_void %}
<button class="btn btn-outline-danger"
onclick="djLedger.toggleModal('{{ po_model.get_mark_as_void_html_id }}')">
<i class="fas fa-times-circle me-2"></i>{% trans 'Void' %}
</button>
{% modal_action_v2 bill po_model.get_mark_as_void_url po_model.get_mark_as_void_message po_model.get_mark_as_void_html_id %}
{% endif %}
{% if po_model.can_cancel %}
<button class="btn btn-outline-danger"
onclick="djLedger.toggleModal('{{ po_model.get_mark_as_canceled_html_id }}')">
<i class="fas fa-window-close me-2"></i>{% trans 'Cancel' %}
</button>
{% modal_action_v2 bill po_model.get_mark_as_canceled_url po_model.get_mark_as_canceled_message po_model.get_mark_as_canceled_html_id %}
{% endif %}
</div>
</div>
</div>
</div>
{% endif %}
{% else %}
<div class="card border-0 shadow-sm text-center py-5">
<div class="card-body">
<a href="{% url 'django_ledger:po-create' entity_slug=entity_slug %}" class="text-decoration-none">
<span class="text-muted mb-3 d-inline-block">{% icon "ic:baseline-add-circle-outline" 48 %}</span>
<h3 class="h4 text-muted">{% trans 'New Purchase Order' %}</h3>
</a>
</div>
</div>
{% endif %}

View File

@ -0,0 +1,21 @@
<form action="{% url 'inventory_item_create' po_model.pk %}" method="post">
{% csrf_token %}
{% include "purchase_orders/partials/po-select.html" with name="make" target="model" data=make_data pk=po_model.pk %}
{% include "purchase_orders/partials/po-select.html" with name="model" target="serie" data=model_data pk=po_model.pk %}
{% include "purchase_orders/partials/po-select.html" with name="serie" target="trim" data=serie_data pk=po_model.pk %}
{% include "purchase_orders/partials/po-select.html" with name="trim" target="none" data=trim_data pk=po_model.pk %}
<div class="form-group">
<label for="account">Account</label>
<select class="form-control" name="account" id="account">
{% for account in inventory_accounts %}
<option value="{{ account.pk }}">{{ account }}"></option>
{% endfor %}
</select>
</div>
<div class="form-group">
<label for="quantity">Quantity</label>
<input type="number" class="form-control" id="quantity" name="quantity" required>
</div>
<button type="submit" class="btn btn-primary">Add New Item To Inventory</button>
</form>

View File

@ -0,0 +1,14 @@
<!-- Modal Template (keep this in your base template) -->
<div class="modal fade" id="POModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-md">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="POModalTitle"></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body" id="POModalBody">
<!-- Content will be inserted here by JavaScript -->
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,101 @@
{% load trans from i18n %}
{% load django_ledger %}
{% load widget_tweaks %}
<form action="{% url 'purchase_order_update_items' entity_slug=entity_slug po_pk=po_model.uuid %}"
method="post">
<div class="row g-3">
<div class="col-12">
<h1 class="display-4 mb-4">{% trans 'Purchase Order Items' %}</h1>
</div>
<div class="col-12">
<div class="table-responsive">
{% csrf_token %}
{{ itemtxs_formset.non_form_errors }}
{{ itemtxs_formset.management_form }}
<table class="table table-hover table-bordered">
<thead class="table-light">
<tr>
<th>{% trans 'Item' %}</th>
<th>{% trans 'Unit Cost' %}</th>
<th>{% trans 'Quantity' %}</th>
<th>{% trans 'Unit' %}</th>
<th class="text-end">{% trans 'Amount' %}</th>
<th>{% trans 'Status' %}</th>
{% if itemtxs_formset.can_delete %}
<th>{% trans 'Delete' %}</th>
{% endif %}
<th>{% trans 'Create Bill' %}</th>
<th>{% trans 'Bill Paid?' %}</th>
</tr>
</thead>
<tbody>
{% for f in itemtxs_formset %}
<tr>
<td>
{% for hidden_field in f.hidden_fields %}
{{ hidden_field }}
{% endfor %}
{{ f.item_model|add_class:"form-control" }}
{% if f.errors %}
<div class="text-danger small">{{ f.errors }}</div>
{% endif %}
</td>
<td id="{{ f.instance.html_id_unit_cost }}">{{ f.po_unit_cost|add_class:"form-control" }}</td>
<td id="{{ f.instance.html_id_quantity }}">{{ f.po_quantity|add_class:"form-control" }}</td>
<td>{{ f.entity_unit|add_class:"form-control" }}</td>
<td class="text-end" id="{{ f.instance.html_id_total_amount }}">
{% currency_symbol %}{{ f.instance.po_total_amount | currency_format }}</td>
<td>{{ f.po_item_status|add_class:"form-control" }}</td>
{% if itemtxs_formset.can_delete %}
<td class="text-center">
{{ f.DELETE|add_class:"form-check-input" }}
</td>
{% endif %}
<td class="text-center">
{% if f.instance.can_create_bill %}
{{ f.create_bill|add_class:"form-check-input" }}
{% elif f.instance.bill_model %}
<a class="btn btn-sm btn-outline-secondary"
href="{% url 'django_ledger:bill-detail' entity_slug=entity_slug bill_pk=f.instance.bill_model_id %}">
{% trans 'View Bill' %}
</a>
{% endif %}
</td>
<td class="text-center">
{% if f.instance.bill_model %}
{% if f.instance.bill_model.is_paid %}
<span class="text-success">{% icon 'bi:check-circle-fill' 24 %}</span>
{% else %}
<span class="text-danger">{% icon 'clarity:no-access-solid' 24 %}</span>
{% endif %}
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
<tfoot>
<tr>
<th></th>
<th></th>
<th></th>
<th class="text-end">{% trans 'Total' %}</th>
<th class="text-end">{% currency_symbol %}{{ po_model.po_amount | currency_format }}</th>
<th></th>
{% if itemtxs_formset.can_delete %}
<th></th>
{% endif %}
<th></th>
<th></th>
</tr>
</tfoot>
</table>
</div>
</div>
<div class="col-12">
<button class="btn btn-primary">{% trans 'Save' %}</button>
</div>
</div>
</form>

View File

@ -0,0 +1,50 @@
{% load django_ledger %}
{% load i18n %}
<div class="table-container">
<table class="table is-fullwidth is-narrow is-striped is-bordered django-ledger-table-bottom-margin-75">
<thead>
<tr>
<th>PO Number</th>
<th>Description</th>
<th>Status Date</th>
<th>PO Status</th>
<th>PO Amount</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for po in po_list %}
<tr>
<td>{% if po.po_number %}{{ po.po_number }}{% endif %}</td>
<td>{{ po.po_title }}</td>
<td>{{ po.get_status_action_date }}</td>
<td>{{ po.get_po_status_display }}</td>
<td>{% currency_symbol %}{{ po.po_amount | currency_format }}</td>
<td class="has-text-centered">
<div class="dropdown is-right is-hoverable" id="bill-action-{{ po.uuid }}">
<div class="dropdown-trigger">
<button class="button is-small is-rounded is-outlined is-dark"
aria-haspopup="true"
aria-controls="dropdown-menu">
<span>Actions</span>
<span class="icon is-small">{% icon 'bi:arrow-down' 24 %}</span>
</button>
</div>
<div class="dropdown-menu" id="dropdown-menu-{{ po.uuid }}" role="menu">
<div class="dropdown-content">
<a href="{% url 'django_ledger:po-detail' entity_slug=entity_slug po_pk=po.uuid %}"
class="dropdown-item has-text-success">Details</a>
<a href="{% url 'django_ledger:po-delete' entity_slug=entity_slug po_pk=po.uuid %}"
class="dropdown-item has-text-weight-bold has-text-danger">{% trans ' Delete' %}</a>
</div>
</div>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>

View File

@ -0,0 +1,53 @@
{% extends "base.html" %}
{% load i18n %}
{% load crispy_forms_filters %}
{% block title %}
{# Check if an 'object' exists in the context #}
{% if object %}
{% trans 'Update Inventory Item'%}
{% else %}
{% trans 'Add New Inventory Item'%}
{% endif %}
{% endblock %}
{% block content %}
<div class="row">
<div class="row">
<div class="col-xl-9">
<div class="d-sm-flex justify-content-between">
<h3 class="mb-3">
{% if vendor.created %}
<!--<i class="bi bi-pencil-square"></i>-->
{{ _("Edit Inventory Item") }}
{% else %}
<!--<i class="bi bi-person-plus"></i> -->
{{ _("Add New Inventory Item") }}
{% endif %}
</h3>
</div>
</div>
</div>
<div class="row">
<div class="col-xl-9">
<form class="row g-3 mb-9" method="post" class="form" enctype="multipart/form-data" novalidate >
{% csrf_token %}
{{ redirect_field }}
{{ form|crispy }}
{% for error in form.errors %}
<div class="text-danger">{{ error }}</div>
{% endfor %}
<div class="d-flex justify-content-start">
<button class="btn btn-sm btn-success me-2" type="submit"><i class="fa-solid fa-floppy-disk me-1"></i>
<!--<i class="bi bi-save"></i> -->
{{ _("Save") }}
</button>
<a href="{{request.META.HTTP_REFERER}}" class="btn btn-sm btn-danger"><i class="fa-solid fa-ban me-1"></i>{% trans "Cancel" %}</a>
</div>
</form>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,13 @@
<div class="form-group" id="form-{{name}}">
<label for="{{name}}">{{name|capfirst}}</label>
<select class="form-control"
name="{{name}}" id="{{name}}"
hx-get="{% url 'inventory_items_filter' po_pk=po_model.pk %}"
hx-target="#form-{{target}}"
hx-select="#form-{{target}}"
hx-swap="outerHTML">
{% for item in data %}
<option value="{{ item.pk }}">{{ item.name }}</option>
{% endfor %}
</select>
</div>

View File

@ -0,0 +1,19 @@
<!-- templates/purchase_orders/po_confirm_delete.html -->
{% extends "base.html" %}
{% block title %}
Confirm Delete - {{ block.super }}
{% endblock %}
{% block content %}
<div class="container mt-4">
<h2>Confirm Deletion</h2>
<p>Are you sure you want to delete the Purchase Order <strong>"{{ po.po_number }}"</strong>?</p>
<form method="post">
{% csrf_token %}
<button type="submit" class="btn btn-danger">Yes, Delete</button>
<a href="{% url 'purchase_order_detail' po.id %}" class="btn btn-secondary">Cancel</a>
</form>
</div>
{% endblock %}

View File

@ -0,0 +1,30 @@
{% extends 'base.html' %}
{% load i18n %}
{% load static %}
{% load django_ledger %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-6">
<form action="{% url 'po-delete' entity_slug=view.kwargs.entity_slug po_pk=po_model.uuid %}"
method="post">
{% csrf_token %}
<div class="card shadow">
<div class="card-body text-center py-4">
<h2 class="card-title h3 fw-light mb-4">Are you sure you want to delete
Purchase Order {{ po_model.po_number }}?</h2>
<p class="card-text text-muted mb-4">All transactions associated with this Purchase Order will be deleted.
If you want to void the PO instead, <a href="{% url 'django_ledger:po-detail' entity_slug=view.kwargs.entity_slug po_pk=po_model.uuid %}" class="text-decoration-none">click here</a></p>
<div class="d-flex justify-content-center gap-3 mt-4">
<a href="{% url 'purchase_order_update' entity_slug=view.kwargs.entity_slug po_pk=po_model.uuid %}"
class="btn btn-primary px-4">{% trans 'Go Back' %}</a>
<button type="submit" class="btn btn-danger px-4">{% trans 'Delete' %}</button>
</div>
</div>
</div>
</form>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,100 @@
{% extends 'base.html' %}
{% load trans from i18n %}
{% load static %}
{% load custom_filters %}
{% load django_ledger %}
{% block content %}
<div class="container-fluid mt-4">
<div class="row g-4">
<!-- Left Sidebar -->
<div class="col-lg-4">
<div class="d-flex flex-column gap-3">
<!-- PO Card -->
<div class="card">
<div class="card-body">
{% include 'purchase_orders/includes/card_po.html' with po_model=po_model entity_slug=entity_slug style='po-detail' %}
</div>
</div>
<!-- PO List Button -->
<a class="btn btn-outline-primary w-100 py-2"
href="{% url 'purchase_order_list' %}">
<i class="fas fa-list me-2"></i>{% trans 'PO List' %}
</a>
</div>
</div>
<!-- Main Content -->
<div class="col-lg-8">
<!-- Stats Cards -->
<div class="card mb-4">
<div class="card-body">
<div class="row text-center">
<div class="col-md-6 border-end">
<div class="p-3">
<h6 class="text-muted mb-2">{% trans 'PO Amount' %}</h6>
<h3 class="fw-light mb-0">
{% currency_symbol %}{{ po_model.po_amount | absolute | currency_format }}
</h3>
</div>
</div>
<div class="col-md-6">
<div class="p-3">
<h6 class="text-muted mb-2">{% trans 'Amount Received' %}</h6>
<h3 class="fw-light mb-0 text-success">
{% currency_symbol %}{{ po_model.po_amount_received | currency_format }}
</h3>
</div>
</div>
</div>
</div>
</div>
<!-- PO Details -->
<div class="card">
<div class="card-body">
<h3 class="h4 fw-light mb-4">{{ po_model.po_title }}</h3>
<!-- PO Items Table -->
<div class="table-responsive">
{% po_item_table1 po_items %}
</div>
</div>
</div>
</div>
</div>
</div>
{% include "purchase_orders/includes/mark_as.html" %}
{% endblock %}
{% block customJS %}
<script>
document.addEventListener('DOMContentLoaded', function() {
window.showPOModal = function(title, actionUrl, buttonText) {
const modalEl = document.getElementById('POModal');
if (!modalEl) {
console.error('Modal element not found');
return;
}
const modal = bootstrap.Modal.getOrCreateInstance(modalEl);
document.getElementById('POModalTitle').textContent = title;
document.getElementById('POModalBody').innerHTML = `
<div class="d-flex justify-content-center gap-3">
<a class="btn btn-primary px-4" href="${actionUrl}">
<i class="fas fa-check-circle me-2"></i>${buttonText}
</a>
<button class="btn btn-outline-secondary" data-bs-dismiss="modal">
<i class="fas fa-times me-2"></i>Cancel
</button>
</div>
`;
modal.show();
};
});
</script>
{% endblock customJS %}

View File

@ -0,0 +1,95 @@
<!-- templates/purchase_orders/po_detail.html -->
{% extends "base.html" %}
{% load static i18n %}
{% block title %}
{{ po.po_number }} - Purchase Order - {{ block.super }}
{% endblock %}
{% block content %}
<div class="container mt-4">
<h2>Purchase Order: {{ po.po_number }}</h2>
<p><strong>Status:</strong>
<span class="">
{{ po.po_status }}
</span>
</p>
<div class="row">
<div class="col-md-6">
<dl class="row">
<dt class="col-sm-4">Supplier</dt>
<dd class="col-sm-8">{{ po.supplier.name }}</dd>
<dt class="col-sm-4">Created At</dt>
<dd class="col-sm-8">{{ po.created|date:"M d, Y H:i" }}</dd>
<dt class="col-sm-4">Total Amount</dt>
<dd class="col-sm-8">${{ po.total_amount|floatformat:2 }}</dd>
</dl>
</div>
</div>
<h4 class="mt-4">Ordered Items</h4>
<table class="table table-bordered">
<thead>
<tr>
<th>Item Number</th>
<th>Item</th>
<th>Unit of Measure</th>
<th>Account</th>
</tr>
</thead>
<tbody>
{% for item in inventory_items %}
<tr>
<td>{{ item.item_number }}</td>
<td>{{ item.name }}</td>
<td>${{ item.uom.name }}</td>
<td>${{ item.inventory_account }}</td>
</tr>
{% empty %}
<tr><td colspan="4" class="text-center">No items found.</td></tr>
{% endfor %}
</tbody>
</table>
<div class="mt-3">
<button class="btn btn-phoenix-primary" data-bs-toggle="modal" data-bs-target="#POModal"><span class="d-none d-sm-inline-block"><i class="fa-solid fa-receipt"></i> {% trans 'View Purchase Order' %}</span></button>
<a href="{% url 'purchase_order_list' %}" class="btn btn-secondary">Back to List</a>
</div>
</div>
<div class="modal fade" id="POModal" data-bs-keyboard="true" tabindex="-1" aria-labelledby="POModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header justify-content-between align-items-start gap-5 px-4 pt-4 pb-3 border-0">
<h4 class="mb-0 me-2 text-primary"><i class="fas text-info ms-2"></i></h4>
<button class="btn p-0 text-body-quaternary fs-6" data-bs-dismiss="modal" aria-label="Close"><span class="fas fa-times"></span></button>
</div>
<div class="modal-body p-4">
<form action="{% url 'inventory_item_create' po.pk %}" method="post">
{% csrf_token %}
{% include "purchase_orders/partials/po-select.html" with name="make" target="model" data=make_data pk=po.pk %}
{% include "purchase_orders/partials/po-select.html" with name="model" target="serie" data=model_data pk=po.pk %}
{% include "purchase_orders/partials/po-select.html" with name="serie" target="trim" data=serie_data pk=po.pk %}
{% include "purchase_orders/partials/po-select.html" with name="trim" target="none" data=trim_data pk=po.pk %}
<div class="form-group">
<label for="account">Account</label>
<select class="form-control" name="account" id="account">
{% for account in inventory_accounts %}
<option value="{{ account.pk }}">{{ account }}"></option>
{% endfor %}
</select>
</div>
<div class="form-group">
<label for="quantity">Quantity</label>
<input type="number" class="form-control" id="quantity" name="quantity" required>
</div>
<button type="submit" class="btn btn-primary">Add New Item To Inventory</button>
</form>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,53 @@
{% extends "base.html" %}
{% load i18n %}
{% load crispy_forms_filters %}
{% block title %}
{# Check if an 'object' exists in the context #}
{% if object %}
{% trans 'Update Vendor'%}
{% else %}
{% trans 'Add New Vendor'%}
{% endif %}
{% endblock %}
{% block content %}
<div class="row">
<div class="row">
<div class="col-xl-9">
<div class="d-sm-flex justify-content-between">
<h3 class="mb-3">
{% if vendor.created %}
<!--<i class="bi bi-pencil-square"></i>-->
{{ _("Edit Purchase Order") }}
{% else %}
<!--<i class="bi bi-person-plus"></i> -->
{{ _("Add New Purchase Order") }}
{% endif %}
</h3>
</div>
</div>
</div>
<div class="row">
<div class="col-xl-9">
<form class="row g-3 mb-9" method="post" class="form" enctype="multipart/form-data" novalidate >
{% csrf_token %}
{{ redirect_field }}
{{ form|crispy }}
{% for error in form.errors %}
<div class="text-danger">{{ error }}</div>
{% endfor %}
<div class="d-flex justify-content-start">
<button class="btn btn-sm btn-success me-2" type="submit"><i class="fa-solid fa-floppy-disk me-1"></i>
<!--<i class="bi bi-save"></i> -->
{{ _("Save") }}
</button>
<a href="{{request.META.HTTP_REFERER}}" class="btn btn-sm btn-danger"><i class="fa-solid fa-ban me-1"></i>{% trans "Cancel" %}</a>
</div>
</form>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,74 @@
<!-- po_list.html -->
{% extends "base.html" %}
{% block title %}
Purchase Orders - {{ block.super }}
{% endblock %}
{% block content %}
<div class="container mt-4">
<h2 class="mb-4">Purchase Orders</h2>
<!-- Success Message -->
{% if messages %}
{% for message in messages %}
<div class="alert alert-success">{{ message }}</div>
{% endfor %}
{% endif %}
<!-- Add New PO Button -->
<div class="d-flex justify-content-between align-items-center mb-3">
<a href="{% url 'purchase_order_create' %}" class="btn btn-primary">
<i class="bi bi-plus-lg"></i> Create New PO
</a>
</div>
<!-- PO Table -->
<div class="table-responsive">
<table class="table table-striped table-hover align-middle">
<thead class="table-dark">
<tr>
<th scope="col">PO Number</th>
<th scope="col">Description</th>
<th scope="col">Status</th>
<th scope="col">Created At</th>
<th scope="col">Actions</th>
</tr>
</thead>
<tbody>
{% if purchase_orders %}
{% for po in purchase_orders %}
<tr>
<td>{{ po.po_number }}</td>
<td>{{ po.po_title }}</td>
<td>
<span class="">
{{ po.po_status|capfirst }}
</span>
</td>
<td>{{ po.created|date:"M d, Y" }}</td>
<td>
<a href="{% url 'purchase_order_detail' po.pk %}" class="btn btn-sm btn-info me-1">
View
</a>
{% comment %} <a href="{% url 'purchase_order_detail' po.id %}" class="btn btn-sm btn-info me-1">
View
</a>
<a href="{% url 'purchase_order_update' po.id %}" class="btn btn-sm btn-warning me-1">
Edit
</a>
<a href="{% url 'purchase_order_delete' po.id %}" class="btn btn-sm btn-danger">
Delete
</a> {% endcomment %}
</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="6" class="text-center">No purchase orders found.</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,109 @@
{% extends 'base.html' %}
{% load i18n %}
{% load static %}
{% load django_ledger %}
{% load custom_filters %}
{% load widget_tweaks %}
{% load crispy_forms_tags %}
{% block content %}
<div class="container-fluid py-4">
<div class="row g-4">
<div class="col-12">
{% include 'purchase_orders/includes/card_po.html' with style='po-detail' po_model=po_model entity_slug=entity_slug %}
</div>
<div class="col-12">
<div class="card shadow-sm">
<div class="card-body">
<form action="{% url 'purchase_order_update' entity_slug=view.kwargs.entity_slug po_pk=po_model.uuid %}"
method="post">
<div class="row g-3">
<div class="col-12">
{% csrf_token %}
{{ form|crispy }}
<button type="submit"
class="btn btn-primary w-100 my-2">{% trans 'Save PO' %}
</button>
<a href="{% url 'purchase_order_detail' po_model.uuid %}"
class="btn btn-dark w-100 my-2">{% trans 'Back to PO Detail' %}</a>
<a href="{% url 'purchase_order_list' %}"
class="btn btn-info w-100 my-2">{% trans 'PO List' %}</a>
</div>
</div>
</form>
</div>
</div>
</div>
{% if po_model.is_contract_bound %}
<div class="col-12">
<div class="card shadow-sm">
<div class="card-header bg-light">
<h3 class="h4 mb-0">
{% trans 'Contract' %} {{ po_model.ce_model.estimate_number }}
</h3>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-bordered">
<thead>
<tr>
<th>{% trans 'Item' %}</th>
<th>{% trans 'Quantity' %}</th>
<th>{% trans 'Avg Unit Price' %}</th>
<th>{% trans 'Total Contracted Cost' %}</th>
</tr>
</thead>
<tfoot>
<tr>
<th></th>
<th></th>
<th></th>
<th>{% currency_symbol %}{{ ce_cost_estimate__sum | currency_format }}</th>
</tr>
</tfoot>
<tbody>
{% for i in ce_itemtxs_agg %}
<tr>
<td>{{ i.item_model__name }}</td>
<td>{{ i.ce_quantity__sum }}</td>
<td>{% currency_symbol %}{{ i.avg_unit_cost | currency_format }}</td>
<td>{% currency_symbol %}{{ i.ce_cost_estimate__sum | currency_format }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
{% endif %}
<div class="col-12">
<div class="card shadow-sm">
<div class="card-body">
{% po_item_formset_table po_model itemtxs_formset %}
</div>
</div>
</div>
</div>
</div>
{% include "purchase_orders/includes/mark_as.html" %}
{% endblock %}
{% block customJS %}
<script>
function showPOModal(title, actionUrl, buttonText) {
const modal = new bootstrap.Modal(document.getElementById('POModal'));
document.getElementById('POModalTitle').innerText = title;
// Set the modal body content
document.getElementById('POModalBody').innerHTML = `
<a class="btn btn-primary" href="${actionUrl}">
${buttonText}
</a>
`;
modal.show();
}
</script>
{% endblock customJS %}

View File

@ -0,0 +1,49 @@
{% load i18n %}
{% load django_ledger %}
<div class="table-container">
<table class="table is-fullwidth is-striped is-narrow is-bordered">
<thead>
<tr class="has-text-centered">
<th>{% trans 'Item' %}</th>
<th>{% trans 'Unit Cost' %}</th>
<th>{% trans 'PO Qty' %}</th>
<th>{% trans 'Amount' %}</th>
<th>{% trans 'Status' %}</th>
<th>{% trans 'Billed' %}</th>
</tr>
</thead>
<tbody>
{% for item in po_item_list %}
<tr>
<td>{{ item.item_model }}</td>
<td class="has-text-centered">{{ item.po_unit_cost }}</td>
<td class="has-text-centered">{{ item.po_quantity }}</td>
<td class="{% if item.is_cancelled %}djl-is-strikethrough{% endif %} has-text-centered">
{% currency_symbol %}{{ item.po_total_amount | currency_format }}</td>
<td class="has-text-weight-bold has-text-centered {% if item.is_cancelled %}has-text-danger{% endif %}">
{% if item.po_item_status %}
{{ item.get_po_item_status_display }}
{% endif %}
</td>
<td class="has-text-centered">{% if item.bill_model_id %}
<a class="is-small is-info button"
href="{% url 'django_ledger:bill-detail' entity_slug=entity_slug bill_pk=item.bill_model_id %}">View
Bill</a>{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
<tfoot>
<tr>
<td></td>
<td></td>
<td class="has-text-right">{% trans 'Total PO Amount' %}</td>
<td class="has-text-weight-bold has-text-centered">
{% currency_symbol %}{{ po_model.po_amount | currency_format }}</td>
<td></td>
<td></td>
</tr>
</tfoot>
</table>
</div>