mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge remote-tracking branch 'inventree/master'
This commit is contained in:
commit
755962c6a2
@ -81,6 +81,15 @@
|
|||||||
max-width: 250px;
|
max-width: 250px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bomrowvalid {
|
||||||
|
color: #050;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bomrowinvalid {
|
||||||
|
color: #A00;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
/* Part image icons with full-display on mouse hover */
|
/* Part image icons with full-display on mouse hover */
|
||||||
|
|
||||||
.hover-img-thumb {
|
.hover-img-thumb {
|
||||||
|
@ -113,14 +113,19 @@ function loadBomTable(table, options) {
|
|||||||
];
|
];
|
||||||
|
|
||||||
if (options.editable) {
|
if (options.editable) {
|
||||||
|
|
||||||
|
/*
|
||||||
|
// TODO - Enable multi-select functionality
|
||||||
cols.push({
|
cols.push({
|
||||||
checkbox: true,
|
checkbox: true,
|
||||||
title: 'Select',
|
title: 'Select',
|
||||||
searchable: false,
|
searchable: false,
|
||||||
sortable: false,
|
sortable: false,
|
||||||
});
|
});
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Part column
|
// Part column
|
||||||
cols.push(
|
cols.push(
|
||||||
{
|
{
|
||||||
@ -230,10 +235,27 @@ function loadBomTable(table, options) {
|
|||||||
if (options.editable) {
|
if (options.editable) {
|
||||||
cols.push({
|
cols.push({
|
||||||
formatter: function(value, row, index, field) {
|
formatter: function(value, row, index, field) {
|
||||||
|
|
||||||
|
var bValidate = "<button title='Validate BOM Item' class='bom-validate-button btn btn-default btn-glyph' type='button' pk='" + row.pk + "'><span class='glyphicon glyphicon-check'/></button>";
|
||||||
|
var bValid = "<span class='glyphicon glyphicon-ok'/>";
|
||||||
|
|
||||||
var bEdit = "<button title='Edit BOM Item' class='bom-edit-button btn btn-default btn-glyph' type='button' url='/part/bom/" + row.pk + "/edit'><span class='glyphicon glyphicon-edit'/></button>";
|
var bEdit = "<button title='Edit BOM Item' class='bom-edit-button btn btn-default btn-glyph' type='button' url='/part/bom/" + row.pk + "/edit'><span class='glyphicon glyphicon-edit'/></button>";
|
||||||
var bDelt = "<button title='Delete BOM Item' class='bom-delete-button btn btn-default btn-glyph' type='button' url='/part/bom/" + row.pk + "/delete'><span class='glyphicon glyphicon-trash'/></button>";
|
var bDelt = "<button title='Delete BOM Item' class='bom-delete-button btn btn-default btn-glyph' type='button' url='/part/bom/" + row.pk + "/delete'><span class='glyphicon glyphicon-trash'/></button>";
|
||||||
|
|
||||||
return "<div class='btn-group' role='group'>" + bEdit + bDelt + "</div>";
|
var html = "<div class='btn-group' role='group'>";
|
||||||
|
|
||||||
|
html += bEdit;
|
||||||
|
html += bDelt;
|
||||||
|
|
||||||
|
if (!row.validated) {
|
||||||
|
html += bValidate;
|
||||||
|
} else {
|
||||||
|
html += bValid;
|
||||||
|
}
|
||||||
|
|
||||||
|
html += "</div>";
|
||||||
|
|
||||||
|
return html;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -256,6 +278,13 @@ function loadBomTable(table, options) {
|
|||||||
table.bootstrapTable({
|
table.bootstrapTable({
|
||||||
sortable: true,
|
sortable: true,
|
||||||
search: true,
|
search: true,
|
||||||
|
rowStyle: function(row, index) {
|
||||||
|
if (row.validated) {
|
||||||
|
return {classes: 'bomrowvalid'};
|
||||||
|
} else {
|
||||||
|
return {classes: 'bomrowinvalid'};
|
||||||
|
}
|
||||||
|
},
|
||||||
formatNoMatches: function() { return "No BOM items found"; },
|
formatNoMatches: function() { return "No BOM items found"; },
|
||||||
clickToSelect: true,
|
clickToSelect: true,
|
||||||
showFooter: true,
|
showFooter: true,
|
||||||
@ -288,5 +317,22 @@ function loadBomTable(table, options) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
table.on('click', '.bom-validate-button', function() {
|
||||||
|
var button = $(this);
|
||||||
|
|
||||||
|
var url = '/api/bom/' + button.attr('pk') + '/validate/';
|
||||||
|
|
||||||
|
inventreePut(
|
||||||
|
url,
|
||||||
|
{
|
||||||
|
valid: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
method: 'PATCH',
|
||||||
|
reloadOnSuccess: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -12,7 +12,7 @@ from django.db.models import Sum
|
|||||||
|
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework import filters
|
from rest_framework import filters, serializers
|
||||||
from rest_framework import generics, permissions
|
from rest_framework import generics, permissions
|
||||||
|
|
||||||
from django.conf.urls import url, include
|
from django.conf.urls import url, include
|
||||||
@ -303,7 +303,7 @@ class BomList(generics.ListCreateAPIView):
|
|||||||
|
|
||||||
filter_fields = [
|
filter_fields = [
|
||||||
'part',
|
'part',
|
||||||
'sub_part'
|
'sub_part',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -318,6 +318,35 @@ class BomDetail(generics.RetrieveUpdateDestroyAPIView):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class BomItemValidate(generics.UpdateAPIView):
|
||||||
|
""" API endpoint for validating a BomItem """
|
||||||
|
|
||||||
|
# Very simple serializers
|
||||||
|
class BomItemValidationSerializer(serializers.Serializer):
|
||||||
|
|
||||||
|
valid = serializers.BooleanField(default=False)
|
||||||
|
|
||||||
|
queryset = BomItem.objects.all()
|
||||||
|
serializer_class = BomItemValidationSerializer
|
||||||
|
|
||||||
|
def update(self, request, *args, **kwargs):
|
||||||
|
""" Perform update request """
|
||||||
|
|
||||||
|
partial = kwargs.pop('partial', False)
|
||||||
|
|
||||||
|
valid = request.data.get('valid', False)
|
||||||
|
|
||||||
|
instance = self.get_object()
|
||||||
|
|
||||||
|
serializer = self.get_serializer(instance, data=request.data, partial=partial)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
|
if type(instance) == BomItem:
|
||||||
|
instance.validate_hash(valid)
|
||||||
|
|
||||||
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
|
||||||
cat_api_urls = [
|
cat_api_urls = [
|
||||||
|
|
||||||
url(r'^(?P<pk>\d+)/?', CategoryDetail.as_view(), name='api-part-category-detail'),
|
url(r'^(?P<pk>\d+)/?', CategoryDetail.as_view(), name='api-part-category-detail'),
|
||||||
@ -345,10 +374,16 @@ part_api_urls = [
|
|||||||
url(r'^.*$', PartList.as_view(), name='api-part-list'),
|
url(r'^.*$', PartList.as_view(), name='api-part-list'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
bom_item_urls = [
|
||||||
|
|
||||||
|
url(r'^validate/?', BomItemValidate.as_view(), name='api-bom-item-validate'),
|
||||||
|
|
||||||
|
url(r'^.*$', BomDetail.as_view(), name='api-bom-item-detail'),
|
||||||
|
]
|
||||||
|
|
||||||
bom_api_urls = [
|
bom_api_urls = [
|
||||||
# BOM Item Detail
|
# BOM Item Detail
|
||||||
url(r'^(?P<pk>\d+)/?', BomDetail.as_view(), name='api-bom-detail'),
|
url(r'^(?P<pk>\d+)/', include(bom_item_urls)),
|
||||||
|
|
||||||
# Catch-all
|
# Catch-all
|
||||||
url(r'^.*$', BomList.as_view(), name='api-bom-list'),
|
url(r'^.*$', BomList.as_view(), name='api-bom-list'),
|
||||||
|
18
InvenTree/part/migrations/0017_bomitem_checksum.py
Normal file
18
InvenTree/part/migrations/0017_bomitem_checksum.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 2.2.4 on 2019-09-05 02:57
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('part', '0016_auto_20190820_0257'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='bomitem',
|
||||||
|
name='checksum',
|
||||||
|
field=models.CharField(blank=True, help_text='BOM line checksum', max_length=128),
|
||||||
|
),
|
||||||
|
]
|
@ -631,12 +631,7 @@ class Part(models.Model):
|
|||||||
""" Return a checksum hash for the BOM for this part.
|
""" Return a checksum hash for the BOM for this part.
|
||||||
Used to determine if the BOM has changed (and needs to be signed off!)
|
Used to determine if the BOM has changed (and needs to be signed off!)
|
||||||
|
|
||||||
For hash is calculated from the following fields of each BOM item:
|
The hash is calculated by hashing each line item in the BOM.
|
||||||
|
|
||||||
- Part.full_name (if the part name changes, the BOM checksum is invalidated)
|
|
||||||
- Quantity
|
|
||||||
- Reference field
|
|
||||||
- Note field
|
|
||||||
|
|
||||||
returns a string representation of a hash object which can be compared with a stored value
|
returns a string representation of a hash object which can be compared with a stored value
|
||||||
"""
|
"""
|
||||||
@ -644,11 +639,7 @@ class Part(models.Model):
|
|||||||
hash = hashlib.md5(str(self.id).encode())
|
hash = hashlib.md5(str(self.id).encode())
|
||||||
|
|
||||||
for item in self.bom_items.all().prefetch_related('sub_part'):
|
for item in self.bom_items.all().prefetch_related('sub_part'):
|
||||||
hash.update(str(item.sub_part.id).encode())
|
hash.update(str(item.get_item_hash()).encode())
|
||||||
hash.update(str(item.sub_part.full_name).encode())
|
|
||||||
hash.update(str(item.quantity).encode())
|
|
||||||
hash.update(str(item.note).encode())
|
|
||||||
hash.update(str(item.reference).encode())
|
|
||||||
|
|
||||||
return str(hash.digest())
|
return str(hash.digest())
|
||||||
|
|
||||||
@ -667,6 +658,10 @@ class Part(models.Model):
|
|||||||
- Saves the current date and the checking user
|
- Saves the current date and the checking user
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# Validate each line item too
|
||||||
|
for item in self.bom_items.all():
|
||||||
|
item.validate_hash()
|
||||||
|
|
||||||
self.bom_checksum = self.get_bom_hash()
|
self.bom_checksum = self.get_bom_hash()
|
||||||
self.bom_checked_by = user
|
self.bom_checked_by = user
|
||||||
self.bom_checked_date = datetime.now().date()
|
self.bom_checked_date = datetime.now().date()
|
||||||
@ -1121,6 +1116,7 @@ class BomItem(models.Model):
|
|||||||
reference: BOM reference field (e.g. part designators)
|
reference: BOM reference field (e.g. part designators)
|
||||||
overage: Estimated losses for a Build. Can be expressed as absolute value (e.g. '7') or a percentage (e.g. '2%')
|
overage: Estimated losses for a Build. Can be expressed as absolute value (e.g. '7') or a percentage (e.g. '2%')
|
||||||
note: Note field for this BOM item
|
note: Note field for this BOM item
|
||||||
|
checksum: Validation checksum for the particular BOM line item
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
@ -1154,6 +1150,56 @@ class BomItem(models.Model):
|
|||||||
# Note attached to this BOM line item
|
# Note attached to this BOM line item
|
||||||
note = models.CharField(max_length=500, blank=True, help_text='BOM item notes')
|
note = models.CharField(max_length=500, blank=True, help_text='BOM item notes')
|
||||||
|
|
||||||
|
checksum = models.CharField(max_length=128, blank=True, help_text='BOM line checksum')
|
||||||
|
|
||||||
|
def get_item_hash(self):
|
||||||
|
""" Calculate the checksum hash of this BOM line item:
|
||||||
|
|
||||||
|
The hash is calculated from the following fields:
|
||||||
|
|
||||||
|
- Part.full_name (if the part name changes, the BOM checksum is invalidated)
|
||||||
|
- Quantity
|
||||||
|
- Reference field
|
||||||
|
- Note field
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Seed the hash with the ID of this BOM item
|
||||||
|
hash = hashlib.md5(str(self.id).encode())
|
||||||
|
|
||||||
|
# Update the hash based on line information
|
||||||
|
hash.update(str(self.sub_part.id).encode())
|
||||||
|
hash.update(str(self.sub_part.full_name).encode())
|
||||||
|
hash.update(str(self.quantity).encode())
|
||||||
|
hash.update(str(self.note).encode())
|
||||||
|
hash.update(str(self.reference).encode())
|
||||||
|
|
||||||
|
return str(hash.digest())
|
||||||
|
|
||||||
|
def validate_hash(self, valid=True):
|
||||||
|
""" Mark this item as 'valid' (store the checksum hash).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
valid: If true, validate the hash, otherwise invalidate it (default = True)
|
||||||
|
"""
|
||||||
|
|
||||||
|
if valid:
|
||||||
|
self.checksum = str(self.get_item_hash())
|
||||||
|
else:
|
||||||
|
self.checksum = ''
|
||||||
|
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_line_valid(self):
|
||||||
|
""" Check if this line item has been validated by the user """
|
||||||
|
|
||||||
|
# Ensure an empty checksum returns False
|
||||||
|
if len(self.checksum) == 0:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return self.get_item_hash() == self.checksum
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
""" Check validity of the BomItem model.
|
""" Check validity of the BomItem model.
|
||||||
|
|
||||||
|
@ -131,6 +131,7 @@ class BomItemSerializer(InvenTreeModelSerializer):
|
|||||||
part_detail = PartBriefSerializer(source='part', many=False, read_only=True)
|
part_detail = PartBriefSerializer(source='part', many=False, read_only=True)
|
||||||
sub_part_detail = PartBriefSerializer(source='sub_part', many=False, read_only=True)
|
sub_part_detail = PartBriefSerializer(source='sub_part', many=False, read_only=True)
|
||||||
price_range = serializers.CharField(read_only=True)
|
price_range = serializers.CharField(read_only=True)
|
||||||
|
validated = serializers.BooleanField(read_only=True, source='is_line_valid')
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
# part_detail and sub_part_detail serializers are only included if requested.
|
# part_detail and sub_part_detail serializers are only included if requested.
|
||||||
@ -171,4 +172,5 @@ class BomItemSerializer(InvenTreeModelSerializer):
|
|||||||
'price_range',
|
'price_range',
|
||||||
'overage',
|
'overage',
|
||||||
'note',
|
'note',
|
||||||
|
'validated',
|
||||||
]
|
]
|
||||||
|
@ -2,4 +2,9 @@
|
|||||||
|
|
||||||
{% block pre_form_content %}
|
{% block pre_form_content %}
|
||||||
Confirm that the Bill of Materials (BOM) is valid for:<br><i>{{ part.full_name }}</i>
|
Confirm that the Bill of Materials (BOM) is valid for:<br><i>{{ part.full_name }}</i>
|
||||||
|
|
||||||
|
<div class='alert alert-warning alert-block'>
|
||||||
|
This will validate each line in the BOM.
|
||||||
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
@ -120,7 +120,7 @@ class PartAPITest(APITestCase):
|
|||||||
|
|
||||||
def test_get_bom_detail(self):
|
def test_get_bom_detail(self):
|
||||||
# Get the detail for a single BomItem
|
# Get the detail for a single BomItem
|
||||||
url = reverse('api-bom-detail', kwargs={'pk': 3})
|
url = reverse('api-bom-item-detail', kwargs={'pk': 3})
|
||||||
response = self.client.get(url, format='json')
|
response = self.client.get(url, format='json')
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(response.data['quantity'], 25)
|
self.assertEqual(response.data['quantity'], 25)
|
||||||
|
@ -117,10 +117,12 @@ class AdjustStockForm(forms.ModelForm):
|
|||||||
|
|
||||||
return choices
|
return choices
|
||||||
|
|
||||||
destination = forms.ChoiceField(label='Destination', required=True, help_text='Destination stock location')
|
destination = forms.ChoiceField(label='Destination', required=True, help_text=_('Destination stock location'))
|
||||||
note = forms.CharField(label='Notes', required=True, help_text='Add note (required)')
|
note = forms.CharField(label='Notes', required=True, help_text='Add note (required)')
|
||||||
# transaction = forms.BooleanField(required=False, initial=False, label='Create Transaction', help_text='Create a stock transaction for these parts')
|
# transaction = forms.BooleanField(required=False, initial=False, label='Create Transaction', help_text='Create a stock transaction for these parts')
|
||||||
confirm = forms.BooleanField(required=False, initial=False, label='Confirm stock adjustment', help_text='Confirm movement of stock items')
|
confirm = forms.BooleanField(required=False, initial=False, label='Confirm stock adjustment', help_text=_('Confirm movement of stock items'))
|
||||||
|
|
||||||
|
set_loc = forms.BooleanField(required=False, initial=False, label='Set Default Location', help_text=_('Set the destination as the default location for selected parts'))
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
@ -148,7 +148,15 @@ class StockAdjust(AjaxView, FormMixin):
|
|||||||
stock_items = []
|
stock_items = []
|
||||||
|
|
||||||
def get_GET_items(self):
|
def get_GET_items(self):
|
||||||
""" Return list of stock items initally requested using GET """
|
""" Return list of stock items initally requested using GET.
|
||||||
|
|
||||||
|
Items can be retrieved by:
|
||||||
|
|
||||||
|
a) List of stock ID - stock[]=1,2,3,4,5
|
||||||
|
b) Parent part - part=3
|
||||||
|
c) Parent location - location=78
|
||||||
|
d) Single item - item=2
|
||||||
|
"""
|
||||||
|
|
||||||
# Start with all 'in stock' items
|
# Start with all 'in stock' items
|
||||||
items = StockItem.objects.filter(customer=None, belongs_to=None)
|
items = StockItem.objects.filter(customer=None, belongs_to=None)
|
||||||
@ -224,6 +232,7 @@ class StockAdjust(AjaxView, FormMixin):
|
|||||||
|
|
||||||
if not self.stock_action == 'move':
|
if not self.stock_action == 'move':
|
||||||
form.fields.pop('destination')
|
form.fields.pop('destination')
|
||||||
|
form.fields.pop('set_loc')
|
||||||
|
|
||||||
return form
|
return form
|
||||||
|
|
||||||
@ -257,7 +266,7 @@ class StockAdjust(AjaxView, FormMixin):
|
|||||||
|
|
||||||
self.request = request
|
self.request = request
|
||||||
|
|
||||||
self.stock_action = request.POST.get('stock_action').lower()
|
self.stock_action = request.POST.get('stock_action', 'invalid').lower()
|
||||||
|
|
||||||
# Update list of stock items
|
# Update list of stock items
|
||||||
self.stock_items = self.get_POST_items()
|
self.stock_items = self.get_POST_items()
|
||||||
@ -297,8 +306,9 @@ class StockAdjust(AjaxView, FormMixin):
|
|||||||
}
|
}
|
||||||
|
|
||||||
if valid:
|
if valid:
|
||||||
|
result = self.do_action()
|
||||||
|
|
||||||
data['success'] = self.do_action()
|
data['success'] = result
|
||||||
|
|
||||||
return self.renderJsonResponse(request, form, data=data)
|
return self.renderJsonResponse(request, form, data=data)
|
||||||
|
|
||||||
@ -308,6 +318,8 @@ class StockAdjust(AjaxView, FormMixin):
|
|||||||
if self.stock_action == 'move':
|
if self.stock_action == 'move':
|
||||||
destination = None
|
destination = None
|
||||||
|
|
||||||
|
set_default_loc = str2bool(self.request.POST.get('set_loc', False))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
destination = StockLocation.objects.get(id=self.request.POST.get('destination'))
|
destination = StockLocation.objects.get(id=self.request.POST.get('destination'))
|
||||||
except StockLocation.DoesNotExist:
|
except StockLocation.DoesNotExist:
|
||||||
@ -315,7 +327,7 @@ class StockAdjust(AjaxView, FormMixin):
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return self.do_move(destination)
|
return self.do_move(destination, set_default_loc)
|
||||||
|
|
||||||
elif self.stock_action == 'add':
|
elif self.stock_action == 'add':
|
||||||
return self.do_add()
|
return self.do_add()
|
||||||
@ -372,7 +384,7 @@ class StockAdjust(AjaxView, FormMixin):
|
|||||||
|
|
||||||
return _("Counted stock for {n} items".format(n=count))
|
return _("Counted stock for {n} items".format(n=count))
|
||||||
|
|
||||||
def do_move(self, destination):
|
def do_move(self, destination, set_loc=None):
|
||||||
""" Perform actual stock movement """
|
""" Perform actual stock movement """
|
||||||
|
|
||||||
count = 0
|
count = 0
|
||||||
@ -384,6 +396,11 @@ class StockAdjust(AjaxView, FormMixin):
|
|||||||
if item.new_quantity <= 0:
|
if item.new_quantity <= 0:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# If we wish to set the destination location to the default one
|
||||||
|
if set_loc:
|
||||||
|
item.part.default_location = destination
|
||||||
|
item.part.save()
|
||||||
|
|
||||||
# Do not move to the same location (unless the quantity is different)
|
# Do not move to the same location (unless the quantity is different)
|
||||||
if destination == item.location and item.new_quantity == item.quantity:
|
if destination == item.location and item.new_quantity == item.quantity:
|
||||||
continue
|
continue
|
||||||
|
1
Makefile
1
Makefile
@ -18,6 +18,7 @@ migrate:
|
|||||||
python3 InvenTree/manage.py migrate
|
python3 InvenTree/manage.py migrate
|
||||||
python3 InvenTree/manage.py migrate --run-syncdb
|
python3 InvenTree/manage.py migrate --run-syncdb
|
||||||
python3 InvenTree/manage.py check
|
python3 InvenTree/manage.py check
|
||||||
|
python3 InvenTree/manage.py collectstatic
|
||||||
|
|
||||||
# Install all required packages
|
# Install all required packages
|
||||||
install:
|
install:
|
||||||
|
Loading…
Reference in New Issue
Block a user