Merge branch 'inventree:master' into matmair/issue2788

This commit is contained in:
Matthias Mair 2022-05-02 21:50:57 +02:00 committed by GitHub
commit 0c338cb2ae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
143 changed files with 39337 additions and 29974 deletions

View File

@ -19,42 +19,31 @@ jobs:
INVENTREE_STATIC_ROOT: ./static INVENTREE_STATIC_ROOT: ./static
steps: steps:
- uses: actions/checkout@v2 - name: Checkout Code
- name: Get Current Translations uses: actions/checkout@v2
run: | - name: Set up Python 3.7
git fetch uses: actions/setup-python@v1
git checkout origin/l10 -- `git ls-tree origin/l10 -r --name-only | grep ".po"` with:
git reset python-version: 3.7
- name: Set up Python 3.7 - name: Install Dependencies
uses: actions/setup-python@v1 run: |
with: sudo apt-get update
python-version: 3.7 sudo apt-get install -y gettext
- name: Install Dependencies pip3 install invoke
run: | invoke install
sudo apt-get update - name: Make Translations
sudo apt-get install -y gettext run: |
pip3 install invoke invoke translate
invoke install - name: Commit files
- name: Make Translations run: |
run: | git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
invoke translate git config --local user.name "github-actions[bot]"
- name: stash changes git checkout -b l10_local
run: | git add "*.po"
git stash git commit -m "updated translation base"
- name: Checkout Translation Branch - name: Push changes
uses: actions/checkout@v2.3.4 uses: ad-m/github-push-action@master
with: with:
ref: l10 github_token: ${{ secrets.GITHUB_TOKEN }}
- name: Commit files branch: l10
run: | force: true
git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
git checkout stash -- .
git reset
git add "*.po"
git commit -m "updated translation base"
- name: Push changes
uses: ad-m/github-push-action@master
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
branch: l10

View File

@ -9,6 +9,9 @@ on:
jobs: jobs:
run: run:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
pull-requests: write
steps: steps:
- uses: actions/first-interaction@v1 - uses: actions/first-interaction@v1
with: with:

3
.gitignore vendored
View File

@ -85,3 +85,6 @@ maintenance_mode_state.txt
# plugin dev directory # plugin dev directory
plugins/ plugins/
# Compiled translation files
*.mo

View File

@ -44,6 +44,31 @@ The HEAD of the "stable" branch represents the latest stable release code.
- When approved, the branch is merged back *into* stable, with an incremented PATCH number (e.g. 0.4.1 -> 0.4.2) - When approved, the branch is merged back *into* stable, with an incremented PATCH number (e.g. 0.4.1 -> 0.4.2)
- The bugfix *must* also be cherry picked into the *master* branch. - The bugfix *must* also be cherry picked into the *master* branch.
## Environment
#### Target version
We are currently targeting:
| Name | Minimum version |
|---|---|
| Python | 3.9 |
| Django | 3.2 |
### Auto creating updates
The following tools can be used to auto-upgrade syntax that was depreciated in new versions:
```bash
pip install pyupgrade
pip install django-upgrade
```
To update the codebase run the following script.
```bash
pyupgrade `find . -name "*.py"`
django-upgrade --target-version 3.2 `find . -name "*.py"`
```
### Credits
If you add any new dependencies / libraries, they need to be added to [the docs](https://github.com/inventree/inventree-docs/blob/master/docs/credits.md). Please try to do that as timely as possible.
## Migration Files ## Migration Files
Any required migration files **must** be included in the commit, or the pull-request will be rejected. If you change the underlying database schema, make sure you run `invoke migrate` and commit the migration files before submitting the PR. Any required migration files **must** be included in the commit, or the pull-request will be rejected. If you change the underlying database schema, make sure you run `invoke migrate` and commit the migration files before submitting the PR.

View File

@ -5,7 +5,7 @@ Main JSON interface views
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import unicode_literals from __future__ import unicode_literals
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.conf import settings from django.conf import settings
from django.http import JsonResponse from django.http import JsonResponse

View File

@ -6,7 +6,7 @@ import sys
from .validators import allowable_url_schemes from .validators import allowable_url_schemes
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.forms.fields import URLField as FormURLField from django.forms.fields import URLField as FormURLField
from django.db import models as models from django.db import models as models

View File

@ -7,7 +7,7 @@ from __future__ import unicode_literals
from urllib.parse import urlencode from urllib.parse import urlencode
import logging import logging
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django import forms from django import forms
from django.contrib.auth.models import User, Group from django.contrib.auth.models import User, Group
from django.conf import settings from django.conf import settings
@ -319,7 +319,7 @@ class CustomSocialAccountAdapter(RegistratonMixin, DefaultSocialAccountAdapter):
redirect_url = reverse('two-factor-authenticate') redirect_url = reverse('two-factor-authenticate')
# Add GET parameters to the URL if they exist. # Add GET parameters to the URL if they exist.
if request.GET: if request.GET:
redirect_url += u'?' + urlencode(request.GET) redirect_url += '?' + urlencode(request.GET)
raise ImmediateHttpResponse( raise ImmediateHttpResponse(
response=HttpResponseRedirect(redirect_url) response=HttpResponseRedirect(redirect_url)

View File

@ -13,7 +13,7 @@ from decimal import Decimal, InvalidOperation
from wsgiref.util import FileWrapper from wsgiref.util import FileWrapper
from django.http import StreamingHttpResponse from django.http import StreamingHttpResponse
from django.core.exceptions import ValidationError, FieldError from django.core.exceptions import ValidationError, FieldError
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.contrib.auth.models import Permission from django.contrib.auth.models import Permission
@ -432,7 +432,7 @@ def extract_serial_numbers(serials, expected_quantity, next_number: int):
next_number += 1 next_number += 1
# Split input string by whitespace or comma (,) characters # Split input string by whitespace or comma (,) characters
groups = re.split("[\s,]+", serials) groups = re.split(r"[\s,]+", serials)
numbers = [] numbers = []
errors = [] errors = []

View File

@ -1,7 +1,7 @@
from django.shortcuts import HttpResponseRedirect from django.shortcuts import HttpResponseRedirect
from django.urls import reverse_lazy, Resolver404 from django.urls import reverse_lazy, Resolver404
from django.shortcuts import redirect from django.shortcuts import redirect
from django.conf.urls import include, url from django.urls import include, re_path
from django.conf import settings from django.conf import settings
from django.contrib.auth.middleware import PersistentRemoteUserMiddleware from django.contrib.auth.middleware import PersistentRemoteUserMiddleware
@ -85,14 +85,14 @@ class AuthRequiredMiddleware(object):
if path not in urls and not path.startswith('/api/'): if path not in urls and not path.startswith('/api/'):
# Save the 'next' parameter to pass through to the login view # Save the 'next' parameter to pass through to the login view
return redirect('%s?next=%s' % (reverse_lazy('account_login'), request.path)) return redirect('{}?next={}'.format(reverse_lazy('account_login'), request.path))
response = self.get_response(request) response = self.get_response(request)
return response return response
url_matcher = url('', include(frontendpatterns)) url_matcher = re_path('', include(frontendpatterns))
class Check2FAMiddleware(BaseRequire2FAMiddleware): class Check2FAMiddleware(BaseRequire2FAMiddleware):

View File

@ -15,7 +15,7 @@ from collections import OrderedDict
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.exceptions import ValidationError as DjangoValidationError from django.core.exceptions import ValidationError as DjangoValidationError
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.db import models from django.db import models
from djmoney.contrib.django_rest_framework.fields import MoneyField from djmoney.contrib.django_rest_framework.fields import MoneyField

View File

@ -306,6 +306,10 @@ if DEBUG and CONFIG.get('debug_toolbar', False): # pragma: no cover
INSTALLED_APPS.append('debug_toolbar') INSTALLED_APPS.append('debug_toolbar')
MIDDLEWARE.append('debug_toolbar.middleware.DebugToolbarMiddleware') MIDDLEWARE.append('debug_toolbar.middleware.DebugToolbarMiddleware')
# Allow secure http developer server in debug mode
if DEBUG:
INSTALLED_APPS.append('sslserver')
# InvenTree URL configuration # InvenTree URL configuration
# Base URL for admin pages (default="admin") # Base URL for admin pages (default="admin")

View File

@ -0,0 +1,99 @@
/*! qr-scanner v1.4.1 https://github.com/nimiq/qr-scanner Licensed MIT */
export const createWorker=()=>new Worker(URL.createObjectURL(new Blob([`class x{constructor(a,b){this.width=b;this.height=a.length/b;this.data=a}static createEmpty(a,b){return new x(new Uint8ClampedArray(a*b),a)}get(a,b){return 0>a||a>=this.width||0>b||b>=this.height?!1:!!this.data[b*this.width+a]}set(a,b,c){this.data[b*this.width+a]=c?1:0}setRegion(a,b,c,d,e){for(let f=b;f<b+d;f++)for(let g=a;g<a+c;g++)this.set(g,f,!!e)}}
class A{constructor(a,b,c){this.width=a;a*=b;if(c&&c.length!==a)throw Error("Wrong buffer size");this.data=c||new Uint8ClampedArray(a)}get(a,b){return this.data[b*this.width+a]}set(a,b,c){this.data[b*this.width+a]=c}}
class ba{constructor(a){this.bitOffset=this.byteOffset=0;this.bytes=a}readBits(a){if(1>a||32<a||a>this.available())throw Error("Cannot read "+a.toString()+" bits");var b=0;if(0<this.bitOffset){b=8-this.bitOffset;var c=a<b?a:b;b-=c;b=(this.bytes[this.byteOffset]&255>>8-c<<b)>>b;a-=c;this.bitOffset+=c;8===this.bitOffset&&(this.bitOffset=0,this.byteOffset++)}if(0<a){for(;8<=a;)b=b<<8|this.bytes[this.byteOffset]&255,this.byteOffset++,a-=8;0<a&&(c=8-a,b=b<<a|(this.bytes[this.byteOffset]&255>>c<<c)>>c,
this.bitOffset+=a)}return b}available(){return 8*(this.bytes.length-this.byteOffset)-this.bitOffset}}var B,C=B||(B={});C.Numeric="numeric";C.Alphanumeric="alphanumeric";C.Byte="byte";C.Kanji="kanji";C.ECI="eci";C.StructuredAppend="structuredappend";var D,E=D||(D={});E[E.Terminator=0]="Terminator";E[E.Numeric=1]="Numeric";E[E.Alphanumeric=2]="Alphanumeric";E[E.Byte=4]="Byte";E[E.Kanji=8]="Kanji";E[E.ECI=7]="ECI";E[E.StructuredAppend=3]="StructuredAppend";let F="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:".split("");
function ca(a,b){let c=[],d="";b=a.readBits([8,16,16][b]);for(let e=0;e<b;e++){let f=a.readBits(8);c.push(f)}try{d+=decodeURIComponent(c.map(e=>\`%\${("0"+e.toString(16)).substr(-2)}\`).join(""))}catch(e){}return{bytes:c,text:d}}
function da(a,b){a=new ba(a);let c=9>=b?0:26>=b?1:2;for(b={text:"",bytes:[],chunks:[],version:b};4<=a.available();){var d=a.readBits(4);if(d===D.Terminator)return b;if(d===D.ECI)0===a.readBits(1)?b.chunks.push({type:B.ECI,assignmentNumber:a.readBits(7)}):0===a.readBits(1)?b.chunks.push({type:B.ECI,assignmentNumber:a.readBits(14)}):0===a.readBits(1)?b.chunks.push({type:B.ECI,assignmentNumber:a.readBits(21)}):b.chunks.push({type:B.ECI,assignmentNumber:-1});else if(d===D.Numeric){var e=a,f=[];d="";for(var g=
e.readBits([10,12,14][c]);3<=g;){var h=e.readBits(10);if(1E3<=h)throw Error("Invalid numeric value above 999");var k=Math.floor(h/100),m=Math.floor(h/10)%10;h%=10;f.push(48+k,48+m,48+h);d+=k.toString()+m.toString()+h.toString();g-=3}if(2===g){g=e.readBits(7);if(100<=g)throw Error("Invalid numeric value above 99");e=Math.floor(g/10);g%=10;f.push(48+e,48+g);d+=e.toString()+g.toString()}else if(1===g){e=e.readBits(4);if(10<=e)throw Error("Invalid numeric value above 9");f.push(48+e);d+=e.toString()}b.text+=
d;b.bytes.push(...f);b.chunks.push({type:B.Numeric,text:d})}else if(d===D.Alphanumeric){e=a;f=[];d="";for(g=e.readBits([9,11,13][c]);2<=g;)m=e.readBits(11),k=Math.floor(m/45),m%=45,f.push(F[k].charCodeAt(0),F[m].charCodeAt(0)),d+=F[k]+F[m],g-=2;1===g&&(e=e.readBits(6),f.push(F[e].charCodeAt(0)),d+=F[e]);b.text+=d;b.bytes.push(...f);b.chunks.push({type:B.Alphanumeric,text:d})}else if(d===D.Byte)d=ca(a,c),b.text+=d.text,b.bytes.push(...d.bytes),b.chunks.push({type:B.Byte,bytes:d.bytes,text:d.text});
else if(d===D.Kanji){f=a;d=[];e=f.readBits([8,10,12][c]);for(g=0;g<e;g++)k=f.readBits(13),k=Math.floor(k/192)<<8|k%192,k=7936>k?k+33088:k+49472,d.push(k>>8,k&255);f=(new TextDecoder("shift-jis")).decode(Uint8Array.from(d));b.text+=f;b.bytes.push(...d);b.chunks.push({type:B.Kanji,bytes:d,text:f})}else d===D.StructuredAppend&&b.chunks.push({type:B.StructuredAppend,currentSequence:a.readBits(4),totalSequence:a.readBits(4),parity:a.readBits(8)})}if(0===a.available()||0===a.readBits(a.available()))return b}
class G{constructor(a,b){if(0===b.length)throw Error("No coefficients.");this.field=a;let c=b.length;if(1<c&&0===b[0]){let d=1;for(;d<c&&0===b[d];)d++;if(d===c)this.coefficients=a.zero.coefficients;else for(this.coefficients=new Uint8ClampedArray(c-d),a=0;a<this.coefficients.length;a++)this.coefficients[a]=b[d+a]}else this.coefficients=b}degree(){return this.coefficients.length-1}isZero(){return 0===this.coefficients[0]}getCoefficient(a){return this.coefficients[this.coefficients.length-1-a]}addOrSubtract(a){if(this.isZero())return a;
if(a.isZero())return this;let b=this.coefficients;a=a.coefficients;b.length>a.length&&([b,a]=[a,b]);let c=new Uint8ClampedArray(a.length),d=a.length-b.length;for(var e=0;e<d;e++)c[e]=a[e];for(e=d;e<a.length;e++)c[e]=b[e-d]^a[e];return new G(this.field,c)}multiply(a){if(0===a)return this.field.zero;if(1===a)return this;let b=this.coefficients.length,c=new Uint8ClampedArray(b);for(let d=0;d<b;d++)c[d]=this.field.multiply(this.coefficients[d],a);return new G(this.field,c)}multiplyPoly(a){if(this.isZero()||
a.isZero())return this.field.zero;let b=this.coefficients,c=b.length;a=a.coefficients;let d=a.length,e=new Uint8ClampedArray(c+d-1);for(let f=0;f<c;f++){let g=b[f];for(let h=0;h<d;h++)e[f+h]=H(e[f+h],this.field.multiply(g,a[h]))}return new G(this.field,e)}multiplyByMonomial(a,b){if(0>a)throw Error("Invalid degree less than 0");if(0===b)return this.field.zero;let c=this.coefficients.length;a=new Uint8ClampedArray(c+a);for(let d=0;d<c;d++)a[d]=this.field.multiply(this.coefficients[d],b);return new G(this.field,
a)}evaluateAt(a){let b=0;if(0===a)return this.getCoefficient(0);let c=this.coefficients.length;if(1===a)return this.coefficients.forEach(d=>{b^=d}),b;b=this.coefficients[0];for(let d=1;d<c;d++)b=H(this.field.multiply(a,b),this.coefficients[d]);return b}}function H(a,b){return a^b}
class ea{constructor(a,b,c){this.primitive=a;this.size=b;this.generatorBase=c;this.expTable=Array(this.size);this.logTable=Array(this.size);a=1;for(b=0;b<this.size;b++)this.expTable[b]=a,a*=2,a>=this.size&&(a=(a^this.primitive)&this.size-1);for(a=0;a<this.size-1;a++)this.logTable[this.expTable[a]]=a;this.zero=new G(this,Uint8ClampedArray.from([0]));this.one=new G(this,Uint8ClampedArray.from([1]))}multiply(a,b){return 0===a||0===b?0:this.expTable[(this.logTable[a]+this.logTable[b])%(this.size-1)]}inverse(a){if(0===
a)throw Error("Can't invert 0");return this.expTable[this.size-this.logTable[a]-1]}buildMonomial(a,b){if(0>a)throw Error("Invalid monomial degree less than 0");if(0===b)return this.zero;a=new Uint8ClampedArray(a+1);a[0]=b;return new G(this,a)}log(a){if(0===a)throw Error("Can't take log(0)");return this.logTable[a]}exp(a){return this.expTable[a]}}
function fa(a,b,c,d){b.degree()<c.degree()&&([b,c]=[c,b]);let e=a.zero;for(var f=a.one;c.degree()>=d/2;){var g=b;let h=e;b=c;e=f;if(b.isZero())return null;c=g;f=a.zero;g=b.getCoefficient(b.degree());for(g=a.inverse(g);c.degree()>=b.degree()&&!c.isZero();){let k=c.degree()-b.degree(),m=a.multiply(c.getCoefficient(c.degree()),g);f=f.addOrSubtract(a.buildMonomial(k,m));c=c.addOrSubtract(b.multiplyByMonomial(k,m))}f=f.multiplyPoly(e).addOrSubtract(h);if(c.degree()>=b.degree())return null}d=f.getCoefficient(0);
if(0===d)return null;a=a.inverse(d);return[f.multiply(a),c.multiply(a)]}
function ha(a,b){let c=new Uint8ClampedArray(a.length);c.set(a);a=new ea(285,256,0);var d=new G(a,c),e=new Uint8ClampedArray(b),f=!1;for(var g=0;g<b;g++){var h=d.evaluateAt(a.exp(g+a.generatorBase));e[e.length-1-g]=h;0!==h&&(f=!0)}if(!f)return c;d=new G(a,e);d=fa(a,a.buildMonomial(b,1),d,b);if(null===d)return null;b=d[0];g=b.degree();if(1===g)b=[b.getCoefficient(1)];else{e=Array(g);f=0;for(h=1;h<a.size&&f<g;h++)0===b.evaluateAt(h)&&(e[f]=a.inverse(h),f++);b=f!==g?null:e}if(null==b)return null;e=d[1];
f=b.length;d=Array(f);for(g=0;g<f;g++){h=a.inverse(b[g]);let k=1;for(let m=0;m<f;m++)g!==m&&(k=a.multiply(k,H(1,a.multiply(b[m],h))));d[g]=a.multiply(e.evaluateAt(h),a.inverse(k));0!==a.generatorBase&&(d[g]=a.multiply(d[g],h))}for(e=0;e<b.length;e++){f=c.length-1-a.log(b[e]);if(0>f)return null;c[f]^=d[e]}return c}
let I=[{infoBits:null,versionNumber:1,alignmentPatternCenters:[],errorCorrectionLevels:[{ecCodewordsPerBlock:7,ecBlocks:[{numBlocks:1,dataCodewordsPerBlock:19}]},{ecCodewordsPerBlock:10,ecBlocks:[{numBlocks:1,dataCodewordsPerBlock:16}]},{ecCodewordsPerBlock:13,ecBlocks:[{numBlocks:1,dataCodewordsPerBlock:13}]},{ecCodewordsPerBlock:17,ecBlocks:[{numBlocks:1,dataCodewordsPerBlock:9}]}]},{infoBits:null,versionNumber:2,alignmentPatternCenters:[6,18],errorCorrectionLevels:[{ecCodewordsPerBlock:10,ecBlocks:[{numBlocks:1,
dataCodewordsPerBlock:34}]},{ecCodewordsPerBlock:16,ecBlocks:[{numBlocks:1,dataCodewordsPerBlock:28}]},{ecCodewordsPerBlock:22,ecBlocks:[{numBlocks:1,dataCodewordsPerBlock:22}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:1,dataCodewordsPerBlock:16}]}]},{infoBits:null,versionNumber:3,alignmentPatternCenters:[6,22],errorCorrectionLevels:[{ecCodewordsPerBlock:15,ecBlocks:[{numBlocks:1,dataCodewordsPerBlock:55}]},{ecCodewordsPerBlock:26,ecBlocks:[{numBlocks:1,dataCodewordsPerBlock:44}]},{ecCodewordsPerBlock:18,
ecBlocks:[{numBlocks:2,dataCodewordsPerBlock:17}]},{ecCodewordsPerBlock:22,ecBlocks:[{numBlocks:2,dataCodewordsPerBlock:13}]}]},{infoBits:null,versionNumber:4,alignmentPatternCenters:[6,26],errorCorrectionLevels:[{ecCodewordsPerBlock:20,ecBlocks:[{numBlocks:1,dataCodewordsPerBlock:80}]},{ecCodewordsPerBlock:18,ecBlocks:[{numBlocks:2,dataCodewordsPerBlock:32}]},{ecCodewordsPerBlock:26,ecBlocks:[{numBlocks:2,dataCodewordsPerBlock:24}]},{ecCodewordsPerBlock:16,ecBlocks:[{numBlocks:4,dataCodewordsPerBlock:9}]}]},
{infoBits:null,versionNumber:5,alignmentPatternCenters:[6,30],errorCorrectionLevels:[{ecCodewordsPerBlock:26,ecBlocks:[{numBlocks:1,dataCodewordsPerBlock:108}]},{ecCodewordsPerBlock:24,ecBlocks:[{numBlocks:2,dataCodewordsPerBlock:43}]},{ecCodewordsPerBlock:18,ecBlocks:[{numBlocks:2,dataCodewordsPerBlock:15},{numBlocks:2,dataCodewordsPerBlock:16}]},{ecCodewordsPerBlock:22,ecBlocks:[{numBlocks:2,dataCodewordsPerBlock:11},{numBlocks:2,dataCodewordsPerBlock:12}]}]},{infoBits:null,versionNumber:6,alignmentPatternCenters:[6,
34],errorCorrectionLevels:[{ecCodewordsPerBlock:18,ecBlocks:[{numBlocks:2,dataCodewordsPerBlock:68}]},{ecCodewordsPerBlock:16,ecBlocks:[{numBlocks:4,dataCodewordsPerBlock:27}]},{ecCodewordsPerBlock:24,ecBlocks:[{numBlocks:4,dataCodewordsPerBlock:19}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:4,dataCodewordsPerBlock:15}]}]},{infoBits:31892,versionNumber:7,alignmentPatternCenters:[6,22,38],errorCorrectionLevels:[{ecCodewordsPerBlock:20,ecBlocks:[{numBlocks:2,dataCodewordsPerBlock:78}]},{ecCodewordsPerBlock:18,
ecBlocks:[{numBlocks:4,dataCodewordsPerBlock:31}]},{ecCodewordsPerBlock:18,ecBlocks:[{numBlocks:2,dataCodewordsPerBlock:14},{numBlocks:4,dataCodewordsPerBlock:15}]},{ecCodewordsPerBlock:26,ecBlocks:[{numBlocks:4,dataCodewordsPerBlock:13},{numBlocks:1,dataCodewordsPerBlock:14}]}]},{infoBits:34236,versionNumber:8,alignmentPatternCenters:[6,24,42],errorCorrectionLevels:[{ecCodewordsPerBlock:24,ecBlocks:[{numBlocks:2,dataCodewordsPerBlock:97}]},{ecCodewordsPerBlock:22,ecBlocks:[{numBlocks:2,dataCodewordsPerBlock:38},
{numBlocks:2,dataCodewordsPerBlock:39}]},{ecCodewordsPerBlock:22,ecBlocks:[{numBlocks:4,dataCodewordsPerBlock:18},{numBlocks:2,dataCodewordsPerBlock:19}]},{ecCodewordsPerBlock:26,ecBlocks:[{numBlocks:4,dataCodewordsPerBlock:14},{numBlocks:2,dataCodewordsPerBlock:15}]}]},{infoBits:39577,versionNumber:9,alignmentPatternCenters:[6,26,46],errorCorrectionLevels:[{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:2,dataCodewordsPerBlock:116}]},{ecCodewordsPerBlock:22,ecBlocks:[{numBlocks:3,dataCodewordsPerBlock:36},
{numBlocks:2,dataCodewordsPerBlock:37}]},{ecCodewordsPerBlock:20,ecBlocks:[{numBlocks:4,dataCodewordsPerBlock:16},{numBlocks:4,dataCodewordsPerBlock:17}]},{ecCodewordsPerBlock:24,ecBlocks:[{numBlocks:4,dataCodewordsPerBlock:12},{numBlocks:4,dataCodewordsPerBlock:13}]}]},{infoBits:42195,versionNumber:10,alignmentPatternCenters:[6,28,50],errorCorrectionLevels:[{ecCodewordsPerBlock:18,ecBlocks:[{numBlocks:2,dataCodewordsPerBlock:68},{numBlocks:2,dataCodewordsPerBlock:69}]},{ecCodewordsPerBlock:26,ecBlocks:[{numBlocks:4,
dataCodewordsPerBlock:43},{numBlocks:1,dataCodewordsPerBlock:44}]},{ecCodewordsPerBlock:24,ecBlocks:[{numBlocks:6,dataCodewordsPerBlock:19},{numBlocks:2,dataCodewordsPerBlock:20}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:6,dataCodewordsPerBlock:15},{numBlocks:2,dataCodewordsPerBlock:16}]}]},{infoBits:48118,versionNumber:11,alignmentPatternCenters:[6,30,54],errorCorrectionLevels:[{ecCodewordsPerBlock:20,ecBlocks:[{numBlocks:4,dataCodewordsPerBlock:81}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:1,
dataCodewordsPerBlock:50},{numBlocks:4,dataCodewordsPerBlock:51}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:4,dataCodewordsPerBlock:22},{numBlocks:4,dataCodewordsPerBlock:23}]},{ecCodewordsPerBlock:24,ecBlocks:[{numBlocks:3,dataCodewordsPerBlock:12},{numBlocks:8,dataCodewordsPerBlock:13}]}]},{infoBits:51042,versionNumber:12,alignmentPatternCenters:[6,32,58],errorCorrectionLevels:[{ecCodewordsPerBlock:24,ecBlocks:[{numBlocks:2,dataCodewordsPerBlock:92},{numBlocks:2,dataCodewordsPerBlock:93}]},
{ecCodewordsPerBlock:22,ecBlocks:[{numBlocks:6,dataCodewordsPerBlock:36},{numBlocks:2,dataCodewordsPerBlock:37}]},{ecCodewordsPerBlock:26,ecBlocks:[{numBlocks:4,dataCodewordsPerBlock:20},{numBlocks:6,dataCodewordsPerBlock:21}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:7,dataCodewordsPerBlock:14},{numBlocks:4,dataCodewordsPerBlock:15}]}]},{infoBits:55367,versionNumber:13,alignmentPatternCenters:[6,34,62],errorCorrectionLevels:[{ecCodewordsPerBlock:26,ecBlocks:[{numBlocks:4,dataCodewordsPerBlock:107}]},
{ecCodewordsPerBlock:22,ecBlocks:[{numBlocks:8,dataCodewordsPerBlock:37},{numBlocks:1,dataCodewordsPerBlock:38}]},{ecCodewordsPerBlock:24,ecBlocks:[{numBlocks:8,dataCodewordsPerBlock:20},{numBlocks:4,dataCodewordsPerBlock:21}]},{ecCodewordsPerBlock:22,ecBlocks:[{numBlocks:12,dataCodewordsPerBlock:11},{numBlocks:4,dataCodewordsPerBlock:12}]}]},{infoBits:58893,versionNumber:14,alignmentPatternCenters:[6,26,46,66],errorCorrectionLevels:[{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:3,dataCodewordsPerBlock:115},
{numBlocks:1,dataCodewordsPerBlock:116}]},{ecCodewordsPerBlock:24,ecBlocks:[{numBlocks:4,dataCodewordsPerBlock:40},{numBlocks:5,dataCodewordsPerBlock:41}]},{ecCodewordsPerBlock:20,ecBlocks:[{numBlocks:11,dataCodewordsPerBlock:16},{numBlocks:5,dataCodewordsPerBlock:17}]},{ecCodewordsPerBlock:24,ecBlocks:[{numBlocks:11,dataCodewordsPerBlock:12},{numBlocks:5,dataCodewordsPerBlock:13}]}]},{infoBits:63784,versionNumber:15,alignmentPatternCenters:[6,26,48,70],errorCorrectionLevels:[{ecCodewordsPerBlock:22,
ecBlocks:[{numBlocks:5,dataCodewordsPerBlock:87},{numBlocks:1,dataCodewordsPerBlock:88}]},{ecCodewordsPerBlock:24,ecBlocks:[{numBlocks:5,dataCodewordsPerBlock:41},{numBlocks:5,dataCodewordsPerBlock:42}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:5,dataCodewordsPerBlock:24},{numBlocks:7,dataCodewordsPerBlock:25}]},{ecCodewordsPerBlock:24,ecBlocks:[{numBlocks:11,dataCodewordsPerBlock:12},{numBlocks:7,dataCodewordsPerBlock:13}]}]},{infoBits:68472,versionNumber:16,alignmentPatternCenters:[6,26,50,
74],errorCorrectionLevels:[{ecCodewordsPerBlock:24,ecBlocks:[{numBlocks:5,dataCodewordsPerBlock:98},{numBlocks:1,dataCodewordsPerBlock:99}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:7,dataCodewordsPerBlock:45},{numBlocks:3,dataCodewordsPerBlock:46}]},{ecCodewordsPerBlock:24,ecBlocks:[{numBlocks:15,dataCodewordsPerBlock:19},{numBlocks:2,dataCodewordsPerBlock:20}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:3,dataCodewordsPerBlock:15},{numBlocks:13,dataCodewordsPerBlock:16}]}]},{infoBits:70749,
versionNumber:17,alignmentPatternCenters:[6,30,54,78],errorCorrectionLevels:[{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:1,dataCodewordsPerBlock:107},{numBlocks:5,dataCodewordsPerBlock:108}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:10,dataCodewordsPerBlock:46},{numBlocks:1,dataCodewordsPerBlock:47}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:1,dataCodewordsPerBlock:22},{numBlocks:15,dataCodewordsPerBlock:23}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:2,dataCodewordsPerBlock:14},{numBlocks:17,
dataCodewordsPerBlock:15}]}]},{infoBits:76311,versionNumber:18,alignmentPatternCenters:[6,30,56,82],errorCorrectionLevels:[{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:5,dataCodewordsPerBlock:120},{numBlocks:1,dataCodewordsPerBlock:121}]},{ecCodewordsPerBlock:26,ecBlocks:[{numBlocks:9,dataCodewordsPerBlock:43},{numBlocks:4,dataCodewordsPerBlock:44}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:17,dataCodewordsPerBlock:22},{numBlocks:1,dataCodewordsPerBlock:23}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:2,
dataCodewordsPerBlock:14},{numBlocks:19,dataCodewordsPerBlock:15}]}]},{infoBits:79154,versionNumber:19,alignmentPatternCenters:[6,30,58,86],errorCorrectionLevels:[{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:3,dataCodewordsPerBlock:113},{numBlocks:4,dataCodewordsPerBlock:114}]},{ecCodewordsPerBlock:26,ecBlocks:[{numBlocks:3,dataCodewordsPerBlock:44},{numBlocks:11,dataCodewordsPerBlock:45}]},{ecCodewordsPerBlock:26,ecBlocks:[{numBlocks:17,dataCodewordsPerBlock:21},{numBlocks:4,dataCodewordsPerBlock:22}]},
{ecCodewordsPerBlock:26,ecBlocks:[{numBlocks:9,dataCodewordsPerBlock:13},{numBlocks:16,dataCodewordsPerBlock:14}]}]},{infoBits:84390,versionNumber:20,alignmentPatternCenters:[6,34,62,90],errorCorrectionLevels:[{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:3,dataCodewordsPerBlock:107},{numBlocks:5,dataCodewordsPerBlock:108}]},{ecCodewordsPerBlock:26,ecBlocks:[{numBlocks:3,dataCodewordsPerBlock:41},{numBlocks:13,dataCodewordsPerBlock:42}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:15,dataCodewordsPerBlock:24},
{numBlocks:5,dataCodewordsPerBlock:25}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:15,dataCodewordsPerBlock:15},{numBlocks:10,dataCodewordsPerBlock:16}]}]},{infoBits:87683,versionNumber:21,alignmentPatternCenters:[6,28,50,72,94],errorCorrectionLevels:[{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:4,dataCodewordsPerBlock:116},{numBlocks:4,dataCodewordsPerBlock:117}]},{ecCodewordsPerBlock:26,ecBlocks:[{numBlocks:17,dataCodewordsPerBlock:42}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:17,dataCodewordsPerBlock:22},
{numBlocks:6,dataCodewordsPerBlock:23}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:19,dataCodewordsPerBlock:16},{numBlocks:6,dataCodewordsPerBlock:17}]}]},{infoBits:92361,versionNumber:22,alignmentPatternCenters:[6,26,50,74,98],errorCorrectionLevels:[{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:2,dataCodewordsPerBlock:111},{numBlocks:7,dataCodewordsPerBlock:112}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:17,dataCodewordsPerBlock:46}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:7,dataCodewordsPerBlock:24},
{numBlocks:16,dataCodewordsPerBlock:25}]},{ecCodewordsPerBlock:24,ecBlocks:[{numBlocks:34,dataCodewordsPerBlock:13}]}]},{infoBits:96236,versionNumber:23,alignmentPatternCenters:[6,30,54,74,102],errorCorrectionLevels:[{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:4,dataCodewordsPerBlock:121},{numBlocks:5,dataCodewordsPerBlock:122}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:4,dataCodewordsPerBlock:47},{numBlocks:14,dataCodewordsPerBlock:48}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:11,dataCodewordsPerBlock:24},
{numBlocks:14,dataCodewordsPerBlock:25}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:16,dataCodewordsPerBlock:15},{numBlocks:14,dataCodewordsPerBlock:16}]}]},{infoBits:102084,versionNumber:24,alignmentPatternCenters:[6,28,54,80,106],errorCorrectionLevels:[{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:6,dataCodewordsPerBlock:117},{numBlocks:4,dataCodewordsPerBlock:118}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:6,dataCodewordsPerBlock:45},{numBlocks:14,dataCodewordsPerBlock:46}]},{ecCodewordsPerBlock:30,
ecBlocks:[{numBlocks:11,dataCodewordsPerBlock:24},{numBlocks:16,dataCodewordsPerBlock:25}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:30,dataCodewordsPerBlock:16},{numBlocks:2,dataCodewordsPerBlock:17}]}]},{infoBits:102881,versionNumber:25,alignmentPatternCenters:[6,32,58,84,110],errorCorrectionLevels:[{ecCodewordsPerBlock:26,ecBlocks:[{numBlocks:8,dataCodewordsPerBlock:106},{numBlocks:4,dataCodewordsPerBlock:107}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:8,dataCodewordsPerBlock:47},{numBlocks:13,
dataCodewordsPerBlock:48}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:7,dataCodewordsPerBlock:24},{numBlocks:22,dataCodewordsPerBlock:25}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:22,dataCodewordsPerBlock:15},{numBlocks:13,dataCodewordsPerBlock:16}]}]},{infoBits:110507,versionNumber:26,alignmentPatternCenters:[6,30,58,86,114],errorCorrectionLevels:[{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:10,dataCodewordsPerBlock:114},{numBlocks:2,dataCodewordsPerBlock:115}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:19,
dataCodewordsPerBlock:46},{numBlocks:4,dataCodewordsPerBlock:47}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:28,dataCodewordsPerBlock:22},{numBlocks:6,dataCodewordsPerBlock:23}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:33,dataCodewordsPerBlock:16},{numBlocks:4,dataCodewordsPerBlock:17}]}]},{infoBits:110734,versionNumber:27,alignmentPatternCenters:[6,34,62,90,118],errorCorrectionLevels:[{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:8,dataCodewordsPerBlock:122},{numBlocks:4,dataCodewordsPerBlock:123}]},
{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:22,dataCodewordsPerBlock:45},{numBlocks:3,dataCodewordsPerBlock:46}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:8,dataCodewordsPerBlock:23},{numBlocks:26,dataCodewordsPerBlock:24}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:12,dataCodewordsPerBlock:15},{numBlocks:28,dataCodewordsPerBlock:16}]}]},{infoBits:117786,versionNumber:28,alignmentPatternCenters:[6,26,50,74,98,122],errorCorrectionLevels:[{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:3,dataCodewordsPerBlock:117},
{numBlocks:10,dataCodewordsPerBlock:118}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:3,dataCodewordsPerBlock:45},{numBlocks:23,dataCodewordsPerBlock:46}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:4,dataCodewordsPerBlock:24},{numBlocks:31,dataCodewordsPerBlock:25}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:11,dataCodewordsPerBlock:15},{numBlocks:31,dataCodewordsPerBlock:16}]}]},{infoBits:119615,versionNumber:29,alignmentPatternCenters:[6,30,54,78,102,126],errorCorrectionLevels:[{ecCodewordsPerBlock:30,
ecBlocks:[{numBlocks:7,dataCodewordsPerBlock:116},{numBlocks:7,dataCodewordsPerBlock:117}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:21,dataCodewordsPerBlock:45},{numBlocks:7,dataCodewordsPerBlock:46}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:1,dataCodewordsPerBlock:23},{numBlocks:37,dataCodewordsPerBlock:24}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:19,dataCodewordsPerBlock:15},{numBlocks:26,dataCodewordsPerBlock:16}]}]},{infoBits:126325,versionNumber:30,alignmentPatternCenters:[6,
26,52,78,104,130],errorCorrectionLevels:[{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:5,dataCodewordsPerBlock:115},{numBlocks:10,dataCodewordsPerBlock:116}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:19,dataCodewordsPerBlock:47},{numBlocks:10,dataCodewordsPerBlock:48}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:15,dataCodewordsPerBlock:24},{numBlocks:25,dataCodewordsPerBlock:25}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:23,dataCodewordsPerBlock:15},{numBlocks:25,dataCodewordsPerBlock:16}]}]},
{infoBits:127568,versionNumber:31,alignmentPatternCenters:[6,30,56,82,108,134],errorCorrectionLevels:[{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:13,dataCodewordsPerBlock:115},{numBlocks:3,dataCodewordsPerBlock:116}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:2,dataCodewordsPerBlock:46},{numBlocks:29,dataCodewordsPerBlock:47}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:42,dataCodewordsPerBlock:24},{numBlocks:1,dataCodewordsPerBlock:25}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:23,dataCodewordsPerBlock:15},
{numBlocks:28,dataCodewordsPerBlock:16}]}]},{infoBits:133589,versionNumber:32,alignmentPatternCenters:[6,34,60,86,112,138],errorCorrectionLevels:[{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:17,dataCodewordsPerBlock:115}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:10,dataCodewordsPerBlock:46},{numBlocks:23,dataCodewordsPerBlock:47}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:10,dataCodewordsPerBlock:24},{numBlocks:35,dataCodewordsPerBlock:25}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:19,
dataCodewordsPerBlock:15},{numBlocks:35,dataCodewordsPerBlock:16}]}]},{infoBits:136944,versionNumber:33,alignmentPatternCenters:[6,30,58,86,114,142],errorCorrectionLevels:[{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:17,dataCodewordsPerBlock:115},{numBlocks:1,dataCodewordsPerBlock:116}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:14,dataCodewordsPerBlock:46},{numBlocks:21,dataCodewordsPerBlock:47}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:29,dataCodewordsPerBlock:24},{numBlocks:19,dataCodewordsPerBlock:25}]},
{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:11,dataCodewordsPerBlock:15},{numBlocks:46,dataCodewordsPerBlock:16}]}]},{infoBits:141498,versionNumber:34,alignmentPatternCenters:[6,34,62,90,118,146],errorCorrectionLevels:[{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:13,dataCodewordsPerBlock:115},{numBlocks:6,dataCodewordsPerBlock:116}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:14,dataCodewordsPerBlock:46},{numBlocks:23,dataCodewordsPerBlock:47}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:44,
dataCodewordsPerBlock:24},{numBlocks:7,dataCodewordsPerBlock:25}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:59,dataCodewordsPerBlock:16},{numBlocks:1,dataCodewordsPerBlock:17}]}]},{infoBits:145311,versionNumber:35,alignmentPatternCenters:[6,30,54,78,102,126,150],errorCorrectionLevels:[{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:12,dataCodewordsPerBlock:121},{numBlocks:7,dataCodewordsPerBlock:122}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:12,dataCodewordsPerBlock:47},{numBlocks:26,dataCodewordsPerBlock:48}]},
{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:39,dataCodewordsPerBlock:24},{numBlocks:14,dataCodewordsPerBlock:25}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:22,dataCodewordsPerBlock:15},{numBlocks:41,dataCodewordsPerBlock:16}]}]},{infoBits:150283,versionNumber:36,alignmentPatternCenters:[6,24,50,76,102,128,154],errorCorrectionLevels:[{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:6,dataCodewordsPerBlock:121},{numBlocks:14,dataCodewordsPerBlock:122}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:6,
dataCodewordsPerBlock:47},{numBlocks:34,dataCodewordsPerBlock:48}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:46,dataCodewordsPerBlock:24},{numBlocks:10,dataCodewordsPerBlock:25}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:2,dataCodewordsPerBlock:15},{numBlocks:64,dataCodewordsPerBlock:16}]}]},{infoBits:152622,versionNumber:37,alignmentPatternCenters:[6,28,54,80,106,132,158],errorCorrectionLevels:[{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:17,dataCodewordsPerBlock:122},{numBlocks:4,dataCodewordsPerBlock:123}]},
{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:29,dataCodewordsPerBlock:46},{numBlocks:14,dataCodewordsPerBlock:47}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:49,dataCodewordsPerBlock:24},{numBlocks:10,dataCodewordsPerBlock:25}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:24,dataCodewordsPerBlock:15},{numBlocks:46,dataCodewordsPerBlock:16}]}]},{infoBits:158308,versionNumber:38,alignmentPatternCenters:[6,32,58,84,110,136,162],errorCorrectionLevels:[{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:4,
dataCodewordsPerBlock:122},{numBlocks:18,dataCodewordsPerBlock:123}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:13,dataCodewordsPerBlock:46},{numBlocks:32,dataCodewordsPerBlock:47}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:48,dataCodewordsPerBlock:24},{numBlocks:14,dataCodewordsPerBlock:25}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:42,dataCodewordsPerBlock:15},{numBlocks:32,dataCodewordsPerBlock:16}]}]},{infoBits:161089,versionNumber:39,alignmentPatternCenters:[6,26,54,82,110,138,166],
errorCorrectionLevels:[{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:20,dataCodewordsPerBlock:117},{numBlocks:4,dataCodewordsPerBlock:118}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:40,dataCodewordsPerBlock:47},{numBlocks:7,dataCodewordsPerBlock:48}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:43,dataCodewordsPerBlock:24},{numBlocks:22,dataCodewordsPerBlock:25}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:10,dataCodewordsPerBlock:15},{numBlocks:67,dataCodewordsPerBlock:16}]}]},{infoBits:167017,
versionNumber:40,alignmentPatternCenters:[6,30,58,86,114,142,170],errorCorrectionLevels:[{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:19,dataCodewordsPerBlock:118},{numBlocks:6,dataCodewordsPerBlock:119}]},{ecCodewordsPerBlock:28,ecBlocks:[{numBlocks:18,dataCodewordsPerBlock:47},{numBlocks:31,dataCodewordsPerBlock:48}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:34,dataCodewordsPerBlock:24},{numBlocks:34,dataCodewordsPerBlock:25}]},{ecCodewordsPerBlock:30,ecBlocks:[{numBlocks:20,dataCodewordsPerBlock:15},
{numBlocks:61,dataCodewordsPerBlock:16}]}]}];function J(a,b){a^=b;for(b=0;a;)b++,a&=a-1;return b}function K(a,b){return b<<1|a}
let ia=[{bits:21522,formatInfo:{errorCorrectionLevel:1,dataMask:0}},{bits:20773,formatInfo:{errorCorrectionLevel:1,dataMask:1}},{bits:24188,formatInfo:{errorCorrectionLevel:1,dataMask:2}},{bits:23371,formatInfo:{errorCorrectionLevel:1,dataMask:3}},{bits:17913,formatInfo:{errorCorrectionLevel:1,dataMask:4}},{bits:16590,formatInfo:{errorCorrectionLevel:1,dataMask:5}},{bits:20375,formatInfo:{errorCorrectionLevel:1,dataMask:6}},{bits:19104,formatInfo:{errorCorrectionLevel:1,dataMask:7}},{bits:30660,formatInfo:{errorCorrectionLevel:0,
dataMask:0}},{bits:29427,formatInfo:{errorCorrectionLevel:0,dataMask:1}},{bits:32170,formatInfo:{errorCorrectionLevel:0,dataMask:2}},{bits:30877,formatInfo:{errorCorrectionLevel:0,dataMask:3}},{bits:26159,formatInfo:{errorCorrectionLevel:0,dataMask:4}},{bits:25368,formatInfo:{errorCorrectionLevel:0,dataMask:5}},{bits:27713,formatInfo:{errorCorrectionLevel:0,dataMask:6}},{bits:26998,formatInfo:{errorCorrectionLevel:0,dataMask:7}},{bits:5769,formatInfo:{errorCorrectionLevel:3,dataMask:0}},{bits:5054,
formatInfo:{errorCorrectionLevel:3,dataMask:1}},{bits:7399,formatInfo:{errorCorrectionLevel:3,dataMask:2}},{bits:6608,formatInfo:{errorCorrectionLevel:3,dataMask:3}},{bits:1890,formatInfo:{errorCorrectionLevel:3,dataMask:4}},{bits:597,formatInfo:{errorCorrectionLevel:3,dataMask:5}},{bits:3340,formatInfo:{errorCorrectionLevel:3,dataMask:6}},{bits:2107,formatInfo:{errorCorrectionLevel:3,dataMask:7}},{bits:13663,formatInfo:{errorCorrectionLevel:2,dataMask:0}},{bits:12392,formatInfo:{errorCorrectionLevel:2,
dataMask:1}},{bits:16177,formatInfo:{errorCorrectionLevel:2,dataMask:2}},{bits:14854,formatInfo:{errorCorrectionLevel:2,dataMask:3}},{bits:9396,formatInfo:{errorCorrectionLevel:2,dataMask:4}},{bits:8579,formatInfo:{errorCorrectionLevel:2,dataMask:5}},{bits:11994,formatInfo:{errorCorrectionLevel:2,dataMask:6}},{bits:11245,formatInfo:{errorCorrectionLevel:2,dataMask:7}}],ja=[a=>0===(a.y+a.x)%2,a=>0===a.y%2,a=>0===a.x%3,a=>0===(a.y+a.x)%3,a=>0===(Math.floor(a.y/2)+Math.floor(a.x/3))%2,a=>0===a.x*a.y%
2+a.x*a.y%3,a=>0===(a.y*a.x%2+a.y*a.x%3)%2,a=>0===((a.y+a.x)%2+a.y*a.x%3)%2];
function ka(a,b,c){c=ja[c.dataMask];let d=a.height;var e=17+4*b.versionNumber;let f=x.createEmpty(e,e);f.setRegion(0,0,9,9,!0);f.setRegion(e-8,0,8,9,!0);f.setRegion(0,e-8,9,8,!0);for(var g of b.alignmentPatternCenters)for(var h of b.alignmentPatternCenters)6===g&&6===h||6===g&&h===e-7||g===e-7&&6===h||f.setRegion(g-2,h-2,5,5,!0);f.setRegion(6,9,1,e-17,!0);f.setRegion(9,6,e-17,1,!0);6<b.versionNumber&&(f.setRegion(e-11,0,3,6,!0),f.setRegion(0,e-11,6,3,!0));b=[];h=g=0;e=!0;for(let k=d-1;0<k;k-=2){6===
k&&k--;for(let m=0;m<d;m++){let l=e?d-1-m:m;for(let n=0;2>n;n++){let q=k-n;if(!f.get(q,l)){h++;let r=a.get(q,l);c({y:l,x:q})&&(r=!r);g=g<<1|r;8===h&&(b.push(g),g=h=0)}}}e=!e}return b}
function la(a){var b=a.height,c=Math.floor((b-17)/4);if(6>=c)return I[c-1];c=0;for(var d=5;0<=d;d--)for(var e=b-9;e>=b-11;e--)c=K(a.get(e,d),c);d=0;for(e=5;0<=e;e--)for(let g=b-9;g>=b-11;g--)d=K(a.get(e,g),d);a=Infinity;let f;for(let g of I){if(g.infoBits===c||g.infoBits===d)return g;b=J(c,g.infoBits);b<a&&(f=g,a=b);b=J(d,g.infoBits);b<a&&(f=g,a=b)}if(3>=a)return f}
function ma(a){let b=0;for(var c=0;8>=c;c++)6!==c&&(b=K(a.get(c,8),b));for(c=7;0<=c;c--)6!==c&&(b=K(a.get(8,c),b));var d=a.height;c=0;for(var e=d-1;e>=d-7;e--)c=K(a.get(8,e),c);for(e=d-8;e<d;e++)c=K(a.get(e,8),c);a=Infinity;d=null;for(let {bits:f,formatInfo:g}of ia){if(f===b||f===c)return g;e=J(b,f);e<a&&(d=g,a=e);b!==c&&(e=J(c,f),e<a&&(d=g,a=e))}return 3>=a?d:null}
function na(a,b,c){let d=b.errorCorrectionLevels[c],e=[],f=0;d.ecBlocks.forEach(h=>{for(let k=0;k<h.numBlocks;k++)e.push({numDataCodewords:h.dataCodewordsPerBlock,codewords:[]}),f+=h.dataCodewordsPerBlock+d.ecCodewordsPerBlock});if(a.length<f)return null;a=a.slice(0,f);b=d.ecBlocks[0].dataCodewordsPerBlock;for(c=0;c<b;c++)for(var g of e)g.codewords.push(a.shift());if(1<d.ecBlocks.length)for(g=d.ecBlocks[0].numBlocks,b=d.ecBlocks[1].numBlocks,c=0;c<b;c++)e[g+c].codewords.push(a.shift());for(;0<a.length;)for(let h of e)h.codewords.push(a.shift());
return e}function L(a){let b=la(a);if(!b)return null;var c=ma(a);if(!c)return null;a=ka(a,b,c);var d=na(a,b,c.errorCorrectionLevel);if(!d)return null;c=d.reduce((e,f)=>e+f.numDataCodewords,0);c=new Uint8ClampedArray(c);a=0;for(let e of d){d=ha(e.codewords,e.codewords.length-e.numDataCodewords);if(!d)return null;for(let f=0;f<e.numDataCodewords;f++)c[a++]=d[f]}try{return da(c,b.versionNumber)}catch(e){return null}}
function M(a,b,c,d){var e=a.x-b.x+c.x-d.x;let f=a.y-b.y+c.y-d.y;if(0===e&&0===f)return{a11:b.x-a.x,a12:b.y-a.y,a13:0,a21:c.x-b.x,a22:c.y-b.y,a23:0,a31:a.x,a32:a.y,a33:1};let g=b.x-c.x;var h=d.x-c.x;let k=b.y-c.y,m=d.y-c.y;c=g*m-h*k;h=(e*m-h*f)/c;e=(g*f-e*k)/c;return{a11:b.x-a.x+h*b.x,a12:b.y-a.y+h*b.y,a13:h,a21:d.x-a.x+e*d.x,a22:d.y-a.y+e*d.y,a23:e,a31:a.x,a32:a.y,a33:1}}
function oa(a,b,c,d){a=M(a,b,c,d);return{a11:a.a22*a.a33-a.a23*a.a32,a12:a.a13*a.a32-a.a12*a.a33,a13:a.a12*a.a23-a.a13*a.a22,a21:a.a23*a.a31-a.a21*a.a33,a22:a.a11*a.a33-a.a13*a.a31,a23:a.a13*a.a21-a.a11*a.a23,a31:a.a21*a.a32-a.a22*a.a31,a32:a.a12*a.a31-a.a11*a.a32,a33:a.a11*a.a22-a.a12*a.a21}}
function pa(a,b){var c=oa({x:3.5,y:3.5},{x:b.dimension-3.5,y:3.5},{x:b.dimension-6.5,y:b.dimension-6.5},{x:3.5,y:b.dimension-3.5}),d=M(b.topLeft,b.topRight,b.alignmentPattern,b.bottomLeft),e=d.a11*c.a11+d.a21*c.a12+d.a31*c.a13,f=d.a12*c.a11+d.a22*c.a12+d.a32*c.a13,g=d.a13*c.a11+d.a23*c.a12+d.a33*c.a13,h=d.a11*c.a21+d.a21*c.a22+d.a31*c.a23,k=d.a12*c.a21+d.a22*c.a22+d.a32*c.a23,m=d.a13*c.a21+d.a23*c.a22+d.a33*c.a23,l=d.a11*c.a31+d.a21*c.a32+d.a31*c.a33,n=d.a12*c.a31+d.a22*c.a32+d.a32*c.a33,q=d.a13*
c.a31+d.a23*c.a32+d.a33*c.a33;c=x.createEmpty(b.dimension,b.dimension);d=(r,u)=>{const p=g*r+m*u+q;return{x:(e*r+h*u+l)/p,y:(f*r+k*u+n)/p}};for(let r=0;r<b.dimension;r++)for(let u=0;u<b.dimension;u++){let p=d(u+.5,r+.5);c.set(u,r,a.get(Math.floor(p.x),Math.floor(p.y)))}return{matrix:c,mappingFunction:d}}let N=(a,b)=>Math.sqrt(Math.pow(b.x-a.x,2)+Math.pow(b.y-a.y,2));function O(a){return a.reduce((b,c)=>b+c)}
function qa(a,b,c){let d=N(a,b),e=N(b,c),f=N(a,c),g,h,k;e>=d&&e>=f?[g,h,k]=[b,a,c]:f>=e&&f>=d?[g,h,k]=[a,b,c]:[g,h,k]=[a,c,b];0>(k.x-h.x)*(g.y-h.y)-(k.y-h.y)*(g.x-h.x)&&([g,k]=[k,g]);return{bottomLeft:g,topLeft:h,topRight:k}}
function ra(a,b,c,d){d=(O(P(a,c,d,5))/7+O(P(a,b,d,5))/7+O(P(c,a,d,5))/7+O(P(b,a,d,5))/7)/4;if(1>d)throw Error("Invalid module size");b=Math.round(N(a,b)/d);a=Math.round(N(a,c)/d);a=Math.floor((b+a)/2)+7;switch(a%4){case 0:a++;break;case 2:a--}return{dimension:a,moduleSize:d}}
function Q(a,b,c,d){let e=[{x:Math.floor(a.x),y:Math.floor(a.y)}];var f=Math.abs(b.y-a.y)>Math.abs(b.x-a.x);if(f){var g=Math.floor(a.y);var h=Math.floor(a.x);a=Math.floor(b.y);b=Math.floor(b.x)}else g=Math.floor(a.x),h=Math.floor(a.y),a=Math.floor(b.x),b=Math.floor(b.y);let k=Math.abs(a-g),m=Math.abs(b-h),l=Math.floor(-k/2),n=g<a?1:-1,q=h<b?1:-1,r=!0;for(let u=g,p=h;u!==a+n;u+=n){g=f?p:u;h=f?u:p;if(c.get(g,h)!==r&&(r=!r,e.push({x:g,y:h}),e.length===d+1))break;l+=m;if(0<l){if(p===b)break;p+=q;l-=k}}c=
[];for(f=0;f<d;f++)e[f]&&e[f+1]?c.push(N(e[f],e[f+1])):c.push(0);return c}function P(a,b,c,d){let e=b.y-a.y,f=b.x-a.x;b=Q(a,b,c,Math.ceil(d/2));a=Q(a,{x:a.x-f,y:a.y-e},c,Math.ceil(d/2));c=b.shift()+a.shift()-1;return a.concat(c).concat(...b)}function R(a,b){let c=O(a)/O(b),d=0;b.forEach((e,f)=>{d+=Math.pow(a[f]-e*c,2)});return{averageSize:c,error:d}}
function S(a,b,c){try{let d=P(a,{x:-1,y:a.y},c,b.length),e=P(a,{x:a.x,y:-1},c,b.length),f=P(a,{x:Math.max(0,a.x-a.y)-1,y:Math.max(0,a.y-a.x)-1},c,b.length),g=P(a,{x:Math.min(c.width,a.x+a.y)+1,y:Math.min(c.height,a.y+a.x)+1},c,b.length),h=R(d,b),k=R(e,b),m=R(f,b),l=R(g,b),n=(h.averageSize+k.averageSize+m.averageSize+l.averageSize)/4;return Math.sqrt(h.error*h.error+k.error*k.error+m.error*m.error+l.error*l.error)+(Math.pow(h.averageSize-n,2)+Math.pow(k.averageSize-n,2)+Math.pow(m.averageSize-n,2)+
Math.pow(l.averageSize-n,2))/n}catch(d){return Infinity}}function T(a,b){for(var c=Math.round(b.x);a.get(c,Math.round(b.y));)c--;for(var d=Math.round(b.x);a.get(d,Math.round(b.y));)d++;c=(c+d)/2;for(d=Math.round(b.y);a.get(Math.round(c),d);)d--;for(b=Math.round(b.y);a.get(Math.round(c),b);)b++;return{x:c,y:(d+b)/2}}
function sa(a){var b=[],c=[];let d=[];var e=[];for(let p=0;p<=a.height;p++){var f=0,g=!1;let t=[0,0,0,0,0];for(let v=-1;v<=a.width;v++){var h=a.get(v,p);if(h===g)f++;else{t=[t[1],t[2],t[3],t[4],f];f=1;g=h;var k=O(t)/7;k=Math.abs(t[0]-k)<k&&Math.abs(t[1]-k)<k&&Math.abs(t[2]-3*k)<3*k&&Math.abs(t[3]-k)<k&&Math.abs(t[4]-k)<k&&!h;var m=O(t.slice(-3))/3;h=Math.abs(t[2]-m)<m&&Math.abs(t[3]-m)<m&&Math.abs(t[4]-m)<m&&h;if(k){let z=v-t[3]-t[4],y=z-t[2];k={startX:y,endX:z,y:p};m=c.filter(w=>y>=w.bottom.startX&&
y<=w.bottom.endX||z>=w.bottom.startX&&y<=w.bottom.endX||y<=w.bottom.startX&&z>=w.bottom.endX&&1.5>t[2]/(w.bottom.endX-w.bottom.startX)&&.5<t[2]/(w.bottom.endX-w.bottom.startX));0<m.length?m[0].bottom=k:c.push({top:k,bottom:k})}if(h){let z=v-t[4],y=z-t[3];h={startX:y,y:p,endX:z};k=e.filter(w=>y>=w.bottom.startX&&y<=w.bottom.endX||z>=w.bottom.startX&&y<=w.bottom.endX||y<=w.bottom.startX&&z>=w.bottom.endX&&1.5>t[2]/(w.bottom.endX-w.bottom.startX)&&.5<t[2]/(w.bottom.endX-w.bottom.startX));0<k.length?
k[0].bottom=h:e.push({top:h,bottom:h})}}}b.push(...c.filter(v=>v.bottom.y!==p&&2<=v.bottom.y-v.top.y));c=c.filter(v=>v.bottom.y===p);d.push(...e.filter(v=>v.bottom.y!==p));e=e.filter(v=>v.bottom.y===p)}b.push(...c.filter(p=>2<=p.bottom.y-p.top.y));d.push(...e);c=[];for(var l of b)2>l.bottom.y-l.top.y||(b=(l.top.startX+l.top.endX+l.bottom.startX+l.bottom.endX)/4,e=(l.top.y+l.bottom.y+1)/2,a.get(Math.round(b),Math.round(e))&&(f=[l.top.endX-l.top.startX,l.bottom.endX-l.bottom.startX,l.bottom.y-l.top.y+
1],f=O(f)/f.length,g=S({x:Math.round(b),y:Math.round(e)},[1,1,3,1,1],a),c.push({score:g,x:b,y:e,size:f})));if(3>c.length)return null;c.sort((p,t)=>p.score-t.score);l=[];for(b=0;b<Math.min(c.length,5);++b){e=c[b];f=[];for(var n of c)n!==e&&f.push(Object.assign(Object.assign({},n),{score:n.score+Math.pow(n.size-e.size,2)/e.size}));f.sort((p,t)=>p.score-t.score);l.push({points:[e,f[0],f[1]],score:e.score+f[0].score+f[1].score})}l.sort((p,t)=>p.score-t.score);let {topRight:q,topLeft:r,bottomLeft:u}=qa(...l[0].points);
l=U(a,d,q,r,u);n=[];l&&n.push({alignmentPattern:{x:l.alignmentPattern.x,y:l.alignmentPattern.y},bottomLeft:{x:u.x,y:u.y},dimension:l.dimension,topLeft:{x:r.x,y:r.y},topRight:{x:q.x,y:q.y}});l=T(a,q);b=T(a,r);c=T(a,u);(a=U(a,d,l,b,c))&&n.push({alignmentPattern:{x:a.alignmentPattern.x,y:a.alignmentPattern.y},bottomLeft:{x:c.x,y:c.y},topLeft:{x:b.x,y:b.y},topRight:{x:l.x,y:l.y},dimension:a.dimension});return 0===n.length?null:n}
function U(a,b,c,d,e){let f,g;try{({dimension:f,moduleSize:g}=ra(d,c,e,a))}catch(l){return null}var h=c.x-d.x+e.x,k=c.y-d.y+e.y;c=(N(d,e)+N(d,c))/2/g;e=1-3/c;let m={x:d.x+e*(h-d.x),y:d.y+e*(k-d.y)};b=b.map(l=>{const n=(l.top.startX+l.top.endX+l.bottom.startX+l.bottom.endX)/4;l=(l.top.y+l.bottom.y+1)/2;if(a.get(Math.floor(n),Math.floor(l))){var q=S({x:Math.floor(n),y:Math.floor(l)},[1,1,1],a)+N({x:n,y:l},m);return{x:n,y:l,score:q}}}).filter(l=>!!l).sort((l,n)=>l.score-n.score);return{alignmentPattern:15<=
c&&b.length?b[0]:m,dimension:f}}
function V(a){var b=sa(a);if(!b)return null;for(let e of b){b=pa(a,e);var c=b.matrix;if(null==c)c=null;else{var d=L(c);if(d)c=d;else{for(d=0;d<c.width;d++)for(let f=d+1;f<c.height;f++)c.get(d,f)!==c.get(f,d)&&(c.set(d,f,!c.get(d,f)),c.set(f,d,!c.get(f,d)));c=L(c)}}if(c)return{binaryData:c.bytes,data:c.text,chunks:c.chunks,version:c.version,location:{topRightCorner:b.mappingFunction(e.dimension,0),topLeftCorner:b.mappingFunction(0,0),bottomRightCorner:b.mappingFunction(e.dimension,e.dimension),bottomLeftCorner:b.mappingFunction(0,
e.dimension),topRightFinderPattern:e.topRight,topLeftFinderPattern:e.topLeft,bottomLeftFinderPattern:e.bottomLeft,bottomRightAlignmentPattern:e.alignmentPattern},matrix:b.matrix}}return null}let ta={inversionAttempts:"attemptBoth",greyScaleWeights:{red:.2126,green:.7152,blue:.0722,useIntegerApproximation:!1},canOverwriteImage:!0};function W(a,b){Object.keys(b).forEach(c=>{a[c]=b[c]})}
function X(a,b,c,d={}){let e=Object.create(null);W(e,ta);W(e,d);d="onlyInvert"===e.inversionAttempts||"invertFirst"===e.inversionAttempts;var f="attemptBoth"===e.inversionAttempts||d;var g=e.greyScaleWeights,h=e.canOverwriteImage,k=b*c;if(a.length!==4*k)throw Error("Malformed data passed to binarizer.");var m=0;if(h){var l=new Uint8ClampedArray(a.buffer,m,k);m+=k}l=new A(b,c,l);if(g.useIntegerApproximation)for(var n=0;n<c;n++)for(var q=0;q<b;q++){var r=4*(n*b+q);l.set(q,n,g.red*a[r]+g.green*a[r+1]+
g.blue*a[r+2]+128>>8)}else for(n=0;n<c;n++)for(q=0;q<b;q++)r=4*(n*b+q),l.set(q,n,g.red*a[r]+g.green*a[r+1]+g.blue*a[r+2]);g=Math.ceil(b/8);n=Math.ceil(c/8);q=g*n;if(h){var u=new Uint8ClampedArray(a.buffer,m,q);m+=q}u=new A(g,n,u);for(q=0;q<n;q++)for(r=0;r<g;r++){var p=Infinity,t=0;for(var v=0;8>v;v++)for(let w=0;8>w;w++){let aa=l.get(8*r+w,8*q+v);p=Math.min(p,aa);t=Math.max(t,aa)}v=(p+t)/2;v=Math.min(255,1.11*v);24>=t-p&&(v=p/2,0<q&&0<r&&(t=(u.get(r,q-1)+2*u.get(r-1,q)+u.get(r-1,q-1))/4,p<t&&(v=t)));
u.set(r,q,v)}h?(q=new Uint8ClampedArray(a.buffer,m,k),m+=k,q=new x(q,b)):q=x.createEmpty(b,c);r=null;f&&(h?(a=new Uint8ClampedArray(a.buffer,m,k),r=new x(a,b)):r=x.createEmpty(b,c));for(b=0;b<n;b++)for(a=0;a<g;a++){c=g-3;c=2>a?2:a>c?c:a;h=n-3;h=2>b?2:b>h?h:b;k=0;for(m=-2;2>=m;m++)for(p=-2;2>=p;p++)k+=u.get(c+m,h+p);c=k/25;for(h=0;8>h;h++)for(k=0;8>k;k++)m=8*a+h,p=8*b+k,t=l.get(m,p),q.set(m,p,t<=c),f&&r.set(m,p,!(t<=c))}f=f?{binarized:q,inverted:r}:{binarized:q};let {binarized:z,inverted:y}=f;(f=V(d?
y:z))||"attemptBoth"!==e.inversionAttempts&&"invertFirst"!==e.inversionAttempts||(f=V(d?z:y));return f}X.default=X;let Y="dontInvert",Z={red:77,green:150,blue:29,useIntegerApproximation:!0};
self.onmessage=a=>{let b=a.data.id,c=a.data.data;switch(a.data.type){case "decode":(a=X(c.data,c.width,c.height,{inversionAttempts:Y,greyScaleWeights:Z}))?self.postMessage({id:b,type:"qrResult",data:a.data,cornerPoints:[a.location.topLeftCorner,a.location.topRightCorner,a.location.bottomRightCorner,a.location.bottomLeftCorner]}):self.postMessage({id:b,type:"qrResult",data:null});break;case "grayscaleWeights":Z.red=c.red;Z.green=c.green;Z.blue=c.blue;Z.useIntegerApproximation=c.useIntegerApproximation;
break;case "inversionMode":switch(c){case "original":Y="dontInvert";break;case "invert":Y="onlyInvert";break;case "both":Y="attemptBoth";break;default:throw Error("Invalid inversion mode");}break;case "close":self.close()}}
`]),{type:"application/javascript"}))//# sourceMappingURL=qr-scanner-worker.min.js.map

View File

@ -0,0 +1,32 @@
/*! qr-scanner v1.4.1 https://github.com/nimiq/qr-scanner Licensed MIT */
'use strict';(function(e,a){"object"===typeof exports&&"undefined"!==typeof module?module.exports=a():"function"===typeof define&&define.amd?define(a):(e="undefined"!==typeof globalThis?globalThis:e||self,e.QrScanner=a())})(this,function(){class e{constructor(a,b,c,d,f){this._legacyCanvasSize=e.DEFAULT_CANVAS_SIZE;this._preferredCamera="environment";this._maxScansPerSecond=25;this._lastScanTimestamp=-1;this._destroyed=this._flashOn=this._paused=this._active=!1;this.$video=a;this.$canvas=document.createElement("canvas");
c&&"object"===typeof c?this._onDecode=b:(c||d||f?console.warn("You're using a deprecated version of the QrScanner constructor which will be removed in the future"):console.warn("Note that the type of the scan result passed to onDecode will change in the future. To already switch to the new api today, you can pass returnDetailedScanResult: true."),this._legacyOnDecode=b);b="object"===typeof c?c:{};this._onDecodeError=b.onDecodeError||("function"===typeof c?c:this._onDecodeError);this._calculateScanRegion=
b.calculateScanRegion||("function"===typeof d?d:this._calculateScanRegion);this._preferredCamera=b.preferredCamera||f||this._preferredCamera;this._legacyCanvasSize="number"===typeof c?c:"number"===typeof d?d:this._legacyCanvasSize;this._maxScansPerSecond=b.maxScansPerSecond||this._maxScansPerSecond;this._onPlay=this._onPlay.bind(this);this._onLoadedMetaData=this._onLoadedMetaData.bind(this);this._onVisibilityChange=this._onVisibilityChange.bind(this);this._updateOverlay=this._updateOverlay.bind(this);
a.disablePictureInPicture=!0;a.playsInline=!0;a.muted=!0;let h=!1;a.hidden&&(a.hidden=!1,h=!0);document.body.contains(a)||(document.body.appendChild(a),h=!0);c=a.parentElement;if(b.highlightScanRegion||b.highlightCodeOutline){d=!!b.overlay;this.$overlay=b.overlay||document.createElement("div");f=this.$overlay.style;f.position="absolute";f.display="none";f.pointerEvents="none";this.$overlay.classList.add("scan-region-highlight");if(!d&&b.highlightScanRegion){this.$overlay.innerHTML='<svg class="scan-region-highlight-svg" viewBox="0 0 238 238" preserveAspectRatio="none" style="position:absolute;width:100%;height:100%;left:0;top:0;fill:none;stroke:#e9b213;stroke-width:4;stroke-linecap:round;stroke-linejoin:round"><path d="M31 2H10a8 8 0 0 0-8 8v21M207 2h21a8 8 0 0 1 8 8v21m0 176v21a8 8 0 0 1-8 8h-21m-176 0H10a8 8 0 0 1-8-8v-21"/></svg>';
try{this.$overlay.firstElementChild.animate({transform:["scale(.98)","scale(1.01)"]},{duration:400,iterations:Infinity,direction:"alternate",easing:"ease-in-out"})}catch(m){}c.insertBefore(this.$overlay,this.$video.nextSibling)}b.highlightCodeOutline&&(this.$overlay.insertAdjacentHTML("beforeend",'<svg class="code-outline-highlight" preserveAspectRatio="none" style="display:none;width:100%;height:100%;fill:none;stroke:#e9b213;stroke-width:5;stroke-dasharray:25;stroke-linecap:round;stroke-linejoin:round"><polygon/></svg>'),
this.$codeOutlineHighlight=this.$overlay.lastElementChild)}this._scanRegion=this._calculateScanRegion(a);requestAnimationFrame(()=>{let m=window.getComputedStyle(a);"none"===m.display&&(a.style.setProperty("display","block","important"),h=!0);"visible"!==m.visibility&&(a.style.setProperty("visibility","visible","important"),h=!0);h&&(console.warn("QrScanner has overwritten the video hiding style to avoid Safari stopping the playback."),a.style.opacity="0",a.style.width="0",a.style.height="0",this.$overlay&&
this.$overlay.parentElement&&this.$overlay.parentElement.removeChild(this.$overlay),delete this.$overlay,delete this.$codeOutlineHighlight);this.$overlay&&this._updateOverlay()});a.addEventListener("play",this._onPlay);a.addEventListener("loadedmetadata",this._onLoadedMetaData);document.addEventListener("visibilitychange",this._onVisibilityChange);window.addEventListener("resize",this._updateOverlay);this._qrEnginePromise=e.createQrEngine()}static set WORKER_PATH(a){console.warn("Setting QrScanner.WORKER_PATH is not required and not supported anymore. Have a look at the README for new setup instructions.")}static async hasCamera(){try{return!!(await e.listCameras(!1)).length}catch(a){return!1}}static async listCameras(a=
!1){if(!navigator.mediaDevices)return[];let b=async()=>(await navigator.mediaDevices.enumerateDevices()).filter(d=>"videoinput"===d.kind),c;try{a&&(await b()).every(d=>!d.label)&&(c=await navigator.mediaDevices.getUserMedia({audio:!1,video:!0}))}catch(d){}try{return(await b()).map((d,f)=>({id:d.deviceId,label:d.label||(0===f?"Default Camera":`Camera ${f+1}`)}))}finally{c&&(console.warn("Call listCameras after successfully starting a QR scanner to avoid creating a temporary video stream"),e._stopVideoStream(c))}}async hasFlash(){let a;
try{if(this.$video.srcObject){if(!(this.$video.srcObject instanceof MediaStream))return!1;a=this.$video.srcObject}else a=(await this._getCameraStream()).stream;return"torch"in a.getVideoTracks()[0].getSettings()}catch(b){return!1}finally{a&&a!==this.$video.srcObject&&(console.warn("Call hasFlash after successfully starting the scanner to avoid creating a temporary video stream"),e._stopVideoStream(a))}}isFlashOn(){return this._flashOn}async toggleFlash(){this._flashOn?await this.turnFlashOff():await this.turnFlashOn()}async turnFlashOn(){if(!this._flashOn&&
!this._destroyed&&(this._flashOn=!0,this._active&&!this._paused))try{if(!await this.hasFlash())throw"No flash available";await this.$video.srcObject.getVideoTracks()[0].applyConstraints({advanced:[{torch:!0}]})}catch(a){throw this._flashOn=!1,a;}}async turnFlashOff(){this._flashOn&&(this._flashOn=!1,await this._restartVideoStream())}destroy(){this.$video.removeEventListener("loadedmetadata",this._onLoadedMetaData);this.$video.removeEventListener("play",this._onPlay);document.removeEventListener("visibilitychange",
this._onVisibilityChange);window.removeEventListener("resize",this._updateOverlay);this._destroyed=!0;this._flashOn=!1;this.stop();e._postWorkerMessage(this._qrEnginePromise,"close")}async start(){if(this._destroyed)throw Error("The QR scanner can not be started as it had been destroyed.");if(!this._active||this._paused)if("https:"!==window.location.protocol&&console.warn("The camera stream is only accessible if the page is transferred via https."),this._active=!0,!document.hidden)if(this._paused=
!1,this.$video.srcObject)await this.$video.play();else try{let {stream:a,facingMode:b}=await this._getCameraStream();!this._active||this._paused?e._stopVideoStream(a):(this._setVideoMirror(b),this.$video.srcObject=a,await this.$video.play(),this._flashOn&&(this._flashOn=!1,this.turnFlashOn().catch(()=>{})))}catch(a){if(!this._paused)throw this._active=!1,a;}}stop(){this.pause();this._active=!1}async pause(a=!1){this._paused=!0;if(!this._active)return!0;this.$video.pause();this.$overlay&&(this.$overlay.style.display=
"none");let b=()=>{this.$video.srcObject instanceof MediaStream&&(e._stopVideoStream(this.$video.srcObject),this.$video.srcObject=null)};if(a)return b(),!0;await new Promise(c=>setTimeout(c,300));if(!this._paused)return!1;b();return!0}async setCamera(a){a!==this._preferredCamera&&(this._preferredCamera=a,await this._restartVideoStream())}static async scanImage(a,b,c,d,f=!1,h=!1){let m,n=!1;b&&("scanRegion"in b||"qrEngine"in b||"canvas"in b||"disallowCanvasResizing"in b||"alsoTryWithoutScanRegion"in
b||"returnDetailedScanResult"in b)?(m=b.scanRegion,c=b.qrEngine,d=b.canvas,f=b.disallowCanvasResizing||!1,h=b.alsoTryWithoutScanRegion||!1,n=!0):b||c||d||f||h?console.warn("You're using a deprecated api for scanImage which will be removed in the future."):console.warn("Note that the return type of scanImage will change in the future. To already switch to the new api today, you can pass returnDetailedScanResult: true.");b=!!c;try{let p,k;[c,p]=await Promise.all([c||e.createQrEngine(),e._loadImage(a)]);
[d,k]=e._drawToCanvas(p,m,d,f);let q;if(c instanceof Worker){let g=c;b||e._postWorkerMessageSync(g,"inversionMode","both");q=await new Promise((l,v)=>{let w,u,r,y=-1;u=t=>{t.data.id===y&&(g.removeEventListener("message",u),g.removeEventListener("error",r),clearTimeout(w),null!==t.data.data?l({data:t.data.data,cornerPoints:e._convertPoints(t.data.cornerPoints,m)}):v(e.NO_QR_CODE_FOUND))};r=t=>{g.removeEventListener("message",u);g.removeEventListener("error",r);clearTimeout(w);v("Scanner error: "+(t?
t.message||t:"Unknown Error"))};g.addEventListener("message",u);g.addEventListener("error",r);w=setTimeout(()=>r("timeout"),1E4);let x=k.getImageData(0,0,d.width,d.height);y=e._postWorkerMessageSync(g,"decode",x,[x.data.buffer])})}else q=await Promise.race([new Promise((g,l)=>window.setTimeout(()=>l("Scanner error: timeout"),1E4)),(async()=>{try{var [g]=await c.detect(d);if(!g)throw e.NO_QR_CODE_FOUND;return{data:g.rawValue,cornerPoints:e._convertPoints(g.cornerPoints,m)}}catch(l){g=l.message||l;
if(/not implemented|service unavailable/.test(g))return e._disableBarcodeDetector=!0,e.scanImage(a,{scanRegion:m,canvas:d,disallowCanvasResizing:f,alsoTryWithoutScanRegion:h});throw`Scanner error: ${g}`;}})()]);return n?q:q.data}catch(p){if(!m||!h)throw p;let k=await e.scanImage(a,{qrEngine:c,canvas:d,disallowCanvasResizing:f});return n?k:k.data}finally{b||e._postWorkerMessage(c,"close")}}setGrayscaleWeights(a,b,c,d=!0){e._postWorkerMessage(this._qrEnginePromise,"grayscaleWeights",{red:a,green:b,
blue:c,useIntegerApproximation:d})}setInversionMode(a){e._postWorkerMessage(this._qrEnginePromise,"inversionMode",a)}static async createQrEngine(a){a&&console.warn("Specifying a worker path is not required and not supported anymore.");return!e._disableBarcodeDetector&&"BarcodeDetector"in window&&BarcodeDetector.getSupportedFormats&&(await BarcodeDetector.getSupportedFormats()).includes("qr_code")?new BarcodeDetector({formats:["qr_code"]}):import("./qr-scanner-worker.min.js").then(b=>b.createWorker())}_onPlay(){this._scanRegion=
this._calculateScanRegion(this.$video);this._updateOverlay();this.$overlay&&(this.$overlay.style.display="");this._scanFrame()}_onLoadedMetaData(){this._scanRegion=this._calculateScanRegion(this.$video);this._updateOverlay()}_onVisibilityChange(){document.hidden?this.pause():this._active&&this.start()}_calculateScanRegion(a){let b=Math.round(2/3*Math.min(a.videoWidth,a.videoHeight));return{x:Math.round((a.videoWidth-b)/2),y:Math.round((a.videoHeight-b)/2),width:b,height:b,downScaledWidth:this._legacyCanvasSize,
downScaledHeight:this._legacyCanvasSize}}_updateOverlay(){requestAnimationFrame(()=>{if(this.$overlay){var a=this.$video,b=a.videoWidth,c=a.videoHeight,d=a.offsetWidth,f=a.offsetHeight,h=a.offsetLeft,m=a.offsetTop,n=window.getComputedStyle(a),p=n.objectFit,k=b/c,q=d/f;switch(p){case "none":var g=b;var l=c;break;case "fill":g=d;l=f;break;default:("cover"===p?k>q:k<q)?(l=f,g=l*k):(g=d,l=g/k),"scale-down"===p&&(g=Math.min(g,b),l=Math.min(l,c))}var [v,w]=n.objectPosition.split(" ").map((r,y)=>{const x=
parseFloat(r);return r.endsWith("%")?(y?f-l:d-g)*x/100:x});n=this._scanRegion.width||b;q=this._scanRegion.height||c;p=this._scanRegion.x||0;var u=this._scanRegion.y||0;k=this.$overlay.style;k.width=`${n/b*g}px`;k.height=`${q/c*l}px`;k.top=`${m+w+u/c*l}px`;c=/scaleX\(-1\)/.test(a.style.transform);k.left=`${h+(c?d-v-g:v)+(c?b-p-n:p)/b*g}px`;k.transform=a.style.transform}})}static _convertPoints(a,b){if(!b)return a;let c=b.x||0,d=b.y||0,f=b.width&&b.downScaledWidth?b.width/b.downScaledWidth:1;b=b.height&&
b.downScaledHeight?b.height/b.downScaledHeight:1;for(let h of a)h.x=h.x*f+c,h.y=h.y*b+d;return a}_scanFrame(){!this._active||this.$video.paused||this.$video.ended||("requestVideoFrameCallback"in this.$video?this.$video.requestVideoFrameCallback.bind(this.$video):requestAnimationFrame)(async()=>{if(!(1>=this.$video.readyState)){var a=Date.now()-this._lastScanTimestamp,b=1E3/this._maxScansPerSecond;a<b&&await new Promise(d=>setTimeout(d,b-a));this._lastScanTimestamp=Date.now();try{var c=await e.scanImage(this.$video,
{scanRegion:this._scanRegion,qrEngine:this._qrEnginePromise,canvas:this.$canvas})}catch(d){if(!this._active)return;this._onDecodeError(d)}!e._disableBarcodeDetector||await this._qrEnginePromise instanceof Worker||(this._qrEnginePromise=e.createQrEngine());c?(this._onDecode?this._onDecode(c):this._legacyOnDecode&&this._legacyOnDecode(c.data),this.$codeOutlineHighlight&&(clearTimeout(this._codeOutlineHighlightRemovalTimeout),this._codeOutlineHighlightRemovalTimeout=void 0,this.$codeOutlineHighlight.setAttribute("viewBox",
`${this._scanRegion.x||0} `+`${this._scanRegion.y||0} `+`${this._scanRegion.width||this.$video.videoWidth} `+`${this._scanRegion.height||this.$video.videoHeight}`),this.$codeOutlineHighlight.firstElementChild.setAttribute("points",c.cornerPoints.map(({x:d,y:f})=>`${d},${f}`).join(" ")),this.$codeOutlineHighlight.style.display="")):this.$codeOutlineHighlight&&!this._codeOutlineHighlightRemovalTimeout&&(this._codeOutlineHighlightRemovalTimeout=setTimeout(()=>this.$codeOutlineHighlight.style.display=
"none",100))}this._scanFrame()})}_onDecodeError(a){a!==e.NO_QR_CODE_FOUND&&console.log(a)}async _getCameraStream(){if(!navigator.mediaDevices)throw"Camera not found.";let a=/^(environment|user)$/.test(this._preferredCamera)?"facingMode":"deviceId",b=[{width:{min:1024}},{width:{min:768}},{}],c=b.map(d=>Object.assign({},d,{[a]:{exact:this._preferredCamera}}));for(let d of[...c,...b])try{let f=await navigator.mediaDevices.getUserMedia({video:d,audio:!1}),h=this._getFacingMode(f)||(d.facingMode?this._preferredCamera:
"environment"===this._preferredCamera?"user":"environment");return{stream:f,facingMode:h}}catch(f){}throw"Camera not found.";}async _restartVideoStream(){let a=this._paused;await this.pause(!0)&&!a&&this._active&&await this.start()}static _stopVideoStream(a){for(let b of a.getTracks())b.stop(),a.removeTrack(b)}_setVideoMirror(a){this.$video.style.transform="scaleX("+("user"===a?-1:1)+")"}_getFacingMode(a){return(a=a.getVideoTracks()[0])?/rear|back|environment/i.test(a.label)?"environment":/front|user|face/i.test(a.label)?
"user":null:null}static _drawToCanvas(a,b,c,d=!1){c=c||document.createElement("canvas");let f=b&&b.x?b.x:0,h=b&&b.y?b.y:0,m=b&&b.width?b.width:a.videoWidth||a.width,n=b&&b.height?b.height:a.videoHeight||a.height;d||(d=b&&b.downScaledWidth?b.downScaledWidth:m,b=b&&b.downScaledHeight?b.downScaledHeight:n,c.width!==d&&(c.width=d),c.height!==b&&(c.height=b));b=c.getContext("2d",{alpha:!1});b.imageSmoothingEnabled=!1;b.drawImage(a,f,h,m,n,0,0,c.width,c.height);return[c,b]}static async _loadImage(a){if(a instanceof
Image)return await e._awaitImageLoad(a),a;if(a instanceof HTMLVideoElement||a instanceof HTMLCanvasElement||a instanceof SVGImageElement||"OffscreenCanvas"in window&&a instanceof OffscreenCanvas||"ImageBitmap"in window&&a instanceof ImageBitmap)return a;if(a instanceof File||a instanceof Blob||a instanceof URL||"string"===typeof a){let b=new Image;b.src=a instanceof File||a instanceof Blob?URL.createObjectURL(a):a.toString();try{return await e._awaitImageLoad(b),b}finally{(a instanceof File||a instanceof
Blob)&&URL.revokeObjectURL(b.src)}}else throw"Unsupported image type.";}static async _awaitImageLoad(a){a.complete&&0!==a.naturalWidth||await new Promise((b,c)=>{let d=f=>{a.removeEventListener("load",d);a.removeEventListener("error",d);f instanceof ErrorEvent?c("Image load error"):b()};a.addEventListener("load",d);a.addEventListener("error",d)})}static async _postWorkerMessage(a,b,c,d){return e._postWorkerMessageSync(await a,b,c,d)}static _postWorkerMessageSync(a,b,c,d){if(!(a instanceof Worker))return-1;
let f=e._workerMessageId++;a.postMessage({id:f,type:b,data:c},d);return f}}e.DEFAULT_CANVAS_SIZE=400;e.NO_QR_CODE_FOUND="No QR code found";e._disableBarcodeDetector=!1;e._workerMessageId=0;return e})
//# sourceMappingURL=qr-scanner.umd.min.js.map

View File

@ -3,7 +3,7 @@ Provides system status functionality checks.
""" """
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.utils import timezone from django.utils import timezone
import logging import logging

View File

@ -1,4 +1,4 @@
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
class StatusCode: class StatusCode:

View File

@ -72,7 +72,7 @@ class ViewTests(TestCase):
""" """
# Change this number as more javascript files are added to the index page # Change this number as more javascript files are added to the index page
N_SCRIPT_FILES = 39 N_SCRIPT_FILES = 40
content = self.get_index_page() content = self.get_index_page()

View File

@ -1,4 +1,3 @@
import json import json
from test.support import EnvironmentVarGuard from test.support import EnvironmentVarGuard
@ -186,7 +185,7 @@ class TestDownloadFile(TestCase):
def test_download(self): def test_download(self):
helpers.DownloadFile("hello world", "out.txt") helpers.DownloadFile("hello world", "out.txt")
helpers.DownloadFile(bytes("hello world".encode("utf8")), "out.bin") helpers.DownloadFile(bytes(b"hello world"), "out.bin")
class TestMPTT(TestCase): class TestMPTT(TestCase):

View File

@ -4,8 +4,7 @@ Top-level URL lookup for InvenTree application.
Passes URL lookup downstream to each app as required. Passes URL lookup downstream to each app as required.
""" """
from django.conf.urls import url, include from django.urls import include, path, re_path
from django.urls import path
from django.contrib import admin from django.contrib import admin
from company.urls import company_urls from company.urls import company_urls
@ -56,144 +55,144 @@ apipatterns = []
if settings.PLUGINS_ENABLED: if settings.PLUGINS_ENABLED:
apipatterns.append( apipatterns.append(
url(r'^plugin/', include(plugin_api_urls)) re_path(r'^plugin/', include(plugin_api_urls))
) )
apipatterns += [ apipatterns += [
url(r'^barcode/', include(barcode_api_urls)), re_path(r'^barcode/', include(barcode_api_urls)),
url(r'^settings/', include(settings_api_urls)), re_path(r'^settings/', include(settings_api_urls)),
url(r'^part/', include(part_api_urls)), re_path(r'^part/', include(part_api_urls)),
url(r'^bom/', include(bom_api_urls)), re_path(r'^bom/', include(bom_api_urls)),
url(r'^company/', include(company_api_urls)), re_path(r'^company/', include(company_api_urls)),
url(r'^stock/', include(stock_api_urls)), re_path(r'^stock/', include(stock_api_urls)),
url(r'^build/', include(build_api_urls)), re_path(r'^build/', include(build_api_urls)),
url(r'^order/', include(order_api_urls)), re_path(r'^order/', include(order_api_urls)),
url(r'^label/', include(label_api_urls)), re_path(r'^label/', include(label_api_urls)),
url(r'^report/', include(report_api_urls)), re_path(r'^report/', include(report_api_urls)),
# User URLs # User URLs
url(r'^user/', include(user_urls)), re_path(r'^user/', include(user_urls)),
# Plugin endpoints # Plugin endpoints
url(r'^action/', ActionPluginView.as_view(), name='api-action-plugin'), re_path(r'^action/', ActionPluginView.as_view(), name='api-action-plugin'),
# Webhook enpoint # Webhook enpoint
path('', include(common_api_urls)), path('', include(common_api_urls)),
# InvenTree information endpoint # InvenTree information endpoint
url(r'^$', InfoView.as_view(), name='api-inventree-info'), path('', InfoView.as_view(), name='api-inventree-info'),
# Unknown endpoint # Unknown endpoint
url(r'^.*$', NotFoundView.as_view(), name='api-404'), re_path(r'^.*$', NotFoundView.as_view(), name='api-404'),
] ]
settings_urls = [ settings_urls = [
url(r'^i18n/?', include('django.conf.urls.i18n')), re_path(r'^i18n/?', include('django.conf.urls.i18n')),
url(r'^appearance/?', AppearanceSelectView.as_view(), name='settings-appearance'), re_path(r'^appearance/?', AppearanceSelectView.as_view(), name='settings-appearance'),
url(r'^currencies-refresh/', CurrencyRefreshView.as_view(), name='settings-currencies-refresh'), re_path(r'^currencies-refresh/', CurrencyRefreshView.as_view(), name='settings-currencies-refresh'),
url(r'^category/', SettingCategorySelectView.as_view(), name='settings-category'), re_path(r'^category/', SettingCategorySelectView.as_view(), name='settings-category'),
# Catch any other urls # Catch any other urls
url(r'^.*$', SettingsView.as_view(template_name='InvenTree/settings/settings.html'), name='settings'), re_path(r'^.*$', SettingsView.as_view(template_name='InvenTree/settings/settings.html'), name='settings'),
] ]
notifications_urls = [ notifications_urls = [
# Catch any other urls # Catch any other urls
url(r'^.*$', NotificationsView.as_view(), name='notifications'), re_path(r'^.*$', NotificationsView.as_view(), name='notifications'),
] ]
# These javascript files are served "dynamically" - i.e. rendered on demand # These javascript files are served "dynamically" - i.e. rendered on demand
dynamic_javascript_urls = [ dynamic_javascript_urls = [
url(r'^calendar.js', DynamicJsView.as_view(template_name='js/dynamic/calendar.js'), name='calendar.js'), re_path(r'^calendar.js', DynamicJsView.as_view(template_name='js/dynamic/calendar.js'), name='calendar.js'),
url(r'^nav.js', DynamicJsView.as_view(template_name='js/dynamic/nav.js'), name='nav.js'), re_path(r'^nav.js', DynamicJsView.as_view(template_name='js/dynamic/nav.js'), name='nav.js'),
url(r'^settings.js', DynamicJsView.as_view(template_name='js/dynamic/settings.js'), name='settings.js'), re_path(r'^settings.js', DynamicJsView.as_view(template_name='js/dynamic/settings.js'), name='settings.js'),
] ]
# These javascript files are pased through the Django translation layer # These javascript files are pased through the Django translation layer
translated_javascript_urls = [ translated_javascript_urls = [
url(r'^api.js', DynamicJsView.as_view(template_name='js/translated/api.js'), name='api.js'), re_path(r'^api.js', DynamicJsView.as_view(template_name='js/translated/api.js'), name='api.js'),
url(r'^attachment.js', DynamicJsView.as_view(template_name='js/translated/attachment.js'), name='attachment.js'), re_path(r'^attachment.js', DynamicJsView.as_view(template_name='js/translated/attachment.js'), name='attachment.js'),
url(r'^barcode.js', DynamicJsView.as_view(template_name='js/translated/barcode.js'), name='barcode.js'), re_path(r'^barcode.js', DynamicJsView.as_view(template_name='js/translated/barcode.js'), name='barcode.js'),
url(r'^bom.js', DynamicJsView.as_view(template_name='js/translated/bom.js'), name='bom.js'), re_path(r'^bom.js', DynamicJsView.as_view(template_name='js/translated/bom.js'), name='bom.js'),
url(r'^build.js', DynamicJsView.as_view(template_name='js/translated/build.js'), name='build.js'), re_path(r'^build.js', DynamicJsView.as_view(template_name='js/translated/build.js'), name='build.js'),
url(r'^company.js', DynamicJsView.as_view(template_name='js/translated/company.js'), name='company.js'), re_path(r'^company.js', DynamicJsView.as_view(template_name='js/translated/company.js'), name='company.js'),
url(r'^filters.js', DynamicJsView.as_view(template_name='js/translated/filters.js'), name='filters.js'), re_path(r'^filters.js', DynamicJsView.as_view(template_name='js/translated/filters.js'), name='filters.js'),
url(r'^forms.js', DynamicJsView.as_view(template_name='js/translated/forms.js'), name='forms.js'), re_path(r'^forms.js', DynamicJsView.as_view(template_name='js/translated/forms.js'), name='forms.js'),
url(r'^helpers.js', DynamicJsView.as_view(template_name='js/translated/helpers.js'), name='helpers.js'), re_path(r'^helpers.js', DynamicJsView.as_view(template_name='js/translated/helpers.js'), name='helpers.js'),
url(r'^label.js', DynamicJsView.as_view(template_name='js/translated/label.js'), name='label.js'), re_path(r'^label.js', DynamicJsView.as_view(template_name='js/translated/label.js'), name='label.js'),
url(r'^model_renderers.js', DynamicJsView.as_view(template_name='js/translated/model_renderers.js'), name='model_renderers.js'), re_path(r'^model_renderers.js', DynamicJsView.as_view(template_name='js/translated/model_renderers.js'), name='model_renderers.js'),
url(r'^modals.js', DynamicJsView.as_view(template_name='js/translated/modals.js'), name='modals.js'), re_path(r'^modals.js', DynamicJsView.as_view(template_name='js/translated/modals.js'), name='modals.js'),
url(r'^order.js', DynamicJsView.as_view(template_name='js/translated/order.js'), name='order.js'), re_path(r'^order.js', DynamicJsView.as_view(template_name='js/translated/order.js'), name='order.js'),
url(r'^part.js', DynamicJsView.as_view(template_name='js/translated/part.js'), name='part.js'), re_path(r'^part.js', DynamicJsView.as_view(template_name='js/translated/part.js'), name='part.js'),
url(r'^report.js', DynamicJsView.as_view(template_name='js/translated/report.js'), name='report.js'), re_path(r'^report.js', DynamicJsView.as_view(template_name='js/translated/report.js'), name='report.js'),
url(r'^search.js', DynamicJsView.as_view(template_name='js/translated/search.js'), name='search.js'), re_path(r'^search.js', DynamicJsView.as_view(template_name='js/translated/search.js'), name='search.js'),
url(r'^stock.js', DynamicJsView.as_view(template_name='js/translated/stock.js'), name='stock.js'), re_path(r'^stock.js', DynamicJsView.as_view(template_name='js/translated/stock.js'), name='stock.js'),
url(r'^plugin.js', DynamicJsView.as_view(template_name='js/translated/plugin.js'), name='plugin.js'), re_path(r'^plugin.js', DynamicJsView.as_view(template_name='js/translated/plugin.js'), name='plugin.js'),
url(r'^tables.js', DynamicJsView.as_view(template_name='js/translated/tables.js'), name='tables.js'), re_path(r'^tables.js', DynamicJsView.as_view(template_name='js/translated/tables.js'), name='tables.js'),
url(r'^table_filters.js', DynamicJsView.as_view(template_name='js/translated/table_filters.js'), name='table_filters.js'), re_path(r'^table_filters.js', DynamicJsView.as_view(template_name='js/translated/table_filters.js'), name='table_filters.js'),
url(r'^notification.js', DynamicJsView.as_view(template_name='js/translated/notification.js'), name='notification.js'), re_path(r'^notification.js', DynamicJsView.as_view(template_name='js/translated/notification.js'), name='notification.js'),
] ]
backendpatterns = [ backendpatterns = [
# "Dynamic" javascript files which are rendered using InvenTree templating. # "Dynamic" javascript files which are rendered using InvenTree templating.
url(r'^js/dynamic/', include(dynamic_javascript_urls)), re_path(r'^js/dynamic/', include(dynamic_javascript_urls)),
url(r'^js/i18n/', include(translated_javascript_urls)), re_path(r'^js/i18n/', include(translated_javascript_urls)),
url(r'^auth/', include('rest_framework.urls', namespace='rest_framework')), re_path(r'^auth/', include('rest_framework.urls', namespace='rest_framework')),
url(r'^auth/?', auth_request), re_path(r'^auth/?', auth_request),
url(r'^api/', include(apipatterns)), re_path(r'^api/', include(apipatterns)),
url(r'^api-doc/', include_docs_urls(title='InvenTree API')), re_path(r'^api-doc/', include_docs_urls(title='InvenTree API')),
# 3rd party endpoints # 3rd party endpoints
url(r'^markdownx/', include('markdownx.urls')), re_path(r'^markdownx/', include('markdownx.urls')),
] ]
frontendpatterns = [ frontendpatterns = [
url(r'^part/', include(part_urls)), re_path(r'^part/', include(part_urls)),
url(r'^manufacturer-part/', include(manufacturer_part_urls)), re_path(r'^manufacturer-part/', include(manufacturer_part_urls)),
url(r'^supplier-part/', include(supplier_part_urls)), re_path(r'^supplier-part/', include(supplier_part_urls)),
url(r'^common/', include(common_urls)), re_path(r'^common/', include(common_urls)),
url(r'^stock/', include(stock_urls)), re_path(r'^stock/', include(stock_urls)),
url(r'^company/', include(company_urls)), re_path(r'^company/', include(company_urls)),
url(r'^order/', include(order_urls)), re_path(r'^order/', include(order_urls)),
url(r'^build/', include(build_urls)), re_path(r'^build/', include(build_urls)),
url(r'^settings/', include(settings_urls)), re_path(r'^settings/', include(settings_urls)),
url(r'^notifications/', include(notifications_urls)), re_path(r'^notifications/', include(notifications_urls)),
url(r'^edit-user/', EditUserView.as_view(), name='edit-user'), re_path(r'^edit-user/', EditUserView.as_view(), name='edit-user'),
url(r'^set-password/', SetPasswordView.as_view(), name='set-password'), re_path(r'^set-password/', SetPasswordView.as_view(), name='set-password'),
url(r'^index/', IndexView.as_view(), name='index'), re_path(r'^index/', IndexView.as_view(), name='index'),
url(r'^search/', SearchView.as_view(), name='search'), re_path(r'^search/', SearchView.as_view(), name='search'),
url(r'^stats/', DatabaseStatsView.as_view(), name='stats'), re_path(r'^stats/', DatabaseStatsView.as_view(), name='stats'),
# admin sites # admin sites
url(f'^{settings.INVENTREE_ADMIN_URL}/error_log/', include('error_report.urls')), re_path(f'^{settings.INVENTREE_ADMIN_URL}/error_log/', include('error_report.urls')),
url(f'^{settings.INVENTREE_ADMIN_URL}/shell/', include('django_admin_shell.urls')), re_path(f'^{settings.INVENTREE_ADMIN_URL}/shell/', include('django_admin_shell.urls')),
url(f'^{settings.INVENTREE_ADMIN_URL}/', admin.site.urls, name='inventree-admin'), re_path(f'^{settings.INVENTREE_ADMIN_URL}/', admin.site.urls, name='inventree-admin'),
# DB user sessions # DB user sessions
url(r'^accounts/sessions/other/delete/$', view=CustomSessionDeleteOtherView.as_view(), name='session_delete_other', ), path('accounts/sessions/other/delete/', view=CustomSessionDeleteOtherView.as_view(), name='session_delete_other', ),
url(r'^accounts/sessions/(?P<pk>\w+)/delete/$', view=CustomSessionDeleteView.as_view(), name='session_delete', ), re_path(r'^accounts/sessions/(?P<pk>\w+)/delete/$', view=CustomSessionDeleteView.as_view(), name='session_delete', ),
# Single Sign On / allauth # Single Sign On / allauth
# overrides of urlpatterns # overrides of urlpatterns
url(r'^accounts/email/', CustomEmailView.as_view(), name='account_email'), re_path(r'^accounts/email/', CustomEmailView.as_view(), name='account_email'),
url(r'^accounts/social/connections/', CustomConnectionsView.as_view(), name='socialaccount_connections'), re_path(r'^accounts/social/connections/', CustomConnectionsView.as_view(), name='socialaccount_connections'),
url(r"^accounts/password/reset/key/(?P<uidb36>[0-9A-Za-z]+)-(?P<key>.+)/$", CustomPasswordResetFromKeyView.as_view(), name="account_reset_password_from_key"), re_path(r"^accounts/password/reset/key/(?P<uidb36>[0-9A-Za-z]+)-(?P<key>.+)/$", CustomPasswordResetFromKeyView.as_view(), name="account_reset_password_from_key"),
url(r'^accounts/', include('allauth_2fa.urls')), # MFA support re_path(r'^accounts/', include('allauth_2fa.urls')), # MFA support
url(r'^accounts/', include('allauth.urls')), # included urlpatterns re_path(r'^accounts/', include('allauth.urls')), # included urlpatterns
] ]
# Append custom plugin URLs (if plugin support is enabled) # Append custom plugin URLs (if plugin support is enabled)
@ -201,8 +200,8 @@ if settings.PLUGINS_ENABLED:
frontendpatterns.append(get_plugin_urls()) frontendpatterns.append(get_plugin_urls())
urlpatterns = [ urlpatterns = [
url('', include(frontendpatterns)), re_path('', include(frontendpatterns)),
url('', include(backendpatterns)), re_path('', include(backendpatterns)),
] ]
# Server running in "DEBUG" mode? # Server running in "DEBUG" mode?
@ -221,4 +220,4 @@ if settings.DEBUG:
] + urlpatterns ] + urlpatterns
# Send any unknown URLs to the parts page # Send any unknown URLs to the parts page
urlpatterns += [url(r'^.*$', RedirectView.as_view(url='/index/', permanent=False), name='index')] urlpatterns += [re_path(r'^.*$', RedirectView.as_view(url='/index/', permanent=False), name='index')]

View File

@ -1,8 +1,8 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from django.urls import reverse from django.urls import reverse
from django.conf.urls import url from django.urls import path, re_path
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
from rest_framework.exceptions import ValidationError from rest_framework.exceptions import ValidationError
from rest_framework import permissions from rest_framework import permissions
@ -240,8 +240,8 @@ class BarcodeAssign(APIView):
barcode_api_urls = [ barcode_api_urls = [
url(r'^link/$', BarcodeAssign.as_view(), name='api-barcode-link'), path('link/', BarcodeAssign.as_view(), name='api-barcode-link'),
# Catch-all performs barcode 'scan' # Catch-all performs barcode 'scan'
url(r'^.*$', BarcodeScan.as_view(), name='api-barcode-scan'), re_path(r'^.*$', BarcodeScan.as_view(), name='api-barcode-scan'),
] ]

View File

@ -5,7 +5,7 @@ JSON API for the Build app
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import unicode_literals from __future__ import unicode_literals
from django.conf.urls import url, include from django.urls import include, re_path
from rest_framework import filters, generics from rest_framework import filters, generics
@ -508,29 +508,29 @@ class BuildAttachmentDetail(generics.RetrieveUpdateDestroyAPIView, AttachmentMix
build_api_urls = [ build_api_urls = [
# Attachments # Attachments
url(r'^attachment/', include([ re_path(r'^attachment/', include([
url(r'^(?P<pk>\d+)/', BuildAttachmentDetail.as_view(), name='api-build-attachment-detail'), re_path(r'^(?P<pk>\d+)/', BuildAttachmentDetail.as_view(), name='api-build-attachment-detail'),
url(r'^.*$', BuildAttachmentList.as_view(), name='api-build-attachment-list'), re_path(r'^.*$', BuildAttachmentList.as_view(), name='api-build-attachment-list'),
])), ])),
# Build Items # Build Items
url(r'^item/', include([ re_path(r'^item/', include([
url(r'^(?P<pk>\d+)/', BuildItemDetail.as_view(), name='api-build-item-detail'), re_path(r'^(?P<pk>\d+)/', BuildItemDetail.as_view(), name='api-build-item-detail'),
url(r'^.*$', BuildItemList.as_view(), name='api-build-item-list'), re_path(r'^.*$', BuildItemList.as_view(), name='api-build-item-list'),
])), ])),
# Build Detail # Build Detail
url(r'^(?P<pk>\d+)/', include([ re_path(r'^(?P<pk>\d+)/', include([
url(r'^allocate/', BuildAllocate.as_view(), name='api-build-allocate'), re_path(r'^allocate/', BuildAllocate.as_view(), name='api-build-allocate'),
url(r'^auto-allocate/', BuildAutoAllocate.as_view(), name='api-build-auto-allocate'), re_path(r'^auto-allocate/', BuildAutoAllocate.as_view(), name='api-build-auto-allocate'),
url(r'^complete/', BuildOutputComplete.as_view(), name='api-build-output-complete'), re_path(r'^complete/', BuildOutputComplete.as_view(), name='api-build-output-complete'),
url(r'^create-output/', BuildOutputCreate.as_view(), name='api-build-output-create'), re_path(r'^create-output/', BuildOutputCreate.as_view(), name='api-build-output-create'),
url(r'^delete-outputs/', BuildOutputDelete.as_view(), name='api-build-output-delete'), re_path(r'^delete-outputs/', BuildOutputDelete.as_view(), name='api-build-output-delete'),
url(r'^finish/', BuildFinish.as_view(), name='api-build-finish'), re_path(r'^finish/', BuildFinish.as_view(), name='api-build-finish'),
url(r'^unallocate/', BuildUnallocate.as_view(), name='api-build-unallocate'), re_path(r'^unallocate/', BuildUnallocate.as_view(), name='api-build-unallocate'),
url(r'^.*$', BuildDetail.as_view(), name='api-build-detail'), re_path(r'^.*$', BuildDetail.as_view(), name='api-build-detail'),
])), ])),
# Build List # Build List
url(r'^.*$', BuildList.as_view(), name='api-build-list'), re_path(r'^.*$', BuildList.as_view(), name='api-build-list'),
] ]

View File

@ -6,7 +6,7 @@ Django Forms for interacting with Build objects
from __future__ import unicode_literals from __future__ import unicode_literals
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django import forms from django import forms
from InvenTree.forms import HelperForm from InvenTree.forms import HelperForm

View File

@ -17,8 +17,6 @@ def assign_bom_items(apps, schema_editor):
BuildItem = apps.get_model('build', 'builditem') BuildItem = apps.get_model('build', 'builditem')
BomItem = apps.get_model('part', 'bomitem') BomItem = apps.get_model('part', 'bomitem')
Part = apps.get_model('part', 'part') Part = apps.get_model('part', 'part')
logger.info("Assigning BomItems to existing BuildItem objects")
count_valid = 0 count_valid = 0
count_total = 0 count_total = 0
@ -29,6 +27,10 @@ def assign_bom_items(apps, schema_editor):
# Note: Before this migration, variant stock assignment was not allowed, # Note: Before this migration, variant stock assignment was not allowed,
# so BomItem lookup should be pretty easy # so BomItem lookup should be pretty easy
if count_total == 0:
# First time around
logger.info("Assigning BomItems to existing BuildItem objects")
count_total += 1 count_total += 1
try: try:

View File

@ -18,7 +18,7 @@ from django.db.models.functions import Coalesce
from django.db.models.signals import post_save from django.db.models.signals import post_save
from django.dispatch.dispatcher import receiver from django.dispatch.dispatcher import receiver
from django.urls import reverse from django.urls import reverse
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
from markdownx.models import MarkdownxField from markdownx.models import MarkdownxField
@ -53,7 +53,7 @@ def get_next_build_number():
build = Build.objects.exclude(reference=None).last() build = Build.objects.exclude(reference=None).last()
attempts = set([build.reference]) attempts = {build.reference}
reference = build.reference reference = build.reference

View File

@ -7,7 +7,7 @@ from __future__ import unicode_literals
from django.db import transaction from django.db import transaction
from django.core.exceptions import ValidationError as DjangoValidationError from django.core.exceptions import ValidationError as DjangoValidationError
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.db.models import Case, When, Value from django.db.models import Case, When, Value
from django.db.models import BooleanField from django.db.models import BooleanField

View File

@ -4,7 +4,7 @@ from __future__ import unicode_literals
from decimal import Decimal from decimal import Decimal
import logging import logging
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.template.loader import render_to_string from django.template.loader import render_to_string
from allauth.account.models import EmailAddress from allauth.account.models import EmailAddress

View File

@ -2,20 +2,20 @@
URL lookup for Build app URL lookup for Build app
""" """
from django.conf.urls import url, include from django.urls import include, re_path
from . import views from . import views
build_detail_urls = [ build_detail_urls = [
url(r'^cancel/', views.BuildCancel.as_view(), name='build-cancel'), re_path(r'^cancel/', views.BuildCancel.as_view(), name='build-cancel'),
url(r'^delete/', views.BuildDelete.as_view(), name='build-delete'), re_path(r'^delete/', views.BuildDelete.as_view(), name='build-delete'),
url(r'^.*$', views.BuildDetail.as_view(), name='build-detail'), re_path(r'^.*$', views.BuildDetail.as_view(), name='build-detail'),
] ]
build_urls = [ build_urls = [
url(r'^(?P<pk>\d+)/', include(build_detail_urls)), re_path(r'^(?P<pk>\d+)/', include(build_detail_urls)),
url(r'.*$', views.BuildIndex.as_view(), name='build-index'), re_path(r'.*$', views.BuildIndex.as_view(), name='build-index'),
] ]

View File

@ -5,7 +5,7 @@ Django views for interacting with Build objects
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import unicode_literals from __future__ import unicode_literals
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views.generic import DetailView, ListView from django.views.generic import DetailView, ListView
from .models import Build from .models import Build

View File

@ -11,7 +11,7 @@ from django.http.response import HttpResponse
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.urls import path from django.urls import path
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from django.conf.urls import url, include from django.urls import include, re_path
from rest_framework.views import APIView from rest_framework.views import APIView
from rest_framework.response import Response from rest_framework.response import Response
@ -336,21 +336,21 @@ class NotificationReadAll(generics.RetrieveAPIView):
settings_api_urls = [ settings_api_urls = [
# User settings # User settings
url(r'^user/', include([ re_path(r'^user/', include([
# User Settings Detail # User Settings Detail
url(r'^(?P<pk>\d+)/', UserSettingsDetail.as_view(), name='api-user-setting-detail'), re_path(r'^(?P<pk>\d+)/', UserSettingsDetail.as_view(), name='api-user-setting-detail'),
# User Settings List # User Settings List
url(r'^.*$', UserSettingsList.as_view(), name='api-user-setting-list'), re_path(r'^.*$', UserSettingsList.as_view(), name='api-user-setting-list'),
])), ])),
# Global settings # Global settings
url(r'^global/', include([ re_path(r'^global/', include([
# Global Settings Detail # Global Settings Detail
url(r'^(?P<pk>\d+)/', GlobalSettingsDetail.as_view(), name='api-global-setting-detail'), re_path(r'^(?P<pk>\d+)/', GlobalSettingsDetail.as_view(), name='api-global-setting-detail'),
# Global Settings List # Global Settings List
url(r'^.*$', GlobalSettingsList.as_view(), name='api-global-setting-list'), re_path(r'^.*$', GlobalSettingsList.as_view(), name='api-global-setting-list'),
])), ])),
] ]
@ -359,18 +359,18 @@ common_api_urls = [
path('webhook/<slug:endpoint>/', WebhookView.as_view(), name='api-webhook'), path('webhook/<slug:endpoint>/', WebhookView.as_view(), name='api-webhook'),
# Notifications # Notifications
url(r'^notifications/', include([ re_path(r'^notifications/', include([
# Individual purchase order detail URLs # Individual purchase order detail URLs
url(r'^(?P<pk>\d+)/', include([ re_path(r'^(?P<pk>\d+)/', include([
url(r'^read/', NotificationRead.as_view(), name='api-notifications-read'), re_path(r'^read/', NotificationRead.as_view(), name='api-notifications-read'),
url(r'^unread/', NotificationUnread.as_view(), name='api-notifications-unread'), re_path(r'^unread/', NotificationUnread.as_view(), name='api-notifications-unread'),
url(r'.*$', NotificationDetail.as_view(), name='api-notifications-detail'), re_path(r'.*$', NotificationDetail.as_view(), name='api-notifications-detail'),
])), ])),
# Read all # Read all
url(r'^readall/', NotificationReadAll.as_view(), name='api-notifications-readall'), re_path(r'^readall/', NotificationReadAll.as_view(), name='api-notifications-readall'),
# Notification messages list # Notification messages list
url(r'^.*$', NotificationList.as_view(), name='api-notifications-list'), re_path(r'^.*$', NotificationList.as_view(), name='api-notifications-list'),
])), ])),
] ]

View File

@ -33,7 +33,7 @@ from djmoney.contrib.exchange.exceptions import MissingRate
from rest_framework.exceptions import PermissionDenied from rest_framework.exceptions import PermissionDenied
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.core.validators import MinValueValidator, URLValidator from django.core.validators import MinValueValidator, URLValidator
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
@ -406,6 +406,13 @@ class BaseInvenTreeSetting(models.Model):
super().clean() super().clean()
# Encode as native values
if self.is_int():
self.value = self.as_int()
elif self.is_bool():
self.value = self.as_bool()
validator = self.__class__.get_setting_validator(self.key, **kwargs) validator = self.__class__.get_setting_validator(self.key, **kwargs)
if validator is not None: if validator is not None:
@ -455,7 +462,14 @@ class BaseInvenTreeSetting(models.Model):
if callable(validator): if callable(validator):
# We can accept function validators with a single argument # We can accept function validators with a single argument
validator(self.value)
if self.is_bool():
value = self.as_bool()
if self.is_int():
value = self.as_int()
validator(value)
def validate_unique(self, exclude=None, **kwargs): def validate_unique(self, exclude=None, **kwargs):
""" """
@ -629,6 +643,10 @@ class BaseInvenTreeSetting(models.Model):
return setting.get('protected', False) return setting.get('protected', False)
@property
def protected(self):
return self.__class__.is_protected(self.key)
def settings_group_options(): def settings_group_options():
""" """
@ -1011,48 +1029,56 @@ class InvenTreeSetting(BaseInvenTreeSetting):
'default': True, 'default': True,
'validator': bool, 'validator': bool,
}, },
'LOGIN_ENABLE_REG': { 'LOGIN_ENABLE_REG': {
'name': _('Enable registration'), 'name': _('Enable registration'),
'description': _('Enable self-registration for users on the login pages'), 'description': _('Enable self-registration for users on the login pages'),
'default': False, 'default': False,
'validator': bool, 'validator': bool,
}, },
'LOGIN_ENABLE_SSO': { 'LOGIN_ENABLE_SSO': {
'name': _('Enable SSO'), 'name': _('Enable SSO'),
'description': _('Enable SSO on the login pages'), 'description': _('Enable SSO on the login pages'),
'default': False, 'default': False,
'validator': bool, 'validator': bool,
}, },
'LOGIN_MAIL_REQUIRED': { 'LOGIN_MAIL_REQUIRED': {
'name': _('Email required'), 'name': _('Email required'),
'description': _('Require user to supply mail on signup'), 'description': _('Require user to supply mail on signup'),
'default': False, 'default': False,
'validator': bool, 'validator': bool,
}, },
'LOGIN_SIGNUP_SSO_AUTO': { 'LOGIN_SIGNUP_SSO_AUTO': {
'name': _('Auto-fill SSO users'), 'name': _('Auto-fill SSO users'),
'description': _('Automatically fill out user-details from SSO account-data'), 'description': _('Automatically fill out user-details from SSO account-data'),
'default': True, 'default': True,
'validator': bool, 'validator': bool,
}, },
'LOGIN_SIGNUP_MAIL_TWICE': { 'LOGIN_SIGNUP_MAIL_TWICE': {
'name': _('Mail twice'), 'name': _('Mail twice'),
'description': _('On signup ask users twice for their mail'), 'description': _('On signup ask users twice for their mail'),
'default': False, 'default': False,
'validator': bool, 'validator': bool,
}, },
'LOGIN_SIGNUP_PWD_TWICE': { 'LOGIN_SIGNUP_PWD_TWICE': {
'name': _('Password twice'), 'name': _('Password twice'),
'description': _('On signup ask users twice for their password'), 'description': _('On signup ask users twice for their password'),
'default': True, 'default': True,
'validator': bool, 'validator': bool,
}, },
'SIGNUP_GROUP': { 'SIGNUP_GROUP': {
'name': _('Group on signup'), 'name': _('Group on signup'),
'description': _('Group to which new users are assigned on registration'), 'description': _('Group to which new users are assigned on registration'),
'default': '', 'default': '',
'choices': settings_group_options 'choices': settings_group_options
}, },
'LOGIN_ENFORCE_MFA': { 'LOGIN_ENFORCE_MFA': {
'name': _('Enforce MFA'), 'name': _('Enforce MFA'),
'description': _('Users must use multifactor security.'), 'description': _('Users must use multifactor security.'),
@ -1067,6 +1093,7 @@ class InvenTreeSetting(BaseInvenTreeSetting):
'validator': bool, 'validator': bool,
'requires_restart': True, 'requires_restart': True,
}, },
# Settings for plugin mixin features # Settings for plugin mixin features
'ENABLE_PLUGINS_URL': { 'ENABLE_PLUGINS_URL': {
'name': _('Enable URL integration'), 'name': _('Enable URL integration'),
@ -1075,6 +1102,7 @@ class InvenTreeSetting(BaseInvenTreeSetting):
'validator': bool, 'validator': bool,
'requires_restart': True, 'requires_restart': True,
}, },
'ENABLE_PLUGINS_NAVIGATION': { 'ENABLE_PLUGINS_NAVIGATION': {
'name': _('Enable navigation integration'), 'name': _('Enable navigation integration'),
'description': _('Enable plugins to integrate into navigation'), 'description': _('Enable plugins to integrate into navigation'),
@ -1082,6 +1110,7 @@ class InvenTreeSetting(BaseInvenTreeSetting):
'validator': bool, 'validator': bool,
'requires_restart': True, 'requires_restart': True,
}, },
'ENABLE_PLUGINS_APP': { 'ENABLE_PLUGINS_APP': {
'name': _('Enable app integration'), 'name': _('Enable app integration'),
'description': _('Enable plugins to add apps'), 'description': _('Enable plugins to add apps'),
@ -1089,6 +1118,7 @@ class InvenTreeSetting(BaseInvenTreeSetting):
'validator': bool, 'validator': bool,
'requires_restart': True, 'requires_restart': True,
}, },
'ENABLE_PLUGINS_SCHEDULE': { 'ENABLE_PLUGINS_SCHEDULE': {
'name': _('Enable schedule integration'), 'name': _('Enable schedule integration'),
'description': _('Enable plugins to run scheduled tasks'), 'description': _('Enable plugins to run scheduled tasks'),
@ -1096,6 +1126,7 @@ class InvenTreeSetting(BaseInvenTreeSetting):
'validator': bool, 'validator': bool,
'requires_restart': True, 'requires_restart': True,
}, },
'ENABLE_PLUGINS_EVENTS': { 'ENABLE_PLUGINS_EVENTS': {
'name': _('Enable event integration'), 'name': _('Enable event integration'),
'description': _('Enable plugins to respond to internal events'), 'description': _('Enable plugins to respond to internal events'),
@ -1149,18 +1180,21 @@ class InvenTreeUserSetting(BaseInvenTreeSetting):
'default': True, 'default': True,
'validator': bool, 'validator': bool,
}, },
'HOMEPAGE_CATEGORY_STARRED': { 'HOMEPAGE_CATEGORY_STARRED': {
'name': _('Show subscribed categories'), 'name': _('Show subscribed categories'),
'description': _('Show subscribed part categories on the homepage'), 'description': _('Show subscribed part categories on the homepage'),
'default': True, 'default': True,
'validator': bool, 'validator': bool,
}, },
'HOMEPAGE_PART_LATEST': { 'HOMEPAGE_PART_LATEST': {
'name': _('Show latest parts'), 'name': _('Show latest parts'),
'description': _('Show latest parts on the homepage'), 'description': _('Show latest parts on the homepage'),
'default': True, 'default': True,
'validator': bool, 'validator': bool,
}, },
'PART_RECENT_COUNT': { 'PART_RECENT_COUNT': {
'name': _('Recent Part Count'), 'name': _('Recent Part Count'),
'description': _('Number of recent parts to display on index page'), 'description': _('Number of recent parts to display on index page'),
@ -1174,78 +1208,91 @@ class InvenTreeUserSetting(BaseInvenTreeSetting):
'default': True, 'default': True,
'validator': bool, 'validator': bool,
}, },
'HOMEPAGE_STOCK_RECENT': { 'HOMEPAGE_STOCK_RECENT': {
'name': _('Show recent stock changes'), 'name': _('Show recent stock changes'),
'description': _('Show recently changed stock items on the homepage'), 'description': _('Show recently changed stock items on the homepage'),
'default': True, 'default': True,
'validator': bool, 'validator': bool,
}, },
'STOCK_RECENT_COUNT': { 'STOCK_RECENT_COUNT': {
'name': _('Recent Stock Count'), 'name': _('Recent Stock Count'),
'description': _('Number of recent stock items to display on index page'), 'description': _('Number of recent stock items to display on index page'),
'default': 10, 'default': 10,
'validator': [int, MinValueValidator(1)] 'validator': [int, MinValueValidator(1)]
}, },
'HOMEPAGE_STOCK_LOW': { 'HOMEPAGE_STOCK_LOW': {
'name': _('Show low stock'), 'name': _('Show low stock'),
'description': _('Show low stock items on the homepage'), 'description': _('Show low stock items on the homepage'),
'default': True, 'default': True,
'validator': bool, 'validator': bool,
}, },
'HOMEPAGE_STOCK_DEPLETED': { 'HOMEPAGE_STOCK_DEPLETED': {
'name': _('Show depleted stock'), 'name': _('Show depleted stock'),
'description': _('Show depleted stock items on the homepage'), 'description': _('Show depleted stock items on the homepage'),
'default': True, 'default': True,
'validator': bool, 'validator': bool,
}, },
'HOMEPAGE_STOCK_NEEDED': { 'HOMEPAGE_STOCK_NEEDED': {
'name': _('Show needed stock'), 'name': _('Show needed stock'),
'description': _('Show stock items needed for builds on the homepage'), 'description': _('Show stock items needed for builds on the homepage'),
'default': True, 'default': True,
'validator': bool, 'validator': bool,
}, },
'HOMEPAGE_STOCK_EXPIRED': { 'HOMEPAGE_STOCK_EXPIRED': {
'name': _('Show expired stock'), 'name': _('Show expired stock'),
'description': _('Show expired stock items on the homepage'), 'description': _('Show expired stock items on the homepage'),
'default': True, 'default': True,
'validator': bool, 'validator': bool,
}, },
'HOMEPAGE_STOCK_STALE': { 'HOMEPAGE_STOCK_STALE': {
'name': _('Show stale stock'), 'name': _('Show stale stock'),
'description': _('Show stale stock items on the homepage'), 'description': _('Show stale stock items on the homepage'),
'default': True, 'default': True,
'validator': bool, 'validator': bool,
}, },
'HOMEPAGE_BUILD_PENDING': { 'HOMEPAGE_BUILD_PENDING': {
'name': _('Show pending builds'), 'name': _('Show pending builds'),
'description': _('Show pending builds on the homepage'), 'description': _('Show pending builds on the homepage'),
'default': True, 'default': True,
'validator': bool, 'validator': bool,
}, },
'HOMEPAGE_BUILD_OVERDUE': { 'HOMEPAGE_BUILD_OVERDUE': {
'name': _('Show overdue builds'), 'name': _('Show overdue builds'),
'description': _('Show overdue builds on the homepage'), 'description': _('Show overdue builds on the homepage'),
'default': True, 'default': True,
'validator': bool, 'validator': bool,
}, },
'HOMEPAGE_PO_OUTSTANDING': { 'HOMEPAGE_PO_OUTSTANDING': {
'name': _('Show outstanding POs'), 'name': _('Show outstanding POs'),
'description': _('Show outstanding POs on the homepage'), 'description': _('Show outstanding POs on the homepage'),
'default': True, 'default': True,
'validator': bool, 'validator': bool,
}, },
'HOMEPAGE_PO_OVERDUE': { 'HOMEPAGE_PO_OVERDUE': {
'name': _('Show overdue POs'), 'name': _('Show overdue POs'),
'description': _('Show overdue POs on the homepage'), 'description': _('Show overdue POs on the homepage'),
'default': True, 'default': True,
'validator': bool, 'validator': bool,
}, },
'HOMEPAGE_SO_OUTSTANDING': { 'HOMEPAGE_SO_OUTSTANDING': {
'name': _('Show outstanding SOs'), 'name': _('Show outstanding SOs'),
'description': _('Show outstanding SOs on the homepage'), 'description': _('Show outstanding SOs on the homepage'),
'default': True, 'default': True,
'validator': bool, 'validator': bool,
}, },
'HOMEPAGE_SO_OVERDUE': { 'HOMEPAGE_SO_OVERDUE': {
'name': _('Show overdue SOs'), 'name': _('Show overdue SOs'),
'description': _('Show overdue SOs on the homepage'), 'description': _('Show overdue SOs on the homepage'),

View File

@ -50,11 +50,12 @@ class SettingsSerializer(InvenTreeModelSerializer):
""" """
Make sure protected values are not returned Make sure protected values are not returned
""" """
result = obj.value
# never return protected values # never return protected values
if obj.is_protected: if obj.protected:
result = '***' result = '***'
else:
result = obj.value
return result return result

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from django.test import TestCase from django.test import TestCase

View File

@ -9,7 +9,9 @@ from django.contrib.auth import get_user_model
from django.urls import reverse from django.urls import reverse
from InvenTree.api_tester import InvenTreeAPITestCase from InvenTree.api_tester import InvenTreeAPITestCase
from .models import InvenTreeSetting, WebhookEndpoint, WebhookMessage, NotificationEntry from InvenTree.helpers import str2bool
from .models import InvenTreeSetting, InvenTreeUserSetting, WebhookEndpoint, WebhookMessage, NotificationEntry
from .api import WebhookView from .api import WebhookView
CONTENT_TYPE_JSON = 'application/json' CONTENT_TYPE_JSON = 'application/json'
@ -156,13 +158,224 @@ class SettingsTest(TestCase):
raise ValueError(f'Non-boolean default value specified for {key}') # pragma: no cover raise ValueError(f'Non-boolean default value specified for {key}') # pragma: no cover
class SettingsApiTest(InvenTreeAPITestCase): class GlobalSettingsApiTest(InvenTreeAPITestCase):
"""
Tests for the global settings API
"""
def test_settings_api(self): def test_global_settings_api_list(self):
# test setting with choice """
Test list URL for global settings
"""
url = reverse('api-global-setting-list')
# Read out each of the global settings value, to ensure they are instantiated in the database
for key in InvenTreeSetting.SETTINGS:
InvenTreeSetting.get_setting_object(key)
response = self.get(url, expected_code=200)
# Number of results should match the number of settings
self.assertEqual(len(response.data), len(InvenTreeSetting.SETTINGS.keys()))
def test_company_name(self):
setting = InvenTreeSetting.get_setting_object('INVENTREE_COMPANY_NAME')
# Check default value
self.assertEqual(setting.value, 'My company name')
url = reverse('api-global-setting-detail', kwargs={'pk': setting.pk})
# Test getting via the API
for val in ['test', '123', 'My company nam3']:
setting.value = val
setting.save()
response = self.get(url, expected_code=200)
self.assertEqual(response.data['value'], val)
# Test setting via the API
for val in ['cat', 'hat', 'bat', 'mat']:
response = self.patch(
url,
{
'value': val,
},
expected_code=200
)
self.assertEqual(response.data['value'], val)
setting.refresh_from_db()
self.assertEqual(setting.value, val)
class UserSettingsApiTest(InvenTreeAPITestCase):
"""
Tests for the user settings API
"""
def test_user_settings_api_list(self):
"""
Test list URL for user settings
"""
url = reverse('api-user-setting-list') url = reverse('api-user-setting-list')
self.get(url, expected_code=200) self.get(url, expected_code=200)
def test_user_setting_boolean(self):
"""
Test a boolean user setting value
"""
# Ensure we have a boolean setting available
setting = InvenTreeUserSetting.get_setting_object(
'SEARCH_PREVIEW_SHOW_PARTS',
user=self.user
)
# Check default values
self.assertEqual(setting.to_native_value(), True)
# Fetch via API
url = reverse('api-user-setting-detail', kwargs={'pk': setting.pk})
response = self.get(url, expected_code=200)
self.assertEqual(response.data['pk'], setting.pk)
self.assertEqual(response.data['key'], 'SEARCH_PREVIEW_SHOW_PARTS')
self.assertEqual(response.data['description'], 'Display parts in search preview window')
self.assertEqual(response.data['type'], 'boolean')
self.assertEqual(len(response.data['choices']), 0)
self.assertTrue(str2bool(response.data['value']))
# Assign some truthy values
for v in ['true', True, 1, 'y', 'TRUE']:
self.patch(
url,
{
'value': str(v),
},
expected_code=200,
)
response = self.get(url, expected_code=200)
self.assertTrue(str2bool(response.data['value']))
# Assign some falsey values
for v in ['false', False, '0', 'n', 'FalSe']:
self.patch(
url,
{
'value': str(v),
},
expected_code=200,
)
response = self.get(url, expected_code=200)
self.assertFalse(str2bool(response.data['value']))
# Assign some invalid values
for v in ['x', '', 'invalid', None, '-1', 'abcde']:
response = self.patch(
url,
{
'value': str(v),
},
expected_code=200
)
# Invalid values evaluate to False
self.assertFalse(str2bool(response.data['value']))
def test_user_setting_choice(self):
setting = InvenTreeUserSetting.get_setting_object(
'DATE_DISPLAY_FORMAT',
user=self.user
)
url = reverse('api-user-setting-detail', kwargs={'pk': setting.pk})
# Check default value
self.assertEqual(setting.value, 'YYYY-MM-DD')
# Check that a valid option can be assigned via the API
for opt in ['YYYY-MM-DD', 'DD-MM-YYYY', 'MM/DD/YYYY']:
self.patch(
url,
{
'value': opt,
},
expected_code=200,
)
setting.refresh_from_db()
self.assertEqual(setting.value, opt)
# Send an invalid option
for opt in ['cat', 'dog', 12345]:
response = self.patch(
url,
{
'value': opt,
},
expected_code=400,
)
self.assertIn('Chosen value is not a valid option', str(response.data))
def test_user_setting_integer(self):
setting = InvenTreeUserSetting.get_setting_object(
'SEARCH_PREVIEW_RESULTS',
user=self.user
)
url = reverse('api-user-setting-detail', kwargs={'pk': setting.pk})
# Check default value for this setting
self.assertEqual(setting.value, 10)
for v in [1, 9, 99]:
setting.value = v
setting.save()
response = self.get(url)
self.assertEqual(response.data['value'], str(v))
# Set valid options via the api
for v in [5, 15, 25]:
self.patch(
url,
{
'value': v,
},
expected_code=200,
)
setting.refresh_from_db()
self.assertEqual(setting.to_native_value(), v)
# Set invalid options via the API
# Note that this particular setting has a MinValueValidator(1) associated with it
for v in [0, -1, -5]:
response = self.patch(
url,
{
'value': v,
},
expected_code=400,
)
class WebhookMessageTests(TestCase): class WebhookMessageTests(TestCase):
def setUp(self): def setUp(self):

View File

@ -7,7 +7,7 @@ from __future__ import unicode_literals
import os import os
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.conf import settings from django.conf import settings
from django.core.files.storage import FileSystemStorage from django.core.files.storage import FileSystemStorage

View File

@ -11,7 +11,7 @@ from django_filters import rest_framework as rest_filters
from rest_framework import filters from rest_framework import filters
from rest_framework import generics from rest_framework import generics
from django.conf.urls import url, include from django.urls import include, re_path
from django.db.models import Q from django.db.models import Q
from InvenTree.helpers import str2bool from InvenTree.helpers import str2bool
@ -390,42 +390,42 @@ class SupplierPriceBreakDetail(generics.RetrieveUpdateDestroyAPIView):
manufacturer_part_api_urls = [ manufacturer_part_api_urls = [
url(r'^parameter/', include([ re_path(r'^parameter/', include([
url(r'^(?P<pk>\d+)/', ManufacturerPartParameterDetail.as_view(), name='api-manufacturer-part-parameter-detail'), re_path(r'^(?P<pk>\d+)/', ManufacturerPartParameterDetail.as_view(), name='api-manufacturer-part-parameter-detail'),
# Catch anything else # Catch anything else
url(r'^.*$', ManufacturerPartParameterList.as_view(), name='api-manufacturer-part-parameter-list'), re_path(r'^.*$', ManufacturerPartParameterList.as_view(), name='api-manufacturer-part-parameter-list'),
])), ])),
url(r'^(?P<pk>\d+)/?', ManufacturerPartDetail.as_view(), name='api-manufacturer-part-detail'), re_path(r'^(?P<pk>\d+)/?', ManufacturerPartDetail.as_view(), name='api-manufacturer-part-detail'),
# Catch anything else # Catch anything else
url(r'^.*$', ManufacturerPartList.as_view(), name='api-manufacturer-part-list'), re_path(r'^.*$', ManufacturerPartList.as_view(), name='api-manufacturer-part-list'),
] ]
supplier_part_api_urls = [ supplier_part_api_urls = [
url(r'^(?P<pk>\d+)/?', SupplierPartDetail.as_view(), name='api-supplier-part-detail'), re_path(r'^(?P<pk>\d+)/?', SupplierPartDetail.as_view(), name='api-supplier-part-detail'),
# Catch anything else # Catch anything else
url(r'^.*$', SupplierPartList.as_view(), name='api-supplier-part-list'), re_path(r'^.*$', SupplierPartList.as_view(), name='api-supplier-part-list'),
] ]
company_api_urls = [ company_api_urls = [
url(r'^part/manufacturer/', include(manufacturer_part_api_urls)), re_path(r'^part/manufacturer/', include(manufacturer_part_api_urls)),
url(r'^part/', include(supplier_part_api_urls)), re_path(r'^part/', include(supplier_part_api_urls)),
# Supplier price breaks # Supplier price breaks
url(r'^price-break/', include([ re_path(r'^price-break/', include([
url(r'^(?P<pk>\d+)/?', SupplierPriceBreakDetail.as_view(), name='api-part-supplier-price-detail'), re_path(r'^(?P<pk>\d+)/?', SupplierPriceBreakDetail.as_view(), name='api-part-supplier-price-detail'),
url(r'^.*$', SupplierPriceBreakList.as_view(), name='api-part-supplier-price-list'), re_path(r'^.*$', SupplierPriceBreakList.as_view(), name='api-part-supplier-price-list'),
])), ])),
url(r'^(?P<pk>\d+)/?', CompanyDetail.as_view(), name='api-company-detail'), re_path(r'^(?P<pk>\d+)/?', CompanyDetail.as_view(), name='api-company-detail'),
url(r'^.*$', CompanyList.as_view(), name='api-company-list'), re_path(r'^.*$', CompanyList.as_view(), name='api-company-list'),
] ]

View File

@ -8,7 +8,7 @@ from __future__ import unicode_literals
from InvenTree.forms import HelperForm from InvenTree.forms import HelperForm
from InvenTree.fields import RoundingDecimalFormField from InvenTree.fields import RoundingDecimalFormField
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
import django.forms import django.forms
from .models import Company from .models import Company

View File

@ -7,7 +7,7 @@ from __future__ import unicode_literals
import os import os
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.core.validators import MinValueValidator from django.core.validators import MinValueValidator
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
@ -634,7 +634,7 @@ class SupplierPart(models.Model):
get_price = common.models.get_price get_price = common.models.get_price
def open_orders(self): def open_orders(self):
""" Return a database query for PO line items for this SupplierPart, """ Return a database query for PurchaseOrder line items for this SupplierPart,
limited to purchase orders that are open / outstanding. limited to purchase orders that are open / outstanding.
""" """

View File

@ -2,7 +2,7 @@
JSON serializers for Company app JSON serializers for Company app
""" """
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
from rest_framework import serializers from rest_framework import serializers

View File

@ -2,37 +2,37 @@
URL lookup for Company app URL lookup for Company app
""" """
from django.conf.urls import url, include from django.urls import include, re_path
from . import views from . import views
company_detail_urls = [ company_detail_urls = [
url(r'^thumb-download/', views.CompanyImageDownloadFromURL.as_view(), name='company-image-download'), re_path(r'^thumb-download/', views.CompanyImageDownloadFromURL.as_view(), name='company-image-download'),
# Any other URL # Any other URL
url(r'^.*$', views.CompanyDetail.as_view(), name='company-detail'), re_path(r'^.*$', views.CompanyDetail.as_view(), name='company-detail'),
] ]
company_urls = [ company_urls = [
url(r'^(?P<pk>\d+)/', include(company_detail_urls)), re_path(r'^(?P<pk>\d+)/', include(company_detail_urls)),
url(r'suppliers/', views.CompanyIndex.as_view(), name='supplier-index'), re_path(r'suppliers/', views.CompanyIndex.as_view(), name='supplier-index'),
url(r'manufacturers/', views.CompanyIndex.as_view(), name='manufacturer-index'), re_path(r'manufacturers/', views.CompanyIndex.as_view(), name='manufacturer-index'),
url(r'customers/', views.CompanyIndex.as_view(), name='customer-index'), re_path(r'customers/', views.CompanyIndex.as_view(), name='customer-index'),
# Redirect any other patterns to the 'company' index which displays all companies # Redirect any other patterns to the 'company' index which displays all companies
url(r'^.*$', views.CompanyIndex.as_view(), name='company-index'), re_path(r'^.*$', views.CompanyIndex.as_view(), name='company-index'),
] ]
manufacturer_part_urls = [ manufacturer_part_urls = [
url(r'^(?P<pk>\d+)/', views.ManufacturerPartDetail.as_view(template_name='company/manufacturer_part.html'), name='manufacturer-part-detail'), re_path(r'^(?P<pk>\d+)/', views.ManufacturerPartDetail.as_view(template_name='company/manufacturer_part.html'), name='manufacturer-part-detail'),
] ]
supplier_part_urls = [ supplier_part_urls = [
url(r'^(?P<pk>\d+)/', views.SupplierPartDetail.as_view(template_name='company/supplier_part.html'), name='supplier-part-detail'), re_path(r'^(?P<pk>\d+)/', views.SupplierPartDetail.as_view(template_name='company/supplier_part.html'), name='supplier-part-detail'),
] ]

View File

@ -6,7 +6,7 @@ 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.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views.generic import DetailView, ListView from django.views.generic import DetailView, ListView
from django.urls import reverse from django.urls import reverse

View File

@ -4,10 +4,10 @@ from __future__ import unicode_literals
from io import BytesIO from io import BytesIO
from PIL import Image from PIL import Image
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.conf import settings from django.conf import settings
from django.conf.urls import url, include from django.urls import include, re_path
from django.core.exceptions import ValidationError, FieldError from django.core.exceptions import ValidationError, FieldError
from django.http import HttpResponse, JsonResponse from django.http import HttpResponse, JsonResponse
@ -579,38 +579,38 @@ class PartLabelPrint(generics.RetrieveAPIView, PartLabelMixin, LabelPrintMixin):
label_api_urls = [ label_api_urls = [
# Stock item labels # Stock item labels
url(r'stock/', include([ re_path(r'stock/', include([
# Detail views # Detail views
url(r'^(?P<pk>\d+)/', include([ re_path(r'^(?P<pk>\d+)/', include([
url(r'print/?', StockItemLabelPrint.as_view(), name='api-stockitem-label-print'), re_path(r'print/?', StockItemLabelPrint.as_view(), name='api-stockitem-label-print'),
url(r'^.*$', StockItemLabelDetail.as_view(), name='api-stockitem-label-detail'), re_path(r'^.*$', StockItemLabelDetail.as_view(), name='api-stockitem-label-detail'),
])), ])),
# List view # List view
url(r'^.*$', StockItemLabelList.as_view(), name='api-stockitem-label-list'), re_path(r'^.*$', StockItemLabelList.as_view(), name='api-stockitem-label-list'),
])), ])),
# Stock location labels # Stock location labels
url(r'location/', include([ re_path(r'location/', include([
# Detail views # Detail views
url(r'^(?P<pk>\d+)/', include([ re_path(r'^(?P<pk>\d+)/', include([
url(r'print/?', StockLocationLabelPrint.as_view(), name='api-stocklocation-label-print'), re_path(r'print/?', StockLocationLabelPrint.as_view(), name='api-stocklocation-label-print'),
url(r'^.*$', StockLocationLabelDetail.as_view(), name='api-stocklocation-label-detail'), re_path(r'^.*$', StockLocationLabelDetail.as_view(), name='api-stocklocation-label-detail'),
])), ])),
# List view # List view
url(r'^.*$', StockLocationLabelList.as_view(), name='api-stocklocation-label-list'), re_path(r'^.*$', StockLocationLabelList.as_view(), name='api-stocklocation-label-list'),
])), ])),
# Part labels # Part labels
url(r'^part/', include([ re_path(r'^part/', include([
# Detail views # Detail views
url(r'^(?P<pk>\d+)/', include([ re_path(r'^(?P<pk>\d+)/', include([
url(r'^print/', PartLabelPrint.as_view(), name='api-part-label-print'), re_path(r'^print/', PartLabelPrint.as_view(), name='api-part-label-print'),
url(r'^.*$', PartLabelDetail.as_view(), name='api-part-label-detail'), re_path(r'^.*$', PartLabelDetail.as_view(), name='api-part-label-detail'),
])), ])),
# List view # List view
url(r'^.*$', PartLabelList.as_view(), name='api-part-label-list'), re_path(r'^.*$', PartLabelList.as_view(), name='api-part-label-list'),
])), ])),
] ]

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

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

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

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

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

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

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

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

File diff suppressed because it is too large Load Diff

View File

@ -8,11 +8,35 @@ from import_export.admin import ImportExportModelAdmin
from import_export.resources import ModelResource from import_export.resources import ModelResource
from import_export.fields import Field from import_export.fields import Field
from .models import PurchaseOrder, PurchaseOrderLineItem from .models import PurchaseOrder, PurchaseOrderLineItem, PurchaseOrderExtraLine
from .models import SalesOrder, SalesOrderLineItem from .models import SalesOrder, SalesOrderLineItem, SalesOrderExtraLine
from .models import SalesOrderShipment, SalesOrderAllocation from .models import SalesOrderShipment, SalesOrderAllocation
# region general classes
class GeneralExtraLineAdmin:
list_display = (
'order',
'quantity',
'reference'
)
search_fields = [
'order__reference',
'order__customer__name',
'reference',
]
autocomplete_fields = ('order', )
class GeneralExtraLineMeta:
skip_unchanged = True
report_skipped = False
clean_model_instances = True
# endregion
class PurchaseOrderLineItemInlineAdmin(admin.StackedInline): class PurchaseOrderLineItemInlineAdmin(admin.StackedInline):
model = PurchaseOrderLineItem model = PurchaseOrderLineItem
extra = 0 extra = 0
@ -68,8 +92,8 @@ class SalesOrderAdmin(ImportExportModelAdmin):
autocomplete_fields = ('customer',) autocomplete_fields = ('customer',)
class POLineItemResource(ModelResource): class PurchaseOrderLineItemResource(ModelResource):
""" Class for managing import / export of POLineItem data """ """ Class for managing import / export of PurchaseOrderLineItem data """
part_name = Field(attribute='part__part__name', readonly=True) part_name = Field(attribute='part__part__name', readonly=True)
@ -86,9 +110,16 @@ class POLineItemResource(ModelResource):
clean_model_instances = True clean_model_instances = True
class SOLineItemResource(ModelResource): class PurchaseOrderExtraLineResource(ModelResource):
""" Class for managing import / export of PurchaseOrderExtraLine data """
class Meta(GeneralExtraLineMeta):
model = PurchaseOrderExtraLine
class SalesOrderLineItemResource(ModelResource):
""" """
Class for managing import / export of SOLineItem data Class for managing import / export of SalesOrderLineItem data
""" """
part_name = Field(attribute='part__name', readonly=True) part_name = Field(attribute='part__name', readonly=True)
@ -117,9 +148,16 @@ class SOLineItemResource(ModelResource):
clean_model_instances = True clean_model_instances = True
class SalesOrderExtraLineResource(ModelResource):
""" Class for managing import / export of SalesOrderExtraLine data """
class Meta(GeneralExtraLineMeta):
model = SalesOrderExtraLine
class PurchaseOrderLineItemAdmin(ImportExportModelAdmin): class PurchaseOrderLineItemAdmin(ImportExportModelAdmin):
resource_class = POLineItemResource resource_class = PurchaseOrderLineItemResource
list_display = ( list_display = (
'order', 'order',
@ -133,9 +171,14 @@ class PurchaseOrderLineItemAdmin(ImportExportModelAdmin):
autocomplete_fields = ('order', 'part', 'destination',) autocomplete_fields = ('order', 'part', 'destination',)
class PurchaseOrderExtraLineAdmin(GeneralExtraLineAdmin, ImportExportModelAdmin):
resource_class = PurchaseOrderExtraLineResource
class SalesOrderLineItemAdmin(ImportExportModelAdmin): class SalesOrderLineItemAdmin(ImportExportModelAdmin):
resource_class = SOLineItemResource resource_class = SalesOrderLineItemResource
list_display = ( list_display = (
'order', 'order',
@ -154,6 +197,11 @@ class SalesOrderLineItemAdmin(ImportExportModelAdmin):
autocomplete_fields = ('order', 'part',) autocomplete_fields = ('order', 'part',)
class SalesOrderExtraLineAdmin(GeneralExtraLineAdmin, ImportExportModelAdmin):
resource_class = SalesOrderExtraLineResource
class SalesOrderShipmentAdmin(ImportExportModelAdmin): class SalesOrderShipmentAdmin(ImportExportModelAdmin):
list_display = [ list_display = [
@ -184,9 +232,11 @@ class SalesOrderAllocationAdmin(ImportExportModelAdmin):
admin.site.register(PurchaseOrder, PurchaseOrderAdmin) admin.site.register(PurchaseOrder, PurchaseOrderAdmin)
admin.site.register(PurchaseOrderLineItem, PurchaseOrderLineItemAdmin) admin.site.register(PurchaseOrderLineItem, PurchaseOrderLineItemAdmin)
admin.site.register(PurchaseOrderExtraLine, PurchaseOrderExtraLineAdmin)
admin.site.register(SalesOrder, SalesOrderAdmin) admin.site.register(SalesOrder, SalesOrderAdmin)
admin.site.register(SalesOrderLineItem, SalesOrderLineItemAdmin) admin.site.register(SalesOrderLineItem, SalesOrderLineItemAdmin)
admin.site.register(SalesOrderExtraLine, SalesOrderExtraLineAdmin)
admin.site.register(SalesOrderShipment, SalesOrderShipmentAdmin) admin.site.register(SalesOrderShipment, SalesOrderShipmentAdmin)
admin.site.register(SalesOrderAllocation, SalesOrderAllocationAdmin) admin.site.register(SalesOrderAllocation, SalesOrderAllocationAdmin)

View File

@ -5,7 +5,7 @@ JSON API for the Order app
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import unicode_literals from __future__ import unicode_literals
from django.conf.urls import url, include from django.urls import include, path, re_path
from django.db.models import Q, F from django.db.models import Q, F
from django_filters import rest_framework as rest_filters from django_filters import rest_framework as rest_filters
@ -20,16 +20,68 @@ from InvenTree.helpers import str2bool, DownloadFile
from InvenTree.api import AttachmentMixin from InvenTree.api import AttachmentMixin
from InvenTree.status_codes import PurchaseOrderStatus, SalesOrderStatus from InvenTree.status_codes import PurchaseOrderStatus, SalesOrderStatus
from order.admin import POLineItemResource from order.admin import PurchaseOrderLineItemResource
import order.models as models import order.models as models
import order.serializers as serializers import order.serializers as serializers
from part.models import Part from part.models import Part
from users.models import Owner from users.models import Owner
class POFilter(rest_filters.FilterSet): class GeneralExtraLineList:
""" """
Custom API filters for the POList endpoint General template for ExtraLine API classes
"""
def get_serializer(self, *args, **kwargs):
try:
params = self.request.query_params
kwargs['order_detail'] = str2bool(params.get('order_detail', False))
except AttributeError:
pass
kwargs['context'] = self.get_serializer_context()
return self.serializer_class(*args, **kwargs)
def get_queryset(self, *args, **kwargs):
queryset = super().get_queryset(*args, **kwargs)
queryset = queryset.prefetch_related(
'order',
)
return queryset
filter_backends = [
rest_filters.DjangoFilterBackend,
filters.SearchFilter,
filters.OrderingFilter
]
ordering_fields = [
'title',
'quantity',
'note',
'reference',
]
search_fields = [
'title',
'quantity',
'note',
'reference'
]
filter_fields = [
'order',
]
class PurchaseOrderFilter(rest_filters.FilterSet):
"""
Custom API filters for the PurchaseOrderList endpoint
""" """
assigned_to_me = rest_filters.BooleanFilter(label='assigned_to_me', method='filter_assigned_to_me') assigned_to_me = rest_filters.BooleanFilter(label='assigned_to_me', method='filter_assigned_to_me')
@ -58,16 +110,16 @@ class POFilter(rest_filters.FilterSet):
] ]
class POList(generics.ListCreateAPIView): class PurchaseOrderList(generics.ListCreateAPIView):
""" API endpoint for accessing a list of PurchaseOrder objects """ API endpoint for accessing a list of PurchaseOrder objects
- GET: Return list of PO objects (with filters) - GET: Return list of PurchaseOrder objects (with filters)
- POST: Create a new PurchaseOrder object - POST: Create a new PurchaseOrder object
""" """
queryset = models.PurchaseOrder.objects.all() queryset = models.PurchaseOrder.objects.all()
serializer_class = serializers.POSerializer serializer_class = serializers.PurchaseOrderSerializer
filterset_class = POFilter filterset_class = PurchaseOrderFilter
def create(self, request, *args, **kwargs): def create(self, request, *args, **kwargs):
""" """
@ -104,7 +156,7 @@ class POList(generics.ListCreateAPIView):
'lines', 'lines',
) )
queryset = serializers.POSerializer.annotate_queryset(queryset) queryset = serializers.PurchaseOrderSerializer.annotate_queryset(queryset)
return queryset return queryset
@ -202,11 +254,11 @@ class POList(generics.ListCreateAPIView):
ordering = '-creation_date' ordering = '-creation_date'
class PODetail(generics.RetrieveUpdateDestroyAPIView): class PurchaseOrderDetail(generics.RetrieveUpdateDestroyAPIView):
""" API endpoint for detail view of a PurchaseOrder object """ """ API endpoint for detail view of a PurchaseOrder object """
queryset = models.PurchaseOrder.objects.all() queryset = models.PurchaseOrder.objects.all()
serializer_class = serializers.POSerializer serializer_class = serializers.PurchaseOrderSerializer
def get_serializer(self, *args, **kwargs): def get_serializer(self, *args, **kwargs):
@ -229,12 +281,12 @@ class PODetail(generics.RetrieveUpdateDestroyAPIView):
'lines', 'lines',
) )
queryset = serializers.POSerializer.annotate_queryset(queryset) queryset = serializers.PurchaseOrderSerializer.annotate_queryset(queryset)
return queryset return queryset
class POReceive(generics.CreateAPIView): class PurchaseOrderReceive(generics.CreateAPIView):
""" """
API endpoint to receive stock items against a purchase order. API endpoint to receive stock items against a purchase order.
@ -249,7 +301,7 @@ class POReceive(generics.CreateAPIView):
queryset = models.PurchaseOrderLineItem.objects.none() queryset = models.PurchaseOrderLineItem.objects.none()
serializer_class = serializers.POReceiveSerializer serializer_class = serializers.PurchaseOrderReceiveSerializer
def get_serializer_context(self): def get_serializer_context(self):
@ -266,9 +318,9 @@ class POReceive(generics.CreateAPIView):
return context return context
class POLineItemFilter(rest_filters.FilterSet): class PurchaseOrderLineItemFilter(rest_filters.FilterSet):
""" """
Custom filters for the POLineItemList endpoint Custom filters for the PurchaseOrderLineItemList endpoint
""" """
class Meta: class Meta:
@ -318,22 +370,22 @@ class POLineItemFilter(rest_filters.FilterSet):
return queryset return queryset
class POLineItemList(generics.ListCreateAPIView): class PurchaseOrderLineItemList(generics.ListCreateAPIView):
""" API endpoint for accessing a list of POLineItem objects """ API endpoint for accessing a list of PurchaseOrderLineItem objects
- GET: Return a list of PO Line Item objects - GET: Return a list of PurchaseOrder Line Item objects
- POST: Create a new PurchaseOrderLineItem object - POST: Create a new PurchaseOrderLineItem object
""" """
queryset = models.PurchaseOrderLineItem.objects.all() queryset = models.PurchaseOrderLineItem.objects.all()
serializer_class = serializers.POLineItemSerializer serializer_class = serializers.PurchaseOrderLineItemSerializer
filterset_class = POLineItemFilter filterset_class = PurchaseOrderLineItemFilter
def get_queryset(self, *args, **kwargs): def get_queryset(self, *args, **kwargs):
queryset = super().get_queryset(*args, **kwargs) queryset = super().get_queryset(*args, **kwargs)
queryset = serializers.POLineItemSerializer.annotate_queryset(queryset) queryset = serializers.PurchaseOrderLineItemSerializer.annotate_queryset(queryset)
return queryset return queryset
@ -382,7 +434,7 @@ class POLineItemList(generics.ListCreateAPIView):
export_format = str(export_format).strip().lower() export_format = str(export_format).strip().lower()
if export_format in ['csv', 'tsv', 'xls', 'xlsx']: if export_format in ['csv', 'tsv', 'xls', 'xlsx']:
dataset = POLineItemResource().export(queryset=queryset) dataset = PurchaseOrderLineItemResource().export(queryset=queryset)
filedata = dataset.export(export_format) filedata = dataset.export(export_format)
@ -432,30 +484,46 @@ class POLineItemList(generics.ListCreateAPIView):
] ]
class POLineItemDetail(generics.RetrieveUpdateDestroyAPIView): class PurchaseOrderLineItemDetail(generics.RetrieveUpdateDestroyAPIView):
""" """
Detail API endpoint for PurchaseOrderLineItem object Detail API endpoint for PurchaseOrderLineItem object
""" """
queryset = models.PurchaseOrderLineItem.objects.all() queryset = models.PurchaseOrderLineItem.objects.all()
serializer_class = serializers.POLineItemSerializer serializer_class = serializers.PurchaseOrderLineItemSerializer
def get_queryset(self): def get_queryset(self):
queryset = super().get_queryset() queryset = super().get_queryset()
queryset = serializers.POLineItemSerializer.annotate_queryset(queryset) queryset = serializers.PurchaseOrderLineItemSerializer.annotate_queryset(queryset)
return queryset return queryset
class SOAttachmentList(generics.ListCreateAPIView, AttachmentMixin): class PurchaseOrderExtraLineList(GeneralExtraLineList, generics.ListCreateAPIView):
"""
API endpoint for accessing a list of PurchaseOrderExtraLine objects.
"""
queryset = models.PurchaseOrderExtraLine.objects.all()
serializer_class = serializers.PurchaseOrderExtraLineSerializer
class PurchaseOrderExtraLineDetail(generics.RetrieveUpdateDestroyAPIView):
""" API endpoint for detail view of a PurchaseOrderExtraLine object """
queryset = models.PurchaseOrderExtraLine.objects.all()
serializer_class = serializers.PurchaseOrderExtraLineSerializer
class SalesOrderAttachmentList(generics.ListCreateAPIView, AttachmentMixin):
""" """
API endpoint for listing (and creating) a SalesOrderAttachment (file upload) API endpoint for listing (and creating) a SalesOrderAttachment (file upload)
""" """
queryset = models.SalesOrderAttachment.objects.all() queryset = models.SalesOrderAttachment.objects.all()
serializer_class = serializers.SOAttachmentSerializer serializer_class = serializers.SalesOrderAttachmentSerializer
filter_backends = [ filter_backends = [
rest_filters.DjangoFilterBackend, rest_filters.DjangoFilterBackend,
@ -466,20 +534,20 @@ class SOAttachmentList(generics.ListCreateAPIView, AttachmentMixin):
] ]
class SOAttachmentDetail(generics.RetrieveUpdateDestroyAPIView, AttachmentMixin): class SalesOrderAttachmentDetail(generics.RetrieveUpdateDestroyAPIView, AttachmentMixin):
""" """
Detail endpoint for SalesOrderAttachment Detail endpoint for SalesOrderAttachment
""" """
queryset = models.SalesOrderAttachment.objects.all() queryset = models.SalesOrderAttachment.objects.all()
serializer_class = serializers.SOAttachmentSerializer serializer_class = serializers.SalesOrderAttachmentSerializer
class SOList(generics.ListCreateAPIView): class SalesOrderList(generics.ListCreateAPIView):
""" """
API endpoint for accessing a list of SalesOrder objects. API endpoint for accessing a list of SalesOrder objects.
- GET: Return list of SO objects (with filters) - GET: Return list of SalesOrder objects (with filters)
- POST: Create a new SalesOrder - POST: Create a new SalesOrder
""" """
@ -616,7 +684,7 @@ class SOList(generics.ListCreateAPIView):
ordering = '-creation_date' ordering = '-creation_date'
class SODetail(generics.RetrieveUpdateDestroyAPIView): class SalesOrderDetail(generics.RetrieveUpdateDestroyAPIView):
""" """
API endpoint for detail view of a SalesOrder object. API endpoint for detail view of a SalesOrder object.
""" """
@ -646,9 +714,9 @@ class SODetail(generics.RetrieveUpdateDestroyAPIView):
return queryset return queryset
class SOLineItemFilter(rest_filters.FilterSet): class SalesOrderLineItemFilter(rest_filters.FilterSet):
""" """
Custom filters for SOLineItemList endpoint Custom filters for SalesOrderLineItemList endpoint
""" """
class Meta: class Meta:
@ -679,14 +747,14 @@ class SOLineItemFilter(rest_filters.FilterSet):
return queryset return queryset
class SOLineItemList(generics.ListCreateAPIView): class SalesOrderLineItemList(generics.ListCreateAPIView):
""" """
API endpoint for accessing a list of SalesOrderLineItem objects. API endpoint for accessing a list of SalesOrderLineItem objects.
""" """
queryset = models.SalesOrderLineItem.objects.all() queryset = models.SalesOrderLineItem.objects.all()
serializer_class = serializers.SOLineItemSerializer serializer_class = serializers.SalesOrderLineItemSerializer
filterset_class = SOLineItemFilter filterset_class = SalesOrderLineItemFilter
def get_serializer(self, *args, **kwargs): def get_serializer(self, *args, **kwargs):
@ -743,11 +811,27 @@ class SOLineItemList(generics.ListCreateAPIView):
] ]
class SOLineItemDetail(generics.RetrieveUpdateDestroyAPIView): class SalesOrderExtraLineList(GeneralExtraLineList, generics.ListCreateAPIView):
"""
API endpoint for accessing a list of SalesOrderExtraLine objects.
"""
queryset = models.SalesOrderExtraLine.objects.all()
serializer_class = serializers.SalesOrderExtraLineSerializer
class SalesOrderExtraLineDetail(generics.RetrieveUpdateDestroyAPIView):
""" API endpoint for detail view of a SalesOrderExtraLine object """
queryset = models.SalesOrderExtraLine.objects.all()
serializer_class = serializers.SalesOrderExtraLineSerializer
class SalesOrderLineItemDetail(generics.RetrieveUpdateDestroyAPIView):
""" API endpoint for detail view of a SalesOrderLineItem object """ """ API endpoint for detail view of a SalesOrderLineItem object """
queryset = models.SalesOrderLineItem.objects.all() queryset = models.SalesOrderLineItem.objects.all()
serializer_class = serializers.SOLineItemSerializer serializer_class = serializers.SalesOrderLineItemSerializer
class SalesOrderComplete(generics.CreateAPIView): class SalesOrderComplete(generics.CreateAPIView):
@ -779,7 +863,7 @@ class SalesOrderAllocateSerials(generics.CreateAPIView):
""" """
queryset = models.SalesOrder.objects.none() queryset = models.SalesOrder.objects.none()
serializer_class = serializers.SOSerialAllocationSerializer serializer_class = serializers.SalesOrderSerialAllocationSerializer
def get_serializer_context(self): def get_serializer_context(self):
@ -801,11 +885,11 @@ class SalesOrderAllocate(generics.CreateAPIView):
API endpoint to allocate stock items against a SalesOrder API endpoint to allocate stock items against a SalesOrder
- The SalesOrder is specified in the URL - The SalesOrder is specified in the URL
- See the SOShipmentAllocationSerializer class - See the SalesOrderShipmentAllocationSerializer class
""" """
queryset = models.SalesOrder.objects.none() queryset = models.SalesOrder.objects.none()
serializer_class = serializers.SOShipmentAllocationSerializer serializer_class = serializers.SalesOrderShipmentAllocationSerializer
def get_serializer_context(self): def get_serializer_context(self):
@ -822,7 +906,7 @@ class SalesOrderAllocate(generics.CreateAPIView):
return ctx return ctx
class SOAllocationDetail(generics.RetrieveUpdateDestroyAPIView): class SalesOrderAllocationDetail(generics.RetrieveUpdateDestroyAPIView):
""" """
API endpoint for detali view of a SalesOrderAllocation object API endpoint for detali view of a SalesOrderAllocation object
""" """
@ -831,7 +915,7 @@ class SOAllocationDetail(generics.RetrieveUpdateDestroyAPIView):
serializer_class = serializers.SalesOrderAllocationSerializer serializer_class = serializers.SalesOrderAllocationSerializer
class SOAllocationList(generics.ListAPIView): class SalesOrderAllocationList(generics.ListAPIView):
""" """
API endpoint for listing SalesOrderAllocation objects API endpoint for listing SalesOrderAllocation objects
""" """
@ -909,9 +993,9 @@ class SOAllocationList(generics.ListAPIView):
] ]
class SOShipmentFilter(rest_filters.FilterSet): class SalesOrderShipmentFilter(rest_filters.FilterSet):
""" """
Custom filterset for the SOShipmentList endpoint Custom filterset for the SalesOrderShipmentList endpoint
""" """
shipped = rest_filters.BooleanFilter(label='shipped', method='filter_shipped') shipped = rest_filters.BooleanFilter(label='shipped', method='filter_shipped')
@ -934,21 +1018,21 @@ class SOShipmentFilter(rest_filters.FilterSet):
] ]
class SOShipmentList(generics.ListCreateAPIView): class SalesOrderShipmentList(generics.ListCreateAPIView):
""" """
API list endpoint for SalesOrderShipment model API list endpoint for SalesOrderShipment model
""" """
queryset = models.SalesOrderShipment.objects.all() queryset = models.SalesOrderShipment.objects.all()
serializer_class = serializers.SalesOrderShipmentSerializer serializer_class = serializers.SalesOrderShipmentSerializer
filterset_class = SOShipmentFilter filterset_class = SalesOrderShipmentFilter
filter_backends = [ filter_backends = [
rest_filters.DjangoFilterBackend, rest_filters.DjangoFilterBackend,
] ]
class SOShipmentDetail(generics.RetrieveUpdateDestroyAPIView): class SalesOrderShipmentDetail(generics.RetrieveUpdateDestroyAPIView):
""" """
API detail endpooint for SalesOrderShipment model API detail endpooint for SalesOrderShipment model
""" """
@ -957,7 +1041,7 @@ class SOShipmentDetail(generics.RetrieveUpdateDestroyAPIView):
serializer_class = serializers.SalesOrderShipmentSerializer serializer_class = serializers.SalesOrderShipmentSerializer
class SOShipmentComplete(generics.CreateAPIView): class SalesOrderShipmentComplete(generics.CreateAPIView):
""" """
API endpoint for completing (shipping) a SalesOrderShipment API endpoint for completing (shipping) a SalesOrderShipment
""" """
@ -983,13 +1067,13 @@ class SOShipmentComplete(generics.CreateAPIView):
return ctx return ctx
class POAttachmentList(generics.ListCreateAPIView, AttachmentMixin): class PurchaseOrderAttachmentList(generics.ListCreateAPIView, AttachmentMixin):
""" """
API endpoint for listing (and creating) a PurchaseOrderAttachment (file upload) API endpoint for listing (and creating) a PurchaseOrderAttachment (file upload)
""" """
queryset = models.PurchaseOrderAttachment.objects.all() queryset = models.PurchaseOrderAttachment.objects.all()
serializer_class = serializers.POAttachmentSerializer serializer_class = serializers.PurchaseOrderAttachmentSerializer
filter_backends = [ filter_backends = [
rest_filters.DjangoFilterBackend, rest_filters.DjangoFilterBackend,
@ -1000,78 +1084,90 @@ class POAttachmentList(generics.ListCreateAPIView, AttachmentMixin):
] ]
class POAttachmentDetail(generics.RetrieveUpdateDestroyAPIView, AttachmentMixin): class PurchaseOrderAttachmentDetail(generics.RetrieveUpdateDestroyAPIView, AttachmentMixin):
""" """
Detail endpoint for a PurchaseOrderAttachment Detail endpoint for a PurchaseOrderAttachment
""" """
queryset = models.PurchaseOrderAttachment.objects.all() queryset = models.PurchaseOrderAttachment.objects.all()
serializer_class = serializers.POAttachmentSerializer serializer_class = serializers.PurchaseOrderAttachmentSerializer
order_api_urls = [ order_api_urls = [
# API endpoints for purchase orders # API endpoints for purchase orders
url(r'^po/', include([ re_path(r'^po/', include([
# Purchase order attachments # Purchase order attachments
url(r'attachment/', include([ re_path(r'attachment/', include([
url(r'^(?P<pk>\d+)/$', POAttachmentDetail.as_view(), name='api-po-attachment-detail'), path('<int:pk>/', PurchaseOrderAttachmentDetail.as_view(), name='api-po-attachment-detail'),
url(r'^.*$', POAttachmentList.as_view(), name='api-po-attachment-list'), re_path(r'^.*$', PurchaseOrderAttachmentList.as_view(), name='api-po-attachment-list'),
])), ])),
# Individual purchase order detail URLs # Individual purchase order detail URLs
url(r'^(?P<pk>\d+)/', include([ re_path(r'^(?P<pk>\d+)/', include([
url(r'^receive/', POReceive.as_view(), name='api-po-receive'), re_path(r'^receive/', PurchaseOrderReceive.as_view(), name='api-po-receive'),
url(r'.*$', PODetail.as_view(), name='api-po-detail'), re_path(r'.*$', PurchaseOrderDetail.as_view(), name='api-po-detail'),
])), ])),
# Purchase order list # Purchase order list
url(r'^.*$', POList.as_view(), name='api-po-list'), re_path(r'^.*$', PurchaseOrderList.as_view(), name='api-po-list'),
])), ])),
# API endpoints for purchase order line items # API endpoints for purchase order line items
url(r'^po-line/', include([ re_path(r'^po-line/', include([
url(r'^(?P<pk>\d+)/$', POLineItemDetail.as_view(), name='api-po-line-detail'), path('<int:pk>/', PurchaseOrderLineItemDetail.as_view(), name='api-po-line-detail'),
url(r'^.*$', POLineItemList.as_view(), name='api-po-line-list'), re_path(r'^.*$', PurchaseOrderLineItemList.as_view(), name='api-po-line-list'),
])), ])),
# API endpoints for sales orders # API endpoints for purchase order extra line
url(r'^so/', include([ re_path(r'^po-extra-line/', include([
url(r'attachment/', include([ path('<int:pk>/', PurchaseOrderExtraLineDetail.as_view(), name='api-po-extra-line-detail'),
url(r'^(?P<pk>\d+)/$', SOAttachmentDetail.as_view(), name='api-so-attachment-detail'), path('', PurchaseOrderExtraLineList.as_view(), name='api-po-extra-line-list'),
url(r'^.*$', SOAttachmentList.as_view(), name='api-so-attachment-list'), ])),
# API endpoints for sales ordesr
re_path(r'^so/', include([
re_path(r'attachment/', include([
path('<int:pk>/', SalesOrderAttachmentDetail.as_view(), name='api-so-attachment-detail'),
re_path(r'^.*$', SalesOrderAttachmentList.as_view(), name='api-so-attachment-list'),
])), ])),
url(r'^shipment/', include([ re_path(r'^shipment/', include([
url(r'^(?P<pk>\d+)/', include([ re_path(r'^(?P<pk>\d+)/', include([
url(r'^ship/$', SOShipmentComplete.as_view(), name='api-so-shipment-ship'), path('ship/', SalesOrderShipmentComplete.as_view(), name='api-so-shipment-ship'),
url(r'^.*$', SOShipmentDetail.as_view(), name='api-so-shipment-detail'), re_path(r'^.*$', SalesOrderShipmentDetail.as_view(), name='api-so-shipment-detail'),
])), ])),
url(r'^.*$', SOShipmentList.as_view(), name='api-so-shipment-list'), re_path(r'^.*$', SalesOrderShipmentList.as_view(), name='api-so-shipment-list'),
])), ])),
# Sales order detail view # Sales order detail view
url(r'^(?P<pk>\d+)/', include([ re_path(r'^(?P<pk>\d+)/', include([
url(r'^complete/', SalesOrderComplete.as_view(), name='api-so-complete'), re_path(r'^complete/', SalesOrderComplete.as_view(), name='api-so-complete'),
url(r'^allocate/', SalesOrderAllocate.as_view(), name='api-so-allocate'), re_path(r'^allocate/', SalesOrderAllocate.as_view(), name='api-so-allocate'),
url(r'^allocate-serials/', SalesOrderAllocateSerials.as_view(), name='api-so-allocate-serials'), re_path(r'^allocate-serials/', SalesOrderAllocateSerials.as_view(), name='api-so-allocate-serials'),
url(r'^.*$', SODetail.as_view(), name='api-so-detail'), re_path(r'^.*$', SalesOrderDetail.as_view(), name='api-so-detail'),
])), ])),
# Sales order list view # Sales order list view
url(r'^.*$', SOList.as_view(), name='api-so-list'), re_path(r'^.*$', SalesOrderList.as_view(), name='api-so-list'),
])), ])),
# API endpoints for sales order line items # API endpoints for sales order line items
url(r'^so-line/', include([ re_path(r'^so-line/', include([
url(r'^(?P<pk>\d+)/$', SOLineItemDetail.as_view(), name='api-so-line-detail'), path('<int:pk>/', SalesOrderLineItemDetail.as_view(), name='api-so-line-detail'),
url(r'^$', SOLineItemList.as_view(), name='api-so-line-list'), path('', SalesOrderLineItemList.as_view(), name='api-so-line-list'),
])),
# API endpoints for sales order extra line
re_path(r'^so-extra-line/', include([
path('<int:pk>/', SalesOrderExtraLineDetail.as_view(), name='api-so-extra-line-detail'),
path('', SalesOrderExtraLineList.as_view(), name='api-so-extra-line-list'),
])), ])),
# API endpoints for sales order allocations # API endpoints for sales order allocations
url(r'^so-allocation/', include([ re_path(r'^so-allocation/', include([
url(r'^(?P<pk>\d+)/$', SOAllocationDetail.as_view(), name='api-so-allocation-detail'), path('<int:pk>/', SalesOrderAllocationDetail.as_view(), name='api-so-allocation-detail'),
url(r'^.*$', SOAllocationList.as_view(), name='api-so-allocation-list'), re_path(r'^.*$', SalesOrderAllocationList.as_view(), name='api-so-allocation-list'),
])), ])),
] ]

View File

@ -6,7 +6,7 @@ Django Forms for interacting with Order objects
from __future__ import unicode_literals from __future__ import unicode_literals
from django import forms from django import forms
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
from InvenTree.forms import HelperForm from InvenTree.forms import HelperForm
from InvenTree.fields import InvenTreeMoneyField from InvenTree.fields import InvenTreeMoneyField

View File

@ -33,7 +33,7 @@ def calculate_shipped_quantity(apps, schema_editor):
part=item.part part=item.part
) )
q = sum([item.quantity for item in items]) q = sum(item.quantity for item in items)
item.shipped = q item.shipped = q

View File

@ -0,0 +1,109 @@
# Generated by Django 3.2.12 on 2022-03-27 01:11
import InvenTree.fields
import django.core.validators
from django.core import serializers
from django.db import migrations, models
import django.db.models.deletion
import djmoney.models.fields
import djmoney.models.validators
def _convert_model(apps, line_item_ref, extra_line_ref, price_ref):
"""Convert the OrderLineItem instances if applicable to new ExtraLine instances"""
OrderLineItem = apps.get_model('order', line_item_ref)
OrderExtraLine = apps.get_model('order', extra_line_ref)
items_to_change = OrderLineItem.objects.filter(part=None)
if items_to_change.count() == 0:
return
print(f'\nFound {items_to_change.count()} old {line_item_ref} instance(s)')
print(f'Starting to convert - currently at {OrderExtraLine.objects.all().count()} {extra_line_ref} / {OrderLineItem.objects.all().count()} {line_item_ref} instance(s)')
for lineItem in items_to_change:
newitem = OrderExtraLine(
order=lineItem.order,
notes=lineItem.notes,
price=getattr(lineItem, price_ref),
quantity=lineItem.quantity,
reference=lineItem.reference,
)
newitem.context = {'migration': serializers.serialize('json', [lineItem, ])}
newitem.save()
lineItem.delete()
print(f'Done converting line items - now at {OrderExtraLine.objects.all().count()} {extra_line_ref} / {OrderLineItem.objects.all().count()} {line_item_ref} instance(s)')
def _reconvert_model(apps, line_item_ref, extra_line_ref):
"""Convert ExtraLine instances back to OrderLineItem instances"""
OrderLineItem = apps.get_model('order', line_item_ref)
OrderExtraLine = apps.get_model('order', extra_line_ref)
print(f'\nStarting to convert - currently at {OrderExtraLine.objects.all().count()} {extra_line_ref} / {OrderLineItem.objects.all().count()} {line_item_ref} instance(s)')
for extra_line in OrderExtraLine.objects.all():
# regenreate item
if extra_line.context:
context_string = getattr(extra_line.context, 'migration')
if not context_string:
continue
[item.save() for item in serializers.deserialize('json', context_string)]
extra_line.delete()
print(f'Done converting line items - now at {OrderExtraLine.objects.all().count()} {extra_line_ref} / {OrderLineItem.objects.all().count()} {line_item_ref} instance(s)')
def convert_line_items(apps, schema_editor):
"""convert line items"""
_convert_model(apps, 'PurchaseOrderLineItem', 'PurchaseOrderExtraLine', 'purchase_price')
_convert_model(apps, 'SalesOrderLineItem', 'SalesOrderExtraLine', 'sale_price')
def nunconvert_line_items(apps, schema_editor): # pragma: no cover
"""reconvert line items"""
_reconvert_model(apps, 'PurchaseOrderLineItem', 'PurchaseOrderExtraLine')
_reconvert_model(apps, 'SalesOrderLineItem', 'SalesOrderExtraLine')
class Migration(migrations.Migration):
dependencies = [
('order', '0063_alter_purchaseorderlineitem_unique_together'),
]
operations = [
migrations.CreateModel(
name='SalesOrderExtraLine',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('quantity', InvenTree.fields.RoundingDecimalField(decimal_places=5, default=1, help_text='Item quantity', max_digits=15, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Quantity')),
('reference', models.CharField(blank=True, help_text='Line item reference', max_length=100, verbose_name='Reference')),
('notes', models.CharField(blank=True, help_text='Line item notes', max_length=500, verbose_name='Notes')),
('target_date', models.DateField(blank=True, help_text='Target shipping date for this line item', null=True, verbose_name='Target Date')),
('context', models.JSONField(blank=True, help_text='Additional context for this line', null=True, verbose_name='Context')),
('price_currency', djmoney.models.fields.CurrencyField(choices=[], default='', editable=False, max_length=3)),
('price', InvenTree.fields.InvenTreeModelMoneyField(blank=True, currency_choices=[], decimal_places=4, default_currency='', help_text='Unit price', max_digits=19, null=True, validators=[djmoney.models.validators.MinMoneyValidator(0)], verbose_name='Price')),
('order', models.ForeignKey(help_text='Sales Order', on_delete=django.db.models.deletion.CASCADE, related_name='extra_lines', to='order.salesorder', verbose_name='Order')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='PurchaseOrderExtraLine',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('quantity', InvenTree.fields.RoundingDecimalField(decimal_places=5, default=1, help_text='Item quantity', max_digits=15, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Quantity')),
('reference', models.CharField(blank=True, help_text='Line item reference', max_length=100, verbose_name='Reference')),
('notes', models.CharField(blank=True, help_text='Line item notes', max_length=500, verbose_name='Notes')),
('target_date', models.DateField(blank=True, help_text='Target shipping date for this line item', null=True, verbose_name='Target Date')),
('context', models.JSONField(blank=True, help_text='Additional context for this line', null=True, verbose_name='Context')),
('price_currency', djmoney.models.fields.CurrencyField(choices=[], default='', editable=False, max_length=3)),
('price', InvenTree.fields.InvenTreeModelMoneyField(blank=True, currency_choices=[], decimal_places=4, default_currency='', help_text='Unit price', max_digits=19, null=True, validators=[djmoney.models.validators.MinMoneyValidator(0)], verbose_name='Price')),
('order', models.ForeignKey(help_text='Purchase Order', on_delete=django.db.models.deletion.CASCADE, related_name='extra_lines', to='order.purchaseorder', verbose_name='Order')),
],
options={
'abstract': False,
},
),
migrations.RunPython(convert_line_items, reverse_code=nunconvert_line_items),
]

View File

@ -0,0 +1,20 @@
# Generated by Django 3.2.12 on 2022-03-28 22:02
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('company', '0042_supplierpricebreak_updated'),
('order', '0064_purchaseorderextraline_salesorderextraline'),
]
operations = [
migrations.AlterField(
model_name='purchaseorderlineitem',
name='part',
field=models.ForeignKey(help_text='Supplier part', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='purchase_order_line_items', to='company.supplierpart', verbose_name='Part'),
),
]

View File

@ -5,6 +5,7 @@ Order model definitions
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import os import os
from datetime import datetime from datetime import datetime
from decimal import Decimal from decimal import Decimal
@ -16,11 +17,15 @@ from django.core.validators import MinValueValidator
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.urls import reverse from django.urls import reverse
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
from markdownx.models import MarkdownxField from markdownx.models import MarkdownxField
from mptt.models import TreeForeignKey from mptt.models import TreeForeignKey
from djmoney.contrib.exchange.models import convert_money
from djmoney.money import Money
from common.settings import currency_code_default
from users import models as UserModels from users import models as UserModels
from part import models as PartModels from part import models as PartModels
from stock import models as stock_models from stock import models as stock_models
@ -44,7 +49,7 @@ def get_next_po_number():
order = PurchaseOrder.objects.exclude(reference=None).last() order = PurchaseOrder.objects.exclude(reference=None).last()
attempts = set([order.reference]) attempts = {order.reference}
reference = order.reference reference = order.reference
@ -73,7 +78,7 @@ def get_next_so_number():
order = SalesOrder.objects.exclude(reference=None).last() order = SalesOrder.objects.exclude(reference=None).last()
attempts = set([order.reference]) attempts = {order.reference}
reference = order.reference reference = order.reference
@ -146,6 +151,25 @@ class Order(ReferenceIndexingMixin):
notes = MarkdownxField(blank=True, verbose_name=_('Notes'), help_text=_('Order notes')) notes = MarkdownxField(blank=True, verbose_name=_('Notes'), help_text=_('Order notes'))
def get_total_price(self):
"""
Calculates the total price of all order lines
"""
target_currency = currency_code_default()
total = Money(0, target_currency)
# gather name reference
price_ref = 'sale_price' if isinstance(self, SalesOrder) else 'purchase_price'
# order items
total += sum(a.quantity * convert_money(getattr(a, price_ref), target_currency) for a in self.lines.all() if getattr(a, price_ref))
# extra lines
total += sum(a.quantity * convert_money(a.price, target_currency) for a in self.extra_lines.all() if a.price)
# set decimal-places
total.decimal_places = 4
return total
class PurchaseOrder(Order): class PurchaseOrder(Order):
""" A PurchaseOrder represents goods shipped inwards from an external supplier. """ A PurchaseOrder represents goods shipped inwards from an external supplier.
@ -286,7 +310,7 @@ class PurchaseOrder(Order):
raise ValidationError({'supplier': _("Part supplier must match PO supplier")}) raise ValidationError({'supplier': _("Part supplier must match PO supplier")})
if group: if group:
# Check if there is already a matching line item (for this PO) # Check if there is already a matching line item (for this PurchaseOrder)
matches = self.lines.filter(part=supplier_part) matches = self.lines.filter(part=supplier_part)
if matches.count() > 0: if matches.count() > 0:
@ -401,7 +425,7 @@ class PurchaseOrder(Order):
@transaction.atomic @transaction.atomic
def receive_line_item(self, line, location, quantity, user, status=StockStatus.OK, **kwargs): def receive_line_item(self, line, location, quantity, user, status=StockStatus.OK, **kwargs):
""" """
Receive a line item (or partial line item) against this PO Receive a line item (or partial line item) against this PurchaseOrder
""" """
# Extract optional batch code for the new stock item # Extract optional batch code for the new stock item
@ -852,12 +876,44 @@ class OrderLineItem(models.Model):
) )
class OrderExtraLine(OrderLineItem):
"""
Abstract Model for a single ExtraLine in a Order
Attributes:
price: The unit sale price for this OrderLineItem
"""
class Meta:
abstract = True
unique_together = [
]
context = models.JSONField(
blank=True, null=True,
verbose_name=_('Context'),
help_text=_('Additional context for this line'),
)
price = InvenTreeModelMoneyField(
max_digits=19,
decimal_places=4,
null=True, blank=True,
verbose_name=_('Price'),
help_text=_('Unit price'),
)
def price_converted(self):
return convert_money(self.price, currency_code_default())
def price_converted_currency(self):
return currency_code_default()
class PurchaseOrderLineItem(OrderLineItem): class PurchaseOrderLineItem(OrderLineItem):
""" Model for a purchase order line item. """ Model for a purchase order line item.
Attributes: Attributes:
order: Reference to a PurchaseOrder object order: Reference to a PurchaseOrder object
""" """
class Meta: class Meta:
@ -904,11 +960,9 @@ class PurchaseOrderLineItem(OrderLineItem):
else: else:
return self.part.part return self.part.part
# TODO - Function callback for when the SupplierPart is deleted?
part = models.ForeignKey( part = models.ForeignKey(
SupplierPart, on_delete=models.SET_NULL, SupplierPart, on_delete=models.SET_NULL,
blank=True, null=True, blank=False, null=True,
related_name='purchase_order_line_items', related_name='purchase_order_line_items',
verbose_name=_('Part'), verbose_name=_('Part'),
help_text=_("Supplier part"), help_text=_("Supplier part"),
@ -961,6 +1015,21 @@ class PurchaseOrderLineItem(OrderLineItem):
return max(r, 0) return max(r, 0)
class PurchaseOrderExtraLine(OrderExtraLine):
"""
Model for a single ExtraLine in a PurchaseOrder
Attributes:
order: Link to the PurchaseOrder that this line belongs to
title: title of line
price: The unit price for this OrderLine
"""
@staticmethod
def get_api_url():
return reverse('api-po-extra-line-list')
order = models.ForeignKey(PurchaseOrder, on_delete=models.CASCADE, related_name='extra_lines', verbose_name=_('Order'), help_text=_('Purchase Order'))
class SalesOrderLineItem(OrderLineItem): class SalesOrderLineItem(OrderLineItem):
""" """
Model for a single LineItem in a SalesOrder Model for a single LineItem in a SalesOrder
@ -1164,6 +1233,21 @@ class SalesOrderShipment(models.Model):
trigger_event('salesordershipment.completed', id=self.pk) trigger_event('salesordershipment.completed', id=self.pk)
class SalesOrderExtraLine(OrderExtraLine):
"""
Model for a single ExtraLine in a SalesOrder
Attributes:
order: Link to the SalesOrder that this line belongs to
title: title of line
price: The unit price for this OrderLine
"""
@staticmethod
def get_api_url():
return reverse('api-so-extra-line-list')
order = models.ForeignKey(SalesOrder, on_delete=models.CASCADE, related_name='extra_lines', verbose_name=_('Order'), help_text=_('Sales Order'))
class SalesOrderAllocation(models.Model): class SalesOrderAllocation(models.Model):
""" """
This model is used to 'allocate' stock items to a SalesOrder. This model is used to 'allocate' stock items to a SalesOrder.

View File

@ -7,7 +7,7 @@ from __future__ import unicode_literals
from decimal import Decimal from decimal import Decimal
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.core.exceptions import ValidationError as DjangoValidationError from django.core.exceptions import ValidationError as DjangoValidationError
from django.db import models, transaction from django.db import models, transaction
@ -40,7 +40,64 @@ import stock.serializers
from users.serializers import OwnerSerializer from users.serializers import OwnerSerializer
class POSerializer(ReferenceIndexingSerializerMixin, InvenTreeModelSerializer): class AbstractOrderSerializer(serializers.Serializer):
"""
Abstract field definitions for OrderSerializers
"""
total_price = InvenTreeMoneySerializer(
source='get_total_price',
allow_null=True,
read_only=True,
)
total_price_string = serializers.CharField(source='get_total_price', read_only=True)
class AbstractExtraLineSerializer(serializers.Serializer):
""" Abstract Serializer for a ExtraLine object """
def __init__(self, *args, **kwargs):
order_detail = kwargs.pop('order_detail', False)
super().__init__(*args, **kwargs)
if order_detail is not True:
self.fields.pop('order_detail')
quantity = serializers.FloatField()
price = InvenTreeMoneySerializer(
allow_null=True
)
price_string = serializers.CharField(source='price', read_only=True)
price_currency = serializers.ChoiceField(
choices=currency_code_mappings(),
help_text=_('Price currency'),
)
class AbstractExtraLineMeta:
"""
Abstract Meta for ExtraLine
"""
fields = [
'pk',
'quantity',
'reference',
'notes',
'context',
'order',
'order_detail',
'price',
'price_currency',
'price_string',
]
class PurchaseOrderSerializer(AbstractOrderSerializer, ReferenceIndexingSerializerMixin, InvenTreeModelSerializer):
""" Serializer for a PurchaseOrder object """ """ Serializer for a PurchaseOrder object """
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -110,6 +167,8 @@ class POSerializer(ReferenceIndexingSerializerMixin, InvenTreeModelSerializer):
'status_text', 'status_text',
'target_date', 'target_date',
'notes', 'notes',
'total_price',
'total_price_string',
] ]
read_only_fields = [ read_only_fields = [
@ -120,7 +179,7 @@ class POSerializer(ReferenceIndexingSerializerMixin, InvenTreeModelSerializer):
] ]
class POLineItemSerializer(InvenTreeModelSerializer): class PurchaseOrderLineItemSerializer(InvenTreeModelSerializer):
@staticmethod @staticmethod
def annotate_queryset(queryset): def annotate_queryset(queryset):
@ -187,7 +246,7 @@ class POLineItemSerializer(InvenTreeModelSerializer):
help_text=_('Purchase price currency'), help_text=_('Purchase price currency'),
) )
order_detail = POSerializer(source='order', read_only=True, many=False) order_detail = PurchaseOrderSerializer(source='order', read_only=True, many=False)
class Meta: class Meta:
model = order.models.PurchaseOrderLineItem model = order.models.PurchaseOrderLineItem
@ -214,7 +273,16 @@ class POLineItemSerializer(InvenTreeModelSerializer):
] ]
class POLineItemReceiveSerializer(serializers.Serializer): class PurchaseOrderExtraLineSerializer(AbstractExtraLineSerializer, InvenTreeModelSerializer):
""" Serializer for a PurchaseOrderExtraLine object """
order_detail = PurchaseOrderSerializer(source='order', many=False, read_only=True)
class Meta(AbstractExtraLineMeta):
model = order.models.PurchaseOrderExtraLine
class PurchaseOrderLineItemReceiveSerializer(serializers.Serializer):
""" """
A serializer for receiving a single purchase order line item against a purchase order A serializer for receiving a single purchase order line item against a purchase order
""" """
@ -344,12 +412,12 @@ class POLineItemReceiveSerializer(serializers.Serializer):
return data return data
class POReceiveSerializer(serializers.Serializer): class PurchaseOrderReceiveSerializer(serializers.Serializer):
""" """
Serializer for receiving items against a purchase order Serializer for receiving items against a purchase order
""" """
items = POLineItemReceiveSerializer(many=True) items = PurchaseOrderLineItemReceiveSerializer(many=True)
location = serializers.PrimaryKeyRelatedField( location = serializers.PrimaryKeyRelatedField(
queryset=stock.models.StockLocation.objects.all(), queryset=stock.models.StockLocation.objects.all(),
@ -444,7 +512,7 @@ class POReceiveSerializer(serializers.Serializer):
] ]
class POAttachmentSerializer(InvenTreeAttachmentSerializer): class PurchaseOrderAttachmentSerializer(InvenTreeAttachmentSerializer):
""" """
Serializers for the PurchaseOrderAttachment model Serializers for the PurchaseOrderAttachment model
""" """
@ -467,7 +535,7 @@ class POAttachmentSerializer(InvenTreeAttachmentSerializer):
] ]
class SalesOrderSerializer(ReferenceIndexingSerializerMixin, InvenTreeModelSerializer): class SalesOrderSerializer(AbstractOrderSerializer, ReferenceIndexingSerializerMixin, InvenTreeModelSerializer):
""" """
Serializers for the SalesOrder object Serializers for the SalesOrder object
""" """
@ -535,6 +603,8 @@ class SalesOrderSerializer(ReferenceIndexingSerializerMixin, InvenTreeModelSeria
'status_text', 'status_text',
'shipment_date', 'shipment_date',
'target_date', 'target_date',
'total_price',
'total_price_string',
] ]
read_only_fields = [ read_only_fields = [
@ -612,7 +682,7 @@ class SalesOrderAllocationSerializer(InvenTreeModelSerializer):
] ]
class SOLineItemSerializer(InvenTreeModelSerializer): class SalesOrderLineItemSerializer(InvenTreeModelSerializer):
""" Serializer for a SalesOrderLineItem object """ """ Serializer for a SalesOrderLineItem object """
@staticmethod @staticmethod
@ -862,7 +932,7 @@ class SalesOrderCompleteSerializer(serializers.Serializer):
order.complete_order(user) order.complete_order(user)
class SOSerialAllocationSerializer(serializers.Serializer): class SalesOrderSerialAllocationSerializer(serializers.Serializer):
""" """
DRF serializer for allocation of serial numbers against a sales order / shipment DRF serializer for allocation of serial numbers against a sales order / shipment
""" """
@ -1025,7 +1095,7 @@ class SOSerialAllocationSerializer(serializers.Serializer):
) )
class SOShipmentAllocationSerializer(serializers.Serializer): class SalesOrderShipmentAllocationSerializer(serializers.Serializer):
""" """
DRF serializer for allocation of stock items against a sales order / shipment DRF serializer for allocation of stock items against a sales order / shipment
""" """
@ -1099,7 +1169,16 @@ class SOShipmentAllocationSerializer(serializers.Serializer):
) )
class SOAttachmentSerializer(InvenTreeAttachmentSerializer): class SalesOrderExtraLineSerializer(AbstractExtraLineSerializer, InvenTreeModelSerializer):
""" Serializer for a SalesOrderExtraLine object """
order_detail = SalesOrderSerializer(source='order', many=False, read_only=True)
class Meta(AbstractExtraLineMeta):
model = order.models.SalesOrderExtraLine
class SalesOrderAttachmentSerializer(InvenTreeAttachmentSerializer):
""" """
Serializers for the SalesOrderAttachment model Serializers for the SalesOrderAttachment model
""" """

View File

@ -171,6 +171,12 @@ src="{% static 'img/blank_image.png' %}"
<td>{{ order.responsible }}</td> <td>{{ order.responsible }}</td>
</tr> </tr>
{% endif %} {% endif %}
<tr>
<td><span class='fas fa-dollar-sign'></span></td>
<td>{% trans "Total cost" %}</td>
<td id="poTotalPrice">{{ order.get_total_price }}</td>
</tr>
</table> </table>
{% endblock %} {% endblock %}

View File

@ -42,6 +42,29 @@
<table class='table table-striped table-condensed' id='po-line-table' data-toolbar='#order-toolbar-buttons'> <table class='table table-striped table-condensed' id='po-line-table' data-toolbar='#order-toolbar-buttons'>
</table> </table>
</div> </div>
<div class='panel-heading'>
<div class='d-flex flex-wrap'>
<h4>{% trans "Extra Lines" %}</h4>
{% include "spacer.html" %}
<div class='btn-group' role='group'>
{% if roles.purchase_order.change and order.status == PurchaseOrderStatus.PENDING %}
<button type='button' class='btn btn-success' id='new-po-extra-line'>
<span class='fas fa-plus-circle'></span> {% trans "Add Extra Line" %}
</button>
{% endif %}
</div>
</div>
</div>
<div class='panel-content'>
<div id='order-extra-toolbar-buttons' class='btn-group' style='float: right;'>
<div class='btn-group'>
{% include "filter_list.html" with id="purchase-order-extra-lines" %}
</div>
</div>
<table class='table table-striped table-condensed' id='po-extra-lines-table' data-toolbar='#order-extra-toolbar-buttons'>
</table>
</div>
</div> </div>
<div class='panel panel-hidden' id='panel-received-items'> <div class='panel panel-hidden' id='panel-received-items'>
@ -200,6 +223,37 @@ loadPurchaseOrderLineItemTable('#po-line-table', {
{% endif %} {% endif %}
}); });
$("#new-po-extra-line").click(function() {
var fields = extraLineFields({
order: {{ order.pk }},
});
constructForm('{% url "api-po-extra-line-list" %}', {
fields: fields,
method: 'POST',
title: '{% trans "Add Order Line" %}',
onSuccess: function() {
$("#po-extra-lines-table").bootstrapTable("refresh");
},
});
});
loadPurchaseOrderExtraLineTable(
'#po-extra-lines-table',
{
order: {{ order.pk }},
status: {{ order.status }},
}
);
loadOrderTotal(
'#poTotalPrice',
{
url: '{% url "api-po-detail" order.pk %}',
}
);
enableSidebar('purchaseorder'); enableSidebar('purchaseorder');
{% endblock %} {% endblock %}

Some files were not shown because too many files have changed in this diff Show More