mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
commit
a444f21e64
@ -53,6 +53,8 @@ class EditBuildForm(HelperForm):
|
|||||||
'parent',
|
'parent',
|
||||||
'sales_order',
|
'sales_order',
|
||||||
'link',
|
'link',
|
||||||
|
'issued_by',
|
||||||
|
'responsible',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
27
InvenTree/build/migrations/0026_auto_20210216_1539.py
Normal file
27
InvenTree/build/migrations/0026_auto_20210216_1539.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
# Generated by Django 3.0.7 on 2021-02-16 04:39
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('users', '0005_owner_model'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('build', '0025_build_target_date'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='build',
|
||||||
|
name='issued_by',
|
||||||
|
field=models.ForeignKey(blank=True, help_text='User who issued this build order', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='builds_issued', to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='build',
|
||||||
|
name='responsible',
|
||||||
|
field=models.ForeignKey(blank=True, help_text='User responsible for this build order', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='builds_responsible', to='users.Owner'),
|
||||||
|
),
|
||||||
|
]
|
@ -33,6 +33,7 @@ import InvenTree.fields
|
|||||||
|
|
||||||
from stock import models as StockModels
|
from stock import models as StockModels
|
||||||
from part import models as PartModels
|
from part import models as PartModels
|
||||||
|
from users import models as UserModels
|
||||||
|
|
||||||
|
|
||||||
class Build(MPTTModel):
|
class Build(MPTTModel):
|
||||||
@ -53,6 +54,9 @@ class Build(MPTTModel):
|
|||||||
completion_date: Date the build was completed (or, if incomplete, the expected date of completion)
|
completion_date: Date the build was completed (or, if incomplete, the expected date of completion)
|
||||||
link: External URL for extra information
|
link: External URL for extra information
|
||||||
notes: Text notes
|
notes: Text notes
|
||||||
|
completed_by: User that completed the build
|
||||||
|
issued_by: User that issued the build
|
||||||
|
responsible: User (or group) responsible for completing the build
|
||||||
"""
|
"""
|
||||||
|
|
||||||
OVERDUE_FILTER = Q(status__in=BuildStatus.ACTIVE_CODES) & ~Q(target_date=None) & Q(target_date__lte=datetime.now().date())
|
OVERDUE_FILTER = Q(status__in=BuildStatus.ACTIVE_CODES) & ~Q(target_date=None) & Q(target_date__lte=datetime.now().date())
|
||||||
@ -214,6 +218,22 @@ class Build(MPTTModel):
|
|||||||
blank=True, null=True,
|
blank=True, null=True,
|
||||||
related_name='builds_completed'
|
related_name='builds_completed'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
issued_by = models.ForeignKey(
|
||||||
|
User,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
blank=True, null=True,
|
||||||
|
help_text=_('User who issued this build order'),
|
||||||
|
related_name='builds_issued',
|
||||||
|
)
|
||||||
|
|
||||||
|
responsible = models.ForeignKey(
|
||||||
|
UserModels.Owner,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
blank=True, null=True,
|
||||||
|
help_text=_('User responsible for this build order'),
|
||||||
|
related_name='builds_responsible',
|
||||||
|
)
|
||||||
|
|
||||||
link = InvenTree.fields.InvenTreeURLField(
|
link = InvenTree.fields.InvenTreeURLField(
|
||||||
verbose_name=_('External Link'),
|
verbose_name=_('External Link'),
|
||||||
|
@ -45,27 +45,35 @@ src="{% static 'img/blank_image.png' %}"
|
|||||||
</h3>
|
</h3>
|
||||||
<hr>
|
<hr>
|
||||||
<p>{{ build.title }}</p>
|
<p>{{ build.title }}</p>
|
||||||
<div class='btn-row'>
|
|
||||||
<div class='btn-group action-buttons'>
|
<div class='btn-group action-buttons' role='group'>
|
||||||
{% if roles.build.change %}
|
<!-- Printing options -->
|
||||||
<button type='button' class='btn btn-default' id='build-edit' title='{% trans "Edit Build" %}'>
|
<div class='btn-group'>
|
||||||
<span class='fas fa-edit icon-green'/>
|
<button id='print-options' title='{% trans "Print actions" %}' class='btn btn-default dropdown-toggle' type='button' data-toggle='dropdown'>
|
||||||
|
<span class='fas fa-print'></span> <span class='caret'></span>
|
||||||
</button>
|
</button>
|
||||||
{% if build.is_active %}
|
<ul class='dropdown-menu' role='menu'>
|
||||||
<button type='button' class='btn btn-default' id='build-complete' title='{% trans "Complete Build" %}'>
|
<li><a href='#' id='print-build-report'><span class='fas fa-file-pdf'></span> {% trans "Print Build Order" %}</a></li>
|
||||||
<span class='fas fa-tools'/>
|
</ul>
|
||||||
</button>
|
|
||||||
<button type='button' class='btn btn-default btn-glyph' id='build-cancel' title='{% trans "Cancel Build" %}'>
|
|
||||||
<span class='fas fa-times-circle icon-red'/>
|
|
||||||
</button>
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
{% if build.status == BuildStatus.CANCELLED and roles.build.delete %}
|
|
||||||
<button type='button' class='btn btn-default btn-glyph' id='build-delete' title='{% trans "Delete Build" %}'>
|
|
||||||
<span class='fas fa-trash-alt icon-red'/>
|
|
||||||
</button>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Build actions -->
|
||||||
|
{% if roles.build.change %}
|
||||||
|
<div class='btn-group'>
|
||||||
|
<button id='build-options' title='{% trans "Build actions" %}' class='btn btn-default dropdown-toggle' type='button' data-toggle='dropdown'>
|
||||||
|
<span class='fas fa-tools'></span> <span class='caret'></span>
|
||||||
|
</button>
|
||||||
|
<ul class='dropdown-menu' role='menu'>
|
||||||
|
<li><a href='#' id='build-edit'><span class='fas fa-edit icon-green'></span> {% trans "Edit Build" %}</a></li>
|
||||||
|
{% if build.is_active %}
|
||||||
|
<li><a href='#' id='build-complete'><span class='fas fa-tools'></span> {% trans "Complete Build" %}</a></li>
|
||||||
|
<li><a href='#' id='build-cancel'><span class='fas fa-times-circle icon-red'></span> {% trans "Cancel Build" %}</a></li>
|
||||||
|
{% endif %}
|
||||||
|
{% if build.status == BuildStatus.CANCELLED and roles.build.delete %}
|
||||||
|
<li><a href='#' id='build-delete'><span class='fas fa-trash-alt'></span> {% trans "Delete Build"% }</a>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
@ -121,6 +129,20 @@ src="{% static 'img/blank_image.png' %}"
|
|||||||
<td><a href="{% url 'so-detail' build.sales_order.id %}">{{ build.sales_order }}</a></td>
|
<td><a href="{% url 'so-detail' build.sales_order.id %}">{{ build.sales_order }}</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if build.issued_by %}
|
||||||
|
<tr>
|
||||||
|
<td><span class='fas fa-user'></span></td>
|
||||||
|
<td>{% trans "Issued By" %}</td>
|
||||||
|
<td>{{ build.issued_by }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
{% if build.responsible %}
|
||||||
|
<tr>
|
||||||
|
<td><span class='fas fa-users'></span></td>
|
||||||
|
<td>{% trans "Responsible" %}</td>
|
||||||
|
<td>{{ build.responsible }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
</table>
|
</table>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
@ -151,6 +173,10 @@ src="{% static 'img/blank_image.png' %}"
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$('#print-build-report').click(function() {
|
||||||
|
printBuildReports([{{ build.pk }}]);
|
||||||
|
});
|
||||||
|
|
||||||
$("#build-delete").on('click', function() {
|
$("#build-delete").on('click', function() {
|
||||||
launchModalForm(
|
launchModalForm(
|
||||||
"{% url 'build-delete' build.id %}",
|
"{% url 'build-delete' build.id %}",
|
||||||
|
@ -90,31 +90,50 @@
|
|||||||
<td><a href="{{ build.link }}">{{ build.link }}</a></td>
|
<td><a href="{{ build.link }}">{{ build.link }}</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if build.issued_by %}
|
||||||
<tr>
|
<tr>
|
||||||
<td><span class='fas fa-calendar-alt'></span></td>
|
<td><span class='fas fa-user'></span></td>
|
||||||
<td>{% trans "Created" %}</td>
|
<td>{% trans "Issued By" %}</td>
|
||||||
<td>{{ build.creation_date }}</td>
|
<td>{{ build.issued_by }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
{% if build.responsible %}
|
||||||
<tr>
|
<tr>
|
||||||
<td><span class='fas fa-calendar-alt'></span></td>
|
<td><span class='fas fa-users'></span></td>
|
||||||
<td>{% trans "Target Date" %}</td>
|
<td>{% trans "Responsible" %}</td>
|
||||||
{% if build.target_date %}
|
<td>{{ build.responsible }}</td>
|
||||||
<td>
|
|
||||||
{{ build.target_date }}{% if build.is_overdue %} <span class='fas fa-calendar-times icon-red'></span>{% endif %}
|
|
||||||
</td>
|
|
||||||
{% else %}
|
|
||||||
<td><i>{% trans "No target date set" %}</i></td>
|
|
||||||
{% endif %}
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><span class='fas fa-calendar-alt'></span></td>
|
|
||||||
<td>{% trans "Completed" %}</td>
|
|
||||||
{% if build.completion_date %}
|
|
||||||
<td>{{ build.completion_date }}{% if build.completed_by %}<span class='badge'>{{ build.completed_by }}</span>{% endif %}</td>
|
|
||||||
{% else %}
|
|
||||||
<td><i>{% trans "Build not complete" %}</i></td>
|
|
||||||
{% endif %}
|
|
||||||
</tr>
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class='col-sm-6'>
|
||||||
|
<table class='table table-striped'>
|
||||||
|
<col width='25'>
|
||||||
|
<tr>
|
||||||
|
<td><span class='fas fa-calendar-alt'></span></td>
|
||||||
|
<td>{% trans "Created" %}</td>
|
||||||
|
<td>{{ build.creation_date }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><span class='fas fa-calendar-alt'></span></td>
|
||||||
|
<td>{% trans "Target Date" %}</td>
|
||||||
|
{% if build.target_date %}
|
||||||
|
<td>
|
||||||
|
{{ build.target_date }}{% if build.is_overdue %} <span class='fas fa-calendar-times icon-red'></span>{% endif %}
|
||||||
|
</td>
|
||||||
|
{% else %}
|
||||||
|
<td><i>{% trans "No target date set" %}</i></td>
|
||||||
|
{% endif %}
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><span class='fas fa-calendar-alt'></span></td>
|
||||||
|
<td>{% trans "Completed" %}</td>
|
||||||
|
{% if build.completion_date %}
|
||||||
|
<td>{{ build.completion_date }}{% if build.completed_by %}<span class='badge'>{{ build.completed_by }}</span>{% endif %}</td>
|
||||||
|
{% else %}
|
||||||
|
<td><i>{% trans "Build not complete" %}</i></td>
|
||||||
|
{% endif %}
|
||||||
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -22,19 +22,33 @@ InvenTree | {% trans "Build Orders" %}
|
|||||||
|
|
||||||
<div id='button-toolbar'>
|
<div id='button-toolbar'>
|
||||||
<div class='button-toolbar container-fluid' style='float: right;'>
|
<div class='button-toolbar container-fluid' style='float: right;'>
|
||||||
{% if roles.build.add %}
|
<div class='btn-group'>
|
||||||
<button type='button' class="btn btn-success" id='new-build'>
|
{% if roles.build.add %}
|
||||||
<span class='fas fa-tools'></span> {% trans "New Build Order" %}
|
<button type='button' class="btn btn-success" id='new-build'>
|
||||||
</button>
|
<span class='fas fa-tools'></span> {% trans "New Build Order" %}
|
||||||
{% endif %}
|
</button>
|
||||||
<button class='btn btn-default' type='button' id='view-calendar' title='{% trans "Display calendar view" %}'>
|
{% endif %}
|
||||||
<span class='fas fa-calendar-alt'></span>
|
<div class='btn-group'>
|
||||||
</button>
|
<!-- Print actions -->
|
||||||
<button class='btn btn-default' type='button' id='view-list' title='{% trans "Display list view" %}'>
|
<button id='build-print-options' class='btn btn-primary dropdown-toggle' data-toggle='dropdown'>
|
||||||
<span class='fas fa-th-list'></span>
|
<span class='fas fa-print'></span> <span class='caret'></span>
|
||||||
</button>
|
</button>
|
||||||
<div class='filter-list' id='filter-list-build'>
|
<ul class='dropdown-menu'>
|
||||||
<!-- An empty div in which the filter list will be constructed -->
|
<li><a href='#' id='multi-build-print' title='{% trans "Print Build Orders" %}'>
|
||||||
|
<span class='fas fa-file-pdf'></span> {% trans "Print Build Orders" %}
|
||||||
|
</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<!-- Buttons to switch between list and calendar views -->
|
||||||
|
<button class='btn btn-default' type='button' id='view-calendar' title='{% trans "Display calendar view" %}'>
|
||||||
|
<span class='fas fa-calendar-alt'></span>
|
||||||
|
</button>
|
||||||
|
<button class='btn btn-default' type='button' id='view-list' title='{% trans "Display list view" %}'>
|
||||||
|
<span class='fas fa-th-list'></span>
|
||||||
|
</button>
|
||||||
|
<div class='filter-list' id='filter-list-build'>
|
||||||
|
<!-- An empty div in which the filter list will be constructed -->
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -157,17 +171,29 @@ $("#view-list").click(function() {
|
|||||||
$("#view-calendar").show();
|
$("#view-calendar").show();
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#collapse-item-active").collapse().show();
|
$("#collapse-item-active").collapse().show();
|
||||||
|
|
||||||
$("#new-build").click(function() {
|
$("#new-build").click(function() {
|
||||||
newBuildOrder();
|
newBuildOrder();
|
||||||
|
});
|
||||||
|
|
||||||
|
loadBuildTable($("#build-table"), {
|
||||||
|
url: "{% url 'api-build-list' %}",
|
||||||
|
params: {
|
||||||
|
part_detail: "true",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#multi-build-print').click(function() {
|
||||||
|
var rows = $("#build-table").bootstrapTable('getSelections');
|
||||||
|
|
||||||
|
var build_ids = [];
|
||||||
|
|
||||||
|
rows.forEach(function(row) {
|
||||||
|
build_ids.push(row.pk);
|
||||||
});
|
});
|
||||||
|
|
||||||
loadBuildTable($("#build-table"), {
|
printBuildReports(build_ids);
|
||||||
url: "{% url 'api-build-list' %}",
|
});
|
||||||
params: {
|
|
||||||
part_detail: "true",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
@ -696,6 +696,9 @@ class BuildCreate(AjaxCreateView):
|
|||||||
|
|
||||||
initials['quantity'] = self.request.GET.get('quantity', 1)
|
initials['quantity'] = self.request.GET.get('quantity', 1)
|
||||||
|
|
||||||
|
# Pre-fill the issued_by user
|
||||||
|
initials['issued_by'] = self.request.user
|
||||||
|
|
||||||
return initials
|
return initials
|
||||||
|
|
||||||
def get_data(self):
|
def get_data(self):
|
||||||
|
@ -18,7 +18,7 @@ from djmoney.contrib.exchange.models import convert_money
|
|||||||
from djmoney.contrib.exchange.exceptions import MissingRate
|
from djmoney.contrib.exchange.exceptions import MissingRate
|
||||||
|
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
from django.core.validators import MinValueValidator
|
from django.core.validators import MinValueValidator, URLValidator
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
import InvenTree.helpers
|
import InvenTree.helpers
|
||||||
@ -64,6 +64,13 @@ class InvenTreeSetting(models.Model):
|
|||||||
'default': 'My company name',
|
'default': 'My company name',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
'INVENTREE_BASE_URL': {
|
||||||
|
'name': _('Base URL'),
|
||||||
|
'description': _('Base URL for server instance'),
|
||||||
|
'validator': URLValidator(),
|
||||||
|
'default': '',
|
||||||
|
},
|
||||||
|
|
||||||
'INVENTREE_DEFAULT_CURRENCY': {
|
'INVENTREE_DEFAULT_CURRENCY': {
|
||||||
'name': _('Default Currency'),
|
'name': _('Default Currency'),
|
||||||
'description': _('Default currency'),
|
'description': _('Default currency'),
|
||||||
@ -528,6 +535,11 @@ class InvenTreeSetting(models.Model):
|
|||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if callable(validator):
|
||||||
|
# We can accept function validators with a single argument
|
||||||
|
print("Running validator function")
|
||||||
|
validator(self.value)
|
||||||
|
|
||||||
# Boolean validator
|
# Boolean validator
|
||||||
if validator == bool:
|
if validator == bool:
|
||||||
# Value must "look like" a boolean value
|
# Value must "look like" a boolean value
|
||||||
|
@ -35,34 +35,37 @@
|
|||||||
<span class='fas fa-trash-alt icon-red'></span>
|
<span class='fas fa-trash-alt icon-red'></span>
|
||||||
</button>
|
</button>
|
||||||
<button class='btn btn-primary' type='button' title='{% trans "Import BOM data" %}' id='bom-upload'>
|
<button class='btn btn-primary' type='button' title='{% trans "Import BOM data" %}' id='bom-upload'>
|
||||||
<span class='fas fa-file-upload'></span> {% trans "Import from File" %}
|
<span class='fas fa-file-upload'></span>
|
||||||
</button>
|
</button>
|
||||||
{% if part.variant_of %}
|
{% if part.variant_of %}
|
||||||
<button class='btn btn-default' type='button' title='{% trans "Copy BOM from parent part" %}' id='bom-duplicate'>
|
<button class='btn btn-default' type='button' title='{% trans "Copy BOM from parent part" %}' id='bom-duplicate'>
|
||||||
<span class='fas fa-clone'></span> {% trans "Copy from Parent" %}
|
<span class='fas fa-clone'></span>
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<button class='btn btn-default' type='button' title='{% trans "New BOM Item" %}' id='bom-item-new'>
|
<button class='btn btn-default' type='button' title='{% trans "New BOM Item" %}' id='bom-item-new'>
|
||||||
<span class='fas fa-plus-circle'></span> {% trans "Add Item" %}
|
<span class='fas fa-plus-circle'></span>
|
||||||
</button>
|
</button>
|
||||||
<button class='btn btn-success' type='button' title='{% trans "Finish Editing" %}' id='editing-finished'>
|
<button class='btn btn-success' type='button' title='{% trans "Finish Editing" %}' id='editing-finished'>
|
||||||
<span class='fas fa-check-circle'></span> {% trans "Finished" %}
|
<span class='fas fa-check-circle'></span>
|
||||||
</button>
|
</button>
|
||||||
{% elif part.active %}
|
{% elif part.active %}
|
||||||
{% if roles.part.change %}
|
{% if roles.part.change %}
|
||||||
<button class='btn btn-primary' type='button' title='{% trans "Edit BOM" %}' id='edit-bom'>
|
<button class='btn btn-primary' type='button' title='{% trans "Edit BOM" %}' id='edit-bom'>
|
||||||
<span class='fas fa-edit'></span> {% trans "Edit" %}
|
<span class='fas fa-edit'></span>
|
||||||
</button>
|
</button>
|
||||||
{% if part.is_bom_valid == False %}
|
{% if part.is_bom_valid == False %}
|
||||||
<button class='btn btn-success' id='validate-bom' title='{% trans "Validate Bill of Materials" %}' type='button'>
|
<button class='btn btn-success' id='validate-bom' title='{% trans "Validate Bill of Materials" %}' type='button'>
|
||||||
<span class='fas fa-clipboard-check'></span> {% trans "Validate" %}
|
<span class='fas fa-clipboard-check'></span>
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
<button title='{% trans "Export Bill of Materials" %}' class='btn btn-default' id='download-bom' type='button'>
|
<button title='{% trans "Export Bill of Materials" %}' class='btn btn-default' id='download-bom' type='button'>
|
||||||
<span class='fas fa-file-download'></span> {% trans "Export" %}
|
<span class='fas fa-file-download'></span>
|
||||||
|
</button>
|
||||||
|
<button title='{% trans "Print BOM Report" %}' class='btn btn-default' id='print-bom-report' type='button'>
|
||||||
|
<span class='fas fa-file-pdf'></span>
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
|
||||||
<div class='filter-list' id='filter-list-bom'>
|
<div class='filter-list' id='filter-list-bom'>
|
||||||
<!-- Empty div (will be filled out with avilable BOM filters) -->
|
<!-- Empty div (will be filled out with avilable BOM filters) -->
|
||||||
</div>
|
</div>
|
||||||
@ -215,4 +218,8 @@
|
|||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
$("#print-bom-report").click(function() {
|
||||||
|
printBomReports([{{ part.pk }}]);
|
||||||
|
});
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -3,7 +3,10 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from .models import ReportSnippet, TestReport, ReportAsset
|
from .models import ReportSnippet, ReportAsset
|
||||||
|
from .models import TestReport
|
||||||
|
from .models import BuildReport
|
||||||
|
from .models import BillOfMaterialsReport
|
||||||
|
|
||||||
|
|
||||||
class ReportTemplateAdmin(admin.ModelAdmin):
|
class ReportTemplateAdmin(admin.ModelAdmin):
|
||||||
@ -22,5 +25,8 @@ class ReportAssetAdmin(admin.ModelAdmin):
|
|||||||
|
|
||||||
|
|
||||||
admin.site.register(ReportSnippet, ReportSnippetAdmin)
|
admin.site.register(ReportSnippet, ReportSnippetAdmin)
|
||||||
admin.site.register(TestReport, ReportTemplateAdmin)
|
|
||||||
admin.site.register(ReportAsset, ReportAssetAdmin)
|
admin.site.register(ReportAsset, ReportAssetAdmin)
|
||||||
|
|
||||||
|
admin.site.register(TestReport, ReportTemplateAdmin)
|
||||||
|
admin.site.register(BuildReport, ReportTemplateAdmin)
|
||||||
|
admin.site.register(BillOfMaterialsReport, ReportTemplateAdmin)
|
||||||
|
@ -3,7 +3,7 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
from django.conf.urls import url, include
|
from django.conf.urls import url, include
|
||||||
from django.core.exceptions import FieldError
|
from django.core.exceptions import ValidationError, FieldError
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
|
|
||||||
from django_filters.rest_framework import DjangoFilterBackend
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
@ -16,8 +16,16 @@ import InvenTree.helpers
|
|||||||
|
|
||||||
from stock.models import StockItem
|
from stock.models import StockItem
|
||||||
|
|
||||||
|
import build.models
|
||||||
|
import part.models
|
||||||
|
|
||||||
from .models import TestReport
|
from .models import TestReport
|
||||||
|
from .models import BuildReport
|
||||||
|
from .models import BillOfMaterialsReport
|
||||||
|
|
||||||
from .serializers import TestReportSerializer
|
from .serializers import TestReportSerializer
|
||||||
|
from .serializers import BuildReportSerializer
|
||||||
|
from .serializers import BOMReportSerializer
|
||||||
|
|
||||||
|
|
||||||
class ReportListView(generics.ListAPIView):
|
class ReportListView(generics.ListAPIView):
|
||||||
@ -54,13 +62,7 @@ class StockItemReportMixin:
|
|||||||
|
|
||||||
params = self.request.query_params
|
params = self.request.query_params
|
||||||
|
|
||||||
if 'items[]' in params:
|
items = params.getlist('item', [])
|
||||||
items = params.getlist('items[]', [])
|
|
||||||
elif 'item' in params:
|
|
||||||
items = [params.get('item', None)]
|
|
||||||
|
|
||||||
if type(items) not in [list, tuple]:
|
|
||||||
item = [items]
|
|
||||||
|
|
||||||
valid_ids = []
|
valid_ids = []
|
||||||
|
|
||||||
@ -76,6 +78,131 @@ class StockItemReportMixin:
|
|||||||
return valid_items
|
return valid_items
|
||||||
|
|
||||||
|
|
||||||
|
class BuildReportMixin:
|
||||||
|
"""
|
||||||
|
Mixin for extracting Build items from query params
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_builds(self):
|
||||||
|
"""
|
||||||
|
Return a list of requested Build objects
|
||||||
|
"""
|
||||||
|
|
||||||
|
builds = []
|
||||||
|
|
||||||
|
params = self.request.query_params
|
||||||
|
|
||||||
|
builds = params.getlist('build', [])
|
||||||
|
|
||||||
|
valid_ids = []
|
||||||
|
|
||||||
|
for b in builds:
|
||||||
|
try:
|
||||||
|
valid_ids.append(int(b))
|
||||||
|
except (ValueError):
|
||||||
|
continue
|
||||||
|
|
||||||
|
return build.models.Build.objects.filter(pk__in=valid_ids)
|
||||||
|
|
||||||
|
|
||||||
|
class PartReportMixin:
|
||||||
|
"""
|
||||||
|
Mixin for extracting part items from query params
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_parts(self):
|
||||||
|
"""
|
||||||
|
Return a list of requested part objects
|
||||||
|
"""
|
||||||
|
|
||||||
|
parts = []
|
||||||
|
|
||||||
|
params = self.request.query_params
|
||||||
|
|
||||||
|
parts = params.getlist('part', [])
|
||||||
|
|
||||||
|
valid_ids = []
|
||||||
|
|
||||||
|
for p in parts:
|
||||||
|
try:
|
||||||
|
valid_ids.append(int(p))
|
||||||
|
except (ValueError):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Extract a valid set of Part objects
|
||||||
|
valid_parts = part.models.Part.objects.filter(pk__in=valid_ids)
|
||||||
|
|
||||||
|
return valid_parts
|
||||||
|
|
||||||
|
|
||||||
|
class ReportPrintMixin:
|
||||||
|
"""
|
||||||
|
Mixin for printing reports
|
||||||
|
"""
|
||||||
|
|
||||||
|
def print(self, request, items_to_print):
|
||||||
|
"""
|
||||||
|
Print this report template against a number of pre-validated items.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if len(items_to_print) == 0:
|
||||||
|
# No valid items provided, return an error message
|
||||||
|
data = {
|
||||||
|
'error': _('No valid objects provided to template'),
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response(data, status=400)
|
||||||
|
|
||||||
|
outputs = []
|
||||||
|
|
||||||
|
# In debug mode, generate single HTML output, rather than PDF
|
||||||
|
debug_mode = common.models.InvenTreeSetting.get_setting('REPORT_DEBUG_MODE')
|
||||||
|
|
||||||
|
# Merge one or more PDF files into a single download
|
||||||
|
for item in items_to_print:
|
||||||
|
report = self.get_object()
|
||||||
|
report.object_to_print = item
|
||||||
|
|
||||||
|
if debug_mode:
|
||||||
|
outputs.append(report.render_to_string(request))
|
||||||
|
else:
|
||||||
|
outputs.append(report.render(request))
|
||||||
|
|
||||||
|
if debug_mode:
|
||||||
|
"""
|
||||||
|
Contatenate all rendered templates into a single HTML string,
|
||||||
|
and return the string as a HTML response.
|
||||||
|
"""
|
||||||
|
|
||||||
|
html = "\n".join(outputs)
|
||||||
|
|
||||||
|
return HttpResponse(html)
|
||||||
|
else:
|
||||||
|
"""
|
||||||
|
Concatenate all rendered pages into a single PDF object,
|
||||||
|
and return the resulting document!
|
||||||
|
"""
|
||||||
|
|
||||||
|
pages = []
|
||||||
|
|
||||||
|
if len(outputs) > 1:
|
||||||
|
# If more than one output is generated, merge them into a single file
|
||||||
|
for output in outputs:
|
||||||
|
doc = output.get_document()
|
||||||
|
for page in doc.pages:
|
||||||
|
pages.append(page)
|
||||||
|
|
||||||
|
pdf = outputs[0].get_document().copy(pages).write_pdf()
|
||||||
|
else:
|
||||||
|
pdf = outputs[0].get_document().write_pdf()
|
||||||
|
|
||||||
|
return InvenTree.helpers.DownloadFile(
|
||||||
|
pdf,
|
||||||
|
'inventree_report.pdf',
|
||||||
|
content_type='application/pdf'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class StockItemTestReportList(ReportListView, StockItemReportMixin):
|
class StockItemTestReportList(ReportListView, StockItemReportMixin):
|
||||||
"""
|
"""
|
||||||
API endpoint for viewing list of TestReport objects.
|
API endpoint for viewing list of TestReport objects.
|
||||||
@ -83,8 +210,7 @@ class StockItemTestReportList(ReportListView, StockItemReportMixin):
|
|||||||
Filterable by:
|
Filterable by:
|
||||||
|
|
||||||
- enabled: Filter by enabled / disabled status
|
- enabled: Filter by enabled / disabled status
|
||||||
- item: Filter by single stock item
|
- item: Filter by stock item(s)
|
||||||
- items: Filter by list of stock items
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -150,7 +276,7 @@ class StockItemTestReportDetail(generics.RetrieveUpdateDestroyAPIView):
|
|||||||
serializer_class = TestReportSerializer
|
serializer_class = TestReportSerializer
|
||||||
|
|
||||||
|
|
||||||
class StockItemTestReportPrint(generics.RetrieveAPIView, StockItemReportMixin):
|
class StockItemTestReportPrint(generics.RetrieveAPIView, StockItemReportMixin, ReportPrintMixin):
|
||||||
"""
|
"""
|
||||||
API endpoint for printing a TestReport object
|
API endpoint for printing a TestReport object
|
||||||
"""
|
"""
|
||||||
@ -165,67 +291,212 @@ class StockItemTestReportPrint(generics.RetrieveAPIView, StockItemReportMixin):
|
|||||||
|
|
||||||
items = self.get_items()
|
items = self.get_items()
|
||||||
|
|
||||||
if len(items) == 0:
|
return self.print(request, items)
|
||||||
# No valid items provided, return an error message
|
|
||||||
data = {
|
|
||||||
'error': _('Must provide valid StockItem(s)')
|
|
||||||
}
|
|
||||||
|
|
||||||
return Response(data, status=400)
|
class BOMReportList(ReportListView, PartReportMixin):
|
||||||
|
"""
|
||||||
|
API endpoint for viewing a list of BillOfMaterialReport objects.
|
||||||
|
|
||||||
outputs = []
|
Filterably by:
|
||||||
|
|
||||||
# In debug mode, generate single HTML output, rather than PDF
|
- enabled: Filter by enabled / disabled status
|
||||||
debug_mode = common.models.InvenTreeSetting.get_setting('REPORT_DEBUG_MODE')
|
- part: Filter by part(s)
|
||||||
|
"""
|
||||||
|
|
||||||
# Merge one or more PDF files into a single download
|
queryset = BillOfMaterialsReport.objects.all()
|
||||||
for item in items:
|
serializer_class = BOMReportSerializer
|
||||||
report = self.get_object()
|
|
||||||
report.stock_item = item
|
|
||||||
|
|
||||||
if debug_mode:
|
def filter_queryset(self, queryset):
|
||||||
outputs.append(report.render_to_string(request))
|
|
||||||
else:
|
|
||||||
outputs.append(report.render(request))
|
|
||||||
|
|
||||||
if debug_mode:
|
queryset = super().filter_queryset(queryset)
|
||||||
|
|
||||||
|
# List of Part objects to match against
|
||||||
|
parts = self.get_parts()
|
||||||
|
|
||||||
|
if len(parts) > 0:
|
||||||
"""
|
"""
|
||||||
Contatenate all rendered templates into a single HTML string,
|
We wish to filter by part(s).
|
||||||
and return the string as a HTML response.
|
|
||||||
|
We need to compare the 'filters' string of each report,
|
||||||
|
and see if it matches against each of the specified parts.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
html = "\n".join(outputs)
|
valid_report_ids = set()
|
||||||
|
|
||||||
return HttpResponse(html)
|
for report in queryset.all():
|
||||||
|
|
||||||
else:
|
matches = True
|
||||||
|
|
||||||
|
try:
|
||||||
|
filters = InvenTree.helpers.validateFilterString(report.filters)
|
||||||
|
except ValidationError:
|
||||||
|
# Filters are ill-defined
|
||||||
|
continue
|
||||||
|
|
||||||
|
for p in parts:
|
||||||
|
part_query = part.models.Part.objects.filter(pk=p.pk)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if not part_query.filter(**filters).exists():
|
||||||
|
matches = False
|
||||||
|
break
|
||||||
|
except FieldError:
|
||||||
|
matches = False
|
||||||
|
break
|
||||||
|
|
||||||
|
if matches:
|
||||||
|
valid_report_ids.add(report.pk)
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Reduce queryset to only valid matches
|
||||||
|
queryset = queryset.filter(pk__in=[pk for pk in valid_report_ids])
|
||||||
|
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
|
class BOMReportDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||||
|
"""
|
||||||
|
API endpoint for a single BillOfMaterialReport object
|
||||||
|
"""
|
||||||
|
|
||||||
|
queryset = BillOfMaterialsReport.objects.all()
|
||||||
|
serializer_class = BOMReportSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class BOMReportPrint(generics.RetrieveAPIView, PartReportMixin, ReportPrintMixin):
|
||||||
|
"""
|
||||||
|
API endpoint for printing a BillOfMaterialReport object
|
||||||
|
"""
|
||||||
|
|
||||||
|
queryset = BillOfMaterialsReport.objects.all()
|
||||||
|
serializer_class = BOMReportSerializer
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Check if valid part item(s) have been provided
|
||||||
|
"""
|
||||||
|
|
||||||
|
parts = self.get_parts()
|
||||||
|
|
||||||
|
return self.print(request, parts)
|
||||||
|
|
||||||
|
|
||||||
|
class BuildReportList(ReportListView, BuildReportMixin):
|
||||||
|
"""
|
||||||
|
API endpoint for viewing a list of BuildReport objects.
|
||||||
|
|
||||||
|
Can be filtered by:
|
||||||
|
|
||||||
|
- enabled: Filter by enabled / disabled status
|
||||||
|
- build: Filter by Build object
|
||||||
|
"""
|
||||||
|
|
||||||
|
queryset = BuildReport.objects.all()
|
||||||
|
serializer_class = BuildReportSerializer
|
||||||
|
|
||||||
|
def filter_queryset(self, queryset):
|
||||||
|
|
||||||
|
queryset = super().filter_queryset(queryset)
|
||||||
|
|
||||||
|
# List of Build objects to match against
|
||||||
|
builds = self.get_builds()
|
||||||
|
|
||||||
|
if len(builds) > 0:
|
||||||
"""
|
"""
|
||||||
Concatenate all rendered pages into a single PDF object,
|
We wish to filter by Build(s)
|
||||||
and return the resulting document!
|
|
||||||
|
We need to compare the 'filters' string of each report,
|
||||||
|
and see if it matches against each of the specified parts
|
||||||
|
|
||||||
|
# TODO: This code needs some refactoring!
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pages = []
|
valid_build_ids = set()
|
||||||
|
|
||||||
if len(outputs) > 1:
|
for report in queryset.all():
|
||||||
# If more than one output is generated, merge them into a single file
|
|
||||||
for output in outputs:
|
|
||||||
doc = output.get_document()
|
|
||||||
for page in doc.pages:
|
|
||||||
pages.append(page)
|
|
||||||
|
|
||||||
pdf = outputs[0].get_document().copy(pages).write_pdf()
|
matches = True
|
||||||
else:
|
|
||||||
pdf = outputs[0].get_document().write_pdf()
|
|
||||||
|
|
||||||
return InvenTree.helpers.DownloadFile(
|
try:
|
||||||
pdf,
|
filters = InvenTree.helpers.validateFilterString(report.filters)
|
||||||
'test_report.pdf',
|
except ValidationError:
|
||||||
content_type='application/pdf'
|
continue
|
||||||
)
|
|
||||||
|
for b in builds:
|
||||||
|
build_query = build.models.Build.objects.filter(pk=b.pk)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if not build_query.filter(**filters).exists():
|
||||||
|
matches = False
|
||||||
|
break
|
||||||
|
except FieldError:
|
||||||
|
matches = False
|
||||||
|
break
|
||||||
|
|
||||||
|
if matches:
|
||||||
|
valid_build_ids.add(report.pk)
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Reduce queryset to only valid matches
|
||||||
|
queryset = queryset.filter(pk__in=[pk for pk in valid_build_ids])
|
||||||
|
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
|
class BuildReportDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||||
|
"""
|
||||||
|
API endpoint for a single BuildReport object
|
||||||
|
"""
|
||||||
|
|
||||||
|
queryset = BuildReport.objects.all()
|
||||||
|
serializer_class = BuildReportSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class BuildReportPrint(generics.RetrieveAPIView, BuildReportMixin, ReportPrintMixin):
|
||||||
|
"""
|
||||||
|
API endpoint for printing a BuildReport
|
||||||
|
"""
|
||||||
|
|
||||||
|
queryset = BuildReport.objects.all()
|
||||||
|
serializer_class = BuildReportSerializer
|
||||||
|
|
||||||
|
def get(self, request, *ars, **kwargs):
|
||||||
|
|
||||||
|
builds = self.get_builds()
|
||||||
|
|
||||||
|
return self.print(request, builds)
|
||||||
|
|
||||||
|
|
||||||
report_api_urls = [
|
report_api_urls = [
|
||||||
|
|
||||||
|
# Build reports
|
||||||
|
url(r'build/', include([
|
||||||
|
# Detail views
|
||||||
|
url(r'^(?P<pk>\d+)/', include([
|
||||||
|
url(r'print/?', BuildReportPrint.as_view(), name='api-build-report-print'),
|
||||||
|
url(r'^.*$', BuildReportDetail.as_view(), name='api-build-report-detail'),
|
||||||
|
])),
|
||||||
|
|
||||||
|
# List view
|
||||||
|
url(r'^.*$', BuildReportList.as_view(), name='api-build-report-list'),
|
||||||
|
])),
|
||||||
|
|
||||||
|
# Bill of Material reports
|
||||||
|
url(r'bom/', include([
|
||||||
|
|
||||||
|
# Detail views
|
||||||
|
url(r'^(?P<pk>\d+)/', include([
|
||||||
|
url(r'print/?', BOMReportPrint.as_view(), name='api-bom-report-print'),
|
||||||
|
url(r'^.*$', BOMReportDetail.as_view(), name='api-bom-report-detail'),
|
||||||
|
])),
|
||||||
|
|
||||||
|
# List view
|
||||||
|
url(r'^.*$', BOMReportList.as_view(), name='api-bom-report-list'),
|
||||||
|
])),
|
||||||
|
|
||||||
# Stock item test reports
|
# Stock item test reports
|
||||||
url(r'test/', include([
|
url(r'test/', include([
|
||||||
# Detail views
|
# Detail views
|
||||||
|
@ -18,6 +18,66 @@ class ReportConfig(AppConfig):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
self.create_default_test_reports()
|
self.create_default_test_reports()
|
||||||
|
self.create_default_build_reports()
|
||||||
|
|
||||||
|
def create_default_reports(self, model, reports):
|
||||||
|
"""
|
||||||
|
Copy defualt report files across to the media directory.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Source directory for report templates
|
||||||
|
src_dir = os.path.join(
|
||||||
|
os.path.dirname(os.path.realpath(__file__)),
|
||||||
|
'templates',
|
||||||
|
'report',
|
||||||
|
)
|
||||||
|
|
||||||
|
# Destination directory
|
||||||
|
dst_dir = os.path.join(
|
||||||
|
settings.MEDIA_ROOT,
|
||||||
|
'report',
|
||||||
|
'inventree',
|
||||||
|
model.getSubdir(),
|
||||||
|
)
|
||||||
|
|
||||||
|
if not os.path.exists(dst_dir):
|
||||||
|
logger.info(f"Creating missing directory: '{dst_dir}'")
|
||||||
|
os.makedirs(dst_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# Copy each report template across (if required)
|
||||||
|
for report in reports:
|
||||||
|
|
||||||
|
# Destination filename
|
||||||
|
filename = os.path.join(
|
||||||
|
'report',
|
||||||
|
'inventree',
|
||||||
|
model.getSubdir(),
|
||||||
|
report['file'],
|
||||||
|
)
|
||||||
|
|
||||||
|
src_file = os.path.join(src_dir, report['file'])
|
||||||
|
dst_file = os.path.join(settings.MEDIA_ROOT, filename)
|
||||||
|
|
||||||
|
if not os.path.exists(dst_file):
|
||||||
|
logger.info(f"Copying test report template '{dst_file}'")
|
||||||
|
shutil.copyfile(src_file, dst_file)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Check if a report matching the template already exists
|
||||||
|
if model.objects.filter(template=filename).exists():
|
||||||
|
continue
|
||||||
|
|
||||||
|
logger.info(f"Creating new TestReport for '{report['name']}'")
|
||||||
|
|
||||||
|
model.objects.create(
|
||||||
|
name=report['name'],
|
||||||
|
description=report['description'],
|
||||||
|
template=filename,
|
||||||
|
enabled=True
|
||||||
|
)
|
||||||
|
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
def create_default_test_reports(self):
|
def create_default_test_reports(self):
|
||||||
"""
|
"""
|
||||||
@ -31,23 +91,6 @@ class ReportConfig(AppConfig):
|
|||||||
# Database is not ready yet
|
# Database is not ready yet
|
||||||
return
|
return
|
||||||
|
|
||||||
src_dir = os.path.join(
|
|
||||||
os.path.dirname(os.path.realpath(__file__)),
|
|
||||||
'templates',
|
|
||||||
'report',
|
|
||||||
)
|
|
||||||
|
|
||||||
dst_dir = os.path.join(
|
|
||||||
settings.MEDIA_ROOT,
|
|
||||||
'report',
|
|
||||||
'inventree', # Stored in secret directory!
|
|
||||||
'test',
|
|
||||||
)
|
|
||||||
|
|
||||||
if not os.path.exists(dst_dir):
|
|
||||||
logger.info(f"Creating missing directory: '{dst_dir}'")
|
|
||||||
os.makedirs(dst_dir, exist_ok=True)
|
|
||||||
|
|
||||||
# List of test reports to copy across
|
# List of test reports to copy across
|
||||||
reports = [
|
reports = [
|
||||||
{
|
{
|
||||||
@ -57,36 +100,27 @@ class ReportConfig(AppConfig):
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
for report in reports:
|
self.create_default_reports(TestReport, reports)
|
||||||
|
|
||||||
# Create destination file name
|
def create_default_build_reports(self):
|
||||||
filename = os.path.join(
|
"""
|
||||||
'report',
|
Create database entries for the default BuildReport templates
|
||||||
'inventree',
|
(if they do not already exist)
|
||||||
'test',
|
"""
|
||||||
report['file']
|
|
||||||
)
|
|
||||||
|
|
||||||
src_file = os.path.join(src_dir, report['file'])
|
try:
|
||||||
dst_file = os.path.join(settings.MEDIA_ROOT, filename)
|
from .models import BuildReport
|
||||||
|
except:
|
||||||
|
# Database is not ready yet
|
||||||
|
return
|
||||||
|
|
||||||
if not os.path.exists(dst_file):
|
# List of Build reports to copy across
|
||||||
logger.info(f"Copying test report template '{dst_file}'")
|
reports = [
|
||||||
shutil.copyfile(src_file, dst_file)
|
{
|
||||||
|
'file': 'inventree_build_order.html',
|
||||||
|
'name': 'InvenTree Build Order',
|
||||||
|
'description': 'Build Order job sheet',
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
try:
|
self.create_default_reports(BuildReport, reports)
|
||||||
# Check if a report matching the template already exists
|
|
||||||
if TestReport.objects.filter(template=filename).exists():
|
|
||||||
continue
|
|
||||||
|
|
||||||
logger.info(f"Creating new TestReport for '{report['name']}'")
|
|
||||||
|
|
||||||
TestReport.objects.create(
|
|
||||||
name=report['name'],
|
|
||||||
description=report['description'],
|
|
||||||
template=filename,
|
|
||||||
filters='',
|
|
||||||
enabled=True
|
|
||||||
)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
35
InvenTree/report/migrations/0011_auto_20210212_2024.py
Normal file
35
InvenTree/report/migrations/0011_auto_20210212_2024.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
# Generated by Django 3.0.7 on 2021-02-12 09:24
|
||||||
|
|
||||||
|
import django.core.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
import report.models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('report', '0010_auto_20210205_1201'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='BillOfMaterialsReport',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(help_text='Template name', max_length=100, verbose_name='Name')),
|
||||||
|
('template', models.FileField(help_text='Report template file', upload_to=report.models.rename_template, validators=[django.core.validators.FileExtensionValidator(allowed_extensions=['html', 'htm'])], verbose_name='Template')),
|
||||||
|
('description', models.CharField(help_text='Report template description', max_length=250, verbose_name='Description')),
|
||||||
|
('revision', models.PositiveIntegerField(default=1, editable=False, help_text='Report revision number (auto-increments)', verbose_name='Revision')),
|
||||||
|
('enabled', models.BooleanField(default=True, help_text='Report template is enabled', verbose_name='Enabled')),
|
||||||
|
('filters', models.CharField(blank=True, help_text='Part query filters (comma-separated list of key=value pairs', max_length=250, validators=[report.models.validate_part_report_filters], verbose_name='Part Filters')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='testreport',
|
||||||
|
name='filters',
|
||||||
|
field=models.CharField(blank=True, help_text='StockItem query filters (comma-separated list of key=value pairs)', max_length=250, validators=[report.models.validate_stock_item_report_filters], verbose_name='Filters'),
|
||||||
|
),
|
||||||
|
]
|
30
InvenTree/report/migrations/0012_buildreport.py
Normal file
30
InvenTree/report/migrations/0012_buildreport.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# Generated by Django 3.0.7 on 2021-02-15 21:08
|
||||||
|
|
||||||
|
import django.core.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
import report.models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('report', '0011_auto_20210212_2024'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='BuildReport',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(help_text='Template name', max_length=100, verbose_name='Name')),
|
||||||
|
('template', models.FileField(help_text='Report template file', upload_to=report.models.rename_template, validators=[django.core.validators.FileExtensionValidator(allowed_extensions=['html', 'htm'])], verbose_name='Template')),
|
||||||
|
('description', models.CharField(help_text='Report template description', max_length=250, verbose_name='Description')),
|
||||||
|
('revision', models.PositiveIntegerField(default=1, editable=False, help_text='Report revision number (auto-increments)', verbose_name='Revision')),
|
||||||
|
('enabled', models.BooleanField(default=True, help_text='Report template is enabled', verbose_name='Enabled')),
|
||||||
|
('filters', models.CharField(blank=True, help_text='Build query filters (comma-separated list of key=value pairs', max_length=250, validators=[report.models.validate_build_report_filters], verbose_name='Build Filters')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
@ -20,8 +20,10 @@ from django.template.loader import render_to_string
|
|||||||
from django.core.files.storage import FileSystemStorage
|
from django.core.files.storage import FileSystemStorage
|
||||||
from django.core.validators import FileExtensionValidator
|
from django.core.validators import FileExtensionValidator
|
||||||
|
|
||||||
import stock.models
|
import build.models
|
||||||
import common.models
|
import common.models
|
||||||
|
import part.models
|
||||||
|
import stock.models
|
||||||
|
|
||||||
from InvenTree.helpers import validateFilterString
|
from InvenTree.helpers import validateFilterString
|
||||||
|
|
||||||
@ -60,7 +62,6 @@ class ReportFileUpload(FileSystemStorage):
|
|||||||
|
|
||||||
def get_available_name(self, name, max_length=None):
|
def get_available_name(self, name, max_length=None):
|
||||||
|
|
||||||
print("Name:", name)
|
|
||||||
return super().get_available_name(name, max_length)
|
return super().get_available_name(name, max_length)
|
||||||
|
|
||||||
|
|
||||||
@ -70,10 +71,29 @@ def rename_template(instance, filename):
|
|||||||
|
|
||||||
|
|
||||||
def validate_stock_item_report_filters(filters):
|
def validate_stock_item_report_filters(filters):
|
||||||
|
"""
|
||||||
|
Validate filter string against StockItem model
|
||||||
|
"""
|
||||||
|
|
||||||
return validateFilterString(filters, model=stock.models.StockItem)
|
return validateFilterString(filters, model=stock.models.StockItem)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_part_report_filters(filters):
|
||||||
|
"""
|
||||||
|
Validate filter string against Part model
|
||||||
|
"""
|
||||||
|
|
||||||
|
return validateFilterString(filters, model=part.models.Part)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_build_report_filters(filters):
|
||||||
|
"""
|
||||||
|
Validate filter string against Build model
|
||||||
|
"""
|
||||||
|
|
||||||
|
return validateFilterString(filters, model=build.models.Build)
|
||||||
|
|
||||||
|
|
||||||
class WeasyprintReportMixin(WeasyTemplateResponseMixin):
|
class WeasyprintReportMixin(WeasyTemplateResponseMixin):
|
||||||
"""
|
"""
|
||||||
Class for rendering a HTML template to a PDF.
|
Class for rendering a HTML template to a PDF.
|
||||||
@ -107,7 +127,8 @@ class ReportBase(models.Model):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "{n} - {d}".format(n=self.name, d=self.description)
|
return "{n} - {d}".format(n=self.name, d=self.description)
|
||||||
|
|
||||||
def getSubdir(self):
|
@classmethod
|
||||||
|
def getSubdir(cls):
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
def rename_file(self, filename):
|
def rename_file(self, filename):
|
||||||
@ -171,6 +192,9 @@ class ReportTemplateBase(ReportBase):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# Pass a single top-level object to the report template
|
||||||
|
object_to_print = None
|
||||||
|
|
||||||
def get_context_data(self, request):
|
def get_context_data(self, request):
|
||||||
"""
|
"""
|
||||||
Supply context data to the template for rendering
|
Supply context data to the template for rendering
|
||||||
@ -185,6 +209,7 @@ class ReportTemplateBase(ReportBase):
|
|||||||
|
|
||||||
context = self.get_context_data(request)
|
context = self.get_context_data(request)
|
||||||
|
|
||||||
|
context['base_url'] = common.models.InvenTreeSetting.get_setting('INVENTREE_BASE_URL')
|
||||||
context['date'] = datetime.datetime.now().date()
|
context['date'] = datetime.datetime.now().date()
|
||||||
context['datetime'] = datetime.datetime.now()
|
context['datetime'] = datetime.datetime.now()
|
||||||
context['default_page_size'] = common.models.InvenTreeSetting.get_setting('REPORT_DEFAULT_PAGE_SIZE')
|
context['default_page_size'] = common.models.InvenTreeSetting.get_setting('REPORT_DEFAULT_PAGE_SIZE')
|
||||||
@ -242,17 +267,15 @@ class TestReport(ReportTemplateBase):
|
|||||||
Render a TestReport against a StockItem object.
|
Render a TestReport against a StockItem object.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def getSubdir(self):
|
@classmethod
|
||||||
|
def getSubdir(cls):
|
||||||
return 'test'
|
return 'test'
|
||||||
|
|
||||||
# Requires a stock_item object to be given to it before rendering
|
|
||||||
stock_item = None
|
|
||||||
|
|
||||||
filters = models.CharField(
|
filters = models.CharField(
|
||||||
blank=True,
|
blank=True,
|
||||||
max_length=250,
|
max_length=250,
|
||||||
verbose_name=_('Filters'),
|
verbose_name=_('Filters'),
|
||||||
help_text=_("Part query filters (comma-separated list of key=value pairs)"),
|
help_text=_("StockItem query filters (comma-separated list of key=value pairs)"),
|
||||||
validators=[
|
validators=[
|
||||||
validate_stock_item_report_filters
|
validate_stock_item_report_filters
|
||||||
]
|
]
|
||||||
@ -275,11 +298,80 @@ class TestReport(ReportTemplateBase):
|
|||||||
return items.exists()
|
return items.exists()
|
||||||
|
|
||||||
def get_context_data(self, request):
|
def get_context_data(self, request):
|
||||||
|
|
||||||
|
stock_item = self.object_to_print
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'stock_item': self.stock_item,
|
'stock_item': stock_item,
|
||||||
'part': self.stock_item.part,
|
'part': stock_item.part,
|
||||||
'results': self.stock_item.testResultMap(),
|
'results': stock_item.testResultMap(),
|
||||||
'result_list': self.stock_item.testResultList()
|
'result_list': stock_item.testResultList()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class BuildReport(ReportTemplateBase):
|
||||||
|
"""
|
||||||
|
Build order / work order report
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def getSubdir(cls):
|
||||||
|
return 'build'
|
||||||
|
|
||||||
|
filters = models.CharField(
|
||||||
|
blank=True,
|
||||||
|
max_length=250,
|
||||||
|
verbose_name=_('Build Filters'),
|
||||||
|
help_text=_('Build query filters (comma-separated list of key=value pairs'),
|
||||||
|
validators=[
|
||||||
|
validate_build_report_filters,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_context_data(self, request):
|
||||||
|
"""
|
||||||
|
Custom context data for the build report
|
||||||
|
"""
|
||||||
|
|
||||||
|
my_build = self.object_to_print
|
||||||
|
|
||||||
|
if not type(my_build) == build.models.Build:
|
||||||
|
raise TypeError('Provided model is not a Build object')
|
||||||
|
|
||||||
|
return {
|
||||||
|
'build': my_build,
|
||||||
|
'part': my_build.part,
|
||||||
|
'reference': my_build.reference,
|
||||||
|
'quantity': my_build.quantity,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class BillOfMaterialsReport(ReportTemplateBase):
|
||||||
|
"""
|
||||||
|
Render a Bill of Materials against a Part object
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def getSubdir(cls):
|
||||||
|
return 'bom'
|
||||||
|
|
||||||
|
filters = models.CharField(
|
||||||
|
blank=True,
|
||||||
|
max_length=250,
|
||||||
|
verbose_name=_('Part Filters'),
|
||||||
|
help_text=_('Part query filters (comma-separated list of key=value pairs'),
|
||||||
|
validators=[
|
||||||
|
validate_part_report_filters
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_context_data(self, request):
|
||||||
|
|
||||||
|
part = self.object_to_print
|
||||||
|
|
||||||
|
return {
|
||||||
|
'part': part,
|
||||||
|
'category': part.category,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -5,6 +5,8 @@ from InvenTree.serializers import InvenTreeModelSerializer
|
|||||||
from InvenTree.serializers import InvenTreeAttachmentSerializerField
|
from InvenTree.serializers import InvenTreeAttachmentSerializerField
|
||||||
|
|
||||||
from .models import TestReport
|
from .models import TestReport
|
||||||
|
from .models import BuildReport
|
||||||
|
from .models import BillOfMaterialsReport
|
||||||
|
|
||||||
|
|
||||||
class TestReportSerializer(InvenTreeModelSerializer):
|
class TestReportSerializer(InvenTreeModelSerializer):
|
||||||
@ -21,3 +23,35 @@ class TestReportSerializer(InvenTreeModelSerializer):
|
|||||||
'filters',
|
'filters',
|
||||||
'enabled',
|
'enabled',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class BuildReportSerializer(InvenTreeModelSerializer):
|
||||||
|
|
||||||
|
template = InvenTreeAttachmentSerializerField(required=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = BuildReport
|
||||||
|
fields = [
|
||||||
|
'pk',
|
||||||
|
'name',
|
||||||
|
'description',
|
||||||
|
'template',
|
||||||
|
'filters',
|
||||||
|
'enabled',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class BOMReportSerializer(InvenTreeModelSerializer):
|
||||||
|
|
||||||
|
template = InvenTreeAttachmentSerializerField(required=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = BillOfMaterialsReport
|
||||||
|
fields = [
|
||||||
|
'pk',
|
||||||
|
'name',
|
||||||
|
'description',
|
||||||
|
'template',
|
||||||
|
'filters',
|
||||||
|
'enabled',
|
||||||
|
]
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
{% extends "report/inventree_build_order_base.html" %}
|
||||||
|
|
||||||
|
<!-- Refer to the inventree_build_order_base template -->
|
@ -0,0 +1,181 @@
|
|||||||
|
{% extends "report/inventree_report_base.html" %}
|
||||||
|
|
||||||
|
{% load i18n %}
|
||||||
|
{% load report %}
|
||||||
|
{% load inventree_extras %}
|
||||||
|
{% load markdownify %}
|
||||||
|
|
||||||
|
{% block page_margin %}
|
||||||
|
margin: 2cm;
|
||||||
|
margin-top: 4cm;
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block style %}
|
||||||
|
|
||||||
|
.header-right {
|
||||||
|
text-align: right;
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
height: 20mm;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.part-image {
|
||||||
|
border: 1px solid;
|
||||||
|
border-radius: 2px;
|
||||||
|
vertical-align: middle;
|
||||||
|
height: 40mm;
|
||||||
|
width: 100%;
|
||||||
|
display: inline-block;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details-image {
|
||||||
|
float: right;
|
||||||
|
width: 30%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details {
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid;
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 5px;
|
||||||
|
min-height: 42mm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details table {
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
word-wrap: break-word;
|
||||||
|
width: 65%;
|
||||||
|
table-layout: fixed;
|
||||||
|
font-size: 75%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details table td:not(:last-child){
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details table td:last-child{
|
||||||
|
width: 50%;
|
||||||
|
padding-left: 1cm;
|
||||||
|
padding-right: 1cm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details-table td {
|
||||||
|
padding-left: 10px;
|
||||||
|
padding-top: 5px;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
border-bottom: 1px solid #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block bottom_left %}
|
||||||
|
content: "v{{report_revision}} - {{ date.isoformat }}";
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block bottom_center %}
|
||||||
|
content: "www.currawong.aero";
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block header_content %}
|
||||||
|
<img class='logo' src="{% asset 'logo_black_with_black_bird.png' %}" alt="hello" width="150">
|
||||||
|
|
||||||
|
<div class='header-right'>
|
||||||
|
<h3>
|
||||||
|
Build Order {{ build }}
|
||||||
|
</h3>
|
||||||
|
<small>{{ quantity }} x {{ part.full_name }}</small>
|
||||||
|
<br>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block page_content %}
|
||||||
|
|
||||||
|
<div class='details'>
|
||||||
|
<div class='details-image'>
|
||||||
|
<img class='part-image' src="{% part_image part %}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class='details-container'>
|
||||||
|
|
||||||
|
<table class='details-table'>
|
||||||
|
<tr>
|
||||||
|
<th>{% trans "Build Order" %}</th>
|
||||||
|
<td>{% internal_link build.get_absolute_url build %}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>{% trans "Part" %}</th>
|
||||||
|
<td>{% internal_link part.get_absolute_url part.full_name %}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>{% trans "Quantity" %}</th>
|
||||||
|
<td>{{ build.quantity }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>{% trans "Description" %}</th>
|
||||||
|
<td>{{ build.title }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>{% trans "Issued" %}</th>
|
||||||
|
<td>{{ build.creation_date }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>{% trans "Target Date" %}</th>
|
||||||
|
<td>
|
||||||
|
{% if build.target_date %}
|
||||||
|
{{ build.target_date }}
|
||||||
|
{% else %}
|
||||||
|
<i>Not specified</i>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>{% trans "Sales Order" %}</th>
|
||||||
|
<td>
|
||||||
|
{% if build.sales_order %}
|
||||||
|
{% internal_link build.sales_order.get_absolute_url build.sales_order %}
|
||||||
|
{% else %}
|
||||||
|
<i>Not specified</i>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% if build.parent %}
|
||||||
|
<tr>
|
||||||
|
<th>{% trans "Required For" %}</th>
|
||||||
|
<td>{% internal_link build.parent.get_absolute_url build.parent %}</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
{% if build.issued_by %}
|
||||||
|
<tr>
|
||||||
|
<th>{% trans "Issued By" %}</th>
|
||||||
|
<td>{{ build.issued_by }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
{% if build.responsible %}
|
||||||
|
<tr>
|
||||||
|
<th>{% trans "Responsible" %}</th>
|
||||||
|
<td>{{ build.responsible }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
{% if build.link %}
|
||||||
|
<tr>
|
||||||
|
<th>{% trans "Link" %}</th>
|
||||||
|
<td><a href="{{ build.link }}">{{ build.link }}</a></td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>{% trans "Notes" %}</h3>
|
||||||
|
|
||||||
|
{% if build.notes %}
|
||||||
|
{{ build.notes|markdownify }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endblock %}
|
@ -4,8 +4,12 @@
|
|||||||
<style>
|
<style>
|
||||||
@page {
|
@page {
|
||||||
{% block page_style %}
|
{% block page_style %}
|
||||||
size: {% block page_size %}{{ default_page_size }}{% endblock %};
|
{% block page_size %}
|
||||||
margin: {% block page_margin %}2cm{% endblock %};
|
size: {{ default_page_size }};
|
||||||
|
{% endblock %}
|
||||||
|
{% block page_margin %}
|
||||||
|
margin: 2cm;
|
||||||
|
{% endblock %}
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
font-size: 75%;
|
font-size: 75%;
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -6,10 +6,13 @@ import os
|
|||||||
|
|
||||||
from django import template
|
from django import template
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
from part.models import Part
|
from part.models import Part
|
||||||
from stock.models import StockItem
|
from stock.models import StockItem
|
||||||
|
|
||||||
|
from common.models import InvenTreeSetting
|
||||||
|
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
@ -19,10 +22,17 @@ def asset(filename):
|
|||||||
Return fully-qualified path for an upload report asset file.
|
Return fully-qualified path for an upload report asset file.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
path = os.path.join(settings.MEDIA_ROOT, 'report', 'assets', filename)
|
# If in debug mode, return URL to the image, not a local file
|
||||||
path = os.path.abspath(path)
|
debug_mode = InvenTreeSetting.get_setting('REPORT_DEBUG_MODE')
|
||||||
|
|
||||||
return f"file://{path}"
|
if debug_mode:
|
||||||
|
path = os.path.join(settings.MEDIA_URL, 'report', 'assets', filename)
|
||||||
|
else:
|
||||||
|
|
||||||
|
path = os.path.join(settings.MEDIA_ROOT, 'report', 'assets', filename)
|
||||||
|
path = os.path.abspath(path)
|
||||||
|
|
||||||
|
return f"file://{path}"
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag()
|
@register.simple_tag()
|
||||||
@ -31,6 +41,9 @@ def part_image(part):
|
|||||||
Return a fully-qualified path for a part image
|
Return a fully-qualified path for a part image
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# If in debug mode, return URL to the image, not a local file
|
||||||
|
debug_mode = InvenTreeSetting.get_setting('REPORT_DEBUG_MODE')
|
||||||
|
|
||||||
if type(part) is Part:
|
if type(part) is Part:
|
||||||
img = part.image.name
|
img = part.image.name
|
||||||
|
|
||||||
@ -40,13 +53,50 @@ def part_image(part):
|
|||||||
else:
|
else:
|
||||||
img = ''
|
img = ''
|
||||||
|
|
||||||
path = os.path.join(settings.MEDIA_ROOT, img)
|
if debug_mode:
|
||||||
path = os.path.abspath(path)
|
if img:
|
||||||
|
return os.path.join(settings.MEDIA_URL, img)
|
||||||
|
else:
|
||||||
|
return os.path.join(settings.STATIC_URL, 'img', 'blank_image.png')
|
||||||
|
|
||||||
if not os.path.exists(path) or not os.path.isfile(path):
|
else:
|
||||||
# Image does not exist
|
path = os.path.join(settings.MEDIA_ROOT, img)
|
||||||
# Return the 'blank' image
|
|
||||||
path = os.path.join(settings.STATIC_ROOT, 'img', 'blank_image.png')
|
|
||||||
path = os.path.abspath(path)
|
path = os.path.abspath(path)
|
||||||
|
|
||||||
return f"file://{path}"
|
if not os.path.exists(path) or not os.path.isfile(path):
|
||||||
|
# Image does not exist
|
||||||
|
# Return the 'blank' image
|
||||||
|
path = os.path.join(settings.STATIC_ROOT, 'img', 'blank_image.png')
|
||||||
|
path = os.path.abspath(path)
|
||||||
|
|
||||||
|
return f"file://{path}"
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag()
|
||||||
|
def internal_link(link, text):
|
||||||
|
"""
|
||||||
|
Make a <a></a> href which points to an InvenTree URL.
|
||||||
|
|
||||||
|
Important Note: This only works if the INVENTREE_BASE_URL parameter is set!
|
||||||
|
|
||||||
|
If the INVENTREE_BASE_URL parameter is not configured,
|
||||||
|
the text will be returned (unlinked)
|
||||||
|
"""
|
||||||
|
|
||||||
|
text = str(text)
|
||||||
|
|
||||||
|
base_url = InvenTreeSetting.get_setting('INVENTREE_BASE_URL')
|
||||||
|
|
||||||
|
# If the base URL is not set, just return the text
|
||||||
|
if not base_url:
|
||||||
|
return text
|
||||||
|
|
||||||
|
if not base_url.endswith('/'):
|
||||||
|
base_url += '/'
|
||||||
|
|
||||||
|
if base_url.endswith('/') and link.startswith('/'):
|
||||||
|
link = link[1:]
|
||||||
|
|
||||||
|
url = f"{base_url}{link}"
|
||||||
|
|
||||||
|
return mark_safe(f'<a href="{url}">{text}</a>')
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
{% include "InvenTree/settings/header.html" %}
|
{% include "InvenTree/settings/header.html" %}
|
||||||
<tbody>
|
<tbody>
|
||||||
{% include "InvenTree/settings/setting.html" with key="INVENTREE_INSTANCE" icon="fa-info-circle" %}
|
{% include "InvenTree/settings/setting.html" with key="INVENTREE_INSTANCE" icon="fa-info-circle" %}
|
||||||
|
{% include "InvenTree/settings/setting.html" with key="INVENTREE_BASE_URL" icon="fa-globe" %}
|
||||||
{% include "InvenTree/settings/setting.html" with key="INVENTREE_COMPANY_NAME" icon="fa-building" %}
|
{% include "InvenTree/settings/setting.html" with key="INVENTREE_COMPANY_NAME" icon="fa-building" %}
|
||||||
{% include "InvenTree/settings/setting.html" with key="INVENTREE_DEFAULT_CURRENCY" icon="fa-dollar-sign" %}
|
{% include "InvenTree/settings/setting.html" with key="INVENTREE_DEFAULT_CURRENCY" icon="fa-dollar-sign" %}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
@ -637,6 +637,12 @@ function loadBuildTable(table, options) {
|
|||||||
visible: false,
|
visible: false,
|
||||||
switchable: false,
|
switchable: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
checkbox: true,
|
||||||
|
title: '{% trans "Select" %}',
|
||||||
|
searchable: false,
|
||||||
|
switchable: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
field: 'reference',
|
field: 'reference',
|
||||||
title: '{% trans "Build" %}',
|
title: '{% trans "Build" %}',
|
||||||
@ -717,6 +723,13 @@ function loadBuildTable(table, options) {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
linkButtonsToSelection(
|
||||||
|
table,
|
||||||
|
[
|
||||||
|
'#build-print-options',
|
||||||
|
]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
|
|
||||||
function selectTestReport(reports, items, options={}) {
|
function selectReport(reports, items, options={}) {
|
||||||
/**
|
/**
|
||||||
* Present the user with the available test reports,
|
* Present the user with the available reports,
|
||||||
* and allow them to select which test report to print.
|
* and allow them to select which report to print.
|
||||||
*
|
*
|
||||||
* The intent is that the available report templates have been requested
|
* The intent is that the available report templates have been requested
|
||||||
* (via AJAX) from the server.
|
* (via AJAX) from the server.
|
||||||
@ -44,7 +44,7 @@ function selectTestReport(reports, items, options={}) {
|
|||||||
|
|
||||||
html += `
|
html += `
|
||||||
<div class='alert alert-block alert-info'>
|
<div class='alert alert-block alert-info'>
|
||||||
${items.length} {% trans "stock items selected" %}
|
${items.length} {% trans "items selected" %}
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,7 +102,7 @@ function printTestReports(items, options={}) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Request available labels from the server
|
// Request available reports from the server
|
||||||
inventreeGet(
|
inventreeGet(
|
||||||
'{% url "api-stockitem-testreport-list" %}',
|
'{% url "api-stockitem-testreport-list" %}',
|
||||||
{
|
{
|
||||||
@ -121,7 +121,7 @@ function printTestReports(items, options={}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Select report template to print
|
// Select report template to print
|
||||||
selectTestReport(
|
selectReport(
|
||||||
response,
|
response,
|
||||||
items,
|
items,
|
||||||
{
|
{
|
||||||
@ -129,7 +129,7 @@ function printTestReports(items, options={}) {
|
|||||||
var href = `/api/report/test/${pk}/print/?`;
|
var href = `/api/report/test/${pk}/print/?`;
|
||||||
|
|
||||||
items.forEach(function(item) {
|
items.forEach(function(item) {
|
||||||
href += `items[]=${item}&`;
|
href += `item=${item}&`;
|
||||||
});
|
});
|
||||||
|
|
||||||
window.location.href = href;
|
window.location.href = href;
|
||||||
@ -139,4 +139,111 @@ function printTestReports(items, options={}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function printBuildReports(builds, options={}) {
|
||||||
|
/**
|
||||||
|
* Print Build report for the provided build(s)
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (builds.length == 0) {
|
||||||
|
showAlertDialog(
|
||||||
|
'{% trans "Select Builds" %}',
|
||||||
|
'{% trans "Build(s) must be selected before printing reports" %}',
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
inventreeGet(
|
||||||
|
'{% url "api-build-report-list" %}',
|
||||||
|
{
|
||||||
|
enabled: true,
|
||||||
|
builds: builds,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
success: function(response) {
|
||||||
|
if (response.length == 0) {
|
||||||
|
showAlertDialog(
|
||||||
|
'{% trans "No Reports Found" %}',
|
||||||
|
'{% trans "No report templates found which match selected build(s)" %}'
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select which report to print
|
||||||
|
selectReport(
|
||||||
|
response,
|
||||||
|
builds,
|
||||||
|
{
|
||||||
|
success: function(pk) {
|
||||||
|
var href = `/api/report/build/${pk}/print/?`;
|
||||||
|
|
||||||
|
builds.forEach(function(build) {
|
||||||
|
href += `build=${build}&`;
|
||||||
|
});
|
||||||
|
|
||||||
|
window.location.href = href;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function printBomReports(parts, options={}) {
|
||||||
|
/**
|
||||||
|
* Print BOM reports for the provided part(s)
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (parts.length == 0) {
|
||||||
|
showAlertDialog(
|
||||||
|
'{% trans "Select Parts" %}',
|
||||||
|
'{% trans "Part(s) must be selected before printing reports" %}'
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request available reports from the server
|
||||||
|
inventreeGet(
|
||||||
|
'{% url "api-bom-report-list" %}',
|
||||||
|
{
|
||||||
|
enabled: true,
|
||||||
|
parts: parts,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
success: function(response) {
|
||||||
|
if (response.length == 0) {
|
||||||
|
showAlertDialog(
|
||||||
|
'{% trans "No Reports Found" %}',
|
||||||
|
'{% trans "No report templates found which match selected part(s)" %}',
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select which report to print
|
||||||
|
selectReport(
|
||||||
|
response,
|
||||||
|
parts,
|
||||||
|
{
|
||||||
|
success: function(pk) {
|
||||||
|
var href = `/api/report/bom/${pk}/print/?`;
|
||||||
|
|
||||||
|
parts.forEach(function(part) {
|
||||||
|
href += `part=${part}&`;
|
||||||
|
});
|
||||||
|
|
||||||
|
window.location.href = href;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
@ -120,6 +120,8 @@ class RuleSet(models.Model):
|
|||||||
'report_reportasset',
|
'report_reportasset',
|
||||||
'report_reportsnippet',
|
'report_reportsnippet',
|
||||||
'report_testreport',
|
'report_testreport',
|
||||||
|
'report_buildreport',
|
||||||
|
'report_billofmaterialsreport',
|
||||||
'part_partstar',
|
'part_partstar',
|
||||||
'users_owner',
|
'users_owner',
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user