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 fr-1421-sso
This commit is contained in:
commit
fe7ab40b48
3
.github/workflows/docker_stable.yaml
vendored
3
.github/workflows/docker_stable.yaml
vendored
@ -1,4 +1,5 @@
|
|||||||
# Build and push latest docker image on push to master branch
|
# Build and push docker image on push to 'stable' branch
|
||||||
|
# Docker build will be uploaded to dockerhub with the 'inventree:stable' tag
|
||||||
|
|
||||||
name: Docker Build
|
name: Docker Build
|
||||||
|
|
||||||
|
3
.github/workflows/docker_tag.yaml
vendored
3
.github/workflows/docker_tag.yaml
vendored
@ -1,4 +1,5 @@
|
|||||||
# Publish docker images to dockerhub
|
# Publish docker images to dockerhub on a tagged release
|
||||||
|
# Docker build will be uploaded to dockerhub with the 'invetree:<tag>' tag
|
||||||
|
|
||||||
name: Docker Publish
|
name: Docker Publish
|
||||||
|
|
||||||
|
37
.github/workflows/docker_test.yaml
vendored
Normal file
37
.github/workflows/docker_test.yaml
vendored
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
# Test that the InvenTree docker image compiles correctly
|
||||||
|
|
||||||
|
# This CI action runs on pushes to either the master or stable branches
|
||||||
|
|
||||||
|
# 1. Build the development docker image (as per the documentation)
|
||||||
|
# 2. Install requied python libs into the docker container
|
||||||
|
# 3. Launch the container
|
||||||
|
# 4. Check that the API endpoint is available
|
||||||
|
|
||||||
|
name: Docker Test
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- 'master'
|
||||||
|
- 'stable'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
|
||||||
|
docker:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout Code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Build Docker Image
|
||||||
|
run: |
|
||||||
|
cd docker
|
||||||
|
docker-compose -f docker-compose.dev.yml build
|
||||||
|
docker-compose -f docker-compose.dev.yml run inventree-dev-server invoke update
|
||||||
|
docker-compose -f docker-compose.dev.yml up -d
|
||||||
|
- name: Sleepy Time
|
||||||
|
run: sleep 60
|
||||||
|
- name: Test API
|
||||||
|
run: |
|
||||||
|
pip install requests
|
||||||
|
python3 ci/check_api_endpoint.py
|
@ -63,6 +63,12 @@ class InvenTreeConfig(AppConfig):
|
|||||||
schedule_type=Schedule.DAILY,
|
schedule_type=Schedule.DAILY,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Delete old error messages
|
||||||
|
InvenTree.tasks.schedule_task(
|
||||||
|
'InvenTree.tasks.delete_old_error_logs',
|
||||||
|
schedule_type=Schedule.DAILY,
|
||||||
|
)
|
||||||
|
|
||||||
# Delete "old" stock items
|
# Delete "old" stock items
|
||||||
InvenTree.tasks.schedule_task(
|
InvenTree.tasks.schedule_task(
|
||||||
'stock.tasks.delete_old_stock_items',
|
'stock.tasks.delete_old_stock_items',
|
||||||
|
@ -242,6 +242,14 @@
|
|||||||
border-color: var(--label-red);
|
border-color: var(--label-red);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.label-form {
|
||||||
|
margin: 2px;
|
||||||
|
padding: 3px;
|
||||||
|
padding-left: 10px;
|
||||||
|
padding-right: 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
.label-red {
|
.label-red {
|
||||||
background: var(--label-red);
|
background: var(--label-red);
|
||||||
}
|
}
|
||||||
|
@ -156,7 +156,34 @@ def delete_successful_tasks():
|
|||||||
started__lte=threshold
|
started__lte=threshold
|
||||||
)
|
)
|
||||||
|
|
||||||
results.delete()
|
if results.count() > 0:
|
||||||
|
logger.info(f"Deleting {results.count()} successful task records")
|
||||||
|
results.delete()
|
||||||
|
|
||||||
|
|
||||||
|
def delete_old_error_logs():
|
||||||
|
"""
|
||||||
|
Delete old error logs from the server
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
from error_report.models import Error
|
||||||
|
|
||||||
|
# Delete any error logs more than 30 days old
|
||||||
|
threshold = timezone.now() - timedelta(days=30)
|
||||||
|
|
||||||
|
errors = Error.objects.filter(
|
||||||
|
when__lte=threshold,
|
||||||
|
)
|
||||||
|
|
||||||
|
if errors.count() > 0:
|
||||||
|
logger.info(f"Deleting {errors.count()} old error logs")
|
||||||
|
errors.delete()
|
||||||
|
|
||||||
|
except AppRegistryNotReady:
|
||||||
|
# Apps not yet loaded
|
||||||
|
logger.info("Could not perform 'delete_old_error_logs' - App registry not ready")
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
def check_for_updates():
|
def check_for_updates():
|
||||||
@ -215,7 +242,7 @@ def delete_expired_sessions():
|
|||||||
# Delete any sessions that expired more than a day ago
|
# Delete any sessions that expired more than a day ago
|
||||||
expired = Session.objects.filter(expire_date__lt=timezone.now() - timedelta(days=1))
|
expired = Session.objects.filter(expire_date__lt=timezone.now() - timedelta(days=1))
|
||||||
|
|
||||||
if True or expired.count() > 0:
|
if expired.count() > 0:
|
||||||
logger.info(f"Deleting {expired.count()} expired sessions.")
|
logger.info(f"Deleting {expired.count()} expired sessions.")
|
||||||
expired.delete()
|
expired.delete()
|
||||||
|
|
||||||
@ -247,15 +274,15 @@ def update_exchange_rates():
|
|||||||
pass
|
pass
|
||||||
except:
|
except:
|
||||||
# Some other error
|
# Some other error
|
||||||
print("Database not ready")
|
logger.warning("update_exchange_rates: Database not ready")
|
||||||
return
|
return
|
||||||
|
|
||||||
backend = InvenTreeExchange()
|
backend = InvenTreeExchange()
|
||||||
print(f"Updating exchange rates from {backend.url}")
|
logger.info(f"Updating exchange rates from {backend.url}")
|
||||||
|
|
||||||
base = currency_code_default()
|
base = currency_code_default()
|
||||||
|
|
||||||
print(f"Using base currency '{base}'")
|
logger.info(f"Using base currency '{base}'")
|
||||||
|
|
||||||
backend.update_rates(base_currency=base)
|
backend.update_rates(base_currency=base)
|
||||||
|
|
||||||
|
@ -10,12 +10,16 @@ import common.models
|
|||||||
|
|
||||||
INVENTREE_SW_VERSION = "0.6.0 dev"
|
INVENTREE_SW_VERSION = "0.6.0 dev"
|
||||||
|
|
||||||
INVENTREE_API_VERSION = 14
|
INVENTREE_API_VERSION = 15
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Increment this API version number whenever there is a significant change to the API that any clients need to know about
|
Increment this API version number whenever there is a significant change to the API that any clients need to know about
|
||||||
|
|
||||||
v14 -> 2021-20-05
|
v15 -> 2021-10-06
|
||||||
|
- Adds detail endpoint for SalesOrderAllocation model
|
||||||
|
- Allows use of the API forms interface for adjusting SalesOrderAllocation objects
|
||||||
|
|
||||||
|
v14 -> 2021-10-05
|
||||||
- Stock adjustment actions API is improved, using native DRF serializer support
|
- Stock adjustment actions API is improved, using native DRF serializer support
|
||||||
- However adjustment actions now only support 'pk' as a lookup field
|
- However adjustment actions now only support 'pk' as a lookup field
|
||||||
|
|
||||||
@ -104,7 +108,7 @@ def inventreeDocsVersion():
|
|||||||
Return the version string matching the latest documentation.
|
Return the version string matching the latest documentation.
|
||||||
|
|
||||||
Development -> "latest"
|
Development -> "latest"
|
||||||
Release -> "major.minor"
|
Release -> "major.minor.sub" e.g. "0.5.2"
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -113,7 +117,7 @@ def inventreeDocsVersion():
|
|||||||
else:
|
else:
|
||||||
major, minor, patch = inventreeVersionTuple()
|
major, minor, patch = inventreeVersionTuple()
|
||||||
|
|
||||||
return f"{major}.{minor}"
|
return f"{major}.{minor}.{patch}"
|
||||||
|
|
||||||
|
|
||||||
def isInvenTreeUpToDate():
|
def isInvenTreeUpToDate():
|
||||||
|
@ -648,14 +648,6 @@ class InvenTreeSetting(BaseInvenTreeSetting):
|
|||||||
'validator': bool,
|
'validator': bool,
|
||||||
},
|
},
|
||||||
|
|
||||||
# TODO: Remove this setting in future, new API forms make this not useful
|
|
||||||
'PART_SHOW_QUANTITY_IN_FORMS': {
|
|
||||||
'name': _('Show Quantity in Forms'),
|
|
||||||
'description': _('Display available part quantity in some forms'),
|
|
||||||
'default': True,
|
|
||||||
'validator': bool,
|
|
||||||
},
|
|
||||||
|
|
||||||
'PART_SHOW_IMPORT': {
|
'PART_SHOW_IMPORT': {
|
||||||
'name': _('Show Import in Views'),
|
'name': _('Show Import in Views'),
|
||||||
'description': _('Display the import wizard in some part views'),
|
'description': _('Display the import wizard in some part views'),
|
||||||
@ -1014,6 +1006,13 @@ class InvenTreeUserSetting(BaseInvenTreeSetting):
|
|||||||
'default': 10,
|
'default': 10,
|
||||||
'validator': [int, MinValueValidator(1)]
|
'validator': [int, MinValueValidator(1)]
|
||||||
},
|
},
|
||||||
|
|
||||||
|
'PART_SHOW_QUANTITY_IN_FORMS': {
|
||||||
|
'name': _('Show Quantity in Forms'),
|
||||||
|
'description': _('Display available part quantity in some forms'),
|
||||||
|
'default': True,
|
||||||
|
'validator': bool,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -158,6 +158,12 @@
|
|||||||
function reloadImage(data) {
|
function reloadImage(data) {
|
||||||
if (data.image) {
|
if (data.image) {
|
||||||
$('#company-image').attr('src', data.image);
|
$('#company-image').attr('src', data.image);
|
||||||
|
|
||||||
|
// Reset the "modal image" view
|
||||||
|
$('#company-image').click(function() {
|
||||||
|
showModalImage(data.image);
|
||||||
|
});
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
location.reload();
|
location.reload();
|
||||||
}
|
}
|
||||||
|
@ -77,6 +77,14 @@ class POLineItemResource(ModelResource):
|
|||||||
class SOLineItemResource(ModelResource):
|
class SOLineItemResource(ModelResource):
|
||||||
""" Class for managing import / export of SOLineItem data """
|
""" Class for managing import / export of SOLineItem data """
|
||||||
|
|
||||||
|
part_name = Field(attribute='part__name', readonly=True)
|
||||||
|
|
||||||
|
IPN = Field(attribute='part__IPN', readonly=True)
|
||||||
|
|
||||||
|
description = Field(attribute='part__description', readonly=True)
|
||||||
|
|
||||||
|
fulfilled = Field(attribute='fulfilled_quantity', readonly=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = SalesOrderLineItem
|
model = SalesOrderLineItem
|
||||||
skip_unchanged = True
|
skip_unchanged = True
|
||||||
|
@ -631,6 +631,15 @@ class SOLineItemDetail(generics.RetrieveUpdateDestroyAPIView):
|
|||||||
serializer_class = SOLineItemSerializer
|
serializer_class = SOLineItemSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class SOAllocationDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||||
|
"""
|
||||||
|
API endpoint for detali view of a SalesOrderAllocation object
|
||||||
|
"""
|
||||||
|
|
||||||
|
queryset = SalesOrderAllocation.objects.all()
|
||||||
|
serializer_class = SalesOrderAllocationSerializer
|
||||||
|
|
||||||
|
|
||||||
class SOAllocationList(generics.ListCreateAPIView):
|
class SOAllocationList(generics.ListCreateAPIView):
|
||||||
"""
|
"""
|
||||||
API endpoint for listing SalesOrderAllocation objects
|
API endpoint for listing SalesOrderAllocation objects
|
||||||
@ -743,8 +752,10 @@ order_api_urls = [
|
|||||||
])),
|
])),
|
||||||
|
|
||||||
# API endpoints for purchase order line items
|
# API endpoints for purchase order line items
|
||||||
url(r'^po-line/(?P<pk>\d+)/$', POLineItemDetail.as_view(), name='api-po-line-detail'),
|
url(r'^po-line/', include([
|
||||||
url(r'^po-line/$', POLineItemList.as_view(), name='api-po-line-list'),
|
url(r'^(?P<pk>\d+)/$', POLineItemDetail.as_view(), name='api-po-line-detail'),
|
||||||
|
url(r'^.*$', POLineItemList.as_view(), name='api-po-line-list'),
|
||||||
|
])),
|
||||||
|
|
||||||
# API endpoints for sales ordesr
|
# API endpoints for sales ordesr
|
||||||
url(r'^so/', include([
|
url(r'^so/', include([
|
||||||
@ -764,9 +775,8 @@ order_api_urls = [
|
|||||||
])),
|
])),
|
||||||
|
|
||||||
# API endpoints for sales order allocations
|
# API endpoints for sales order allocations
|
||||||
url(r'^so-allocation', include([
|
url(r'^so-allocation/', include([
|
||||||
|
url(r'^(?P<pk>\d+)/$', SOAllocationDetail.as_view(), name='api-so-allocation-detail'),
|
||||||
# List all sales order allocations
|
|
||||||
url(r'^.*$', SOAllocationList.as_view(), name='api-so-allocation-list'),
|
url(r'^.*$', SOAllocationList.as_view(), name='api-so-allocation-list'),
|
||||||
])),
|
])),
|
||||||
]
|
]
|
||||||
|
@ -115,23 +115,6 @@ class AllocateSerialsToSalesOrderForm(forms.Form):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class CreateSalesOrderAllocationForm(HelperForm):
|
|
||||||
"""
|
|
||||||
Form for creating a SalesOrderAllocation item.
|
|
||||||
"""
|
|
||||||
|
|
||||||
quantity = RoundingDecimalFormField(max_digits=10, decimal_places=5, label=_('Quantity'))
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = SalesOrderAllocation
|
|
||||||
|
|
||||||
fields = [
|
|
||||||
'line',
|
|
||||||
'item',
|
|
||||||
'quantity',
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class EditSalesOrderAllocationForm(HelperForm):
|
class EditSalesOrderAllocationForm(HelperForm):
|
||||||
"""
|
"""
|
||||||
Form for editing a SalesOrderAllocation item
|
Form for editing a SalesOrderAllocation item
|
||||||
|
@ -840,7 +840,13 @@ class SalesOrderLineItem(OrderLineItem):
|
|||||||
def get_api_url():
|
def get_api_url():
|
||||||
return reverse('api-so-line-list')
|
return reverse('api-so-line-list')
|
||||||
|
|
||||||
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})
|
||||||
|
|
||||||
@ -954,7 +960,11 @@ class SalesOrderAllocation(models.Model):
|
|||||||
if len(errors) > 0:
|
if len(errors) > 0:
|
||||||
raise ValidationError(errors)
|
raise ValidationError(errors)
|
||||||
|
|
||||||
line = models.ForeignKey(SalesOrderLineItem, on_delete=models.CASCADE, verbose_name=_('Line'), related_name='allocations')
|
line = models.ForeignKey(
|
||||||
|
SalesOrderLineItem,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
verbose_name=_('Line'),
|
||||||
|
related_name='allocations')
|
||||||
|
|
||||||
item = models.ForeignKey(
|
item = models.ForeignKey(
|
||||||
'stock.StockItem',
|
'stock.StockItem',
|
||||||
|
@ -478,7 +478,7 @@ class SalesOrderAllocationSerializer(InvenTreeModelSerializer):
|
|||||||
part = serializers.PrimaryKeyRelatedField(source='item.part', read_only=True)
|
part = serializers.PrimaryKeyRelatedField(source='item.part', read_only=True)
|
||||||
order = serializers.PrimaryKeyRelatedField(source='line.order', many=False, read_only=True)
|
order = serializers.PrimaryKeyRelatedField(source='line.order', many=False, read_only=True)
|
||||||
serial = serializers.CharField(source='get_serial', read_only=True)
|
serial = serializers.CharField(source='get_serial', read_only=True)
|
||||||
quantity = serializers.FloatField(read_only=True)
|
quantity = serializers.FloatField(read_only=False)
|
||||||
location = serializers.PrimaryKeyRelatedField(source='item.location', many=False, read_only=True)
|
location = serializers.PrimaryKeyRelatedField(source='item.location', many=False, read_only=True)
|
||||||
|
|
||||||
# Extra detail fields
|
# Extra detail fields
|
||||||
@ -549,7 +549,7 @@ class SOLineItemSerializer(InvenTreeModelSerializer):
|
|||||||
|
|
||||||
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, location_detail=True)
|
||||||
|
|
||||||
quantity = serializers.FloatField()
|
quantity = serializers.FloatField()
|
||||||
|
|
||||||
|
@ -39,6 +39,9 @@ src="{% static 'img/blank_image.png' %}"
|
|||||||
<button type='button' class='btn btn-default' id='print-order-report' title='{% trans "Print" %}'>
|
<button type='button' class='btn btn-default' id='print-order-report' title='{% trans "Print" %}'>
|
||||||
<span class='fas fa-print'></span>
|
<span class='fas fa-print'></span>
|
||||||
</button>
|
</button>
|
||||||
|
<button type='button' class='btn btn-default' id='export-order' title='{% trans "Export order to file" %}'>
|
||||||
|
<span class='fas fa-file-download'></span>
|
||||||
|
</button>
|
||||||
{% if roles.purchase_order.change %}
|
{% if roles.purchase_order.change %}
|
||||||
<button type='button' class='btn btn-default' id='edit-order' title='{% trans "Edit order information" %}'>
|
<button type='button' class='btn btn-default' id='edit-order' title='{% trans "Edit order information" %}'>
|
||||||
<span class='fas fa-edit icon-green'></span>
|
<span class='fas fa-edit icon-green'></span>
|
||||||
@ -61,9 +64,6 @@ src="{% static 'img/blank_image.png' %}"
|
|||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<button type='button' class='btn btn-default' id='export-order' title='{% trans "Export order to file" %}'>
|
|
||||||
<span class='fas fa-file-download'></span>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@ -224,7 +224,7 @@ $("#cancel-order").click(function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
$("#export-order").click(function() {
|
$("#export-order").click(function() {
|
||||||
location.href = "{% url 'po-export' order.id %}";
|
exportOrder('{% url "po-export" order.id %}');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
@ -50,6 +50,9 @@ src="{% static 'img/blank_image.png' %}"
|
|||||||
<button type='button' class='btn btn-default' id='print-order-report' title='{% trans "Print" %}'>
|
<button type='button' class='btn btn-default' id='print-order-report' title='{% trans "Print" %}'>
|
||||||
<span class='fas fa-print'></span>
|
<span class='fas fa-print'></span>
|
||||||
</button>
|
</button>
|
||||||
|
<button type='button' class='btn btn-default' id='export-order' title='{% trans "Export order to file" %}'>
|
||||||
|
<span class='fas fa-file-download'></span>
|
||||||
|
</button>
|
||||||
{% if roles.sales_order.change %}
|
{% if roles.sales_order.change %}
|
||||||
<button type='button' class='btn btn-default' id='edit-order' title='{% trans "Edit order information" %}'>
|
<button type='button' class='btn btn-default' id='edit-order' title='{% trans "Edit order information" %}'>
|
||||||
<span class='fas fa-edit icon-green'></span>
|
<span class='fas fa-edit icon-green'></span>
|
||||||
@ -63,9 +66,11 @@ src="{% static 'img/blank_image.png' %}"
|
|||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<!--
|
||||||
<button type='button' disabled='' class='btn btn-default' id='packing-list' title='{% trans "Packing List" %}'>
|
<button type='button' disabled='' class='btn btn-default' id='packing-list' title='{% trans "Packing List" %}'>
|
||||||
<span class='fas fa-clipboard-list'></span>
|
<span class='fas fa-clipboard-list'></span>
|
||||||
</button>
|
</button>
|
||||||
|
-->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@ -196,4 +201,8 @@ $('#print-order-report').click(function() {
|
|||||||
printSalesOrderReports([{{ order.pk }}]);
|
printSalesOrderReports([{{ order.pk }}]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$('#export-order').click(function() {
|
||||||
|
exportOrder('{% url "so-export" order.id %}');
|
||||||
|
});
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
@ -158,467 +158,38 @@
|
|||||||
$("#so-lines-table").bootstrapTable("refresh");
|
$("#so-lines-table").bootstrapTable("refresh");
|
||||||
}
|
}
|
||||||
|
|
||||||
$("#new-so-line").click(function() {
|
$("#new-so-line").click(function() {
|
||||||
|
|
||||||
constructForm('{% url "api-so-line-list" %}', {
|
constructForm('{% url "api-so-line-list" %}', {
|
||||||
fields: {
|
|
||||||
order: {
|
|
||||||
value: {{ order.pk }},
|
|
||||||
hidden: true,
|
|
||||||
},
|
|
||||||
part: {},
|
|
||||||
quantity: {},
|
|
||||||
reference: {},
|
|
||||||
sale_price: {},
|
|
||||||
sale_price_currency: {},
|
|
||||||
notes: {},
|
|
||||||
},
|
|
||||||
method: 'POST',
|
|
||||||
title: '{% trans "Add Line Item" %}',
|
|
||||||
onSuccess: reloadTable,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
{% if order.status == SalesOrderStatus.PENDING %}
|
|
||||||
function showAllocationSubTable(index, row, element) {
|
|
||||||
// Construct a table showing stock items which have been allocated against this line item
|
|
||||||
|
|
||||||
var html = `<div class='sub-table'><table class='table table-striped table-condensed' id='allocation-table-${row.pk}'></table></div>`;
|
|
||||||
|
|
||||||
element.html(html);
|
|
||||||
|
|
||||||
var lineItem = row;
|
|
||||||
|
|
||||||
var table = $(`#allocation-table-${row.pk}`);
|
|
||||||
|
|
||||||
table.bootstrapTable({
|
|
||||||
data: row.allocations,
|
|
||||||
showHeader: false,
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
width: '50%',
|
|
||||||
field: 'allocated',
|
|
||||||
title: '{% trans "Quantity" %}',
|
|
||||||
formatter: function(value, row, index, field) {
|
|
||||||
var text = '';
|
|
||||||
|
|
||||||
if (row.serial != null && row.quantity == 1) {
|
|
||||||
text = `{% trans "Serial Number" %}: ${row.serial}`;
|
|
||||||
} else {
|
|
||||||
text = `{% trans "Quantity" %}: ${row.quantity}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return renderLink(text, `/stock/item/${row.item}/`);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'location',
|
|
||||||
title: 'Location',
|
|
||||||
formatter: function(value, row, index, field) {
|
|
||||||
return renderLink(row.location_path, `/stock/location/${row.location}/`);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'po'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'buttons',
|
|
||||||
title: '{% trans "Actions" %}',
|
|
||||||
formatter: function(value, row, index, field) {
|
|
||||||
|
|
||||||
var html = "<div class='btn-group float-right' role='group'>";
|
|
||||||
var pk = row.pk;
|
|
||||||
|
|
||||||
{% if order.status == SalesOrderStatus.PENDING %}
|
|
||||||
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" %}');
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
html += "</div>";
|
|
||||||
|
|
||||||
return html;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
table.find(".button-allocation-edit").click(function() {
|
|
||||||
|
|
||||||
var pk = $(this).attr('pk');
|
|
||||||
|
|
||||||
launchModalForm(`/order/sales-order/allocation/${pk}/edit/`, {
|
|
||||||
success: reloadTable,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
table.find(".button-allocation-delete").click(function() {
|
|
||||||
var pk = $(this).attr('pk');
|
|
||||||
|
|
||||||
launchModalForm(`/order/sales-order/allocation/${pk}/delete/`, {
|
|
||||||
success: reloadTable,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
function showFulfilledSubTable(index, row, element) {
|
|
||||||
// Construct a table showing stock items which have been fulfilled against this line item
|
|
||||||
|
|
||||||
var id = `fulfilled-table-${row.pk}`;
|
|
||||||
var html = `<div class='sub-table'><table class='table table-striped table-condensed' id='${id}'></table></div>`;
|
|
||||||
|
|
||||||
element.html(html);
|
|
||||||
|
|
||||||
var lineItem = row;
|
|
||||||
|
|
||||||
$(`#${id}`).bootstrapTable({
|
|
||||||
url: "{% url 'api-stock-list' %}",
|
|
||||||
queryParams: {
|
|
||||||
part: row.part,
|
|
||||||
sales_order: {{ order.id }},
|
|
||||||
},
|
|
||||||
showHeader: false,
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
field: 'pk',
|
|
||||||
visible: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'stock',
|
|
||||||
formatter: function(value, row) {
|
|
||||||
var text = '';
|
|
||||||
if (row.serial && row.quantity == 1) {
|
|
||||||
text = `{% trans "Serial Number" %}: ${row.serial}`;
|
|
||||||
} else {
|
|
||||||
text = `{% trans "Quantity" %}: ${row.quantity}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return renderLink(text, `/stock/item/${row.pk}/`);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'po'
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
$("#so-lines-table").inventreeTable({
|
|
||||||
formatNoMatches: function() { return "{% trans 'No matching line items' %}"; },
|
|
||||||
queryParams: {
|
|
||||||
order: {{ order.id }},
|
|
||||||
part_detail: true,
|
|
||||||
allocations: true,
|
|
||||||
},
|
|
||||||
sidePagination: 'server',
|
|
||||||
uniqueId: 'pk',
|
|
||||||
url: "{% url 'api-so-line-list' %}",
|
|
||||||
onPostBody: setupCallbacks,
|
|
||||||
{% if order.status == SalesOrderStatus.PENDING or order.status == SalesOrderStatus.SHIPPED %}
|
|
||||||
detailViewByClick: true,
|
|
||||||
detailView: true,
|
|
||||||
detailFilter: function(index, row) {
|
|
||||||
{% if order.status == SalesOrderStatus.PENDING %}
|
|
||||||
return row.allocated > 0;
|
|
||||||
{% else %}
|
|
||||||
return row.fulfilled > 0;
|
|
||||||
{% endif %}
|
|
||||||
},
|
|
||||||
{% if order.status == SalesOrderStatus.PENDING %}
|
|
||||||
detailFormatter: showAllocationSubTable,
|
|
||||||
{% else %}
|
|
||||||
detailFormatter: showFulfilledSubTable,
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
showFooter: true,
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
field: 'pk',
|
|
||||||
title: '{% trans "ID" %}',
|
|
||||||
visible: false,
|
|
||||||
switchable: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
sortable: true,
|
|
||||||
sortName: 'part__name',
|
|
||||||
field: 'part',
|
|
||||||
title: '{% trans "Part" %}',
|
|
||||||
formatter: function(value, row, index, field) {
|
|
||||||
if (row.part) {
|
|
||||||
return imageHoverIcon(row.part_detail.thumbnail) + renderLink(row.part_detail.full_name, `/part/${value}/`);
|
|
||||||
} else {
|
|
||||||
return '-';
|
|
||||||
}
|
|
||||||
},
|
|
||||||
footerFormatter: function() {
|
|
||||||
return '{% trans "Total" %}'
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
sortable: true,
|
|
||||||
field: 'reference',
|
|
||||||
title: '{% trans "Reference" %}'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
sortable: true,
|
|
||||||
field: 'quantity',
|
|
||||||
title: '{% trans "Quantity" %}',
|
|
||||||
footerFormatter: function(data) {
|
|
||||||
return data.map(function (row) {
|
|
||||||
return +row['quantity']
|
|
||||||
}).reduce(function (sum, i) {
|
|
||||||
return sum + i
|
|
||||||
}, 0)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
sortable: true,
|
|
||||||
field: 'sale_price',
|
|
||||||
title: '{% trans "Unit Price" %}',
|
|
||||||
formatter: function(value, row) {
|
|
||||||
return row.sale_price_string || row.sale_price;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
sortable: true,
|
|
||||||
title: '{% trans "Total price" %}',
|
|
||||||
formatter: function(value, row) {
|
|
||||||
var total = row.sale_price * row.quantity;
|
|
||||||
var formatter = new Intl.NumberFormat('en-US', {style: 'currency', currency: row.sale_price_currency});
|
|
||||||
return formatter.format(total)
|
|
||||||
},
|
|
||||||
footerFormatter: function(data) {
|
|
||||||
var total = data.map(function (row) {
|
|
||||||
return +row['sale_price']*row['quantity']
|
|
||||||
}).reduce(function (sum, i) {
|
|
||||||
return sum + i
|
|
||||||
}, 0)
|
|
||||||
var currency = (data.slice(-1)[0] && data.slice(-1)[0].sale_price_currency) || 'USD';
|
|
||||||
var formatter = new Intl.NumberFormat('en-US', {style: 'currency', currency: currency});
|
|
||||||
return formatter.format(total)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
field: 'allocated',
|
|
||||||
{% if order.status == SalesOrderStatus.PENDING %}
|
|
||||||
title: '{% trans "Allocated" %}',
|
|
||||||
{% else %}
|
|
||||||
title: '{% trans "Fulfilled" %}',
|
|
||||||
{% endif %}
|
|
||||||
formatter: function(value, row, index, field) {
|
|
||||||
{% if order.status == SalesOrderStatus.PENDING %}
|
|
||||||
var quantity = row.allocated;
|
|
||||||
{% else %}
|
|
||||||
var quantity = row.fulfilled;
|
|
||||||
{% endif %}
|
|
||||||
return makeProgressBar(quantity, row.quantity, {
|
|
||||||
id: `order-line-progress-${row.pk}`,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
sorter: function(valA, valB, rowA, rowB) {
|
|
||||||
{% if order.status == SalesOrderStatus.PENDING %}
|
|
||||||
var A = rowA.allocated;
|
|
||||||
var B = rowB.allocated;
|
|
||||||
{% else %}
|
|
||||||
var A = rowA.fulfilled;
|
|
||||||
var B = rowB.fulfilled;
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
if (A == 0 && B == 0) {
|
|
||||||
return (rowA.quantity > rowB.quantity) ? 1 : -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
var progressA = parseFloat(A) / rowA.quantity;
|
|
||||||
var progressB = parseFloat(B) / rowB.quantity;
|
|
||||||
|
|
||||||
return (progressA < progressB) ? 1 : -1;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'notes',
|
|
||||||
title: '{% trans "Notes" %}',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'po',
|
|
||||||
title: '{% trans "PO" %}',
|
|
||||||
formatter: function(value, row, index, field) {
|
|
||||||
var po_name = "";
|
|
||||||
if (row.allocated) {
|
|
||||||
row.allocations.forEach(function(allocation) {
|
|
||||||
if (allocation.po != po_name) {
|
|
||||||
if (po_name) {
|
|
||||||
po_name = "-";
|
|
||||||
} else {
|
|
||||||
po_name = allocation.po
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return `<div>` + po_name + `</div>`;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{% if order.status == SalesOrderStatus.PENDING %}
|
|
||||||
{
|
|
||||||
field: 'buttons',
|
|
||||||
formatter: function(value, row, index, field) {
|
|
||||||
|
|
||||||
var html = `<div class='btn-group float-right' role='group'>`;
|
|
||||||
|
|
||||||
var pk = row.pk;
|
|
||||||
|
|
||||||
if (row.part) {
|
|
||||||
var part = row.part_detail;
|
|
||||||
|
|
||||||
if (part.trackable) {
|
|
||||||
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" %}');
|
|
||||||
|
|
||||||
if (part.purchaseable) {
|
|
||||||
html += makeIconButton('fa-shopping-cart', 'button-buy', row.part, '{% trans "Purchase stock" %}');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (part.assembly) {
|
|
||||||
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-trash-alt icon-red', 'button-delete', pk, '{% trans "Delete line item " %}');
|
|
||||||
|
|
||||||
html += `</div>`;
|
|
||||||
|
|
||||||
return html;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{% endif %}
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
function setupCallbacks() {
|
|
||||||
|
|
||||||
var table = $("#so-lines-table");
|
|
||||||
|
|
||||||
// Set up callbacks for the row buttons
|
|
||||||
table.find(".button-edit").click(function() {
|
|
||||||
|
|
||||||
var pk = $(this).attr('pk');
|
|
||||||
|
|
||||||
constructForm(`/api/order/so-line/${pk}/`, {
|
|
||||||
fields: {
|
fields: {
|
||||||
|
order: {
|
||||||
|
value: {{ order.pk }},
|
||||||
|
hidden: true,
|
||||||
|
},
|
||||||
|
part: {},
|
||||||
quantity: {},
|
quantity: {},
|
||||||
reference: {},
|
reference: {},
|
||||||
sale_price: {},
|
sale_price: {},
|
||||||
sale_price_currency: {},
|
sale_price_currency: {},
|
||||||
notes: {},
|
notes: {},
|
||||||
},
|
},
|
||||||
title: '{% trans "Edit Line Item" %}',
|
method: 'POST',
|
||||||
|
title: '{% trans "Add Line Item" %}',
|
||||||
onSuccess: reloadTable,
|
onSuccess: reloadTable,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
table.find(".button-delete").click(function() {
|
loadSalesOrderLineItemTable(
|
||||||
var pk = $(this).attr('pk');
|
'#so-lines-table',
|
||||||
|
{
|
||||||
constructForm(`/api/order/so-line/${pk}/`, {
|
order: {{ order.pk }},
|
||||||
method: 'DELETE',
|
status: {{ order.status }},
|
||||||
title: '{% trans "Delete Line Item" %}',
|
|
||||||
onSuccess: reloadTable,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
table.find(".button-add-by-sn").click(function() {
|
|
||||||
var pk = $(this).attr('pk');
|
|
||||||
|
|
||||||
inventreeGet(`/api/order/so-line/${pk}/`, {},
|
|
||||||
{
|
|
||||||
success: function(response) {
|
|
||||||
launchModalForm('{% url "so-assign-serials" %}', {
|
|
||||||
success: reloadTable,
|
|
||||||
data: {
|
|
||||||
line: pk,
|
|
||||||
part: response.part,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
table.find(".button-add").click(function() {
|
|
||||||
var pk = $(this).attr('pk');
|
|
||||||
|
|
||||||
launchModalForm(`/order/sales-order/allocation/new/`, {
|
|
||||||
success: reloadTable,
|
|
||||||
data: {
|
|
||||||
line: pk,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
table.find(".button-build").click(function() {
|
|
||||||
|
|
||||||
var pk = $(this).attr('pk');
|
|
||||||
|
|
||||||
// Extract the row data from the table!
|
|
||||||
var idx = $(this).closest('tr').attr('data-index');
|
|
||||||
|
|
||||||
var row = table.bootstrapTable('getData')[idx];
|
|
||||||
|
|
||||||
var quantity = 1;
|
|
||||||
|
|
||||||
if (row.allocated < row.quantity) {
|
|
||||||
quantity = row.quantity - row.allocated;
|
|
||||||
}
|
}
|
||||||
|
);
|
||||||
launchModalForm(`/build/new/`, {
|
|
||||||
follow: true,
|
|
||||||
data: {
|
|
||||||
part: pk,
|
|
||||||
sales_order: {{ order.id }},
|
|
||||||
quantity: quantity,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
table.find(".button-buy").click(function() {
|
|
||||||
var pk = $(this).attr('pk');
|
|
||||||
|
|
||||||
launchModalForm("{% url 'order-parts' %}", {
|
|
||||||
data: {
|
|
||||||
parts: [pk],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
$(".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,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
attachNavCallbacks({
|
attachNavCallbacks({
|
||||||
name: 'sales-order',
|
name: 'sales-order',
|
||||||
default: 'order-items'
|
default: 'order-items'
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
@ -1,14 +0,0 @@
|
|||||||
{% extends "modal_delete_form.html" %}
|
|
||||||
{% load i18n %}
|
|
||||||
{% load inventree_extras %}
|
|
||||||
|
|
||||||
{% block pre_form_content %}
|
|
||||||
<div class='alert alert-block alert-warning'>
|
|
||||||
{% trans "This action will unallocate the following stock from the Sales Order" %}:
|
|
||||||
<br>
|
|
||||||
<strong>
|
|
||||||
{% decimal allocation.get_allocated %} x {{ allocation.line.part.full_name }}
|
|
||||||
{% if allocation.item.location %} ({{ allocation.get_location }}){% endif %}
|
|
||||||
</strong>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
@ -36,6 +36,7 @@ purchase_order_urls = [
|
|||||||
sales_order_detail_urls = [
|
sales_order_detail_urls = [
|
||||||
url(r'^cancel/', views.SalesOrderCancel.as_view(), name='so-cancel'),
|
url(r'^cancel/', views.SalesOrderCancel.as_view(), name='so-cancel'),
|
||||||
url(r'^ship/', views.SalesOrderShip.as_view(), name='so-ship'),
|
url(r'^ship/', views.SalesOrderShip.as_view(), name='so-ship'),
|
||||||
|
url(r'^export/', views.SalesOrderExport.as_view(), name='so-export'),
|
||||||
|
|
||||||
url(r'^.*$', views.SalesOrderDetail.as_view(), name='so-detail'),
|
url(r'^.*$', views.SalesOrderDetail.as_view(), name='so-detail'),
|
||||||
]
|
]
|
||||||
@ -43,12 +44,7 @@ sales_order_detail_urls = [
|
|||||||
sales_order_urls = [
|
sales_order_urls = [
|
||||||
# URLs for sales order allocations
|
# URLs for sales order allocations
|
||||||
url(r'^allocation/', include([
|
url(r'^allocation/', include([
|
||||||
url(r'^new/', views.SalesOrderAllocationCreate.as_view(), name='so-allocation-create'),
|
|
||||||
url(r'^assign-serials/', views.SalesOrderAssignSerials.as_view(), name='so-assign-serials'),
|
url(r'^assign-serials/', views.SalesOrderAssignSerials.as_view(), name='so-assign-serials'),
|
||||||
url(r'(?P<pk>\d+)/', include([
|
|
||||||
url(r'^edit/', views.SalesOrderAllocationEdit.as_view(), name='so-allocation-edit'),
|
|
||||||
url(r'^delete/', views.SalesOrderAllocationDelete.as_view(), name='so-allocation-delete'),
|
|
||||||
])),
|
|
||||||
])),
|
])),
|
||||||
|
|
||||||
# Display detail view for a single SalesOrder
|
# Display detail view for a single SalesOrder
|
||||||
|
@ -23,13 +23,12 @@ from decimal import Decimal, InvalidOperation
|
|||||||
from .models import PurchaseOrder, PurchaseOrderLineItem
|
from .models import PurchaseOrder, PurchaseOrderLineItem
|
||||||
from .models import SalesOrder, SalesOrderLineItem
|
from .models import SalesOrder, SalesOrderLineItem
|
||||||
from .models import SalesOrderAllocation
|
from .models import SalesOrderAllocation
|
||||||
from .admin import POLineItemResource
|
from .admin import POLineItemResource, SOLineItemResource
|
||||||
from build.models import Build
|
from build.models import Build
|
||||||
from company.models import Company, SupplierPart # ManufacturerPart
|
from company.models import Company, SupplierPart # ManufacturerPart
|
||||||
from stock.models import StockItem
|
from stock.models import StockItem
|
||||||
from part.models import Part
|
from part.models import Part
|
||||||
|
|
||||||
from common.models import InvenTreeSetting
|
|
||||||
from common.forms import UploadFileForm, MatchFieldForm
|
from common.forms import UploadFileForm, MatchFieldForm
|
||||||
from common.views import FileManagementFormView
|
from common.views import FileManagementFormView
|
||||||
from common.files import FileManager
|
from common.files import FileManager
|
||||||
@ -37,7 +36,7 @@ from common.files import FileManager
|
|||||||
from . import forms as order_forms
|
from . import forms as order_forms
|
||||||
from part.views import PartPricing
|
from part.views import PartPricing
|
||||||
|
|
||||||
from InvenTree.views import AjaxView, AjaxCreateView, AjaxUpdateView, AjaxDeleteView
|
from InvenTree.views import AjaxView, AjaxUpdateView
|
||||||
from InvenTree.helpers import DownloadFile, str2bool
|
from InvenTree.helpers import DownloadFile, str2bool
|
||||||
from InvenTree.helpers import extract_serial_numbers
|
from InvenTree.helpers import extract_serial_numbers
|
||||||
from InvenTree.views import InvenTreeRoleMixin
|
from InvenTree.views import InvenTreeRoleMixin
|
||||||
@ -437,6 +436,33 @@ class PurchaseOrderUpload(FileManagementFormView):
|
|||||||
return HttpResponseRedirect(reverse('po-detail', kwargs={'pk': self.kwargs['pk']}))
|
return HttpResponseRedirect(reverse('po-detail', kwargs={'pk': self.kwargs['pk']}))
|
||||||
|
|
||||||
|
|
||||||
|
class SalesOrderExport(AjaxView):
|
||||||
|
"""
|
||||||
|
Export a sales order
|
||||||
|
|
||||||
|
- File format can optionally be passed as a query parameter e.g. ?format=CSV
|
||||||
|
- Default file format is CSV
|
||||||
|
"""
|
||||||
|
|
||||||
|
model = SalesOrder
|
||||||
|
|
||||||
|
role_required = 'sales_order.view'
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
|
||||||
|
order = get_object_or_404(SalesOrder, pk=self.kwargs.get('pk', None))
|
||||||
|
|
||||||
|
export_format = request.GET.get('format', 'csv')
|
||||||
|
|
||||||
|
filename = f"{str(order)} - {order.customer.name}.{export_format}"
|
||||||
|
|
||||||
|
dataset = SOLineItemResource().export(queryset=order.lines.all())
|
||||||
|
|
||||||
|
filedata = dataset.export(format=export_format)
|
||||||
|
|
||||||
|
return DownloadFile(filedata, filename)
|
||||||
|
|
||||||
|
|
||||||
class PurchaseOrderExport(AjaxView):
|
class PurchaseOrderExport(AjaxView):
|
||||||
""" File download for a purchase order
|
""" File download for a purchase order
|
||||||
|
|
||||||
@ -451,7 +477,7 @@ class PurchaseOrderExport(AjaxView):
|
|||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
|
|
||||||
order = get_object_or_404(PurchaseOrder, pk=self.kwargs['pk'])
|
order = get_object_or_404(PurchaseOrder, pk=self.kwargs.get('pk', None))
|
||||||
|
|
||||||
export_format = request.GET.get('format', 'csv')
|
export_format = request.GET.get('format', 'csv')
|
||||||
|
|
||||||
@ -976,105 +1002,6 @@ class SalesOrderAssignSerials(AjaxView, FormMixin):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class SalesOrderAllocationCreate(AjaxCreateView):
|
|
||||||
""" View for creating a new SalesOrderAllocation """
|
|
||||||
|
|
||||||
model = SalesOrderAllocation
|
|
||||||
form_class = order_forms.CreateSalesOrderAllocationForm
|
|
||||||
ajax_form_title = _('Allocate Stock to Order')
|
|
||||||
|
|
||||||
def get_initial(self):
|
|
||||||
initials = super().get_initial().copy()
|
|
||||||
|
|
||||||
line_id = self.request.GET.get('line', None)
|
|
||||||
|
|
||||||
if line_id is not None:
|
|
||||||
line = SalesOrderLineItem.objects.get(pk=line_id)
|
|
||||||
|
|
||||||
initials['line'] = line
|
|
||||||
|
|
||||||
# Search for matching stock items, pre-fill if there is only one
|
|
||||||
items = StockItem.objects.filter(part=line.part)
|
|
||||||
|
|
||||||
quantity = line.quantity - line.allocated_quantity()
|
|
||||||
|
|
||||||
if quantity < 0:
|
|
||||||
quantity = 0
|
|
||||||
|
|
||||||
if items.count() == 1:
|
|
||||||
item = items.first()
|
|
||||||
initials['item'] = item
|
|
||||||
|
|
||||||
# Reduce the quantity IF there is not enough stock
|
|
||||||
qmax = item.quantity - item.allocation_count()
|
|
||||||
|
|
||||||
if qmax < quantity:
|
|
||||||
quantity = qmax
|
|
||||||
|
|
||||||
initials['quantity'] = quantity
|
|
||||||
|
|
||||||
return initials
|
|
||||||
|
|
||||||
def get_form(self):
|
|
||||||
|
|
||||||
form = super().get_form()
|
|
||||||
|
|
||||||
line_id = form['line'].value()
|
|
||||||
|
|
||||||
# If a line item has been specified, reduce the queryset for the stockitem accordingly
|
|
||||||
try:
|
|
||||||
line = SalesOrderLineItem.objects.get(pk=line_id)
|
|
||||||
|
|
||||||
# Construct a queryset for allowable stock items
|
|
||||||
queryset = StockItem.objects.filter(StockItem.IN_STOCK_FILTER)
|
|
||||||
|
|
||||||
# Ensure the part reference matches
|
|
||||||
queryset = queryset.filter(part=line.part)
|
|
||||||
|
|
||||||
# Exclude StockItem which are already allocated to this order
|
|
||||||
allocated = [allocation.item.pk for allocation in line.allocations.all()]
|
|
||||||
|
|
||||||
queryset = queryset.exclude(pk__in=allocated)
|
|
||||||
|
|
||||||
# Exclude stock items which have expired
|
|
||||||
if not InvenTreeSetting.get_setting('STOCK_ALLOW_EXPIRED_SALE'):
|
|
||||||
queryset = queryset.exclude(StockItem.EXPIRED_FILTER)
|
|
||||||
|
|
||||||
form.fields['item'].queryset = queryset
|
|
||||||
|
|
||||||
# Hide the 'line' field
|
|
||||||
form.fields['line'].widget = HiddenInput()
|
|
||||||
|
|
||||||
except (ValueError, SalesOrderLineItem.DoesNotExist):
|
|
||||||
pass
|
|
||||||
|
|
||||||
return form
|
|
||||||
|
|
||||||
|
|
||||||
class SalesOrderAllocationEdit(AjaxUpdateView):
|
|
||||||
|
|
||||||
model = SalesOrderAllocation
|
|
||||||
form_class = order_forms.EditSalesOrderAllocationForm
|
|
||||||
ajax_form_title = _('Edit Allocation Quantity')
|
|
||||||
|
|
||||||
def get_form(self):
|
|
||||||
form = super().get_form()
|
|
||||||
|
|
||||||
# Prevent the user from editing particular fields
|
|
||||||
form.fields.pop('item')
|
|
||||||
form.fields.pop('line')
|
|
||||||
|
|
||||||
return form
|
|
||||||
|
|
||||||
|
|
||||||
class SalesOrderAllocationDelete(AjaxDeleteView):
|
|
||||||
|
|
||||||
model = SalesOrderAllocation
|
|
||||||
ajax_form_title = _("Remove allocation")
|
|
||||||
context_object_name = 'allocation'
|
|
||||||
ajax_template_name = "order/so_allocation_delete.html"
|
|
||||||
|
|
||||||
|
|
||||||
class LineItemPricing(PartPricing):
|
class LineItemPricing(PartPricing):
|
||||||
""" View for inspecting part pricing information """
|
""" View for inspecting part pricing information """
|
||||||
|
|
||||||
|
@ -189,12 +189,15 @@ def ExportBom(part, fmt='csv', cascade=False, max_levels=None, parameter_data=Fa
|
|||||||
# Process manufacturer part
|
# Process manufacturer part
|
||||||
for manufacturer_idx, manufacturer_part in enumerate(manufacturer_parts):
|
for manufacturer_idx, manufacturer_part in enumerate(manufacturer_parts):
|
||||||
|
|
||||||
if manufacturer_part:
|
if manufacturer_part and manufacturer_part.manufacturer:
|
||||||
manufacturer_name = manufacturer_part.manufacturer.name
|
manufacturer_name = manufacturer_part.manufacturer.name
|
||||||
else:
|
else:
|
||||||
manufacturer_name = ''
|
manufacturer_name = ''
|
||||||
|
|
||||||
manufacturer_mpn = manufacturer_part.MPN
|
if manufacturer_part:
|
||||||
|
manufacturer_mpn = manufacturer_part.MPN
|
||||||
|
else:
|
||||||
|
manufacturer_mpn = ''
|
||||||
|
|
||||||
# Generate column names for this manufacturer
|
# Generate column names for this manufacturer
|
||||||
k_man = manufacturer_headers[0] + "_" + str(manufacturer_idx)
|
k_man = manufacturer_headers[0] + "_" + str(manufacturer_idx)
|
||||||
@ -210,12 +213,15 @@ def ExportBom(part, fmt='csv', cascade=False, max_levels=None, parameter_data=Fa
|
|||||||
# Process supplier parts
|
# Process supplier parts
|
||||||
for supplier_idx, supplier_part in enumerate(manufacturer_part.supplier_parts.all()):
|
for supplier_idx, supplier_part in enumerate(manufacturer_part.supplier_parts.all()):
|
||||||
|
|
||||||
if supplier_part.supplier:
|
if supplier_part.supplier and supplier_part.supplier:
|
||||||
supplier_name = supplier_part.supplier.name
|
supplier_name = supplier_part.supplier.name
|
||||||
else:
|
else:
|
||||||
supplier_name = ''
|
supplier_name = ''
|
||||||
|
|
||||||
supplier_sku = supplier_part.SKU
|
if supplier_part:
|
||||||
|
supplier_sku = supplier_part.SKU
|
||||||
|
else:
|
||||||
|
supplier_sku = ''
|
||||||
|
|
||||||
# Generate column names for this supplier
|
# Generate column names for this supplier
|
||||||
k_sup = str(supplier_headers[0]) + "_" + str(manufacturer_idx) + "_" + str(supplier_idx)
|
k_sup = str(supplier_headers[0]) + "_" + str(manufacturer_idx) + "_" + str(supplier_idx)
|
||||||
|
@ -328,6 +328,12 @@
|
|||||||
// If image / thumbnail data present, live update
|
// If image / thumbnail data present, live update
|
||||||
if (data.image) {
|
if (data.image) {
|
||||||
$('#part-image').attr('src', data.image);
|
$('#part-image').attr('src', data.image);
|
||||||
|
|
||||||
|
// Reset the "modal image" view
|
||||||
|
$('#part-thumb').click(function() {
|
||||||
|
showModalImage(data.image);
|
||||||
|
});
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// Otherwise, reload the page
|
// Otherwise, reload the page
|
||||||
location.reload();
|
location.reload();
|
||||||
|
@ -34,6 +34,7 @@ from company.models import Company, SupplierPart
|
|||||||
from company.serializers import CompanySerializer, SupplierPartSerializer
|
from company.serializers import CompanySerializer, SupplierPartSerializer
|
||||||
|
|
||||||
from order.models import PurchaseOrder
|
from order.models import PurchaseOrder
|
||||||
|
from order.models import SalesOrder, SalesOrderAllocation
|
||||||
from order.serializers import POSerializer
|
from order.serializers import POSerializer
|
||||||
|
|
||||||
import common.settings
|
import common.settings
|
||||||
@ -645,6 +646,31 @@ class StockList(generics.ListCreateAPIView):
|
|||||||
# Filter StockItem without build allocations or sales order allocations
|
# Filter StockItem without build allocations or sales order allocations
|
||||||
queryset = queryset.filter(Q(sales_order_allocations__isnull=True) & Q(allocations__isnull=True))
|
queryset = queryset.filter(Q(sales_order_allocations__isnull=True) & Q(allocations__isnull=True))
|
||||||
|
|
||||||
|
# Exclude StockItems which are already allocated to a particular SalesOrder
|
||||||
|
exclude_so_allocation = params.get('exclude_so_allocation', None)
|
||||||
|
|
||||||
|
if exclude_so_allocation is not None:
|
||||||
|
|
||||||
|
try:
|
||||||
|
order = SalesOrder.objects.get(pk=exclude_so_allocation)
|
||||||
|
|
||||||
|
# Grab all the active SalesOrderAllocations for this order
|
||||||
|
allocations = SalesOrderAllocation.objects.filter(
|
||||||
|
line__pk__in=[
|
||||||
|
line.pk for line in order.lines.all()
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Exclude any stock item which is already allocated to the sales order
|
||||||
|
queryset = queryset.exclude(
|
||||||
|
pk__in=[
|
||||||
|
a.item.pk for a in allocations
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
except (ValueError, SalesOrder.DoesNotExist):
|
||||||
|
pass
|
||||||
|
|
||||||
# Does the client wish to filter by the Part ID?
|
# Does the client wish to filter by the Part ID?
|
||||||
part_id = params.get('part', None)
|
part_id = params.get('part', None)
|
||||||
|
|
||||||
|
@ -42,6 +42,12 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
<li class='list-group-item' title='{% trans "Forms" %}'>
|
||||||
|
<a href='#' class='nav-toggle' id='select-user-forms'>
|
||||||
|
<span class='fas fa-table'></span>{% trans "Forms" %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
<li class='list-group-item' title='{% trans "Settings" %}'>
|
<li class='list-group-item' title='{% trans "Settings" %}'>
|
||||||
<a href='#' class='nav-toggle' id='select-user-settings'>
|
<a href='#' class='nav-toggle' id='select-user-settings'>
|
||||||
|
@ -17,7 +17,6 @@
|
|||||||
{% include "InvenTree/settings/setting.html" with key="PART_IPN_REGEX" %}
|
{% include "InvenTree/settings/setting.html" with key="PART_IPN_REGEX" %}
|
||||||
{% include "InvenTree/settings/setting.html" with key="PART_ALLOW_DUPLICATE_IPN" %}
|
{% include "InvenTree/settings/setting.html" with key="PART_ALLOW_DUPLICATE_IPN" %}
|
||||||
{% include "InvenTree/settings/setting.html" with key="PART_ALLOW_EDIT_IPN" %}
|
{% include "InvenTree/settings/setting.html" with key="PART_ALLOW_EDIT_IPN" %}
|
||||||
{% include "InvenTree/settings/setting.html" with key="PART_SHOW_QUANTITY_IN_FORMS" icon="fa-hashtag" %}
|
|
||||||
{% include "InvenTree/settings/setting.html" with key="PART_SHOW_PRICE_IN_FORMS" icon="fa-dollar-sign" %}
|
{% include "InvenTree/settings/setting.html" with key="PART_SHOW_PRICE_IN_FORMS" icon="fa-dollar-sign" %}
|
||||||
{% include "InvenTree/settings/setting.html" with key="PART_SHOW_RELATED" icon="fa-random" %}
|
{% include "InvenTree/settings/setting.html" with key="PART_SHOW_RELATED" icon="fa-random" %}
|
||||||
{% include "InvenTree/settings/setting.html" with key="PART_CREATE_INITIAL" icon="fa-boxes" %}
|
{% include "InvenTree/settings/setting.html" with key="PART_CREATE_INITIAL" icon="fa-boxes" %}
|
||||||
|
@ -20,6 +20,7 @@
|
|||||||
{% include "InvenTree/settings/user_search.html" %}
|
{% include "InvenTree/settings/user_search.html" %}
|
||||||
{% include "InvenTree/settings/user_labels.html" %}
|
{% include "InvenTree/settings/user_labels.html" %}
|
||||||
{% include "InvenTree/settings/user_reports.html" %}
|
{% include "InvenTree/settings/user_reports.html" %}
|
||||||
|
{% include "InvenTree/settings/user_forms.html" %}
|
||||||
|
|
||||||
{% if user.is_staff %}
|
{% if user.is_staff %}
|
||||||
|
|
||||||
|
22
InvenTree/templates/InvenTree/settings/user_forms.html
Normal file
22
InvenTree/templates/InvenTree/settings/user_forms.html
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{% extends "panel.html" %}
|
||||||
|
|
||||||
|
{% load i18n %}
|
||||||
|
{% load inventree_extras %}
|
||||||
|
|
||||||
|
{% block label %}user-forms{% endblock %}
|
||||||
|
|
||||||
|
{% block heading %}
|
||||||
|
{% trans "Form Settings" %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<div class='row'>
|
||||||
|
<table class='table table-striped table-condensed'>
|
||||||
|
<tbody>
|
||||||
|
{% include "InvenTree/settings/setting.html" with key="PART_SHOW_QUANTITY_IN_FORMS" icon="fa-hashtag" user_setting=True %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
@ -10,6 +10,7 @@
|
|||||||
/* exported
|
/* exported
|
||||||
attachClipboard,
|
attachClipboard,
|
||||||
enableDragAndDrop,
|
enableDragAndDrop,
|
||||||
|
exportFormatOptions,
|
||||||
inventreeDocReady,
|
inventreeDocReady,
|
||||||
inventreeLoad,
|
inventreeLoad,
|
||||||
inventreeSave,
|
inventreeSave,
|
||||||
@ -46,6 +47,31 @@ function attachClipboard(selector, containerselector, textElement) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a standard list of export format options *
|
||||||
|
*/
|
||||||
|
function exportFormatOptions() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
value: 'csv',
|
||||||
|
display_name: 'CSV',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'tsv',
|
||||||
|
display_name: 'TSV',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'xls',
|
||||||
|
display_name: 'XLS',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'xlsx',
|
||||||
|
display_name: 'XLSX',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
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"
|
||||||
|
@ -42,6 +42,8 @@ function buildFormFields() {
|
|||||||
part_detail: true,
|
part_detail: true,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
sales_order: {
|
||||||
|
},
|
||||||
batch: {},
|
batch: {},
|
||||||
target_date: {},
|
target_date: {},
|
||||||
take_from: {},
|
take_from: {},
|
||||||
@ -76,23 +78,32 @@ function newBuildOrder(options={}) {
|
|||||||
|
|
||||||
var fields = buildFormFields();
|
var fields = buildFormFields();
|
||||||
|
|
||||||
|
// Specify the target part
|
||||||
if (options.part) {
|
if (options.part) {
|
||||||
fields.part.value = options.part;
|
fields.part.value = options.part;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Specify the desired quantity
|
||||||
if (options.quantity) {
|
if (options.quantity) {
|
||||||
fields.quantity.value = options.quantity;
|
fields.quantity.value = options.quantity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Specify the parent build order
|
||||||
if (options.parent) {
|
if (options.parent) {
|
||||||
fields.parent.value = options.parent;
|
fields.parent.value = options.parent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Specify a parent sales order
|
||||||
|
if (options.sales_order) {
|
||||||
|
fields.sales_order.value = options.sales_order;
|
||||||
|
}
|
||||||
|
|
||||||
constructForm(`/api/build/`, {
|
constructForm(`/api/build/`, {
|
||||||
fields: fields,
|
fields: fields,
|
||||||
follow: true,
|
follow: true,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
title: '{% trans "Create Build Order" %}'
|
title: '{% trans "Create Build Order" %}',
|
||||||
|
onSuccess: options.onSuccess,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1568,6 +1568,9 @@ function renderModelData(name, model, data, parameters, options) {
|
|||||||
case 'partparametertemplate':
|
case 'partparametertemplate':
|
||||||
renderer = renderPartParameterTemplate;
|
renderer = renderPartParameterTemplate;
|
||||||
break;
|
break;
|
||||||
|
case 'salesorder':
|
||||||
|
renderer = renderSalesOrder;
|
||||||
|
break;
|
||||||
case 'manufacturerpart':
|
case 'manufacturerpart':
|
||||||
renderer = renderManufacturerPart;
|
renderer = renderManufacturerPart;
|
||||||
break;
|
break;
|
||||||
|
@ -159,7 +159,24 @@ function renderPart(name, data, parameters, options) {
|
|||||||
html += ` - <i>${data.description}</i>`;
|
html += ` - <i>${data.description}</i>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
html += `<span class='float-right'><small>{% trans "Part ID" %}: ${data.pk}</small></span>`;
|
var stock = '';
|
||||||
|
|
||||||
|
// Display available part quantity
|
||||||
|
if (user_settings.PART_SHOW_QUANTITY_IN_FORMS) {
|
||||||
|
if (data.in_stock == 0) {
|
||||||
|
stock = `<span class='label-form label-red'>{% trans "No Stock" %}</span>`;
|
||||||
|
} else {
|
||||||
|
stock = `<span class='label-form label-green'>{% trans "In Stock" %}: ${data.in_stock}</span>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
html += `
|
||||||
|
<span class='float-right'>
|
||||||
|
<small>
|
||||||
|
${stock}
|
||||||
|
{% trans "Part ID" %}: ${data.pk}
|
||||||
|
</small>
|
||||||
|
</span>`;
|
||||||
|
|
||||||
return html;
|
return html;
|
||||||
}
|
}
|
||||||
@ -199,6 +216,26 @@ function renderOwner(name, data, parameters, options) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Renderer for "SalesOrder" model
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
function renderSalesOrder(name, data, parameters, options) {
|
||||||
|
var html = `<span>${data.reference}</span>`;
|
||||||
|
|
||||||
|
if (data.description) {
|
||||||
|
html += ` - <i>${data.description}</i>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
html += `
|
||||||
|
<span class='float-right'>
|
||||||
|
<small>
|
||||||
|
{% trans "Order ID" %}: ${data.pk}
|
||||||
|
</small>
|
||||||
|
</span>`;
|
||||||
|
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Renderer for "PartCategory" model
|
// Renderer for "PartCategory" model
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
function renderPartCategory(name, data, parameters, options) {
|
function renderPartCategory(name, data, parameters, options) {
|
||||||
|
@ -21,9 +21,11 @@
|
|||||||
/* exported
|
/* exported
|
||||||
createSalesOrder,
|
createSalesOrder,
|
||||||
editPurchaseOrderLineItem,
|
editPurchaseOrderLineItem,
|
||||||
|
exportOrder,
|
||||||
loadPurchaseOrderLineItemTable,
|
loadPurchaseOrderLineItemTable,
|
||||||
loadPurchaseOrderTable,
|
loadPurchaseOrderTable,
|
||||||
loadSalesOrderAllocationTable,
|
loadSalesOrderAllocationTable,
|
||||||
|
loadSalesOrderLineItemTable,
|
||||||
loadSalesOrderTable,
|
loadSalesOrderTable,
|
||||||
newPurchaseOrderFromOrderWizard,
|
newPurchaseOrderFromOrderWizard,
|
||||||
newSupplierPartFromOrderWizard,
|
newSupplierPartFromOrderWizard,
|
||||||
@ -186,6 +188,49 @@ function newSupplierPartFromOrderWizard(e) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export an order (PurchaseOrder or SalesOrder)
|
||||||
|
*
|
||||||
|
* - Display a simple form which presents the user with export options
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
function exportOrder(redirect_url, options={}) {
|
||||||
|
|
||||||
|
var format = options.format;
|
||||||
|
|
||||||
|
// If default format is not provided, lookup
|
||||||
|
if (!format) {
|
||||||
|
format = inventreeLoad('order-export-format', 'csv');
|
||||||
|
}
|
||||||
|
|
||||||
|
constructFormBody({}, {
|
||||||
|
title: '{% trans "Export Order" %}',
|
||||||
|
fields: {
|
||||||
|
format: {
|
||||||
|
label: '{% trans "Format" %}',
|
||||||
|
help_text: '{% trans "Select file format" %}',
|
||||||
|
required: true,
|
||||||
|
type: 'choice',
|
||||||
|
value: format,
|
||||||
|
choices: exportFormatOptions(),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSubmit: function(fields, opts) {
|
||||||
|
|
||||||
|
var format = getFormFieldValue('format', fields['format'], opts);
|
||||||
|
|
||||||
|
// Save the format for next time
|
||||||
|
inventreeSave('order-export-format', format);
|
||||||
|
|
||||||
|
// Hide the modal
|
||||||
|
$(opts.modal).modal('hide');
|
||||||
|
|
||||||
|
// Download the file!
|
||||||
|
location.href = `${redirect_url}?format=${format}`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function newPurchaseOrderFromOrderWizard(e) {
|
function newPurchaseOrderFromOrderWizard(e) {
|
||||||
/* Create a new purchase order directly from an order form.
|
/* Create a new purchase order directly from an order form.
|
||||||
* Launches a secondary modal and (if successful),
|
* Launches a secondary modal and (if successful),
|
||||||
@ -531,6 +576,7 @@ function editPurchaseOrderLineItem(e) {
|
|||||||
|
|
||||||
var url = $(src).attr('url');
|
var url = $(src).attr('url');
|
||||||
|
|
||||||
|
// TODO: Migrate this to the API forms
|
||||||
launchModalForm(url, {
|
launchModalForm(url, {
|
||||||
reload: true,
|
reload: true,
|
||||||
});
|
});
|
||||||
@ -546,7 +592,8 @@ function removePurchaseOrderLineItem(e) {
|
|||||||
var src = e.target || e.srcElement;
|
var src = e.target || e.srcElement;
|
||||||
|
|
||||||
var url = $(src).attr('url');
|
var url = $(src).attr('url');
|
||||||
|
|
||||||
|
// TODO: Migrate this to the API forms
|
||||||
launchModalForm(url, {
|
launchModalForm(url, {
|
||||||
reload: true,
|
reload: true,
|
||||||
});
|
});
|
||||||
@ -1126,3 +1173,601 @@ function loadSalesOrderAllocationTable(table, options={}) {
|
|||||||
]
|
]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display an "allocations" sub table, showing stock items allocated againt a sales order
|
||||||
|
* @param {*} index
|
||||||
|
* @param {*} row
|
||||||
|
* @param {*} element
|
||||||
|
*/
|
||||||
|
function showAllocationSubTable(index, row, element, options) {
|
||||||
|
|
||||||
|
// Construct a sub-table element
|
||||||
|
var html = `
|
||||||
|
<div class='sub-table'>
|
||||||
|
<table class='table table-striped table-condensed' id='allocation-table-${row.pk}'>
|
||||||
|
</table>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
element.html(html);
|
||||||
|
|
||||||
|
var table = $(`#allocation-table-${row.pk}`);
|
||||||
|
|
||||||
|
// Is the parent SalesOrder pending?
|
||||||
|
var pending = options.status == {{ SalesOrderStatus.PENDING }};
|
||||||
|
|
||||||
|
function setupCallbacks() {
|
||||||
|
// Add callbacks for 'edit' buttons
|
||||||
|
table.find('.button-allocation-edit').click(function() {
|
||||||
|
|
||||||
|
var pk = $(this).attr('pk');
|
||||||
|
|
||||||
|
// Edit the sales order alloction
|
||||||
|
constructForm(
|
||||||
|
`/api/order/so-allocation/${pk}/`,
|
||||||
|
{
|
||||||
|
fields: {
|
||||||
|
quantity: {},
|
||||||
|
},
|
||||||
|
title: '{% trans "Edit Stock Allocation" %}',
|
||||||
|
onSuccess: function() {
|
||||||
|
// Refresh the parent table
|
||||||
|
$(options.table).bootstrapTable('refresh');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add callbacks for 'delete' buttons
|
||||||
|
table.find('.button-allocation-delete').click(function() {
|
||||||
|
var pk = $(this).attr('pk');
|
||||||
|
|
||||||
|
constructForm(
|
||||||
|
`/api/order/so-allocation/${pk}/`,
|
||||||
|
{
|
||||||
|
method: 'DELETE',
|
||||||
|
confirmMessage: '{% trans "Confirm Delete Operation" %}',
|
||||||
|
title: '{% trans "Delete Stock Allocation" %}',
|
||||||
|
onSuccess: function() {
|
||||||
|
// Refresh the parent table
|
||||||
|
$(options.table).bootstrapTable('refresh');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
table.bootstrapTable({
|
||||||
|
onPostBody: setupCallbacks,
|
||||||
|
data: row.allocations,
|
||||||
|
showHeader: false,
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
field: 'allocated',
|
||||||
|
title: '{% trans "Quantity" %}',
|
||||||
|
formatter: function(value, row, index, field) {
|
||||||
|
var text = '';
|
||||||
|
|
||||||
|
if (row.serial != null && row.quantity == 1) {
|
||||||
|
text = `{% trans "Serial Number" %}: ${row.serial}`;
|
||||||
|
} else {
|
||||||
|
text = `{% trans "Quantity" %}: ${row.quantity}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return renderLink(text, `/stock/item/${row.item}/`);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'location',
|
||||||
|
title: '{% trans "Location" %}',
|
||||||
|
formatter: function(value, row, index, field) {
|
||||||
|
|
||||||
|
// Location specified
|
||||||
|
if (row.location) {
|
||||||
|
return renderLink(
|
||||||
|
row.location_detail.pathstring || '{% trans "Location" %}',
|
||||||
|
`/stock/location/${row.location}/`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return `<i>{% trans "Stock location not specified" %}`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// TODO: ?? What is 'po' field all about?
|
||||||
|
/*
|
||||||
|
{
|
||||||
|
field: 'po'
|
||||||
|
},
|
||||||
|
*/
|
||||||
|
{
|
||||||
|
field: 'buttons',
|
||||||
|
title: '{% trans "Actions" %}',
|
||||||
|
formatter: function(value, row, index, field) {
|
||||||
|
|
||||||
|
var html = `<div class='btn-group float-right' role='group'>`;
|
||||||
|
var pk = row.pk;
|
||||||
|
|
||||||
|
if (pending) {
|
||||||
|
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 += '</div>';
|
||||||
|
|
||||||
|
return html;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display a "fulfilled" sub table, showing stock items fulfilled against a purchase order
|
||||||
|
*/
|
||||||
|
function showFulfilledSubTable(index, row, element, options) {
|
||||||
|
// Construct a table showing stock items which have been fulfilled against this line item
|
||||||
|
|
||||||
|
if (!options.order) {
|
||||||
|
return 'ERROR: Order ID not supplied';
|
||||||
|
}
|
||||||
|
|
||||||
|
var id = `fulfilled-table-${row.pk}`;
|
||||||
|
|
||||||
|
var html = `
|
||||||
|
<div class='sub-table'>
|
||||||
|
<table class='table table-striped table-condensed' id='${id}'>
|
||||||
|
</table>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
element.html(html);
|
||||||
|
|
||||||
|
$(`#${id}`).bootstrapTable({
|
||||||
|
url: '{% url "api-stock-list" %}',
|
||||||
|
queryParams: {
|
||||||
|
part: row.part,
|
||||||
|
sales_order: options.order,
|
||||||
|
},
|
||||||
|
showHeader: false,
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
field: 'pk',
|
||||||
|
visible: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'stock',
|
||||||
|
formatter: function(value, row) {
|
||||||
|
var text = '';
|
||||||
|
if (row.serial && row.quantity == 1) {
|
||||||
|
text = `{% trans "Serial Number" %}: ${row.serial}`;
|
||||||
|
} else {
|
||||||
|
text = `{% trans "Quantity" %}: ${row.quantity}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return renderLink(text, `/stock/item/${row.pk}/`);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
/*
|
||||||
|
{
|
||||||
|
field: 'po'
|
||||||
|
},
|
||||||
|
*/
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load a table displaying line items for a particular SalesOrder
|
||||||
|
*
|
||||||
|
* @param {String} table : HTML ID tag e.g. '#table'
|
||||||
|
* @param {Object} options : object which contains:
|
||||||
|
* - order {integer} : pk of the SalesOrder
|
||||||
|
* - status: {integer} : status code for the order
|
||||||
|
*/
|
||||||
|
function loadSalesOrderLineItemTable(table, options={}) {
|
||||||
|
|
||||||
|
options.table = table;
|
||||||
|
|
||||||
|
options.params = options.params || {};
|
||||||
|
|
||||||
|
if (!options.order) {
|
||||||
|
console.log('ERROR: function called without order ID');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!options.status) {
|
||||||
|
console.log('ERROR: function called without order status');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
options.params.order = options.order;
|
||||||
|
options.params.part_detail = true;
|
||||||
|
options.params.allocations = true;
|
||||||
|
|
||||||
|
var filters = loadTableFilters('salesorderlineitem');
|
||||||
|
|
||||||
|
for (var key in options.params) {
|
||||||
|
filters[key] = options.params[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
options.url = options.url || '{% url "api-so-line-list" %}';
|
||||||
|
|
||||||
|
var filter_target = options.filter_target || '#filter-list-sales-order-lines';
|
||||||
|
|
||||||
|
setupFilterList('salesorderlineitems', $(table), filter_target);
|
||||||
|
|
||||||
|
// Is the order pending?
|
||||||
|
var pending = options.status == {{ SalesOrderStatus.PENDING }};
|
||||||
|
|
||||||
|
// Has the order shipped?
|
||||||
|
var shipped = options.status == {{ SalesOrderStatus.SHIPPED }};
|
||||||
|
|
||||||
|
// Show detail view if the PurchaseOrder is PENDING or SHIPPED
|
||||||
|
var show_detail = pending || shipped;
|
||||||
|
|
||||||
|
// Table columns to display
|
||||||
|
var columns = [
|
||||||
|
/*
|
||||||
|
{
|
||||||
|
checkbox: true,
|
||||||
|
visible: true,
|
||||||
|
switchable: false,
|
||||||
|
},
|
||||||
|
*/
|
||||||
|
{
|
||||||
|
sortable: true,
|
||||||
|
sortName: 'part__name',
|
||||||
|
field: 'part',
|
||||||
|
title: '{% trans "Part" %}',
|
||||||
|
switchable: false,
|
||||||
|
formatter: function(value, row, index, field) {
|
||||||
|
if (row.part) {
|
||||||
|
return imageHoverIcon(row.part_detail.thumbnail) + renderLink(row.part_detail.full_name, `/part/${value}/`);
|
||||||
|
} else {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
footerFormatter: function() {
|
||||||
|
return '{% trans "Total" %}';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sortable: true,
|
||||||
|
field: 'reference',
|
||||||
|
title: '{% trans "Reference" %}',
|
||||||
|
switchable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sortable: true,
|
||||||
|
field: 'quantity',
|
||||||
|
title: '{% trans "Quantity" %}',
|
||||||
|
footerFormatter: function(data) {
|
||||||
|
return data.map(function(row) {
|
||||||
|
return +row['quantity'];
|
||||||
|
}).reduce(function(sum, i) {
|
||||||
|
return sum + i;
|
||||||
|
}, 0);
|
||||||
|
},
|
||||||
|
switchable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sortable: true,
|
||||||
|
field: 'sale_price',
|
||||||
|
title: '{% trans "Unit Price" %}',
|
||||||
|
formatter: function(value, row) {
|
||||||
|
return row.sale_price_string || row.sale_price;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sortable: true,
|
||||||
|
title: '{% trans "Total price" %}',
|
||||||
|
formatter: function(value, row) {
|
||||||
|
var total = row.sale_price * row.quantity;
|
||||||
|
var formatter = new Intl.NumberFormat(
|
||||||
|
'en-US',
|
||||||
|
{
|
||||||
|
style: 'currency',
|
||||||
|
currency: row.sale_price_currency
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return formatter.format(total);
|
||||||
|
},
|
||||||
|
footerFormatter: function(data) {
|
||||||
|
var total = data.map(function(row) {
|
||||||
|
return +row['sale_price'] * row['quantity'];
|
||||||
|
}).reduce(function(sum, i) {
|
||||||
|
return sum + i;
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
var currency = (data.slice(-1)[0] && data.slice(-1)[0].sale_price_currency) || 'USD';
|
||||||
|
|
||||||
|
var formatter = new Intl.NumberFormat(
|
||||||
|
'en-US',
|
||||||
|
{
|
||||||
|
style: 'currency',
|
||||||
|
currency: currency
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return formatter.format(total);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (pending) {
|
||||||
|
columns.push(
|
||||||
|
{
|
||||||
|
field: 'stock',
|
||||||
|
title: '{% trans "In Stock" %}',
|
||||||
|
formatter: function(value, row) {
|
||||||
|
return row.part_detail.stock;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
columns.push(
|
||||||
|
{
|
||||||
|
field: 'allocated',
|
||||||
|
title: pending ? '{% trans "Allocated" %}' : '{% trans "Fulfilled" %}',
|
||||||
|
switchable: false,
|
||||||
|
formatter: function(value, row, index, field) {
|
||||||
|
|
||||||
|
var quantity = pending ? row.allocated : row.fulfilled;
|
||||||
|
return makeProgressBar(quantity, row.quantity, {
|
||||||
|
id: `order-line-progress-${row.pk}`,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
sorter: function(valA, valB, rowA, rowB) {
|
||||||
|
|
||||||
|
var A = pending ? rowA.allocated : rowA.fulfilled;
|
||||||
|
var B = pending ? rowB.allocated : rowB.fulfilled;
|
||||||
|
|
||||||
|
if (A == 0 && B == 0) {
|
||||||
|
return (rowA.quantity > rowB.quantity) ? 1 : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var progressA = parseFloat(A) / rowA.quantity;
|
||||||
|
var progressB = parseFloat(B) / rowB.quantity;
|
||||||
|
|
||||||
|
return (progressA < progressB) ? 1 : -1;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'notes',
|
||||||
|
title: '{% trans "Notes" %}',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (pending) {
|
||||||
|
columns.push({
|
||||||
|
field: 'buttons',
|
||||||
|
formatter: function(value, row, index, field) {
|
||||||
|
|
||||||
|
var html = `<div class='btn-group float-right' role='group'>`;
|
||||||
|
|
||||||
|
var pk = row.pk;
|
||||||
|
|
||||||
|
if (row.part) {
|
||||||
|
var part = row.part_detail;
|
||||||
|
|
||||||
|
if (part.trackable) {
|
||||||
|
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" %}');
|
||||||
|
|
||||||
|
if (part.purchaseable) {
|
||||||
|
html += makeIconButton('fa-shopping-cart', 'button-buy', row.part, '{% trans "Purchase stock" %}');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (part.assembly) {
|
||||||
|
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-trash-alt icon-red', 'button-delete', pk, '{% trans "Delete line item " %}');
|
||||||
|
|
||||||
|
html += `</div>`;
|
||||||
|
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function reloadTable() {
|
||||||
|
$(table).bootstrapTable('refresh');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure callback functions once the table is loaded
|
||||||
|
function setupCallbacks() {
|
||||||
|
|
||||||
|
// Callback for editing line items
|
||||||
|
$(table).find('.button-edit').click(function() {
|
||||||
|
var pk = $(this).attr('pk');
|
||||||
|
|
||||||
|
constructForm(`/api/order/so-line/${pk}/`, {
|
||||||
|
fields: {
|
||||||
|
quantity: {},
|
||||||
|
reference: {},
|
||||||
|
sale_price: {},
|
||||||
|
sale_price_currency: {},
|
||||||
|
notes: {},
|
||||||
|
},
|
||||||
|
title: '{% trans "Edit Line Item" %}',
|
||||||
|
onSuccess: reloadTable,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Callback for deleting line items
|
||||||
|
$(table).find('.button-delete').click(function() {
|
||||||
|
var pk = $(this).attr('pk');
|
||||||
|
|
||||||
|
constructForm(`/api/order/so-line/${pk}/`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
title: '{% trans "Delete Line Item" %}',
|
||||||
|
onSuccess: reloadTable,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Callback for allocating stock items by serial number
|
||||||
|
$(table).find('.button-add-by-sn').click(function() {
|
||||||
|
var pk = $(this).attr('pk');
|
||||||
|
|
||||||
|
// TODO: Migrate this form to the API forms
|
||||||
|
inventreeGet(`/api/order/so-line/${pk}/`, {},
|
||||||
|
{
|
||||||
|
success: function(response) {
|
||||||
|
launchModalForm('{% url "so-assign-serials" %}', {
|
||||||
|
success: reloadTable,
|
||||||
|
data: {
|
||||||
|
line: pk,
|
||||||
|
part: response.part,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Callback for allocation stock items to the order
|
||||||
|
$(table).find('.button-add').click(function() {
|
||||||
|
var pk = $(this).attr('pk');
|
||||||
|
|
||||||
|
var line_item = $(table).bootstrapTable('getRowByUniqueId', pk);
|
||||||
|
|
||||||
|
var fields = {
|
||||||
|
// SalesOrderLineItem reference
|
||||||
|
line: {
|
||||||
|
hidden: true,
|
||||||
|
value: pk,
|
||||||
|
},
|
||||||
|
item: {
|
||||||
|
filters: {
|
||||||
|
part_detail: true,
|
||||||
|
location_detail: true,
|
||||||
|
in_stock: true,
|
||||||
|
part: line_item.part,
|
||||||
|
exclude_so_allocation: options.order,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
quantity: {
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Exclude expired stock?
|
||||||
|
if (global_settings.STOCK_ENABLE_EXPIRY && !global_settings.STOCK_ALLOW_EXPIRED_SALE) {
|
||||||
|
fields.item.filters.expired = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructForm(
|
||||||
|
`/api/order/so-allocation/`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
fields: fields,
|
||||||
|
title: '{% trans "Allocate Stock Item" %}',
|
||||||
|
onSuccess: reloadTable,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Callback for creating a new build
|
||||||
|
$(table).find('.button-build').click(function() {
|
||||||
|
var pk = $(this).attr('pk');
|
||||||
|
|
||||||
|
// Extract the row data from the table!
|
||||||
|
var idx = $(this).closest('tr').attr('data-index');
|
||||||
|
|
||||||
|
var row = $(table).bootstrapTable('getData')[idx];
|
||||||
|
|
||||||
|
var quantity = 1;
|
||||||
|
|
||||||
|
if (row.allocated < row.quantity) {
|
||||||
|
quantity = row.quantity - row.allocated;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new build order
|
||||||
|
newBuildOrder({
|
||||||
|
part: pk,
|
||||||
|
sales_order: options.order,
|
||||||
|
quantity: quantity,
|
||||||
|
success: reloadTable
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Callback for purchasing parts
|
||||||
|
$(table).find('.button-buy').click(function() {
|
||||||
|
var pk = $(this).attr('pk');
|
||||||
|
|
||||||
|
launchModalForm('{% url "order-parts" %}', {
|
||||||
|
data: {
|
||||||
|
parts: [
|
||||||
|
pk
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Callback for displaying price
|
||||||
|
$(table).find('.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,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$(table).inventreeTable({
|
||||||
|
onPostBody: setupCallbacks,
|
||||||
|
name: 'salesorderlineitems',
|
||||||
|
sidePagination: 'server',
|
||||||
|
formatNoMatches: function() {
|
||||||
|
return '{% trans "No matching line items" %}';
|
||||||
|
},
|
||||||
|
queryParams: filters,
|
||||||
|
original: options.params,
|
||||||
|
url: options.url,
|
||||||
|
showFooter: true,
|
||||||
|
uniqueId: 'pk',
|
||||||
|
detailView: show_detail,
|
||||||
|
detailViewByClick: show_detail,
|
||||||
|
detailFilter: function(index, row) {
|
||||||
|
if (pending) {
|
||||||
|
// Order is pending
|
||||||
|
return row.allocated > 0;
|
||||||
|
} else {
|
||||||
|
return row.fulfilled > 0;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
detailFormatter: function(index, row, element) {
|
||||||
|
if (pending) {
|
||||||
|
return showAllocationSubTable(index, row, element, options);
|
||||||
|
} else {
|
||||||
|
return showFulfilledSubTable(index, row, element, options);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
columns: columns,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
@ -98,24 +98,7 @@ function exportStock(params={}) {
|
|||||||
required: true,
|
required: true,
|
||||||
type: 'choice',
|
type: 'choice',
|
||||||
value: 'csv',
|
value: 'csv',
|
||||||
choices: [
|
choices: exportFormatOptions(),
|
||||||
{
|
|
||||||
value: 'csv',
|
|
||||||
display_name: 'CSV',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'tsv',
|
|
||||||
display_name: 'TSV',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'xls',
|
|
||||||
display_name: 'XLS',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'xlsx',
|
|
||||||
display_name: 'XLSX',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
sublocations: {
|
sublocations: {
|
||||||
label: '{% trans "Include Sublocations" %}',
|
label: '{% trans "Include Sublocations" %}',
|
||||||
|
40
ci/check_api_endpoint.py
Normal file
40
ci/check_api_endpoint.py
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
"""
|
||||||
|
Test that the root API endpoint is available.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import json
|
||||||
|
import requests
|
||||||
|
|
||||||
|
# We expect the server to be running on the local host
|
||||||
|
url = "http://localhost:8000/api/"
|
||||||
|
|
||||||
|
print("Testing InvenTree API endpoint")
|
||||||
|
|
||||||
|
response = requests.get(url)
|
||||||
|
|
||||||
|
assert(response.status_code == 200)
|
||||||
|
|
||||||
|
print("- Response 200 OK")
|
||||||
|
|
||||||
|
data = json.loads(response.text)
|
||||||
|
|
||||||
|
required_keys = [
|
||||||
|
'server',
|
||||||
|
'version',
|
||||||
|
'apiVersion',
|
||||||
|
'worker_running',
|
||||||
|
]
|
||||||
|
|
||||||
|
for key in required_keys:
|
||||||
|
assert(key in data)
|
||||||
|
print(f"- Found key '{key}'")
|
||||||
|
|
||||||
|
# Check that the worker is running
|
||||||
|
assert(data['worker_running'])
|
||||||
|
|
||||||
|
print("- Background worker is operational")
|
||||||
|
|
||||||
|
print("API Endpoint Tests Passed OK")
|
Loading…
Reference in New Issue
Block a user