From 01f8d2114e42dbd5bb2e7a7a59060c925fddf7f1 Mon Sep 17 00:00:00 2001 From: Suherdy Yacob Date: Tue, 19 Aug 2025 19:06:26 +0700 Subject: [PATCH] fix bugs --- db.sqlite3 | Bin 368640 -> 401408 bytes inventory/__pycache__/forms.cpython-311.pyc | Bin 6191 -> 6183 bytes .../migrations/0003_remove_supplier_rating.py | 17 + manufacture/__pycache__/admin.cpython-311.pyc | Bin 3291 -> 5235 bytes manufacture/__pycache__/forms.cpython-311.pyc | Bin 2895 -> 10794 bytes .../__pycache__/models.cpython-311.pyc | Bin 8763 -> 14421 bytes manufacture/__pycache__/urls.cpython-311.pyc | Bin 1044 -> 1757 bytes manufacture/__pycache__/views.cpython-311.pyc | Bin 5576 -> 13578 bytes manufacture/admin.py | 36 +- manufacture/forms.py | 148 +++++++- ...02_billofmaterials_billofmaterialstotal.py | 49 +++ manufacture/models.py | 120 ++++++- manufacture/templatetags/__init__.py | 0 .../templatetags/manufacture_extras.py | 79 ++++ manufacture/urls.py | 8 + manufacture/views.py | 195 +++++++++- purchase/__pycache__/forms.cpython-311.pyc | Bin 6200 -> 4647 bytes ..._purchaseorder_supplier_delete_supplier.py | 23 ++ sales/__pycache__/forms.cpython-311.pyc | Bin 6169 -> 4624 bytes ...lter_saleorder_customer_delete_customer.py | 23 ++ templates/base.html | 6 + templates/inventory/product_detail.html | 5 +- templates/inventory/product_list.html | 5 +- templates/manufacture/bom_confirm_delete.html | 47 +++ templates/manufacture/bom_detail.html | 130 +++++++ templates/manufacture/bom_form.html | 338 ++++++++++++++++++ templates/manufacture/bom_list.html | 95 +++++ templates/manufacture/manufacture_detail.html | 13 +- templates/manufacture/manufacture_list.html | 3 +- templates/purchase/purchase_detail.html | 11 +- templates/purchase/purchase_list.html | 5 +- templates/sales/sales_detail.html | 11 +- templates/sales/sales_list.html | 5 +- users/__pycache__/views.cpython-311.pyc | Bin 10375 -> 10885 bytes users/views.py | 11 + 35 files changed, 1347 insertions(+), 36 deletions(-) create mode 100644 inventory/migrations/0003_remove_supplier_rating.py create mode 100644 manufacture/migrations/0002_billofmaterials_billofmaterialstotal.py create mode 100644 manufacture/templatetags/__init__.py create mode 100644 manufacture/templatetags/manufacture_extras.py create mode 100644 purchase/migrations/0002_alter_purchaseorder_supplier_delete_supplier.py create mode 100644 sales/migrations/0002_alter_saleorder_customer_delete_customer.py create mode 100644 templates/manufacture/bom_confirm_delete.html create mode 100644 templates/manufacture/bom_detail.html create mode 100644 templates/manufacture/bom_form.html create mode 100644 templates/manufacture/bom_list.html diff --git a/db.sqlite3 b/db.sqlite3 index 587f20c2c966d446583af41647ed1c345e6be7ff..d7db1628a419a2c684cee4cbbb6f55d680c7ee55 100644 GIT binary patch delta 5668 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 delta 1405 zcmb7Edu&rx7{BM-bI)xryZyQcy)9#1!2qj+wQDz686^}pWjMBO%Xm56%GN!SG3r`K zOaNCEc?9;bW+x_&n4v5&I)`;!iGMg^Fae1?5*1-f43Ur}MqPZxXvEVpH75MyNxpOM z_ucb5=lp*8eYbDQ+Banx%+1^=2*NHNOL!n2PY$j0=4}*;_8*gYCc!;E3w7{2`~q|E zE!?9SXAiUktaL05MfjJEK|ZanOw2*HXfuu`WV1I}?C3NmM{T>QRRO^8A>4%ycpDfT zfO_!5a)4xJk zc6W!GXfanMFZ<10j6D8|LDbYs2|4KE=O@(UrKbwA!0lb@@vQZh z6|E`utXb`TFG3t*lRA@-2ha0WdFl%@Bl^q;T2Y@Rn??k<1TRA~cue-J6--l1J zNIx!O!zb`TsW%ks>S50{;pJ-jwPUGcz2D(>>gJR9r=_WFT*F^uO2!2k=OVoU_8?M) zY@1Ex!(a>q-0tFNs4Wz2>kY?3UES=;_juVn%~DU%no_T~Z1uSx@Y(!a4qK8!p?_h~ zwrG2_tzFcwfLOXAk5f|cEkS)+Mary_VJtIvWT#{x7x688Ot%NcF+u|sRBLpx3j^57 zwg<)KqJ5LBXEch{h`rw^W~F=(2|c?f+!Je#g?sm~;4z%bvQOf5@HfA%o>;ge+{@uWeCT+2EVR2joQS(D>?JF;pbCA&N}DmOt{06ox-Syx z?qYEpandKMsMhRzpuQvAui3z5mU&JfLjv$9~q|C~jh>`tfO2 zvrSa~ulmmn;H}8dK3Yh8N<2_4Yh3{p52qB%W%m~mt3I`e)FQTHn|S?<897EL+}z-2 z9LPL6VV-}a?Vfgefl5jQGtLy?^Bt4gY;**ReFugl6B&G@mD;CGkJ5) zA|;;PB5QT4P`s2~Ye%`PJcnfHG>1$g=9479flHSPK|i>dv>^QpD(&PT>ke6KEJSfX z8rK{X!tBQRNcgg&JsH~*S>-GHwo;8y? diff --git a/inventory/__pycache__/forms.cpython-311.pyc b/inventory/__pycache__/forms.cpython-311.pyc index a200f8efa19cbe31242719d6c9ce50cbac0d693a..dd0549c22a38b8e745f4a515fa6b1df511515610 100644 GIT binary patch delta 157 zcmZ2)u-t%mIWI340}xoSEXjDik@q7Lqs!+1Oj>M=9Fw&;u2ugUYoT-m^;cujsOCd(MRnO$6i5daKxE0X{K delta 162 zcmZ2(u-<@oIWI340}w=?UX<~5BkxBh#+1$fnY7p#IVWpz%5mIcD@rWM%uAo_!P&@O zenAj~HecX8z|81Ac_ME(s~b?O;p7dRlAGE1>X_JEfZQUN$z1}Cj4qQCcw#1J2wn%O zbP(Fg<_?kyoBUfSn9+B$zwmx0HgAxK_hb>VMn><+)5KgMy1geq6q99)+x%ZlgAoAb CQZPIK diff --git a/inventory/migrations/0003_remove_supplier_rating.py b/inventory/migrations/0003_remove_supplier_rating.py new file mode 100644 index 0000000..e4156bc --- /dev/null +++ b/inventory/migrations/0003_remove_supplier_rating.py @@ -0,0 +1,17 @@ +# Generated by Django 5.2.5 on 2025-08-19 08:54 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('inventory', '0002_customer_supplier'), + ] + + operations = [ + migrations.RemoveField( + model_name='supplier', + name='rating', + ), + ] diff --git a/manufacture/__pycache__/admin.cpython-311.pyc b/manufacture/__pycache__/admin.cpython-311.pyc index c0343d2f5484aaf9f4093c729f1cf77b5bfec265..c2038703e9068e5d3dc89e375067d98dbd79b6f2 100644 GIT binary patch literal 5235 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 diff --git a/manufacture/__pycache__/forms.cpython-311.pyc b/manufacture/__pycache__/forms.cpython-311.pyc index 0f34f68d893f7c9485d88490f0ca2666cc119c6d..272837871ed56dcfaabe5fe9e5a25a26c66eac9e 100644 GIT binary patch literal 10794 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 F{{re(zuN!+ delta 883 zcmY+BJ#5oJ6vyvupT8Pcg|H#R#YjP@5(5YdENvMYkSHKU2xH~kHmwtvvm>NP zs0>A7V4%7Ih9UtA0*L{2VPa#2uti>(Iv`Tf0kOb6r-k5E78$~RP6&BnJH)YTy23M?GplF+XNj{1RUz11q2l3j^<|I6y5m%8mB?{ehF{h$WqL%+SgkKpH}cp3d>|`e^OxFq zR}RckfyM~`A!WLEUBM^JZA=~L!#rbHc!+;IG{7eehHd`d7{Mw2)hLMbALB&7DB6ZF zDe#d5@_Cly6Xsb0(L;XUJih{U**fbL8|wq~3&fh^(4Z_59R>-~VB?@$j+4|}_I%%Q zY|IKm4WVZjS&}jQMFxOIg$;pU576#xDbr15x~-^0{Mrr%{~w9D1>*M34Kexiv6K99 zm%*QO5%H*plO90uqOHd&U36>n0fX+wYBivqA2Py&QunTIg7F|7xKl&Hbyb^1;i*@Gmfzm|2<00000 diff --git a/manufacture/__pycache__/models.cpython-311.pyc b/manufacture/__pycache__/models.cpython-311.pyc index bb98a7bf3aee228d0087f8f42b4acf5e60e56f30..9d0e76eef331ee83a233f3b882b7e5e354adf375 100644 GIT binary patch literal 14421 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 diff --git a/manufacture/__pycache__/urls.cpython-311.pyc b/manufacture/__pycache__/urls.cpython-311.pyc index 06147f9160e4d4cde46979bfdbbf0dac1d419dde..9a9b74d8ac27bf1b33fc4d7da44c694a1f116857 100644 GIT binary patch delta 677 zcmbQjahF$PIWI340}$vrEy<8!VPJR+;=lkul=0b*X`_Y)2j*2Nfs_;! z<)@S;mq6`+aN;vl?DRA9((?6h38bf%#6#7@gM|DvMJAgwO^6eB%FM~}PxA%3xF|C* zr`QMRjj+tr@*+@J-I7F;bpbmWEDs8$B4H3A0#eCcT9i|eSOPR9uXrWHXP^+nFB_N1 zcbVL{6@hFmrNS z6&9}>EF3^3vNKq(NNB96xxiv`k;Uc;iw#6reunB5Da-3pj+dkyFG@LIk#atvbAcuN zB1`xcmT<6=3!JhuO6SyV@VdZaf04!h3X45dSbl-1`4vUO9XuCUTraY?USV;CO6aaA hy`t}W!6)tlOZ-Ka_$w^&AEf#CnHso3ut*;0d;r>=&CLJ+ delta 131 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 YO>EU$=_^VZKmDq?uhk?ygW!_?0ok(%`~Uy| delta 1912 zcmbVMO>7%Q6rR~#|L)pe+llkz#&IL+bYmw35>QD?)T&8ZP}7Q5;t&a1E3=_Cwga>4 z0Fk30krSM%Ie@~Cq9O$$N8*qJXHM;@G7^W%Lh6YFQV;Hh11H{F$F3b06){?Wp8e*{ zn{VcQJD=z8r-MHP0)7JRkNWqwpXhgjgHndxIlX+=%{+xbF}M<>gt?iw5Gv{`IwcY@ z{ic`s4*dHPAyxP_n?x=VrtT1?nJzIUjVy;DDzHQLVYu1~A}bMKgFS2%*iaAK_jE0x z(IGJ@B|QBsa)TIAaa-DWQy*VVFRa%*ZMXMSYy3pns10-kf-WpcjGIhugG5Hy4g zLeqlSn?#sGID_yK!c#xG_-kzfvfpa1%Tb1fDU;av2=wZDvHj7QH%*qa`6AGm&l-or z{FF4@#5Gw{GM&#uHC|KWUtRj<%CD;Nn`#^mMkGp4hQvJHrobbfiUQX3KFgY@XaW=swX!*;@`0`7 z9NmYjpo-Ul)8KYe#cp3hbL_>>1GzZ|emW%{_05LbR%+TujP);VJJtyBix7Ft5_|!< zXBh(PRChD8VA^KW2`IogR{x{&J4HQK{z$w0;`e_4%w?GUCj6`#z<;Z~P-0am{TJzt z1>C_^!^8XFpZ6og5FGywU~dos50A$YoI4g>$Z<^v-Vuu#Lvsd@yK2P5gX!J3-Nkj- zq~)ZlyVl_TwwfP97$@RA>)4i~{${f71^)J)gAC<@InS>{2w0R@pHY#1xjdGtk*?<< zF>2k9&}Ww5%KvU=MgU_Mk((Ag;|W1&K_j J8wk$c{{bq!U;zLC diff --git a/manufacture/admin.py b/manufacture/admin.py index e97f979..c68ee47 100644 --- a/manufacture/admin.py +++ b/manufacture/admin.py @@ -1,5 +1,5 @@ from django.contrib import admin -from .models import ManufacturingOrder, ManufacturingLine, ManufacturingOrderLine +from .models import ManufacturingOrder, ManufacturingLine, ManufacturingOrderLine, BillOfMaterials, BillOfMaterialsTotal @admin.register(ManufacturingOrder) class ManufacturingOrderAdmin(admin.ModelAdmin): @@ -20,6 +20,7 @@ class ManufacturingOrderAdmin(admin.ModelAdmin): def get_queryset(self, request): return super().get_queryset(request).select_related('product', 'created_by') + @admin.register(ManufacturingLine) class ManufacturingLineAdmin(admin.ModelAdmin): list_display = ('name', 'capacity_per_hour', 'is_active', 'created_at') @@ -32,6 +33,7 @@ class ManufacturingLineAdmin(admin.ModelAdmin): ('Status', {'fields': ('is_active',)}), ) + @admin.register(ManufacturingOrderLine) class ManufacturingOrderLineAdmin(admin.ModelAdmin): list_display = ('manufacturing_order', 'manufacturing_line', 'actual_quantity', @@ -48,3 +50,35 @@ class ManufacturingOrderLineAdmin(admin.ModelAdmin): def get_queryset(self, request): return super().get_queryset(request).select_related('manufacturing_order', 'manufacturing_line') + + +@admin.register(BillOfMaterials) +class BillOfMaterialsAdmin(admin.ModelAdmin): + list_display = ('manufactured_product', 'component', 'quantity', 'unit', 'created_at') + list_filter = ('manufactured_product__category', 'created_at') + search_fields = ('manufactured_product__name', 'manufactured_product__code', + 'component__name', 'component__code') + ordering = ('manufactured_product__name', 'component__name') + + fieldsets = ( + ('BOM Information', {'fields': ('manufactured_product', 'component', 'quantity', 'unit')}), + ) + + def get_queryset(self, request): + return super().get_queryset(request).select_related('manufactured_product', 'component') + + +@admin.register(BillOfMaterialsTotal) +class BillOfMaterialsTotalAdmin(admin.ModelAdmin): + list_display = ('bom', 'total_cost', 'total_weight', 'last_calculated') + list_filter = ('last_calculated',) + search_fields = ('bom__manufactured_product__name', 'bom__component__name') + ordering = ('-last_calculated',) + + fieldsets = ( + ('BOM Total Information', {'fields': ('bom', 'total_cost', 'total_weight')}), + ) + readonly_fields = ('last_calculated',) + + def get_queryset(self, request): + return super().get_queryset(request).select_related('bom__manufactured_product', 'bom__component') diff --git a/manufacture/forms.py b/manufacture/forms.py index 0ea3d05..602815a 100644 --- a/manufacture/forms.py +++ b/manufacture/forms.py @@ -1,5 +1,7 @@ from django import forms -from .models import ManufacturingOrder +from django.forms import formset_factory +from django.forms.widgets import NumberInput +from .models import ManufacturingOrder, BillOfMaterials from inventory.models import Product class ManufacturingOrderForm(forms.ModelForm): @@ -10,6 +12,7 @@ class ManufacturingOrderForm(forms.ModelForm): fields = ['product', 'quantity', 'date', 'notes'] widgets = { 'date': forms.DateInput(attrs={'type': 'date'}), + 'quantity': forms.NumberInput(attrs={'step': '0.01'}), 'notes': forms.Textarea(attrs={'rows': 3}), } @@ -31,4 +34,145 @@ class ManufacturingOrderForm(forms.ModelForm): quantity = self.cleaned_data.get('quantity') if quantity <= 0: raise forms.ValidationError("Quantity must be greater than zero.") - return quantity \ No newline at end of file + return quantity + + +class BillOfMaterialsForm(forms.ModelForm): + """Form for creating and editing Bill of Materials entries""" + + class Meta: + model = BillOfMaterials + fields = ['manufactured_product', 'component', 'quantity', 'unit'] + widgets = { + 'quantity': forms.NumberInput(attrs={'step': '0.0001'}), + } + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # Add Bootstrap classes + for field in self.fields.values(): + if isinstance(field.widget, (forms.CheckboxInput, forms.RadioSelect)): + field.widget.attrs.update({'class': 'form-check-input'}) + elif isinstance(field.widget, forms.Textarea): + field.widget.attrs.update({'class': 'form-control'}) + elif isinstance(field.widget, forms.DateInput): + field.widget.attrs.update({'class': 'form-control'}) + else: + field.widget.attrs.update({'class': 'form-control'}) + + # Filter manufactured products to only show those marked as manufactured + self.fields['manufactured_product'].queryset = Product.objects.filter(is_manufactured=True) + + def clean_quantity(self): + """Ensure quantity is positive""" + quantity = self.cleaned_data.get('quantity') + if quantity <= 0: + raise forms.ValidationError("Quantity must be greater than zero.") + return quantity + + def clean(self): + """Validate that manufactured product and component are not the same""" + cleaned_data = super().clean() + manufactured_product = cleaned_data.get('manufactured_product') + component = cleaned_data.get('component') + + if manufactured_product and component and manufactured_product == component: + raise forms.ValidationError("A product cannot be a component of itself.") + + return cleaned_data + + +class MultipleBillOfMaterialsForm(forms.Form): + """Form for creating multiple BOM entries at once""" + + manufactured_product = forms.ModelChoiceField( + queryset=Product.objects.filter(is_manufactured=True), + label="Manufactured Product", + help_text="The product that is manufactured using these components" + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # Add Bootstrap classes + for field in self.fields.values(): + if isinstance(field.widget, (forms.CheckboxInput, forms.RadioSelect)): + field.widget.attrs.update({'class': 'form-check-input'}) + elif isinstance(field.widget, forms.Textarea): + field.widget.attrs.update({'class': 'form-control'}) + elif isinstance(field.widget, forms.DateInput): + field.widget.attrs.update({'class': 'form-control'}) + else: + field.widget.attrs.update({'class': 'form-control'}) + + def clean_manufactured_product(self): + """Ensure the selected product is marked as manufactured""" + manufactured_product = self.cleaned_data.get('manufactured_product') + if manufactured_product and not manufactured_product.is_manufactured: + raise forms.ValidationError("Selected product is not marked as manufactured.") + return manufactured_product + + +class BOMComponentForm(forms.Form): + """Form for individual BOM component entry""" + + component = forms.ModelChoiceField( + queryset=Product.objects.all(), + label="Component", + help_text="A component used in manufacturing" + ) + quantity = forms.DecimalField( + max_digits=10, + decimal_places=4, + min_value=0.0001, + label="Quantity", + help_text="Quantity of component needed per unit of manufactured product" + ) + unit = forms.CharField( + max_length=20, + required=False, + label="Unit", + help_text="Unit of measurement for the component (e.g., kg, pieces, meters)", + widget=forms.TextInput(attrs={'readonly': 'readonly'}) + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # Add Bootstrap classes + for field in self.fields.values(): + if isinstance(field.widget, (forms.CheckboxInput, forms.RadioSelect)): + field.widget.attrs.update({'class': 'form-check-input'}) + elif isinstance(field.widget, forms.Textarea): + field.widget.attrs.update({'class': 'form-control'}) + elif isinstance(field.widget, forms.DateInput): + field.widget.attrs.update({'class': 'form-control'}) + else: + field.widget.attrs.update({'class': 'form-control'}) + + def clean_quantity(self): + """Ensure quantity is positive""" + quantity = self.cleaned_data.get('quantity') + if quantity <= 0: + raise forms.ValidationError("Quantity must be greater than zero.") + return quantity + + def clean(self): + """Validate that component is provided""" + cleaned_data = super().clean() + component = cleaned_data.get('component') + + # If no component is selected, this might be an empty form + if not component: + # Check if any other fields have data + quantity = cleaned_data.get('quantity') + unit = cleaned_data.get('unit') + + # If other fields have data but no component, raise error + if quantity or unit: + raise forms.ValidationError("Component is required.") + + # If all fields are empty, mark form as empty + raise forms.ValidationError("This form is empty.") + + return cleaned_data +# Create a formset for BOM components +BOMComponentFormSet = formset_factory(BOMComponentForm, extra=1, min_num=1, validate_min=True) \ No newline at end of file diff --git a/manufacture/migrations/0002_billofmaterials_billofmaterialstotal.py b/manufacture/migrations/0002_billofmaterials_billofmaterialstotal.py new file mode 100644 index 0000000..474e932 --- /dev/null +++ b/manufacture/migrations/0002_billofmaterials_billofmaterialstotal.py @@ -0,0 +1,49 @@ +# Generated by Django 5.2.5 on 2025-08-19 08:54 + +import django.core.validators +import django.db.models.deletion +from decimal import Decimal +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('inventory', '0003_remove_supplier_rating'), + ('manufacture', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='BillOfMaterials', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('quantity', models.DecimalField(decimal_places=4, help_text='Quantity of component needed per unit of manufactured product', max_digits=10, validators=[django.core.validators.MinValueValidator(Decimal('0.0001'))])), + ('unit', models.CharField(blank=True, help_text='Unit of measurement for the component (e.g., kg, pieces, meters)', max_length=20)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('component', models.ForeignKey(help_text='A component used in manufacturing', on_delete=django.db.models.deletion.CASCADE, related_name='bom_components', to='inventory.product')), + ('manufactured_product', models.ForeignKey(help_text='The product that is manufactured using this BOM', on_delete=django.db.models.deletion.CASCADE, related_name='bom_manufactured', to='inventory.product')), + ], + options={ + 'verbose_name': 'Bill of Materials', + 'verbose_name_plural': 'Bills of Materials', + 'ordering': ['manufactured_product__name', 'component__name'], + 'unique_together': {('manufactured_product', 'component')}, + }, + ), + migrations.CreateModel( + name='BillOfMaterialsTotal', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('total_cost', models.DecimalField(decimal_places=4, default=0, max_digits=12)), + ('total_weight', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)), + ('last_calculated', models.DateTimeField(auto_now=True)), + ('bom', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='totals', to='manufacture.billofmaterials')), + ], + options={ + 'verbose_name': 'BOM Total', + 'verbose_name_plural': 'BOM Totals', + }, + ), + ] diff --git a/manufacture/models.py b/manufacture/models.py index 546ece0..02b288d 100644 --- a/manufacture/models.py +++ b/manufacture/models.py @@ -60,11 +60,51 @@ class ManufacturingOrder(models.Model): def save(self, *args, **kwargs): """Override save to calculate total cost and update product stock""" - # Calculate total cost - self.total_cost = self.labor_cost + self.overhead_cost + # Check if product has BOM entries + bom_entries = BillOfMaterials.objects.filter(manufactured_product=self.product) + + # Calculate total cost based on BOM if available, otherwise use manual costs + if bom_entries.exists(): + # Calculate total cost based on BOM + bom_total_cost = 0 + for bom_entry in bom_entries: + component_cost = bom_entry.get_total_component_cost() + bom_total_cost += component_cost * self.quantity + + # Add labor and overhead costs + self.total_cost = bom_total_cost + self.labor_cost + self.overhead_cost + else: + # Calculate total cost using manual costs + self.total_cost = self.labor_cost + self.overhead_cost # If this is a new order and status is completed, add to inventory if not self.pk and self.status == 'completed': + # Check if product has BOM entries and deduct components from stock + if bom_entries.exists(): + # Check if there's enough stock for all components + insufficient_stock = [] + for bom_entry in bom_entries: + required_quantity = bom_entry.quantity * self.quantity + if bom_entry.component.current_stock < required_quantity: + insufficient_stock.append({ + 'component': bom_entry.component.name, + 'required': required_quantity, + 'available': bom_entry.component.current_stock + }) + + # If there's insufficient stock, raise an error + if insufficient_stock: + error_msg = "Insufficient stock for components: " + for item in insufficient_stock: + error_msg += f"{item['component']} (required: {item['required']}, available: {item['available']}) " + raise ValueError(error_msg) + + # Deduct components from stock + for bom_entry in bom_entries: + required_quantity = bom_entry.quantity * self.quantity + bom_entry.component.current_stock -= required_quantity + bom_entry.component.save() + # Update product stock self.product.current_stock += self.quantity @@ -101,6 +141,7 @@ class ManufacturingOrder(models.Model): return ((self.product.selling_price - unit_cost) / unit_cost) * 100 return Decimal('0') + class ManufacturingLine(models.Model): """Manufacturing line/workstation information""" @@ -125,6 +166,7 @@ class ManufacturingLine(models.Model): def __str__(self): return self.name + class ManufacturingOrderLine(models.Model): """Individual line items for manufacturing orders (optional for future expansion)""" @@ -151,3 +193,77 @@ class ManufacturingOrderLine(models.Model): if self.start_time and self.end_time: return self.end_time - self.start_time return None + + +class BillOfMaterials(models.Model): + """Bill of Materials - defines components needed to manufacture a product""" + + manufactured_product = models.ForeignKey( + 'inventory.Product', + on_delete=models.CASCADE, + related_name='bom_manufactured', + help_text='The product that is manufactured using this BOM' + ) + component = models.ForeignKey( + 'inventory.Product', + on_delete=models.CASCADE, + related_name='bom_components', + help_text='A component used in manufacturing' + ) + quantity = models.DecimalField( + max_digits=10, + decimal_places=4, + validators=[MinValueValidator(Decimal('0.0001'))], + help_text='Quantity of component needed per unit of manufactured product' + ) + unit = models.CharField( + max_length=20, + blank=True, + help_text='Unit of measurement for the component (e.g., kg, pieces, meters)' + ) + + # Timestamps + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + unique_together = ('manufactured_product', 'component') + ordering = ['manufactured_product__name', 'component__name'] + verbose_name = _('Bill of Materials') + verbose_name_plural = _('Bills of Materials') + + def __str__(self): + return f"{self.manufactured_product.name} - {self.component.name} ({self.quantity} {self.unit})" + + def save(self, *args, **kwargs): + """Override save to ensure the manufactured product is marked as manufactured""" + # Ensure the manufactured product is marked as manufactured + if not self.manufactured_product.is_manufactured: + self.manufactured_product.is_manufactured = True + self.manufactured_product.save() + + # If unit is not provided, use the component's unit + if not self.unit and self.component.unit: + self.unit = self.component.unit + + super().save(*args, **kwargs) + + def get_total_component_cost(self): + """Calculate total cost of this component for one unit of manufactured product""" + return self.quantity * self.component.cost_price + + +class BillOfMaterialsTotal(models.Model): + """Pre-calculated totals for a BOM to improve performance""" + + bom = models.OneToOneField(BillOfMaterials, on_delete=models.CASCADE, related_name='totals') + total_cost = models.DecimalField(max_digits=12, decimal_places=4, default=0) + total_weight = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True) + last_calculated = models.DateTimeField(auto_now=True) + + class Meta: + verbose_name = _('BOM Total') + verbose_name_plural = _('BOM Totals') + + def __str__(self): + return f"{self.bom} - Total: {self.total_cost}" diff --git a/manufacture/templatetags/__init__.py b/manufacture/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/manufacture/templatetags/manufacture_extras.py b/manufacture/templatetags/manufacture_extras.py new file mode 100644 index 0000000..bed17c6 --- /dev/null +++ b/manufacture/templatetags/manufacture_extras.py @@ -0,0 +1,79 @@ +from django import template + +register = template.Library() + +@register.filter +def format_number(value, decimal_places=2): + """ + Format a decimal value to display with Indonesian number formatting. + Uses '.' as thousand separator and ',' as decimal separator. + Removes trailing zeros after decimal. + """ + if value is None: + return '' + + try: + # Convert to float + num = float(value) + + # Format with specified decimal places + formatted = f"{num:.{decimal_places}f}" + + # Split into integer and decimal parts + if '.' in formatted: + integer_part, decimal_part = formatted.split('.') + else: + integer_part, decimal_part = formatted, '' + + # Add thousand separators to integer part + # Reverse the string, add separators every 3 digits, then reverse back + integer_part = '{:,}'.format(int(integer_part)).replace(',', '.') + + # Handle decimal part - remove trailing zeros + if decimal_part and int(decimal_part) != 0: + # Remove trailing zeros + decimal_part = decimal_part.rstrip('0') + return f"{integer_part},{decimal_part}" + else: + return integer_part + + except (ValueError, TypeError): + return str(value) + +@register.filter +def format_quantity(value): + """ + Format a decimal value to display with Indonesian number formatting for quantities. + Uses '.' as thousand separator and ',' as decimal separator. + Removes trailing zeros after decimal. + """ + return format_number(value, 4) + +@register.filter +def format_currency(value): + """ + Format a decimal value to display with Indonesian number formatting for currency. + Uses '.' as thousand separator and ',' as decimal separator. + Always shows 2 decimal places for currency. + """ + if value is None: + return '' + + try: + # Convert to float + num = float(value) + + # Format with 2 decimal places for currency + formatted = f"{num:,.2f}" + + # Split into integer and decimal parts + integer_part, decimal_part = formatted.split('.') + + # Add thousand separators to integer part + integer_part = '{:,}'.format(int(integer_part)).replace(',', '.') + + # Combine with decimal part + return f"{integer_part},{decimal_part}" + + except (ValueError, TypeError): + return str(value) \ No newline at end of file diff --git a/manufacture/urls.py b/manufacture/urls.py index 59021f5..d3ac8bb 100644 --- a/manufacture/urls.py +++ b/manufacture/urls.py @@ -10,4 +10,12 @@ urlpatterns = [ path('/', views.ManufactureDetailView.as_view(), name='manufacture_detail'), path('/edit/', views.manufacture_edit, name='manufacture_edit'), path('/delete/', views.manufacture_delete, name='manufacture_delete'), + + # BOM URLs + path('bom/', views.BillOfMaterialsListView.as_view(), name='bom_list'), + path('bom/create/', views.bom_create, name='bom_create'), + path('bom//', views.BillOfMaterialsDetailView.as_view(), name='bom_detail'), + path('bom//edit/', views.bom_edit, name='bom_edit'), + path('bom//delete/', views.bom_delete, name='bom_delete'), + path('bom/product//info/', views.get_product_info, name='get_product_info'), ] diff --git a/manufacture/views.py b/manufacture/views.py index 1074b79..0ff26e4 100644 --- a/manufacture/views.py +++ b/manufacture/views.py @@ -3,8 +3,11 @@ from django.views.generic import ListView, DetailView from django.contrib.auth.decorators import login_required from django.utils.decorators import method_decorator from django.contrib import messages -from .models import ManufacturingOrder -from .forms import ManufacturingOrderForm +from django.http import JsonResponse +from .models import ManufacturingOrder, BillOfMaterials +from inventory.models import Product +from .forms import ManufacturingOrderForm, BillOfMaterialsForm, MultipleBillOfMaterialsForm, BOMComponentFormSet +from users.views import has_manufacturing_access @method_decorator(login_required, name='dispatch') class ManufactureListView(ListView): @@ -13,6 +16,23 @@ class ManufactureListView(ListView): context_object_name = 'manufacturing_orders' paginate_by = 20 + +@method_decorator(login_required, name='dispatch') +class BillOfMaterialsListView(ListView): + model = BillOfMaterials + template_name = 'manufacture/bom_list.html' + context_object_name = 'boms' + paginate_by = 20 + + def get_queryset(self): + queryset = super().get_queryset() + # Optionally filter by manufactured product + product_id = self.request.GET.get('product_id') + if product_id: + queryset = queryset.filter(manufactured_product_id=product_id) + return queryset + + @login_required def manufacture_create(request): """Create a new manufacturing order with simple input""" @@ -46,11 +66,11 @@ def manufacture_create(request): return render(request, 'manufacture/manufacture_form.html', {'form': form, 'title': 'Create Manufacturing Order'}) + @login_required def manufacture_edit(request, pk): """Edit an existing manufacturing order""" - from users.views import is_administrator - if not is_administrator(request.user): + if not has_manufacturing_access(request.user): messages.error(request, 'You do not have permission to edit manufacturing orders.') return redirect('manufacture:manufacture_detail', pk=pk) @@ -71,11 +91,11 @@ def manufacture_edit(request, pk): 'manufacturing_order': manufacturing_order }) + @login_required def manufacture_delete(request, pk): """Delete a manufacturing order""" - from users.views import is_administrator - if not is_administrator(request.user): + if not has_manufacturing_access(request.user): messages.error(request, 'You do not have permission to delete manufacturing orders.') return redirect('manufacture:manufacture_list') @@ -90,8 +110,171 @@ def manufacture_delete(request, pk): return render(request, 'manufacture/manufacture_confirm_delete.html', {'manufacturing_order': manufacturing_order}) +@login_required +def bom_create(request): + """Create a new bill of materials entry""" + if not has_manufacturing_access(request.user): + messages.error(request, 'You do not have permission to create bill of materials entries.') + return redirect('manufacture:bom_list') + + if request.method == 'POST': + main_form = MultipleBillOfMaterialsForm(request.POST) + component_formset = BOMComponentFormSet(request.POST) + + if main_form.is_valid() and component_formset.is_valid(): + manufactured_product = main_form.cleaned_data['manufactured_product'] + + # Save all component entries + saved_components = [] + for component_form in component_formset: + # Skip empty forms + if not component_form.cleaned_data: + continue + + component = component_form.cleaned_data['component'] + quantity = component_form.cleaned_data['quantity'] + unit = component_form.cleaned_data['unit'] + + # Create BOM entry + bom = BillOfMaterials( + manufactured_product=manufactured_product, + component=component, + quantity=quantity, + unit=unit + ) + bom.save() + saved_components.append(bom) + + messages.success(request, f'Bill of Materials for "{manufactured_product.name}" with {len(saved_components)} components created successfully!') + return redirect('manufacture:bom_list') + else: + main_form = MultipleBillOfMaterialsForm() + component_formset = BOMComponentFormSet() + + return render(request, 'manufacture/bom_form.html', { + 'main_form': main_form, + 'component_formset': component_formset, + 'title': 'Create Bill of Materials' + }) + + +@login_required +def bom_edit(request, pk): + """Edit an existing bill of materials entry""" + if not has_manufacturing_access(request.user): + messages.error(request, 'You do not have permission to edit bill of materials.') + return redirect('manufacture:bom_list') + + # Get the BOM entry to edit + bom = get_object_or_404(BillOfMaterials, pk=pk) + + if request.method == 'POST': + main_form = MultipleBillOfMaterialsForm(request.POST) + component_formset = BOMComponentFormSet(request.POST) + + if main_form.is_valid() and component_formset.is_valid(): + manufactured_product = main_form.cleaned_data['manufactured_product'] + + # Delete existing BOM entries for this manufactured product + BillOfMaterials.objects.filter(manufactured_product=manufactured_product).delete() + + # Save all component entries + saved_components = [] + for component_form in component_formset: + # Skip empty forms + if not component_form.cleaned_data: + continue + + component = component_form.cleaned_data['component'] + quantity = component_form.cleaned_data['quantity'] + unit = component_form.cleaned_data['unit'] + + # Create BOM entry + bom_entry = BillOfMaterials( + manufactured_product=manufactured_product, + component=component, + quantity=quantity, + unit=unit + ) + bom_entry.save() + saved_components.append(bom_entry) + + messages.success(request, f'Bill of Materials for "{manufactured_product.name}" with {len(saved_components)} components updated successfully!') + return redirect('manufacture:bom_list') + else: + # Pre-populate the form with existing components + main_form = MultipleBillOfMaterialsForm(initial={'manufactured_product': bom.manufactured_product}) + + # Get all existing components for this manufactured product + existing_components = BillOfMaterials.objects.filter(manufactured_product=bom.manufactured_product) + + # Create initial data for the formset + initial_data = [] + for existing_bom in existing_components: + initial_data.append({ + 'component': existing_bom.component, + 'quantity': existing_bom.quantity, + 'unit': existing_bom.unit + }) + + # Add extra empty forms if needed + while len(initial_data) < 1: + initial_data.append({}) + + component_formset = BOMComponentFormSet(initial=initial_data) + + return render(request, 'manufacture/bom_form.html', { + 'main_form': main_form, + 'component_formset': component_formset, + 'title': 'Edit Bill of Materials', + 'bom': bom + }) + + +@login_required +def bom_delete(request, pk): + """Delete a bill of materials entry""" + if not has_manufacturing_access(request.user): + messages.error(request, 'You do not have permission to delete bill of materials entries.') + return redirect('manufacture:bom_list') + + bom = get_object_or_404(BillOfMaterials, pk=pk) + + if request.method == 'POST': + manufactured_product_name = bom.manufactured_product.name + bom.delete() + messages.success(request, f'Bill of Materials entry for "{manufactured_product_name}" deleted successfully!') + return redirect('manufacture:bom_list') + + return render(request, 'manufacture/bom_confirm_delete.html', {'bom': bom}) + + @method_decorator(login_required, name='dispatch') class ManufactureDetailView(DetailView): model = ManufacturingOrder template_name = 'manufacture/manufacture_detail.html' context_object_name = 'manufacturing_order' + + +@method_decorator(login_required, name='dispatch') +class BillOfMaterialsDetailView(DetailView): + model = BillOfMaterials + template_name = 'manufacture/bom_detail.html' + context_object_name = 'bom' + +@login_required +def get_product_info(request, product_id): + """Return product information including unit as JSON""" + if not has_manufacturing_access(request.user): + return JsonResponse({'error': 'Permission denied'}, status=403) + + try: + product = Product.objects.get(id=product_id) + return JsonResponse({ + 'id': product.id, + 'name': product.name, + 'unit': product.unit, + 'unit_display': product.get_unit_display() + }) + except Product.DoesNotExist: + return JsonResponse({'error': 'Product not found'}, status=404) diff --git a/purchase/__pycache__/forms.cpython-311.pyc b/purchase/__pycache__/forms.cpython-311.pyc index bbd7400849fa7e4d214ba894c7a197014b8f451a..490f5601d5e76e84e2a04b07db7f4203b89bb6b5 100644 GIT binary patch delta 1382 zcmb7DT}TvB6ux(NX7=v-@9wyy`7i1^7U~+AE0sSH1TLXTg~hP#OttH_*_~M>(4vQ+ zzz5~_P!Z*WD3*cXixC991rh8n11kgd+@q2`1QFf4vZ>`pU1q*H_slov`_7!1)s}_2 z{Dr(c2cYYNeePIeIRZ@=(DRFKC2?m*sR9*C#B$9#X@y|I8s^wz zY|ZjyhI2U->k>B)0eAt)XGf*XZ^c^_046}#mWdT%UnT@H%7ob%VS9!VPCI40*&HTW zr`Qw}g1d#Nw;fWmxs+Jb*9WHI#o|d$FU!=Tj=1g0`u^>FnDv}qyRwBO!Iz6!y|hs; z_CLK$oz5;v-U|)c3r6>DZfYib!HjPJ!o`_*-a;or&#i@Pc6R;y`^+Yp{3l6gMr607 zR{b~0`n|iHhEpyD3<8hCkC}P$I=776vXzpzHfOQR`rs;+vYPo5FZVxuWV=WN9Hbsswb^6vhIPn5vnMkQRz+dLSB9v6Xb8s^$tx!{ur2 zpc+wwx<@cMO^sYOxd6VRnZn38yJGgelE?bQt!gjWYgXE6YXFV AM*si- delta 2894 zcma)8Uu@gP89!1Y^^datS)O9mF>Seal*+M_Hg)W|u^Tr|ngVf9qurcf6J(OMsW6gu zq-sx)rgX@I2jrmv4K!$1Ov8XAK!PDdS|D#N+LyfypbVlGI2i3P?0N276h$9~eJ5F( z6*z5A#~<&$`~KhEN8KHGJlb*B>vbb&{O+Gt?s{)^{FgeTd#N$M$lO4e*>$u=U3Sc) zIQ!@#^$qQD0jCe1-2t2ag?ECk(E1)weqi>*F1PssEJhTDWNr*6e9dbEdW2 zKq$=}NcHM}XL`adiF8MktKx(B!!OYyD^qtU@CTkoqtMN{A7#<6|AUQwuWs7`y5IRx zx&z;@dxRDb(p}x3ohM^0A1t#gA~+?Ei`Wa{Ta2KjRP{SxpaB@#8wh{ZDnYxWxSyDv&3{db90&%j&&P$3UXcqfcO38y8ZdsKx zs+P(K5ZotJKGFyM{pCQ5MBzS&NN62o zn2_5`^1AyuP~Sx^BvK-+pc17K>Ru~SqD{2l0m}UrwM1*Jn~#vl6zTWT2lSH{1+;lt z#dghJvQemLgIoKty*K1|;B05CILKX#x!S34ulJj97wr{ivnG|_N22EgTe|>-L~a=@ z-hd+T3c3T{-*1M13NR`$Z3;Vz%t1aEnRygLTCgRG1`s|~V%ucRBdUiT-6;pcjxxm)PI1^Ly`AY89>77SJjc4-}4`lQh10s`$M#&#%hb3cr+6 zl*N>AmCxohIh#>24ZdBp*d?4TNSWCJ|9U=?ku%G@oZ*G+wVbR-A}_5;LS9QP+7)Sn z*Z<~-mJ94`HlGpsg1co$jnft?xQ3gp?QD?Z8G>hb0^nV=3}yEhkrGv;(lBqd_EXZn z8%y*p`sSf|gn8fvJ7qd|)v~D?hB19?)S^_C#KH5pA70&QsVHToyiz0`xU_|eQ`>Mye&&ffcF zYo&f83c>lkTGPz?1YbCG7UL}Dl`PZBdf{ghGQq8xLSVVAu^{Vd*RxJ>Pg3D21VBqRT&!X;Ng> zm!U>nM?3a_OQ62wy4Ej{-*$IjC01E`7_N4YncZXSuWfSyBY5WF6#(c}ZpP$h3~uI2 z?zqw3YZ2az9NCJDY(_@Pj|Ys%NHsELMy9qR=Qktg|JqfJTrwk<)?ckFJav2g4^!_= zRigod5%TIaLIP|64`8r+lpTWC8Tq8|n-`MCV{Ozg}c`gG7ebHw)P=x|^rX8Wv%g2`tw z1~Bf_4fkO-0i1YP|F=8bOD>^GO80bQ|HxlEOn}r#^=&=oIeoU1A;teGmydT6z z8=ki)Dye(CKWDBGdyiJUkEp?CiM>I9xEnu5#&zH7U*J(PJx<`L{<1GL`U2r%8t3#y zLLMV9PM{tI@tB;&>y1bf`VC)*7F>ZQl8v&ZM_Q+tDYJO<>_Pc7N4rqoqDaL5fK;4VkCRh?_KrEbtbL z1)gpRqSd@?d5k#j8KUoO`7*!Fw=+0yk_)9Ixgs1%QclK6dQe8ln#>3pK4G?y3Og33 zC^(4r?!%nz+G)Yg^4!#Aceb{25p*kK%A)5c<(MhUj<|kwCmw>Ljj<={qOWx zcm&P46|S4}**b`c`J<2QxlP2uSb`5|z2Wdrd*28dstSw@>tsZmaBa^9)WBd>aCme~ z_qh{%cx+T92`tOO;h-+dwB1=-Lf}ji_|BM0?uJ!8A&du=epM$TgoUwDrB_#}3ya81 zOL0ME9t#t%vDU3>YC!i12}@Yj`Vy92(jQI;gX8cd@X65`T#PE{Q(Rzri0HIfG>EHW zd#?@2AD0@|rRFuM`R&!1)VwNn#ig!wsb@{kt!-$N10;pa_EHL+b>-DnK#>kvHo6;7HBFY65NTl1F#7|!v!sY2 zf#1ad9u`LE2Unxt#e?2|VKk6Fh0q21G2;f`!jyc`#>^_$h#TfYSg*1>m-Tjp^+wKz1UhyH`^?iK3E=x;k{8eNA*! zw4xe1A+8jVGw^N`gWc7igCIaD4P{NCbM$joDUxWKCy(*t9xv*omp#|&ya*byKStW_ YcWxPL-*#s-Bl^peMV;9K{o*P64N?6WzyJUM delta 2852 zcma)8Uu@gP8NZW6>K`TRkLAjC64Q}mN2#1RNmIv$V|!8Uq!|!r^Rms&HkBc1n+h$d zN2>M&Y08E^_@NImXrSG?U;+kY84?UBvJQC}Ftjh*&P@; z%2ch*S$8`A?(Vzq-`#ijo$d_$cqI6XK)?rZx$w`QFW(Jp1pfsuYCrXlh6L&@@EW}i z*5GUId61+Z&%(bt{9er2fxo8%PVM8sxTEIKHiA*@Sg<2oajI^js9CZuyH|FWL6)*d z0_?WCAWPfb=W?_(?$@n-hhYaFQunZl2Vaf7uIZuLrwd*yqiG z8l6OA@15ss)Gp9NU*eUeqr1L;<E@U75-%zyC381L+4LasjiQ7^!K7CuVM$a=p{Cff$ln^4a)qLrjF^;CEQrYT zWHM4tQZpGdmcgtUzNo1A6%k#^>}f?#V&9*(Y|KicETBV}f{qhNk~w-_lto@O={GsK zh~4l>ij-4SF2{@LgjVseT_o-%T`UNkDxxDq@Gu7LSDvnj8L}UG6En_WygEquHzoo@&JF$0*N+ScCNT8P6*1*FRpKG-(Asu>+(n4 z4?XR&H8;5j$AI8%r{nv#->$Fhs(I|(QGE{x{=3e00qg{`1cks`__DqN?qSa!*#3bM z?p3HZg~2j)&_c@8JV*dF(h>y&0G+SUZL*1uGJTM+wWq#{yX>y{TfP#%gwT?McP!w` zUZH6MfW$E(tt zqUp+UD`JVB%@=b5Tk^H6D@lh5O5S0+@U1{&9VDPp{5>7V@D5nQY4f}B3M|7c9yd$d zk*dP>FfBSZ9B+5e1H@uxY{*f#WjYlVk?zS66Y>gK9m}GlxcS^SAxnx%7ByKKo#$k6 z4hbTf#cAba+x@q1!0r`muhB53loIwP zSO3Q@lVqxV1)qxBVAmD)^6(R;YyArO?ez5561BBQv3k#_(KEXK)g30RM=n0PfdSV# zlQx*N&ZIwKPV4Qp5oe9~@$LAjt@x=Y1A4rAsve&*;#1r4%UkiwzwfHYuNm=c>#tf0 z&%ZnNv#Fm<)q7J$Z%U7xZ`L|9W-wzqGuC+FxZaiA3-khR>!~>dW7pjJB};y!wyH-) zaIG_^4a<*4v}gV5vz?CkE}-y&c0sk-5c~^S!|D4R23+gR8G|{aGiMsiA>FPGCbrG= zZ83dyrr%)tb*8`3c}yST^yp2U`Oo?cmt8;6j5`W>V19Hlad8lQIOt0scYZiBoK84D z3LJ)b@KJ)onDl7B@f|so!kku=%s?h%y|BC}%bCnCAsW~I<~xiZAg_ONrjsI1!E-Pb zrO4{O6d#xT6$oF~o-yC1ZeY99kFN(FL+KcCVGu*ohh8Lo%O~rF9U;Th1Wphj4@mRz zB+n^&2}6==XAVdNeG%i$u3)-JUgYb$*hmZ7GQ!JS$cfFZtJQFK + {% endif %} {% if user.has_inventory_permission %} diff --git a/templates/inventory/product_detail.html b/templates/inventory/product_detail.html index 19757ab..b1117f7 100644 --- a/templates/inventory/product_detail.html +++ b/templates/inventory/product_detail.html @@ -1,4 +1,5 @@ {% extends 'base.html' %} +{% load manufacture_extras %} {% block title %}Product - {{ product.name }}{% endblock %} @@ -110,11 +111,11 @@
Cost Price
-

Rp {{ product.cost_price|floatformat:0 }}

+

Rp {{ product.cost_price|format_currency }}

Selling Price
-

Rp {{ product.selling_price|floatformat:0 }}

+

Rp {{ product.selling_price|format_currency }}

{% if product.selling_price > 0 and product.cost_price > 0 %}
diff --git a/templates/inventory/product_list.html b/templates/inventory/product_list.html index b5be6b1..852f3dc 100644 --- a/templates/inventory/product_list.html +++ b/templates/inventory/product_list.html @@ -1,4 +1,5 @@ {% extends 'base.html' %} +{% load manufacture_extras %} {% block title %}Inventory - Products{% endblock %} @@ -46,8 +47,8 @@ {{ product.min_stock_level }} {{ product.unit }} - Rp {{ product.cost_price|floatformat:0 }} - Rp {{ product.selling_price|floatformat:0 }} + Rp {{ product.cost_price|format_currency }} + Rp {{ product.selling_price|format_currency }} {{ product.is_active|yesno:"Active,Inactive" }} diff --git a/templates/manufacture/bom_confirm_delete.html b/templates/manufacture/bom_confirm_delete.html new file mode 100644 index 0000000..366192b --- /dev/null +++ b/templates/manufacture/bom_confirm_delete.html @@ -0,0 +1,47 @@ +{% extends 'base.html' %} + +{% block title %}Delete BOM Entry{% endblock %} + +{% block content %} +
+

+ + Delete BOM Entry +

+ + + Back to BOM List + +
+ +
+
+
+
+
+ + Confirm Deletion +
+
+
+

Are you sure you want to delete the BOM entry for "{{ bom.manufactured_product.name }}" with component "{{ bom.component.name }}"?

+

This action cannot be undone.

+ +
+ {% csrf_token %} +
+ + + Cancel + + +
+
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/manufacture/bom_detail.html b/templates/manufacture/bom_detail.html new file mode 100644 index 0000000..7faa73b --- /dev/null +++ b/templates/manufacture/bom_detail.html @@ -0,0 +1,130 @@ +{% extends 'base.html' %} +{% load manufacture_extras %} + +{% block title %}BOM - {{ bom.manufactured_product.name }}{% endblock %} + +{% block content %} +
+

+ + Bill of Materials +

+
+ + + Back to List + + {% if user.is_superuser or user.is_staff %} + + + Edit + + {% endif %} +
+
+ +
+
+
+
+
BOM Details
+
+
+
+
+

Manufactured Product: {{ bom.manufactured_product.name }}

+

Product Code: {{ bom.manufactured_product.code }}

+
+
+

Component: {{ bom.component.name }}

+

Quantity: {{ bom.quantity|format_quantity }}

+

Unit: {{ bom.unit|default:bom.component.unit }}

+
+
+
+ + + {% with bom.manufactured_product.bom_manufactured.all as all_components %} + {% if all_components.count > 1 %} +
+
+
All Components for {{ bom.manufactured_product.name }}
+
+
+
+ + + + + + + + + + + {% for component_bom in all_components %} + + + + + + + {% endfor %} + +
ComponentQuantityUnitActions
{{ component_bom.component.name }}{{ component_bom.quantity|format_quantity }}{{ component_bom.unit|default:component_bom.component.unit }} + {% if component_bom.pk != bom.pk %} + + + + + + + {% endif %} +
+
+
+
+ {% endif %} + {% endwith %} + +
+
+
Cost Information
+
+
+
+
+

Component Cost Price: Rp {{ bom.component.cost_price|format_currency }}

+
+
+

Total Component Cost: Rp {{ bom.get_total_component_cost|format_currency }}

+
+
+
+
+ +
+
+
+
Product Information
+
+
+

Component Code: {{ bom.component.code }}

+

Component Category: {{ bom.component.category.name|default:"N/A" }}

+

Component Current Stock: {{ bom.component.current_stock }}

+

Component Unit: {{ bom.component.unit }}

+
+
+ +
+
+
Timestamps
+
+
+

Created: {{ bom.created_at|date:"d/m/Y H:i" }}

+

Updated: {{ bom.updated_at|date:"d/m/Y H:i" }}

+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/manufacture/bom_form.html b/templates/manufacture/bom_form.html new file mode 100644 index 0000000..87e768b --- /dev/null +++ b/templates/manufacture/bom_form.html @@ -0,0 +1,338 @@ +{% extends 'base.html' %} +{% load crispy_forms_tags %} + +{% block title %}{{ title }}{% endblock %} + +{% block content %} +
+

+ + {{ title }} +

+ + + Back to BOM List + +
+ +
+
+
+
+
Bill of Materials Information
+
+
+
+ {% csrf_token %} + + {# Use dynamic form for both creation and editing #} +
+
+ {% if main_form %} + {{ main_form.manufactured_product|as_crispy_field }} + {% else %} + {{ form.manufactured_product|as_crispy_field }} + {% endif %} +
+
+ +
Components
+ {% if component_formset %} + {{ component_formset.management_form }} + +
+ {% for component_form in component_formset %} +
+
+
Component #{{ forloop.counter }}
+ {% if forloop.counter > 1 %} + + {% endif %} +
+
+
+
+ {{ component_form.component|as_crispy_field }} +
+
+ {{ component_form.quantity|as_crispy_field }} +
+
+
+ {{ component_form.unit|as_crispy_field }} +
+
+
+
+ {% endfor %} +
+ +
+ +
+ {% else %} + {# Single BOM form for backward compatibility #} +
+
+ {{ form.component|as_crispy_field }} +
+
+ {{ form.quantity|as_crispy_field }} +
+
+
+ {{ form.unit|as_crispy_field }} +
+
+ {% endif %} + +
+ + + Cancel + +
+ +
+
+ +
+
+
+
BOM Tips
+
+
+
    +
  • + + Select the manufactured product and component +
  • +
  • + + Enter the exact quantity needed +
  • +
  • + + Unit of measurement is automatically populated +
  • + {% if component_formset %} +
  • + + Add multiple components dynamically +
  • +
  • + + Remove components you don't need +
  • + {% endif %} +
+
+
+
+
+{% endblock %} + +{% block extra_js %} + +{% endblock %} \ No newline at end of file diff --git a/templates/manufacture/bom_list.html b/templates/manufacture/bom_list.html new file mode 100644 index 0000000..43f8b8f --- /dev/null +++ b/templates/manufacture/bom_list.html @@ -0,0 +1,95 @@ +{% extends 'base.html' %} +{% load manufacture_extras %} + +{% block title %}Bill of Materials{% endblock %} + +{% block content %} +
+

+ + Bill of Materials +

+ + + New BOM Entry (Multiple Components) + +
+ +
+
+ {% if boms %} +
+ + + + + + + + + + + + + {% for bom in boms %} + + + + + + + + + {% endfor %} + +
Manufactured ProductComponentQuantityUnitCreatedActions
+ {{ bom.manufactured_product.name }} +
{{ bom.manufactured_product.code }}
+
{{ bom.component.name }}{{ bom.quantity|format_quantity }}{{ bom.unit|default:bom.component.unit }}{{ bom.created_at|date:"d/m/Y" }} + + + + + + + + + +
+
+ + {% if is_paginated %} + + {% endif %} + {% else %} +
+ +

No Bill of Materials Entries

+

Start by creating your first BOM entry.

+ + + Create BOM Entry + +
+ {% endif %} +
+
+{% endblock %} \ No newline at end of file diff --git a/templates/manufacture/manufacture_detail.html b/templates/manufacture/manufacture_detail.html index 66ad3e7..db6a2e0 100644 --- a/templates/manufacture/manufacture_detail.html +++ b/templates/manufacture/manufacture_detail.html @@ -1,4 +1,5 @@ {% extends 'base.html' %} +{% load manufacture_extras %} {% block title %}Manufacturing Order - {{ manufacturing_order.order_number }}{% endblock %} @@ -64,26 +65,26 @@
Labor Cost
-

Rp {{ manufacturing_order.labor_cost|floatformat:0 }}

+

Rp {{ manufacturing_order.labor_cost|format_currency }}

Overhead Cost
-

Rp {{ manufacturing_order.overhead_cost|floatformat:0 }}

+

Rp {{ manufacturing_order.overhead_cost|format_currency }}

Total Cost
-

Rp {{ manufacturing_order.total_cost|floatformat:0 }}

+

Rp {{ manufacturing_order.total_cost|format_currency }}

Unit Cost
-
Rp {{ manufacturing_order.get_unit_cost|floatformat:0 }}
+
Rp {{ manufacturing_order.get_unit_cost|format_currency }}
@@ -98,8 +99,8 @@

Product Code: {{ manufacturing_order.product.code }}

Category: {{ manufacturing_order.product.category.name|default:"N/A" }}

Current Stock: {{ manufacturing_order.product.current_stock }}

-

Cost Price: Rp {{ manufacturing_order.product.cost_price|floatformat:0 }}

-

Selling Price: Rp {{ manufacturing_order.product.selling_price|floatformat:0 }}

+

Cost Price: Rp {{ manufacturing_order.product.cost_price|format_currency }}

+

Selling Price: Rp {{ manufacturing_order.product.selling_price|format_currency }}

diff --git a/templates/manufacture/manufacture_list.html b/templates/manufacture/manufacture_list.html index dcb2362..5cb6d46 100644 --- a/templates/manufacture/manufacture_list.html +++ b/templates/manufacture/manufacture_list.html @@ -1,4 +1,5 @@ {% extends 'base.html' %} +{% load manufacture_extras %} {% block title %}Manufacturing Orders{% endblock %} @@ -44,7 +45,7 @@ {{ order.get_status_display }}
- Rp {{ order.total_cost|floatformat:0 }} + Rp {{ order.total_cost|format_currency }} diff --git a/templates/purchase/purchase_detail.html b/templates/purchase/purchase_detail.html index 24783fd..5f1a4e7 100644 --- a/templates/purchase/purchase_detail.html +++ b/templates/purchase/purchase_detail.html @@ -1,4 +1,5 @@ {% extends 'base.html' %} +{% load manufacture_extras %} {% block title %}Purchase Order - {{ purchase_order.order_number }}{% endblock %} @@ -62,25 +63,25 @@
Subtotal
-

Rp {{ purchase_order.subtotal|floatformat:0 }}

+

Rp {{ purchase_order.subtotal|format_currency }}

Tax
-

Rp {{ purchase_order.tax_amount|floatformat:0 }}

+

Rp {{ purchase_order.tax_amount|format_currency }}

Shipping
-

Rp {{ purchase_order.shipping_cost|floatformat:0 }}

+

Rp {{ purchase_order.shipping_cost|format_currency }}

Total
-

Rp {{ purchase_order.total_amount|floatformat:0 }}

+

Rp {{ purchase_order.total_amount|format_currency }}

@@ -103,7 +104,7 @@ {{ purchase_order.supplier.rating }}/5

-

Credit Limit: Rp {{ purchase_order.supplier.credit_limit|floatformat:0 }}

+

Credit Limit: Rp {{ purchase_order.supplier.credit_limit|format_currency }}

diff --git a/templates/purchase/purchase_list.html b/templates/purchase/purchase_list.html index cfe370e..a19001d 100644 --- a/templates/purchase/purchase_list.html +++ b/templates/purchase/purchase_list.html @@ -1,4 +1,5 @@ {% extends 'base.html' %} +{% load manufacture_extras %} {% block title %}Purchase Orders{% endblock %} @@ -43,8 +44,8 @@ {{ order.get_status_display }} - Rp {{ order.subtotal|floatformat:0 }} - Rp {{ order.total_amount|floatformat:0 }} + Rp {{ order.subtotal|format_currency }} + Rp {{ order.total_amount|format_currency }} diff --git a/templates/sales/sales_detail.html b/templates/sales/sales_detail.html index 2964c7c..e1320fc 100644 --- a/templates/sales/sales_detail.html +++ b/templates/sales/sales_detail.html @@ -1,4 +1,5 @@ {% extends 'base.html' %} +{% load manufacture_extras %} {% block title %}Sales Order - {{ sale_order.order_number }}{% endblock %} @@ -62,25 +63,25 @@
Subtotal
-

Rp {{ sale_order.subtotal|floatformat:0 }}

+

Rp {{ sale_order.subtotal|format_currency }}

Tax
-

Rp {{ sale_order.tax_amount|floatformat:0 }}

+

Rp {{ sale_order.tax_amount|format_currency }}

Discount
-

Rp {{ sale_order.discount_amount|floatformat:0 }}

+

Rp {{ sale_order.discount_amount|format_currency }}

Total
-

Rp {{ sale_order.total_amount|floatformat:0 }}

+

Rp {{ sale_order.total_amount|format_currency }}

@@ -99,7 +100,7 @@

Contact: {{ sale_order.customer.contact_person|default:"N/A" }}

Email: {{ sale_order.customer.email|default:"N/A" }}

Phone: {{ sale_order.customer.phone|default:"N/A" }}

-

Credit Limit: Rp {{ sale_order.customer.credit_limit|floatformat:0 }}

+

Credit Limit: Rp {{ sale_order.customer.credit_limit|format_currency }}

diff --git a/templates/sales/sales_list.html b/templates/sales/sales_list.html index 9ee8e27..5b6d24d 100644 --- a/templates/sales/sales_list.html +++ b/templates/sales/sales_list.html @@ -1,4 +1,5 @@ {% extends 'base.html' %} +{% load manufacture_extras %} {% block title %}Sales Orders{% endblock %} @@ -43,8 +44,8 @@ {{ order.get_status_display }} - Rp {{ order.subtotal|floatformat:0 }} - Rp {{ order.total_amount|floatformat:0 }} + Rp {{ order.subtotal|format_currency }} + Rp {{ order.total_amount|format_currency }}
diff --git a/users/__pycache__/views.cpython-311.pyc b/users/__pycache__/views.cpython-311.pyc index 6a28a040184d83ba8475deaae6113c7742965ee1..5580f1b4abfd70b85846bdf0d35a727ea054f5aa 100644 GIT binary patch delta 453 zcmZni7`%_e2djnse~P*4FYNyQka)9F)*wKVhAV_t6`WdEv!;s#FWAq%#gwYG>Nq; z&^aSDIa?t!O`)_nwMZc&u~;EDF|RZ&F}b9)C^IizAu%~QwYXS8BQ>)m1E?S|B{wrq zA-@PsRY7V|Zf0?DW`3S#NRb@S!do&xqvBDFiiaqQ1gUb_82P zB_%~gnIItvAW_T+BovCIK(Z2;#z9L3R0agc{7`_#a(v3j*<(a#uwR5uCSYY zU|?hw*&%Y>GU}3L)J4mfE0!@}p&R_d9VIuo1v*TNWPp18G&MIjD%&wKW>4O&A}d@9 s6av{@yo#BD;R7=xBjb(9A63LB|5st#%%PgkEUUu7DENT^lK>kA06c_%od5s; delta 104 zcmZn-Z4cyI&dbZi00eDM7iF+XP2`hcjM}Ik&CC+apeeh#hxr&Iqs!)CRzo>vKTVCz zxvF-IlQ*bI3Ks%(FamM$TxJG_56p~=j5j9VSCiWOO|6JofPtSefbj#EERq2#0sv?# B83F(R diff --git a/users/views.py b/users/views.py index 075ad49..353f965 100644 --- a/users/views.py +++ b/users/views.py @@ -176,3 +176,14 @@ def group_delete(request, pk): return redirect('users:group_list') return render(request, 'users/group_confirm_delete.html', {'group': group}) + + +def has_manufacturing_access(user): + """Check if user has manufacturing access (either admin or manufacturing permission)""" + if user.is_superuser: + return True + if user.group and user.group.name == 'Administrators': + return True + if hasattr(user, 'has_manufacturing_permission') and user.has_manufacturing_permission(): + return True + return False