From 13c7e2af491150700b6eec9ccf1b11c0c717a25d Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 20 Apr 2020 19:28:09 +1000 Subject: [PATCH 001/104] Update version.py Modify version number for release --- InvenTree/InvenTree/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/InvenTree/version.py b/InvenTree/InvenTree/version.py index 2b0d02bfe9..7aa59dc9d2 100644 --- a/InvenTree/InvenTree/version.py +++ b/InvenTree/InvenTree/version.py @@ -6,7 +6,7 @@ import subprocess from common.models import InvenTreeSetting import django -INVENTREE_SW_VERSION = "0.0.12 pre" +INVENTREE_SW_VERSION = "0.0.12" def inventreeInstanceName(): From c5166ec845ffe9477ab488931775dcdfd1dce7e7 Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 20 Apr 2020 19:30:58 +1000 Subject: [PATCH 002/104] Update version.py --- InvenTree/InvenTree/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/InvenTree/version.py b/InvenTree/InvenTree/version.py index 7aa59dc9d2..213cad3d81 100644 --- a/InvenTree/InvenTree/version.py +++ b/InvenTree/InvenTree/version.py @@ -6,7 +6,7 @@ import subprocess from common.models import InvenTreeSetting import django -INVENTREE_SW_VERSION = "0.0.12" +INVENTREE_SW_VERSION = "0.0.13 pre" def inventreeInstanceName(): From 974c98c95af45967bfd603d380ebf34eb8fdd8c3 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 20 Apr 2020 19:41:58 +1000 Subject: [PATCH 003/104] Add "SalesOrder" concept - SalesOrder model - SalesOrderLineItem - SalesOrderAttachment --- .../migrations/0020_auto_20200420_0940.py | 76 +++++++++++++++++++ InvenTree/order/models.py | 46 ++++++++++- 2 files changed, 120 insertions(+), 2 deletions(-) create mode 100644 InvenTree/order/migrations/0020_auto_20200420_0940.py diff --git a/InvenTree/order/migrations/0020_auto_20200420_0940.py b/InvenTree/order/migrations/0020_auto_20200420_0940.py new file mode 100644 index 0000000000..59431353dd --- /dev/null +++ b/InvenTree/order/migrations/0020_auto_20200420_0940.py @@ -0,0 +1,76 @@ +# Generated by Django 3.0.5 on 2020-04-20 09:40 + +import InvenTree.fields +import InvenTree.models +from django.conf import settings +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion +import markdownx.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('company', '0021_remove_supplierpart_manufacturer_name'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('order', '0019_purchaseorder_supplier_reference'), + ] + + operations = [ + migrations.CreateModel( + name='SalesOrder', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('reference', models.CharField(help_text='Order reference', max_length=64, unique=True)), + ('description', models.CharField(help_text='Order description', max_length=250)), + ('link', models.URLField(blank=True, help_text='Link to external page')), + ('creation_date', models.DateField(blank=True, null=True)), + ('status', models.PositiveIntegerField(choices=[(10, 'Pending'), (20, 'Placed'), (30, 'Complete'), (40, 'Cancelled'), (50, 'Lost'), (60, 'Returned')], default=10, help_text='Order status')), + ('issue_date', models.DateField(blank=True, null=True)), + ('complete_date', models.DateField(blank=True, null=True)), + ('notes', markdownx.models.MarkdownxField(blank=True, help_text='Order notes')), + ('customer_reference', models.CharField(blank=True, help_text='Customer order reference code', max_length=64)), + ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ('customer', models.ForeignKey(help_text='Customer', limit_choices_to={True, 'is_supplier'}, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sales_orders', to='company.Company')), + ], + options={ + 'abstract': False, + }, + ), + migrations.AlterField( + model_name='purchaseorder', + name='supplier', + field=models.ForeignKey(help_text='Supplier', limit_choices_to={'is_supplier': True}, on_delete=django.db.models.deletion.CASCADE, related_name='purchase_orders', to='company.Company'), + ), + migrations.AlterField( + model_name='purchaseorder', + name='supplier_reference', + field=models.CharField(blank=True, help_text='Supplier order reference code', max_length=64), + ), + migrations.CreateModel( + name='SalesOrderLineItem', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('quantity', InvenTree.fields.RoundingDecimalField(decimal_places=5, default=1, help_text='Item quantity', max_digits=15, validators=[django.core.validators.MinValueValidator(0)])), + ('reference', models.CharField(blank=True, help_text='Line item reference', max_length=100)), + ('notes', models.CharField(blank=True, help_text='Line item notes', max_length=500)), + ('order', models.ForeignKey(help_text='Sales Order', on_delete=django.db.models.deletion.CASCADE, related_name='lines', to='order.SalesOrder')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='SalesOrderAttachment', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('attachment', models.FileField(help_text='Select file to attach', upload_to=InvenTree.models.rename_attachment)), + ('comment', models.CharField(help_text='File comment', max_length=100)), + ('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='attachments', to='order.SalesOrder')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index 3a7d65abac..77ea885b5c 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -117,6 +117,7 @@ class PurchaseOrder(Order): Attributes: supplier: Reference to the company supplying the goods in the order + supplier_reference: Optional field for supplier order reference code received_by: User that received the goods """ @@ -128,10 +129,10 @@ class PurchaseOrder(Order): 'is_supplier': True, }, related_name='purchase_orders', - help_text=_('Company') + help_text=_('Supplier') ) - supplier_reference = models.CharField(max_length=64, blank=True, help_text=_("Supplier order reference")) + supplier_reference = models.CharField(max_length=64, blank=True, help_text=_("Supplier order reference code")) received_by = models.ForeignKey( User, @@ -244,6 +245,26 @@ class PurchaseOrder(Order): self.complete_order() # This will save the model +class SalesOrder(Order): + """ + A SalesOrder represents a list of goods shipped outwards to a customer. + + Attributes: + customer: Reference to the company receiving the goods in the order + customer_reference: Optional field for customer order reference code + """ + + customer = models.ForeignKey(Company, + on_delete=models.SET_NULL, + null=True, + limit_choices_to={'is_supplier', True}, + related_name='sales_orders', + help_text=_("Customer"), + ) + + customer_reference = models.CharField(max_length=64, blank=True, help_text=_("Customer order reference code")) + + class PurchaseOrderAttachment(InvenTreeAttachment): """ Model for storing file attachments against a PurchaseOrder object @@ -255,6 +276,17 @@ class PurchaseOrderAttachment(InvenTreeAttachment): order = models.ForeignKey(PurchaseOrder, on_delete=models.CASCADE, related_name="attachments") +class SalesOrderAttachment(InvenTreeAttachment): + """ + Model for storing file attachments against a SalesOrder object + """ + + def getSubDir(self): + return os.path.join("so_files", str(self.order.id)) + + order = models.ForeignKey(SalesOrder, on_delete=models.CASCADE, related_name='attachments') + + class OrderLineItem(models.Model): """ Abstract model for an order line item @@ -315,3 +347,13 @@ class PurchaseOrderLineItem(OrderLineItem): """ Calculate the number of items remaining to be received """ r = self.quantity - self.received return max(r, 0) + + +class SalesOrderLineItem(OrderLineItem): + """ + Model for a single LineItem in a SalesOrder + """ + + order = models.ForeignKey(SalesOrder, on_delete=models.CASCADE, related_name='lines', help_text=_('Sales Order')) + + # TODO - Add link for part items From 5901b21e787b2b225b1b3eb81e80fb02b94d5ed9 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 20 Apr 2020 19:47:29 +1000 Subject: [PATCH 004/104] UI elements - Add a "sales order" menu item to the main navbar - Add a "sales order" tab to customer detail page --- ...ail_purchase_orders.html => purchase_orders.html} | 3 ++- .../company/templates/company/sales_orders.html | 12 ++++++++++++ InvenTree/company/templates/company/tabs.html | 4 +--- InvenTree/company/urls.py | 3 ++- InvenTree/templates/navbar.html | 1 + 5 files changed, 18 insertions(+), 5 deletions(-) rename InvenTree/company/templates/company/{detail_purchase_orders.html => purchase_orders.html} (99%) create mode 100644 InvenTree/company/templates/company/sales_orders.html diff --git a/InvenTree/company/templates/company/detail_purchase_orders.html b/InvenTree/company/templates/company/purchase_orders.html similarity index 99% rename from InvenTree/company/templates/company/detail_purchase_orders.html rename to InvenTree/company/templates/company/purchase_orders.html index c83bb90eb1..3f940a93e7 100644 --- a/InvenTree/company/templates/company/detail_purchase_orders.html +++ b/InvenTree/company/templates/company/purchase_orders.html @@ -1,8 +1,9 @@ {% extends "company/company_base.html" %} {% load static %} -{% block details %} {% load i18n %} +{% block details %} + {% include 'company/tabs.html' with tab='po' %}

{% trans "Purchase Orders" %}

diff --git a/InvenTree/company/templates/company/sales_orders.html b/InvenTree/company/templates/company/sales_orders.html new file mode 100644 index 0000000000..57181b0667 --- /dev/null +++ b/InvenTree/company/templates/company/sales_orders.html @@ -0,0 +1,12 @@ +{% extends "company/company_base.html" %} +{% load static %} +{% load i18n %} + +{% block details %} + +{% include 'company/tabs.html' with tab='co' %} + +

{% trans "Sales Orders" %}

+
+ +{% endblock %} \ No newline at end of file diff --git a/InvenTree/company/templates/company/tabs.html b/InvenTree/company/templates/company/tabs.html index ea61c40574..8e01bf30c0 100644 --- a/InvenTree/company/templates/company/tabs.html +++ b/InvenTree/company/templates/company/tabs.html @@ -18,12 +18,10 @@ {% endif %} {% if company.is_customer %} - {% if 0 %} - {% trans "Sales Orders" %} + {% trans "Sales Orders" %} {{ company.sales_orders.count }} {% endif %} - {% endif %} {% trans "Notes" %}{% if company.notes %} {% endif %} diff --git a/InvenTree/company/urls.py b/InvenTree/company/urls.py index 3140b7c2d7..af8e1846e1 100644 --- a/InvenTree/company/urls.py +++ b/InvenTree/company/urls.py @@ -15,7 +15,8 @@ company_detail_urls = [ url(r'parts/', views.CompanyDetail.as_view(template_name='company/detail_part.html'), name='company-detail-parts'), url(r'stock/?', views.CompanyDetail.as_view(template_name='company/detail_stock.html'), name='company-detail-stock'), - url(r'purchase-orders/?', views.CompanyDetail.as_view(template_name='company/detail_purchase_orders.html'), name='company-detail-purchase-orders'), + url(r'purchase-orders/?', views.CompanyDetail.as_view(template_name='company/purchase_orders.html'), name='company-detail-purchase-orders'), + url(r'sales-orders/?', views.CompanyDetail.as_view(template_name='company/sales_orders.html'), name='company-detail-sales-orders'), url(r'notes/?', views.CompanyNotes.as_view(), name='company-notes'), url(r'thumbnail/?', views.CompanyImage.as_view(), name='company-image'), diff --git a/InvenTree/templates/navbar.html b/InvenTree/templates/navbar.html index c41ef3718f..3e87faa869 100644 --- a/InvenTree/templates/navbar.html +++ b/InvenTree/templates/navbar.html @@ -22,6 +22,7 @@ {% trans "Sell" %} From 9f97d81e834a06fb0a1f6a527c37f98161ef73de Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 20 Apr 2020 20:11:21 +1000 Subject: [PATCH 005/104] API endpoint for serializing SalesOrder objects --- InvenTree/InvenTree/api.py | 5 +- InvenTree/InvenTree/urls.py | 4 +- InvenTree/order/api.py | 129 +++++++++++++++++- .../migrations/0021_auto_20200420_1010.py | 20 +++ InvenTree/order/models.py | 2 +- InvenTree/order/serializers.py | 58 +++++++- 6 files changed, 206 insertions(+), 12 deletions(-) create mode 100644 InvenTree/order/migrations/0021_auto_20200420_1010.py diff --git a/InvenTree/InvenTree/api.py b/InvenTree/InvenTree/api.py index d68ecd67ad..bfcc02794e 100644 --- a/InvenTree/InvenTree/api.py +++ b/InvenTree/InvenTree/api.py @@ -18,9 +18,10 @@ from .version import inventreeVersion, inventreeInstanceName from plugins import plugins as inventree_plugins # Load barcode plugins -print("INFO: Loading plugins") - +print("Loading barcode plugins") barcode_plugins = inventree_plugins.load_barcode_plugins() + +print("Loading action plugins") action_plugins = inventree_plugins.load_action_plugins() diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index d9600333f4..7d9633ced6 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -25,7 +25,7 @@ from part.api import part_api_urls, bom_api_urls from company.api import company_api_urls from stock.api import stock_api_urls from build.api import build_api_urls -from order.api import po_api_urls +from order.api import order_api_urls from django.conf import settings from django.conf.urls.static import static @@ -49,7 +49,7 @@ apipatterns = [ url(r'^company/', include(company_api_urls)), url(r'^stock/', include(stock_api_urls)), url(r'^build/', include(build_api_urls)), - url(r'^po/', include(po_api_urls)), + url(r'^order/', include(order_api_urls)), # User URLs url(r'^user/', include(user_urls)), diff --git a/InvenTree/order/api.py b/InvenTree/order/api.py index 1dd0657930..0a7426b394 100644 --- a/InvenTree/order/api.py +++ b/InvenTree/order/api.py @@ -19,9 +19,12 @@ from company.models import SupplierPart from .models import PurchaseOrder, PurchaseOrderLineItem from .serializers import POSerializer, POLineItemSerializer +from .models import SalesOrder, SalesOrderLineItem +from .serializers import SalseOrderSerializer + class POList(generics.ListCreateAPIView): - """ API endpoint for accessing a list of Order objects + """ API endpoint for accessing a list of PurchaseOrder objects - GET: Return list of PO objects (with filters) - POST: Create a new PurchaseOrder object @@ -184,10 +187,124 @@ class POLineItemDetail(generics.RetrieveUpdateAPIView): ] -po_api_urls = [ - url(r'^order/(?P\d+)/?$', PODetail.as_view(), name='api-po-detail'), - url(r'^order/?$', POList.as_view(), name='api-po-list'), +class SOList(generics.ListCreateAPIView): + """ + API endpoint for accessing a list of SalesOrder objects. - url(r'^line/(?P\d+)/?$', POLineItemDetail.as_view(), name='api-po-line-detail'), - url(r'^line/?$', POLineItemList.as_view(), name='api-po-line-list'), + - GET: Return list of SO objects (with filters) + - POST: Create a new SalesOrder + """ + + queryset = SalesOrder.objects.all() + serializer_class = SalseOrderSerializer + + def get_serializer(self, *args, **kwargs): + + try: + kwargs['customer_detail'] = str2bool(self.request.query_params.get('customer_detail', False)) + except AttributeError: + pass + + # Ensure the context is passed through to the serializer + kwargs['context'] = self.get_serializer_context() + + return self.serializer_class(*args, **kwargs) + + def get_queryset(self, *args, **kwargs): + + queryset = super().get_queryset(*args, **kwargs) + + queryset = queryset.prefetch_related( + 'customer', + 'lines' + ) + + queryset = SalseOrderSerializer.annotate_queryset(queryset) + + return queryset + + def filter_queryset(self, queryset): + """ + Perform custom filtering operations on the SalesOrder queryset. + """ + + queryset = super().filter_queryset(queryset) + + params = self.request.query_params + + status = params.get('status', None) + + if status is not None: + queryset = queryset.filter(status=status) + + # TODO - Filter by part / stockitem / etc + + return queryset + + permission_classes = [ + permissions.IsAuthenticated + ] + + filter_backends = [ + DjangoFilterBackend, + filters.SearchFilter, + filters.OrderingFilter, + ] + + filter_fields = [ + 'customer', + ] + + ordering_fields = [ + 'creation_date', + 'reference' + ] + + ordering = '-creation_date' + + +class SODetail(generics.RetrieveUpdateAPIView): + """ + API endpoint for detail view of a SalesOrder object. + """ + + queryset = SalesOrder.objects.all() + serializer_class = SalseOrderSerializer + + def get_serializer(self, *args, **kwargs): + + try: + kwargs['customer_detail'] = str2bool(self.request.query_params.get('customer_detail', False)) + except AttributeError: + pass + + kwargs['context'] = self.get_serializer_context() + + return self.serializer_class(*args, **kwargs) + + def get_queryset(self, *args, **kwargs): + + queryset = super().get_queryset(*args, **kwargs) + + queryset = queryset.prefetch_related('customer', 'lines') + + queryset = SalseOrderSerializer.annotate_queryset(queryset) + + return queryset + + permission_classes = [permissions.IsAuthenticated] + + +order_api_urls = [ + # API endpoints for purchase orders + url(r'^po/(?P\d+)/$', PODetail.as_view(), name='api-po-detail'), + url(r'^po/$', POList.as_view(), name='api-po-list'), + + # API endpoints for purchase order line items + url(r'^po-line/(?P\d+)/$', POLineItemDetail.as_view(), name='api-po-line-detail'), + url(r'^po-line/$', POLineItemList.as_view(), name='api-po-line-list'), + + # API endpoints for sales ordesr + url(r'^so/(?P\d+)/$', SODetail.as_view(), name='api-so-detail'), + url(r'^so/$', SOList.as_view(), name='api-so-list'), ] diff --git a/InvenTree/order/migrations/0021_auto_20200420_1010.py b/InvenTree/order/migrations/0021_auto_20200420_1010.py new file mode 100644 index 0000000000..0f8351b660 --- /dev/null +++ b/InvenTree/order/migrations/0021_auto_20200420_1010.py @@ -0,0 +1,20 @@ +# Generated by Django 3.0.5 on 2020-04-20 10:10 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('company', '0021_remove_supplierpart_manufacturer_name'), + ('order', '0020_auto_20200420_0940'), + ] + + operations = [ + migrations.AlterField( + model_name='salesorder', + name='customer', + field=models.ForeignKey(help_text='Customer', limit_choices_to={'is_customer': True}, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sales_orders', to='company.Company'), + ), + ] diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index 77ea885b5c..544c54ec7b 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -257,7 +257,7 @@ class SalesOrder(Order): customer = models.ForeignKey(Company, on_delete=models.SET_NULL, null=True, - limit_choices_to={'is_supplier', True}, + limit_choices_to={'is_customer': True}, related_name='sales_orders', help_text=_("Customer"), ) diff --git a/InvenTree/order/serializers.py b/InvenTree/order/serializers.py index 935166e82a..9171a093a6 100644 --- a/InvenTree/order/serializers.py +++ b/InvenTree/order/serializers.py @@ -13,10 +13,11 @@ from InvenTree.serializers import InvenTreeModelSerializer from company.serializers import CompanyBriefSerializer from .models import PurchaseOrder, PurchaseOrderLineItem +from .models import SalesOrder, SalesOrderLineItem class POSerializer(InvenTreeModelSerializer): - """ Serializes an Order object """ + """ Serializer for a PurchaseOrder object """ def __init__(self, *args, **kwargs): @@ -83,3 +84,58 @@ class POLineItemSerializer(InvenTreeModelSerializer): 'part', 'received', ] + + +class SalseOrderSerializer(InvenTreeModelSerializer): + """ + Serializers for the SalesOrder object + """ + + def __init__(self, *args, **kwargs): + + customer_detail = kwargs.pop('customer_detail', False) + + super().__init__(*args, **kwargs) + + if customer_detail is not True: + self.fields.pop('customer_detail') + + @staticmethod + def annotate_queryset(queryset): + """ + Add extra information to the queryset + """ + + return queryset.annotate( + line_items=Count('lines'), + ) + + customer_detail = CompanyBriefSerializer(source='customer', many=False, read_only=True) + + line_items = serializers.IntegerField(read_only=True) + + status_text = serializers.CharField(source='get_status_display', read_only=True) + + class Meta: + model = SalesOrder + + fields = [ + 'pk', + 'issue_date', + 'complete_date', + 'creation_date', + 'line_items', + 'link', + 'reference', + 'customer', + 'customer_detail', + 'customer_reference', + 'status', + 'status_text', + 'notes', + ] + + read_only_fields = [ + 'reference', + 'status' + ] From c7fd22924f5544ed7b094f1ae024664476fa14d6 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 20 Apr 2020 20:27:35 +1000 Subject: [PATCH 006/104] Register salesorder classes in the admin interface --- InvenTree/order/admin.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/InvenTree/order/admin.py b/InvenTree/order/admin.py index 05598928a7..5ad1a6541d 100644 --- a/InvenTree/order/admin.py +++ b/InvenTree/order/admin.py @@ -9,6 +9,7 @@ from import_export.resources import ModelResource from import_export.fields import Field from .models import PurchaseOrder, PurchaseOrderLineItem +from .models import SalesOrder, SalesOrderLineItem class PurchaseOrderAdmin(ImportExportModelAdmin): @@ -22,6 +23,17 @@ class PurchaseOrderAdmin(ImportExportModelAdmin): ) +class SalesOrderAdmin(ImportExportModelAdmin): + + list_display = ( + 'reference', + 'customer', + 'status', + 'description', + 'creation_date', + ) + + class POLineItemResource(ModelResource): """ Class for managing import / export of POLineItem data """ @@ -40,6 +52,16 @@ class POLineItemResource(ModelResource): clean_model_instances = True +class SOLineItemResource(ModelResource): + """ Class for managing import / export of SOLineItem data """ + + class Meta: + model = SalesOrderLineItem + skip_unchanged = True + report_skipped = False + clean_model_instances = True + + class PurchaseOrderLineItemAdmin(ImportExportModelAdmin): resource_class = POLineItemResource @@ -52,5 +74,19 @@ class PurchaseOrderLineItemAdmin(ImportExportModelAdmin): ) +class SalesOrderLineItemAdmin(ImportExportModelAdmin): + + resource_class = SOLineItemResource + + list_display = ( + 'order', + 'quantity', + 'reference' + ) + + admin.site.register(PurchaseOrder, PurchaseOrderAdmin) admin.site.register(PurchaseOrderLineItem, PurchaseOrderLineItemAdmin) + +admin.site.register(SalesOrder, SalesOrderAdmin) +admin.site.register(SalesOrderLineItem, SalesOrderLineItemAdmin) From 627c50e465ec7b4a4e1ea7a49b6ffb42f7068205 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 20 Apr 2020 20:27:52 +1000 Subject: [PATCH 007/104] Render a table of sales orders --- .../static/script/inventree/order.js | 68 +++++++++++++++++++ .../templates/company/purchase_orders.html | 2 +- .../templates/company/sales_orders.html | 26 +++++++ InvenTree/order/serializers.py | 1 + 4 files changed, 96 insertions(+), 1 deletion(-) diff --git a/InvenTree/InvenTree/static/script/inventree/order.js b/InvenTree/InvenTree/static/script/inventree/order.js index d583ebcdc4..b48c0f4e64 100644 --- a/InvenTree/InvenTree/static/script/inventree/order.js +++ b/InvenTree/InvenTree/static/script/inventree/order.js @@ -144,11 +144,74 @@ function loadPurchaseOrderTable(table, options) { return imageHoverIcon(row.supplier_detail.image) + renderLink(row.supplier_detail.name, `/company/${row.supplier}/purchase-orders/`); } }, + { + sortable: true, + field: 'description', + title: 'Description', + }, + { + sortable: true, + field: 'status', + title: 'Status', + formatter: function(value, row, index, field) { + return orderStatusDisplay(row.status, row.status_text); + } + }, { sortable: true, field: 'creation_date', title: 'Date', }, + { + sortable: true, + field: 'line_items', + title: 'Items' + }, + ], + }); +} + +function loadSalesOrderTable(table, options) { + + options.params = options.params || {}; + options.params['customer_detail'] = true; + + var filters = loadTableFilters("table"); + + for (var key in options.params) { + filters[key] = options.params[key]; + } + + setupFilterList("order", $(table)); + + $(table).inventreeTable({ + url: options.url, + queryParams: filters, + groupBy: false, + original: options.params, + formatNoMatches: function() { return "No sales orders found"; }, + columns: [ + { + field: 'pk', + title: 'ID', + visible: false, + }, + { + sortable: true, + field: 'reference', + title: 'Sales Order', + formatter: function(value, row, index, field) { + return renderLink(value, `/order/sales-order/${row.pk}/`); + }, + }, + { + sortable: true, + field: 'customer_detail', + title: 'Customer', + formatter: function(value, row, index, field) { + return imageHoverIcon(row.customer_detail.image) + renderLink(row.customer_detail.name, `/company/${row.customer}/sales-orders/`); + } + }, { sortable: true, field: 'description', @@ -162,6 +225,11 @@ function loadPurchaseOrderTable(table, options) { return orderStatusDisplay(row.status, row.status_text); } }, + { + sortable: true, + field: 'creation_date', + title: 'Date', + }, { sortable: true, field: 'line_items', diff --git a/InvenTree/company/templates/company/purchase_orders.html b/InvenTree/company/templates/company/purchase_orders.html index 3f940a93e7..eac4066f2d 100644 --- a/InvenTree/company/templates/company/purchase_orders.html +++ b/InvenTree/company/templates/company/purchase_orders.html @@ -11,7 +11,7 @@
- +
diff --git a/InvenTree/company/templates/company/sales_orders.html b/InvenTree/company/templates/company/sales_orders.html index 57181b0667..7d5c7684bc 100644 --- a/InvenTree/company/templates/company/sales_orders.html +++ b/InvenTree/company/templates/company/sales_orders.html @@ -9,4 +9,30 @@

{% trans "Sales Orders" %}


+
+
+ +
+ +
+
+
+ + +
+ +{% endblock %} + +{% block js_ready %} +{{ block.super }} + + loadSalesOrderTable("#sales-order-table", { + url: "{% url 'api-so-list' %}?customer={{ company.id }}", + }); + + + $("#new-sales-order").click(function() { + // TODO - Create a new sales order + }); + {% endblock %} \ No newline at end of file diff --git a/InvenTree/order/serializers.py b/InvenTree/order/serializers.py index 9171a093a6..a1845b4c71 100644 --- a/InvenTree/order/serializers.py +++ b/InvenTree/order/serializers.py @@ -124,6 +124,7 @@ class SalseOrderSerializer(InvenTreeModelSerializer): 'issue_date', 'complete_date', 'creation_date', + 'description', 'line_items', 'link', 'reference', From 1ebf26ab7c2e3758993f08cfaf6c2fd23fd8d3e3 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 20 Apr 2020 20:40:45 +1000 Subject: [PATCH 008/104] Add page for displaying all sales orders --- .../templates/company/purchase_orders.html | 5 ++- .../templates/company/sales_orders.html | 5 ++- .../templates/order/purchase_orders.html | 6 ++-- .../order/templates/order/sales_orders.html | 36 +++++++++++++++++++ InvenTree/order/urls.py | 5 +++ InvenTree/order/views.py | 8 +++++ 6 files changed, 60 insertions(+), 5 deletions(-) create mode 100644 InvenTree/order/templates/order/sales_orders.html diff --git a/InvenTree/company/templates/company/purchase_orders.html b/InvenTree/company/templates/company/purchase_orders.html index eac4066f2d..aa30dcd4d9 100644 --- a/InvenTree/company/templates/company/purchase_orders.html +++ b/InvenTree/company/templates/company/purchase_orders.html @@ -27,7 +27,10 @@ {{ block.super }} loadPurchaseOrderTable("#purchase-order-table", { - url: "{% url 'api-po-list' %}?supplier={{ company.id }}", + url: "{% url 'api-po-list' %}", + params: { + supplier: company.id, + } }); diff --git a/InvenTree/company/templates/company/sales_orders.html b/InvenTree/company/templates/company/sales_orders.html index 7d5c7684bc..b19ba563b2 100644 --- a/InvenTree/company/templates/company/sales_orders.html +++ b/InvenTree/company/templates/company/sales_orders.html @@ -27,7 +27,10 @@ {{ block.super }} loadSalesOrderTable("#sales-order-table", { - url: "{% url 'api-so-list' %}?customer={{ company.id }}", + url: "{% url 'api-so-list' %}", + params: { + customer: {{ company.id }}, + } }); diff --git a/InvenTree/order/templates/order/purchase_orders.html b/InvenTree/order/templates/order/purchase_orders.html index 54d57d2d8c..5e77c01181 100644 --- a/InvenTree/order/templates/order/purchase_orders.html +++ b/InvenTree/order/templates/order/purchase_orders.html @@ -4,17 +4,17 @@ {% load i18n %} {% block page_title %} -InvenTree | Purchase Orders +InvenTree | {% trans "Purchase Orders" %} {% endblock %} {% block content %} -

Purchase Orders

+

{% trans "Purchase Orders" %}


- +
diff --git a/InvenTree/order/templates/order/sales_orders.html b/InvenTree/order/templates/order/sales_orders.html new file mode 100644 index 0000000000..d8d5e22d4d --- /dev/null +++ b/InvenTree/order/templates/order/sales_orders.html @@ -0,0 +1,36 @@ +{% extends "base.html" %} + +{% load static %} +{% load i18n %} + +{% block page_title %} +InvenTree | {% trans "Sales Orders" %} +{% endblock %} + +{% block content %} + +

{% trans "Sales Orders" %}

+
+ +
+
+ +
+ +
+
+
+ + +
+ +{% endblock %} + +{% block js_ready %} +{{ block.super }} + +loadSalesOrderTable("#sales-order-table", { + url: "{% url 'api-so-list' %}", +}); + +{% endblock %} \ No newline at end of file diff --git a/InvenTree/order/urls.py b/InvenTree/order/urls.py index 3346d7c44d..a81cfcf239 100644 --- a/InvenTree/order/urls.py +++ b/InvenTree/order/urls.py @@ -61,6 +61,11 @@ purchase_order_urls = [ url(r'^.*$', views.PurchaseOrderIndex.as_view(), name='po-index'), ] +sales_order_urls = [ + url(r'^.*$', views.SalesOrderIndex.as_view(), name='so-index'), +] + order_urls = [ url(r'^purchase-order/', include(purchase_order_urls)), + url(r'^sales-order/', include(sales_order_urls)), ] diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index 67d5b926d7..9e556e0321 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -16,6 +16,7 @@ import logging from decimal import Decimal, InvalidOperation from .models import PurchaseOrder, PurchaseOrderLineItem, PurchaseOrderAttachment +from .models import SalesOrder, SalesOrderLineItem, SalesOrderAttachment from .admin import POLineItemResource from build.models import Build from company.models import Company, SupplierPart @@ -55,6 +56,13 @@ class PurchaseOrderIndex(ListView): return ctx +class SalesOrderIndex(ListView): + + model = SalesOrder + template_name = 'order/sales_orders.html' + context_object_name = 'orders' + + class PurchaseOrderDetail(DetailView): """ Detail view for a PurchaseOrder object """ From 47ada25315b9b051214cf60b28920a0634ab8cbc Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 20 Apr 2020 20:59:14 +1000 Subject: [PATCH 009/104] Add detail view for SalesOrder --- .../order/templates/order/order_base.html | 2 +- .../order/templates/order/order_notes.html | 2 +- .../order/templates/order/po_attachments.html | 2 +- .../order/{tabs.html => po_tabs.html} | 0 .../order/purchase_order_detail.html | 2 +- .../templates/order/sales_order_base.html | 110 ++++++++++++++++++ .../templates/order/sales_order_detail.html | 19 +++ InvenTree/order/templates/order/so_tabs.html | 17 +++ InvenTree/order/urls.py | 10 ++ InvenTree/order/views.py | 8 ++ 10 files changed, 168 insertions(+), 4 deletions(-) rename InvenTree/order/templates/order/{tabs.html => po_tabs.html} (100%) create mode 100644 InvenTree/order/templates/order/sales_order_base.html create mode 100644 InvenTree/order/templates/order/sales_order_detail.html create mode 100644 InvenTree/order/templates/order/so_tabs.html diff --git a/InvenTree/order/templates/order/order_base.html b/InvenTree/order/templates/order/order_base.html index 03aa4c4ce2..7576f061bc 100644 --- a/InvenTree/order/templates/order/order_base.html +++ b/InvenTree/order/templates/order/order_base.html @@ -6,7 +6,7 @@ {% load status_codes %} {% block page_title %} -InvenTree | {{ order }} +InvenTree | {% trans "Purchase Order" %} {% endblock %} {% block content %} diff --git a/InvenTree/order/templates/order/order_notes.html b/InvenTree/order/templates/order/order_notes.html index de4f18ba6b..1d2c19c6cb 100644 --- a/InvenTree/order/templates/order/order_notes.html +++ b/InvenTree/order/templates/order/order_notes.html @@ -7,7 +7,7 @@ {% block details %} -{% include 'order/tabs.html' with tab='notes' %} +{% include 'order/po_tabs.html' with tab='notes' %} {% if editing %}

{% trans "Order Notes" %}

diff --git a/InvenTree/order/templates/order/po_attachments.html b/InvenTree/order/templates/order/po_attachments.html index 173b0e1fb6..e8e2b4bbff 100644 --- a/InvenTree/order/templates/order/po_attachments.html +++ b/InvenTree/order/templates/order/po_attachments.html @@ -6,7 +6,7 @@ {% block details %} -{% include 'order/tabs.html' with tab='attachments' %} +{% include 'order/po_tabs.html' with tab='attachments' %}

{% trans "Purchase Order Attachments" %} diff --git a/InvenTree/order/templates/order/tabs.html b/InvenTree/order/templates/order/po_tabs.html similarity index 100% rename from InvenTree/order/templates/order/tabs.html rename to InvenTree/order/templates/order/po_tabs.html diff --git a/InvenTree/order/templates/order/purchase_order_detail.html b/InvenTree/order/templates/order/purchase_order_detail.html index 8196ada70d..5cea64ff53 100644 --- a/InvenTree/order/templates/order/purchase_order_detail.html +++ b/InvenTree/order/templates/order/purchase_order_detail.html @@ -7,7 +7,7 @@ {% block details %} -{% include 'order/tabs.html' with tab='details' %} +{% include 'order/po_tabs.html' with tab='details' %}
diff --git a/InvenTree/order/templates/order/sales_order_base.html b/InvenTree/order/templates/order/sales_order_base.html new file mode 100644 index 0000000000..adaed2b456 --- /dev/null +++ b/InvenTree/order/templates/order/sales_order_base.html @@ -0,0 +1,110 @@ +{% extends "base.html" %} + +{% load i18n %} +{% load static %} +{% load inventree_extras %} +{% load status_codes %} + +{% block page_title %} +InvenTree | {% trans "Sales Order" %} +{% endblock %} + +{% block content %} + + +
+
+
+
+ +
+
+

{{ order }}

+

{{ order.description }}

+

+

+
+ +
+
+

+
+
+
+
+

{% trans "Sales Order Details" %}

+ + + + + + + + + + + + + + + + + + {% if order.customer_reference %} + + + + + + {% endif %} + {% if order.link %} + + + + + + {% endif %} + + + + + + {% if order.issue_date %} + + + + + + {% endif %} + {% if order.status == OrderStatus.COMPLETE %} + + + + + + {% endif %} +
{% trans "Order Reference" %}{{ order.reference }}
{% trans "Order Status" %}{% order_status order.status %}
{% trans "Customer" %}{{ order.customer.name }}
{% trans "Customer Reference" %}{{ order.customer_reference }}
External Link{{ order.link }}
{% trans "Created" %}{{ order.creation_date }}{{ order.created_by }}
{% trans "Issued" %}{{ order.issue_date }}
{% trans "Received" %}{{ order.complete_date }}{{ order.received_by }}
+
+
+ +
+
+ {% block details %} + + {% endblock %} +
+ +{% endblock %} + +{% block js_ready %} +{{ block.super }} + +{% endblock %} \ No newline at end of file diff --git a/InvenTree/order/templates/order/sales_order_detail.html b/InvenTree/order/templates/order/sales_order_detail.html new file mode 100644 index 0000000000..3dc02363af --- /dev/null +++ b/InvenTree/order/templates/order/sales_order_detail.html @@ -0,0 +1,19 @@ +{% extends "order/sales_order_base.html" %} + +{% load inventree_extras %} +{% load status_codes %} +{% load i18n %} +{% load static %} + +{% block details %} + +{% include 'order/so_tabs.html' with tab='details' %} + +
+ +{% endblock %} + +{% block js_ready %} +{{ block.super }} + +{% endblock %} \ No newline at end of file diff --git a/InvenTree/order/templates/order/so_tabs.html b/InvenTree/order/templates/order/so_tabs.html new file mode 100644 index 0000000000..2bb313b0bb --- /dev/null +++ b/InvenTree/order/templates/order/so_tabs.html @@ -0,0 +1,17 @@ +{% load i18n %} + + \ No newline at end of file diff --git a/InvenTree/order/urls.py b/InvenTree/order/urls.py index a81cfcf239..e9a68f16a7 100644 --- a/InvenTree/order/urls.py +++ b/InvenTree/order/urls.py @@ -61,7 +61,17 @@ purchase_order_urls = [ url(r'^.*$', views.PurchaseOrderIndex.as_view(), name='po-index'), ] +sales_order_detail_urls = [ + + url(r'^.*$', views.SalesOrderDetail.as_view(), name='so-detail'), +] + sales_order_urls = [ + + # Display detail view for a single SalesOrder + url(r'^(?P\d+)/', include(sales_order_detail_urls)), + + # Display list of all sales orders url(r'^.*$', views.SalesOrderIndex.as_view(), name='so-index'), ] diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index 9e556e0321..5a2f0e1920 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -78,6 +78,14 @@ class PurchaseOrderDetail(DetailView): return ctx +class SalesOrderDetail(DetailView): + """ Detail view for a SalesOrder object """ + + context_object_name = 'order' + queryset = SalesOrder.objects.all().prefetch_related('lines') + template_name = 'order/sales_order_detail.html' + + class PurchaseOrderAttachmentCreate(AjaxCreateView): """ View for creating a new PurchaseOrderAtt From b2569d5cba3a88ce6cf714c156fee9fa6511ab41 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 20 Apr 2020 21:11:59 +1000 Subject: [PATCH 010/104] Expose SalesOrderLineItem objects to the REST API --- InvenTree/order/api.py | 36 +++++++++++++++++++++++++++++++--- InvenTree/order/serializers.py | 15 ++++++++++++++ 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/InvenTree/order/api.py b/InvenTree/order/api.py index 0a7426b394..6d6d16ff6f 100644 --- a/InvenTree/order/api.py +++ b/InvenTree/order/api.py @@ -20,7 +20,7 @@ from .models import PurchaseOrder, PurchaseOrderLineItem from .serializers import POSerializer, POLineItemSerializer from .models import SalesOrder, SalesOrderLineItem -from .serializers import SalseOrderSerializer +from .serializers import SalseOrderSerializer, SOLineItemSerializer class POList(generics.ListCreateAPIView): @@ -153,7 +153,7 @@ class PODetail(generics.RetrieveUpdateAPIView): class POLineItemList(generics.ListCreateAPIView): - """ API endpoint for accessing a list of PO Line Item objects + """ API endpoint for accessing a list of POLineItem objects - GET: Return a list of PO Line Item objects - POST: Create a new PurchaseOrderLineItem object @@ -293,7 +293,33 @@ class SODetail(generics.RetrieveUpdateAPIView): return queryset permission_classes = [permissions.IsAuthenticated] - + + +class SOLineItemList(generics.ListCreateAPIView): + """ + API endpoint for accessing a list of SalesOrderLineItem objects. + """ + + queryset = SalesOrderLineItem.objects.all() + serializer_class = SOLineItemSerializer + + permission_classes = [permissions.IsAuthenticated] + + filter_backends = [DjangoFilterBackend] + + filter_fields = [ + 'order', + ] + + +class SOLineItemDetail(generics.RetrieveUpdateAPIView): + """ API endpoint for detail view of a SalesOrderLineItem object """ + + queryset = SalesOrderLineItem.objects.all() + serializer_class = SOLineItemSerializer + + permission_classes = [permissions.IsAuthenticated] + order_api_urls = [ # API endpoints for purchase orders @@ -307,4 +333,8 @@ order_api_urls = [ # API endpoints for sales ordesr url(r'^so/(?P\d+)/$', SODetail.as_view(), name='api-so-detail'), url(r'^so/$', SOList.as_view(), name='api-so-list'), + + # API endpoints for sales order line items + url(r'^so-line/(?P\d+)/$', SOLineItemDetail.as_view(), name='api-so-line-detail'), + url(r'^so-line/$', SOLineItemList.as_view(), name='api-so-line-list'), ] diff --git a/InvenTree/order/serializers.py b/InvenTree/order/serializers.py index a1845b4c71..69c0969c32 100644 --- a/InvenTree/order/serializers.py +++ b/InvenTree/order/serializers.py @@ -140,3 +140,18 @@ class SalseOrderSerializer(InvenTreeModelSerializer): 'reference', 'status' ] + + +class SOLineItemSerializer(InvenTreeModelSerializer): + """ Serializer for a SalesOrderLineItem object """ + + class Meta: + model = SalesOrderLineItem + + fields = [ + 'pk', + 'quantity', + 'reference', + 'notes', + 'order', + ] From ebbcff3c7f67ec40154a2e43a131db22c30171bc Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 20 Apr 2020 21:22:34 +1000 Subject: [PATCH 011/104] Render a table of line items --- .../order/purchase_order_detail.html | 2 +- .../templates/order/sales_order_detail.html | 42 +++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/InvenTree/order/templates/order/purchase_order_detail.html b/InvenTree/order/templates/order/purchase_order_detail.html index 5cea64ff53..444da8f88f 100644 --- a/InvenTree/order/templates/order/purchase_order_detail.html +++ b/InvenTree/order/templates/order/purchase_order_detail.html @@ -17,7 +17,7 @@ {% endif %}

-

{% trans "Order Items" %}

+

{% trans "Purchase Order Items" %}

diff --git a/InvenTree/order/templates/order/sales_order_detail.html b/InvenTree/order/templates/order/sales_order_detail.html index 3dc02363af..e82f234ac3 100644 --- a/InvenTree/order/templates/order/sales_order_detail.html +++ b/InvenTree/order/templates/order/sales_order_detail.html @@ -11,9 +11,51 @@
+

{% trans "Sales Order Items" %}

+ +
+ {% if order.status == OrderStatus.PENDING %} + + {% endif %} +
+ +
+ +
+ {% endblock %} {% block js_ready %} {{ block.super }} +$("#so-lines-table").inventreeTable({ + formatNoMatches: function() { return "No matching line items"; }, + queryParams: { + order: {{ order.id }}, + }, + url: "{% url 'api-so-line-list' %}", + columns: [ + { + field: 'pk', + title: 'ID', + visible: false, + }, + { + field: 'reference', + title: 'Reference' + }, + { + field: 'quantity', + title: 'Quantity', + formatter: function(value, row, index, field) { + return +parseFloat(value).toFixed(5); + } + }, + { + field: 'notes', + title: 'Notes', + }, + ], + }); + {% endblock %} \ No newline at end of file From ce1dd8812996b711b198e9756aa583ac5b01752c Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 20 Apr 2020 22:13:07 +1000 Subject: [PATCH 012/104] Form for creating a new SalesOrder --- .../static/script/inventree/order.js | 10 +++++++ InvenTree/order/forms.py | 16 ++++++++++ InvenTree/order/models.py | 3 ++ .../templates/order/sales_order_detail.html | 2 ++ .../order/templates/order/sales_orders.html | 8 +++++ InvenTree/order/urls.py | 2 ++ InvenTree/order/views.py | 29 +++++++++++++++++++ InvenTree/templates/navbar.html | 2 +- 8 files changed, 71 insertions(+), 1 deletion(-) diff --git a/InvenTree/InvenTree/static/script/inventree/order.js b/InvenTree/InvenTree/static/script/inventree/order.js index b48c0f4e64..1d331491ca 100644 --- a/InvenTree/InvenTree/static/script/inventree/order.js +++ b/InvenTree/InvenTree/static/script/inventree/order.js @@ -144,6 +144,11 @@ function loadPurchaseOrderTable(table, options) { return imageHoverIcon(row.supplier_detail.image) + renderLink(row.supplier_detail.name, `/company/${row.supplier}/purchase-orders/`); } }, + { + field: 'supplier_reference', + title: 'Supplier Reference', + sortable: true, + }, { sortable: true, field: 'description', @@ -212,6 +217,11 @@ function loadSalesOrderTable(table, options) { return imageHoverIcon(row.customer_detail.image) + renderLink(row.customer_detail.name, `/company/${row.customer}/sales-orders/`); } }, + { + field: 'customer_reference', + title: 'Customer Reference', + sotrable: true, + }, { sortable: true, field: 'description', diff --git a/InvenTree/order/forms.py b/InvenTree/order/forms.py index 52c761e03e..56569812cd 100644 --- a/InvenTree/order/forms.py +++ b/InvenTree/order/forms.py @@ -15,6 +15,7 @@ from InvenTree.fields import RoundingDecimalFormField from stock.models import StockLocation from .models import PurchaseOrder, PurchaseOrderLineItem, PurchaseOrderAttachment +from .models import SalesOrder, SalesOrderLineItem class IssuePurchaseOrderForm(HelperForm): @@ -75,6 +76,21 @@ class EditPurchaseOrderForm(HelperForm): ] +class EditSalesOrderForm(HelperForm): + """ Form for editing a SalesOrder object """ + + class Meta: + model = SalesOrder + fields = [ + 'reference', + 'customer', + 'customer_reference', + 'description', + 'link' + ] + + + class EditPurchaseOrderAttachmentForm(HelperForm): """ Form for editing a PurchaseOrderAttachment object """ diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index 544c54ec7b..2d678097d4 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -254,6 +254,9 @@ class SalesOrder(Order): customer_reference: Optional field for customer order reference code """ + def get_absolute_url(self): + return reverse('so-detail', kwargs={'pk': self.id}) + customer = models.ForeignKey(Company, on_delete=models.SET_NULL, null=True, diff --git a/InvenTree/order/templates/order/sales_order_detail.html b/InvenTree/order/templates/order/sales_order_detail.html index e82f234ac3..8a47b9d044 100644 --- a/InvenTree/order/templates/order/sales_order_detail.html +++ b/InvenTree/order/templates/order/sales_order_detail.html @@ -41,10 +41,12 @@ $("#so-lines-table").inventreeTable({ visible: false, }, { + sortable: true, field: 'reference', title: 'Reference' }, { + sortable: true, field: 'quantity', title: 'Quantity', formatter: function(value, row, index, field) { diff --git a/InvenTree/order/templates/order/sales_orders.html b/InvenTree/order/templates/order/sales_orders.html index d8d5e22d4d..394d45b3d8 100644 --- a/InvenTree/order/templates/order/sales_orders.html +++ b/InvenTree/order/templates/order/sales_orders.html @@ -33,4 +33,12 @@ loadSalesOrderTable("#sales-order-table", { url: "{% url 'api-so-list' %}", }); +$("#so-create").click(function() { + launchModalForm("{% url 'so-create' %}", + { + follow: true, + } + ); +}); + {% endblock %} \ No newline at end of file diff --git a/InvenTree/order/urls.py b/InvenTree/order/urls.py index e9a68f16a7..086033db23 100644 --- a/InvenTree/order/urls.py +++ b/InvenTree/order/urls.py @@ -68,6 +68,8 @@ sales_order_detail_urls = [ sales_order_urls = [ + url(r'^new/', views.SalesOrderCreate.as_view(), name='so-create'), + # Display detail view for a single SalesOrder url(r'^(?P\d+)/', include(sales_order_detail_urls)), diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index 5a2f0e1920..cdf3676136 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -216,6 +216,35 @@ class PurchaseOrderCreate(AjaxCreateView): self.object.save() +class SalesOrderCreate(AjaxCreateView): + """ View for creating a new SalesOrder object """ + + model = SalesOrder + ajax_form_title = _("Create Sales Order") + form_class = order_forms.EditSalesOrderForm + + def get_initial(self): + initials = super().get_initial().copy() + + initials['status'] = OrderStatus.PENDING + + customer_id = self.request.GET.get('customer', None) + + if customer_id is not None: + try: + customer = Company.objects.get(id=customer_id) + initials['customer'] = customer + except (Company.DoesNotExist, ValueError): + pass + + return initials + + def post_save(self, **kwargs): + # Record the user who created this sales order + self.object.created_by = self.request.user + self.object.save() + + class PurchaseOrderEdit(AjaxUpdateView): """ View for editing a PurchaseOrder using a modal form """ diff --git a/InvenTree/templates/navbar.html b/InvenTree/templates/navbar.html index 3e87faa869..bb5dcebad7 100644 --- a/InvenTree/templates/navbar.html +++ b/InvenTree/templates/navbar.html @@ -22,7 +22,7 @@ {% trans "Sell" %} From e12824df2e7394fa41106221e4bae18c81b6661f Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 20 Apr 2020 22:20:03 +1000 Subject: [PATCH 013/104] Add form to edit a SalesOrder --- .../templates/order/sales_order_base.html | 6 ++++++ InvenTree/order/urls.py | 14 ++++++++------ InvenTree/order/views.py | 18 ++++++++++++++++++ 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/InvenTree/order/templates/order/sales_order_base.html b/InvenTree/order/templates/order/sales_order_base.html index adaed2b456..3ee3198b28 100644 --- a/InvenTree/order/templates/order/sales_order_base.html +++ b/InvenTree/order/templates/order/sales_order_base.html @@ -107,4 +107,10 @@ InvenTree | {% trans "Sales Order" %} {% block js_ready %} {{ block.super }} +$("#edit-order").click(function() { + launchModalForm("{% url 'so-edit' order.id %}", { + reload: true, + }); +}); + {% endblock %} \ No newline at end of file diff --git a/InvenTree/order/urls.py b/InvenTree/order/urls.py index 086033db23..9a836e4e0b 100644 --- a/InvenTree/order/urls.py +++ b/InvenTree/order/urls.py @@ -17,13 +17,13 @@ purchase_order_attachment_urls = [ purchase_order_detail_urls = [ - url(r'^cancel/?', views.PurchaseOrderCancel.as_view(), name='po-cancel'), - url(r'^edit/?', views.PurchaseOrderEdit.as_view(), name='po-edit'), - url(r'^issue/?', views.PurchaseOrderIssue.as_view(), name='po-issue'), - url(r'^receive/?', views.PurchaseOrderReceive.as_view(), name='po-receive'), - url(r'^complete/?', views.PurchaseOrderComplete.as_view(), name='po-complete'), + url(r'^cancel/', views.PurchaseOrderCancel.as_view(), name='po-cancel'), + url(r'^edit/', views.PurchaseOrderEdit.as_view(), name='po-edit'), + url(r'^issue/', views.PurchaseOrderIssue.as_view(), name='po-issue'), + url(r'^receive/', views.PurchaseOrderReceive.as_view(), name='po-receive'), + url(r'^complete/', views.PurchaseOrderComplete.as_view(), name='po-complete'), - url(r'^export/?', views.PurchaseOrderExport.as_view(), name='po-export'), + url(r'^export/', views.PurchaseOrderExport.as_view(), name='po-export'), url(r'^notes/', views.PurchaseOrderNotes.as_view(), name='po-notes'), @@ -63,6 +63,8 @@ purchase_order_urls = [ sales_order_detail_urls = [ + url(r'^edit/', views.SalesOrderEdit.as_view(), name='so-edit'), + url(r'^.*$', views.SalesOrderDetail.as_view(), name='so-detail'), ] diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index cdf3676136..ed4febcdc8 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -265,6 +265,24 @@ class PurchaseOrderEdit(AjaxUpdateView): return form +class SalesOrderEdit(AjaxUpdateView): + """ View for editing a SalesOrder """ + + model = SalesOrder + ajax_form_title = _('Edit Sales Order') + form_class = order_forms.EditSalesOrderForm + + def get_form(self): + form = super().get_form() + + order = self.get_object() + + # Prevent user from editing customer + form.fields['customer'].widget = HiddenInput() + + return form + + class PurchaseOrderCancel(AjaxUpdateView): """ View for cancelling a purchase order """ From 0c56079b4174fba5641f313c3e977346ee3dcf28 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 20 Apr 2020 22:33:49 +1000 Subject: [PATCH 014/104] Create missing tabs for sales orders - Attachments - Notes --- .../templates/order/sales_order_notes.html | 62 ++++++++++++++ .../order/templates/order/so_attachments.html | 81 +++++++++++++++++++ InvenTree/order/templates/order/so_tabs.html | 6 +- InvenTree/order/urls.py | 3 + InvenTree/order/views.py | 23 +++++- 5 files changed, 171 insertions(+), 4 deletions(-) create mode 100644 InvenTree/order/templates/order/sales_order_notes.html create mode 100644 InvenTree/order/templates/order/so_attachments.html diff --git a/InvenTree/order/templates/order/sales_order_notes.html b/InvenTree/order/templates/order/sales_order_notes.html new file mode 100644 index 0000000000..671b592569 --- /dev/null +++ b/InvenTree/order/templates/order/sales_order_notes.html @@ -0,0 +1,62 @@ +{% extends "order/sales_order_base.html" %} + +{% load i18n %} +{% load static %} +{% load inventree_extras %} +{% load status_codes %} +{% load markdownify %} + +{% block page_title %} +InvenTree | {% trans "Sales Order" %} +{% endblock %} + +{% block details %} + +{% include "order/so_tabs.html" with tab='notes' %} + +{% if editing %} +

{% trans "Order Notes" %}

+
+ +
+ {% csrf_token %} + + {{ form }} +
+ +
+ +{{ form.media }} + +{% else %} +
+
+

{% trans "Order Notes" %}

+
+
+ +
+
+
+
+
+ {{ order.notes | markdownify }} +
+
+ +{% endif %} + +{% endblock %} + +{% block js_ready %} + +{{ block.super }} + +{% if editing %} +{% else %} +$("#edit-notes").click(function() { + location.href = "{% url 'so-notes' order.id %}?edit=1"; +}); +{% endif %} + +{% endblock %} \ No newline at end of file diff --git a/InvenTree/order/templates/order/so_attachments.html b/InvenTree/order/templates/order/so_attachments.html new file mode 100644 index 0000000000..5d7a66e313 --- /dev/null +++ b/InvenTree/order/templates/order/so_attachments.html @@ -0,0 +1,81 @@ +{% extends "order/sales_order_base.html" %} + +{% load inventree_extras %} +{% load i18n %} +{% load static %} + +{% block details %} + +{% include 'order/so_tabs.html' with tab='attachments' %} + +

{% trans "Sales Order Attachments" %} + +
+ +
+
+ +
+
+ + + + + + + + + + + {% for attachment in order.attachments.all %} + + + + + + {% endfor %} + +
{% trans "File" %}{% trans "Comment" %}
{{ attachment.basename }}{{ attachment.comment }} +
+ + +
+
+ +{% endblock %} + +{% block js_ready %} +{{ block.super }} + +$("#new-attachment").click(function() { + launchModalForm("{% url 'po-attachment-create' %}?order={{ order.id }}", + { + reload: true, + } + ); +}); + +$("#attachment-table").on('click', '.attachment-edit-button', function() { + var button = $(this); + + launchModalForm(button.attr('url'), { + reload: true, + }); +}); + +$("#attachment-table").on('click', '.attachment-delete-button', function() { + var button = $(this); + + launchModalForm(button.attr('url'), { + reload: true, + }); +}); + +$("#attachment-table").inventreeTable({ +}); + +{% endblock %} \ No newline at end of file diff --git a/InvenTree/order/templates/order/so_tabs.html b/InvenTree/order/templates/order/so_tabs.html index 2bb313b0bb..fbb931d8a2 100644 --- a/InvenTree/order/templates/order/so_tabs.html +++ b/InvenTree/order/templates/order/so_tabs.html @@ -2,16 +2,16 @@ \ No newline at end of file diff --git a/InvenTree/order/urls.py b/InvenTree/order/urls.py index 9a836e4e0b..21ab912492 100644 --- a/InvenTree/order/urls.py +++ b/InvenTree/order/urls.py @@ -65,6 +65,9 @@ sales_order_detail_urls = [ url(r'^edit/', views.SalesOrderEdit.as_view(), name='so-edit'), + url(r'^attachments/', views.SalesOrderDetail.as_view(template_name='order/so_attachments.html'), name='so-attachments'), + url(r'^notes/', views.SalesOrderNotes.as_view(), name='so-notes'), + url(r'^.*$', views.SalesOrderDetail.as_view(), name='so-detail'), ] diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index ed4febcdc8..2aafc9a677 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -181,7 +181,28 @@ class PurchaseOrderNotes(UpdateView): ctx = super().get_context_data(**kwargs) - ctx['editing'] = str2bool(self.request.GET.get('edit', '')) + ctx['editing'] = str2bool(self.request.GET.get('edit', False)) + + return ctx + + +class SalesOrderNotes(UpdateView): + """ View for editing the 'notes' field of a SalesORder """ + + context_object_name = 'order' + template_name = 'order/sales_order_notes.html' + model = SalesOrder + + fields = ['notes'] + + def get_success_url(self): + return reverse('so-notes', kwargs={'pk': self.get_object().pk}) + + def get_context_data(self, **kwargs): + + ctx = super().get_context_data(**kwargs) + + ctx['editing'] = str2bool(self.request.GET.get('edit', False)) return ctx From 9e4d09343c89f90303397e6ea9bc645dbee7ad06 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 20 Apr 2020 22:39:00 +1000 Subject: [PATCH 015/104] Add ability to filter parts list by "salable" status --- InvenTree/InvenTree/static/script/inventree/part.js | 4 ++++ InvenTree/part/templates/part/detail.html | 4 +--- InvenTree/templates/table_filters.html | 4 ++++ 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/InvenTree/InvenTree/static/script/inventree/part.js b/InvenTree/InvenTree/static/script/inventree/part.js index 7dc2c6a82a..b7e5fc3155 100644 --- a/InvenTree/InvenTree/static/script/inventree/part.js +++ b/InvenTree/InvenTree/static/script/inventree/part.js @@ -158,6 +158,10 @@ function loadPartTable(table, url, options={}) { display += ``; } + if (row.salable) { + display += ``; + } + /* if (row.component) { display = display + ``; diff --git a/InvenTree/part/templates/part/detail.html b/InvenTree/part/templates/part/detail.html index 2c3e5a2884..6d7400d4da 100644 --- a/InvenTree/part/templates/part/detail.html +++ b/InvenTree/part/templates/part/detail.html @@ -164,9 +164,8 @@ {% trans "Part can be purchased from external suppliers" %} {% endif %} - {% if 0 %} - {% trans "Sellable" %} + {% trans "Salable" %} {% include "slide.html" with state=part.salable field='salable' %} {% if part.salable %} {% trans "Part can be sold to customers" %} @@ -174,7 +173,6 @@ {% trans "Part cannot be sold to customers" %} {% endif %} - {% endif %}

diff --git a/InvenTree/templates/table_filters.html b/InvenTree/templates/table_filters.html index f976e977a6..29de965f83 100644 --- a/InvenTree/templates/table_filters.html +++ b/InvenTree/templates/table_filters.html @@ -93,6 +93,10 @@ function getAvailableTableFilters(tableKey) { type: 'bool', title: '{% trans "Starred" %}', }, + salable: { + type: 'bool', + title: '{% trans "Salable" %}', + }, }; } From 34d3dca8b7fbda1ba0d434399820113f31d81fd4 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 20 Apr 2020 22:40:52 +1000 Subject: [PATCH 016/104] Add ability to filter parts by "purchasable" status --- InvenTree/templates/table_filters.html | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/InvenTree/templates/table_filters.html b/InvenTree/templates/table_filters.html index 29de965f83..322acedb33 100644 --- a/InvenTree/templates/table_filters.html +++ b/InvenTree/templates/table_filters.html @@ -97,6 +97,10 @@ function getAvailableTableFilters(tableKey) { type: 'bool', title: '{% trans "Salable" %}', }, + purchaseable: { + type: 'bool', + title: '{% trans "Purchasable" %}', + }, }; } From a2c0c7c76a39e1a322b57e4af29f93902038ab72 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 21 Apr 2020 08:33:32 +1000 Subject: [PATCH 017/104] Add "part" reference to SalesOrderLineItem model --- .../0022_salesorderlineitem_part.py | 20 +++++++++++++++++++ InvenTree/order/models.py | 8 +++++++- 2 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 InvenTree/order/migrations/0022_salesorderlineitem_part.py diff --git a/InvenTree/order/migrations/0022_salesorderlineitem_part.py b/InvenTree/order/migrations/0022_salesorderlineitem_part.py new file mode 100644 index 0000000000..1ef32fba1b --- /dev/null +++ b/InvenTree/order/migrations/0022_salesorderlineitem_part.py @@ -0,0 +1,20 @@ +# Generated by Django 3.0.5 on 2020-04-20 22:33 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('part', '0035_auto_20200406_0045'), + ('order', '0021_auto_20200420_1010'), + ] + + operations = [ + migrations.AddField( + model_name='salesorderlineitem', + name='part', + field=models.ForeignKey(help_text='Part', limit_choices_to={'salable': True}, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sales_orders', to='part.Part'), + ), + ] diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index 2d678097d4..2eb60055ca 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -19,6 +19,7 @@ from datetime import datetime from stock.models import StockItem from company.models import Company, SupplierPart +from part.models import Part from InvenTree.fields import RoundingDecimalField from InvenTree.helpers import decimal2string @@ -355,8 +356,13 @@ class PurchaseOrderLineItem(OrderLineItem): class SalesOrderLineItem(OrderLineItem): """ Model for a single LineItem in a SalesOrder + + Attributes: + order: Link to the SalesOrder that this line item belongs to + part: Link to a Part object (may be null) """ order = models.ForeignKey(SalesOrder, on_delete=models.CASCADE, related_name='lines', help_text=_('Sales Order')) - # TODO - Add link for part items + part = models.ForeignKey(Part, on_delete=models.SET_NULL, related_name='sales_orders', null=True, help_text=_('Part'), limit_choices_to={'salable': True}) + From 617fbf2f0225530ee444338530db382310714318 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 21 Apr 2020 08:57:13 +1000 Subject: [PATCH 018/104] Moar stuffs: - Expose part_detail and order_detail to SOLineItem serializer - Update SalesOrder line item table --- InvenTree/order/admin.py | 1 + InvenTree/order/api.py | 36 ++++++++++++++++--- InvenTree/order/serializers.py | 22 +++++++++++- .../templates/order/sales_order_detail.html | 13 +++++++ InvenTree/order/urls.py | 6 ++++ 5 files changed, 72 insertions(+), 6 deletions(-) diff --git a/InvenTree/order/admin.py b/InvenTree/order/admin.py index 5ad1a6541d..459b7a3821 100644 --- a/InvenTree/order/admin.py +++ b/InvenTree/order/admin.py @@ -80,6 +80,7 @@ class SalesOrderLineItemAdmin(ImportExportModelAdmin): list_display = ( 'order', + 'part', 'quantity', 'reference' ) diff --git a/InvenTree/order/api.py b/InvenTree/order/api.py index 6d6d16ff6f..7efd277544 100644 --- a/InvenTree/order/api.py +++ b/InvenTree/order/api.py @@ -20,7 +20,7 @@ from .models import PurchaseOrder, PurchaseOrderLineItem from .serializers import POSerializer, POLineItemSerializer from .models import SalesOrder, SalesOrderLineItem -from .serializers import SalseOrderSerializer, SOLineItemSerializer +from .serializers import SalesOrderSerializer, SOLineItemSerializer class POList(generics.ListCreateAPIView): @@ -196,7 +196,7 @@ class SOList(generics.ListCreateAPIView): """ queryset = SalesOrder.objects.all() - serializer_class = SalseOrderSerializer + serializer_class = SalesOrderSerializer def get_serializer(self, *args, **kwargs): @@ -219,7 +219,7 @@ class SOList(generics.ListCreateAPIView): 'lines' ) - queryset = SalseOrderSerializer.annotate_queryset(queryset) + queryset = SalesOrderSerializer.annotate_queryset(queryset) return queryset @@ -269,7 +269,7 @@ class SODetail(generics.RetrieveUpdateAPIView): """ queryset = SalesOrder.objects.all() - serializer_class = SalseOrderSerializer + serializer_class = SalesOrderSerializer def get_serializer(self, *args, **kwargs): @@ -288,7 +288,7 @@ class SODetail(generics.RetrieveUpdateAPIView): queryset = queryset.prefetch_related('customer', 'lines') - queryset = SalseOrderSerializer.annotate_queryset(queryset) + queryset = SalesOrderSerializer.annotate_queryset(queryset) return queryset @@ -303,6 +303,32 @@ class SOLineItemList(generics.ListCreateAPIView): queryset = SalesOrderLineItem.objects.all() serializer_class = SOLineItemSerializer + def get_serializer(self, *args, **kwargs): + + try: + kwargs['part_detail'] = str2bool(self.request.query_params.get('part_detail', False)) + except AttributeError: + pass + + try: + kwargs['order_detail'] = str2bool(self.request.query_params.get('order_detail', False)) + except AttributeError: + pass + + kwargs['context'] = self.get_serializer_context() + + return self.serializer_class(*args, **kwargs) + + def get_queryset(self, *args, **kwargs): + + queryset = super().get_queryset(*args, **kwargs) + + return queryset.prefetch_related( + 'part', + 'part__stock_items', + 'order', + ) + permission_classes = [permissions.IsAuthenticated] filter_backends = [DjangoFilterBackend] diff --git a/InvenTree/order/serializers.py b/InvenTree/order/serializers.py index 69c0969c32..6e482d019e 100644 --- a/InvenTree/order/serializers.py +++ b/InvenTree/order/serializers.py @@ -11,6 +11,7 @@ from django.db.models import Count from InvenTree.serializers import InvenTreeModelSerializer from company.serializers import CompanyBriefSerializer +from part.serializers import PartBriefSerializer from .models import PurchaseOrder, PurchaseOrderLineItem from .models import SalesOrder, SalesOrderLineItem @@ -86,7 +87,7 @@ class POLineItemSerializer(InvenTreeModelSerializer): ] -class SalseOrderSerializer(InvenTreeModelSerializer): +class SalesOrderSerializer(InvenTreeModelSerializer): """ Serializers for the SalesOrder object """ @@ -145,6 +146,22 @@ class SalseOrderSerializer(InvenTreeModelSerializer): class SOLineItemSerializer(InvenTreeModelSerializer): """ Serializer for a SalesOrderLineItem object """ + def __init__(self, *args, **kwargs): + + part_detail = kwargs.pop('part_detail', False) + order_detail = kwargs.pop('order_detail', False) + + super().__init__(*args, **kwargs) + + if part_detail is not True: + self.fields.pop('part_detail') + + if order_detail is not True: + self.fields.pop('order_detail') + + order_detail = SalesOrderSerializer(source='order', many=False, read_only=True) + part_detail = PartBriefSerializer(source='part', many=False, read_only=True) + class Meta: model = SalesOrderLineItem @@ -154,4 +171,7 @@ class SOLineItemSerializer(InvenTreeModelSerializer): 'reference', 'notes', 'order', + 'order_detail', + 'part', + 'part_detail', ] diff --git a/InvenTree/order/templates/order/sales_order_detail.html b/InvenTree/order/templates/order/sales_order_detail.html index 8a47b9d044..3fa9a918d4 100644 --- a/InvenTree/order/templates/order/sales_order_detail.html +++ b/InvenTree/order/templates/order/sales_order_detail.html @@ -32,6 +32,7 @@ $("#so-lines-table").inventreeTable({ formatNoMatches: function() { return "No matching line items"; }, queryParams: { order: {{ order.id }}, + part_detail: true, }, url: "{% url 'api-so-line-list' %}", columns: [ @@ -40,6 +41,18 @@ $("#so-lines-table").inventreeTable({ title: 'ID', visible: false, }, + { + sortable: true, + field: 'part', + title: 'Part', + formatter: function(value, row, index, field) { + if (row.part) { + return imageHoverIcon(row.part_detail.thumbnail) + renderLink(row.part_detail.full_name, `/part/${value}/`); + } else { + return '-'; + } + } + }, { sortable: true, field: 'reference', diff --git a/InvenTree/order/urls.py b/InvenTree/order/urls.py index 21ab912492..c3fdac407b 100644 --- a/InvenTree/order/urls.py +++ b/InvenTree/order/urls.py @@ -61,6 +61,10 @@ purchase_order_urls = [ url(r'^.*$', views.PurchaseOrderIndex.as_view(), name='po-index'), ] +so_line_urls = [ + url(r'^new/', views.SOLineItemCreate.as_view(), name='so-line-item-create'), +] + sales_order_detail_urls = [ url(r'^edit/', views.SalesOrderEdit.as_view(), name='so-edit'), @@ -75,6 +79,8 @@ sales_order_urls = [ url(r'^new/', views.SalesOrderCreate.as_view(), name='so-create'), + url(r'^line/', include(so_line_urls)), + # Display detail view for a single SalesOrder url(r'^(?P\d+)/', include(sales_order_detail_urls)), From b204618e7991cecdf7f31762f2c40e65832c575c Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 21 Apr 2020 09:02:10 +1000 Subject: [PATCH 019/104] Button / view / form to create a new SalesOrderLineItem --- InvenTree/order/forms.py | 16 +++++++ InvenTree/order/models.py | 6 +++ .../templates/order/sales_order_detail.html | 15 +++++-- InvenTree/order/views.py | 42 ++++++++++++++++++- 4 files changed, 75 insertions(+), 4 deletions(-) diff --git a/InvenTree/order/forms.py b/InvenTree/order/forms.py index 56569812cd..6b4e0706ee 100644 --- a/InvenTree/order/forms.py +++ b/InvenTree/order/forms.py @@ -117,3 +117,19 @@ class EditPurchaseOrderLineItemForm(HelperForm): 'reference', 'notes', ] + + +class EditSalesOrderLineItemForm(HelperForm): + """ Form for editing a SalesOrderLineItem object """ + + quantity = RoundingDecimalFormField(max_digits=10, decimal_places=5) + + class Meta: + model = SalesOrderLineItem + fields = [ + 'order', + 'part', + 'quantity', + 'reference', + 'notes' + ] diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index 2eb60055ca..a01ab0caed 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -124,6 +124,9 @@ class PurchaseOrder(Order): ORDER_PREFIX = "PO" + def __str__(self): + return "PO {ref} - {company}".format(ref=self.reference, company=self.supplier.name) + supplier = models.ForeignKey( Company, on_delete=models.CASCADE, limit_choices_to={ @@ -255,6 +258,9 @@ class SalesOrder(Order): customer_reference: Optional field for customer order reference code """ + def __str__(self): + return "SO {ref} - {company}".format(ref=self.reference, company=self.customer.name) + def get_absolute_url(self): return reverse('so-detail', kwargs={'pk': self.id}) diff --git a/InvenTree/order/templates/order/sales_order_detail.html b/InvenTree/order/templates/order/sales_order_detail.html index 3fa9a918d4..52759e5b8a 100644 --- a/InvenTree/order/templates/order/sales_order_detail.html +++ b/InvenTree/order/templates/order/sales_order_detail.html @@ -14,9 +14,7 @@

{% trans "Sales Order Items" %}

- {% if order.status == OrderStatus.PENDING %} - - {% endif %} +
@@ -28,6 +26,17 @@ {% block js_ready %} {{ block.super }} +$("#new-so-line").click(function() { + launchModalForm("{% url 'so-line-item-create' %}", { + reload: true, + data: { + order: {{ order.id }}, + }, + secondary: [ + ] + }); +}); + $("#so-lines-table").inventreeTable({ formatNoMatches: function() { return "No matching line items"; }, queryParams: { diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index 2aafc9a677..d9ea9d01d1 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -1008,7 +1008,47 @@ class POLineItemCreate(AjaxCreateView): order = PurchaseOrder.objects.get(id=order_id) initials['order'] = order - except PurchaseOrder.DoesNotExist: + except (PurchaseOrder.DoesNotExist, ValueError): + pass + + return initials + + +class SOLineItemCreate(AjaxCreateView): + """ Ajax view for creating a new SalesOrderLineItem object """ + + model = SalesOrderLineItem + context_order_name = 'line' + form_class = order_forms.EditSalesOrderLineItemForm + ajax_form_title = _('Add Line Item') + + def get_initial(self): + """ + Extract initial data for this line item: + + Options: + order: The SalesOrder object + part: The Part object + """ + + initials = super().get_initial().copy() + + order_id = self.request.GET.get('order', None) + part_id = self.request.GET.get('part', None) + + if order_id: + try: + order = SalesOrder.objects.get(id=order_id) + initials['order'] = order + except (SalesOrder.DoesNotExist, ValueError): + pass + + if part_id: + try: + part = Part.objects.get(id=part_id) + if part.salable: + initials['part'] = part + except (Part.DoesNotExist, ValueError): pass return initials From 3d2e907d5e67da82ebfabac751deb062398c00ce Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 21 Apr 2020 09:15:01 +1000 Subject: [PATCH 020/104] Add a 'sales order' view for each part --- .../migrations/0023_auto_20200420_2309.py | 20 +++++++++++ InvenTree/order/models.py | 2 +- InvenTree/part/models.py | 11 ++++++ InvenTree/part/templates/part/orders.html | 10 ++++-- .../part/templates/part/sales_orders.html | 36 +++++++++++++++++++ InvenTree/part/templates/part/tabs.html | 5 +++ InvenTree/part/urls.py | 1 + 7 files changed, 81 insertions(+), 4 deletions(-) create mode 100644 InvenTree/order/migrations/0023_auto_20200420_2309.py create mode 100644 InvenTree/part/templates/part/sales_orders.html diff --git a/InvenTree/order/migrations/0023_auto_20200420_2309.py b/InvenTree/order/migrations/0023_auto_20200420_2309.py new file mode 100644 index 0000000000..32d47f593a --- /dev/null +++ b/InvenTree/order/migrations/0023_auto_20200420_2309.py @@ -0,0 +1,20 @@ +# Generated by Django 3.0.5 on 2020-04-20 23:09 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('part', '0035_auto_20200406_0045'), + ('order', '0022_salesorderlineitem_part'), + ] + + operations = [ + migrations.AlterField( + model_name='salesorderlineitem', + name='part', + field=models.ForeignKey(help_text='Part', limit_choices_to={'salable': True}, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sales_order_line_items', to='part.Part'), + ), + ] diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index a01ab0caed..2e5cdc3372 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -370,5 +370,5 @@ class SalesOrderLineItem(OrderLineItem): order = models.ForeignKey(SalesOrder, on_delete=models.CASCADE, related_name='lines', help_text=_('Sales Order')) - part = models.ForeignKey(Part, on_delete=models.SET_NULL, related_name='sales_orders', null=True, help_text=_('Part'), limit_choices_to={'salable': True}) + part = models.ForeignKey(Part, on_delete=models.SET_NULL, related_name='sales_order_line_items', null=True, help_text=_('Part'), limit_choices_to={'salable': True}) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index ca5b8f11c2..8405afd972 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -924,6 +924,17 @@ class Part(models.Model): return n + def sales_orders(self): + """ Return a list of sales orders which reference this part """ + + orders = [] + + for line in self.sales_order_line_items.all().prefetch_related('order'): + if line.order not in orders: + orders.append(line.order) + + return orders + def purchase_orders(self): """ Return a list of purchase orders which reference this part """ diff --git a/InvenTree/part/templates/part/orders.html b/InvenTree/part/templates/part/orders.html index d53c6ca04f..82f759b82c 100644 --- a/InvenTree/part/templates/part/orders.html +++ b/InvenTree/part/templates/part/orders.html @@ -1,16 +1,17 @@ {% extends "part/part_base.html" %} {% load static %} +{% load i18n %} {% block details %} {% include 'part/tabs.html' with tab='orders' %} -

Part Orders

+

{% trans "Purchase Orders" %}


- +
@@ -27,7 +28,10 @@ {{ block.super }} loadPurchaseOrderTable($("#purchase-order-table"), { - url: "{% url 'api-po-list' %}?part={{ part.id }}", + url: "{% url 'api-po-list' %}", + params: { + part: {{ part.id }}, + }, }); $("#part-order2").click(function() { diff --git a/InvenTree/part/templates/part/sales_orders.html b/InvenTree/part/templates/part/sales_orders.html new file mode 100644 index 0000000000..3878dbee34 --- /dev/null +++ b/InvenTree/part/templates/part/sales_orders.html @@ -0,0 +1,36 @@ +{% extends "part/part_base.html" %} +{% load static %} +{% load i18n %} + +{% block details %} + +{% include 'part/tabs.html' with tab='sales-orders' %} + +

{% trans "Sales Orders" %}

+
+ +
+
+ +
+ +
+
+
+ +
+
+ +{% endblock %} + +{% block js_ready %} +{{ block.super }} + +loadSalesOrderTable($("#sales-order-table"), { + url: "{% url 'api-so-list' %}", + params: { + part: {{ part.id }}, + }, +}); + +{% endblock %} diff --git a/InvenTree/part/templates/part/tabs.html b/InvenTree/part/templates/part/tabs.html index b0cf14be5d..0f894a5942 100644 --- a/InvenTree/part/templates/part/tabs.html +++ b/InvenTree/part/templates/part/tabs.html @@ -43,6 +43,11 @@ {% trans "Purchase Orders" %} {{ part.purchase_orders|length }} {% endif %} + {% if part.salable %} + + {% trans "Sales Orders" %} {{ part.sales_orders|length }} + + {% endif %} {% if part.trackable and 0 %} {% trans "Tracking" %} diff --git a/InvenTree/part/urls.py b/InvenTree/part/urls.py index 0d376f8e5b..10db202fb9 100644 --- a/InvenTree/part/urls.py +++ b/InvenTree/part/urls.py @@ -51,6 +51,7 @@ part_detail_urls = [ url(r'^used/?', views.PartDetail.as_view(template_name='part/used_in.html'), name='part-used-in'), url(r'^suppliers/?', views.PartDetail.as_view(template_name='part/supplier.html'), name='part-suppliers'), url(r'^orders/?', views.PartDetail.as_view(template_name='part/orders.html'), name='part-orders'), + url(r'^sales-orders/', views.PartDetail.as_view(template_name='part/sales_orders.html'), name='part-sales-orders'), url(r'^track/?', views.PartDetail.as_view(template_name='part/track.html'), name='part-track'), url(r'^attachments/?', views.PartDetail.as_view(template_name='part/attachments.html'), name='part-attachments'), url(r'^notes/?', views.PartNotes.as_view(), name='part-notes'), From 22c96ad2b71b27fd0989dd480e5470ece824968f Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 21 Apr 2020 09:17:50 +1000 Subject: [PATCH 021/104] Add ability to filter SalesOrder list by part --- InvenTree/order/api.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/InvenTree/order/api.py b/InvenTree/order/api.py index 7efd277544..6dfcfb6077 100644 --- a/InvenTree/order/api.py +++ b/InvenTree/order/api.py @@ -237,7 +237,16 @@ class SOList(generics.ListCreateAPIView): if status is not None: queryset = queryset.filter(status=status) - # TODO - Filter by part / stockitem / etc + # Filter by "Part" + # Only return SalesOrder which have LineItem referencing the part + part = params.get('part', None) + + if part is not None: + try: + part = Part.objects.get(pk=part) + queryset = queryset.filter(id__in=[so.id for so in part.sales_orders()]) + except (Part.DoesNotExist, ValueError): + pass return queryset From a06595c152e11716c16c8616b98aee4a50947869 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 21 Apr 2020 09:20:44 +1000 Subject: [PATCH 022/104] Add line numbering to SalesOrderLineItem table --- InvenTree/order/templates/order/sales_order_detail.html | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/InvenTree/order/templates/order/sales_order_detail.html b/InvenTree/order/templates/order/sales_order_detail.html index 52759e5b8a..9d092a5f45 100644 --- a/InvenTree/order/templates/order/sales_order_detail.html +++ b/InvenTree/order/templates/order/sales_order_detail.html @@ -50,6 +50,12 @@ $("#so-lines-table").inventreeTable({ title: 'ID', visible: false, }, + { + title: 'Line', + formatter: function(value, row, index, field) { + return index + 1; + }, + }, { sortable: true, field: 'part', From 19cd0707a29b0e8f73ec30fa964d5147176bb5d3 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 21 Apr 2020 09:42:11 +1000 Subject: [PATCH 023/104] Add / edit / delete attachments for SalesOrder --- InvenTree/order/forms.py | 14 +++- ...{po_delete.html => delete_attachment.html} | 0 .../order/templates/order/so_attachments.html | 6 +- InvenTree/order/urls.py | 9 +++ InvenTree/order/views.py | 64 ++++++++++++++++++- 5 files changed, 88 insertions(+), 5 deletions(-) rename InvenTree/order/templates/order/{po_delete.html => delete_attachment.html} (100%) diff --git a/InvenTree/order/forms.py b/InvenTree/order/forms.py index 6b4e0706ee..1b7dda7b5d 100644 --- a/InvenTree/order/forms.py +++ b/InvenTree/order/forms.py @@ -15,7 +15,7 @@ from InvenTree.fields import RoundingDecimalFormField from stock.models import StockLocation from .models import PurchaseOrder, PurchaseOrderLineItem, PurchaseOrderAttachment -from .models import SalesOrder, SalesOrderLineItem +from .models import SalesOrder, SalesOrderLineItem, SalesOrderAttachment class IssuePurchaseOrderForm(HelperForm): @@ -103,6 +103,18 @@ class EditPurchaseOrderAttachmentForm(HelperForm): ] +class EditSalesOrderAttachmentForm(HelperForm): + """ Form for editing a SalesOrderAttachment object """ + + class Meta: + model = SalesOrderAttachment + fields = [ + 'order', + 'attachment', + 'comment' + ] + + class EditPurchaseOrderLineItemForm(HelperForm): """ Form for editing a PurchaseOrderLineItem object """ diff --git a/InvenTree/order/templates/order/po_delete.html b/InvenTree/order/templates/order/delete_attachment.html similarity index 100% rename from InvenTree/order/templates/order/po_delete.html rename to InvenTree/order/templates/order/delete_attachment.html diff --git a/InvenTree/order/templates/order/so_attachments.html b/InvenTree/order/templates/order/so_attachments.html index 5d7a66e313..82248fd5eb 100644 --- a/InvenTree/order/templates/order/so_attachments.html +++ b/InvenTree/order/templates/order/so_attachments.html @@ -33,10 +33,10 @@ {{ attachment.comment }}
- -
@@ -52,7 +52,7 @@ {{ block.super }} $("#new-attachment").click(function() { - launchModalForm("{% url 'po-attachment-create' %}?order={{ order.id }}", + launchModalForm("{% url 'so-attachment-create' %}?order={{ order.id }}", { reload: true, } diff --git a/InvenTree/order/urls.py b/InvenTree/order/urls.py index c3fdac407b..e56d0c9312 100644 --- a/InvenTree/order/urls.py +++ b/InvenTree/order/urls.py @@ -65,6 +65,13 @@ so_line_urls = [ url(r'^new/', views.SOLineItemCreate.as_view(), name='so-line-item-create'), ] +sales_order_attachment_urls = [ + url(r'^new/', views.SalesOrderAttachmentCreate.as_view(), name='so-attachment-create'), + url(r'^(?P\d+)/edit/', views.SalesOrderAttachmentEdit.as_view(), name='so-attachment-edit'), + url(r'^(?P\d+)/delete/', views.SalesOrderAttachmentDelete.as_view(), name='so-attachment-delete'), + +] + sales_order_detail_urls = [ url(r'^edit/', views.SalesOrderEdit.as_view(), name='so-edit'), @@ -81,6 +88,8 @@ sales_order_urls = [ url(r'^line/', include(so_line_urls)), + url(r'^attachments/', include(sales_order_attachment_urls)), + # Display detail view for a single SalesOrder url(r'^(?P\d+)/', include(sales_order_detail_urls)), diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index d9ea9d01d1..a718c96cc4 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -129,6 +129,34 @@ class PurchaseOrderAttachmentCreate(AjaxCreateView): return form +class SalesOrderAttachmentCreate(AjaxCreateView): + """ View for creating a new SalesOrderAttachment """ + + model = SalesOrderAttachment + form_class = order_forms.EditSalesOrderAttachmentForm + ajax_form_title = _('Add Sales Order Attachment') + + def get_data(self): + return { + 'success': _('Added attachment') + } + + def get_initial(self): + initials = super().get_initial().copy() + + initials['order'] = SalesOrder.objects.get(id=self.request.GET.get('order', None)) + + return initials + + def get_form(self): + """ Hide the 'order' field """ + + form = super().get_form() + form.fields['order'].widget = HiddenInput() + + return form + + class PurchaseOrderAttachmentEdit(AjaxUpdateView): """ View for editing a PurchaseOrderAttachment object """ @@ -150,12 +178,46 @@ class PurchaseOrderAttachmentEdit(AjaxUpdateView): return form +class SalesOrderAttachmentEdit(AjaxUpdateView): + """ View for editing a SalesOrderAttachment object """ + + model = SalesOrderAttachment + form_class = order_forms.EditSalesOrderAttachmentForm + ajax_form_title = _("Edit Attachment") + + def get_data(self): + return { + 'success': _('Attachment updated') + } + + def get_form(self): + form = super().get_form() + + form.fields['order'].widget = HiddenInput() + + return form + + class PurchaseOrderAttachmentDelete(AjaxDeleteView): """ View for deleting a PurchaseOrderAttachment """ model = PurchaseOrderAttachment ajax_form_title = _("Delete Attachment") - ajax_template_name = "order/po_delete.html" + ajax_template_name = "order/delete_attachment.html" + context_object_name = "attachment" + + def get_data(self): + return { + "danger": _("Deleted attachment") + } + + +class SalesOrderAttachmentDelete(AjaxDeleteView): + """ View for deleting a SalesOrderAttachment """ + + model = SalesOrderAttachment + ajax_form_title = _("Delete Attachment") + ajax_template_name = "order/delete_attachment.html" context_object_name = "attachment" def get_data(self): From 73850991947b587db59df51286a3ab1ed6707e55 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 21 Apr 2020 10:14:55 +1000 Subject: [PATCH 024/104] Add a model to map multiple StockItem objects to a single SalesOrderLineItem --- ...0024_salesorderlineitemstockassociation.py | 23 +++++++++++++++++++ InvenTree/order/models.py | 17 +++++++++++++- .../templates/order/sales_order_detail.html | 2 ++ InvenTree/part/templates/part/tabs.html | 2 +- 4 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 InvenTree/order/migrations/0024_salesorderlineitemstockassociation.py diff --git a/InvenTree/order/migrations/0024_salesorderlineitemstockassociation.py b/InvenTree/order/migrations/0024_salesorderlineitemstockassociation.py new file mode 100644 index 0000000000..bad3914a74 --- /dev/null +++ b/InvenTree/order/migrations/0024_salesorderlineitemstockassociation.py @@ -0,0 +1,23 @@ +# Generated by Django 3.0.5 on 2020-04-21 00:14 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('stock', '0026_stockitem_uid'), + ('order', '0023_auto_20200420_2309'), + ] + + operations = [ + migrations.CreateModel( + name='SalesOrderLineItemStockAssociation', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('line', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='stock_items', to='order.SalesOrderLineItem')), + ('stock_item', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='sales_order', to='stock.StockItem')), + ], + ), + ] diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index 2e5cdc3372..1490b45d98 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -291,7 +291,7 @@ class SalesOrderAttachment(InvenTreeAttachment): Model for storing file attachments against a SalesOrder object """ - def getSubDir(self): + def getSubdir(self): return os.path.join("so_files", str(self.order.id)) order = models.ForeignKey(SalesOrder, on_delete=models.CASCADE, related_name='attachments') @@ -372,3 +372,18 @@ class SalesOrderLineItem(OrderLineItem): part = models.ForeignKey(Part, on_delete=models.SET_NULL, related_name='sales_order_line_items', null=True, help_text=_('Part'), limit_choices_to={'salable': True}) + +class SalesOrderLineItemStockAssociation(models.Model): + """ + Associates StockItem objects with a SalesOrderLineItem. + This model is used to match stock items with a sales order, + for the purpose of sale / packing / shipping etc. + + Attributes: + line: ForeignKey link to a SalesOrderLineItem object -> A single SalesOrderLineItem can have multiple associated StockItem objects + stock: OneToOne link to a StockItem object -> A StockItem object can only be mapped to a single SalesOrderLineItem + """ + + line = models.ForeignKey(SalesOrderLineItem, on_delete=models.CASCADE, related_name='stock_items') + + stock_item = models.OneToOneField(StockItem, on_delete=models.CASCADE, related_name='sales_order') diff --git a/InvenTree/order/templates/order/sales_order_detail.html b/InvenTree/order/templates/order/sales_order_detail.html index 9d092a5f45..907effebc7 100644 --- a/InvenTree/order/templates/order/sales_order_detail.html +++ b/InvenTree/order/templates/order/sales_order_detail.html @@ -51,10 +51,12 @@ $("#so-lines-table").inventreeTable({ visible: false, }, { + field: 'line', title: 'Line', formatter: function(value, row, index, field) { return index + 1; }, + width: 50, }, { sortable: true, diff --git a/InvenTree/part/templates/part/tabs.html b/InvenTree/part/templates/part/tabs.html index 0f894a5942..4f23e345b6 100644 --- a/InvenTree/part/templates/part/tabs.html +++ b/InvenTree/part/templates/part/tabs.html @@ -25,7 +25,7 @@
{% trans "BOM" %}{{ part.bom_count }} - {% trans "Build" %}{{ part.builds|length }} + {% trans "Build" %}{{ part.active_builds|length }} {% endif %} {% if part.component or part.used_in_count > 0 %} From 8052a1989c3cbed8b7d2cd2cbbcb2994dddd6664 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 21 Apr 2020 11:41:08 +1000 Subject: [PATCH 025/104] Serialize the allocated quantity for a purchase-order line item --- InvenTree/order/api.py | 1 + InvenTree/order/models.py | 14 +++++++++++++- InvenTree/order/serializers.py | 4 ++++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/InvenTree/order/api.py b/InvenTree/order/api.py index 6dfcfb6077..6599d1a209 100644 --- a/InvenTree/order/api.py +++ b/InvenTree/order/api.py @@ -335,6 +335,7 @@ class SOLineItemList(generics.ListCreateAPIView): return queryset.prefetch_related( 'part', 'part__stock_items', + 'stock_items', 'order', ) diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index 1490b45d98..7a694fbc8d 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -5,7 +5,8 @@ Order model definitions # -*- coding: utf-8 -*- from django.db import models, transaction -from django.db.models import F +from django.db.models import F, Sum +from django.db.models.functions import Coalesce from django.core.validators import MinValueValidator from django.core.exceptions import ValidationError from django.contrib.auth.models import User @@ -16,6 +17,7 @@ from markdownx.models import MarkdownxField import os from datetime import datetime +from decimal import Decimal from stock.models import StockItem from company.models import Company, SupplierPart @@ -372,6 +374,16 @@ class SalesOrderLineItem(OrderLineItem): part = models.ForeignKey(Part, on_delete=models.SET_NULL, related_name='sales_order_line_items', null=True, help_text=_('Part'), limit_choices_to={'salable': True}) + def allocated_quantity(self): + """ Return the total stock quantity allocated to this LineItem. + + This is a summation of the quantity of each attached StockItem + """ + + query = self.stock_items.aggregate(allocated=Coalesce(Sum('stock_item__quantity'), Decimal(0))) + + return query['allocated'] + class SalesOrderLineItemStockAssociation(models.Model): """ diff --git a/InvenTree/order/serializers.py b/InvenTree/order/serializers.py index 6e482d019e..deb470baab 100644 --- a/InvenTree/order/serializers.py +++ b/InvenTree/order/serializers.py @@ -162,11 +162,15 @@ class SOLineItemSerializer(InvenTreeModelSerializer): order_detail = SalesOrderSerializer(source='order', many=False, read_only=True) part_detail = PartBriefSerializer(source='part', many=False, read_only=True) + quantity = serializers.FloatField() + allocated = serializers.FloatField(source='allocated_quantity', read_only=True) + class Meta: model = SalesOrderLineItem fields = [ 'pk', + 'allocated', 'quantity', 'reference', 'notes', From a1376eeb9e85c104200acb1b85420892a9177600 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 21 Apr 2020 15:04:21 +1000 Subject: [PATCH 026/104] The 'StockItem' model now has a reference to a SalesOrderLineItem --- InvenTree/InvenTree/static/css/inventree.css | 33 +++++++++++++++++++ ...0024_salesorderlineitemstockassociation.py | 23 ------------- InvenTree/order/models.py | 16 --------- .../templates/order/sales_order_detail.html | 15 ++++----- .../migrations/0027_stockitem_sales_order.py | 20 +++++++++++ InvenTree/stock/models.py | 7 ++++ 6 files changed, 66 insertions(+), 48 deletions(-) delete mode 100644 InvenTree/order/migrations/0024_salesorderlineitemstockassociation.py create mode 100644 InvenTree/stock/migrations/0027_stockitem_sales_order.py diff --git a/InvenTree/InvenTree/static/css/inventree.css b/InvenTree/InvenTree/static/css/inventree.css index 3724835621..550884608f 100644 --- a/InvenTree/InvenTree/static/css/inventree.css +++ b/InvenTree/InvenTree/static/css/inventree.css @@ -29,6 +29,39 @@ padding: 10px; } +/* Progress bars */ + +.progress-bar { + height: 25px; + background: #eee; + border-radius: 5px; + border: 1px solid #ddd; + width: 100%; + position: relative; +} + +.progress-bar-value { + color: #000; + position: absolute; + top: 0px; + width: 100%; + left: 0px; + font-weight: bold; + font-size: 120%; +} + +.progress-bar-inner { + height: 23px; + width: 20%; + text-align: center; + vertical-align: middle; + background: #3a3; + opacity: 40%; + position: absolute; + top: 0px; + left: 0px; +} + .qr-code { max-width: 400px; max-height: 400px; diff --git a/InvenTree/order/migrations/0024_salesorderlineitemstockassociation.py b/InvenTree/order/migrations/0024_salesorderlineitemstockassociation.py deleted file mode 100644 index bad3914a74..0000000000 --- a/InvenTree/order/migrations/0024_salesorderlineitemstockassociation.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 3.0.5 on 2020-04-21 00:14 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('stock', '0026_stockitem_uid'), - ('order', '0023_auto_20200420_2309'), - ] - - operations = [ - migrations.CreateModel( - name='SalesOrderLineItemStockAssociation', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('line', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='stock_items', to='order.SalesOrderLineItem')), - ('stock_item', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='sales_order', to='stock.StockItem')), - ], - ), - ] diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index 7a694fbc8d..9c6d5dcbb8 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -383,19 +383,3 @@ class SalesOrderLineItem(OrderLineItem): query = self.stock_items.aggregate(allocated=Coalesce(Sum('stock_item__quantity'), Decimal(0))) return query['allocated'] - - -class SalesOrderLineItemStockAssociation(models.Model): - """ - Associates StockItem objects with a SalesOrderLineItem. - This model is used to match stock items with a sales order, - for the purpose of sale / packing / shipping etc. - - Attributes: - line: ForeignKey link to a SalesOrderLineItem object -> A single SalesOrderLineItem can have multiple associated StockItem objects - stock: OneToOne link to a StockItem object -> A StockItem object can only be mapped to a single SalesOrderLineItem - """ - - line = models.ForeignKey(SalesOrderLineItem, on_delete=models.CASCADE, related_name='stock_items') - - stock_item = models.OneToOneField(StockItem, on_delete=models.CASCADE, related_name='sales_order') diff --git a/InvenTree/order/templates/order/sales_order_detail.html b/InvenTree/order/templates/order/sales_order_detail.html index 907effebc7..64b75358da 100644 --- a/InvenTree/order/templates/order/sales_order_detail.html +++ b/InvenTree/order/templates/order/sales_order_detail.html @@ -50,14 +50,6 @@ $("#so-lines-table").inventreeTable({ title: 'ID', visible: false, }, - { - field: 'line', - title: 'Line', - formatter: function(value, row, index, field) { - return index + 1; - }, - width: 50, - }, { sortable: true, field: 'part', @@ -80,7 +72,12 @@ $("#so-lines-table").inventreeTable({ field: 'quantity', title: 'Quantity', formatter: function(value, row, index, field) { - return +parseFloat(value).toFixed(5); + return ` +
+
+
${row.allocated} / ${row.quantity}
+
+ `; } }, { diff --git a/InvenTree/stock/migrations/0027_stockitem_sales_order.py b/InvenTree/stock/migrations/0027_stockitem_sales_order.py new file mode 100644 index 0000000000..048609ae5f --- /dev/null +++ b/InvenTree/stock/migrations/0027_stockitem_sales_order.py @@ -0,0 +1,20 @@ +# Generated by Django 3.0.5 on 2020-04-21 05:03 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('order', '0023_auto_20200420_2309'), + ('stock', '0026_stockitem_uid'), + ] + + operations = [ + migrations.AddField( + model_name='stockitem', + name='sales_order', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='stock_items', to='order.SalesOrderLineItem'), + ), + ] diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 51b61ff3fd..f009f09791 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -126,6 +126,7 @@ class StockItem(MPTTModel): build: Link to a Build (if this stock item was created from a build) purchase_order: Link to a PurchaseOrder (if this stock item was created from a PurchaseOrder) infinite: If True this StockItem can never be exhausted + sales_order: Link to a SalesOrderLineItem (if this stockitem has been allocated to a sales order) """ def save(self, *args, **kwargs): @@ -353,6 +354,12 @@ class StockItem(MPTTModel): help_text=_('Purchase order for this stock item') ) + sales_order = models.ForeignKey( + 'order.SalesOrderLineItem', + on_delete=models.SET_NULL, + related_name='stock_items', + null=True) + # last time the stock was checked / counted stocktake_date = models.DateField(blank=True, null=True) From 89ede3e1038e5119316f2732a90aaa5c77c68bde Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 21 Apr 2020 16:45:44 +1000 Subject: [PATCH 027/104] Fix for SalesOrderLineItem allocation calculation Also function to render a progress bar --- InvenTree/InvenTree/static/css/inventree.css | 2 +- .../static/script/inventree/inventree.js | 27 +++++++++++++++++++ InvenTree/order/models.py | 6 ++--- .../templates/order/sales_order_detail.html | 7 +---- InvenTree/stock/models.py | 19 +++++++++++-- 5 files changed, 48 insertions(+), 13 deletions(-) diff --git a/InvenTree/InvenTree/static/css/inventree.css b/InvenTree/InvenTree/static/css/inventree.css index 550884608f..a1968c317e 100644 --- a/InvenTree/InvenTree/static/css/inventree.css +++ b/InvenTree/InvenTree/static/css/inventree.css @@ -47,7 +47,7 @@ width: 100%; left: 0px; font-weight: bold; - font-size: 120%; + font-size: 110%; } .progress-bar-inner { diff --git a/InvenTree/InvenTree/static/script/inventree/inventree.js b/InvenTree/InvenTree/static/script/inventree/inventree.js index 04539c6d96..1e060cd6d3 100644 --- a/InvenTree/InvenTree/static/script/inventree/inventree.js +++ b/InvenTree/InvenTree/static/script/inventree/inventree.js @@ -78,6 +78,33 @@ function getImageUrlFromTransfer(transfer) { return url; } +function makeProgressBar(value, maximum, opts) { + /* + * Render a progessbar! + * + * @param value is the current value of the progress bar + * @param maximum is the maximum value of the progress bar + */ + + var options = opts || {}; + + value = parseFloat(value); + maximum = parseFloat(maximum); + + var percent = parseInt(value / maximum * 100); + + if (percent > 100) { + percent = 100; + } + + return ` +
+
+
${value} / ${maximum}
+
+ `; +} + function enableDragAndDrop(element, url, options) { /* Enable drag-and-drop file uploading for a given element. diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index 9c6d5dcbb8..ba8e6b0337 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -19,9 +19,7 @@ import os from datetime import datetime from decimal import Decimal -from stock.models import StockItem from company.models import Company, SupplierPart -from part.models import Part from InvenTree.fields import RoundingDecimalField from InvenTree.helpers import decimal2string @@ -372,7 +370,7 @@ class SalesOrderLineItem(OrderLineItem): order = models.ForeignKey(SalesOrder, on_delete=models.CASCADE, related_name='lines', help_text=_('Sales Order')) - part = models.ForeignKey(Part, on_delete=models.SET_NULL, related_name='sales_order_line_items', null=True, help_text=_('Part'), limit_choices_to={'salable': True}) + part = models.ForeignKey('part.Part', on_delete=models.SET_NULL, related_name='sales_order_line_items', null=True, help_text=_('Part'), limit_choices_to={'salable': True}) def allocated_quantity(self): """ Return the total stock quantity allocated to this LineItem. @@ -380,6 +378,6 @@ class SalesOrderLineItem(OrderLineItem): This is a summation of the quantity of each attached StockItem """ - query = self.stock_items.aggregate(allocated=Coalesce(Sum('stock_item__quantity'), Decimal(0))) + query = self.stock_items.aggregate(allocated=Coalesce(Sum('quantity'), Decimal(0))) return query['allocated'] diff --git a/InvenTree/order/templates/order/sales_order_detail.html b/InvenTree/order/templates/order/sales_order_detail.html index 64b75358da..59ac73cd0a 100644 --- a/InvenTree/order/templates/order/sales_order_detail.html +++ b/InvenTree/order/templates/order/sales_order_detail.html @@ -72,12 +72,7 @@ $("#so-lines-table").inventreeTable({ field: 'quantity', title: 'Quantity', formatter: function(value, row, index, field) { - return ` -
-
-
${row.allocated} / ${row.quantity}
-
- `; + return makeProgressBar(row.allocated, row.quantity); } }, { diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index f009f09791..1129a3ce79 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -29,6 +29,7 @@ from InvenTree.models import InvenTreeTree from InvenTree.fields import InvenTreeURLField from part.models import Part +from order.models import PurchaseOrder, SalesOrderLineItem class StockLocation(InvenTreeTree): @@ -262,6 +263,20 @@ class StockItem(MPTTModel): # TODO - Find a test than can be perfomed... pass + try: + # If this StockItem is assigned to a SalesOrderLineItem, + # the "Part" that the line item references is the same as the part that THIS references + if self.sales_order is not None: + + if self.sales_order.part == None: + raise ValidationError({'sales_order': _('Stock item cannot be assigned to a LineItem which does not reference a part')}) + + if not self.sales_order.part == self.part: + raise ValidationError({'sales_order': _('Stock item does not reference the same part object as the LineItem')}) + + except SalesOrderLineItem.DoesNotExist: + pass + if self.belongs_to and self.belongs_to.pk == self.pk: raise ValidationError({ 'belongs_to': _('Item cannot belong to itself') @@ -347,7 +362,7 @@ class StockItem(MPTTModel): ) purchase_order = models.ForeignKey( - 'order.PurchaseOrder', + PurchaseOrder, on_delete=models.SET_NULL, related_name='stock_items', blank=True, null=True, @@ -355,7 +370,7 @@ class StockItem(MPTTModel): ) sales_order = models.ForeignKey( - 'order.SalesOrderLineItem', + SalesOrderLineItem, on_delete=models.SET_NULL, related_name='stock_items', null=True) From 0d1919f10b137e2cd46b5f8e432b5b834bfde2fa Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 21 Apr 2020 16:59:14 +1000 Subject: [PATCH 028/104] Display an alert on a stock item page if that stock item is allocated to a salesorder --- InvenTree/stock/templates/stock/item_base.html | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/InvenTree/stock/templates/stock/item_base.html b/InvenTree/stock/templates/stock/item_base.html index 9785b78850..f339258d0f 100644 --- a/InvenTree/stock/templates/stock/item_base.html +++ b/InvenTree/stock/templates/stock/item_base.html @@ -5,6 +5,12 @@ {% load i18n %} {% block content %} +{% if item.sales_order %} +
+ {% trans "This stock item is allocated to " %} + {{ item.sales_order.order }} +
+{% endif %}
From 2c6e8da90ec55ed3da1a6eea1b1594a0bbfdd73b Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 21 Apr 2020 17:33:02 +1000 Subject: [PATCH 029/104] Ability to filter StockItemList API by sales_order or sales_order_line --- InvenTree/stock/api.py | 20 ++++++++++++++++--- .../migrations/0028_auto_20200421_0724.py | 18 +++++++++++++++++ InvenTree/stock/models.py | 12 +++++------ 3 files changed, 41 insertions(+), 9 deletions(-) create mode 100644 InvenTree/stock/migrations/0028_auto_20200421_0724.py diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index 77b35f45d0..450f6e95f1 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -12,6 +12,7 @@ from django.db.models import Q from .models import StockLocation, StockItem from .models import StockItemTracking +from order.models import SalesOrder from part.models import Part, PartCategory from .serializers import StockItemSerializer @@ -387,7 +388,7 @@ class StockList(generics.ListCreateAPIView): stock_list = stock_list.filter(part=part_id) except (ValueError, Part.DoesNotExist): - pass + raise ValidationError({"part": "Invalid Part ID specified"}) # Does the client wish to filter by the 'ancestor'? anc_id = self.request.query_params.get('ancestor', None) @@ -400,7 +401,7 @@ class StockList(generics.ListCreateAPIView): stock_list = stock_list.filter(id__in=[item.pk for item in ancestor.children.all()]) except (ValueError, Part.DoesNotExist): - pass + raise ValidationError({"ancestor": "Invalid ancestor ID specified"}) # Does the client wish to filter by stock location? loc_id = self.request.query_params.get('location', None) @@ -433,7 +434,7 @@ class StockList(generics.ListCreateAPIView): stock_list = stock_list.filter(part__category__in=category.getUniqueChildren()) except (ValueError, PartCategory.DoesNotExist): - pass + raise ValidationError({"category": "Invalid category id specified"}) # Filter by StockItem status status = self.request.query_params.get('status', None) @@ -465,10 +466,22 @@ class StockList(generics.ListCreateAPIView): if manufacturer is not None: stock_list = stock_list.filter(supplier_part__manufacturer=manufacturer) + # Filter by sales order + sales_order = self.request.query_params.get('sales_order', None) + + if sales_order is not None: + try: + sales_order = SalesOrder.objects.get(pk=sales_order) + lines = [line.pk for line in sales_order.lines.all()] + stock_list = stock_list.filter(sales_order_line__in=lines) + except (SalesOrder.DoesNotExist, ValueError): + raise ValidationError({'sales_order': 'Invalid SalesOrder object specified'}) + # Also ensure that we pre-fecth all the related items stock_list = stock_list.prefetch_related( 'part', 'part__category', + 'sales_order_line__order', 'location' ) @@ -493,6 +506,7 @@ class StockList(generics.ListCreateAPIView): 'customer', 'belongs_to', 'build', + 'sales_order_line' ] diff --git a/InvenTree/stock/migrations/0028_auto_20200421_0724.py b/InvenTree/stock/migrations/0028_auto_20200421_0724.py new file mode 100644 index 0000000000..61ebe97039 --- /dev/null +++ b/InvenTree/stock/migrations/0028_auto_20200421_0724.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.5 on 2020-04-21 07:24 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('stock', '0027_stockitem_sales_order'), + ] + + operations = [ + migrations.RenameField( + model_name='stockitem', + old_name='sales_order', + new_name='sales_order_line', + ), + ] diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 1129a3ce79..4970083f0c 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -266,13 +266,13 @@ class StockItem(MPTTModel): try: # If this StockItem is assigned to a SalesOrderLineItem, # the "Part" that the line item references is the same as the part that THIS references - if self.sales_order is not None: + if self.sales_order_line is not None: - if self.sales_order.part == None: - raise ValidationError({'sales_order': _('Stock item cannot be assigned to a LineItem which does not reference a part')}) + if self.sales_order_line.part == None: + raise ValidationError({'sales_order_line': _('Stock item cannot be assigned to a LineItem which does not reference a part')}) - if not self.sales_order.part == self.part: - raise ValidationError({'sales_order': _('Stock item does not reference the same part object as the LineItem')}) + if not self.sales_order_line.part == self.part: + raise ValidationError({'sales_order_line': _('Stock item does not reference the same part object as the LineItem')}) except SalesOrderLineItem.DoesNotExist: pass @@ -369,7 +369,7 @@ class StockItem(MPTTModel): help_text=_('Purchase order for this stock item') ) - sales_order = models.ForeignKey( + sales_order_line = models.ForeignKey( SalesOrderLineItem, on_delete=models.SET_NULL, related_name='stock_items', From 399dcafeded83d78c73881a10c605941b1e30c83 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 21 Apr 2020 18:14:02 +1000 Subject: [PATCH 030/104] Use the existing bootstrap CSS for progress bars --- InvenTree/InvenTree/static/css/inventree.css | 33 +++++++------------ .../static/script/inventree/inventree.js | 8 ++--- 2 files changed, 15 insertions(+), 26 deletions(-) diff --git a/InvenTree/InvenTree/static/css/inventree.css b/InvenTree/InvenTree/static/css/inventree.css index a1968c317e..cd4b63f299 100644 --- a/InvenTree/InvenTree/static/css/inventree.css +++ b/InvenTree/InvenTree/static/css/inventree.css @@ -31,37 +31,26 @@ /* Progress bars */ -.progress-bar { - height: 25px; - background: #eee; - border-radius: 5px; - border: 1px solid #ddd; - width: 100%; +.progress { position: relative; + width: 100%; } -.progress-bar-value { - color: #000; - position: absolute; - top: 0px; +.progress-bar { + opacity: 50%; +} + +.progress-value { width: 100%; + color: #333; + position: absolute; + text-align: center; + top: 0px; left: 0px; font-weight: bold; font-size: 110%; } -.progress-bar-inner { - height: 23px; - width: 20%; - text-align: center; - vertical-align: middle; - background: #3a3; - opacity: 40%; - position: absolute; - top: 0px; - left: 0px; -} - .qr-code { max-width: 400px; max-height: 400px; diff --git a/InvenTree/InvenTree/static/script/inventree/inventree.js b/InvenTree/InvenTree/static/script/inventree/inventree.js index 1e060cd6d3..ce812afc4f 100644 --- a/InvenTree/InvenTree/static/script/inventree/inventree.js +++ b/InvenTree/InvenTree/static/script/inventree/inventree.js @@ -98,10 +98,10 @@ function makeProgressBar(value, maximum, opts) { } return ` -
-
-
${value} / ${maximum}
-
+
+
+
${value} / ${maximum}
+
`; } From b40234e40368c85580dd67dae0e48cad65c8b237 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 21 Apr 2020 20:05:15 +1000 Subject: [PATCH 031/104] UI tweaks --- InvenTree/InvenTree/static/css/inventree.css | 8 +++++--- InvenTree/company/templates/company/company_base.html | 2 +- InvenTree/order/templates/order/sales_order_base.html | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/InvenTree/InvenTree/static/css/inventree.css b/InvenTree/InvenTree/static/css/inventree.css index cd4b63f299..a8efcd0844 100644 --- a/InvenTree/InvenTree/static/css/inventree.css +++ b/InvenTree/InvenTree/static/css/inventree.css @@ -34,10 +34,12 @@ .progress { position: relative; width: 100%; + margin-bottom: 0px; + background: #eeeef5; } .progress-bar { - opacity: 50%; + opacity: 60%; } .progress-value { @@ -47,7 +49,6 @@ text-align: center; top: 0px; left: 0px; - font-weight: bold; font-size: 110%; } @@ -270,7 +271,6 @@ /* dropzone class - for Drag-n-Drop file uploads */ .dropzone { - border: 1px solid #555; z-index: 2; } @@ -330,6 +330,8 @@ margin: 2px; padding: 3px; object-fit: contain; + border: 1px solid #aaa; + border-radius: 3px; } .part-thumb-container:hover .part-thumb-overlay { diff --git a/InvenTree/company/templates/company/company_base.html b/InvenTree/company/templates/company/company_base.html index 8ec68a401c..693693c39d 100644 --- a/InvenTree/company/templates/company/company_base.html +++ b/InvenTree/company/templates/company/company_base.html @@ -23,7 +23,7 @@ InvenTree | {% trans "Company" %} - {{ company.name }}
-

{{ company.name }}

+

{{ company.name }}

{{ company.description }}

{% if company.is_supplier %} diff --git a/InvenTree/order/templates/order/sales_order_base.html b/InvenTree/order/templates/order/sales_order_base.html index 3ee3198b28..ed8be4256b 100644 --- a/InvenTree/order/templates/order/sales_order_base.html +++ b/InvenTree/order/templates/order/sales_order_base.html @@ -25,7 +25,7 @@ InvenTree | {% trans "Sales Order" %} />
-

{{ order }}

+

{{ order }}

{{ order.description }}

From cb636e000d4c97b45bf0b564eb14864a6ff3c9ab Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 21 Apr 2020 21:38:04 +1000 Subject: [PATCH 032/104] Display a sub-list of stock items which are allocated to a SalseOrderLineItem --- InvenTree/InvenTree/static/css/inventree.css | 9 +++ .../InvenTree/static/script/inventree/bom.js | 1 - .../templates/order/sales_order_detail.html | 78 +++++++++++++++++++ InvenTree/part/serializers.py | 4 +- .../stock/templates/stock/item_base.html | 6 +- 5 files changed, 93 insertions(+), 5 deletions(-) diff --git a/InvenTree/InvenTree/static/css/inventree.css b/InvenTree/InvenTree/static/css/inventree.css index a8efcd0844..29eac04c55 100644 --- a/InvenTree/InvenTree/static/css/inventree.css +++ b/InvenTree/InvenTree/static/css/inventree.css @@ -158,6 +158,15 @@ background-color: #ebf4f4; } +.sub-table { + margin-left: 25px; + margin-right: 25px; +} + +.detail-icon .glyphicon { + color: #98d296; +} + /* Force select2 elements in modal forms to be full width */ .select-full-width { width: 100%; diff --git a/InvenTree/InvenTree/static/script/inventree/bom.js b/InvenTree/InvenTree/static/script/inventree/bom.js index 3d1e8fc594..def3910999 100644 --- a/InvenTree/InvenTree/static/script/inventree/bom.js +++ b/InvenTree/InvenTree/static/script/inventree/bom.js @@ -221,7 +221,6 @@ function loadBomTable(table, options) { } } }); - } // Part notes diff --git a/InvenTree/order/templates/order/sales_order_detail.html b/InvenTree/order/templates/order/sales_order_detail.html index 59ac73cd0a..53b3dd869c 100644 --- a/InvenTree/order/templates/order/sales_order_detail.html +++ b/InvenTree/order/templates/order/sales_order_detail.html @@ -44,6 +44,67 @@ $("#so-lines-table").inventreeTable({ part_detail: true, }, url: "{% url 'api-so-line-list' %}", + detailView: true, + detailFilter: function(index, row) { + return row.allocated > 0; + }, + detailFormatter: function(index, row, element) { + inventreeGet("{% url 'api-stock-list' %}", + { + location_detail: true, + sales_order_line: row.pk, + }, + { + success: function(response) { + + var html = `
`; + + element.html(html); + + $(`#allocation-table-${row.pk}`).bootstrapTable({ + data: response, + showHeader: false, + columns: [ + { + width: '50%', + field: 'quantity', + title: 'Quantity', + formatter: function(value, row, index, field) { + var html = ''; + if (row.serial && row.quantity == 1) { + html = `Serial Number: ${row.serial}`; + } else { + html = `Quantity: ${row.quantity}`; + } + + return renderLink(html, `/stock/item/${row.pk}/`); + }, + }, + { + field: 'location', + title: 'Location', + formatter: function(value, row, index, field) { + return renderLink(row.location_detail.pathstring, `/stock/location/${row.location}/`); + }, + }, + { + field: 'buttons', + title: 'Actions', + formatter: function(value, row, index, field) { + return ''; + }, + }, + ], + }); + }, + error: function(response) { + console.log("An error!"); + }, + } + ); + + return "{% trans 'Loading data' %}"; + }, columns: [ { field: 'pk', @@ -73,12 +134,29 @@ $("#so-lines-table").inventreeTable({ title: 'Quantity', formatter: function(value, row, index, field) { return makeProgressBar(row.allocated, row.quantity); + }, + sorter: function(valA, valB, rowA, rowB) { + + if (rowA.allocated == 0 && rowB.allocated == 0) { + return (rowA.quantity > rowB.quantity) ? 1 : -1; + } + + var progressA = rowA.allocated / rowA.quantity; + var progressB = rowB.allocated / rowA.quantity; + + return (progressA < progressB) ? 1 : -1; } }, { field: 'notes', title: 'Notes', }, + { + field: 'buttons', + formatter: function(value, row, index, field) { + return '-'; + } + }, ], }); diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index a7305dcb2e..4da079d4cc 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -233,9 +233,11 @@ class PartStarSerializer(InvenTreeModelSerializer): class BomItemSerializer(InvenTreeModelSerializer): """ Serializer for BomItem object """ + price_range = serializers.CharField(read_only=True) + part_detail = PartBriefSerializer(source='part', many=False, read_only=True) sub_part_detail = PartBriefSerializer(source='sub_part', many=False, read_only=True) - price_range = serializers.CharField(read_only=True) + validated = serializers.BooleanField(read_only=True, source='is_line_valid') def __init__(self, *args, **kwargs): diff --git a/InvenTree/stock/templates/stock/item_base.html b/InvenTree/stock/templates/stock/item_base.html index f339258d0f..bcb6de9af8 100644 --- a/InvenTree/stock/templates/stock/item_base.html +++ b/InvenTree/stock/templates/stock/item_base.html @@ -5,10 +5,10 @@ {% load i18n %} {% block content %} -{% if item.sales_order %} +{% if item.sales_order_line %}
- {% trans "This stock item is allocated to " %} - {{ item.sales_order.order }} + {% trans "This stock item is allocated to Sales Order" %} + {{ item.sales_order_line.order }}
{% endif %} From 4979c690d9deefa43435f7efcfec23071ff96796 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 21 Apr 2020 21:38:22 +1000 Subject: [PATCH 033/104] Prevent BOM price calculation from becoming too recursive --- InvenTree/part/models.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 8405afd972..79445fd02d 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -824,6 +824,11 @@ class Part(models.Model): max_price = None for item in self.bom_items.all().select_related('sub_part'): + + if item.sub_part.pk == self.pk: + print("Warning: Item contains itself in BOM") + continue + prices = item.sub_part.get_price_range(quantity * item.quantity) if prices is None: From 15166c7797caee10e3f58aabb225e690f7ee8db6 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 21 Apr 2020 21:43:04 +1000 Subject: [PATCH 034/104] Add a custom "id" to the progress bar --- InvenTree/InvenTree/static/script/inventree/inventree.js | 4 +++- InvenTree/order/templates/order/sales_order_detail.html | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/InvenTree/InvenTree/static/script/inventree/inventree.js b/InvenTree/InvenTree/static/script/inventree/inventree.js index ce812afc4f..c3e64d80de 100644 --- a/InvenTree/InvenTree/static/script/inventree/inventree.js +++ b/InvenTree/InvenTree/static/script/inventree/inventree.js @@ -97,8 +97,10 @@ function makeProgressBar(value, maximum, opts) { percent = 100; } + var id = opts.id || 'progress-bar'; + return ` -
+
${value} / ${maximum}
diff --git a/InvenTree/order/templates/order/sales_order_detail.html b/InvenTree/order/templates/order/sales_order_detail.html index 53b3dd869c..88b7fa8976 100644 --- a/InvenTree/order/templates/order/sales_order_detail.html +++ b/InvenTree/order/templates/order/sales_order_detail.html @@ -133,7 +133,9 @@ $("#so-lines-table").inventreeTable({ field: 'quantity', title: 'Quantity', formatter: function(value, row, index, field) { - return makeProgressBar(row.allocated, row.quantity); + return makeProgressBar(row.allocated, row.quantity, { + id: `order-line-progress-${row.pk}`, + }); }, sorter: function(valA, valB, rowA, rowB) { From b75c3432368ac45e0bb2181e6d11f0409c4a075b Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 21 Apr 2020 22:02:17 +1000 Subject: [PATCH 035/104] Add action buttons to the sales order page --- .../static/script/inventree/inventree.js | 16 ++++++++++++ .../templates/order/sales_order_detail.html | 25 ++++++++++++++++++- InvenTree/part/serializers.py | 2 ++ 3 files changed, 42 insertions(+), 1 deletion(-) diff --git a/InvenTree/InvenTree/static/script/inventree/inventree.js b/InvenTree/InvenTree/static/script/inventree/inventree.js index c3e64d80de..e2626e3c0a 100644 --- a/InvenTree/InvenTree/static/script/inventree/inventree.js +++ b/InvenTree/InvenTree/static/script/inventree/inventree.js @@ -78,6 +78,22 @@ function getImageUrlFromTransfer(transfer) { return url; } +function makeIconButton(icon, id, opts) { + // Construct an 'icon button' using the fontawesome set + + var options = opts || {}; + + var title = options.title || ''; + + var html = ''; + + html += ``; + + return html; +} + function makeProgressBar(value, maximum, opts) { /* * Render a progessbar! diff --git a/InvenTree/order/templates/order/sales_order_detail.html b/InvenTree/order/templates/order/sales_order_detail.html index 88b7fa8976..ed7293f126 100644 --- a/InvenTree/order/templates/order/sales_order_detail.html +++ b/InvenTree/order/templates/order/sales_order_detail.html @@ -156,7 +156,30 @@ $("#so-lines-table").inventreeTable({ { field: 'buttons', formatter: function(value, row, index, field) { - return '-'; + + var html = ''; + + var pk = row.pk; + + if (row.part) { + var part = row.part_detail; + + html = `
`; + + html += makeIconButton('fa-plus', `button-add-${pk}`); + + if (part.purchaseable) { + html += makeIconButton('fa-shopping-cart', `button-buy-${pk}`); + } + + if (part.assembly) { + html += makeIconButton('fa-tools', `button-build-${pk}`); + } + + html += `
`; + } + + return html; } }, ], diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index 4da079d4cc..1ee58062fe 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -65,6 +65,8 @@ class PartBriefSerializer(InvenTreeModelSerializer): 'thumbnail', 'active', 'assembly', + 'purchaseable', + 'salable', 'virtual', ] From 09ccd6c5e2549eb8b52c123e9d497e91508e2b72 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 21 Apr 2020 22:37:35 +1000 Subject: [PATCH 036/104] PEP style fixes --- InvenTree/order/forms.py | 1 - InvenTree/order/models.py | 6 ++++-- InvenTree/order/views.py | 2 -- InvenTree/stock/models.py | 2 +- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/InvenTree/order/forms.py b/InvenTree/order/forms.py index 1b7dda7b5d..19580e7226 100644 --- a/InvenTree/order/forms.py +++ b/InvenTree/order/forms.py @@ -90,7 +90,6 @@ class EditSalesOrderForm(HelperForm): ] - class EditPurchaseOrderAttachmentForm(HelperForm): """ Form for editing a PurchaseOrderAttachment object """ diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index ba8e6b0337..624519dd8b 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -19,6 +19,7 @@ import os from datetime import datetime from decimal import Decimal +from stock import models as stock_models from company.models import Company, SupplierPart from InvenTree.fields import RoundingDecimalField @@ -223,7 +224,7 @@ class PurchaseOrder(Order): # Create a new stock item if line.part: - stock = StockItem( + stock = stock_models.StockItem( part=line.part.part, supplier_part=line.part, location=location, @@ -264,7 +265,8 @@ class SalesOrder(Order): def get_absolute_url(self): return reverse('so-detail', kwargs={'pk': self.id}) - customer = models.ForeignKey(Company, + customer = models.ForeignKey( + Company, on_delete=models.SET_NULL, null=True, limit_choices_to={'is_customer': True}, diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index a718c96cc4..b25e5686f7 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -358,8 +358,6 @@ class SalesOrderEdit(AjaxUpdateView): def get_form(self): form = super().get_form() - order = self.get_object() - # Prevent user from editing customer form.fields['customer'].widget = HiddenInput() diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 4970083f0c..e34bbcfb11 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -268,7 +268,7 @@ class StockItem(MPTTModel): # the "Part" that the line item references is the same as the part that THIS references if self.sales_order_line is not None: - if self.sales_order_line.part == None: + if self.sales_order_line.part is None: raise ValidationError({'sales_order_line': _('Stock item cannot be assigned to a LineItem which does not reference a part')}) if not self.sales_order_line.part == self.part: From 79ea744280ccf7b4c3d1dc77a8e94f6753140e35 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 21 Apr 2020 22:39:47 +1000 Subject: [PATCH 037/104] Supplier part rendering fix --- .../templates/company/supplier_part_base.html | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/InvenTree/company/templates/company/supplier_part_base.html b/InvenTree/company/templates/company/supplier_part_base.html index fec430628b..665abbeea2 100644 --- a/InvenTree/company/templates/company/supplier_part_base.html +++ b/InvenTree/company/templates/company/supplier_part_base.html @@ -10,24 +10,28 @@ InvenTree | {% trans "Supplier Part" %}
-

{% trans "Supplier Part" %}

-
-
- - -
-
-
+
+
+
+
+

{% trans "Supplier Part" %}

+
+
+ + +
+
+
@@ -45,6 +49,7 @@ InvenTree | {% trans "Supplier Part" %} {% if part.description %} + {% trans "Description" %} {{ part.description }} From 808a636484857797c4e5f2ad94af1e8a9a646bd3 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 22 Apr 2020 09:01:52 +1000 Subject: [PATCH 038/104] Move "Company" view to new two-column template --- .../templates/company/company_base.html | 151 ++++++++---------- InvenTree/templates/base.html | 1 + InvenTree/templates/two_column.html | 42 +++++ 3 files changed, 108 insertions(+), 86 deletions(-) create mode 100644 InvenTree/templates/two_column.html diff --git a/InvenTree/company/templates/company/company_base.html b/InvenTree/company/templates/company/company_base.html index 693693c39d..fdd6abddd9 100644 --- a/InvenTree/company/templates/company/company_base.html +++ b/InvenTree/company/templates/company/company_base.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "two_column.html" %} {% load static %} {% load i18n %} @@ -7,100 +7,79 @@ InvenTree | {% trans "Company" %} - {{ company.name }} {% endblock %} -{% block content %} - -
-
-
-
-
- -
-
-
-

{{ company.name }}

-

{{ company.description }}

-
- {% if company.is_supplier %} - - {% endif %} - - -
-
-
-
-
- - - {% if company.website %} - - - - - - {% endif %} - {% if company.address %} - - - - - - {% endif %} - {% if company.phone %} - - - - - - {% endif %} - {% if company.email %} - - - - - - {% endif %} - {% if company.contact %} - - - - - - {% endif %} -
{% trans "Website" %}{{ company.website }}
{% trans "Address" %}{{ company.address }}
{% trans "Phone" %}{{ company.phone }}
{% trans "Email" %}{{ company.email }}
{% trans "Contact" %}{{ company.contact }}
-
+{% block thumbnail %} +
+
- -
- -
- -{% block details %} - {% endblock %} +{% block page_data %} +

{{ company.name }}

+

{{ company.description }}

+
+ {% if company.is_supplier %} + + {% endif %} + +
- {% endblock %} -{% block js_load %} -{{ block.super }} - +{% block page_details %} +

{% trans "Company Details" %}

+ + +{% if company.website %} + + + + + +{% endif %} +{% if company.address %} + + + + + +{% endif %} +{% if company.phone %} + + + + + +{% endif %} +{% if company.email %} + + + + + +{% endif %} +{% if company.contact %} + + + + + +{% endif %} +
{% trans "Website" %}{{ company.website }}
{% trans "Address" %}{{ company.address }}
{% trans "Phone" %}{{ company.phone }}
{% trans "Email" %}{{ company.email }}
{% trans "Contact" %}{{ company.contact }}
{% endblock %} {% block js_ready %} +{{ block.super }} $('#company-edit').click(function() { launchModalForm( diff --git a/InvenTree/templates/base.html b/InvenTree/templates/base.html index 8559e6d5f1..bf5fa4a1c3 100644 --- a/InvenTree/templates/base.html +++ b/InvenTree/templates/base.html @@ -107,6 +107,7 @@ InvenTree + diff --git a/InvenTree/templates/two_column.html b/InvenTree/templates/two_column.html new file mode 100644 index 0000000000..0aa3b441e2 --- /dev/null +++ b/InvenTree/templates/two_column.html @@ -0,0 +1,42 @@ +{% extends "base.html" %} + +{% load static %} +{% load i18n %} + +{% block content %} + +
+
+
+ {% block thumbnail %} + + {% endblock %} +
+
+ {% block page_data %} + + {% endblock %} +
+ +
+
+ {% block page_details %} + + {% endblock %} +
+
+ +
+ +
+ {% block details %} + + {% endblock %} +
+ +{% endblock %} + +{% block js_ready %} +{{ block.super }} + +{% endblock %} \ No newline at end of file From 372958d939a636badc3cc3689ae404961b877f28 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 22 Apr 2020 09:50:10 +1000 Subject: [PATCH 039/104] Migrate more pages to the two_column template --- .../templates/company/company_base.html | 4 +- .../templates/company/supplier_part_base.html | 170 ++++---- .../order/templates/order/order_base.html | 205 +++++---- .../templates/order/sales_order_base.html | 164 ++++---- .../stock/templates/stock/item_base.html | 394 +++++++++--------- .../stock/templates/stock/stock_app_base.html | 4 +- InvenTree/templates/two_column.html | 1 - 7 files changed, 457 insertions(+), 485 deletions(-) diff --git a/InvenTree/company/templates/company/company_base.html b/InvenTree/company/templates/company/company_base.html index fdd6abddd9..8e1a743d10 100644 --- a/InvenTree/company/templates/company/company_base.html +++ b/InvenTree/company/templates/company/company_base.html @@ -19,7 +19,9 @@ InvenTree | {% trans "Company" %} - {{ company.name }} {% endblock %} {% block page_data %} -

{{ company.name }}

+

{% trans "Company" %}

+
+

{{ company.name }}

{{ company.description }}

{% if company.is_supplier %} diff --git a/InvenTree/company/templates/company/supplier_part_base.html b/InvenTree/company/templates/company/supplier_part_base.html index 665abbeea2..5083af4f3a 100644 --- a/InvenTree/company/templates/company/supplier_part_base.html +++ b/InvenTree/company/templates/company/supplier_part_base.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "two_column.html" %} {% load static %} {% load i18n %} @@ -6,104 +6,86 @@ InvenTree | {% trans "Supplier Part" %} {% endblock %} -{% block content %} +{% block thumbnail %} + +{% endblock %} -
-
-
-
- -
-
-

{% trans "Supplier Part" %}

-
-
- - -
-
-
-
-
-
-

{% trans "Supplier Part Details" %}

- - - - - - - - {% if part.description %} - - - - - - {% endif %} - {% if part.link %} - - - - - - {% endif %} - - - - - - - - - - {% if part.manufacturer %} - - - - - - - - - - {% endif %} - {% if part.note %} - - - - - - {% endif %} -
{% trans "Internal Part" %} - {% if part.part %} - {{ part.part.full_name }} - {% endif %} -
{% trans "Description" %}{{ part.description }}
{% trans "External Link" %}{{ part.link }}
{% trans "Supplier" %}{{ part.supplier.name }}
{% trans "SKU" %}{{ part.SKU }}
{% trans "Manufacturer" %}{{ part.manufacturer.name }}
{% trans "MPN" %}{{ part.MPN }}
{% trans "Note" %}{{ part.note }}
+{% block page_data %} +

{% trans "Supplier Part" %}

+

{{ part.supplier.name }} - {{ part.SKU }}

+
+
+ +
+{% endblock %} +{% block page_details %} - - - -
- -
- {% block details %} - - {% endblock %} -
- +

{% trans "Supplier Part Details" %}

+ + + + + + + + {% if part.description %} + + + + + + {% endif %} + {% if part.link %} + + + + + + {% endif %} + + + + + + + + + + {% if part.manufacturer %} + + + + + + + + + + {% endif %} + {% if part.note %} + + + + + + {% endif %} +
{% trans "Internal Part" %} + {% if part.part %} + {{ part.part.full_name }} + {% endif %} +
{% trans "Description" %}{{ part.description }}
{% trans "External Link" %}{{ part.link }}
{% trans "Supplier" %}{{ part.supplier.name }}
{% trans "SKU" %}{{ part.SKU }}
{% trans "Manufacturer" %}{{ part.manufacturer.name }}
{% trans "MPN" %}{{ part.MPN }}
{% trans "Note" %}{{ part.note }}
{% endblock %} {% block js_ready %} diff --git a/InvenTree/order/templates/order/order_base.html b/InvenTree/order/templates/order/order_base.html index 7576f061bc..64026f2c5f 100644 --- a/InvenTree/order/templates/order/order_base.html +++ b/InvenTree/order/templates/order/order_base.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "two_column.html" %} {% load i18n %} {% load static %} @@ -9,120 +9,105 @@ InvenTree | {% trans "Purchase Order" %} {% endblock %} -{% block content %} +{% block thumbnail %} + +{% endblock %} -
-
-
-
- -
-
-

{{ order }}

-

{{ order.description }}

-

-

-
- - - {% if order.status == OrderStatus.PENDING and order.lines.count > 0 %} - - {% elif order.status == OrderStatus.PLACED %} - - - {% endif %} - {% if order.status == OrderStatus.PENDING or order.status == OrderStatus.PLACED %} - - {% endif %} -
-
-

-
+{% block page_data %} +

{% trans "Purchase Order" %}

+
+

{{ order }}

+

{{ order.description }}

+

+

+
+ + + {% if order.status == OrderStatus.PENDING and order.lines.count > 0 %} + + {% elif order.status == OrderStatus.PLACED %} + + + {% endif %} + {% if order.status == OrderStatus.PENDING or order.status == OrderStatus.PLACED %} + + {% endif %}
-
-

{% trans "Purchase Order Details" %}

- - - - - - - - - - - - - - - - - - {% if order.supplier_reference %} - - - - - - {% endif %} - {% if order.link %} - - - - - - {% endif %} - - - - - - {% if order.issue_date %} - - - - - - {% endif %} - {% if order.status == OrderStatus.COMPLETE %} - - - - - - {% endif %} -
{% trans "Order Reference" %}{{ order.reference }}
{% trans "Order Status" %}{% order_status order.status %}
{% trans "Supplier" %}{{ order.supplier.name }}
{% trans "Supplier Reference" %}{{ order.supplier_reference }}
External Link{{ order.link }}
{% trans "Created" %}{{ order.creation_date }}{{ order.created_by }}
{% trans "Issued" %}{{ order.issue_date }}
{% trans "Received" %}{{ order.complete_date }}{{ order.received_by }}
-
-
- -
-
-{% block details %} - - - +

{% endblock %} -
+{% block page_details %} +

{% trans "Purchase Order Details" %}

+ + + + + + + + + + + + + + + + + + {% if order.supplier_reference %} + + + + + + {% endif %} + {% if order.link %} + + + + + + {% endif %} + + + + + + {% if order.issue_date %} + + + + + + {% endif %} + {% if order.status == OrderStatus.COMPLETE %} + + + + + + {% endif %} +
{% trans "Order Reference" %}{{ order.reference }}
{% trans "Order Status" %}{% order_status order.status %}
{% trans "Supplier" %}{{ order.supplier.name }}
{% trans "Supplier Reference" %}{{ order.supplier_reference }}
External Link{{ order.link }}
{% trans "Created" %}{{ order.creation_date }}{{ order.created_by }}
{% trans "Issued" %}{{ order.issue_date }}
{% trans "Received" %}{{ order.complete_date }}{{ order.received_by }}
{% endblock %} {% block js_ready %} diff --git a/InvenTree/order/templates/order/sales_order_base.html b/InvenTree/order/templates/order/sales_order_base.html index ed8be4256b..98faadb1f4 100644 --- a/InvenTree/order/templates/order/sales_order_base.html +++ b/InvenTree/order/templates/order/sales_order_base.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "two_column.html" %} {% load i18n %} {% load static %} @@ -9,99 +9,83 @@ InvenTree | {% trans "Sales Order" %} {% endblock %} -{% block content %} - - -
-
-
-
- -
-
-

{{ order }}

-

{{ order.description }}

-

-

-
- -
-
-

-
-
-
-
-

{% trans "Sales Order Details" %}

- - - - - - - - - - - - - - - - - - {% if order.customer_reference %} - - - - - - {% endif %} - {% if order.link %} - - - - - - {% endif %} - - - - - - {% if order.issue_date %} - - - - - - {% endif %} - {% if order.status == OrderStatus.COMPLETE %} - - - - - - {% endif %} -
{% trans "Order Reference" %}{{ order.reference }}
{% trans "Order Status" %}{% order_status order.status %}
{% trans "Customer" %}{{ order.customer.name }}
{% trans "Customer Reference" %}{{ order.customer_reference }}
External Link{{ order.link }}
{% trans "Created" %}{{ order.creation_date }}{{ order.created_by }}
{% trans "Issued" %}{{ order.issue_date }}
{% trans "Received" %}{{ order.complete_date }}{{ order.received_by }}
-
-
+{% block thumbnail %} + +{% endblock %} +{% block page_data %} +

{% trans "Sales Order" %}


-
- {% block details %} - - {% endblock %} +

{{ order }}

+

{{ order.description }}

+
+
+ +
+{% endblock %} +{% block page_details %} +

{% trans "Sales Order Details" %}

+ + + + + + + + + + + + + + + + + + {% if order.customer_reference %} + + + + + + {% endif %} + {% if order.link %} + + + + + + {% endif %} + + + + + + {% if order.issue_date %} + + + + + + {% endif %} + {% if order.status == OrderStatus.COMPLETE %} + + + + + + {% endif %} +
{% trans "Order Reference" %}{{ order.reference }}
{% trans "Order Status" %}{% order_status order.status %}
{% trans "Customer" %}{{ order.customer.name }}
{% trans "Customer Reference" %}{{ order.customer_reference }}
External Link{{ order.link }}
{% trans "Created" %}{{ order.creation_date }}{{ order.created_by }}
{% trans "Issued" %}{{ order.issue_date }}
{% trans "Received" %}{{ order.complete_date }}{{ order.received_by }}
{% endblock %} {% block js_ready %} diff --git a/InvenTree/stock/templates/stock/item_base.html b/InvenTree/stock/templates/stock/item_base.html index bcb6de9af8..da002c2ac9 100644 --- a/InvenTree/stock/templates/stock/item_base.html +++ b/InvenTree/stock/templates/stock/item_base.html @@ -1,9 +1,19 @@ -{% extends "stock/stock_app_base.html" %} +{% extends "two_column.html" %} {% load static %} {% load inventree_extras %} {% load status_codes %} {% load i18n %} -{% block content %} + +{% block page_title %} +InvenTree | {% trans "Stock Item" %} - {{ item }} +{% endblock %} + +{% block sidenav %} +
+{% endblock %} + +{% block pre_content %} +{% include 'stock/loc_link.html' with location=item.location %} {% if item.sales_order_line %}
@@ -12,200 +22,212 @@
{% endif %} -
-
-

{% trans "Stock Item Details" %}

- {% if item.serialized %} -

{{ item.part.full_name}} # {{ item.serial }}

- {% else %} -

{% decimal item.quantity %} × {{ item.part.full_name }}

- {% endif %} -

-

- {% include "qr_button.html" %} - {% if item.in_stock %} - {% if not item.serialized %} - - - - {% if item.part.trackable %} - - {% endif %} - {% endif %} - - - {% endif %} - - {% if item.can_delete %} - - {% endif %} -
-

- {% if item.serialized %} -
- {% trans "This stock item is serialized - it has a unique serial number and the quantity cannot be adjusted." %} -
- {% elif item.child_count > 0 %} -
- {% trans "This stock item cannot be deleted as it has child items" %} -
- {% elif item.delete_on_deplete %} -
- {% trans "This stock item will be automatically deleted when all stock is depleted." %} -
- {% endif %} - {% if item.parent %} -
- {% trans "This stock item was split from " %}{{ item.parent }} -
- {% endif %} -
- -
-
- - - - - - - - {% if item.belongs_to %} - - - - - - {% elif item.location %} - - - - - - {% endif %} - {% if item.uid %} - - - - - - {% endif %} - {% if item.serialized %} - - - - - - {% else %} - - - - - - {% endif %} - {% if item.batch %} - - - - - - {% endif %} - {% if item.build %} - - - - - - {% endif %} - {% if item.purchase_order %} - - - - - - {% endif %} - {% if item.customer %} - - - - - - {% endif %} - {% if item.link %} - - - - - {% endif %} - {% if item.supplier_part %} - - - - - - - - - - - {% endif %} - - - - - - - - - {% if item.stocktake_date %} - - {% else %} - - {% endif %} - - - - - - -
Part - {% include "hover_image.html" with image=item.part.image hover=True %} - {{ item.part.full_name }} -
{% trans "Belongs To" %}{{ item.belongs_to }}
{% trans "Location" %}{{ item.location.name }}
{% trans "Unique Identifier" %}{{ item.uid }}
{% trans "Serial Number" %}{{ item.serial }}
{% trans "Quantity" %}{% decimal item.quantity %} {% if item.part.units %}{{ item.part.units }}{% endif %}
{% trans "Batch" %}{{ item.batch }}
{% trans "Build" %}{{ item.build }}
{% trans "Purchase Order" %}{{ item.purchase_order }}
{% trans "Customer" %}{{ item.customer.name }}
- {% trans "External Link" %}{{ item.link }}
{% trans "Supplier" %}{{ item.supplier_part.supplier.name }}
{% trans "Supplier Part" %}{{ item.supplier_part.SKU }}
{% trans "Last Updated" %}{{ item.updated }}
{% trans "Last Stocktake" %}{{ item.stocktake_date }} {{ item.stocktake_user }}{% trans "No stocktake performed" %}
{% trans "Status" %}{% stock_status item.status %}
-
-
+{% if item.serialized %} +
+ {% trans "This stock item is serialized - it has a unique serial number and the quantity cannot be adjusted." %} +
+{% elif item.child_count > 0 %} +
+ {% trans "This stock item cannot be deleted as it has child items" %} +
+{% elif item.delete_on_deplete %} +
+ {% trans "This stock item will be automatically deleted when all stock is depleted." %} +
+{% endif %} +{% if item.parent %} +
+ {% trans "This stock item was split from " %}{{ item.parent }} +
+{% endif %} - -
-
-{% block details %} - {% endblock %} + +{% block thumbnail %} + +{% endblock %} + +{% block page_data %} +

{% trans "Stock Item" %}

+
+

+{% if item.serialized %} +{{ item.part.full_name}} # {{ item.serial }} +{% else %} +{% decimal item.quantity %} × {{ item.part.full_name }} +{% endif %} +

+ +
+ {% include "qr_button.html" %} + {% if item.in_stock %} + {% if not item.serialized %} + + + + {% if item.part.trackable %} + + {% endif %} + {% endif %} + + + {% endif %} + + {% if item.can_delete %} + + {% endif %}
{% endblock %} +{% block page_details %} +

{% trans "Stock Item Details" %}

+ + + + + + + + {% if item.belongs_to %} + + + + + + {% elif item.location %} + + + + + + {% endif %} + {% if item.uid %} + + + + + + {% endif %} + {% if item.serialized %} + + + + + + {% else %} + + + + + + {% endif %} + {% if item.batch %} + + + + + + {% endif %} + {% if item.build %} + + + + + + {% endif %} + {% if item.purchase_order %} + + + + + + {% endif %} + {% if item.customer %} + + + + + + {% endif %} + {% if item.link %} + + + + + {% endif %} + {% if item.supplier_part %} + + + + + + + + + + + {% endif %} + + + + + + + + + {% if item.stocktake_date %} + + {% else %} + + {% endif %} + + + + + + +
Part + {% include "hover_image.html" with image=item.part.image hover=True %} + {{ item.part.full_name }} +
{% trans "Belongs To" %}{{ item.belongs_to }}
{% trans "Location" %}{{ item.location.name }}
{% trans "Unique Identifier" %}{{ item.uid }}
{% trans "Serial Number" %}{{ item.serial }}
{% trans "Quantity" %}{% decimal item.quantity %} {% if item.part.units %}{{ item.part.units }}{% endif %}
{% trans "Batch" %}{{ item.batch }}
{% trans "Build" %}{{ item.build }}
{% trans "Purchase Order" %}{{ item.purchase_order }}
{% trans "Customer" %}{{ item.customer.name }}
+ {% trans "External Link" %}{{ item.link }}
{% trans "Supplier" %}{{ item.supplier_part.supplier.name }}
{% trans "Supplier Part" %}{{ item.supplier_part.SKU }}
{% trans "Last Updated" %}{{ item.updated }}
{% trans "Last Stocktake" %}{{ item.stocktake_date }} {{ item.stocktake_user }}{% trans "No stocktake performed" %}
{% trans "Status" %}{% stock_status item.status %}
+{% endblock %} + {% block js_ready %} {{ block.super }} +loadTree("{% url 'api-stock-tree' %}", + "#stock-tree", + { + name: 'stock', + } + ); + + $("#toggle-stock-tree").click(function() { + toggleSideNav("#sidenav"); + return false; + }) + + initSideNav(); + $("#stock-serialize").click(function() { launchModalForm( "{% url 'stock-item-serialize' item.id %}", diff --git a/InvenTree/stock/templates/stock/stock_app_base.html b/InvenTree/stock/templates/stock/stock_app_base.html index 8f5f051003..7f31ac21b0 100644 --- a/InvenTree/stock/templates/stock/stock_app_base.html +++ b/InvenTree/stock/templates/stock/stock_app_base.html @@ -3,9 +3,7 @@ {% load i18n %} {% block page_title %} -{% if item %} -InvenTree | {% trans "Stock Item" %} - {{ item }} -{% elif location %} +{% if location %} InvenTree | {% trans "Stock Location" %} - {{ location }} {% else %} InvenTree | Stock diff --git a/InvenTree/templates/two_column.html b/InvenTree/templates/two_column.html index 0aa3b441e2..1c90887b3c 100644 --- a/InvenTree/templates/two_column.html +++ b/InvenTree/templates/two_column.html @@ -17,7 +17,6 @@ {% endblock %}
-
{% block page_details %} From 12daf15406ddc4d81ddcc065d64575db300e912e Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 22 Apr 2020 09:53:11 +1000 Subject: [PATCH 040/104] Update build page --- .../build/templates/build/build_base.html | 159 ++++++++---------- 1 file changed, 71 insertions(+), 88 deletions(-) diff --git a/InvenTree/build/templates/build/build_base.html b/InvenTree/build/templates/build/build_base.html index b1bd07773d..cd238a2299 100644 --- a/InvenTree/build/templates/build/build_base.html +++ b/InvenTree/build/templates/build/build_base.html @@ -1,104 +1,87 @@ -{% extends "base.html" %} +{% extends "two_column.html" %} {% load static %} {% load i18n %} {% load status_codes %} {% block page_title %} -InvenTree | Build - {{ build }} +InvenTree | {% trans "Build" %} - {{ build }} {% endblock %} -{% block content %} - -
-
-
-
-
- -
-
-
-

{% trans "Build" %}

-
-
- - {% if build.is_active %} - - - {% endif %} - {% if build.status == BuildStatus.CANCELLED %} - - {% endif %} -
-
-
-
-
-
-

{% trans "Build Details" %}

- - - - - - - - - - - - - - - - - - - - - - - - - - -
{% trans "Build Title" %}{{ build.title }}
Part{{ build.part.full_name }}
{% trans "Quantity" %}{{ build.quantity }}
{% trans "Status" %}{% build_status build.status %}
{% trans "BOM Price" %} - {% if bom_price %} - {{ bom_price }} - {% if build.part.has_complete_bom_pricing == False %} -
{% trans "BOM pricing is incomplete" %} - {% endif %} - {% else %} - {% trans "No pricing information" %} - {% endif %} -
-
-
-
+{% block thumbnail %} + +{% endblock %} +{% block page_data %} +

{% trans "Build" %}


- -
-{% block details %} - +

{{ build.quantity }} x {{ build.part.full_name }}

+
+
+ + {% if build.is_active %} + + + {% endif %} + {% if build.status == BuildStatus.CANCELLED %} + + {% endif %} +
+
{% endblock %} -
+{% block page_details %} +

{% trans "Build Details" %}

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
{% trans "Build Title" %}{{ build.title }}
Part{{ build.part.full_name }}
{% trans "Quantity" %}{{ build.quantity }}
{% trans "Status" %}{% build_status build.status %}
{% trans "BOM Price" %} + {% if bom_price %} + {{ bom_price }} + {% if build.part.has_complete_bom_pricing == False %} +
{% trans "BOM pricing is incomplete" %} + {% endif %} + {% else %} + {% trans "No pricing information" %} + {% endif %} +
{% endblock %} {% block js_load %} From 6dd79af0b6de8e1779515fda03f3ad28fac556db Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 22 Apr 2020 10:11:40 +1000 Subject: [PATCH 041/104] Expose "is_allocated" parameter on StockItem API --- .../static/script/inventree/stock.js | 8 +++++++- InvenTree/InvenTree/status_codes.py | 12 ++++++++++++ InvenTree/stock/api.py | 11 +++++++++++ .../migrations/0029_auto_20200421_2359.py | 19 +++++++++++++++++++ InvenTree/stock/models.py | 10 ++++++++++ InvenTree/stock/serializers.py | 3 +++ InvenTree/templates/table_filters.html | 7 ++++++- 7 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 InvenTree/stock/migrations/0029_auto_20200421_2359.py diff --git a/InvenTree/InvenTree/static/script/inventree/stock.js b/InvenTree/InvenTree/static/script/inventree/stock.js index 3fe11ed087..ef8e5ab761 100644 --- a/InvenTree/InvenTree/static/script/inventree/stock.js +++ b/InvenTree/InvenTree/static/script/inventree/stock.js @@ -228,8 +228,14 @@ function loadStockTable(table, options) { } else { url = `/part/${row.part}/`; } + + html = imageHoverIcon(thumb) + renderLink(name, url); + + if (row.allocated) { + html += ``; + } - return imageHoverIcon(thumb) + renderLink(name, url); + return html; } }, { diff --git a/InvenTree/InvenTree/status_codes.py b/InvenTree/InvenTree/status_codes.py index a07a8d6f99..fa2a4d9bfd 100644 --- a/InvenTree/InvenTree/status_codes.py +++ b/InvenTree/InvenTree/status_codes.py @@ -119,6 +119,13 @@ class StockStatus(StatusCode): DAMAGED = 55 # Item is damaged DESTROYED = 60 # Item is destroyed LOST = 70 # Item has been lost + RETURNED = 85 # Item has been returned from a customer + + # Any stock code above 100 means that the stock item is not "in stock" + # This can be used as a quick check for filtering + NOT_IN_STOCK = 100 + + SHIPPED = 110 # Item has been shipped to a customer options = { OK: _("OK"), @@ -126,12 +133,15 @@ class StockStatus(StatusCode): DAMAGED: _("Damaged"), DESTROYED: _("Destroyed"), LOST: _("Lost"), + SHIPPED: _("Shipped"), + RETURNED: _("Returned"), } labels = { OK: 'success', ATTENTION: 'warning', DAMAGED: 'danger', + DESTROYED: 'danger', } # The following codes correspond to parts that are 'available' or 'in stock' @@ -139,12 +149,14 @@ class StockStatus(StatusCode): OK, ATTENTION, DAMAGED, + RETURNED, ] # The following codes correspond to parts that are 'unavailable' UNAVAILABLE_CODES = [ DESTROYED, LOST, + SHIPPED, ] diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index 450f6e95f1..c144316c70 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -367,6 +367,17 @@ class StockList(generics.ListCreateAPIView): # Filter out parts which are not actually "in stock" stock_list = stock_list.filter(customer=None, belongs_to=None) + # Filter by 'allocated' patrs? + allocated = self.request.query_params.get('allocated', None) + + if allocated is not None: + allocated = str2bool(allocated) + + if allocated: + stock_list = stock_list.exclude(Q(sales_order_line=None)) + else: + stock_list = stock_list.filter(Q(sales_order_line=None)) + # Do we wish to filter by "active parts" active = self.request.query_params.get('active', None) diff --git a/InvenTree/stock/migrations/0029_auto_20200421_2359.py b/InvenTree/stock/migrations/0029_auto_20200421_2359.py new file mode 100644 index 0000000000..1b89a9d143 --- /dev/null +++ b/InvenTree/stock/migrations/0029_auto_20200421_2359.py @@ -0,0 +1,19 @@ +# Generated by Django 3.0.5 on 2020-04-21 23:59 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('stock', '0028_auto_20200421_0724'), + ] + + operations = [ + migrations.AlterField( + model_name='stockitem', + name='status', + field=models.PositiveIntegerField(choices=[(10, 'OK'), (50, 'Attention needed'), (55, 'Damaged'), (60, 'Destroyed'), (70, 'Lost'), (110, 'Shipped'), (85, 'Returned')], default=10, validators=[django.core.validators.MinValueValidator(0)]), + ), + ] diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index e34bbcfb11..a29018fb88 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -397,6 +397,16 @@ class StockItem(MPTTModel): infinite = models.BooleanField(default=False) + def is_allocated(self): + """ + Return True if this StockItem is allocated to a SalesOrder or a Build + """ + + # TODO - For now this only checks if the StockItem is allocated to a SalesOrder + # TODO - In future, once the "build" is working better, check this too + + return self.sales_order_line is not None + def can_delete(self): """ Can this stock item be deleted? It can NOT be deleted under the following circumstances: diff --git a/InvenTree/stock/serializers.py b/InvenTree/stock/serializers.py index 616755b11b..e0176f5192 100644 --- a/InvenTree/stock/serializers.py +++ b/InvenTree/stock/serializers.py @@ -90,6 +90,8 @@ class StockItemSerializer(InvenTreeModelSerializer): tracking_items = serializers.IntegerField(source='tracking_info_count', read_only=True) + allocated = serializers.BooleanField(source='is_allocated', read_only=True) + def __init__(self, *args, **kwargs): part_detail = kwargs.pop('part_detail', False) @@ -110,6 +112,7 @@ class StockItemSerializer(InvenTreeModelSerializer): class Meta: model = StockItem fields = [ + 'allocated', 'batch', 'in_stock', 'link', diff --git a/InvenTree/templates/table_filters.html b/InvenTree/templates/table_filters.html index 322acedb33..8dff5ed1be 100644 --- a/InvenTree/templates/table_filters.html +++ b/InvenTree/templates/table_filters.html @@ -27,11 +27,16 @@ function getAvailableTableFilters(tableKey) { title: '{% trans "Active parts" %}', description: '{% trans "Show stock for active parts" %}', }, - 'status': { + status: { options: stockCodes, title: '{% trans "Stock status" %}', description: '{% trans "Stock status" %}', }, + allocated: { + type: 'bool', + title: '{% trans "Is allocated" %}', + description: '{% trans "Item has been alloacted" %}', + }, }; } From 3a71a4f63a78c2b4e315f5830dce2c7ee529c81c Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 22 Apr 2020 10:16:54 +1000 Subject: [PATCH 042/104] Fix for StockItem model - Allow sales_order_line to be blank --- .../static/script/inventree/stock.js | 17 ++++++++++------ .../migrations/0030_auto_20200422_0015.py | 20 +++++++++++++++++++ InvenTree/stock/models.py | 2 +- 3 files changed, 32 insertions(+), 7 deletions(-) create mode 100644 InvenTree/stock/migrations/0030_auto_20200422_0015.py diff --git a/InvenTree/InvenTree/static/script/inventree/stock.js b/InvenTree/InvenTree/static/script/inventree/stock.js index ef8e5ab761..e21971bb0f 100644 --- a/InvenTree/InvenTree/static/script/inventree/stock.js +++ b/InvenTree/InvenTree/static/script/inventree/stock.js @@ -230,10 +230,6 @@ function loadStockTable(table, options) { } html = imageHoverIcon(thumb) + renderLink(name, url); - - if (row.allocated) { - html += ``; - } return html; } @@ -261,9 +257,18 @@ function loadStockTable(table, options) { val = +val.toFixed(5); } - var text = renderLink(val, '/stock/item/' + row.pk + '/'); + var html = renderLink(val, `/stock/item/${row.pk}/`); - return text; + if (row.allocated) { + html += ``; + } + + // 70 = "LOST" + if (row.status == 70) { + html += ``; + } + + return html; } }, { diff --git a/InvenTree/stock/migrations/0030_auto_20200422_0015.py b/InvenTree/stock/migrations/0030_auto_20200422_0015.py new file mode 100644 index 0000000000..c720ac48ef --- /dev/null +++ b/InvenTree/stock/migrations/0030_auto_20200422_0015.py @@ -0,0 +1,20 @@ +# Generated by Django 3.0.5 on 2020-04-22 00:15 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('order', '0023_auto_20200420_2309'), + ('stock', '0029_auto_20200421_2359'), + ] + + operations = [ + migrations.AlterField( + model_name='stockitem', + name='sales_order_line', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='stock_items', to='order.SalesOrderLineItem'), + ), + ] diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index a29018fb88..61683a361f 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -373,7 +373,7 @@ class StockItem(MPTTModel): SalesOrderLineItem, on_delete=models.SET_NULL, related_name='stock_items', - null=True) + null=True, blank=True) # last time the stock was checked / counted stocktake_date = models.DateField(blank=True, null=True) From 2cb1b076f613115d3323c3ca8a979b4690eb0e0d Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 22 Apr 2020 12:12:48 +1000 Subject: [PATCH 043/104] Create "SalesOrderAllocation" object - Links multiple StockItem objects to a single SalesOrderLineItem --- InvenTree/order/api.py | 2 +- .../migrations/0024_salesorderallocation.py | 26 +++++++++++++++++++ InvenTree/order/models.py | 22 +++++++++++++++- InvenTree/order/tests.py | 4 +-- InvenTree/stock/__init__.py | 2 +- .../migrations/0031_auto_20200422_0209.py | 24 +++++++++++++++++ InvenTree/stock/models.py | 22 +++------------- 7 files changed, 79 insertions(+), 23 deletions(-) create mode 100644 InvenTree/order/migrations/0024_salesorderallocation.py create mode 100644 InvenTree/stock/migrations/0031_auto_20200422_0209.py diff --git a/InvenTree/order/api.py b/InvenTree/order/api.py index 6599d1a209..127ae7399a 100644 --- a/InvenTree/order/api.py +++ b/InvenTree/order/api.py @@ -335,7 +335,7 @@ class SOLineItemList(generics.ListCreateAPIView): return queryset.prefetch_related( 'part', 'part__stock_items', - 'stock_items', + 'allocations', 'order', ) diff --git a/InvenTree/order/migrations/0024_salesorderallocation.py b/InvenTree/order/migrations/0024_salesorderallocation.py new file mode 100644 index 0000000000..ca8ed182d9 --- /dev/null +++ b/InvenTree/order/migrations/0024_salesorderallocation.py @@ -0,0 +1,26 @@ +# Generated by Django 3.0.5 on 2020-04-22 02:09 + +import InvenTree.fields +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('stock', '0030_auto_20200422_0015'), + ('order', '0023_auto_20200420_2309'), + ] + + operations = [ + migrations.CreateModel( + name='SalesOrderAllocation', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('quantity', InvenTree.fields.RoundingDecimalField(decimal_places=5, default=1, max_digits=15, validators=[django.core.validators.MinValueValidator(0)])), + ('item', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='sales_order_allocation', to='stock.StockItem')), + ('line', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='allocations', to='order.SalesOrderLineItem')), + ], + ), + ] diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index 624519dd8b..bfaff7e84e 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -380,6 +380,26 @@ class SalesOrderLineItem(OrderLineItem): This is a summation of the quantity of each attached StockItem """ - query = self.stock_items.aggregate(allocated=Coalesce(Sum('quantity'), Decimal(0))) + query = self.allocations.aggregate(allocated=Coalesce(Sum('quantity'), Decimal(0))) return query['allocated'] + + +class SalesOrderAllocation(models.Model): + """ + This model is used to 'allocate' stock items to a SalesOrder. + Items that are "allocated" to a SalesOrder are not yet "attached" to the order, + but they will be once the order is fulfilled. + + Attributes: + line: SalesOrderLineItem reference + item: StockItem reference + quantity: Quantity to take from the StockItem + + """ + + line = models.ForeignKey(SalesOrderLineItem, on_delete=models.CASCADE, related_name='allocations') + + item = models.OneToOneField('stock.StockItem', on_delete=models.CASCADE, related_name='sales_order_allocation') + + quantity = RoundingDecimalField(max_digits=15, decimal_places=5, validators=[MinValueValidator(0)], default=1) diff --git a/InvenTree/order/tests.py b/InvenTree/order/tests.py index 35cf8909be..8ad7422259 100644 --- a/InvenTree/order/tests.py +++ b/InvenTree/order/tests.py @@ -31,11 +31,11 @@ class OrderTest(TestCase): self.assertEqual(order.get_absolute_url(), '/order/purchase-order/1/') - self.assertEqual(str(order), 'PO 1') + self.assertEqual(str(order), 'PO 1 - ACME') line = PurchaseOrderLineItem.objects.get(pk=1) - self.assertEqual(str(line), "100 x ACME0001 from ACME (for PO 1)") + self.assertEqual(str(line), "100 x ACME0001 from ACME (for PO 1 - ACME)") def test_on_order(self): """ There should be 3 separate items on order for the M2x4 LPHS part """ diff --git a/InvenTree/stock/__init__.py b/InvenTree/stock/__init__.py index 6970329be1..7b58c08fc9 100644 --- a/InvenTree/stock/__init__.py +++ b/InvenTree/stock/__init__.py @@ -6,4 +6,4 @@ It includes models for: - StockLocation - StockItem - StockItemTracking -""" +""" \ No newline at end of file diff --git a/InvenTree/stock/migrations/0031_auto_20200422_0209.py b/InvenTree/stock/migrations/0031_auto_20200422_0209.py new file mode 100644 index 0000000000..1da143aac5 --- /dev/null +++ b/InvenTree/stock/migrations/0031_auto_20200422_0209.py @@ -0,0 +1,24 @@ +# Generated by Django 3.0.5 on 2020-04-22 02:09 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('order', '0024_salesorderallocation'), + ('stock', '0030_auto_20200422_0015'), + ] + + operations = [ + migrations.RemoveField( + model_name='stockitem', + name='sales_order_line', + ), + migrations.AddField( + model_name='stockitem', + name='sales_order', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='stock_items', to='order.SalesOrder'), + ), + ] diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 61683a361f..17dea77305 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -29,7 +29,7 @@ from InvenTree.models import InvenTreeTree from InvenTree.fields import InvenTreeURLField from part.models import Part -from order.models import PurchaseOrder, SalesOrderLineItem +from order.models import PurchaseOrder, SalesOrder class StockLocation(InvenTreeTree): @@ -127,7 +127,7 @@ class StockItem(MPTTModel): build: Link to a Build (if this stock item was created from a build) purchase_order: Link to a PurchaseOrder (if this stock item was created from a PurchaseOrder) infinite: If True this StockItem can never be exhausted - sales_order: Link to a SalesOrderLineItem (if this stockitem has been allocated to a sales order) + sales_order: Link to a SalesOrder object (if the StockItem has been assigned to a SalesOrder) """ def save(self, *args, **kwargs): @@ -263,20 +263,6 @@ class StockItem(MPTTModel): # TODO - Find a test than can be perfomed... pass - try: - # If this StockItem is assigned to a SalesOrderLineItem, - # the "Part" that the line item references is the same as the part that THIS references - if self.sales_order_line is not None: - - if self.sales_order_line.part is None: - raise ValidationError({'sales_order_line': _('Stock item cannot be assigned to a LineItem which does not reference a part')}) - - if not self.sales_order_line.part == self.part: - raise ValidationError({'sales_order_line': _('Stock item does not reference the same part object as the LineItem')}) - - except SalesOrderLineItem.DoesNotExist: - pass - if self.belongs_to and self.belongs_to.pk == self.pk: raise ValidationError({ 'belongs_to': _('Item cannot belong to itself') @@ -369,8 +355,8 @@ class StockItem(MPTTModel): help_text=_('Purchase order for this stock item') ) - sales_order_line = models.ForeignKey( - SalesOrderLineItem, + sales_order = models.ForeignKey( + SalesOrder, on_delete=models.SET_NULL, related_name='stock_items', null=True, blank=True) From 1373425c292a4905fd81538b7a9b4f53e24b2ebb Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 22 Apr 2020 13:11:19 +1000 Subject: [PATCH 044/104] Update definition for StockItemAllocation model - Limit foreignkey choices - Error checking - Check if a StockItem is over-allocated - Fix API serialization and filtering --- InvenTree/build/models.py | 9 ++++ InvenTree/order/admin.py | 13 ++++- .../migrations/0025_auto_20200422_0222.py | 18 +++++++ .../migrations/0026_auto_20200422_0224.py | 20 ++++++++ .../migrations/0027_auto_20200422_0236.py | 20 ++++++++ InvenTree/order/models.py | 49 ++++++++++++++++++- InvenTree/stock/api.py | 22 +++------ InvenTree/stock/models.py | 38 +++++++++++++- InvenTree/stock/serializers.py | 2 + .../stock/templates/stock/item_base.html | 13 +++-- 10 files changed, 181 insertions(+), 23 deletions(-) create mode 100644 InvenTree/order/migrations/0025_auto_20200422_0222.py create mode 100644 InvenTree/order/migrations/0026_auto_20200422_0224.py create mode 100644 InvenTree/order/migrations/0027_auto_20200422_0236.py diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index 8730fd6700..532526ea60 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -393,6 +393,15 @@ class BuildItem(models.Model): q=self.stock_item.quantity ))] + if self.stock_item.quantity - self.stock_item.allocation_count() < self.quantity: + errors['quantity'] = _('StockItem is over-allocated') + + if self.quantity <= 0: + errors['quantity'] = _('Allocation quantity must be greater than zero') + + if self.stock_item.serial and not self.quantity == 1: + errors['quantity'] = _('Quantity must be 1 for serialized stock') + except StockItem.DoesNotExist: pass diff --git a/InvenTree/order/admin.py b/InvenTree/order/admin.py index 459b7a3821..a213f09764 100644 --- a/InvenTree/order/admin.py +++ b/InvenTree/order/admin.py @@ -10,7 +10,7 @@ from import_export.fields import Field from .models import PurchaseOrder, PurchaseOrderLineItem from .models import SalesOrder, SalesOrderLineItem - +from .models import SalesOrderAllocation class PurchaseOrderAdmin(ImportExportModelAdmin): @@ -86,8 +86,19 @@ class SalesOrderLineItemAdmin(ImportExportModelAdmin): ) +class SalesOrderAllocationAdmin(ImportExportModelAdmin): + + list_display = ( + 'line', + 'item', + 'quantity' + ) + + admin.site.register(PurchaseOrder, PurchaseOrderAdmin) admin.site.register(PurchaseOrderLineItem, PurchaseOrderLineItemAdmin) admin.site.register(SalesOrder, SalesOrderAdmin) admin.site.register(SalesOrderLineItem, SalesOrderLineItemAdmin) + +admin.site.register(SalesOrderAllocation, SalesOrderAllocationAdmin) diff --git a/InvenTree/order/migrations/0025_auto_20200422_0222.py b/InvenTree/order/migrations/0025_auto_20200422_0222.py new file mode 100644 index 0000000000..34d0114ac9 --- /dev/null +++ b/InvenTree/order/migrations/0025_auto_20200422_0222.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.5 on 2020-04-22 02:22 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('stock', '0031_auto_20200422_0209'), + ('order', '0024_salesorderallocation'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='salesorderallocation', + unique_together={('line', 'item')}, + ), + ] diff --git a/InvenTree/order/migrations/0026_auto_20200422_0224.py b/InvenTree/order/migrations/0026_auto_20200422_0224.py new file mode 100644 index 0000000000..c92280898e --- /dev/null +++ b/InvenTree/order/migrations/0026_auto_20200422_0224.py @@ -0,0 +1,20 @@ +# Generated by Django 3.0.5 on 2020-04-22 02:24 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('stock', '0031_auto_20200422_0209'), + ('order', '0025_auto_20200422_0222'), + ] + + operations = [ + migrations.AlterField( + model_name='salesorderallocation', + name='item', + field=models.OneToOneField(limit_choices_to={'part__salable': True}, on_delete=django.db.models.deletion.CASCADE, related_name='sales_order_allocation', to='stock.StockItem'), + ), + ] diff --git a/InvenTree/order/migrations/0027_auto_20200422_0236.py b/InvenTree/order/migrations/0027_auto_20200422_0236.py new file mode 100644 index 0000000000..a4af5aedd3 --- /dev/null +++ b/InvenTree/order/migrations/0027_auto_20200422_0236.py @@ -0,0 +1,20 @@ +# Generated by Django 3.0.5 on 2020-04-22 02:36 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('stock', '0031_auto_20200422_0209'), + ('order', '0026_auto_20200422_0224'), + ] + + operations = [ + migrations.AlterField( + model_name='salesorderallocation', + name='item', + field=models.ForeignKey(limit_choices_to={'part__salable': True}, on_delete=django.db.models.deletion.CASCADE, related_name='sales_order_allocations', to='stock.StockItem'), + ), + ] diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index bfaff7e84e..8e9910cfc6 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -398,8 +398,55 @@ class SalesOrderAllocation(models.Model): """ + class Meta: + unique_together = [ + # Cannot allocate any given StockItem to the same line more than once + ('line', 'item'), + ] + + def clean(self): + """ + Validate the SalesOrderAllocation object: + + - Cannot allocate stock to a line item without a part reference + - The referenced part must match the part associated with the line item + - Allocated quantity cannot exceed the quantity of the stock item + - Allocation quantity must be "1" if the StockItem is serialized + - Allocation quantity cannot be zero + """ + + super().clean() + + errors = {} + + try: + if not self.line.part == self.item.part: + errors['item'] = _('Cannot allocate stock item to a line with a different part') + except Part.DoesNotExist: + errors['line'] = _('Cannot allocate stock to a line without a part') + + if self.quantity > self.item.quantity: + errors['quantity'] = _('Allocation quantity cannot exceed stock quantity') + + if self.item.quantity - self.item.allocation_count() < self.quantity: + errors['quantity'] = _('StockItem is over-allocated') + + if self.quantity <= 0: + errors['quantity'] = _('Allocation quantity must be greater than zero') + + if self.item.serial and not self.quantity == 1: + errors['quantity'] = _('Quantity must be 1 for serialized stock item') + + if len(errors) > 0: + raise ValidationError(errors) + line = models.ForeignKey(SalesOrderLineItem, on_delete=models.CASCADE, related_name='allocations') - item = models.OneToOneField('stock.StockItem', on_delete=models.CASCADE, related_name='sales_order_allocation') + item = models.ForeignKey( + 'stock.StockItem', + on_delete=models.CASCADE, + related_name='sales_order_allocations', + limit_choices_to={'part__salable': True}, + ) quantity = RoundingDecimalField(max_digits=15, decimal_places=5, validators=[MinValueValidator(0)], default=1) diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index c144316c70..bcec2c2bf1 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -374,10 +374,12 @@ class StockList(generics.ListCreateAPIView): allocated = str2bool(allocated) if allocated: - stock_list = stock_list.exclude(Q(sales_order_line=None)) + # Filter StockItem with either build allocations or sales order allocations + stock_list = stock_list.filter(Q(sales_order_allocations__isnull=False) | Q(allocations__isnull=False)) else: - stock_list = stock_list.filter(Q(sales_order_line=None)) - + # Filter StockItem without build allocations or sales order allocations + stock_list = stock_list.filter(Q(sales_order_allocations__isnull=True) & Q(allocations__isnull=True)) + # Do we wish to filter by "active parts" active = self.request.query_params.get('active', None) @@ -477,22 +479,10 @@ class StockList(generics.ListCreateAPIView): if manufacturer is not None: stock_list = stock_list.filter(supplier_part__manufacturer=manufacturer) - # Filter by sales order - sales_order = self.request.query_params.get('sales_order', None) - - if sales_order is not None: - try: - sales_order = SalesOrder.objects.get(pk=sales_order) - lines = [line.pk for line in sales_order.lines.all()] - stock_list = stock_list.filter(sales_order_line__in=lines) - except (SalesOrder.DoesNotExist, ValueError): - raise ValidationError({'sales_order': 'Invalid SalesOrder object specified'}) - # Also ensure that we pre-fecth all the related items stock_list = stock_list.prefetch_related( 'part', 'part__category', - 'sales_order_line__order', 'location' ) @@ -517,7 +507,7 @@ class StockList(generics.ListCreateAPIView): 'customer', 'belongs_to', 'build', - 'sales_order_line' + 'sales_order', ] diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 17dea77305..7a30b694b5 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -11,6 +11,8 @@ from django.core.exceptions import ValidationError from django.urls import reverse from django.db import models, transaction +from django.db.models import Sum +from django.db.models.functions import Coalesce from django.core.validators import MinValueValidator from django.contrib.auth.models import User from django.db.models.signals import pre_delete @@ -29,7 +31,7 @@ from InvenTree.models import InvenTreeTree from InvenTree.fields import InvenTreeURLField from part.models import Part -from order.models import PurchaseOrder, SalesOrder +from order.models import PurchaseOrder, SalesOrder, SalesOrderAllocation class StockLocation(InvenTreeTree): @@ -391,7 +393,39 @@ class StockItem(MPTTModel): # TODO - For now this only checks if the StockItem is allocated to a SalesOrder # TODO - In future, once the "build" is working better, check this too - return self.sales_order_line is not None + if self.allocations.count() > 0: + return True + + if self.sales_order_allocations.count() > 0: + return True + + return False + + def build_allocation_count(self): + """ + Return the total quantity allocated to builds + """ + + query = self.allocations.aggregate(q=Coalesce(Sum('quantity'), Decimal(0))) + + return query['q'] + + def sales_order_allocation_count(self): + """ + Return the total quantity allocated to SalesOrders + """ + + query = self.sales_order_allocations.aggregate(q=Coalesce(Sum('quantity'), Decimal(0))) + + return query['q'] + + def allocation_count(self): + """ + Return the total quantity allocated to builds or orders + """ + + return self.build_allocation_count() + self.sales_order_allocation_count() + def can_delete(self): """ Can this stock item be deleted? It can NOT be deleted under the following circumstances: diff --git a/InvenTree/stock/serializers.py b/InvenTree/stock/serializers.py index e0176f5192..a2fbd5bb6a 100644 --- a/InvenTree/stock/serializers.py +++ b/InvenTree/stock/serializers.py @@ -67,6 +67,8 @@ class StockItemSerializer(InvenTreeModelSerializer): 'supplier_part', 'supplier_part__supplier', 'supplier_part__manufacturer', + 'allocations', + 'sales_order_allocations', 'location', 'part', 'tracking_info', diff --git a/InvenTree/stock/templates/stock/item_base.html b/InvenTree/stock/templates/stock/item_base.html index da002c2ac9..4e9bd81f0e 100644 --- a/InvenTree/stock/templates/stock/item_base.html +++ b/InvenTree/stock/templates/stock/item_base.html @@ -15,12 +15,19 @@ InvenTree | {% trans "Stock Item" %} - {{ item }} {% block pre_content %} {% include 'stock/loc_link.html' with location=item.location %} -{% if item.sales_order_line %} +{% for allocation in item.sales_order_allocations.all %}
{% trans "This stock item is allocated to Sales Order" %} - {{ item.sales_order_line.order }} + {{ allcation.line.order }}
-{% endif %} +{% endfor %} + +{% for allocation in item.allocations.all %} +
+ {% trans "This stock item is allocated to Build" %} + {{ allocation.build }} +
+{% endfor %} {% if item.serialized %}
From d9698b10ccf34a4e3b79b1a106ac560ebee10314 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 22 Apr 2020 13:21:45 +1000 Subject: [PATCH 045/104] PEP fixes --- InvenTree/order/admin.py | 1 + InvenTree/order/models.py | 1 + InvenTree/stock/__init__.py | 2 +- InvenTree/stock/api.py | 1 - InvenTree/stock/models.py | 3 +-- 5 files changed, 4 insertions(+), 4 deletions(-) diff --git a/InvenTree/order/admin.py b/InvenTree/order/admin.py index a213f09764..b7ac71976c 100644 --- a/InvenTree/order/admin.py +++ b/InvenTree/order/admin.py @@ -12,6 +12,7 @@ from .models import PurchaseOrder, PurchaseOrderLineItem from .models import SalesOrder, SalesOrderLineItem from .models import SalesOrderAllocation + class PurchaseOrderAdmin(ImportExportModelAdmin): list_display = ( diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index 8e9910cfc6..3452cc6bb7 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -19,6 +19,7 @@ import os from datetime import datetime from decimal import Decimal +from part.models import Part from stock import models as stock_models from company.models import Company, SupplierPart diff --git a/InvenTree/stock/__init__.py b/InvenTree/stock/__init__.py index 7b58c08fc9..6970329be1 100644 --- a/InvenTree/stock/__init__.py +++ b/InvenTree/stock/__init__.py @@ -6,4 +6,4 @@ It includes models for: - StockLocation - StockItem - StockItemTracking -""" \ No newline at end of file +""" diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index bcec2c2bf1..2c22665da0 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -12,7 +12,6 @@ from django.db.models import Q from .models import StockLocation, StockItem from .models import StockItemTracking -from order.models import SalesOrder from part.models import Part, PartCategory from .serializers import StockItemSerializer diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 7a30b694b5..4ea271c6ab 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -31,7 +31,7 @@ from InvenTree.models import InvenTreeTree from InvenTree.fields import InvenTreeURLField from part.models import Part -from order.models import PurchaseOrder, SalesOrder, SalesOrderAllocation +from order.models import PurchaseOrder, SalesOrder class StockLocation(InvenTreeTree): @@ -426,7 +426,6 @@ class StockItem(MPTTModel): return self.build_allocation_count() + self.sales_order_allocation_count() - def can_delete(self): """ Can this stock item be deleted? It can NOT be deleted under the following circumstances: From eb7b49784b2a8b404a5e45ca8b7d4ff16eb5fecc Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 22 Apr 2020 15:24:11 +1000 Subject: [PATCH 046/104] StockItem serializer now includes the allocated quantity --- InvenTree/stock/serializers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/InvenTree/stock/serializers.py b/InvenTree/stock/serializers.py index a2fbd5bb6a..44b03daf2a 100644 --- a/InvenTree/stock/serializers.py +++ b/InvenTree/stock/serializers.py @@ -92,7 +92,8 @@ class StockItemSerializer(InvenTreeModelSerializer): tracking_items = serializers.IntegerField(source='tracking_info_count', read_only=True) - allocated = serializers.BooleanField(source='is_allocated', read_only=True) + quantity = serializers.FloatField() + allocated = serializers.FloatField(source='allocation_count', read_only=True) def __init__(self, *args, **kwargs): From 5d1754ec32396d122ac66ff78aaff8a83f5acf88 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 22 Apr 2020 17:39:59 +1000 Subject: [PATCH 047/104] Better display of where a StockItem is allocated --- InvenTree/stock/templates/stock/item_base.html | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/InvenTree/stock/templates/stock/item_base.html b/InvenTree/stock/templates/stock/item_base.html index 4e9bd81f0e..c90ca6aaba 100644 --- a/InvenTree/stock/templates/stock/item_base.html +++ b/InvenTree/stock/templates/stock/item_base.html @@ -17,15 +17,13 @@ InvenTree | {% trans "Stock Item" %} - {{ item }} {% for allocation in item.sales_order_allocations.all %}
- {% trans "This stock item is allocated to Sales Order" %} - {{ allcation.line.order }} + {% trans "This stock item is allocated to Sales Order" %} #{{ allocation.line.order.id }} ({% trans "Quantity" %}: {% decimal allocation.quantity %})
{% endfor %} {% for allocation in item.allocations.all %}
- {% trans "This stock item is allocated to Build" %} - {{ allocation.build }} + {% trans "This stock item is allocated to Build" %} #{{ allocation.build.id }} ({% trans "Quantity" %}: {% decimal allocation.quantity %})
{% endfor %} From b70e79b7783f603eec730df2d77d59d77f757ac7 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 22 Apr 2020 20:10:23 +1000 Subject: [PATCH 048/104] Optionally add all SalesOrderAllocations to the SalesOrderLineItem serializer --- InvenTree/order/api.py | 10 +++++-- InvenTree/order/models.py | 10 +++++++ InvenTree/order/serializers.py | 30 +++++++++++++++++++ .../stock/templates/stock/item_base.html | 6 ++-- 4 files changed, 51 insertions(+), 5 deletions(-) diff --git a/InvenTree/order/api.py b/InvenTree/order/api.py index 127ae7399a..d5ba2ce83e 100644 --- a/InvenTree/order/api.py +++ b/InvenTree/order/api.py @@ -19,8 +19,8 @@ from company.models import SupplierPart from .models import PurchaseOrder, PurchaseOrderLineItem from .serializers import POSerializer, POLineItemSerializer -from .models import SalesOrder, SalesOrderLineItem -from .serializers import SalesOrderSerializer, SOLineItemSerializer +from .models import SalesOrder, SalesOrderLineItem, SalesOrderAllocation +from .serializers import SalesOrderSerializer, SOLineItemSerializer, SalesOrderAllocationSerializer class POList(generics.ListCreateAPIView): @@ -324,6 +324,11 @@ class SOLineItemList(generics.ListCreateAPIView): except AttributeError: pass + try: + kwargs['allocations'] = str2bool(self.request.query_params.get('allocations', False)) + except AttributeError: + pass + kwargs['context'] = self.get_serializer_context() return self.serializer_class(*args, **kwargs) @@ -345,6 +350,7 @@ class SOLineItemList(generics.ListCreateAPIView): filter_fields = [ 'order', + 'part', ] diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index 3452cc6bb7..db7dc8c29b 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -451,3 +451,13 @@ class SalesOrderAllocation(models.Model): ) quantity = RoundingDecimalField(max_digits=15, decimal_places=5, validators=[MinValueValidator(0)], default=1) + + def get_location(self): + return self.item.location.id if self.item.location else None + + def get_location_path(self): + if self.item.location: + return self.item.location.pathstring + else: + return "" + \ No newline at end of file diff --git a/InvenTree/order/serializers.py b/InvenTree/order/serializers.py index deb470baab..721aaa6338 100644 --- a/InvenTree/order/serializers.py +++ b/InvenTree/order/serializers.py @@ -12,9 +12,11 @@ from django.db.models import Count from InvenTree.serializers import InvenTreeModelSerializer from company.serializers import CompanyBriefSerializer from part.serializers import PartBriefSerializer +from stock.serializers import StockItemSerializer from .models import PurchaseOrder, PurchaseOrderLineItem from .models import SalesOrder, SalesOrderLineItem +from .models import SalesOrderAllocation class POSerializer(InvenTreeModelSerializer): @@ -143,6 +145,28 @@ class SalesOrderSerializer(InvenTreeModelSerializer): ] +class SalesOrderAllocationSerializer(InvenTreeModelSerializer): + """ + Serializer for the SalesOrderAllocation model. + This includes some fields from the related model objects. + """ + + location_path = serializers.CharField(source='get_location_path') + location_id = serializers.IntegerField(source='get_location') + + class Meta: + model = SalesOrderAllocation + + fields = [ + 'pk', + 'line', + 'location_id', + 'location_path', + 'quantity', + 'item', + ] + + class SOLineItemSerializer(InvenTreeModelSerializer): """ Serializer for a SalesOrderLineItem object """ @@ -150,6 +174,7 @@ class SOLineItemSerializer(InvenTreeModelSerializer): part_detail = kwargs.pop('part_detail', False) order_detail = kwargs.pop('order_detail', False) + allocations = kwargs.pop('allocations', False) super().__init__(*args, **kwargs) @@ -159,8 +184,12 @@ class SOLineItemSerializer(InvenTreeModelSerializer): if order_detail is not True: self.fields.pop('order_detail') + if allocations is not True: + self.fields.pop('allocations') + order_detail = SalesOrderSerializer(source='order', many=False, read_only=True) part_detail = PartBriefSerializer(source='part', many=False, read_only=True) + allocations = SalesOrderAllocationSerializer(many=True, read_only=True) quantity = serializers.FloatField() allocated = serializers.FloatField(source='allocated_quantity', read_only=True) @@ -171,6 +200,7 @@ class SOLineItemSerializer(InvenTreeModelSerializer): fields = [ 'pk', 'allocated', + 'allocations', 'quantity', 'reference', 'notes', diff --git a/InvenTree/stock/templates/stock/item_base.html b/InvenTree/stock/templates/stock/item_base.html index c90ca6aaba..e0a69e15fb 100644 --- a/InvenTree/stock/templates/stock/item_base.html +++ b/InvenTree/stock/templates/stock/item_base.html @@ -17,18 +17,18 @@ InvenTree | {% trans "Stock Item" %} - {{ item }} {% for allocation in item.sales_order_allocations.all %}
- {% trans "This stock item is allocated to Sales Order" %} #{{ allocation.line.order.id }} ({% trans "Quantity" %}: {% decimal allocation.quantity %}) + {% trans "This stock item is allocated to Sales Order" %} #{{ allocation.line.order.reference }} ({% trans "Quantity" %}: {% decimal allocation.quantity %})
{% endfor %} {% for allocation in item.allocations.all %}
- {% trans "This stock item is allocated to Build" %} #{{ allocation.build.id }} ({% trans "Quantity" %}: {% decimal allocation.quantity %}) + {% trans "This stock item is allocated to Build" %} #{{ allocation.build.id }} ({% trans "Quantity" %}: {% decimal allocation.quantity %})
{% endfor %} {% if item.serialized %} -
+
{% trans "This stock item is serialized - it has a unique serial number and the quantity cannot be adjusted." %}
{% elif item.child_count > 0 %} From 2972aec7598ec61159405d1a578740de5a06aa3c Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 22 Apr 2020 20:26:05 +1000 Subject: [PATCH 049/104] Cleverer rendering of sales order allocations --- InvenTree/InvenTree/static/css/inventree.css | 4 +- InvenTree/order/api.py | 1 + InvenTree/order/models.py | 7 ++ InvenTree/order/serializers.py | 3 +- .../templates/order/sales_order_detail.html | 79 +++++++------------ 5 files changed, 41 insertions(+), 53 deletions(-) diff --git a/InvenTree/InvenTree/static/css/inventree.css b/InvenTree/InvenTree/static/css/inventree.css index 29eac04c55..834070adae 100644 --- a/InvenTree/InvenTree/static/css/inventree.css +++ b/InvenTree/InvenTree/static/css/inventree.css @@ -159,8 +159,8 @@ } .sub-table { - margin-left: 25px; - margin-right: 25px; + margin-left: 45px; + margin-right: 45px; } .detail-icon .glyphicon { diff --git a/InvenTree/order/api.py b/InvenTree/order/api.py index d5ba2ce83e..e305dd4e26 100644 --- a/InvenTree/order/api.py +++ b/InvenTree/order/api.py @@ -341,6 +341,7 @@ class SOLineItemList(generics.ListCreateAPIView): 'part', 'part__stock_items', 'allocations', + 'allocations__item__location', 'order', ) diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index db7dc8c29b..bcc2beb1a5 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -452,6 +452,13 @@ class SalesOrderAllocation(models.Model): quantity = RoundingDecimalField(max_digits=15, decimal_places=5, validators=[MinValueValidator(0)], default=1) + def get_allocated(self): + """ String representation of the allocated quantity """ + if self.item.serial and self.quantity == 1: + return "# {sn}".format(sn=self.item.serial) + else: + return self.quantity + def get_location(self): return self.item.location.id if self.item.location else None diff --git a/InvenTree/order/serializers.py b/InvenTree/order/serializers.py index 721aaa6338..46274cf226 100644 --- a/InvenTree/order/serializers.py +++ b/InvenTree/order/serializers.py @@ -153,6 +153,7 @@ class SalesOrderAllocationSerializer(InvenTreeModelSerializer): location_path = serializers.CharField(source='get_location_path') location_id = serializers.IntegerField(source='get_location') + allocated = serializers.CharField(source='get_allocated') class Meta: model = SalesOrderAllocation @@ -160,9 +161,9 @@ class SalesOrderAllocationSerializer(InvenTreeModelSerializer): fields = [ 'pk', 'line', + 'allocated', 'location_id', 'location_path', - 'quantity', 'item', ] diff --git a/InvenTree/order/templates/order/sales_order_detail.html b/InvenTree/order/templates/order/sales_order_detail.html index ed7293f126..7870f77baf 100644 --- a/InvenTree/order/templates/order/sales_order_detail.html +++ b/InvenTree/order/templates/order/sales_order_detail.html @@ -42,6 +42,7 @@ $("#so-lines-table").inventreeTable({ queryParams: { order: {{ order.id }}, part_detail: true, + allocations: true, }, url: "{% url 'api-so-line-list' %}", detailView: true, @@ -49,61 +50,39 @@ $("#so-lines-table").inventreeTable({ return row.allocated > 0; }, detailFormatter: function(index, row, element) { - inventreeGet("{% url 'api-stock-list' %}", + + var html = `
`; + + element.html(html); + + $(`#allocation-table-${row.pk}`).bootstrapTable({ + data: row.allocations, + showHeader: false, + columns: [ { - location_detail: true, - sales_order_line: row.pk, + width: '50%', + field: 'allocated', + title: 'Quantity', + formatter: function(value, row, index, field) { + return renderLink(value, `/stock/item/${row.pk}/`); + }, }, { - success: function(response) { - - var html = `
`; - - element.html(html); - - $(`#allocation-table-${row.pk}`).bootstrapTable({ - data: response, - showHeader: false, - columns: [ - { - width: '50%', - field: 'quantity', - title: 'Quantity', - formatter: function(value, row, index, field) { - var html = ''; - if (row.serial && row.quantity == 1) { - html = `Serial Number: ${row.serial}`; - } else { - html = `Quantity: ${row.quantity}`; - } - - return renderLink(html, `/stock/item/${row.pk}/`); - }, - }, - { - field: 'location', - title: 'Location', - formatter: function(value, row, index, field) { - return renderLink(row.location_detail.pathstring, `/stock/location/${row.location}/`); - }, - }, - { - field: 'buttons', - title: 'Actions', - formatter: function(value, row, index, field) { - return ''; - }, - }, - ], - }); + field: 'location_id', + title: 'Location', + formatter: function(value, row, index, field) { + return renderLink(row.location_path, `/stock/location/${row.location_id}/`); }, - error: function(response) { - console.log("An error!"); + }, + { + field: 'buttons', + title: 'Actions', + formatter: function(value, row, index, field) { + return ''; }, - } - ); - - return "{% trans 'Loading data' %}"; + }, + ], + }); }, columns: [ { From 6ab03bd05a153fbd3b6e533bc7a49b07ab40e7bb Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 22 Apr 2020 21:26:38 +1000 Subject: [PATCH 050/104] Add form for creating a new StockItem allocation --- .../static/script/inventree/inventree.js | 8 +-- InvenTree/order/forms.py | 14 ++++ .../templates/order/sales_order_detail.html | 68 ++++++++++++++++--- InvenTree/order/urls.py | 9 +++ InvenTree/order/views.py | 65 ++++++++++++++++++ 5 files changed, 149 insertions(+), 15 deletions(-) diff --git a/InvenTree/InvenTree/static/script/inventree/inventree.js b/InvenTree/InvenTree/static/script/inventree/inventree.js index e2626e3c0a..6d29ca3704 100644 --- a/InvenTree/InvenTree/static/script/inventree/inventree.js +++ b/InvenTree/InvenTree/static/script/inventree/inventree.js @@ -78,16 +78,16 @@ function getImageUrlFromTransfer(transfer) { return url; } -function makeIconButton(icon, id, opts) { +function makeIconButton(icon, cls, pk, title) { // Construct an 'icon button' using the fontawesome set - var options = opts || {}; + var classes = `btn btn-default btn-glyph ${cls}`; - var title = options.title || ''; + var id = `${cls}-${pk}`; var html = ''; - html += ``; diff --git a/InvenTree/order/forms.py b/InvenTree/order/forms.py index 19580e7226..43a8d4a529 100644 --- a/InvenTree/order/forms.py +++ b/InvenTree/order/forms.py @@ -16,6 +16,7 @@ from InvenTree.fields import RoundingDecimalFormField from stock.models import StockLocation from .models import PurchaseOrder, PurchaseOrderLineItem, PurchaseOrderAttachment from .models import SalesOrder, SalesOrderLineItem, SalesOrderAttachment +from .models import SalesOrderAllocation class IssuePurchaseOrderForm(HelperForm): @@ -144,3 +145,16 @@ class EditSalesOrderLineItemForm(HelperForm): 'reference', 'notes' ] + + +class EditSalesOrderAllocationForm(HelperForm): + + quantity = RoundingDecimalFormField(max_digits=10, decimal_places=5) + + class Meta: + model = SalesOrderAllocation + + fields = [ + 'line', + 'item', + 'quantity'] diff --git a/InvenTree/order/templates/order/sales_order_detail.html b/InvenTree/order/templates/order/sales_order_detail.html index 7870f77baf..92acf5edc0 100644 --- a/InvenTree/order/templates/order/sales_order_detail.html +++ b/InvenTree/order/templates/order/sales_order_detail.html @@ -136,32 +136,78 @@ $("#so-lines-table").inventreeTable({ field: 'buttons', formatter: function(value, row, index, field) { - var html = ''; + var html = `
`; var pk = row.pk; if (row.part) { var part = row.part_detail; - html = `
`; - - html += makeIconButton('fa-plus', `button-add-${pk}`); - if (part.purchaseable) { - html += makeIconButton('fa-shopping-cart', `button-buy-${pk}`); - } - - if (part.assembly) { - html += makeIconButton('fa-tools', `button-build-${pk}`); + html += makeIconButton('fa-shopping-cart', 'button-buy', pk, '{% trans "Buy parts" %}'); } - html += `
`; + if (part.assembly) { + html += makeIconButton('fa-tools', 'button-build', pk, '{% trans "Build parts" %}'); + } + + html += makeIconButton('fa-plus', 'button-add', pk, '{% trans "Allocate parts" %}'); + } + + html += makeIconButton('fa-edit', 'button-edit', pk, '{% trans "Edit line item" %}'); + html += `
`; return html; } }, ], +}); + +function reloadTable() { + $("#so-lines-table").bootstrapTable("refresh"); +} + +// Called when the table is loaded +$("#so-lines-table").on('load-success.bs.table', function() { + + var table = $(this); + + // Set up callbacks for the row buttons + table.find(".button-edit").click(function() { + + var pk = $(this).attr('pk'); + + launchModalForm(`/order/sales-order/line/${pk}/edit/`, { + success: reloadTable, + }); + console.log("clicked!"); }); + table.find(".button-add").click(function() { + console.log("add"); + + var pk = $(this).attr('pk'); + + launchModalForm(`/order/sales-order/allocation/new/`, { + reload: table, + data: { + line: pk, + }, + }); + + }); + + table.find(".button-build").click(function() { + console.log("build"); + + var pk = $(this).attr('pk'); + }); + + table.find(".button-buy").click(function() { + console.log("buy"); + }); + +}); + {% endblock %} \ No newline at end of file diff --git a/InvenTree/order/urls.py b/InvenTree/order/urls.py index e56d0c9312..fdc1aef3b5 100644 --- a/InvenTree/order/urls.py +++ b/InvenTree/order/urls.py @@ -61,8 +61,12 @@ purchase_order_urls = [ url(r'^.*$', views.PurchaseOrderIndex.as_view(), name='po-index'), ] + so_line_urls = [ url(r'^new/', views.SOLineItemCreate.as_view(), name='so-line-item-create'), + url(r'^(?P\d+)/', include([ + url(r'^edit/', views.SOLineItemEdit.as_view(), name='so-line-item-edit') + ])), ] sales_order_attachment_urls = [ @@ -88,6 +92,11 @@ sales_order_urls = [ url(r'^line/', include(so_line_urls)), + # URLs for sales order allocations + url(r'^allocation/', include([ + url(r'^new/', views.SalesOrderAllocationCreate.as_view(), name='so-allocation-create'), + ])), + url(r'^attachments/', include(sales_order_attachment_urls)), # Display detail view for a single SalesOrder diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index b25e5686f7..82b83b92cb 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -17,6 +17,7 @@ from decimal import Decimal, InvalidOperation from .models import PurchaseOrder, PurchaseOrderLineItem, PurchaseOrderAttachment from .models import SalesOrder, SalesOrderLineItem, SalesOrderAttachment +from .models import SalesOrderAllocation from .admin import POLineItemResource from build.models import Build from company.models import Company, SupplierPart @@ -1114,6 +1115,22 @@ class SOLineItemCreate(AjaxCreateView): return initials +class SOLineItemEdit(AjaxUpdateView): + """ View for editing a SalesOrderLineItem """ + + model = SalesOrderLineItem + form_class = order_forms.EditSalesOrderLineItemForm + ajax_form_title = _('Edit Line Item') + + def get_form(self): + form = super().get_form() + + form.fields.pop('order') + form.fields.pop('part') + + return form + + class POLineItemEdit(AjaxUpdateView): """ View for editing a PurchaseOrderLineItem object in a modal form. """ @@ -1144,3 +1161,51 @@ class POLineItemDelete(AjaxDeleteView): return { 'danger': _('Deleted line item'), } + + +class SalesOrderAllocationCreate(AjaxCreateView): + """ View for creating a new SalesOrderAllocation """ + + model = SalesOrderAllocation + form_class = order_forms.EditSalesOrderAllocationForm + ajax_form_title = _('Allocate Stock to Order') + + def get_initial(self): + initials = super().get_initial().copy() + + line = self.request.GET.get('line', None) + + if line is not None: + initials['line'] = SalesOrderLineItem.objects.get(pk=line) + + return initials + + def get_form(self): + + form = super().get_form() + + line_id = form['line'].value() + + # If a line item has been specified, reduce the queryset for the stockitem accordingly + try: + line = SalesOrderLineItem.objects.get(pk=line_id) + + queryset = form.fields['item'].queryset + + # Ensure the part reference matches + queryset = queryset.filter(part=line.part) + + # Exclude StockItem which are already allocated to this order + allocated = [allocation.item.pk for allocation in line.allocations.all()] + + queryset = queryset.exclude(pk__in=allocated) + + form.fields['item'].queryset = queryset + + # Hide the 'line' field + form.fields['line'].widget = HiddenInput() + + except KeyError: # (ValueError, SalesOrderLineItem.DoesNotExist): + pass + + return form From fd42149f6749eec631aec626e270b340ae758534 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 22 Apr 2020 22:22:22 +1000 Subject: [PATCH 051/104] More stuff --- InvenTree/InvenTree/static/css/inventree.css | 4 ++ .../static/script/inventree/inventree.js | 8 +++- InvenTree/order/models.py | 19 +++++++- .../templates/order/sales_order_base.html | 8 ++++ .../templates/order/sales_order_detail.html | 2 +- InvenTree/order/urls.py | 3 ++ InvenTree/order/views.py | 43 +++++++++++++++++-- 7 files changed, 80 insertions(+), 7 deletions(-) diff --git a/InvenTree/InvenTree/static/css/inventree.css b/InvenTree/InvenTree/static/css/inventree.css index 834070adae..f95f81ec1e 100644 --- a/InvenTree/InvenTree/static/css/inventree.css +++ b/InvenTree/InvenTree/static/css/inventree.css @@ -42,6 +42,10 @@ opacity: 60%; } +.progress-bar-exceed { + background: #eeaa33; +} + .progress-value { width: 100%; color: #333; diff --git a/InvenTree/InvenTree/static/script/inventree/inventree.js b/InvenTree/InvenTree/static/script/inventree/inventree.js index 6d29ca3704..fdf9b2281d 100644 --- a/InvenTree/InvenTree/static/script/inventree/inventree.js +++ b/InvenTree/InvenTree/static/script/inventree/inventree.js @@ -113,11 +113,17 @@ function makeProgressBar(value, maximum, opts) { percent = 100; } + var extraclass = ''; + + if (value > maximum) { + extraclass='progress-bar-exceed'; + } + var id = opts.id || 'progress-bar'; return `
-
+
${value} / ${maximum}
`; diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index bcc2beb1a5..942a22a316 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -24,7 +24,7 @@ from stock import models as stock_models from company.models import Company, SupplierPart from InvenTree.fields import RoundingDecimalField -from InvenTree.helpers import decimal2string +from InvenTree.helpers import decimal2string, normalize from InvenTree.status_codes import OrderStatus from InvenTree.models import InvenTreeAttachment @@ -277,6 +277,15 @@ class SalesOrder(Order): customer_reference = models.CharField(max_length=64, blank=True, help_text=_("Customer order reference code")) + def is_fully_allocated(self): + """ Return True if all line items are fully allocated """ + + for line in self.lines.all(): + if not line.is_fully_allocated(): + return False + + return True + class PurchaseOrderAttachment(InvenTreeAttachment): """ @@ -385,6 +394,12 @@ class SalesOrderLineItem(OrderLineItem): return query['allocated'] + def is_fully_allocated(self): + return self.allocated_quantity() >= self.quantity + + def is_over_allocated(self): + return self.allocated_quantity() > self.quantity + class SalesOrderAllocation(models.Model): """ @@ -457,7 +472,7 @@ class SalesOrderAllocation(models.Model): if self.item.serial and self.quantity == 1: return "# {sn}".format(sn=self.item.serial) else: - return self.quantity + return normalize(self.quantity) def get_location(self): return self.item.location.id if self.item.location else None diff --git a/InvenTree/order/templates/order/sales_order_base.html b/InvenTree/order/templates/order/sales_order_base.html index 98faadb1f4..a3b38b1f90 100644 --- a/InvenTree/order/templates/order/sales_order_base.html +++ b/InvenTree/order/templates/order/sales_order_base.html @@ -9,6 +9,14 @@ InvenTree | {% trans "Sales Order" %} {% endblock %} +{% block pre_content %} +{% if not order.is_fully_allocated %} +
+ {% trans "This SalesOrder has not been fully allocated" %} +
+{% endif %} +{% endblock %} + {% block thumbnail %} \d+)/', include([ + url(r'^edit/', views.SalesOrderAllocationEdit.as_view(), name='so-allocation-edit'), + ])), ])), url(r'^attachments/', include(sales_order_attachment_urls)), diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index 82b83b92cb..98dad9511a 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -1173,11 +1173,33 @@ class SalesOrderAllocationCreate(AjaxCreateView): def get_initial(self): initials = super().get_initial().copy() - line = self.request.GET.get('line', None) + line_id = self.request.GET.get('line', None) - if line is not None: - initials['line'] = SalesOrderLineItem.objects.get(pk=line) + if line_id is not None: + line = SalesOrderLineItem.objects.get(pk=line_id) + + initials['line'] = line + + # Search for matching stock items, pre-fill if there is only one + items = StockItem.objects.filter(part=line.part) + + quantity = line.quantity - line.allocated_quantity() + + if quantity < 0: + quantity = 0 + if items.count() == 1: + item = items.first() + initials['item'] = item + + # Reduce the quantity IF there is not enough stock + qmax = item.quantity - item.allocation_count() + + if qmax < quantity: + quantity = qmax + + initials['quantity'] = quantity + return initials def get_form(self): @@ -1209,3 +1231,18 @@ class SalesOrderAllocationCreate(AjaxCreateView): pass return form + + +class SalesOrderAllocationEdit(AjaxUpdateView): + + model = SalesOrderAllocation + ajax_form_title = _('Edit Allocation Quantity') + + def get_form(self): + form = super().get_form() + + # Prevent the user from editing particular fields + form.fields.pop('item') + form.fields.pop('line') + + return form From 26d1a25f31da3caf7690ab82d66882104ae08810 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 22 Apr 2020 22:24:06 +1000 Subject: [PATCH 052/104] PEP style fixes --- InvenTree/order/api.py | 4 ++-- InvenTree/order/models.py | 1 - InvenTree/order/serializers.py | 1 - InvenTree/order/views.py | 2 +- 4 files changed, 3 insertions(+), 5 deletions(-) diff --git a/InvenTree/order/api.py b/InvenTree/order/api.py index e305dd4e26..08a69f7dec 100644 --- a/InvenTree/order/api.py +++ b/InvenTree/order/api.py @@ -19,8 +19,8 @@ from company.models import SupplierPart from .models import PurchaseOrder, PurchaseOrderLineItem from .serializers import POSerializer, POLineItemSerializer -from .models import SalesOrder, SalesOrderLineItem, SalesOrderAllocation -from .serializers import SalesOrderSerializer, SOLineItemSerializer, SalesOrderAllocationSerializer +from .models import SalesOrder, SalesOrderLineItem +from .serializers import SalesOrderSerializer, SOLineItemSerializer class POList(generics.ListCreateAPIView): diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index 942a22a316..53ee8be502 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -482,4 +482,3 @@ class SalesOrderAllocation(models.Model): return self.item.location.pathstring else: return "" - \ No newline at end of file diff --git a/InvenTree/order/serializers.py b/InvenTree/order/serializers.py index 46274cf226..6f85be6ef0 100644 --- a/InvenTree/order/serializers.py +++ b/InvenTree/order/serializers.py @@ -12,7 +12,6 @@ from django.db.models import Count from InvenTree.serializers import InvenTreeModelSerializer from company.serializers import CompanyBriefSerializer from part.serializers import PartBriefSerializer -from stock.serializers import StockItemSerializer from .models import PurchaseOrder, PurchaseOrderLineItem from .models import SalesOrder, SalesOrderLineItem diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index 98dad9511a..d09f543989 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -1227,7 +1227,7 @@ class SalesOrderAllocationCreate(AjaxCreateView): # Hide the 'line' field form.fields['line'].widget = HiddenInput() - except KeyError: # (ValueError, SalesOrderLineItem.DoesNotExist): + except (ValueError, SalesOrderLineItem.DoesNotExist): pass return form From 2a4e9037857dd783b80f9700d53be0bd3ee17865 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 22 Apr 2020 22:36:55 +1000 Subject: [PATCH 053/104] Add button to delete a SalesOrderLineItem --- InvenTree/InvenTree/fields.py | 4 +++- InvenTree/order/models.py | 3 ++- .../templates/order/sales_order_detail.html | 17 ++++++++++------- .../templates/order/so_lineitem_delete.html | 5 +++++ InvenTree/order/urls.py | 3 ++- InvenTree/order/views.py | 12 ++++++++++++ 6 files changed, 34 insertions(+), 10 deletions(-) create mode 100644 InvenTree/order/templates/order/so_lineitem_delete.html diff --git a/InvenTree/InvenTree/fields.py b/InvenTree/InvenTree/fields.py index 4d8ab9ef82..ba3648da30 100644 --- a/InvenTree/InvenTree/fields.py +++ b/InvenTree/InvenTree/fields.py @@ -69,5 +69,7 @@ class RoundingDecimalField(models.DecimalField): defaults = { 'form_class': RoundingDecimalFormField } + defaults.update(kwargs) - return super(RoundingDecimalField, self).formfield(**kwargs) + + return super().formfield(**kwargs) diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index 53ee8be502..e6b35e1921 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -463,9 +463,10 @@ class SalesOrderAllocation(models.Model): on_delete=models.CASCADE, related_name='sales_order_allocations', limit_choices_to={'part__salable': True}, + help_text=_('Select stock item to allocate') ) - quantity = RoundingDecimalField(max_digits=15, decimal_places=5, validators=[MinValueValidator(0)], default=1) + quantity = RoundingDecimalField(max_digits=15, decimal_places=5, validators=[MinValueValidator(0)], default=1, help_text=_('Enter stock allocation quantity')) def get_allocated(self): """ String representation of the allocated quantity """ diff --git a/InvenTree/order/templates/order/sales_order_detail.html b/InvenTree/order/templates/order/sales_order_detail.html index 8f0ae24441..801669c1d7 100644 --- a/InvenTree/order/templates/order/sales_order_detail.html +++ b/InvenTree/order/templates/order/sales_order_detail.html @@ -156,6 +156,8 @@ $("#so-lines-table").inventreeTable({ } html += makeIconButton('fa-edit', 'button-edit', pk, '{% trans "Edit line item" %}'); + html += makeIconButton('fa-trash-alt', 'button-delete', pk, '{% trans "Delete line item " %}'); + html += `
`; return html; @@ -181,12 +183,17 @@ $("#so-lines-table").on('load-success.bs.table', function() { launchModalForm(`/order/sales-order/line/${pk}/edit/`, { success: reloadTable, }); - console.log("clicked!"); + }); + + table.find(".button-delete").click(function() { + var pk = $(this).attr('pk'); + + launchModalForm(`/order/sales-order/line/${pk}/delete/`, { + reload: true, + }); }); table.find(".button-add").click(function() { - console.log("add"); - var pk = $(this).attr('pk'); launchModalForm(`/order/sales-order/allocation/new/`, { @@ -199,13 +206,9 @@ $("#so-lines-table").on('load-success.bs.table', function() { }); table.find(".button-build").click(function() { - console.log("build"); - - var pk = $(this).attr('pk'); }); table.find(".button-buy").click(function() { - console.log("buy"); }); }); diff --git a/InvenTree/order/templates/order/so_lineitem_delete.html b/InvenTree/order/templates/order/so_lineitem_delete.html new file mode 100644 index 0000000000..3264fea625 --- /dev/null +++ b/InvenTree/order/templates/order/so_lineitem_delete.html @@ -0,0 +1,5 @@ +{% extends "modal_delete_form.html" %} + +{% block pre_form_content %} +Are you sure you wish to delete this line item? +{% endblock %} \ No newline at end of file diff --git a/InvenTree/order/urls.py b/InvenTree/order/urls.py index 5f816ddfb5..b61dd445aa 100644 --- a/InvenTree/order/urls.py +++ b/InvenTree/order/urls.py @@ -65,7 +65,8 @@ purchase_order_urls = [ so_line_urls = [ url(r'^new/', views.SOLineItemCreate.as_view(), name='so-line-item-create'), url(r'^(?P\d+)/', include([ - url(r'^edit/', views.SOLineItemEdit.as_view(), name='so-line-item-edit') + url(r'^edit/', views.SOLineItemEdit.as_view(), name='so-line-item-edit'), + url(r'^delete/', views.SOLineItemDelete.as_view(), name='so-line-item-delete'), ])), ] diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index d09f543989..549bf1f496 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -1163,6 +1163,18 @@ class POLineItemDelete(AjaxDeleteView): } +class SOLineItemDelete(AjaxDeleteView): + + model = SalesOrderLineItem + ajax_form_title = _("Delete Line Item") + ajax_template_name = "order/so_lineitem_delete.html" + + def get_data(self): + return { + 'danger': _('Deleted line item'), + } + + class SalesOrderAllocationCreate(AjaxCreateView): """ View for creating a new SalesOrderAllocation """ From 6112be2df0478be4da3b900756922431859a4206 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 22 Apr 2020 23:21:54 +1000 Subject: [PATCH 054/104] Add forms for editing and deleting a SalesOrderAllocation item --- InvenTree/InvenTree/static/css/inventree.css | 6 +++ .../order/templates/order/order_base.html | 24 ++++++------ .../templates/order/sales_order_base.html | 4 +- .../templates/order/sales_order_detail.html | 37 +++++++++++++++++-- InvenTree/order/urls.py | 1 + InvenTree/order/views.py | 7 ++++ 6 files changed, 61 insertions(+), 18 deletions(-) diff --git a/InvenTree/InvenTree/static/css/inventree.css b/InvenTree/InvenTree/static/css/inventree.css index f95f81ec1e..784828969e 100644 --- a/InvenTree/InvenTree/static/css/inventree.css +++ b/InvenTree/InvenTree/static/css/inventree.css @@ -325,6 +325,12 @@ padding-bottom: 2px; } +.btn-large { + font-size: 150%; + align-content: center; + vertical-align: middle; +} + .badge { float: right; background-color: #777; diff --git a/InvenTree/order/templates/order/order_base.html b/InvenTree/order/templates/order/order_base.html index 64026f2c5f..3840457a5e 100644 --- a/InvenTree/order/templates/order/order_base.html +++ b/InvenTree/order/templates/order/order_base.html @@ -27,27 +27,27 @@ src="{% static 'img/blank_image.png' %}"

- - {% if order.status == OrderStatus.PENDING and order.lines.count > 0 %} - {% elif order.status == OrderStatus.PLACED %} - - {% endif %} {% if order.status == OrderStatus.PENDING or order.status == OrderStatus.PLACED %} - {% endif %}
diff --git a/InvenTree/order/templates/order/sales_order_base.html b/InvenTree/order/templates/order/sales_order_base.html index a3b38b1f90..7ada7751d0 100644 --- a/InvenTree/order/templates/order/sales_order_base.html +++ b/InvenTree/order/templates/order/sales_order_base.html @@ -34,8 +34,8 @@ src="{% static 'img/blank_image.png' %}"

{{ order.description }}

-
diff --git a/InvenTree/order/templates/order/sales_order_detail.html b/InvenTree/order/templates/order/sales_order_detail.html index 801669c1d7..272a99af4e 100644 --- a/InvenTree/order/templates/order/sales_order_detail.html +++ b/InvenTree/order/templates/order/sales_order_detail.html @@ -55,7 +55,11 @@ $("#so-lines-table").inventreeTable({ element.html(html); - $(`#allocation-table-${row.pk}`).bootstrapTable({ + var lineItem = row; + + var table = $(`#allocation-table-${row.pk}`); + + table.bootstrapTable({ data: row.allocations, showHeader: false, columns: [ @@ -64,7 +68,7 @@ $("#so-lines-table").inventreeTable({ field: 'allocated', title: 'Quantity', formatter: function(value, row, index, field) { - return renderLink(value, `/stock/item/${row.pk}/`); + return renderLink(value, `/stock/item/${row.item}/`); }, }, { @@ -78,11 +82,37 @@ $("#so-lines-table").inventreeTable({ field: 'buttons', title: 'Actions', formatter: function(value, row, index, field) { - return ''; + + var html = "
"; + var pk = row.pk; + + html += makeIconButton('fa-edit', 'button-allocation-edit', pk, '{% trans "Edit stock allocation" %}'); + html += makeIconButton('fa-trash-alt', 'button-allocation-delete', pk, '{% trans "Delete stock allocation" %}'); + + html += "
"; + + return html; }, }, ], }); + + table.find(".button-allocation-edit").click(function() { + + var pk = $(this).attr('pk'); + + launchModalForm(`/order/sales-order/allocation/${pk}/edit/`, { + success: reloadTable, + }); + }); + + table.find(".button-allocation-delete").click(function() { + var pk = $(this).attr('pk'); + + launchModalForm(`/order/sales-order/allocation/${pk}/delete/`, { + success: reloadTable, + }); + }); }, columns: [ { @@ -202,7 +232,6 @@ $("#so-lines-table").on('load-success.bs.table', function() { line: pk, }, }); - }); table.find(".button-build").click(function() { diff --git a/InvenTree/order/urls.py b/InvenTree/order/urls.py index b61dd445aa..f390c23f54 100644 --- a/InvenTree/order/urls.py +++ b/InvenTree/order/urls.py @@ -98,6 +98,7 @@ sales_order_urls = [ url(r'^new/', views.SalesOrderAllocationCreate.as_view(), name='so-allocation-create'), url(r'(?P\d+)/', include([ url(r'^edit/', views.SalesOrderAllocationEdit.as_view(), name='so-allocation-edit'), + url(r'^delete/', views.SalesOrderAllocationDelete.as_view(), name='so-allocation-delete'), ])), ])), diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index 549bf1f496..1b91f6e9b4 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -1248,6 +1248,7 @@ class SalesOrderAllocationCreate(AjaxCreateView): class SalesOrderAllocationEdit(AjaxUpdateView): model = SalesOrderAllocation + form_class = order_forms.EditSalesOrderAllocationForm ajax_form_title = _('Edit Allocation Quantity') def get_form(self): @@ -1258,3 +1259,9 @@ class SalesOrderAllocationEdit(AjaxUpdateView): form.fields.pop('line') return form + + +class SalesOrderAllocationDelete(AjaxDeleteView): + + model = SalesOrderAllocation + ajax_form_title = _("Remove allocation") From a803f21e0cea3a39c9b07a7028b35939e4314aee Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 22 Apr 2020 23:34:27 +1000 Subject: [PATCH 055/104] Add buttons to create new builds or orders for sales order parts - Need to pre-fill the forms a bit better --- .../templates/order/sales_order_detail.html | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/InvenTree/order/templates/order/sales_order_detail.html b/InvenTree/order/templates/order/sales_order_detail.html index 272a99af4e..24c1c93929 100644 --- a/InvenTree/order/templates/order/sales_order_detail.html +++ b/InvenTree/order/templates/order/sales_order_detail.html @@ -45,6 +45,7 @@ $("#so-lines-table").inventreeTable({ allocations: true, }, url: "{% url 'api-so-line-list' %}", + detailViewByClick: true, detailView: true, detailFilter: function(index, row) { return row.allocated > 0; @@ -174,11 +175,11 @@ $("#so-lines-table").inventreeTable({ var part = row.part_detail; if (part.purchaseable) { - html += makeIconButton('fa-shopping-cart', 'button-buy', pk, '{% trans "Buy parts" %}'); + html += makeIconButton('fa-shopping-cart', 'button-buy', row.part, '{% trans "Buy parts" %}'); } if (part.assembly) { - html += makeIconButton('fa-tools', 'button-build', pk, '{% trans "Build parts" %}'); + html += makeIconButton('fa-tools', 'button-build', row.part, '{% trans "Build parts" %}'); } html += makeIconButton('fa-plus', 'button-add', pk, '{% trans "Allocate parts" %}'); @@ -235,9 +236,24 @@ $("#so-lines-table").on('load-success.bs.table', function() { }); table.find(".button-build").click(function() { + var pk = $(this).attr('pk'); + + launchModalForm(`/build/new/`, { + follow: true, + data: { + part: pk, + }, + }); }); table.find(".button-buy").click(function() { + var pk = $(this).attr('pk'); + + launchModalForm("{% url 'order-parts' %}", { + data: { + parts: [pk], + }, + }); }); }); From 1a0f091e0c6849192b06402d5e5bdcde08be4418 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 23 Apr 2020 09:20:18 +1000 Subject: [PATCH 056/104] Improve progress bar rendering --- InvenTree/InvenTree/static/css/inventree.css | 7 ++++++- InvenTree/InvenTree/static/script/inventree/inventree.js | 4 +++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/InvenTree/InvenTree/static/css/inventree.css b/InvenTree/InvenTree/static/css/inventree.css index 784828969e..591a4a3760 100644 --- a/InvenTree/InvenTree/static/css/inventree.css +++ b/InvenTree/InvenTree/static/css/inventree.css @@ -40,12 +40,17 @@ .progress-bar { opacity: 60%; + background: #2aa02a; } -.progress-bar-exceed { +.progress-bar-under { background: #eeaa33; } +.progress-bar-over { + background: #337ab7; +} + .progress-value { width: 100%; color: #333; diff --git a/InvenTree/InvenTree/static/script/inventree/inventree.js b/InvenTree/InvenTree/static/script/inventree/inventree.js index fdf9b2281d..650a115e9c 100644 --- a/InvenTree/InvenTree/static/script/inventree/inventree.js +++ b/InvenTree/InvenTree/static/script/inventree/inventree.js @@ -116,7 +116,9 @@ function makeProgressBar(value, maximum, opts) { var extraclass = ''; if (value > maximum) { - extraclass='progress-bar-exceed'; + extraclass='progress-bar-over'; + } else if (value < maximum) { + extraclass = 'progress-bar-under'; } var id = opts.id || 'progress-bar'; From d59c6711bbc15b049dd7c69141f20e8ac5d0cb2d Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 23 Apr 2020 09:20:25 +1000 Subject: [PATCH 057/104] Update translations --- InvenTree/locale/de/LC_MESSAGES/django.mo | Bin 26331 -> 26245 bytes InvenTree/locale/de/LC_MESSAGES/django.po | 1134 ++++++++++++----- InvenTree/locale/en/LC_MESSAGES/django.po | 959 +++++++++----- InvenTree/locale/es/LC_MESSAGES/django.po | 959 +++++++++----- .../templates/order/sales_order_base.html | 3 + 5 files changed, 2083 insertions(+), 972 deletions(-) diff --git a/InvenTree/locale/de/LC_MESSAGES/django.mo b/InvenTree/locale/de/LC_MESSAGES/django.mo index 4038658f79cb21087ae837bd8467e380985ea54e..6cd6ed477ebc14df3564db68199751b661859d4c 100644 GIT binary patch delta 7251 zcmYk=3w+P@9>?+T#?0R~yJ7d6*~ae7L^Did?#!JfOreNFNT>J}iHC^#bEN2~JaDR$ zYE~g>PUY5eStO)JxkM_Jr8u3}d%tgw$N4>c_WXXozuWiw`F_8@;Z(KHqG})KOjyI! zhT|R|V`8y5z?iQnPi>)AV=l%SlZThEAI8NS(-iN+AbbdeaWO{X8VtoZY<&*~Qa^Cy7=pV|9XO17@EKG`E@A`x4b{<` z7={6fZpY%0SDIY(@P0Fpf*%c|kUwTDKXk#P*aZJ+>#v}$+mCwjAykLHM0M~IM&Wf^ z4@s&Yp^3vrwCAHb+8JA8Pt4=}W;%srd>J!vFY19mqi!%Q-ApvWW>mvb8EB2_NGJ5i z5>$rmpeO1DWww15 zYL1^o4d6LchhIiz=vC``sLWPdZ=gDyl0yE~lY$g?k@QEM7>DZE9Mt(|Fbg-LMtl^N z>hDmQxNZ$ibu*ZW>PQFFTz5f|W+q`aE=4_OyF)=QJc3Hi1=N((p)wJW=C-Gx7Gpl@ zh278}%TRMW0hOs4n2AeJ9jru>YN}8f_!jA_38hz^(7BC*MmPf-;zHDoOHhks6*j^b zty{1G^{uEmuE54vgJjj5w)(Vjrz{oKu>#bt8f=|`Y)i+iv=_XLnzJ3Kksm~*=v(C5 zV{Ty=j7ev~;b>I*0$bmJS|j^V9sUxPsc%ppwm(ssiX{zxn2t@g|8ptmg@ zM!l#K191mxweLlx@N3j{b*RPZ$5XYal2Dl|Mh#>b>b~jdi%U>1d=m8~+<+QD1v+Zj zOF^kViF(mBR7X54Ri&ya>VeIyS*VfsM7>}UYGg~W2|kShxX!u-wZ^vD`p2kD9?d5I zxfIUQpg9W9aYvMkO{ot-b+8Kt8<2Be0PoOdrz)ONL76UO8 zbzi4k@~;OK)4-t36x5*HfsJy@{IR_fZ|$izLNdMO~M` zu(U?HqZVNq-iEVLQ~PgJMh~DmehGP=V>}()41{4QCt^`IWTRf#7uCU`wm!i+71goX zs5xJNdhudp(B?(dR2)O4{v0+(54{?T2^gjQ?@$P%VJ#|EuVFKM7qu3uZT&JTGp3{4 z(Gb*&TB2Sw9C?Si8?`;3$NDvdn%d)7e}5i*HTJ_Q}k#e51H`3u+rUqSV>3YFp` z7>#F89lL>=`!)scsvnK|CG!Y|;|5emwxRCdgPO`iw*3TZVCM_SzZTQ4_C#!FcMj7r zhW29A?ihz`2y-7s;Cd{_O6-l1g>DAQQP)pKy>KQf6Az<0xDu6-ZT9@`Lh`TevyX;Y z{1nxp%cyM>#=Pi;Xw(B!P$SGno$ro%a39qEpN0)^B~J8V3Q-;Zqbpk%v%9%dF(1{j zryUBK+fAr_zT4Kn!`9Rr6uGN56V;LKsE+i-M)j9)}f%)i%OHnthM6K$VP$PT8)_0<|;eP93d;W9O zbIxKsUd0p)>gf(F4-1b0`^zU4wH8WH_uqq> z>&H=3wj4F`EvO9dww^*q`>>9JA2#ac_BaqV;z-nLPe<+hE~t)8Lv>`G^-WaDKStem z8q@G9GRLL`do&wUQM+syYCto4lYgy7hX&pF40>=Q`s3@^65qyT`~uaHTc}-=)yExS zPt*vznl6ZRBDG| zE1ZJL#7fkNx1mOM5Y^#xsHyqIwtEJ+8A(FcpD99RW-)5bIB!zW7wQn|fxZLX)g6Vp zuoY@92Vf(dW6#e+JzxoHB%4sF-jBg}3e}Ozs8pLl?uW;NT2rxDp#7gqL92ACbusFJ zFQGd05w^wCs0TGIbu$uyA=I-l2)m%p_roSQ40ZoF)V6!j*4JYq^=%la{eO&tdU_VM zIOq`N8q{Jvg}Uw+9Ezbs$hP+X zL<;Kpy{M7QL_PRXR4Uh?9Qh_i?bF}(QmlB=rWN(o0(XI zA7MWFk0AeAWQ8N#f3;3Rjd&Hdz>OGzJ5eL6K|b&1ChFT?G}0Z>!>EodMcuyx^(8!j zdeJ4!z=Tn52l}FR+4523UvssU1}(Zu)Z#gT{4wYFp;Q)*cDHFMDpO^sDVvPi?=w)T zooCN4L#>&$s5MZD+C96l9e!x7b10~%>1D3HF_QXZEX2o9`+hfSyM1Hzz0>VbBB~?l z7>#+T)jq(UpM{#b`KS)AKwbX=Y9P*L3R(l(P$_;NZ^H|yR3?mZJCK9w=un)957_!i zjG%r4b$#ev?$jk=9`%kGg;TLPK7m>z>#(c#{|6M*6aA%}uc`^cI2?tVqX$uoYc1*l z2T^OI78CF{?1+)$+!vK1eK%83tA8KH;7L>luA|!h#%oF(dQU+eh(}$Jfx6*#)Z7k7 zy|^5;7Uo))VJ!6xsQY)>`T^AS7f|=#LS@!d&NH!{^?pp}{bmaVrKAS6ZR${S7C*t= zZaJt$S%^w;FVy*wsKs=zZC{6p)L%pGs%q4Ib*QQEz1tmF2b@KHFly_Xy%e+y;_h)X zkc?U+?NFI0KrPA<7>0jCt(k?^<(NW!4eG_aP%plK%FH#?^?#z)iiZWH=O;~MycrbI zX^_Lw7Z;!p#^?vfm)^B45}asTOSdLimvRLg+&n-uw`~J(5cU1Ub>chXEkY}+C+7xZ zThs&%w)bd=y==LRvOneS7>~{LZ@W*asGC0$h4w;z&zh$Q9gB#8v?mdo=>bF|@BIwoO~5ntQ3wb<0&*UkC| zZF{t;wouTo@iCleNIXk9pEyihC3Mtz^CQ#ZK5;AcpQ`}-m`7<#B|^LpMJD*=(0b9k zJ~G1Rg10g~){<=btYs+mK)K?Kp3PiW^pPH1|yG&I|5hytQ7ZR3dFC_hZ3 zQPwd6@3dvrs|i-DnN5V~6ViZ!j$OoFqPHp>(}b>^_7@OY*zzE7Ewa`4$MQ3 zcV28nn^;PPv?LP4DeLHiS8z&wjsL5_Bi>!H30XCiZrawz@Mq!%v5&Y${FmrVI7BC} zCoa#arKF=5(TZrpvlrrjh$6~5N{Qw5CHMDQXWEAluh14}uld}buf*20l@m?uIbWP- z&x>KU{Tn`f_00|KNgqy}q3lmwCT0;Gh`)~G6vBvL+b|j9iO33{_^ObKPqW|lR4nh9 R7gKS3$n1cM;bRy1{||YT5@!Ga delta 7318 zcmYk=33yId9>?($vPWc-MHYF}*g`}WK}%?C2@zsR>>?rBs-Y37_Q=-$MR8_5Qx=>?ltCp5jRV`CfiuwL>&pglE$3LHQ&fU)WpL1@opvLE9jgRwP zr0+7r@qmvpi8wgKm{XJ&$Eekqz$V7zU=a4jJZy-IFceo{7_P-Qd>paZCaQoyfs_%)>l%P#5k%Jzy`=H**-n@f4~9zoPEHgX&12$C&yUhw5k| zMq+DJ$GRYWF(nwp`^^*zel*NN{uqZJy5MyT#+|l)5OrNG>cy8)9lDL`U{H!NjWGsQ z&qPLO^3WekQ5_wLEpa^N@P4y|LMraY)_5NEfT(-i8$75?w8BQ1gUUc5sw2ZN03Seg zXo_{VZGRed-4axWUO_#7BRWwOwo%XvK1cQNsP#1J!52|;dkx95X+Uq|F&#C+0*uCT z)O9mZ*F9(3-$8Z!6I2I|qo$%ZRr62bmc1~xshh$!s23NY)?t6t1ID9XFx$4jfSTj= zr~zz4-M1T+p--*fpfY>O+PIn9;hxRNzj`u)1}&0F_QYIN$6iF8-;8bW6V!;WqEa2) z+|5LcH4Bx&zNn6rq2_utl05S?X5ibX=Nxw^D0Np*sR?MoykjhCF||gu_e3qmQdH{4 zU;xfW&Fy?trj}wlu0wV32$EcL8kGS*dPCn$7T$}_5DFUMQuM_&s2kUz7ReU$$6eO_ zSfBbAs5w52fp`tcuKB~7lIBiXFI2}ypmx<%>ry1+j(OK!up2dJCr~55h)R)PD`S!{ z3AC(jmnfS!`6o_8I`Hd=!bo&{hI)W}9+Lv&CbeGbELwRID+am+5%i!Y#Fd<`{#yO@o>JX{&-fMHmO%J9%k z@~<1KXwZWmL6TsWqZZvB)T%v;`B*Q@m>$>z`D32p2kX>)fFAq-`Fb0F-WrN6F&x{Y z7GnWw`&D8tE_5g`c=I6!;U&}qZ=gn=lH+c(Ok_cu5>!WLV^e&?p5KR>s`Ivf2?MBy zwlk(XMx!z_47EF|P#tq7+J?tbFMJZU2E3?Ly^WfRZK#xei<;}7P#w93B*(-v99>t0 zS}T)Li}7jffU8m4@Hi@?zaifb$3(YxADD~EKo^X_Levez?fIFg4$igpCDxZv9b1i> z^LJ1$u11nzYEV<*Loby2FpS1rEZ6=oqM#erVAYCKh*O_q89s~Py?NVjuycZ+weLvC#D*efx{Ssr%-F* zH&m+qJGmW=LUk+ywFX9`R{s*zFP%*og-1{wsYTs?1C=S?&g5SwLOZ)7NI)&BR;cy? z*ci((0cWAs$TDPunAb2C4`C(#guSsi&&|Mc)b+2RUbq&OiFZ*Q+>=NCm6BR}!F5#X zZet?WXCBlc4{AFVpsw$adf;%>2*=vGi5_hAZA9nZXtw42TCe|%FOrrj*ZGQ{(f-P8tzTMnRl%UqiaMXRy zLlo43si?VLfO_yMRO;8F*1|5-+Ni;ncnRY%rn~zk%tGBa7B%9@s1MXsOu^--^IK6< z`3ct7{{P;dIEPwfmu>wn>cZe2?v2e+9mqmo9DrI(WysDnW08$vR$^;Bg6+_!r<<|% zsI}17T81&&|6?d1sPuGE{=5cquvSC<9yVJi&3k+9JTMKp*pq()se5PKcW`t zE!2Gx?8z3Gikjjg%)k-7$-lPQA{vzHwWulBg1T`Z2H|lGz_WM{Uc^*v*vIWiHfq;Y zqDD9yHNqEBncawKSc7`*4b*odsEGWl11Uvrin^e_hy|z@3`H%XIktThdZ>Sld~3{E zd*0L6{Q{PvGVv5DgDWuvSEEM05gXtR)PN2;6qNcysFD4GK^ReN48JE#0&4pVMy38y zRB9JtQ+yrO(LJaU*P=#d`ndy%Lv<(()!qe_kuqd`8s||8O3ilEqWKZC(YL?*zz!Hp zy&vkjVW_#BkN&vcp5KhRe+OzHCsC=sgJBq6;&#M?fz)%6sdh{k3L4n}?1WXQIb3bs zj(Xrx)Cg~47DfzkAJiR{k-iv?l^BZC?D=`90W3n@zYLqUUu9E_>fln;3tvWcbiJ)_#kSNBViw*+EzUY3m)PVM)I&uVc zzuz$Tix`c$)Y}as|E(!Zpg|pY5mWFOYR+z=7M~gJE}~SV$z-5Xxe&EoSD`ZXCMq-4 zsC~a3)sg-7{87}}IfvT+#u?#mqd?529%k)^`gYH-zJPJmt1%BhL+$_Ik?yu@Yb{1~ z=wVbxW?(%28MW$H*z-G4Q|BC@pdOw;-B61f$yL;Y?qW*}8s+|)%|@ki0;&U#p*s2+ zj>ivez1e7Yih7`~ABmc}iI{`)v9b35`xK&S_zJa1&SO^$DR)y|gz8Wk^0hT%F$w>U znyMPqBD;)wPRbbfMIA7idOz%llaUviwaA#vE{x#)rb&f6Cz+^B^hNE85vVWRBvc2c z+w=1(;&ba6Or-uB>i(!ox9&k*pNAp5-wdLl6qn&IuiEqPpw`q*+kO>2)a#9NcULMpy0Mso=42>pWQ%b+ zuESDHs&aS3G*pIWq1MVm)b&eIi*qAJ;$GC+Ic`0R&8T0(G>l@Q=)HO4O|QDt6w;s@ zN>Tf|9QA^ksO>q|x&iA^KZZV-s2?2Pd1u8WI|;UyZuM9faRna$GmVI`Z6#Pj{UC9J zxJYa!w6Y30HxP4B6Fk`7qapUOWwxIQq}&aYv623*_XQPo_X^R~URX@|IYP$*VgT*; z5Sr+IM1Z$AHq|*xNhA76a_n(2skC*VJP=0^I=T`gZTZW(7WYRV_`udvIIsWF??;@r zZ7VR0x{hwvKPZ2!Rke_wl$`p9|h)amh}my{ZRSWK-^J>r~C7#Jfa8+By*WL_A6OP}Zi=Y%eD| z5yiAs62DWPO{7uQF$_oBvg)4`tXwmLV4pa3{~n=ib`blC-l}j+CWdokTVfgIYBh44 zAa>T3_;vDs4{k@foc2{jf7?C^Cs99XuP>vXN_jESobtbocXQ*Iidc$=?ImIi^<9`x z93k|fs3f%dZxO!|I{aMBH`tEw651)B5pNKYJm*tv;Z2Kg5t>I$hrWj$y`$n=w7K85 zOrd26v5(LZOhj>R3$9fIM}~J(d{j^stp(J+_1474hPI*hJ+-Msk@re`YUm(Z;)sQ` z{NhbXNa%TvQYbA?Az%NxBcHmCv3M2ZQU3rAC(ctJq>~)YsQ-vPyz>$gDw@;Vvci$l8!>6Dbbo|&&Sb8L=ty*-y_9&Haua*a zF`eXik7z~PI3mQJtB;S@oppZ\n" "Language-Team: C \n" @@ -17,30 +17,56 @@ msgstr "" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Generator: Lokalize 19.12.0\n" -#: InvenTree/helpers.py:259 order/models.py:164 order/models.py:215 +#: InvenTree/api.py:61 +#, fuzzy +#| msgid "No lines specified" +msgid "No action specified" +msgstr "Keine Zeilen angegeben" + +#: InvenTree/api.py:75 +msgid "No matching action found" +msgstr "" + +#: InvenTree/api.py:106 +msgid "No barcode data provided" +msgstr "" + +#: InvenTree/api.py:121 +msgid "Barcode successfully decoded" +msgstr "" + +#: InvenTree/api.py:124 +msgid "Barcode plugin returned incorrect response" +msgstr "" + +#: InvenTree/api.py:134 +msgid "Unknown barcode format" +msgstr "" + +#: InvenTree/helpers.py:258 order/models.py:173 order/models.py:224 msgid "Invalid quantity provided" msgstr "Keine gültige Menge" -#: InvenTree/helpers.py:262 +#: InvenTree/helpers.py:261 msgid "Empty serial number string" msgstr "Keine Seriennummer angegeben" -#: InvenTree/helpers.py:283 InvenTree/helpers.py:300 +#: InvenTree/helpers.py:282 InvenTree/helpers.py:299 #, python-brace-format msgid "Duplicate serial: {n}" msgstr "Doppelte Seriennummer: {n}" -#: InvenTree/helpers.py:287 InvenTree/helpers.py:290 InvenTree/helpers.py:293 -#: InvenTree/helpers.py:304 +#: InvenTree/helpers.py:286 InvenTree/helpers.py:289 InvenTree/helpers.py:292 +#: InvenTree/helpers.py:303 #, python-brace-format msgid "Invalid group: {g}" msgstr "Ungültige Gruppe: {g}" -#: InvenTree/helpers.py:310 +#: InvenTree/helpers.py:309 msgid "No serial numbers found" msgstr "Keine Seriennummern gefunden" -#: InvenTree/helpers.py:314 +#: InvenTree/helpers.py:313 #, python-brace-format msgid "Number of unique serial number ({s}) must match quantity ({q})" msgstr "" @@ -71,47 +97,51 @@ msgstr "Französisch" msgid "Polish" msgstr "Polnisch" -#: InvenTree/status_codes.py:86 InvenTree/status_codes.py:162 +#: InvenTree/status_codes.py:84 InvenTree/status_codes.py:172 msgid "Pending" msgstr "Ausstehend" -#: InvenTree/status_codes.py:87 +#: InvenTree/status_codes.py:85 msgid "Placed" msgstr "Platziert" -#: InvenTree/status_codes.py:88 InvenTree/status_codes.py:165 +#: InvenTree/status_codes.py:86 InvenTree/status_codes.py:175 msgid "Complete" msgstr "Fertig" -#: InvenTree/status_codes.py:89 InvenTree/status_codes.py:164 +#: InvenTree/status_codes.py:87 InvenTree/status_codes.py:174 msgid "Cancelled" msgstr "Storniert" -#: InvenTree/status_codes.py:90 InvenTree/status_codes.py:130 +#: InvenTree/status_codes.py:88 InvenTree/status_codes.py:135 msgid "Lost" msgstr "Verloren" -#: InvenTree/status_codes.py:91 +#: InvenTree/status_codes.py:89 InvenTree/status_codes.py:137 msgid "Returned" msgstr "Zurückgegeben" -#: InvenTree/status_codes.py:126 +#: InvenTree/status_codes.py:131 msgid "OK" msgstr "OK" -#: InvenTree/status_codes.py:127 +#: InvenTree/status_codes.py:132 msgid "Attention needed" msgstr "erfordert Eingriff" -#: InvenTree/status_codes.py:128 +#: InvenTree/status_codes.py:133 msgid "Damaged" msgstr "Beschädigt" -#: InvenTree/status_codes.py:129 +#: InvenTree/status_codes.py:134 msgid "Destroyed" msgstr "Zerstört" -#: InvenTree/status_codes.py:163 build/templates/build/allocate_edit.html:28 +#: InvenTree/status_codes.py:136 +msgid "Shipped" +msgstr "" + +#: InvenTree/status_codes.py:173 build/templates/build/allocate_edit.html:28 #: build/templates/build/allocate_view.html:21 #: part/templates/part/part_base.html:114 part/templates/part/tabs.html:21 msgid "Allocated" @@ -142,7 +172,7 @@ msgstr "Überschuss darf 100% nicht überschreiten" msgid "Overage must be an integer value or a percentage" msgstr "Überschuss muss eine Ganzzahl oder ein Prozentwert sein" -#: InvenTree/views.py:549 +#: InvenTree/views.py:536 msgid "Database Statistics" msgstr "" @@ -178,7 +208,7 @@ msgstr "" msgid "Number of parts to build" msgstr "Anzahl der zu bauenden Teile" -#: build/models.py:82 templates/table_filters.html:42 +#: build/models.py:82 templates/table_filters.html:47 msgid "Build status" msgstr "Bau-Status" @@ -186,7 +216,7 @@ msgstr "Bau-Status" msgid "Batch code for this build output" msgstr "Chargennummer für diese Bau-Ausgabe" -#: build/models.py:97 stock/models.py:331 +#: build/models.py:97 stock/models.py:336 msgid "Link to external URL" msgstr "Link zu einer externen URL" @@ -205,15 +235,33 @@ msgid "Allocated quantity ({n}) must not exceed available quantity ({q})" msgstr "" "zugewiesene Anzahl ({n}) darf nicht die verfügbare ({q}) Anzahl überschreiten" -#: build/models.py:409 +#: build/models.py:397 order/models.py:448 +#, fuzzy +#| msgid "Stock Item to allocate to build" +msgid "StockItem is over-allocated" +msgstr "Lagerobjekt dem Bau zuweisen" + +#: build/models.py:400 order/models.py:451 +#, fuzzy +#| msgid "Quantity must be greater than zero" +msgid "Allocation quantity must be greater than zero" +msgstr "Anzahl muss größer Null sein" + +#: build/models.py:403 +#, fuzzy +#| msgid "Quantity must be 1 for item with a serial number" +msgid "Quantity must be 1 for serialized stock" +msgstr "Anzahl muss für Objekte mit Seriennummer \"1\" sein" + +#: build/models.py:418 msgid "Build to allocate parts" msgstr "Bau starten um Teile zuzuweisen" -#: build/models.py:416 +#: build/models.py:425 msgid "Stock Item to allocate to build" msgstr "Lagerobjekt dem Bau zuweisen" -#: build/models.py:424 +#: build/models.py:433 msgid "Stock quantity to allocate to build" msgstr "Lagerobjekt-Anzahl dem Bau zuweisen" @@ -231,8 +279,7 @@ msgstr "Zuweisung aufheben" #: build/templates/build/allocate_edit.html:19 #: build/templates/build/allocate_view.html:17 -#: build/templates/build/detail.html:22 -#: company/templates/company/detail_part.html:65 +#: build/templates/build/detail.html:22 order/models.py:385 #: order/templates/order/order_wizard/select_parts.html:30 #: order/templates/order/purchase_order_detail.html:26 #: part/templates/part/part_app_base.html:7 @@ -258,12 +305,11 @@ msgid "Allocate" msgstr "zuweisen" #: build/templates/build/allocate_view.html:10 -#: company/templates/company/detail_part.html:18 order/views.py:526 +#: company/templates/company/detail_part.html:18 order/views.py:671 msgid "Order Parts" msgstr "Teile bestellen" #: build/templates/build/allocate_view.html:18 -#: company/templates/company/index.html:54 #: company/templates/company/supplier_part_base.html:50 #: company/templates/company/supplier_part_detail.html:27 #: order/templates/order/purchase_order_detail.html:27 @@ -276,47 +322,49 @@ msgstr "Beschreibung" msgid "On Order" msgstr "bestellt" -#: build/templates/build/build_base.html:27 part/templates/part/tabs.html:28 -#: stock/templates/stock/item_base.html:122 templates/navbar.html:12 +#: build/templates/build/build_base.html:8 +#: build/templates/build/build_base.html:21 part/templates/part/tabs.html:28 +#: stock/templates/stock/item_base.html:159 templates/navbar.html:12 msgid "Build" msgstr "Bau" -#: build/templates/build/build_base.html:52 build/templates/build/detail.html:9 +#: build/templates/build/build_base.html:48 build/templates/build/detail.html:9 msgid "Build Details" msgstr "Bau-Status" -#: build/templates/build/build_base.html:56 +#: build/templates/build/build_base.html:52 #, fuzzy #| msgid "Build Notes" msgid "Build Title" msgstr "Bau-Bemerkungen" -#: build/templates/build/build_base.html:66 +#: build/templates/build/build_base.html:62 #: build/templates/build/detail.html:27 #: company/templates/company/supplier_part_pricing.html:27 #: order/templates/order/order_wizard/select_parts.html:32 #: order/templates/order/purchase_order_detail.html:30 -#: stock/templates/stock/item_base.html:108 +#: stock/templates/stock/item_base.html:20 +#: stock/templates/stock/item_base.html:26 +#: stock/templates/stock/item_base.html:145 #: stock/templates/stock/stock_adjust.html:18 msgid "Quantity" msgstr "Anzahl" -#: build/templates/build/build_base.html:71 +#: build/templates/build/build_base.html:67 #: build/templates/build/detail.html:42 -#: order/templates/order/order_base.html:72 -#: stock/templates/stock/item_base.html:175 +#: stock/templates/stock/item_base.html:212 msgid "Status" msgstr "Status" -#: build/templates/build/build_base.html:76 +#: build/templates/build/build_base.html:72 msgid "BOM Price" msgstr "" -#: build/templates/build/build_base.html:81 +#: build/templates/build/build_base.html:77 msgid "BOM pricing is incomplete" msgstr "" -#: build/templates/build/build_base.html:84 +#: build/templates/build/build_base.html:80 #, fuzzy #| msgid "Show pricing information" msgid "No pricing information" @@ -348,20 +396,21 @@ msgid "Stock can be taken from any available location." msgstr "Bestand kann jedem verfügbaren Lagerort entnommen werden." #: build/templates/build/detail.html:48 -#: stock/templates/stock/item_base.html:115 +#: stock/templates/stock/item_base.html:152 msgid "Batch" msgstr "Los" #: build/templates/build/detail.html:55 -#: company/templates/company/supplier_part_base.html:47 +#: company/templates/company/supplier_part_base.html:57 #: company/templates/company/supplier_part_detail.html:24 #: part/templates/part/detail.html:67 part/templates/part/part_base.html:85 -#: stock/templates/stock/item_base.html:143 +#: stock/templates/stock/item_base.html:180 msgid "External Link" msgstr "" #: build/templates/build/detail.html:61 -#: order/templates/order/order_base.html:84 +#: order/templates/order/order_base.html:93 +#: order/templates/order/sales_order_base.html:82 msgid "Created" msgstr "Erstellt" @@ -386,14 +435,16 @@ msgid "Build Notes" msgstr "Bau-Bemerkungen" #: build/templates/build/notes.html:20 company/templates/company/notes.html:17 -#: order/templates/order/order_notes.html:21 part/templates/part/notes.html:20 -#: stock/templates/stock/item_notes.html:21 +#: order/templates/order/order_notes.html:21 +#: order/templates/order/sales_order_notes.html:26 +#: part/templates/part/notes.html:20 stock/templates/stock/item_notes.html:21 msgid "Save" msgstr "Speichern" #: build/templates/build/notes.html:33 company/templates/company/notes.html:30 -#: order/templates/order/order_notes.html:32 part/templates/part/notes.html:32 -#: stock/templates/stock/item_notes.html:32 +#: order/templates/order/order_notes.html:32 +#: order/templates/order/sales_order_notes.html:37 +#: part/templates/part/notes.html:32 stock/templates/stock/item_notes.html:32 msgid "Edit notes" msgstr "Bermerkungen bearbeiten" @@ -406,9 +457,10 @@ msgstr "Details" msgid "Outputs" msgstr "" -#: build/templates/build/tabs.html:11 company/models.py:264 -#: company/templates/company/tabs.html:26 order/templates/order/tabs.html:15 -#: part/templates/part/tabs.html:58 stock/templates/stock/tabs.html:17 +#: build/templates/build/tabs.html:11 company/models.py:302 +#: company/templates/company/tabs.html:26 order/templates/order/po_tabs.html:15 +#: order/templates/order/so_tabs.html:15 part/templates/part/tabs.html:63 +#: stock/templates/stock/tabs.html:17 msgid "Notes" msgstr "Notizen" @@ -597,132 +649,144 @@ msgstr "" msgid "Delete Currency" msgstr "" -#: company/models.py:76 +#: company/models.py:83 msgid "Company name" msgstr "Firmenname" -#: company/models.py:78 +#: company/models.py:85 msgid "Description of the company" msgstr "Firmenbeschreibung" -#: company/models.py:80 +#: company/models.py:87 msgid "Company website URL" msgstr "Firmenwebsite" -#: company/models.py:83 +#: company/models.py:90 msgid "Company address" msgstr "Firmenadresse" -#: company/models.py:86 +#: company/models.py:93 msgid "Contact phone number" msgstr "Kontakt-Tel." -#: company/models.py:88 +#: company/models.py:95 msgid "Contact email address" msgstr "Kontakt-Email" -#: company/models.py:91 +#: company/models.py:98 msgid "Point of contact" msgstr "Anlaufstelle" -#: company/models.py:93 +#: company/models.py:100 msgid "Link to external company information" msgstr "Link auf externe Firmeninformation" -#: company/models.py:105 +#: company/models.py:112 msgid "Do you sell items to this company?" msgstr "Verkaufen Sie Teile an diese Firma?" -#: company/models.py:107 +#: company/models.py:114 msgid "Do you purchase items from this company?" msgstr "Kaufen Sie Teile von dieser Firma?" -#: company/models.py:245 +#: company/models.py:116 +#, fuzzy +#| msgid "Is this part a template part?" +msgid "Does this company manufacture parts?" +msgstr "Ist dieses Teil eine Vorlage?" + +#: company/models.py:276 msgid "Select part" msgstr "Teil auswählen" -#: company/models.py:251 +#: company/models.py:282 msgid "Select supplier" msgstr "Zulieferer auswählen" -#: company/models.py:254 +#: company/models.py:285 msgid "Supplier stock keeping unit" msgstr "Stock Keeping Units (SKU) des Zulieferers" -#: company/models.py:256 company/templates/company/detail_part.html:96 -#: company/templates/company/supplier_part_base.html:53 -#: company/templates/company/supplier_part_detail.html:30 -msgid "Manufacturer" +#: company/models.py:292 +#, fuzzy +#| msgid "Manufacturer" +msgid "Select manufacturer" msgstr "Hersteller" -#: company/models.py:258 +#: company/models.py:296 msgid "Manufacturer part number" msgstr "Hersteller-Teilenummer" -#: company/models.py:260 +#: company/models.py:298 msgid "URL for external supplier part link" msgstr "Teil-URL des Zulieferers" -#: company/models.py:262 +#: company/models.py:300 msgid "Supplier part description" msgstr "Zuliefererbeschreibung des Teils" -#: company/models.py:266 +#: company/models.py:304 msgid "Minimum charge (e.g. stocking fee)" msgstr "Mindestpreis" -#: company/models.py:268 +#: company/models.py:306 msgid "Part packaging" msgstr "Teile-Packaging" -#: company/templates/company/company_base.html:7 order/models.py:131 +#: company/templates/company/company_base.html:7 +#: company/templates/company/company_base.html:22 msgid "Company" msgstr "Firma" -#: company/templates/company/company_base.html:50 -#: company/templates/company/index.html:59 -msgid "Website" -msgstr "" - -#: company/templates/company/company_base.html:57 -msgid "Address" -msgstr "" - -#: company/templates/company/company_base.html:64 -msgid "Phone" -msgstr "" - -#: company/templates/company/company_base.html:71 -msgid "Email" -msgstr "" - -#: company/templates/company/company_base.html:78 -msgid "Contact" -msgstr "" - +#: company/templates/company/company_base.html:42 #: company/templates/company/detail.html:8 #, fuzzy #| msgid "Company Notes" msgid "Company Details" msgstr "Firmenbemerkungen" +#: company/templates/company/company_base.html:48 +msgid "Website" +msgstr "" + +#: company/templates/company/company_base.html:55 +msgid "Address" +msgstr "" + +#: company/templates/company/company_base.html:62 +msgid "Phone" +msgstr "" + +#: company/templates/company/company_base.html:69 +msgid "Email" +msgstr "" + +#: company/templates/company/company_base.html:76 +msgid "Contact" +msgstr "" + #: company/templates/company/detail.html:16 -#: stock/templates/stock/item_base.html:136 -msgid "Customer" -msgstr "Kunde" +#: company/templates/company/supplier_part_base.html:73 +#: company/templates/company/supplier_part_detail.html:30 +msgid "Manufacturer" +msgstr "Hersteller" #: company/templates/company/detail.html:21 -#: company/templates/company/index.html:46 -#: company/templates/company/supplier_part_base.html:44 -#: company/templates/company/supplier_part_detail.html:21 -#: order/templates/order/order_base.html:67 +#: company/templates/company/supplier_part_base.html:63 +#: company/templates/company/supplier_part_detail.html:21 order/models.py:138 +#: order/templates/order/order_base.html:74 #: order/templates/order/order_wizard/select_pos.html:30 -#: stock/templates/stock/item_base.html:150 +#: stock/templates/stock/item_base.html:187 msgid "Supplier" msgstr "Zulieferer" +#: company/templates/company/detail.html:26 order/models.py:275 +#: order/templates/order/sales_order_base.html:63 +#: stock/templates/stock/item_base.html:173 +msgid "Customer" +msgstr "Kunde" + #: company/templates/company/detail_part.html:8 -#: company/templates/company/tabs.html:9 msgid "Supplier Parts" msgstr "Zulieferer-Teile" @@ -743,26 +807,42 @@ msgstr "" msgid "Delete Parts" msgstr "Anhang löschen" -#: company/templates/company/detail_part.html:88 -#: company/templates/company/supplier_part_base.html:45 -#: company/templates/company/supplier_part_detail.html:22 -msgid "SKU" -msgstr "" - -#: company/templates/company/detail_part.html:105 -msgid "Link" -msgstr "" - -#: company/templates/company/detail_purchase_orders.html:8 -#: company/templates/company/tabs.html:15 part/templates/part/tabs.html:43 -msgid "Purchase Orders" -msgstr "Bestellungen" - -#: company/templates/company/detail_purchase_orders.html:13 +#: company/templates/company/detail_part.html:43 +#: part/templates/part/stock.html:75 #, fuzzy -#| msgid "Purchase Order" -msgid "New Purchase Order" -msgstr "Kaufvertrag" +#| msgid "Part" +msgid "New Part" +msgstr "Teil" + +#: company/templates/company/detail_part.html:44 +#, fuzzy +#| msgid "Create new Stock Item" +msgid "Create new Part" +msgstr "Neues Lagerobjekt hinzufügen" + +#: company/templates/company/detail_part.html:49 company/views.py:52 +#, fuzzy +#| msgid "Supplier" +msgid "New Supplier" +msgstr "Zulieferer" + +#: company/templates/company/detail_part.html:50 company/views.py:184 +#, fuzzy +#| msgid "Supplier Part" +msgid "Create new Supplier" +msgstr "Zulieferer-Teil" + +#: company/templates/company/detail_part.html:55 company/views.py:58 +#, fuzzy +#| msgid "Manufacturer" +msgid "New Manufacturer" +msgstr "Hersteller" + +#: company/templates/company/detail_part.html:56 company/views.py:187 +#, fuzzy +#| msgid "Manufacturer" +msgid "Create new Manufacturer" +msgstr "Hersteller" #: company/templates/company/detail_stock.html:9 #, fuzzy @@ -770,34 +850,18 @@ msgstr "Kaufvertrag" msgid "Supplier Stock" msgstr "Zulieferer-Teil" -#: company/templates/company/detail_stock.html:33 +#: company/templates/company/detail_stock.html:34 #: company/templates/company/supplier_part_stock.html:38 #: part/templates/part/stock.html:53 templates/stock_table.html:5 msgid "Export" msgstr "" #: company/templates/company/index.html:7 -#: company/templates/company/index.html:12 #, fuzzy #| msgid "Suppliers" msgid "Supplier List" msgstr "Zulieferer" -#: company/templates/company/index.html:17 -#, fuzzy -#| msgid "Supplier" -msgid "New Supplier" -msgstr "Zulieferer" - -#: company/templates/company/index.html:41 -msgid "ID" -msgstr "" - -#: company/templates/company/index.html:69 part/templates/part/category.html:83 -#: templates/navbar.html:10 templates/stats.html:8 templates/stats.html:17 -msgid "Parts" -msgstr "Teile" - #: company/templates/company/notes.html:10 #: company/templates/company/notes.html:27 msgid "Company Notes" @@ -808,34 +872,85 @@ msgid "Are you sure you want to delete the following Supplier Parts?" msgstr "" "Sind Sie sicher, dass sie die folgenden Zulieferer-Teile löschen möchten?" +#: company/templates/company/purchase_orders.html:9 +#: company/templates/company/tabs.html:17 +#: order/templates/order/purchase_orders.html:7 +#: order/templates/order/purchase_orders.html:12 +#: part/templates/part/orders.html:9 part/templates/part/tabs.html:43 +#: templates/navbar.html:18 +msgid "Purchase Orders" +msgstr "Bestellungen" + +#: company/templates/company/purchase_orders.html:14 +#: order/templates/order/purchase_orders.html:17 +#, fuzzy +#| msgid "Purchase Order" +msgid "Create new purchase order" +msgstr "Kaufvertrag" + +#: company/templates/company/purchase_orders.html:14 +#: order/templates/order/purchase_orders.html:17 +#, fuzzy +#| msgid "Purchase Order" +msgid "New Purchase Order" +msgstr "Kaufvertrag" + +#: company/templates/company/sales_orders.html:9 +#: company/templates/company/tabs.html:22 +#: order/templates/order/sales_orders.html:7 +#: order/templates/order/sales_orders.html:12 +#: part/templates/part/sales_orders.html:9 part/templates/part/tabs.html:48 +#: templates/navbar.html:25 +msgid "Sales Orders" +msgstr "Bestellungen" + +#: company/templates/company/sales_orders.html:14 +#: order/templates/order/sales_orders.html:17 +#, fuzzy +#| msgid "Create new Stock Item" +msgid "Create new sales order" +msgstr "Neues Lagerobjekt hinzufügen" + +#: company/templates/company/sales_orders.html:14 +#: order/templates/order/sales_orders.html:17 +#, fuzzy +#| msgid "Sales Orders" +msgid "New Sales Order" +msgstr "Bestellungen" + #: company/templates/company/supplier_part_base.html:6 -#: company/templates/company/supplier_part_base.html:13 -#: stock/templates/stock/item_base.html:155 +#: company/templates/company/supplier_part_base.html:19 +#: stock/templates/stock/item_base.html:192 msgid "Supplier Part" msgstr "Zulieferer-Teil" -#: company/templates/company/supplier_part_base.html:34 +#: company/templates/company/supplier_part_base.html:35 #: company/templates/company/supplier_part_detail.html:11 #, fuzzy #| msgid "Supplier Parts" msgid "Supplier Part Details" msgstr "Zulieferer-Teile" -#: company/templates/company/supplier_part_base.html:37 +#: company/templates/company/supplier_part_base.html:40 #: company/templates/company/supplier_part_detail.html:14 #, fuzzy #| msgid "Internal Part Number" msgid "Internal Part" msgstr "Interne Teilenummer" -#: company/templates/company/supplier_part_base.html:54 +#: company/templates/company/supplier_part_base.html:67 +#: company/templates/company/supplier_part_detail.html:22 +msgid "SKU" +msgstr "" + +#: company/templates/company/supplier_part_base.html:77 #: company/templates/company/supplier_part_detail.html:31 #, fuzzy #| msgid "IPN" msgid "MPN" msgstr "IPN (Interne Produktnummer)" -#: company/templates/company/supplier_part_base.html:57 +#: company/templates/company/supplier_part_base.html:84 #: company/templates/company/supplier_part_detail.html:34 #: order/templates/order/purchase_order_detail.html:34 msgid "Note" @@ -912,178 +1027,306 @@ msgid "Stock" msgstr "Lagerbestand" #: company/templates/company/supplier_part_tabs.html:11 -#: templates/navbar.html:14 #, fuzzy #| msgid "On Order" msgid "Orders" msgstr "bestellt" -#: company/templates/company/tabs.html:21 -msgid "Sales Orders" -msgstr "Bestellungen" +#: company/templates/company/tabs.html:9 part/templates/part/category.html:83 +#: templates/navbar.html:10 templates/stats.html:8 templates/stats.html:17 +msgid "Parts" +msgstr "Teile" -#: company/views.py:99 +#: company/views.py:51 part/templates/part/tabs.html:37 +#: templates/navbar.html:16 +msgid "Suppliers" +msgstr "Zulieferer" + +#: company/views.py:57 templates/navbar.html:17 +#, fuzzy +#| msgid "Manufacturer" +msgid "Manufacturers" +msgstr "Hersteller" + +#: company/views.py:63 templates/navbar.html:24 +#, fuzzy +#| msgid "Customer" +msgid "Customers" +msgstr "Kunde" + +#: company/views.py:64 +#, fuzzy +#| msgid "Customer" +msgid "New Customer" +msgstr "Kunde" + +#: company/views.py:71 +#, fuzzy +#| msgid "Company" +msgid "Companies" +msgstr "Firma" + +#: company/views.py:72 +#, fuzzy +#| msgid "Company" +msgid "New Company" +msgstr "Firma" + +#: company/views.py:149 #, fuzzy #| msgid "Company name" msgid "Update Company Image" msgstr "Firmenname" -#: company/views.py:104 +#: company/views.py:154 msgid "Updated company image" msgstr "" -#: company/views.py:114 +#: company/views.py:164 #, fuzzy #| msgid "Company" msgid "Edit Company" msgstr "Firma" -#: company/views.py:118 +#: company/views.py:168 #, fuzzy #| msgid "Link to external company information" msgid "Edited company information" msgstr "Link auf externe Firmeninformation" -#: company/views.py:128 +#: company/views.py:190 +#, fuzzy +#| msgid "Create new Stock Item" +msgid "Create new Customer" +msgstr "Neues Lagerobjekt hinzufügen" + +#: company/views.py:192 #, fuzzy #| msgid "Create new Stock Item" msgid "Create new Company" msgstr "Neues Lagerobjekt hinzufügen" -#: company/views.py:132 +#: company/views.py:219 #, fuzzy #| msgid "Created new stock item" msgid "Created new company" msgstr "Neues Lagerobjekt erstellt" -#: company/views.py:142 +#: company/views.py:229 #, fuzzy #| msgid "Company" msgid "Delete Company" msgstr "Firma" -#: company/views.py:147 +#: company/views.py:234 #, fuzzy #| msgid "Company address" msgid "Company was deleted" msgstr "Firmenadresse" -#: company/views.py:172 +#: company/views.py:259 #, fuzzy #| msgid "Supplier Part" msgid "Edit Supplier Part" msgstr "Zulieferer-Teil" -#: company/views.py:181 part/templates/part/stock.html:82 +#: company/views.py:268 part/templates/part/stock.html:82 #, fuzzy #| msgid "Supplier Part" msgid "Create new Supplier Part" msgstr "Zulieferer-Teil" -#: company/views.py:238 +#: company/views.py:328 #, fuzzy #| msgid "Supplier Part" msgid "Delete Supplier Part" msgstr "Zulieferer-Teil" -#: company/views.py:308 +#: company/views.py:398 msgid "Add Price Break" msgstr "" -#: company/views.py:350 +#: company/views.py:440 msgid "Edit Price Break" msgstr "" -#: company/views.py:365 +#: company/views.py:455 #, fuzzy #| msgid "Delete attachment" msgid "Delete Price Break" msgstr "Anhang löschen" -#: order/forms.py:22 +#: order/forms.py:24 msgid "Place order" msgstr "Bestellung aufgeben" -#: order/forms.py:33 +#: order/forms.py:35 msgid "Mark order as complete" msgstr "Bestellung als vollständig markieren" -#: order/forms.py:44 +#: order/forms.py:46 msgid "Cancel order" msgstr "Bestellung stornieren" -#: order/forms.py:55 +#: order/forms.py:57 msgid "Receive parts to this location" msgstr "Teile in diesen Ort empfangen" -#: order/models.py:68 +#: order/models.py:71 msgid "Order reference" msgstr "Bestell-Referenz" -#: order/models.py:70 +#: order/models.py:73 msgid "Order description" msgstr "Bestellungs-Beschreibung" -#: order/models.py:72 +#: order/models.py:75 msgid "Link to external page" msgstr "Link auf externe Seite" -#: order/models.py:89 +#: order/models.py:92 msgid "Order notes" msgstr "Bestell-Notizen" -#: order/models.py:162 order/models.py:213 part/views.py:1119 -#: stock/models.py:467 +#: order/models.py:141 +#, fuzzy +#| msgid "Order reference" +msgid "Supplier order reference code" +msgstr "Bestell-Referenz" + +#: order/models.py:171 order/models.py:222 part/views.py:1119 +#: stock/models.py:519 msgid "Quantity must be greater than zero" msgstr "Anzahl muss größer Null sein" -#: order/models.py:167 +#: order/models.py:176 msgid "Part supplier must match PO supplier" msgstr "Teile-Zulieferer muss dem Zulieferer des Kaufvertrags entsprechen" -#: order/models.py:208 +#: order/models.py:217 msgid "Lines can only be received against an order marked as 'Placed'" msgstr "Nur Teile aufgegebener Bestllungen können empfangen werden" -#: order/models.py:268 +#: order/models.py:278 +#, fuzzy +#| msgid "Order reference" +msgid "Customer order reference code" +msgstr "Bestell-Referenz" + +#: order/models.py:324 msgid "Item quantity" msgstr "Anzahl" -#: order/models.py:270 +#: order/models.py:326 msgid "Line item reference" msgstr "Position - Referenz" -#: order/models.py:272 +#: order/models.py:328 msgid "Line item notes" msgstr "Position - Notizen" -#: order/models.py:298 stock/templates/stock/item_base.html:129 +#: order/models.py:354 order/templates/order/order_base.html:9 +#: order/templates/order/order_base.html:23 +#: stock/templates/stock/item_base.html:166 msgid "Purchase Order" msgstr "Kaufvertrag" -#: order/models.py:307 +#: order/models.py:363 msgid "Supplier part" msgstr "Zulieferer-Teil" -#: order/models.py:310 +#: order/models.py:366 msgid "Number of items received" msgstr "Empfangene Objekt-Anzahl" -#: order/templates/order/order_base.html:62 +#: order/models.py:383 order/templates/order/sales_order_base.html:9 +#: order/templates/order/sales_order_base.html:31 +#: order/templates/order/sales_order_notes.html:10 +#, fuzzy +#| msgid "Sales Orders" +msgid "Sales Order" +msgstr "Bestellungen" + +#: order/models.py:440 +msgid "Cannot allocate stock item to a line with a different part" +msgstr "" + +#: order/models.py:442 +msgid "Cannot allocate stock to a line without a part" +msgstr "" + +#: order/models.py:445 +#, fuzzy +#| msgid "Allocated quantity ({n}) must not exceed available quantity ({q})" +msgid "Allocation quantity cannot exceed stock quantity" +msgstr "" +"zugewiesene Anzahl ({n}) darf nicht die verfügbare ({q}) Anzahl überschreiten" + +#: order/models.py:454 +#, fuzzy +#| msgid "Quantity must be 1 for item with a serial number" +msgid "Quantity must be 1 for serialized stock item" +msgstr "Anzahl muss für Objekte mit Seriennummer \"1\" sein" + +#: order/models.py:466 +#, fuzzy +#| msgid "Stock Item to allocate to build" +msgid "Select stock item to allocate" +msgstr "Lagerobjekt dem Bau zuweisen" + +#: order/models.py:469 +#, fuzzy +#| msgid "Enter a valid quantity" +msgid "Enter stock allocation quantity" +msgstr "Bitte eine gültige Anzahl eingeben" + +#: order/templates/order/delete_attachment.html:5 +#: part/templates/part/attachment_delete.html:5 +#, fuzzy +#| msgid "Are you sure you want to delete the following Supplier Parts?" +msgid "Are you sure you want to delete this attachment?" +msgstr "" +"Sind Sie sicher, dass sie die folgenden Zulieferer-Teile löschen möchten?" + +#: order/templates/order/order_base.html:59 msgid "Purchase Order Details" msgstr "Bestelldetails" -#: order/templates/order/order_base.html:90 +#: order/templates/order/order_base.html:64 +#: order/templates/order/sales_order_base.html:53 +#, fuzzy +#| msgid "Order reference" +msgid "Order Reference" +msgstr "Bestell-Referenz" + +#: order/templates/order/order_base.html:69 +#: order/templates/order/sales_order_base.html:58 +#, fuzzy +#| msgid "Order Parts" +msgid "Order Status" +msgstr "Teile bestellen" + +#: order/templates/order/order_base.html:80 +#, fuzzy +#| msgid "Reference" +msgid "Supplier Reference" +msgstr "Referenz" + +#: order/templates/order/order_base.html:99 +#: order/templates/order/sales_order_base.html:88 msgid "Issued" msgstr "Aufgegeben" -#: order/templates/order/order_base.html:97 +#: order/templates/order/order_base.html:106 #: order/templates/order/purchase_order_detail.html:32 +#: order/templates/order/sales_order_base.html:95 msgid "Received" msgstr "Empfangen" #: order/templates/order/order_notes.html:13 #: order/templates/order/order_notes.html:29 +#: order/templates/order/sales_order_notes.html:18 +#: order/templates/order/sales_order_notes.html:34 msgid "Order Notes" msgstr "Bestellungsbemerkungen" @@ -1124,7 +1367,7 @@ msgid "Select existing purchase orders, or create new orders." msgstr "" #: order/templates/order/order_wizard/select_pos.html:31 -#: order/templates/order/tabs.html:5 +#: order/templates/order/po_tabs.html:5 order/templates/order/so_tabs.html:5 msgid "Items" msgstr "Positionen" @@ -1147,45 +1390,51 @@ msgid "Purchase Order Attachments" msgstr "Bestelldetails" #: order/templates/order/po_attachments.html:17 +#: order/templates/order/so_attachments.html:17 #: part/templates/part/attachments.html:14 msgid "Add Attachment" msgstr "Anhang hinzufügen" #: order/templates/order/po_attachments.html:24 +#: order/templates/order/so_attachments.html:24 #: part/templates/part/attachments.html:22 msgid "File" msgstr "Datei" #: order/templates/order/po_attachments.html:25 +#: order/templates/order/so_attachments.html:25 #: part/templates/part/attachments.html:23 msgid "Comment" msgstr "Kommentar" #: order/templates/order/po_attachments.html:36 +#: order/templates/order/so_attachments.html:36 #: part/templates/part/attachments.html:34 part/views.py:119 msgid "Edit attachment" msgstr "Anhang bearbeiten" #: order/templates/order/po_attachments.html:39 +#: order/templates/order/so_attachments.html:39 #: part/templates/part/attachments.html:37 msgid "Delete attachment" msgstr "Anhang löschen" -#: order/templates/order/po_delete.html:5 -#: part/templates/part/attachment_delete.html:5 -#, fuzzy -#| msgid "Are you sure you want to delete the following Supplier Parts?" -msgid "Are you sure you want to delete this attachment?" -msgstr "" -"Sind Sie sicher, dass sie die folgenden Zulieferer-Teile löschen möchten?" +#: order/templates/order/po_tabs.html:8 order/templates/order/so_tabs.html:8 +#: part/templates/part/tabs.html:60 +msgid "Attachments" +msgstr "Anhänge" -#: order/templates/order/purchase_order_detail.html:16 order/views.py:825 +#: order/templates/order/purchase_order_detail.html:16 +#: order/templates/order/sales_order_detail.html:17 order/views.py:970 +#: order/views.py:1084 msgid "Add Line Item" msgstr "Position hinzufügen" #: order/templates/order/purchase_order_detail.html:20 -msgid "Order Items" -msgstr "Bestellungspositionen" +#, fuzzy +#| msgid "Purchase Orders" +msgid "Purchase Order Items" +msgstr "Bestellungen" #: order/templates/order/purchase_order_detail.html:25 msgid "Line" @@ -1199,140 +1448,248 @@ msgstr "Bestellnummer" msgid "Reference" msgstr "Referenz" -#: order/templates/order/tabs.html:8 part/templates/part/tabs.html:55 -msgid "Attachments" -msgstr "Anhänge" +#: order/templates/order/sales_order_base.html:15 +msgid "This SalesOrder has not been fully allocated" +msgstr "" -#: order/views.py:80 +#: order/templates/order/sales_order_base.html:40 +#, fuzzy +#| msgid "Parts" +msgid "Packing List" +msgstr "Teile" + +#: order/templates/order/sales_order_base.html:48 +#, fuzzy +#| msgid "Purchase Order Details" +msgid "Sales Order Details" +msgstr "Bestelldetails" + +#: order/templates/order/sales_order_base.html:69 +#, fuzzy +#| msgid "Reference" +msgid "Customer Reference" +msgstr "Referenz" + +#: order/templates/order/sales_order_detail.html:14 +#, fuzzy +#| msgid "Sales Orders" +msgid "Sales Order Items" +msgstr "Bestellungen" + +#: order/templates/order/sales_order_detail.html:90 +#, fuzzy +#| msgid "Edit Stock Location" +msgid "Edit stock allocation" +msgstr "Lagerobjekt-Standort bearbeiten" + +#: order/templates/order/sales_order_detail.html:91 +#, fuzzy +#| msgid "Delete Stock Location" +msgid "Delete stock allocation" +msgstr "Standort löschen" + +#: order/templates/order/sales_order_detail.html:178 +#, fuzzy +#| msgid "All parts" +msgid "Buy parts" +msgstr "Alle Teile" + +#: order/templates/order/sales_order_detail.html:182 +#, fuzzy +#| msgid "Build status" +msgid "Build parts" +msgstr "Bau-Status" + +#: order/templates/order/sales_order_detail.html:185 +#, fuzzy +#| msgid "All parts" +msgid "Allocate parts" +msgstr "Alle Teile" + +#: order/templates/order/sales_order_detail.html:189 +#, fuzzy +#| msgid "Add Line Item" +msgid "Edit line item" +msgstr "Position hinzufügen" + +#: order/templates/order/sales_order_detail.html:190 +#, fuzzy +#| msgid "Deleted {n} stock items" +msgid "Delete line item " +msgstr "{n} Teile im Lager gelöscht" + +#: order/templates/order/so_attachments.html:11 +#, fuzzy +#| msgid "Purchase Order Details" +msgid "Sales Order Attachments" +msgstr "Bestelldetails" + +#: order/views.py:97 #, fuzzy #| msgid "Purchase Order Details" msgid "Add Purchase Order Attachment" msgstr "Bestelldetails" -#: order/views.py:85 part/views.py:80 +#: order/views.py:102 order/views.py:142 part/views.py:80 #, fuzzy #| msgid "Add Attachment" msgid "Added attachment" msgstr "Anhang hinzufügen" -#: order/views.py:121 +#: order/views.py:138 +#, fuzzy +#| msgid "Purchase Order Details" +msgid "Add Sales Order Attachment" +msgstr "Bestelldetails" + +#: order/views.py:166 order/views.py:187 #, fuzzy #| msgid "Edit attachment" msgid "Edit Attachment" msgstr "Anhang bearbeiten" -#: order/views.py:125 +#: order/views.py:170 order/views.py:191 #, fuzzy #| msgid "Part Attachments" msgid "Attachment updated" msgstr "Anhänge" -#: order/views.py:141 +#: order/views.py:206 order/views.py:220 #, fuzzy #| msgid "Delete attachment" msgid "Delete Attachment" msgstr "Anhang löschen" -#: order/views.py:147 +#: order/views.py:212 order/views.py:226 #, fuzzy #| msgid "Delete attachment" msgid "Deleted attachment" msgstr "Anhang löschen" -#: order/views.py:177 +#: order/views.py:277 #, fuzzy #| msgid "Purchase Order" msgid "Create Purchase Order" msgstr "Kaufvertrag" -#: order/views.py:207 +#: order/views.py:307 +#, fuzzy +#| msgid "Purchase Order" +msgid "Create Sales Order" +msgstr "Kaufvertrag" + +#: order/views.py:336 #, fuzzy #| msgid "Purchase Order" msgid "Edit Purchase Order" msgstr "Kaufvertrag" -#: order/views.py:227 +#: order/views.py:356 +#, fuzzy +#| msgid "Sales Orders" +msgid "Edit Sales Order" +msgstr "Bestellungen" + +#: order/views.py:372 #, fuzzy #| msgid "Cancel order" msgid "Cancel Order" msgstr "Bestellung stornieren" -#: order/views.py:242 +#: order/views.py:387 msgid "Confirm order cancellation" msgstr "Bestell-Stornierung bestätigen" -#: order/views.py:260 +#: order/views.py:405 #, fuzzy #| msgid "Issued" msgid "Issue Order" msgstr "Aufgegeben" -#: order/views.py:275 +#: order/views.py:420 msgid "Confirm order placement" msgstr "Bestellungstätigung bestätigen" -#: order/views.py:296 +#: order/views.py:441 #, fuzzy #| msgid "Completed" msgid "Complete Order" msgstr "Fertig" -#: order/views.py:362 +#: order/views.py:507 #, fuzzy #| msgid "Required Parts" msgid "Receive Parts" msgstr "benötigte Teile" -#: order/views.py:429 +#: order/views.py:574 msgid "Items received" msgstr "Anzahl empfangener Positionen" -#: order/views.py:443 +#: order/views.py:588 msgid "No destination set" msgstr "Kein Ziel gesetzt" -#: order/views.py:474 +#: order/views.py:619 msgid "Error converting quantity to number" msgstr "Fehler beim Konvertieren zu Zahl" -#: order/views.py:480 +#: order/views.py:625 msgid "Receive quantity less than zero" msgstr "Anzahl kleiner null empfangen" -#: order/views.py:486 +#: order/views.py:631 msgid "No lines specified" msgstr "Keine Zeilen angegeben" -#: order/views.py:845 +#: order/views.py:990 msgid "Invalid Purchase Order" msgstr "Ungültige Bestellung" -#: order/views.py:853 +#: order/views.py:998 msgid "Supplier must match for Part and Order" msgstr "Zulieferer muss zum Teil und zur Bestellung passen" -#: order/views.py:858 +#: order/views.py:1003 msgid "Invalid SupplierPart selection" msgstr "Ungültige Wahl des Zulieferer-Teils" -#: order/views.py:940 +#: order/views.py:1123 order/views.py:1141 #, fuzzy #| msgid "Add Line Item" msgid "Edit Line Item" msgstr "Position hinzufügen" -#: order/views.py:956 +#: order/views.py:1157 order/views.py:1169 #, fuzzy #| msgid "Delete Stock Item" msgid "Delete Line Item" msgstr "Lagerobjekt löschen" -#: order/views.py:961 +#: order/views.py:1162 order/views.py:1174 #, fuzzy #| msgid "Deleted {n} stock items" msgid "Deleted line item" msgstr "{n} Teile im Lager gelöscht" +#: order/views.py:1183 +#, fuzzy +#| msgid "Allocate Stock to Build" +msgid "Allocate Stock to Order" +msgstr "Lagerbestand dem Bau zuweisen" + +#: order/views.py:1252 +#, fuzzy +#| msgid "Edit Stock Location" +msgid "Edit Allocation Quantity" +msgstr "Lagerobjekt-Standort bearbeiten" + +#: order/views.py:1267 +#, fuzzy +#| msgid "Receive parts to this location" +msgid "Remove allocation" +msgstr "Teile in diesen Ort empfangen" + #: part/bom.py:140 #, python-brace-format msgid "Unsupported file format: {f}" @@ -1502,63 +1859,63 @@ msgstr "Bemerkungen - unterstüzt Markdown-Formatierung" msgid "Stored BOM checksum" msgstr "Prüfsumme der Stückliste gespeichert" -#: part/models.py:1049 +#: part/models.py:1065 msgid "Parameter template name must be unique" msgstr "Vorlagen-Name des Parameters muss eindeutig sein" -#: part/models.py:1054 +#: part/models.py:1070 msgid "Parameter Name" msgstr "Name des Parameters" -#: part/models.py:1056 +#: part/models.py:1072 msgid "Parameter Units" msgstr "Parameter Einheit" -#: part/models.py:1082 +#: part/models.py:1098 msgid "Parent Part" msgstr "Ausgangsteil" -#: part/models.py:1084 +#: part/models.py:1100 msgid "Parameter Template" msgstr "Parameter Vorlage" -#: part/models.py:1086 +#: part/models.py:1102 msgid "Parameter Value" msgstr "Parameter Wert" -#: part/models.py:1110 +#: part/models.py:1126 msgid "Select parent part" msgstr "Ausgangsteil auswählen" -#: part/models.py:1119 +#: part/models.py:1135 msgid "Select part to be used in BOM" msgstr "Teil für die Nutzung in der Stückliste auswählen" -#: part/models.py:1126 +#: part/models.py:1142 msgid "BOM quantity for this BOM item" msgstr "Stücklisten-Anzahl für dieses Stücklisten-Teil" -#: part/models.py:1129 +#: part/models.py:1145 msgid "Estimated build wastage quantity (absolute or percentage)" msgstr "Geschätzter Ausschuss (absolut oder prozentual)" -#: part/models.py:1132 +#: part/models.py:1148 msgid "BOM item reference" msgstr "Referenz des Objekts auf der Stückliste" -#: part/models.py:1135 +#: part/models.py:1151 msgid "BOM item notes" msgstr "Notizen zum Stücklisten-Objekt" -#: part/models.py:1137 +#: part/models.py:1153 msgid "BOM line checksum" msgstr "Prüfsumme der Stückliste" -#: part/models.py:1200 +#: part/models.py:1216 msgid "Part cannot be added to its own Bill of Materials" msgstr "Teil kann nicht zu seiner eigenen Stückliste hinzugefügt werden" -#: part/models.py:1207 +#: part/models.py:1223 #, python-brace-format msgid "Part '{p1}' is used in BOM for '{p2}' (recursive)" msgstr "Teil '{p1}' wird in Stückliste für Teil '{p2}' benutzt (rekursiv)" @@ -1664,7 +2021,7 @@ msgstr "Teil ist virtuell (kein physisches Teil)" msgid "Part is not a virtual part" msgstr "Teil ist nicht virtuell" -#: part/templates/part/detail.html:132 templates/table_filters.html:86 +#: part/templates/part/detail.html:132 templates/table_filters.html:91 msgid "Assembly" msgstr "Baugruppe" @@ -1676,7 +2033,7 @@ msgstr "Teil kann aus anderen Teilen angefertigt werden" msgid "Part cannot be assembled from other parts" msgstr "Teil kann nicht aus anderen Teilen angefertigt werden" -#: part/templates/part/detail.html:141 templates/table_filters.html:90 +#: part/templates/part/detail.html:141 templates/table_filters.html:95 msgid "Component" msgstr "Komponente" @@ -1708,15 +2065,17 @@ msgstr "Kaufbar" msgid "Part can be purchased from external suppliers" msgstr "Teil kann von externen Zulieferern gekauft werden" -#: part/templates/part/detail.html:169 -msgid "Sellable" +#: part/templates/part/detail.html:168 templates/table_filters.html:103 +#, fuzzy +#| msgid "Sellable" +msgid "Salable" msgstr "Verkaufbar" -#: part/templates/part/detail.html:172 +#: part/templates/part/detail.html:171 msgid "Part can be sold to customers" msgstr "Teil kann an Kunden verkauft werden" -#: part/templates/part/detail.html:174 +#: part/templates/part/detail.html:173 msgid "Part cannot be sold to customers" msgstr "Teil kann nicht an Kunden verkauft werden" @@ -1724,6 +2083,18 @@ msgstr "Teil kann nicht an Kunden verkauft werden" msgid "Part Notes" msgstr "Teil-Bemerkungen" +#: part/templates/part/orders.html:14 +#, fuzzy +#| msgid "Order Parts" +msgid "Order part" +msgstr "Teile bestellen" + +#: part/templates/part/orders.html:14 +#, fuzzy +#| msgid "Order Parts" +msgid "Order Part" +msgstr "Teile bestellen" + #: part/templates/part/part_app_base.html:9 #, fuzzy #| msgid "Part category" @@ -1794,11 +2165,17 @@ msgstr "Teil auswählen" msgid "Upload new image" msgstr "" -#: part/templates/part/stock.html:75 +#: part/templates/part/sales_orders.html:14 #, fuzzy -#| msgid "Part" -msgid "New Part" -msgstr "Teil" +#| msgid "Sales Orders" +msgid "New sales order" +msgstr "Bestellungen" + +#: part/templates/part/sales_orders.html:14 +#, fuzzy +#| msgid "On Order" +msgid "New Order" +msgstr "bestellt" #: part/templates/part/stock.html:76 #, fuzzy @@ -1812,7 +2189,7 @@ msgstr "Neues Lagerobjekt hinzufügen" msgid "No Stock" msgstr "Lagerbestand" -#: part/templates/part/stock_count.html:9 +#: part/templates/part/stock_count.html:9 templates/InvenTree/low_stock.html:7 #, fuzzy #| msgid "Stock" msgid "Low Stock" @@ -1834,11 +2211,7 @@ msgstr "Stückliste" msgid "Used In" msgstr "Benutzt in" -#: part/templates/part/tabs.html:37 templates/navbar.html:13 -msgid "Suppliers" -msgstr "Zulieferer" - -#: part/templates/part/tabs.html:48 stock/templates/stock/tabs.html:5 +#: part/templates/part/tabs.html:53 stock/templates/stock/tabs.html:5 msgid "Tracking" msgstr "Tracking" @@ -1965,102 +2338,124 @@ msgstr "Teil auswählen" msgid "Specify quantity" msgstr "Anzahl angeben" -#: part/views.py:1366 +#: part/views.py:1364 msgid "Export Bill of Materials" msgstr "" -#: part/views.py:1404 +#: part/views.py:1402 #, fuzzy #| msgid "Confirm part creation" msgid "Confirm Part Deletion" msgstr "Erstellen des Teils bestätigen" -#: part/views.py:1411 +#: part/views.py:1409 msgid "Part was deleted" msgstr "" -#: part/views.py:1420 +#: part/views.py:1418 #, fuzzy #| msgid "Part packaging" msgid "Part Pricing" msgstr "Teile-Packaging" -#: part/views.py:1542 +#: part/views.py:1540 #, fuzzy #| msgid "Parameter Template" msgid "Create Part Parameter Template" msgstr "Parameter Vorlage" -#: part/views.py:1550 +#: part/views.py:1548 #, fuzzy #| msgid "Parameter Template" msgid "Edit Part Parameter Template" msgstr "Parameter Vorlage" -#: part/views.py:1557 +#: part/views.py:1555 #, fuzzy #| msgid "Parameter Template" msgid "Delete Part Parameter Template" msgstr "Parameter Vorlage" -#: part/views.py:1565 +#: part/views.py:1563 msgid "Create Part Parameter" msgstr "" -#: part/views.py:1615 +#: part/views.py:1613 #, fuzzy #| msgid "Edit attachment" msgid "Edit Part Parameter" msgstr "Anhang bearbeiten" -#: part/views.py:1629 +#: part/views.py:1627 #, fuzzy #| msgid "Delete attachment" msgid "Delete Part Parameter" msgstr "Anhang löschen" -#: part/views.py:1645 +#: part/views.py:1643 #, fuzzy #| msgid "Part category" msgid "Edit Part Category" msgstr "Teile-Kategorie" -#: part/views.py:1680 +#: part/views.py:1678 #, fuzzy #| msgid "Select part category" msgid "Delete Part Category" msgstr "Teilekategorie wählen" -#: part/views.py:1686 +#: part/views.py:1684 #, fuzzy #| msgid "Part category" msgid "Part category was deleted" msgstr "Teile-Kategorie" -#: part/views.py:1694 +#: part/views.py:1692 #, fuzzy #| msgid "Select part category" msgid "Create new part category" msgstr "Teilekategorie wählen" -#: part/views.py:1745 +#: part/views.py:1743 #, fuzzy #| msgid "Created new stock item" msgid "Create BOM item" msgstr "Neues Lagerobjekt erstellt" -#: part/views.py:1811 +#: part/views.py:1809 #, fuzzy #| msgid "Edit Stock Item" msgid "Edit BOM item" msgstr "Lagerobjekt bearbeiten" -#: part/views.py:1859 +#: part/views.py:1857 #, fuzzy #| msgid "Confirm build completion" msgid "Confim BOM item deletion" msgstr "Bau-Fertigstellung bestätigen" +#: plugins/barcode/inventree.py:70 +#, fuzzy +#| msgid "Part Notes" +msgid "Part does not exist" +msgstr "Teil-Bemerkungen" + +#: plugins/barcode/inventree.py:79 +#, fuzzy +#| msgid "Stock Location QR code" +msgid "StockLocation does not exist" +msgstr "QR-Code für diesen Standort" + +#: plugins/barcode/inventree.py:89 +#, fuzzy +#| msgid "Stock Item Notes" +msgid "StockItem does not exist" +msgstr "Lagerobjekt-Notizen" + +#: plugins/barcode/inventree.py:92 +msgid "No matching data" +msgstr "" + #: stock/forms.py:93 msgid "Include stock items in sub locations" msgstr "Lagerobjekte in untergeordneten Lagerorten einschließen" @@ -2077,7 +2472,7 @@ msgstr "Bewegung der Lagerobjekte bestätigen" msgid "Set the destination as the default location for selected parts" msgstr "Setze das Ziel als Standard-Ziel für ausgewählte Teile" -#: stock/models.py:205 +#: stock/models.py:210 #, python-brace-format msgid "" "A stock item with this serial number already exists for template part {part}" @@ -2085,116 +2480,116 @@ msgstr "" "Ein Teil mit dieser Seriennummer existiert bereits für die Teilevorlage " "{part}" -#: stock/models.py:210 +#: stock/models.py:215 msgid "A stock item with this serial number already exists" msgstr "Ein Teil mit dieser Seriennummer existiert bereits" -#: stock/models.py:229 +#: stock/models.py:234 #, python-brace-format msgid "Part type ('{pf}') must be {pe}" msgstr "Teile-Typ ('{pf}') muss {pe} sein" -#: stock/models.py:239 stock/models.py:248 +#: stock/models.py:244 stock/models.py:253 msgid "Quantity must be 1 for item with a serial number" msgstr "Anzahl muss für Objekte mit Seriennummer \"1\" sein" -#: stock/models.py:240 +#: stock/models.py:245 msgid "Serial number cannot be set if quantity greater than 1" msgstr "" "Seriennummer kann nicht gesetzt werden wenn die Anzahl größer als \"1\" ist" -#: stock/models.py:256 +#: stock/models.py:261 msgid "Stock item cannot be created for a template Part" msgstr "Lagerobjekt kann nicht für Vorlagen-Teile angelegt werden" -#: stock/models.py:265 +#: stock/models.py:270 msgid "Item cannot belong to itself" msgstr "Teil kann nicht zu sich selbst gehören" -#: stock/models.py:306 +#: stock/models.py:311 msgid "Base part" msgstr "Basis-Teil" -#: stock/models.py:314 +#: stock/models.py:319 msgid "Select a matching supplier part for this stock item" msgstr "Passenden Zulieferer für dieses Lagerobjekt auswählen" -#: stock/models.py:318 +#: stock/models.py:323 msgid "Where is this stock item located?" msgstr "Wo wird dieses Teil normalerweise gelagert?" -#: stock/models.py:322 +#: stock/models.py:327 msgid "Is this item installed in another item?" msgstr "Ist dieses Teil in einem anderen verbaut?" -#: stock/models.py:326 +#: stock/models.py:331 msgid "Item assigned to customer?" msgstr "Ist dieses Objekt einem Kunden zugeteilt?" -#: stock/models.py:329 +#: stock/models.py:334 msgid "Serial number for this item" msgstr "Seriennummer für dieses Teil" -#: stock/models.py:334 +#: stock/models.py:339 msgid "Batch code for this stock item" msgstr "Losnummer für dieses Lagerobjekt" -#: stock/models.py:343 +#: stock/models.py:348 msgid "Build for this stock item" msgstr "Bau für dieses Lagerobjekt" -#: stock/models.py:352 +#: stock/models.py:357 msgid "Purchase order for this stock item" msgstr "Bestellung für dieses Teil" -#: stock/models.py:363 +#: stock/models.py:374 msgid "Delete this Stock Item when stock is depleted" msgstr "Objekt löschen wenn Lagerbestand aufgebraucht" -#: stock/models.py:370 stock/templates/stock/item_notes.html:13 +#: stock/models.py:381 stock/templates/stock/item_notes.html:13 #: stock/templates/stock/item_notes.html:29 msgid "Stock Item Notes" msgstr "Lagerobjekt-Notizen" -#: stock/models.py:464 +#: stock/models.py:516 msgid "Quantity must be integer" msgstr "Anzahl muss eine Ganzzahl sein" -#: stock/models.py:470 +#: stock/models.py:522 #, python-brace-format msgid "Quantity must not exceed available stock quantity ({n})" msgstr "Anzahl darf nicht die verfügbare Anzahl überschreiten ({n})" -#: stock/models.py:473 stock/models.py:476 +#: stock/models.py:525 stock/models.py:528 msgid "Serial numbers must be a list of integers" msgstr "Seriennummern muss eine Liste von Ganzzahlen sein" -#: stock/models.py:479 +#: stock/models.py:531 msgid "Quantity does not match serial numbers" msgstr "Anzahl stimmt nicht mit den Seriennummern überein" -#: stock/models.py:489 +#: stock/models.py:541 msgid "Serial numbers already exist: " msgstr "Seriennummern existieren bereits:" -#: stock/models.py:511 +#: stock/models.py:563 msgid "Add serial number" msgstr "Seriennummer hinzufügen" -#: stock/models.py:514 +#: stock/models.py:566 #, python-brace-format msgid "Serialized {n} items" msgstr "{n} Teile serialisiert" -#: stock/models.py:814 +#: stock/models.py:866 msgid "Tracking entry title" msgstr "Name des Eintrags-Trackings" -#: stock/models.py:816 +#: stock/models.py:868 msgid "Entry notes" msgstr "Eintrags-Notizen" -#: stock/models.py:818 +#: stock/models.py:870 msgid "Link to external page for further information" msgstr "Link auf externe Seite für weitere Informationen" @@ -2202,11 +2597,25 @@ msgstr "Link auf externe Seite für weitere Informationen" msgid "Stock Tracking Information" msgstr "Informationen zum Lagerbestands-Tracking" -#: stock/templates/stock/item_base.html:11 -msgid "Stock Item Details" -msgstr "Lagerbestands-Details" - +#: stock/templates/stock/item_base.html:8 #: stock/templates/stock/item_base.html:56 +#: stock/templates/stock/stock_adjust.html:16 +msgid "Stock Item" +msgstr "Lagerobjekt" + +#: stock/templates/stock/item_base.html:20 +#, fuzzy +#| msgid "Stock Item to allocate to build" +msgid "This stock item is allocated to Sales Order" +msgstr "Lagerobjekt dem Bau zuweisen" + +#: stock/templates/stock/item_base.html:26 +#, fuzzy +#| msgid "Stock Item to allocate to build" +msgid "This stock item is allocated to Build" +msgstr "Lagerobjekt dem Bau zuweisen" + +#: stock/templates/stock/item_base.html:32 msgid "" "This stock item is serialized - it has a unique serial number and the " "quantity cannot be adjusted." @@ -2214,45 +2623,53 @@ msgstr "" "Dieses Lagerobjekt ist serialisiert. Es hat eine eindeutige Seriennummer und " "die Anzahl kann nicht angepasst werden." -#: stock/templates/stock/item_base.html:60 +#: stock/templates/stock/item_base.html:36 #, fuzzy #| msgid "Stock item cannot be created for a template Part" msgid "This stock item cannot be deleted as it has child items" msgstr "Lagerobjekt kann nicht für Vorlagen-Teile angelegt werden" -#: stock/templates/stock/item_base.html:64 +#: stock/templates/stock/item_base.html:40 msgid "" "This stock item will be automatically deleted when all stock is depleted." msgstr "" "Dieses Lagerobjekt wird automatisch gelöscht wenn der Lagerbestand " "aufgebraucht ist." -#: stock/templates/stock/item_base.html:69 +#: stock/templates/stock/item_base.html:45 msgid "This stock item was split from " msgstr "" -#: stock/templates/stock/item_base.html:89 +#: stock/templates/stock/item_base.html:105 +msgid "Stock Item Details" +msgstr "Lagerbestands-Details" + +#: stock/templates/stock/item_base.html:119 msgid "Belongs To" msgstr "Gehört zu" -#: stock/templates/stock/item_base.html:95 +#: stock/templates/stock/item_base.html:125 #: stock/templates/stock/stock_adjust.html:17 msgid "Location" msgstr "Standort" -#: stock/templates/stock/item_base.html:102 +#: stock/templates/stock/item_base.html:132 +msgid "Unique Identifier" +msgstr "" + +#: stock/templates/stock/item_base.html:139 msgid "Serial Number" msgstr "Seriennummer" -#: stock/templates/stock/item_base.html:161 +#: stock/templates/stock/item_base.html:198 msgid "Last Updated" msgstr "Zuletzt aktualisiert" -#: stock/templates/stock/item_base.html:166 +#: stock/templates/stock/item_base.html:203 msgid "Last Stocktake" msgstr "Letzte Inventur" -#: stock/templates/stock/item_base.html:170 +#: stock/templates/stock/item_base.html:207 msgid "No stocktake performed" msgstr "Keine Inventur ausgeführt" @@ -2297,12 +2714,7 @@ msgstr "Objekt-Details" msgid "Stock Locations" msgstr "Lagerobjekt-Standorte" -#: stock/templates/stock/stock_adjust.html:16 #: stock/templates/stock/stock_app_base.html:7 -msgid "Stock Item" -msgstr "Lagerobjekt" - -#: stock/templates/stock/stock_app_base.html:9 #, fuzzy #| msgid "Stock Locations" msgid "Stock Location" @@ -2470,6 +2882,12 @@ msgstr "" msgid "No results found" msgstr "Keine Seriennummern gefunden" +#: templates/InvenTree/starred_parts.html:7 +#, fuzzy +#| msgid "Required Parts" +msgid "Starred Parts" +msgstr "benötigte Teile" + #: templates/about.html:13 msgid "InvenTree Version Information" msgstr "InvenTree-Versionsinformationen" @@ -2485,48 +2903,64 @@ msgid "InvenTree Version" msgstr "InvenTree-Versionsinformationen" #: templates/about.html:30 +#, fuzzy +#| msgid "Version" +msgid "Django Version" +msgstr "Version" + +#: templates/about.html:34 msgid "Commit Hash" msgstr "Commit-Hash" -#: templates/about.html:34 +#: templates/about.html:38 msgid "Commit Date" msgstr "Commit-Datum" -#: templates/about.html:38 +#: templates/about.html:42 msgid "InvenTree Documentation" msgstr "InvenTree-Dokumentation" -#: templates/about.html:43 +#: templates/about.html:47 msgid "View Code on GitHub" msgstr "Code auf GitHub ansehen" -#: templates/about.html:47 +#: templates/about.html:51 msgid "Submit Bug Report" msgstr "" -#: templates/navbar.html:23 +#: templates/navbar.html:14 +msgid "Buy" +msgstr "" + +#: templates/navbar.html:22 +#, fuzzy +#| msgid "Sellable" +msgid "Sell" +msgstr "Verkaufbar" + +#: templates/navbar.html:36 msgid "Admin" msgstr "" -#: templates/navbar.html:26 +#: templates/navbar.html:39 #, fuzzy #| msgid "Settings value" msgid "Settings" msgstr "Einstellungs-Wert" -#: templates/navbar.html:27 +#: templates/navbar.html:40 msgid "Logout" msgstr "" -#: templates/navbar.html:29 +#: templates/navbar.html:42 msgid "Login" msgstr "" -#: templates/navbar.html:32 +#: templates/navbar.html:45 msgid "About InvenTree" msgstr "" -#: templates/navbar.html:33 +#: templates/navbar.html:46 #, fuzzy #| msgid "Status" msgid "Statistics" @@ -2606,54 +3040,74 @@ msgstr "" msgid "Stock status" msgstr "Objekt-Details" -#: templates/table_filters.html:53 +#: templates/table_filters.html:37 +#, fuzzy +#| msgid "Allocated" +msgid "Is allocated" +msgstr "Zugeordnet" + +#: templates/table_filters.html:38 +msgid "Item has been alloacted" +msgstr "" + +#: templates/table_filters.html:58 #, fuzzy #| msgid "Order Parts" msgid "Order status" msgstr "Teile bestellen" -#: templates/table_filters.html:64 +#: templates/table_filters.html:69 #, fuzzy #| msgid "Parts (Including subcategories)" msgid "Include subcategories" msgstr "Teile (inklusive Unter-Kategorien)" -#: templates/table_filters.html:65 +#: templates/table_filters.html:70 #, fuzzy #| msgid "Parts (Including subcategories)" msgid "Include parts in subcategories" msgstr "Teile (inklusive Unter-Kategorien)" -#: templates/table_filters.html:69 +#: templates/table_filters.html:74 msgid "Active" msgstr "" -#: templates/table_filters.html:70 +#: templates/table_filters.html:75 #, fuzzy #| msgid "Build to allocate parts" msgid "Show active parts" msgstr "Bau starten um Teile zuzuweisen" -#: templates/table_filters.html:74 +#: templates/table_filters.html:79 #, fuzzy #| msgid "Parameter Template" msgid "Template" msgstr "Parameter Vorlage" -#: templates/table_filters.html:78 +#: templates/table_filters.html:83 #, fuzzy #| msgid "Available" msgid "Stock available" msgstr "verfügbar" -#: templates/table_filters.html:82 +#: templates/table_filters.html:87 #, fuzzy #| msgid "Stock" msgid "Low stock" msgstr "Lagerbestand" +#: templates/table_filters.html:99 +msgid "Starred" +msgstr "" + +#: templates/table_filters.html:107 +#, fuzzy +#| msgid "Purchaseable" +msgid "Purchasable" +msgstr "Kaufbar" + +#~ msgid "Order Items" +#~ msgstr "Bestellungspositionen" + #~ msgid "URL" #~ msgstr "URL" - -#~ msgid "Version" -#~ msgstr "Version" diff --git a/InvenTree/locale/en/LC_MESSAGES/django.po b/InvenTree/locale/en/LC_MESSAGES/django.po index fcb353a3ca..ee9168f369 100644 --- a/InvenTree/locale/en/LC_MESSAGES/django.po +++ b/InvenTree/locale/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-04-11 15:00+0000\n" +"POT-Creation-Date: 2020-04-22 23:17+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -18,30 +18,54 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: InvenTree/helpers.py:259 order/models.py:164 order/models.py:215 +#: InvenTree/api.py:61 +msgid "No action specified" +msgstr "" + +#: InvenTree/api.py:75 +msgid "No matching action found" +msgstr "" + +#: InvenTree/api.py:106 +msgid "No barcode data provided" +msgstr "" + +#: InvenTree/api.py:121 +msgid "Barcode successfully decoded" +msgstr "" + +#: InvenTree/api.py:124 +msgid "Barcode plugin returned incorrect response" +msgstr "" + +#: InvenTree/api.py:134 +msgid "Unknown barcode format" +msgstr "" + +#: InvenTree/helpers.py:258 order/models.py:173 order/models.py:224 msgid "Invalid quantity provided" msgstr "" -#: InvenTree/helpers.py:262 +#: InvenTree/helpers.py:261 msgid "Empty serial number string" msgstr "" -#: InvenTree/helpers.py:283 InvenTree/helpers.py:300 +#: InvenTree/helpers.py:282 InvenTree/helpers.py:299 #, python-brace-format msgid "Duplicate serial: {n}" msgstr "" -#: InvenTree/helpers.py:287 InvenTree/helpers.py:290 InvenTree/helpers.py:293 -#: InvenTree/helpers.py:304 +#: InvenTree/helpers.py:286 InvenTree/helpers.py:289 InvenTree/helpers.py:292 +#: InvenTree/helpers.py:303 #, python-brace-format msgid "Invalid group: {g}" msgstr "" -#: InvenTree/helpers.py:310 +#: InvenTree/helpers.py:309 msgid "No serial numbers found" msgstr "" -#: InvenTree/helpers.py:314 +#: InvenTree/helpers.py:313 #, python-brace-format msgid "Number of unique serial number ({s}) must match quantity ({q})" msgstr "" @@ -70,47 +94,51 @@ msgstr "" msgid "Polish" msgstr "" -#: InvenTree/status_codes.py:86 InvenTree/status_codes.py:162 +#: InvenTree/status_codes.py:84 InvenTree/status_codes.py:172 msgid "Pending" msgstr "" -#: InvenTree/status_codes.py:87 +#: InvenTree/status_codes.py:85 msgid "Placed" msgstr "" -#: InvenTree/status_codes.py:88 InvenTree/status_codes.py:165 +#: InvenTree/status_codes.py:86 InvenTree/status_codes.py:175 msgid "Complete" msgstr "" -#: InvenTree/status_codes.py:89 InvenTree/status_codes.py:164 +#: InvenTree/status_codes.py:87 InvenTree/status_codes.py:174 msgid "Cancelled" msgstr "" -#: InvenTree/status_codes.py:90 InvenTree/status_codes.py:130 +#: InvenTree/status_codes.py:88 InvenTree/status_codes.py:135 msgid "Lost" msgstr "" -#: InvenTree/status_codes.py:91 +#: InvenTree/status_codes.py:89 InvenTree/status_codes.py:137 msgid "Returned" msgstr "" -#: InvenTree/status_codes.py:126 +#: InvenTree/status_codes.py:131 msgid "OK" msgstr "" -#: InvenTree/status_codes.py:127 +#: InvenTree/status_codes.py:132 msgid "Attention needed" msgstr "" -#: InvenTree/status_codes.py:128 +#: InvenTree/status_codes.py:133 msgid "Damaged" msgstr "" -#: InvenTree/status_codes.py:129 +#: InvenTree/status_codes.py:134 msgid "Destroyed" msgstr "" -#: InvenTree/status_codes.py:163 build/templates/build/allocate_edit.html:28 +#: InvenTree/status_codes.py:136 +msgid "Shipped" +msgstr "" + +#: InvenTree/status_codes.py:173 build/templates/build/allocate_edit.html:28 #: build/templates/build/allocate_view.html:21 #: part/templates/part/part_base.html:114 part/templates/part/tabs.html:21 msgid "Allocated" @@ -141,7 +169,7 @@ msgstr "" msgid "Overage must be an integer value or a percentage" msgstr "" -#: InvenTree/views.py:549 +#: InvenTree/views.py:536 msgid "Database Statistics" msgstr "" @@ -175,7 +203,7 @@ msgstr "" msgid "Number of parts to build" msgstr "" -#: build/models.py:82 templates/table_filters.html:42 +#: build/models.py:82 templates/table_filters.html:47 msgid "Build status" msgstr "" @@ -183,7 +211,7 @@ msgstr "" msgid "Batch code for this build output" msgstr "" -#: build/models.py:97 stock/models.py:331 +#: build/models.py:97 stock/models.py:336 msgid "Link to external URL" msgstr "" @@ -201,15 +229,27 @@ msgstr "" msgid "Allocated quantity ({n}) must not exceed available quantity ({q})" msgstr "" -#: build/models.py:409 +#: build/models.py:397 order/models.py:448 +msgid "StockItem is over-allocated" +msgstr "" + +#: build/models.py:400 order/models.py:451 +msgid "Allocation quantity must be greater than zero" +msgstr "" + +#: build/models.py:403 +msgid "Quantity must be 1 for serialized stock" +msgstr "" + +#: build/models.py:418 msgid "Build to allocate parts" msgstr "" -#: build/models.py:416 +#: build/models.py:425 msgid "Stock Item to allocate to build" msgstr "" -#: build/models.py:424 +#: build/models.py:433 msgid "Stock quantity to allocate to build" msgstr "" @@ -227,8 +267,7 @@ msgstr "" #: build/templates/build/allocate_edit.html:19 #: build/templates/build/allocate_view.html:17 -#: build/templates/build/detail.html:22 -#: company/templates/company/detail_part.html:65 +#: build/templates/build/detail.html:22 order/models.py:385 #: order/templates/order/order_wizard/select_parts.html:30 #: order/templates/order/purchase_order_detail.html:26 #: part/templates/part/part_app_base.html:7 @@ -254,12 +293,11 @@ msgid "Allocate" msgstr "" #: build/templates/build/allocate_view.html:10 -#: company/templates/company/detail_part.html:18 order/views.py:526 +#: company/templates/company/detail_part.html:18 order/views.py:671 msgid "Order Parts" msgstr "" #: build/templates/build/allocate_view.html:18 -#: company/templates/company/index.html:54 #: company/templates/company/supplier_part_base.html:50 #: company/templates/company/supplier_part_detail.html:27 #: order/templates/order/purchase_order_detail.html:27 @@ -272,45 +310,47 @@ msgstr "" msgid "On Order" msgstr "" -#: build/templates/build/build_base.html:27 part/templates/part/tabs.html:28 -#: stock/templates/stock/item_base.html:122 templates/navbar.html:12 +#: build/templates/build/build_base.html:8 +#: build/templates/build/build_base.html:21 part/templates/part/tabs.html:28 +#: stock/templates/stock/item_base.html:159 templates/navbar.html:12 msgid "Build" msgstr "" -#: build/templates/build/build_base.html:52 build/templates/build/detail.html:9 +#: build/templates/build/build_base.html:48 build/templates/build/detail.html:9 msgid "Build Details" msgstr "" -#: build/templates/build/build_base.html:56 +#: build/templates/build/build_base.html:52 msgid "Build Title" msgstr "" -#: build/templates/build/build_base.html:66 +#: build/templates/build/build_base.html:62 #: build/templates/build/detail.html:27 #: company/templates/company/supplier_part_pricing.html:27 #: order/templates/order/order_wizard/select_parts.html:32 #: order/templates/order/purchase_order_detail.html:30 -#: stock/templates/stock/item_base.html:108 +#: stock/templates/stock/item_base.html:20 +#: stock/templates/stock/item_base.html:26 +#: stock/templates/stock/item_base.html:145 #: stock/templates/stock/stock_adjust.html:18 msgid "Quantity" msgstr "" -#: build/templates/build/build_base.html:71 +#: build/templates/build/build_base.html:67 #: build/templates/build/detail.html:42 -#: order/templates/order/order_base.html:72 -#: stock/templates/stock/item_base.html:175 +#: stock/templates/stock/item_base.html:212 msgid "Status" msgstr "" -#: build/templates/build/build_base.html:76 +#: build/templates/build/build_base.html:72 msgid "BOM Price" msgstr "" -#: build/templates/build/build_base.html:81 +#: build/templates/build/build_base.html:77 msgid "BOM pricing is incomplete" msgstr "" -#: build/templates/build/build_base.html:84 +#: build/templates/build/build_base.html:80 msgid "No pricing information" msgstr "" @@ -335,20 +375,21 @@ msgid "Stock can be taken from any available location." msgstr "" #: build/templates/build/detail.html:48 -#: stock/templates/stock/item_base.html:115 +#: stock/templates/stock/item_base.html:152 msgid "Batch" msgstr "" #: build/templates/build/detail.html:55 -#: company/templates/company/supplier_part_base.html:47 +#: company/templates/company/supplier_part_base.html:57 #: company/templates/company/supplier_part_detail.html:24 #: part/templates/part/detail.html:67 part/templates/part/part_base.html:85 -#: stock/templates/stock/item_base.html:143 +#: stock/templates/stock/item_base.html:180 msgid "External Link" msgstr "" #: build/templates/build/detail.html:61 -#: order/templates/order/order_base.html:84 +#: order/templates/order/order_base.html:93 +#: order/templates/order/sales_order_base.html:82 msgid "Created" msgstr "" @@ -373,14 +414,16 @@ msgid "Build Notes" msgstr "" #: build/templates/build/notes.html:20 company/templates/company/notes.html:17 -#: order/templates/order/order_notes.html:21 part/templates/part/notes.html:20 -#: stock/templates/stock/item_notes.html:21 +#: order/templates/order/order_notes.html:21 +#: order/templates/order/sales_order_notes.html:26 +#: part/templates/part/notes.html:20 stock/templates/stock/item_notes.html:21 msgid "Save" msgstr "" #: build/templates/build/notes.html:33 company/templates/company/notes.html:30 -#: order/templates/order/order_notes.html:32 part/templates/part/notes.html:32 -#: stock/templates/stock/item_notes.html:32 +#: order/templates/order/order_notes.html:32 +#: order/templates/order/sales_order_notes.html:37 +#: part/templates/part/notes.html:32 stock/templates/stock/item_notes.html:32 msgid "Edit notes" msgstr "" @@ -393,9 +436,10 @@ msgstr "" msgid "Outputs" msgstr "" -#: build/templates/build/tabs.html:11 company/models.py:264 -#: company/templates/company/tabs.html:26 order/templates/order/tabs.html:15 -#: part/templates/part/tabs.html:58 stock/templates/stock/tabs.html:17 +#: build/templates/build/tabs.html:11 company/models.py:302 +#: company/templates/company/tabs.html:26 order/templates/order/po_tabs.html:15 +#: order/templates/order/so_tabs.html:15 part/templates/part/tabs.html:63 +#: stock/templates/stock/tabs.html:17 msgid "Notes" msgstr "" @@ -552,130 +596,138 @@ msgstr "" msgid "Delete Currency" msgstr "" -#: company/models.py:76 +#: company/models.py:83 msgid "Company name" msgstr "" -#: company/models.py:78 +#: company/models.py:85 msgid "Description of the company" msgstr "" -#: company/models.py:80 +#: company/models.py:87 msgid "Company website URL" msgstr "" -#: company/models.py:83 +#: company/models.py:90 msgid "Company address" msgstr "" -#: company/models.py:86 +#: company/models.py:93 msgid "Contact phone number" msgstr "" -#: company/models.py:88 +#: company/models.py:95 msgid "Contact email address" msgstr "" -#: company/models.py:91 +#: company/models.py:98 msgid "Point of contact" msgstr "" -#: company/models.py:93 +#: company/models.py:100 msgid "Link to external company information" msgstr "" -#: company/models.py:105 +#: company/models.py:112 msgid "Do you sell items to this company?" msgstr "" -#: company/models.py:107 +#: company/models.py:114 msgid "Do you purchase items from this company?" msgstr "" -#: company/models.py:245 +#: company/models.py:116 +msgid "Does this company manufacture parts?" +msgstr "" + +#: company/models.py:276 msgid "Select part" msgstr "" -#: company/models.py:251 +#: company/models.py:282 msgid "Select supplier" msgstr "" -#: company/models.py:254 +#: company/models.py:285 msgid "Supplier stock keeping unit" msgstr "" -#: company/models.py:256 company/templates/company/detail_part.html:96 -#: company/templates/company/supplier_part_base.html:53 -#: company/templates/company/supplier_part_detail.html:30 -msgid "Manufacturer" +#: company/models.py:292 +msgid "Select manufacturer" msgstr "" -#: company/models.py:258 +#: company/models.py:296 msgid "Manufacturer part number" msgstr "" -#: company/models.py:260 +#: company/models.py:298 msgid "URL for external supplier part link" msgstr "" -#: company/models.py:262 +#: company/models.py:300 msgid "Supplier part description" msgstr "" -#: company/models.py:266 +#: company/models.py:304 msgid "Minimum charge (e.g. stocking fee)" msgstr "" -#: company/models.py:268 +#: company/models.py:306 msgid "Part packaging" msgstr "" -#: company/templates/company/company_base.html:7 order/models.py:131 +#: company/templates/company/company_base.html:7 +#: company/templates/company/company_base.html:22 msgid "Company" msgstr "" -#: company/templates/company/company_base.html:50 -#: company/templates/company/index.html:59 -msgid "Website" -msgstr "" - -#: company/templates/company/company_base.html:57 -msgid "Address" -msgstr "" - -#: company/templates/company/company_base.html:64 -msgid "Phone" -msgstr "" - -#: company/templates/company/company_base.html:71 -msgid "Email" -msgstr "" - -#: company/templates/company/company_base.html:78 -msgid "Contact" -msgstr "" - +#: company/templates/company/company_base.html:42 #: company/templates/company/detail.html:8 msgid "Company Details" msgstr "" +#: company/templates/company/company_base.html:48 +msgid "Website" +msgstr "" + +#: company/templates/company/company_base.html:55 +msgid "Address" +msgstr "" + +#: company/templates/company/company_base.html:62 +msgid "Phone" +msgstr "" + +#: company/templates/company/company_base.html:69 +msgid "Email" +msgstr "" + +#: company/templates/company/company_base.html:76 +msgid "Contact" +msgstr "" + #: company/templates/company/detail.html:16 -#: stock/templates/stock/item_base.html:136 -msgid "Customer" +#: company/templates/company/supplier_part_base.html:73 +#: company/templates/company/supplier_part_detail.html:30 +msgid "Manufacturer" msgstr "" #: company/templates/company/detail.html:21 -#: company/templates/company/index.html:46 -#: company/templates/company/supplier_part_base.html:44 -#: company/templates/company/supplier_part_detail.html:21 -#: order/templates/order/order_base.html:67 +#: company/templates/company/supplier_part_base.html:63 +#: company/templates/company/supplier_part_detail.html:21 order/models.py:138 +#: order/templates/order/order_base.html:74 #: order/templates/order/order_wizard/select_pos.html:30 -#: stock/templates/stock/item_base.html:150 +#: stock/templates/stock/item_base.html:187 msgid "Supplier" msgstr "" +#: company/templates/company/detail.html:26 order/models.py:275 +#: order/templates/order/sales_order_base.html:63 +#: stock/templates/stock/item_base.html:173 +msgid "Customer" +msgstr "" + #: company/templates/company/detail_part.html:8 -#: company/templates/company/tabs.html:9 msgid "Supplier Parts" msgstr "" @@ -692,53 +744,45 @@ msgstr "" msgid "Delete Parts" msgstr "" -#: company/templates/company/detail_part.html:88 -#: company/templates/company/supplier_part_base.html:45 -#: company/templates/company/supplier_part_detail.html:22 -msgid "SKU" +#: company/templates/company/detail_part.html:43 +#: part/templates/part/stock.html:75 +msgid "New Part" msgstr "" -#: company/templates/company/detail_part.html:105 -msgid "Link" +#: company/templates/company/detail_part.html:44 +msgid "Create new Part" msgstr "" -#: company/templates/company/detail_purchase_orders.html:8 -#: company/templates/company/tabs.html:15 part/templates/part/tabs.html:43 -msgid "Purchase Orders" +#: company/templates/company/detail_part.html:49 company/views.py:52 +msgid "New Supplier" msgstr "" -#: company/templates/company/detail_purchase_orders.html:13 -msgid "New Purchase Order" +#: company/templates/company/detail_part.html:50 company/views.py:184 +msgid "Create new Supplier" +msgstr "" + +#: company/templates/company/detail_part.html:55 company/views.py:58 +msgid "New Manufacturer" +msgstr "" + +#: company/templates/company/detail_part.html:56 company/views.py:187 +msgid "Create new Manufacturer" msgstr "" #: company/templates/company/detail_stock.html:9 msgid "Supplier Stock" msgstr "" -#: company/templates/company/detail_stock.html:33 +#: company/templates/company/detail_stock.html:34 #: company/templates/company/supplier_part_stock.html:38 #: part/templates/part/stock.html:53 templates/stock_table.html:5 msgid "Export" msgstr "" #: company/templates/company/index.html:7 -#: company/templates/company/index.html:12 msgid "Supplier List" msgstr "" -#: company/templates/company/index.html:17 -msgid "New Supplier" -msgstr "" - -#: company/templates/company/index.html:41 -msgid "ID" -msgstr "" - -#: company/templates/company/index.html:69 part/templates/part/category.html:83 -#: templates/navbar.html:10 templates/stats.html:8 templates/stats.html:17 -msgid "Parts" -msgstr "" - #: company/templates/company/notes.html:10 #: company/templates/company/notes.html:27 msgid "Company Notes" @@ -748,28 +792,71 @@ msgstr "" msgid "Are you sure you want to delete the following Supplier Parts?" msgstr "" +#: company/templates/company/purchase_orders.html:9 +#: company/templates/company/tabs.html:17 +#: order/templates/order/purchase_orders.html:7 +#: order/templates/order/purchase_orders.html:12 +#: part/templates/part/orders.html:9 part/templates/part/tabs.html:43 +#: templates/navbar.html:18 +msgid "Purchase Orders" +msgstr "" + +#: company/templates/company/purchase_orders.html:14 +#: order/templates/order/purchase_orders.html:17 +msgid "Create new purchase order" +msgstr "" + +#: company/templates/company/purchase_orders.html:14 +#: order/templates/order/purchase_orders.html:17 +msgid "New Purchase Order" +msgstr "" + +#: company/templates/company/sales_orders.html:9 +#: company/templates/company/tabs.html:22 +#: order/templates/order/sales_orders.html:7 +#: order/templates/order/sales_orders.html:12 +#: part/templates/part/sales_orders.html:9 part/templates/part/tabs.html:48 +#: templates/navbar.html:25 +msgid "Sales Orders" +msgstr "" + +#: company/templates/company/sales_orders.html:14 +#: order/templates/order/sales_orders.html:17 +msgid "Create new sales order" +msgstr "" + +#: company/templates/company/sales_orders.html:14 +#: order/templates/order/sales_orders.html:17 +msgid "New Sales Order" +msgstr "" + #: company/templates/company/supplier_part_base.html:6 -#: company/templates/company/supplier_part_base.html:13 -#: stock/templates/stock/item_base.html:155 +#: company/templates/company/supplier_part_base.html:19 +#: stock/templates/stock/item_base.html:192 msgid "Supplier Part" msgstr "" -#: company/templates/company/supplier_part_base.html:34 +#: company/templates/company/supplier_part_base.html:35 #: company/templates/company/supplier_part_detail.html:11 msgid "Supplier Part Details" msgstr "" -#: company/templates/company/supplier_part_base.html:37 +#: company/templates/company/supplier_part_base.html:40 #: company/templates/company/supplier_part_detail.html:14 msgid "Internal Part" msgstr "" -#: company/templates/company/supplier_part_base.html:54 +#: company/templates/company/supplier_part_base.html:67 +#: company/templates/company/supplier_part_detail.html:22 +msgid "SKU" +msgstr "" + +#: company/templates/company/supplier_part_base.html:77 #: company/templates/company/supplier_part_detail.html:31 msgid "MPN" msgstr "" -#: company/templates/company/supplier_part_base.html:57 +#: company/templates/company/supplier_part_base.html:84 #: company/templates/company/supplier_part_detail.html:34 #: order/templates/order/purchase_order_detail.html:34 msgid "Note" @@ -832,154 +919,246 @@ msgid "Stock" msgstr "" #: company/templates/company/supplier_part_tabs.html:11 -#: templates/navbar.html:14 msgid "Orders" msgstr "" -#: company/templates/company/tabs.html:21 -msgid "Sales Orders" +#: company/templates/company/tabs.html:9 part/templates/part/category.html:83 +#: templates/navbar.html:10 templates/stats.html:8 templates/stats.html:17 +msgid "Parts" msgstr "" -#: company/views.py:99 +#: company/views.py:51 part/templates/part/tabs.html:37 +#: templates/navbar.html:16 +msgid "Suppliers" +msgstr "" + +#: company/views.py:57 templates/navbar.html:17 +msgid "Manufacturers" +msgstr "" + +#: company/views.py:63 templates/navbar.html:24 +msgid "Customers" +msgstr "" + +#: company/views.py:64 +msgid "New Customer" +msgstr "" + +#: company/views.py:71 +msgid "Companies" +msgstr "" + +#: company/views.py:72 +msgid "New Company" +msgstr "" + +#: company/views.py:149 msgid "Update Company Image" msgstr "" -#: company/views.py:104 +#: company/views.py:154 msgid "Updated company image" msgstr "" -#: company/views.py:114 +#: company/views.py:164 msgid "Edit Company" msgstr "" -#: company/views.py:118 +#: company/views.py:168 msgid "Edited company information" msgstr "" -#: company/views.py:128 +#: company/views.py:190 +msgid "Create new Customer" +msgstr "" + +#: company/views.py:192 msgid "Create new Company" msgstr "" -#: company/views.py:132 +#: company/views.py:219 msgid "Created new company" msgstr "" -#: company/views.py:142 +#: company/views.py:229 msgid "Delete Company" msgstr "" -#: company/views.py:147 +#: company/views.py:234 msgid "Company was deleted" msgstr "" -#: company/views.py:172 +#: company/views.py:259 msgid "Edit Supplier Part" msgstr "" -#: company/views.py:181 part/templates/part/stock.html:82 +#: company/views.py:268 part/templates/part/stock.html:82 msgid "Create new Supplier Part" msgstr "" -#: company/views.py:238 +#: company/views.py:328 msgid "Delete Supplier Part" msgstr "" -#: company/views.py:308 +#: company/views.py:398 msgid "Add Price Break" msgstr "" -#: company/views.py:350 +#: company/views.py:440 msgid "Edit Price Break" msgstr "" -#: company/views.py:365 +#: company/views.py:455 msgid "Delete Price Break" msgstr "" -#: order/forms.py:22 +#: order/forms.py:24 msgid "Place order" msgstr "" -#: order/forms.py:33 +#: order/forms.py:35 msgid "Mark order as complete" msgstr "" -#: order/forms.py:44 +#: order/forms.py:46 msgid "Cancel order" msgstr "" -#: order/forms.py:55 +#: order/forms.py:57 msgid "Receive parts to this location" msgstr "" -#: order/models.py:68 +#: order/models.py:71 msgid "Order reference" msgstr "" -#: order/models.py:70 +#: order/models.py:73 msgid "Order description" msgstr "" -#: order/models.py:72 +#: order/models.py:75 msgid "Link to external page" msgstr "" -#: order/models.py:89 +#: order/models.py:92 msgid "Order notes" msgstr "" -#: order/models.py:162 order/models.py:213 part/views.py:1119 -#: stock/models.py:467 +#: order/models.py:141 +msgid "Supplier order reference code" +msgstr "" + +#: order/models.py:171 order/models.py:222 part/views.py:1119 +#: stock/models.py:519 msgid "Quantity must be greater than zero" msgstr "" -#: order/models.py:167 +#: order/models.py:176 msgid "Part supplier must match PO supplier" msgstr "" -#: order/models.py:208 +#: order/models.py:217 msgid "Lines can only be received against an order marked as 'Placed'" msgstr "" -#: order/models.py:268 +#: order/models.py:278 +msgid "Customer order reference code" +msgstr "" + +#: order/models.py:324 msgid "Item quantity" msgstr "" -#: order/models.py:270 +#: order/models.py:326 msgid "Line item reference" msgstr "" -#: order/models.py:272 +#: order/models.py:328 msgid "Line item notes" msgstr "" -#: order/models.py:298 stock/templates/stock/item_base.html:129 +#: order/models.py:354 order/templates/order/order_base.html:9 +#: order/templates/order/order_base.html:23 +#: stock/templates/stock/item_base.html:166 msgid "Purchase Order" msgstr "" -#: order/models.py:307 +#: order/models.py:363 msgid "Supplier part" msgstr "" -#: order/models.py:310 +#: order/models.py:366 msgid "Number of items received" msgstr "" -#: order/templates/order/order_base.html:62 +#: order/models.py:383 order/templates/order/sales_order_base.html:9 +#: order/templates/order/sales_order_base.html:31 +#: order/templates/order/sales_order_notes.html:10 +msgid "Sales Order" +msgstr "" + +#: order/models.py:440 +msgid "Cannot allocate stock item to a line with a different part" +msgstr "" + +#: order/models.py:442 +msgid "Cannot allocate stock to a line without a part" +msgstr "" + +#: order/models.py:445 +msgid "Allocation quantity cannot exceed stock quantity" +msgstr "" + +#: order/models.py:454 +msgid "Quantity must be 1 for serialized stock item" +msgstr "" + +#: order/models.py:466 +msgid "Select stock item to allocate" +msgstr "" + +#: order/models.py:469 +msgid "Enter stock allocation quantity" +msgstr "" + +#: order/templates/order/delete_attachment.html:5 +#: part/templates/part/attachment_delete.html:5 +msgid "Are you sure you want to delete this attachment?" +msgstr "" + +#: order/templates/order/order_base.html:59 msgid "Purchase Order Details" msgstr "" -#: order/templates/order/order_base.html:90 +#: order/templates/order/order_base.html:64 +#: order/templates/order/sales_order_base.html:53 +msgid "Order Reference" +msgstr "" + +#: order/templates/order/order_base.html:69 +#: order/templates/order/sales_order_base.html:58 +msgid "Order Status" +msgstr "" + +#: order/templates/order/order_base.html:80 +msgid "Supplier Reference" +msgstr "" + +#: order/templates/order/order_base.html:99 +#: order/templates/order/sales_order_base.html:88 msgid "Issued" msgstr "" -#: order/templates/order/order_base.html:97 +#: order/templates/order/order_base.html:106 #: order/templates/order/purchase_order_detail.html:32 +#: order/templates/order/sales_order_base.html:95 msgid "Received" msgstr "" #: order/templates/order/order_notes.html:13 #: order/templates/order/order_notes.html:29 +#: order/templates/order/sales_order_notes.html:18 +#: order/templates/order/sales_order_notes.html:34 msgid "Order Notes" msgstr "" @@ -1012,7 +1191,7 @@ msgid "Select existing purchase orders, or create new orders." msgstr "" #: order/templates/order/order_wizard/select_pos.html:31 -#: order/templates/order/tabs.html:5 +#: order/templates/order/po_tabs.html:5 order/templates/order/so_tabs.html:5 msgid "Items" msgstr "" @@ -1029,41 +1208,48 @@ msgid "Purchase Order Attachments" msgstr "" #: order/templates/order/po_attachments.html:17 +#: order/templates/order/so_attachments.html:17 #: part/templates/part/attachments.html:14 msgid "Add Attachment" msgstr "" #: order/templates/order/po_attachments.html:24 +#: order/templates/order/so_attachments.html:24 #: part/templates/part/attachments.html:22 msgid "File" msgstr "" #: order/templates/order/po_attachments.html:25 +#: order/templates/order/so_attachments.html:25 #: part/templates/part/attachments.html:23 msgid "Comment" msgstr "" #: order/templates/order/po_attachments.html:36 +#: order/templates/order/so_attachments.html:36 #: part/templates/part/attachments.html:34 part/views.py:119 msgid "Edit attachment" msgstr "" #: order/templates/order/po_attachments.html:39 +#: order/templates/order/so_attachments.html:39 #: part/templates/part/attachments.html:37 msgid "Delete attachment" msgstr "" -#: order/templates/order/po_delete.html:5 -#: part/templates/part/attachment_delete.html:5 -msgid "Are you sure you want to delete this attachment?" +#: order/templates/order/po_tabs.html:8 order/templates/order/so_tabs.html:8 +#: part/templates/part/tabs.html:60 +msgid "Attachments" msgstr "" -#: order/templates/order/purchase_order_detail.html:16 order/views.py:825 +#: order/templates/order/purchase_order_detail.html:16 +#: order/templates/order/sales_order_detail.html:17 order/views.py:970 +#: order/views.py:1084 msgid "Add Line Item" msgstr "" #: order/templates/order/purchase_order_detail.html:20 -msgid "Order Items" +msgid "Purchase Order Items" msgstr "" #: order/templates/order/purchase_order_detail.html:25 @@ -1078,110 +1264,182 @@ msgstr "" msgid "Reference" msgstr "" -#: order/templates/order/tabs.html:8 part/templates/part/tabs.html:55 -msgid "Attachments" +#: order/templates/order/sales_order_base.html:15 +msgid "This SalesOrder has not been fully allocated" msgstr "" -#: order/views.py:80 +#: order/templates/order/sales_order_base.html:40 +msgid "Packing List" +msgstr "" + +#: order/templates/order/sales_order_base.html:48 +msgid "Sales Order Details" +msgstr "" + +#: order/templates/order/sales_order_base.html:69 +msgid "Customer Reference" +msgstr "" + +#: order/templates/order/sales_order_detail.html:14 +msgid "Sales Order Items" +msgstr "" + +#: order/templates/order/sales_order_detail.html:90 +msgid "Edit stock allocation" +msgstr "" + +#: order/templates/order/sales_order_detail.html:91 +msgid "Delete stock allocation" +msgstr "" + +#: order/templates/order/sales_order_detail.html:178 +msgid "Buy parts" +msgstr "" + +#: order/templates/order/sales_order_detail.html:182 +msgid "Build parts" +msgstr "" + +#: order/templates/order/sales_order_detail.html:185 +msgid "Allocate parts" +msgstr "" + +#: order/templates/order/sales_order_detail.html:189 +msgid "Edit line item" +msgstr "" + +#: order/templates/order/sales_order_detail.html:190 +msgid "Delete line item " +msgstr "" + +#: order/templates/order/so_attachments.html:11 +msgid "Sales Order Attachments" +msgstr "" + +#: order/views.py:97 msgid "Add Purchase Order Attachment" msgstr "" -#: order/views.py:85 part/views.py:80 +#: order/views.py:102 order/views.py:142 part/views.py:80 msgid "Added attachment" msgstr "" -#: order/views.py:121 +#: order/views.py:138 +msgid "Add Sales Order Attachment" +msgstr "" + +#: order/views.py:166 order/views.py:187 msgid "Edit Attachment" msgstr "" -#: order/views.py:125 +#: order/views.py:170 order/views.py:191 msgid "Attachment updated" msgstr "" -#: order/views.py:141 +#: order/views.py:206 order/views.py:220 msgid "Delete Attachment" msgstr "" -#: order/views.py:147 +#: order/views.py:212 order/views.py:226 msgid "Deleted attachment" msgstr "" -#: order/views.py:177 +#: order/views.py:277 msgid "Create Purchase Order" msgstr "" -#: order/views.py:207 +#: order/views.py:307 +msgid "Create Sales Order" +msgstr "" + +#: order/views.py:336 msgid "Edit Purchase Order" msgstr "" -#: order/views.py:227 +#: order/views.py:356 +msgid "Edit Sales Order" +msgstr "" + +#: order/views.py:372 msgid "Cancel Order" msgstr "" -#: order/views.py:242 +#: order/views.py:387 msgid "Confirm order cancellation" msgstr "" -#: order/views.py:260 +#: order/views.py:405 msgid "Issue Order" msgstr "" -#: order/views.py:275 +#: order/views.py:420 msgid "Confirm order placement" msgstr "" -#: order/views.py:296 +#: order/views.py:441 msgid "Complete Order" msgstr "" -#: order/views.py:362 +#: order/views.py:507 msgid "Receive Parts" msgstr "" -#: order/views.py:429 +#: order/views.py:574 msgid "Items received" msgstr "" -#: order/views.py:443 +#: order/views.py:588 msgid "No destination set" msgstr "" -#: order/views.py:474 +#: order/views.py:619 msgid "Error converting quantity to number" msgstr "" -#: order/views.py:480 +#: order/views.py:625 msgid "Receive quantity less than zero" msgstr "" -#: order/views.py:486 +#: order/views.py:631 msgid "No lines specified" msgstr "" -#: order/views.py:845 +#: order/views.py:990 msgid "Invalid Purchase Order" msgstr "" -#: order/views.py:853 +#: order/views.py:998 msgid "Supplier must match for Part and Order" msgstr "" -#: order/views.py:858 +#: order/views.py:1003 msgid "Invalid SupplierPart selection" msgstr "" -#: order/views.py:940 +#: order/views.py:1123 order/views.py:1141 msgid "Edit Line Item" msgstr "" -#: order/views.py:956 +#: order/views.py:1157 order/views.py:1169 msgid "Delete Line Item" msgstr "" -#: order/views.py:961 +#: order/views.py:1162 order/views.py:1174 msgid "Deleted line item" msgstr "" +#: order/views.py:1183 +msgid "Allocate Stock to Order" +msgstr "" + +#: order/views.py:1252 +msgid "Edit Allocation Quantity" +msgstr "" + +#: order/views.py:1267 +msgid "Remove allocation" +msgstr "" + #: part/bom.py:140 #, python-brace-format msgid "Unsupported file format: {f}" @@ -1347,63 +1605,63 @@ msgstr "" msgid "Stored BOM checksum" msgstr "" -#: part/models.py:1049 +#: part/models.py:1065 msgid "Parameter template name must be unique" msgstr "" -#: part/models.py:1054 +#: part/models.py:1070 msgid "Parameter Name" msgstr "" -#: part/models.py:1056 +#: part/models.py:1072 msgid "Parameter Units" msgstr "" -#: part/models.py:1082 +#: part/models.py:1098 msgid "Parent Part" msgstr "" -#: part/models.py:1084 +#: part/models.py:1100 msgid "Parameter Template" msgstr "" -#: part/models.py:1086 +#: part/models.py:1102 msgid "Parameter Value" msgstr "" -#: part/models.py:1110 +#: part/models.py:1126 msgid "Select parent part" msgstr "" -#: part/models.py:1119 +#: part/models.py:1135 msgid "Select part to be used in BOM" msgstr "" -#: part/models.py:1126 +#: part/models.py:1142 msgid "BOM quantity for this BOM item" msgstr "" -#: part/models.py:1129 +#: part/models.py:1145 msgid "Estimated build wastage quantity (absolute or percentage)" msgstr "" -#: part/models.py:1132 +#: part/models.py:1148 msgid "BOM item reference" msgstr "" -#: part/models.py:1135 +#: part/models.py:1151 msgid "BOM item notes" msgstr "" -#: part/models.py:1137 +#: part/models.py:1153 msgid "BOM line checksum" msgstr "" -#: part/models.py:1200 +#: part/models.py:1216 msgid "Part cannot be added to its own Bill of Materials" msgstr "" -#: part/models.py:1207 +#: part/models.py:1223 #, python-brace-format msgid "Part '{p1}' is used in BOM for '{p2}' (recursive)" msgstr "" @@ -1505,7 +1763,7 @@ msgstr "" msgid "Part is not a virtual part" msgstr "" -#: part/templates/part/detail.html:132 templates/table_filters.html:86 +#: part/templates/part/detail.html:132 templates/table_filters.html:91 msgid "Assembly" msgstr "" @@ -1517,7 +1775,7 @@ msgstr "" msgid "Part cannot be assembled from other parts" msgstr "" -#: part/templates/part/detail.html:141 templates/table_filters.html:90 +#: part/templates/part/detail.html:141 templates/table_filters.html:95 msgid "Component" msgstr "" @@ -1549,15 +1807,15 @@ msgstr "" msgid "Part can be purchased from external suppliers" msgstr "" -#: part/templates/part/detail.html:169 -msgid "Sellable" +#: part/templates/part/detail.html:168 templates/table_filters.html:103 +msgid "Salable" msgstr "" -#: part/templates/part/detail.html:172 +#: part/templates/part/detail.html:171 msgid "Part can be sold to customers" msgstr "" -#: part/templates/part/detail.html:174 +#: part/templates/part/detail.html:173 msgid "Part cannot be sold to customers" msgstr "" @@ -1565,6 +1823,14 @@ msgstr "" msgid "Part Notes" msgstr "" +#: part/templates/part/orders.html:14 +msgid "Order part" +msgstr "" + +#: part/templates/part/orders.html:14 +msgid "Order Part" +msgstr "" + #: part/templates/part/part_app_base.html:9 msgid "Part Category" msgstr "" @@ -1625,8 +1891,12 @@ msgstr "" msgid "Upload new image" msgstr "" -#: part/templates/part/stock.html:75 -msgid "New Part" +#: part/templates/part/sales_orders.html:14 +msgid "New sales order" +msgstr "" + +#: part/templates/part/sales_orders.html:14 +msgid "New Order" msgstr "" #: part/templates/part/stock.html:76 @@ -1637,7 +1907,7 @@ msgstr "" msgid "No Stock" msgstr "" -#: part/templates/part/stock_count.html:9 +#: part/templates/part/stock_count.html:9 templates/InvenTree/low_stock.html:7 msgid "Low Stock" msgstr "" @@ -1657,11 +1927,7 @@ msgstr "" msgid "Used In" msgstr "" -#: part/templates/part/tabs.html:37 templates/navbar.html:13 -msgid "Suppliers" -msgstr "" - -#: part/templates/part/tabs.html:48 stock/templates/stock/tabs.html:5 +#: part/templates/part/tabs.html:53 stock/templates/stock/tabs.html:5 msgid "Tracking" msgstr "" @@ -1762,74 +2028,90 @@ msgstr "" msgid "Specify quantity" msgstr "" -#: part/views.py:1366 +#: part/views.py:1364 msgid "Export Bill of Materials" msgstr "" -#: part/views.py:1404 +#: part/views.py:1402 msgid "Confirm Part Deletion" msgstr "" -#: part/views.py:1411 +#: part/views.py:1409 msgid "Part was deleted" msgstr "" -#: part/views.py:1420 +#: part/views.py:1418 msgid "Part Pricing" msgstr "" -#: part/views.py:1542 +#: part/views.py:1540 msgid "Create Part Parameter Template" msgstr "" -#: part/views.py:1550 +#: part/views.py:1548 msgid "Edit Part Parameter Template" msgstr "" -#: part/views.py:1557 +#: part/views.py:1555 msgid "Delete Part Parameter Template" msgstr "" -#: part/views.py:1565 +#: part/views.py:1563 msgid "Create Part Parameter" msgstr "" -#: part/views.py:1615 +#: part/views.py:1613 msgid "Edit Part Parameter" msgstr "" -#: part/views.py:1629 +#: part/views.py:1627 msgid "Delete Part Parameter" msgstr "" -#: part/views.py:1645 +#: part/views.py:1643 msgid "Edit Part Category" msgstr "" -#: part/views.py:1680 +#: part/views.py:1678 msgid "Delete Part Category" msgstr "" -#: part/views.py:1686 +#: part/views.py:1684 msgid "Part category was deleted" msgstr "" -#: part/views.py:1694 +#: part/views.py:1692 msgid "Create new part category" msgstr "" -#: part/views.py:1745 +#: part/views.py:1743 msgid "Create BOM item" msgstr "" -#: part/views.py:1811 +#: part/views.py:1809 msgid "Edit BOM item" msgstr "" -#: part/views.py:1859 +#: part/views.py:1857 msgid "Confim BOM item deletion" msgstr "" +#: plugins/barcode/inventree.py:70 +msgid "Part does not exist" +msgstr "" + +#: plugins/barcode/inventree.py:79 +msgid "StockLocation does not exist" +msgstr "" + +#: plugins/barcode/inventree.py:89 +msgid "StockItem does not exist" +msgstr "" + +#: plugins/barcode/inventree.py:92 +msgid "No matching data" +msgstr "" + #: stock/forms.py:93 msgid "Include stock items in sub locations" msgstr "" @@ -1846,121 +2128,121 @@ msgstr "" msgid "Set the destination as the default location for selected parts" msgstr "" -#: stock/models.py:205 +#: stock/models.py:210 #, python-brace-format msgid "" "A stock item with this serial number already exists for template part {part}" msgstr "" -#: stock/models.py:210 +#: stock/models.py:215 msgid "A stock item with this serial number already exists" msgstr "" -#: stock/models.py:229 +#: stock/models.py:234 #, python-brace-format msgid "Part type ('{pf}') must be {pe}" msgstr "" -#: stock/models.py:239 stock/models.py:248 +#: stock/models.py:244 stock/models.py:253 msgid "Quantity must be 1 for item with a serial number" msgstr "" -#: stock/models.py:240 +#: stock/models.py:245 msgid "Serial number cannot be set if quantity greater than 1" msgstr "" -#: stock/models.py:256 +#: stock/models.py:261 msgid "Stock item cannot be created for a template Part" msgstr "" -#: stock/models.py:265 +#: stock/models.py:270 msgid "Item cannot belong to itself" msgstr "" -#: stock/models.py:306 +#: stock/models.py:311 msgid "Base part" msgstr "" -#: stock/models.py:314 +#: stock/models.py:319 msgid "Select a matching supplier part for this stock item" msgstr "" -#: stock/models.py:318 +#: stock/models.py:323 msgid "Where is this stock item located?" msgstr "" -#: stock/models.py:322 +#: stock/models.py:327 msgid "Is this item installed in another item?" msgstr "" -#: stock/models.py:326 +#: stock/models.py:331 msgid "Item assigned to customer?" msgstr "" -#: stock/models.py:329 +#: stock/models.py:334 msgid "Serial number for this item" msgstr "" -#: stock/models.py:334 +#: stock/models.py:339 msgid "Batch code for this stock item" msgstr "" -#: stock/models.py:343 +#: stock/models.py:348 msgid "Build for this stock item" msgstr "" -#: stock/models.py:352 +#: stock/models.py:357 msgid "Purchase order for this stock item" msgstr "" -#: stock/models.py:363 +#: stock/models.py:374 msgid "Delete this Stock Item when stock is depleted" msgstr "" -#: stock/models.py:370 stock/templates/stock/item_notes.html:13 +#: stock/models.py:381 stock/templates/stock/item_notes.html:13 #: stock/templates/stock/item_notes.html:29 msgid "Stock Item Notes" msgstr "" -#: stock/models.py:464 +#: stock/models.py:516 msgid "Quantity must be integer" msgstr "" -#: stock/models.py:470 +#: stock/models.py:522 #, python-brace-format msgid "Quantity must not exceed available stock quantity ({n})" msgstr "" -#: stock/models.py:473 stock/models.py:476 +#: stock/models.py:525 stock/models.py:528 msgid "Serial numbers must be a list of integers" msgstr "" -#: stock/models.py:479 +#: stock/models.py:531 msgid "Quantity does not match serial numbers" msgstr "" -#: stock/models.py:489 +#: stock/models.py:541 msgid "Serial numbers already exist: " msgstr "" -#: stock/models.py:511 +#: stock/models.py:563 msgid "Add serial number" msgstr "" -#: stock/models.py:514 +#: stock/models.py:566 #, python-brace-format msgid "Serialized {n} items" msgstr "" -#: stock/models.py:814 +#: stock/models.py:866 msgid "Tracking entry title" msgstr "" -#: stock/models.py:816 +#: stock/models.py:868 msgid "Entry notes" msgstr "" -#: stock/models.py:818 +#: stock/models.py:870 msgid "Link to external page for further information" msgstr "" @@ -1968,51 +2250,69 @@ msgstr "" msgid "Stock Tracking Information" msgstr "" -#: stock/templates/stock/item_base.html:11 -msgid "Stock Item Details" +#: stock/templates/stock/item_base.html:8 +#: stock/templates/stock/item_base.html:56 +#: stock/templates/stock/stock_adjust.html:16 +msgid "Stock Item" msgstr "" -#: stock/templates/stock/item_base.html:56 +#: stock/templates/stock/item_base.html:20 +msgid "This stock item is allocated to Sales Order" +msgstr "" + +#: stock/templates/stock/item_base.html:26 +msgid "This stock item is allocated to Build" +msgstr "" + +#: stock/templates/stock/item_base.html:32 msgid "" "This stock item is serialized - it has a unique serial number and the " "quantity cannot be adjusted." msgstr "" -#: stock/templates/stock/item_base.html:60 +#: stock/templates/stock/item_base.html:36 msgid "This stock item cannot be deleted as it has child items" msgstr "" -#: stock/templates/stock/item_base.html:64 +#: stock/templates/stock/item_base.html:40 msgid "" "This stock item will be automatically deleted when all stock is depleted." msgstr "" -#: stock/templates/stock/item_base.html:69 +#: stock/templates/stock/item_base.html:45 msgid "This stock item was split from " msgstr "" -#: stock/templates/stock/item_base.html:89 +#: stock/templates/stock/item_base.html:105 +msgid "Stock Item Details" +msgstr "" + +#: stock/templates/stock/item_base.html:119 msgid "Belongs To" msgstr "" -#: stock/templates/stock/item_base.html:95 +#: stock/templates/stock/item_base.html:125 #: stock/templates/stock/stock_adjust.html:17 msgid "Location" msgstr "" -#: stock/templates/stock/item_base.html:102 +#: stock/templates/stock/item_base.html:132 +msgid "Unique Identifier" +msgstr "" + +#: stock/templates/stock/item_base.html:139 msgid "Serial Number" msgstr "" -#: stock/templates/stock/item_base.html:161 +#: stock/templates/stock/item_base.html:198 msgid "Last Updated" msgstr "" -#: stock/templates/stock/item_base.html:166 +#: stock/templates/stock/item_base.html:203 msgid "Last Stocktake" msgstr "" -#: stock/templates/stock/item_base.html:170 +#: stock/templates/stock/item_base.html:207 msgid "No stocktake performed" msgstr "" @@ -2055,12 +2355,7 @@ msgstr "" msgid "Stock Locations" msgstr "" -#: stock/templates/stock/stock_adjust.html:16 #: stock/templates/stock/stock_app_base.html:7 -msgid "Stock Item" -msgstr "" - -#: stock/templates/stock/stock_app_base.html:9 msgid "Stock Location" msgstr "" @@ -2223,6 +2518,10 @@ msgstr "" msgid "No results found" msgstr "" +#: templates/InvenTree/starred_parts.html:7 +msgid "Starred Parts" +msgstr "" + #: templates/about.html:13 msgid "InvenTree Version Information" msgstr "" @@ -2236,46 +2535,58 @@ msgid "InvenTree Version" msgstr "" #: templates/about.html:30 -msgid "Commit Hash" +msgid "Django Version" msgstr "" #: templates/about.html:34 -msgid "Commit Date" +msgid "Commit Hash" msgstr "" #: templates/about.html:38 +msgid "Commit Date" +msgstr "" + +#: templates/about.html:42 msgid "InvenTree Documentation" msgstr "" -#: templates/about.html:43 +#: templates/about.html:47 msgid "View Code on GitHub" msgstr "" -#: templates/about.html:47 +#: templates/about.html:51 msgid "Submit Bug Report" msgstr "" -#: templates/navbar.html:23 +#: templates/navbar.html:14 +msgid "Buy" +msgstr "" + +#: templates/navbar.html:22 +msgid "Sell" +msgstr "" + +#: templates/navbar.html:36 msgid "Admin" msgstr "" -#: templates/navbar.html:26 +#: templates/navbar.html:39 msgid "Settings" msgstr "" -#: templates/navbar.html:27 +#: templates/navbar.html:40 msgid "Logout" msgstr "" -#: templates/navbar.html:29 +#: templates/navbar.html:42 msgid "Login" msgstr "" -#: templates/navbar.html:32 +#: templates/navbar.html:45 msgid "About InvenTree" msgstr "" -#: templates/navbar.html:33 +#: templates/navbar.html:46 msgid "Statistics" msgstr "" @@ -2331,34 +2642,50 @@ msgstr "" msgid "Stock status" msgstr "" -#: templates/table_filters.html:53 +#: templates/table_filters.html:37 +msgid "Is allocated" +msgstr "" + +#: templates/table_filters.html:38 +msgid "Item has been alloacted" +msgstr "" + +#: templates/table_filters.html:58 msgid "Order status" msgstr "" -#: templates/table_filters.html:64 +#: templates/table_filters.html:69 msgid "Include subcategories" msgstr "" -#: templates/table_filters.html:65 +#: templates/table_filters.html:70 msgid "Include parts in subcategories" msgstr "" -#: templates/table_filters.html:69 +#: templates/table_filters.html:74 msgid "Active" msgstr "" -#: templates/table_filters.html:70 +#: templates/table_filters.html:75 msgid "Show active parts" msgstr "" -#: templates/table_filters.html:74 +#: templates/table_filters.html:79 msgid "Template" msgstr "" -#: templates/table_filters.html:78 +#: templates/table_filters.html:83 msgid "Stock available" msgstr "" -#: templates/table_filters.html:82 +#: templates/table_filters.html:87 msgid "Low stock" msgstr "" + +#: templates/table_filters.html:99 +msgid "Starred" +msgstr "" + +#: templates/table_filters.html:107 +msgid "Purchasable" +msgstr "" diff --git a/InvenTree/locale/es/LC_MESSAGES/django.po b/InvenTree/locale/es/LC_MESSAGES/django.po index fcb353a3ca..ee9168f369 100644 --- a/InvenTree/locale/es/LC_MESSAGES/django.po +++ b/InvenTree/locale/es/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-04-11 15:00+0000\n" +"POT-Creation-Date: 2020-04-22 23:17+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -18,30 +18,54 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: InvenTree/helpers.py:259 order/models.py:164 order/models.py:215 +#: InvenTree/api.py:61 +msgid "No action specified" +msgstr "" + +#: InvenTree/api.py:75 +msgid "No matching action found" +msgstr "" + +#: InvenTree/api.py:106 +msgid "No barcode data provided" +msgstr "" + +#: InvenTree/api.py:121 +msgid "Barcode successfully decoded" +msgstr "" + +#: InvenTree/api.py:124 +msgid "Barcode plugin returned incorrect response" +msgstr "" + +#: InvenTree/api.py:134 +msgid "Unknown barcode format" +msgstr "" + +#: InvenTree/helpers.py:258 order/models.py:173 order/models.py:224 msgid "Invalid quantity provided" msgstr "" -#: InvenTree/helpers.py:262 +#: InvenTree/helpers.py:261 msgid "Empty serial number string" msgstr "" -#: InvenTree/helpers.py:283 InvenTree/helpers.py:300 +#: InvenTree/helpers.py:282 InvenTree/helpers.py:299 #, python-brace-format msgid "Duplicate serial: {n}" msgstr "" -#: InvenTree/helpers.py:287 InvenTree/helpers.py:290 InvenTree/helpers.py:293 -#: InvenTree/helpers.py:304 +#: InvenTree/helpers.py:286 InvenTree/helpers.py:289 InvenTree/helpers.py:292 +#: InvenTree/helpers.py:303 #, python-brace-format msgid "Invalid group: {g}" msgstr "" -#: InvenTree/helpers.py:310 +#: InvenTree/helpers.py:309 msgid "No serial numbers found" msgstr "" -#: InvenTree/helpers.py:314 +#: InvenTree/helpers.py:313 #, python-brace-format msgid "Number of unique serial number ({s}) must match quantity ({q})" msgstr "" @@ -70,47 +94,51 @@ msgstr "" msgid "Polish" msgstr "" -#: InvenTree/status_codes.py:86 InvenTree/status_codes.py:162 +#: InvenTree/status_codes.py:84 InvenTree/status_codes.py:172 msgid "Pending" msgstr "" -#: InvenTree/status_codes.py:87 +#: InvenTree/status_codes.py:85 msgid "Placed" msgstr "" -#: InvenTree/status_codes.py:88 InvenTree/status_codes.py:165 +#: InvenTree/status_codes.py:86 InvenTree/status_codes.py:175 msgid "Complete" msgstr "" -#: InvenTree/status_codes.py:89 InvenTree/status_codes.py:164 +#: InvenTree/status_codes.py:87 InvenTree/status_codes.py:174 msgid "Cancelled" msgstr "" -#: InvenTree/status_codes.py:90 InvenTree/status_codes.py:130 +#: InvenTree/status_codes.py:88 InvenTree/status_codes.py:135 msgid "Lost" msgstr "" -#: InvenTree/status_codes.py:91 +#: InvenTree/status_codes.py:89 InvenTree/status_codes.py:137 msgid "Returned" msgstr "" -#: InvenTree/status_codes.py:126 +#: InvenTree/status_codes.py:131 msgid "OK" msgstr "" -#: InvenTree/status_codes.py:127 +#: InvenTree/status_codes.py:132 msgid "Attention needed" msgstr "" -#: InvenTree/status_codes.py:128 +#: InvenTree/status_codes.py:133 msgid "Damaged" msgstr "" -#: InvenTree/status_codes.py:129 +#: InvenTree/status_codes.py:134 msgid "Destroyed" msgstr "" -#: InvenTree/status_codes.py:163 build/templates/build/allocate_edit.html:28 +#: InvenTree/status_codes.py:136 +msgid "Shipped" +msgstr "" + +#: InvenTree/status_codes.py:173 build/templates/build/allocate_edit.html:28 #: build/templates/build/allocate_view.html:21 #: part/templates/part/part_base.html:114 part/templates/part/tabs.html:21 msgid "Allocated" @@ -141,7 +169,7 @@ msgstr "" msgid "Overage must be an integer value or a percentage" msgstr "" -#: InvenTree/views.py:549 +#: InvenTree/views.py:536 msgid "Database Statistics" msgstr "" @@ -175,7 +203,7 @@ msgstr "" msgid "Number of parts to build" msgstr "" -#: build/models.py:82 templates/table_filters.html:42 +#: build/models.py:82 templates/table_filters.html:47 msgid "Build status" msgstr "" @@ -183,7 +211,7 @@ msgstr "" msgid "Batch code for this build output" msgstr "" -#: build/models.py:97 stock/models.py:331 +#: build/models.py:97 stock/models.py:336 msgid "Link to external URL" msgstr "" @@ -201,15 +229,27 @@ msgstr "" msgid "Allocated quantity ({n}) must not exceed available quantity ({q})" msgstr "" -#: build/models.py:409 +#: build/models.py:397 order/models.py:448 +msgid "StockItem is over-allocated" +msgstr "" + +#: build/models.py:400 order/models.py:451 +msgid "Allocation quantity must be greater than zero" +msgstr "" + +#: build/models.py:403 +msgid "Quantity must be 1 for serialized stock" +msgstr "" + +#: build/models.py:418 msgid "Build to allocate parts" msgstr "" -#: build/models.py:416 +#: build/models.py:425 msgid "Stock Item to allocate to build" msgstr "" -#: build/models.py:424 +#: build/models.py:433 msgid "Stock quantity to allocate to build" msgstr "" @@ -227,8 +267,7 @@ msgstr "" #: build/templates/build/allocate_edit.html:19 #: build/templates/build/allocate_view.html:17 -#: build/templates/build/detail.html:22 -#: company/templates/company/detail_part.html:65 +#: build/templates/build/detail.html:22 order/models.py:385 #: order/templates/order/order_wizard/select_parts.html:30 #: order/templates/order/purchase_order_detail.html:26 #: part/templates/part/part_app_base.html:7 @@ -254,12 +293,11 @@ msgid "Allocate" msgstr "" #: build/templates/build/allocate_view.html:10 -#: company/templates/company/detail_part.html:18 order/views.py:526 +#: company/templates/company/detail_part.html:18 order/views.py:671 msgid "Order Parts" msgstr "" #: build/templates/build/allocate_view.html:18 -#: company/templates/company/index.html:54 #: company/templates/company/supplier_part_base.html:50 #: company/templates/company/supplier_part_detail.html:27 #: order/templates/order/purchase_order_detail.html:27 @@ -272,45 +310,47 @@ msgstr "" msgid "On Order" msgstr "" -#: build/templates/build/build_base.html:27 part/templates/part/tabs.html:28 -#: stock/templates/stock/item_base.html:122 templates/navbar.html:12 +#: build/templates/build/build_base.html:8 +#: build/templates/build/build_base.html:21 part/templates/part/tabs.html:28 +#: stock/templates/stock/item_base.html:159 templates/navbar.html:12 msgid "Build" msgstr "" -#: build/templates/build/build_base.html:52 build/templates/build/detail.html:9 +#: build/templates/build/build_base.html:48 build/templates/build/detail.html:9 msgid "Build Details" msgstr "" -#: build/templates/build/build_base.html:56 +#: build/templates/build/build_base.html:52 msgid "Build Title" msgstr "" -#: build/templates/build/build_base.html:66 +#: build/templates/build/build_base.html:62 #: build/templates/build/detail.html:27 #: company/templates/company/supplier_part_pricing.html:27 #: order/templates/order/order_wizard/select_parts.html:32 #: order/templates/order/purchase_order_detail.html:30 -#: stock/templates/stock/item_base.html:108 +#: stock/templates/stock/item_base.html:20 +#: stock/templates/stock/item_base.html:26 +#: stock/templates/stock/item_base.html:145 #: stock/templates/stock/stock_adjust.html:18 msgid "Quantity" msgstr "" -#: build/templates/build/build_base.html:71 +#: build/templates/build/build_base.html:67 #: build/templates/build/detail.html:42 -#: order/templates/order/order_base.html:72 -#: stock/templates/stock/item_base.html:175 +#: stock/templates/stock/item_base.html:212 msgid "Status" msgstr "" -#: build/templates/build/build_base.html:76 +#: build/templates/build/build_base.html:72 msgid "BOM Price" msgstr "" -#: build/templates/build/build_base.html:81 +#: build/templates/build/build_base.html:77 msgid "BOM pricing is incomplete" msgstr "" -#: build/templates/build/build_base.html:84 +#: build/templates/build/build_base.html:80 msgid "No pricing information" msgstr "" @@ -335,20 +375,21 @@ msgid "Stock can be taken from any available location." msgstr "" #: build/templates/build/detail.html:48 -#: stock/templates/stock/item_base.html:115 +#: stock/templates/stock/item_base.html:152 msgid "Batch" msgstr "" #: build/templates/build/detail.html:55 -#: company/templates/company/supplier_part_base.html:47 +#: company/templates/company/supplier_part_base.html:57 #: company/templates/company/supplier_part_detail.html:24 #: part/templates/part/detail.html:67 part/templates/part/part_base.html:85 -#: stock/templates/stock/item_base.html:143 +#: stock/templates/stock/item_base.html:180 msgid "External Link" msgstr "" #: build/templates/build/detail.html:61 -#: order/templates/order/order_base.html:84 +#: order/templates/order/order_base.html:93 +#: order/templates/order/sales_order_base.html:82 msgid "Created" msgstr "" @@ -373,14 +414,16 @@ msgid "Build Notes" msgstr "" #: build/templates/build/notes.html:20 company/templates/company/notes.html:17 -#: order/templates/order/order_notes.html:21 part/templates/part/notes.html:20 -#: stock/templates/stock/item_notes.html:21 +#: order/templates/order/order_notes.html:21 +#: order/templates/order/sales_order_notes.html:26 +#: part/templates/part/notes.html:20 stock/templates/stock/item_notes.html:21 msgid "Save" msgstr "" #: build/templates/build/notes.html:33 company/templates/company/notes.html:30 -#: order/templates/order/order_notes.html:32 part/templates/part/notes.html:32 -#: stock/templates/stock/item_notes.html:32 +#: order/templates/order/order_notes.html:32 +#: order/templates/order/sales_order_notes.html:37 +#: part/templates/part/notes.html:32 stock/templates/stock/item_notes.html:32 msgid "Edit notes" msgstr "" @@ -393,9 +436,10 @@ msgstr "" msgid "Outputs" msgstr "" -#: build/templates/build/tabs.html:11 company/models.py:264 -#: company/templates/company/tabs.html:26 order/templates/order/tabs.html:15 -#: part/templates/part/tabs.html:58 stock/templates/stock/tabs.html:17 +#: build/templates/build/tabs.html:11 company/models.py:302 +#: company/templates/company/tabs.html:26 order/templates/order/po_tabs.html:15 +#: order/templates/order/so_tabs.html:15 part/templates/part/tabs.html:63 +#: stock/templates/stock/tabs.html:17 msgid "Notes" msgstr "" @@ -552,130 +596,138 @@ msgstr "" msgid "Delete Currency" msgstr "" -#: company/models.py:76 +#: company/models.py:83 msgid "Company name" msgstr "" -#: company/models.py:78 +#: company/models.py:85 msgid "Description of the company" msgstr "" -#: company/models.py:80 +#: company/models.py:87 msgid "Company website URL" msgstr "" -#: company/models.py:83 +#: company/models.py:90 msgid "Company address" msgstr "" -#: company/models.py:86 +#: company/models.py:93 msgid "Contact phone number" msgstr "" -#: company/models.py:88 +#: company/models.py:95 msgid "Contact email address" msgstr "" -#: company/models.py:91 +#: company/models.py:98 msgid "Point of contact" msgstr "" -#: company/models.py:93 +#: company/models.py:100 msgid "Link to external company information" msgstr "" -#: company/models.py:105 +#: company/models.py:112 msgid "Do you sell items to this company?" msgstr "" -#: company/models.py:107 +#: company/models.py:114 msgid "Do you purchase items from this company?" msgstr "" -#: company/models.py:245 +#: company/models.py:116 +msgid "Does this company manufacture parts?" +msgstr "" + +#: company/models.py:276 msgid "Select part" msgstr "" -#: company/models.py:251 +#: company/models.py:282 msgid "Select supplier" msgstr "" -#: company/models.py:254 +#: company/models.py:285 msgid "Supplier stock keeping unit" msgstr "" -#: company/models.py:256 company/templates/company/detail_part.html:96 -#: company/templates/company/supplier_part_base.html:53 -#: company/templates/company/supplier_part_detail.html:30 -msgid "Manufacturer" +#: company/models.py:292 +msgid "Select manufacturer" msgstr "" -#: company/models.py:258 +#: company/models.py:296 msgid "Manufacturer part number" msgstr "" -#: company/models.py:260 +#: company/models.py:298 msgid "URL for external supplier part link" msgstr "" -#: company/models.py:262 +#: company/models.py:300 msgid "Supplier part description" msgstr "" -#: company/models.py:266 +#: company/models.py:304 msgid "Minimum charge (e.g. stocking fee)" msgstr "" -#: company/models.py:268 +#: company/models.py:306 msgid "Part packaging" msgstr "" -#: company/templates/company/company_base.html:7 order/models.py:131 +#: company/templates/company/company_base.html:7 +#: company/templates/company/company_base.html:22 msgid "Company" msgstr "" -#: company/templates/company/company_base.html:50 -#: company/templates/company/index.html:59 -msgid "Website" -msgstr "" - -#: company/templates/company/company_base.html:57 -msgid "Address" -msgstr "" - -#: company/templates/company/company_base.html:64 -msgid "Phone" -msgstr "" - -#: company/templates/company/company_base.html:71 -msgid "Email" -msgstr "" - -#: company/templates/company/company_base.html:78 -msgid "Contact" -msgstr "" - +#: company/templates/company/company_base.html:42 #: company/templates/company/detail.html:8 msgid "Company Details" msgstr "" +#: company/templates/company/company_base.html:48 +msgid "Website" +msgstr "" + +#: company/templates/company/company_base.html:55 +msgid "Address" +msgstr "" + +#: company/templates/company/company_base.html:62 +msgid "Phone" +msgstr "" + +#: company/templates/company/company_base.html:69 +msgid "Email" +msgstr "" + +#: company/templates/company/company_base.html:76 +msgid "Contact" +msgstr "" + #: company/templates/company/detail.html:16 -#: stock/templates/stock/item_base.html:136 -msgid "Customer" +#: company/templates/company/supplier_part_base.html:73 +#: company/templates/company/supplier_part_detail.html:30 +msgid "Manufacturer" msgstr "" #: company/templates/company/detail.html:21 -#: company/templates/company/index.html:46 -#: company/templates/company/supplier_part_base.html:44 -#: company/templates/company/supplier_part_detail.html:21 -#: order/templates/order/order_base.html:67 +#: company/templates/company/supplier_part_base.html:63 +#: company/templates/company/supplier_part_detail.html:21 order/models.py:138 +#: order/templates/order/order_base.html:74 #: order/templates/order/order_wizard/select_pos.html:30 -#: stock/templates/stock/item_base.html:150 +#: stock/templates/stock/item_base.html:187 msgid "Supplier" msgstr "" +#: company/templates/company/detail.html:26 order/models.py:275 +#: order/templates/order/sales_order_base.html:63 +#: stock/templates/stock/item_base.html:173 +msgid "Customer" +msgstr "" + #: company/templates/company/detail_part.html:8 -#: company/templates/company/tabs.html:9 msgid "Supplier Parts" msgstr "" @@ -692,53 +744,45 @@ msgstr "" msgid "Delete Parts" msgstr "" -#: company/templates/company/detail_part.html:88 -#: company/templates/company/supplier_part_base.html:45 -#: company/templates/company/supplier_part_detail.html:22 -msgid "SKU" +#: company/templates/company/detail_part.html:43 +#: part/templates/part/stock.html:75 +msgid "New Part" msgstr "" -#: company/templates/company/detail_part.html:105 -msgid "Link" +#: company/templates/company/detail_part.html:44 +msgid "Create new Part" msgstr "" -#: company/templates/company/detail_purchase_orders.html:8 -#: company/templates/company/tabs.html:15 part/templates/part/tabs.html:43 -msgid "Purchase Orders" +#: company/templates/company/detail_part.html:49 company/views.py:52 +msgid "New Supplier" msgstr "" -#: company/templates/company/detail_purchase_orders.html:13 -msgid "New Purchase Order" +#: company/templates/company/detail_part.html:50 company/views.py:184 +msgid "Create new Supplier" +msgstr "" + +#: company/templates/company/detail_part.html:55 company/views.py:58 +msgid "New Manufacturer" +msgstr "" + +#: company/templates/company/detail_part.html:56 company/views.py:187 +msgid "Create new Manufacturer" msgstr "" #: company/templates/company/detail_stock.html:9 msgid "Supplier Stock" msgstr "" -#: company/templates/company/detail_stock.html:33 +#: company/templates/company/detail_stock.html:34 #: company/templates/company/supplier_part_stock.html:38 #: part/templates/part/stock.html:53 templates/stock_table.html:5 msgid "Export" msgstr "" #: company/templates/company/index.html:7 -#: company/templates/company/index.html:12 msgid "Supplier List" msgstr "" -#: company/templates/company/index.html:17 -msgid "New Supplier" -msgstr "" - -#: company/templates/company/index.html:41 -msgid "ID" -msgstr "" - -#: company/templates/company/index.html:69 part/templates/part/category.html:83 -#: templates/navbar.html:10 templates/stats.html:8 templates/stats.html:17 -msgid "Parts" -msgstr "" - #: company/templates/company/notes.html:10 #: company/templates/company/notes.html:27 msgid "Company Notes" @@ -748,28 +792,71 @@ msgstr "" msgid "Are you sure you want to delete the following Supplier Parts?" msgstr "" +#: company/templates/company/purchase_orders.html:9 +#: company/templates/company/tabs.html:17 +#: order/templates/order/purchase_orders.html:7 +#: order/templates/order/purchase_orders.html:12 +#: part/templates/part/orders.html:9 part/templates/part/tabs.html:43 +#: templates/navbar.html:18 +msgid "Purchase Orders" +msgstr "" + +#: company/templates/company/purchase_orders.html:14 +#: order/templates/order/purchase_orders.html:17 +msgid "Create new purchase order" +msgstr "" + +#: company/templates/company/purchase_orders.html:14 +#: order/templates/order/purchase_orders.html:17 +msgid "New Purchase Order" +msgstr "" + +#: company/templates/company/sales_orders.html:9 +#: company/templates/company/tabs.html:22 +#: order/templates/order/sales_orders.html:7 +#: order/templates/order/sales_orders.html:12 +#: part/templates/part/sales_orders.html:9 part/templates/part/tabs.html:48 +#: templates/navbar.html:25 +msgid "Sales Orders" +msgstr "" + +#: company/templates/company/sales_orders.html:14 +#: order/templates/order/sales_orders.html:17 +msgid "Create new sales order" +msgstr "" + +#: company/templates/company/sales_orders.html:14 +#: order/templates/order/sales_orders.html:17 +msgid "New Sales Order" +msgstr "" + #: company/templates/company/supplier_part_base.html:6 -#: company/templates/company/supplier_part_base.html:13 -#: stock/templates/stock/item_base.html:155 +#: company/templates/company/supplier_part_base.html:19 +#: stock/templates/stock/item_base.html:192 msgid "Supplier Part" msgstr "" -#: company/templates/company/supplier_part_base.html:34 +#: company/templates/company/supplier_part_base.html:35 #: company/templates/company/supplier_part_detail.html:11 msgid "Supplier Part Details" msgstr "" -#: company/templates/company/supplier_part_base.html:37 +#: company/templates/company/supplier_part_base.html:40 #: company/templates/company/supplier_part_detail.html:14 msgid "Internal Part" msgstr "" -#: company/templates/company/supplier_part_base.html:54 +#: company/templates/company/supplier_part_base.html:67 +#: company/templates/company/supplier_part_detail.html:22 +msgid "SKU" +msgstr "" + +#: company/templates/company/supplier_part_base.html:77 #: company/templates/company/supplier_part_detail.html:31 msgid "MPN" msgstr "" -#: company/templates/company/supplier_part_base.html:57 +#: company/templates/company/supplier_part_base.html:84 #: company/templates/company/supplier_part_detail.html:34 #: order/templates/order/purchase_order_detail.html:34 msgid "Note" @@ -832,154 +919,246 @@ msgid "Stock" msgstr "" #: company/templates/company/supplier_part_tabs.html:11 -#: templates/navbar.html:14 msgid "Orders" msgstr "" -#: company/templates/company/tabs.html:21 -msgid "Sales Orders" +#: company/templates/company/tabs.html:9 part/templates/part/category.html:83 +#: templates/navbar.html:10 templates/stats.html:8 templates/stats.html:17 +msgid "Parts" msgstr "" -#: company/views.py:99 +#: company/views.py:51 part/templates/part/tabs.html:37 +#: templates/navbar.html:16 +msgid "Suppliers" +msgstr "" + +#: company/views.py:57 templates/navbar.html:17 +msgid "Manufacturers" +msgstr "" + +#: company/views.py:63 templates/navbar.html:24 +msgid "Customers" +msgstr "" + +#: company/views.py:64 +msgid "New Customer" +msgstr "" + +#: company/views.py:71 +msgid "Companies" +msgstr "" + +#: company/views.py:72 +msgid "New Company" +msgstr "" + +#: company/views.py:149 msgid "Update Company Image" msgstr "" -#: company/views.py:104 +#: company/views.py:154 msgid "Updated company image" msgstr "" -#: company/views.py:114 +#: company/views.py:164 msgid "Edit Company" msgstr "" -#: company/views.py:118 +#: company/views.py:168 msgid "Edited company information" msgstr "" -#: company/views.py:128 +#: company/views.py:190 +msgid "Create new Customer" +msgstr "" + +#: company/views.py:192 msgid "Create new Company" msgstr "" -#: company/views.py:132 +#: company/views.py:219 msgid "Created new company" msgstr "" -#: company/views.py:142 +#: company/views.py:229 msgid "Delete Company" msgstr "" -#: company/views.py:147 +#: company/views.py:234 msgid "Company was deleted" msgstr "" -#: company/views.py:172 +#: company/views.py:259 msgid "Edit Supplier Part" msgstr "" -#: company/views.py:181 part/templates/part/stock.html:82 +#: company/views.py:268 part/templates/part/stock.html:82 msgid "Create new Supplier Part" msgstr "" -#: company/views.py:238 +#: company/views.py:328 msgid "Delete Supplier Part" msgstr "" -#: company/views.py:308 +#: company/views.py:398 msgid "Add Price Break" msgstr "" -#: company/views.py:350 +#: company/views.py:440 msgid "Edit Price Break" msgstr "" -#: company/views.py:365 +#: company/views.py:455 msgid "Delete Price Break" msgstr "" -#: order/forms.py:22 +#: order/forms.py:24 msgid "Place order" msgstr "" -#: order/forms.py:33 +#: order/forms.py:35 msgid "Mark order as complete" msgstr "" -#: order/forms.py:44 +#: order/forms.py:46 msgid "Cancel order" msgstr "" -#: order/forms.py:55 +#: order/forms.py:57 msgid "Receive parts to this location" msgstr "" -#: order/models.py:68 +#: order/models.py:71 msgid "Order reference" msgstr "" -#: order/models.py:70 +#: order/models.py:73 msgid "Order description" msgstr "" -#: order/models.py:72 +#: order/models.py:75 msgid "Link to external page" msgstr "" -#: order/models.py:89 +#: order/models.py:92 msgid "Order notes" msgstr "" -#: order/models.py:162 order/models.py:213 part/views.py:1119 -#: stock/models.py:467 +#: order/models.py:141 +msgid "Supplier order reference code" +msgstr "" + +#: order/models.py:171 order/models.py:222 part/views.py:1119 +#: stock/models.py:519 msgid "Quantity must be greater than zero" msgstr "" -#: order/models.py:167 +#: order/models.py:176 msgid "Part supplier must match PO supplier" msgstr "" -#: order/models.py:208 +#: order/models.py:217 msgid "Lines can only be received against an order marked as 'Placed'" msgstr "" -#: order/models.py:268 +#: order/models.py:278 +msgid "Customer order reference code" +msgstr "" + +#: order/models.py:324 msgid "Item quantity" msgstr "" -#: order/models.py:270 +#: order/models.py:326 msgid "Line item reference" msgstr "" -#: order/models.py:272 +#: order/models.py:328 msgid "Line item notes" msgstr "" -#: order/models.py:298 stock/templates/stock/item_base.html:129 +#: order/models.py:354 order/templates/order/order_base.html:9 +#: order/templates/order/order_base.html:23 +#: stock/templates/stock/item_base.html:166 msgid "Purchase Order" msgstr "" -#: order/models.py:307 +#: order/models.py:363 msgid "Supplier part" msgstr "" -#: order/models.py:310 +#: order/models.py:366 msgid "Number of items received" msgstr "" -#: order/templates/order/order_base.html:62 +#: order/models.py:383 order/templates/order/sales_order_base.html:9 +#: order/templates/order/sales_order_base.html:31 +#: order/templates/order/sales_order_notes.html:10 +msgid "Sales Order" +msgstr "" + +#: order/models.py:440 +msgid "Cannot allocate stock item to a line with a different part" +msgstr "" + +#: order/models.py:442 +msgid "Cannot allocate stock to a line without a part" +msgstr "" + +#: order/models.py:445 +msgid "Allocation quantity cannot exceed stock quantity" +msgstr "" + +#: order/models.py:454 +msgid "Quantity must be 1 for serialized stock item" +msgstr "" + +#: order/models.py:466 +msgid "Select stock item to allocate" +msgstr "" + +#: order/models.py:469 +msgid "Enter stock allocation quantity" +msgstr "" + +#: order/templates/order/delete_attachment.html:5 +#: part/templates/part/attachment_delete.html:5 +msgid "Are you sure you want to delete this attachment?" +msgstr "" + +#: order/templates/order/order_base.html:59 msgid "Purchase Order Details" msgstr "" -#: order/templates/order/order_base.html:90 +#: order/templates/order/order_base.html:64 +#: order/templates/order/sales_order_base.html:53 +msgid "Order Reference" +msgstr "" + +#: order/templates/order/order_base.html:69 +#: order/templates/order/sales_order_base.html:58 +msgid "Order Status" +msgstr "" + +#: order/templates/order/order_base.html:80 +msgid "Supplier Reference" +msgstr "" + +#: order/templates/order/order_base.html:99 +#: order/templates/order/sales_order_base.html:88 msgid "Issued" msgstr "" -#: order/templates/order/order_base.html:97 +#: order/templates/order/order_base.html:106 #: order/templates/order/purchase_order_detail.html:32 +#: order/templates/order/sales_order_base.html:95 msgid "Received" msgstr "" #: order/templates/order/order_notes.html:13 #: order/templates/order/order_notes.html:29 +#: order/templates/order/sales_order_notes.html:18 +#: order/templates/order/sales_order_notes.html:34 msgid "Order Notes" msgstr "" @@ -1012,7 +1191,7 @@ msgid "Select existing purchase orders, or create new orders." msgstr "" #: order/templates/order/order_wizard/select_pos.html:31 -#: order/templates/order/tabs.html:5 +#: order/templates/order/po_tabs.html:5 order/templates/order/so_tabs.html:5 msgid "Items" msgstr "" @@ -1029,41 +1208,48 @@ msgid "Purchase Order Attachments" msgstr "" #: order/templates/order/po_attachments.html:17 +#: order/templates/order/so_attachments.html:17 #: part/templates/part/attachments.html:14 msgid "Add Attachment" msgstr "" #: order/templates/order/po_attachments.html:24 +#: order/templates/order/so_attachments.html:24 #: part/templates/part/attachments.html:22 msgid "File" msgstr "" #: order/templates/order/po_attachments.html:25 +#: order/templates/order/so_attachments.html:25 #: part/templates/part/attachments.html:23 msgid "Comment" msgstr "" #: order/templates/order/po_attachments.html:36 +#: order/templates/order/so_attachments.html:36 #: part/templates/part/attachments.html:34 part/views.py:119 msgid "Edit attachment" msgstr "" #: order/templates/order/po_attachments.html:39 +#: order/templates/order/so_attachments.html:39 #: part/templates/part/attachments.html:37 msgid "Delete attachment" msgstr "" -#: order/templates/order/po_delete.html:5 -#: part/templates/part/attachment_delete.html:5 -msgid "Are you sure you want to delete this attachment?" +#: order/templates/order/po_tabs.html:8 order/templates/order/so_tabs.html:8 +#: part/templates/part/tabs.html:60 +msgid "Attachments" msgstr "" -#: order/templates/order/purchase_order_detail.html:16 order/views.py:825 +#: order/templates/order/purchase_order_detail.html:16 +#: order/templates/order/sales_order_detail.html:17 order/views.py:970 +#: order/views.py:1084 msgid "Add Line Item" msgstr "" #: order/templates/order/purchase_order_detail.html:20 -msgid "Order Items" +msgid "Purchase Order Items" msgstr "" #: order/templates/order/purchase_order_detail.html:25 @@ -1078,110 +1264,182 @@ msgstr "" msgid "Reference" msgstr "" -#: order/templates/order/tabs.html:8 part/templates/part/tabs.html:55 -msgid "Attachments" +#: order/templates/order/sales_order_base.html:15 +msgid "This SalesOrder has not been fully allocated" msgstr "" -#: order/views.py:80 +#: order/templates/order/sales_order_base.html:40 +msgid "Packing List" +msgstr "" + +#: order/templates/order/sales_order_base.html:48 +msgid "Sales Order Details" +msgstr "" + +#: order/templates/order/sales_order_base.html:69 +msgid "Customer Reference" +msgstr "" + +#: order/templates/order/sales_order_detail.html:14 +msgid "Sales Order Items" +msgstr "" + +#: order/templates/order/sales_order_detail.html:90 +msgid "Edit stock allocation" +msgstr "" + +#: order/templates/order/sales_order_detail.html:91 +msgid "Delete stock allocation" +msgstr "" + +#: order/templates/order/sales_order_detail.html:178 +msgid "Buy parts" +msgstr "" + +#: order/templates/order/sales_order_detail.html:182 +msgid "Build parts" +msgstr "" + +#: order/templates/order/sales_order_detail.html:185 +msgid "Allocate parts" +msgstr "" + +#: order/templates/order/sales_order_detail.html:189 +msgid "Edit line item" +msgstr "" + +#: order/templates/order/sales_order_detail.html:190 +msgid "Delete line item " +msgstr "" + +#: order/templates/order/so_attachments.html:11 +msgid "Sales Order Attachments" +msgstr "" + +#: order/views.py:97 msgid "Add Purchase Order Attachment" msgstr "" -#: order/views.py:85 part/views.py:80 +#: order/views.py:102 order/views.py:142 part/views.py:80 msgid "Added attachment" msgstr "" -#: order/views.py:121 +#: order/views.py:138 +msgid "Add Sales Order Attachment" +msgstr "" + +#: order/views.py:166 order/views.py:187 msgid "Edit Attachment" msgstr "" -#: order/views.py:125 +#: order/views.py:170 order/views.py:191 msgid "Attachment updated" msgstr "" -#: order/views.py:141 +#: order/views.py:206 order/views.py:220 msgid "Delete Attachment" msgstr "" -#: order/views.py:147 +#: order/views.py:212 order/views.py:226 msgid "Deleted attachment" msgstr "" -#: order/views.py:177 +#: order/views.py:277 msgid "Create Purchase Order" msgstr "" -#: order/views.py:207 +#: order/views.py:307 +msgid "Create Sales Order" +msgstr "" + +#: order/views.py:336 msgid "Edit Purchase Order" msgstr "" -#: order/views.py:227 +#: order/views.py:356 +msgid "Edit Sales Order" +msgstr "" + +#: order/views.py:372 msgid "Cancel Order" msgstr "" -#: order/views.py:242 +#: order/views.py:387 msgid "Confirm order cancellation" msgstr "" -#: order/views.py:260 +#: order/views.py:405 msgid "Issue Order" msgstr "" -#: order/views.py:275 +#: order/views.py:420 msgid "Confirm order placement" msgstr "" -#: order/views.py:296 +#: order/views.py:441 msgid "Complete Order" msgstr "" -#: order/views.py:362 +#: order/views.py:507 msgid "Receive Parts" msgstr "" -#: order/views.py:429 +#: order/views.py:574 msgid "Items received" msgstr "" -#: order/views.py:443 +#: order/views.py:588 msgid "No destination set" msgstr "" -#: order/views.py:474 +#: order/views.py:619 msgid "Error converting quantity to number" msgstr "" -#: order/views.py:480 +#: order/views.py:625 msgid "Receive quantity less than zero" msgstr "" -#: order/views.py:486 +#: order/views.py:631 msgid "No lines specified" msgstr "" -#: order/views.py:845 +#: order/views.py:990 msgid "Invalid Purchase Order" msgstr "" -#: order/views.py:853 +#: order/views.py:998 msgid "Supplier must match for Part and Order" msgstr "" -#: order/views.py:858 +#: order/views.py:1003 msgid "Invalid SupplierPart selection" msgstr "" -#: order/views.py:940 +#: order/views.py:1123 order/views.py:1141 msgid "Edit Line Item" msgstr "" -#: order/views.py:956 +#: order/views.py:1157 order/views.py:1169 msgid "Delete Line Item" msgstr "" -#: order/views.py:961 +#: order/views.py:1162 order/views.py:1174 msgid "Deleted line item" msgstr "" +#: order/views.py:1183 +msgid "Allocate Stock to Order" +msgstr "" + +#: order/views.py:1252 +msgid "Edit Allocation Quantity" +msgstr "" + +#: order/views.py:1267 +msgid "Remove allocation" +msgstr "" + #: part/bom.py:140 #, python-brace-format msgid "Unsupported file format: {f}" @@ -1347,63 +1605,63 @@ msgstr "" msgid "Stored BOM checksum" msgstr "" -#: part/models.py:1049 +#: part/models.py:1065 msgid "Parameter template name must be unique" msgstr "" -#: part/models.py:1054 +#: part/models.py:1070 msgid "Parameter Name" msgstr "" -#: part/models.py:1056 +#: part/models.py:1072 msgid "Parameter Units" msgstr "" -#: part/models.py:1082 +#: part/models.py:1098 msgid "Parent Part" msgstr "" -#: part/models.py:1084 +#: part/models.py:1100 msgid "Parameter Template" msgstr "" -#: part/models.py:1086 +#: part/models.py:1102 msgid "Parameter Value" msgstr "" -#: part/models.py:1110 +#: part/models.py:1126 msgid "Select parent part" msgstr "" -#: part/models.py:1119 +#: part/models.py:1135 msgid "Select part to be used in BOM" msgstr "" -#: part/models.py:1126 +#: part/models.py:1142 msgid "BOM quantity for this BOM item" msgstr "" -#: part/models.py:1129 +#: part/models.py:1145 msgid "Estimated build wastage quantity (absolute or percentage)" msgstr "" -#: part/models.py:1132 +#: part/models.py:1148 msgid "BOM item reference" msgstr "" -#: part/models.py:1135 +#: part/models.py:1151 msgid "BOM item notes" msgstr "" -#: part/models.py:1137 +#: part/models.py:1153 msgid "BOM line checksum" msgstr "" -#: part/models.py:1200 +#: part/models.py:1216 msgid "Part cannot be added to its own Bill of Materials" msgstr "" -#: part/models.py:1207 +#: part/models.py:1223 #, python-brace-format msgid "Part '{p1}' is used in BOM for '{p2}' (recursive)" msgstr "" @@ -1505,7 +1763,7 @@ msgstr "" msgid "Part is not a virtual part" msgstr "" -#: part/templates/part/detail.html:132 templates/table_filters.html:86 +#: part/templates/part/detail.html:132 templates/table_filters.html:91 msgid "Assembly" msgstr "" @@ -1517,7 +1775,7 @@ msgstr "" msgid "Part cannot be assembled from other parts" msgstr "" -#: part/templates/part/detail.html:141 templates/table_filters.html:90 +#: part/templates/part/detail.html:141 templates/table_filters.html:95 msgid "Component" msgstr "" @@ -1549,15 +1807,15 @@ msgstr "" msgid "Part can be purchased from external suppliers" msgstr "" -#: part/templates/part/detail.html:169 -msgid "Sellable" +#: part/templates/part/detail.html:168 templates/table_filters.html:103 +msgid "Salable" msgstr "" -#: part/templates/part/detail.html:172 +#: part/templates/part/detail.html:171 msgid "Part can be sold to customers" msgstr "" -#: part/templates/part/detail.html:174 +#: part/templates/part/detail.html:173 msgid "Part cannot be sold to customers" msgstr "" @@ -1565,6 +1823,14 @@ msgstr "" msgid "Part Notes" msgstr "" +#: part/templates/part/orders.html:14 +msgid "Order part" +msgstr "" + +#: part/templates/part/orders.html:14 +msgid "Order Part" +msgstr "" + #: part/templates/part/part_app_base.html:9 msgid "Part Category" msgstr "" @@ -1625,8 +1891,12 @@ msgstr "" msgid "Upload new image" msgstr "" -#: part/templates/part/stock.html:75 -msgid "New Part" +#: part/templates/part/sales_orders.html:14 +msgid "New sales order" +msgstr "" + +#: part/templates/part/sales_orders.html:14 +msgid "New Order" msgstr "" #: part/templates/part/stock.html:76 @@ -1637,7 +1907,7 @@ msgstr "" msgid "No Stock" msgstr "" -#: part/templates/part/stock_count.html:9 +#: part/templates/part/stock_count.html:9 templates/InvenTree/low_stock.html:7 msgid "Low Stock" msgstr "" @@ -1657,11 +1927,7 @@ msgstr "" msgid "Used In" msgstr "" -#: part/templates/part/tabs.html:37 templates/navbar.html:13 -msgid "Suppliers" -msgstr "" - -#: part/templates/part/tabs.html:48 stock/templates/stock/tabs.html:5 +#: part/templates/part/tabs.html:53 stock/templates/stock/tabs.html:5 msgid "Tracking" msgstr "" @@ -1762,74 +2028,90 @@ msgstr "" msgid "Specify quantity" msgstr "" -#: part/views.py:1366 +#: part/views.py:1364 msgid "Export Bill of Materials" msgstr "" -#: part/views.py:1404 +#: part/views.py:1402 msgid "Confirm Part Deletion" msgstr "" -#: part/views.py:1411 +#: part/views.py:1409 msgid "Part was deleted" msgstr "" -#: part/views.py:1420 +#: part/views.py:1418 msgid "Part Pricing" msgstr "" -#: part/views.py:1542 +#: part/views.py:1540 msgid "Create Part Parameter Template" msgstr "" -#: part/views.py:1550 +#: part/views.py:1548 msgid "Edit Part Parameter Template" msgstr "" -#: part/views.py:1557 +#: part/views.py:1555 msgid "Delete Part Parameter Template" msgstr "" -#: part/views.py:1565 +#: part/views.py:1563 msgid "Create Part Parameter" msgstr "" -#: part/views.py:1615 +#: part/views.py:1613 msgid "Edit Part Parameter" msgstr "" -#: part/views.py:1629 +#: part/views.py:1627 msgid "Delete Part Parameter" msgstr "" -#: part/views.py:1645 +#: part/views.py:1643 msgid "Edit Part Category" msgstr "" -#: part/views.py:1680 +#: part/views.py:1678 msgid "Delete Part Category" msgstr "" -#: part/views.py:1686 +#: part/views.py:1684 msgid "Part category was deleted" msgstr "" -#: part/views.py:1694 +#: part/views.py:1692 msgid "Create new part category" msgstr "" -#: part/views.py:1745 +#: part/views.py:1743 msgid "Create BOM item" msgstr "" -#: part/views.py:1811 +#: part/views.py:1809 msgid "Edit BOM item" msgstr "" -#: part/views.py:1859 +#: part/views.py:1857 msgid "Confim BOM item deletion" msgstr "" +#: plugins/barcode/inventree.py:70 +msgid "Part does not exist" +msgstr "" + +#: plugins/barcode/inventree.py:79 +msgid "StockLocation does not exist" +msgstr "" + +#: plugins/barcode/inventree.py:89 +msgid "StockItem does not exist" +msgstr "" + +#: plugins/barcode/inventree.py:92 +msgid "No matching data" +msgstr "" + #: stock/forms.py:93 msgid "Include stock items in sub locations" msgstr "" @@ -1846,121 +2128,121 @@ msgstr "" msgid "Set the destination as the default location for selected parts" msgstr "" -#: stock/models.py:205 +#: stock/models.py:210 #, python-brace-format msgid "" "A stock item with this serial number already exists for template part {part}" msgstr "" -#: stock/models.py:210 +#: stock/models.py:215 msgid "A stock item with this serial number already exists" msgstr "" -#: stock/models.py:229 +#: stock/models.py:234 #, python-brace-format msgid "Part type ('{pf}') must be {pe}" msgstr "" -#: stock/models.py:239 stock/models.py:248 +#: stock/models.py:244 stock/models.py:253 msgid "Quantity must be 1 for item with a serial number" msgstr "" -#: stock/models.py:240 +#: stock/models.py:245 msgid "Serial number cannot be set if quantity greater than 1" msgstr "" -#: stock/models.py:256 +#: stock/models.py:261 msgid "Stock item cannot be created for a template Part" msgstr "" -#: stock/models.py:265 +#: stock/models.py:270 msgid "Item cannot belong to itself" msgstr "" -#: stock/models.py:306 +#: stock/models.py:311 msgid "Base part" msgstr "" -#: stock/models.py:314 +#: stock/models.py:319 msgid "Select a matching supplier part for this stock item" msgstr "" -#: stock/models.py:318 +#: stock/models.py:323 msgid "Where is this stock item located?" msgstr "" -#: stock/models.py:322 +#: stock/models.py:327 msgid "Is this item installed in another item?" msgstr "" -#: stock/models.py:326 +#: stock/models.py:331 msgid "Item assigned to customer?" msgstr "" -#: stock/models.py:329 +#: stock/models.py:334 msgid "Serial number for this item" msgstr "" -#: stock/models.py:334 +#: stock/models.py:339 msgid "Batch code for this stock item" msgstr "" -#: stock/models.py:343 +#: stock/models.py:348 msgid "Build for this stock item" msgstr "" -#: stock/models.py:352 +#: stock/models.py:357 msgid "Purchase order for this stock item" msgstr "" -#: stock/models.py:363 +#: stock/models.py:374 msgid "Delete this Stock Item when stock is depleted" msgstr "" -#: stock/models.py:370 stock/templates/stock/item_notes.html:13 +#: stock/models.py:381 stock/templates/stock/item_notes.html:13 #: stock/templates/stock/item_notes.html:29 msgid "Stock Item Notes" msgstr "" -#: stock/models.py:464 +#: stock/models.py:516 msgid "Quantity must be integer" msgstr "" -#: stock/models.py:470 +#: stock/models.py:522 #, python-brace-format msgid "Quantity must not exceed available stock quantity ({n})" msgstr "" -#: stock/models.py:473 stock/models.py:476 +#: stock/models.py:525 stock/models.py:528 msgid "Serial numbers must be a list of integers" msgstr "" -#: stock/models.py:479 +#: stock/models.py:531 msgid "Quantity does not match serial numbers" msgstr "" -#: stock/models.py:489 +#: stock/models.py:541 msgid "Serial numbers already exist: " msgstr "" -#: stock/models.py:511 +#: stock/models.py:563 msgid "Add serial number" msgstr "" -#: stock/models.py:514 +#: stock/models.py:566 #, python-brace-format msgid "Serialized {n} items" msgstr "" -#: stock/models.py:814 +#: stock/models.py:866 msgid "Tracking entry title" msgstr "" -#: stock/models.py:816 +#: stock/models.py:868 msgid "Entry notes" msgstr "" -#: stock/models.py:818 +#: stock/models.py:870 msgid "Link to external page for further information" msgstr "" @@ -1968,51 +2250,69 @@ msgstr "" msgid "Stock Tracking Information" msgstr "" -#: stock/templates/stock/item_base.html:11 -msgid "Stock Item Details" +#: stock/templates/stock/item_base.html:8 +#: stock/templates/stock/item_base.html:56 +#: stock/templates/stock/stock_adjust.html:16 +msgid "Stock Item" msgstr "" -#: stock/templates/stock/item_base.html:56 +#: stock/templates/stock/item_base.html:20 +msgid "This stock item is allocated to Sales Order" +msgstr "" + +#: stock/templates/stock/item_base.html:26 +msgid "This stock item is allocated to Build" +msgstr "" + +#: stock/templates/stock/item_base.html:32 msgid "" "This stock item is serialized - it has a unique serial number and the " "quantity cannot be adjusted." msgstr "" -#: stock/templates/stock/item_base.html:60 +#: stock/templates/stock/item_base.html:36 msgid "This stock item cannot be deleted as it has child items" msgstr "" -#: stock/templates/stock/item_base.html:64 +#: stock/templates/stock/item_base.html:40 msgid "" "This stock item will be automatically deleted when all stock is depleted." msgstr "" -#: stock/templates/stock/item_base.html:69 +#: stock/templates/stock/item_base.html:45 msgid "This stock item was split from " msgstr "" -#: stock/templates/stock/item_base.html:89 +#: stock/templates/stock/item_base.html:105 +msgid "Stock Item Details" +msgstr "" + +#: stock/templates/stock/item_base.html:119 msgid "Belongs To" msgstr "" -#: stock/templates/stock/item_base.html:95 +#: stock/templates/stock/item_base.html:125 #: stock/templates/stock/stock_adjust.html:17 msgid "Location" msgstr "" -#: stock/templates/stock/item_base.html:102 +#: stock/templates/stock/item_base.html:132 +msgid "Unique Identifier" +msgstr "" + +#: stock/templates/stock/item_base.html:139 msgid "Serial Number" msgstr "" -#: stock/templates/stock/item_base.html:161 +#: stock/templates/stock/item_base.html:198 msgid "Last Updated" msgstr "" -#: stock/templates/stock/item_base.html:166 +#: stock/templates/stock/item_base.html:203 msgid "Last Stocktake" msgstr "" -#: stock/templates/stock/item_base.html:170 +#: stock/templates/stock/item_base.html:207 msgid "No stocktake performed" msgstr "" @@ -2055,12 +2355,7 @@ msgstr "" msgid "Stock Locations" msgstr "" -#: stock/templates/stock/stock_adjust.html:16 #: stock/templates/stock/stock_app_base.html:7 -msgid "Stock Item" -msgstr "" - -#: stock/templates/stock/stock_app_base.html:9 msgid "Stock Location" msgstr "" @@ -2223,6 +2518,10 @@ msgstr "" msgid "No results found" msgstr "" +#: templates/InvenTree/starred_parts.html:7 +msgid "Starred Parts" +msgstr "" + #: templates/about.html:13 msgid "InvenTree Version Information" msgstr "" @@ -2236,46 +2535,58 @@ msgid "InvenTree Version" msgstr "" #: templates/about.html:30 -msgid "Commit Hash" +msgid "Django Version" msgstr "" #: templates/about.html:34 -msgid "Commit Date" +msgid "Commit Hash" msgstr "" #: templates/about.html:38 +msgid "Commit Date" +msgstr "" + +#: templates/about.html:42 msgid "InvenTree Documentation" msgstr "" -#: templates/about.html:43 +#: templates/about.html:47 msgid "View Code on GitHub" msgstr "" -#: templates/about.html:47 +#: templates/about.html:51 msgid "Submit Bug Report" msgstr "" -#: templates/navbar.html:23 +#: templates/navbar.html:14 +msgid "Buy" +msgstr "" + +#: templates/navbar.html:22 +msgid "Sell" +msgstr "" + +#: templates/navbar.html:36 msgid "Admin" msgstr "" -#: templates/navbar.html:26 +#: templates/navbar.html:39 msgid "Settings" msgstr "" -#: templates/navbar.html:27 +#: templates/navbar.html:40 msgid "Logout" msgstr "" -#: templates/navbar.html:29 +#: templates/navbar.html:42 msgid "Login" msgstr "" -#: templates/navbar.html:32 +#: templates/navbar.html:45 msgid "About InvenTree" msgstr "" -#: templates/navbar.html:33 +#: templates/navbar.html:46 msgid "Statistics" msgstr "" @@ -2331,34 +2642,50 @@ msgstr "" msgid "Stock status" msgstr "" -#: templates/table_filters.html:53 +#: templates/table_filters.html:37 +msgid "Is allocated" +msgstr "" + +#: templates/table_filters.html:38 +msgid "Item has been alloacted" +msgstr "" + +#: templates/table_filters.html:58 msgid "Order status" msgstr "" -#: templates/table_filters.html:64 +#: templates/table_filters.html:69 msgid "Include subcategories" msgstr "" -#: templates/table_filters.html:65 +#: templates/table_filters.html:70 msgid "Include parts in subcategories" msgstr "" -#: templates/table_filters.html:69 +#: templates/table_filters.html:74 msgid "Active" msgstr "" -#: templates/table_filters.html:70 +#: templates/table_filters.html:75 msgid "Show active parts" msgstr "" -#: templates/table_filters.html:74 +#: templates/table_filters.html:79 msgid "Template" msgstr "" -#: templates/table_filters.html:78 +#: templates/table_filters.html:83 msgid "Stock available" msgstr "" -#: templates/table_filters.html:82 +#: templates/table_filters.html:87 msgid "Low stock" msgstr "" + +#: templates/table_filters.html:99 +msgid "Starred" +msgstr "" + +#: templates/table_filters.html:107 +msgid "Purchasable" +msgstr "" diff --git a/InvenTree/order/templates/order/sales_order_base.html b/InvenTree/order/templates/order/sales_order_base.html index 7ada7751d0..77db9793dc 100644 --- a/InvenTree/order/templates/order/sales_order_base.html +++ b/InvenTree/order/templates/order/sales_order_base.html @@ -37,6 +37,9 @@ src="{% static 'img/blank_image.png' %}" +
{% endblock %} From c9ea33e22e624bf59d08284f97cd2751bd9d1001 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 23 Apr 2020 10:58:26 +1000 Subject: [PATCH 058/104] Fix order of javascript table events --- InvenTree/order/templates/order/sales_order_detail.html | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/InvenTree/order/templates/order/sales_order_detail.html b/InvenTree/order/templates/order/sales_order_detail.html index 24c1c93929..e11956214e 100644 --- a/InvenTree/order/templates/order/sales_order_detail.html +++ b/InvenTree/order/templates/order/sales_order_detail.html @@ -45,6 +45,7 @@ $("#so-lines-table").inventreeTable({ allocations: true, }, url: "{% url 'api-so-line-list' %}", + onPostBody: setupCallbacks, detailViewByClick: true, detailView: true, detailFilter: function(index, row) { @@ -201,10 +202,9 @@ function reloadTable() { $("#so-lines-table").bootstrapTable("refresh"); } -// Called when the table is loaded -$("#so-lines-table").on('load-success.bs.table', function() { +function setupCallbacks(table) { - var table = $(this); + var table = $("#so-lines-table"); // Set up callbacks for the row buttons table.find(".button-edit").click(function() { @@ -255,7 +255,6 @@ $("#so-lines-table").on('load-success.bs.table', function() { }, }); }); - -}); +} {% endblock %} \ No newline at end of file From 5d71cf85ccf42bf7404a79b211ba12e45cd0369b Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 23 Apr 2020 16:00:09 +1000 Subject: [PATCH 059/104] Add separate 'quantity' and 'allocated' columns in sales order view --- InvenTree/order/templates/order/sales_order_detail.html | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/InvenTree/order/templates/order/sales_order_detail.html b/InvenTree/order/templates/order/sales_order_detail.html index e11956214e..a809f498b4 100644 --- a/InvenTree/order/templates/order/sales_order_detail.html +++ b/InvenTree/order/templates/order/sales_order_detail.html @@ -143,6 +143,11 @@ $("#so-lines-table").inventreeTable({ sortable: true, field: 'quantity', title: 'Quantity', + }, + { + sortable: true, + field: 'allocated', + title: 'Allocated', formatter: function(value, row, index, field) { return makeProgressBar(row.allocated, row.quantity, { id: `order-line-progress-${row.pk}`, From 426aa9258c59fac280ab5967277b97687d1d620e Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 23 Apr 2020 17:37:53 +1000 Subject: [PATCH 060/104] URL cleanup --- InvenTree/InvenTree/urls.py | 6 ++++ InvenTree/build/urls.py | 18 +++++------ InvenTree/order/urls.py | 63 ++++++++++++++----------------------- 3 files changed, 37 insertions(+), 50 deletions(-) diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index 7d9633ced6..cf53667fb9 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -73,11 +73,17 @@ settings_urls = [ url(r'^.*$', SettingsView.as_view(template_name='InvenTree/settings/user.html'), name='settings'), ] +dynamic_javascript_urls = [ +] + urlpatterns = [ url(r'^part/', include(part_urls)), url(r'^supplier-part/', include(supplier_part_urls)), url(r'^price-break/', include(price_break_urls)), + # "Dynamic" javascript files which are rendered using InvenTree templating. + url(r'^dynamic/', include(dynamic_javascript_urls)), + url(r'^common/', include(common_urls)), url(r'^stock/', include(stock_urls)), diff --git a/InvenTree/build/urls.py b/InvenTree/build/urls.py index 5d23c55a2d..d8cb3c03ea 100644 --- a/InvenTree/build/urls.py +++ b/InvenTree/build/urls.py @@ -6,16 +6,6 @@ from django.conf.urls import url, include from . import views -build_item_detail_urls = [ - url('^edit/?', views.BuildItemEdit.as_view(), name='build-item-edit'), - url('^delete/?', views.BuildItemDelete.as_view(), name='build-item-delete'), -] - -build_item_urls = [ - url(r'^(?P\d+)/', include(build_item_detail_urls)), - url('^new/', views.BuildItemCreate.as_view(), name='build-item-create'), -] - build_detail_urls = [ url(r'^edit/', views.BuildUpdate.as_view(), name='build-edit'), url(r'^allocate/', views.BuildAllocate.as_view(), name='build-allocate'), @@ -33,7 +23,13 @@ build_detail_urls = [ ] build_urls = [ - url(r'item/', include(build_item_urls)), + url(r'item/', include([ + url(r'^(?P\d+)/', include([ + url('^edit/?', views.BuildItemEdit.as_view(), name='build-item-edit'), + url('^delete/?', views.BuildItemDelete.as_view(), name='build-item-delete'), + ])), + url('^new/', views.BuildItemCreate.as_view(), name='build-item-create'), + ])), url(r'new/', views.BuildCreate.as_view(), name='build-create'), diff --git a/InvenTree/order/urls.py b/InvenTree/order/urls.py index f390c23f54..87b8e9e0e4 100644 --- a/InvenTree/order/urls.py +++ b/InvenTree/order/urls.py @@ -9,12 +9,6 @@ from django.conf.urls import url, include from . import views -purchase_order_attachment_urls = [ - url(r'^new/', views.PurchaseOrderAttachmentCreate.as_view(), name='po-attachment-create'), - url(r'^(?P\d+)/edit/', views.PurchaseOrderAttachmentEdit.as_view(), name='po-attachment-edit'), - url(r'^(?P\d+)/delete/', views.PurchaseOrderAttachmentDelete.as_view(), name='po-attachment-delete'), -] - purchase_order_detail_urls = [ url(r'^cancel/', views.PurchaseOrderCancel.as_view(), name='po-cancel'), @@ -31,19 +25,6 @@ purchase_order_detail_urls = [ url(r'^.*$', views.PurchaseOrderDetail.as_view(), name='po-detail'), ] -po_line_item_detail_urls = [ - - url(r'^edit/', views.POLineItemEdit.as_view(), name='po-line-item-edit'), - url(r'^delete/', views.POLineItemDelete.as_view(), name='po-line-item-delete'), -] - -po_line_urls = [ - - url(r'^new/', views.POLineItemCreate.as_view(), name='po-line-item-create'), - - url(r'^(?P\d+)/', include(po_line_item_detail_urls)), -] - purchase_order_urls = [ url(r'^new/', views.PurchaseOrderCreate.as_view(), name='po-create'), @@ -53,30 +34,24 @@ purchase_order_urls = [ # Display detail view for a single purchase order url(r'^(?P\d+)/', include(purchase_order_detail_urls)), - url(r'^line/', include(po_line_urls)), + url(r'^line/', include([ + url(r'^new/', views.POLineItemCreate.as_view(), name='po-line-item-create'), + url(r'^(?P\d+)/', include([ + url(r'^edit/', views.POLineItemEdit.as_view(), name='po-line-item-edit'), + url(r'^delete/', views.POLineItemDelete.as_view(), name='po-line-item-delete'), + ])), + ])), - url(r'^attachments/', include(purchase_order_attachment_urls)), + url(r'^attachments/', include([ + url(r'^new/', views.PurchaseOrderAttachmentCreate.as_view(), name='po-attachment-create'), + url(r'^(?P\d+)/edit/', views.PurchaseOrderAttachmentEdit.as_view(), name='po-attachment-edit'), + url(r'^(?P\d+)/delete/', views.PurchaseOrderAttachmentDelete.as_view(), name='po-attachment-delete'), + ])), # Display complete list of purchase orders url(r'^.*$', views.PurchaseOrderIndex.as_view(), name='po-index'), ] - -so_line_urls = [ - url(r'^new/', views.SOLineItemCreate.as_view(), name='so-line-item-create'), - url(r'^(?P\d+)/', include([ - url(r'^edit/', views.SOLineItemEdit.as_view(), name='so-line-item-edit'), - url(r'^delete/', views.SOLineItemDelete.as_view(), name='so-line-item-delete'), - ])), -] - -sales_order_attachment_urls = [ - url(r'^new/', views.SalesOrderAttachmentCreate.as_view(), name='so-attachment-create'), - url(r'^(?P\d+)/edit/', views.SalesOrderAttachmentEdit.as_view(), name='so-attachment-edit'), - url(r'^(?P\d+)/delete/', views.SalesOrderAttachmentDelete.as_view(), name='so-attachment-delete'), - -] - sales_order_detail_urls = [ url(r'^edit/', views.SalesOrderEdit.as_view(), name='so-edit'), @@ -91,7 +66,13 @@ sales_order_urls = [ url(r'^new/', views.SalesOrderCreate.as_view(), name='so-create'), - url(r'^line/', include(so_line_urls)), + url(r'^line/', include([ + url(r'^new/', views.SOLineItemCreate.as_view(), name='so-line-item-create'), + url(r'^(?P\d+)/', include([ + url(r'^edit/', views.SOLineItemEdit.as_view(), name='so-line-item-edit'), + url(r'^delete/', views.SOLineItemDelete.as_view(), name='so-line-item-delete'), + ])), + ])), # URLs for sales order allocations url(r'^allocation/', include([ @@ -102,7 +83,11 @@ sales_order_urls = [ ])), ])), - url(r'^attachments/', include(sales_order_attachment_urls)), + url(r'^attachments/', include([ + url(r'^new/', views.SalesOrderAttachmentCreate.as_view(), name='so-attachment-create'), + url(r'^(?P\d+)/edit/', views.SalesOrderAttachmentEdit.as_view(), name='so-attachment-edit'), + url(r'^(?P\d+)/delete/', views.SalesOrderAttachmentDelete.as_view(), name='so-attachment-delete'), + ])), # Display detail view for a single SalesOrder url(r'^(?P\d+)/', include(sales_order_detail_urls)), From 7f020cbbf61a8ac118c24db891dcaba08e1ce0e0 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 23 Apr 2020 19:41:14 +1000 Subject: [PATCH 061/104] Enbiggen a whole lotta buttons --- InvenTree/InvenTree/static/css/inventree.css | 32 ++++++++--------- .../InvenTree/static/script/inventree/part.js | 4 +-- .../build/templates/build/build_base.html | 14 ++++---- .../templates/company/company_base.html | 14 ++++---- .../order/templates/order/order_base.html | 14 ++++---- .../templates/order/sales_order_base.html | 6 ++-- InvenTree/part/templates/part/category.html | 14 ++++---- InvenTree/part/templates/part/part_base.html | 30 ++++++++-------- .../stock/templates/stock/item_base.html | 34 +++++++++---------- InvenTree/stock/templates/stock/location.html | 14 ++++---- InvenTree/templates/qr_button.html | 2 +- 11 files changed, 89 insertions(+), 89 deletions(-) diff --git a/InvenTree/InvenTree/static/css/inventree.css b/InvenTree/InvenTree/static/css/inventree.css index 591a4a3760..19b92e2817 100644 --- a/InvenTree/InvenTree/static/css/inventree.css +++ b/InvenTree/InvenTree/static/css/inventree.css @@ -111,24 +111,20 @@ color: rgb(13, 245, 25); } -.glyphicon-ok { - color: #5C5; +.icon-red { + color: #c55; } -.glyphicon-ok-circle { +.icon-green { + color: #5c5; +} + +.icon-blue { color: #55c; } -.glyphicon-remove { - color: #C55; -} - -.glyphicon-trash { - color: #C55; -} - -.glyphicon-plus { - color: #5C5; +.icon-yellow { + color: #CC2; } /* CSS overrides for treeview */ @@ -330,11 +326,15 @@ padding-bottom: 2px; } -.btn-large { - font-size: 150%; +.action-buttons .btn { + font-size: 175%; align-content: center; vertical-align: middle; -} + padding-left: 6px; + padding-right: 6px; + padding-top: 3px; + padding-bottom: 2px; +}; .badge { float: right; diff --git a/InvenTree/InvenTree/static/script/inventree/part.js b/InvenTree/InvenTree/static/script/inventree/part.js index b7e5fc3155..92460a51a7 100644 --- a/InvenTree/InvenTree/static/script/inventree/part.js +++ b/InvenTree/InvenTree/static/script/inventree/part.js @@ -50,7 +50,7 @@ function toggleStar(options) { { method: 'POST', success: function(response, status) { - $(options.button).removeClass('glyphicon-star-empty').addClass('glyphicon-star'); + $(options.button).addClass('icon-yellow'); }, } ); @@ -64,7 +64,7 @@ function toggleStar(options) { { method: 'DELETE', success: function(response, status) { - $(options.button).removeClass('glyphicon-star').addClass('glyphicon-star-empty'); + $(options.button).removeClass('icon-yellow'); }, } ); diff --git a/InvenTree/build/templates/build/build_base.html b/InvenTree/build/templates/build/build_base.html index cd238a2299..f7c8ace219 100644 --- a/InvenTree/build/templates/build/build_base.html +++ b/InvenTree/build/templates/build/build_base.html @@ -22,21 +22,21 @@ src="{% static 'img/blank_image.png' %}"

{{ build.quantity }} x {{ build.part.full_name }}

-
- {% if build.is_active %} - {% endif %} {% if build.status == BuildStatus.CANCELLED %} {% endif %}
diff --git a/InvenTree/company/templates/company/company_base.html b/InvenTree/company/templates/company/company_base.html index 8e1a743d10..da73a7b7b3 100644 --- a/InvenTree/company/templates/company/company_base.html +++ b/InvenTree/company/templates/company/company_base.html @@ -23,17 +23,17 @@ InvenTree | {% trans "Company" %} - {{ company.name }}

{{ company.name }}

{{ company.description }}

-
+
{% if company.is_supplier %} - {% endif %} - -
{% endblock %} diff --git a/InvenTree/order/templates/order/order_base.html b/InvenTree/order/templates/order/order_base.html index 3840457a5e..85d766a02e 100644 --- a/InvenTree/order/templates/order/order_base.html +++ b/InvenTree/order/templates/order/order_base.html @@ -26,27 +26,27 @@ src="{% static 'img/blank_image.png' %}"

{{ order.description }}

-
- - {% if order.status == OrderStatus.PENDING and order.lines.count > 0 %} - {% elif order.status == OrderStatus.PLACED %} - - {% endif %} {% if order.status == OrderStatus.PENDING or order.status == OrderStatus.PLACED %} - {% endif %} diff --git a/InvenTree/order/templates/order/sales_order_base.html b/InvenTree/order/templates/order/sales_order_base.html index 77db9793dc..85eaeb6ba8 100644 --- a/InvenTree/order/templates/order/sales_order_base.html +++ b/InvenTree/order/templates/order/sales_order_base.html @@ -33,11 +33,11 @@ src="{% static 'img/blank_image.png' %}"

{{ order }}

{{ order.description }}

-
- -
diff --git a/InvenTree/part/templates/part/category.html b/InvenTree/part/templates/part/category.html index 9382259cce..672edfb587 100644 --- a/InvenTree/part/templates/part/category.html +++ b/InvenTree/part/templates/part/category.html @@ -14,16 +14,16 @@

{% trans "All parts" %}

{% endif %}

-

- {% if category %} - - {% endif %}
diff --git a/InvenTree/part/templates/part/part_base.html b/InvenTree/part/templates/part/part_base.html index c4f0483ec2..0c329c72be 100644 --- a/InvenTree/part/templates/part/part_base.html +++ b/InvenTree/part/templates/part/part_base.html @@ -34,37 +34,37 @@

{{ part.description }}

-
- {% if part.is_template == False %} {% include "qr_button.html" %} {% if part.active %} - {% if not part.virtual %} - {% endif %} {% if part.purchaseable %} - {% endif %} {% endif %} {% endif %} - - {% if not part.active %} - {% endif %}
diff --git a/InvenTree/stock/templates/stock/item_base.html b/InvenTree/stock/templates/stock/item_base.html index e0a69e15fb..c90a062fa1 100644 --- a/InvenTree/stock/templates/stock/item_base.html +++ b/InvenTree/stock/templates/stock/item_base.html @@ -63,38 +63,38 @@ InvenTree | {% trans "Stock Item" %} - {{ item }} {% endif %} -
+
{% include "qr_button.html" %} {% if item.in_stock %} {% if not item.serialized %} - - - {% if item.part.trackable %} - {% endif %} {% endif %} - - {% endif %} - {% if item.can_delete %} - {% endif %}
diff --git a/InvenTree/stock/templates/stock/location.html b/InvenTree/stock/templates/stock/location.html index 7117e54c60..f7b5a7a70d 100644 --- a/InvenTree/stock/templates/stock/location.html +++ b/InvenTree/stock/templates/stock/location.html @@ -13,20 +13,20 @@

All stock items

{% endif %}

-

- {% if location %} {% include "qr_button.html" %} - {% endif %}
diff --git a/InvenTree/templates/qr_button.html b/InvenTree/templates/qr_button.html index 7aafd834bc..cc10e0cd26 100644 --- a/InvenTree/templates/qr_button.html +++ b/InvenTree/templates/qr_button.html @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file From 435c13cf7cc750ffb2a6cb5537b4a59d248eb5f9 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 23 Apr 2020 20:38:09 +1000 Subject: [PATCH 062/104] Separate concept of "OrderStatus" into "SalesOrderStatus" and "PurchaseOrderStatus" --- .../static/script/inventree/filters.js | 6 ++ .../static/script/inventree/order.js | 12 ++-- InvenTree/InvenTree/status_codes.py | 32 +++++++++- InvenTree/company/models.py | 14 ++--- InvenTree/company/views.py | 4 +- .../migrations/0028_auto_20200423_0956.py | 37 ++++++++++++ InvenTree/order/models.py | 59 ++++++++++--------- .../order/templates/order/order_base.html | 2 +- .../templates/order/purchase_orders.html | 2 +- .../templates/order/sales_order_base.html | 2 +- .../order/templates/order/sales_orders.html | 2 +- InvenTree/order/test_views.py | 12 ++-- InvenTree/order/tests.py | 12 ++-- InvenTree/order/views.py | 16 ++--- InvenTree/part/models.py | 8 +-- InvenTree/part/serializers.py | 4 +- InvenTree/part/templatetags/status_codes.py | 15 +++-- InvenTree/part/views.py | 7 +-- InvenTree/templates/table_filters.html | 16 ++++- 19 files changed, 171 insertions(+), 91 deletions(-) create mode 100644 InvenTree/order/migrations/0028_auto_20200423_0956.py diff --git a/InvenTree/InvenTree/static/script/inventree/filters.js b/InvenTree/InvenTree/static/script/inventree/filters.js index de04bcd86d..b29e787e18 100644 --- a/InvenTree/InvenTree/static/script/inventree/filters.js +++ b/InvenTree/InvenTree/static/script/inventree/filters.js @@ -18,6 +18,8 @@ function defaultFilters() { build: "", parts: "cascade=1", company: "", + salesorder: "", + purchaseorder: "", }; } @@ -258,6 +260,8 @@ function setupFilterList(tableKey, table, target) { var element = $(target); + console.log(tableKey + " - " + element); + // One blank slate, please element.empty(); @@ -298,6 +302,8 @@ function setupFilterList(tableKey, table, target) { element.find(`#filter-tag-${tableKey}`).on('change', function() { var list = element.find(`#filter-value-${tableKey}`); + console.log('index was changed!'); + list.replaceWith(generateFilterInput(tableKey, this.value)); }); diff --git a/InvenTree/InvenTree/static/script/inventree/order.js b/InvenTree/InvenTree/static/script/inventree/order.js index 1d331491ca..7e2b83a406 100644 --- a/InvenTree/InvenTree/static/script/inventree/order.js +++ b/InvenTree/InvenTree/static/script/inventree/order.js @@ -108,13 +108,13 @@ function loadPurchaseOrderTable(table, options) { options.params['supplier_detail'] = true; - var filters = loadTableFilters("order"); + var filters = loadTableFilters("purchaseorder"); for (var key in options.params) { filters[key] = options.params[key]; } - setupFilterList("order", $(table)); + setupFilterList("purchaseorder", $(table)); $(table).inventreeTable({ url: options.url, @@ -159,7 +159,7 @@ function loadPurchaseOrderTable(table, options) { field: 'status', title: 'Status', formatter: function(value, row, index, field) { - return orderStatusDisplay(row.status, row.status_text); + return purchaseOrderStatusDisplay(row.status, row.status_text); } }, { @@ -181,13 +181,13 @@ function loadSalesOrderTable(table, options) { options.params = options.params || {}; options.params['customer_detail'] = true; - var filters = loadTableFilters("table"); + var filters = loadTableFilters("salesorder"); for (var key in options.params) { filters[key] = options.params[key]; } - setupFilterList("order", $(table)); + setupFilterList("salesorder", $(table)); $(table).inventreeTable({ url: options.url, @@ -232,7 +232,7 @@ function loadSalesOrderTable(table, options) { field: 'status', title: 'Status', formatter: function(value, row, index, field) { - return orderStatusDisplay(row.status, row.status_text); + return salesOrderStatusDisplay(row.status, row.status_text); } }, { diff --git a/InvenTree/InvenTree/status_codes.py b/InvenTree/InvenTree/status_codes.py index fa2a4d9bfd..f1367b9c65 100644 --- a/InvenTree/InvenTree/status_codes.py +++ b/InvenTree/InvenTree/status_codes.py @@ -70,11 +70,14 @@ class StatusCode: raise ValueError("Label not found") -class OrderStatus(StatusCode): +class PurchaseOrderStatus(StatusCode): + """ + Defines a set of status codes for a PurchaseOrder + """ # Order status codes PENDING = 10 # Order is pending (not yet placed) - PLACED = 20 # Order has been placed + PLACED = 20 # Order has been placed with supplier COMPLETE = 30 # Order has been completed CANCELLED = 40 # Order was cancelled LOST = 50 # Order was lost @@ -112,6 +115,31 @@ class OrderStatus(StatusCode): ] +class SalesOrderStatus(StatusCode): + """ Defines a set of status codes for a SalesOrder """ + + PENDING = 10 # Order is pending + SHIPPED = 20 # Order has been shipped to customer + CANCELLED = 40 # Order has been cancelled + LOST = 50 # Order was lost + RETURNED = 60 # Order was returned + + options = { + PENDING: _("Pending"), + SHIPPED: _("Shipped"), + CANCELLED: _("Cancelled"), + LOST: _("Lost"), + RETURNED: _("Returned"), + } + + labels = { + PENDING: "primary", + SHIPPED: "success", + CANCELLED: "danger", + LOST: "warning", + RETURNED: "warning", + } + class StockStatus(StatusCode): OK = 10 # Item is OK diff --git a/InvenTree/company/models.py b/InvenTree/company/models.py index 3b8f058bfd..ec87619cdb 100644 --- a/InvenTree/company/models.py +++ b/InvenTree/company/models.py @@ -25,7 +25,7 @@ from stdimage.models import StdImageField from InvenTree.helpers import getMediaUrl, getBlankImage, getBlankThumbnail from InvenTree.helpers import normalize from InvenTree.fields import InvenTreeURLField, RoundingDecimalField -from InvenTree.status_codes import OrderStatus +from InvenTree.status_codes import PurchaseOrderStatus from common.models import Currency @@ -185,11 +185,11 @@ class Company(models.Model): def outstanding_purchase_orders(self): """ Return purchase orders which are 'outstanding' """ - return self.purchase_orders.filter(status__in=OrderStatus.OPEN) + return self.purchase_orders.filter(status__in=PurchaseOrderStatus.OPEN) def pending_purchase_orders(self): """ Return purchase orders which are PENDING (not yet issued) """ - return self.purchase_orders.filter(status=OrderStatus.PENDING) + return self.purchase_orders.filter(status=PurchaseOrderStatus.PENDING) def closed_purchase_orders(self): """ Return purchase orders which are not 'outstanding' @@ -199,15 +199,15 @@ class Company(models.Model): - Returned """ - return self.purchase_orders.exclude(status__in=OrderStatus.OPEN) + return self.purchase_orders.exclude(status__in=PurchaseOrderStatus.OPEN) def complete_purchase_orders(self): - return self.purchase_orders.filter(status=OrderStatus.COMPLETE) + return self.purchase_orders.filter(status=PurchaseOrderStatus.COMPLETE) def failed_purchase_orders(self): """ Return any purchase orders which were not successful """ - return self.purchase_orders.filter(status__in=OrderStatus.FAILED) + return self.purchase_orders.filter(status__in=PurchaseOrderStatus.FAILED) class Contact(models.Model): @@ -384,7 +384,7 @@ class SupplierPart(models.Model): limited to purchase orders that are open / outstanding. """ - return self.purchase_order_line_items.prefetch_related('order').filter(order__status__in=OrderStatus.OPEN) + return self.purchase_order_line_items.prefetch_related('order').filter(order__status__in=PurchaseOrderStatus.OPEN) def on_order(self): """ Return the total quantity of items currently on order. diff --git a/InvenTree/company/views.py b/InvenTree/company/views.py index ae88629505..5fe784ddc4 100644 --- a/InvenTree/company/views.py +++ b/InvenTree/company/views.py @@ -13,7 +13,7 @@ from django.urls import reverse from django.forms import HiddenInput from InvenTree.views import AjaxCreateView, AjaxUpdateView, AjaxDeleteView -from InvenTree.status_codes import OrderStatus +from InvenTree.status_codes import PurchaseOrderStatus from InvenTree.helpers import str2bool from common.models import Currency @@ -137,7 +137,6 @@ class CompanyDetail(DetailView): def get_context_data(self, **kwargs): ctx = super().get_context_data(**kwargs) - ctx['OrderStatus'] = OrderStatus return ctx @@ -244,7 +243,6 @@ class SupplierPartDetail(DetailView): def get_context_data(self, **kwargs): ctx = super().get_context_data(**kwargs) - ctx['OrderStatus'] = OrderStatus return ctx diff --git a/InvenTree/order/migrations/0028_auto_20200423_0956.py b/InvenTree/order/migrations/0028_auto_20200423_0956.py new file mode 100644 index 0000000000..cf9cd1e0e2 --- /dev/null +++ b/InvenTree/order/migrations/0028_auto_20200423_0956.py @@ -0,0 +1,37 @@ +# Generated by Django 3.0.5 on 2020-04-23 09:56 + +import InvenTree.fields +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('stock', '0031_auto_20200422_0209'), + ('order', '0027_auto_20200422_0236'), + ] + + operations = [ + migrations.AlterField( + model_name='purchaseorder', + name='status', + field=models.PositiveIntegerField(choices=[(10, 'Pending'), (20, 'Placed'), (30, 'Complete'), (40, 'Cancelled'), (50, 'Lost'), (60, 'Returned')], default=10, help_text='Purchase order status'), + ), + migrations.AlterField( + model_name='salesorder', + name='status', + field=models.PositiveIntegerField(choices=[(10, 'Pending'), (20, 'Shipped'), (40, 'Cancelled'), (50, 'Lost'), (60, 'Returned')], default=10, help_text='Purchase order status'), + ), + migrations.AlterField( + model_name='salesorderallocation', + name='item', + field=models.ForeignKey(help_text='Select stock item to allocate', limit_choices_to={'part__salable': True}, on_delete=django.db.models.deletion.CASCADE, related_name='sales_order_allocations', to='stock.StockItem'), + ), + migrations.AlterField( + model_name='salesorderallocation', + name='quantity', + field=InvenTree.fields.RoundingDecimalField(decimal_places=5, default=1, help_text='Enter stock allocation quantity', max_digits=15, validators=[django.core.validators.MinValueValidator(0)]), + ), + ] diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index e6b35e1921..1f8dd2f2ac 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -25,7 +25,7 @@ from company.models import Company, SupplierPart from InvenTree.fields import RoundingDecimalField from InvenTree.helpers import decimal2string, normalize -from InvenTree.status_codes import OrderStatus +from InvenTree.status_codes import PurchaseOrderStatus, SalesOrderStatus from InvenTree.models import InvenTreeAttachment @@ -76,9 +76,6 @@ class Order(models.Model): creation_date = models.DateField(blank=True, null=True) - status = models.PositiveIntegerField(default=OrderStatus.PENDING, choices=OrderStatus.items(), - help_text='Order status') - created_by = models.ForeignKey(User, on_delete=models.SET_NULL, blank=True, null=True, @@ -91,29 +88,6 @@ class Order(models.Model): notes = MarkdownxField(blank=True, help_text=_('Order notes')) - def place_order(self): - """ Marks the order as PLACED. Order must be currently PENDING. """ - - if self.status == OrderStatus.PENDING: - self.status = OrderStatus.PLACED - self.issue_date = datetime.now().date() - self.save() - - def complete_order(self): - """ Marks the order as COMPLETE. Order must be currently PLACED. """ - - if self.status == OrderStatus.PLACED: - self.status = OrderStatus.COMPLETE - self.complete_date = datetime.now().date() - self.save() - - def cancel_order(self): - """ Marks the order as CANCELLED. """ - - if self.status in [OrderStatus.PLACED, OrderStatus.PENDING]: - self.status = OrderStatus.CANCELLED - self.save() - class PurchaseOrder(Order): """ A PurchaseOrder represents goods shipped inwards from an external supplier. @@ -129,6 +103,9 @@ class PurchaseOrder(Order): def __str__(self): return "PO {ref} - {company}".format(ref=self.reference, company=self.supplier.name) + status = models.PositiveIntegerField(default=PurchaseOrderStatus.PENDING, choices=PurchaseOrderStatus.items(), + help_text='Purchase order status') + supplier = models.ForeignKey( Company, on_delete=models.CASCADE, limit_choices_to={ @@ -195,6 +172,29 @@ class PurchaseOrder(Order): line.save() + def place_order(self): + """ Marks the PurchaseOrder as PLACED. Order must be currently PENDING. """ + + if self.status == PurchaseOrderStatus.PENDING: + self.status = PurchaseOrderStatus.PLACED + self.issue_date = datetime.now().date() + self.save() + + def complete_order(self): + """ Marks the PurchaseOrder as COMPLETE. Order must be currently PLACED. """ + + if self.status == PurchaseOrderStatus.PLACED: + self.status = PurchaseOrderStatus.COMPLETE + self.complete_date = datetime.now().date() + self.save() + + def cancel_order(self): + """ Marks the PurchaseOrder as CANCELLED. """ + + if self.status in [PurchaseOrderStatus.PLACED, PurchaseOrderStatus.PENDING]: + self.status = PurchaseOrderStatus.CANCELLED + self.save() + def pending_line_items(self): """ Return a list of pending line items for this order. Any line item where 'received' < 'quantity' will be returned. @@ -213,7 +213,7 @@ class PurchaseOrder(Order): """ Receive a line item (or partial line item) against this PO """ - if not self.status == OrderStatus.PLACED: + if not self.status == PurchaseOrderStatus.PLACED: raise ValidationError({"status": _("Lines can only be received against an order marked as 'Placed'")}) try: @@ -275,6 +275,9 @@ class SalesOrder(Order): help_text=_("Customer"), ) + status = models.PositiveIntegerField(default=SalesOrderStatus.PENDING, choices=SalesOrderStatus.items(), + help_text='Purchase order status') + customer_reference = models.CharField(max_length=64, blank=True, help_text=_("Customer order reference code")) def is_fully_allocated(self): diff --git a/InvenTree/order/templates/order/order_base.html b/InvenTree/order/templates/order/order_base.html index 85d766a02e..0105e71e83 100644 --- a/InvenTree/order/templates/order/order_base.html +++ b/InvenTree/order/templates/order/order_base.html @@ -67,7 +67,7 @@ src="{% static 'img/blank_image.png' %}" {% trans "Order Status" %} - {% order_status order.status %} + {% purchase_order_status order.status %} diff --git a/InvenTree/order/templates/order/purchase_orders.html b/InvenTree/order/templates/order/purchase_orders.html index 5e77c01181..1019092151 100644 --- a/InvenTree/order/templates/order/purchase_orders.html +++ b/InvenTree/order/templates/order/purchase_orders.html @@ -15,7 +15,7 @@ InvenTree | {% trans "Purchase Orders" %}
-
+
diff --git a/InvenTree/order/templates/order/sales_order_base.html b/InvenTree/order/templates/order/sales_order_base.html index 85eaeb6ba8..234a040a31 100644 --- a/InvenTree/order/templates/order/sales_order_base.html +++ b/InvenTree/order/templates/order/sales_order_base.html @@ -56,7 +56,7 @@ src="{% static 'img/blank_image.png' %}" {% trans "Order Status" %} - {% order_status order.status %} + {% sales_order_status order.status %} diff --git a/InvenTree/order/templates/order/sales_orders.html b/InvenTree/order/templates/order/sales_orders.html index 394d45b3d8..4e29156773 100644 --- a/InvenTree/order/templates/order/sales_orders.html +++ b/InvenTree/order/templates/order/sales_orders.html @@ -15,7 +15,7 @@ InvenTree | {% trans "Sales Orders" %}
-
+
diff --git a/InvenTree/order/test_views.py b/InvenTree/order/test_views.py index bf3608e2b0..932cac9060 100644 --- a/InvenTree/order/test_views.py +++ b/InvenTree/order/test_views.py @@ -7,7 +7,7 @@ from django.test import TestCase from django.urls import reverse from django.contrib.auth import get_user_model -from InvenTree.status_codes import OrderStatus +from InvenTree.status_codes import PurchaseOrderStatus from .models import PurchaseOrder, PurchaseOrderLineItem @@ -53,7 +53,7 @@ class POTests(OrderViewTestCase): response = self.client.get(reverse('po-detail', args=(1,))) self.assertEqual(response.status_code, 200) keys = response.context.keys() - self.assertIn('OrderStatus', keys) + self.assertIn('PurchaseOrderStatus', keys) def test_po_create(self): """ Launch forms to create new PurchaseOrder""" @@ -91,7 +91,7 @@ class POTests(OrderViewTestCase): url = reverse('po-issue', args=(1,)) order = PurchaseOrder.objects.get(pk=1) - self.assertEqual(order.status, OrderStatus.PENDING) + self.assertEqual(order.status, PurchaseOrderStatus.PENDING) # Test without confirmation response = self.client.post(url, {'confirm': 0}, HTTP_X_REQUESTED_WITH='XMLHttpRequest') @@ -109,7 +109,7 @@ class POTests(OrderViewTestCase): # Test that the order was actually placed order = PurchaseOrder.objects.get(pk=1) - self.assertEqual(order.status, OrderStatus.PLACED) + self.assertEqual(order.status, PurchaseOrderStatus.PLACED) def test_line_item_create(self): """ Test the form for adding a new LineItem to a PurchaseOrder """ @@ -117,7 +117,7 @@ class POTests(OrderViewTestCase): # Record the number of line items in the PurchaseOrder po = PurchaseOrder.objects.get(pk=1) n = po.lines.count() - self.assertEqual(po.status, OrderStatus.PENDING) + self.assertEqual(po.status, PurchaseOrderStatus.PENDING) url = reverse('po-line-item-create') @@ -181,7 +181,7 @@ class TestPOReceive(OrderViewTestCase): super().setUp() self.po = PurchaseOrder.objects.get(pk=1) - self.po.status = OrderStatus.PLACED + self.po.status = PurchaseOrderStatus.PLACED self.po.save() self.url = reverse('po-receive', args=(1,)) diff --git a/InvenTree/order/tests.py b/InvenTree/order/tests.py index 8ad7422259..ca24b9586d 100644 --- a/InvenTree/order/tests.py +++ b/InvenTree/order/tests.py @@ -6,7 +6,7 @@ from .models import PurchaseOrder, PurchaseOrderLineItem from stock.models import StockLocation from company.models import SupplierPart -from InvenTree.status_codes import OrderStatus +from InvenTree.status_codes import PurchaseOrderStatus class OrderTest(TestCase): @@ -57,7 +57,7 @@ class OrderTest(TestCase): order = PurchaseOrder.objects.get(pk=1) - self.assertEqual(order.status, OrderStatus.PENDING) + self.assertEqual(order.status, PurchaseOrderStatus.PENDING) self.assertEqual(order.lines.count(), 3) sku = SupplierPart.objects.get(SKU='ACME-WIDGET') @@ -104,14 +104,14 @@ class OrderTest(TestCase): self.assertEqual(len(order.pending_line_items()), 3) # Should fail, as order is 'PENDING' not 'PLACED" - self.assertEqual(order.status, OrderStatus.PENDING) + self.assertEqual(order.status, PurchaseOrderStatus.PENDING) with self.assertRaises(django_exceptions.ValidationError): order.receive_line_item(line, loc, 50, user=None) order.place_order() - self.assertEqual(order.status, OrderStatus.PLACED) + self.assertEqual(order.status, PurchaseOrderStatus.PLACED) order.receive_line_item(line, loc, 50, user=None) @@ -134,9 +134,9 @@ class OrderTest(TestCase): order.receive_line_item(line, loc, 500, user=None) self.assertEqual(part.on_order, 800) - self.assertEqual(order.status, OrderStatus.PLACED) + self.assertEqual(order.status, PurchaseOrderStatus.PLACED) for line in order.pending_line_items(): order.receive_line_item(line, loc, line.quantity, user=None) - self.assertEqual(order.status, OrderStatus.COMPLETE) + self.assertEqual(order.status, PurchaseOrderStatus.COMPLETE) diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index 1b91f6e9b4..79edc22bd6 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -29,7 +29,7 @@ from . import forms as order_forms from InvenTree.views import AjaxView, AjaxCreateView, AjaxUpdateView, AjaxDeleteView from InvenTree.helpers import DownloadFile, str2bool -from InvenTree.status_codes import OrderStatus +from InvenTree.status_codes import PurchaseOrderStatus logger = logging.getLogger(__name__) @@ -52,8 +52,6 @@ class PurchaseOrderIndex(ListView): def get_context_data(self, **kwargs): ctx = super().get_context_data(**kwargs) - ctx['OrderStatus'] = OrderStatus - return ctx @@ -74,8 +72,6 @@ class PurchaseOrderDetail(DetailView): def get_context_data(self, **kwargs): ctx = super().get_context_data(**kwargs) - ctx['OrderStatus'] = OrderStatus - return ctx @@ -280,7 +276,7 @@ class PurchaseOrderCreate(AjaxCreateView): def get_initial(self): initials = super().get_initial().copy() - initials['status'] = OrderStatus.PENDING + initials['status'] = PurchaseOrderStatus.PENDING supplier_id = self.request.GET.get('supplier', None) @@ -310,7 +306,7 @@ class SalesOrderCreate(AjaxCreateView): def get_initial(self): initials = super().get_initial().copy() - initials['status'] = OrderStatus.PENDING + initials['status'] = PurchaseOrderStatus.PENDING customer_id = self.request.GET.get('customer', None) @@ -343,7 +339,7 @@ class PurchaseOrderEdit(AjaxUpdateView): order = self.get_object() # Prevent user from editing supplier if there are already lines in the order - if order.lines.count() > 0 or not order.status == OrderStatus.PENDING: + if order.lines.count() > 0 or not order.status == PurchaseOrderStatus.PENDING: form.fields['supplier'].widget = HiddenInput() return form @@ -455,7 +451,7 @@ class PurchaseOrderComplete(AjaxUpdateView): if confirm: po = self.get_object() - po.status = OrderStatus.COMPLETE + po.status = PurchaseOrderStatus.COMPLETE po.save() data = { @@ -1024,7 +1020,7 @@ class POLineItemCreate(AjaxCreateView): # Limit the available to orders to ones that are PENDING query = form.fields['order'].queryset - query = query.filter(status=OrderStatus.PENDING) + query = query.filter(status=PurchaseOrderStatus.PENDING) form.fields['order'].queryset = query order_id = form['order'].value() diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 79445fd02d..597c9a277c 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -39,7 +39,7 @@ from InvenTree.models import InvenTreeTree, InvenTreeAttachment from InvenTree.fields import InvenTreeURLField from InvenTree.helpers import decimal2string, normalize -from InvenTree.status_codes import BuildStatus, StockStatus, OrderStatus +from InvenTree.status_codes import BuildStatus, StockStatus, PurchaseOrderStatus from company.models import SupplierPart @@ -955,18 +955,18 @@ class Part(models.Model): def open_purchase_orders(self): """ Return a list of open purchase orders against this part """ - return [order for order in self.purchase_orders() if order.status in OrderStatus.OPEN] + return [order for order in self.purchase_orders() if order.status in PurchaseOrderStatus.OPEN] def closed_purchase_orders(self): """ Return a list of closed purchase orders against this part """ - return [order for order in self.purchase_orders() if order.status not in OrderStatus.OPEN] + return [order for order in self.purchase_orders() if order.status not in PurchaseOrderStatus.OPEN] @property def on_order(self): """ Return the total number of items on order for this part. """ - orders = self.supplier_parts.filter(purchase_order_line_items__order__status__in=OrderStatus.OPEN).aggregate( + orders = self.supplier_parts.filter(purchase_order_line_items__order__status__in=PurchaseOrderStatus.OPEN).aggregate( quantity=Sum('purchase_order_line_items__quantity'), received=Sum('purchase_order_line_items__received') ) diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index 1ee58062fe..352d6c9546 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -15,7 +15,7 @@ from decimal import Decimal from django.db.models import Q, Sum from django.db.models.functions import Coalesce -from InvenTree.status_codes import StockStatus, OrderStatus, BuildStatus +from InvenTree.status_codes import StockStatus, PurchaseOrderStatus, BuildStatus from InvenTree.serializers import InvenTreeModelSerializer @@ -120,7 +120,7 @@ class PartSerializer(InvenTreeModelSerializer): stock_filter = Q(stock_items__status__in=StockStatus.AVAILABLE_CODES) # Filter to limit orders to "open" - order_filter = Q(supplier_parts__purchase_order_line_items__order__status__in=OrderStatus.OPEN) + order_filter = Q(supplier_parts__purchase_order_line_items__order__status__in=PurchaseOrderStatus.OPEN) # Filter to limit builds to "active" build_filter = Q(builds__status__in=BuildStatus.ACTIVE_CODES) diff --git a/InvenTree/part/templatetags/status_codes.py b/InvenTree/part/templatetags/status_codes.py index 18f84032ce..704bcd3b7c 100644 --- a/InvenTree/part/templatetags/status_codes.py +++ b/InvenTree/part/templatetags/status_codes.py @@ -4,14 +4,20 @@ Provide templates for the various model status codes. from django import template from django.utils.safestring import mark_safe -from InvenTree.status_codes import OrderStatus, StockStatus, BuildStatus +from InvenTree.status_codes import PurchaseOrderStatus, SalesOrderStatus +from InvenTree.status_codes import StockStatus, BuildStatus register = template.Library() @register.simple_tag -def order_status(key, *args, **kwargs): - return mark_safe(OrderStatus.render(key)) +def purchase_order_status(key, *args, **kwargs): + return mark_safe(PurchaseOrderStatus.render(key)) + + +@register.simple_tag +def sales_order_status(key, *args, **kwargs): + return mark_safe(SalesOrderStatus.render(key)) @register.simple_tag @@ -30,7 +36,8 @@ def load_status_codes(context): Make the various StatusCodes available to the page context """ - context['order_status_codes'] = OrderStatus.list() + context['purchase_order_status_codes'] = PurchaseOrderStatus.list() + context['sales_order_status_codes'] = SalesOrderStatus.list() context['stock_status_codes'] = StockStatus.list() context['build_status_codes'] = BuildStatus.list() diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index b36d097185..fd38356fea 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -38,7 +38,7 @@ from InvenTree.views import AjaxView, AjaxCreateView, AjaxUpdateView, AjaxDelete from InvenTree.views import QRCodeView from InvenTree.helpers import DownloadFile, str2bool -from InvenTree.status_codes import OrderStatus, BuildStatus +from InvenTree.status_codes import PurchaseOrderStatus, BuildStatus class PartIndex(ListView): @@ -561,8 +561,6 @@ class PartNotes(UpdateView): ctx['starred'] = part.isStarredBy(self.request.user) ctx['disabled'] = not part.active - ctx['OrderStatus'] = OrderStatus - return ctx @@ -593,9 +591,6 @@ class PartDetail(DetailView): context['starred'] = part.isStarredBy(self.request.user) context['disabled'] = not part.active - context['OrderStatus'] = OrderStatus - context['BuildStatus'] = BuildStatus - return context diff --git a/InvenTree/templates/table_filters.html b/InvenTree/templates/table_filters.html index 8dff5ed1be..ccaea8ecab 100644 --- a/InvenTree/templates/table_filters.html +++ b/InvenTree/templates/table_filters.html @@ -7,7 +7,8 @@ {% include "status_codes.html" with label='stock' options=stock_status_codes %} {% include "status_codes.html" with label='build' options=build_status_codes %} -{% include "status_codes.html" with label='order' options=order_status_codes %} +{% include "status_codes.html" with label='purchaseOrder' options=purchase_order_status_codes %} +{% include "status_codes.html" with label='salesOrder' options=sales_order_status_codes %} function getAvailableTableFilters(tableKey) { @@ -52,11 +53,20 @@ function getAvailableTableFilters(tableKey) { } // Filters for the "Order" table - if (tableKey == "order") { + if (tableKey == "purchaseorder") { return { status: { title: '{% trans "Order status" %}', - options: orderCodes, + options: purchaseOrderCodes, + }, + }; + } + + if (tableKey == "salesorder") { + return { + status: { + title: '{% trans "Order status" %}', + options: salesOrderCodes, }, }; } From e384f9e94cc1fe6b749ed849b4ae31f51d315a3a Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 23 Apr 2020 20:42:59 +1000 Subject: [PATCH 063/104] Order date adjustment Sales order now has a "shipment date" --- .../migrations/0029_auto_20200423_1042.py | 30 +++++++++++++++++++ InvenTree/order/models.py | 17 ++++++++--- InvenTree/order/serializers.py | 3 +- 3 files changed, 44 insertions(+), 6 deletions(-) create mode 100644 InvenTree/order/migrations/0029_auto_20200423_1042.py diff --git a/InvenTree/order/migrations/0029_auto_20200423_1042.py b/InvenTree/order/migrations/0029_auto_20200423_1042.py new file mode 100644 index 0000000000..2f7e0072ca --- /dev/null +++ b/InvenTree/order/migrations/0029_auto_20200423_1042.py @@ -0,0 +1,30 @@ +# Generated by Django 3.0.5 on 2020-04-23 10:42 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('order', '0028_auto_20200423_0956'), + ] + + operations = [ + migrations.RenameField( + model_name='salesorder', + old_name='complete_date', + new_name='shipment_date', + ), + migrations.RemoveField( + model_name='salesorder', + name='issue_date', + ), + migrations.AddField( + model_name='salesorder', + name='shipped_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index 1f8dd2f2ac..136402f471 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -82,10 +82,6 @@ class Order(models.Model): related_name='+' ) - issue_date = models.DateField(blank=True, null=True) - - complete_date = models.DateField(blank=True, null=True) - notes = MarkdownxField(blank=True, help_text=_('Order notes')) @@ -124,6 +120,10 @@ class PurchaseOrder(Order): related_name='+' ) + issue_date = models.DateField(blank=True, null=True) + + complete_date = models.DateField(blank=True, null=True) + def get_absolute_url(self): return reverse('po-detail', kwargs={'pk': self.id}) @@ -280,6 +280,15 @@ class SalesOrder(Order): customer_reference = models.CharField(max_length=64, blank=True, help_text=_("Customer order reference code")) + shipment_date = models.DateField(blank=True, null=True) + + shipped_by = models.ForeignKey( + User, + on_delete=models.SET_NULL, + blank=True, null=True, + related_name='+' + ) + def is_fully_allocated(self): """ Return True if all line items are fully allocated """ diff --git a/InvenTree/order/serializers.py b/InvenTree/order/serializers.py index 6f85be6ef0..df3b68c924 100644 --- a/InvenTree/order/serializers.py +++ b/InvenTree/order/serializers.py @@ -123,8 +123,7 @@ class SalesOrderSerializer(InvenTreeModelSerializer): fields = [ 'pk', - 'issue_date', - 'complete_date', + 'shipment_date', 'creation_date', 'description', 'line_items', From e5fa94b4f80762ebf83ddf69b848511d20bba33a Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 23 Apr 2020 21:38:40 +1000 Subject: [PATCH 064/104] Add functionality to cancel a sales order --- InvenTree/order/forms.py | 11 +++++ InvenTree/order/models.py | 40 +++++++++++++++++++ .../order/templates/order/order_cancel.html | 4 +- .../templates/order/sales_order_base.html | 20 ++++++++-- .../templates/order/sales_order_cancel.html | 9 +++++ InvenTree/order/urls.py | 1 + InvenTree/order/views.py | 32 +++++++++++++++ InvenTree/part/templatetags/status_codes.py | 17 ++------ InvenTree/templates/table_filters.html | 2 - 9 files changed, 116 insertions(+), 20 deletions(-) create mode 100644 InvenTree/order/templates/order/sales_order_cancel.html diff --git a/InvenTree/order/forms.py b/InvenTree/order/forms.py index 43a8d4a529..cc7535bac9 100644 --- a/InvenTree/order/forms.py +++ b/InvenTree/order/forms.py @@ -50,6 +50,17 @@ class CancelPurchaseOrderForm(HelperForm): fields = [ 'confirm', ] + + +class CancelSalesOrderForm(HelperForm): + + confirm = forms.BooleanField(required=False, help_text=_('Cancel order')) + + class Meta: + model = SalesOrder + fields = [ + 'confirm', + ] class ReceivePurchaseOrderForm(HelperForm): diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index 136402f471..85eabfa3d0 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -289,6 +289,10 @@ class SalesOrder(Order): related_name='+' ) + @property + def is_pending(self): + return self.status == SalesOrderStatus.PENDING + def is_fully_allocated(self): """ Return True if all line items are fully allocated """ @@ -298,6 +302,42 @@ class SalesOrder(Order): return True + @transaction.atomic + def ship_order(self, user): + """ Mark this order as 'shipped' """ + + if not self.status == SalesOrderStatus.PENDING: + return False + + # Ensure the order status is marked as "Shipped" + self.status = SalesOrderStatus.SHIPPED + self.shipment_date = datetime.now().date() + self.shipped_by = user + self.save() + + return True + + @transaction.atomic + def cancel_order(self): + """ + Cancel this order (only if it is "pending") + + - Mark the order as 'cancelled' + - Delete any StockItems which have been allocated + """ + + if not self.status == SalesOrderStatus.PENDING: + return False + + self.status = SalesOrderStatus.CANCELLED + self.save() + + for line in self.lines.all(): + for allocation in line.allocations.all(): + allocation.delete() + + return True + class PurchaseOrderAttachment(InvenTreeAttachment): """ diff --git a/InvenTree/order/templates/order/order_cancel.html b/InvenTree/order/templates/order/order_cancel.html index 3c71028b06..91707ae737 100644 --- a/InvenTree/order/templates/order/order_cancel.html +++ b/InvenTree/order/templates/order/order_cancel.html @@ -1,7 +1,9 @@ {% extends "modal_form.html" %} +{% load i18n %} + {% block pre_form_content %} -Cancelling this order means that the order will no longer be editable. +{% trans "Cancelling this order means that the order will no longer be editable." %} {% endblock %} \ No newline at end of file diff --git a/InvenTree/order/templates/order/sales_order_base.html b/InvenTree/order/templates/order/sales_order_base.html index 234a040a31..5cba99a0cb 100644 --- a/InvenTree/order/templates/order/sales_order_base.html +++ b/InvenTree/order/templates/order/sales_order_base.html @@ -27,6 +27,7 @@ src="{% static 'img/blank_image.png' %}" /> {% endblock %} + {% block page_data %}

{% trans "Sales Order" %}


@@ -40,6 +41,11 @@ src="{% static 'img/blank_image.png' %}" + {% if order.is_pending %} + + {% endif %}
{% endblock %} @@ -82,11 +88,11 @@ src="{% static 'img/blank_image.png' %}" {% trans "Created" %} {{ order.creation_date }}{{ order.created_by }} - {% if order.issue_date %} + {% if order.shipment_date %} - - {% trans "Issued" %} - {{ order.issue_date }} + + {% trans "Shipped" %} + {{ order.shipment_date }}{{ order.shipped_by }} {% endif %} {% if order.status == OrderStatus.COMPLETE %} @@ -108,4 +114,10 @@ $("#edit-order").click(function() { }); }); +$("#cancel-order").click(function() { + launchModalForm("{% url 'so-cancel' order.id %}", { + reload: true, + }); +}); + {% endblock %} \ No newline at end of file diff --git a/InvenTree/order/templates/order/sales_order_cancel.html b/InvenTree/order/templates/order/sales_order_cancel.html new file mode 100644 index 0000000000..91707ae737 --- /dev/null +++ b/InvenTree/order/templates/order/sales_order_cancel.html @@ -0,0 +1,9 @@ +{% extends "modal_form.html" %} + +{% load i18n %} + +{% block pre_form_content %} + +{% trans "Cancelling this order means that the order will no longer be editable." %} + +{% endblock %} \ No newline at end of file diff --git a/InvenTree/order/urls.py b/InvenTree/order/urls.py index 87b8e9e0e4..b6b57e9518 100644 --- a/InvenTree/order/urls.py +++ b/InvenTree/order/urls.py @@ -55,6 +55,7 @@ purchase_order_urls = [ sales_order_detail_urls = [ url(r'^edit/', views.SalesOrderEdit.as_view(), name='so-edit'), + url(r'^cancel/', views.SalesOrderCancel.as_view(), name='so-cancel'), url(r'^attachments/', views.SalesOrderDetail.as_view(template_name='order/so_attachments.html'), name='so-attachments'), url(r'^notes/', views.SalesOrderNotes.as_view(), name='so-notes'), diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index 79edc22bd6..9f150246b7 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -394,6 +394,38 @@ class PurchaseOrderCancel(AjaxUpdateView): return self.renderJsonResponse(request, form, data) +class SalesOrderCancel(AjaxUpdateView): + """ View for cancelling a sales order """ + + model = SalesOrder + ajax_form_title = _("Cancel sales order") + ajax_template_name = "order/sales_order_cancel.html" + form_class = order_forms.CancelSalesOrderForm + + def post(self, request, *args, **kwargs): + + order = self.get_object() + form = self.get_form() + + confirm = str2bool(request.POST.get('confirm', False)) + + valid = False + + if not confirm: + forms.errors['confirm'] = [_('Confirm order cancellation')] + else: + valid = True + + data = { + 'form_valid': valid, + } + + if valid: + order.cancel_order() + + return self.renderJsonResponse(request, form, data) + + class PurchaseOrderIssue(AjaxUpdateView): """ View for changing a purchase order from 'PENDING' to 'ISSUED' """ diff --git a/InvenTree/part/templatetags/status_codes.py b/InvenTree/part/templatetags/status_codes.py index 704bcd3b7c..6ede4415e2 100644 --- a/InvenTree/part/templatetags/status_codes.py +++ b/InvenTree/part/templatetags/status_codes.py @@ -30,16 +30,7 @@ def build_status(key, *args, **kwargs): return mark_safe(BuildStatus.render(key)) -@register.simple_tag(takes_context=True) -def load_status_codes(context): - """ - Make the various StatusCodes available to the page context - """ - - context['purchase_order_status_codes'] = PurchaseOrderStatus.list() - context['sales_order_status_codes'] = SalesOrderStatus.list() - context['stock_status_codes'] = StockStatus.list() - context['build_status_codes'] = BuildStatus.list() - - # Need to return something as the result is rendered to the page - return '' +@register.simple_tag +def sales_order_codes(*args, **kwargs): + print("doing") + return "hello world" \ No newline at end of file diff --git a/InvenTree/templates/table_filters.html b/InvenTree/templates/table_filters.html index ccaea8ecab..31264f461a 100644 --- a/InvenTree/templates/table_filters.html +++ b/InvenTree/templates/table_filters.html @@ -1,8 +1,6 @@ {% load i18n %} {% load status_codes %} -{% load_status_codes %} - - -{% endblock %} - {% block js_ready %} {{ block.super }} @@ -45,16 +42,149 @@ InvenTree | Allocate Parts return quantity; } + function getUnallocated(row) { + // Return the number of items remaining to be allocated for a given row + return {{ build.quantity }} * row.quantity - sumAllocations(row); + } + + function reloadTable() { + // Reload the build allocation table + buildTable.bootstrapTable('refresh'); + } + + function setupCallbacks() { + // Register button callbacks once the table data are loaded + + buildTable.find(".button-add").click(function() { + var pk = $(this).attr('pk'); + + // Extract row data from the table + var idx = $(this).closest('tr').attr('data-index'); + var row = buildTable.bootstrapTable('getData')[idx]; + + launchModalForm('/build/item/new/', { + success: reloadTable, + data: { + part: row.sub_part, + build: {{ build.id }}, + quantity: getUnallocated(row), + }, + }); + }); + + + buildTable.find(".button-build").click(function() { + // Start a new build for the sub_part + + var pk = $(this).attr('pk'); + + // Extract row data from the table + var idx = $(this).closest('tr').attr('data-index'); + var row = buildTable.bootstrapTable('getData')[idx]; + + launchModalForm('/build/new/', { + follow: true, + data: { + part: row.sub_part, + parent: {{ build.id }}, + quantity: getUnallocated(row), + }, + }); + + }); + + buildTable.find(".button-buy").click(function() { + var pk = $(this).attr('pk'); + + // Extract row data from the table + var idx = $(this).closest('tr').attr('data-index'); + var row = buildTable.bootstrapTable('getData')[idx]; + + launchModalForm("{% url 'order-parts' %}", { + data: { + parts: [row.sub_part], + }, + }); + }); + } + buildTable.inventreeTable({ uniqueId: 'sub_part', url: "{% url 'api-bom-list' %}", + onPostBody: setupCallbacks, detailViewByClick: true, detailView: true, detailFilter: function(index, row) { return row.allocations != null; }, detailFormatter: function(index, row, element) { - return "Hello world"; + // Construct an 'inner table' which shows the stock allocations + + var subTableId = `allocation-table-${row.pk}`; + + var html = `
`; + + element.html(html); + + var lineItem = row; + + var subTable = $(`#${subTableId}`); + + subTable.bootstrapTable({ + data: row.allocations, + showHeader: false, + columns: [ + { + width: '50%', + field: 'quantity', + title: 'Quantity', + formatter: function(value, row, index, field) { + return renderLink(value, `/stock/item/${row.stock_item}/`); + }, + }, + { + field: 'location', + title: '{% trans "Location" %}', + formatter: function(value, row, index, field) { + return renderLink(row.stock_item_detail.location_name, `/stock/location/${row.stock_item_detail.location}/`); + } + }, + { + field: 'buttons', + title: 'Actions', + formatter: function(value, row) { + + var pk = row.pk; + + var html = `
`; + + {% if build.status == BuildStatus.PENDING %} + html += makeIconButton('fa-edit', 'button-allocation-edit', pk, '{% trans "Edit stock allocation" %}'); + html += makeIconButton('fa-trash-alt', 'button-allocation-delete', pk, '{% trans "Delete stock allocation" %}'); + {% endif %} + + html += `
`; + + return html; + }, + }, + ] + }); + + // Assign button callbacks to the newly created allocation buttons + subTable.find(".button-allocation-edit").click(function() { + var pk = $(this).attr('pk'); + launchModalForm(`/build/item/${pk}/edit/`, { + success: reloadTable, + }); + }); + + subTable.find('.button-allocation-delete').click(function() { + var pk = $(this).attr('pk'); + launchModalForm(`/build/item/${pk}/delete/`, { + success: reloadTable, + }); + }); }, formatNoMatches: function() { return "{% trans 'No BOM items found' %}"; }, onLoadSuccess: function(tableData) { @@ -172,7 +302,7 @@ InvenTree | Allocate Parts html += makeIconButton('fa-shopping-cart', 'button-buy', pk, '{% trans "Buy parts" %}'); } - if (row.sub_part.assembly) { + if (row.sub_part_detail.assembly) { html += makeIconButton('fa-tools', 'button-build', pk, '{% trans "Build parts" %}'); } diff --git a/InvenTree/build/templates/build/allocate_view.html b/InvenTree/build/templates/build/allocate_view.html deleted file mode 100644 index ed404a8fc1..0000000000 --- a/InvenTree/build/templates/build/allocate_view.html +++ /dev/null @@ -1,42 +0,0 @@ -{% load i18n %} -{% load inventree_extras %} - -

{% trans "Allocated Parts" %}

-
- -
-
- - -
-
- -
- - - - - - - - - - - - - - {% for item in build.required_parts %} - - - - - - - - - {% endfor %} - -
{% trans "Part" %}{% trans "Description" %}{% trans "Available" %}{% trans "Required" %}{% trans "Allocated" %}{% trans "On Order" %}
- {% include "hover_image.html" with image=item.part.image hover=True %} - {{ item.part.full_name }} - {{ item.part.description }}{% decimal item.part.total_stock %}{% decimal item.quantity %}{{ item.allocated }}{% decimal item.part.on_order %}
\ No newline at end of file diff --git a/InvenTree/build/templates/build/build_base.html b/InvenTree/build/templates/build/build_base.html index f83b6c3e79..20eb3b0672 100644 --- a/InvenTree/build/templates/build/build_base.html +++ b/InvenTree/build/templates/build/build_base.html @@ -36,7 +36,7 @@ src="{% static 'img/blank_image.png' %}" {% if build.is_active %} + {% if 0 %}{% endif %} +
@@ -317,21 +318,7 @@ InvenTree | Allocate Parts ], }); - {% if editing %} - - {% for bom_item in bom_items.all %} - - loadAllocationTable( - $("#allocate-table-id-{{ bom_item.sub_part.id }}"), - {{ bom_item.sub_part.id }}, - "{{ bom_item.sub_part.full_name }}", - "{% url 'api-build-item-list' %}?build={{ build.id }}&part={{ bom_item.sub_part.id }}", - {% multiply build.quantity bom_item.quantity %}, - $("#new-item-{{ bom_item.sub_part.id }}") - ); - - {% endfor %} - + {% if 0 %} $("#auto-allocate-build").on('click', function() { launchModalForm( "{% url 'build-auto-allocate' build.id %}", @@ -340,8 +327,9 @@ InvenTree | Allocate Parts } ); }); + {% endif %} - $('#unallocate-build').on('click', function() { + $('#btn-unallocate').on('click', function() { launchModalForm( "{% url 'build-unallocate' build.id %}", { @@ -350,15 +338,6 @@ InvenTree | Allocate Parts ); }); - {% else %} - - $("#build-list").inventreeTable({ - }); - - $("#btn-allocate").click(function() { - location.href = "{% url 'build-allocate' build.id %}?edit=1"; - }); - $("#btn-order-parts").click(function() { launchModalForm("/order/purchase-order/order-parts/", { data: { @@ -367,6 +346,4 @@ InvenTree | Allocate Parts }); }); - {% endif %} - {% endblock %} diff --git a/InvenTree/build/templates/build/allocate_edit.html b/InvenTree/build/templates/build/allocate_edit.html deleted file mode 100644 index ca6990ee00..0000000000 --- a/InvenTree/build/templates/build/allocate_edit.html +++ /dev/null @@ -1,34 +0,0 @@ -{% load i18n %} -{% load inventree_extras %} - -
-

{% trans "Allocate Stock to Build" %}

-
-
-
-
- - -
-
-
-
- -
-
-

{% trans "Part" %}

-
-
-

{% trans "Available" %}

-
-
-

{% trans "Required" %}

-
-
-

{% trans "Allocated" %}

-
-
- -{% for bom_item in bom_items.all %} -{% include "build/allocation_item.html" with item=bom_item build=build collapse_id=bom_item.id %} -{% endfor %} diff --git a/InvenTree/build/templates/build/allocation_item.html b/InvenTree/build/templates/build/allocation_item.html deleted file mode 100644 index 0492e44280..0000000000 --- a/InvenTree/build/templates/build/allocation_item.html +++ /dev/null @@ -1,34 +0,0 @@ -{% extends "collapse.html" %} - -{% load static %} -{% load inventree_extras %} - -{% block collapse_panel_setup %}class='panel part-allocation' id='allocation-panel-{{ item.sub_part.id }}'{% endblock %} - -{% block collapse_title %} - {% include "hover_image.html" with image=item.sub_part.image hover=false %} -
- {{ item.sub_part.full_name }} - {{ item.sub_part.description }} -
-{% endblock %} - -{% block collapse_heading %} -
- {% decimal item.sub_part.total_stock %} -
-
- {% multiply build.quantity item.quantity %}{% if item.overage %} (+ {{ item.overage }}){% endif %} -
-
- {% part_allocation_count build item.sub_part %} -
- -
-
-{% endblock %} - -{% block collapse_content %} - -
-{% endblock %} \ No newline at end of file From 4f0efec39f4be995c896b6a506b034b16719ee32 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 25 Apr 2020 23:24:00 +1000 Subject: [PATCH 084/104] PEP and unit testing fixes --- InvenTree/build/fixtures/build.yaml | 10 +++++++++- InvenTree/build/tests.py | 2 +- InvenTree/stock/models.py | 1 - 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/InvenTree/build/fixtures/build.yaml b/InvenTree/build/fixtures/build.yaml index c46afe625c..47e77dff07 100644 --- a/InvenTree/build/fixtures/build.yaml +++ b/InvenTree/build/fixtures/build.yaml @@ -10,6 +10,10 @@ status: 10 # PENDING creation_date: '2019-03-16' link: http://www.google.com + level: 0 + lft: 0 + rght: 0 + tree_id: 0 - model: build.build fields: @@ -19,4 +23,8 @@ status: 40 # COMPLETE quantity: 21 notes: 'Some more simple notes' - creation_date: '2019-03-16' \ No newline at end of file + creation_date: '2019-03-16' + level: 0 + lft: 0 + rght: 0 + tree_id: 1 \ No newline at end of file diff --git a/InvenTree/build/tests.py b/InvenTree/build/tests.py index acd614425a..42b953fe20 100644 --- a/InvenTree/build/tests.py +++ b/InvenTree/build/tests.py @@ -39,7 +39,7 @@ class BuildTestSimple(TestCase): self.assertEqual(b.batch, 'B2') self.assertEqual(b.quantity, 21) - self.assertEqual(str(b), 'Build 21 x Orphan - A part without a category') + self.assertEqual(str(b), '21 x Orphan') def test_url(self): b1 = Build.objects.get(pk=1) diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 5607854589..cc2c08b2d0 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -433,7 +433,6 @@ class StockItem(MPTTModel): return max(self.quantity - self.allocation_count(), 0) - def can_delete(self): """ Can this stock item be deleted? It can NOT be deleted under the following circumstances: From 81f789d857e0222fb84e42c94bd0081f2d41af96 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 25 Apr 2020 23:26:56 +1000 Subject: [PATCH 085/104] Add link to parent build --- InvenTree/build/templates/build/build_base.html | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/InvenTree/build/templates/build/build_base.html b/InvenTree/build/templates/build/build_base.html index 20eb3b0672..9ea112911f 100644 --- a/InvenTree/build/templates/build/build_base.html +++ b/InvenTree/build/templates/build/build_base.html @@ -14,6 +14,11 @@ InvenTree | {% trans "Build" %} - {{ build }} {% trans "This build is allocated to Sales Order" %} {{ build.sales_order }}
{% endif %} +{% if build.parent %} +
+ {% trans "This build is a child of Build" %} {{ build.parent }} +
+{% endif %} {% endblock %} {% block thumbnail %} From 50dbebdf5987b74890134a7f673732f8aec96e5d Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 25 Apr 2020 23:44:03 +1000 Subject: [PATCH 086/104] Improve rendering of BuildComplete template --- InvenTree/build/models.py | 24 ++++++++ InvenTree/build/templates/build/complete.html | 59 +++++++++---------- 2 files changed, 51 insertions(+), 32 deletions(-) diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index 2b97b2d6d4..6d5ac73081 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -319,6 +319,30 @@ class Build(MPTTModel): self.status = BuildStatus.COMPLETE self.save() + def isFullyAllocated(self): + """ + Return True if this build has been fully allocated. + """ + + bom_items = self.part.bom_items.all() + + for item in bom_items: + part = item.sub_part + + print("Checking:", part) + + if not self.isPartFullyAllocated(part): + return False + + return True + + def isPartFullyAllocated(self, part): + """ + Check if a given Part is fully allocated for this Build + """ + + return self.getAllocatedQuantity(part) >= self.getRequiredQuantity(part) + def getRequiredQuantity(self, part): """ Calculate the quantity of required to make this build. """ diff --git a/InvenTree/build/templates/build/complete.html b/InvenTree/build/templates/build/complete.html index 230092d766..a75cee2785 100644 --- a/InvenTree/build/templates/build/complete.html +++ b/InvenTree/build/templates/build/complete.html @@ -1,42 +1,37 @@ {% extends "modal_form.html" %} +{% load i18n %} {% block pre_form_content %} -Build: {{ build.title }} - {{ build.quantity }} x {{ build.part.full_name }} -
-Are you sure you want to mark this build as complete? -
-{% if taking %} -The following items will be removed from stock: - - - - - - - -{% for item in taking %} - - - - - - -{% endfor %} -
PartQuantityLocation
- {% include "hover_image.html" with image=item.stock_item.part.image hover=True %} - - {{ item.stock_item.part.full_name }}
- {{ item.stock_item.part.description }} -
{{ item.quantity }}{{ item.stock_item.location }}
+

{% trans "Build" %} - {{ build }}

+ +{% if not build.isFullyAllocated %} +
+

{% trans "Warning: Build order allocation is not complete" %}

+ {% trans "Build Order has not been fully allocated. Ensure that all Stock Items have been allocated to the Build" %} +
{% else %} -No parts have been allocated to this build. +
+

{% trans "Build order allocation is complete" %}

+
{% endif %} -
-The following items will be created: + +
+

{% trans "The following actions will be performed:" %}

+
    +
  • {% trans "Remove allocated items from stock" %}
  • +
  • {% trans "Add completed items to stock" %}
  • +
+
+
- {% include "hover_image.html" with image=build.part.image hover=True %} - {{ build.quantity }} x {{ build.part.full_name }} +
+ {% trans "The following items will be created" %} +
+
+ {% include "hover_image.html" with image=build.part.image hover=True %} + {{ build.quantity }} x {{ build.part.full_name }} +
{% endblock %} \ No newline at end of file From 72c43d0c2d9b22bd3d73b762cfc5a89ab321ae53 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 25 Apr 2020 23:59:28 +1000 Subject: [PATCH 087/104] Bug fix for build completion form --- .../migrations/0015_auto_20200425_1350.py | 26 +++++++++++++++++++ InvenTree/build/models.py | 6 +---- InvenTree/build/templates/build/allocate.html | 11 ++++++++ InvenTree/build/views.py | 8 +++--- InvenTree/part/templates/part/tabs.html | 2 +- 5 files changed, 44 insertions(+), 9 deletions(-) create mode 100644 InvenTree/build/migrations/0015_auto_20200425_1350.py diff --git a/InvenTree/build/migrations/0015_auto_20200425_1350.py b/InvenTree/build/migrations/0015_auto_20200425_1350.py new file mode 100644 index 0000000000..4f57df066e --- /dev/null +++ b/InvenTree/build/migrations/0015_auto_20200425_1350.py @@ -0,0 +1,26 @@ +# Generated by Django 3.0.5 on 2020-04-25 13:50 + +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion +import mptt.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('build', '0014_auto_20200425_1243'), + ] + + operations = [ + migrations.AlterField( + model_name='build', + name='parent', + field=mptt.fields.TreeForeignKey(blank=True, help_text='Parent build to which this build is allocated', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='children', to='build.Build', verbose_name='Parent Build'), + ), + migrations.AlterField( + model_name='builditem', + name='quantity', + field=models.DecimalField(decimal_places=5, default=1, help_text='Stock quantity to allocate to build', max_digits=15, validators=[django.core.validators.MinValueValidator(0)]), + ), + ] diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index 6d5ac73081..0f2084f019 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -329,8 +329,6 @@ class Build(MPTTModel): for item in bom_items: part = item.sub_part - print("Checking:", part) - if not self.isPartFullyAllocated(part): return False @@ -353,8 +351,6 @@ class Build(MPTTModel): except BomItem.DoesNotExist: q = 0 - print("required quantity:", q, "*", self.quantity) - return q * self.quantity def getAllocatedQuantity(self, part): @@ -499,6 +495,6 @@ class BuildItem(models.Model): decimal_places=5, max_digits=15, default=1, - validators=[MinValueValidator(1)], + validators=[MinValueValidator(0)], help_text=_('Stock quantity to allocate to build') ) diff --git a/InvenTree/build/templates/build/allocate.html b/InvenTree/build/templates/build/allocate.html index 725e612947..55ce042087 100644 --- a/InvenTree/build/templates/build/allocate.html +++ b/InvenTree/build/templates/build/allocate.html @@ -70,6 +70,17 @@ InvenTree | Allocate Parts build: {{ build.id }}, quantity: getUnallocated(row), }, + secondary: [ + { + field: 'stock_item', + label: '{% trans "New Stock Item" %}', + title: '{% trans "Create new Stock Item"', + url: '{% url "stock-item-create" %}', + data: { + part: row.sub_part, + }, + }, + ] }); }); diff --git a/InvenTree/build/views.py b/InvenTree/build/views.py index 3db0a7503e..6f651a7628 100644 --- a/InvenTree/build/views.py +++ b/InvenTree/build/views.py @@ -261,13 +261,13 @@ class BuildComplete(AjaxUpdateView): try: location = StockLocation.objects.get(id=loc_id) valid = True - except StockLocation.DoesNotExist: + except (ValueError, StockLocation.DoesNotExist): form.errors['location'] = [_('Invalid location selected')] serials = [] if build.part.trackable: - # A build for a trackable part must specify serial numbers + # A build for a trackable part may optionally specify serial numbers. sn = request.POST.get('serial_numbers', '') @@ -295,7 +295,9 @@ class BuildComplete(AjaxUpdateView): valid = False if valid: - build.completeBuild(location, serials, request.user) + if not build.completeBuild(location, serials, request.user): + form.non_field_errors = [('Build could not be completed')] + valid = False data = { 'form_valid': valid, diff --git a/InvenTree/part/templates/part/tabs.html b/InvenTree/part/templates/part/tabs.html index 4f23e345b6..b612106706 100644 --- a/InvenTree/part/templates/part/tabs.html +++ b/InvenTree/part/templates/part/tabs.html @@ -25,7 +25,7 @@ {% trans "BOM" %}{{ part.bom_count }} - {% trans "Build" %}{{ part.active_builds|length }} + {% trans "Build Orders" %}{{ part.active_builds|length }} {% endif %} {% if part.component or part.used_in_count > 0 %} From 0892b160c6efe02da4eb50d8ea6e7d6be8a24b4f Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 26 Apr 2020 00:32:09 +1000 Subject: [PATCH 088/104] "Fixes" for completing a build - This will require a lot of unit testing to get right --- InvenTree/InvenTree/status_codes.py | 5 +- InvenTree/build/models.py | 46 +++++++++++-------- InvenTree/order/models.py | 9 ++-- InvenTree/stock/api.py | 6 +-- .../migrations/0032_stockitem_build_order.py | 20 ++++++++ InvenTree/stock/models.py | 28 ++++++++++- 6 files changed, 85 insertions(+), 29 deletions(-) create mode 100644 InvenTree/stock/migrations/0032_stockitem_build_order.py diff --git a/InvenTree/InvenTree/status_codes.py b/InvenTree/InvenTree/status_codes.py index bc57f5d3f1..a9f0048867 100644 --- a/InvenTree/InvenTree/status_codes.py +++ b/InvenTree/InvenTree/status_codes.py @@ -168,7 +168,9 @@ class StockStatus(StatusCode): # This can be used as a quick check for filtering NOT_IN_STOCK = 100 - SHIPPED = 110 # Item has been shipped to a customer + SENT_TO_CUSTOMER = 110 # Item has been shipped to a customer + ASSIGNED_TO_BUILD = 120 + ASSIGNED_TO_OTHER_ITEM = 130 options = { OK: _("OK"), @@ -176,7 +178,6 @@ class StockStatus(StatusCode): DAMAGED: _("Damaged"), DESTROYED: _("Destroyed"), LOST: _("Lost"), - SHIPPED: _("Shipped"), RETURNED: _("Returned"), } diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index 0f2084f019..28bcc6e62e 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -21,7 +21,7 @@ from markdownx.models import MarkdownxField from mptt.models import MPTTModel, TreeForeignKey -from InvenTree.status_codes import BuildStatus +from InvenTree.status_codes import BuildStatus, StockStatus from InvenTree.fields import InvenTreeURLField from InvenTree.helpers import decimal2string @@ -261,32 +261,18 @@ class Build(MPTTModel): - Delete pending BuildItem objects """ - for item in self.allocated_stock.all().prefetch_related('stock_item'): - - # Subtract stock from the item - item.stock_item.take_stock( - item.quantity, - user, - 'Removed {n} items to build {m} x {part}'.format( - n=item.quantity, - m=self.quantity, - part=self.part.full_name - ) - ) + print("Complete build...") - # Delete the item - item.delete() - - # Mark the date of completion - self.completion_date = datetime.now().date() - - self.completed_by = user + # Complete the build allocation for each BuildItem + for build_item in self.allocated_stock.all().prefetch_related('stock_item'): + build_item.complete_allocation(user) notes = 'Built {q} on {now}'.format( q=self.quantity, now=str(datetime.now().date()) ) + # Generate the build outputs if self.part.trackable and serial_numbers: # Add new serial numbers for serial in serial_numbers: @@ -316,9 +302,13 @@ class Build(MPTTModel): item.save() # Finally, mark the build as complete + self.completion_date = datetime.now().date() + self.completed_by = user self.status = BuildStatus.COMPLETE self.save() + return True + def isFullyAllocated(self): """ Return True if this build has been fully allocated. @@ -477,6 +467,22 @@ class BuildItem(models.Model): if len(errors) > 0: raise ValidationError(errors) + def complete_allocation(self, user): + + item = self.stock_item + + # Split the allocated stock if there are more available than allocated + if item.quantity > self.quantity: + item = item.splitStock(self.quantity, None, user) + + # Update our own reference to the new item + self.stock_item = item + self.save() + + item.status = StockStatus.ASSIGNED_TO_BUILD + item.build_order = self.build + item.save() + build = models.ForeignKey( Build, on_delete=models.CASCADE, diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index 7e56858126..beed4457e6 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -25,7 +25,7 @@ from company.models import Company, SupplierPart from InvenTree.fields import RoundingDecimalField from InvenTree.helpers import decimal2string, normalize -from InvenTree.status_codes import PurchaseOrderStatus, SalesOrderStatus +from InvenTree.status_codes import PurchaseOrderStatus, SalesOrderStatus, StockStatus from InvenTree.models import InvenTreeAttachment @@ -574,12 +574,15 @@ class SalesOrderAllocation(models.Model): # Grab a copy of the new stock item (which will keep track of its "parent") item = item.splitStock(self.quantity, None, user) + # Update our own reference to the new item + self.item = item + self.save() + # Assign the StockItem to the SalesOrder customer item.customer = self.line.order.customer # Clear the location item.location = None + item.status = StockStatus.SENT_TO_CUSTOMER item.save() - - print("Finalizing allocation for: " + str(self.item)) diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index 9b7518580e..75deff6bd3 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -314,7 +314,6 @@ class StockList(generics.ListCreateAPIView): - POST: Create a new StockItem Additional query parameters are available: - - aggregate: If 'true' then stock items are aggregated by Part and Location - location: Filter stock by location - category: Filter by parts belonging to a certain category - supplier: Filter by supplier @@ -370,10 +369,10 @@ class StockList(generics.ListCreateAPIView): if in_stock: # Filter out parts which are not actually "in stock" - stock_list = stock_list.filter(customer=None, belongs_to=None) + stock_list = stock_list.filter(customer=None, belongs_to=None, build_order=None) else: # Only show parts which are not in stock - stock_list = stock_list.exclude(customer=None, belongs_to=None) + stock_list = stock_list.exclude(customer=None, belongs_to=None, build_order=None) # Filter by 'allocated' patrs? allocated = self.request.query_params.get('allocated', None) @@ -516,6 +515,7 @@ class StockList(generics.ListCreateAPIView): 'belongs_to', 'build', 'sales_order', + 'build_order', ] diff --git a/InvenTree/stock/migrations/0032_stockitem_build_order.py b/InvenTree/stock/migrations/0032_stockitem_build_order.py new file mode 100644 index 0000000000..849178c39c --- /dev/null +++ b/InvenTree/stock/migrations/0032_stockitem_build_order.py @@ -0,0 +1,20 @@ +# Generated by Django 3.0.5 on 2020-04-25 14:02 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('build', '0015_auto_20200425_1350'), + ('stock', '0031_auto_20200422_0209'), + ] + + operations = [ + migrations.AddField( + model_name='stockitem', + name='build_order', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='stock_items', to='build.Build'), + ), + ] diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index cc2c08b2d0..89103703c6 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -130,6 +130,7 @@ class StockItem(MPTTModel): purchase_order: Link to a PurchaseOrder (if this stock item was created from a PurchaseOrder) infinite: If True this StockItem can never be exhausted sales_order: Link to a SalesOrder object (if the StockItem has been assigned to a SalesOrder) + build_order: Link to a BuildOrder object (if the StockItem has been assigned to a BuildOrder) """ def save(self, *args, **kwargs): @@ -363,6 +364,13 @@ class StockItem(MPTTModel): related_name='stock_items', null=True, blank=True) + build_order = models.ForeignKey( + 'build.Build', + on_delete=models.SET_NULL, + related_name='stock_items', + null=True, blank=True + ) + # last time the stock was checked / counted stocktake_date = models.DateField(blank=True, null=True) @@ -439,6 +447,8 @@ class StockItem(MPTTModel): - Has child StockItems - Has a serial number and is tracked - Is installed inside another StockItem + - It has been delivered to a customer + - It has been assigned to a BuildOrder """ if self.child_count > 0: @@ -447,6 +457,12 @@ class StockItem(MPTTModel): if self.part.trackable and self.serial is not None: return False + if self.customer is not None: + return False + + if self.build_order is not None: + return False + return True @property @@ -464,7 +480,16 @@ class StockItem(MPTTModel): @property def in_stock(self): - if self.belongs_to or self.customer: + # Not 'in stock' if it has been installed inside another StockItem + if self.belongs_to is not None: + return False + + # Not 'in stock' if it has been sent to a customer + if self.customer is not None: + return False + + # Not 'in stock' if it has been allocated to a BuildOrder + if self.build_order is not None: return False return True @@ -642,6 +667,7 @@ class StockItem(MPTTModel): self.take_stock(quantity, user, 'Split {n} items into new stock item'.format(n=quantity)) # Return a copy of the "new" stock item + return new_stock @transaction.atomic def move(self, location, notes, user, **kwargs): From 1f4bd95d758db4e2388b180f637963e26a033790 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 26 Apr 2020 08:50:37 +1000 Subject: [PATCH 089/104] Remove the problematic migration entirely - The thumbnail check code is run every time the server is started anyway! --- .../migrations/0034_auto_20200404_1238.py | 30 ++++++------------- 1 file changed, 9 insertions(+), 21 deletions(-) diff --git a/InvenTree/part/migrations/0034_auto_20200404_1238.py b/InvenTree/part/migrations/0034_auto_20200404_1238.py index b93fb64607..afd463d30d 100644 --- a/InvenTree/part/migrations/0034_auto_20200404_1238.py +++ b/InvenTree/part/migrations/0034_auto_20200404_1238.py @@ -1,32 +1,20 @@ # Generated by Django 2.2.10 on 2020-04-04 12:38 from django.db import migrations -from django.db.utils import OperationalError, ProgrammingError - -from part.models import Part -from stdimage.utils import render_variations def create_thumbnails(apps, schema_editor): """ Create thumbnails for all existing Part images. - """ - try: - for part in Part.objects.all(): - # Render thumbnail for each existing Part - if part.image: - try: - part.image.render_variations() - except FileNotFoundError: - print("Missing image:", part.image()) - # The image is missing, so clear the field - part.image = None - part.save() - - except (OperationalError, ProgrammingError): - # Migrations have not yet been applied - table does not exist - print("Could not generate Part thumbnails") + Note: This functionality is now performed in apps.py, + as running the thumbnail script here caused too many database level errors. + + This migration is left here to maintain the database migration history + + """ + pass + class Migration(migrations.Migration): @@ -35,5 +23,5 @@ class Migration(migrations.Migration): ] operations = [ - migrations.RunPython(create_thumbnails), + migrations.RunPython(create_thumbnails, reverse_code=create_thumbnails), ] From ae4717401f2770b3dc659c884f8cedb3e5a436a7 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 26 Apr 2020 08:56:36 +1000 Subject: [PATCH 090/104] Add "sudo" to makefile --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index cb38a601d8..630784becf 100644 --- a/Makefile +++ b/Makefile @@ -31,12 +31,12 @@ superuser: # Install pre-requisites for mysql setup mysql: - apt-get install mysql-server libmysqlclient-dev + sudo apt-get install mysql-server libmysqlclient-dev pip3 install mysqlclient # Install pre-requisites for postgresql setup postgresql: - apt-get install postgresql postgresql-contrib libpq-dev + sudo apt-get install postgresql postgresql-contrib libpq-dev pip3 install psycopg2 # Update translation files From 4147163418b6f325c518521eb478fe2456851cfe Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 26 Apr 2020 15:29:21 +1000 Subject: [PATCH 091/104] Improve status code label rendering --- InvenTree/InvenTree/static/css/inventree.css | 51 ++++++++++++++ .../static/script/inventree/order.js | 7 +- InvenTree/InvenTree/status_codes.py | 67 +++++++++---------- InvenTree/build/api.py | 17 +++-- InvenTree/order/models.py | 2 +- InvenTree/order/serializers.py | 1 + InvenTree/part/templates/part/tabs.html | 6 +- .../stock/templates/stock/item_base.html | 18 +++-- InvenTree/templates/status_codes.html | 6 +- 9 files changed, 122 insertions(+), 53 deletions(-) diff --git a/InvenTree/InvenTree/static/css/inventree.css b/InvenTree/InvenTree/static/css/inventree.css index 72e5dbeeb5..2a1215d7a8 100644 --- a/InvenTree/InvenTree/static/css/inventree.css +++ b/InvenTree/InvenTree/static/css/inventree.css @@ -3,6 +3,12 @@ --secondary-color: #b69c80; --highlight-color: #f5efe8; --basic-color: #333; + + --label-red: #e35a57; + --label-blue: #4194bd; + --label-green: #50aa51; + --label-grey: #aaa; + --label-yellow: #fdc82a; } .markdownx .row { @@ -158,6 +164,51 @@ padding-bottom: 5px; } +.label-large-red { + color: var(--label-red); + border-color: var(--label-red); +} + +.label-red { + background: var(--label-red); +} + +.label-large-blue { + color: var(--label-blue); + border-color: var(--label-blue); +} + +.label-blue { + background: var(--label-blue); +} + +.label-large-green { + color: var(--label-green); + border-color: var(--label-green); +} + +.label-green { + background: var(--label-green); +} + +.label-large-grey { + color: var(--label-grey); + border-color: var(--label-grey); +} + +.label-grey { + background: var(--label-grey); +} + +.label-large-yellow { + color: var(--label-yellow); + border-color: var(--label-yellow); +} + +.label-yellow { + background: var(--label-yellow); +} + .label-right { float: right; margin-left: 3px; diff --git a/InvenTree/InvenTree/static/script/inventree/order.js b/InvenTree/InvenTree/static/script/inventree/order.js index 7e2b83a406..c4e39d9e1d 100644 --- a/InvenTree/InvenTree/static/script/inventree/order.js +++ b/InvenTree/InvenTree/static/script/inventree/order.js @@ -238,7 +238,12 @@ function loadSalesOrderTable(table, options) { { sortable: true, field: 'creation_date', - title: 'Date', + title: 'Creation Date', + }, + { + sortable: true, + field: 'shipment_date', + title: "Shipment Date", }, { sortable: true, diff --git a/InvenTree/InvenTree/status_codes.py b/InvenTree/InvenTree/status_codes.py index a9f0048867..efb76b86fa 100644 --- a/InvenTree/InvenTree/status_codes.py +++ b/InvenTree/InvenTree/status_codes.py @@ -7,14 +7,6 @@ class StatusCode: This is used to map a set of integer values to text. """ - # Colors used for label rendering - LBL_WHITE = '#FFF' - LBL_GREY = "#AAA" - LBL_GREEN = "#50aa51" - LBL_BLUE = "#4194bd" - LBL_YELLOW = "#fdc82a" - LBL_RED = "#e35a57" - @classmethod def render(cls, key, large=False): """ @@ -26,18 +18,15 @@ class StatusCode: return key value = cls.options.get(key, key) - color = cls.colors.get(key, StatusCode.LBL_GREY) + color = cls.colors.get(key, 'grey') if large: - span_class = 'label label-large' - style = 'color: {c}; border-color: {c}; background: none;'.format(c=color) + span_class = 'label label-large label-large-{c}'.format(c=color) else: - span_class = 'label' - style = 'color: {w}; background: {c}'.format(w=StatusCode.LBL_WHITE, c=color) + span_class = 'label label-{c}'.format(c=color) - return "{value}".format( + return "{value}".format( cl=span_class, - st=style, value=value ) @@ -107,12 +96,12 @@ class PurchaseOrderStatus(StatusCode): } colors = { - PENDING: StatusCode.LBL_BLUE, - PLACED: StatusCode.LBL_BLUE, - COMPLETE: StatusCode.LBL_GREEN, - CANCELLED: StatusCode.LBL_RED, - LOST: StatusCode.LBL_YELLOW, - RETURNED: StatusCode.LBL_YELLOW, + PENDING: 'blue', + PLACED: 'blue', + COMPLETE: 'green', + CANCELLED: 'red', + LOST: 'yellow', + RETURNED: 'yellow', } # Open orders @@ -147,11 +136,11 @@ class SalesOrderStatus(StatusCode): } colors = { - PENDING: StatusCode.LBL_BLUE, - SHIPPED: StatusCode.LBL_GREEN, - CANCELLED: StatusCode.LBL_RED, - LOST: StatusCode.LBL_YELLOW, - RETURNED: StatusCode.LBL_YELLOW, + PENDING: 'blue', + SHIPPED: 'green', + CANCELLED: 'red', + LOST: 'yellow', + RETURNED: 'yellow', } @@ -168,7 +157,7 @@ class StockStatus(StatusCode): # This can be used as a quick check for filtering NOT_IN_STOCK = 100 - SENT_TO_CUSTOMER = 110 # Item has been shipped to a customer + SHIPPED = 110 # Item has been shipped to a customer ASSIGNED_TO_BUILD = 120 ASSIGNED_TO_OTHER_ITEM = 130 @@ -179,13 +168,19 @@ class StockStatus(StatusCode): DESTROYED: _("Destroyed"), LOST: _("Lost"), RETURNED: _("Returned"), + SHIPPED: _('Shipped'), + ASSIGNED_TO_BUILD: _("Used for Build"), + ASSIGNED_TO_OTHER_ITEM: _("Installed in Stock Item") } colors = { - OK: StatusCode.LBL_GREEN, - ATTENTION: StatusCode.LBL_YELLOW, - DAMAGED: StatusCode.LBL_RED, - DESTROYED: StatusCode.LBL_RED, + OK: 'green', + ATTENTION: 'yellow', + DAMAGED: 'red', + DESTROYED: 'red', + SHIPPED: 'green', + ASSIGNED_TO_BUILD: 'blue', + ASSIGNED_TO_OTHER_ITEM: 'blue', } # The following codes correspond to parts that are 'available' or 'in stock' @@ -201,6 +196,8 @@ class StockStatus(StatusCode): DESTROYED, LOST, SHIPPED, + ASSIGNED_TO_BUILD, + ASSIGNED_TO_OTHER_ITEM, ] @@ -220,10 +217,10 @@ class BuildStatus(StatusCode): } colors = { - PENDING: StatusCode.LBL_BLUE, - ALLOCATED: StatusCode.LBL_BLUE, - COMPLETE: StatusCode.LBL_GREEN, - CANCELLED: StatusCode.LBL_RED, + PENDING: 'blue', + ALLOCATED: 'blue', + COMPLETE: 'green', + CANCELLED: 'red', } ACTIVE_CODES = [ diff --git a/InvenTree/build/api.py b/InvenTree/build/api.py index de44f1185c..1c0f91570e 100644 --- a/InvenTree/build/api.py +++ b/InvenTree/build/api.py @@ -38,7 +38,6 @@ class BuildList(generics.ListCreateAPIView): ] filter_fields = [ - 'part', 'sales_order', ] @@ -48,15 +47,25 @@ class BuildList(generics.ListCreateAPIView): as some of the fields don't natively play nicely with DRF """ - build_list = super().get_queryset() + queryset = super().get_queryset().prefetch_related('part') + + return queryset + + def filter_queryset(self, queryset): # Filter by build status? status = self.request.query_params.get('status', None) if status is not None: - build_list = build_list.filter(status=status) + queryset = queryset.filter(status=status) - return build_list + # Filter by associated part? + part = self.request.query_params.get('part', None) + + if part is not None: + queryset = queryset.filter(part=part) + + return queryset def get_serializer(self, *args, **kwargs): diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index beed4457e6..0ca5b33a4b 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -583,6 +583,6 @@ class SalesOrderAllocation(models.Model): # Clear the location item.location = None - item.status = StockStatus.SENT_TO_CUSTOMER + item.status = StockStatus.SHIPPED item.save() diff --git a/InvenTree/order/serializers.py b/InvenTree/order/serializers.py index 992e05a80c..95ebae34da 100644 --- a/InvenTree/order/serializers.py +++ b/InvenTree/order/serializers.py @@ -152,6 +152,7 @@ class SalesOrderSerializer(InvenTreeModelSerializer): 'customer_reference', 'status', 'status_text', + 'shipment_date', 'notes', ] diff --git a/InvenTree/part/templates/part/tabs.html b/InvenTree/part/templates/part/tabs.html index b612106706..e092708a0a 100644 --- a/InvenTree/part/templates/part/tabs.html +++ b/InvenTree/part/templates/part/tabs.html @@ -6,7 +6,7 @@ {% trans "Details" %} - {% trans "Parameters" %} {{ part.parameters.count }} + {% trans "Parameters" %}{{ part.parameters.count }} {% if part.is_template %} @@ -25,7 +25,7 @@ {% trans "BOM" %}{{ part.bom_count }} - {% trans "Build Orders" %}{{ part.active_builds|length }} + {% trans "Build Orders" %}{{ part.builds.count }} {% endif %} {% if part.component or part.used_in_count > 0 %} @@ -60,6 +60,6 @@ {% trans "Attachments" %} {% if part.attachment_count > 0 %}{{ part.attachment_count }}{% endif %} - {% trans "Notes" %}{% if part.notes %} {% endif %} + {% trans "Notes" %}{% if part.notes %} {% endif %} \ No newline at end of file diff --git a/InvenTree/stock/templates/stock/item_base.html b/InvenTree/stock/templates/stock/item_base.html index 77023eba35..ae38263001 100644 --- a/InvenTree/stock/templates/stock/item_base.html +++ b/InvenTree/stock/templates/stock/item_base.html @@ -41,7 +41,7 @@ InvenTree | {% trans "Stock Item" %} - {{ item }}
{% trans "This stock item cannot be deleted as it has child items" %}
-{% elif item.delete_on_deplete %} +{% elif item.delete_on_deplete and item.can_delete %}
{% trans "This stock item will be automatically deleted when all stock is depleted." %}
@@ -59,7 +59,7 @@ InvenTree | {% trans "Stock Item" %} - {{ item }} {% endblock %} {% block page_data %} -

{% trans "Stock Item" %}

+

{% trans "Stock Item" %}{% if item.status in StockStatus.UNAVAILABLE_CODES %}{% stock_status_label item.status large=True %}{% endif %}


{% if item.serialized %} @@ -125,6 +125,12 @@ InvenTree | {% trans "Stock Item" %} - {{ item }} {% trans "Belongs To" %} {{ item.belongs_to }} + {% elif item.customer %} + + + {% trans "Customer" %} + {{ item.customer.name }} + {% elif item.location %} @@ -173,11 +179,11 @@ InvenTree | {% trans "Stock Item" %} - {{ item }} {{ item.purchase_order }} {% endif %} - {% if item.customer %} + {% if item.parent %} - - {% trans "Customer" %} - {{ item.customer.name }} + + {% trans "Parent Item" %} + {{ item.parent }} {% endif %} {% if item.link %} diff --git a/InvenTree/templates/status_codes.html b/InvenTree/templates/status_codes.html index eb2b4b144d..029252a842 100644 --- a/InvenTree/templates/status_codes.html +++ b/InvenTree/templates/status_codes.html @@ -5,7 +5,7 @@ var {{ label }}Codes = { {% for opt in options %}'{{ opt.key }}': { key: '{{ opt.key }}', value: '{{ opt.value }}',{% if opt.color %} - color: '{{ opt.color }}',{% endif %} + label: 'label-{{ opt.color }}',{% endif %} },{% endfor %} }; @@ -25,7 +25,7 @@ function {{ label }}StatusDisplay(key) { } // Select the label color - var color = {{ label }}Codes[key].color ?? '#AAA'; + var label = {{ label }}Codes[key].label ?? ''; - return `${value}`; + return `${value}`; } From e768ada83b42ecd7ee374d57200bed85ae9f4146 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 26 Apr 2020 16:38:29 +1000 Subject: [PATCH 092/104] More work - Consolidated "in_stock" filter to single code location - Improve 'limit_choices_to' for BuildItem and SalesOrderAllocation - Various template improvements etc --- .../static/script/inventree/stock.js | 1 - InvenTree/build/api.py | 2 + .../migrations/0016_auto_20200426_0551.py | 20 +++ .../migrations/0017_auto_20200426_0612.py | 20 +++ InvenTree/build/models.py | 7 +- .../build/templates/build/build_base.html | 2 +- .../migrations/0030_auto_20200426_0551.py | 20 +++ .../migrations/0031_auto_20200426_0612.py | 20 +++ InvenTree/order/models.py | 15 ++- InvenTree/part/models.py | 6 +- InvenTree/part/templates/part/part_base.html | 14 +- InvenTree/stock/admin.py | 8 +- InvenTree/stock/api.py | 6 +- .../migrations/0033_auto_20200426_0539.py | 19 +++ .../migrations/0034_auto_20200426_0602.py | 96 ++++++++++++++ InvenTree/stock/models.py | 123 ++++++++++++------ InvenTree/stock/serializers.py | 3 + .../stock/templates/stock/item_base.html | 48 ++++--- InvenTree/stock/views.py | 6 +- InvenTree/templates/table_filters.html | 5 + 20 files changed, 362 insertions(+), 79 deletions(-) create mode 100644 InvenTree/build/migrations/0016_auto_20200426_0551.py create mode 100644 InvenTree/build/migrations/0017_auto_20200426_0612.py create mode 100644 InvenTree/order/migrations/0030_auto_20200426_0551.py create mode 100644 InvenTree/order/migrations/0031_auto_20200426_0612.py create mode 100644 InvenTree/stock/migrations/0033_auto_20200426_0539.py create mode 100644 InvenTree/stock/migrations/0034_auto_20200426_0602.py diff --git a/InvenTree/InvenTree/static/script/inventree/stock.js b/InvenTree/InvenTree/static/script/inventree/stock.js index 3f1331dd2a..e21971bb0f 100644 --- a/InvenTree/InvenTree/static/script/inventree/stock.js +++ b/InvenTree/InvenTree/static/script/inventree/stock.js @@ -48,7 +48,6 @@ function loadStockTable(table, options) { options.params['part_detail'] = true; options.params['location_detail'] = true; - options.params['in_stock'] = true; var params = options.params || {}; diff --git a/InvenTree/build/api.py b/InvenTree/build/api.py index 1c0f91570e..22514ee7b7 100644 --- a/InvenTree/build/api.py +++ b/InvenTree/build/api.py @@ -53,6 +53,8 @@ class BuildList(generics.ListCreateAPIView): def filter_queryset(self, queryset): + queryset = super().filter_queryset(queryset) + # Filter by build status? status = self.request.query_params.get('status', None) diff --git a/InvenTree/build/migrations/0016_auto_20200426_0551.py b/InvenTree/build/migrations/0016_auto_20200426_0551.py new file mode 100644 index 0000000000..f44a37712c --- /dev/null +++ b/InvenTree/build/migrations/0016_auto_20200426_0551.py @@ -0,0 +1,20 @@ +# Generated by Django 3.0.5 on 2020-04-26 05:51 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('stock', '0033_auto_20200426_0539'), + ('build', '0015_auto_20200425_1350'), + ] + + operations = [ + migrations.AlterField( + model_name='builditem', + name='stock_item', + field=models.ForeignKey(help_text='Stock Item to allocate to build', limit_choices_to={'belongs_to': None, 'build_order': None, 'customer': None}, on_delete=django.db.models.deletion.CASCADE, related_name='allocations', to='stock.StockItem'), + ), + ] diff --git a/InvenTree/build/migrations/0017_auto_20200426_0612.py b/InvenTree/build/migrations/0017_auto_20200426_0612.py new file mode 100644 index 0000000000..83eb02ce35 --- /dev/null +++ b/InvenTree/build/migrations/0017_auto_20200426_0612.py @@ -0,0 +1,20 @@ +# Generated by Django 3.0.5 on 2020-04-26 06:12 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('stock', '0034_auto_20200426_0602'), + ('build', '0016_auto_20200426_0551'), + ] + + operations = [ + migrations.AlterField( + model_name='builditem', + name='stock_item', + field=models.ForeignKey(help_text='Stock Item to allocate to build', limit_choices_to={'belongs_to': None, 'build_order': None, 'sales_order': None}, on_delete=django.db.models.deletion.CASCADE, related_name='allocations', to='stock.StockItem'), + ), + ] diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index 28bcc6e62e..893f9ed439 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -261,8 +261,6 @@ class Build(MPTTModel): - Delete pending BuildItem objects """ - print("Complete build...") - # Complete the build allocation for each BuildItem for build_item in self.allocated_stock.all().prefetch_related('stock_item'): build_item.complete_allocation(user) @@ -495,6 +493,11 @@ class BuildItem(models.Model): on_delete=models.CASCADE, related_name='allocations', help_text=_('Stock Item to allocate to build'), + limit_choices_to={ + 'build_order': None, + 'sales_order': None, + 'belongs_to': None, + } ) quantity = models.DecimalField( diff --git a/InvenTree/build/templates/build/build_base.html b/InvenTree/build/templates/build/build_base.html index 9ea112911f..8f5ec62663 100644 --- a/InvenTree/build/templates/build/build_base.html +++ b/InvenTree/build/templates/build/build_base.html @@ -82,7 +82,7 @@ src="{% static 'img/blank_image.png' %}" {% if build.parent %} - + {% trans "Parent Build" %} {{ build.parent }} diff --git a/InvenTree/order/migrations/0030_auto_20200426_0551.py b/InvenTree/order/migrations/0030_auto_20200426_0551.py new file mode 100644 index 0000000000..7236088be3 --- /dev/null +++ b/InvenTree/order/migrations/0030_auto_20200426_0551.py @@ -0,0 +1,20 @@ +# Generated by Django 3.0.5 on 2020-04-26 05:51 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('stock', '0033_auto_20200426_0539'), + ('order', '0029_auto_20200423_1042'), + ] + + operations = [ + migrations.AlterField( + model_name='salesorderallocation', + name='item', + field=models.ForeignKey(help_text='Select stock item to allocate', limit_choices_to={'belongs_to': None, 'build_order': None, 'customer': None, 'part__salable': True}, on_delete=django.db.models.deletion.CASCADE, related_name='sales_order_allocations', to='stock.StockItem'), + ), + ] diff --git a/InvenTree/order/migrations/0031_auto_20200426_0612.py b/InvenTree/order/migrations/0031_auto_20200426_0612.py new file mode 100644 index 0000000000..aa1cd055ec --- /dev/null +++ b/InvenTree/order/migrations/0031_auto_20200426_0612.py @@ -0,0 +1,20 @@ +# Generated by Django 3.0.5 on 2020-04-26 06:12 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('stock', '0034_auto_20200426_0602'), + ('order', '0030_auto_20200426_0551'), + ] + + operations = [ + migrations.AlterField( + model_name='salesorderallocation', + name='item', + field=models.ForeignKey(help_text='Select stock item to allocate', limit_choices_to={'belongs_to': None, 'build_order': None, 'part__salable': True, 'sales_order': None}, on_delete=django.db.models.deletion.CASCADE, related_name='sales_order_allocations', to='stock.StockItem'), + ), + ] diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index 0ca5b33a4b..2869b227a1 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -19,7 +19,7 @@ import os from datetime import datetime from decimal import Decimal -from part.models import Part +from part import models as PartModels from stock import models as stock_models from company.models import Company, SupplierPart @@ -511,7 +511,7 @@ class SalesOrderAllocation(models.Model): try: if not self.line.part == self.item.part: errors['item'] = _('Cannot allocate stock item to a line with a different part') - except Part.DoesNotExist: + except PartModels.Part.DoesNotExist: errors['line'] = _('Cannot allocate stock to a line without a part') if self.quantity > self.item.quantity: @@ -535,7 +535,12 @@ class SalesOrderAllocation(models.Model): 'stock.StockItem', on_delete=models.CASCADE, related_name='sales_order_allocations', - limit_choices_to={'part__salable': True}, + limit_choices_to={ + 'part__salable': True, + 'belongs_to': None, + 'sales_order': None, + 'build_order': None, + }, help_text=_('Select stock item to allocate') ) @@ -565,6 +570,8 @@ class SalesOrderAllocation(models.Model): - Mark the StockItem as belonging to the Customer (this will remove it from stock) """ + order = self.line.order + item = self.item # If the allocated quantity is less than the amount available, @@ -579,7 +586,7 @@ class SalesOrderAllocation(models.Model): self.save() # Assign the StockItem to the SalesOrder customer - item.customer = self.line.order.customer + item.sales_order = order # Clear the location item.location = None diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 597c9a277c..a03f11cbfa 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -42,6 +42,7 @@ from InvenTree.helpers import decimal2string, normalize from InvenTree.status_codes import BuildStatus, StockStatus, PurchaseOrderStatus from company.models import SupplierPart +from stock import models as StockModels class PartCategory(InvenTreeTree): @@ -639,11 +640,12 @@ class Part(models.Model): def stock_entries(self): """ Return all 'in stock' items. To be in stock: - - customer is None + - build_order is None + - sales_order is None - belongs_to is None """ - return self.stock_items.filter(customer=None, belongs_to=None) + return self.stock_items.filter(StockModels.StockItem.IN_STOCK_FILTER).exclude(status__in=StockStatus.UNAVAILABLE_CODES) @property def total_stock(self): diff --git a/InvenTree/part/templates/part/part_base.html b/InvenTree/part/templates/part/part_base.html index 0c329c72be..4da8ec7ffe 100644 --- a/InvenTree/part/templates/part/part_base.html +++ b/InvenTree/part/templates/part/part_base.html @@ -6,11 +6,6 @@ {% block content %} -{% if part.active == False %} -
- {% trans "This part is not active" %} -
-{% endif %} {% if part.is_template %}
{% trans "This part is a template part." %} @@ -28,9 +23,14 @@
{% include "part/part_thumb.html" %}
-

+

{{ part.full_name }} -

+ {% if not part.active %} +
+ {% trans 'Inactive' %} +
+ {% endif %} +

{{ part.description }}

diff --git a/InvenTree/stock/admin.py b/InvenTree/stock/admin.py index 8c91518de0..193e807b7f 100644 --- a/InvenTree/stock/admin.py +++ b/InvenTree/stock/admin.py @@ -13,7 +13,7 @@ from .models import StockItemTracking from build.models import Build from company.models import Company, SupplierPart -from order.models import PurchaseOrder +from order.models import PurchaseOrder, SalesOrder from part.models import Part @@ -74,10 +74,12 @@ class StockItemResource(ModelResource): belongs_to = Field(attribute='belongs_to', widget=widgets.ForeignKeyWidget(StockItem)) - customer = Field(attribute='customer', widget=widgets.ForeignKeyWidget(Company)) - build = Field(attribute='build', widget=widgets.ForeignKeyWidget(Build)) + sales_order = Field(attribute='sales_order', widget=widgets.ForeignKeyWidget(SalesOrder)) + + build_order = Field(attribute='build_order', widget=widgets.ForeignKeyWidget(Build)) + purchase_order = Field(attribute='purchase_order', widget=widgets.ForeignKeyWidget(PurchaseOrder)) # Date management diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index 75deff6bd3..c31c1b8993 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -369,10 +369,10 @@ class StockList(generics.ListCreateAPIView): if in_stock: # Filter out parts which are not actually "in stock" - stock_list = stock_list.filter(customer=None, belongs_to=None, build_order=None) + stock_list = stock_list.filter(StockItem.IN_STOCK_FILTER) else: # Only show parts which are not in stock - stock_list = stock_list.exclude(customer=None, belongs_to=None, build_order=None) + stock_list = stock_list.exclude(StockItem.IN_STOCK_FILTER) # Filter by 'allocated' patrs? allocated = self.request.query_params.get('allocated', None) @@ -511,9 +511,9 @@ class StockList(generics.ListCreateAPIView): filter_fields = [ 'supplier_part', - 'customer', 'belongs_to', 'build', + 'build_order', 'sales_order', 'build_order', ] diff --git a/InvenTree/stock/migrations/0033_auto_20200426_0539.py b/InvenTree/stock/migrations/0033_auto_20200426_0539.py new file mode 100644 index 0000000000..214a66feeb --- /dev/null +++ b/InvenTree/stock/migrations/0033_auto_20200426_0539.py @@ -0,0 +1,19 @@ +# Generated by Django 3.0.5 on 2020-04-26 05:39 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('stock', '0032_stockitem_build_order'), + ] + + operations = [ + migrations.AlterField( + model_name='stockitem', + name='status', + field=models.PositiveIntegerField(choices=[(10, 'OK'), (50, 'Attention needed'), (55, 'Damaged'), (60, 'Destroyed'), (70, 'Lost'), (85, 'Returned'), (110, 'Shipped'), (120, 'Used for Build'), (130, 'Installed in Stock Item')], default=10, validators=[django.core.validators.MinValueValidator(0)]), + ), + ] diff --git a/InvenTree/stock/migrations/0034_auto_20200426_0602.py b/InvenTree/stock/migrations/0034_auto_20200426_0602.py new file mode 100644 index 0000000000..4bf3171aa2 --- /dev/null +++ b/InvenTree/stock/migrations/0034_auto_20200426_0602.py @@ -0,0 +1,96 @@ +# Generated by Django 3.0.5 on 2020-04-26 06:02 + +import InvenTree.fields +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion +import markdownx.models +import mptt.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('order', '0030_auto_20200426_0551'), + ('build', '0016_auto_20200426_0551'), + ('part', '0035_auto_20200406_0045'), + ('company', '0021_remove_supplierpart_manufacturer_name'), + ('stock', '0033_auto_20200426_0539'), + ] + + operations = [ + migrations.RemoveField( + model_name='stockitem', + name='customer', + ), + migrations.AlterField( + model_name='stockitem', + name='batch', + field=models.CharField(blank=True, help_text='Batch code for this stock item', max_length=100, null=True, verbose_name='Batch Code'), + ), + migrations.AlterField( + model_name='stockitem', + name='belongs_to', + field=models.ForeignKey(blank=True, help_text='Is this item installed in another item?', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='owned_parts', to='stock.StockItem', verbose_name='Installed In'), + ), + migrations.AlterField( + model_name='stockitem', + name='build', + field=models.ForeignKey(blank=True, help_text='Build for this stock item', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='build_outputs', to='build.Build', verbose_name='Source Build'), + ), + migrations.AlterField( + model_name='stockitem', + name='build_order', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='stock_items', to='build.Build', verbose_name='Destination Build Order'), + ), + migrations.AlterField( + model_name='stockitem', + name='link', + field=InvenTree.fields.InvenTreeURLField(blank=True, help_text='Link to external URL', max_length=125, verbose_name='External Link'), + ), + migrations.AlterField( + model_name='stockitem', + name='location', + field=mptt.fields.TreeForeignKey(blank=True, help_text='Where is this stock item located?', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='stock_items', to='stock.StockLocation', verbose_name='Stock Location'), + ), + migrations.AlterField( + model_name='stockitem', + name='notes', + field=markdownx.models.MarkdownxField(blank=True, help_text='Stock Item Notes', null=True, verbose_name='Notes'), + ), + migrations.AlterField( + model_name='stockitem', + name='parent', + field=mptt.fields.TreeForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='children', to='stock.StockItem', verbose_name='Parent Stock Item'), + ), + migrations.AlterField( + model_name='stockitem', + name='part', + field=models.ForeignKey(help_text='Base part', limit_choices_to={'active': True, 'is_template': False, 'virtual': False}, on_delete=django.db.models.deletion.CASCADE, related_name='stock_items', to='part.Part', verbose_name='Base Part'), + ), + migrations.AlterField( + model_name='stockitem', + name='purchase_order', + field=models.ForeignKey(blank=True, help_text='Purchase order for this stock item', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='stock_items', to='order.PurchaseOrder', verbose_name='Source Purchase Order'), + ), + migrations.AlterField( + model_name='stockitem', + name='quantity', + field=models.DecimalField(decimal_places=5, default=1, max_digits=15, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Stock Quantity'), + ), + migrations.AlterField( + model_name='stockitem', + name='sales_order', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='stock_items', to='order.SalesOrder', verbose_name='Destination Sales Order'), + ), + migrations.AlterField( + model_name='stockitem', + name='serial', + field=models.PositiveIntegerField(blank=True, help_text='Serial number for this item', null=True, verbose_name='Serial Number'), + ), + migrations.AlterField( + model_name='stockitem', + name='supplier_part', + field=models.ForeignKey(blank=True, help_text='Select a matching supplier part for this stock item', null=True, on_delete=django.db.models.deletion.SET_NULL, to='company.SupplierPart', verbose_name='Supplier Part'), + ), + ] diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 89103703c6..25c00ca65a 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -11,7 +11,7 @@ from django.core.exceptions import ValidationError from django.urls import reverse from django.db import models, transaction -from django.db.models import Sum +from django.db.models import Sum, Q from django.db.models.functions import Coalesce from django.core.validators import MinValueValidator from django.contrib.auth.models import User @@ -30,7 +30,7 @@ from InvenTree.status_codes import StockStatus from InvenTree.models import InvenTreeTree from InvenTree.fields import InvenTreeURLField -from part.models import Part +from part import models as PartModels from order.models import PurchaseOrder, SalesOrder @@ -133,6 +133,9 @@ class StockItem(MPTTModel): build_order: Link to a BuildOrder object (if the StockItem has been assigned to a BuildOrder) """ + # A Query filter which will be re-used in multiple places to determine if a StockItem is actually "in stock" + IN_STOCK_FILTER = Q(sales_order=None, build_order=None, belongs_to=None) + def save(self, *args, **kwargs): if not self.pk: add_note = True @@ -215,7 +218,7 @@ class StockItem(MPTTModel): raise ValidationError({ 'serial': _('A stock item with this serial number already exists') }) - except Part.DoesNotExist: + except PartModels.Part.DoesNotExist: pass def clean(self): @@ -228,6 +231,18 @@ class StockItem(MPTTModel): - Quantity must be 1 if the StockItem has a serial number """ + if self.status == StockStatus.SHIPPED and self.sales_order is None: + raise ValidationError({ + 'sales_order': "SalesOrder must be specified as status is marked as SHIPPED", + 'status': "Status cannot be marked as SHIPPED if the Customer is not set", + }) + + if self.status == StockStatus.ASSIGNED_TO_OTHER_ITEM and self.belongs_to is None: + raise ValidationError({ + 'belongs_to': "Belongs_to field must be specified as statis is marked as ASSIGNED_TO_OTHER_ITEM", + 'status': 'Status cannot be marked as ASSIGNED_TO_OTHER_ITEM if the belongs_to field is not set', + }) + # The 'supplier_part' field must point to the same part! try: if self.supplier_part is not None: @@ -261,7 +276,7 @@ class StockItem(MPTTModel): if self.part.is_template: raise ValidationError({'part': _('Stock item cannot be created for a template Part')}) - except Part.DoesNotExist: + except PartModels.Part.DoesNotExist: # This gets thrown if self.supplier_part is null # TODO - Find a test than can be perfomed... pass @@ -303,48 +318,75 @@ class StockItem(MPTTModel): uid = models.CharField(blank=True, max_length=128, help_text=("Unique identifier field")) - parent = TreeForeignKey('self', - on_delete=models.DO_NOTHING, - blank=True, null=True, - related_name='children') + parent = TreeForeignKey( + 'self', + verbose_name=_('Parent Stock Item'), + on_delete=models.DO_NOTHING, + blank=True, null=True, + related_name='children' + ) - part = models.ForeignKey('part.Part', on_delete=models.CASCADE, - related_name='stock_items', help_text=_('Base part'), - limit_choices_to={ - 'is_template': False, - 'active': True, - 'virtual': False - }) + part = models.ForeignKey( + 'part.Part', on_delete=models.CASCADE, + verbose_name=_('Base Part'), + related_name='stock_items', help_text=_('Base part'), + limit_choices_to={ + 'is_template': False, + 'active': True, + 'virtual': False + }) - supplier_part = models.ForeignKey('company.SupplierPart', blank=True, null=True, on_delete=models.SET_NULL, - help_text=_('Select a matching supplier part for this stock item')) + supplier_part = models.ForeignKey( + 'company.SupplierPart', blank=True, null=True, on_delete=models.SET_NULL, + verbose_name=_('Supplier Part'), + help_text=_('Select a matching supplier part for this stock item') + ) - location = TreeForeignKey(StockLocation, on_delete=models.DO_NOTHING, - related_name='stock_items', blank=True, null=True, - help_text=_('Where is this stock item located?')) + location = TreeForeignKey( + StockLocation, on_delete=models.DO_NOTHING, + verbose_name=_('Stock Location'), + related_name='stock_items', + blank=True, null=True, + help_text=_('Where is this stock item located?') + ) - belongs_to = models.ForeignKey('self', on_delete=models.DO_NOTHING, - related_name='owned_parts', blank=True, null=True, - help_text=_('Is this item installed in another item?')) + belongs_to = models.ForeignKey( + 'self', + verbose_name=_('Installed In'), + on_delete=models.DO_NOTHING, + related_name='owned_parts', blank=True, null=True, + help_text=_('Is this item installed in another item?') + ) - customer = models.ForeignKey('company.Company', on_delete=models.SET_NULL, - related_name='stockitems', blank=True, null=True, - help_text=_('Item assigned to customer?')) - - serial = models.PositiveIntegerField(blank=True, null=True, - help_text=_('Serial number for this item')) + serial = models.PositiveIntegerField( + verbose_name=_('Serial Number'), + blank=True, null=True, + help_text=_('Serial number for this item') + ) - link = InvenTreeURLField(max_length=125, blank=True, help_text=_("Link to external URL")) + link = InvenTreeURLField( + verbose_name=_('External Link'), + max_length=125, blank=True, + help_text=_("Link to external URL") + ) - batch = models.CharField(max_length=100, blank=True, null=True, - help_text=_('Batch code for this stock item')) + batch = models.CharField( + verbose_name=_('Batch Code'), + max_length=100, blank=True, null=True, + help_text=_('Batch code for this stock item') + ) - quantity = models.DecimalField(max_digits=15, decimal_places=5, validators=[MinValueValidator(0)], default=1) + quantity = models.DecimalField( + verbose_name=_("Stock Quantity"), + max_digits=15, decimal_places=5, validators=[MinValueValidator(0)], + default=1 + ) updated = models.DateField(auto_now=True, null=True) build = models.ForeignKey( 'build.Build', on_delete=models.SET_NULL, + verbose_name=_('Source Build'), blank=True, null=True, help_text=_('Build for this stock item'), related_name='build_outputs', @@ -353,6 +395,7 @@ class StockItem(MPTTModel): purchase_order = models.ForeignKey( PurchaseOrder, on_delete=models.SET_NULL, + verbose_name=_('Source Purchase Order'), related_name='stock_items', blank=True, null=True, help_text=_('Purchase order for this stock item') @@ -361,12 +404,14 @@ class StockItem(MPTTModel): sales_order = models.ForeignKey( SalesOrder, on_delete=models.SET_NULL, + verbose_name=_("Destination Sales Order"), related_name='stock_items', null=True, blank=True) build_order = models.ForeignKey( 'build.Build', on_delete=models.SET_NULL, + verbose_name=_("Destination Build Order"), related_name='stock_items', null=True, blank=True ) @@ -386,7 +431,11 @@ class StockItem(MPTTModel): choices=StockStatus.items(), validators=[MinValueValidator(0)]) - notes = MarkdownxField(blank=True, null=True, help_text=_('Stock Item Notes')) + notes = MarkdownxField( + blank=True, null=True, + verbose_name=_("Notes"), + help_text=_('Stock Item Notes') + ) # If stock item is incoming, an (optional) ETA field # expected_arrival = models.DateField(null=True, blank=True) @@ -447,7 +496,7 @@ class StockItem(MPTTModel): - Has child StockItems - Has a serial number and is tracked - Is installed inside another StockItem - - It has been delivered to a customer + - It has been assigned to a SalesOrder - It has been assigned to a BuildOrder """ @@ -457,7 +506,7 @@ class StockItem(MPTTModel): if self.part.trackable and self.serial is not None: return False - if self.customer is not None: + if self.sales_order is not None: return False if self.build_order is not None: @@ -485,7 +534,7 @@ class StockItem(MPTTModel): return False # Not 'in stock' if it has been sent to a customer - if self.customer is not None: + if self.sales_order is not None: return False # Not 'in stock' if it has been allocated to a BuildOrder diff --git a/InvenTree/stock/serializers.py b/InvenTree/stock/serializers.py index 0c7674a088..4e586b789e 100644 --- a/InvenTree/stock/serializers.py +++ b/InvenTree/stock/serializers.py @@ -118,6 +118,8 @@ class StockItemSerializer(InvenTreeModelSerializer): fields = [ 'allocated', 'batch', + 'build_order', + 'belongs_to', 'in_stock', 'link', 'location', @@ -127,6 +129,7 @@ class StockItemSerializer(InvenTreeModelSerializer): 'part_detail', 'pk', 'quantity', + 'sales_order', 'serial', 'supplier_part', 'supplier_part_detail', diff --git a/InvenTree/stock/templates/stock/item_base.html b/InvenTree/stock/templates/stock/item_base.html index ae38263001..bb209f309c 100644 --- a/InvenTree/stock/templates/stock/item_base.html +++ b/InvenTree/stock/templates/stock/item_base.html @@ -15,11 +15,15 @@ InvenTree | {% trans "Stock Item" %} - {{ item }} {% block pre_content %} {% include 'stock/loc_link.html' with location=item.location %} -{% if item.customer %} +{% if item.sales_order %}
- {% trans "This stock item has been sent to" %} {{ item.customer.name }} + {% trans "This stock item was assigned to" %} {% trans "Sales Order" %} {{ item.sales_order.id }}
-{% endif %} +{% elif item.build_order %} +
+ {% trans "This stock item was assigned to" %} {% trans "Build Order" %} #{{ item.build_order.id }} +
+{% else %} {% for allocation in item.sales_order_allocations.all %}
@@ -32,6 +36,7 @@ InvenTree | {% trans "Stock Item" %} - {{ item }} {% trans "This stock item is allocated to Build" %} #{{ allocation.build.id }} ({% trans "Quantity" %}: {% decimal allocation.quantity %})
{% endfor %} +{% endif %} {% if item.serialized %}
@@ -46,11 +51,6 @@ InvenTree | {% trans "Stock Item" %} - {{ item }} {% trans "This stock item will be automatically deleted when all stock is depleted." %}
{% endif %} -{% if item.parent %} -
- {% trans "This stock item was split from " %}{{ item.parent }} -
-{% endif %} {% endblock %} @@ -59,7 +59,19 @@ InvenTree | {% trans "Stock Item" %} - {{ item }} {% endblock %} {% block page_data %} -

{% trans "Stock Item" %}{% if item.status in StockStatus.UNAVAILABLE_CODES %}{% stock_status_label item.status large=True %}{% endif %}

+

+ {% trans "Stock Item" %} + {% if item.sales_order %} +
+ {% trans "Sold" $} +
+ {% elif item.build_order %} +
+ {% trans "Used in Build" %} +
+ {% elif item.status in StockStatus.UNAVAILABLE_CODES %}{% stock_status_label item.status large=True %} + {% endif %} +


{% if item.serialized %} @@ -74,10 +86,10 @@ InvenTree | {% trans "Stock Item" %} - {{ item }} {% if item.in_stock %} {% if not item.serialized %} - -

+
{% endblock %} \ No newline at end of file diff --git a/InvenTree/stock/templates/stock/item_base.html b/InvenTree/stock/templates/stock/item_base.html index 25f582a27c..8428418392 100644 --- a/InvenTree/stock/templates/stock/item_base.html +++ b/InvenTree/stock/templates/stock/item_base.html @@ -58,7 +58,8 @@ InvenTree | {% trans "Stock Item" %} - {{ item }}
{% trans "Used in Build" %}
- {% elif item.status in StockStatus.UNAVAILABLE_CODES %}{% stock_status_label item.status large=True %} + {% else %} + {% stock_status_label item.status large=True %} {% endif %}
diff --git a/InvenTree/stock/templates/stock/location.html b/InvenTree/stock/templates/stock/location.html index f7b5a7a70d..521e73bf7e 100644 --- a/InvenTree/stock/templates/stock/location.html +++ b/InvenTree/stock/templates/stock/location.html @@ -233,6 +233,7 @@ {% endif %} part_detail: true, location_detail: true, + in_stock: true, }, url: "{% url 'api-stock-list' %}", }); diff --git a/InvenTree/templates/table_filters.html b/InvenTree/templates/table_filters.html index a8a2fddcdf..b337b25ac8 100644 --- a/InvenTree/templates/table_filters.html +++ b/InvenTree/templates/table_filters.html @@ -21,11 +21,6 @@ function getAvailableTableFilters(tableKey) { title: '{% trans "Include sublocations" %}', description: '{% trans "Include stock in sublocations" %}', }, - in_stock: { - type: 'bool', - title: '{% trans "In stock" %}', - description: '{% trans "Item is in stock" %}', - }, active: { type: 'bool', title: '{% trans "Active parts" %}', From 9b882f4d17f16677286d1ef268cbb6e13071e7a2 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 27 Apr 2020 09:17:17 +1000 Subject: [PATCH 095/104] Update to latest version of django-qr-code --- requirements.txt | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/requirements.txt b/requirements.txt index a97937271d..9d6ad3e3af 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,10 +14,7 @@ tablib==0.13.0 # Import / export data files django-crispy-forms==1.8.1 # Form helpers django-import-export==2.0.0 # Data import / export for admin interface django-cleanup==4.0.0 # Manage deletion of old / unused uploaded files -# TODO: Once the official django-qr-code package has been updated with Django3.x support, -# the following line should be removed. -git+git://github.com/chrissam/django-qr-code -# django-qr-code==1.1.0 # Generate QR codes +django-qr-code==1.2.0 # Generate QR codes flake8==3.3.0 # PEP checking coverage==4.0.3 # Unit test coverage python-coveralls==2.9.1 # Coveralls linking (for Travis) From 5e309a62f736a6b2b7977e042aad21b9ca3110b4 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 27 Apr 2020 10:31:38 +1000 Subject: [PATCH 096/104] Display "Fulfilled" items - Once a salesorder has been marked as "shipped" then the table is displayed differently - The sub rows show stock items which have been fulfilled against the sales order --- InvenTree/build/templates/build/allocate.html | 5 +- InvenTree/order/models.py | 2 +- .../templates/order/sales_order_base.html | 2 +- .../templates/order/sales_order_detail.html | 219 ++++++++++++------ InvenTree/stock/models.py | 4 + .../stock/templates/stock/item_base.html | 10 - 6 files changed, 153 insertions(+), 89 deletions(-) diff --git a/InvenTree/build/templates/build/allocate.html b/InvenTree/build/templates/build/allocate.html index 55ce042087..0e4a39cc29 100644 --- a/InvenTree/build/templates/build/allocate.html +++ b/InvenTree/build/templates/build/allocate.html @@ -201,8 +201,9 @@ InvenTree | Allocate Parts formatNoMatches: function() { return "{% trans 'No BOM items found' %}"; }, onLoadSuccess: function(tableData) { // Once the BOM data are loaded, request allocation data for the build - inventreeGet('/api/build/item/', { - build: {{ build.id }}, + inventreeGet('/api/build/item/', + { + build: {{ build.id }}, }, { success: function(data) { diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index a7a176a7bf..35661d60fe 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -325,7 +325,7 @@ class SalesOrder(Order): allocation.complete_allocation(user) # Remove the allocation from the database once it has been 'fulfilled' - if allocation.item.sales_order == self.order: + if allocation.item.sales_order == self: allocation.delete() else: raise ValidationError("Could not complete order - allocation item not fulfilled") diff --git a/InvenTree/order/templates/order/sales_order_base.html b/InvenTree/order/templates/order/sales_order_base.html index 298b7441b1..6028157a22 100644 --- a/InvenTree/order/templates/order/sales_order_base.html +++ b/InvenTree/order/templates/order/sales_order_base.html @@ -10,7 +10,7 @@ InvenTree | {% trans "Sales Order" %} {% endblock %} {% block pre_content %} -{% if not order.is_fully_allocated %} +{% if order.status == SalesOrderStatus.PENDING and not order.is_fully_allocated %}
{% trans "This SalesOrder has not been fully allocated" %}
diff --git a/InvenTree/order/templates/order/sales_order_detail.html b/InvenTree/order/templates/order/sales_order_detail.html index f4a6425a97..9f21d8fc35 100644 --- a/InvenTree/order/templates/order/sales_order_detail.html +++ b/InvenTree/order/templates/order/sales_order_detail.html @@ -41,6 +41,116 @@ $("#new-so-line").click(function() { }); }); +{% if order.status == SalesOrderStatus.PENDING %} +function showAllocationSubTable(index, row, element) { + // Construct a table showing stock items which have been allocated against this line item + + var html = `
`; + + element.html(html); + + var lineItem = row; + + var table = $(`#allocation-table-${row.pk}`); + + table.bootstrapTable({ + data: row.allocations, + showHeader: false, + columns: [ + { + width: '50%', + field: 'allocated', + title: 'Quantity', + formatter: function(value, row, index, field) { + return renderLink(value, `/stock/item/${row.item}/`); + }, + }, + { + field: 'location_id', + title: 'Location', + formatter: function(value, row, index, field) { + return renderLink(row.location_path, `/stock/location/${row.location_id}/`); + }, + }, + { + field: 'buttons', + title: 'Actions', + formatter: function(value, row, index, field) { + + var html = "
"; + var pk = row.pk; + + {% if order.status == SalesOrderStatus.PENDING %} + html += makeIconButton('fa-edit', 'button-allocation-edit', pk, '{% trans "Edit stock allocation" %}'); + html += makeIconButton('fa-trash-alt', 'button-allocation-delete', pk, '{% trans "Delete stock allocation" %}'); + {% endif %} + + html += "
"; + + return html; + }, + }, + ], + }); + + table.find(".button-allocation-edit").click(function() { + + var pk = $(this).attr('pk'); + + launchModalForm(`/order/sales-order/allocation/${pk}/edit/`, { + success: reloadTable, + }); + }); + + table.find(".button-allocation-delete").click(function() { + var pk = $(this).attr('pk'); + + launchModalForm(`/order/sales-order/allocation/${pk}/delete/`, { + success: reloadTable, + }); + }); +} +{% endif %} + +function showFulfilledSubTable(index, row, element) { + // Construct a table showing stock items which have been fulfilled against this line item + + var id = `fulfilled-table-${row.pk}`; + var html = `
`; + + element.html(html); + + var lineItem = row; + + $(`#${id}`).bootstrapTable({ + url: "{% url 'api-stock-list' %}", + queryParams: { + part: row.part, + sales_order: {{ order.id }}, + }, + showHeader: false, + columns: [ + { + field: 'pk', + visible: false, + }, + { + field: 'stock', + formatter: function(value, row) { + var text = ''; + if (row.serial) { + text = `{% trans "Serial Number" %}: ${row.serial}`; + } else { + text = `{% trans "Quantity" %}: ${row.quantity}`; + } + + return renderLink(text, `/stock/item/${row.pk}/`); + }, + } + ], + }); +} + $("#so-lines-table").inventreeTable({ formatNoMatches: function() { return "No matching line items"; }, queryParams: { @@ -51,78 +161,22 @@ $("#so-lines-table").inventreeTable({ uniqueId: 'pk', url: "{% url 'api-so-line-list' %}", onPostBody: setupCallbacks, + {% if order.status == SalesOrderStatus.PENDING or order.status == SalesOrderStatus.SHIPPED %} detailViewByClick: true, detailView: true, detailFilter: function(index, row) { + {% if order.status == SalesOrderStatus.PENDING %} return row.allocated > 0; + {% else %} + return row.fulfilled > 0; + {% endif %} }, - detailFormatter: function(index, row, element) { - - var html = `
`; - - element.html(html); - - var lineItem = row; - - var table = $(`#allocation-table-${row.pk}`); - - table.bootstrapTable({ - data: row.allocations, - showHeader: false, - columns: [ - { - width: '50%', - field: 'allocated', - title: 'Quantity', - formatter: function(value, row, index, field) { - return renderLink(value, `/stock/item/${row.item}/`); - }, - }, - { - field: 'location_id', - title: 'Location', - formatter: function(value, row, index, field) { - return renderLink(row.location_path, `/stock/location/${row.location_id}/`); - }, - }, - { - field: 'buttons', - title: 'Actions', - formatter: function(value, row, index, field) { - - var html = "
"; - var pk = row.pk; - - {% if order.status == SalesOrderStatus.PENDING %} - html += makeIconButton('fa-edit', 'button-allocation-edit', pk, '{% trans "Edit stock allocation" %}'); - html += makeIconButton('fa-trash-alt', 'button-allocation-delete', pk, '{% trans "Delete stock allocation" %}'); - {% endif %} - - html += "
"; - - return html; - }, - }, - ], - }); - - table.find(".button-allocation-edit").click(function() { - - var pk = $(this).attr('pk'); - - launchModalForm(`/order/sales-order/allocation/${pk}/edit/`, { - success: reloadTable, - }); - }); - - table.find(".button-allocation-delete").click(function() { - var pk = $(this).attr('pk'); - - launchModalForm(`/order/sales-order/allocation/${pk}/delete/`, { - success: reloadTable, - }); - }); - }, + {% if order.status == SalesOrderStatus.PENDING %} + detailFormatter: showAllocationSubTable, + {% else %} + detailFormatter: showFulfilledSubTable, + {% endif %} + {% endif %} columns: [ { field: 'pk', @@ -154,20 +208,36 @@ $("#so-lines-table").inventreeTable({ { sortable: true, field: 'allocated', - title: 'Allocated', + {% if order.status == SalesOrderStatus.PENDING %} + title: '{% trans "Allocated" %}', + {% else %} + title: '{% trans "Fulfilled" %}', + {% endif %} formatter: function(value, row, index, field) { - return makeProgressBar(row.allocated, row.quantity, { + {% if order.status == SalesOrderStatus.PENDING %} + var quantity = row.allocated; + {% else %} + var quantity = row.fulfilled; + {% endif %} + return makeProgressBar(quantity, row.quantity, { id: `order-line-progress-${row.pk}`, }); }, sorter: function(valA, valB, rowA, rowB) { + {% if order.status == SalesOrderStatus.PENDING %} + var A = rowA.allocated; + var B = rowB.allocated; + {% else %} + var A = rowA.fulfilled; + var B = rowB.fulfilled; + {% endif %} - if (rowA.allocated == 0 && rowB.allocated == 0) { + if (A == 0 && B == 0) { return (rowA.quantity > rowB.quantity) ? 1 : -1; } - var progressA = parseFloat(rowA.allocated) / rowA.quantity; - var progressB = parseFloat(rowB.allocated) / rowB.quantity; + var progressA = parseFloat(A) / rowA.quantity; + var progressB = parseFloat(B) / rowB.quantity; return (progressA < progressB) ? 1 : -1; } @@ -176,6 +246,7 @@ $("#so-lines-table").inventreeTable({ field: 'notes', title: 'Notes', }, + {% if order.status == SalesOrderStatus.PENDING %} { field: 'buttons', formatter: function(value, row, index, field) { @@ -184,7 +255,6 @@ $("#so-lines-table").inventreeTable({ var pk = row.pk; - {% if order.status == SalesOrderStatus.PENDING %} if (row.part) { var part = row.part_detail; @@ -202,13 +272,12 @@ $("#so-lines-table").inventreeTable({ html += makeIconButton('fa-edit', 'button-edit', pk, '{% trans "Edit line item" %}'); html += makeIconButton('fa-trash-alt', 'button-delete', pk, '{% trans "Delete line item " %}'); - {% endif %} - html += `
`; return html; } }, + {% endif %} ], }); diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 25c00ca65a..2261d8ad23 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -541,6 +541,10 @@ class StockItem(MPTTModel): if self.build_order is not None: return False + # Not 'in stock' if the status code makes it unavailable + if self.status in StockStatus.UNAVAILABLE_CODES: + return False + return True @property diff --git a/InvenTree/stock/templates/stock/item_base.html b/InvenTree/stock/templates/stock/item_base.html index 8428418392..bfbc737181 100644 --- a/InvenTree/stock/templates/stock/item_base.html +++ b/InvenTree/stock/templates/stock/item_base.html @@ -50,17 +50,7 @@ InvenTree | {% trans "Stock Item" %} - {{ item }} {% block page_data %}

{% trans "Stock Item" %} - {% if item.sales_order %} - -
{% trans "Sold" $}
-
- {% elif item.build_order %} - -
{% trans "Used in Build" %}
-
- {% else %} {% stock_status_label item.status large=True %} - {% endif %}


From 3685ca4b95c43900717937fb3fca780c2ee82fc8 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 27 Apr 2020 11:32:20 +1000 Subject: [PATCH 097/104] Add some unit testing for the SalesOrder model --- .../migrations/0032_auto_20200427_0044.py | 18 +++ InvenTree/order/models.py | 19 ++- InvenTree/order/serializers.py | 6 +- .../templates/order/sales_order_detail.html | 12 +- InvenTree/order/test_sales_order.py | 138 ++++++++++++++++++ 5 files changed, 182 insertions(+), 11 deletions(-) create mode 100644 InvenTree/order/migrations/0032_auto_20200427_0044.py create mode 100644 InvenTree/order/test_sales_order.py diff --git a/InvenTree/order/migrations/0032_auto_20200427_0044.py b/InvenTree/order/migrations/0032_auto_20200427_0044.py new file mode 100644 index 0000000000..4648ca911b --- /dev/null +++ b/InvenTree/order/migrations/0032_auto_20200427_0044.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.5 on 2020-04-27 00:44 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('part', '0035_auto_20200406_0045'), + ('order', '0031_auto_20200426_0612'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='salesorderlineitem', + unique_together={('order', 'part')}, + ), + ] diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index 35661d60fe..0b2ebd3707 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -24,7 +24,7 @@ from stock import models as stock_models from company.models import Company, SupplierPart from InvenTree.fields import RoundingDecimalField -from InvenTree.helpers import decimal2string, normalize +from InvenTree.helpers import decimal2string from InvenTree.status_codes import PurchaseOrderStatus, SalesOrderStatus, StockStatus from InvenTree.models import InvenTreeAttachment @@ -461,6 +461,11 @@ class SalesOrderLineItem(OrderLineItem): part = models.ForeignKey('part.Part', on_delete=models.SET_NULL, related_name='sales_order_line_items', null=True, help_text=_('Part'), limit_choices_to={'salable': True}) + class Meta: + unique_together = [ + ('order', 'part'), + ] + def fulfilled_quantity(self): """ Return the total stock quantity fulfilled against this line item. @@ -482,6 +487,10 @@ class SalesOrderLineItem(OrderLineItem): def is_fully_allocated(self): """ Return True if this line item is fully allocated """ + + if self.order.status == SalesOrderStatus.SHIPPED: + return self.fulfilled_quantity() >= self.quantity + return self.allocated_quantity() >= self.quantity def is_over_allocated(self): @@ -561,12 +570,8 @@ class SalesOrderAllocation(models.Model): quantity = RoundingDecimalField(max_digits=15, decimal_places=5, validators=[MinValueValidator(0)], default=1, help_text=_('Enter stock allocation quantity')) - def get_allocated(self): - """ String representation of the allocated quantity """ - if self.item.serial and self.quantity == 1: - return "# {sn}".format(sn=self.item.serial) - else: - return normalize(self.quantity) + def get_serial(self): + return self.item.serial def get_location(self): return self.item.location.id if self.item.location else None diff --git a/InvenTree/order/serializers.py b/InvenTree/order/serializers.py index 0738b9dfbe..e0ef57f802 100644 --- a/InvenTree/order/serializers.py +++ b/InvenTree/order/serializers.py @@ -170,7 +170,8 @@ class SalesOrderAllocationSerializer(InvenTreeModelSerializer): location_path = serializers.CharField(source='get_location_path') location_id = serializers.IntegerField(source='get_location') - allocated = serializers.CharField(source='get_allocated') + serial = serializers.CharField(source='get_serial') + quantity = serializers.FloatField() class Meta: model = SalesOrderAllocation @@ -178,7 +179,8 @@ class SalesOrderAllocationSerializer(InvenTreeModelSerializer): fields = [ 'pk', 'line', - 'allocated', + 'serial', + 'quantity', 'location_id', 'location_path', 'item', diff --git a/InvenTree/order/templates/order/sales_order_detail.html b/InvenTree/order/templates/order/sales_order_detail.html index 9f21d8fc35..0ca63882b2 100644 --- a/InvenTree/order/templates/order/sales_order_detail.html +++ b/InvenTree/order/templates/order/sales_order_detail.html @@ -62,7 +62,15 @@ function showAllocationSubTable(index, row, element) { field: 'allocated', title: 'Quantity', formatter: function(value, row, index, field) { - return renderLink(value, `/stock/item/${row.item}/`); + var text = ''; + + if (row.serial != null && row.quantity == 1) { + text = `{% trans "Serial Number" %}: ${row.serial}`; + } else { + text = `{% trans "Quantity" %}: ${row.quantity}`; + } + + return renderLink(text, `/stock/item/${row.item}/`); }, }, { @@ -138,7 +146,7 @@ function showFulfilledSubTable(index, row, element) { field: 'stock', formatter: function(value, row) { var text = ''; - if (row.serial) { + if (row.serial && row.quantity == 1) { text = `{% trans "Serial Number" %}: ${row.serial}`; } else { text = `{% trans "Quantity" %}: ${row.quantity}`; diff --git a/InvenTree/order/test_sales_order.py b/InvenTree/order/test_sales_order.py new file mode 100644 index 0000000000..d0d34a517c --- /dev/null +++ b/InvenTree/order/test_sales_order.py @@ -0,0 +1,138 @@ +from django.test import TestCase + +from django.core.exceptions import ValidationError +from django.db.utils import IntegrityError + +from company.models import Company +from stock.models import StockItem +from order.models import SalesOrder, SalesOrderLineItem, SalesOrderAllocation +from part.models import Part +from InvenTree import status_codes as status + + +class SalesOrderTest(TestCase): + """ + Run tests to ensure that the SalesOrder model is working correctly. + + """ + + def setUp(self): + + # Create a Company to ship the goods to + self.customer = Company.objects.create(name="ABC Co", description="My customer", is_customer=True) + + # Create a Part to ship + self.part = Part.objects.create(name='Spanner', salable=True, description='A spanner that I sell') + + # Create some stock! + StockItem.objects.create(part=self.part, quantity=100) + StockItem.objects.create(part=self.part, quantity=200) + + # Create a SalesOrder to ship against + self.order = SalesOrder.objects.create( + customer=self.customer, + reference='1234', + customer_reference='ABC 55555' + ) + + # Create a line item + self.line = SalesOrderLineItem.objects.create(quantity=50, order=self.order, part=self.part) + + def test_empty_order(self): + self.assertEqual(self.line.quantity, 50) + self.assertEqual(self.line.allocated_quantity(), 0) + self.assertEqual(self.line.fulfilled_quantity(), 0) + self.assertFalse(self.line.is_fully_allocated()) + self.assertFalse(self.line.is_over_allocated()) + + self.assertTrue(self.order.is_pending) + self.assertFalse(self.order.is_fully_allocated()) + + def test_add_duplicate_line_item(self): + # Adding a duplicate line item to a SalesOrder must throw an error + + with self.assertRaises(IntegrityError): + SalesOrderLineItem.objects.create(order=self.order, part=self.part) + + def allocate_stock(self, full=True): + # Allocate stock to the order + SalesOrderAllocation.objects.create( + line=self.line, + item=StockItem.objects.get(pk=1), + quantity=25) + + SalesOrderAllocation.objects.create( + line=self.line, + item=StockItem.objects.get(pk=2), + quantity=25 if full else 20 + ) + + def test_allocate_partial(self): + # Partially allocate stock + self.allocate_stock(False) + + self.assertFalse(self.order.is_fully_allocated()) + self.assertFalse(self.line.is_fully_allocated()) + self.assertEqual(self.line.allocated_quantity(), 45) + self.assertEqual(self.line.fulfilled_quantity(), 0) + + def test_allocate_full(self): + # Fully allocate stock + self.allocate_stock(True) + + self.assertTrue(self.order.is_fully_allocated()) + self.assertTrue(self.line.is_fully_allocated()) + self.assertEqual(self.line.allocated_quantity(), 50) + + def test_order_cancel(self): + # Allocate line items then cancel the order + + self.allocate_stock(True) + + self.assertEqual(SalesOrderAllocation.objects.count(), 2) + self.assertEqual(self.order.status, status.SalesOrderStatus.PENDING) + + self.order.cancel_order() + self.assertEqual(SalesOrderAllocation.objects.count(), 0) + self.assertEqual(self.order.status, status.SalesOrderStatus.CANCELLED) + + # Now try to ship it - should fail + with self.assertRaises(ValidationError): + self.order.ship_order(None) + + def test_ship_order(self): + # Allocate line items, then ship the order + + # Assert some stuff before we run the test + # Initially there are two stock items + self.assertEqual(StockItem.objects.count(), 2) + + # Take 25 units from each StockItem + self.allocate_stock(True) + + self.assertEqual(SalesOrderAllocation.objects.count(), 2) + + self.order.ship_order(None) + + # There should now be 4 stock items + self.assertEqual(StockItem.objects.count(), 4) + + self.assertEqual(StockItem.objects.get(pk=1).quantity, 75) + self.assertEqual(StockItem.objects.get(pk=2).quantity, 175) + self.assertEqual(StockItem.objects.get(pk=3).quantity, 25) + self.assertEqual(StockItem.objects.get(pk=3).quantity, 25) + + self.assertEqual(StockItem.objects.get(pk=1).sales_order, None) + self.assertEqual(StockItem.objects.get(pk=2).sales_order, None) + self.assertEqual(StockItem.objects.get(pk=3).sales_order, self.order) + self.assertEqual(StockItem.objects.get(pk=4).sales_order, self.order) + + # And no allocations + self.assertEqual(SalesOrderAllocation.objects.count(), 0) + + self.assertEqual(self.order.status, status.SalesOrderStatus.SHIPPED) + + self.assertTrue(self.order.is_fully_allocated()) + self.assertTrue(self.line.is_fully_allocated()) + self.assertEqual(self.line.fulfilled_quantity(), 50) + self.assertEqual(self.line.allocated_quantity(), 0) From 646dd65d278bd7f80bc6eff469173d3dc8091222 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 27 Apr 2020 20:05:02 +1000 Subject: [PATCH 098/104] Re-enable auto-allocation for build --- InvenTree/build/models.py | 2 +- InvenTree/build/templates/build/allocate.html | 26 ++++++++++++++----- .../build/templates/build/auto_allocate.html | 21 ++++++++------- InvenTree/part/templates/part/tabs.html | 2 +- 4 files changed, 33 insertions(+), 18 deletions(-) diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index 6bc6160af6..f58f3b4206 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -450,7 +450,7 @@ class BuildItem(models.Model): q=self.stock_item.quantity ))] - if self.stock_item.quantity - self.stock_item.allocation_count() < self.quantity: + if self.stock_item.quantity - self.stock_item.allocation_count() + self.quantity < self.quantity: errors['quantity'] = _('StockItem is over-allocated') if self.quantity <= 0: diff --git a/InvenTree/build/templates/build/allocate.html b/InvenTree/build/templates/build/allocate.html index 0e4a39cc29..17c6ba7ef6 100644 --- a/InvenTree/build/templates/build/allocate.html +++ b/InvenTree/build/templates/build/allocate.html @@ -12,11 +12,13 @@ InvenTree | Allocate Parts {% include "build/tabs.html" with tab='allocate' %}
+ {% if build.status == BuildStatus.PENDING %}
- {% if 0 %}{% endif %} +
+ {% endif %}
@@ -151,7 +153,15 @@ InvenTree | Allocate Parts field: 'quantity', title: 'Quantity', formatter: function(value, row, index, field) { - return renderLink(value, `/stock/item/${row.stock_item}/`); + var text = ''; + + if (row.serial && row.quantity == 1) { + text = `{% trans "Serial Number" %}: ${row.serial}`; + } else { + text = `{% trans "Quantity" %}: ${row.quantity}`; + } + + return renderLink(text, `/stock/item/${row.stock_item}/`); }, }, { @@ -330,8 +340,8 @@ InvenTree | Allocate Parts ], }); - {% if 0 %} - $("#auto-allocate-build").on('click', function() { + {% if build.status == BuildStatus.PENDING %} + $("#btn-allocate").on('click', function() { launchModalForm( "{% url 'build-auto-allocate' build.id %}", { @@ -339,8 +349,7 @@ InvenTree | Allocate Parts } ); }); - {% endif %} - + $('#btn-unallocate').on('click', function() { launchModalForm( "{% url 'build-unallocate' build.id %}", @@ -349,7 +358,7 @@ InvenTree | Allocate Parts } ); }); - + $("#btn-order-parts").click(function() { launchModalForm("/order/purchase-order/order-parts/", { data: { @@ -357,5 +366,8 @@ InvenTree | Allocate Parts }, }); }); + + {% endif %} {% endblock %} + \ No newline at end of file diff --git a/InvenTree/build/templates/build/auto_allocate.html b/InvenTree/build/templates/build/auto_allocate.html index d278b9cd18..fc1e42096b 100644 --- a/InvenTree/build/templates/build/auto_allocate.html +++ b/InvenTree/build/templates/build/auto_allocate.html @@ -1,22 +1,23 @@ {% extends "modal_form.html" %} - +{% load i18n %} {% block pre_form_content %} {{ block.super }} -Build: {{ build.title }} - {{ build.quantity }} x {{ build.part.full_name }} -

-Automatically allocate stock to this build? -
+
+{% trans "Automatically Allocate Stock" %}
+{% trans "Stock Items are selected for automatic allocation if there is only a single stock item available." %}
+{% trans "The following stock items will be allocated to the build:" %}
+
{% if allocations %} - - - + + + {% for item in allocations %} @@ -34,7 +35,9 @@ Automatically allocate stock to this build?
PartQuantityLocation{% trans "Part" %}{% trans "Quantity" %}{% trans "Location" %}
{% else %} -No stock could be selected for automatic build allocation. +
+ {% trans "No stock items found that can be allocated to this build" %} +
{% endif %} {% endblock %} \ No newline at end of file diff --git a/InvenTree/part/templates/part/tabs.html b/InvenTree/part/templates/part/tabs.html index 283afcd2ae..88cdcd587d 100644 --- a/InvenTree/part/templates/part/tabs.html +++ b/InvenTree/part/templates/part/tabs.html @@ -49,7 +49,7 @@ {% endif %} {% if part.trackable %} - + {% trans "Tracking" %} {% if parts.serials.all|length > 0 %} {{ part.serials.all|length }} From 2b99cf353a525e019a6864a4dc4503cb90a96805 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 27 Apr 2020 20:16:41 +1000 Subject: [PATCH 099/104] Fix for build complete form --- InvenTree/build/models.py | 2 +- InvenTree/build/templates/build/complete.html | 10 +++++----- InvenTree/order/models.py | 3 ++- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index f58f3b4206..c6b6a741da 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -323,7 +323,7 @@ class Build(MPTTModel): if not self.isPartFullyAllocated(part): return False - return True + return True def isPartFullyAllocated(self, part): """ diff --git a/InvenTree/build/templates/build/complete.html b/InvenTree/build/templates/build/complete.html index a75cee2785..a48b831645 100644 --- a/InvenTree/build/templates/build/complete.html +++ b/InvenTree/build/templates/build/complete.html @@ -5,15 +5,15 @@

{% trans "Build" %} - {{ build }}

-{% if not build.isFullyAllocated %} +{% if build.isFullyAllocated %} +
+

{% trans "Build order allocation is complete" %}

+
+{% else %}

{% trans "Warning: Build order allocation is not complete" %}

{% trans "Build Order has not been fully allocated. Ensure that all Stock Items have been allocated to the Build" %}
-{% else %} -
-

{% trans "Build order allocation is complete" %}

-
{% endif %}
diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index 0b2ebd3707..6198eb16bc 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -541,7 +541,8 @@ class SalesOrderAllocation(models.Model): if self.quantity > self.item.quantity: errors['quantity'] = _('Allocation quantity cannot exceed stock quantity') - if self.item.quantity - self.item.allocation_count() < self.quantity: + # TODO: The logic here needs improving. Do we need to subtract our own amount, or something? + if self.item.quantity - self.item.allocation_count() + self.quantity < self.quantity: errors['quantity'] = _('StockItem is over-allocated') if self.quantity <= 0: From 489dfa1823b07a653e09ab0a76ff3942378679c0 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 27 Apr 2020 20:45:01 +1000 Subject: [PATCH 100/104] Bug fix for a code path which resulted in a form failing validation but not showing any errors! This one has been here for a while! --- InvenTree/stock/views.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index f32f3a9a95..b6ec8bda85 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -838,9 +838,12 @@ class StockItemCreate(AjaxCreateView): if part_id: try: part = Part.objects.get(pk=part_id) - initials['part'] = part - initials['location'] = part.get_default_location() - initials['supplier_part'] = part.default_supplier + # Check that the supplied part is 'valid' + if not part.is_template and part.active and not part.virtual: + initials['part'] = part + initials['location'] = part.get_default_location() + initials['supplier_part'] = part.default_supplier + except (ValueError, Part.DoesNotExist): pass From 35f48ed89966ae9a55b0b20176e2f1074f9922fc Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 27 Apr 2020 20:46:34 +1000 Subject: [PATCH 101/104] Delete BuildItem objects once a Build has been completed - Much more complicated template for build allocation page! - This will require some refactoring at some point ... --- InvenTree/build/models.py | 5 +- InvenTree/build/templates/build/allocate.html | 65 ++++++++++++++++++- InvenTree/part/templates/part/tabs.html | 4 +- 3 files changed, 68 insertions(+), 6 deletions(-) diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index c6b6a741da..6d90f56402 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -265,8 +265,9 @@ class Build(MPTTModel): for build_item in self.allocated_stock.all().prefetch_related('stock_item'): build_item.complete_allocation(user) - # TODO - Remove the builditem from the database - # build_item.delete() + # Check that the stock-item has been assigned to this build, and remove the builditem from the database + if build_item.stock_item.build_order == self: + build_item.delete() notes = 'Built {q} on {now}'.format( q=self.quantity, diff --git a/InvenTree/build/templates/build/allocate.html b/InvenTree/build/templates/build/allocate.html index 17c6ba7ef6..b90508a7d8 100644 --- a/InvenTree/build/templates/build/allocate.html +++ b/InvenTree/build/templates/build/allocate.html @@ -155,22 +155,39 @@ InvenTree | Allocate Parts formatter: function(value, row, index, field) { var text = ''; + var url = ''; + if (row.serial && row.quantity == 1) { text = `{% trans "Serial Number" %}: ${row.serial}`; } else { text = `{% trans "Quantity" %}: ${row.quantity}`; } - return renderLink(text, `/stock/item/${row.stock_item}/`); + {% if build.status == BuildStatus.COMPLETE %} + url = `/stock/item/${row.pk}/`; + {% else %} + url = `/stock/item/${row.stock_item}/`; + {% endif %} + + return renderLink(text, url); }, }, { field: 'location', title: '{% trans "Location" %}', formatter: function(value, row, index, field) { - return renderLink(row.stock_item_detail.location_name, `/stock/location/${row.stock_item_detail.location}/`); + {% if build.status == BuildStatus.COMPLETE %} + var text = row.location_detail.pathstring; + var url = `/stock/location/${row.location}/`; + {% else %} + var text = row.stock_item_detail.location_name; + var url = `/stock/location/${row.stock_item_detail.location}/`; + {% endif %} + + return renderLink(text, url); } }, + {% if build.status == BuildStatus.PENDING %} { field: 'buttons', title: 'Actions', @@ -190,6 +207,7 @@ InvenTree | Allocate Parts return html; }, }, + {% endif %} ] }); @@ -211,6 +229,42 @@ InvenTree | Allocate Parts formatNoMatches: function() { return "{% trans 'No BOM items found' %}"; }, onLoadSuccess: function(tableData) { // Once the BOM data are loaded, request allocation data for the build + {% if build.status == BuildStatus.COMPLETE %} + // Request StockItem which have been assigned to this build + inventreeGet('/api/stock/', + { + build_order: {{ build.id }}, + location_detail: true, + }, + { + success: function(data) { + // Iterate through the returned data, group by "part", + var allocations = {}; + + data.forEach(function(item) { + // Group allocations by referenced 'part' + var key = parseInt(item.part); + + if (!(key in allocations)) { + allocations[key] = new Array(); + } + + allocations[key].push(item); + }); + + for (var key in allocations) { + + var tableRow = buildTable.bootstrapTable('getRowByUniqueId', key); + + tableRow.allocations = allocations[key]; + + buildTable.bootstrapTable('updateByUniqueId', key, tableRow, true); + } + }, + }, + ); + + {% else %} inventreeGet('/api/build/item/', { build: {{ build.id }}, @@ -249,6 +303,7 @@ InvenTree | Allocate Parts } }, ); + {% endif %} }, queryParams: { part: {{ build.part.id }}, @@ -288,7 +343,11 @@ InvenTree | Allocate Parts { sortable: true, field: 'allocated', + {% if build.status == BuildStatus.COMPLETE %} + title: '{% trans "Assigned" %}', + {% else %} title: '{% trans "Allocated" %}', + {% endif %} formatter: function(value, row) { var allocated = sumAllocations(row); @@ -313,6 +372,7 @@ InvenTree | Allocate Parts return (progressA < progressB) ? 1 : -1; } }, + {% if build.status == BuildStatus.PENDING %} { field: 'buttons', formatter: function(value, row, index, field) { @@ -337,6 +397,7 @@ InvenTree | Allocate Parts return html; }, } + {% endif %} ], }); diff --git a/InvenTree/part/templates/part/tabs.html b/InvenTree/part/templates/part/tabs.html index 88cdcd587d..b47ebfe329 100644 --- a/InvenTree/part/templates/part/tabs.html +++ b/InvenTree/part/templates/part/tabs.html @@ -48,8 +48,8 @@ {% trans "Sales Orders" %} {{ part.sales_orders|length }} {% endif %} - {% if part.trackable %} - + {% if 0 and part.trackable %} + {% trans "Tracking" %} {% if parts.serials.all|length > 0 %} {{ part.serials.all|length }} From cb3fe0fc35825ec9fe6c35a808f2a96a994f55dc Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 27 Apr 2020 21:28:44 +1000 Subject: [PATCH 102/104] Add some more unit testing for the builds --- InvenTree/InvenTree/settings.py | 6 +- InvenTree/build/models.py | 11 +- InvenTree/build/test_build.py | 181 ++++++++++++++++++++++++++++ InvenTree/order/test_sales_order.py | 2 + 4 files changed, 193 insertions(+), 7 deletions(-) create mode 100644 InvenTree/build/test_build.py diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index e409c3dc4b..b787668a9d 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -205,10 +205,12 @@ When running unit tests, enforce usage of sqlite3 database, so that the tests can be run in RAM without any setup requirements """ if 'test' in sys.argv: - eprint('Running tests - Using sqlite3 memory database') + eprint('InvenTree: Running tests - Using sqlite3 memory database') DATABASES['default'] = { + # Ensure sqlite3 backend is being used 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': 'test_db.sqlite3' + # Doesn't matter what the database is called, it is executed in RAM + 'NAME': 'ram_test_db.sqlite3', } # Database backend selection diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index 6d90f56402..abafcb58df 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -371,11 +371,12 @@ class Build(MPTTModel): parts = [] for item in self.part.bom_items.all().prefetch_related('sub_part'): - part = {'part': item.sub_part, - 'per_build': item.quantity, - 'quantity': item.quantity * self.quantity, - 'allocated': self.getAllocatedQuantity(item.sub_part) - } + part = { + 'part': item.sub_part, + 'per_build': item.quantity, + 'quantity': item.quantity * self.quantity, + 'allocated': self.getAllocatedQuantity(item.sub_part) + } parts.append(part) diff --git a/InvenTree/build/test_build.py b/InvenTree/build/test_build.py new file mode 100644 index 0000000000..125bc684da --- /dev/null +++ b/InvenTree/build/test_build.py @@ -0,0 +1,181 @@ +# -*- coding: utf-8 -*- + +from django.test import TestCase + +from django.core.exceptions import ValidationError +from django.db import transaction +from django.db.utils import IntegrityError + +from build.models import Build, BuildItem +from stock.models import StockItem +from part.models import Part, BomItem +from InvenTree import status_codes as status + + +class BuildTest(TestCase): + """ + Run some tests to ensure that the Build model is working properly. + """ + + def setUp(self): + """ + Initialize data to use for these tests. + """ + + # Create a base "Part" + self.assembly = Part.objects.create( + name="An assembled part", + description="Why does it matter what my description is?", + assembly=True + ) + + self.sub_part_1 = Part.objects.create( + name="Widget A", + description="A widget", + component=True + ) + + self.sub_part_2 = Part.objects.create( + name="Widget B", + description="A widget", + component=True + ) + + # Create BOM item links for the parts + BomItem.objects.create( + part=self.assembly, + sub_part=self.sub_part_1, + quantity=10 + ) + + BomItem.objects.create( + part=self.assembly, + sub_part=self.sub_part_2, + quantity=25 + ) + + # Create a "Build" object to make 10x objects + self.build = Build.objects.create( + title="This is a build", + part=self.assembly, + quantity=10 + ) + + # Create some stock items to assign to the build + self.stock_1_1 = StockItem.objects.create(part=self.sub_part_1, quantity=1000) + self.stock_1_2 = StockItem.objects.create(part=self.sub_part_1, quantity=100) + + self.stock_2_1 = StockItem.objects.create(part=self.sub_part_2, quantity=5000) + + def test_init(self): + # Perform some basic tests before we start the ball rolling + + self.assertEqual(StockItem.objects.count(), 3) + self.assertEqual(self.build.status, status.BuildStatus.PENDING) + self.assertFalse(self.build.isFullyAllocated()) + + self.assertFalse(self.build.isPartFullyAllocated(self.sub_part_1)) + self.assertFalse(self.build.isPartFullyAllocated(self.sub_part_2)) + + self.assertEqual(self.build.getRequiredQuantity(self.sub_part_1), 100) + self.assertEqual(self.build.getRequiredQuantity(self.sub_part_2), 250) + + self.assertTrue(self.build.can_build) + self.assertFalse(self.build.is_complete) + + def test_duplicate_bom_line(self): + # Try to add a duplicate BOM item - it should fail! + + with self.assertRaises(IntegrityError): + BomItem.objects.create( + part=self.assembly, + sub_part=self.sub_part_1, + quantity=99 + ) + + def allocate_stock(self, q11, q12, q21): + # Assign stock to this build + + BuildItem.objects.create( + build=self.build, + stock_item=self.stock_1_1, + quantity=q11 + ) + + BuildItem.objects.create( + build=self.build, + stock_item=self.stock_1_2, + quantity=q12 + ) + + BuildItem.objects.create( + build=self.build, + stock_item=self.stock_2_1, + quantity=q21 + ) + + with transaction.atomic(): + with self.assertRaises(IntegrityError): + BuildItem.objects.create( + build=self.build, + stock_item=self.stock_2_1, + quantity=99 + ) + + self.assertEqual(BuildItem.objects.count(), 3) + + def test_partial_allocation(self): + + self.allocate_stock(50, 50, 200) + + self.assertFalse(self.build.isFullyAllocated()) + self.assertTrue(self.build.isPartFullyAllocated(self.sub_part_1)) + self.assertFalse(self.build.isPartFullyAllocated(self.sub_part_2)) + + + def test_cancel(self): + + self.allocate_stock(50, 50, 200) + self.build.cancelBuild(None) + + self.assertEqual(BuildItem.objects.count(), 0) + + def test_complete(self): + + self.allocate_stock(50, 50, 250) + + self.assertTrue(self.build.isFullyAllocated()) + + self.build.completeBuild(None, None, None) + + self.assertEqual(self.build.status, status.BuildStatus.COMPLETE) + + # the original BuildItem objects should have been deleted! + self.assertEqual(BuildItem.objects.count(), 0) + + # Four new stock items should have been created! + # - One for the build output + # - Three for the split items assigned to the build + self.assertEqual(StockItem.objects.count(), 7) + + # Stock should have been subtracted from the original items + self.assertEqual(StockItem.objects.get(pk=1).quantity, 950) + self.assertEqual(StockItem.objects.get(pk=2).quantity, 50) + self.assertEqual(StockItem.objects.get(pk=3).quantity, 4750) + + # New stock items created and assigned to the build + self.assertEqual(StockItem.objects.get(pk=4).quantity, 50) + self.assertEqual(StockItem.objects.get(pk=4).build_order, self.build) + self.assertEqual(StockItem.objects.get(pk=4).status, status.StockStatus.ASSIGNED_TO_BUILD) + + self.assertEqual(StockItem.objects.get(pk=5).quantity, 50) + self.assertEqual(StockItem.objects.get(pk=5).build_order, self.build) + self.assertEqual(StockItem.objects.get(pk=5).status, status.StockStatus.ASSIGNED_TO_BUILD) + + self.assertEqual(StockItem.objects.get(pk=6).quantity, 250) + self.assertEqual(StockItem.objects.get(pk=6).build_order, self.build) + self.assertEqual(StockItem.objects.get(pk=6).status, status.StockStatus.ASSIGNED_TO_BUILD) + + # And a new stock item created for the build output + self.assertEqual(StockItem.objects.get(pk=7).quantity, 10) + self.assertEqual(StockItem.objects.get(pk=7).build, self.build) \ No newline at end of file diff --git a/InvenTree/order/test_sales_order.py b/InvenTree/order/test_sales_order.py index d0d34a517c..6cc48c3b6f 100644 --- a/InvenTree/order/test_sales_order.py +++ b/InvenTree/order/test_sales_order.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- + from django.test import TestCase from django.core.exceptions import ValidationError From db9970e5df664f92fceb4c1a07ce28c61369e334 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 27 Apr 2020 21:44:59 +1000 Subject: [PATCH 103/104] Add some further unit tests for the Build model --- InvenTree/build/models.py | 5 +-- InvenTree/build/test_build.py | 60 +++++++++++++++++++++++++++++++---- 2 files changed, 54 insertions(+), 11 deletions(-) diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index abafcb58df..8a00f0ad63 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -461,10 +461,7 @@ class BuildItem(models.Model): if self.stock_item.serial and not self.quantity == 1: errors['quantity'] = _('Quantity must be 1 for serialized stock') - except StockItem.DoesNotExist: - pass - - except Part.DoesNotExist: + except (StockItem.DoesNotExist, Part.DoesNotExist): pass if len(errors) > 0: diff --git a/InvenTree/build/test_build.py b/InvenTree/build/test_build.py index 125bc684da..c1fb4a5efd 100644 --- a/InvenTree/build/test_build.py +++ b/InvenTree/build/test_build.py @@ -11,6 +11,8 @@ from stock.models import StockItem from part.models import Part, BomItem from InvenTree import status_codes as status +from InvenTree.helpers import ExtractSerialNumbers + class BuildTest(TestCase): """ @@ -26,7 +28,8 @@ class BuildTest(TestCase): self.assembly = Part.objects.create( name="An assembled part", description="Why does it matter what my description is?", - assembly=True + assembly=True, + trackable=True, ) self.sub_part_1 = Part.objects.create( @@ -83,6 +86,33 @@ class BuildTest(TestCase): self.assertTrue(self.build.can_build) self.assertFalse(self.build.is_complete) + # Delete some stock and see if the build can still be completed + self.stock_2_1.delete() + self.assertFalse(self.build.can_build) + + def test_build_item_clean(self): + # Ensure that dodgy BuildItem objects cannot be created + + stock = StockItem.objects.create(part=self.assembly, quantity=99) + + # Create a BuiltItem which points to an invalid StockItem + b = BuildItem(stock_item=stock, build=self.build, quantity=10) + + with self.assertRaises(ValidationError): + b.clean() + + # Create a BuildItem which has too much stock assigned + b = BuildItem(stock_item=self.stock_1_1, build=self.build, quantity=9999999) + + with self.assertRaises(ValidationError): + b.clean() + + # Negative stock? Not on my watch! + b = BuildItem(stock_item=self.stock_1_1, build=self.build, quantity=-99) + + with self.assertRaises(ValidationError): + b.clean() + def test_duplicate_bom_line(self): # Try to add a duplicate BOM item - it should fail! @@ -132,6 +162,18 @@ class BuildTest(TestCase): self.assertTrue(self.build.isPartFullyAllocated(self.sub_part_1)) self.assertFalse(self.build.isPartFullyAllocated(self.sub_part_2)) + self.build.unallocateStock() + self.assertEqual(BuildItem.objects.count(), 0) + + def test_auto_allocate(self): + + allocations = self.build.getAutoAllocations() + + self.assertEqual(len(allocations), 1) + + self.build.autoAllocate() + self.assertEqual(BuildItem.objects.count(), 1) + self.assertTrue(self.build.isPartFullyAllocated(self.sub_part_2)) def test_cancel(self): @@ -146,17 +188,20 @@ class BuildTest(TestCase): self.assertTrue(self.build.isFullyAllocated()) - self.build.completeBuild(None, None, None) + # Generate some serial numbers! + serials = ExtractSerialNumbers("1-10", 10) + + self.build.completeBuild(None, serials, None) self.assertEqual(self.build.status, status.BuildStatus.COMPLETE) # the original BuildItem objects should have been deleted! self.assertEqual(BuildItem.objects.count(), 0) - # Four new stock items should have been created! - # - One for the build output + # New stock items should have been created! + # - Ten for the build output (as the part was serialized) # - Three for the split items assigned to the build - self.assertEqual(StockItem.objects.count(), 7) + self.assertEqual(StockItem.objects.count(), 16) # Stock should have been subtracted from the original items self.assertEqual(StockItem.objects.get(pk=1).quantity, 950) @@ -177,5 +222,6 @@ class BuildTest(TestCase): self.assertEqual(StockItem.objects.get(pk=6).status, status.StockStatus.ASSIGNED_TO_BUILD) # And a new stock item created for the build output - self.assertEqual(StockItem.objects.get(pk=7).quantity, 10) - self.assertEqual(StockItem.objects.get(pk=7).build, self.build) \ No newline at end of file + self.assertEqual(StockItem.objects.get(pk=7).quantity, 1) + self.assertEqual(StockItem.objects.get(pk=7).serial, 1) + self.assertEqual(StockItem.objects.get(pk=7).build, self.build) From 79836c77ef95bf04ad7bb2559c9274cbd43f2fdb Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 27 Apr 2020 22:03:15 +1000 Subject: [PATCH 104/104] Bumped version thing --- InvenTree/InvenTree/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/InvenTree/version.py b/InvenTree/InvenTree/version.py index 2b0d02bfe9..3ac225fd6e 100644 --- a/InvenTree/InvenTree/version.py +++ b/InvenTree/InvenTree/version.py @@ -6,7 +6,7 @@ import subprocess from common.models import InvenTreeSetting import django -INVENTREE_SW_VERSION = "0.0.12 pre" +INVENTREE_SW_VERSION = "0.1.0 pre" def inventreeInstanceName():