mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
commit
5f2e3dbbda
@ -62,6 +62,7 @@ INSTALLED_APPS = [
|
||||
'stock.apps.StockConfig',
|
||||
'company.apps.CompanyConfig',
|
||||
'build.apps.BuildConfig',
|
||||
'order.apps.OrderConfig',
|
||||
|
||||
# Third part add-ons
|
||||
'django_filters', # Extended filter functionality
|
||||
|
91
InvenTree/InvenTree/status_codes.py
Normal file
91
InvenTree/InvenTree/status_codes.py
Normal file
@ -0,0 +1,91 @@
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
|
||||
class StatusCode:
|
||||
|
||||
@classmethod
|
||||
def items(cls):
|
||||
return cls.options.items()
|
||||
|
||||
@classmethod
|
||||
def label(cls, value):
|
||||
""" Return the status code label associated with the provided value """
|
||||
return cls.options.get(value, '')
|
||||
|
||||
|
||||
class OrderStatus(StatusCode):
|
||||
|
||||
# Order status codes
|
||||
PENDING = 10 # Order is pending (not yet placed)
|
||||
PLACED = 20 # Order has been placed
|
||||
COMPLETE = 30 # Order has been completed
|
||||
CANCELLED = 40 # Order was cancelled
|
||||
LOST = 50 # Order was lost
|
||||
RETURNED = 60 # Order was returned
|
||||
|
||||
options = {
|
||||
PENDING: _("Pending"),
|
||||
PLACED: _("Placed"),
|
||||
COMPLETE: _("Complete"),
|
||||
CANCELLED: _("Cancelled"),
|
||||
LOST: _("Lost"),
|
||||
RETURNED: _("Returned"),
|
||||
}
|
||||
|
||||
# Open orders
|
||||
OPEN = [
|
||||
PENDING,
|
||||
PLACED,
|
||||
]
|
||||
|
||||
# Failed orders
|
||||
FAILED = [
|
||||
CANCELLED,
|
||||
LOST,
|
||||
RETURNED
|
||||
]
|
||||
|
||||
|
||||
class StockStatus(StatusCode):
|
||||
|
||||
OK = 10 # Item is OK
|
||||
ATTENTION = 50 # Item requires attention
|
||||
DAMAGED = 55 # Item is damaged
|
||||
DESTROYED = 60 # Item is destroyed
|
||||
LOST = 70 # Item has been lost
|
||||
|
||||
options = {
|
||||
OK: _("OK"),
|
||||
ATTENTION: _("Attention needed"),
|
||||
DAMAGED: _("Damaged"),
|
||||
DESTROYED: _("Destroyed"),
|
||||
LOST: _("Lost"),
|
||||
}
|
||||
|
||||
# The following codes correspond to parts that are 'available'
|
||||
AVAILABLE_CODES = [
|
||||
OK,
|
||||
ATTENTION,
|
||||
DAMAGED
|
||||
]
|
||||
|
||||
|
||||
class BuildStatus(StatusCode):
|
||||
|
||||
# Build status codes
|
||||
PENDING = 10 # Build is pending / active
|
||||
ALLOCATED = 20 # Parts have been removed from stock
|
||||
CANCELLED = 30 # Build was cancelled
|
||||
COMPLETE = 40 # Build is complete
|
||||
|
||||
options = {
|
||||
PENDING: _("Pending"),
|
||||
ALLOCATED: _("Allocated"),
|
||||
CANCELLED: _("Cancelled"),
|
||||
COMPLETE: _("Complete"),
|
||||
}
|
||||
|
||||
ACTIVE_CODES = [
|
||||
PENDING,
|
||||
ALLOCATED
|
||||
]
|
@ -15,10 +15,9 @@ from company.urls import supplier_part_urls
|
||||
from company.urls import price_break_urls
|
||||
|
||||
from part.urls import part_urls
|
||||
|
||||
from stock.urls import stock_urls
|
||||
|
||||
from build.urls import build_urls
|
||||
from order.urls import order_urls
|
||||
|
||||
from part.api import part_api_urls, bom_api_urls
|
||||
from company.api import company_api_urls
|
||||
@ -56,6 +55,7 @@ urlpatterns = [
|
||||
url(r'^stock/', include(stock_urls)),
|
||||
|
||||
url(r'^company/', include(company_urls)),
|
||||
url(r'^order/', include(order_urls)),
|
||||
|
||||
url(r'^build/', include(build_urls)),
|
||||
|
||||
|
@ -92,6 +92,10 @@ class AjaxMixin(object):
|
||||
on the client side.
|
||||
"""
|
||||
|
||||
# By default, point to the modal_form template
|
||||
# (this can be overridden by a child class)
|
||||
ajax_template_name = 'modal_form.html'
|
||||
|
||||
ajax_form_action = ''
|
||||
ajax_form_title = ''
|
||||
|
||||
@ -165,10 +169,6 @@ class AjaxView(AjaxMixin, View):
|
||||
""" An 'AJAXified' View for displaying an object
|
||||
"""
|
||||
|
||||
# By default, point to the modal_form template
|
||||
# (this can be overridden by a child class)
|
||||
ajax_template_name = 'modal_form.html'
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
return JsonResponse('', safe=False)
|
||||
|
||||
|
19
InvenTree/build/migrations/0005_auto_20190604_2217.py
Normal file
19
InvenTree/build/migrations/0005_auto_20190604_2217.py
Normal file
@ -0,0 +1,19 @@
|
||||
# Generated by Django 2.2 on 2019-06-04 12:17
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('build', '0004_auto_20190525_2356'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='build',
|
||||
name='part',
|
||||
field=models.ForeignKey(help_text='Select part to build', limit_choices_to={'active': True, 'assembly': True, 'is_template': False}, on_delete=django.db.models.deletion.CASCADE, related_name='builds', to='part.Part'),
|
||||
),
|
||||
]
|
@ -16,6 +16,8 @@ from django.db import models, transaction
|
||||
from django.db.models import Sum
|
||||
from django.core.validators import MinValueValidator
|
||||
|
||||
from InvenTree.status_codes import BuildStatus
|
||||
|
||||
from stock.models import StockItem
|
||||
from part.models import Part, BomItem
|
||||
|
||||
@ -69,21 +71,8 @@ class Build(models.Model):
|
||||
help_text='Number of parts to build'
|
||||
)
|
||||
|
||||
# Build status codes
|
||||
PENDING = 10 # Build is pending / active
|
||||
ALLOCATED = 20 # Parts have been removed from stock
|
||||
CANCELLED = 30 # Build was cancelled
|
||||
COMPLETE = 40 # Build is complete
|
||||
|
||||
#: Build status codes
|
||||
BUILD_STATUS_CODES = {PENDING: _("Pending"),
|
||||
ALLOCATED: _("Allocated"),
|
||||
CANCELLED: _("Cancelled"),
|
||||
COMPLETE: _("Complete"),
|
||||
}
|
||||
|
||||
status = models.PositiveIntegerField(default=PENDING,
|
||||
choices=BUILD_STATUS_CODES.items(),
|
||||
status = models.PositiveIntegerField(default=BuildStatus.PENDING,
|
||||
choices=BuildStatus.items(),
|
||||
validators=[MinValueValidator(0)],
|
||||
help_text='Build status')
|
||||
|
||||
@ -253,7 +242,7 @@ class Build(models.Model):
|
||||
item.save()
|
||||
|
||||
# Finally, mark the build as complete
|
||||
self.status = self.COMPLETE
|
||||
self.status = BuildStatus.COMPLETE
|
||||
self.save()
|
||||
|
||||
def getRequiredQuantity(self, part):
|
||||
@ -325,14 +314,12 @@ class Build(models.Model):
|
||||
- HOLDING
|
||||
"""
|
||||
|
||||
return self.status in [
|
||||
self.PENDING,
|
||||
]
|
||||
return self.status in BuildStatus.ACTIVE_CODES
|
||||
|
||||
@property
|
||||
def is_complete(self):
|
||||
""" Returns True if the build status is COMPLETE """
|
||||
return self.status == self.COMPLETE
|
||||
return self.status == BuildStatus.COMPLETE
|
||||
|
||||
|
||||
class BuildItem(models.Model):
|
||||
|
@ -6,6 +6,8 @@ from django.test import TestCase
|
||||
from .models import Build
|
||||
from part.models import Part
|
||||
|
||||
from InvenTree.status_codes import BuildStatus
|
||||
|
||||
|
||||
class BuildTestSimple(TestCase):
|
||||
|
||||
@ -14,14 +16,14 @@ class BuildTestSimple(TestCase):
|
||||
description='Simple description')
|
||||
Build.objects.create(part=part,
|
||||
batch='B1',
|
||||
status=Build.PENDING,
|
||||
status=BuildStatus.PENDING,
|
||||
title='Building 7 parts',
|
||||
quantity=7,
|
||||
notes='Some simple notes')
|
||||
|
||||
Build.objects.create(part=part,
|
||||
batch='B2',
|
||||
status=Build.COMPLETE,
|
||||
status=BuildStatus.COMPLETE,
|
||||
title='Building 21 parts',
|
||||
quantity=21,
|
||||
notes='Some simple notes')
|
||||
|
@ -15,6 +15,7 @@ from stock.models import StockLocation, StockItem
|
||||
|
||||
from InvenTree.views import AjaxUpdateView, AjaxCreateView, AjaxDeleteView
|
||||
from InvenTree.helpers import str2bool
|
||||
from InvenTree.status_codes import BuildStatus
|
||||
|
||||
|
||||
class BuildIndex(ListView):
|
||||
@ -32,10 +33,12 @@ class BuildIndex(ListView):
|
||||
|
||||
context = super(BuildIndex, self).get_context_data(**kwargs).copy()
|
||||
|
||||
context['active'] = self.get_queryset().filter(status__in=[Build.PENDING, ])
|
||||
context['BuildStatus'] = BuildStatus
|
||||
|
||||
context['completed'] = self.get_queryset().filter(status=Build.COMPLETE)
|
||||
context['cancelled'] = self.get_queryset().filter(status=Build.CANCELLED)
|
||||
context['active'] = self.get_queryset().filter(status__in=BuildStatus.ACTIVE_CODES)
|
||||
|
||||
context['completed'] = self.get_queryset().filter(status=BuildStatus.COMPLETE)
|
||||
context['cancelled'] = self.get_queryset().filter(status=BuildStatus.CANCELLED)
|
||||
|
||||
return context
|
||||
|
||||
|
@ -10,13 +10,16 @@ import os
|
||||
import math
|
||||
|
||||
from django.core.validators import MinValueValidator
|
||||
from django.db import models
|
||||
from django.db.models import Sum
|
||||
|
||||
from django.apps import apps
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.conf import settings
|
||||
from django.contrib.staticfiles.templatetags.staticfiles import static
|
||||
|
||||
from InvenTree.status_codes import OrderStatus
|
||||
|
||||
|
||||
def rename_company_image(instance, filename):
|
||||
""" Function to rename a company image after upload
|
||||
@ -128,6 +131,28 @@ class Company(models.Model):
|
||||
stock = apps.get_model('stock', 'StockItem')
|
||||
return stock.objects.filter(supplier_part__supplier=self.id).count()
|
||||
|
||||
def outstanding_purchase_orders(self):
|
||||
""" Return purchase orders which are 'outstanding' """
|
||||
return self.purchase_orders.filter(status__in=OrderStatus.OPEN)
|
||||
|
||||
def closed_purchase_orders(self):
|
||||
""" Return purchase orders which are not 'outstanding'
|
||||
|
||||
- Complete
|
||||
- Failed / lost
|
||||
- Returned
|
||||
"""
|
||||
|
||||
return self.purchase_orders.exclude(status__in=OrderStatus.OPEN)
|
||||
|
||||
def complete_purchase_orders(self):
|
||||
return self.purchase_orders.filter(status=OrderStatus.COMPLETE)
|
||||
|
||||
def failed_purchase_orders(self):
|
||||
""" Return any purchase orders which were not successful """
|
||||
|
||||
return self.purchase_orders.filter(status__in=OrderStatus.FAILED)
|
||||
|
||||
|
||||
class Contact(models.Model):
|
||||
""" A Contact represents a person who works at a particular company.
|
||||
@ -223,6 +248,9 @@ class SupplierPart(models.Model):
|
||||
|
||||
@property
|
||||
def manufacturer_string(self):
|
||||
""" Format a MPN string for this SupplierPart.
|
||||
Concatenates manufacture name and part number.
|
||||
"""
|
||||
|
||||
items = []
|
||||
|
||||
@ -286,6 +314,37 @@ class SupplierPart(models.Model):
|
||||
else:
|
||||
return None
|
||||
|
||||
def open_orders(self):
|
||||
""" Return a database query for PO line items for this SupplierPart,
|
||||
limited to purchase orders that are open / outstanding.
|
||||
"""
|
||||
|
||||
return self.purchase_order_line_items.prefetch_related('order').filter(order__status__in=OrderStatus.OPEN)
|
||||
|
||||
def on_order(self):
|
||||
""" Return the total quantity of items currently on order.
|
||||
|
||||
Subtract partially received stock as appropriate
|
||||
"""
|
||||
|
||||
totals = self.open_orders().aggregate(Sum('quantity'), Sum('received'))
|
||||
|
||||
# Quantity on order
|
||||
q = totals.get('quantity__sum', 0)
|
||||
|
||||
# Quantity received
|
||||
r = totals.get('received__sum', 0)
|
||||
|
||||
if q is None or r is None:
|
||||
return 0
|
||||
else:
|
||||
return max(q - r, 0)
|
||||
|
||||
def purchase_orders(self):
|
||||
""" Returns a list of purchase orders relating to this supplier part """
|
||||
|
||||
return [line.order for line in self.purchase_order_line_items.all().prefetch_related('order')]
|
||||
|
||||
def __str__(self):
|
||||
s = "{supplier} ({sku})".format(
|
||||
sku=self.SKU,
|
||||
|
@ -6,7 +6,7 @@
|
||||
|
||||
<div class='row'>
|
||||
<div class='col-sm-6'>
|
||||
<h3>Company Details</h3>
|
||||
<h4>Company Details</h4>
|
||||
</div>
|
||||
<div class='col-sm-6'>
|
||||
<h3>
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
{% include 'company/tabs.html' with tab='parts' %}
|
||||
|
||||
<h3>Supplier Parts</h3>
|
||||
<h4>Supplier Parts</h4>
|
||||
|
||||
<div id='button-toolbar'>
|
||||
<button class="btn btn-success" id='part-create'>New Supplier Part</button>
|
||||
|
@ -0,0 +1,37 @@
|
||||
{% extends "company/company_base.html" %}
|
||||
{% load static %}
|
||||
{% block details %}
|
||||
|
||||
{% include 'company/tabs.html' with tab='po' %}
|
||||
|
||||
<h4>Open Purchase Orders</h4>
|
||||
|
||||
<div class='container' style='float: right;'>
|
||||
<div class='btn-group'>
|
||||
<button class='btn btn-primary' type='button' id='po-create' title='Create new purchase order'>New Purchase Order</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% include "order/po_table.html" with orders=company.outstanding_purchase_orders.all %}
|
||||
|
||||
{% if company.closed_purchase_orders.count > 0 %}
|
||||
{% include "order/po_table_collapse.html" with title="Closed Orders" orders=company.closed_purchase_orders.all %}
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block js_ready %}
|
||||
{{ block.super }}
|
||||
|
||||
$("#po-create").click(function() {
|
||||
launchModalForm("{% url 'purchase-order-create' %}",
|
||||
{
|
||||
data: {
|
||||
supplier: {{ company.id }},
|
||||
},
|
||||
follow: true,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
{% endblock %}
|
@ -5,7 +5,7 @@
|
||||
|
||||
{% include "company/tabs.html" with tab='stock' %}
|
||||
|
||||
<h3>Supplier Stock</h3>
|
||||
<h4>Supplier Stock</h4>
|
||||
|
||||
{% include "stock_table.html" %}
|
||||
|
||||
|
@ -1,13 +0,0 @@
|
||||
{% if order.status == order.PENDING %}
|
||||
<span class='label label-info'>
|
||||
{% elif order.status == order.PLACED %}
|
||||
<span class='label label-primary'>
|
||||
{% elif order.status == order.RECEIVED %}
|
||||
<span class='label label-success'>
|
||||
{% elif order.status == order.CANCELLED %}
|
||||
<span class='label label-warning'>
|
||||
{% else %}
|
||||
<span class='label label-danger'>
|
||||
{% endif %}
|
||||
{{ order.get_status_display }}
|
||||
</span>
|
@ -1,31 +0,0 @@
|
||||
{% extends "supplier/supplier_base.html" %}
|
||||
|
||||
{% block details %}
|
||||
|
||||
{% include "supplier/tabs.html" with tab='order' %}
|
||||
|
||||
<h3>Supplier Orders</h3>
|
||||
|
||||
<table class="table table-striped">
|
||||
<tr>
|
||||
<th>Reference</th>
|
||||
<th>Issued</th>
|
||||
<th>Delivery</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
{% for order in supplier.orders.all %}
|
||||
<tr>
|
||||
<td><a href="{% url 'supplier-order-detail' order.id %}">{{ order.internal_ref }}</a></td>
|
||||
<td>{% if order.issued_date %}{{ order.issued_date }}{% endif %}</td>
|
||||
<td>{% if order.delivery_date %}{{ order.delivery_date }}{% endif %}</td>
|
||||
<td>{% include "supplier/order_status.html" with order=order %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
||||
<div class='container-fluid'>
|
||||
<a href="{% url 'supplier-order-create' %}?supplier={{ supplier.id }}">
|
||||
<button class="btn btn-success">New Order</button>
|
||||
</a>
|
||||
|
||||
{% endblock %}
|
@ -101,7 +101,10 @@ InvenTree | {{ company.name }} - Parts
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
<hr>
|
||||
|
||||
<h4>Purchase Orders</h4>
|
||||
{% include "order/po_table.html" with orders=part.purchase_orders %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
|
7
InvenTree/company/templates/company/po_list.html
Normal file
7
InvenTree/company/templates/company/po_list.html
Normal file
@ -0,0 +1,7 @@
|
||||
{% for order in orders %}
|
||||
<tr>
|
||||
<td>{{ order }}</td>
|
||||
<td>{{ order.description }}</td>
|
||||
<td>{% include "order/order_status.html" with order=order %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
@ -9,12 +9,10 @@
|
||||
<li{% if tab == 'stock' %} class='active'{% endif %}>
|
||||
<a href="{% url 'company-detail-stock' company.id %}">Stock <span class='badge'>{{ company.stock_count }}</a>
|
||||
</li>
|
||||
{% if 0 %}
|
||||
<li{% if tab == 'po' %} class='active'{% endif %}>
|
||||
<a href="#">Purchase Orders</a>
|
||||
<a href="{% url 'company-detail-purchase-orders' company.id %}">Purchase Orders <span class='badge'>{{ company.purchase_orders.count }}</span></a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if company.is_customer %}
|
||||
{% if 0 %}
|
||||
<li{% if tab == 'co' %} class='active'{% endif %}>
|
||||
|
@ -17,6 +17,7 @@ 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'thumbnail/?', views.CompanyImage.as_view(), name='company-image'),
|
||||
|
||||
|
@ -11,6 +11,7 @@ from django.views.generic import DetailView, ListView
|
||||
from django.forms import HiddenInput
|
||||
|
||||
from InvenTree.views import AjaxCreateView, AjaxUpdateView, AjaxDeleteView
|
||||
from InvenTree.status_codes import OrderStatus
|
||||
|
||||
from .models import Company
|
||||
from .models import SupplierPart
|
||||
@ -57,6 +58,12 @@ class CompanyDetail(DetailView):
|
||||
queryset = Company.objects.all()
|
||||
model = Company
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['OrderStatus'] = OrderStatus
|
||||
|
||||
return ctx
|
||||
|
||||
|
||||
class CompanyImage(AjaxUpdateView):
|
||||
""" View for uploading an image for the Company """
|
||||
@ -121,6 +128,12 @@ class SupplierPartDetail(DetailView):
|
||||
context_object_name = 'part'
|
||||
queryset = SupplierPart.objects.all()
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['OrderStatus'] = OrderStatus
|
||||
|
||||
return ctx
|
||||
|
||||
|
||||
class SupplierPartEdit(AjaxUpdateView):
|
||||
""" Update view for editing SupplierPart """
|
||||
|
0
InvenTree/order/__init__.py
Normal file
0
InvenTree/order/__init__.py
Normal file
31
InvenTree/order/admin.py
Normal file
31
InvenTree/order/admin.py
Normal file
@ -0,0 +1,31 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.contrib import admin
|
||||
|
||||
from .models import PurchaseOrder, PurchaseOrderLineItem
|
||||
|
||||
|
||||
class PurchaseOrderAdmin(admin.ModelAdmin):
|
||||
|
||||
list_display = (
|
||||
'reference',
|
||||
'supplier',
|
||||
'status',
|
||||
'description',
|
||||
'creation_date'
|
||||
)
|
||||
|
||||
|
||||
class PurchaseOrderLineItemAdmin(admin.ModelAdmin):
|
||||
|
||||
list_display = (
|
||||
'order',
|
||||
'part',
|
||||
'quantity',
|
||||
'reference'
|
||||
)
|
||||
|
||||
|
||||
admin.site.register(PurchaseOrder, PurchaseOrderAdmin)
|
||||
admin.site.register(PurchaseOrderLineItem, PurchaseOrderLineItemAdmin)
|
5
InvenTree/order/apps.py
Normal file
5
InvenTree/order/apps.py
Normal file
@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class OrderConfig(AppConfig):
|
||||
name = 'order'
|
51
InvenTree/order/forms.py
Normal file
51
InvenTree/order/forms.py
Normal file
@ -0,0 +1,51 @@
|
||||
"""
|
||||
Django Forms for interacting with Order objects
|
||||
"""
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django import forms
|
||||
|
||||
from InvenTree.forms import HelperForm
|
||||
|
||||
from .models import PurchaseOrder, PurchaseOrderLineItem
|
||||
|
||||
|
||||
class IssuePurchaseOrderForm(HelperForm):
|
||||
|
||||
confirm = forms.BooleanField(required=False, help_text='Place order')
|
||||
|
||||
class Meta:
|
||||
model = PurchaseOrder
|
||||
fields = [
|
||||
'confirm',
|
||||
]
|
||||
|
||||
|
||||
class EditPurchaseOrderForm(HelperForm):
|
||||
""" Form for editing a PurchaseOrder object """
|
||||
|
||||
class Meta:
|
||||
model = PurchaseOrder
|
||||
fields = [
|
||||
'reference',
|
||||
'supplier',
|
||||
'description',
|
||||
'URL',
|
||||
'notes'
|
||||
]
|
||||
|
||||
|
||||
class EditPurchaseOrderLineItemForm(HelperForm):
|
||||
""" Form for editing a PurchaseOrderLineItem object """
|
||||
|
||||
class Meta:
|
||||
model = PurchaseOrderLineItem
|
||||
fields = [
|
||||
'order',
|
||||
'part',
|
||||
'quantity',
|
||||
'reference',
|
||||
'received'
|
||||
]
|
48
InvenTree/order/migrations/0001_initial.py
Normal file
48
InvenTree/order/migrations/0001_initial.py
Normal file
@ -0,0 +1,48 @@
|
||||
# Generated by Django 2.2 on 2019-06-04 12:17
|
||||
|
||||
from django.conf import settings
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('company', '0005_auto_20190525_2356'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='PurchaseOrder',
|
||||
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)),
|
||||
('creation_date', models.DateField(auto_now=True)),
|
||||
('issue_date', models.DateField(blank=True, null=True)),
|
||||
('notes', models.TextField(blank=True, help_text='Order notes')),
|
||||
('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
|
||||
('supplier', models.ForeignKey(help_text='Company', on_delete=django.db.models.deletion.CASCADE, related_name='Orders', to='company.Company')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PurchaseOrderLineItem',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('quantity', models.PositiveIntegerField(default=1, help_text='Item quantity', validators=[django.core.validators.MinValueValidator(0)])),
|
||||
('reference', models.CharField(blank=True, help_text='Line item reference', max_length=100)),
|
||||
('received', models.PositiveIntegerField(default=0, help_text='Number of items received')),
|
||||
('order', models.ForeignKey(help_text='Purchase Order', on_delete=django.db.models.deletion.CASCADE, related_name='lines', to='order.PurchaseOrder')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
]
|
29
InvenTree/order/migrations/0002_auto_20190604_2224.py
Normal file
29
InvenTree/order/migrations/0002_auto_20190604_2224.py
Normal file
@ -0,0 +1,29 @@
|
||||
# Generated by Django 2.2 on 2019-06-04 12:24
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('order', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='purchaseorder',
|
||||
name='URL',
|
||||
field=models.URLField(blank=True, help_text='Link to external page'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='purchaseorder',
|
||||
name='description',
|
||||
field=models.CharField(blank=True, help_text='Order description', max_length=250),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='purchaseorder',
|
||||
name='supplier',
|
||||
field=models.ForeignKey(help_text='Company', limit_choices_to={'is_supplier': True}, on_delete=django.db.models.deletion.CASCADE, related_name='Orders', to='company.Company'),
|
||||
),
|
||||
]
|
19
InvenTree/order/migrations/0003_auto_20190604_2226.py
Normal file
19
InvenTree/order/migrations/0003_auto_20190604_2226.py
Normal file
@ -0,0 +1,19 @@
|
||||
# Generated by Django 2.2 on 2019-06-04 12:26
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('order', '0002_auto_20190604_2224'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='purchaseorder',
|
||||
name='supplier',
|
||||
field=models.ForeignKey(help_text='Company', limit_choices_to={'is_supplier': True}, on_delete=django.db.models.deletion.CASCADE, related_name='purchase_orders', to='company.Company'),
|
||||
),
|
||||
]
|
18
InvenTree/order/migrations/0004_purchaseorder_status.py
Normal file
18
InvenTree/order/migrations/0004_purchaseorder_status.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 2.2 on 2019-06-04 12:52
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('order', '0003_auto_20190604_2226'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
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='Order status'),
|
||||
),
|
||||
]
|
@ -0,0 +1,20 @@
|
||||
# Generated by Django 2.2 on 2019-06-05 10:36
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('company', '0005_auto_20190525_2356'),
|
||||
('order', '0004_purchaseorder_status'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='purchaseorderlineitem',
|
||||
name='part',
|
||||
field=models.ForeignKey(blank=True, help_text='Supplier part', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='orders', to='company.SupplierPart'),
|
||||
),
|
||||
]
|
18
InvenTree/order/migrations/0006_auto_20190605_2056.py
Normal file
18
InvenTree/order/migrations/0006_auto_20190605_2056.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 2.2 on 2019-06-05 10:56
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('company', '0005_auto_20190525_2356'),
|
||||
('order', '0005_purchaseorderlineitem_part'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterUniqueTogether(
|
||||
name='purchaseorderlineitem',
|
||||
unique_together={('order', 'part')},
|
||||
),
|
||||
]
|
19
InvenTree/order/migrations/0007_auto_20190605_2138.py
Normal file
19
InvenTree/order/migrations/0007_auto_20190605_2138.py
Normal file
@ -0,0 +1,19 @@
|
||||
# Generated by Django 2.2 on 2019-06-05 11:38
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('order', '0006_auto_20190605_2056'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='purchaseorderlineitem',
|
||||
name='part',
|
||||
field=models.ForeignKey(blank=True, help_text='Supplier part', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='order_line_items', to='company.SupplierPart'),
|
||||
),
|
||||
]
|
19
InvenTree/order/migrations/0008_auto_20190605_2140.py
Normal file
19
InvenTree/order/migrations/0008_auto_20190605_2140.py
Normal file
@ -0,0 +1,19 @@
|
||||
# Generated by Django 2.2 on 2019-06-05 11:40
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('order', '0007_auto_20190605_2138'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='purchaseorderlineitem',
|
||||
name='part',
|
||||
field=models.ForeignKey(blank=True, help_text='Supplier part', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='purchase_order_line_items', to='company.SupplierPart'),
|
||||
),
|
||||
]
|
18
InvenTree/order/migrations/0009_auto_20190606_2133.py
Normal file
18
InvenTree/order/migrations/0009_auto_20190606_2133.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 2.2 on 2019-06-06 11:33
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('order', '0008_auto_20190605_2140'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='purchaseorder',
|
||||
name='description',
|
||||
field=models.CharField(help_text='Order description', max_length=250),
|
||||
),
|
||||
]
|
0
InvenTree/order/migrations/__init__.py
Normal file
0
InvenTree/order/migrations/__init__.py
Normal file
150
InvenTree/order/models.py
Normal file
150
InvenTree/order/models.py
Normal file
@ -0,0 +1,150 @@
|
||||
"""
|
||||
Order model definitions
|
||||
"""
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from django.db import models
|
||||
from django.core.validators import MinValueValidator
|
||||
from django.contrib.auth.models import User
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from company.models import Company, SupplierPart
|
||||
|
||||
from InvenTree.status_codes import OrderStatus
|
||||
|
||||
|
||||
class Order(models.Model):
|
||||
""" Abstract model for an order.
|
||||
|
||||
Instances of this class:
|
||||
|
||||
- PuchaseOrder
|
||||
|
||||
Attributes:
|
||||
reference: Unique order number / reference / code
|
||||
description: Long form description (required)
|
||||
notes: Extra note field (optional)
|
||||
creation_date: Automatic date of order creation
|
||||
created_by: User who created this order (automatically captured)
|
||||
issue_date: Date the order was issued
|
||||
|
||||
"""
|
||||
|
||||
ORDER_PREFIX = ""
|
||||
|
||||
def __str__(self):
|
||||
el = []
|
||||
|
||||
if self.ORDER_PREFIX:
|
||||
el.append(self.ORDER_PREFIX)
|
||||
|
||||
el.append(self.reference)
|
||||
|
||||
return " ".join(el)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
reference = models.CharField(unique=True, max_length=64, blank=False, help_text=_('Order reference'))
|
||||
|
||||
description = models.CharField(max_length=250, help_text=_('Order description'))
|
||||
|
||||
URL = models.URLField(blank=True, help_text=_('Link to external page'))
|
||||
|
||||
creation_date = models.DateField(auto_now=True, editable=False)
|
||||
|
||||
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,
|
||||
related_name='+'
|
||||
)
|
||||
|
||||
issue_date = models.DateField(blank=True, null=True)
|
||||
|
||||
notes = models.TextField(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()
|
||||
|
||||
|
||||
class PurchaseOrder(Order):
|
||||
""" A PurchaseOrder represents goods shipped inwards from an external supplier.
|
||||
|
||||
Attributes:
|
||||
supplier: Reference to the company supplying the goods in the order
|
||||
|
||||
"""
|
||||
|
||||
ORDER_PREFIX = "PO"
|
||||
|
||||
supplier = models.ForeignKey(
|
||||
Company, on_delete=models.CASCADE,
|
||||
limit_choices_to={
|
||||
'is_supplier': True,
|
||||
},
|
||||
related_name='purchase_orders',
|
||||
help_text=_('Company')
|
||||
)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('purchase-order-detail', kwargs={'pk': self.id})
|
||||
|
||||
|
||||
class OrderLineItem(models.Model):
|
||||
""" Abstract model for an order line item
|
||||
|
||||
Attributes:
|
||||
quantity: Number of items
|
||||
note: Annotation for the item
|
||||
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
quantity = models.PositiveIntegerField(validators=[MinValueValidator(0)], default=1, help_text=_('Item quantity'))
|
||||
|
||||
reference = models.CharField(max_length=100, blank=True, help_text=_('Line item reference'))
|
||||
|
||||
|
||||
class PurchaseOrderLineItem(OrderLineItem):
|
||||
""" Model for a purchase order line item.
|
||||
|
||||
Attributes:
|
||||
order: Reference to a PurchaseOrder object
|
||||
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
unique_together = (
|
||||
('order', 'part')
|
||||
)
|
||||
|
||||
order = models.ForeignKey(
|
||||
PurchaseOrder, on_delete=models.CASCADE,
|
||||
related_name='lines',
|
||||
help_text=_('Purchase Order')
|
||||
)
|
||||
|
||||
# TODO - Function callback for when the SupplierPart is deleted?
|
||||
|
||||
part = models.ForeignKey(
|
||||
SupplierPart, on_delete=models.SET_NULL,
|
||||
blank=True, null=True,
|
||||
related_name='purchase_order_line_items',
|
||||
help_text=_("Supplier part"),
|
||||
)
|
||||
|
||||
received = models.PositiveIntegerField(default=0, help_text=_('Number of items received'))
|
7
InvenTree/order/templates/order/order_issue.html
Normal file
7
InvenTree/order/templates/order/order_issue.html
Normal file
@ -0,0 +1,7 @@
|
||||
{% extends "modal_form.html" %}
|
||||
|
||||
{% block pre_form_content %}
|
||||
|
||||
After placing this purchase order, line items will no longer be editable.
|
||||
|
||||
{% endblock %}
|
13
InvenTree/order/templates/order/order_status.html
Normal file
13
InvenTree/order/templates/order/order_status.html
Normal file
@ -0,0 +1,13 @@
|
||||
{% if order.status == OrderStatus.PENDING %}
|
||||
<span class='label label-large label-info'>
|
||||
{% elif order.status == OrderStatus.PLACED %}
|
||||
<span class='label label-large label-primary'>
|
||||
{% elif order.status == OrderStatus.COMPLETE %}
|
||||
<span class='label label-large label-success'>
|
||||
{% elif order.status == OrderStatus.CANCELLED or order.status == OrderStatus.RETURNED %}
|
||||
<span class='label label-large label-warning'>
|
||||
{% else %}
|
||||
<span class='label label-large label-danger'>
|
||||
{% endif %}
|
||||
{{ order.get_status_display }}
|
||||
</span>
|
16
InvenTree/order/templates/order/po_table.html
Normal file
16
InvenTree/order/templates/order/po_table.html
Normal file
@ -0,0 +1,16 @@
|
||||
<table class='table table-striped table-condensed' id='po-table'>
|
||||
<tr>
|
||||
<th data-field='company'>Company</th>
|
||||
<th data-field='reference'>Order Reference</th>
|
||||
<th data-field='description'>Description</th>
|
||||
<th data-field='status'>Status</th>
|
||||
</tr>
|
||||
{% for order in orders %}
|
||||
<tr>
|
||||
<td>{% include "hover_image.html" with image=order.supplier.image hover=True %}<a href="{{ order.supplier.get_absolute_url }}purchase-orders/">{{ order.supplier.name }}</a></td>
|
||||
<td><a href="{% url 'purchase-order-detail' order.id %}">{{ order }}</a></td>
|
||||
<td>{{ order.description }}</td>
|
||||
<td>{% include "order/order_status.html" %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
11
InvenTree/order/templates/order/po_table_collapse.html
Normal file
11
InvenTree/order/templates/order/po_table_collapse.html
Normal file
@ -0,0 +1,11 @@
|
||||
{% extends "collapse.html" %}
|
||||
|
||||
{% load static %}
|
||||
|
||||
{% block collapse_title %}
|
||||
<h4>{{ title }}</h4>
|
||||
{% endblock %}
|
||||
|
||||
{% block collapse_content %}
|
||||
{% include "order/po_table.html" %}
|
||||
{% endblock %}
|
154
InvenTree/order/templates/order/purchase_order_detail.html
Normal file
154
InvenTree/order/templates/order/purchase_order_detail.html
Normal file
@ -0,0 +1,154 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% load static %}
|
||||
|
||||
{% block page_title %}
|
||||
InvenTree | {{ order }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class='row'>
|
||||
<div class='col-sm-6'>
|
||||
<div class='media'>
|
||||
<div class='media-left'>
|
||||
<img class='part-thumb'
|
||||
{% if order.supplier.image %}
|
||||
src="{{ order.supplier.image.url }}"
|
||||
{% else %}
|
||||
src="{% static 'img/blank_image.png' %}"
|
||||
{% endif %}
|
||||
/>
|
||||
</div>
|
||||
<div class='media-body'>
|
||||
<h4>{{ order }}</h4>
|
||||
<p>{{ order.description }}</p>
|
||||
{% if order.URL %}
|
||||
<a href="{{ order.URL }}">{{ order.URL }}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class='col-sm-6'>
|
||||
<h4>Purchase Order Details</h4>
|
||||
<table class='table'>
|
||||
<tr>
|
||||
<td>Supplier</td>
|
||||
<td><a href="{% url 'company-detail' order.supplier.id %}">{{ order.supplier }}</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Status</td>
|
||||
<td>{% include "order/order_status.html" %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Created</td>
|
||||
<td>{{ order.creation_date }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Created By</td>
|
||||
<td>{{ order.created_by }}</td>
|
||||
</tr>
|
||||
{% if order.issue_date %}
|
||||
<tr>
|
||||
<td>Issued</td>
|
||||
<td>{{ order.issue_date }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class='btn-group' style='float: right;'>
|
||||
<button type='button' class='btn btn-primary' id='edit-order'>Edit Order</button>
|
||||
{% if order.status == OrderStatus.PENDING and order.lines.count > 0 %}
|
||||
<button type='button' class='btn btn-primary' id='place-order'>Place Order</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<h4>Order Items</h4>
|
||||
|
||||
{% if order.status == OrderStatus.PENDING %}
|
||||
<button type='button' class='btn btn-default' id='new-po-line'>Add Line Item</button>
|
||||
{% endif %}
|
||||
|
||||
<table class='table table-striped table-condensed' id='po-lines-table'>
|
||||
<tr>
|
||||
<th data-field='line'>Line</th>
|
||||
<th data-field='part'>Part</th>
|
||||
<th data-field='sku'>Order Code</th>
|
||||
<th data-field='reference'>Reference</th>
|
||||
<th data-field='quantity'>Quantity</th>
|
||||
<th data-field='received'>Received</th>
|
||||
</tr>
|
||||
{% for line in order.lines.all %}
|
||||
<tr>
|
||||
<td>{{ forloop.counter }}</td>
|
||||
{% if line.part %}
|
||||
<td><a href="{% url 'part-detail' line.part.part.id %}">{{ line.part.part.full_name }}</a></td>
|
||||
<td><a href="{% url 'supplier-part-detail' line.part.id %}">{{ line.part.SKU }}</a></td>
|
||||
{% else %}
|
||||
<td colspan='2'><strong>Warning: Part has been deleted.</strong></td>
|
||||
{% endif %}
|
||||
<td>{{ line.reference }}</td>
|
||||
<td>{{ line.quantity }}</td>
|
||||
<td>{{ line.received }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
||||
{% if order.notes %}
|
||||
<hr>
|
||||
<div class='panel panel-default'>
|
||||
<div class='panel-heading'><b>Notes</b></div>
|
||||
<div class='panel-body'>{{ order.notes }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block js_ready %}
|
||||
|
||||
$("#place-order").click(function() {
|
||||
launchModalForm("{% url 'purchase-order-issue' order.id %}",
|
||||
{
|
||||
reload: true,
|
||||
});
|
||||
});
|
||||
|
||||
$("#edit-order").click(function() {
|
||||
launchModalForm("{% url 'purchase-order-edit' order.id %}",
|
||||
{
|
||||
reload: true,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
{% if order.status == OrderStatus.PENDING %}
|
||||
$('#new-po-line').click(function() {
|
||||
launchModalForm("{% url 'po-line-item-create' %}",
|
||||
{
|
||||
reload: true,
|
||||
data: {
|
||||
order: {{ order.id }},
|
||||
},
|
||||
secondary: [
|
||||
{
|
||||
field: 'part',
|
||||
label: 'New Supplier Part',
|
||||
title: 'Create new supplier part',
|
||||
url: "{% url 'supplier-part-create' %}",
|
||||
data: {
|
||||
supplier: {{ order.supplier.id }},
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
});
|
||||
{% endif %}
|
||||
|
||||
$("#po-lines-table").bootstrapTable({
|
||||
});
|
||||
|
||||
{% endblock %}
|
37
InvenTree/order/templates/order/purchase_orders.html
Normal file
37
InvenTree/order/templates/order/purchase_orders.html
Normal file
@ -0,0 +1,37 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% load static %}
|
||||
|
||||
{% block page_title %}
|
||||
InvenTree | Purchase Orders
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class='row'>
|
||||
<div class='col-sm-6'>
|
||||
<h3>Purchase Orders</h3>
|
||||
</div>
|
||||
<div class='col-sm-6'>
|
||||
<div class='btn-group' style='float: right;'>
|
||||
<button class='btn btn-primary' type='button' id='po-create' title='Create new purchase order'>New Purchase Order</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% include "order/po_table.html" %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block js_ready %}
|
||||
{{ block.super }}
|
||||
|
||||
$("#po-create").click(function() {
|
||||
launchModalForm("{% url 'purchase-order-create' %}",
|
||||
{
|
||||
reload: true,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
{% endblock %}
|
1
InvenTree/order/tests.py
Normal file
1
InvenTree/order/tests.py
Normal file
@ -0,0 +1 @@
|
||||
# TODO - Implement tests for the order app
|
40
InvenTree/order/urls.py
Normal file
40
InvenTree/order/urls.py
Normal file
@ -0,0 +1,40 @@
|
||||
"""
|
||||
URL lookup for the Order app. Provides URL endpoints for:
|
||||
|
||||
- List view of Purchase Orders
|
||||
- Detail view of Purchase Orders
|
||||
"""
|
||||
|
||||
from django.conf.urls import url, include
|
||||
|
||||
from . import views
|
||||
|
||||
purchase_order_detail_urls = [
|
||||
|
||||
url(r'^edit/?', views.PurchaseOrderEdit.as_view(), name='purchase-order-edit'),
|
||||
url(r'^issue/?', views.PurchaseOrderIssue.as_view(), name='purchase-order-issue'),
|
||||
|
||||
url(r'^.*$', views.PurchaseOrderDetail.as_view(), name='purchase-order-detail'),
|
||||
]
|
||||
|
||||
po_line_urls = [
|
||||
|
||||
url(r'^new/', views.POLineItemCreate.as_view(), name='po-line-item-create'),
|
||||
]
|
||||
|
||||
purchase_order_urls = [
|
||||
|
||||
url(r'^new/', views.PurchaseOrderCreate.as_view(), name='purchase-order-create'),
|
||||
|
||||
# Display detail view for a single purchase order
|
||||
url(r'^(?P<pk>\d+)/', include(purchase_order_detail_urls)),
|
||||
|
||||
url(r'^line/', include(po_line_urls)),
|
||||
|
||||
# Display complete list of purchase orders
|
||||
url(r'^.*$', views.PurchaseOrderIndex.as_view(), name='purchase-order-index'),
|
||||
]
|
||||
|
||||
order_urls = [
|
||||
url(r'^purchase-order/', include(purchase_order_urls)),
|
||||
]
|
236
InvenTree/order/views.py
Normal file
236
InvenTree/order/views.py
Normal file
@ -0,0 +1,236 @@
|
||||
"""
|
||||
Django views for interacting with Order app
|
||||
"""
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.views.generic import DetailView, ListView
|
||||
from django.forms import HiddenInput
|
||||
|
||||
from .models import PurchaseOrder, PurchaseOrderLineItem
|
||||
from company.models import Company, SupplierPart
|
||||
|
||||
from . import forms as order_forms
|
||||
|
||||
from InvenTree.views import AjaxCreateView, AjaxUpdateView
|
||||
from InvenTree.helpers import str2bool
|
||||
|
||||
from InvenTree.status_codes import OrderStatus
|
||||
|
||||
|
||||
class PurchaseOrderIndex(ListView):
|
||||
""" List view for all purchase orders """
|
||||
|
||||
model = PurchaseOrder
|
||||
template_name = 'order/purchase_orders.html'
|
||||
context_object_name = 'orders'
|
||||
|
||||
def get_queryset(self):
|
||||
""" Retrieve the list of purchase orders,
|
||||
ensure that the most recent ones are returned first. """
|
||||
|
||||
queryset = PurchaseOrder.objects.all().order_by('-creation_date')
|
||||
|
||||
return queryset
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
|
||||
ctx['OrderStatus'] = OrderStatus
|
||||
|
||||
return ctx
|
||||
|
||||
|
||||
class PurchaseOrderDetail(DetailView):
|
||||
""" Detail view for a PurchaseOrder object """
|
||||
|
||||
context_object_name = 'order'
|
||||
queryset = PurchaseOrder.objects.all().prefetch_related('lines')
|
||||
template_name = 'order/purchase_order_detail.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
|
||||
ctx['OrderStatus'] = OrderStatus
|
||||
|
||||
return ctx
|
||||
|
||||
|
||||
class PurchaseOrderCreate(AjaxCreateView):
|
||||
""" View for creating a new PurchaseOrder object using a modal form """
|
||||
|
||||
model = PurchaseOrder
|
||||
ajax_form_title = "Create Purchase Order"
|
||||
form_class = order_forms.EditPurchaseOrderForm
|
||||
|
||||
def get_initial(self):
|
||||
initials = super().get_initial().copy()
|
||||
|
||||
initials['status'] = OrderStatus.PENDING
|
||||
|
||||
supplier_id = self.request.GET.get('supplier', None)
|
||||
|
||||
if supplier_id:
|
||||
try:
|
||||
supplier = Company.objects.get(id=supplier_id)
|
||||
initials['supplier'] = supplier
|
||||
except Company.DoesNotExist:
|
||||
pass
|
||||
|
||||
return initials
|
||||
|
||||
|
||||
class PurchaseOrderEdit(AjaxUpdateView):
|
||||
""" View for editing a PurchaseOrder using a modal form """
|
||||
|
||||
model = PurchaseOrder
|
||||
ajax_form_title = 'Edit Purchase Order'
|
||||
form_class = order_forms.EditPurchaseOrderForm
|
||||
|
||||
def get_form(self):
|
||||
|
||||
form = super(AjaxUpdateView, self).get_form()
|
||||
|
||||
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:
|
||||
form.fields['supplier'].widget = HiddenInput()
|
||||
|
||||
return form
|
||||
|
||||
|
||||
class PurchaseOrderIssue(AjaxUpdateView):
|
||||
""" View for changing a purchase order from 'PENDING' to 'ISSUED' """
|
||||
|
||||
model = PurchaseOrder
|
||||
ajax_form_title = 'Issue Order'
|
||||
ajax_template_name = "order/order_issue.html"
|
||||
form_class = order_forms.IssuePurchaseOrderForm
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
""" Mark the purchase order as 'PLACED' """
|
||||
|
||||
order = self.get_object()
|
||||
form = self.get_form()
|
||||
|
||||
confirm = str2bool(request.POST.get('confirm', False))
|
||||
|
||||
valid = False
|
||||
|
||||
if not confirm:
|
||||
form.errors['confirm'] = [_('Confirm order placement')]
|
||||
else:
|
||||
valid = True
|
||||
|
||||
data = {
|
||||
'form_valid': valid,
|
||||
}
|
||||
|
||||
if valid:
|
||||
order.place_order()
|
||||
|
||||
return self.renderJsonResponse(request, form, data)
|
||||
|
||||
|
||||
class POLineItemCreate(AjaxCreateView):
|
||||
""" AJAX view for creating a new PurchaseOrderLineItem object
|
||||
"""
|
||||
|
||||
model = PurchaseOrderLineItem
|
||||
context_object_name = 'line'
|
||||
form_class = order_forms.EditPurchaseOrderLineItemForm
|
||||
ajax_form_title = 'Add Line Item'
|
||||
|
||||
def post(self, request, *arg, **kwargs):
|
||||
|
||||
self.request = request
|
||||
|
||||
form = self.get_form()
|
||||
|
||||
valid = form.is_valid()
|
||||
|
||||
part_id = form['part'].value()
|
||||
|
||||
try:
|
||||
SupplierPart.objects.get(id=part_id)
|
||||
except (SupplierPart.DoesNotExist, ValueError):
|
||||
valid = False
|
||||
form.errors['part'] = [_('This field is required')]
|
||||
|
||||
data = {
|
||||
'form_valid': valid,
|
||||
}
|
||||
|
||||
if valid:
|
||||
self.object = form.save()
|
||||
|
||||
data['pk'] = self.object.pk
|
||||
data['text'] = str(self.object)
|
||||
else:
|
||||
self.object = None
|
||||
|
||||
return self.renderJsonResponse(request, form, data,)
|
||||
|
||||
def get_form(self):
|
||||
""" Limit choice options based on the selected order, etc
|
||||
"""
|
||||
|
||||
form = super().get_form()
|
||||
|
||||
order_id = form['order'].value()
|
||||
|
||||
try:
|
||||
order = PurchaseOrder.objects.get(id=order_id)
|
||||
|
||||
query = form.fields['part'].queryset
|
||||
|
||||
# Only allow parts from the selected supplier
|
||||
query = query.filter(supplier=order.supplier.id)
|
||||
|
||||
exclude = []
|
||||
|
||||
for line in order.lines.all():
|
||||
if line.part and line.part.id not in exclude:
|
||||
exclude.append(line.part.id)
|
||||
|
||||
# Remove parts that are already in the order
|
||||
query = query.exclude(id__in=exclude)
|
||||
|
||||
form.fields['part'].queryset = query
|
||||
form.fields['order'].widget = HiddenInput()
|
||||
except PurchaseOrder.DoesNotExist:
|
||||
pass
|
||||
|
||||
return form
|
||||
|
||||
def get_initial(self):
|
||||
""" Extract initial data for the line item.
|
||||
|
||||
- The 'order' will be passed as a query parameter
|
||||
- Use this to set the 'order' field and limit the options for 'part'
|
||||
"""
|
||||
|
||||
initials = super().get_initial().copy()
|
||||
|
||||
order_id = self.request.GET.get('order', None)
|
||||
|
||||
if order_id:
|
||||
try:
|
||||
order = PurchaseOrder.objects.get(id=order_id)
|
||||
initials['order'] = order
|
||||
|
||||
except PurchaseOrder.DoesNotExist:
|
||||
pass
|
||||
|
||||
return initials
|
||||
|
||||
|
||||
class POLineItemEdit(AjaxUpdateView):
|
||||
|
||||
model = PurchaseOrderLineItem
|
||||
form_class = order_forms.EditPurchaseOrderLineItemForm
|
||||
ajax_template_name = 'modal_form.html'
|
||||
ajax_form_action = 'Edit Line Item'
|
@ -33,6 +33,8 @@ from InvenTree import helpers
|
||||
from InvenTree import validators
|
||||
from InvenTree.models import InvenTreeTree
|
||||
|
||||
from InvenTree.status_codes import BuildStatus, StockStatus, OrderStatus
|
||||
|
||||
from company.models import SupplierPart
|
||||
|
||||
|
||||
@ -425,7 +427,7 @@ class Part(models.Model):
|
||||
then we need to restock.
|
||||
"""
|
||||
|
||||
return (self.total_stock - self.allocation_count) < self.minimum_stock
|
||||
return (self.total_stock + self.on_order - self.allocation_count) < self.minimum_stock
|
||||
|
||||
@property
|
||||
def can_build(self):
|
||||
@ -454,14 +456,14 @@ class Part(models.Model):
|
||||
Builds marked as 'complete' or 'cancelled' are ignored
|
||||
"""
|
||||
|
||||
return [b for b in self.builds.all() if b.is_active]
|
||||
return self.builds.filter(status__in=BuildStatus.ACTIVE_CODES)
|
||||
|
||||
@property
|
||||
def inactive_builds(self):
|
||||
""" Return a list of inactive builds
|
||||
"""
|
||||
|
||||
return [b for b in self.builds.all() if not b.is_active]
|
||||
return self.builds.exclude(status__in=BuildStatus.ACTIVE_CODES)
|
||||
|
||||
@property
|
||||
def quantity_being_built(self):
|
||||
@ -531,7 +533,7 @@ class Part(models.Model):
|
||||
if self.is_template:
|
||||
total = sum([variant.total_stock for variant in self.variants.all()])
|
||||
else:
|
||||
total = self.stock_entries.aggregate(total=Sum('quantity'))['total']
|
||||
total = self.stock_entries.filter(status__in=StockStatus.AVAILABLE_CODES).aggregate(total=Sum('quantity'))['total']
|
||||
|
||||
if total:
|
||||
return total
|
||||
@ -792,6 +794,34 @@ class Part(models.Model):
|
||||
|
||||
return n
|
||||
|
||||
def purchase_orders(self):
|
||||
""" Return a list of purchase orders which reference this part """
|
||||
|
||||
orders = []
|
||||
|
||||
for part in self.supplier_parts.all().prefetch_related('purchase_order_line_items'):
|
||||
for order in part.purchase_orders():
|
||||
if order not in orders:
|
||||
orders.append(order)
|
||||
|
||||
return orders
|
||||
|
||||
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]
|
||||
|
||||
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]
|
||||
|
||||
@property
|
||||
def on_order(self):
|
||||
""" Return the total number of items on order for this part. """
|
||||
|
||||
return sum([part.on_order() for part in self.supplier_parts.all().prefetch_related('purchase_order_line_items')])
|
||||
|
||||
|
||||
def attach_file(instance, filename):
|
||||
""" Function for storing a file for a PartAttachment
|
||||
|
23
InvenTree/part/templates/part/orders.html
Normal file
23
InvenTree/part/templates/part/orders.html
Normal file
@ -0,0 +1,23 @@
|
||||
{% extends "part/part_base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block details %}
|
||||
|
||||
{% include 'part/tabs.html' with tab='orders' %}
|
||||
|
||||
<div class='row'>
|
||||
<div class='col-sm-6'>
|
||||
<h4>Open Part Orders</h4>
|
||||
</div>
|
||||
<div class='col-sm-6'>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% include "order/po_table.html" with orders=part.open_purchase_orders %}
|
||||
|
||||
{% if part.closed_purchase_orders|length > 0 %}
|
||||
<h4>Closed Orders</h4>
|
||||
{% include "order/po_table.html" with orders=part.closed_purchase_orders %}
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
@ -96,6 +96,12 @@
|
||||
<td>{{ part.allocation_count }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if part.on_order > 0 %}
|
||||
<tr>
|
||||
<td>On Order</td>
|
||||
<td>{{ part.on_order }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -25,11 +25,17 @@
|
||||
<li{% ifequal tab 'used' %} class="active"{% endifequal %}>
|
||||
<a href="{% url 'part-used-in' part.id %}">Used In{% if part.used_in_count > 0 %}<span class="badge">{{ part.used_in_count }}</span>{% endif %}</a></li>
|
||||
{% endif %}
|
||||
{% if part.purchaseable and part.is_template == False %}
|
||||
{% if part.purchaseable %}
|
||||
{% if part.is_template == False %}
|
||||
<li{% ifequal tab 'suppliers' %} class="active"{% endifequal %}>
|
||||
<a href="{% url 'part-suppliers' part.id %}">Suppliers
|
||||
<span class="badge">{{ part.supplier_count }}</span>
|
||||
</a></li>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li{% ifequal tab 'orders' %} class='active'{% endifequal %}>
|
||||
<a href="{% url 'part-orders' part.id %}">Purchase Orders <span class='badge'>{{ part.purchase_orders|length }}</span></a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if part.trackable and 0 %}
|
||||
<li{% ifequal tab 'track' %} class="active"{% endifequal %}>
|
||||
|
@ -34,6 +34,7 @@ part_detail_urls = [
|
||||
url(r'^build/?', views.PartDetail.as_view(template_name='part/build.html'), name='part-build'),
|
||||
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'^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'),
|
||||
|
||||
|
@ -24,6 +24,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
|
||||
|
||||
|
||||
class PartIndex(ListView):
|
||||
@ -446,6 +447,8 @@ class PartDetail(DetailView):
|
||||
|
||||
context['starred'] = part.isStarredBy(self.request.user)
|
||||
|
||||
context['OrderStatus'] = OrderStatus
|
||||
|
||||
return context
|
||||
|
||||
|
||||
|
@ -38,7 +38,6 @@
|
||||
/* Extra label styles */
|
||||
|
||||
.label-large {
|
||||
padding: 5px;
|
||||
margin: 3px;
|
||||
font-size: 100%;
|
||||
}
|
||||
@ -297,6 +296,10 @@
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.panel-group .panel {
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
@ -195,6 +195,18 @@ function loadStockTable(table, options) {
|
||||
stock.push(item.pk);
|
||||
});
|
||||
|
||||
// Buttons for launching secondary modals
|
||||
var secondary = [];
|
||||
|
||||
if (action == 'move') {
|
||||
secondary.push({
|
||||
field: 'destination',
|
||||
label: 'New Location',
|
||||
title: 'Create new location',
|
||||
url: "/stock/location/new/",
|
||||
});
|
||||
}
|
||||
|
||||
launchModalForm("/stock/adjust/",
|
||||
{
|
||||
data: {
|
||||
@ -204,6 +216,7 @@ function loadStockTable(table, options) {
|
||||
success: function() {
|
||||
$("#stock-table").bootstrapTable('refresh');
|
||||
},
|
||||
secondary: secondary,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ from .serializers import StockTrackingSerializer
|
||||
|
||||
from InvenTree.views import TreeSerializer
|
||||
from InvenTree.helpers import str2bool
|
||||
from InvenTree.status_codes import StockStatus
|
||||
|
||||
import os
|
||||
|
||||
@ -311,7 +312,7 @@ class StockList(generics.ListCreateAPIView):
|
||||
else:
|
||||
item['location__path'] = None
|
||||
|
||||
item['status_text'] = StockItem.ITEM_STATUS_CODES[item['status']]
|
||||
item['status_text'] = StockStatus.label(item['status'])
|
||||
|
||||
return Response(data)
|
||||
|
||||
|
@ -19,6 +19,7 @@ from django.dispatch import receiver
|
||||
from datetime import datetime
|
||||
from InvenTree import helpers
|
||||
|
||||
from InvenTree.status_codes import StockStatus
|
||||
from InvenTree.models import InvenTreeTree
|
||||
|
||||
from part.models import Part
|
||||
@ -93,7 +94,7 @@ class StockItem(models.Model):
|
||||
stocktake_user: User that performed the most recent stocktake
|
||||
review_needed: Flag if StockItem needs review
|
||||
delete_on_deplete: If True, StockItem will be deleted when the stock level gets to zero
|
||||
status: Status of this StockItem (ref: ITEM_STATUS_CODES)
|
||||
status: Status of this StockItem (ref: InvenTree.status_codes.StockStatus)
|
||||
notes: Extra notes field
|
||||
infinite: If True this StockItem can never be exhausted
|
||||
"""
|
||||
@ -256,23 +257,9 @@ class StockItem(models.Model):
|
||||
|
||||
delete_on_deplete = models.BooleanField(default=True, help_text='Delete this Stock Item when stock is depleted')
|
||||
|
||||
ITEM_OK = 10
|
||||
ITEM_ATTENTION = 50
|
||||
ITEM_DAMAGED = 55
|
||||
ITEM_DESTROYED = 60
|
||||
ITEM_LOST = 70
|
||||
|
||||
ITEM_STATUS_CODES = {
|
||||
ITEM_OK: _("OK"),
|
||||
ITEM_ATTENTION: _("Attention needed"),
|
||||
ITEM_DAMAGED: _("Damaged"),
|
||||
ITEM_DESTROYED: _("Destroyed"),
|
||||
ITEM_LOST: _("Lost")
|
||||
}
|
||||
|
||||
status = models.PositiveIntegerField(
|
||||
default=ITEM_OK,
|
||||
choices=ITEM_STATUS_CODES.items(),
|
||||
default=StockStatus.OK,
|
||||
choices=StockStatus.items(),
|
||||
validators=[MinValueValidator(0)])
|
||||
|
||||
notes = models.CharField(max_length=250, blank=True, help_text='Stock Item Notes')
|
||||
|
@ -25,6 +25,8 @@ InvenTree | Search Results
|
||||
|
||||
{% include "InvenTree/search_parts.html" with collapse_id='parts' %}
|
||||
|
||||
{% include "InvenTree/search_company.html" with collapse_id='companies' %}
|
||||
|
||||
{% include "InvenTree/search_supplier_parts.html" with collapse_id='supplier_parts' %}
|
||||
|
||||
{% include "InvenTree/search_stock_location.html" with collapse_id="locations" %}
|
||||
@ -77,6 +79,8 @@ InvenTree | Search Results
|
||||
|
||||
onSearchResults('#part-results-table', '#part-result-count');
|
||||
|
||||
onSearchResults('#company-results-table', '#company-result-count');
|
||||
|
||||
onSearchResults('#supplier-part-results-table', '#supplier-part-result-count');
|
||||
|
||||
$("#category-results-table").bootstrapTable({
|
||||
@ -130,6 +134,29 @@ InvenTree | Search Results
|
||||
}
|
||||
);
|
||||
|
||||
$("#company-results-table").bootstrapTable({
|
||||
url: "{% url 'api-company-list' %}",
|
||||
queryParams: {
|
||||
search: "{{ query }}",
|
||||
},
|
||||
pagination: true,
|
||||
pageSize: 25,
|
||||
search: true,
|
||||
columns: [
|
||||
{
|
||||
field: 'name',
|
||||
title: 'Name',
|
||||
formatter: function(value, row, index, field) {
|
||||
return imageHoverIcon(row.image) + renderLink(value, row.url);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'description',
|
||||
title: 'Description',
|
||||
},
|
||||
]
|
||||
});
|
||||
|
||||
$("#supplier-part-results-table").bootstrapTable({
|
||||
url: "{% url 'api-part-supplier-list' %}",
|
||||
queryParams: {
|
||||
|
14
InvenTree/templates/InvenTree/search_company.html
Normal file
14
InvenTree/templates/InvenTree/search_company.html
Normal file
@ -0,0 +1,14 @@
|
||||
{% extends "collapse.html" %}
|
||||
|
||||
{% block collapse_title %}
|
||||
<h4>Companies</h4>
|
||||
{% endblock %}
|
||||
|
||||
{% block collapse_heading %}
|
||||
<h4><span id='company-result-count'>{% include "InvenTree/searching.html" %}</span></h4>
|
||||
{% endblock %}
|
||||
|
||||
{% block collapse_content %}
|
||||
<table class='table table-striped table-condensed' data-toolbar="#button-toolbar" id='company-results-table'>
|
||||
</table>
|
||||
{% endblock %}
|
@ -1,10 +1,10 @@
|
||||
{% if build.status == build.PENDING %}
|
||||
{% if build.status == BuildStatus.PENDING %}
|
||||
<span class='label label-large label-info'>
|
||||
{% elif build.status == build.ALLOCATED %}
|
||||
{% elif build.status == BuildStatus.ALLOCATED %}
|
||||
<span class='label label-large label-primary'>
|
||||
{% elif build.status == build.CANCELLED %}
|
||||
{% elif build.status == BuildStatus.CANCELLED %}
|
||||
<span class='label label-large label-danger'>
|
||||
{% elif build.status == build.COMPLETE %}
|
||||
{% elif build.status == BuildStatus.COMPLETE %}
|
||||
<span class='label label-large label-success'>
|
||||
{% endif %}
|
||||
{{ build.get_status_display }}
|
||||
|
@ -10,6 +10,7 @@
|
||||
<li><a href="{% url 'stock-index' %}">Stock</a></li>
|
||||
<li><a href="{% url 'build-index' %}">Build</a></li>
|
||||
<li><a href="{% url 'company-index' %}">Suppliers</a></li>
|
||||
<li><a href="{% url 'purchase-order-index' %}">Orders</a></li>
|
||||
</ul>
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
{% include "search_form.html" %}
|
||||
|
@ -2,16 +2,18 @@
|
||||
<tr>
|
||||
<th>Part</th>
|
||||
<th>Description</th>
|
||||
<th>Required</th>
|
||||
<th>In Stock</th>
|
||||
<th>Allocated</th>
|
||||
<th>On Order</th>
|
||||
<th>Net Stock</th>
|
||||
</tr>
|
||||
{% for part in parts %}
|
||||
<tr>
|
||||
<td><a href="{% url 'part-detail' part.id %}">{{ part.full_name }}</a></td>
|
||||
<td>{{ part.description }}</td>
|
||||
<td>{{ part.total_stock }}</td>
|
||||
<td>{{ part.allocation_count }}</td>
|
||||
<td>{{ part.total_stock }}</td>
|
||||
<td>{{ part.on_order }}</td>
|
||||
<td>{{ part.available_stock }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
5
Makefile
5
Makefile
@ -12,6 +12,7 @@ migrate:
|
||||
python InvenTree/manage.py makemigrations part
|
||||
python InvenTree/manage.py makemigrations stock
|
||||
python InvenTree/manage.py makemigrations build
|
||||
python InvenTree/manage.py makemigrations order
|
||||
python InvenTree/manage.py migrate --run-syncdb
|
||||
python InvenTree/manage.py check
|
||||
|
||||
@ -27,11 +28,11 @@ style:
|
||||
|
||||
test:
|
||||
python InvenTree/manage.py check
|
||||
python InvenTree/manage.py test build company part stock
|
||||
python InvenTree/manage.py test build company part stock order
|
||||
|
||||
coverage:
|
||||
python InvenTree/manage.py check
|
||||
coverage run InvenTree/manage.py test build company part stock
|
||||
coverage run InvenTree/manage.py test build company part stock order
|
||||
coverage html
|
||||
|
||||
documentation:
|
||||
|
Loading…
Reference in New Issue
Block a user