Merge remote-tracking branch 'inventree/master'

This commit is contained in:
Oliver Walters 2020-02-02 12:48:04 +11:00
commit 40037d826d
57 changed files with 2535 additions and 1128 deletions

View File

@ -7,7 +7,7 @@ python:
addons:
apt-packages:
-sqlite3
- sqlite3
before_install:
- sudo apt-get update

View File

@ -17,6 +17,8 @@ import logging
import tempfile
import yaml
from datetime import datetime
from django.utils.translation import gettext_lazy as _
@ -101,6 +103,8 @@ INSTALLED_APPS = [
'django_cleanup', # Automatically delete orphaned MEDIA files
'qr_code', # Generate QR codes
'mptt', # Modified Preorder Tree Traversal
'markdownx', # Markdown editing
'markdownify', # Markdown template rendering
]
LOGGING = {
@ -161,6 +165,37 @@ REST_FRAMEWORK = {
WSGI_APPLICATION = 'InvenTree.wsgi.application'
# Markdownx configuration
# Ref: https://neutronx.github.io/django-markdownx/customization/
MARKDOWNX_MEDIA_PATH = datetime.now().strftime('markdownx/%Y/%m/%d')
# Markdownify configuration
# Ref: https://django-markdownify.readthedocs.io/en/latest/settings.html
MARKDOWNIFY_WHITELIST_TAGS = [
'a',
'abbr',
'b',
'blockquote',
'em',
'h1', 'h2', 'h3',
'i',
'img',
'li',
'ol',
'p',
'strong',
'ul'
]
MARKDOWNIFY_WHITELIST_ATTRS = [
'href',
'src',
'alt',
]
MARKDOWNIFY_BLEACH = True
DATABASES = {}
"""

View File

@ -5,6 +5,29 @@
--basic-color: #333;
}
.markdownx .row {
margin: 5px;
padding: 5px;
border: 1px solid #cce;
border-radius: 4px;
}
.markdownx-editor {
width: 100%;
border: 1px solid #cce;
border-radius: 3px;
padding: 10px;
}
.panel-content {
padding: 10px;
}
.markdownx-preview {
border: 1px solid #cce;
border-radius: 3px;
padding: 10px;
}
.qr-code {
max-width: 400px;
@ -305,6 +328,12 @@
width: 100%;
}
input[type="submit"] {
color: #333;
background-color: #e6e6e6;
border-color: #adadad;
}
.modal textarea {
width: 100%;
}

View File

@ -100,6 +100,8 @@ urlpatterns = [
url(r'^api/', include(apipatterns)),
url(r'^api-doc/', include_docs_urls(title='InvenTree API')),
url(r'^markdownx/', include('markdownx.urls')),
]
# Static file access

View File

@ -4,7 +4,7 @@ Provides information on the current InvenTree version
import subprocess
INVENTREE_SW_VERSION = "0.0.7"
INVENTREE_SW_VERSION = "0.0.8"
def inventreeVersion():
@ -15,6 +15,12 @@ def inventreeVersion():
def inventreeCommitHash():
""" Returns the git commit hash for the running codebase """
commit = str(subprocess.check_output('git rev-parse --short HEAD'.split()), 'utf-8').strip()
return str(subprocess.check_output('git rev-parse --short HEAD'.split()), 'utf-8').strip()
return commit
def inventreeCommitDate():
""" Returns the git commit date for the running codebase """
d = str(subprocess.check_output('git show -s --format=%ci'.split()), 'utf-8').strip()
return d.split(' ')[0]

View File

@ -26,7 +26,6 @@ class EditBuildForm(HelperForm):
'take_from',
'batch',
'URL',
'notes',
]

View File

@ -0,0 +1,19 @@
# Generated by Django 2.2.9 on 2020-02-01 12:47
from django.db import migrations
import markdownx.models
class Migration(migrations.Migration):
dependencies = [
('build', '0007_auto_20191118_2321'),
]
operations = [
migrations.AlterField(
model_name='build',
name='notes',
field=markdownx.models.MarkdownxField(blank=True, help_text='Extra build notes'),
),
]

View File

@ -16,6 +16,8 @@ from django.db import models, transaction
from django.db.models import Sum
from django.core.validators import MinValueValidator
from markdownx.models import MarkdownxField
from InvenTree.status_codes import BuildStatus
from InvenTree.fields import InvenTreeURLField
@ -92,7 +94,7 @@ class Build(models.Model):
URL = InvenTreeURLField(blank=True, help_text=_('Link to external URL'))
notes = models.TextField(blank=True, help_text=_('Extra build notes'))
notes = MarkdownxField(blank=True, help_text=_('Extra build notes'))
@transaction.atomic
def cancelBuild(self, user):

View File

@ -1,74 +1,67 @@
{% extends "build/build_base.html" %}
{% load static %}
{% load i18n %}
{% block details %}
{% include "build/tabs.html" with tab='details' %}
<h4>Build Details</h4>
<h4>{% trans "Build Details" %}</h4>
<hr>
<table class='table table-striped'>
<tr>
<td>Title</td><td>{{ build.title }}</td>
<td>{% trans "Title" %}</td><td>{{ build.title }}</td>
</tr>
<tr>
<td>Part</td><td><a href="{% url 'part-build' build.part.id %}">{{ build.part.full_name }}</a></td>
<td>{% trans "Part" %}</td><td><a href="{% url 'part-build' build.part.id %}">{{ build.part.full_name }}</a></td>
</tr>
<tr>
<td>Quantity</td><td>{{ build.quantity }}</td>
<td>{% trans "Quantity" %}</td><td>{{ build.quantity }}</td>
</tr>
<tr>
<td>Stock Source</td>
<td>{% trans "Stock Source" %}</td>
<td>
{% if build.take_from %}
<a href="{% url 'stock-location-detail' build.take_from.id %}">{{ build.take_from }}</a>
{% else %}
Stock can be taken from any available location.
{% trans "Stock can be taken from any available location." %}
{% endif %}
</td>
</tr>
<tr>
<td>Status</td><td>{% include "build_status.html" with build=build %}</td>
<td>{% trans "Status" %}</td><td>{% include "build_status.html" with build=build %}</td>
</tr>
{% if build.batch %}
<tr>
<td>Batch</td><td>{{ build.batch }}</td>
<td>{% trans "Batch" %}</td><td>{{ build.batch }}</td>
</tr>
{% endif %}
{% if build.URL %}
<tr>
<td>URL</td><td><a href="{{ build.URL }}">{{ build.URL }}</a></td>
<td>{% trans "URL" %}</td><td><a href="{{ build.URL }}">{{ build.URL }}</a></td>
</tr>
{% endif %}
<tr>
<td>Created</td><td>{{ build.creation_date }}</td>
<td>{% trans "Created" %}</td><td>{{ build.creation_date }}</td>
</tr>
{% if build.is_active %}
<tr>
<td>Enough Parts?</td>
<td>{% trans "Enough Parts?" %}</td>
<td>
{% if build.can_build %}
Yes
{% trans "Yes" %}
{% else %}
No
{% trans "No" %}
{% endif %}
</td>
</tr>
{% endif %}
{% if build.completion_date %}
<tr>
<td>Completed</td><td>{{ build.completion_date }}{% if build.completed_by %}<span class='badge'>{{ build.completed_by }}</span>{% endif %}</td>
<td>{% trans "Completed" %}</td><td>{{ build.completion_date }}{% if build.completed_by %}<span class='badge'>{{ build.completed_by }}</span>{% endif %}</td>
</tr>
{% endif %}
</table>
{% if build.notes %}
<div class="panel panel-default">
<div class="panel-heading"><b>Notes</b></div>
<div class="panel-body">{{ build.notes }}</div>
</div>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,56 @@
{% extends "build/build_base.html" %}
{% load static %}
{% load i18n %}
{% load markdownify %}
{% block details %}
{% include "build/tabs.html" with tab='notes' %}
{% if editing %}
<h4>{% trans "Build Notes" %}</h4>
<hr>
<form method='POST'>
{% csrf_token %}
{{ form }}
<hr>
<input type="submit" value='{% trans "Save" %}'/>
</form>
{{ form.media }}
{% else %}
<div class='row'>
<div class='col-sm-6'>
<h4>{% trans "Build Notes" %}</h4>
</div>
<div class='col-sm-6'>
<button title='{% trans "Edit notes" %}' class='btn btn-default btn-glyph float-right' id='edit-notes'><span class='glyphicon glyphicon-edit'></span></button>
</div>
</div>
<hr>
<div class='panel panel-default'>
<div class='panel-content'>
{{ build.notes | markdownify }}
</div>
</div>
{% endif %}
{% endblock %}
{% block js_ready %}
{{ block.super }}
{% if editing %}
{% else %}
$("#edit-notes").click(function() {
location.href = "{% url 'build-notes' build.id %}?edit=1";
});
{% endif %}
{% endblock %}

View File

@ -1,8 +1,13 @@
{% load i18n %}
<ul class='nav nav-tabs'>
<li{% if tab == 'details' %} class='active'{% endif %}>
<a href="{% url 'build-detail' build.id %}">Details</a>
<a href="{% url 'build-detail' build.id %}">{% trans "Details" %}</a>
</li>
<li{% if tab == 'notes' %} class='active'{% endif %}>
<a href="{% url 'build-notes' build.id %}">{% trans "Notes" %}{% if build.notes %} <span class='glyphicon glyphicon-small glyphicon-info-sign'></span>{% endif %}</a>
</li>
<li{% if tab == 'allocate' %} class='active'{% endif %}>
<a href="{% url 'build-allocate' build.id %}">Assign Parts</a>
<a href="{% url 'build-allocate' build.id %}">{% trans "Assign Parts" %}</a>
</li>
</ul>

View File

@ -25,6 +25,7 @@ build_detail_urls = [
url(r'^auto-allocate/?', views.BuildAutoAllocate.as_view(), name='build-auto-allocate'),
url(r'^unallocate/', views.BuildUnallocate.as_view(), name='build-unallocate'),
url(r'^notes/', views.BuildNotes.as_view(), name='build-notes'),
url(r'^.*$', views.BuildDetail.as_view(), name='build-detail'),
]

View File

@ -7,8 +7,9 @@ from __future__ import unicode_literals
from django.utils.translation import ugettext as _
from django.core.exceptions import ValidationError
from django.views.generic import DetailView, ListView
from django.views.generic import DetailView, ListView, UpdateView
from django.forms import HiddenInput
from django.urls import reverse
from part.models import Part
from .models import Build, BuildItem
@ -309,6 +310,28 @@ class BuildComplete(AjaxUpdateView):
}
class BuildNotes(UpdateView):
""" View for editing the 'notes' field of a Build object.
"""
context_object_name = 'build'
template_name = 'build/notes.html'
model = Build
fields = ['notes']
def get_success_url(self):
return reverse('build-notes', kwargs={'pk': self.get_object().id})
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['editing'] = str2bool(self.request.GET.get('edit', ''))
return ctx
class BuildDetail(DetailView):
""" Detail view of a single Build object. """
model = Build

View File

@ -27,7 +27,6 @@ class EditCompanyForm(HelperForm):
'contact',
'is_customer',
'is_supplier',
'notes'
]

View File

@ -0,0 +1,19 @@
# Generated by Django 2.2.9 on 2020-02-01 12:31
from django.db import migrations
import markdownx.models
class Migration(migrations.Migration):
dependencies = [
('company', '0009_auto_20191118_2323'),
]
operations = [
migrations.AlterField(
model_name='company',
name='notes',
field=markdownx.models.MarkdownxField(blank=True),
),
]

View File

@ -10,6 +10,7 @@ import os
import math
from decimal import Decimal
from django.utils.translation import gettext_lazy as _
from django.core.validators import MinValueValidator
from django.db import models
from django.db.models import Sum
@ -18,6 +19,8 @@ from django.apps import apps
from django.urls import reverse
from django.conf import settings
from markdownx.models import MarkdownxField
from InvenTree.fields import InvenTreeURLField
from InvenTree.status_codes import OrderStatus
from common.models import Currency
@ -68,32 +71,32 @@ class Company(models.Model):
"""
name = models.CharField(max_length=100, blank=False, unique=True,
help_text='Company name')
help_text=_('Company name'))
description = models.CharField(max_length=500, help_text='Description of the company')
description = models.CharField(max_length=500, help_text=_('Description of the company'))
website = models.URLField(blank=True, help_text='Company website URL')
website = models.URLField(blank=True, help_text=_('Company website URL'))
address = models.CharField(max_length=200,
blank=True, help_text='Company address')
blank=True, help_text=_('Company address'))
phone = models.CharField(max_length=50,
blank=True, help_text='Contact phone number')
blank=True, help_text=_('Contact phone number'))
email = models.EmailField(blank=True, help_text='Contact email address')
email = models.EmailField(blank=True, help_text=_('Contact email address'))
contact = models.CharField(max_length=100,
blank=True, help_text='Point of contact')
blank=True, help_text=_('Point of contact'))
URL = InvenTreeURLField(blank=True, help_text='Link to external company information')
URL = InvenTreeURLField(blank=True, help_text=_('Link to external company information'))
image = models.ImageField(upload_to=rename_company_image, max_length=255, null=True, blank=True)
notes = models.TextField(blank=True)
notes = MarkdownxField(blank=True)
is_customer = models.BooleanField(default=False, help_text='Do you sell items to this company?')
is_customer = models.BooleanField(default=False, help_text=_('Do you sell items to this company?'))
is_supplier = models.BooleanField(default=True, help_text='Do you purchase items from this company?')
is_supplier = models.BooleanField(default=True, help_text=_('Do you purchase items from this company?'))
def __str__(self):
""" Get string representation of a Company """
@ -223,32 +226,32 @@ class SupplierPart(models.Model):
'purchaseable': True,
'is_template': False,
},
help_text='Select part',
help_text=_('Select part'),
)
supplier = models.ForeignKey(Company, on_delete=models.CASCADE,
related_name='parts',
limit_choices_to={'is_supplier': True},
help_text='Select supplier',
help_text=_('Select supplier'),
)
SKU = models.CharField(max_length=100, help_text='Supplier stock keeping unit')
SKU = models.CharField(max_length=100, help_text=_('Supplier stock keeping unit'))
manufacturer = models.CharField(max_length=100, blank=True, help_text='Manufacturer')
manufacturer = models.CharField(max_length=100, blank=True, help_text=_('Manufacturer'))
MPN = models.CharField(max_length=100, blank=True, help_text='Manufacturer part number')
MPN = models.CharField(max_length=100, blank=True, help_text=_('Manufacturer part number'))
URL = InvenTreeURLField(blank=True, help_text='URL for external supplier part link')
URL = InvenTreeURLField(blank=True, help_text=_('URL for external supplier part link'))
description = models.CharField(max_length=250, blank=True, help_text='Supplier part description')
description = models.CharField(max_length=250, blank=True, help_text=_('Supplier part description'))
note = models.CharField(max_length=100, blank=True, help_text='Notes')
note = models.CharField(max_length=100, blank=True, help_text=_('Notes'))
base_cost = models.DecimalField(max_digits=10, decimal_places=3, default=0, validators=[MinValueValidator(0)], help_text='Minimum charge (e.g. stocking fee)')
base_cost = models.DecimalField(max_digits=10, decimal_places=3, default=0, validators=[MinValueValidator(0)], help_text=_('Minimum charge (e.g. stocking fee)'))
packaging = models.CharField(max_length=50, blank=True, help_text='Part packaging')
packaging = models.CharField(max_length=50, blank=True, help_text=_('Part packaging'))
multiple = models.PositiveIntegerField(default=1, validators=[MinValueValidator(1)], help_text='Order multiple')
multiple = models.PositiveIntegerField(default=1, validators=[MinValueValidator(1)], help_text=('Order multiple'))
# TODO - Reimplement lead-time as a charfield with special validation (pattern matching).
# lead_time = models.DurationField(blank=True, null=True)

View File

@ -18,13 +18,6 @@
</tr>
</table>
{% if company.notes %}
<div class="panel panel-default">
<div class="panel-heading"><b>Notes</b></div>
<div class="panel-body">{{ company.notes }}</div>
</div>
{% endif %}
{% endblock %}
{% block js_ready %}
{{ block.super }}

View File

@ -0,0 +1,53 @@
{% extends "company/company_base.html" %}
{% load static %}
{% load i18n %}
{% block details %}
{% load markdownify %}
{% include 'company/tabs.html' with tab='notes' %}
{% if editing %}
<h4>{% trans "Company Notes" %}</h4>
<hr>
<form method='POST'>
{% csrf_token %}
{{ form }}
<hr>
<input type="submit" value='{% trans "Save" %}'/>
</form>
{{ form.media }}
{% else %}
<div class='row'>
<div class='col-sm-6'>
<h4>{% trans "Company Notes" %}</h4>
</div>
<div class='col-sm-6'>
<button title='{% trans "Edit notes" %}' class='btn btn-default btn-glyph float-right' id='edit-notes'><span class='glyphicon glyphicon-edit'></span></button>
</div>
</div>
<hr>
<div class='panel panel-default'>
<div class='panel-content'>
{{ company.notes | markdownify }}
</div>
</div>
{% endif %}
{% endblock %}
{% block js_ready %}
{{ block.super }}
{% if editing %}
{% else %}
$("#edit-notes").click(function() {
location.href = "{% url 'company-notes' company.id %}?edit=1";
});
{% endif %}
{% endblock %}

View File

@ -1,23 +1,28 @@
{% load i18n %}
<ul class='nav nav-tabs'>
<li{% if tab == 'details' %} class='active'{% endif %}>
<a href="{% url 'company-detail' company.id %}">Details</a>
<a href="{% url 'company-detail' company.id %}">{% trans "Details" %}</a>
</li>
{% if company.is_supplier %}
<li{% if tab == 'parts' %} class='active'{% endif %}>
<a href="{% url 'company-detail-parts' company.id %}">Supplier Parts <span class='badge'>{{ company.part_count }}</span></a>
<a href="{% url 'company-detail-parts' company.id %}">{% trans "Supplier Parts" %} <span class='badge'>{{ company.part_count }}</span></a>
</li>
<li{% if tab == 'stock' %} class='active'{% endif %}>
<a href="{% url 'company-detail-stock' company.id %}">Stock <span class='badge'>{{ company.stock_count }}</a>
<a href="{% url 'company-detail-stock' company.id %}">{% trans "Stock" %} <span class='badge'>{{ company.stock_count }}</a>
</li>
<li{% if tab == 'po' %} class='active'{% endif %}>
<a href="{% url 'company-detail-purchase-orders' company.id %}">Purchase Orders <span class='badge'>{{ company.purchase_orders.count }}</span></a>
<a href="{% url 'company-detail-purchase-orders' company.id %}">{% trans "Purchase Orders" %} <span class='badge'>{{ company.purchase_orders.count }}</span></a>
</li>
{% endif %}
{% if company.is_customer %}
{% if 0 %}
<li{% if tab == 'co' %} class='active'{% endif %}>
<a href="#">Sales Orders</a>
<a href="#">{% trans "Sales Orders" %}</a>
</li>
{% endif %}
{% endif %}
<li{% if tab == 'notes' %} class='active'{% endif %}>
<a href="{% url 'company-notes' company.id %}">{% trans "Notes" %}{% if company.notes %} <span class='glyphicon glyphicon-small glyphicon-info-sign'></span>{% endif %}</a>
</li>
</ul>

View File

@ -18,6 +18,7 @@ company_detail_urls = [
url(r'parts/?', views.CompanyDetail.as_view(template_name='company/detail_part.html'), name='company-detail-parts'),
url(r'stock/?', views.CompanyDetail.as_view(template_name='company/detail_stock.html'), name='company-detail-stock'),
url(r'purchase-orders/?', views.CompanyDetail.as_view(template_name='company/detail_purchase_orders.html'), name='company-detail-purchase-orders'),
url(r'notes/?', views.CompanyNotes.as_view(), name='company-notes'),
url(r'thumbnail/?', views.CompanyImage.as_view(), name='company-image'),

View File

@ -6,8 +6,9 @@ Django views for interacting with Company app
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.views.generic import DetailView, ListView
from django.views.generic import DetailView, ListView, UpdateView
from django.urls import reverse
from django.forms import HiddenInput
from InvenTree.views import AjaxCreateView, AjaxUpdateView, AjaxDeleteView
@ -52,6 +53,28 @@ class CompanyIndex(ListView):
return queryset
class CompanyNotes(UpdateView):
""" View for editing the 'notes' field of a Company object.
"""
context_object_name = 'company'
template_name = 'company/notes.html'
model = Company
fields = ['notes']
def get_success_url(self):
return reverse('company-notes', kwargs={'pk': self.get_object().id})
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['editing'] = str2bool(self.request.GET.get('edit', ''))
return ctx
class CompanyDetail(DetailView):
""" Detail view for Company object """
context_obect_name = 'company'

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,19 @@
# Generated by Django 2.2.9 on 2020-02-01 23:46
from django.db import migrations
import markdownx.models
class Migration(migrations.Migration):
dependencies = [
('order', '0014_auto_20191118_2328'),
]
operations = [
migrations.AlterField(
model_name='purchaseorder',
name='notes',
field=markdownx.models.MarkdownxField(blank=True, help_text='Order notes'),
),
]

View File

@ -12,6 +12,8 @@ from django.contrib.auth.models import User
from django.urls import reverse
from django.utils.translation import ugettext as _
from markdownx.models import MarkdownxField
from datetime import datetime
from stock.models import StockItem
@ -81,7 +83,7 @@ class Order(models.Model):
complete_date = models.DateField(blank=True, null=True)
notes = models.TextField(blank=True, help_text=_('Order notes'))
notes = MarkdownxField(blank=True, help_text=_('Order notes'))
def place_order(self):
""" Marks the order as PLACED. Order must be currently PENDING. """

View File

@ -0,0 +1,132 @@
{% extends "base.html" %}
{% load i18n %}
{% load static %}
{% load inventree_extras %}
{% block page_title %}
InvenTree | {{ order }}
{% endblock %}
{% block content %}
<div class='row'>
<div class='col-sm-6'>
<div class='media'>
<div class='media-left'>
<img class='part-thumb'
{% if order.supplier.image %}
src="{{ order.supplier.image.url }}"
{% else %}
src="{% static 'img/blank_image.png' %}"
{% endif %}
/>
</div>
<div class='media-body'>
<h4>{{ order }}</h4>
<p>{{ order.description }}</p>
{% if order.URL %}
<a href="{{ order.URL }}">{{ order.URL }}</a>
{% endif %}
<p>
<div class='btn-row'>
<div class='btn-group'>
<button type='button' class='btn btn-default btn-glyph' id='edit-order' title='Edit order information'>
<span class='glyphicon glyphicon-edit'></span>
</button>
<button type='button' class='btn btn-default btn-glyph' id='export-order' title='Export order to file'>
<span class='glyphicon glyphicon-download-alt'></span>
</button>
{% if order.status == OrderStatus.PENDING and order.lines.count > 0 %}
<button type='button' class='btn btn-default btn-glyph' id='place-order' title='Place order'>
<span class='glyphicon glyphicon-send'></span>
</button>
{% elif order.status == OrderStatus.PLACED %}
<button type='button' class='btn btn-default btn-glyph' id='receive-order' title='Receive items'>
<span class='glyphicon glyphicon-check'></span>
</button>
<button type='button' class='btn btn-default btn-glyph' id='complete-order' title='Mark order as complete'>
<span class='glyphicon glyphicon-ok'></span>
</button>
{% endif %}
{% if order.status == OrderStatus.PENDING or order.status == OrderStatus.PLACED %}
<button type='button' class='btn btn-default btn-glyph' id='cancel-order' title='Cancel order'>
<span class='glyphicon glyphicon-remove'></span>
</button>
{% endif %}
</div>
</div>
</p>
</div>
</div>
</div>
<div class='col-sm-6'>
<h4>{% trans "Purchase Order Details" %}</h4>
<table class='table'>
<tr>
<td>{% trans "Supplier" %}</td>
<td><a href="{% url 'company-detail' order.supplier.id %}">{{ order.supplier }}</a></td>
</tr>
<tr>
<td>{% trans "Status" %}</td>
<td>{% include "order/order_status.html" %}</td>
</tr>
<tr>
<td>{% trans "Created" %}</td>
<td>{{ order.creation_date }}<span class='badge'>{{ order.created_by }}</span></td>
</tr>
{% if order.issue_date %}
<tr>
<td>{% trans "Issued" %}</td>
<td>{{ order.issue_date }}</td>
</tr>
{% endif %}
{% if order.status == OrderStatus.COMPLETE %}
<tr>
<td>{% trans "Received" %}</td>
<td>{{ order.complete_date }}<span class='badge'>{{ order.received_by }}</span></td>
</tr>
{% endif %}
</table>
</div>
</div>
<hr>
<div class='container-fluid'>
{% block details %}
<!-- Specific order details to go here -->
{% endblock %}
</div>
{% endblock %}
{% block js_ready %}
{{ block.super }}
{% if order.status == OrderStatus.PENDING and order.lines.count > 0 %}
$("#place-order").click(function() {
launchModalForm("{% url 'purchase-order-issue' order.id %}",
{
reload: true,
});
});
{% endif %}
$("#edit-order").click(function() {
launchModalForm("{% url 'purchase-order-edit' order.id %}",
{
reload: true,
}
);
});
$("#cancel-order").click(function() {
launchModalForm("{% url 'purchase-order-cancel' order.id %}", {
reload: true,
});
});
{% endblock %}

View File

@ -0,0 +1,57 @@
{% extends "order/order_base.html" %}
{% load inventree_extras %}
{% load i18n %}
{% load static %}
{% load markdownify %}
{% block details %}
{% include 'order/tabs.html' with tab='notes' %}
{% if editing %}
<h4>{% trans "Order Notes" %}</h4>
<hr>
<form method='POST'>
{% csrf_token %}
{{ form }}
<hr>
<input type='submit' value='{% trans "Save" %}'/>
</form>
{{ form.media }}
{% else %}
<div class='row'>
<div class='col-sm-6'>
<h4>{% trans "Order Notes" %}</h4>
</div>
<div class='col-sm-6'>
<button title='{% trans "Edit notes" %}' class='btn btn-default btn-glyph float-right' id='edit-notes'><span class='glyphicon glyphicon-edit'></span></button>
</div>
</div>
<hr>
<div class='panel panel-default'>
<div class='panel-content'>
{{ order.notes | markdownify }}
</div>
</div>
{% endif %}
{% endblock %}
{% block js_ready %}
{{ block.super }}
{% if editing %}
{% else %}
$("#edit-notes").click(function() {
location.href = "{% url 'purchase-order-notes' order.id %}?edit=1";
});
{% endif %}
{% endblock %}

View File

@ -1,104 +1,22 @@
{% extends "base.html" %}
{% extends "order/order_base.html" %}
{% load inventree_extras %}
{% load i18n %}
{% load static %}
{% load inventree_extras %}
{% block page_title %}
InvenTree | {{ order }}
{% endblock %}
{% block details %}
{% block content %}
<div class='row'>
<div class='col-sm-6'>
<div class='media'>
<div class='media-left'>
<img class='part-thumb'
{% if order.supplier.image %}
src="{{ order.supplier.image.url }}"
{% else %}
src="{% static 'img/blank_image.png' %}"
{% endif %}
/>
</div>
<div class='media-body'>
<h4>{{ order }}</h4>
<p>{{ order.description }}</p>
{% if order.URL %}
<a href="{{ order.URL }}">{{ order.URL }}</a>
{% endif %}
<p>
<div class='btn-row'>
<div class='btn-group'>
<button type='button' class='btn btn-default btn-glyph' id='edit-order' title='Edit order information'>
<span class='glyphicon glyphicon-edit'></span>
</button>
<button type='button' class='btn btn-default btn-glyph' id='export-order' title='Export order to file'>
<span class='glyphicon glyphicon-download-alt'></span>
</button>
{% if order.status == OrderStatus.PENDING and order.lines.count > 0 %}
<button type='button' class='btn btn-default btn-glyph' id='place-order' title='Place order'>
<span class='glyphicon glyphicon-send'></span>
</button>
{% elif order.status == OrderStatus.PLACED %}
<button type='button' class='btn btn-default btn-glyph' id='receive-order' title='Receive items'>
<span class='glyphicon glyphicon-check'></span>
</button>
<button type='button' class='btn btn-default btn-glyph' id='complete-order' title='Mark order as complete'>
<span class='glyphicon glyphicon-ok'></span>
</button>
{% endif %}
{% if order.status == OrderStatus.PENDING or order.status == OrderStatus.PLACED %}
<button type='button' class='btn btn-default btn-glyph' id='cancel-order' title='Cancel order'>
<span class='glyphicon glyphicon-remove'></span>
</button>
{% endif %}
</div>
</div>
</p>
</div>
</div>
</div>
<div class='col-sm-6'>
<h4>{% trans "Purchase Order Details" %}</h4>
<table class='table'>
<tr>
<td>{% trans "Supplier" %}</td>
<td><a href="{% url 'company-detail' order.supplier.id %}">{{ order.supplier }}</a></td>
</tr>
<tr>
<td>{% trans "Status" %}</td>
<td>{% include "order/order_status.html" %}</td>
</tr>
<tr>
<td>{% trans "Created" %}</td>
<td>{{ order.creation_date }}<span class='badge'>{{ order.created_by }}</span></td>
</tr>
{% if order.issue_date %}
<tr>
<td>{% trans "Issued" %}</td>
<td>{{ order.issue_date }}</td>
</tr>
{% endif %}
{% if order.status == OrderStatus.COMPLETE %}
<tr>
<td>{% trans "Received" %}</td>
<td>{{ order.complete_date }}<span class='badge'>{{ order.received_by }}</span></td>
</tr>
{% endif %}
</table>
</div>
</div>
{% include 'order/tabs.html' with tab='details' %}
<hr>
<div id='order-toolbar-buttons' class='btn-group' style='float: right;'>
{% if order.status == OrderStatus.PENDING %}
<button type='button' class='btn btn-default' id='new-po-line'>Add Line Item</button>
<button type='button' class='btn btn-default' id='new-po-line'>{% trans "Add Line Item" %}</button>
{% endif %}
</div>
<h4>Order Items</h4>
<h4>{% trans "Order Items" %}</h4>
<table class='table table-striped table-condensed' id='po-lines-table' data-toolbar='#order-toolbar-buttons'>
<thead>
@ -162,40 +80,11 @@ InvenTree | {{ order }}
</tbody>
</table>
{% if order.notes %}
<hr>
<div class='panel panel-default'>
<div class='panel-heading'><b>{% trans "Notes" %}</b></div>
<div class='panel-body'>{{ order.notes }}</div>
</div>
{% endif %}
{% endblock %}
{% block js_ready %}
{% if order.status == OrderStatus.PENDING and order.lines.count > 0 %}
$("#place-order").click(function() {
launchModalForm("{% url 'purchase-order-issue' order.id %}",
{
reload: true,
});
});
{% endif %}
$("#edit-order").click(function() {
launchModalForm("{% url 'purchase-order-edit' order.id %}",
{
reload: true,
}
);
});
$("#cancel-order").click(function() {
launchModalForm("{% url 'purchase-order-cancel' order.id %}", {
reload: true,
});
});
{{ block.super }}
$("#po-lines-table").on('click', ".line-receive", function() {

View File

@ -0,0 +1,10 @@
{% load i18n %}
<ul class='nav nav-tabs'>
<li{% ifequal tab 'details' %} class='active'{% endifequal %}>
<a href="{% url 'purchase-order-detail' order.id %}">{% trans "Items" %}</a>
</li>
<li{% ifequal tab 'notes' %} class='active'{% endifequal %}>
<a href="{% url 'purchase-order-notes' order.id %}">{% trans "Notes" %}{% if order.notes %} <span class='glyphicon glyphicon-small glyphicon-info-sign'></span>{% endif %}</a>
</li>
</ul>

View File

@ -19,6 +19,8 @@ purchase_order_detail_urls = [
url(r'^export/?', views.PurchaseOrderExport.as_view(), name='purchase-order-export'),
url(r'^notes/', views.PurchaseOrderNotes.as_view(), name='purchase-order-notes'),
url(r'^.*$', views.PurchaseOrderDetail.as_view(), name='purchase-order-detail'),
]

View File

@ -7,8 +7,9 @@ from __future__ import unicode_literals
from django.db import transaction
from django.shortcuts import get_object_or_404
from django.urls import reverse
from django.utils.translation import ugettext as _
from django.views.generic import DetailView, ListView
from django.views.generic import DetailView, ListView, UpdateView
from django.forms import HiddenInput
import logging
@ -69,6 +70,28 @@ class PurchaseOrderDetail(DetailView):
return ctx
class PurchaseOrderNotes(UpdateView):
""" View for updating the 'notes' field of a PurchaseOrder """
context_object_name = 'order'
template_name = 'order/order_notes.html'
model = PurchaseOrder
fields = ['notes']
def get_success_url(self):
return reverse('purchase-order-notes', kwargs={'pk': self.get_object().id})
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['editing'] = str2bool(self.request.GET.get('edit', ''))
return ctx
class PurchaseOrderCreate(AjaxCreateView):
""" View for creating a new PurchaseOrder object using a modal form """

View File

@ -104,7 +104,6 @@ class EditPartForm(HelperForm):
'default_supplier',
'units',
'minimum_stock',
'notes',
'active',
]

View File

@ -0,0 +1,19 @@
# Generated by Django 2.2.9 on 2020-01-31 10:22
from django.db import migrations
import markdownx.models
class Migration(migrations.Migration):
dependencies = [
('part', '0025_auto_20191118_2316'),
]
operations = [
migrations.AlterField(
model_name='part',
name='notes',
field=markdownx.models.MarkdownxField(help_text='Part notes - supports Markdown formatting'),
),
]

View File

@ -22,6 +22,8 @@ from django.contrib.auth.models import User
from django.db.models.signals import pre_delete
from django.dispatch import receiver
from markdownx.models import MarkdownxField
from mptt.models import TreeForeignKey
from datetime import datetime
@ -422,7 +424,7 @@ class Part(models.Model):
virtual = models.BooleanField(default=False, help_text=_('Is this a virtual part, such as a software product or license?'))
notes = models.TextField(blank=True)
notes = MarkdownxField(help_text=_('Part notes - supports Markdown formatting'))
bom_checksum = models.CharField(max_length=128, blank=True, help_text=_('Stored BOM checksum'))

View File

@ -0,0 +1,10 @@
{% load i18n %}
<div class="markdownx row">
<div class="markdown col-md-6">
{% include 'django/forms/widgets/textarea.html' %}
</div>
<div class="markdown col-md-6">
<div class="markdownx-preview"></div>
</div>
</div>

View File

@ -3,10 +3,8 @@
{% include "part/tabs.html" with tab="allocation" %}
<h3>Part Allocation</h3>
<h4>Part Allocation</h4>
{% if part.allocated_build_count > 0 %}
<h4>Allocated to Part Builds</h4>
<table class='table table-striped table-condensed' id='build-table'>
<tr>
<th>Build</th>
@ -23,7 +21,6 @@
</tr>
{% endfor %}
</table>
{% endif %}
{% endblock %}

View File

@ -1,17 +1,17 @@
{% extends "part/part_base.html" %}
{% load static %}
{% load i18n %}
{% block details %}
{% include 'part/tabs.html' with tab='attachments' %}
<h4>Part Attachments</h4>
<h4>{% trans "Part Attachments" %}</h4>
<hr>
<div id='attachment-buttons'>
<div class="btn-group">
<button type='button' class='btn btn-success' id='new-attachment'>Add Attachment</button>
<button type='button' class='btn btn-success' id='new-attachment'>{% trans "Add Attachment" %}</button>
</div>
</div>
@ -19,8 +19,8 @@
<table class='table table-striped table-condensed' data-toolbar='#attachment-buttons' id='attachment-table'>
<thead>
<tr>
<th data-field='file' data-searchable='true'>File</th>
<th data-field='comment' data-searchable='true'>Comment</th>
<th data-field='file' data-searchable='true'>{% trans "File" %}</th>
<th data-field='comment' data-searchable='true'>{% trans "Comment" %}</th>
<th></th>
</tr>
</thead>
@ -31,10 +31,10 @@
<td>{{ attachment.comment }}</td>
<td>
<div class='btn-group' style='float: right;'>
<button type='button' class='btn btn-default btn-glyph attachment-edit-button' url="{% url 'part-attachment-edit' attachment.id %}" data-toggle='tooltip' title='Edit attachment ({{ attachment.basename }})'>
<button type='button' class='btn btn-default btn-glyph attachment-edit-button' url="{% url 'part-attachment-edit' attachment.id %}" data-toggle='tooltip' title='{% trans "Edit attachment" %}'>
<span class='glyphicon glyphicon-edit'/>
</button>
<button type='button' class='btn btn-default btn-glyph attachment-delete-button' url="{% url 'part-attachment-delete' attachment.id %}" data-toggle='tooltip' title='Delete attachment ({{ attachment.basename }})'>
<button type='button' class='btn btn-default btn-glyph attachment-delete-button' url="{% url 'part-attachment-delete' attachment.id %}" data-toggle='tooltip' title='{% trans "Delete attachment" %}'>
<span class='glyphicon glyphicon-trash'/>
</button>
</div>

View File

@ -147,13 +147,6 @@
</div>
</div>
{% if part.notes %}
<div class="panel panel-default">
<div class="panel-heading"><b>{% trans "Notes" %}</b></div>
<div class="panel-body">{{ part.notes }}</div>
</div>
{% endif %}
{% endblock %}
{% block js_load %}

View File

@ -0,0 +1,56 @@
{% extends "part/part_base.html" %}
{% load static %}
{% load crispy_forms_tags %}
{% load i18n %}
{% load markdownify %}
{% block details %}
{% include 'part/tabs.html' with tab='notes' %}
{% if editing %}
<h4>{% trans "Part Notes" %}</h4>
<hr>
<form method='POST'>
{% csrf_token %}
{{ form }}
<hr>
<input type="submit" value='{% trans "Save" %}'/>
</form>
{{ form.media }}
{% else %}
<div class='row'>
<div class='col-sm-6'>
<h4>{% trans "Part Notes" %}</h4>
</div>
<div class='col-sm-6'>
<button title='{% trans "Edit notes" %}' class='btn btn-default btn-glyph float-right' id='edit-notes'><span class='glyphicon glyphicon-edit'></span></button>
</div>
</div>
<hr>
<div class='panel panel-default'>
<div class='panel-content'>
{{ part.notes | markdownify }}
</div>
</div>
{% endif %}
{% endblock %}
{% block js_ready %}
{{ block.super }}
{% if editing %}
{% else %}
$("#edit-notes").click(function() {
location.href = "{% url 'part-notes' part.id %}?edit=1";
});
{% endif %}
{% endblock %}

View File

@ -54,4 +54,7 @@
<li{% ifequal tab 'attachments' %} class="active"{% endifequal %}>
<a href="{% url 'part-attachments' part.id %}">{% trans "Attachments" %} {% if part.attachment_count > 0 %}<span class="badge">{{ part.attachment_count }}</span>{% endif %}</a>
</li>
<li{% ifequal tab 'notes' %} class="active"{% endifequal %}>
<a href="{% url 'part-notes' part.id %}">{% trans "Notes" %}{% if part.notes %} <span class='glyphicon glyphicon-small glyphicon-info-sign'></span>{% endif %}</a>
</li>
</ul>

View File

@ -48,11 +48,17 @@ def inventree_version(*args, **kwargs):
@register.simple_tag()
def inventree_commit(*args, **kwargs):
def inventree_commit_hash(*args, **kwargs):
""" Return InvenTree git commit hash string """
return version.inventreeCommitHash()
@register.simple_tag()
def inventree_commit_date(*args, **kwargs):
""" Return InvenTree git commit date string """
return version.inventreeCommitDate()
@register.simple_tag()
def inventree_github_url(*args, **kwargs):
""" Return URL for InvenTree github site """

View File

@ -22,9 +22,13 @@ class TemplateTagTest(TestCase):
self.assertEqual(type(inventree_extras.inventree_version()), str)
def test_hash(self):
hash = inventree_extras.inventree_commit()
hash = inventree_extras.inventree_commit_hash()
self.assertEqual(len(hash), 7)
def test_date(self):
d = inventree_extras.inventree_commit_date()
self.assertEqual(len(d.split('-')), 3)
def test_github(self):
self.assertIn('github.com', inventree_extras.inventree_github_url())

View File

@ -52,6 +52,7 @@ part_detail_urls = [
url(r'^orders/?', views.PartDetail.as_view(template_name='part/orders.html'), name='part-orders'),
url(r'^track/?', views.PartDetail.as_view(template_name='part/track.html'), name='part-track'),
url(r'^attachments/?', views.PartDetail.as_view(template_name='part/attachments.html'), name='part-attachments'),
url(r'^notes/?', views.PartNotes.as_view(), name='part-notes'),
url(r'^qr_code/?', views.PartQRCode.as_view(), name='part-qr'),

View File

@ -11,7 +11,7 @@ from django.shortcuts import get_object_or_404
from django.shortcuts import HttpResponseRedirect
from django.utils.translation import gettext_lazy as _
from django.urls import reverse, reverse_lazy
from django.views.generic import DetailView, ListView, FormView
from django.views.generic import DetailView, ListView, FormView, UpdateView
from django.forms.models import model_to_dict
from django.forms import HiddenInput, CheckboxInput
@ -519,6 +519,39 @@ class PartCreate(AjaxCreateView):
return initials
class PartNotes(UpdateView):
""" View for editing the 'notes' field of a Part object.
Presents a live markdown editor.
"""
context_object_name = 'part'
# form_class = part_forms.EditNotesForm
template_name = 'part/notes.html'
model = Part
fields = ['notes']
def get_success_url(self):
""" Return the success URL for this form """
return reverse('part-notes', kwargs={'pk': self.get_object().id})
def get_context_data(self, **kwargs):
part = self.get_object()
ctx = super().get_context_data(**kwargs)
ctx['editing'] = str2bool(self.request.GET.get('edit', ''))
ctx['starred'] = part.isStarredBy(self.request.user)
ctx['disabled'] = not part.active
ctx['OrderStatus'] = OrderStatus
return ctx
class PartDetail(DetailView):
""" Detail view for Part object
"""

View File

@ -162,7 +162,6 @@ class EditStockItemForm(HelperForm):
'serial',
'batch',
'status',
'notes',
'URL',
'delete_on_deplete',
]

View File

@ -0,0 +1,19 @@
# Generated by Django 2.2.9 on 2020-02-02 01:03
from django.db import migrations
import markdownx.models
class Migration(migrations.Migration):
dependencies = [
('stock', '0017_auto_20191118_2311'),
]
operations = [
migrations.AlterField(
model_name='stockitem',
name='notes',
field=markdownx.models.MarkdownxField(blank=True, help_text='Stock Item Notes'),
),
]

View File

@ -16,6 +16,8 @@ from django.contrib.auth.models import User
from django.db.models.signals import pre_delete
from django.dispatch import receiver
from markdownx.models import MarkdownxField
from mptt.models import TreeForeignKey
from decimal import Decimal, InvalidOperation
@ -358,7 +360,7 @@ class StockItem(models.Model):
choices=StockStatus.items(),
validators=[MinValueValidator(0)])
notes = models.CharField(max_length=250, blank=True, help_text=_('Stock Item Notes'))
notes = MarkdownxField(blank=True, help_text=_('Stock Item Notes'))
# If stock item is incoming, an (optional) ETA field
# expected_arrival = models.DateField(null=True, blank=True)

View File

@ -1,160 +1,12 @@
{% extends "stock/stock_app_base.html" %}
{% extends "stock/item_base.html" %}
{% load static %}
{% load inventree_extras %}
{% load i18n %}
{% block content %}
<div class='row'>
<div class='col-sm-6'>
<h3>{% trans "Stock Item Details" %}</h3>
{% if item.serialized %}
<p><i>{{ item.part.full_name}} # {{ item.serial }}</i></p>
{% else %}
<p><i>{{ item.quantity }} &times {{ item.part.full_name }}</i></p>
{% endif %}
<p>
<div class='btn-group'>
{% include "qr_button.html" %}
{% if item.in_stock %}
{% if not item.serialized %}
<button type='button' class='btn btn-default btn-glyph' id='stock-add' title='Add to stock'>
<span class='glyphicon glyphicon-plus-sign' style='color: #1a1;'/>
</button>
<button type='button' class='btn btn-default btn-glyph' id='stock-remove' title='Take from stock'>
<span class='glyphicon glyphicon-minus-sign' style='color: #a11;'/>
</button>
<button type='button' class='btn btn-default btn-glyph' id='stock-count' title='Count stock'>
<span class='glyphicon glyphicon-ok-circle'/>
</button>
{% if item.part.trackable %}
<button type='button' class='btn btn-default btn-glyph' id='stock-serialize' title='Serialize stock'>
<span class='glyphicon glyphicon-th-list'/>
</button>
{% endif %}
{% endif %}
<button type='button' class='btn btn-default btn-glyph' id='stock-move' title='Transfer stock'>
<span class='glyphicon glyphicon-transfer' style='color: #11a;'/>
</button>
<button type='button' class='btn btn-default btn-glyph' id='stock-duplicate' title='Duplicate stock item'>
<span class='glyphicon glyphicon-duplicate'/>
</button>
{% endif %}
<button type='button' class='btn btn-default btn-glyph' id='stock-edit' title='Edit stock item'>
<span class='glyphicon glyphicon-edit'/>
</button>
<button type='button' class='btn btn-default btn-glyph' id='stock-delete' title='Edit stock item'>
<span class='glyphicon glyphicon-trash'/>
</button>
</div>
</p>
{% if item.serialized %}
<div class='alert alert-block alert-info'>
{% trans "This stock item is serialized - it has a unique serial number and the quantity cannot be adjusted." %}
</div>
{% elif item.delete_on_deplete %}
<div class='alert alert-block alert-warning'>
{% trans "This stock item will be automatically deleted when all stock is depleted." %}
</div>
{% endif %}
</div>
{% block details %}
<div class='row'>
<div class='col-sm-6'>
<table class="table table-striped">
<tr>
<td>Part</td>
<td>
{% include "hover_image.html" with image=item.part.image hover=True %}
<a href="{% url 'part-stock' item.part.id %}">{{ item.part.full_name }}
</td>
</tr>
{% if item.belongs_to %}
<tr>
<td>{% trans "Belongs To" %}</td>
<td><a href="{% url 'stock-item-detail' item.belongs_to.id %}">{{ item.belongs_to }}</a></td>
</tr>
{% elif item.location %}
<tr>
<td>{% trans "Location" %}</td>
<td><a href="{% url 'stock-location-detail' item.location.id %}">{{ item.location.name }}</a></td>
</tr>
{% endif %}
{% if item.serialized %}
<tr>
<td>{% trans "Serial Number" %}</td>
<td>{{ item.serial }}</td>
</tr>
{% else %}
<tr>
<td>{% trans "Quantity" %}</td>
<td>{% decimal item.quantity %} {% if item.part.units %}{{ item.part.units }}{% endif %}</td>
</tr>
{% endif %}
{% if item.batch %}
<tr>
<td>{% trans "Batch" %}</td>
<td>{{ item.batch }}</td>
</tr>
{% endif %}
{% if item.build %}
<tr>
<td>{% trans "Build" %}</td>
<td><a href="{% url 'build-detail' item.build.id %}">{{ item.build }}</a></td>
</tr>
{% endif %}
{% if item.purchase_order %}
<tr>
<td>{% trans "Purchase Order" %}</td>
<td><a href="{% url 'purchase-order-detail' item.purchase_order.id %}">{{ item.purchase_order }}</a></td>
</tr>
{% endif %}
{% if item.customer %}
<tr>
<td>{% trans "Customer" %}</td>
<td>{{ item.customer.name }}</td>
</tr>
{% endif %}
{% if item.URL %}
<tr>
<td>{% trans "URL" %}</td>
<td><a href="{{ item.URL }}">{{ item.URL }}</a></td>
</tr>
{% endif %}
{% if item.supplier_part %}
<tr>
<td>{% trans "Supplier" %}</td>
<td><a href="{% url 'company-detail' item.supplier_part.supplier.id %}">{{ item.supplier_part.supplier.name }}</a></td>
</tr>
<tr>
<td>{% trans "Supplier Part" %}</td>
<td><a href="{% url 'supplier-part-detail' item.supplier_part.id %}">{{ item.supplier_part.SKU }}</a></td>
</tr>
{% endif %}
<tr>
<td>{% trans "Last Updated" %}</td>
<td>{{ item.updated }}</td>
</tr>
<tr>
<td>{% trans "Last Stocktake" %}</td>
{% if item.stocktake_date %}
<td>{{ item.stocktake_date }} <span class='badge'>{{ item.stocktake_user }}</span></td>
{% else %}
<td>{% trans "No stocktake performed" %}</td>
{% endif %}
</tr>
<tr>
<td>{% trans "Status" %}</td>
<td>{{ item.get_status_display }}</td>
</tr>
{% if item.notes %}
<tr>
<td>{% trans "Notes" %}</td>
<td>{{ item.notes }}</td>
</tr>
{% endif %}
</table>
</div>
</div>
{% include "stock/tabs.html" with tab="tracking" %}
<hr>
<h4>{% trans "Stock Tracking Information" %}</h4>
@ -167,6 +19,7 @@
</table>
{% endblock %}
{% block js_ready %}
{{ block.super }}
@ -179,84 +32,6 @@
);
});
$("#stock-serialize").click(function() {
launchModalForm(
"{% url 'stock-item-serialize' item.id %}",
{
reload: true,
}
);
});
$("#stock-duplicate").click(function() {
launchModalForm(
"{% url 'stock-item-create' %}",
{
follow: true,
data: {
copy: {{ item.id }},
},
}
);
});
$("#stock-edit").click(function () {
launchModalForm(
"{% url 'stock-item-edit' item.id %}",
{
reload: true,
submit_text: "Save",
}
);
});
$("#show-qr-code").click(function() {
launchModalForm("{% url 'stock-item-qr' item.id %}",
{
no_post: true,
});
});
{% if item.in_stock %}
function itemAdjust(action) {
launchModalForm("/stock/adjust/",
{
data: {
action: action,
item: {{ item.id }},
},
reload: true,
follow: true,
}
);
}
$("#stock-move").click(function() {
itemAdjust("move");
});
$("#stock-count").click(function() {
itemAdjust('count');
});
$('#stock-remove').click(function() {
itemAdjust('take');
});
$('#stock-add').click(function() {
itemAdjust('add');
});
{% endif %}
$("#stock-delete").click(function () {
launchModalForm(
"{% url 'stock-item-delete' item.id %}",
{
redirect: "{% url 'part-stock' item.part.id %}"
});
});
loadStockTrackingTable($("#track-table"), {
params: function(p) {

View File

@ -0,0 +1,247 @@
{% extends "stock/stock_app_base.html" %}
{% load static %}
{% load inventree_extras %}
{% load i18n %}
{% block content %}
<div class='row'>
<div class='col-sm-6'>
<h3>{% trans "Stock Item Details" %}</h3>
{% if item.serialized %}
<p><i>{{ item.part.full_name}} # {{ item.serial }}</i></p>
{% else %}
<p><i>{{ item.quantity }} &times {{ item.part.full_name }}</i></p>
{% endif %}
<p>
<div class='btn-group'>
{% include "qr_button.html" %}
{% if item.in_stock %}
{% if not item.serialized %}
<button type='button' class='btn btn-default btn-glyph' id='stock-add' title='Add to stock'>
<span class='glyphicon glyphicon-plus-sign' style='color: #1a1;'/>
</button>
<button type='button' class='btn btn-default btn-glyph' id='stock-remove' title='Take from stock'>
<span class='glyphicon glyphicon-minus-sign' style='color: #a11;'/>
</button>
<button type='button' class='btn btn-default btn-glyph' id='stock-count' title='Count stock'>
<span class='glyphicon glyphicon-ok-circle'/>
</button>
{% if item.part.trackable %}
<button type='button' class='btn btn-default btn-glyph' id='stock-serialize' title='Serialize stock'>
<span class='glyphicon glyphicon-th-list'/>
</button>
{% endif %}
{% endif %}
<button type='button' class='btn btn-default btn-glyph' id='stock-move' title='Transfer stock'>
<span class='glyphicon glyphicon-transfer' style='color: #11a;'/>
</button>
<button type='button' class='btn btn-default btn-glyph' id='stock-duplicate' title='Duplicate stock item'>
<span class='glyphicon glyphicon-duplicate'/>
</button>
{% endif %}
<button type='button' class='btn btn-default btn-glyph' id='stock-edit' title='Edit stock item'>
<span class='glyphicon glyphicon-edit'/>
</button>
<button type='button' class='btn btn-default btn-glyph' id='stock-delete' title='Edit stock item'>
<span class='glyphicon glyphicon-trash'/>
</button>
</div>
</p>
{% if item.serialized %}
<div class='alert alert-block alert-info'>
{% trans "This stock item is serialized - it has a unique serial number and the quantity cannot be adjusted." %}
</div>
{% elif item.delete_on_deplete %}
<div class='alert alert-block alert-warning'>
{% trans "This stock item will be automatically deleted when all stock is depleted." %}
</div>
{% endif %}
</div>
<div class='row'>
<div class='col-sm-6'>
<table class="table table-striped">
<tr>
<td>Part</td>
<td>
{% include "hover_image.html" with image=item.part.image hover=True %}
<a href="{% url 'part-stock' item.part.id %}">{{ item.part.full_name }}
</td>
</tr>
{% if item.belongs_to %}
<tr>
<td>{% trans "Belongs To" %}</td>
<td><a href="{% url 'stock-item-detail' item.belongs_to.id %}">{{ item.belongs_to }}</a></td>
</tr>
{% elif item.location %}
<tr>
<td>{% trans "Location" %}</td>
<td><a href="{% url 'stock-location-detail' item.location.id %}">{{ item.location.name }}</a></td>
</tr>
{% endif %}
{% if item.serialized %}
<tr>
<td>{% trans "Serial Number" %}</td>
<td>{{ item.serial }}</td>
</tr>
{% else %}
<tr>
<td>{% trans "Quantity" %}</td>
<td>{% decimal item.quantity %} {% if item.part.units %}{{ item.part.units }}{% endif %}</td>
</tr>
{% endif %}
{% if item.batch %}
<tr>
<td>{% trans "Batch" %}</td>
<td>{{ item.batch }}</td>
</tr>
{% endif %}
{% if item.build %}
<tr>
<td>{% trans "Build" %}</td>
<td><a href="{% url 'build-detail' item.build.id %}">{{ item.build }}</a></td>
</tr>
{% endif %}
{% if item.purchase_order %}
<tr>
<td>{% trans "Purchase Order" %}</td>
<td><a href="{% url 'purchase-order-detail' item.purchase_order.id %}">{{ item.purchase_order }}</a></td>
</tr>
{% endif %}
{% if item.customer %}
<tr>
<td>{% trans "Customer" %}</td>
<td>{{ item.customer.name }}</td>
</tr>
{% endif %}
{% if item.URL %}
<tr>
<td>{% trans "URL" %}</td>
<td><a href="{{ item.URL }}">{{ item.URL }}</a></td>
</tr>
{% endif %}
{% if item.supplier_part %}
<tr>
<td>{% trans "Supplier" %}</td>
<td><a href="{% url 'company-detail' item.supplier_part.supplier.id %}">{{ item.supplier_part.supplier.name }}</a></td>
</tr>
<tr>
<td>{% trans "Supplier Part" %}</td>
<td><a href="{% url 'supplier-part-detail' item.supplier_part.id %}">{{ item.supplier_part.SKU }}</a></td>
</tr>
{% endif %}
<tr>
<td>{% trans "Last Updated" %}</td>
<td>{{ item.updated }}</td>
</tr>
<tr>
<td>{% trans "Last Stocktake" %}</td>
{% if item.stocktake_date %}
<td>{{ item.stocktake_date }} <span class='badge'>{{ item.stocktake_user }}</span></td>
{% else %}
<td>{% trans "No stocktake performed" %}</td>
{% endif %}
</tr>
<tr>
<td>{% trans "Status" %}</td>
<td>{{ item.get_status_display }}</td>
</tr>
</table>
</div>
</div>
<hr>
<div class='container-fluid'>
{% block details %}
<!-- Stock item details go here -->
{% endblock %}
</div>
{% endblock %}
{% block js_ready %}
{{ block.super }}
$("#stock-serialize").click(function() {
launchModalForm(
"{% url 'stock-item-serialize' item.id %}",
{
reload: true,
}
);
});
$("#stock-duplicate").click(function() {
launchModalForm(
"{% url 'stock-item-create' %}",
{
follow: true,
data: {
copy: {{ item.id }},
},
}
);
});
$("#stock-edit").click(function () {
launchModalForm(
"{% url 'stock-item-edit' item.id %}",
{
reload: true,
submit_text: "Save",
}
);
});
$("#show-qr-code").click(function() {
launchModalForm("{% url 'stock-item-qr' item.id %}",
{
no_post: true,
});
});
{% if item.in_stock %}
function itemAdjust(action) {
launchModalForm("/stock/adjust/",
{
data: {
action: action,
item: {{ item.id }},
},
reload: true,
follow: true,
}
);
}
$("#stock-move").click(function() {
itemAdjust("move");
});
$("#stock-count").click(function() {
itemAdjust('count');
});
$('#stock-remove').click(function() {
itemAdjust('take');
});
$('#stock-add').click(function() {
itemAdjust('add');
});
{% endif %}
$("#stock-delete").click(function () {
launchModalForm(
"{% url 'stock-item-delete' item.id %}",
{
redirect: "{% url 'part-stock' item.part.id %}"
});
});
{% endblock %}

View File

@ -0,0 +1,57 @@
{% extends "stock/item_base.html" %}
{% load static %}
{% load inventree_extras %}
{% load i18n %}
{% load markdownify %}
{% block details %}
{% include "stock/tabs.html" with tab="notes" %}
{% if editing %}
<h4>{% trans "Stock Item Notes" %}</h4>
<hr>
<form method='POST'>
{% csrf_token %}
{{ form }}
<hr>
<input type='submit' value='{% trans "Save" %}'/>
</form>
{{ form.media }}
{% else %}
<div class='row'>
<div class='col-sm-6'>
<h4>{% trans "Stock Item Notes" %}</h4>
</div>
<div class='col-sm-6'>
<button title='{% trans "Edit notes" %}' class='btn btn-default btn-glyph float-right' id='edit-notes'><span class='glyphicon glyphicon-edit'></span></button>
</div>
</div>
<hr>
<div class='panel panel-default'>
<div class='panel-content'>
{{ item.notes | markdownify }}
</div>
</div>
{% endif %}
{% endblock %}
{% block js_ready %}
{{ block.super }}
{% if editing %}
{% else %}
$("#edit-notes").click(function() {
location.href = "{% url 'stock-item-notes' item.id %}?edit=1";
});
{% endif %}
{% endblock %}

View File

@ -0,0 +1,16 @@
{% load i18n %}
<ul class='nav nav-tabs'>
<li{% ifequal tab 'tracking' %} class='active'{% endifequal %}>
<a href="{% url 'stock-item-detail' item.id %}">{% trans "Tracking" %}</a>
</li>
{% if 0 %}
<!-- These tabs are to be implemented in the future -->
<li{% ifequal tab 'builds' %} class='active'{% endifequal %}>
<a href='#'>{% trans "Builds" %}</a>
</li>
{% endif %}
<li{% ifequal tab 'notes' %} class='active'{% endifequal %}>
<a href="{% url 'stock-item-notes' item.id %}">{% trans "Notes" %}{% if item.notes %} <span class='glyphicon glyphicon-small glyphicon-info-sign'></span>{% endif %}</a>
</li>
</ul>

View File

@ -24,6 +24,8 @@ stock_item_detail_urls = [
url(r'^add_tracking/', views.StockItemTrackingCreate.as_view(), name='stock-tracking-create'),
url(r'^notes/', views.StockItemNotes.as_view(), name='stock-item-notes'),
url('^.*$', views.StockItemDetail.as_view(), name='stock-item-detail'),
]

View File

@ -7,7 +7,7 @@ from __future__ import unicode_literals
from django.core.exceptions import ValidationError
from django.views.generic.edit import FormMixin
from django.views.generic import DetailView, ListView
from django.views.generic import DetailView, ListView, UpdateView
from django.forms.models import model_to_dict
from django.forms import HiddenInput
from django.urls import reverse
@ -83,6 +83,27 @@ class StockItemDetail(DetailView):
model = StockItem
class StockItemNotes(UpdateView):
""" View for editing the 'notes' field of a StockItem object """
context_object_name = 'item'
template_name = 'stock/item_notes.html'
model = StockItem
fields = ['notes']
def get_success_url(self):
return reverse('stock-item-notes', kwargs={'pk': self.get_object().id})
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['editing'] = str2bool(self.request.GET.get('edit', ''))
return ctx
class StockLocationEdit(AjaxUpdateView):
"""
View for editing details of a StockLocation.

View File

@ -1,5 +1,6 @@
{% load static %}
{% load inventree_extras %}
{% load i18n %}
<div class='modal fade modal-fixed-footer' tabindex='-1' role='dialog' id='modal-about'>
<div class='modal-dialog'>
@ -14,23 +15,26 @@
<div>
<!--
-->
<h4>InvenTree Version Information</h4>
<h4>{% trans "InvenTree Version Information" %}</h4>
<table class='table table-striped table-condensed'>
<tr>
<td>Version</td><td><a href="https://github.com/inventree/InvenTree/releases">{% inventree_version %}</a></td>
<td>{% trans "Version" %}</td><td><a href="https://github.com/inventree/InvenTree/releases">{% inventree_version %}</a></td>
</tr>
<tr>
<td>Commit Hash</td><td><a href="https://github.com/inventree/InvenTree/commit/{% inventree_commit %}">{% inventree_commit %}</a></td>
<td>{% trans "Commit Hash" %}</td><td><a href="https://github.com/inventree/InvenTree/commit/{% inventree_commit_hash %}">{% inventree_commit_hash %}</a></td>
</tr>
<tr>
<td>{% trans "Commit Date" %}</td><td>{% inventree_commit_date %}</td>
</tr>
<tr>
<td colspan="2"></td>
</tr>
<tr>
<td>InvenTree Documenation</td>
<td>{% trans "InvenTree Documentation" %}</td>
<td><a href="{% inventree_docs_url %}">{% inventree_docs_url %}</a></td>
</tr>
<tr>
<td>View Code on GitHub</td><td><a href="{% inventree_github_url %}">{% inventree_github_url %}</a></td>
<td>{% trans "View Code on GitHub" %}</td><td><a href="{% inventree_github_url %}">{% inventree_github_url %}</a></td>
</tr>
<tr>
<td></td>

View File

@ -5,6 +5,8 @@ django-cors-headers==3.2.0 # CORS headers extension for DRF
django_filter==2.2.0 # Extended filtering options
django-mptt==0.10.0 # Modified Preorder Tree Traversal
django-dbbackup==3.2.0 # Database backup / restore functionality
django-markdownx==3.0.1 # Markdown form fields
django-markdownify==0.8.0 # Markdown rendering
coreapi==2.3.0 # API documentation
pygments==2.2.0 # Syntax highlighting
tablib==0.13.0 # Import / export data files