mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge branch 'master' of https://github.com/inventree/InvenTree into price-history
This commit is contained in:
commit
4156b71c4b
2
.github/workflows/docker_build.yaml
vendored
2
.github/workflows/docker_build.yaml
vendored
@ -8,7 +8,7 @@ on:
|
|||||||
- 'master'
|
- 'master'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
||||||
docker:
|
docker:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
2
.github/workflows/mysql.yaml
vendored
2
.github/workflows/mysql.yaml
vendored
@ -33,7 +33,7 @@ jobs:
|
|||||||
options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3
|
options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3
|
||||||
ports:
|
ports:
|
||||||
- 3306:3306
|
- 3306:3306
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Code
|
- name: Checkout Code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
2
.github/workflows/postgresql.yaml
vendored
2
.github/workflows/postgresql.yaml
vendored
@ -5,7 +5,7 @@ name: PostgreSQL
|
|||||||
on: ["push", "pull_request"]
|
on: ["push", "pull_request"]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
@ -83,7 +83,7 @@ class InvenTreeAPITestCase(APITestCase):
|
|||||||
self.assertEqual(response.status_code, code)
|
self.assertEqual(response.status_code, code)
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def post(self, url, data):
|
def post(self, url, data):
|
||||||
"""
|
"""
|
||||||
Issue a POST request
|
Issue a POST request
|
||||||
|
@ -71,7 +71,7 @@ def status_codes(request):
|
|||||||
def user_roles(request):
|
def user_roles(request):
|
||||||
"""
|
"""
|
||||||
Return a map of the current roles assigned to the user.
|
Return a map of the current roles assigned to the user.
|
||||||
|
|
||||||
Roles are denoted by their simple names, and then the permission type.
|
Roles are denoted by their simple names, and then the permission type.
|
||||||
|
|
||||||
Permissions can be access as follows:
|
Permissions can be access as follows:
|
||||||
|
@ -17,5 +17,5 @@ class InvenTreeManualExchangeBackend(BaseExchangeBackend):
|
|||||||
"""
|
"""
|
||||||
Do not get any rates...
|
Do not get any rates...
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return {}
|
return {}
|
||||||
|
@ -102,5 +102,5 @@ class RoundingDecimalField(models.DecimalField):
|
|||||||
}
|
}
|
||||||
|
|
||||||
defaults.update(kwargs)
|
defaults.update(kwargs)
|
||||||
|
|
||||||
return super().formfield(**kwargs)
|
return super().formfield(**kwargs)
|
||||||
|
@ -35,7 +35,7 @@ def generateTestKey(test_name):
|
|||||||
"""
|
"""
|
||||||
Generate a test 'key' for a given test name.
|
Generate a test 'key' for a given test name.
|
||||||
This must not have illegal chars as it will be used for dict lookup in a template.
|
This must not have illegal chars as it will be used for dict lookup in a template.
|
||||||
|
|
||||||
Tests must be named such that they will have unique keys.
|
Tests must be named such that they will have unique keys.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -102,7 +102,7 @@ def TestIfImageURL(url):
|
|||||||
'.tif', '.tiff',
|
'.tif', '.tiff',
|
||||||
'.webp', '.gif',
|
'.webp', '.gif',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def str2bool(text, test=True):
|
def str2bool(text, test=True):
|
||||||
""" Test if a string 'looks' like a boolean value.
|
""" Test if a string 'looks' like a boolean value.
|
||||||
@ -137,10 +137,10 @@ def isNull(text):
|
|||||||
"""
|
"""
|
||||||
Test if a string 'looks' like a null value.
|
Test if a string 'looks' like a null value.
|
||||||
This is useful for querying the API against a null key.
|
This is useful for querying the API against a null key.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
text: Input text
|
text: Input text
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
True if the text looks like a null value
|
True if the text looks like a null value
|
||||||
"""
|
"""
|
||||||
@ -157,7 +157,7 @@ def normalize(d):
|
|||||||
d = Decimal(d)
|
d = Decimal(d)
|
||||||
|
|
||||||
d = d.normalize()
|
d = d.normalize()
|
||||||
|
|
||||||
# Ref: https://docs.python.org/3/library/decimal.html
|
# Ref: https://docs.python.org/3/library/decimal.html
|
||||||
return d.quantize(Decimal(1)) if d == d.to_integral() else d.normalize()
|
return d.quantize(Decimal(1)) if d == d.to_integral() else d.normalize()
|
||||||
|
|
||||||
@ -165,14 +165,14 @@ def normalize(d):
|
|||||||
def increment(n):
|
def increment(n):
|
||||||
"""
|
"""
|
||||||
Attempt to increment an integer (or a string that looks like an integer!)
|
Attempt to increment an integer (or a string that looks like an integer!)
|
||||||
|
|
||||||
e.g.
|
e.g.
|
||||||
|
|
||||||
001 -> 002
|
001 -> 002
|
||||||
2 -> 3
|
2 -> 3
|
||||||
AB01 -> AB02
|
AB01 -> AB02
|
||||||
QQQ -> QQQ
|
QQQ -> QQQ
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
value = str(n).strip()
|
value = str(n).strip()
|
||||||
@ -314,7 +314,7 @@ def MakeBarcode(object_name, object_pk, object_data={}, **kwargs):
|
|||||||
|
|
||||||
def GetExportFormats():
|
def GetExportFormats():
|
||||||
""" Return a list of allowable file formats for exporting data """
|
""" Return a list of allowable file formats for exporting data """
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'csv',
|
'csv',
|
||||||
'tsv',
|
'tsv',
|
||||||
@ -327,7 +327,7 @@ def GetExportFormats():
|
|||||||
|
|
||||||
def DownloadFile(data, filename, content_type='application/text'):
|
def DownloadFile(data, filename, content_type='application/text'):
|
||||||
""" Create a dynamic file for the user to download.
|
""" Create a dynamic file for the user to download.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
data: Raw file data (string or bytes)
|
data: Raw file data (string or bytes)
|
||||||
filename: Filename for the file download
|
filename: Filename for the file download
|
||||||
@ -525,7 +525,7 @@ def addUserPermission(user, permission):
|
|||||||
"""
|
"""
|
||||||
Shortcut function for adding a certain permission to a user.
|
Shortcut function for adding a certain permission to a user.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
perm = Permission.objects.get(codename=permission)
|
perm = Permission.objects.get(codename=permission)
|
||||||
user.user_permissions.add(perm)
|
user.user_permissions.add(perm)
|
||||||
|
|
||||||
@ -576,7 +576,7 @@ def getOldestMigrationFile(app, exclude_extension=True, ignore_initial=True):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
num = int(f.split('_')[0])
|
num = int(f.split('_')[0])
|
||||||
|
|
||||||
if oldest_file is None or num < oldest_num:
|
if oldest_file is None or num < oldest_num:
|
||||||
oldest_num = num
|
oldest_num = num
|
||||||
oldest_file = f
|
oldest_file = f
|
||||||
@ -585,7 +585,7 @@ def getOldestMigrationFile(app, exclude_extension=True, ignore_initial=True):
|
|||||||
oldest_file = oldest_file.replace('.py', '')
|
oldest_file = oldest_file.replace('.py', '')
|
||||||
|
|
||||||
return oldest_file
|
return oldest_file
|
||||||
|
|
||||||
|
|
||||||
def getNewestMigrationFile(app, exclude_extension=True):
|
def getNewestMigrationFile(app, exclude_extension=True):
|
||||||
"""
|
"""
|
||||||
|
@ -77,12 +77,20 @@ class AuthRequiredMiddleware(object):
|
|||||||
if request.path_info == reverse_lazy('logout'):
|
if request.path_info == reverse_lazy('logout'):
|
||||||
return HttpResponseRedirect(reverse_lazy('login'))
|
return HttpResponseRedirect(reverse_lazy('login'))
|
||||||
|
|
||||||
login = reverse_lazy('login')
|
path = request.path_info
|
||||||
|
|
||||||
if not request.path_info == login and not request.path_info.startswith('/api/'):
|
# List of URL endpoints we *do not* want to redirect to
|
||||||
|
urls = [
|
||||||
|
reverse_lazy('login'),
|
||||||
|
reverse_lazy('logout'),
|
||||||
|
reverse_lazy('admin:login'),
|
||||||
|
reverse_lazy('admin:logout'),
|
||||||
|
]
|
||||||
|
|
||||||
|
if path not in urls and not path.startswith('/api/'):
|
||||||
# Save the 'next' parameter to pass through to the login view
|
# Save the 'next' parameter to pass through to the login view
|
||||||
|
|
||||||
return redirect('%s?next=%s' % (login, request.path))
|
return redirect('%s?next=%s' % (reverse_lazy('login'), request.path))
|
||||||
|
|
||||||
# Code to be executed for each request/response after
|
# Code to be executed for each request/response after
|
||||||
# the view is called.
|
# the view is called.
|
||||||
|
@ -129,7 +129,7 @@ class InvenTreeTree(MPTTModel):
|
|||||||
|
|
||||||
Here an 'item' is considered to be the 'leaf' at the end of each branch,
|
Here an 'item' is considered to be the 'leaf' at the end of each branch,
|
||||||
and the exact nature here will depend on the class implementation.
|
and the exact nature here will depend on the class implementation.
|
||||||
|
|
||||||
The default implementation returns zero
|
The default implementation returns zero
|
||||||
"""
|
"""
|
||||||
return 0
|
return 0
|
||||||
|
@ -17,7 +17,7 @@ class RolePermission(permissions.BasePermission):
|
|||||||
- PUT
|
- PUT
|
||||||
- PATCH
|
- PATCH
|
||||||
- DELETE
|
- DELETE
|
||||||
|
|
||||||
Specify the required "role" using the role_required attribute.
|
Specify the required "role" using the role_required attribute.
|
||||||
|
|
||||||
e.g.
|
e.g.
|
||||||
|
@ -44,7 +44,7 @@ class InvenTreeModelSerializer(serializers.ModelSerializer):
|
|||||||
In addition to running validators on the serializer fields,
|
In addition to running validators on the serializer fields,
|
||||||
this class ensures that the underlying model is also validated.
|
this class ensures that the underlying model is also validated.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Run any native validation checks first (may throw an ValidationError)
|
# Run any native validation checks first (may throw an ValidationError)
|
||||||
data = super(serializers.ModelSerializer, self).validate(data)
|
data = super(serializers.ModelSerializer, self).validate(data)
|
||||||
|
|
||||||
|
@ -466,6 +466,24 @@
|
|||||||
background: #eee;
|
background: #eee;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* pricing table widths */
|
||||||
|
.table-price-two tr td:first-child {
|
||||||
|
width: 40%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-price-three tr td:first-child {
|
||||||
|
width: 40%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-price-two tr td:last-child {
|
||||||
|
width: 60%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-price-three tr td:last-child {
|
||||||
|
width: 30%;
|
||||||
|
}
|
||||||
|
/* !pricing table widths */
|
||||||
|
|
||||||
.btn-glyph {
|
.btn-glyph {
|
||||||
padding-left: 6px;
|
padding-left: 6px;
|
||||||
padding-right: 6px;
|
padding-right: 6px;
|
||||||
@ -489,7 +507,7 @@
|
|||||||
padding-right: 6px;
|
padding-right: 6px;
|
||||||
padding-top: 3px;
|
padding-top: 3px;
|
||||||
padding-bottom: 2px;
|
padding-bottom: 2px;
|
||||||
};
|
}
|
||||||
|
|
||||||
.panel-heading .badge {
|
.panel-heading .badge {
|
||||||
float: right;
|
float: right;
|
||||||
@ -550,7 +568,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.media {
|
.media {
|
||||||
//padding-top: 15px;
|
/* padding-top: 15px; */
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -576,8 +594,8 @@
|
|||||||
width: 160px;
|
width: 160px;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
//top: 0;
|
/* top: 0;
|
||||||
//left: 0;
|
left: 0; */
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
padding-top: 20px;
|
padding-top: 20px;
|
||||||
padding-right: 25px;
|
padding-right: 25px;
|
||||||
@ -808,7 +826,7 @@ input[type="submit"] {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
z-index: 5000;
|
z-index: 5000;
|
||||||
pointer-events: none; // Prevent this div from blocking links underneath
|
pointer-events: none; /* Prevent this div from blocking links underneath */
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert {
|
.alert {
|
||||||
@ -918,4 +936,15 @@ input[type="submit"] {
|
|||||||
|
|
||||||
input[type="date"].form-control, input[type="time"].form-control, input[type="datetime-local"].form-control, input[type="month"].form-control {
|
input[type="date"].form-control, input[type="time"].form-control, input[type="datetime-local"].form-control, input[type="month"].form-control {
|
||||||
line-height: unset;
|
line-height: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.clip-btn {
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 0px 6px;
|
||||||
|
color: var(--label-grey);
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clip-btn:hover {
|
||||||
|
background: var(--label-grey);
|
||||||
|
}
|
||||||
|
7
InvenTree/InvenTree/static/script/clipboard.min.js
vendored
Normal file
7
InvenTree/InvenTree/static/script/clipboard.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -1,3 +1,14 @@
|
|||||||
|
function attachClipboard(selector) {
|
||||||
|
|
||||||
|
new ClipboardJS(selector, {
|
||||||
|
text: function(trigger) {
|
||||||
|
var content = trigger.parentElement.parentElement.textContent;
|
||||||
|
|
||||||
|
return content.trim();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function inventreeDocReady() {
|
function inventreeDocReady() {
|
||||||
/* Run this function when the HTML document is loaded.
|
/* Run this function when the HTML document is loaded.
|
||||||
* This will be called for every page that extends "base.html"
|
* This will be called for every page that extends "base.html"
|
||||||
@ -48,6 +59,10 @@ function inventreeDocReady() {
|
|||||||
no_post: true,
|
no_post: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Initialize clipboard-buttons
|
||||||
|
attachClipboard('.clip-btn');
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function isFileTransfer(transfer) {
|
function isFileTransfer(transfer) {
|
||||||
@ -100,7 +115,7 @@ function makeIconButton(icon, cls, pk, title, options={}) {
|
|||||||
if (options.disabled) {
|
if (options.disabled) {
|
||||||
extraProps += "disabled='true' ";
|
extraProps += "disabled='true' ";
|
||||||
}
|
}
|
||||||
|
|
||||||
html += `<button pk='${pk}' id='${id}' class='${classes}' title='${title}' ${extraProps}>`;
|
html += `<button pk='${pk}' id='${id}' class='${classes}' title='${title}' ${extraProps}>`;
|
||||||
html += `<span class='fas ${icon}'></span>`;
|
html += `<span class='fas ${icon}'></span>`;
|
||||||
html += `</button>`;
|
html += `</button>`;
|
||||||
|
@ -64,7 +64,7 @@ def is_email_configured():
|
|||||||
|
|
||||||
if not settings.EMAIL_HOST_USER:
|
if not settings.EMAIL_HOST_USER:
|
||||||
configured = False
|
configured = False
|
||||||
|
|
||||||
# Display warning unless in test mode
|
# Display warning unless in test mode
|
||||||
if not settings.TESTING:
|
if not settings.TESTING:
|
||||||
logger.warning("EMAIL_HOST_USER is not configured")
|
logger.warning("EMAIL_HOST_USER is not configured")
|
||||||
|
@ -16,7 +16,7 @@ class StatusCode:
|
|||||||
# If the key cannot be found, pass it back
|
# If the key cannot be found, pass it back
|
||||||
if key not in cls.options.keys():
|
if key not in cls.options.keys():
|
||||||
return key
|
return key
|
||||||
|
|
||||||
value = cls.options.get(key, key)
|
value = cls.options.get(key, key)
|
||||||
color = cls.colors.get(key, 'grey')
|
color = cls.colors.get(key, 'grey')
|
||||||
|
|
||||||
|
@ -119,7 +119,7 @@ class APITests(InvenTreeAPITestCase):
|
|||||||
self.assertNotIn('add', roles[rule])
|
self.assertNotIn('add', roles[rule])
|
||||||
self.assertNotIn('change', roles[rule])
|
self.assertNotIn('change', roles[rule])
|
||||||
self.assertNotIn('delete', roles[rule])
|
self.assertNotIn('delete', roles[rule])
|
||||||
|
|
||||||
def test_with_superuser(self):
|
def test_with_superuser(self):
|
||||||
"""
|
"""
|
||||||
Superuser should have *all* roles assigned
|
Superuser should have *all* roles assigned
|
||||||
|
@ -37,7 +37,7 @@ class ScheduledTaskTests(TestCase):
|
|||||||
# Attempt to schedule the same task again
|
# Attempt to schedule the same task again
|
||||||
InvenTree.tasks.schedule_task(task, schedule_type=Schedule.MINUTES, minutes=5)
|
InvenTree.tasks.schedule_task(task, schedule_type=Schedule.MINUTES, minutes=5)
|
||||||
self.assertEqual(self.get_tasks(task).count(), 1)
|
self.assertEqual(self.get_tasks(task).count(), 1)
|
||||||
|
|
||||||
# But the 'minutes' should have been updated
|
# But the 'minutes' should have been updated
|
||||||
t = Schedule.objects.get(func=task)
|
t = Schedule.objects.get(func=task)
|
||||||
self.assertEqual(t.minutes, 5)
|
self.assertEqual(t.minutes, 5)
|
||||||
|
@ -97,7 +97,7 @@ class TestHelpers(TestCase):
|
|||||||
self.assertEqual(helpers.getMediaUrl('xx/yy.png'), '/media/xx/yy.png')
|
self.assertEqual(helpers.getMediaUrl('xx/yy.png'), '/media/xx/yy.png')
|
||||||
|
|
||||||
def testDecimal2String(self):
|
def testDecimal2String(self):
|
||||||
|
|
||||||
self.assertEqual(helpers.decimal2string(Decimal('1.2345000')), '1.2345')
|
self.assertEqual(helpers.decimal2string(Decimal('1.2345000')), '1.2345')
|
||||||
self.assertEqual(helpers.decimal2string('test'), 'test')
|
self.assertEqual(helpers.decimal2string('test'), 'test')
|
||||||
|
|
||||||
@ -205,7 +205,7 @@ class TestMPTT(TestCase):
|
|||||||
child = StockLocation.objects.get(pk=5)
|
child = StockLocation.objects.get(pk=5)
|
||||||
|
|
||||||
parent.parent = child
|
parent.parent = child
|
||||||
|
|
||||||
with self.assertRaises(InvalidMove):
|
with self.assertRaises(InvalidMove):
|
||||||
parent.save()
|
parent.save()
|
||||||
|
|
||||||
@ -223,7 +223,7 @@ class TestMPTT(TestCase):
|
|||||||
drawer.save()
|
drawer.save()
|
||||||
|
|
||||||
self.assertNotEqual(tree, drawer.tree_id)
|
self.assertNotEqual(tree, drawer.tree_id)
|
||||||
|
|
||||||
|
|
||||||
class TestSerialNumberExtraction(TestCase):
|
class TestSerialNumberExtraction(TestCase):
|
||||||
""" Tests for serial number extraction code """
|
""" Tests for serial number extraction code """
|
||||||
|
@ -81,7 +81,7 @@ settings_urls = [
|
|||||||
url(r'^user/?', SettingsView.as_view(template_name='InvenTree/settings/user.html'), name='settings-user'),
|
url(r'^user/?', SettingsView.as_view(template_name='InvenTree/settings/user.html'), name='settings-user'),
|
||||||
url(r'^appearance/?', AppearanceSelectView.as_view(), name='settings-appearance'),
|
url(r'^appearance/?', AppearanceSelectView.as_view(), name='settings-appearance'),
|
||||||
url(r'^i18n/?', include('django.conf.urls.i18n')),
|
url(r'^i18n/?', include('django.conf.urls.i18n')),
|
||||||
|
|
||||||
url(r'^global/?', SettingsView.as_view(template_name='InvenTree/settings/global.html'), name='settings-global'),
|
url(r'^global/?', SettingsView.as_view(template_name='InvenTree/settings/global.html'), name='settings-global'),
|
||||||
url(r'^report/?', SettingsView.as_view(template_name='InvenTree/settings/report.html'), name='settings-report'),
|
url(r'^report/?', SettingsView.as_view(template_name='InvenTree/settings/report.html'), name='settings-report'),
|
||||||
url(r'^category/?', SettingCategorySelectView.as_view(), name='settings-category'),
|
url(r'^category/?', SettingCategorySelectView.as_view(), name='settings-category'),
|
||||||
@ -137,7 +137,7 @@ urlpatterns = [
|
|||||||
|
|
||||||
url(r'^login/?', auth_views.LoginView.as_view(), name='login'),
|
url(r'^login/?', auth_views.LoginView.as_view(), name='login'),
|
||||||
url(r'^logout/', auth_views.LogoutView.as_view(template_name='registration/logged_out.html'), name='logout'),
|
url(r'^logout/', auth_views.LogoutView.as_view(template_name='registration/logged_out.html'), name='logout'),
|
||||||
|
|
||||||
url(r'^settings/', include(settings_urls)),
|
url(r'^settings/', include(settings_urls)),
|
||||||
|
|
||||||
url(r'^edit-user/', EditUserView.as_view(), name='edit-user'),
|
url(r'^edit-user/', EditUserView.as_view(), name='edit-user'),
|
||||||
|
@ -74,7 +74,7 @@ def validate_build_order_reference(value):
|
|||||||
match = re.search(pattern, value)
|
match = re.search(pattern, value)
|
||||||
|
|
||||||
if match is None:
|
if match is None:
|
||||||
raise ValidationError(_('Reference must match pattern') + f" '{pattern}'")
|
raise ValidationError(_('Reference must match pattern {pattern}').format(pattern=pattern))
|
||||||
|
|
||||||
|
|
||||||
def validate_purchase_order_reference(value):
|
def validate_purchase_order_reference(value):
|
||||||
@ -88,7 +88,7 @@ def validate_purchase_order_reference(value):
|
|||||||
match = re.search(pattern, value)
|
match = re.search(pattern, value)
|
||||||
|
|
||||||
if match is None:
|
if match is None:
|
||||||
raise ValidationError(_('Reference must match pattern') + f" '{pattern}'")
|
raise ValidationError(_('Reference must match pattern {pattern}').format(pattern=pattern))
|
||||||
|
|
||||||
|
|
||||||
def validate_sales_order_reference(value):
|
def validate_sales_order_reference(value):
|
||||||
@ -102,7 +102,7 @@ def validate_sales_order_reference(value):
|
|||||||
match = re.search(pattern, value)
|
match = re.search(pattern, value)
|
||||||
|
|
||||||
if match is None:
|
if match is None:
|
||||||
raise ValidationError(_('Reference must match pattern') + f" '{pattern}'")
|
raise ValidationError(_('Reference must match pattern {pattern}').format(pattern=pattern))
|
||||||
|
|
||||||
|
|
||||||
def validate_tree_name(value):
|
def validate_tree_name(value):
|
||||||
@ -130,7 +130,7 @@ def validate_overage(value):
|
|||||||
|
|
||||||
if i < 0:
|
if i < 0:
|
||||||
raise ValidationError(_("Overage value must not be negative"))
|
raise ValidationError(_("Overage value must not be negative"))
|
||||||
|
|
||||||
# Looks like an integer!
|
# Looks like an integer!
|
||||||
return True
|
return True
|
||||||
except ValueError:
|
except ValueError:
|
||||||
|
@ -176,7 +176,7 @@ class InvenTreeRoleMixin(PermissionRequiredMixin):
|
|||||||
|
|
||||||
if role not in RuleSet.RULESET_NAMES:
|
if role not in RuleSet.RULESET_NAMES:
|
||||||
raise ValueError(f"Role '{role}' is not a valid role")
|
raise ValueError(f"Role '{role}' is not a valid role")
|
||||||
|
|
||||||
if permission not in RuleSet.RULESET_PERMISSIONS:
|
if permission not in RuleSet.RULESET_PERMISSIONS:
|
||||||
raise ValueError(f"Permission '{permission}' is not a valid permission")
|
raise ValueError(f"Permission '{permission}' is not a valid permission")
|
||||||
|
|
||||||
@ -223,7 +223,7 @@ class InvenTreeRoleMixin(PermissionRequiredMixin):
|
|||||||
Return the 'permission_class' required for the current View.
|
Return the 'permission_class' required for the current View.
|
||||||
|
|
||||||
Must be one of:
|
Must be one of:
|
||||||
|
|
||||||
- view
|
- view
|
||||||
- change
|
- change
|
||||||
- add
|
- add
|
||||||
@ -389,7 +389,7 @@ class QRCodeView(AjaxView):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
ajax_template_name = "qr_code.html"
|
ajax_template_name = "qr_code.html"
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
self.request = request
|
self.request = request
|
||||||
self.pk = self.kwargs['pk']
|
self.pk = self.kwargs['pk']
|
||||||
@ -398,7 +398,7 @@ class QRCodeView(AjaxView):
|
|||||||
def get_qr_data(self):
|
def get_qr_data(self):
|
||||||
""" Returns the text object to render to a QR code.
|
""" Returns the text object to render to a QR code.
|
||||||
The actual rendering will be handled by the template """
|
The actual rendering will be handled by the template """
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_context_data(self):
|
def get_context_data(self):
|
||||||
@ -406,7 +406,7 @@ class QRCodeView(AjaxView):
|
|||||||
|
|
||||||
Explicity passes the parameter 'qr_data'
|
Explicity passes the parameter 'qr_data'
|
||||||
"""
|
"""
|
||||||
|
|
||||||
context = {}
|
context = {}
|
||||||
|
|
||||||
qr = self.get_qr_data()
|
qr = self.get_qr_data()
|
||||||
@ -415,7 +415,7 @@ class QRCodeView(AjaxView):
|
|||||||
context['qr_data'] = qr
|
context['qr_data'] = qr
|
||||||
else:
|
else:
|
||||||
context['error_msg'] = 'Error generating QR code'
|
context['error_msg'] = 'Error generating QR code'
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
@ -507,7 +507,7 @@ class AjaxUpdateView(AjaxMixin, UpdateView):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
super(UpdateView, self).get(request, *args, **kwargs)
|
super(UpdateView, self).get(request, *args, **kwargs)
|
||||||
|
|
||||||
return self.renderJsonResponse(request, self.get_form(), context=self.get_context_data())
|
return self.renderJsonResponse(request, self.get_form(), context=self.get_context_data())
|
||||||
|
|
||||||
def save(self, object, form, **kwargs):
|
def save(self, object, form, **kwargs):
|
||||||
@ -673,7 +673,7 @@ class SetPasswordView(AjaxUpdateView):
|
|||||||
|
|
||||||
p1 = request.POST.get('enter_password', '')
|
p1 = request.POST.get('enter_password', '')
|
||||||
p2 = request.POST.get('confirm_password', '')
|
p2 = request.POST.get('confirm_password', '')
|
||||||
|
|
||||||
if valid:
|
if valid:
|
||||||
# Passwords must match
|
# Passwords must match
|
||||||
|
|
||||||
@ -712,7 +712,7 @@ class IndexView(TemplateView):
|
|||||||
# Generate a list of orderable parts which have stock below their minimum values
|
# Generate a list of orderable parts which have stock below their minimum values
|
||||||
# TODO - Is there a less expensive way to get these from the database
|
# TODO - Is there a less expensive way to get these from the database
|
||||||
# context['to_order'] = [part for part in Part.objects.filter(purchaseable=True) if part.need_to_restock()]
|
# context['to_order'] = [part for part in Part.objects.filter(purchaseable=True) if part.need_to_restock()]
|
||||||
|
|
||||||
# Generate a list of assembly parts which have stock below their minimum values
|
# Generate a list of assembly parts which have stock below their minimum values
|
||||||
# TODO - Is there a less expensive way to get these from the database
|
# TODO - Is there a less expensive way to get these from the database
|
||||||
# context['to_build'] = [part for part in Part.objects.filter(assembly=True) if part.need_to_restock()]
|
# context['to_build'] = [part for part in Part.objects.filter(assembly=True) if part.need_to_restock()]
|
||||||
@ -752,7 +752,7 @@ class DynamicJsView(TemplateView):
|
|||||||
|
|
||||||
template_name = ""
|
template_name = ""
|
||||||
content_type = 'text/javascript'
|
content_type = 'text/javascript'
|
||||||
|
|
||||||
|
|
||||||
class SettingsView(TemplateView):
|
class SettingsView(TemplateView):
|
||||||
""" View for configuring User settings
|
""" View for configuring User settings
|
||||||
@ -830,7 +830,7 @@ class AppearanceSelectView(FormView):
|
|||||||
|
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
theme_selected = form.cleaned_data['name']
|
theme_selected = form.cleaned_data['name']
|
||||||
|
|
||||||
# Set color theme to form selection
|
# Set color theme to form selection
|
||||||
user_theme.name = theme_selected
|
user_theme.name = theme_selected
|
||||||
user_theme.save()
|
user_theme.save()
|
||||||
@ -893,7 +893,7 @@ class DatabaseStatsView(AjaxView):
|
|||||||
# Part stats
|
# Part stats
|
||||||
ctx['part_count'] = Part.objects.count()
|
ctx['part_count'] = Part.objects.count()
|
||||||
ctx['part_cat_count'] = PartCategory.objects.count()
|
ctx['part_cat_count'] = PartCategory.objects.count()
|
||||||
|
|
||||||
# Stock stats
|
# Stock stats
|
||||||
ctx['stock_item_count'] = StockItem.objects.count()
|
ctx['stock_item_count'] = StockItem.objects.count()
|
||||||
ctx['stock_loc_count'] = StockLocation.objects.count()
|
ctx['stock_loc_count'] = StockLocation.objects.count()
|
||||||
|
@ -73,7 +73,7 @@ class BarcodeScan(APIView):
|
|||||||
|
|
||||||
# A plugin has been found!
|
# A plugin has been found!
|
||||||
if plugin is not None:
|
if plugin is not None:
|
||||||
|
|
||||||
# Try to associate with a stock item
|
# Try to associate with a stock item
|
||||||
item = plugin.getStockItem()
|
item = plugin.getStockItem()
|
||||||
|
|
||||||
@ -133,7 +133,7 @@ class BarcodeScan(APIView):
|
|||||||
class BarcodeAssign(APIView):
|
class BarcodeAssign(APIView):
|
||||||
"""
|
"""
|
||||||
Endpoint for assigning a barcode to a stock item.
|
Endpoint for assigning a barcode to a stock item.
|
||||||
|
|
||||||
- This only works if the barcode is not already associated with an object in the database
|
- This only works if the barcode is not already associated with an object in the database
|
||||||
- If the barcode does not match an object, then the barcode hash is assigned to the StockItem
|
- If the barcode does not match an object, then the barcode hash is assigned to the StockItem
|
||||||
"""
|
"""
|
||||||
@ -178,7 +178,7 @@ class BarcodeAssign(APIView):
|
|||||||
|
|
||||||
# Matching plugin was found
|
# Matching plugin was found
|
||||||
if plugin is not None:
|
if plugin is not None:
|
||||||
|
|
||||||
hash = plugin.hash()
|
hash = plugin.hash()
|
||||||
response['hash'] = hash
|
response['hash'] = hash
|
||||||
response['plugin'] = plugin.name
|
response['plugin'] = plugin.name
|
||||||
@ -234,7 +234,7 @@ class BarcodeAssign(APIView):
|
|||||||
barcode_api_urls = [
|
barcode_api_urls = [
|
||||||
|
|
||||||
url(r'^link/$', BarcodeAssign.as_view(), name='api-barcode-link'),
|
url(r'^link/$', BarcodeAssign.as_view(), name='api-barcode-link'),
|
||||||
|
|
||||||
# Catch-all performs barcode 'scan'
|
# Catch-all performs barcode 'scan'
|
||||||
url(r'^.*$', BarcodeScan.as_view(), name='api-barcode-scan'),
|
url(r'^.*$', BarcodeScan.as_view(), name='api-barcode-scan'),
|
||||||
]
|
]
|
||||||
|
@ -21,7 +21,7 @@ def hash_barcode(barcode_data):
|
|||||||
|
|
||||||
HACK: Remove any 'non printable' characters from the hash,
|
HACK: Remove any 'non printable' characters from the hash,
|
||||||
as it seems browers will remove special control characters...
|
as it seems browers will remove special control characters...
|
||||||
|
|
||||||
TODO: Work out a way around this!
|
TODO: Work out a way around this!
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -92,7 +92,7 @@ class BarcodeAPITest(APITestCase):
|
|||||||
data = response.data
|
data = response.data
|
||||||
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
|
||||||
self.assertIn('stockitem', data)
|
self.assertIn('stockitem', data)
|
||||||
|
|
||||||
pk = data['stockitem']['pk']
|
pk = data['stockitem']['pk']
|
||||||
@ -121,7 +121,7 @@ class BarcodeAPITest(APITestCase):
|
|||||||
data = response.data
|
data = response.data
|
||||||
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
|
||||||
self.assertIn('success', data)
|
self.assertIn('success', data)
|
||||||
|
|
||||||
hash = data['hash']
|
hash = data['hash']
|
||||||
|
@ -20,7 +20,7 @@ from .serializers import BuildSerializer, BuildItemSerializer
|
|||||||
|
|
||||||
class BuildList(generics.ListCreateAPIView):
|
class BuildList(generics.ListCreateAPIView):
|
||||||
""" API endpoint for accessing a list of Build objects.
|
""" API endpoint for accessing a list of Build objects.
|
||||||
|
|
||||||
- GET: Return list of objects (with filters)
|
- GET: Return list of objects (with filters)
|
||||||
- POST: Create a new Build object
|
- POST: Create a new Build object
|
||||||
"""
|
"""
|
||||||
@ -65,7 +65,7 @@ class BuildList(generics.ListCreateAPIView):
|
|||||||
queryset = BuildSerializer.annotate_queryset(queryset)
|
queryset = BuildSerializer.annotate_queryset(queryset)
|
||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
def filter_queryset(self, queryset):
|
def filter_queryset(self, queryset):
|
||||||
|
|
||||||
queryset = super().filter_queryset(queryset)
|
queryset = super().filter_queryset(queryset)
|
||||||
|
@ -118,7 +118,7 @@ class Build(MPTTModel):
|
|||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('build-detail', kwargs={'pk': self.id})
|
return reverse('build-detail', kwargs={'pk': self.id})
|
||||||
|
|
||||||
reference = models.CharField(
|
reference = models.CharField(
|
||||||
unique=True,
|
unique=True,
|
||||||
max_length=64,
|
max_length=64,
|
||||||
@ -168,7 +168,7 @@ class Build(MPTTModel):
|
|||||||
null=True, blank=True,
|
null=True, blank=True,
|
||||||
help_text=_('SalesOrder to which this build is allocated')
|
help_text=_('SalesOrder to which this build is allocated')
|
||||||
)
|
)
|
||||||
|
|
||||||
take_from = models.ForeignKey(
|
take_from = models.ForeignKey(
|
||||||
'stock.StockLocation',
|
'stock.StockLocation',
|
||||||
verbose_name=_('Source Location'),
|
verbose_name=_('Source Location'),
|
||||||
@ -177,7 +177,7 @@ class Build(MPTTModel):
|
|||||||
null=True, blank=True,
|
null=True, blank=True,
|
||||||
help_text=_('Select location to take stock from for this build (leave blank to take from any stock location)')
|
help_text=_('Select location to take stock from for this build (leave blank to take from any stock location)')
|
||||||
)
|
)
|
||||||
|
|
||||||
destination = models.ForeignKey(
|
destination = models.ForeignKey(
|
||||||
'stock.StockLocation',
|
'stock.StockLocation',
|
||||||
verbose_name=_('Destination Location'),
|
verbose_name=_('Destination Location'),
|
||||||
@ -207,7 +207,7 @@ class Build(MPTTModel):
|
|||||||
validators=[MinValueValidator(0)],
|
validators=[MinValueValidator(0)],
|
||||||
help_text=_('Build status code')
|
help_text=_('Build status code')
|
||||||
)
|
)
|
||||||
|
|
||||||
batch = models.CharField(
|
batch = models.CharField(
|
||||||
verbose_name=_('Batch Code'),
|
verbose_name=_('Batch Code'),
|
||||||
max_length=100,
|
max_length=100,
|
||||||
@ -215,9 +215,9 @@ class Build(MPTTModel):
|
|||||||
null=True,
|
null=True,
|
||||||
help_text=_('Batch code for this build output')
|
help_text=_('Batch code for this build output')
|
||||||
)
|
)
|
||||||
|
|
||||||
creation_date = models.DateField(auto_now_add=True, editable=False, verbose_name=_('Creation Date'))
|
creation_date = models.DateField(auto_now_add=True, editable=False, verbose_name=_('Creation Date'))
|
||||||
|
|
||||||
target_date = models.DateField(
|
target_date = models.DateField(
|
||||||
null=True, blank=True,
|
null=True, blank=True,
|
||||||
verbose_name=_('Target completion date'),
|
verbose_name=_('Target completion date'),
|
||||||
@ -251,7 +251,7 @@ class Build(MPTTModel):
|
|||||||
help_text=_('User responsible for this build order'),
|
help_text=_('User responsible for this build order'),
|
||||||
related_name='builds_responsible',
|
related_name='builds_responsible',
|
||||||
)
|
)
|
||||||
|
|
||||||
link = InvenTree.fields.InvenTreeURLField(
|
link = InvenTree.fields.InvenTreeURLField(
|
||||||
verbose_name=_('External Link'),
|
verbose_name=_('External Link'),
|
||||||
blank=True, help_text=_('Link to external URL')
|
blank=True, help_text=_('Link to external URL')
|
||||||
@ -272,7 +272,7 @@ class Build(MPTTModel):
|
|||||||
else:
|
else:
|
||||||
descendants = self.get_descendants(include_self=True)
|
descendants = self.get_descendants(include_self=True)
|
||||||
Build.objects.filter(parent__pk__in=[d.pk for d in descendants])
|
Build.objects.filter(parent__pk__in=[d.pk for d in descendants])
|
||||||
|
|
||||||
def sub_build_count(self, cascade=True):
|
def sub_build_count(self, cascade=True):
|
||||||
"""
|
"""
|
||||||
Return the number of sub builds under this one.
|
Return the number of sub builds under this one.
|
||||||
@ -295,7 +295,7 @@ class Build(MPTTModel):
|
|||||||
query = query.filter(Build.OVERDUE_FILTER)
|
query = query.filter(Build.OVERDUE_FILTER)
|
||||||
|
|
||||||
return query.exists()
|
return query.exists()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def active(self):
|
def active(self):
|
||||||
"""
|
"""
|
||||||
@ -441,7 +441,7 @@ class Build(MPTTModel):
|
|||||||
|
|
||||||
# Extract the "most recent" build order reference
|
# Extract the "most recent" build order reference
|
||||||
builds = cls.objects.exclude(reference=None)
|
builds = cls.objects.exclude(reference=None)
|
||||||
|
|
||||||
if not builds.exists():
|
if not builds.exists():
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@ -543,7 +543,7 @@ class Build(MPTTModel):
|
|||||||
- The sub_item in the BOM line must *not* be trackable
|
- The sub_item in the BOM line must *not* be trackable
|
||||||
- There is only a single stock item available (which has not already been allocated to this build)
|
- There is only a single stock item available (which has not already been allocated to this build)
|
||||||
- The stock item has an availability greater than zero
|
- The stock item has an availability greater than zero
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A list object containing the StockItem objects to be allocated (and the quantities).
|
A list object containing the StockItem objects to be allocated (and the quantities).
|
||||||
Each item in the list is a dict as follows:
|
Each item in the list is a dict as follows:
|
||||||
@ -648,7 +648,7 @@ class Build(MPTTModel):
|
|||||||
"""
|
"""
|
||||||
Deletes all stock allocations for this build.
|
Deletes all stock allocations for this build.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
allocations = BuildItem.objects.filter(build=self)
|
allocations = BuildItem.objects.filter(build=self)
|
||||||
|
|
||||||
allocations.delete()
|
allocations.delete()
|
||||||
@ -1145,7 +1145,7 @@ class BuildItem(models.Model):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
self.validate_unique()
|
self.validate_unique()
|
||||||
|
|
||||||
super().clean()
|
super().clean()
|
||||||
|
|
||||||
errors = {}
|
errors = {}
|
||||||
@ -1159,7 +1159,7 @@ class BuildItem(models.Model):
|
|||||||
# Allocated part must be in the BOM for the master part
|
# Allocated part must be in the BOM for the master part
|
||||||
if self.stock_item.part not in self.build.part.getRequiredParts(recursive=False):
|
if self.stock_item.part not in self.build.part.getRequiredParts(recursive=False):
|
||||||
errors['stock_item'] = [_("Selected stock item not found in BOM for part '{p}'").format(p=self.build.part.full_name)]
|
errors['stock_item'] = [_("Selected stock item not found in BOM for part '{p}'").format(p=self.build.part.full_name)]
|
||||||
|
|
||||||
# Allocated quantity cannot exceed available stock quantity
|
# Allocated quantity cannot exceed available stock quantity
|
||||||
if self.quantity > self.stock_item.quantity:
|
if self.quantity > self.stock_item.quantity:
|
||||||
errors['quantity'] = [_("Allocated quantity ({n}) must not exceed available quantity ({q})").format(
|
errors['quantity'] = [_("Allocated quantity ({n}) must not exceed available quantity ({q})").format(
|
||||||
|
@ -86,7 +86,7 @@
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#btn-order-parts").click(function() {
|
$("#btn-order-parts").click(function() {
|
||||||
launchModalForm("/order/purchase-order/order-parts/", {
|
launchModalForm("/order/purchase-order/order-parts/", {
|
||||||
data: {
|
data: {
|
||||||
@ -94,7 +94,7 @@
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -230,5 +230,5 @@ src="{% static 'img/blank_image.png' %}"
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
@ -17,9 +17,9 @@
|
|||||||
<div class='col-sm-6'>
|
<div class='col-sm-6'>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
<div id='button-toolbar'>
|
<div id='button-toolbar'>
|
||||||
<div class='button-toolbar container-fluid' style='float: right;'>
|
<div class='button-toolbar container-fluid' style='float: right;'>
|
||||||
<div class='btn-group'>
|
<div class='btn-group'>
|
||||||
@ -66,7 +66,7 @@
|
|||||||
|
|
||||||
<script type='text/javascript'>
|
<script type='text/javascript'>
|
||||||
function loadOrderEvents(calendar) {
|
function loadOrderEvents(calendar) {
|
||||||
|
|
||||||
var start = startDate(calendar);
|
var start = startDate(calendar);
|
||||||
var end = endDate(calendar);
|
var end = endDate(calendar);
|
||||||
|
|
||||||
@ -85,7 +85,7 @@
|
|||||||
var prefix = '{% settings_value "BUILDORDER_REFERENCE_PREFIX" %}';
|
var prefix = '{% settings_value "BUILDORDER_REFERENCE_PREFIX" %}';
|
||||||
|
|
||||||
for (var idx = 0; idx < response.length; idx++) {
|
for (var idx = 0; idx < response.length; idx++) {
|
||||||
|
|
||||||
var order = response[idx];
|
var order = response[idx];
|
||||||
|
|
||||||
var date = order.creation_date;
|
var date = order.creation_date;
|
||||||
@ -155,16 +155,18 @@ $('#view-calendar').click(function() {
|
|||||||
$(".fixed-table-pagination").hide();
|
$(".fixed-table-pagination").hide();
|
||||||
$(".columns-right").hide();
|
$(".columns-right").hide();
|
||||||
$(".search").hide();
|
$(".search").hide();
|
||||||
|
|
||||||
$("#build-order-calendar").show();
|
$("#build-order-calendar").show();
|
||||||
$("#view-list").show();
|
$("#view-list").show();
|
||||||
|
|
||||||
|
calendar.render();
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#view-list").click(function() {
|
$("#view-list").click(function() {
|
||||||
// Hide the calendar view, show the list view
|
// Hide the calendar view, show the list view
|
||||||
$("#build-order-calendar").hide();
|
$("#build-order-calendar").hide();
|
||||||
$("#view-list").hide();
|
$("#view-list").hide();
|
||||||
|
|
||||||
$(".fixed-table-pagination").show();
|
$(".fixed-table-pagination").show();
|
||||||
$(".columns-right").show();
|
$(".columns-right").show();
|
||||||
$(".search").show();
|
$(".search").show();
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
</li>
|
</li>
|
||||||
|
|
||||||
{% if build.active %}
|
{% if build.active %}
|
||||||
|
|
||||||
<li class='list-group-item {% if tab == "allocate" %}active{% endif %}' title='{% trans "Allocate Stock" %}'>
|
<li class='list-group-item {% if tab == "allocate" %}active{% endif %}' title='{% trans "Allocate Stock" %}'>
|
||||||
<a href='{% url "build-allocate" build.id %}'>
|
<a href='{% url "build-allocate" build.id %}'>
|
||||||
<span class='fas fa-tools'></span>
|
<span class='fas fa-tools'></span>
|
||||||
|
@ -20,11 +20,11 @@
|
|||||||
<hr>
|
<hr>
|
||||||
<form method='POST'>
|
<form method='POST'>
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
||||||
{{ form }}
|
{{ form }}
|
||||||
<hr>
|
<hr>
|
||||||
<button type="submit" class='btn btn-default'>{% trans "Save" %}</button>
|
<button type="submit" class='btn btn-default'>{% trans "Save" %}</button>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{{ form.media }}
|
{{ form.media }}
|
||||||
|
@ -30,7 +30,7 @@ class BuildAPITest(InvenTreeAPITestCase):
|
|||||||
'build.change',
|
'build.change',
|
||||||
'build.add'
|
'build.add'
|
||||||
]
|
]
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
|
||||||
super().setUp()
|
super().setUp()
|
||||||
@ -54,7 +54,7 @@ class BuildListTest(BuildAPITest):
|
|||||||
|
|
||||||
builds = self.get(self.url, data={'active': True})
|
builds = self.get(self.url, data={'active': True})
|
||||||
self.assertEqual(len(builds.data), 1)
|
self.assertEqual(len(builds.data), 1)
|
||||||
|
|
||||||
builds = self.get(self.url, data={'status': BuildStatus.COMPLETE})
|
builds = self.get(self.url, data={'status': BuildStatus.COMPLETE})
|
||||||
self.assertEqual(len(builds.data), 4)
|
self.assertEqual(len(builds.data), 4)
|
||||||
|
|
||||||
|
@ -114,7 +114,7 @@ class BuildTest(TestCase):
|
|||||||
# Perform some basic tests before we start the ball rolling
|
# Perform some basic tests before we start the ball rolling
|
||||||
|
|
||||||
self.assertEqual(StockItem.objects.count(), 6)
|
self.assertEqual(StockItem.objects.count(), 6)
|
||||||
|
|
||||||
# Build is PENDING
|
# Build is PENDING
|
||||||
self.assertEqual(self.build.status, status.BuildStatus.PENDING)
|
self.assertEqual(self.build.status, status.BuildStatus.PENDING)
|
||||||
|
|
||||||
@ -142,7 +142,7 @@ class BuildTest(TestCase):
|
|||||||
|
|
||||||
# Create a BuiltItem which points to an invalid StockItem
|
# Create a BuiltItem which points to an invalid StockItem
|
||||||
b = BuildItem(stock_item=stock, build=self.build, quantity=10)
|
b = BuildItem(stock_item=stock, build=self.build, quantity=10)
|
||||||
|
|
||||||
with self.assertRaises(ValidationError):
|
with self.assertRaises(ValidationError):
|
||||||
b.save()
|
b.save()
|
||||||
|
|
||||||
@ -339,7 +339,7 @@ class BuildTest(TestCase):
|
|||||||
self.assertTrue(self.build.can_complete)
|
self.assertTrue(self.build.can_complete)
|
||||||
|
|
||||||
self.build.complete_build(None)
|
self.build.complete_build(None)
|
||||||
|
|
||||||
self.assertEqual(self.build.status, status.BuildStatus.COMPLETE)
|
self.assertEqual(self.build.status, status.BuildStatus.COMPLETE)
|
||||||
|
|
||||||
# the original BuildItem objects should have been deleted!
|
# the original BuildItem objects should have been deleted!
|
||||||
@ -351,12 +351,12 @@ class BuildTest(TestCase):
|
|||||||
# This stock item has been depleted!
|
# This stock item has been depleted!
|
||||||
with self.assertRaises(StockItem.DoesNotExist):
|
with self.assertRaises(StockItem.DoesNotExist):
|
||||||
StockItem.objects.get(pk=self.stock_1_1.pk)
|
StockItem.objects.get(pk=self.stock_1_1.pk)
|
||||||
|
|
||||||
# This stock item has *not* been depleted
|
# This stock item has *not* been depleted
|
||||||
x = StockItem.objects.get(pk=self.stock_2_1.pk)
|
x = StockItem.objects.get(pk=self.stock_2_1.pk)
|
||||||
|
|
||||||
self.assertEqual(x.quantity, 4970)
|
self.assertEqual(x.quantity, 4970)
|
||||||
|
|
||||||
# And 10 new stock items created for the build output
|
# And 10 new stock items created for the build output
|
||||||
outputs = StockItem.objects.filter(build=self.build)
|
outputs = StockItem.objects.filter(build=self.build)
|
||||||
|
|
||||||
|
@ -251,7 +251,7 @@ class TestBuildViews(TestCase):
|
|||||||
content = str(response.content)
|
content = str(response.content)
|
||||||
|
|
||||||
self.assertIn(build.title, content)
|
self.assertIn(build.title, content)
|
||||||
|
|
||||||
def test_build_create(self):
|
def test_build_create(self):
|
||||||
""" Test the build creation view (ajax form) """
|
""" Test the build creation view (ajax form) """
|
||||||
|
|
||||||
@ -260,7 +260,7 @@ class TestBuildViews(TestCase):
|
|||||||
# Create build without specifying part
|
# Create build without specifying part
|
||||||
response = self.client.get(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
response = self.client.get(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
# Create build with valid part
|
# Create build with valid part
|
||||||
response = self.client.get(url, {'part': 1}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
response = self.client.get(url, {'part': 1}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
@ -281,7 +281,7 @@ class TestBuildViews(TestCase):
|
|||||||
# Get the page in editing mode
|
# Get the page in editing mode
|
||||||
response = self.client.get(url, {'edit': 1})
|
response = self.client.get(url, {'edit': 1})
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
def test_build_item_create(self):
|
def test_build_item_create(self):
|
||||||
""" Test the BuildItem creation view (ajax form) """
|
""" Test the BuildItem creation view (ajax form) """
|
||||||
|
|
||||||
@ -305,7 +305,7 @@ class TestBuildViews(TestCase):
|
|||||||
|
|
||||||
def test_build_item_edit(self):
|
def test_build_item_edit(self):
|
||||||
""" Test the BuildItem edit view (ajax form) """
|
""" Test the BuildItem edit view (ajax form) """
|
||||||
|
|
||||||
# TODO
|
# TODO
|
||||||
# url = reverse('build-item-edit')
|
# url = reverse('build-item-edit')
|
||||||
pass
|
pass
|
||||||
@ -323,7 +323,7 @@ class TestBuildViews(TestCase):
|
|||||||
# Test without confirmation
|
# Test without confirmation
|
||||||
response = self.client.post(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
response = self.client.post(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
data = json.loads(response.content)
|
data = json.loads(response.content)
|
||||||
self.assertFalse(data['form_valid'])
|
self.assertFalse(data['form_valid'])
|
||||||
|
|
||||||
@ -353,7 +353,7 @@ class TestBuildViews(TestCase):
|
|||||||
# Test with confirmation, invalid location
|
# Test with confirmation, invalid location
|
||||||
response = self.client.post(url, {'confirm': 1, 'location': 9999}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
response = self.client.post(url, {'confirm': 1, 'location': 9999}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
data = json.loads(response.content)
|
data = json.loads(response.content)
|
||||||
self.assertFalse(data['form_valid'])
|
self.assertFalse(data['form_valid'])
|
||||||
|
|
||||||
@ -365,7 +365,7 @@ class TestBuildViews(TestCase):
|
|||||||
# Test without confirmation
|
# Test without confirmation
|
||||||
response = self.client.post(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
response = self.client.post(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
data = json.loads(response.content)
|
data = json.loads(response.content)
|
||||||
self.assertFalse(data['form_valid'])
|
self.assertFalse(data['form_valid'])
|
||||||
|
|
||||||
@ -393,7 +393,7 @@ class TestBuildViews(TestCase):
|
|||||||
|
|
||||||
data = json.loads(response.content)
|
data = json.loads(response.content)
|
||||||
self.assertFalse(data['form_valid'])
|
self.assertFalse(data['form_valid'])
|
||||||
|
|
||||||
# Test with confirmation
|
# Test with confirmation
|
||||||
response = self.client.post(url, {'confirm': 1}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
response = self.client.post(url, {'confirm': 1}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
@ -159,7 +159,7 @@ class BuildOutputCreate(AjaxUpdateView):
|
|||||||
|
|
||||||
if quantity:
|
if quantity:
|
||||||
build = self.get_object()
|
build = self.get_object()
|
||||||
|
|
||||||
# Check that requested output don't exceed build remaining quantity
|
# Check that requested output don't exceed build remaining quantity
|
||||||
maximum_output = int(build.remaining - build.incomplete_count)
|
maximum_output = int(build.remaining - build.incomplete_count)
|
||||||
if quantity > maximum_output:
|
if quantity > maximum_output:
|
||||||
@ -318,7 +318,7 @@ class BuildUnallocate(AjaxUpdateView):
|
|||||||
form_class = forms.UnallocateBuildForm
|
form_class = forms.UnallocateBuildForm
|
||||||
ajax_form_title = _("Unallocate Stock")
|
ajax_form_title = _("Unallocate Stock")
|
||||||
ajax_template_name = "build/unallocate.html"
|
ajax_template_name = "build/unallocate.html"
|
||||||
|
|
||||||
def get_initial(self):
|
def get_initial(self):
|
||||||
|
|
||||||
initials = super().get_initial()
|
initials = super().get_initial()
|
||||||
@ -341,7 +341,7 @@ class BuildUnallocate(AjaxUpdateView):
|
|||||||
|
|
||||||
build = self.get_object()
|
build = self.get_object()
|
||||||
form = self.get_form()
|
form = self.get_form()
|
||||||
|
|
||||||
confirm = request.POST.get('confirm', False)
|
confirm = request.POST.get('confirm', False)
|
||||||
|
|
||||||
output_id = request.POST.get('output_id', None)
|
output_id = request.POST.get('output_id', None)
|
||||||
@ -382,7 +382,7 @@ class BuildUnallocate(AjaxUpdateView):
|
|||||||
# Unallocate "untracked" parts
|
# Unallocate "untracked" parts
|
||||||
else:
|
else:
|
||||||
build.unallocateUntracked(part=part)
|
build.unallocateUntracked(part=part)
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
'form_valid': valid,
|
'form_valid': valid,
|
||||||
}
|
}
|
||||||
@ -401,7 +401,7 @@ class BuildComplete(AjaxUpdateView):
|
|||||||
|
|
||||||
model = Build
|
model = Build
|
||||||
form_class = forms.CompleteBuildForm
|
form_class = forms.CompleteBuildForm
|
||||||
|
|
||||||
ajax_form_title = _('Complete Build Order')
|
ajax_form_title = _('Complete Build Order')
|
||||||
ajax_template_name = 'build/complete.html'
|
ajax_template_name = 'build/complete.html'
|
||||||
|
|
||||||
@ -437,9 +437,9 @@ class BuildOutputComplete(AjaxUpdateView):
|
|||||||
context_object_name = "build"
|
context_object_name = "build"
|
||||||
ajax_form_title = _("Complete Build Output")
|
ajax_form_title = _("Complete Build Output")
|
||||||
ajax_template_name = "build/complete_output.html"
|
ajax_template_name = "build/complete_output.html"
|
||||||
|
|
||||||
def get_form(self):
|
def get_form(self):
|
||||||
|
|
||||||
build = self.get_object()
|
build = self.get_object()
|
||||||
|
|
||||||
form = super().get_form()
|
form = super().get_form()
|
||||||
@ -500,7 +500,7 @@ class BuildOutputComplete(AjaxUpdateView):
|
|||||||
|
|
||||||
- If the part being built has a default location, pre-select that location
|
- If the part being built has a default location, pre-select that location
|
||||||
"""
|
"""
|
||||||
|
|
||||||
initials = super().get_initial()
|
initials = super().get_initial()
|
||||||
build = self.get_object()
|
build = self.get_object()
|
||||||
|
|
||||||
@ -585,7 +585,7 @@ class BuildOutputComplete(AjaxUpdateView):
|
|||||||
location=location,
|
location=location,
|
||||||
status=stock_status,
|
status=stock_status,
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_data(self):
|
def get_data(self):
|
||||||
""" Provide feedback data back to the form """
|
""" Provide feedback data back to the form """
|
||||||
return {
|
return {
|
||||||
@ -600,7 +600,7 @@ class BuildNotes(InvenTreeRoleMixin, UpdateView):
|
|||||||
context_object_name = 'build'
|
context_object_name = 'build'
|
||||||
template_name = 'build/notes.html'
|
template_name = 'build/notes.html'
|
||||||
model = Build
|
model = Build
|
||||||
|
|
||||||
# Override the default permission role for this View
|
# Override the default permission role for this View
|
||||||
role_required = 'build.view'
|
role_required = 'build.view'
|
||||||
|
|
||||||
@ -612,7 +612,7 @@ class BuildNotes(InvenTreeRoleMixin, UpdateView):
|
|||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
|
|
||||||
ctx = super().get_context_data(**kwargs)
|
ctx = super().get_context_data(**kwargs)
|
||||||
|
|
||||||
ctx['editing'] = str2bool(self.request.GET.get('edit', ''))
|
ctx['editing'] = str2bool(self.request.GET.get('edit', ''))
|
||||||
|
|
||||||
return ctx
|
return ctx
|
||||||
@ -746,7 +746,7 @@ class BuildCreate(AjaxCreateView):
|
|||||||
|
|
||||||
class BuildUpdate(AjaxUpdateView):
|
class BuildUpdate(AjaxUpdateView):
|
||||||
""" View for editing a Build object """
|
""" View for editing a Build object """
|
||||||
|
|
||||||
model = Build
|
model = Build
|
||||||
form_class = forms.EditBuildForm
|
form_class = forms.EditBuildForm
|
||||||
context_object_name = 'build'
|
context_object_name = 'build'
|
||||||
@ -804,7 +804,7 @@ class BuildItemDelete(AjaxDeleteView):
|
|||||||
ajax_template_name = 'build/delete_build_item.html'
|
ajax_template_name = 'build/delete_build_item.html'
|
||||||
ajax_form_title = _('Unallocate Stock')
|
ajax_form_title = _('Unallocate Stock')
|
||||||
context_object_name = 'item'
|
context_object_name = 'item'
|
||||||
|
|
||||||
def get_data(self):
|
def get_data(self):
|
||||||
return {
|
return {
|
||||||
'danger': _('Removed parts from build allocation')
|
'danger': _('Removed parts from build allocation')
|
||||||
@ -826,7 +826,7 @@ class BuildItemCreate(AjaxCreateView):
|
|||||||
|
|
||||||
# The "part" which is being allocated to the output
|
# The "part" which is being allocated to the output
|
||||||
part = None
|
part = None
|
||||||
|
|
||||||
available_stock = None
|
available_stock = None
|
||||||
|
|
||||||
def get_context_data(self):
|
def get_context_data(self):
|
||||||
@ -906,7 +906,7 @@ class BuildItemCreate(AjaxCreateView):
|
|||||||
if part_id:
|
if part_id:
|
||||||
try:
|
try:
|
||||||
self.part = Part.objects.get(pk=part_id)
|
self.part = Part.objects.get(pk=part_id)
|
||||||
|
|
||||||
except (ValueError, Part.DoesNotExist):
|
except (ValueError, Part.DoesNotExist):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -958,7 +958,7 @@ class BuildItemCreate(AjaxCreateView):
|
|||||||
|
|
||||||
# Reference to a StockItem object
|
# Reference to a StockItem object
|
||||||
item = None
|
item = None
|
||||||
|
|
||||||
# Reference to a Build object
|
# Reference to a Build object
|
||||||
build = None
|
build = None
|
||||||
|
|
||||||
@ -999,7 +999,7 @@ class BuildItemCreate(AjaxCreateView):
|
|||||||
quantity = float(quantity)
|
quantity = float(quantity)
|
||||||
elif required_quantity is not None:
|
elif required_quantity is not None:
|
||||||
quantity = required_quantity
|
quantity = required_quantity
|
||||||
|
|
||||||
item_id = self.get_param('item')
|
item_id = self.get_param('item')
|
||||||
|
|
||||||
# If the request specifies a particular StockItem
|
# If the request specifies a particular StockItem
|
||||||
@ -1035,7 +1035,7 @@ class BuildItemEdit(AjaxUpdateView):
|
|||||||
ajax_template_name = 'build/edit_build_item.html'
|
ajax_template_name = 'build/edit_build_item.html'
|
||||||
form_class = forms.EditBuildItemForm
|
form_class = forms.EditBuildItemForm
|
||||||
ajax_form_title = _('Edit Stock Allocation')
|
ajax_form_title = _('Edit Stock Allocation')
|
||||||
|
|
||||||
def get_data(self):
|
def get_data(self):
|
||||||
return {
|
return {
|
||||||
'info': _('Updated Build Item'),
|
'info': _('Updated Build Item'),
|
||||||
@ -1068,7 +1068,7 @@ class BuildAttachmentCreate(AjaxCreateView):
|
|||||||
model = BuildOrderAttachment
|
model = BuildOrderAttachment
|
||||||
form_class = forms.EditBuildAttachmentForm
|
form_class = forms.EditBuildAttachmentForm
|
||||||
ajax_form_title = _('Add Build Order Attachment')
|
ajax_form_title = _('Add Build Order Attachment')
|
||||||
|
|
||||||
def save(self, form, **kwargs):
|
def save(self, form, **kwargs):
|
||||||
"""
|
"""
|
||||||
Add information on the user that uploaded the attachment
|
Add information on the user that uploaded the attachment
|
||||||
@ -1105,7 +1105,7 @@ class BuildAttachmentCreate(AjaxCreateView):
|
|||||||
form = super().get_form()
|
form = super().get_form()
|
||||||
|
|
||||||
form.fields['build'].widget = HiddenInput()
|
form.fields['build'].widget = HiddenInput()
|
||||||
|
|
||||||
return form
|
return form
|
||||||
|
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ from .models import InvenTreeSetting
|
|||||||
|
|
||||||
|
|
||||||
class SettingsAdmin(ImportExportModelAdmin):
|
class SettingsAdmin(ImportExportModelAdmin):
|
||||||
|
|
||||||
list_display = ('key', 'value')
|
list_display = ('key', 'value')
|
||||||
|
|
||||||
|
|
||||||
|
@ -7,6 +7,8 @@ These models are 'generic' and do not fit a particular business logic object.
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import decimal
|
||||||
|
import math
|
||||||
|
|
||||||
from django.db import models, transaction
|
from django.db import models, transaction
|
||||||
from django.db.utils import IntegrityError, OperationalError
|
from django.db.utils import IntegrityError, OperationalError
|
||||||
@ -40,7 +42,7 @@ class InvenTreeSetting(models.Model):
|
|||||||
The key of each item is the name of the value as it appears in the database.
|
The key of each item is the name of the value as it appears in the database.
|
||||||
|
|
||||||
Each global setting has the following parameters:
|
Each global setting has the following parameters:
|
||||||
|
|
||||||
- name: Translatable string name of the setting (required)
|
- name: Translatable string name of the setting (required)
|
||||||
- description: Translatable string description of the setting (required)
|
- description: Translatable string description of the setting (required)
|
||||||
- default: Default value (optional)
|
- default: Default value (optional)
|
||||||
@ -419,7 +421,7 @@ class InvenTreeSetting(models.Model):
|
|||||||
# Evaluate the function (we expect it will return a list of tuples...)
|
# Evaluate the function (we expect it will return a list of tuples...)
|
||||||
return choices()
|
return choices()
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return choices
|
return choices
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -529,7 +531,7 @@ class InvenTreeSetting(models.Model):
|
|||||||
# Enforce standard boolean representation
|
# Enforce standard boolean representation
|
||||||
if setting.is_bool():
|
if setting.is_bool():
|
||||||
value = InvenTree.helpers.str2bool(value)
|
value = InvenTree.helpers.str2bool(value)
|
||||||
|
|
||||||
setting.value = str(value)
|
setting.value = str(value)
|
||||||
setting.save()
|
setting.save()
|
||||||
|
|
||||||
@ -671,7 +673,7 @@ class InvenTreeSetting(models.Model):
|
|||||||
|
|
||||||
if validator == int:
|
if validator == int:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if type(validator) in [list, tuple]:
|
if type(validator) in [list, tuple]:
|
||||||
for v in validator:
|
for v in validator:
|
||||||
if v == int:
|
if v == int:
|
||||||
@ -682,7 +684,7 @@ class InvenTreeSetting(models.Model):
|
|||||||
def as_int(self):
|
def as_int(self):
|
||||||
"""
|
"""
|
||||||
Return the value of this setting converted to a boolean value.
|
Return the value of this setting converted to a boolean value.
|
||||||
|
|
||||||
If an error occurs, return the default value
|
If an error occurs, return the default value
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -692,7 +694,7 @@ class InvenTreeSetting(models.Model):
|
|||||||
value = self.default_value()
|
value = self.default_value()
|
||||||
|
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
class PriceBreak(models.Model):
|
class PriceBreak(models.Model):
|
||||||
"""
|
"""
|
||||||
@ -737,6 +739,72 @@ class PriceBreak(models.Model):
|
|||||||
return converted.amount
|
return converted.amount
|
||||||
|
|
||||||
|
|
||||||
|
def get_price(instance, quantity, moq=True, multiples=True, currency=None):
|
||||||
|
""" Calculate the price based on quantity price breaks.
|
||||||
|
|
||||||
|
- Don't forget to add in flat-fee cost (base_cost field)
|
||||||
|
- If MOQ (minimum order quantity) is required, bump quantity
|
||||||
|
- If order multiples are to be observed, then we need to calculate based on that, too
|
||||||
|
"""
|
||||||
|
|
||||||
|
price_breaks = instance.price_breaks.all()
|
||||||
|
|
||||||
|
# No price break information available?
|
||||||
|
if len(price_breaks) == 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Check if quantity is fraction and disable multiples
|
||||||
|
multiples = (quantity % 1 == 0)
|
||||||
|
|
||||||
|
# Order multiples
|
||||||
|
if multiples:
|
||||||
|
quantity = int(math.ceil(quantity / instance.multiple) * instance.multiple)
|
||||||
|
|
||||||
|
pb_found = False
|
||||||
|
pb_quantity = -1
|
||||||
|
pb_cost = 0.0
|
||||||
|
|
||||||
|
if currency is None:
|
||||||
|
# Default currency selection
|
||||||
|
currency = InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY')
|
||||||
|
|
||||||
|
pb_min = None
|
||||||
|
for pb in instance.price_breaks.all():
|
||||||
|
# Store smallest price break
|
||||||
|
if not pb_min:
|
||||||
|
pb_min = pb
|
||||||
|
|
||||||
|
# Ignore this pricebreak (quantity is too high)
|
||||||
|
if pb.quantity > quantity:
|
||||||
|
continue
|
||||||
|
|
||||||
|
pb_found = True
|
||||||
|
|
||||||
|
# If this price-break quantity is the largest so far, use it!
|
||||||
|
if pb.quantity > pb_quantity:
|
||||||
|
pb_quantity = pb.quantity
|
||||||
|
|
||||||
|
# Convert everything to the selected currency
|
||||||
|
pb_cost = pb.convert_to(currency)
|
||||||
|
|
||||||
|
# Use smallest price break
|
||||||
|
if not pb_found and pb_min:
|
||||||
|
# Update price break information
|
||||||
|
pb_quantity = pb_min.quantity
|
||||||
|
pb_cost = pb_min.convert_to(currency)
|
||||||
|
# Trigger cost calculation using smallest price break
|
||||||
|
pb_found = True
|
||||||
|
|
||||||
|
# Convert quantity to decimal.Decimal format
|
||||||
|
quantity = decimal.Decimal(f'{quantity}')
|
||||||
|
|
||||||
|
if pb_found:
|
||||||
|
cost = pb_cost * quantity
|
||||||
|
return InvenTree.helpers.normalize(cost + instance.base_cost)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
class ColorTheme(models.Model):
|
class ColorTheme(models.Model):
|
||||||
""" Color Theme Setting """
|
""" Color Theme Setting """
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ def currency_code_default():
|
|||||||
|
|
||||||
if code not in CURRENCIES:
|
if code not in CURRENCIES:
|
||||||
code = 'USD'
|
code = 'USD'
|
||||||
|
|
||||||
return code
|
return code
|
||||||
|
|
||||||
|
|
||||||
|
@ -117,7 +117,7 @@ class SettingsViewTest(TestCase):
|
|||||||
"""
|
"""
|
||||||
Test for binary value
|
Test for binary value
|
||||||
"""
|
"""
|
||||||
|
|
||||||
setting = InvenTreeSetting.get_setting_object('PART_COMPONENT')
|
setting = InvenTreeSetting.get_setting_object('PART_COMPONENT')
|
||||||
|
|
||||||
self.assertTrue(setting.as_bool())
|
self.assertTrue(setting.as_bool())
|
||||||
|
@ -19,7 +19,7 @@ class SettingsTest(TestCase):
|
|||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
|
||||||
user = get_user_model()
|
user = get_user_model()
|
||||||
|
|
||||||
self.user = user.objects.create_user('username', 'user@email.com', 'password')
|
self.user = user.objects.create_user('username', 'user@email.com', 'password')
|
||||||
self.user.is_staff = True
|
self.user.is_staff = True
|
||||||
self.user.save()
|
self.user.save()
|
||||||
|
@ -48,7 +48,7 @@ class SettingEdit(AjaxUpdateView):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
form = super().get_form()
|
form = super().get_form()
|
||||||
|
|
||||||
setting = self.get_object()
|
setting = self.get_object()
|
||||||
|
|
||||||
choices = setting.choices()
|
choices = setting.choices()
|
||||||
|
@ -41,7 +41,7 @@ class CompanyList(generics.ListCreateAPIView):
|
|||||||
queryset = CompanySerializer.annotate_queryset(queryset)
|
queryset = CompanySerializer.annotate_queryset(queryset)
|
||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
filter_backends = [
|
filter_backends = [
|
||||||
DjangoFilterBackend,
|
DjangoFilterBackend,
|
||||||
filters.SearchFilter,
|
filters.SearchFilter,
|
||||||
@ -116,7 +116,7 @@ class ManufacturerPartList(generics.ListCreateAPIView):
|
|||||||
kwargs['pretty'] = str2bool(self.request.query_params.get('pretty', None))
|
kwargs['pretty'] = str2bool(self.request.query_params.get('pretty', None))
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
kwargs['context'] = self.get_serializer_context()
|
kwargs['context'] = self.get_serializer_context()
|
||||||
|
|
||||||
return self.serializer_class(*args, **kwargs)
|
return self.serializer_class(*args, **kwargs)
|
||||||
@ -167,7 +167,7 @@ class ManufacturerPartList(generics.ListCreateAPIView):
|
|||||||
'part__name',
|
'part__name',
|
||||||
'part__description',
|
'part__description',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class ManufacturerPartDetail(generics.RetrieveUpdateDestroyAPIView):
|
class ManufacturerPartDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||||
""" API endpoint for detail view of ManufacturerPart object
|
""" API endpoint for detail view of ManufacturerPart object
|
||||||
@ -255,7 +255,7 @@ class SupplierPartList(generics.ListCreateAPIView):
|
|||||||
kwargs['part_detail'] = str2bool(self.request.query_params.get('part_detail', None))
|
kwargs['part_detail'] = str2bool(self.request.query_params.get('part_detail', None))
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
try:
|
try:
|
||||||
kwargs['supplier_detail'] = str2bool(self.request.query_params.get('supplier_detail', None))
|
kwargs['supplier_detail'] = str2bool(self.request.query_params.get('supplier_detail', None))
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
@ -270,7 +270,7 @@ class SupplierPartList(generics.ListCreateAPIView):
|
|||||||
kwargs['pretty'] = str2bool(self.request.query_params.get('pretty', None))
|
kwargs['pretty'] = str2bool(self.request.query_params.get('pretty', None))
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
kwargs['context'] = self.get_serializer_context()
|
kwargs['context'] = self.get_serializer_context()
|
||||||
|
|
||||||
return self.serializer_class(*args, **kwargs)
|
return self.serializer_class(*args, **kwargs)
|
||||||
|
@ -24,7 +24,7 @@
|
|||||||
name: A customer
|
name: A customer
|
||||||
description: A company that we sell things to!
|
description: A company that we sell things to!
|
||||||
is_customer: True
|
is_customer: True
|
||||||
|
|
||||||
- model: company.company
|
- model: company.company
|
||||||
pk: 5
|
pk: 5
|
||||||
fields:
|
fields:
|
||||||
|
@ -158,7 +158,7 @@ class EditSupplierPartForm(HelperForm):
|
|||||||
empty_choice = [('', '----------')]
|
empty_choice = [('', '----------')]
|
||||||
|
|
||||||
manufacturers = [(manufacturer.id, manufacturer.name) for manufacturer in Company.objects.filter(is_manufacturer=True)]
|
manufacturers = [(manufacturer.id, manufacturer.name) for manufacturer in Company.objects.filter(is_manufacturer=True)]
|
||||||
|
|
||||||
return empty_choice + manufacturers
|
return empty_choice + manufacturers
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
@ -6,8 +6,6 @@ Company database model definitions
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import decimal
|
|
||||||
import math
|
|
||||||
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.core.validators import MinValueValidator
|
from django.core.validators import MinValueValidator
|
||||||
@ -26,7 +24,6 @@ from markdownx.models import MarkdownxField
|
|||||||
from stdimage.models import StdImageField
|
from stdimage.models import StdImageField
|
||||||
|
|
||||||
from InvenTree.helpers import getMediaUrl, getBlankImage, getBlankThumbnail
|
from InvenTree.helpers import getMediaUrl, getBlankImage, getBlankThumbnail
|
||||||
from InvenTree.helpers import normalize
|
|
||||||
from InvenTree.fields import InvenTreeURLField
|
from InvenTree.fields import InvenTreeURLField
|
||||||
from InvenTree.status_codes import PurchaseOrderStatus
|
from InvenTree.status_codes import PurchaseOrderStatus
|
||||||
|
|
||||||
@ -152,7 +149,7 @@ class Company(models.Model):
|
|||||||
def currency_code(self):
|
def currency_code(self):
|
||||||
"""
|
"""
|
||||||
Return the currency code associated with this company.
|
Return the currency code associated with this company.
|
||||||
|
|
||||||
- If the currency code is invalid, use the default currency
|
- If the currency code is invalid, use the default currency
|
||||||
- If the currency code is not specified, use the default currency
|
- If the currency code is not specified, use the default currency
|
||||||
"""
|
"""
|
||||||
@ -187,7 +184,7 @@ class Company(models.Model):
|
|||||||
return getMediaUrl(self.image.thumbnail.url)
|
return getMediaUrl(self.image.thumbnail.url)
|
||||||
else:
|
else:
|
||||||
return getBlankThumbnail()
|
return getBlankThumbnail()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def manufactured_part_count(self):
|
def manufactured_part_count(self):
|
||||||
""" The number of parts manufactured by this company """
|
""" The number of parts manufactured by this company """
|
||||||
@ -302,7 +299,7 @@ class ManufacturerPart(models.Model):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = ('part', 'manufacturer', 'MPN')
|
unique_together = ('part', 'manufacturer', 'MPN')
|
||||||
|
|
||||||
part = models.ForeignKey('part.Part', on_delete=models.CASCADE,
|
part = models.ForeignKey('part.Part', on_delete=models.CASCADE,
|
||||||
related_name='manufacturer_parts',
|
related_name='manufacturer_parts',
|
||||||
verbose_name=_('Base Part'),
|
verbose_name=_('Base Part'),
|
||||||
@ -311,7 +308,7 @@ class ManufacturerPart(models.Model):
|
|||||||
},
|
},
|
||||||
help_text=_('Select part'),
|
help_text=_('Select part'),
|
||||||
)
|
)
|
||||||
|
|
||||||
manufacturer = models.ForeignKey(
|
manufacturer = models.ForeignKey(
|
||||||
Company,
|
Company,
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
@ -359,7 +356,7 @@ class ManufacturerPart(models.Model):
|
|||||||
if not manufacturer_part:
|
if not manufacturer_part:
|
||||||
manufacturer_part = ManufacturerPart(part=part, manufacturer=manufacturer, MPN=mpn, description=description, link=link)
|
manufacturer_part = ManufacturerPart(part=part, manufacturer=manufacturer, MPN=mpn, description=description, link=link)
|
||||||
manufacturer_part.save()
|
manufacturer_part.save()
|
||||||
|
|
||||||
return manufacturer_part
|
return manufacturer_part
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
@ -414,7 +411,7 @@ class SupplierPart(models.Model):
|
|||||||
MPN = kwargs.pop('MPN')
|
MPN = kwargs.pop('MPN')
|
||||||
else:
|
else:
|
||||||
MPN = None
|
MPN = None
|
||||||
|
|
||||||
if manufacturer or MPN:
|
if manufacturer or MPN:
|
||||||
if not self.manufacturer_part:
|
if not self.manufacturer_part:
|
||||||
# Create ManufacturerPart
|
# Create ManufacturerPart
|
||||||
@ -429,7 +426,7 @@ class SupplierPart(models.Model):
|
|||||||
manufacturer_part_id = self.manufacturer_part.id
|
manufacturer_part_id = self.manufacturer_part.id
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
manufacturer_part_id = None
|
manufacturer_part_id = None
|
||||||
|
|
||||||
if manufacturer_part_id:
|
if manufacturer_part_id:
|
||||||
try:
|
try:
|
||||||
(manufacturer_part, created) = ManufacturerPart.objects.update_or_create(part=self.part,
|
(manufacturer_part, created) = ManufacturerPart.objects.update_or_create(part=self.part,
|
||||||
@ -504,7 +501,7 @@ class SupplierPart(models.Model):
|
|||||||
base_cost = models.DecimalField(max_digits=10, decimal_places=3, default=0, validators=[MinValueValidator(0)], verbose_name=_('base cost'), help_text=_('Minimum charge (e.g. stocking fee)'))
|
base_cost = models.DecimalField(max_digits=10, decimal_places=3, default=0, validators=[MinValueValidator(0)], verbose_name=_('base cost'), help_text=_('Minimum charge (e.g. stocking fee)'))
|
||||||
|
|
||||||
packaging = models.CharField(max_length=50, blank=True, null=True, verbose_name=_('Packaging'), help_text=_('Part packaging'))
|
packaging = models.CharField(max_length=50, blank=True, null=True, verbose_name=_('Packaging'), help_text=_('Part packaging'))
|
||||||
|
|
||||||
multiple = models.PositiveIntegerField(default=1, validators=[MinValueValidator(1)], verbose_name=_('multiple'), help_text=_('Order multiple'))
|
multiple = models.PositiveIntegerField(default=1, validators=[MinValueValidator(1)], verbose_name=_('multiple'), help_text=_('Order multiple'))
|
||||||
|
|
||||||
# TODO - Reimplement lead-time as a charfield with special validation (pattern matching).
|
# TODO - Reimplement lead-time as a charfield with special validation (pattern matching).
|
||||||
@ -558,70 +555,7 @@ class SupplierPart(models.Model):
|
|||||||
price=price
|
price=price
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_price(self, quantity, moq=True, multiples=True, currency=None):
|
get_price = common.models.get_price
|
||||||
""" Calculate the supplier price based on quantity price breaks.
|
|
||||||
|
|
||||||
- Don't forget to add in flat-fee cost (base_cost field)
|
|
||||||
- If MOQ (minimum order quantity) is required, bump quantity
|
|
||||||
- If order multiples are to be observed, then we need to calculate based on that, too
|
|
||||||
"""
|
|
||||||
|
|
||||||
price_breaks = self.price_breaks.all()
|
|
||||||
|
|
||||||
# No price break information available?
|
|
||||||
if len(price_breaks) == 0:
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Check if quantity is fraction and disable multiples
|
|
||||||
multiples = (quantity % 1 == 0)
|
|
||||||
|
|
||||||
# Order multiples
|
|
||||||
if multiples:
|
|
||||||
quantity = int(math.ceil(quantity / self.multiple) * self.multiple)
|
|
||||||
|
|
||||||
pb_found = False
|
|
||||||
pb_quantity = -1
|
|
||||||
pb_cost = 0.0
|
|
||||||
|
|
||||||
if currency is None:
|
|
||||||
# Default currency selection
|
|
||||||
currency = common.models.InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY')
|
|
||||||
|
|
||||||
pb_min = None
|
|
||||||
for pb in self.price_breaks.all():
|
|
||||||
# Store smallest price break
|
|
||||||
if not pb_min:
|
|
||||||
pb_min = pb
|
|
||||||
|
|
||||||
# Ignore this pricebreak (quantity is too high)
|
|
||||||
if pb.quantity > quantity:
|
|
||||||
continue
|
|
||||||
|
|
||||||
pb_found = True
|
|
||||||
|
|
||||||
# If this price-break quantity is the largest so far, use it!
|
|
||||||
if pb.quantity > pb_quantity:
|
|
||||||
pb_quantity = pb.quantity
|
|
||||||
|
|
||||||
# Convert everything to the selected currency
|
|
||||||
pb_cost = pb.convert_to(currency)
|
|
||||||
|
|
||||||
# Use smallest price break
|
|
||||||
if not pb_found and pb_min:
|
|
||||||
# Update price break information
|
|
||||||
pb_quantity = pb_min.quantity
|
|
||||||
pb_cost = pb_min.convert_to(currency)
|
|
||||||
# Trigger cost calculation using smallest price break
|
|
||||||
pb_found = True
|
|
||||||
|
|
||||||
# Convert quantity to decimal.Decimal format
|
|
||||||
quantity = decimal.Decimal(f'{quantity}')
|
|
||||||
|
|
||||||
if pb_found:
|
|
||||||
cost = pb_cost * quantity
|
|
||||||
return normalize(cost + self.base_cost)
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def open_orders(self):
|
def open_orders(self):
|
||||||
""" Return a database query for PO line items for this SupplierPart,
|
""" Return a database query for PO line items for this SupplierPart,
|
||||||
@ -669,7 +603,7 @@ class SupplierPart(models.Model):
|
|||||||
|
|
||||||
if self.manufacturer_string:
|
if self.manufacturer_string:
|
||||||
s = s + ' | ' + self.manufacturer_string
|
s = s + ' | ' + self.manufacturer_string
|
||||||
|
|
||||||
return s
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
@ -51,7 +51,7 @@ class CompanySerializer(InvenTreeModelSerializer):
|
|||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
url = serializers.CharField(source='get_absolute_url', read_only=True)
|
url = serializers.CharField(source='get_absolute_url', read_only=True)
|
||||||
|
|
||||||
image = serializers.CharField(source='get_thumbnail_url', read_only=True)
|
image = serializers.CharField(source='get_thumbnail_url', read_only=True)
|
||||||
|
|
||||||
parts_supplied = serializers.IntegerField(read_only=True)
|
parts_supplied = serializers.IntegerField(read_only=True)
|
||||||
@ -157,9 +157,9 @@ class SupplierPartSerializer(InvenTreeModelSerializer):
|
|||||||
self.fields.pop('pretty_name')
|
self.fields.pop('pretty_name')
|
||||||
|
|
||||||
supplier = serializers.PrimaryKeyRelatedField(queryset=Company.objects.filter(is_supplier=True))
|
supplier = serializers.PrimaryKeyRelatedField(queryset=Company.objects.filter(is_supplier=True))
|
||||||
|
|
||||||
manufacturer = serializers.PrimaryKeyRelatedField(source='manufacturer_part.manufacturer', read_only=True)
|
manufacturer = serializers.PrimaryKeyRelatedField(source='manufacturer_part.manufacturer', read_only=True)
|
||||||
|
|
||||||
MPN = serializers.StringRelatedField(source='manufacturer_part.MPN')
|
MPN = serializers.StringRelatedField(source='manufacturer_part.MPN')
|
||||||
|
|
||||||
manufacturer_part = ManufacturerPartSerializer(read_only=True)
|
manufacturer_part = ManufacturerPartSerializer(read_only=True)
|
||||||
|
@ -75,5 +75,5 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block js_ready %}
|
{% block js_ready %}
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
@ -6,7 +6,7 @@
|
|||||||
{% trans "Are you sure you want to delete the following Manufacturer Parts?" %}
|
{% trans "Are you sure you want to delete the following Manufacturer Parts?" %}
|
||||||
</div>
|
</div>
|
||||||
{% for part in parts %}
|
{% for part in parts %}
|
||||||
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@ -17,7 +17,7 @@
|
|||||||
<table class='table table-striped table-condensed'>
|
<table class='table table-striped table-condensed'>
|
||||||
<tr>
|
<tr>
|
||||||
<input type='hidden' name='manufacturer-part-{{ part.id}}' value='manufacturer-part-{{ part.id }}'/>
|
<input type='hidden' name='manufacturer-part-{{ part.id}}' value='manufacturer-part-{{ part.id }}'/>
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
{% include "hover_image.html" with image=part.part.image %}
|
{% include "hover_image.html" with image=part.part.image %}
|
||||||
{{ part.part.full_name }}
|
{{ part.part.full_name }}
|
||||||
|
@ -33,6 +33,6 @@
|
|||||||
|
|
||||||
{% block js_ready %}
|
{% block js_ready %}
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
@ -53,7 +53,7 @@ $('#supplier-create').click(function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
$("#supplier-part-delete").click(function() {
|
$("#supplier-part-delete").click(function() {
|
||||||
|
|
||||||
var selections = $("#supplier-table").bootstrapTable("getSelections");
|
var selections = $("#supplier-table").bootstrapTable("getSelections");
|
||||||
|
|
||||||
var parts = [];
|
var parts = [];
|
||||||
|
@ -24,7 +24,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if company.is_supplier or company.is_manufacturer %}
|
{% if company.is_supplier or company.is_manufacturer %}
|
||||||
<li class='list-group-item {% if tab == "supplier_parts" %}active{% endif %}' title='{% trans "Supplied Parts" %}'>
|
<li class='list-group-item {% if tab == "supplier_parts" %}active{% endif %}' title='{% trans "Supplied Parts" %}'>
|
||||||
<a href='{% url "company-detail-supplier-parts" company.id %}'>
|
<a href='{% url "company-detail-supplier-parts" company.id %}'>
|
||||||
|
@ -18,11 +18,11 @@
|
|||||||
{% if editing %}
|
{% if editing %}
|
||||||
<form method='POST'>
|
<form method='POST'>
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
||||||
{{ form }}
|
{{ form }}
|
||||||
<hr>
|
<hr>
|
||||||
<button type="submit" class='btn btn-default'>{% trans "Save" %}</button>
|
<button type="submit" class='btn btn-default'>{% trans "Save" %}</button>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{{ form.media }}
|
{{ form.media }}
|
||||||
@ -43,5 +43,5 @@ $("#edit-notes").click(function() {
|
|||||||
location.href = "{% url 'company-notes' company.id %}?edit=1";
|
location.href = "{% url 'company-notes' company.id %}?edit=1";
|
||||||
});
|
});
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
@ -12,7 +12,7 @@
|
|||||||
{% for part in parts %}
|
{% for part in parts %}
|
||||||
<tr>
|
<tr>
|
||||||
<input type='hidden' name='supplier-part-{{ part.id}}' value='supplier-part-{{ part.id }}'/>
|
<input type='hidden' name='supplier-part-{{ part.id}}' value='supplier-part-{{ part.id }}'/>
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
{% include "hover_image.html" with image=part.part.image %}
|
{% include "hover_image.html" with image=part.part.image %}
|
||||||
{{ part.part.full_name }}
|
{{ part.part.full_name }}
|
||||||
|
@ -43,6 +43,6 @@
|
|||||||
|
|
||||||
{% block js_ready %}
|
{% block js_ready %}
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
@ -19,7 +19,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<table class='table table-striped table-condensed po-table' id='purchase-order-table' data-toolbar='#button-bar'>
|
<table class='table table-striped table-condensed po-table' id='purchase-order-table' data-toolbar='#button-bar'>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
@ -90,7 +90,7 @@ $('#price-break-table').inventreeTable({
|
|||||||
html += makeIconButton('fa-trash-alt icon-red', 'button-price-break-delete', row.pk, '{% trans "Delete price break" %}');
|
html += makeIconButton('fa-trash-alt icon-red', 'button-price-break-delete', row.pk, '{% trans "Delete price break" %}');
|
||||||
|
|
||||||
html += `</div>`;
|
html += `</div>`;
|
||||||
|
|
||||||
return html;
|
return html;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -48,7 +48,7 @@ class TestManufacturerField(MigratorTestCase):
|
|||||||
- Company object (supplier)
|
- Company object (supplier)
|
||||||
- SupplierPart object
|
- SupplierPart object
|
||||||
"""
|
"""
|
||||||
|
|
||||||
Part = self.old_state.apps.get_model('part', 'part')
|
Part = self.old_state.apps.get_model('part', 'part')
|
||||||
Company = self.old_state.apps.get_model('company', 'company')
|
Company = self.old_state.apps.get_model('company', 'company')
|
||||||
SupplierPart = self.old_state.apps.get_model('company', 'supplierpart')
|
SupplierPart = self.old_state.apps.get_model('company', 'supplierpart')
|
||||||
@ -123,7 +123,7 @@ class TestManufacturerPart(MigratorTestCase):
|
|||||||
- Company object (supplier)
|
- Company object (supplier)
|
||||||
- SupplierPart object
|
- SupplierPart object
|
||||||
"""
|
"""
|
||||||
|
|
||||||
Part = self.old_state.apps.get_model('part', 'part')
|
Part = self.old_state.apps.get_model('part', 'part')
|
||||||
Company = self.old_state.apps.get_model('company', 'company')
|
Company = self.old_state.apps.get_model('company', 'company')
|
||||||
SupplierPart = self.old_state.apps.get_model('company', 'supplierpart')
|
SupplierPart = self.old_state.apps.get_model('company', 'supplierpart')
|
||||||
@ -220,7 +220,7 @@ class TestManufacturerPart(MigratorTestCase):
|
|||||||
|
|
||||||
# Check on the SupplierPart objects
|
# Check on the SupplierPart objects
|
||||||
SupplierPart = self.new_state.apps.get_model('company', 'supplierpart')
|
SupplierPart = self.new_state.apps.get_model('company', 'supplierpart')
|
||||||
|
|
||||||
supplier_parts = SupplierPart.objects.all()
|
supplier_parts = SupplierPart.objects.all()
|
||||||
self.assertEqual(supplier_parts.count(), 6)
|
self.assertEqual(supplier_parts.count(), 6)
|
||||||
|
|
||||||
@ -229,10 +229,10 @@ class TestManufacturerPart(MigratorTestCase):
|
|||||||
|
|
||||||
# Check on the ManufacturerPart objects
|
# Check on the ManufacturerPart objects
|
||||||
ManufacturerPart = self.new_state.apps.get_model('company', 'manufacturerpart')
|
ManufacturerPart = self.new_state.apps.get_model('company', 'manufacturerpart')
|
||||||
|
|
||||||
manufacturer_parts = ManufacturerPart.objects.all()
|
manufacturer_parts = ManufacturerPart.objects.all()
|
||||||
self.assertEqual(manufacturer_parts.count(), 4)
|
self.assertEqual(manufacturer_parts.count(), 4)
|
||||||
|
|
||||||
manufacturer_part = manufacturer_parts.first()
|
manufacturer_part = manufacturer_parts.first()
|
||||||
self.assertEqual(manufacturer_part.MPN, 'MUR-CAP-123456')
|
self.assertEqual(manufacturer_part.MPN, 'MUR-CAP-123456')
|
||||||
|
|
||||||
@ -293,7 +293,7 @@ class TestCurrencyMigration(MigratorTestCase):
|
|||||||
self.assertIsNone(pb.price)
|
self.assertIsNone(pb.price)
|
||||||
|
|
||||||
def test_currency_migration(self):
|
def test_currency_migration(self):
|
||||||
|
|
||||||
PB = self.new_state.apps.get_model('company', 'supplierpricebreak')
|
PB = self.new_state.apps.get_model('company', 'supplierpricebreak')
|
||||||
|
|
||||||
for pb in PB.objects.all():
|
for pb in PB.objects.all():
|
||||||
|
@ -30,7 +30,7 @@ class CompanyViewTestBase(TestCase):
|
|||||||
|
|
||||||
# Create a user
|
# Create a user
|
||||||
user = get_user_model()
|
user = get_user_model()
|
||||||
|
|
||||||
self.user = user.objects.create_user(
|
self.user = user.objects.create_user(
|
||||||
username='username',
|
username='username',
|
||||||
email='user@email.com',
|
email='user@email.com',
|
||||||
@ -83,7 +83,7 @@ class SupplierPartViewTests(CompanyViewTestBase):
|
|||||||
def test_supplier_part_create(self):
|
def test_supplier_part_create(self):
|
||||||
"""
|
"""
|
||||||
Test the SupplierPartCreate view.
|
Test the SupplierPartCreate view.
|
||||||
|
|
||||||
This view allows some additional functionality,
|
This view allows some additional functionality,
|
||||||
specifically it allows the user to create a single-quantity price break
|
specifically it allows the user to create a single-quantity price break
|
||||||
automatically, when saving the new SupplierPart model.
|
automatically, when saving the new SupplierPart model.
|
||||||
@ -171,7 +171,7 @@ class SupplierPartViewTests(CompanyViewTestBase):
|
|||||||
'confirm_delete': True
|
'confirm_delete': True
|
||||||
},
|
},
|
||||||
HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
self.assertEqual(n - 2, SupplierPart.objects.count())
|
self.assertEqual(n - 2, SupplierPart.objects.count())
|
||||||
@ -213,7 +213,7 @@ class ManufacturerPartViewTests(CompanyViewTestBase):
|
|||||||
"""
|
"""
|
||||||
Test the ManufacturerPartCreate view.
|
Test the ManufacturerPartCreate view.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
url = reverse('manufacturer-part-create')
|
url = reverse('manufacturer-part-create')
|
||||||
|
|
||||||
# First check that we can GET the form
|
# First check that we can GET the form
|
||||||
@ -252,7 +252,7 @@ class ManufacturerPartViewTests(CompanyViewTestBase):
|
|||||||
"""
|
"""
|
||||||
Test that the SupplierPartCreate view creates Manufacturer Part.
|
Test that the SupplierPartCreate view creates Manufacturer Part.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
url = reverse('supplier-part-create')
|
url = reverse('supplier-part-create')
|
||||||
|
|
||||||
# First check that we can GET the form
|
# First check that we can GET the form
|
||||||
@ -297,7 +297,7 @@ class ManufacturerPartViewTests(CompanyViewTestBase):
|
|||||||
'confirm_delete': True
|
'confirm_delete': True
|
||||||
},
|
},
|
||||||
HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
# Check that the ManufacturerPart was deleted
|
# Check that the ManufacturerPart was deleted
|
||||||
|
@ -71,7 +71,7 @@ class CompanySimpleTest(TestCase):
|
|||||||
acme = Company.objects.get(pk=1)
|
acme = Company.objects.get(pk=1)
|
||||||
appel = Company.objects.get(pk=2)
|
appel = Company.objects.get(pk=2)
|
||||||
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.supplied_part_count, 4)
|
self.assertEqual(acme.supplied_part_count, 4)
|
||||||
|
|
||||||
@ -82,7 +82,7 @@ class CompanySimpleTest(TestCase):
|
|||||||
self.assertEqual(zerg.supplied_part_count, 2)
|
self.assertEqual(zerg.supplied_part_count, 2)
|
||||||
|
|
||||||
def test_price_breaks(self):
|
def test_price_breaks(self):
|
||||||
|
|
||||||
self.assertTrue(self.acme0001.has_price_breaks)
|
self.assertTrue(self.acme0001.has_price_breaks)
|
||||||
self.assertTrue(self.acme0002.has_price_breaks)
|
self.assertTrue(self.acme0002.has_price_breaks)
|
||||||
self.assertTrue(self.zergm312.has_price_breaks)
|
self.assertTrue(self.zergm312.has_price_breaks)
|
||||||
@ -121,7 +121,7 @@ class CompanySimpleTest(TestCase):
|
|||||||
pmin, pmax = m2x4.get_price_range(5)
|
pmin, pmax = m2x4.get_price_range(5)
|
||||||
self.assertEqual(pmin, 35)
|
self.assertEqual(pmin, 35)
|
||||||
self.assertEqual(pmax, 37.5)
|
self.assertEqual(pmax, 37.5)
|
||||||
|
|
||||||
m3x12 = Part.objects.get(name='M3x12 SHCS')
|
m3x12 = Part.objects.get(name='M3x12 SHCS')
|
||||||
|
|
||||||
self.assertEqual(m3x12.get_price_info(0.3), Decimal('2.4'))
|
self.assertEqual(m3x12.get_price_info(0.3), Decimal('2.4'))
|
||||||
@ -187,14 +187,14 @@ class ManufacturerPartSimpleTest(TestCase):
|
|||||||
# Create a manufacturer part
|
# Create a manufacturer part
|
||||||
self.part = Part.objects.get(pk=1)
|
self.part = Part.objects.get(pk=1)
|
||||||
manufacturer = Company.objects.get(pk=1)
|
manufacturer = Company.objects.get(pk=1)
|
||||||
|
|
||||||
self.mp = ManufacturerPart.create(
|
self.mp = ManufacturerPart.create(
|
||||||
part=self.part,
|
part=self.part,
|
||||||
manufacturer=manufacturer,
|
manufacturer=manufacturer,
|
||||||
mpn='PART_NUMBER',
|
mpn='PART_NUMBER',
|
||||||
description='THIS IS A MANUFACTURER PART',
|
description='THIS IS A MANUFACTURER PART',
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create a supplier part
|
# Create a supplier part
|
||||||
supplier = Company.objects.get(pk=5)
|
supplier = Company.objects.get(pk=5)
|
||||||
supplier_part = SupplierPart.objects.create(
|
supplier_part = SupplierPart.objects.create(
|
||||||
|
@ -55,7 +55,7 @@ price_break_urls = [
|
|||||||
|
|
||||||
manufacturer_part_detail_urls = [
|
manufacturer_part_detail_urls = [
|
||||||
url(r'^edit/?', views.ManufacturerPartEdit.as_view(), name='manufacturer-part-edit'),
|
url(r'^edit/?', views.ManufacturerPartEdit.as_view(), name='manufacturer-part-edit'),
|
||||||
|
|
||||||
url(r'^suppliers/', views.ManufacturerPartDetail.as_view(template_name='company/manufacturer_part_suppliers.html'), name='manufacturer-part-suppliers'),
|
url(r'^suppliers/', views.ManufacturerPartDetail.as_view(template_name='company/manufacturer_part_suppliers.html'), name='manufacturer-part-suppliers'),
|
||||||
|
|
||||||
url('^.*$', views.ManufacturerPartDetail.as_view(template_name='company/manufacturer_part_suppliers.html'), name='manufacturer-part-detail'),
|
url('^.*$', views.ManufacturerPartDetail.as_view(template_name='company/manufacturer_part_suppliers.html'), name='manufacturer-part-detail'),
|
||||||
|
@ -96,7 +96,7 @@ class CompanyIndex(InvenTreeRoleMixin, ListView):
|
|||||||
if self.request.path == item:
|
if self.request.path == item:
|
||||||
context = lookup[item]
|
context = lookup[item]
|
||||||
break
|
break
|
||||||
|
|
||||||
if context is None:
|
if context is None:
|
||||||
context = default
|
context = default
|
||||||
|
|
||||||
@ -202,7 +202,7 @@ class CompanyImageDownloadFromURL(AjaxUpdateView):
|
|||||||
|
|
||||||
# Check for valid response code
|
# Check for valid response code
|
||||||
if not response.status_code == 200:
|
if not response.status_code == 200:
|
||||||
form.add_error('url', f"{_('Invalid response')}: {response.status_code}")
|
form.add_error('url', _('Invalid response: {code}').format(code=response.status_code))
|
||||||
return
|
return
|
||||||
|
|
||||||
response.raw.decode_content = True
|
response.raw.decode_content = True
|
||||||
@ -279,7 +279,7 @@ class CompanyCreate(AjaxCreateView):
|
|||||||
|
|
||||||
if url == reverse('supplier-create'):
|
if url == reverse('supplier-create'):
|
||||||
return _("Create new Supplier")
|
return _("Create new Supplier")
|
||||||
|
|
||||||
if url == reverse('manufacturer-create'):
|
if url == reverse('manufacturer-create'):
|
||||||
return _('Create new Manufacturer')
|
return _('Create new Manufacturer')
|
||||||
|
|
||||||
@ -298,7 +298,7 @@ class CompanyCreate(AjaxCreateView):
|
|||||||
initials['is_supplier'] = True
|
initials['is_supplier'] = True
|
||||||
initials['is_customer'] = False
|
initials['is_customer'] = False
|
||||||
initials['is_manufacturer'] = False
|
initials['is_manufacturer'] = False
|
||||||
|
|
||||||
elif url == reverse('manufacturer-create'):
|
elif url == reverse('manufacturer-create'):
|
||||||
initials['is_manufacturer'] = True
|
initials['is_manufacturer'] = True
|
||||||
initials['is_supplier'] = True
|
initials['is_supplier'] = True
|
||||||
@ -319,7 +319,7 @@ class CompanyCreate(AjaxCreateView):
|
|||||||
|
|
||||||
class CompanyDelete(AjaxDeleteView):
|
class CompanyDelete(AjaxDeleteView):
|
||||||
""" View for deleting a Company object """
|
""" View for deleting a Company object """
|
||||||
|
|
||||||
model = Company
|
model = Company
|
||||||
success_url = '/company/'
|
success_url = '/company/'
|
||||||
ajax_template_name = 'company/delete.html'
|
ajax_template_name = 'company/delete.html'
|
||||||
@ -415,7 +415,7 @@ class ManufacturerPartCreate(AjaxCreateView):
|
|||||||
initials['manufacturer'] = Company.objects.get(pk=manufacturer_id)
|
initials['manufacturer'] = Company.objects.get(pk=manufacturer_id)
|
||||||
except (ValueError, Company.DoesNotExist):
|
except (ValueError, Company.DoesNotExist):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if part_id:
|
if part_id:
|
||||||
try:
|
try:
|
||||||
initials['part'] = Part.objects.get(pk=part_id)
|
initials['part'] = Part.objects.get(pk=part_id)
|
||||||
@ -427,7 +427,7 @@ class ManufacturerPartCreate(AjaxCreateView):
|
|||||||
|
|
||||||
class ManufacturerPartDelete(AjaxDeleteView):
|
class ManufacturerPartDelete(AjaxDeleteView):
|
||||||
""" Delete view for removing a ManufacturerPart.
|
""" Delete view for removing a ManufacturerPart.
|
||||||
|
|
||||||
ManufacturerParts can be deleted using a variety of 'selectors'.
|
ManufacturerParts can be deleted using a variety of 'selectors'.
|
||||||
|
|
||||||
- ?part=<pk> -> Delete a single ManufacturerPart object
|
- ?part=<pk> -> Delete a single ManufacturerPart object
|
||||||
@ -561,7 +561,7 @@ class SupplierPartEdit(AjaxUpdateView):
|
|||||||
initials = super(SupplierPartEdit, self).get_initial().copy()
|
initials = super(SupplierPartEdit, self).get_initial().copy()
|
||||||
|
|
||||||
supplier_part = self.get_object()
|
supplier_part = self.get_object()
|
||||||
|
|
||||||
if supplier_part.manufacturer_part:
|
if supplier_part.manufacturer_part:
|
||||||
initials['manufacturer'] = supplier_part.manufacturer_part.manufacturer.id
|
initials['manufacturer'] = supplier_part.manufacturer_part.manufacturer.id
|
||||||
initials['MPN'] = supplier_part.manufacturer_part.MPN
|
initials['MPN'] = supplier_part.manufacturer_part.MPN
|
||||||
@ -686,7 +686,7 @@ class SupplierPartCreate(AjaxCreateView):
|
|||||||
initials['MPN'] = manufacturer_part_obj.MPN
|
initials['MPN'] = manufacturer_part_obj.MPN
|
||||||
except (ValueError, ManufacturerPart.DoesNotExist, Part.DoesNotExist, Company.DoesNotExist):
|
except (ValueError, ManufacturerPart.DoesNotExist, Part.DoesNotExist, Company.DoesNotExist):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if part_id:
|
if part_id:
|
||||||
try:
|
try:
|
||||||
initials['part'] = Part.objects.get(pk=part_id)
|
initials['part'] = Part.objects.get(pk=part_id)
|
||||||
@ -703,13 +703,13 @@ class SupplierPartCreate(AjaxCreateView):
|
|||||||
|
|
||||||
if currency_code:
|
if currency_code:
|
||||||
initials['single_pricing'] = ('', currency)
|
initials['single_pricing'] = ('', currency)
|
||||||
|
|
||||||
return initials
|
return initials
|
||||||
|
|
||||||
|
|
||||||
class SupplierPartDelete(AjaxDeleteView):
|
class SupplierPartDelete(AjaxDeleteView):
|
||||||
""" Delete view for removing a SupplierPart.
|
""" Delete view for removing a SupplierPart.
|
||||||
|
|
||||||
SupplierParts can be deleted using a variety of 'selectors'.
|
SupplierParts can be deleted using a variety of 'selectors'.
|
||||||
|
|
||||||
- ?part=<pk> -> Delete a single SupplierPart object
|
- ?part=<pk> -> Delete a single SupplierPart object
|
||||||
@ -840,7 +840,7 @@ class PriceBreakCreate(AjaxCreateView):
|
|||||||
|
|
||||||
# Extract the currency object associated with the code
|
# Extract the currency object associated with the code
|
||||||
currency = CURRENCIES.get(currency_code, None)
|
currency = CURRENCIES.get(currency_code, None)
|
||||||
|
|
||||||
if currency:
|
if currency:
|
||||||
initials['price'] = [1.0, currency]
|
initials['price'] = [1.0, currency]
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ database:
|
|||||||
# or specify database options using environment variables
|
# or specify database options using environment variables
|
||||||
|
|
||||||
# Refer to the django documentation for full list of options
|
# Refer to the django documentation for full list of options
|
||||||
|
|
||||||
# --- Available options: ---
|
# --- Available options: ---
|
||||||
# ENGINE: Database engine. Selection from:
|
# ENGINE: Database engine. Selection from:
|
||||||
# - sqlite3
|
# - sqlite3
|
||||||
@ -114,7 +114,7 @@ allowed_hosts:
|
|||||||
cors:
|
cors:
|
||||||
# CORS_ORIGIN_ALLOW_ALL - If True, the whitelist will not be used and all origins will be accepted.
|
# CORS_ORIGIN_ALLOW_ALL - If True, the whitelist will not be used and all origins will be accepted.
|
||||||
allow_all: True
|
allow_all: True
|
||||||
|
|
||||||
# CORS_ORIGIN_WHITELIST - A list of origins that are authorized to make cross-site HTTP requests. Defaults to []
|
# CORS_ORIGIN_WHITELIST - A list of origins that are authorized to make cross-site HTTP requests. Defaults to []
|
||||||
# whitelist:
|
# whitelist:
|
||||||
# - https://example.com
|
# - https://example.com
|
||||||
|
@ -159,7 +159,7 @@ class StockItemLabelList(LabelListView, StockItemLabelMixin):
|
|||||||
"""
|
"""
|
||||||
Filter the StockItem label queryset.
|
Filter the StockItem label queryset.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
queryset = super().filter_queryset(queryset)
|
queryset = super().filter_queryset(queryset)
|
||||||
|
|
||||||
# List of StockItem objects to match against
|
# List of StockItem objects to match against
|
||||||
@ -178,7 +178,7 @@ class StockItemLabelList(LabelListView, StockItemLabelMixin):
|
|||||||
|
|
||||||
# Keep track of which labels match every specified stockitem
|
# Keep track of which labels match every specified stockitem
|
||||||
valid_label_ids = set()
|
valid_label_ids = set()
|
||||||
|
|
||||||
for label in queryset.all():
|
for label in queryset.all():
|
||||||
|
|
||||||
matches = True
|
matches = True
|
||||||
@ -293,7 +293,7 @@ class StockLocationLabelList(LabelListView, StockLocationLabelMixin):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
queryset = super().filter_queryset(queryset)
|
queryset = super().filter_queryset(queryset)
|
||||||
|
|
||||||
# List of StockLocation objects to match against
|
# List of StockLocation objects to match against
|
||||||
locations = self.get_locations()
|
locations = self.get_locations()
|
||||||
|
|
||||||
|
@ -139,7 +139,7 @@ class LabelConfig(AppConfig):
|
|||||||
except:
|
except:
|
||||||
# Database might not yet be ready
|
# Database might not yet be ready
|
||||||
return
|
return
|
||||||
|
|
||||||
src_dir = os.path.join(
|
src_dir = os.path.join(
|
||||||
os.path.dirname(os.path.realpath(__file__)),
|
os.path.dirname(os.path.realpath(__file__)),
|
||||||
'templates',
|
'templates',
|
||||||
|
@ -44,7 +44,7 @@ def rename_label(instance, filename):
|
|||||||
|
|
||||||
|
|
||||||
def validate_stock_item_filters(filters):
|
def validate_stock_item_filters(filters):
|
||||||
|
|
||||||
filters = validateFilterString(filters, model=stock.models.StockItem)
|
filters = validateFilterString(filters, model=stock.models.StockItem)
|
||||||
|
|
||||||
return filters
|
return filters
|
||||||
@ -82,7 +82,7 @@ class LabelTemplate(models.Model):
|
|||||||
|
|
||||||
# Each class of label files will be stored in a separate subdirectory
|
# Each class of label files will be stored in a separate subdirectory
|
||||||
SUBDIR = "label"
|
SUBDIR = "label"
|
||||||
|
|
||||||
# Object we will be printing against (will be filled out later)
|
# Object we will be printing against (will be filled out later)
|
||||||
object_to_print = None
|
object_to_print = None
|
||||||
|
|
||||||
|
@ -40,7 +40,7 @@ class TestReportTests(InvenTreeAPITestCase):
|
|||||||
return response.data
|
return response.data
|
||||||
|
|
||||||
def test_list(self):
|
def test_list(self):
|
||||||
|
|
||||||
response = self.do_list()
|
response = self.do_list()
|
||||||
|
|
||||||
# TODO - Add some report templates to the fixtures
|
# TODO - Add some report templates to the fixtures
|
||||||
|
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@ -64,7 +64,7 @@ class CancelSalesOrderForm(HelperForm):
|
|||||||
fields = [
|
fields = [
|
||||||
'confirm',
|
'confirm',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class ShipSalesOrderForm(HelperForm):
|
class ShipSalesOrderForm(HelperForm):
|
||||||
|
|
||||||
@ -211,6 +211,7 @@ class EditSalesOrderLineItemForm(HelperForm):
|
|||||||
'part',
|
'part',
|
||||||
'quantity',
|
'quantity',
|
||||||
'reference',
|
'reference',
|
||||||
|
'sale_price',
|
||||||
'notes'
|
'notes'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
24
InvenTree/order/migrations/0045_auto_20210504_1946.py
Normal file
24
InvenTree/order/migrations/0045_auto_20210504_1946.py
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# Generated by Django 3.2 on 2021-05-04 19:46
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
import djmoney.models.fields
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('order', '0044_auto_20210404_2016'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='salesorderlineitem',
|
||||||
|
name='sale_price',
|
||||||
|
field=djmoney.models.fields.MoneyField(blank=True, decimal_places=4, default_currency='USD', help_text='Unit sale price', max_digits=19, null=True, verbose_name='Sale Price'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='salesorderlineitem',
|
||||||
|
name='sale_price_currency',
|
||||||
|
field=djmoney.models.fields.CurrencyField(choices=[('AUD', 'Australian Dollar'), ('GBP', 'British Pound'), ('CAD', 'Canadian Dollar'), ('EUR', 'Euro'), ('JPY', 'Japanese Yen'), ('NZD', 'New Zealand Dollar'), ('USD', 'US Dollar')], default='USD', editable=False, max_length=3),
|
||||||
|
),
|
||||||
|
]
|
@ -309,7 +309,7 @@ class PurchaseOrder(Order):
|
|||||||
"""
|
"""
|
||||||
A PurchaseOrder can only be cancelled under the following circumstances:
|
A PurchaseOrder can only be cancelled under the following circumstances:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return self.status in [
|
return self.status in [
|
||||||
PurchaseOrderStatus.PLACED,
|
PurchaseOrderStatus.PLACED,
|
||||||
PurchaseOrderStatus.PENDING
|
PurchaseOrderStatus.PENDING
|
||||||
@ -367,7 +367,7 @@ class PurchaseOrder(Order):
|
|||||||
stock.save()
|
stock.save()
|
||||||
|
|
||||||
text = _("Received items")
|
text = _("Received items")
|
||||||
note = f"{_('Received')} {quantity} {_('items against order')} {str(self)}"
|
note = _('Received {n} items against order {name}').format(n=quantity, name=str(self))
|
||||||
|
|
||||||
# Add a new transaction note to the newly created stock item
|
# Add a new transaction note to the newly created stock item
|
||||||
stock.addTransactionNote(text, user, note)
|
stock.addTransactionNote(text, user, note)
|
||||||
@ -378,7 +378,7 @@ class PurchaseOrder(Order):
|
|||||||
|
|
||||||
# Has this order been completed?
|
# Has this order been completed?
|
||||||
if len(self.pending_line_items()) == 0:
|
if len(self.pending_line_items()) == 0:
|
||||||
|
|
||||||
self.received_by = user
|
self.received_by = user
|
||||||
self.complete_order() # This will save the model
|
self.complete_order() # This will save the model
|
||||||
|
|
||||||
@ -419,7 +419,7 @@ class SalesOrder(Order):
|
|||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
# Date processing error, return queryset unchanged
|
# Date processing error, return queryset unchanged
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
# Construct a queryset for "completed" orders within the range
|
# Construct a queryset for "completed" orders within the range
|
||||||
completed = Q(status__in=SalesOrderStatus.COMPLETE) & Q(shipment_date__gte=min_date) & Q(shipment_date__lte=max_date)
|
completed = Q(status__in=SalesOrderStatus.COMPLETE) & Q(shipment_date__gte=min_date) & Q(shipment_date__lte=max_date)
|
||||||
|
|
||||||
@ -495,7 +495,7 @@ class SalesOrder(Order):
|
|||||||
for line in self.lines.all():
|
for line in self.lines.all():
|
||||||
if not line.is_fully_allocated():
|
if not line.is_fully_allocated():
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def is_over_allocated(self):
|
def is_over_allocated(self):
|
||||||
@ -590,11 +590,11 @@ class SalesOrderAttachment(InvenTreeAttachment):
|
|||||||
|
|
||||||
class OrderLineItem(models.Model):
|
class OrderLineItem(models.Model):
|
||||||
""" Abstract model for an order line item
|
""" Abstract model for an order line item
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
quantity: Number of items
|
quantity: Number of items
|
||||||
note: Annotation for the item
|
note: Annotation for the item
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -603,13 +603,13 @@ class OrderLineItem(models.Model):
|
|||||||
quantity = RoundingDecimalField(max_digits=15, decimal_places=5, validators=[MinValueValidator(0)], default=1, verbose_name=_('Quantity'), help_text=_('Item quantity'))
|
quantity = RoundingDecimalField(max_digits=15, decimal_places=5, validators=[MinValueValidator(0)], default=1, verbose_name=_('Quantity'), help_text=_('Item quantity'))
|
||||||
|
|
||||||
reference = models.CharField(max_length=100, blank=True, verbose_name=_('Reference'), help_text=_('Line item reference'))
|
reference = models.CharField(max_length=100, blank=True, verbose_name=_('Reference'), help_text=_('Line item reference'))
|
||||||
|
|
||||||
notes = models.CharField(max_length=500, blank=True, verbose_name=_('Notes'), help_text=_('Line item notes'))
|
notes = models.CharField(max_length=500, blank=True, verbose_name=_('Notes'), help_text=_('Line item notes'))
|
||||||
|
|
||||||
|
|
||||||
class PurchaseOrderLineItem(OrderLineItem):
|
class PurchaseOrderLineItem(OrderLineItem):
|
||||||
""" Model for a purchase order line item.
|
""" Model for a purchase order line item.
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
order: Reference to a PurchaseOrder object
|
order: Reference to a PurchaseOrder object
|
||||||
|
|
||||||
@ -637,7 +637,7 @@ class PurchaseOrderLineItem(OrderLineItem):
|
|||||||
def get_base_part(self):
|
def get_base_part(self):
|
||||||
""" Return the base-part for the line item """
|
""" Return the base-part for the line item """
|
||||||
return self.part.part
|
return self.part.part
|
||||||
|
|
||||||
# TODO - Function callback for when the SupplierPart is deleted?
|
# TODO - Function callback for when the SupplierPart is deleted?
|
||||||
|
|
||||||
part = models.ForeignKey(
|
part = models.ForeignKey(
|
||||||
@ -672,12 +672,22 @@ class SalesOrderLineItem(OrderLineItem):
|
|||||||
Attributes:
|
Attributes:
|
||||||
order: Link to the SalesOrder that this line item belongs to
|
order: Link to the SalesOrder that this line item belongs to
|
||||||
part: Link to a Part object (may be null)
|
part: Link to a Part object (may be null)
|
||||||
|
sale_price: The unit sale price for this OrderLineItem
|
||||||
"""
|
"""
|
||||||
|
|
||||||
order = models.ForeignKey(SalesOrder, on_delete=models.CASCADE, related_name='lines', verbose_name=_('Order'), help_text=_('Sales Order'))
|
order = models.ForeignKey(SalesOrder, on_delete=models.CASCADE, related_name='lines', verbose_name=_('Order'), help_text=_('Sales Order'))
|
||||||
|
|
||||||
part = models.ForeignKey('part.Part', on_delete=models.SET_NULL, related_name='sales_order_line_items', null=True, verbose_name=_('Part'), help_text=_('Part'), limit_choices_to={'salable': True})
|
part = models.ForeignKey('part.Part', on_delete=models.SET_NULL, related_name='sales_order_line_items', null=True, verbose_name=_('Part'), help_text=_('Part'), limit_choices_to={'salable': True})
|
||||||
|
|
||||||
|
sale_price = MoneyField(
|
||||||
|
max_digits=19,
|
||||||
|
decimal_places=4,
|
||||||
|
default_currency='USD',
|
||||||
|
null=True, blank=True,
|
||||||
|
verbose_name=_('Sale Price'),
|
||||||
|
help_text=_('Unit sale price'),
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = [
|
unique_together = [
|
||||||
]
|
]
|
||||||
|
@ -61,7 +61,7 @@ class POSerializer(InvenTreeModelSerializer):
|
|||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
supplier_detail = CompanyBriefSerializer(source='supplier', many=False, read_only=True)
|
supplier_detail = CompanyBriefSerializer(source='supplier', many=False, read_only=True)
|
||||||
|
|
||||||
line_items = serializers.IntegerField(read_only=True)
|
line_items = serializers.IntegerField(read_only=True)
|
||||||
|
|
||||||
status_text = serializers.CharField(source='get_status_display', read_only=True)
|
status_text = serializers.CharField(source='get_status_display', read_only=True)
|
||||||
@ -70,7 +70,7 @@ class POSerializer(InvenTreeModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = PurchaseOrder
|
model = PurchaseOrder
|
||||||
|
|
||||||
fields = [
|
fields = [
|
||||||
'pk',
|
'pk',
|
||||||
'issue_date',
|
'issue_date',
|
||||||
@ -89,7 +89,7 @@ class POSerializer(InvenTreeModelSerializer):
|
|||||||
'target_date',
|
'target_date',
|
||||||
'notes',
|
'notes',
|
||||||
]
|
]
|
||||||
|
|
||||||
read_only_fields = [
|
read_only_fields = [
|
||||||
'reference',
|
'reference',
|
||||||
'status'
|
'status'
|
||||||
@ -110,10 +110,10 @@ class POLineItemSerializer(InvenTreeModelSerializer):
|
|||||||
|
|
||||||
quantity = serializers.FloatField()
|
quantity = serializers.FloatField()
|
||||||
received = serializers.FloatField()
|
received = serializers.FloatField()
|
||||||
|
|
||||||
part_detail = PartBriefSerializer(source='get_base_part', many=False, read_only=True)
|
part_detail = PartBriefSerializer(source='get_base_part', many=False, read_only=True)
|
||||||
supplier_part_detail = SupplierPartSerializer(source='part', many=False, read_only=True)
|
supplier_part_detail = SupplierPartSerializer(source='part', many=False, read_only=True)
|
||||||
|
|
||||||
purchase_price_string = serializers.CharField(source='purchase_price', read_only=True)
|
purchase_price_string = serializers.CharField(source='purchase_price', read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -144,7 +144,7 @@ class POAttachmentSerializer(InvenTreeModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = PurchaseOrderAttachment
|
model = PurchaseOrderAttachment
|
||||||
|
|
||||||
fields = [
|
fields = [
|
||||||
'pk',
|
'pk',
|
||||||
'order',
|
'order',
|
||||||
@ -270,7 +270,7 @@ class SOLineItemSerializer(InvenTreeModelSerializer):
|
|||||||
|
|
||||||
if allocations is not True:
|
if allocations is not True:
|
||||||
self.fields.pop('allocations')
|
self.fields.pop('allocations')
|
||||||
|
|
||||||
order_detail = SalesOrderSerializer(source='order', many=False, read_only=True)
|
order_detail = SalesOrderSerializer(source='order', many=False, read_only=True)
|
||||||
part_detail = PartBriefSerializer(source='part', many=False, read_only=True)
|
part_detail = PartBriefSerializer(source='part', many=False, read_only=True)
|
||||||
allocations = SalesOrderAllocationSerializer(many=True, read_only=True)
|
allocations = SalesOrderAllocationSerializer(many=True, read_only=True)
|
||||||
@ -278,6 +278,7 @@ class SOLineItemSerializer(InvenTreeModelSerializer):
|
|||||||
quantity = serializers.FloatField()
|
quantity = serializers.FloatField()
|
||||||
allocated = serializers.FloatField(source='allocated_quantity', read_only=True)
|
allocated = serializers.FloatField(source='allocated_quantity', read_only=True)
|
||||||
fulfilled = serializers.FloatField(source='fulfilled_quantity', read_only=True)
|
fulfilled = serializers.FloatField(source='fulfilled_quantity', read_only=True)
|
||||||
|
sale_price_string = serializers.CharField(source='sale_price', read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = SalesOrderLineItem
|
model = SalesOrderLineItem
|
||||||
@ -294,6 +295,9 @@ class SOLineItemSerializer(InvenTreeModelSerializer):
|
|||||||
'order_detail',
|
'order_detail',
|
||||||
'part',
|
'part',
|
||||||
'part_detail',
|
'part_detail',
|
||||||
|
'sale_price',
|
||||||
|
'sale_price_currency',
|
||||||
|
'sale_price_string',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -306,7 +310,7 @@ class SOAttachmentSerializer(InvenTreeModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = SalesOrderAttachment
|
model = SalesOrderAttachment
|
||||||
|
|
||||||
fields = [
|
fields = [
|
||||||
'pk',
|
'pk',
|
||||||
'order',
|
'order',
|
||||||
|
@ -44,7 +44,7 @@ $("#new-attachment").click(function() {
|
|||||||
|
|
||||||
$("#attachment-table").on('click', '.attachment-edit-button', function() {
|
$("#attachment-table").on('click', '.attachment-edit-button', function() {
|
||||||
var button = $(this);
|
var button = $(this);
|
||||||
|
|
||||||
var url = `/order/purchase-order/attachment/${button.attr('pk')}/edit/`;
|
var url = `/order/purchase-order/attachment/${button.attr('pk')}/edit/`;
|
||||||
|
|
||||||
launchModalForm(url, {
|
launchModalForm(url, {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
{% extends "modal_delete_form.html" %}
|
{% extends "modal_delete_form.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
{% block pre_form_content %}
|
{% block pre_form_content %}
|
||||||
Are you sure you wish to delete this line item?
|
{% trans "Are you sure you wish to delete this line item?" %}
|
||||||
{% endblock %}
|
{% endblock %}
|
@ -193,11 +193,11 @@ $("#po-table").inventreeTable({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
sorter: function(valA, valB, rowA, rowB) {
|
sorter: function(valA, valB, rowA, rowB) {
|
||||||
|
|
||||||
if (rowA.received == 0 && rowB.received == 0) {
|
if (rowA.received == 0 && rowB.received == 0) {
|
||||||
return (rowA.quantity > rowB.quantity) ? 1 : -1;
|
return (rowA.quantity > rowB.quantity) ? 1 : -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
var progressA = parseFloat(rowA.received) / rowA.quantity;
|
var progressA = parseFloat(rowA.received) / rowA.quantity;
|
||||||
var progressB = parseFloat(rowB.received) / rowB.quantity;
|
var progressB = parseFloat(rowB.received) / rowB.quantity;
|
||||||
|
|
||||||
|
@ -83,7 +83,7 @@
|
|||||||
var title = `${prefix}${order.reference} - ${order.supplier_detail.name}`;
|
var title = `${prefix}${order.reference} - ${order.supplier_detail.name}`;
|
||||||
|
|
||||||
var color = '#4c68f5';
|
var color = '#4c68f5';
|
||||||
|
|
||||||
if (order.complete_date) {
|
if (order.complete_date) {
|
||||||
color = '#25c235';
|
color = '#25c235';
|
||||||
} else if (order.overdue) {
|
} else if (order.overdue) {
|
||||||
@ -143,16 +143,18 @@ $('#view-calendar').click(function() {
|
|||||||
$(".columns-right").hide();
|
$(".columns-right").hide();
|
||||||
$(".search").hide();
|
$(".search").hide();
|
||||||
$('#filter-list-salesorder').hide();
|
$('#filter-list-salesorder').hide();
|
||||||
|
|
||||||
$("#purchase-order-calendar").show();
|
$("#purchase-order-calendar").show();
|
||||||
$("#view-list").show();
|
$("#view-list").show();
|
||||||
|
|
||||||
|
calendar.render();
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#view-list").click(function() {
|
$("#view-list").click(function() {
|
||||||
// Hide the calendar view, show the list view
|
// Hide the calendar view, show the list view
|
||||||
$("#purchase-order-calendar").hide();
|
$("#purchase-order-calendar").hide();
|
||||||
$("#view-list").hide();
|
$("#view-list").hide();
|
||||||
|
|
||||||
$(".fixed-table-pagination").show();
|
$(".fixed-table-pagination").show();
|
||||||
$(".columns-right").show();
|
$(".columns-right").show();
|
||||||
$(".search").show();
|
$(".search").show();
|
||||||
|
@ -51,13 +51,13 @@ $("#new-so-line").click(function() {
|
|||||||
{% if order.status == SalesOrderStatus.PENDING %}
|
{% if order.status == SalesOrderStatus.PENDING %}
|
||||||
function showAllocationSubTable(index, row, element) {
|
function showAllocationSubTable(index, row, element) {
|
||||||
// Construct a table showing stock items which have been allocated against this line item
|
// Construct a table showing stock items which have been allocated against this line item
|
||||||
|
|
||||||
var html = `<div class='sub-table'><table class='table table-striped table-condensed' id='allocation-table-${row.pk}'></table></div>`;
|
var html = `<div class='sub-table'><table class='table table-striped table-condensed' id='allocation-table-${row.pk}'></table></div>`;
|
||||||
|
|
||||||
element.html(html);
|
element.html(html);
|
||||||
|
|
||||||
var lineItem = row;
|
var lineItem = row;
|
||||||
|
|
||||||
var table = $(`#allocation-table-${row.pk}`);
|
var table = $(`#allocation-table-${row.pk}`);
|
||||||
|
|
||||||
table.bootstrapTable({
|
table.bootstrapTable({
|
||||||
@ -70,7 +70,7 @@ function showAllocationSubTable(index, row, element) {
|
|||||||
title: '{% trans "Quantity" %}',
|
title: '{% trans "Quantity" %}',
|
||||||
formatter: function(value, row, index, field) {
|
formatter: function(value, row, index, field) {
|
||||||
var text = '';
|
var text = '';
|
||||||
|
|
||||||
if (row.serial != null && row.quantity == 1) {
|
if (row.serial != null && row.quantity == 1) {
|
||||||
text = `{% trans "Serial Number" %}: ${row.serial}`;
|
text = `{% trans "Serial Number" %}: ${row.serial}`;
|
||||||
} else {
|
} else {
|
||||||
@ -91,10 +91,10 @@ function showAllocationSubTable(index, row, element) {
|
|||||||
field: 'buttons',
|
field: 'buttons',
|
||||||
title: '{% trans "Actions" %}',
|
title: '{% trans "Actions" %}',
|
||||||
formatter: function(value, row, index, field) {
|
formatter: function(value, row, index, field) {
|
||||||
|
|
||||||
var html = "<div class='btn-group float-right' role='group'>";
|
var html = "<div class='btn-group float-right' role='group'>";
|
||||||
var pk = row.pk;
|
var pk = row.pk;
|
||||||
|
|
||||||
{% if order.status == SalesOrderStatus.PENDING %}
|
{% if order.status == SalesOrderStatus.PENDING %}
|
||||||
html += makeIconButton('fa-edit icon-blue', 'button-allocation-edit', pk, '{% trans "Edit stock allocation" %}');
|
html += makeIconButton('fa-edit icon-blue', 'button-allocation-edit', pk, '{% trans "Edit stock allocation" %}');
|
||||||
html += makeIconButton('fa-trash-alt icon-red', 'button-allocation-delete', pk, '{% trans "Delete stock allocation" %}');
|
html += makeIconButton('fa-trash-alt icon-red', 'button-allocation-delete', pk, '{% trans "Delete stock allocation" %}');
|
||||||
@ -223,6 +223,14 @@ $("#so-lines-table").inventreeTable({
|
|||||||
field: 'quantity',
|
field: 'quantity',
|
||||||
title: '{% trans "Quantity" %}',
|
title: '{% trans "Quantity" %}',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
sortable: true,
|
||||||
|
field: 'sale_price',
|
||||||
|
title: '{% trans "Unit Price" %}',
|
||||||
|
formatter: function(value, row) {
|
||||||
|
return row.sale_price_string || row.sale_price;
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
field: 'allocated',
|
field: 'allocated',
|
||||||
{% if order.status == SalesOrderStatus.PENDING %}
|
{% if order.status == SalesOrderStatus.PENDING %}
|
||||||
@ -248,11 +256,11 @@ $("#so-lines-table").inventreeTable({
|
|||||||
var A = rowA.fulfilled;
|
var A = rowA.fulfilled;
|
||||||
var B = rowB.fulfilled;
|
var B = rowB.fulfilled;
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
if (A == 0 && B == 0) {
|
if (A == 0 && B == 0) {
|
||||||
return (rowA.quantity > rowB.quantity) ? 1 : -1;
|
return (rowA.quantity > rowB.quantity) ? 1 : -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
var progressA = parseFloat(A) / rowA.quantity;
|
var progressA = parseFloat(A) / rowA.quantity;
|
||||||
var progressB = parseFloat(B) / rowB.quantity;
|
var progressB = parseFloat(B) / rowB.quantity;
|
||||||
|
|
||||||
@ -271,7 +279,7 @@ $("#so-lines-table").inventreeTable({
|
|||||||
var html = `<div class='btn-group float-right' role='group'>`;
|
var html = `<div class='btn-group float-right' role='group'>`;
|
||||||
|
|
||||||
var pk = row.pk;
|
var pk = row.pk;
|
||||||
|
|
||||||
if (row.part) {
|
if (row.part) {
|
||||||
var part = row.part_detail;
|
var part = row.part_detail;
|
||||||
|
|
||||||
@ -279,18 +287,19 @@ $("#so-lines-table").inventreeTable({
|
|||||||
html += makeIconButton('fa-hashtag icon-green', 'button-add-by-sn', pk, '{% trans "Allocate serial numbers" %}');
|
html += makeIconButton('fa-hashtag icon-green', 'button-add-by-sn', pk, '{% trans "Allocate serial numbers" %}');
|
||||||
}
|
}
|
||||||
|
|
||||||
html += makeIconButton('fa-sign-in-alt icon-green', 'button-add', pk, '{% trans "Allocate stock" %}');
|
html += makeIconButton('fa-sign-in-alt icon-green', 'button-add', pk, '{% trans "Allocate stock" %}');
|
||||||
|
|
||||||
if (part.purchaseable) {
|
if (part.purchaseable) {
|
||||||
html += makeIconButton('fa-shopping-cart', 'button-buy', row.part, '{% trans "Purchase stock" %}');
|
html += makeIconButton('fa-shopping-cart', 'button-buy', row.part, '{% trans "Purchase stock" %}');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (part.assembly) {
|
if (part.assembly) {
|
||||||
html += makeIconButton('fa-tools', 'button-build', row.part, '{% trans "Build stock" %}');
|
html += makeIconButton('fa-tools', 'button-build', row.part, '{% trans "Build stock" %}');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html += makeIconButton('fa-dollar-sign icon-green', 'button-price', pk, '{% trans "Calculate price" %}');
|
||||||
}
|
}
|
||||||
|
|
||||||
html += makeIconButton('fa-edit icon-blue', 'button-edit', pk, '{% trans "Edit line item" %}');
|
html += makeIconButton('fa-edit icon-blue', 'button-edit', pk, '{% trans "Edit line item" %}');
|
||||||
html += makeIconButton('fa-trash-alt icon-red', 'button-delete', pk, '{% trans "Delete line item " %}');
|
html += makeIconButton('fa-trash-alt icon-red', 'button-delete', pk, '{% trans "Delete line item " %}');
|
||||||
|
|
||||||
@ -388,6 +397,26 @@ function setupCallbacks() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$(".button-price").click(function() {
|
||||||
|
var pk = $(this).attr('pk');
|
||||||
|
var idx = $(this).closest('tr').attr('data-index');
|
||||||
|
var row = table.bootstrapTable('getData')[idx];
|
||||||
|
|
||||||
|
launchModalForm(
|
||||||
|
"{% url 'line-pricing' %}",
|
||||||
|
{
|
||||||
|
submit_text: '{% trans "Calculate price" %}',
|
||||||
|
data: {
|
||||||
|
line_item: pk,
|
||||||
|
quantity: row.quantity,
|
||||||
|
},
|
||||||
|
buttons: [{name: 'update_price',
|
||||||
|
title: '{% trans "Update Unit Price" %}'},],
|
||||||
|
success: reloadTable,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
@ -141,16 +141,18 @@ $('#view-calendar').click(function() {
|
|||||||
$(".columns-right").hide();
|
$(".columns-right").hide();
|
||||||
$(".search").hide();
|
$(".search").hide();
|
||||||
$('#filter-list-salesorder').hide();
|
$('#filter-list-salesorder').hide();
|
||||||
|
|
||||||
$("#sales-order-calendar").show();
|
$("#sales-order-calendar").show();
|
||||||
$("#view-list").show();
|
$("#view-list").show();
|
||||||
|
|
||||||
|
calendar.render();
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#view-list").click(function() {
|
$("#view-list").click(function() {
|
||||||
// Hide the calendar view, show the list view
|
// Hide the calendar view, show the list view
|
||||||
$("#sales-order-calendar").hide();
|
$("#sales-order-calendar").hide();
|
||||||
$("#view-list").hide();
|
$("#view-list").hide();
|
||||||
|
|
||||||
$(".fixed-table-pagination").show();
|
$(".fixed-table-pagination").show();
|
||||||
$(".columns-right").show();
|
$(".columns-right").show();
|
||||||
$(".search").show();
|
$(".search").show();
|
||||||
|
@ -94,7 +94,7 @@ class PurchaseOrderTest(OrderTest):
|
|||||||
url = '/api/order/po/1/'
|
url = '/api/order/po/1/'
|
||||||
|
|
||||||
response = self.get(url)
|
response = self.get(url)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
data = response.data
|
data = response.data
|
||||||
@ -109,7 +109,7 @@ class PurchaseOrderTest(OrderTest):
|
|||||||
response = self.get(url)
|
response = self.get(url)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
class SalesOrderTest(OrderTest):
|
class SalesOrderTest(OrderTest):
|
||||||
"""
|
"""
|
||||||
|
@ -73,7 +73,7 @@ class SalesOrderTest(TestCase):
|
|||||||
|
|
||||||
def test_add_duplicate_line_item(self):
|
def test_add_duplicate_line_item(self):
|
||||||
# Adding a duplicate line item to a SalesOrder is accepted
|
# Adding a duplicate line item to a SalesOrder is accepted
|
||||||
|
|
||||||
for ii in range(1, 5):
|
for ii in range(1, 5):
|
||||||
SalesOrderLineItem.objects.create(order=self.order, part=self.part, quantity=ii)
|
SalesOrderLineItem.objects.create(order=self.order, part=self.part, quantity=ii)
|
||||||
|
|
||||||
@ -107,7 +107,7 @@ class SalesOrderTest(TestCase):
|
|||||||
self.assertTrue(self.order.is_fully_allocated())
|
self.assertTrue(self.order.is_fully_allocated())
|
||||||
self.assertTrue(self.line.is_fully_allocated())
|
self.assertTrue(self.line.is_fully_allocated())
|
||||||
self.assertEqual(self.line.allocated_quantity(), 50)
|
self.assertEqual(self.line.allocated_quantity(), 50)
|
||||||
|
|
||||||
def test_order_cancel(self):
|
def test_order_cancel(self):
|
||||||
# Allocate line items then cancel the order
|
# Allocate line items then cancel the order
|
||||||
|
|
||||||
@ -154,7 +154,7 @@ class SalesOrderTest(TestCase):
|
|||||||
|
|
||||||
for item in outputs.all():
|
for item in outputs.all():
|
||||||
self.assertEqual(item.quantity, 25)
|
self.assertEqual(item.quantity, 25)
|
||||||
|
|
||||||
self.assertEqual(sa.sales_order, None)
|
self.assertEqual(sa.sales_order, None)
|
||||||
self.assertEqual(sb.sales_order, None)
|
self.assertEqual(sb.sales_order, None)
|
||||||
|
|
||||||
@ -162,7 +162,7 @@ class SalesOrderTest(TestCase):
|
|||||||
self.assertEqual(SalesOrderAllocation.objects.count(), 0)
|
self.assertEqual(SalesOrderAllocation.objects.count(), 0)
|
||||||
|
|
||||||
self.assertEqual(self.order.status, status.SalesOrderStatus.SHIPPED)
|
self.assertEqual(self.order.status, status.SalesOrderStatus.SHIPPED)
|
||||||
|
|
||||||
self.assertTrue(self.order.is_fully_allocated())
|
self.assertTrue(self.order.is_fully_allocated())
|
||||||
self.assertTrue(self.line.is_fully_allocated())
|
self.assertTrue(self.line.is_fully_allocated())
|
||||||
self.assertEqual(self.line.fulfilled_quantity(), 50)
|
self.assertEqual(self.line.fulfilled_quantity(), 50)
|
||||||
|
@ -17,7 +17,7 @@ import json
|
|||||||
|
|
||||||
|
|
||||||
class OrderViewTestCase(TestCase):
|
class OrderViewTestCase(TestCase):
|
||||||
|
|
||||||
fixtures = [
|
fixtures = [
|
||||||
'category',
|
'category',
|
||||||
'part',
|
'part',
|
||||||
@ -193,7 +193,7 @@ class POTests(OrderViewTestCase):
|
|||||||
# Test without confirmation
|
# Test without confirmation
|
||||||
response = self.client.post(url, {'confirm': 0}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
response = self.client.post(url, {'confirm': 0}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
data = json.loads(response.content)
|
data = json.loads(response.content)
|
||||||
|
|
||||||
self.assertFalse(data['form_valid'])
|
self.assertFalse(data['form_valid'])
|
||||||
@ -221,7 +221,7 @@ class POTests(OrderViewTestCase):
|
|||||||
|
|
||||||
# GET the form (pass the correct info)
|
# GET the form (pass the correct info)
|
||||||
response = self.client.get(url, {'order': 1}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
response = self.client.get(url, {'order': 1}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
||||||
|
|
||||||
post_data = {
|
post_data = {
|
||||||
'part': 100,
|
'part': 100,
|
||||||
'quantity': 45,
|
'quantity': 45,
|
||||||
@ -303,7 +303,7 @@ class TestPOReceive(OrderViewTestCase):
|
|||||||
self.client.get(self.url, data, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
self.client.get(self.url, data, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
||||||
|
|
||||||
def test_receive_lines(self):
|
def test_receive_lines(self):
|
||||||
|
|
||||||
post_data = {
|
post_data = {
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -330,7 +330,7 @@ class TestPOReceive(OrderViewTestCase):
|
|||||||
|
|
||||||
# Receive negative number
|
# Receive negative number
|
||||||
post_data['line-1'] = -100
|
post_data['line-1'] = -100
|
||||||
|
|
||||||
self.post(post_data, validate=False)
|
self.post(post_data, validate=False)
|
||||||
|
|
||||||
# Receive 75 items
|
# Receive 75 items
|
||||||
|
@ -37,7 +37,7 @@ class OrderTest(TestCase):
|
|||||||
self.assertEqual(order.get_absolute_url(), '/order/purchase-order/1/')
|
self.assertEqual(order.get_absolute_url(), '/order/purchase-order/1/')
|
||||||
|
|
||||||
self.assertEqual(str(order), 'PO0001 - ACME')
|
self.assertEqual(str(order), 'PO0001 - ACME')
|
||||||
|
|
||||||
line = PurchaseOrderLineItem.objects.get(pk=1)
|
line = PurchaseOrderLineItem.objects.get(pk=1)
|
||||||
|
|
||||||
self.assertEqual(str(line), "100 x ACME0001 from ACME (for PO0001 - ACME)")
|
self.assertEqual(str(line), "100 x ACME0001 from ACME (for PO0001 - ACME)")
|
||||||
@ -114,7 +114,7 @@ class OrderTest(TestCase):
|
|||||||
|
|
||||||
# Try to order a supplier part from the wrong supplier
|
# Try to order a supplier part from the wrong supplier
|
||||||
sku = SupplierPart.objects.get(SKU='ZERG-WIDGET')
|
sku = SupplierPart.objects.get(SKU='ZERG-WIDGET')
|
||||||
|
|
||||||
with self.assertRaises(django_exceptions.ValidationError):
|
with self.assertRaises(django_exceptions.ValidationError):
|
||||||
order.add_line_item(sku, 99)
|
order.add_line_item(sku, 99)
|
||||||
|
|
||||||
@ -187,7 +187,7 @@ class OrderTest(TestCase):
|
|||||||
|
|
||||||
with self.assertRaises(django_exceptions.ValidationError):
|
with self.assertRaises(django_exceptions.ValidationError):
|
||||||
order.receive_line_item(line, loc, 'not a number', user=None)
|
order.receive_line_item(line, loc, 'not a number', user=None)
|
||||||
|
|
||||||
# Receive the rest of the items
|
# Receive the rest of the items
|
||||||
order.receive_line_item(line, loc, 50, user=None)
|
order.receive_line_item(line, loc, 50, user=None)
|
||||||
|
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user