mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge remote-tracking branch 'inventree/master'
This commit is contained in:
commit
4b33b15dd2
@ -227,6 +227,10 @@ USE_L10N = True
|
|||||||
|
|
||||||
USE_TZ = True
|
USE_TZ = True
|
||||||
|
|
||||||
|
DATE_INPUT_FORMATS = [
|
||||||
|
"%Y-%m-%d",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
# Static files (CSS, JavaScript, Images)
|
# Static files (CSS, JavaScript, Images)
|
||||||
# https://docs.djangoproject.com/en/1.10/howto/static-files/
|
# https://docs.djangoproject.com/en/1.10/howto/static-files/
|
||||||
|
@ -137,6 +137,10 @@ function loadPartTable(table, url, options={}) {
|
|||||||
|
|
||||||
var display = imageHoverIcon(row.image) + renderLink(name, '/part/' + row.pk + '/');
|
var display = imageHoverIcon(row.image) + renderLink(name, '/part/' + row.pk + '/');
|
||||||
|
|
||||||
|
if (row.is_template) {
|
||||||
|
display = display + "<span class='label label-info' style='float: right;'>TEMPLATE</span>";
|
||||||
|
}
|
||||||
|
|
||||||
if (!row.active) {
|
if (!row.active) {
|
||||||
display = display + "<span class='label label-warning' style='float: right;'>INACTIVE</span>";
|
display = display + "<span class='label label-warning' style='float: right;'>INACTIVE</span>";
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,14 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
|
from import_export.admin import ImportExportModelAdmin
|
||||||
|
|
||||||
from .models import Currency
|
from .models import Currency
|
||||||
|
|
||||||
|
|
||||||
class CurrencyAdmin(admin.ModelAdmin):
|
class CurrencyAdmin(ImportExportModelAdmin):
|
||||||
list_display = ('symbol', 'suffix', 'description', 'value', 'base')
|
list_display = ('symbol', 'suffix', 'description', 'value', 'base')
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,20 +1,74 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from import_export.admin import ImportExportModelAdmin
|
from import_export.admin import ImportExportModelAdmin
|
||||||
|
from import_export.resources import ModelResource
|
||||||
|
from import_export.fields import Field
|
||||||
|
import import_export.widgets as widgets
|
||||||
|
|
||||||
from .models import Company
|
from .models import Company
|
||||||
from .models import SupplierPart
|
from .models import SupplierPart
|
||||||
from .models import SupplierPriceBreak
|
from .models import SupplierPriceBreak
|
||||||
|
|
||||||
|
from part.models import Part
|
||||||
|
from common.models import Currency
|
||||||
|
|
||||||
|
|
||||||
|
class CompanyResource(ModelResource):
|
||||||
|
""" Class for managing Company data import/export """
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Company
|
||||||
|
skip_unchanged = True
|
||||||
|
report_skipped = False
|
||||||
|
|
||||||
|
|
||||||
class CompanyAdmin(ImportExportModelAdmin):
|
class CompanyAdmin(ImportExportModelAdmin):
|
||||||
|
|
||||||
|
resource_class = CompanyResource
|
||||||
|
|
||||||
list_display = ('name', 'website', 'contact')
|
list_display = ('name', 'website', 'contact')
|
||||||
|
|
||||||
|
|
||||||
|
class SupplierPartResource(ModelResource):
|
||||||
|
""" Class for managing SupplierPart data import/export """
|
||||||
|
|
||||||
|
part = Field(attribute='part', widget=widgets.ForeignKeyWidget(Part))
|
||||||
|
|
||||||
|
supplier = Field(attribute='supplier', widget=widgets.ForeignKeyWidget(Company))
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = SupplierPart
|
||||||
|
skip_unchanged = True
|
||||||
|
report_skipped = False
|
||||||
|
|
||||||
|
|
||||||
class SupplierPartAdmin(ImportExportModelAdmin):
|
class SupplierPartAdmin(ImportExportModelAdmin):
|
||||||
|
|
||||||
|
resource_class = SupplierPartResource
|
||||||
|
|
||||||
list_display = ('part', 'supplier', 'SKU')
|
list_display = ('part', 'supplier', 'SKU')
|
||||||
|
|
||||||
|
|
||||||
|
class SupplierPriceBreakResource(ModelResource):
|
||||||
|
""" Class for managing SupplierPriceBreak data import/export """
|
||||||
|
|
||||||
|
part = Field(attribute='part', widget=widgets.ForeignKeyWidget(SupplierPart))
|
||||||
|
|
||||||
|
currency = Field(attribute='currency', widget=widgets.ForeignKeyWidget(Currency))
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = SupplierPriceBreak
|
||||||
|
skip_unchanged = True
|
||||||
|
report_skipped = False
|
||||||
|
|
||||||
|
|
||||||
class SupplierPriceBreakAdmin(ImportExportModelAdmin):
|
class SupplierPriceBreakAdmin(ImportExportModelAdmin):
|
||||||
|
|
||||||
|
resource_class = SupplierPriceBreakResource
|
||||||
|
|
||||||
list_display = ('part', 'quantity', 'cost')
|
list_display = ('part', 'quantity', 'cost')
|
||||||
|
|
||||||
|
|
||||||
|
@ -15,6 +15,13 @@
|
|||||||
supplier: 1
|
supplier: 1
|
||||||
SKU: 'ACME0002'
|
SKU: 'ACME0002'
|
||||||
|
|
||||||
|
- model: company.supplierpart
|
||||||
|
pk: 3
|
||||||
|
fields:
|
||||||
|
part: 1
|
||||||
|
supplier: 1
|
||||||
|
SKU: 'ACME0003'
|
||||||
|
|
||||||
# Widget purchaseable from ACME
|
# Widget purchaseable from ACME
|
||||||
- model: company.supplierpart
|
- model: company.supplierpart
|
||||||
pk: 100
|
pk: 100
|
||||||
@ -33,7 +40,7 @@
|
|||||||
|
|
||||||
# M2x4 LPHS from Zerg Corp
|
# M2x4 LPHS from Zerg Corp
|
||||||
- model: company.supplierpart
|
- model: company.supplierpart
|
||||||
pk: 3
|
pk: 7
|
||||||
fields:
|
fields:
|
||||||
part: 1
|
part: 1
|
||||||
supplier: 3
|
supplier: 3
|
||||||
|
@ -56,7 +56,7 @@ class CompanySimpleTest(TestCase):
|
|||||||
zerg = Company.objects.get(pk=3)
|
zerg = Company.objects.get(pk=3)
|
||||||
|
|
||||||
self.assertTrue(acme.has_parts)
|
self.assertTrue(acme.has_parts)
|
||||||
self.assertEqual(acme.part_count, 3)
|
self.assertEqual(acme.part_count, 4)
|
||||||
|
|
||||||
self.assertTrue(appel.has_parts)
|
self.assertTrue(appel.has_parts)
|
||||||
self.assertEqual(appel.part_count, 2)
|
self.assertEqual(appel.part_count, 2)
|
||||||
|
@ -2,11 +2,12 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
from import_export.admin import ImportExportModelAdmin
|
||||||
|
|
||||||
from .models import PurchaseOrder, PurchaseOrderLineItem
|
from .models import PurchaseOrder, PurchaseOrderLineItem
|
||||||
|
|
||||||
|
|
||||||
class PurchaseOrderAdmin(admin.ModelAdmin):
|
class PurchaseOrderAdmin(ImportExportModelAdmin):
|
||||||
|
|
||||||
list_display = (
|
list_display = (
|
||||||
'reference',
|
'reference',
|
||||||
@ -17,7 +18,7 @@ class PurchaseOrderAdmin(admin.ModelAdmin):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class PurchaseOrderLineItemAdmin(admin.ModelAdmin):
|
class PurchaseOrderLineItemAdmin(ImportExportModelAdmin):
|
||||||
|
|
||||||
list_display = (
|
list_display = (
|
||||||
'order',
|
'order',
|
||||||
|
@ -35,8 +35,16 @@
|
|||||||
quantity: 250
|
quantity: 250
|
||||||
received: 50
|
received: 50
|
||||||
|
|
||||||
|
# 1000 x ACME0003
|
||||||
|
- model: order.purchaseorderlineitem
|
||||||
|
fields:
|
||||||
|
order: 1
|
||||||
|
part: 3
|
||||||
|
quantity: 1000
|
||||||
|
|
||||||
# 100 x ZERGLPHS (M2x4 LPHS)
|
# 100 x ZERGLPHS (M2x4 LPHS)
|
||||||
- model: order.purchaseorderlineitem
|
- model: order.purchaseorderlineitem
|
||||||
|
pk: 22
|
||||||
fields:
|
fields:
|
||||||
order: 2
|
order: 2
|
||||||
part: 3
|
part: 3
|
||||||
|
@ -5,6 +5,7 @@ Order model definitions
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
from django.db import models, transaction
|
from django.db import models, transaction
|
||||||
|
from django.db.models import F
|
||||||
from django.core.validators import MinValueValidator
|
from django.core.validators import MinValueValidator
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
@ -226,7 +227,7 @@ class PurchaseOrder(Order):
|
|||||||
Any line item where 'received' < 'quantity' will be returned.
|
Any line item where 'received' < 'quantity' will be returned.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return [line for line in self.lines.all() if line.quantity > line.received]
|
return self.lines.filter(quantity__gt=F('received'))
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def receive_line_item(self, line, location, quantity, user):
|
def receive_line_item(self, line, location, quantity, user):
|
||||||
|
@ -4,6 +4,12 @@ from django.test import TestCase
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
|
|
||||||
|
from InvenTree.status_codes import OrderStatus
|
||||||
|
|
||||||
|
from .models import PurchaseOrder, PurchaseOrderLineItem
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
class OrderViewTestCase(TestCase):
|
class OrderViewTestCase(TestCase):
|
||||||
|
|
||||||
@ -75,3 +81,106 @@ class POTests(OrderViewTestCase):
|
|||||||
|
|
||||||
# Response should be streaming-content (file download)
|
# Response should be streaming-content (file download)
|
||||||
self.assertIn('streaming_content', dir(response))
|
self.assertIn('streaming_content', dir(response))
|
||||||
|
|
||||||
|
def test_po_issue(self):
|
||||||
|
""" Test PurchaseOrderIssue view """
|
||||||
|
|
||||||
|
url = reverse('purchase-order-issue', args=(1,))
|
||||||
|
|
||||||
|
order = PurchaseOrder.objects.get(pk=1)
|
||||||
|
self.assertEqual(order.status, OrderStatus.PENDING)
|
||||||
|
|
||||||
|
# Test without confirmation
|
||||||
|
response = self.client.post(url, {'confirm': 0}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
data = json.loads(response.content)
|
||||||
|
self.assertFalse(data['form_valid'])
|
||||||
|
|
||||||
|
# Test WITH confirmation
|
||||||
|
response = self.client.post(url, {'confirm': 1}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
data = json.loads(response.content)
|
||||||
|
self.assertTrue(data['form_valid'])
|
||||||
|
|
||||||
|
# Test that the order was actually placed
|
||||||
|
order = PurchaseOrder.objects.get(pk=1)
|
||||||
|
self.assertEqual(order.status, OrderStatus.PLACED)
|
||||||
|
|
||||||
|
def test_line_item_create(self):
|
||||||
|
""" Test the form for adding a new LineItem to a PurchaseOrder """
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
url = reverse('po-line-item-create')
|
||||||
|
|
||||||
|
# GET the form (pass the correct info)
|
||||||
|
response = self.client.get(url, {'order': 1}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
||||||
|
|
||||||
|
post_data = {
|
||||||
|
'part': 100,
|
||||||
|
'quantity': 45,
|
||||||
|
'reference': 'Test reference field',
|
||||||
|
'notes': 'Test notes field'
|
||||||
|
}
|
||||||
|
|
||||||
|
# POST with an invalid purchase order
|
||||||
|
post_data['order'] = 99
|
||||||
|
response = self.client.post(url, post_data, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
||||||
|
data = json.loads(response.content)
|
||||||
|
self.assertFalse(data['form_valid'])
|
||||||
|
self.assertIn('Invalid Purchase Order', str(data['html_form']))
|
||||||
|
|
||||||
|
# POST with a part that does not match the purchase order
|
||||||
|
post_data['order'] = 1
|
||||||
|
post_data['part'] = 7
|
||||||
|
response = self.client.post(url, post_data, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
||||||
|
data = json.loads(response.content)
|
||||||
|
self.assertFalse(data['form_valid'])
|
||||||
|
self.assertIn('must match for Part and Order', str(data['html_form']))
|
||||||
|
|
||||||
|
# POST with an invalid part
|
||||||
|
post_data['part'] = 12345
|
||||||
|
response = self.client.post(url, post_data, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
||||||
|
data = json.loads(response.content)
|
||||||
|
self.assertFalse(data['form_valid'])
|
||||||
|
self.assertIn('Invalid SupplierPart selection', str(data['html_form']))
|
||||||
|
|
||||||
|
# POST the form with valid data
|
||||||
|
post_data['part'] = 100
|
||||||
|
response = self.client.post(url, post_data, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
data = json.loads(response.content)
|
||||||
|
self.assertTrue(data['form_valid'])
|
||||||
|
|
||||||
|
self.assertEqual(n + 1, PurchaseOrder.objects.get(pk=1).lines.count())
|
||||||
|
|
||||||
|
line = PurchaseOrderLineItem.objects.get(order=1, part=100)
|
||||||
|
self.assertEqual(line.quantity, 45)
|
||||||
|
|
||||||
|
def test_line_item_edit(self):
|
||||||
|
""" Test editing form for PO line item """
|
||||||
|
|
||||||
|
url = reverse('po-line-item-edit', args=(22,))
|
||||||
|
|
||||||
|
response = self.client.get(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
|
||||||
|
class TestPOReceive(OrderViewTestCase):
|
||||||
|
""" Tests for receiving a purchase order """
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
|
||||||
|
self.po = PurchaseOrder.objects.get(pk=1)
|
||||||
|
self.url = reverse('purchase-order-receive', args=(1,))
|
||||||
|
|
||||||
|
def test_receive_lines(self):
|
||||||
|
|
||||||
|
# TODO
|
||||||
|
pass
|
||||||
|
@ -47,10 +47,10 @@ class OrderTest(TestCase):
|
|||||||
for supplier in part.supplier_parts.all():
|
for supplier in part.supplier_parts.all():
|
||||||
open_orders += supplier.open_orders()
|
open_orders += supplier.open_orders()
|
||||||
|
|
||||||
self.assertEqual(len(open_orders), 3)
|
self.assertEqual(len(open_orders), 4)
|
||||||
|
|
||||||
# Test the total on-order quantity
|
# Test the total on-order quantity
|
||||||
self.assertEqual(part.on_order, 400)
|
self.assertEqual(part.on_order, 1400)
|
||||||
|
|
||||||
def test_add_items(self):
|
def test_add_items(self):
|
||||||
""" Test functions for adding line items to an order """
|
""" Test functions for adding line items to an order """
|
||||||
@ -58,7 +58,7 @@ class OrderTest(TestCase):
|
|||||||
order = PurchaseOrder.objects.get(pk=1)
|
order = PurchaseOrder.objects.get(pk=1)
|
||||||
|
|
||||||
self.assertEqual(order.status, OrderStatus.PENDING)
|
self.assertEqual(order.status, OrderStatus.PENDING)
|
||||||
self.assertEqual(order.lines.count(), 2)
|
self.assertEqual(order.lines.count(), 3)
|
||||||
|
|
||||||
sku = SupplierPart.objects.get(SKU='ACME-WIDGET')
|
sku = SupplierPart.objects.get(SKU='ACME-WIDGET')
|
||||||
part = sku.part
|
part = sku.part
|
||||||
@ -76,11 +76,11 @@ class OrderTest(TestCase):
|
|||||||
order.add_line_item(sku, 100)
|
order.add_line_item(sku, 100)
|
||||||
|
|
||||||
self.assertEqual(part.on_order, 100)
|
self.assertEqual(part.on_order, 100)
|
||||||
self.assertEqual(order.lines.count(), 3)
|
self.assertEqual(order.lines.count(), 4)
|
||||||
|
|
||||||
# Order the same part again (it should be merged)
|
# Order the same part again (it should be merged)
|
||||||
order.add_line_item(sku, 50)
|
order.add_line_item(sku, 50)
|
||||||
self.assertEqual(order.lines.count(), 3)
|
self.assertEqual(order.lines.count(), 4)
|
||||||
self.assertEqual(part.on_order, 150)
|
self.assertEqual(part.on_order, 150)
|
||||||
|
|
||||||
# Try to order a supplier part from the wrong supplier
|
# Try to order a supplier part from the wrong supplier
|
||||||
@ -101,7 +101,7 @@ class OrderTest(TestCase):
|
|||||||
loc = StockLocation.objects.get(id=1)
|
loc = StockLocation.objects.get(id=1)
|
||||||
|
|
||||||
# There should be two lines against this order
|
# There should be two lines against this order
|
||||||
self.assertEqual(len(order.pending_line_items()), 2)
|
self.assertEqual(len(order.pending_line_items()), 3)
|
||||||
|
|
||||||
# Should fail, as order is 'PENDING' not 'PLACED"
|
# Should fail, as order is 'PENDING' not 'PLACED"
|
||||||
self.assertEqual(order.status, OrderStatus.PENDING)
|
self.assertEqual(order.status, OrderStatus.PENDING)
|
||||||
@ -117,7 +117,7 @@ class OrderTest(TestCase):
|
|||||||
|
|
||||||
self.assertEqual(line.remaining(), 50)
|
self.assertEqual(line.remaining(), 50)
|
||||||
|
|
||||||
self.assertEqual(part.on_order, 350)
|
self.assertEqual(part.on_order, 1350)
|
||||||
|
|
||||||
# Try to order some invalid things
|
# Try to order some invalid things
|
||||||
with self.assertRaises(django_exceptions.ValidationError):
|
with self.assertRaises(django_exceptions.ValidationError):
|
||||||
@ -132,7 +132,12 @@ class OrderTest(TestCase):
|
|||||||
line = PurchaseOrderLineItem.objects.get(id=2)
|
line = PurchaseOrderLineItem.objects.get(id=2)
|
||||||
order.receive_line_item(line, loc, 2 * line.quantity, user=None)
|
order.receive_line_item(line, loc, 2 * line.quantity, user=None)
|
||||||
|
|
||||||
self.assertEqual(part.on_order, 100)
|
self.assertEqual(part.on_order, 1100)
|
||||||
|
self.assertEqual(order.status, OrderStatus.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, OrderStatus.COMPLETE)
|
||||||
|
|
||||||
def test_export(self):
|
def test_export(self):
|
||||||
|
@ -243,6 +243,11 @@ class PurchaseOrderReceive(AjaxView):
|
|||||||
except (PurchaseOrderLineItem.DoesNotExist, ValueError):
|
except (PurchaseOrderLineItem.DoesNotExist, ValueError):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# Check that line matches the order
|
||||||
|
if not line.order == self.order:
|
||||||
|
# TODO - Display a non-field error?
|
||||||
|
continue
|
||||||
|
|
||||||
# Ignore a part that doesn't map to a SupplierPart
|
# Ignore a part that doesn't map to a SupplierPart
|
||||||
try:
|
try:
|
||||||
if line.part is None:
|
if line.part is None:
|
||||||
@ -277,6 +282,7 @@ class PurchaseOrderReceive(AjaxView):
|
|||||||
|
|
||||||
return self.renderJsonResponse(request, data=data)
|
return self.renderJsonResponse(request, data=data)
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
def receive_parts(self):
|
def receive_parts(self):
|
||||||
""" Called once the form has been validated.
|
""" Called once the form has been validated.
|
||||||
Create new stockitems against received parts.
|
Create new stockitems against received parts.
|
||||||
@ -611,13 +617,30 @@ class POLineItemCreate(AjaxCreateView):
|
|||||||
|
|
||||||
valid = form.is_valid()
|
valid = form.is_valid()
|
||||||
|
|
||||||
|
# Extract the SupplierPart ID from the form
|
||||||
part_id = form['part'].value()
|
part_id = form['part'].value()
|
||||||
|
|
||||||
|
# Extract the Order ID from the form
|
||||||
|
order_id = form['order'].value()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
SupplierPart.objects.get(id=part_id)
|
order = PurchaseOrder.objects.get(id=order_id)
|
||||||
|
except (ValueError, PurchaseOrder.DoesNotExist):
|
||||||
|
order = None
|
||||||
|
form.errors['order'] = [_('Invalid Purchase Order')]
|
||||||
|
valid = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
sp = SupplierPart.objects.get(id=part_id)
|
||||||
|
|
||||||
|
if order is not None:
|
||||||
|
if not sp.supplier == order.supplier:
|
||||||
|
form.errors['part'] = [_('Supplier must match for Part and Order')]
|
||||||
|
valid = False
|
||||||
|
|
||||||
except (SupplierPart.DoesNotExist, ValueError):
|
except (SupplierPart.DoesNotExist, ValueError):
|
||||||
valid = False
|
valid = False
|
||||||
form.errors['part'] = [_('This field is required')]
|
form.errors['part'] = [_('Invalid SupplierPart selection')]
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
'form_valid': valid,
|
'form_valid': valid,
|
||||||
@ -639,6 +662,11 @@ class POLineItemCreate(AjaxCreateView):
|
|||||||
|
|
||||||
form = super().get_form()
|
form = super().get_form()
|
||||||
|
|
||||||
|
# Limit the available to orders to ones that are PENDING
|
||||||
|
query = form.fields['order'].queryset
|
||||||
|
query = query.filter(status=OrderStatus.PENDING)
|
||||||
|
form.fields['order'].queryset = query
|
||||||
|
|
||||||
order_id = form['order'].value()
|
order_id = form['order'].value()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -660,7 +688,7 @@ class POLineItemCreate(AjaxCreateView):
|
|||||||
|
|
||||||
form.fields['part'].queryset = query
|
form.fields['part'].queryset = query
|
||||||
form.fields['order'].widget = HiddenInput()
|
form.fields['order'].widget = HiddenInput()
|
||||||
except PurchaseOrder.DoesNotExist:
|
except (ValueError, PurchaseOrder.DoesNotExist):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return form
|
return form
|
||||||
|
@ -1,21 +1,116 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from import_export.admin import ImportExportModelAdmin
|
from import_export.admin import ImportExportModelAdmin
|
||||||
|
from import_export.resources import ModelResource
|
||||||
|
from import_export.fields import Field
|
||||||
|
import import_export.widgets as widgets
|
||||||
|
|
||||||
from .models import PartCategory, Part
|
from .models import PartCategory, Part
|
||||||
from .models import PartAttachment, PartStar
|
from .models import PartAttachment, PartStar
|
||||||
from .models import BomItem
|
from .models import BomItem
|
||||||
from .models import PartParameterTemplate, PartParameter
|
from .models import PartParameterTemplate, PartParameter
|
||||||
|
|
||||||
|
from stock.models import StockLocation
|
||||||
|
from company.models import SupplierPart
|
||||||
|
|
||||||
|
|
||||||
|
class PartResource(ModelResource):
|
||||||
|
""" Class for managing Part data import/export """
|
||||||
|
|
||||||
|
# ForeignKey fields
|
||||||
|
category = Field(attribute='category', widget=widgets.ForeignKeyWidget(PartCategory))
|
||||||
|
|
||||||
|
default_location = Field(attribute='default_location', widget=widgets.ForeignKeyWidget(StockLocation))
|
||||||
|
|
||||||
|
default_supplier = Field(attribute='default_supplier', widget=widgets.ForeignKeyWidget(SupplierPart))
|
||||||
|
|
||||||
|
category_name = Field(attribute='category__name', readonly=True)
|
||||||
|
|
||||||
|
variant_of = Field(attribute='variant_of', widget=widgets.ForeignKeyWidget(Part))
|
||||||
|
|
||||||
|
# Extra calculated meta-data (readonly)
|
||||||
|
in_stock = Field(attribute='total_stock', readonly=True, widget=widgets.IntegerWidget())
|
||||||
|
|
||||||
|
on_order = Field(attribute='on_order', readonly=True, widget=widgets.IntegerWidget())
|
||||||
|
|
||||||
|
used_in = Field(attribute='used_in_count', readonly=True, widget=widgets.IntegerWidget())
|
||||||
|
|
||||||
|
allocated = Field(attribute='allocation_count', readonly=True, widget=widgets.IntegerWidget())
|
||||||
|
|
||||||
|
building = Field(attribute='quantity_being_built', readonly=True, widget=widgets.IntegerWidget())
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Part
|
||||||
|
skip_unchanged = True
|
||||||
|
report_skipped = False
|
||||||
|
exclude = [
|
||||||
|
'bom_checksum', 'bom_checked_by', 'bom_checked_date'
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
""" Prefetch related data for quicker access """
|
||||||
|
|
||||||
|
query = super().get_queryset()
|
||||||
|
query = query.prefetch_related(
|
||||||
|
'category',
|
||||||
|
'used_in',
|
||||||
|
'builds',
|
||||||
|
'supplier_parts__purchase_order_line_items',
|
||||||
|
'stock_items__allocations'
|
||||||
|
)
|
||||||
|
|
||||||
|
return query
|
||||||
|
|
||||||
|
|
||||||
class PartAdmin(ImportExportModelAdmin):
|
class PartAdmin(ImportExportModelAdmin):
|
||||||
|
|
||||||
|
resource_class = PartResource
|
||||||
|
|
||||||
list_display = ('full_name', 'description', 'total_stock', 'category')
|
list_display = ('full_name', 'description', 'total_stock', 'category')
|
||||||
|
|
||||||
|
list_filter = ('active', 'assembly', 'is_template', 'virtual')
|
||||||
|
|
||||||
|
search_fields = ('name', 'description', 'category__name', 'category__description', 'IPN')
|
||||||
|
|
||||||
|
|
||||||
|
class PartCategoryResource(ModelResource):
|
||||||
|
""" Class for managing PartCategory data import/export """
|
||||||
|
|
||||||
|
parent = Field(attribute='parent', widget=widgets.ForeignKeyWidget(PartCategory))
|
||||||
|
|
||||||
|
parent_name = Field(attribute='parent__name', readonly=True)
|
||||||
|
|
||||||
|
default_location = Field(attribute='default_location', widget=widgets.ForeignKeyWidget(StockLocation))
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = PartCategory
|
||||||
|
skip_unchanged = True
|
||||||
|
report_skipped = False
|
||||||
|
|
||||||
|
exclude = [
|
||||||
|
# Exclude MPTT internal model fields
|
||||||
|
'lft', 'rght', 'tree_id', 'level',
|
||||||
|
]
|
||||||
|
|
||||||
|
def after_import(self, dataset, result, using_transactions, dry_run, **kwargs):
|
||||||
|
|
||||||
|
super().after_import(dataset, result, using_transactions, dry_run, **kwargs)
|
||||||
|
|
||||||
|
# Rebuild the PartCategory tree(s)
|
||||||
|
PartCategory.objects.rebuild()
|
||||||
|
|
||||||
|
|
||||||
class PartCategoryAdmin(ImportExportModelAdmin):
|
class PartCategoryAdmin(ImportExportModelAdmin):
|
||||||
|
|
||||||
|
resource_class = PartCategoryResource
|
||||||
|
|
||||||
list_display = ('name', 'pathstring', 'description')
|
list_display = ('name', 'pathstring', 'description')
|
||||||
|
|
||||||
|
search_fields = ('name', 'description')
|
||||||
|
|
||||||
|
|
||||||
class PartAttachmentAdmin(admin.ModelAdmin):
|
class PartAttachmentAdmin(admin.ModelAdmin):
|
||||||
|
|
||||||
@ -27,15 +122,53 @@ class PartStarAdmin(admin.ModelAdmin):
|
|||||||
list_display = ('part', 'user')
|
list_display = ('part', 'user')
|
||||||
|
|
||||||
|
|
||||||
|
class BomItemResource(ModelResource):
|
||||||
|
""" Class for managing BomItem data import/export """
|
||||||
|
|
||||||
|
part = Field(attribute='part', widget=widgets.ForeignKeyWidget(Part))
|
||||||
|
|
||||||
|
sub_part = Field(attribute='part', widget=widgets.ForeignKeyWidget(Part))
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = BomItem
|
||||||
|
skip_unchanged = True
|
||||||
|
report_skipped = False
|
||||||
|
|
||||||
|
|
||||||
class BomItemAdmin(ImportExportModelAdmin):
|
class BomItemAdmin(ImportExportModelAdmin):
|
||||||
|
|
||||||
|
resource_class = BomItemResource
|
||||||
|
|
||||||
list_display = ('part', 'sub_part', 'quantity')
|
list_display = ('part', 'sub_part', 'quantity')
|
||||||
|
|
||||||
|
search_fields = ('part__name', 'part__description', 'sub_part__name', 'sub_part__description')
|
||||||
|
|
||||||
|
|
||||||
class ParameterTemplateAdmin(ImportExportModelAdmin):
|
class ParameterTemplateAdmin(ImportExportModelAdmin):
|
||||||
list_display = ('name', 'units')
|
list_display = ('name', 'units')
|
||||||
|
|
||||||
|
|
||||||
|
class ParameterResource(ModelResource):
|
||||||
|
""" Class for managing PartParameter data import/export """
|
||||||
|
|
||||||
|
part = Field(attribute='part', widget=widgets.ForeignKeyWidget(Part))
|
||||||
|
|
||||||
|
part_name = Field(attribute='part__name', readonly=True)
|
||||||
|
|
||||||
|
template = Field(attribute='template', widget=widgets.ForeignKeyWidget(PartParameterTemplate))
|
||||||
|
|
||||||
|
template_name = Field(attribute='template__name', readonly=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = PartParameter
|
||||||
|
skip_unchanged = True
|
||||||
|
report_skipped = False
|
||||||
|
|
||||||
|
|
||||||
class ParameterAdmin(ImportExportModelAdmin):
|
class ParameterAdmin(ImportExportModelAdmin):
|
||||||
|
|
||||||
|
resource_class = ParameterResource
|
||||||
|
|
||||||
list_display = ('part', 'template', 'data')
|
list_display = ('part', 'template', 'data')
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,15 +1,93 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from import_export.admin import ImportExportModelAdmin
|
from import_export.admin import ImportExportModelAdmin
|
||||||
|
from import_export.resources import ModelResource
|
||||||
|
from import_export.fields import Field
|
||||||
|
import import_export.widgets as widgets
|
||||||
|
|
||||||
from .models import StockLocation, StockItem
|
from .models import StockLocation, StockItem
|
||||||
from .models import StockItemTracking
|
from .models import StockItemTracking
|
||||||
|
|
||||||
|
from build.models import Build
|
||||||
|
from company.models import Company, SupplierPart
|
||||||
|
from order.models import PurchaseOrder
|
||||||
|
from part.models import Part
|
||||||
|
|
||||||
|
|
||||||
|
class LocationResource(ModelResource):
|
||||||
|
""" Class for managing StockLocation data import/export """
|
||||||
|
|
||||||
|
parent = Field(attribute='parent', widget=widgets.ForeignKeyWidget(StockLocation))
|
||||||
|
|
||||||
|
parent_name = Field(attribute='parent__name', readonly=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = StockLocation
|
||||||
|
skip_unchanged = True
|
||||||
|
report_skipped = False
|
||||||
|
|
||||||
|
exclude = [
|
||||||
|
# Exclude MPTT internal model fields
|
||||||
|
'lft', 'rght', 'tree_id', 'level',
|
||||||
|
]
|
||||||
|
|
||||||
|
def after_import(self, dataset, result, using_transactions, dry_run, **kwargs):
|
||||||
|
|
||||||
|
super().after_import(dataset, result, using_transactions, dry_run, **kwargs)
|
||||||
|
|
||||||
|
# Rebuild the StockLocation tree(s)
|
||||||
|
StockLocation.objects.rebuild()
|
||||||
|
|
||||||
|
|
||||||
class LocationAdmin(ImportExportModelAdmin):
|
class LocationAdmin(ImportExportModelAdmin):
|
||||||
|
|
||||||
|
resource_class = LocationResource
|
||||||
|
|
||||||
list_display = ('name', 'pathstring', 'description')
|
list_display = ('name', 'pathstring', 'description')
|
||||||
|
|
||||||
|
search_fields = ('name', 'description')
|
||||||
|
|
||||||
|
|
||||||
|
class StockItemResource(ModelResource):
|
||||||
|
""" Class for managing StockItem data import/export """
|
||||||
|
|
||||||
|
# Custom manaegrs for ForeignKey fields
|
||||||
|
part = Field(attribute='part', widget=widgets.ForeignKeyWidget(Part))
|
||||||
|
|
||||||
|
part_name = Field(attribute='part__name', readonly=True)
|
||||||
|
|
||||||
|
supplier_part = Field(attribute='supplier_part', widget=widgets.ForeignKeyWidget(SupplierPart))
|
||||||
|
|
||||||
|
location = Field(attribute='location', widget=widgets.ForeignKeyWidget(StockLocation))
|
||||||
|
|
||||||
|
location_name = Field(attribute='location__name', readonly=True)
|
||||||
|
|
||||||
|
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))
|
||||||
|
|
||||||
|
purchase_order = Field(attribute='purchase_order', widget=widgets.ForeignKeyWidget(PurchaseOrder))
|
||||||
|
|
||||||
|
# Date management
|
||||||
|
updated = Field(attribute='updated', widget=widgets.DateWidget())
|
||||||
|
|
||||||
|
stocktake_date = Field(attribute='stocktake_date', widget=widgets.DateWidget())
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = StockItem
|
||||||
|
skip_unchanged = True
|
||||||
|
report_skipped = False
|
||||||
|
|
||||||
|
|
||||||
class StockItemAdmin(ImportExportModelAdmin):
|
class StockItemAdmin(ImportExportModelAdmin):
|
||||||
|
|
||||||
|
resource_class = StockItemResource
|
||||||
|
|
||||||
list_display = ('part', 'quantity', 'location', 'status', 'updated')
|
list_display = ('part', 'quantity', 'location', 'status', 'updated')
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user