Merge pull request from SchrodingersGat/markdown-notes

Markdown notes
This commit is contained in:
Oliver 2020-02-02 12:45:24 +11:00 committed by GitHub
commit 23c0d68330
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
57 changed files with 2534 additions and 1127 deletions

View File

@ -17,6 +17,8 @@ import logging
import tempfile import tempfile
import yaml import yaml
from datetime import datetime
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@ -101,6 +103,8 @@ INSTALLED_APPS = [
'django_cleanup', # Automatically delete orphaned MEDIA files 'django_cleanup', # Automatically delete orphaned MEDIA files
'qr_code', # Generate QR codes 'qr_code', # Generate QR codes
'mptt', # Modified Preorder Tree Traversal 'mptt', # Modified Preorder Tree Traversal
'markdownx', # Markdown editing
'markdownify', # Markdown template rendering
] ]
LOGGING = { LOGGING = {
@ -161,6 +165,37 @@ REST_FRAMEWORK = {
WSGI_APPLICATION = 'InvenTree.wsgi.application' 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 = {} DATABASES = {}
""" """

View File

@ -5,6 +5,29 @@
--basic-color: #333; --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 { .qr-code {
max-width: 400px; max-width: 400px;
@ -305,6 +328,12 @@
width: 100%; width: 100%;
} }
input[type="submit"] {
color: #333;
background-color: #e6e6e6;
border-color: #adadad;
}
.modal textarea { .modal textarea {
width: 100%; width: 100%;
} }

View File

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

View File

@ -15,6 +15,12 @@ def inventreeVersion():
def inventreeCommitHash(): def inventreeCommitHash():
""" Returns the git commit hash for the running codebase """ """ 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', 'take_from',
'batch', 'batch',
'URL', '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.db.models import Sum
from django.core.validators import MinValueValidator from django.core.validators import MinValueValidator
from markdownx.models import MarkdownxField
from InvenTree.status_codes import BuildStatus from InvenTree.status_codes import BuildStatus
from InvenTree.fields import InvenTreeURLField from InvenTree.fields import InvenTreeURLField
@ -92,7 +94,7 @@ class Build(models.Model):
URL = InvenTreeURLField(blank=True, help_text=_('Link to external URL')) 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 @transaction.atomic
def cancelBuild(self, user): def cancelBuild(self, user):

View File

@ -1,74 +1,67 @@
{% extends "build/build_base.html" %} {% extends "build/build_base.html" %}
{% load static %} {% load static %}
{% load i18n %}
{% block details %} {% block details %}
{% include "build/tabs.html" with tab='details' %} {% include "build/tabs.html" with tab='details' %}
<h4>Build Details</h4> <h4>{% trans "Build Details" %}</h4>
<hr> <hr>
<table class='table table-striped'> <table class='table table-striped'>
<tr> <tr>
<td>Title</td><td>{{ build.title }}</td> <td>{% trans "Title" %}</td><td>{{ build.title }}</td>
</tr> </tr>
<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>
<tr> <tr>
<td>Quantity</td><td>{{ build.quantity }}</td> <td>{% trans "Quantity" %}</td><td>{{ build.quantity }}</td>
</tr> </tr>
<tr> <tr>
<td>Stock Source</td> <td>{% trans "Stock Source" %}</td>
<td> <td>
{% if build.take_from %} {% if build.take_from %}
<a href="{% url 'stock-location-detail' build.take_from.id %}">{{ build.take_from }}</a> <a href="{% url 'stock-location-detail' build.take_from.id %}">{{ build.take_from }}</a>
{% else %} {% else %}
Stock can be taken from any available location. {% trans "Stock can be taken from any available location." %}
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
<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> </tr>
{% if build.batch %} {% if build.batch %}
<tr> <tr>
<td>Batch</td><td>{{ build.batch }}</td> <td>{% trans "Batch" %}</td><td>{{ build.batch }}</td>
</tr> </tr>
{% endif %} {% endif %}
{% if build.URL %} {% if build.URL %}
<tr> <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> </tr>
{% endif %} {% endif %}
<tr> <tr>
<td>Created</td><td>{{ build.creation_date }}</td> <td>{% trans "Created" %}</td><td>{{ build.creation_date }}</td>
</tr> </tr>
{% if build.is_active %} {% if build.is_active %}
<tr> <tr>
<td>Enough Parts?</td> <td>{% trans "Enough Parts?" %}</td>
<td> <td>
{% if build.can_build %} {% if build.can_build %}
Yes {% trans "Yes" %}
{% else %} {% else %}
No {% trans "No" %}
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
{% endif %} {% endif %}
{% if build.completion_date %} {% if build.completion_date %}
<tr> <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> </tr>
{% endif %} {% endif %}
</table> </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 %} {% 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'> <ul class='nav nav-tabs'>
<li{% if tab == 'details' %} class='active'{% endif %}> <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>
<li{% if tab == 'allocate' %} class='active'{% endif %}> <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> </li>
</ul> </ul>

View File

@ -25,6 +25,7 @@ build_detail_urls = [
url(r'^auto-allocate/?', views.BuildAutoAllocate.as_view(), name='build-auto-allocate'), url(r'^auto-allocate/?', views.BuildAutoAllocate.as_view(), name='build-auto-allocate'),
url(r'^unallocate/', views.BuildUnallocate.as_view(), name='build-unallocate'), 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'), 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.utils.translation import ugettext as _
from django.core.exceptions import ValidationError 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.forms import HiddenInput
from django.urls import reverse
from part.models import Part from part.models import Part
from .models import Build, BuildItem 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): class BuildDetail(DetailView):
""" Detail view of a single Build object. """ """ Detail view of a single Build object. """
model = Build model = Build

View File

@ -27,7 +27,6 @@ class EditCompanyForm(HelperForm):
'contact', 'contact',
'is_customer', 'is_customer',
'is_supplier', '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 import math
from decimal import Decimal from decimal import Decimal
from django.utils.translation import gettext_lazy as _
from django.core.validators import MinValueValidator from django.core.validators import MinValueValidator
from django.db import models from django.db import models
from django.db.models import Sum from django.db.models import Sum
@ -18,6 +19,8 @@ from django.apps import apps
from django.urls import reverse from django.urls import reverse
from django.conf import settings from django.conf import settings
from markdownx.models import MarkdownxField
from InvenTree.fields import InvenTreeURLField from InvenTree.fields import InvenTreeURLField
from InvenTree.status_codes import OrderStatus from InvenTree.status_codes import OrderStatus
from common.models import Currency from common.models import Currency
@ -68,32 +71,32 @@ class Company(models.Model):
""" """
name = models.CharField(max_length=100, blank=False, unique=True, 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, address = models.CharField(max_length=200,
blank=True, help_text='Company address') blank=True, help_text=_('Company address'))
phone = models.CharField(max_length=50, 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, 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) 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): def __str__(self):
""" Get string representation of a Company """ """ Get string representation of a Company """
@ -223,32 +226,32 @@ class SupplierPart(models.Model):
'purchaseable': True, 'purchaseable': True,
'is_template': False, 'is_template': False,
}, },
help_text='Select part', help_text=_('Select part'),
) )
supplier = models.ForeignKey(Company, on_delete=models.CASCADE, supplier = models.ForeignKey(Company, on_delete=models.CASCADE,
related_name='parts', related_name='parts',
limit_choices_to={'is_supplier': True}, 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). # TODO - Reimplement lead-time as a charfield with special validation (pattern matching).
# lead_time = models.DurationField(blank=True, null=True) # lead_time = models.DurationField(blank=True, null=True)

View File

@ -18,13 +18,6 @@
</tr> </tr>
</table> </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 %} {% endblock %}
{% block js_ready %} {% block js_ready %}
{{ block.super }} {{ 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'> <ul class='nav nav-tabs'>
<li{% if tab == 'details' %} class='active'{% endif %}> <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> </li>
{% if company.is_supplier %} {% if company.is_supplier %}
<li{% if tab == 'parts' %} class='active'{% endif %}> <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>
<li{% if tab == 'stock' %} class='active'{% endif %}> <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>
<li{% if tab == 'po' %} class='active'{% endif %}> <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> </li>
{% endif %} {% endif %}
{% if company.is_customer %} {% if company.is_customer %}
{% if 0 %} {% if 0 %}
<li{% if tab == 'co' %} class='active'{% endif %}> <li{% if tab == 'co' %} class='active'{% endif %}>
<a href="#">Sales Orders</a> <a href="#">{% trans "Sales Orders" %}</a>
</li> </li>
{% endif %} {% endif %}
{% 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> </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'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'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'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'), 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 -*- # -*- coding: utf-8 -*-
from __future__ import unicode_literals 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 django.forms import HiddenInput
from InvenTree.views import AjaxCreateView, AjaxUpdateView, AjaxDeleteView from InvenTree.views import AjaxCreateView, AjaxUpdateView, AjaxDeleteView
@ -52,6 +53,28 @@ class CompanyIndex(ListView):
return queryset 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): class CompanyDetail(DetailView):
""" Detail view for Company object """ """ Detail view for Company object """
context_obect_name = 'company' 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.urls import reverse
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from markdownx.models import MarkdownxField
from datetime import datetime from datetime import datetime
from stock.models import StockItem from stock.models import StockItem
@ -81,7 +83,7 @@ class Order(models.Model):
complete_date = models.DateField(blank=True, null=True) 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): def place_order(self):
""" Marks the order as PLACED. Order must be currently PENDING. """ """ 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 i18n %}
{% load static %} {% load static %}
{% load inventree_extras %}
{% block page_title %} {% block details %}
InvenTree | {{ order }}
{% endblock %}
{% block content %} {% include 'order/tabs.html' with tab='details' %}
<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> <hr>
<div id='order-toolbar-buttons' class='btn-group' style='float: right;'> <div id='order-toolbar-buttons' class='btn-group' style='float: right;'>
{% if order.status == OrderStatus.PENDING %} {% 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 %} {% endif %}
</div> </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'> <table class='table table-striped table-condensed' id='po-lines-table' data-toolbar='#order-toolbar-buttons'>
<thead> <thead>
@ -162,40 +80,11 @@ InvenTree | {{ order }}
</tbody> </tbody>
</table> </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 %} {% endblock %}
{% block js_ready %} {% block js_ready %}
{% if order.status == OrderStatus.PENDING and order.lines.count > 0 %} {{ block.super }}
$("#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,
});
});
$("#po-lines-table").on('click', ".line-receive", function() { $("#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'^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'), 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.db import transaction
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.urls import reverse
from django.utils.translation import ugettext as _ 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 from django.forms import HiddenInput
import logging import logging
@ -69,6 +70,28 @@ class PurchaseOrderDetail(DetailView):
return ctx 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): class PurchaseOrderCreate(AjaxCreateView):
""" View for creating a new PurchaseOrder object using a modal form """ """ View for creating a new PurchaseOrder object using a modal form """

View File

@ -104,7 +104,6 @@ class EditPartForm(HelperForm):
'default_supplier', 'default_supplier',
'units', 'units',
'minimum_stock', 'minimum_stock',
'notes',
'active', '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.db.models.signals import pre_delete
from django.dispatch import receiver from django.dispatch import receiver
from markdownx.models import MarkdownxField
from mptt.models import TreeForeignKey from mptt.models import TreeForeignKey
from datetime import datetime 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?')) 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')) 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" %} {% 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'> <table class='table table-striped table-condensed' id='build-table'>
<tr> <tr>
<th>Build</th> <th>Build</th>
@ -23,7 +21,6 @@
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>
{% endif %}
{% endblock %} {% endblock %}

View File

@ -1,17 +1,17 @@
{% extends "part/part_base.html" %} {% extends "part/part_base.html" %}
{% load static %} {% load static %}
{% load i18n %}
{% block details %} {% block details %}
{% include 'part/tabs.html' with tab='attachments' %} {% include 'part/tabs.html' with tab='attachments' %}
<h4>Part Attachments</h4> <h4>{% trans "Part Attachments" %}</h4>
<hr> <hr>
<div id='attachment-buttons'> <div id='attachment-buttons'>
<div class="btn-group"> <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>
</div> </div>
@ -19,8 +19,8 @@
<table class='table table-striped table-condensed' data-toolbar='#attachment-buttons' id='attachment-table'> <table class='table table-striped table-condensed' data-toolbar='#attachment-buttons' id='attachment-table'>
<thead> <thead>
<tr> <tr>
<th data-field='file' data-searchable='true'>File</th> <th data-field='file' data-searchable='true'>{% trans "File" %}</th>
<th data-field='comment' data-searchable='true'>Comment</th> <th data-field='comment' data-searchable='true'>{% trans "Comment" %}</th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
@ -31,10 +31,10 @@
<td>{{ attachment.comment }}</td> <td>{{ attachment.comment }}</td>
<td> <td>
<div class='btn-group' style='float: right;'> <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'/> <span class='glyphicon glyphicon-edit'/>
</button> </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'/> <span class='glyphicon glyphicon-trash'/>
</button> </button>
</div> </div>

View File

@ -147,13 +147,6 @@
</div> </div>
</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 %} {% endblock %}
{% block js_load %} {% 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 %}> <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> <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>
<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> </ul>

View File

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

View File

@ -22,9 +22,13 @@ class TemplateTagTest(TestCase):
self.assertEqual(type(inventree_extras.inventree_version()), str) self.assertEqual(type(inventree_extras.inventree_version()), str)
def test_hash(self): def test_hash(self):
hash = inventree_extras.inventree_commit() hash = inventree_extras.inventree_commit_hash()
self.assertEqual(len(hash), 7) 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): def test_github(self):
self.assertIn('github.com', inventree_extras.inventree_github_url()) 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'^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'^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'^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'), 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.shortcuts import HttpResponseRedirect
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.urls import reverse, reverse_lazy 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.models import model_to_dict
from django.forms import HiddenInput, CheckboxInput from django.forms import HiddenInput, CheckboxInput
@ -519,6 +519,39 @@ class PartCreate(AjaxCreateView):
return initials 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): class PartDetail(DetailView):
""" Detail view for Part object """ Detail view for Part object
""" """

View File

@ -162,7 +162,6 @@ class EditStockItemForm(HelperForm):
'serial', 'serial',
'batch', 'batch',
'status', 'status',
'notes',
'URL', 'URL',
'delete_on_deplete', '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.db.models.signals import pre_delete
from django.dispatch import receiver from django.dispatch import receiver
from markdownx.models import MarkdownxField
from mptt.models import TreeForeignKey from mptt.models import TreeForeignKey
from decimal import Decimal, InvalidOperation from decimal import Decimal, InvalidOperation
@ -358,7 +360,7 @@ class StockItem(models.Model):
choices=StockStatus.items(), choices=StockStatus.items(),
validators=[MinValueValidator(0)]) 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 # If stock item is incoming, an (optional) ETA field
# expected_arrival = models.DateField(null=True, blank=True) # 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 static %}
{% load inventree_extras %} {% load inventree_extras %}
{% load i18n %} {% load i18n %}
{% block content %}
<div class='row'> {% block details %}
<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'> {% include "stock/tabs.html" with tab="tracking" %}
<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>
<hr> <hr>
<h4>{% trans "Stock Tracking Information" %}</h4> <h4>{% trans "Stock Tracking Information" %}</h4>
@ -167,6 +19,7 @@
</table> </table>
{% endblock %} {% endblock %}
{% block js_ready %} {% block js_ready %}
{{ block.super }} {{ 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"), { loadStockTrackingTable($("#track-table"), {
params: function(p) { 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'^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'), 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.core.exceptions import ValidationError
from django.views.generic.edit import FormMixin 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.models import model_to_dict
from django.forms import HiddenInput from django.forms import HiddenInput
from django.urls import reverse from django.urls import reverse
@ -83,6 +83,27 @@ class StockItemDetail(DetailView):
model = StockItem 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): class StockLocationEdit(AjaxUpdateView):
""" """
View for editing details of a StockLocation. View for editing details of a StockLocation.

View File

@ -1,5 +1,6 @@
{% load static %} {% load static %}
{% load inventree_extras %} {% load inventree_extras %}
{% load i18n %}
<div class='modal fade modal-fixed-footer' tabindex='-1' role='dialog' id='modal-about'> <div class='modal fade modal-fixed-footer' tabindex='-1' role='dialog' id='modal-about'>
<div class='modal-dialog'> <div class='modal-dialog'>
@ -14,23 +15,26 @@
<div> <div>
<!-- <!--
--> -->
<h4>InvenTree Version Information</h4> <h4>{% trans "InvenTree Version Information" %}</h4>
<table class='table table-striped table-condensed'> <table class='table table-striped table-condensed'>
<tr> <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>
<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>
<tr> <tr>
<td colspan="2"></td> <td colspan="2"></td>
</tr> </tr>
<tr> <tr>
<td>InvenTree Documenation</td> <td>{% trans "InvenTree Documentation" %}</td>
<td><a href="{% inventree_docs_url %}">{% inventree_docs_url %}</a></td> <td><a href="{% inventree_docs_url %}">{% inventree_docs_url %}</a></td>
</tr> </tr>
<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>
<tr> <tr>
<td></td> <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_filter==2.2.0 # Extended filtering options
django-mptt==0.10.0 # Modified Preorder Tree Traversal django-mptt==0.10.0 # Modified Preorder Tree Traversal
django-dbbackup==3.2.0 # Database backup / restore functionality 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 coreapi==2.3.0 # API documentation
pygments==2.2.0 # Syntax highlighting pygments==2.2.0 # Syntax highlighting
tablib==0.13.0 # Import / export data files tablib==0.13.0 # Import / export data files