Merge remote-tracking branch 'inventree/master'

This commit is contained in:
Oliver Walters 2022-05-01 10:54:55 +10:00
commit 4a62911696
29 changed files with 1473 additions and 136 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -637,7 +637,7 @@ class SupplierPart(models.Model):
get_price = common.models.get_price get_price = common.models.get_price
def open_orders(self): def open_orders(self):
""" Return a database query for PO line items for this SupplierPart, """ Return a database query for PurchaseOrder line items for this SupplierPart,
limited to purchase orders that are open / outstanding. limited to purchase orders that are open / outstanding.
""" """

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -34,6 +34,29 @@
<table class='table table-striped table-condensed' id='so-lines-table' data-toolbar='#order-toolbar-buttons'> <table class='table table-striped table-condensed' id='so-lines-table' data-toolbar='#order-toolbar-buttons'>
</table> </table>
</div> </div>
<div class='panel-heading'>
<div class='d-flex flex-wrap'>
<h4>{% trans "Extra Lines" %}</h4>
{% include "spacer.html" %}
<div class='btn-group' role='group'>
{% if roles.sales_order.change and order.is_pending %}
<button type='button' class='btn btn-success' id='new-so-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="sales-order-extra-lines" %}
</div>
</div>
<table class='table table-striped table-condensed' id='so-extra-lines-table' data-toolbar='#order-extra-toolbar-buttons'>
</table>
</div>
</div> </div>
{% if order.is_pending %} {% if order.is_pending %}
@ -238,6 +261,37 @@
} }
); );
$("#new-so-extra-line").click(function() {
var fields = extraLineFields({
order: {{ order.pk }},
});
constructForm('{% url "api-so-extra-line-list" %}', {
fields: fields,
method: 'POST',
title: '{% trans "Add Extra Line" %}',
onSuccess: function() {
$("#so-extra-lines-table").bootstrapTable("refresh");
},
});
});
loadSalesOrderExtraLineTable(
'#so-extra-lines-table',
{
order: {{ order.pk }},
status: {{ order.status }},
}
);
loadOrderTotal(
'#soTotalPrice',
{
url: '{% url "api-so-detail" order.pk %}',
}
);
enableSidebar('salesorder'); enableSidebar('salesorder');
{% endblock %} {% endblock %}

View File

@ -63,7 +63,7 @@ class PurchaseOrderTest(OrderTest):
def test_po_list(self): def test_po_list(self):
# List *ALL* PO items # List *ALL* PurchaseOrder items
self.filter({}, 7) self.filter({}, 7)
# Filter by supplier # Filter by supplier
@ -175,7 +175,7 @@ class PurchaseOrderTest(OrderTest):
pk = response.data['pk'] pk = response.data['pk']
# Try to create a PO with identical reference (should fail!) # Try to create a PurchaseOrder with identical reference (should fail!)
response = self.post( response = self.post(
url, url,
{ {
@ -493,7 +493,7 @@ class PurchaseOrderReceiveTest(OrderTest):
self.assertIn('can only be received against', str(response.data)) self.assertIn('can only be received against', str(response.data))
# Now, set the PO back to "PLACED" so the items can be received # Now, set the PurchaseOrder back to "PLACED" so the items can be received
order.status = PurchaseOrderStatus.PLACED order.status = PurchaseOrderStatus.PLACED
order.save() order.save()

View File

@ -122,3 +122,97 @@ class TestShipmentMigration(MigratorTestCase):
# Check that the correct number of Shipments have been created # Check that the correct number of Shipments have been created
self.assertEqual(SalesOrder.objects.count(), 5) self.assertEqual(SalesOrder.objects.count(), 5)
self.assertEqual(Shipment.objects.count(), 5) self.assertEqual(Shipment.objects.count(), 5)
class TestAdditionalLineMigration(MigratorTestCase):
"""
Test entire schema migration
"""
migrate_from = ('order', '0063_alter_purchaseorderlineitem_unique_together')
migrate_to = ('order', '0064_purchaseorderextraline_salesorderextraline')
def prepare(self):
"""
Create initial data set
"""
# Create a purchase order from a supplier
Company = self.old_state.apps.get_model('company', 'company')
PurchaseOrder = self.old_state.apps.get_model('order', 'purchaseorder')
Part = self.old_state.apps.get_model('part', 'part')
Supplierpart = self.old_state.apps.get_model('company', 'supplierpart')
# TODO @matmair fix this test!!!
# SalesOrder = self.old_state.apps.get_model('order', 'salesorder')
supplier = Company.objects.create(
name='Supplier A',
description='A great supplier!',
is_supplier=True,
is_customer=True,
)
part = Part.objects.create(
name='Bob',
description='Can we build it?',
assembly=True,
salable=True,
purchaseable=False,
tree_id=0,
level=0,
lft=0,
rght=0,
)
supplierpart = Supplierpart.objects.create(
part=part,
supplier=supplier
)
# Create some orders
for ii in range(10):
order = PurchaseOrder.objects.create(
supplier=supplier,
reference=f"{ii}-abcde",
description="Just a test order"
)
order.lines.create(
part=supplierpart,
quantity=12,
received=1
)
order.lines.create(
quantity=12,
received=1
)
# TODO @matmair fix this test!!!
# sales_order = SalesOrder.objects.create(
# customer=supplier,
# reference=f"{ii}-xyz",
# description="A test sales order",
# )
# sales_order.lines.create(
# part=part,
# quantity=12,
# received=1
# )
def test_po_migration(self):
"""
Test that the the PO lines where converted correctly
"""
PurchaseOrder = self.new_state.apps.get_model('order', 'purchaseorder')
for ii in range(10):
po = PurchaseOrder.objects.get(reference=f"{ii}-abcde")
self.assertEqual(po.extra_lines.count(), 1)
self.assertEqual(po.lines.count(), 1)
# TODO @matmair fix this test!!!
# SalesOrder = self.new_state.apps.get_model('order', 'salesorder')
# for ii in range(10):
# so = SalesOrder.objects.get(reference=f"{ii}-xyz")
# self.assertEqual(so.extra_lines, 1)
# self.assertEqual(so.lines.count(), 1)

View File

@ -20,7 +20,7 @@ from decimal import Decimal, InvalidOperation
from .models import PurchaseOrder, PurchaseOrderLineItem from .models import PurchaseOrder, PurchaseOrderLineItem
from .models import SalesOrder, SalesOrderLineItem from .models import SalesOrder, SalesOrderLineItem
from .admin import POLineItemResource, SOLineItemResource from .admin import PurchaseOrderLineItemResource, SalesOrderLineItemResource
from build.models import Build from build.models import Build
from company.models import Company, SupplierPart # ManufacturerPart from company.models import Company, SupplierPart # ManufacturerPart
from stock.models import StockItem from stock.models import StockItem
@ -410,7 +410,7 @@ class SalesOrderExport(AjaxView):
filename = f"{str(order)} - {order.customer.name}.{export_format}" filename = f"{str(order)} - {order.customer.name}.{export_format}"
dataset = SOLineItemResource().export(queryset=order.lines.all()) dataset = SalesOrderLineItemResource().export(queryset=order.lines.all())
filedata = dataset.export(format=export_format) filedata = dataset.export(format=export_format)
@ -441,7 +441,7 @@ class PurchaseOrderExport(AjaxView):
fmt=export_format fmt=export_format
) )
dataset = POLineItemResource().export(queryset=order.lines.all()) dataset = PurchaseOrderLineItemResource().export(queryset=order.lines.all())
filedata = dataset.export(format=export_format) filedata = dataset.export(format=export_format)
@ -491,7 +491,7 @@ class OrderParts(AjaxView):
return data return data
def get_suppliers(self): def get_suppliers(self):
""" Calculates a list of suppliers which the user will need to create POs for. """ Calculates a list of suppliers which the user will need to create PurchaseOrders for.
This is calculated AFTER the user finishes selecting the parts to order. This is calculated AFTER the user finishes selecting the parts to order.
Crucially, get_parts() must be called before get_suppliers() Crucially, get_parts() must be called before get_suppliers()
""" """

View File

@ -31,8 +31,8 @@ from .models import SalesOrderReport
from .serializers import TestReportSerializer from .serializers import TestReportSerializer
from .serializers import BuildReportSerializer from .serializers import BuildReportSerializer
from .serializers import BOMReportSerializer from .serializers import BOMReportSerializer
from .serializers import POReportSerializer from .serializers import PurchaseOrderReportSerializer
from .serializers import SOReportSerializer from .serializers import SalesOrderReportSerializer
class ReportListView(generics.ListAPIView): class ReportListView(generics.ListAPIView):
@ -561,12 +561,12 @@ class BuildReportPrint(generics.RetrieveAPIView, BuildReportMixin, ReportPrintMi
return self.print(request, builds) return self.print(request, builds)
class POReportList(ReportListView, OrderReportMixin): class PurchaseOrderReportList(ReportListView, OrderReportMixin):
OrderModel = order.models.PurchaseOrder OrderModel = order.models.PurchaseOrder
queryset = PurchaseOrderReport.objects.all() queryset = PurchaseOrderReport.objects.all()
serializer_class = POReportSerializer serializer_class = PurchaseOrderReportSerializer
def filter_queryset(self, queryset): def filter_queryset(self, queryset):
@ -618,16 +618,16 @@ class POReportList(ReportListView, OrderReportMixin):
return queryset return queryset
class POReportDetail(generics.RetrieveUpdateDestroyAPIView): class PurchaseOrderReportDetail(generics.RetrieveUpdateDestroyAPIView):
""" """
API endpoint for a single PurchaseOrderReport object API endpoint for a single PurchaseOrderReport object
""" """
queryset = PurchaseOrderReport.objects.all() queryset = PurchaseOrderReport.objects.all()
serializer_class = POReportSerializer serializer_class = PurchaseOrderReportSerializer
class POReportPrint(generics.RetrieveAPIView, OrderReportMixin, ReportPrintMixin): class PurchaseOrderReportPrint(generics.RetrieveAPIView, OrderReportMixin, ReportPrintMixin):
""" """
API endpoint for printing a PurchaseOrderReport object API endpoint for printing a PurchaseOrderReport object
""" """
@ -635,7 +635,7 @@ class POReportPrint(generics.RetrieveAPIView, OrderReportMixin, ReportPrintMixin
OrderModel = order.models.PurchaseOrder OrderModel = order.models.PurchaseOrder
queryset = PurchaseOrderReport.objects.all() queryset = PurchaseOrderReport.objects.all()
serializer_class = POReportSerializer serializer_class = PurchaseOrderReportSerializer
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
@ -644,12 +644,12 @@ class POReportPrint(generics.RetrieveAPIView, OrderReportMixin, ReportPrintMixin
return self.print(request, orders) return self.print(request, orders)
class SOReportList(ReportListView, OrderReportMixin): class SalesOrderReportList(ReportListView, OrderReportMixin):
OrderModel = order.models.SalesOrder OrderModel = order.models.SalesOrder
queryset = SalesOrderReport.objects.all() queryset = SalesOrderReport.objects.all()
serializer_class = SOReportSerializer serializer_class = SalesOrderReportSerializer
def filter_queryset(self, queryset): def filter_queryset(self, queryset):
@ -701,16 +701,16 @@ class SOReportList(ReportListView, OrderReportMixin):
return queryset return queryset
class SOReportDetail(generics.RetrieveUpdateDestroyAPIView): class SalesOrderReportDetail(generics.RetrieveUpdateDestroyAPIView):
""" """
API endpoint for a single SalesOrderReport object API endpoint for a single SalesOrderReport object
""" """
queryset = SalesOrderReport.objects.all() queryset = SalesOrderReport.objects.all()
serializer_class = SOReportSerializer serializer_class = SalesOrderReportSerializer
class SOReportPrint(generics.RetrieveAPIView, OrderReportMixin, ReportPrintMixin): class SalesOrderReportPrint(generics.RetrieveAPIView, OrderReportMixin, ReportPrintMixin):
""" """
API endpoint for printing a PurchaseOrderReport object API endpoint for printing a PurchaseOrderReport object
""" """
@ -718,7 +718,7 @@ class SOReportPrint(generics.RetrieveAPIView, OrderReportMixin, ReportPrintMixin
OrderModel = order.models.SalesOrder OrderModel = order.models.SalesOrder
queryset = SalesOrderReport.objects.all() queryset = SalesOrderReport.objects.all()
serializer_class = SOReportSerializer serializer_class = SalesOrderReportSerializer
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
@ -733,23 +733,23 @@ report_api_urls = [
url(r'po/', include([ url(r'po/', include([
# Detail views # Detail views
url(r'^(?P<pk>\d+)/', include([ url(r'^(?P<pk>\d+)/', include([
url(r'print/', POReportPrint.as_view(), name='api-po-report-print'), url(r'print/', PurchaseOrderReportPrint.as_view(), name='api-po-report-print'),
url(r'^$', POReportDetail.as_view(), name='api-po-report-detail'), url(r'^$', PurchaseOrderReportDetail.as_view(), name='api-po-report-detail'),
])), ])),
# List view # List view
url(r'^$', POReportList.as_view(), name='api-po-report-list'), url(r'^$', PurchaseOrderReportList.as_view(), name='api-po-report-list'),
])), ])),
# Sales order reports # Sales order reports
url(r'so/', include([ url(r'so/', include([
# Detail views # Detail views
url(r'^(?P<pk>\d+)/', include([ url(r'^(?P<pk>\d+)/', include([
url(r'print/', SOReportPrint.as_view(), name='api-so-report-print'), url(r'print/', SalesOrderReportPrint.as_view(), name='api-so-report-print'),
url(r'^$', SOReportDetail.as_view(), name='api-so-report-detail'), url(r'^$', SalesOrderReportDetail.as_view(), name='api-so-report-detail'),
])), ])),
url(r'^$', SOReportList.as_view(), name='api-so-report-list'), url(r'^$', SalesOrderReportList.as_view(), name='api-so-report-list'),
])), ])),
# Build reports # Build reports

View File

@ -466,6 +466,7 @@ class PurchaseOrderReport(ReportTemplateBase):
return { return {
'description': order.description, 'description': order.description,
'lines': order.lines, 'lines': order.lines,
'extra_lines': order.extra_lines,
'order': order, 'order': order,
'reference': order.reference, 'reference': order.reference,
'supplier': order.supplier, 'supplier': order.supplier,
@ -505,6 +506,7 @@ class SalesOrderReport(ReportTemplateBase):
'customer': order.customer, 'customer': order.customer,
'description': order.description, 'description': order.description,
'lines': order.lines, 'lines': order.lines,
'extra_lines': order.extra_lines,
'order': order, 'order': order,
'prefix': common.models.InvenTreeSetting.get_setting('SALESORDER_REFERENCE_PREFIX'), 'prefix': common.models.InvenTreeSetting.get_setting('SALESORDER_REFERENCE_PREFIX'),
'reference': order.reference, 'reference': order.reference,

View File

@ -58,7 +58,7 @@ class BOMReportSerializer(InvenTreeModelSerializer):
] ]
class POReportSerializer(InvenTreeModelSerializer): class PurchaseOrderReportSerializer(InvenTreeModelSerializer):
template = InvenTreeAttachmentSerializerField(required=True) template = InvenTreeAttachmentSerializerField(required=True)
@ -74,7 +74,7 @@ class POReportSerializer(InvenTreeModelSerializer):
] ]
class SOReportSerializer(InvenTreeModelSerializer): class SalesOrderReportSerializer(InvenTreeModelSerializer):
template = InvenTreeAttachmentSerializerField(required=True) template = InvenTreeAttachmentSerializerField(required=True)

View File

@ -38,7 +38,7 @@ from InvenTree.filters import InvenTreeOrderingFilter
from order.models import PurchaseOrder from order.models import PurchaseOrder
from order.models import SalesOrder, SalesOrderAllocation from order.models import SalesOrder, SalesOrderAllocation
from order.serializers import POSerializer from order.serializers import PurchaseOrderSerializer
from part.models import BomItem, Part, PartCategory from part.models import BomItem, Part, PartCategory
from part.serializers import PartBriefSerializer from part.serializers import PartBriefSerializer
@ -1315,7 +1315,7 @@ class StockTrackingList(generics.ListAPIView):
if 'purchaseorder' in deltas: if 'purchaseorder' in deltas:
try: try:
order = PurchaseOrder.objects.get(pk=deltas['purchaseorder']) order = PurchaseOrder.objects.get(pk=deltas['purchaseorder'])
serializer = POSerializer(order) serializer = PurchaseOrderSerializer(order)
deltas['purchaseorder_detail'] = serializer.data deltas['purchaseorder_detail'] = serializer.data
except: except:
pass pass

View File

@ -165,6 +165,7 @@
<script type='text/javascript' src="{% static 'script/clipboard.min.js' %}"></script> <script type='text/javascript' src="{% static 'script/clipboard.min.js' %}"></script>
<script type='text/javascript' src="{% static 'script/randomColor.min.js' %}"></script> <script type='text/javascript' src="{% static 'script/randomColor.min.js' %}"></script>
<script type='text/javascript' src="{% static 'script/qr-scanner.umd.min.js' %}"></script>
<!-- general JS --> <!-- general JS -->
<script type='text/javascript' src="{% static 'script/inventree/inventree.js' %}"></script> <script type='text/javascript' src="{% static 'script/inventree/inventree.js' %}"></script>

View File

@ -19,6 +19,7 @@
linkBarcodeDialog, linkBarcodeDialog,
scanItemsIntoLocation, scanItemsIntoLocation,
unlinkBarcode, unlinkBarcode,
onBarcodeScanClicked,
*/ */
function makeBarcodeInput(placeholderText='', hintText='') { function makeBarcodeInput(placeholderText='', hintText='') {
@ -31,6 +32,9 @@ function makeBarcodeInput(placeholderText='', hintText='') {
hintText = hintText || '{% trans "Enter barcode data" %}'; hintText = hintText || '{% trans "Enter barcode data" %}';
var html = ` var html = `
<div id='barcode_scan_video_container' class='text-center' style='height: 240px; display: none;'>
<video id='barcode_scan_video' disablepictureinpicture playsinline height='240' style='object-fit: fill;'></video>
</div>
<div class='form-group'> <div class='form-group'>
<label class='control-label' for='barcode'>{% trans "Barcode" %}</label> <label class='control-label' for='barcode'>{% trans "Barcode" %}</label>
<div class='controls'> <div class='controls'>
@ -39,6 +43,7 @@ function makeBarcodeInput(placeholderText='', hintText='') {
<span class='fas fa-qrcode'></span> <span class='fas fa-qrcode'></span>
</span> </span>
<input id='barcode' class='textinput textInput form-control' type='text' name='barcode' placeholder='${placeholderText}'> <input id='barcode' class='textinput textInput form-control' type='text' name='barcode' placeholder='${placeholderText}'>
<button id='barcode_scan_btn' class='btn btn-secondary' onclick='onBarcodeScanClicked()' style='display: none;'><span class='fas fa-camera'></span></button>
</div> </div>
<div id='hint_barcode_data' class='help-block'>${hintText}</div> <div id='hint_barcode_data' class='help-block'>${hintText}</div>
</div> </div>
@ -48,6 +53,44 @@ function makeBarcodeInput(placeholderText='', hintText='') {
return html; return html;
} }
qrScanner = null;
function startQrScanner() {
$('#barcode_scan_video_container').show();
qrScanner.start();
}
function stopQrScanner() {
if (qrScanner != null) qrScanner.stop();
$('#barcode_scan_video_container').hide();
}
function onBarcodeScanClicked(e) {
if ($('#barcode_scan_video_container').is(':visible') == false) startQrScanner(); else stopQrScanner();
}
function onCameraAvailable(hasCamera, options) {
if ( hasCamera == true ) {
// Camera is only acccessible if page is served over secure connection
if ( window.isSecureContext == true ) {
qrScanner = new QrScanner(document.getElementById('barcode_scan_video'), (result) => {
onBarcodeScanCompleted(result, options);
}, {
highlightScanRegion: true,
highlightCodeOutline: true,
});
$('#barcode_scan_btn').show();
}
}
}
function onBarcodeScanCompleted(result, options) {
if (result.data == '') return;
console.log('decoded qr code:', result.data);
stopQrScanner();
postBarcodeData(result.data, options);
}
function makeNotesField(options={}) { function makeNotesField(options={}) {
var tooltip = options.tooltip || '{% trans "Enter optional notes for stock transfer" %}'; var tooltip = options.tooltip || '{% trans "Enter optional notes for stock transfer" %}';
@ -186,6 +229,11 @@ function barcodeDialog(title, options={}) {
$(modal).on('shown.bs.modal', function() { $(modal).on('shown.bs.modal', function() {
$(modal + ' .modal-form-content').scrollTop(0); $(modal + ' .modal-form-content').scrollTop(0);
// Check for qr-scanner camera
QrScanner.hasCamera().then( (hasCamera) => {
onCameraAvailable(hasCamera, options);
});
var barcode = $(modal + ' #barcode'); var barcode = $(modal + ' #barcode');
// Handle 'enter' key on barcode // Handle 'enter' key on barcode
@ -220,6 +268,12 @@ function barcodeDialog(title, options={}) {
}); });
$(modal).on('hidden.bs.modal', function() {
stopQrScanner();
if (qrScanner != null) qrScanner.destroy();
qrScanner = null;
});
modalSetTitle(modal, title); modalSetTitle(modal, title);
if (options.onSubmit) { if (options.onSubmit) {

View File

@ -26,15 +26,19 @@
editPurchaseOrderLineItem, editPurchaseOrderLineItem,
exportOrder, exportOrder,
loadPurchaseOrderLineItemTable, loadPurchaseOrderLineItemTable,
loadPurchaseOrderExtraLineTable
loadPurchaseOrderTable, loadPurchaseOrderTable,
loadSalesOrderAllocationTable, loadSalesOrderAllocationTable,
loadSalesOrderLineItemTable, loadSalesOrderLineItemTable,
loadSalesOrderExtraLineTable
loadSalesOrderShipmentTable, loadSalesOrderShipmentTable,
loadSalesOrderTable, loadSalesOrderTable,
newPurchaseOrderFromOrderWizard, newPurchaseOrderFromOrderWizard,
newSupplierPartFromOrderWizard, newSupplierPartFromOrderWizard,
removeOrderRowFromOrderWizard, removeOrderRowFromOrderWizard,
removePurchaseOrderLineItem, removePurchaseOrderLineItem,
loadOrderTotal,
extraLineFields,
*/ */
@ -272,7 +276,7 @@ function createPurchaseOrder(options={}) {
if (options.onSuccess) { if (options.onSuccess) {
options.onSuccess(data); options.onSuccess(data);
} else { } else {
// Default action is to redirect browser to the new PO // Default action is to redirect browser to the new PurchaseOrder
location.href = `/order/purchase-order/${data.pk}/`; location.href = `/order/purchase-order/${data.pk}/`;
} }
}, },
@ -305,6 +309,28 @@ function soLineItemFields(options={}) {
} }
/* Construct a set of fields for a OrderExtraLine form */
function extraLineFields(options={}) {
var fields = {
order: {
hidden: true,
},
quantity: {},
reference: {},
price: {},
price_currency: {},
notes: {},
};
if (options.order) {
fields.order.value = options.order;
}
return fields;
}
/* Construct a set of fields for the PurchaseOrderLineItem form */ /* Construct a set of fields for the PurchaseOrderLineItem form */
function poLineItemFields(options={}) { function poLineItemFields(options={}) {
@ -502,7 +528,7 @@ function newPurchaseOrderFromOrderWizard(e) {
/** /**
* Receive stock items against a PurchaseOrder * Receive stock items against a PurchaseOrder
* Uses the POReceive API endpoint * Uses the PurchaseOrderReceive API endpoint
* *
* arguments: * arguments:
* - order_id, ID / PK for the PurchaseOrder instance * - order_id, ID / PK for the PurchaseOrder instance
@ -1373,6 +1399,226 @@ function loadPurchaseOrderLineItemTable(table, options={}) {
} }
/**
* Load a table displaying lines for a particular PurchaseOrder
*
* @param {String} table : HTML ID tag e.g. '#table'
* @param {Object} options : object which contains:
* - order {integer} : pk of the PurchaseOrder
* - status: {integer} : status code for the order
*/
function loadPurchaseOrderExtraLineTable(table, options={}) {
options.table = table;
options.params = options.params || {};
if (!options.order) {
console.log('ERROR: function called without order ID');
return;
}
if (!options.status) {
console.log('ERROR: function called without order status');
return;
}
options.params.order = options.order;
options.params.part_detail = true;
options.params.allocations = true;
var filters = loadTableFilters('purchaseorderextraline');
for (var key in options.params) {
filters[key] = options.params[key];
}
options.url = options.url || '{% url "api-po-extra-line-list" %}';
var filter_target = options.filter_target || '#filter-list-purchase-order-extra-lines';
setupFilterList('purchaseorderextraline', $(table), filter_target);
// Is the order pending?
var pending = options.status == {{ SalesOrderStatus.PENDING }};
// Table columns to display
var columns = [
{
sortable: true,
field: 'reference',
title: '{% trans "Reference" %}',
switchable: true,
},
{
sortable: true,
field: 'quantity',
title: '{% trans "Quantity" %}',
footerFormatter: function(data) {
return data.map(function(row) {
return +row['quantity'];
}).reduce(function(sum, i) {
return sum + i;
}, 0);
},
switchable: false,
},
{
sortable: true,
field: 'price',
title: '{% trans "Unit Price" %}',
formatter: function(value, row) {
var formatter = new Intl.NumberFormat(
'en-US',
{
style: 'currency',
currency: row.price_currency
}
);
return formatter.format(row.price);
}
},
{
field: 'total_price',
sortable: true,
title: '{% trans "Total Price" %}',
formatter: function(value, row) {
var formatter = new Intl.NumberFormat(
'en-US',
{
style: 'currency',
currency: row.price_currency
}
);
return formatter.format(row.price * row.quantity);
},
footerFormatter: function(data) {
var total = data.map(function(row) {
return +row['price'] * row['quantity'];
}).reduce(function(sum, i) {
return sum + i;
}, 0);
var currency = (data.slice(-1)[0] && data.slice(-1)[0].price_currency) || 'USD';
var formatter = new Intl.NumberFormat(
'en-US',
{
style: 'currency',
currency: currency
}
);
return formatter.format(total);
}
}
];
columns.push({
field: 'notes',
title: '{% trans "Notes" %}',
});
if (pending) {
columns.push({
field: 'buttons',
switchable: false,
formatter: function(value, row, index, field) {
var html = `<div class='btn-group float-right' role='group'>`;
var pk = row.pk;
html += makeIconButton('fa-clone', 'button-duplicate', pk, '{% trans "Duplicate line" %}');
html += makeIconButton('fa-edit icon-blue', 'button-edit', pk, '{% trans "Edit line" %}');
html += makeIconButton('fa-trash-alt icon-red', 'button-delete', pk, '{% trans "Delete line" %}', );
html += `</div>`;
return html;
}
});
}
function reloadTable() {
$(table).bootstrapTable('refresh');
reloadTotal();
}
// Configure callback functions once the table is loaded
function setupCallbacks() {
// Callback for duplicating lines
$(table).find('.button-duplicate').click(function() {
var pk = $(this).attr('pk');
inventreeGet(`/api/order/po-extra-line/${pk}/`, {}, {
success: function(data) {
var fields = extraLineFields();
constructForm('{% url "api-po-extra-line-list" %}', {
method: 'POST',
fields: fields,
data: data,
title: '{% trans "Duplicate Line" %}',
onSuccess: function(response) {
$(table).bootstrapTable('refresh');
}
});
}
});
});
// Callback for editing lines
$(table).find('.button-edit').click(function() {
var pk = $(this).attr('pk');
constructForm(`/api/order/po-extra-line/${pk}/`, {
fields: {
quantity: {},
reference: {},
price: {},
price_currency: {},
notes: {},
},
title: '{% trans "Edit Line" %}',
onSuccess: reloadTable,
});
});
// Callback for deleting lines
$(table).find('.button-delete').click(function() {
var pk = $(this).attr('pk');
constructForm(`/api/order/po-extra-line/${pk}/`, {
method: 'DELETE',
title: '{% trans "Delete Line" %}',
onSuccess: reloadTable,
});
});
}
$(table).inventreeTable({
onPostBody: setupCallbacks,
name: 'purchaseorderextraline',
sidePagination: 'client',
formatNoMatches: function() {
return '{% trans "No matching line" %}';
},
queryParams: filters,
original: options.params,
url: options.url,
showFooter: true,
uniqueId: 'pk',
detailViewByClick: false,
columns: columns,
});
}
/* /*
* Load table displaying list of sales orders * Load table displaying list of sales orders
*/ */
@ -2259,6 +2505,26 @@ function showFulfilledSubTable(index, row, element, options) {
}); });
} }
var TotalPriceRef = ''; // reference to total price field
var TotalPriceOptions = {}; // options to reload the price
function loadOrderTotal(reference, options={}) {
TotalPriceRef = reference;
TotalPriceOptions = options;
}
function reloadTotal() {
inventreeGet(
TotalPriceOptions.url,
{},
{
success: function(data) {
$(TotalPriceRef).html(data.total_price_string);
}
}
);
};
/** /**
* Load a table displaying line items for a particular SalesOrder * Load a table displaying line items for a particular SalesOrder
@ -2556,6 +2822,7 @@ function loadSalesOrderLineItemTable(table, options={}) {
function reloadTable() { function reloadTable() {
$(table).bootstrapTable('refresh'); $(table).bootstrapTable('refresh');
reloadTotal();
} }
// Configure callback functions once the table is loaded // Configure callback functions once the table is loaded
@ -2765,3 +3032,223 @@ function loadSalesOrderLineItemTable(table, options={}) {
columns: columns, columns: columns,
}); });
} }
/**
* Load a table displaying lines for a particular SalesOrder
*
* @param {String} table : HTML ID tag e.g. '#table'
* @param {Object} options : object which contains:
* - order {integer} : pk of the SalesOrder
* - status: {integer} : status code for the order
*/
function loadSalesOrderExtraLineTable(table, options={}) {
options.table = table;
options.params = options.params || {};
if (!options.order) {
console.log('ERROR: function called without order ID');
return;
}
if (!options.status) {
console.log('ERROR: function called without order status');
return;
}
options.params.order = options.order;
options.params.part_detail = true;
options.params.allocations = true;
var filters = loadTableFilters('salesorderextraline');
for (var key in options.params) {
filters[key] = options.params[key];
}
options.url = options.url || '{% url "api-so-extra-line-list" %}';
var filter_target = options.filter_target || '#filter-list-sales-order-extra-lines';
setupFilterList('salesorderextraline', $(table), filter_target);
// Is the order pending?
var pending = options.status == {{ SalesOrderStatus.PENDING }};
// Table columns to display
var columns = [
{
sortable: true,
field: 'reference',
title: '{% trans "Reference" %}',
switchable: true,
},
{
sortable: true,
field: 'quantity',
title: '{% trans "Quantity" %}',
footerFormatter: function(data) {
return data.map(function(row) {
return +row['quantity'];
}).reduce(function(sum, i) {
return sum + i;
}, 0);
},
switchable: false,
},
{
sortable: true,
field: 'price',
title: '{% trans "Unit Price" %}',
formatter: function(value, row) {
var formatter = new Intl.NumberFormat(
'en-US',
{
style: 'currency',
currency: row.price_currency
}
);
return formatter.format(row.price);
}
},
{
field: 'total_price',
sortable: true,
title: '{% trans "Total Price" %}',
formatter: function(value, row) {
var formatter = new Intl.NumberFormat(
'en-US',
{
style: 'currency',
currency: row.price_currency
}
);
return formatter.format(row.price * row.quantity);
},
footerFormatter: function(data) {
var total = data.map(function(row) {
return +row['price'] * row['quantity'];
}).reduce(function(sum, i) {
return sum + i;
}, 0);
var currency = (data.slice(-1)[0] && data.slice(-1)[0].price_currency) || 'USD';
var formatter = new Intl.NumberFormat(
'en-US',
{
style: 'currency',
currency: currency
}
);
return formatter.format(total);
}
}
];
columns.push({
field: 'notes',
title: '{% trans "Notes" %}',
});
if (pending) {
columns.push({
field: 'buttons',
switchable: false,
formatter: function(value, row, index, field) {
var html = `<div class='btn-group float-right' role='group'>`;
var pk = row.pk;
html += makeIconButton('fa-clone', 'button-duplicate', pk, '{% trans "Duplicate line" %}');
html += makeIconButton('fa-edit icon-blue', 'button-edit', pk, '{% trans "Edit line" %}');
html += makeIconButton('fa-trash-alt icon-red', 'button-delete', pk, '{% trans "Delete line" %}', );
html += `</div>`;
return html;
}
});
}
function reloadTable() {
$(table).bootstrapTable('refresh');
reloadTotal();
}
// Configure callback functions once the table is loaded
function setupCallbacks() {
// Callback for duplicating lines
$(table).find('.button-duplicate').click(function() {
var pk = $(this).attr('pk');
inventreeGet(`/api/order/so-extra-line/${pk}/`, {}, {
success: function(data) {
var fields = extraLineFields();
constructForm('{% url "api-so-extra-line-list" %}', {
method: 'POST',
fields: fields,
data: data,
title: '{% trans "Duplicate Line" %}',
onSuccess: function(response) {
$(table).bootstrapTable('refresh');
}
});
}
});
});
// Callback for editing lines
$(table).find('.button-edit').click(function() {
var pk = $(this).attr('pk');
constructForm(`/api/order/so-extra-line/${pk}/`, {
fields: {
quantity: {},
reference: {},
price: {},
price_currency: {},
notes: {},
},
title: '{% trans "Edit Line" %}',
onSuccess: reloadTable,
});
});
// Callback for deleting lines
$(table).find('.button-delete').click(function() {
var pk = $(this).attr('pk');
constructForm(`/api/order/so-extra-line/${pk}/`, {
method: 'DELETE',
title: '{% trans "Delete Line" %}',
onSuccess: reloadTable,
});
});
}
$(table).inventreeTable({
onPostBody: setupCallbacks,
name: 'salesorderextraline',
sidePagination: 'client',
formatNoMatches: function() {
return '{% trans "No matching lines" %}';
},
queryParams: filters,
original: options.params,
url: options.url,
showFooter: true,
uniqueId: 'pk',
detailViewByClick: false,
columns: columns,
});
}

View File

@ -271,7 +271,7 @@ function printBomReports(parts) {
function printPurchaseOrderReports(orders) { function printPurchaseOrderReports(orders) {
/** /**
* Print PO reports for the provided purchase order(s) * Print PurchaseOrder reports for the provided purchase order(s)
*/ */
if (orders.length == 0) { if (orders.length == 0) {
@ -325,7 +325,7 @@ function printPurchaseOrderReports(orders) {
function printSalesOrderReports(orders) { function printSalesOrderReports(orders) {
/** /**
* Print SO reports for the provided purchase order(s) * Print SalesOrder reports for the provided purchase order(s)
*/ */
if (orders.length == 0) { if (orders.length == 0) {

View File

@ -132,6 +132,7 @@ class RuleSet(models.Model):
'order_purchaseorder', 'order_purchaseorder',
'order_purchaseorderattachment', 'order_purchaseorderattachment',
'order_purchaseorderlineitem', 'order_purchaseorderlineitem',
'order_purchaseorderextraline',
'company_supplierpart', 'company_supplierpart',
'company_manufacturerpart', 'company_manufacturerpart',
'company_manufacturerpartparameter', 'company_manufacturerpartparameter',
@ -142,6 +143,7 @@ class RuleSet(models.Model):
'order_salesorderallocation', 'order_salesorderallocation',
'order_salesorderattachment', 'order_salesorderattachment',
'order_salesorderlineitem', 'order_salesorderlineitem',
'order_salesorderextraline',
'order_salesordershipment', 'order_salesordershipment',
] ]
} }

View File

@ -25,6 +25,7 @@ django-mptt==0.11.0 # Modified Preorder Tree Traversal
django-redis>=5.0.0 # Redis integration django-redis>=5.0.0 # Redis integration
django-q==1.3.4 # Background task scheduling django-q==1.3.4 # Background task scheduling
django-sql-utils==0.5.0 # Advanced query annotation / aggregation django-sql-utils==0.5.0 # Advanced query annotation / aggregation
django-sslserver==0.22 # Secure HTTP development server
django-stdimage==5.1.1 # Advanced ImageField management django-stdimage==5.1.1 # Advanced ImageField management
django-test-migrations==1.1.0 # Unit testing for database migrations django-test-migrations==1.1.0 # Unit testing for database migrations
django-user-sessions==1.7.1 # user sessions in DB django-user-sessions==1.7.1 # user sessions in DB