mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge branch 'inventree:master' into matmair/issue2788
This commit is contained in:
commit
0c338cb2ae
67
.github/workflows/translations.yml
vendored
67
.github/workflows/translations.yml
vendored
@ -19,42 +19,31 @@ jobs:
|
||||
INVENTREE_STATIC_ROOT: ./static
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Get Current Translations
|
||||
run: |
|
||||
git fetch
|
||||
git checkout origin/l10 -- `git ls-tree origin/l10 -r --name-only | grep ".po"`
|
||||
git reset
|
||||
- name: Set up Python 3.7
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: 3.7
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y gettext
|
||||
pip3 install invoke
|
||||
invoke install
|
||||
- name: Make Translations
|
||||
run: |
|
||||
invoke translate
|
||||
- name: stash changes
|
||||
run: |
|
||||
git stash
|
||||
- name: Checkout Translation Branch
|
||||
uses: actions/checkout@v2.3.4
|
||||
with:
|
||||
ref: l10
|
||||
- name: Commit files
|
||||
run: |
|
||||
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
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v2
|
||||
- name: Set up Python 3.7
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: 3.7
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y gettext
|
||||
pip3 install invoke
|
||||
invoke install
|
||||
- name: Make Translations
|
||||
run: |
|
||||
invoke translate
|
||||
- name: Commit files
|
||||
run: |
|
||||
git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git config --local user.name "github-actions[bot]"
|
||||
git checkout -b l10_local
|
||||
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
|
||||
force: true
|
||||
|
3
.github/workflows/welcome.yml
vendored
3
.github/workflows/welcome.yml
vendored
@ -9,6 +9,9 @@ on:
|
||||
jobs:
|
||||
run:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- uses: actions/first-interaction@v1
|
||||
with:
|
||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -85,3 +85,6 @@ maintenance_mode_state.txt
|
||||
|
||||
# plugin dev directory
|
||||
plugins/
|
||||
|
||||
# Compiled translation files
|
||||
*.mo
|
||||
|
@ -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)
|
||||
- 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
|
||||
|
||||
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.
|
||||
|
@ -5,7 +5,7 @@ Main JSON interface views
|
||||
# -*- coding: utf-8 -*-
|
||||
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.http import JsonResponse
|
||||
|
||||
|
@ -6,7 +6,7 @@ import sys
|
||||
|
||||
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.db import models as models
|
||||
|
@ -7,7 +7,7 @@ from __future__ import unicode_literals
|
||||
from urllib.parse import urlencode
|
||||
import logging
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django import forms
|
||||
from django.contrib.auth.models import User, Group
|
||||
from django.conf import settings
|
||||
@ -319,7 +319,7 @@ class CustomSocialAccountAdapter(RegistratonMixin, DefaultSocialAccountAdapter):
|
||||
redirect_url = reverse('two-factor-authenticate')
|
||||
# Add GET parameters to the URL if they exist.
|
||||
if request.GET:
|
||||
redirect_url += u'?' + urlencode(request.GET)
|
||||
redirect_url += '?' + urlencode(request.GET)
|
||||
|
||||
raise ImmediateHttpResponse(
|
||||
response=HttpResponseRedirect(redirect_url)
|
||||
|
@ -13,7 +13,7 @@ from decimal import Decimal, InvalidOperation
|
||||
from wsgiref.util import FileWrapper
|
||||
from django.http import StreamingHttpResponse
|
||||
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
|
||||
|
||||
@ -432,7 +432,7 @@ def extract_serial_numbers(serials, expected_quantity, next_number: int):
|
||||
next_number += 1
|
||||
|
||||
# Split input string by whitespace or comma (,) characters
|
||||
groups = re.split("[\s,]+", serials)
|
||||
groups = re.split(r"[\s,]+", serials)
|
||||
|
||||
numbers = []
|
||||
errors = []
|
||||
|
@ -1,7 +1,7 @@
|
||||
from django.shortcuts import HttpResponseRedirect
|
||||
from django.urls import reverse_lazy, Resolver404
|
||||
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.contrib.auth.middleware import PersistentRemoteUserMiddleware
|
||||
|
||||
@ -85,14 +85,14 @@ class AuthRequiredMiddleware(object):
|
||||
if path not in urls and not path.startswith('/api/'):
|
||||
# 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)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
url_matcher = url('', include(frontendpatterns))
|
||||
url_matcher = re_path('', include(frontendpatterns))
|
||||
|
||||
|
||||
class Check2FAMiddleware(BaseRequire2FAMiddleware):
|
||||
|
@ -15,7 +15,7 @@ from collections import OrderedDict
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
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 djmoney.contrib.django_rest_framework.fields import MoneyField
|
||||
|
@ -306,6 +306,10 @@ if DEBUG and CONFIG.get('debug_toolbar', False): # pragma: no cover
|
||||
INSTALLED_APPS.append('debug_toolbar')
|
||||
MIDDLEWARE.append('debug_toolbar.middleware.DebugToolbarMiddleware')
|
||||
|
||||
# Allow secure http developer server in debug mode
|
||||
if DEBUG:
|
||||
INSTALLED_APPS.append('sslserver')
|
||||
|
||||
# InvenTree URL configuration
|
||||
|
||||
# Base URL for admin pages (default="admin")
|
||||
|
99
InvenTree/InvenTree/static/script/qr-scanner-worker.min.js
vendored
Normal file
99
InvenTree/InvenTree/static/script/qr-scanner-worker.min.js
vendored
Normal 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
|
32
InvenTree/InvenTree/static/script/qr-scanner.umd.min.js
vendored
Normal file
32
InvenTree/InvenTree/static/script/qr-scanner.umd.min.js
vendored
Normal 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
|
@ -3,7 +3,7 @@ Provides system status functionality checks.
|
||||
"""
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.utils import timezone
|
||||
|
||||
import logging
|
||||
|
@ -1,4 +1,4 @@
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class StatusCode:
|
||||
|
@ -72,7 +72,7 @@ class ViewTests(TestCase):
|
||||
"""
|
||||
|
||||
# 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()
|
||||
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
import json
|
||||
from test.support import EnvironmentVarGuard
|
||||
|
||||
@ -186,7 +185,7 @@ class TestDownloadFile(TestCase):
|
||||
|
||||
def test_download(self):
|
||||
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):
|
||||
|
@ -4,8 +4,7 @@ Top-level URL lookup for InvenTree application.
|
||||
Passes URL lookup downstream to each app as required.
|
||||
"""
|
||||
|
||||
from django.conf.urls import url, include
|
||||
from django.urls import path
|
||||
from django.urls import include, path, re_path
|
||||
from django.contrib import admin
|
||||
|
||||
from company.urls import company_urls
|
||||
@ -56,144 +55,144 @@ apipatterns = []
|
||||
|
||||
if settings.PLUGINS_ENABLED:
|
||||
apipatterns.append(
|
||||
url(r'^plugin/', include(plugin_api_urls))
|
||||
re_path(r'^plugin/', include(plugin_api_urls))
|
||||
)
|
||||
|
||||
apipatterns += [
|
||||
url(r'^barcode/', include(barcode_api_urls)),
|
||||
url(r'^settings/', include(settings_api_urls)),
|
||||
url(r'^part/', include(part_api_urls)),
|
||||
url(r'^bom/', include(bom_api_urls)),
|
||||
url(r'^company/', include(company_api_urls)),
|
||||
url(r'^stock/', include(stock_api_urls)),
|
||||
url(r'^build/', include(build_api_urls)),
|
||||
url(r'^order/', include(order_api_urls)),
|
||||
url(r'^label/', include(label_api_urls)),
|
||||
url(r'^report/', include(report_api_urls)),
|
||||
re_path(r'^barcode/', include(barcode_api_urls)),
|
||||
re_path(r'^settings/', include(settings_api_urls)),
|
||||
re_path(r'^part/', include(part_api_urls)),
|
||||
re_path(r'^bom/', include(bom_api_urls)),
|
||||
re_path(r'^company/', include(company_api_urls)),
|
||||
re_path(r'^stock/', include(stock_api_urls)),
|
||||
re_path(r'^build/', include(build_api_urls)),
|
||||
re_path(r'^order/', include(order_api_urls)),
|
||||
re_path(r'^label/', include(label_api_urls)),
|
||||
re_path(r'^report/', include(report_api_urls)),
|
||||
|
||||
# User URLs
|
||||
url(r'^user/', include(user_urls)),
|
||||
re_path(r'^user/', include(user_urls)),
|
||||
|
||||
# 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
|
||||
path('', include(common_api_urls)),
|
||||
|
||||
# InvenTree information endpoint
|
||||
url(r'^$', InfoView.as_view(), name='api-inventree-info'),
|
||||
path('', InfoView.as_view(), name='api-inventree-info'),
|
||||
|
||||
# Unknown endpoint
|
||||
url(r'^.*$', NotFoundView.as_view(), name='api-404'),
|
||||
re_path(r'^.*$', NotFoundView.as_view(), name='api-404'),
|
||||
]
|
||||
|
||||
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'),
|
||||
url(r'^currencies-refresh/', CurrencyRefreshView.as_view(), name='settings-currencies-refresh'),
|
||||
re_path(r'^appearance/?', AppearanceSelectView.as_view(), name='settings-appearance'),
|
||||
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
|
||||
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 = [
|
||||
|
||||
# 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
|
||||
dynamic_javascript_urls = [
|
||||
url(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'),
|
||||
url(r'^settings.js', DynamicJsView.as_view(template_name='js/dynamic/settings.js'), name='settings.js'),
|
||||
re_path(r'^calendar.js', DynamicJsView.as_view(template_name='js/dynamic/calendar.js'), name='calendar.js'),
|
||||
re_path(r'^nav.js', DynamicJsView.as_view(template_name='js/dynamic/nav.js'), name='nav.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
|
||||
translated_javascript_urls = [
|
||||
url(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'),
|
||||
url(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'),
|
||||
url(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'),
|
||||
url(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'),
|
||||
url(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'),
|
||||
url(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'),
|
||||
url(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'),
|
||||
url(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'),
|
||||
url(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'),
|
||||
url(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'),
|
||||
url(r'^notification.js', DynamicJsView.as_view(template_name='js/translated/notification.js'), name='notification.js'),
|
||||
re_path(r'^api.js', DynamicJsView.as_view(template_name='js/translated/api.js'), name='api.js'),
|
||||
re_path(r'^attachment.js', DynamicJsView.as_view(template_name='js/translated/attachment.js'), name='attachment.js'),
|
||||
re_path(r'^barcode.js', DynamicJsView.as_view(template_name='js/translated/barcode.js'), name='barcode.js'),
|
||||
re_path(r'^bom.js', DynamicJsView.as_view(template_name='js/translated/bom.js'), name='bom.js'),
|
||||
re_path(r'^build.js', DynamicJsView.as_view(template_name='js/translated/build.js'), name='build.js'),
|
||||
re_path(r'^company.js', DynamicJsView.as_view(template_name='js/translated/company.js'), name='company.js'),
|
||||
re_path(r'^filters.js', DynamicJsView.as_view(template_name='js/translated/filters.js'), name='filters.js'),
|
||||
re_path(r'^forms.js', DynamicJsView.as_view(template_name='js/translated/forms.js'), name='forms.js'),
|
||||
re_path(r'^helpers.js', DynamicJsView.as_view(template_name='js/translated/helpers.js'), name='helpers.js'),
|
||||
re_path(r'^label.js', DynamicJsView.as_view(template_name='js/translated/label.js'), name='label.js'),
|
||||
re_path(r'^model_renderers.js', DynamicJsView.as_view(template_name='js/translated/model_renderers.js'), name='model_renderers.js'),
|
||||
re_path(r'^modals.js', DynamicJsView.as_view(template_name='js/translated/modals.js'), name='modals.js'),
|
||||
re_path(r'^order.js', DynamicJsView.as_view(template_name='js/translated/order.js'), name='order.js'),
|
||||
re_path(r'^part.js', DynamicJsView.as_view(template_name='js/translated/part.js'), name='part.js'),
|
||||
re_path(r'^report.js', DynamicJsView.as_view(template_name='js/translated/report.js'), name='report.js'),
|
||||
re_path(r'^search.js', DynamicJsView.as_view(template_name='js/translated/search.js'), name='search.js'),
|
||||
re_path(r'^stock.js', DynamicJsView.as_view(template_name='js/translated/stock.js'), name='stock.js'),
|
||||
re_path(r'^plugin.js', DynamicJsView.as_view(template_name='js/translated/plugin.js'), name='plugin.js'),
|
||||
re_path(r'^tables.js', DynamicJsView.as_view(template_name='js/translated/tables.js'), name='tables.js'),
|
||||
re_path(r'^table_filters.js', DynamicJsView.as_view(template_name='js/translated/table_filters.js'), name='table_filters.js'),
|
||||
re_path(r'^notification.js', DynamicJsView.as_view(template_name='js/translated/notification.js'), name='notification.js'),
|
||||
]
|
||||
|
||||
backendpatterns = [
|
||||
# "Dynamic" javascript files which are rendered using InvenTree templating.
|
||||
url(r'^js/dynamic/', include(dynamic_javascript_urls)),
|
||||
url(r'^js/i18n/', include(translated_javascript_urls)),
|
||||
re_path(r'^js/dynamic/', include(dynamic_javascript_urls)),
|
||||
re_path(r'^js/i18n/', include(translated_javascript_urls)),
|
||||
|
||||
url(r'^auth/', include('rest_framework.urls', namespace='rest_framework')),
|
||||
url(r'^auth/?', auth_request),
|
||||
re_path(r'^auth/', include('rest_framework.urls', namespace='rest_framework')),
|
||||
re_path(r'^auth/?', auth_request),
|
||||
|
||||
url(r'^api/', include(apipatterns)),
|
||||
url(r'^api-doc/', include_docs_urls(title='InvenTree API')),
|
||||
re_path(r'^api/', include(apipatterns)),
|
||||
re_path(r'^api-doc/', include_docs_urls(title='InvenTree API')),
|
||||
|
||||
# 3rd party endpoints
|
||||
url(r'^markdownx/', include('markdownx.urls')),
|
||||
re_path(r'^markdownx/', include('markdownx.urls')),
|
||||
]
|
||||
|
||||
frontendpatterns = [
|
||||
url(r'^part/', include(part_urls)),
|
||||
url(r'^manufacturer-part/', include(manufacturer_part_urls)),
|
||||
url(r'^supplier-part/', include(supplier_part_urls)),
|
||||
re_path(r'^part/', include(part_urls)),
|
||||
re_path(r'^manufacturer-part/', include(manufacturer_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)),
|
||||
url(r'^order/', include(order_urls)),
|
||||
re_path(r'^company/', include(company_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'),
|
||||
url(r'^set-password/', SetPasswordView.as_view(), name='set-password'),
|
||||
re_path(r'^edit-user/', EditUserView.as_view(), name='edit-user'),
|
||||
re_path(r'^set-password/', SetPasswordView.as_view(), name='set-password'),
|
||||
|
||||
url(r'^index/', IndexView.as_view(), name='index'),
|
||||
url(r'^search/', SearchView.as_view(), name='search'),
|
||||
url(r'^stats/', DatabaseStatsView.as_view(), name='stats'),
|
||||
re_path(r'^index/', IndexView.as_view(), name='index'),
|
||||
re_path(r'^search/', SearchView.as_view(), name='search'),
|
||||
re_path(r'^stats/', DatabaseStatsView.as_view(), name='stats'),
|
||||
|
||||
# admin sites
|
||||
url(f'^{settings.INVENTREE_ADMIN_URL}/error_log/', include('error_report.urls')),
|
||||
url(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}/error_log/', include('error_report.urls')),
|
||||
re_path(f'^{settings.INVENTREE_ADMIN_URL}/shell/', include('django_admin_shell.urls')),
|
||||
re_path(f'^{settings.INVENTREE_ADMIN_URL}/', admin.site.urls, name='inventree-admin'),
|
||||
|
||||
# DB user sessions
|
||||
url(r'^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', ),
|
||||
path('accounts/sessions/other/delete/', view=CustomSessionDeleteOtherView.as_view(), name='session_delete_other', ),
|
||||
re_path(r'^accounts/sessions/(?P<pk>\w+)/delete/$', view=CustomSessionDeleteView.as_view(), name='session_delete', ),
|
||||
|
||||
# Single Sign On / allauth
|
||||
# overrides of urlpatterns
|
||||
url(r'^accounts/email/', CustomEmailView.as_view(), name='account_email'),
|
||||
url(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"),
|
||||
url(r'^accounts/', include('allauth_2fa.urls')), # MFA support
|
||||
url(r'^accounts/', include('allauth.urls')), # included urlpatterns
|
||||
re_path(r'^accounts/email/', CustomEmailView.as_view(), name='account_email'),
|
||||
re_path(r'^accounts/social/connections/', CustomConnectionsView.as_view(), name='socialaccount_connections'),
|
||||
re_path(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/', include('allauth_2fa.urls')), # MFA support
|
||||
re_path(r'^accounts/', include('allauth.urls')), # included urlpatterns
|
||||
]
|
||||
|
||||
# Append custom plugin URLs (if plugin support is enabled)
|
||||
@ -201,8 +200,8 @@ if settings.PLUGINS_ENABLED:
|
||||
frontendpatterns.append(get_plugin_urls())
|
||||
|
||||
urlpatterns = [
|
||||
url('', include(frontendpatterns)),
|
||||
url('', include(backendpatterns)),
|
||||
re_path('', include(frontendpatterns)),
|
||||
re_path('', include(backendpatterns)),
|
||||
]
|
||||
|
||||
# Server running in "DEBUG" mode?
|
||||
@ -221,4 +220,4 @@ if settings.DEBUG:
|
||||
] + urlpatterns
|
||||
|
||||
# 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')]
|
||||
|
@ -1,8 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from django.urls import reverse
|
||||
from django.conf.urls import url
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.urls import path, re_path
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework import permissions
|
||||
@ -240,8 +240,8 @@ class BarcodeAssign(APIView):
|
||||
|
||||
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'
|
||||
url(r'^.*$', BarcodeScan.as_view(), name='api-barcode-scan'),
|
||||
re_path(r'^.*$', BarcodeScan.as_view(), name='api-barcode-scan'),
|
||||
]
|
||||
|
@ -5,7 +5,7 @@ JSON API for the Build app
|
||||
# -*- coding: utf-8 -*-
|
||||
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
|
||||
|
||||
@ -508,29 +508,29 @@ class BuildAttachmentDetail(generics.RetrieveUpdateDestroyAPIView, AttachmentMix
|
||||
build_api_urls = [
|
||||
|
||||
# Attachments
|
||||
url(r'^attachment/', include([
|
||||
url(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'^attachment/', include([
|
||||
re_path(r'^(?P<pk>\d+)/', BuildAttachmentDetail.as_view(), name='api-build-attachment-detail'),
|
||||
re_path(r'^.*$', BuildAttachmentList.as_view(), name='api-build-attachment-list'),
|
||||
])),
|
||||
|
||||
# Build Items
|
||||
url(r'^item/', include([
|
||||
url(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'^item/', include([
|
||||
re_path(r'^(?P<pk>\d+)/', BuildItemDetail.as_view(), name='api-build-item-detail'),
|
||||
re_path(r'^.*$', BuildItemList.as_view(), name='api-build-item-list'),
|
||||
])),
|
||||
|
||||
# Build Detail
|
||||
url(r'^(?P<pk>\d+)/', include([
|
||||
url(r'^allocate/', BuildAllocate.as_view(), name='api-build-allocate'),
|
||||
url(r'^auto-allocate/', BuildAutoAllocate.as_view(), name='api-build-auto-allocate'),
|
||||
url(r'^complete/', BuildOutputComplete.as_view(), name='api-build-output-complete'),
|
||||
url(r'^create-output/', BuildOutputCreate.as_view(), name='api-build-output-create'),
|
||||
url(r'^delete-outputs/', BuildOutputDelete.as_view(), name='api-build-output-delete'),
|
||||
url(r'^finish/', BuildFinish.as_view(), name='api-build-finish'),
|
||||
url(r'^unallocate/', BuildUnallocate.as_view(), name='api-build-unallocate'),
|
||||
url(r'^.*$', BuildDetail.as_view(), name='api-build-detail'),
|
||||
re_path(r'^(?P<pk>\d+)/', include([
|
||||
re_path(r'^allocate/', BuildAllocate.as_view(), name='api-build-allocate'),
|
||||
re_path(r'^auto-allocate/', BuildAutoAllocate.as_view(), name='api-build-auto-allocate'),
|
||||
re_path(r'^complete/', BuildOutputComplete.as_view(), name='api-build-output-complete'),
|
||||
re_path(r'^create-output/', BuildOutputCreate.as_view(), name='api-build-output-create'),
|
||||
re_path(r'^delete-outputs/', BuildOutputDelete.as_view(), name='api-build-output-delete'),
|
||||
re_path(r'^finish/', BuildFinish.as_view(), name='api-build-finish'),
|
||||
re_path(r'^unallocate/', BuildUnallocate.as_view(), name='api-build-unallocate'),
|
||||
re_path(r'^.*$', BuildDetail.as_view(), name='api-build-detail'),
|
||||
])),
|
||||
|
||||
# Build List
|
||||
url(r'^.*$', BuildList.as_view(), name='api-build-list'),
|
||||
re_path(r'^.*$', BuildList.as_view(), name='api-build-list'),
|
||||
]
|
||||
|
@ -6,7 +6,7 @@ Django Forms for interacting with Build objects
|
||||
|
||||
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 InvenTree.forms import HelperForm
|
||||
|
@ -17,8 +17,6 @@ def assign_bom_items(apps, schema_editor):
|
||||
BuildItem = apps.get_model('build', 'builditem')
|
||||
BomItem = apps.get_model('part', 'bomitem')
|
||||
Part = apps.get_model('part', 'part')
|
||||
|
||||
logger.info("Assigning BomItems to existing BuildItem objects")
|
||||
|
||||
count_valid = 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,
|
||||
# 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
|
||||
|
||||
try:
|
||||
|
@ -18,7 +18,7 @@ from django.db.models.functions import Coalesce
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch.dispatcher import receiver
|
||||
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
|
||||
|
||||
@ -53,7 +53,7 @@ def get_next_build_number():
|
||||
|
||||
build = Build.objects.exclude(reference=None).last()
|
||||
|
||||
attempts = set([build.reference])
|
||||
attempts = {build.reference}
|
||||
|
||||
reference = build.reference
|
||||
|
||||
|
@ -7,7 +7,7 @@ from __future__ import unicode_literals
|
||||
|
||||
from django.db import transaction
|
||||
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 BooleanField
|
||||
|
@ -4,7 +4,7 @@ from __future__ import unicode_literals
|
||||
from decimal import Decimal
|
||||
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 allauth.account.models import EmailAddress
|
||||
|
@ -2,20 +2,20 @@
|
||||
URL lookup for Build app
|
||||
"""
|
||||
|
||||
from django.conf.urls import url, include
|
||||
from django.urls import include, re_path
|
||||
|
||||
from . import views
|
||||
|
||||
build_detail_urls = [
|
||||
url(r'^cancel/', views.BuildCancel.as_view(), name='build-cancel'),
|
||||
url(r'^delete/', views.BuildDelete.as_view(), name='build-delete'),
|
||||
re_path(r'^cancel/', views.BuildCancel.as_view(), name='build-cancel'),
|
||||
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 = [
|
||||
|
||||
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'),
|
||||
]
|
||||
|
@ -5,7 +5,7 @@ Django views for interacting with Build objects
|
||||
# -*- coding: utf-8 -*-
|
||||
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 .models import Build
|
||||
|
@ -11,7 +11,7 @@ from django.http.response import HttpResponse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.urls import path
|
||||
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.response import Response
|
||||
@ -336,21 +336,21 @@ class NotificationReadAll(generics.RetrieveAPIView):
|
||||
|
||||
settings_api_urls = [
|
||||
# User settings
|
||||
url(r'^user/', include([
|
||||
re_path(r'^user/', include([
|
||||
# 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
|
||||
url(r'^.*$', UserSettingsList.as_view(), name='api-user-setting-list'),
|
||||
re_path(r'^.*$', UserSettingsList.as_view(), name='api-user-setting-list'),
|
||||
])),
|
||||
|
||||
# Global settings
|
||||
url(r'^global/', include([
|
||||
re_path(r'^global/', include([
|
||||
# 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
|
||||
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'),
|
||||
|
||||
# Notifications
|
||||
url(r'^notifications/', include([
|
||||
re_path(r'^notifications/', include([
|
||||
# Individual purchase order detail URLs
|
||||
url(r'^(?P<pk>\d+)/', include([
|
||||
url(r'^read/', NotificationRead.as_view(), name='api-notifications-read'),
|
||||
url(r'^unread/', NotificationUnread.as_view(), name='api-notifications-unread'),
|
||||
url(r'.*$', NotificationDetail.as_view(), name='api-notifications-detail'),
|
||||
re_path(r'^(?P<pk>\d+)/', include([
|
||||
re_path(r'^read/', NotificationRead.as_view(), name='api-notifications-read'),
|
||||
re_path(r'^unread/', NotificationUnread.as_view(), name='api-notifications-unread'),
|
||||
re_path(r'.*$', NotificationDetail.as_view(), name='api-notifications-detail'),
|
||||
])),
|
||||
# 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
|
||||
url(r'^.*$', NotificationList.as_view(), name='api-notifications-list'),
|
||||
re_path(r'^.*$', NotificationList.as_view(), name='api-notifications-list'),
|
||||
])),
|
||||
|
||||
]
|
||||
|
@ -33,7 +33,7 @@ from djmoney.contrib.exchange.exceptions import MissingRate
|
||||
|
||||
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.exceptions import ValidationError
|
||||
|
||||
@ -406,6 +406,13 @@ class BaseInvenTreeSetting(models.Model):
|
||||
|
||||
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)
|
||||
|
||||
if validator is not None:
|
||||
@ -455,7 +462,14 @@ class BaseInvenTreeSetting(models.Model):
|
||||
|
||||
if callable(validator):
|
||||
# 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):
|
||||
"""
|
||||
@ -629,6 +643,10 @@ class BaseInvenTreeSetting(models.Model):
|
||||
|
||||
return setting.get('protected', False)
|
||||
|
||||
@property
|
||||
def protected(self):
|
||||
return self.__class__.is_protected(self.key)
|
||||
|
||||
|
||||
def settings_group_options():
|
||||
"""
|
||||
@ -1011,48 +1029,56 @@ class InvenTreeSetting(BaseInvenTreeSetting):
|
||||
'default': True,
|
||||
'validator': bool,
|
||||
},
|
||||
|
||||
'LOGIN_ENABLE_REG': {
|
||||
'name': _('Enable registration'),
|
||||
'description': _('Enable self-registration for users on the login pages'),
|
||||
'default': False,
|
||||
'validator': bool,
|
||||
},
|
||||
|
||||
'LOGIN_ENABLE_SSO': {
|
||||
'name': _('Enable SSO'),
|
||||
'description': _('Enable SSO on the login pages'),
|
||||
'default': False,
|
||||
'validator': bool,
|
||||
},
|
||||
|
||||
'LOGIN_MAIL_REQUIRED': {
|
||||
'name': _('Email required'),
|
||||
'description': _('Require user to supply mail on signup'),
|
||||
'default': False,
|
||||
'validator': bool,
|
||||
},
|
||||
|
||||
'LOGIN_SIGNUP_SSO_AUTO': {
|
||||
'name': _('Auto-fill SSO users'),
|
||||
'description': _('Automatically fill out user-details from SSO account-data'),
|
||||
'default': True,
|
||||
'validator': bool,
|
||||
},
|
||||
|
||||
'LOGIN_SIGNUP_MAIL_TWICE': {
|
||||
'name': _('Mail twice'),
|
||||
'description': _('On signup ask users twice for their mail'),
|
||||
'default': False,
|
||||
'validator': bool,
|
||||
},
|
||||
|
||||
'LOGIN_SIGNUP_PWD_TWICE': {
|
||||
'name': _('Password twice'),
|
||||
'description': _('On signup ask users twice for their password'),
|
||||
'default': True,
|
||||
'validator': bool,
|
||||
},
|
||||
|
||||
'SIGNUP_GROUP': {
|
||||
'name': _('Group on signup'),
|
||||
'description': _('Group to which new users are assigned on registration'),
|
||||
'default': '',
|
||||
'choices': settings_group_options
|
||||
},
|
||||
|
||||
'LOGIN_ENFORCE_MFA': {
|
||||
'name': _('Enforce MFA'),
|
||||
'description': _('Users must use multifactor security.'),
|
||||
@ -1067,6 +1093,7 @@ class InvenTreeSetting(BaseInvenTreeSetting):
|
||||
'validator': bool,
|
||||
'requires_restart': True,
|
||||
},
|
||||
|
||||
# Settings for plugin mixin features
|
||||
'ENABLE_PLUGINS_URL': {
|
||||
'name': _('Enable URL integration'),
|
||||
@ -1075,6 +1102,7 @@ class InvenTreeSetting(BaseInvenTreeSetting):
|
||||
'validator': bool,
|
||||
'requires_restart': True,
|
||||
},
|
||||
|
||||
'ENABLE_PLUGINS_NAVIGATION': {
|
||||
'name': _('Enable navigation integration'),
|
||||
'description': _('Enable plugins to integrate into navigation'),
|
||||
@ -1082,6 +1110,7 @@ class InvenTreeSetting(BaseInvenTreeSetting):
|
||||
'validator': bool,
|
||||
'requires_restart': True,
|
||||
},
|
||||
|
||||
'ENABLE_PLUGINS_APP': {
|
||||
'name': _('Enable app integration'),
|
||||
'description': _('Enable plugins to add apps'),
|
||||
@ -1089,6 +1118,7 @@ class InvenTreeSetting(BaseInvenTreeSetting):
|
||||
'validator': bool,
|
||||
'requires_restart': True,
|
||||
},
|
||||
|
||||
'ENABLE_PLUGINS_SCHEDULE': {
|
||||
'name': _('Enable schedule integration'),
|
||||
'description': _('Enable plugins to run scheduled tasks'),
|
||||
@ -1096,6 +1126,7 @@ class InvenTreeSetting(BaseInvenTreeSetting):
|
||||
'validator': bool,
|
||||
'requires_restart': True,
|
||||
},
|
||||
|
||||
'ENABLE_PLUGINS_EVENTS': {
|
||||
'name': _('Enable event integration'),
|
||||
'description': _('Enable plugins to respond to internal events'),
|
||||
@ -1149,18 +1180,21 @@ class InvenTreeUserSetting(BaseInvenTreeSetting):
|
||||
'default': True,
|
||||
'validator': bool,
|
||||
},
|
||||
|
||||
'HOMEPAGE_CATEGORY_STARRED': {
|
||||
'name': _('Show subscribed categories'),
|
||||
'description': _('Show subscribed part categories on the homepage'),
|
||||
'default': True,
|
||||
'validator': bool,
|
||||
},
|
||||
|
||||
'HOMEPAGE_PART_LATEST': {
|
||||
'name': _('Show latest parts'),
|
||||
'description': _('Show latest parts on the homepage'),
|
||||
'default': True,
|
||||
'validator': bool,
|
||||
},
|
||||
|
||||
'PART_RECENT_COUNT': {
|
||||
'name': _('Recent Part Count'),
|
||||
'description': _('Number of recent parts to display on index page'),
|
||||
@ -1174,78 +1208,91 @@ class InvenTreeUserSetting(BaseInvenTreeSetting):
|
||||
'default': True,
|
||||
'validator': bool,
|
||||
},
|
||||
|
||||
'HOMEPAGE_STOCK_RECENT': {
|
||||
'name': _('Show recent stock changes'),
|
||||
'description': _('Show recently changed stock items on the homepage'),
|
||||
'default': True,
|
||||
'validator': bool,
|
||||
},
|
||||
|
||||
'STOCK_RECENT_COUNT': {
|
||||
'name': _('Recent Stock Count'),
|
||||
'description': _('Number of recent stock items to display on index page'),
|
||||
'default': 10,
|
||||
'validator': [int, MinValueValidator(1)]
|
||||
},
|
||||
|
||||
'HOMEPAGE_STOCK_LOW': {
|
||||
'name': _('Show low stock'),
|
||||
'description': _('Show low stock items on the homepage'),
|
||||
'default': True,
|
||||
'validator': bool,
|
||||
},
|
||||
|
||||
'HOMEPAGE_STOCK_DEPLETED': {
|
||||
'name': _('Show depleted stock'),
|
||||
'description': _('Show depleted stock items on the homepage'),
|
||||
'default': True,
|
||||
'validator': bool,
|
||||
},
|
||||
|
||||
'HOMEPAGE_STOCK_NEEDED': {
|
||||
'name': _('Show needed stock'),
|
||||
'description': _('Show stock items needed for builds on the homepage'),
|
||||
'default': True,
|
||||
'validator': bool,
|
||||
},
|
||||
|
||||
'HOMEPAGE_STOCK_EXPIRED': {
|
||||
'name': _('Show expired stock'),
|
||||
'description': _('Show expired stock items on the homepage'),
|
||||
'default': True,
|
||||
'validator': bool,
|
||||
},
|
||||
|
||||
'HOMEPAGE_STOCK_STALE': {
|
||||
'name': _('Show stale stock'),
|
||||
'description': _('Show stale stock items on the homepage'),
|
||||
'default': True,
|
||||
'validator': bool,
|
||||
},
|
||||
|
||||
'HOMEPAGE_BUILD_PENDING': {
|
||||
'name': _('Show pending builds'),
|
||||
'description': _('Show pending builds on the homepage'),
|
||||
'default': True,
|
||||
'validator': bool,
|
||||
},
|
||||
|
||||
'HOMEPAGE_BUILD_OVERDUE': {
|
||||
'name': _('Show overdue builds'),
|
||||
'description': _('Show overdue builds on the homepage'),
|
||||
'default': True,
|
||||
'validator': bool,
|
||||
},
|
||||
|
||||
'HOMEPAGE_PO_OUTSTANDING': {
|
||||
'name': _('Show outstanding POs'),
|
||||
'description': _('Show outstanding POs on the homepage'),
|
||||
'default': True,
|
||||
'validator': bool,
|
||||
},
|
||||
|
||||
'HOMEPAGE_PO_OVERDUE': {
|
||||
'name': _('Show overdue POs'),
|
||||
'description': _('Show overdue POs on the homepage'),
|
||||
'default': True,
|
||||
'validator': bool,
|
||||
},
|
||||
|
||||
'HOMEPAGE_SO_OUTSTANDING': {
|
||||
'name': _('Show outstanding SOs'),
|
||||
'description': _('Show outstanding SOs on the homepage'),
|
||||
'default': True,
|
||||
'validator': bool,
|
||||
},
|
||||
|
||||
'HOMEPAGE_SO_OVERDUE': {
|
||||
'name': _('Show overdue SOs'),
|
||||
'description': _('Show overdue SOs on the homepage'),
|
||||
|
@ -50,11 +50,12 @@ class SettingsSerializer(InvenTreeModelSerializer):
|
||||
"""
|
||||
Make sure protected values are not returned
|
||||
"""
|
||||
result = obj.value
|
||||
|
||||
# never return protected values
|
||||
if obj.is_protected:
|
||||
if obj.protected:
|
||||
result = '***'
|
||||
else:
|
||||
result = obj.value
|
||||
|
||||
return result
|
||||
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
from django.test import TestCase
|
||||
|
||||
|
@ -9,7 +9,9 @@ from django.contrib.auth import get_user_model
|
||||
from django.urls import reverse
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
|
||||
class SettingsApiTest(InvenTreeAPITestCase):
|
||||
class GlobalSettingsApiTest(InvenTreeAPITestCase):
|
||||
"""
|
||||
Tests for the global settings API
|
||||
"""
|
||||
|
||||
def test_settings_api(self):
|
||||
# test setting with choice
|
||||
def test_global_settings_api_list(self):
|
||||
"""
|
||||
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')
|
||||
|
||||
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):
|
||||
def setUp(self):
|
||||
|
@ -7,7 +7,7 @@ from __future__ import unicode_literals
|
||||
|
||||
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.core.files.storage import FileSystemStorage
|
||||
|
||||
|
@ -11,7 +11,7 @@ from django_filters import rest_framework as rest_filters
|
||||
from rest_framework import filters
|
||||
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 InvenTree.helpers import str2bool
|
||||
@ -390,42 +390,42 @@ class SupplierPriceBreakDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
|
||||
manufacturer_part_api_urls = [
|
||||
|
||||
url(r'^parameter/', include([
|
||||
url(r'^(?P<pk>\d+)/', ManufacturerPartParameterDetail.as_view(), name='api-manufacturer-part-parameter-detail'),
|
||||
re_path(r'^parameter/', include([
|
||||
re_path(r'^(?P<pk>\d+)/', ManufacturerPartParameterDetail.as_view(), name='api-manufacturer-part-parameter-detail'),
|
||||
|
||||
# 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
|
||||
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 = [
|
||||
|
||||
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
|
||||
url(r'^.*$', SupplierPartList.as_view(), name='api-supplier-part-list'),
|
||||
re_path(r'^.*$', SupplierPartList.as_view(), name='api-supplier-part-list'),
|
||||
]
|
||||
|
||||
|
||||
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
|
||||
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'),
|
||||
url(r'^.*$', SupplierPriceBreakList.as_view(), name='api-part-supplier-price-list'),
|
||||
re_path(r'^(?P<pk>\d+)/?', SupplierPriceBreakDetail.as_view(), name='api-part-supplier-price-detail'),
|
||||
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'),
|
||||
]
|
||||
|
@ -8,7 +8,7 @@ from __future__ import unicode_literals
|
||||
from InvenTree.forms import HelperForm
|
||||
from InvenTree.fields import RoundingDecimalFormField
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
import django.forms
|
||||
|
||||
from .models import Company
|
||||
|
@ -7,7 +7,7 @@ from __future__ import unicode_literals
|
||||
|
||||
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.exceptions import ValidationError
|
||||
|
||||
@ -634,7 +634,7 @@ class SupplierPart(models.Model):
|
||||
get_price = common.models.get_price
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
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
|
||||
|
||||
|
@ -2,37 +2,37 @@
|
||||
URL lookup for Company app
|
||||
"""
|
||||
|
||||
from django.conf.urls import url, include
|
||||
from django.urls import include, re_path
|
||||
|
||||
from . import views
|
||||
|
||||
|
||||
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
|
||||
url(r'^.*$', views.CompanyDetail.as_view(), name='company-detail'),
|
||||
re_path(r'^.*$', views.CompanyDetail.as_view(), name='company-detail'),
|
||||
]
|
||||
|
||||
|
||||
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'),
|
||||
url(r'manufacturers/', views.CompanyIndex.as_view(), name='manufacturer-index'),
|
||||
url(r'customers/', views.CompanyIndex.as_view(), name='customer-index'),
|
||||
re_path(r'suppliers/', views.CompanyIndex.as_view(), name='supplier-index'),
|
||||
re_path(r'manufacturers/', views.CompanyIndex.as_view(), name='manufacturer-index'),
|
||||
re_path(r'customers/', views.CompanyIndex.as_view(), name='customer-index'),
|
||||
|
||||
# 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 = [
|
||||
|
||||
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 = [
|
||||
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'),
|
||||
]
|
||||
|
@ -6,7 +6,7 @@ Django views for interacting with Company app
|
||||
# -*- coding: utf-8 -*-
|
||||
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.urls import reverse
|
||||
|
@ -4,10 +4,10 @@ from __future__ import unicode_literals
|
||||
from io import BytesIO
|
||||
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.urls import url, include
|
||||
from django.urls import include, re_path
|
||||
from django.core.exceptions import ValidationError, FieldError
|
||||
from django.http import HttpResponse, JsonResponse
|
||||
|
||||
@ -579,38 +579,38 @@ class PartLabelPrint(generics.RetrieveAPIView, PartLabelMixin, LabelPrintMixin):
|
||||
label_api_urls = [
|
||||
|
||||
# Stock item labels
|
||||
url(r'stock/', include([
|
||||
re_path(r'stock/', include([
|
||||
# Detail views
|
||||
url(r'^(?P<pk>\d+)/', include([
|
||||
url(r'print/?', StockItemLabelPrint.as_view(), name='api-stockitem-label-print'),
|
||||
url(r'^.*$', StockItemLabelDetail.as_view(), name='api-stockitem-label-detail'),
|
||||
re_path(r'^(?P<pk>\d+)/', include([
|
||||
re_path(r'print/?', StockItemLabelPrint.as_view(), name='api-stockitem-label-print'),
|
||||
re_path(r'^.*$', StockItemLabelDetail.as_view(), name='api-stockitem-label-detail'),
|
||||
])),
|
||||
|
||||
# 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
|
||||
url(r'location/', include([
|
||||
re_path(r'location/', include([
|
||||
# Detail views
|
||||
url(r'^(?P<pk>\d+)/', include([
|
||||
url(r'print/?', StockLocationLabelPrint.as_view(), name='api-stocklocation-label-print'),
|
||||
url(r'^.*$', StockLocationLabelDetail.as_view(), name='api-stocklocation-label-detail'),
|
||||
re_path(r'^(?P<pk>\d+)/', include([
|
||||
re_path(r'print/?', StockLocationLabelPrint.as_view(), name='api-stocklocation-label-print'),
|
||||
re_path(r'^.*$', StockLocationLabelDetail.as_view(), name='api-stocklocation-label-detail'),
|
||||
])),
|
||||
|
||||
# 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
|
||||
url(r'^part/', include([
|
||||
re_path(r'^part/', include([
|
||||
# Detail views
|
||||
url(r'^(?P<pk>\d+)/', include([
|
||||
url(r'^print/', PartLabelPrint.as_view(), name='api-part-label-print'),
|
||||
url(r'^.*$', PartLabelDetail.as_view(), name='api-part-label-detail'),
|
||||
re_path(r'^(?P<pk>\d+)/', include([
|
||||
re_path(r'^print/', PartLabelPrint.as_view(), name='api-part-label-print'),
|
||||
re_path(r'^.*$', PartLabelDetail.as_view(), name='api-part-label-detail'),
|
||||
])),
|
||||
|
||||
# List view
|
||||
url(r'^.*$', PartLabelList.as_view(), name='api-part-label-list'),
|
||||
re_path(r'^.*$', PartLabelList.as_view(), name='api-part-label-list'),
|
||||
])),
|
||||
]
|
||||
|
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@ -8,11 +8,35 @@ from import_export.admin import ImportExportModelAdmin
|
||||
from import_export.resources import ModelResource
|
||||
from import_export.fields import Field
|
||||
|
||||
from .models import PurchaseOrder, PurchaseOrderLineItem
|
||||
from .models import SalesOrder, SalesOrderLineItem
|
||||
from .models import PurchaseOrder, PurchaseOrderLineItem, PurchaseOrderExtraLine
|
||||
from .models import SalesOrder, SalesOrderLineItem, SalesOrderExtraLine
|
||||
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):
|
||||
model = PurchaseOrderLineItem
|
||||
extra = 0
|
||||
@ -68,8 +92,8 @@ class SalesOrderAdmin(ImportExportModelAdmin):
|
||||
autocomplete_fields = ('customer',)
|
||||
|
||||
|
||||
class POLineItemResource(ModelResource):
|
||||
""" Class for managing import / export of POLineItem data """
|
||||
class PurchaseOrderLineItemResource(ModelResource):
|
||||
""" Class for managing import / export of PurchaseOrderLineItem data """
|
||||
|
||||
part_name = Field(attribute='part__part__name', readonly=True)
|
||||
|
||||
@ -86,9 +110,16 @@ class POLineItemResource(ModelResource):
|
||||
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)
|
||||
@ -117,9 +148,16 @@ class SOLineItemResource(ModelResource):
|
||||
clean_model_instances = True
|
||||
|
||||
|
||||
class SalesOrderExtraLineResource(ModelResource):
|
||||
""" Class for managing import / export of SalesOrderExtraLine data """
|
||||
|
||||
class Meta(GeneralExtraLineMeta):
|
||||
model = SalesOrderExtraLine
|
||||
|
||||
|
||||
class PurchaseOrderLineItemAdmin(ImportExportModelAdmin):
|
||||
|
||||
resource_class = POLineItemResource
|
||||
resource_class = PurchaseOrderLineItemResource
|
||||
|
||||
list_display = (
|
||||
'order',
|
||||
@ -133,9 +171,14 @@ class PurchaseOrderLineItemAdmin(ImportExportModelAdmin):
|
||||
autocomplete_fields = ('order', 'part', 'destination',)
|
||||
|
||||
|
||||
class PurchaseOrderExtraLineAdmin(GeneralExtraLineAdmin, ImportExportModelAdmin):
|
||||
|
||||
resource_class = PurchaseOrderExtraLineResource
|
||||
|
||||
|
||||
class SalesOrderLineItemAdmin(ImportExportModelAdmin):
|
||||
|
||||
resource_class = SOLineItemResource
|
||||
resource_class = SalesOrderLineItemResource
|
||||
|
||||
list_display = (
|
||||
'order',
|
||||
@ -154,6 +197,11 @@ class SalesOrderLineItemAdmin(ImportExportModelAdmin):
|
||||
autocomplete_fields = ('order', 'part',)
|
||||
|
||||
|
||||
class SalesOrderExtraLineAdmin(GeneralExtraLineAdmin, ImportExportModelAdmin):
|
||||
|
||||
resource_class = SalesOrderExtraLineResource
|
||||
|
||||
|
||||
class SalesOrderShipmentAdmin(ImportExportModelAdmin):
|
||||
|
||||
list_display = [
|
||||
@ -184,9 +232,11 @@ class SalesOrderAllocationAdmin(ImportExportModelAdmin):
|
||||
|
||||
admin.site.register(PurchaseOrder, PurchaseOrderAdmin)
|
||||
admin.site.register(PurchaseOrderLineItem, PurchaseOrderLineItemAdmin)
|
||||
admin.site.register(PurchaseOrderExtraLine, PurchaseOrderExtraLineAdmin)
|
||||
|
||||
admin.site.register(SalesOrder, SalesOrderAdmin)
|
||||
admin.site.register(SalesOrderLineItem, SalesOrderLineItemAdmin)
|
||||
admin.site.register(SalesOrderExtraLine, SalesOrderExtraLineAdmin)
|
||||
|
||||
admin.site.register(SalesOrderShipment, SalesOrderShipmentAdmin)
|
||||
admin.site.register(SalesOrderAllocation, SalesOrderAllocationAdmin)
|
||||
|
@ -5,7 +5,7 @@ JSON API for the Order app
|
||||
# -*- coding: utf-8 -*-
|
||||
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_filters import rest_framework as rest_filters
|
||||
@ -20,16 +20,68 @@ from InvenTree.helpers import str2bool, DownloadFile
|
||||
from InvenTree.api import AttachmentMixin
|
||||
from InvenTree.status_codes import PurchaseOrderStatus, SalesOrderStatus
|
||||
|
||||
from order.admin import POLineItemResource
|
||||
from order.admin import PurchaseOrderLineItemResource
|
||||
import order.models as models
|
||||
import order.serializers as serializers
|
||||
from part.models import Part
|
||||
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')
|
||||
@ -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
|
||||
|
||||
- GET: Return list of PO objects (with filters)
|
||||
- GET: Return list of PurchaseOrder objects (with filters)
|
||||
- POST: Create a new PurchaseOrder object
|
||||
"""
|
||||
|
||||
queryset = models.PurchaseOrder.objects.all()
|
||||
serializer_class = serializers.POSerializer
|
||||
filterset_class = POFilter
|
||||
serializer_class = serializers.PurchaseOrderSerializer
|
||||
filterset_class = PurchaseOrderFilter
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
"""
|
||||
@ -104,7 +156,7 @@ class POList(generics.ListCreateAPIView):
|
||||
'lines',
|
||||
)
|
||||
|
||||
queryset = serializers.POSerializer.annotate_queryset(queryset)
|
||||
queryset = serializers.PurchaseOrderSerializer.annotate_queryset(queryset)
|
||||
|
||||
return queryset
|
||||
|
||||
@ -202,11 +254,11 @@ class POList(generics.ListCreateAPIView):
|
||||
ordering = '-creation_date'
|
||||
|
||||
|
||||
class PODetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
class PurchaseOrderDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
""" API endpoint for detail view of a PurchaseOrder object """
|
||||
|
||||
queryset = models.PurchaseOrder.objects.all()
|
||||
serializer_class = serializers.POSerializer
|
||||
serializer_class = serializers.PurchaseOrderSerializer
|
||||
|
||||
def get_serializer(self, *args, **kwargs):
|
||||
|
||||
@ -229,12 +281,12 @@ class PODetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
'lines',
|
||||
)
|
||||
|
||||
queryset = serializers.POSerializer.annotate_queryset(queryset)
|
||||
queryset = serializers.PurchaseOrderSerializer.annotate_queryset(queryset)
|
||||
|
||||
return queryset
|
||||
|
||||
|
||||
class POReceive(generics.CreateAPIView):
|
||||
class PurchaseOrderReceive(generics.CreateAPIView):
|
||||
"""
|
||||
API endpoint to receive stock items against a purchase order.
|
||||
|
||||
@ -249,7 +301,7 @@ class POReceive(generics.CreateAPIView):
|
||||
|
||||
queryset = models.PurchaseOrderLineItem.objects.none()
|
||||
|
||||
serializer_class = serializers.POReceiveSerializer
|
||||
serializer_class = serializers.PurchaseOrderReceiveSerializer
|
||||
|
||||
def get_serializer_context(self):
|
||||
|
||||
@ -266,9 +318,9 @@ class POReceive(generics.CreateAPIView):
|
||||
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:
|
||||
@ -318,22 +370,22 @@ class POLineItemFilter(rest_filters.FilterSet):
|
||||
return queryset
|
||||
|
||||
|
||||
class POLineItemList(generics.ListCreateAPIView):
|
||||
""" API endpoint for accessing a list of POLineItem objects
|
||||
class PurchaseOrderLineItemList(generics.ListCreateAPIView):
|
||||
""" 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
|
||||
"""
|
||||
|
||||
queryset = models.PurchaseOrderLineItem.objects.all()
|
||||
serializer_class = serializers.POLineItemSerializer
|
||||
filterset_class = POLineItemFilter
|
||||
serializer_class = serializers.PurchaseOrderLineItemSerializer
|
||||
filterset_class = PurchaseOrderLineItemFilter
|
||||
|
||||
def get_queryset(self, *args, **kwargs):
|
||||
|
||||
queryset = super().get_queryset(*args, **kwargs)
|
||||
|
||||
queryset = serializers.POLineItemSerializer.annotate_queryset(queryset)
|
||||
queryset = serializers.PurchaseOrderLineItemSerializer.annotate_queryset(queryset)
|
||||
|
||||
return queryset
|
||||
|
||||
@ -382,7 +434,7 @@ class POLineItemList(generics.ListCreateAPIView):
|
||||
export_format = str(export_format).strip().lower()
|
||||
|
||||
if export_format in ['csv', 'tsv', 'xls', 'xlsx']:
|
||||
dataset = POLineItemResource().export(queryset=queryset)
|
||||
dataset = PurchaseOrderLineItemResource().export(queryset=queryset)
|
||||
|
||||
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
|
||||
"""
|
||||
|
||||
queryset = models.PurchaseOrderLineItem.objects.all()
|
||||
serializer_class = serializers.POLineItemSerializer
|
||||
serializer_class = serializers.PurchaseOrderLineItemSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
|
||||
queryset = super().get_queryset()
|
||||
|
||||
queryset = serializers.POLineItemSerializer.annotate_queryset(queryset)
|
||||
queryset = serializers.PurchaseOrderLineItemSerializer.annotate_queryset(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)
|
||||
"""
|
||||
|
||||
queryset = models.SalesOrderAttachment.objects.all()
|
||||
serializer_class = serializers.SOAttachmentSerializer
|
||||
serializer_class = serializers.SalesOrderAttachmentSerializer
|
||||
|
||||
filter_backends = [
|
||||
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
|
||||
"""
|
||||
|
||||
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.
|
||||
|
||||
- GET: Return list of SO objects (with filters)
|
||||
- GET: Return list of SalesOrder objects (with filters)
|
||||
- POST: Create a new SalesOrder
|
||||
"""
|
||||
|
||||
@ -616,7 +684,7 @@ class SOList(generics.ListCreateAPIView):
|
||||
ordering = '-creation_date'
|
||||
|
||||
|
||||
class SODetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
class SalesOrderDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
"""
|
||||
API endpoint for detail view of a SalesOrder object.
|
||||
"""
|
||||
@ -646,9 +714,9 @@ class SODetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
return queryset
|
||||
|
||||
|
||||
class SOLineItemFilter(rest_filters.FilterSet):
|
||||
class SalesOrderLineItemFilter(rest_filters.FilterSet):
|
||||
"""
|
||||
Custom filters for SOLineItemList endpoint
|
||||
Custom filters for SalesOrderLineItemList endpoint
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
@ -679,14 +747,14 @@ class SOLineItemFilter(rest_filters.FilterSet):
|
||||
return queryset
|
||||
|
||||
|
||||
class SOLineItemList(generics.ListCreateAPIView):
|
||||
class SalesOrderLineItemList(generics.ListCreateAPIView):
|
||||
"""
|
||||
API endpoint for accessing a list of SalesOrderLineItem objects.
|
||||
"""
|
||||
|
||||
queryset = models.SalesOrderLineItem.objects.all()
|
||||
serializer_class = serializers.SOLineItemSerializer
|
||||
filterset_class = SOLineItemFilter
|
||||
serializer_class = serializers.SalesOrderLineItemSerializer
|
||||
filterset_class = SalesOrderLineItemFilter
|
||||
|
||||
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 """
|
||||
|
||||
queryset = models.SalesOrderLineItem.objects.all()
|
||||
serializer_class = serializers.SOLineItemSerializer
|
||||
serializer_class = serializers.SalesOrderLineItemSerializer
|
||||
|
||||
|
||||
class SalesOrderComplete(generics.CreateAPIView):
|
||||
@ -779,7 +863,7 @@ class SalesOrderAllocateSerials(generics.CreateAPIView):
|
||||
"""
|
||||
|
||||
queryset = models.SalesOrder.objects.none()
|
||||
serializer_class = serializers.SOSerialAllocationSerializer
|
||||
serializer_class = serializers.SalesOrderSerialAllocationSerializer
|
||||
|
||||
def get_serializer_context(self):
|
||||
|
||||
@ -801,11 +885,11 @@ class SalesOrderAllocate(generics.CreateAPIView):
|
||||
API endpoint to allocate stock items against a SalesOrder
|
||||
|
||||
- The SalesOrder is specified in the URL
|
||||
- See the SOShipmentAllocationSerializer class
|
||||
- See the SalesOrderShipmentAllocationSerializer class
|
||||
"""
|
||||
|
||||
queryset = models.SalesOrder.objects.none()
|
||||
serializer_class = serializers.SOShipmentAllocationSerializer
|
||||
serializer_class = serializers.SalesOrderShipmentAllocationSerializer
|
||||
|
||||
def get_serializer_context(self):
|
||||
|
||||
@ -822,7 +906,7 @@ class SalesOrderAllocate(generics.CreateAPIView):
|
||||
return ctx
|
||||
|
||||
|
||||
class SOAllocationDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
class SalesOrderAllocationDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
"""
|
||||
API endpoint for detali view of a SalesOrderAllocation object
|
||||
"""
|
||||
@ -831,7 +915,7 @@ class SOAllocationDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
serializer_class = serializers.SalesOrderAllocationSerializer
|
||||
|
||||
|
||||
class SOAllocationList(generics.ListAPIView):
|
||||
class SalesOrderAllocationList(generics.ListAPIView):
|
||||
"""
|
||||
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')
|
||||
@ -934,21 +1018,21 @@ class SOShipmentFilter(rest_filters.FilterSet):
|
||||
]
|
||||
|
||||
|
||||
class SOShipmentList(generics.ListCreateAPIView):
|
||||
class SalesOrderShipmentList(generics.ListCreateAPIView):
|
||||
"""
|
||||
API list endpoint for SalesOrderShipment model
|
||||
"""
|
||||
|
||||
queryset = models.SalesOrderShipment.objects.all()
|
||||
serializer_class = serializers.SalesOrderShipmentSerializer
|
||||
filterset_class = SOShipmentFilter
|
||||
filterset_class = SalesOrderShipmentFilter
|
||||
|
||||
filter_backends = [
|
||||
rest_filters.DjangoFilterBackend,
|
||||
]
|
||||
|
||||
|
||||
class SOShipmentDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
class SalesOrderShipmentDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
"""
|
||||
API detail endpooint for SalesOrderShipment model
|
||||
"""
|
||||
@ -957,7 +1041,7 @@ class SOShipmentDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
serializer_class = serializers.SalesOrderShipmentSerializer
|
||||
|
||||
|
||||
class SOShipmentComplete(generics.CreateAPIView):
|
||||
class SalesOrderShipmentComplete(generics.CreateAPIView):
|
||||
"""
|
||||
API endpoint for completing (shipping) a SalesOrderShipment
|
||||
"""
|
||||
@ -983,13 +1067,13 @@ class SOShipmentComplete(generics.CreateAPIView):
|
||||
return ctx
|
||||
|
||||
|
||||
class POAttachmentList(generics.ListCreateAPIView, AttachmentMixin):
|
||||
class PurchaseOrderAttachmentList(generics.ListCreateAPIView, AttachmentMixin):
|
||||
"""
|
||||
API endpoint for listing (and creating) a PurchaseOrderAttachment (file upload)
|
||||
"""
|
||||
|
||||
queryset = models.PurchaseOrderAttachment.objects.all()
|
||||
serializer_class = serializers.POAttachmentSerializer
|
||||
serializer_class = serializers.PurchaseOrderAttachmentSerializer
|
||||
|
||||
filter_backends = [
|
||||
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
|
||||
"""
|
||||
|
||||
queryset = models.PurchaseOrderAttachment.objects.all()
|
||||
serializer_class = serializers.POAttachmentSerializer
|
||||
serializer_class = serializers.PurchaseOrderAttachmentSerializer
|
||||
|
||||
|
||||
order_api_urls = [
|
||||
|
||||
# API endpoints for purchase orders
|
||||
url(r'^po/', include([
|
||||
re_path(r'^po/', include([
|
||||
|
||||
# Purchase order attachments
|
||||
url(r'attachment/', include([
|
||||
url(r'^(?P<pk>\d+)/$', POAttachmentDetail.as_view(), name='api-po-attachment-detail'),
|
||||
url(r'^.*$', POAttachmentList.as_view(), name='api-po-attachment-list'),
|
||||
re_path(r'attachment/', include([
|
||||
path('<int:pk>/', PurchaseOrderAttachmentDetail.as_view(), name='api-po-attachment-detail'),
|
||||
re_path(r'^.*$', PurchaseOrderAttachmentList.as_view(), name='api-po-attachment-list'),
|
||||
])),
|
||||
|
||||
# Individual purchase order detail URLs
|
||||
url(r'^(?P<pk>\d+)/', include([
|
||||
url(r'^receive/', POReceive.as_view(), name='api-po-receive'),
|
||||
url(r'.*$', PODetail.as_view(), name='api-po-detail'),
|
||||
re_path(r'^(?P<pk>\d+)/', include([
|
||||
re_path(r'^receive/', PurchaseOrderReceive.as_view(), name='api-po-receive'),
|
||||
re_path(r'.*$', PurchaseOrderDetail.as_view(), name='api-po-detail'),
|
||||
])),
|
||||
|
||||
# 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
|
||||
url(r'^po-line/', include([
|
||||
url(r'^(?P<pk>\d+)/$', POLineItemDetail.as_view(), name='api-po-line-detail'),
|
||||
url(r'^.*$', POLineItemList.as_view(), name='api-po-line-list'),
|
||||
re_path(r'^po-line/', include([
|
||||
path('<int:pk>/', PurchaseOrderLineItemDetail.as_view(), name='api-po-line-detail'),
|
||||
re_path(r'^.*$', PurchaseOrderLineItemList.as_view(), name='api-po-line-list'),
|
||||
])),
|
||||
|
||||
# API endpoints for sales orders
|
||||
url(r'^so/', include([
|
||||
url(r'attachment/', include([
|
||||
url(r'^(?P<pk>\d+)/$', SOAttachmentDetail.as_view(), name='api-so-attachment-detail'),
|
||||
url(r'^.*$', SOAttachmentList.as_view(), name='api-so-attachment-list'),
|
||||
# API endpoints for purchase order extra line
|
||||
re_path(r'^po-extra-line/', include([
|
||||
path('<int:pk>/', PurchaseOrderExtraLineDetail.as_view(), name='api-po-extra-line-detail'),
|
||||
path('', PurchaseOrderExtraLineList.as_view(), name='api-po-extra-line-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([
|
||||
url(r'^(?P<pk>\d+)/', include([
|
||||
url(r'^ship/$', SOShipmentComplete.as_view(), name='api-so-shipment-ship'),
|
||||
url(r'^.*$', SOShipmentDetail.as_view(), name='api-so-shipment-detail'),
|
||||
re_path(r'^shipment/', include([
|
||||
re_path(r'^(?P<pk>\d+)/', include([
|
||||
path('ship/', SalesOrderShipmentComplete.as_view(), name='api-so-shipment-ship'),
|
||||
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
|
||||
url(r'^(?P<pk>\d+)/', include([
|
||||
url(r'^complete/', SalesOrderComplete.as_view(), name='api-so-complete'),
|
||||
url(r'^allocate/', SalesOrderAllocate.as_view(), name='api-so-allocate'),
|
||||
url(r'^allocate-serials/', SalesOrderAllocateSerials.as_view(), name='api-so-allocate-serials'),
|
||||
url(r'^.*$', SODetail.as_view(), name='api-so-detail'),
|
||||
re_path(r'^(?P<pk>\d+)/', include([
|
||||
re_path(r'^complete/', SalesOrderComplete.as_view(), name='api-so-complete'),
|
||||
re_path(r'^allocate/', SalesOrderAllocate.as_view(), name='api-so-allocate'),
|
||||
re_path(r'^allocate-serials/', SalesOrderAllocateSerials.as_view(), name='api-so-allocate-serials'),
|
||||
re_path(r'^.*$', SalesOrderDetail.as_view(), name='api-so-detail'),
|
||||
])),
|
||||
|
||||
# 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
|
||||
url(r'^so-line/', include([
|
||||
url(r'^(?P<pk>\d+)/$', SOLineItemDetail.as_view(), name='api-so-line-detail'),
|
||||
url(r'^$', SOLineItemList.as_view(), name='api-so-line-list'),
|
||||
re_path(r'^so-line/', include([
|
||||
path('<int:pk>/', SalesOrderLineItemDetail.as_view(), name='api-so-line-detail'),
|
||||
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
|
||||
url(r'^so-allocation/', include([
|
||||
url(r'^(?P<pk>\d+)/$', SOAllocationDetail.as_view(), name='api-so-allocation-detail'),
|
||||
url(r'^.*$', SOAllocationList.as_view(), name='api-so-allocation-list'),
|
||||
re_path(r'^so-allocation/', include([
|
||||
path('<int:pk>/', SalesOrderAllocationDetail.as_view(), name='api-so-allocation-detail'),
|
||||
re_path(r'^.*$', SalesOrderAllocationList.as_view(), name='api-so-allocation-list'),
|
||||
])),
|
||||
]
|
||||
|
@ -6,7 +6,7 @@ Django Forms for interacting with Order objects
|
||||
from __future__ import unicode_literals
|
||||
|
||||
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.fields import InvenTreeMoneyField
|
||||
|
@ -33,7 +33,7 @@ def calculate_shipped_quantity(apps, schema_editor):
|
||||
part=item.part
|
||||
)
|
||||
|
||||
q = sum([item.quantity for item in items])
|
||||
q = sum(item.quantity for item in items)
|
||||
|
||||
item.shipped = q
|
||||
|
||||
|
@ -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),
|
||||
]
|
@ -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'),
|
||||
),
|
||||
]
|
@ -5,6 +5,7 @@ Order model definitions
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import os
|
||||
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
|
||||
@ -16,11 +17,15 @@ from django.core.validators import MinValueValidator
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.contrib.auth.models import User
|
||||
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 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 part import models as PartModels
|
||||
from stock import models as stock_models
|
||||
@ -44,7 +49,7 @@ def get_next_po_number():
|
||||
|
||||
order = PurchaseOrder.objects.exclude(reference=None).last()
|
||||
|
||||
attempts = set([order.reference])
|
||||
attempts = {order.reference}
|
||||
|
||||
reference = order.reference
|
||||
|
||||
@ -73,7 +78,7 @@ def get_next_so_number():
|
||||
|
||||
order = SalesOrder.objects.exclude(reference=None).last()
|
||||
|
||||
attempts = set([order.reference])
|
||||
attempts = {order.reference}
|
||||
|
||||
reference = order.reference
|
||||
|
||||
@ -146,6 +151,25 @@ class Order(ReferenceIndexingMixin):
|
||||
|
||||
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):
|
||||
""" 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")})
|
||||
|
||||
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)
|
||||
|
||||
if matches.count() > 0:
|
||||
@ -401,7 +425,7 @@ class PurchaseOrder(Order):
|
||||
@transaction.atomic
|
||||
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
|
||||
@ -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):
|
||||
""" Model for a purchase order line item.
|
||||
|
||||
Attributes:
|
||||
order: Reference to a PurchaseOrder object
|
||||
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
@ -904,11 +960,9 @@ class PurchaseOrderLineItem(OrderLineItem):
|
||||
else:
|
||||
return self.part.part
|
||||
|
||||
# TODO - Function callback for when the SupplierPart is deleted?
|
||||
|
||||
part = models.ForeignKey(
|
||||
SupplierPart, on_delete=models.SET_NULL,
|
||||
blank=True, null=True,
|
||||
blank=False, null=True,
|
||||
related_name='purchase_order_line_items',
|
||||
verbose_name=_('Part'),
|
||||
help_text=_("Supplier part"),
|
||||
@ -961,6 +1015,21 @@ class PurchaseOrderLineItem(OrderLineItem):
|
||||
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):
|
||||
"""
|
||||
Model for a single LineItem in a SalesOrder
|
||||
@ -1164,6 +1233,21 @@ class SalesOrderShipment(models.Model):
|
||||
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):
|
||||
"""
|
||||
This model is used to 'allocate' stock items to a SalesOrder.
|
||||
|
@ -7,7 +7,7 @@ from __future__ import unicode_literals
|
||||
|
||||
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.db import models, transaction
|
||||
@ -40,7 +40,64 @@ import stock.serializers
|
||||
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 """
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@ -110,6 +167,8 @@ class POSerializer(ReferenceIndexingSerializerMixin, InvenTreeModelSerializer):
|
||||
'status_text',
|
||||
'target_date',
|
||||
'notes',
|
||||
'total_price',
|
||||
'total_price_string',
|
||||
]
|
||||
|
||||
read_only_fields = [
|
||||
@ -120,7 +179,7 @@ class POSerializer(ReferenceIndexingSerializerMixin, InvenTreeModelSerializer):
|
||||
]
|
||||
|
||||
|
||||
class POLineItemSerializer(InvenTreeModelSerializer):
|
||||
class PurchaseOrderLineItemSerializer(InvenTreeModelSerializer):
|
||||
|
||||
@staticmethod
|
||||
def annotate_queryset(queryset):
|
||||
@ -187,7 +246,7 @@ class POLineItemSerializer(InvenTreeModelSerializer):
|
||||
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:
|
||||
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
|
||||
"""
|
||||
@ -344,12 +412,12 @@ class POLineItemReceiveSerializer(serializers.Serializer):
|
||||
return data
|
||||
|
||||
|
||||
class POReceiveSerializer(serializers.Serializer):
|
||||
class PurchaseOrderReceiveSerializer(serializers.Serializer):
|
||||
"""
|
||||
Serializer for receiving items against a purchase order
|
||||
"""
|
||||
|
||||
items = POLineItemReceiveSerializer(many=True)
|
||||
items = PurchaseOrderLineItemReceiveSerializer(many=True)
|
||||
|
||||
location = serializers.PrimaryKeyRelatedField(
|
||||
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
|
||||
"""
|
||||
@ -467,7 +535,7 @@ class POAttachmentSerializer(InvenTreeAttachmentSerializer):
|
||||
]
|
||||
|
||||
|
||||
class SalesOrderSerializer(ReferenceIndexingSerializerMixin, InvenTreeModelSerializer):
|
||||
class SalesOrderSerializer(AbstractOrderSerializer, ReferenceIndexingSerializerMixin, InvenTreeModelSerializer):
|
||||
"""
|
||||
Serializers for the SalesOrder object
|
||||
"""
|
||||
@ -535,6 +603,8 @@ class SalesOrderSerializer(ReferenceIndexingSerializerMixin, InvenTreeModelSeria
|
||||
'status_text',
|
||||
'shipment_date',
|
||||
'target_date',
|
||||
'total_price',
|
||||
'total_price_string',
|
||||
]
|
||||
|
||||
read_only_fields = [
|
||||
@ -612,7 +682,7 @@ class SalesOrderAllocationSerializer(InvenTreeModelSerializer):
|
||||
]
|
||||
|
||||
|
||||
class SOLineItemSerializer(InvenTreeModelSerializer):
|
||||
class SalesOrderLineItemSerializer(InvenTreeModelSerializer):
|
||||
""" Serializer for a SalesOrderLineItem object """
|
||||
|
||||
@staticmethod
|
||||
@ -862,7 +932,7 @@ class SalesOrderCompleteSerializer(serializers.Serializer):
|
||||
order.complete_order(user)
|
||||
|
||||
|
||||
class SOSerialAllocationSerializer(serializers.Serializer):
|
||||
class SalesOrderSerialAllocationSerializer(serializers.Serializer):
|
||||
"""
|
||||
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
|
||||
"""
|
||||
@ -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
|
||||
"""
|
||||
|
@ -171,6 +171,12 @@ src="{% static 'img/blank_image.png' %}"
|
||||
<td>{{ order.responsible }}</td>
|
||||
</tr>
|
||||
{% 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>
|
||||
{% endblock %}
|
||||
|
||||
|
@ -42,6 +42,29 @@
|
||||
<table class='table table-striped table-condensed' id='po-line-table' data-toolbar='#order-toolbar-buttons'>
|
||||
</table>
|
||||
</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 class='panel panel-hidden' id='panel-received-items'>
|
||||
@ -200,6 +223,37 @@ loadPurchaseOrderLineItemTable('#po-line-table', {
|
||||
{% 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');
|
||||
|
||||
{% endblock %}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user