From 8639febf794c79ace851f58110e8250efe6702e9 Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 21 Feb 2022 13:20:52 +1100 Subject: [PATCH] 0.6.0 stable (#2655) * fix spelling Thanks @Stephano120 Good catch! * add migration * I18n merge (#2582) * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Adds a warning if no build outputs are created * Throw validation error if no build outputs have been started * Prevent AttributeError from being thrown Ref: https://github.com/inventree/InvenTree/issues/2587 * fix: use default storage backend for Maint Mode * Experimenting with children models permissions * Prevent build outputs being created with zero quantity * PEP style fixes * Reload build output table when an active build output is deleted * Reload completed output table * Fixes issue with BOM export - Cascading BOM export was broken * Boolean settings are now directly clickable * Layout changes * Display error if setting update failes * Skips some specific steps when importing data - We need to prevent certain operations from running when we are importing data - This is to prevent unique database constraints from being violated - Do not register plugins during data import - Do not launch notification events * PEP fixes * Adds new setting to optionally display or hide part pricing information * Hide pricing history tab if not enabled * Only calculate pricing data if required * Disable multi-level BOM requests * Adds serializer for uploading a BOM file and extracting fields * Fix existing bug with BomExport functionality - could not select BOM format * POST request now returns extracted data rows (as an array of dicts) * Attempt to auto-extract part information based on provided data * Basic javascript function to construct BOM table from extracted data * Initialize related field for "part" selection * Add callback for "remove row" button * Construct required form fields - required some additional functionality in forms.js * Add "clear input" callback function * Add optional part lookup by "part" field * Allow decimal values for BOM overage * Adds a BomUpload endpoint to handle upload of complete BOM * Check for duplicate BOM items as part of serializer validation * bug fix * Adds options to clear existing BOM data when uploading * Update upload file template * Remove old templates * PEP fixes * Handle errors when connecting to currency exchange - Also adds timeout when connecting * JS linting * Only update rates on server launch if there are no rates available * PEP fixes * unit test fixes * Do not hide the "submit order" button * Remove incorrect validation routine * Refactored and added permission check for children models * Reverted print statement to logger * Improved approach to permission check at runtime * Fix logic for enabling "place order" button * Update README.md Add "follow on twitter" button * Allow POST of files for unit testing * Update README.md Reorder sections * Catch potential file processing errors * Add unit testing for uploading invalid BOM files * Raise error if imported dataset contains no data rows * Return per-row error messages when extracting data * PEP fixes * Adds check for duplicate parts when importing * Increased error checking when uploading BOM data * Catch potential error when posting invalid numbers via REST API * Improve part "guess" algorithm * Display initial errors when importing data * Add button to display original row data * Disable "submit" button to prevent multiple simultaneous uploads * Add more unit testing for BOM file upload - Test "levels" functionality - Test part guessing / introspection * Adds API endpoint to delete build outputs * Remove old form code which is no longer used * Cleanup * js linting * Update base django version * fix quotes * ignore the django import check * ignore import error * ignore migration * ignore branches * remove coverage from parts migrations * fix migration coverage for orders * fix migration coverage for company * fix migration coverage for build * simpler coverage ignore * run test paralell * ignore coverage on ruleset checks * remove dead code * move up comment so unneeded functions are not not covered * remove dead code * ignore database not ready * imports are not tested * no test for malformed paths * ignore exception ref * only run sqlite paralell * fix import * remove dead code * PEP fix * ignore wrong control view safeties * ignore controls that should not be reached in coverage * test wrong setting defaults * remove paralell coverage * fix setting coverage * Remove settings mods * Pep * Allow BOM file to be "re-uploaded" * ignore ci render_test * add comment about function * ignore debug toolbar * app not ready can not be simulated by tests * use same style for AppNotReady Exception * database not ready events are hard to reproduce consistently * remove dead test * ignore not testable condition * ignore coverage in exsisting migrations * will never be true in testing * add test for system healt checks * test test mode * test Isimporting * ignore whole file * ignore system exit conditions in coverage * ignore testing coditions in coverage * do not cover secret key * ignore db optm in coverage * ignore some default in coverage * ignore currently dead code in coverage * ignore wsgi * remove dead code * should not be reached - ignore in cov * ignore sanity checks for coverage * update system health check * fix label tests * omit coverage via setup.cfg * fix reporting emition * catch more explicit * fix coverage * except import errors * add coverage for labels * PEP fix * do not count unreachable code * ignore unreachable things * user api tests * PEP fix * remove coverage that is not reachable * remove cov from not used feature * remove dead code * make git log call simpler * return cov from feature only used for debug * should not be reached * add more plugin coverage * PEP fixes * spellcheck * add test for non existing token * remove dead code -> permission class does that already * add more user api tests * disable broken test * Enforce proper formatting for 'quantity' field when importing BOM data * Adds unit tests for index page Some fairly simple unit tests to ensure that the index page is being correctly loaded. * Adds a new API endpoint for creating build outputs * Adds query function to Part model to return trackable parts in the BOM * Extract serial numbers from submitted form data * Optionally auto-allocate stock items when creating a new build output * remove code which is now unused * PEP style fixes * Form improvements * Automatically select Bom Items with matching serial numbers when allocating stock to a build order output * Adds generic API endpoint for extracting data from a tabulated file * Adds model mixin for generically determining which fields can be imported on any particular model * Adds functionality to map file columns to model fiels * Refactoring API endpoints - Improved URL naming scheme * Adds generic javascript function for mapping file columns to model fields * Adds a button to quickly "pass" a test * js linting * Fix field name * unit test fixes * Fix breadcrumb tree for stock item page * Create FUNDING.yml Add sponsor file * Update FUNDING.yml Add ko_fi username * Spelling fix * Implement unit test for missing columns * Improve unit testing * Further improvements to unit tests * Adds information on test result being deleted * Adds "refresh" button for stock test table * Ensure unit tests are more resilient * Adds API endpoint for installing stock items into other stock items - Requires more filtering for the Part API - Adds more BOM related functionality for Part model - Removes old server-side form * PEP fixes * Critical bug fix: Check if serial numbers already exist when creating new StockItem * Allow processing of "null" cells (caused by xls / xlsx import) * Reintroduce option to clear (delete) BOM before uploading new data * When uploading a report template, keep the existing filename (if it is the same report!) * Improved error messages when report templates (or snippets) are missing! * Delete template files from cache as they are uploaded * Set default error message visibility in modal options * remove unused code * remove unneeded assignment * merge satement * merge statments * remove unneeded continue * PEP fix * I18n merge (#2647) * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * fix caps * fix string concat * use f-string annotation * I18n release merge (#2654) * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix: New translations django.po from Crowdin * Fix conflict * Removes outdated templates Co-authored-by: Matthias Mair <66015116+matmair@users.noreply.github.com> Co-authored-by: Matthias Co-authored-by: Nigel Co-authored-by: eeintech --- .github/FUNDING.yml | 2 + .github/ISSUE_TEMPLATE/app_issue.md | 30 - .github/ISSUE_TEMPLATE/bug_report.md | 32 +- .github/workflows/check_translations.yaml | 37 + .github/workflows/coverage.yaml | 60 - .github/workflows/docker_latest.yaml | 1 - .github/workflows/docker_stable.yaml | 8 +- .github/workflows/docker_tag.yaml | 8 +- .github/workflows/docker_test.yaml | 37 + .github/workflows/html.yaml | 54 - .github/workflows/javascript.yaml | 50 - .github/workflows/mysql.yaml | 67 - .github/workflows/postgresql.yaml | 63 - .github/workflows/python.yaml | 49 - .github/workflows/qc_checks.yaml | 304 + .github/workflows/stale.yml | 25 + .github/workflows/style.yaml | 34 - .../workflows/{version.yaml => version.yml} | 9 +- .github/workflows/welcome.yml | 17 + .gitignore | 11 +- .gitpod.yml | 47 + InvenTree/InvenTree/api.py | 20 +- InvenTree/InvenTree/api_tester.py | 22 +- InvenTree/InvenTree/apps.py | 56 +- InvenTree/InvenTree/ci_render_js.py | 6 +- InvenTree/InvenTree/config.py | 90 + InvenTree/InvenTree/context.py | 2 +- InvenTree/InvenTree/exchange.py | 34 +- InvenTree/InvenTree/fields.py | 2 +- InvenTree/InvenTree/filters.py | 41 +- InvenTree/InvenTree/forms.py | 127 +- InvenTree/InvenTree/helpers.py | 76 +- InvenTree/InvenTree/locale_stats.json | 1 - .../management/commands/clean_settings.py | 17 +- .../management/commands/rebuild_thumbnails.py | 70 + .../management/commands/remove_mfa.py | 36 + InvenTree/InvenTree/metadata.py | 51 +- InvenTree/InvenTree/middleware.py | 44 +- InvenTree/InvenTree/models.py | 141 +- InvenTree/InvenTree/plugins.py | 43 - InvenTree/InvenTree/ready.py | 12 +- InvenTree/InvenTree/serializers.py | 370 +- InvenTree/InvenTree/settings.py | 495 +- .../bootstrap-table-en-US.min.js | 10 - .../bootstrap-table-accent-neutralise.js | 170 - .../accent-neutralise/extension.json | 17 - .../extensions/auto-refresh/extension.json | 17 - .../extensions/cookie/extension.json | 17 - .../extensions/copy-rows/extension.json | 17 - .../extensions/defer-url/extension.json | 17 - .../extensions/editable/extension.json | 17 - .../extensions/export/extension.json | 17 - .../extensions/filter-control/extension.json | 17 - .../extensions/group-by-v2/extension.json | 12 - .../group-by/bootstrap-table-group-by.css | 53 - .../group-by/bootstrap-table-group-by.js | 243 - .../extensions/group-by/extension.json | 17 - .../extensions/i18n-enhance/extension.json | 17 - .../extensions/key-events/extension.json | 17 - .../extensions/mobile/extension.json | 17 - .../bootstrap-table-multi-toggle.js | 88 - .../multi-column-toggle/extension.json | 17 - .../bootstrap-table-multiple-search.js | 71 - .../extensions/multiple-search/extension.json | 17 - ...bootstrap-table-multiple-selection-row.css | 17 - .../bootstrap-table-multiple-selection-row.js | 127 - .../multiple-selection-row/extension.json | 17 - .../extensions/multiple-sort/extension.json | 17 - .../bootstrap-table-natural-sorting.js | 67 - .../extensions/natural-sorting/extension.json | 17 - .../extensions/pipeline/LICENSE | 21 - .../extensions/pipeline/README.md | 92 - .../extensions/pipeline/extension.json | 18 - .../extensions/reorder-columns/extension.json | 17 - .../extensions/reorder-rows/extension.json | 17 - .../extensions/resizable/extension.json | 17 - .../bootstrap-table-select2-filter.js | 332 - .../extensions/select2-filter/extension.json | 17 - .../extensions/sticky-header/extension.json | 17 - .../extensions/toolbar/extension.json | 17 - .../bootstrap-table-tree-column.css | 1 - .../bootstrap-table-tree-column.js | 130 - .../bootstrap-table-tree-column.less | 43 - .../extensions/tree-column/extension.json | 17 - .../extensions/tree-column/icon.png | Bin 346 -> 0 bytes .../extensions/treegrid/extension.json | 17 - .../bootstrap-table/bootstrap-table.css | 790 ++ .../themes/bootstrap-table/bootstrap-table.js | 1124 ++ .../bootstrap-table/bootstrap-table.min.css | 10 + .../bootstrap-table/bootstrap-table.min.js | 10 + .../bootstrap-table/fonts/bootstrap-table.eot | Bin 0 -> 7176 bytes .../bootstrap-table/fonts/bootstrap-table.svg | 28 + .../bootstrap-table/fonts/bootstrap-table.ttf | Bin 0 -> 6980 bytes .../fonts/bootstrap-table.woff | Bin 0 -> 7056 bytes .../themes/bulma/bootstrap-table-bulma.css | 440 + .../themes/bulma/bootstrap-table-bulma.js | 1116 ++ .../bulma/bootstrap-table-bulma.min.css | 10 + .../themes/bulma/bootstrap-table-bulma.min.js | 10 + .../foundation/bootstrap-table-foundation.css | 423 + .../foundation/bootstrap-table-foundation.js | 1132 ++ .../bootstrap-table-foundation.min.css | 10 + .../bootstrap-table-foundation.min.js | 10 + .../bootstrap-table-materialize.css | 419 + .../bootstrap-table-materialize.js | 1125 ++ .../bootstrap-table-materialize.min.css | 10 + .../bootstrap-table-materialize.min.js | 10 + .../semantic/bootstrap-table-semantic.css | 422 + .../semantic/bootstrap-table-semantic.js | 1093 ++ .../semantic/bootstrap-table-semantic.min.css | 10 + .../semantic/bootstrap-table-semantic.min.js | 10 + .../static/bootstrap/css/bootstrap-grid.css | 5051 +++++++ .../bootstrap/css/bootstrap-grid.css.map | 1 + .../bootstrap/css/bootstrap-grid.min.css | 7 + .../bootstrap/css/bootstrap-grid.min.css.map | 1 + .../bootstrap/css/bootstrap-grid.rtl.css | 5050 +++++++ .../bootstrap/css/bootstrap-grid.rtl.css.map | 1 + .../bootstrap/css/bootstrap-grid.rtl.min.css | 7 + .../css/bootstrap-grid.rtl.min.css.map | 1 + .../static/bootstrap/css/bootstrap-reboot.css | 485 + .../bootstrap/css/bootstrap-reboot.css.map | 1 + .../bootstrap/css/bootstrap-reboot.min.css | 8 + .../css/bootstrap-reboot.min.css.map | 1 + .../bootstrap/css/bootstrap-reboot.rtl.css | 482 + .../css/bootstrap-reboot.rtl.css.map | 1 + .../css/bootstrap-reboot.rtl.min.css | 8 + .../css/bootstrap-reboot.rtl.min.css.map | 1 + .../bootstrap/css/bootstrap-utilities.css | 4866 +++++++ .../bootstrap/css/bootstrap-utilities.css.map | 1 + .../bootstrap/css/bootstrap-utilities.min.css | 7 + .../css/bootstrap-utilities.min.css.map | 1 + .../bootstrap/css/bootstrap-utilities.rtl.css | 4857 +++++++ .../css/bootstrap-utilities.rtl.css.map | 1 + .../css/bootstrap-utilities.rtl.min.css | 7 + .../css/bootstrap-utilities.rtl.min.css.map | 1 + .../static/bootstrap/css/bootstrap.css | 11266 ++++++++++++++++ .../static/bootstrap/css/bootstrap.css.map | 1 + .../static/bootstrap/css/bootstrap.min.css | 7 + .../bootstrap/css/bootstrap.min.css.map | 1 + .../static/bootstrap/css/bootstrap.rtl.css | 11242 +++++++++++++++ .../bootstrap/css/bootstrap.rtl.css.map | 1 + .../bootstrap/css/bootstrap.rtl.min.css | 7 + .../bootstrap/css/bootstrap.rtl.min.css.map | 1 + .../static/bootstrap/js/bootstrap.bundle.js | 6812 ++++++++++ .../bootstrap/js/bootstrap.bundle.js.map | 1 + .../bootstrap/js/bootstrap.bundle.min.js | 7 + .../bootstrap/js/bootstrap.bundle.min.js.map | 1 + .../static/bootstrap/js/bootstrap.esm.js | 4999 +++++++ .../static/bootstrap/js/bootstrap.esm.js.map | 1 + .../static/bootstrap/js/bootstrap.esm.min.js | 7 + .../bootstrap/js/bootstrap.esm.min.js.map | 1 + .../static/bootstrap/js/bootstrap.js | 5046 +++++++ .../static/bootstrap/js/bootstrap.js.map | 1 + .../static/bootstrap/js/bootstrap.min.js | 7 + .../static/bootstrap/js/bootstrap.min.js.map | 1 + .../InvenTree/static/css/bootstrap-toggle.css | 83 - .../static/css/bootstrap.min.css.map | 1 - .../css/bootstrap_3.3.7_css_bootstrap.min.css | 6 - .../static/css/color-themes/dark-reader.css | 4549 ++++--- InvenTree/InvenTree/static/css/inventree.css | 420 +- .../InvenTree/static/img/paper_splash.jpg | Bin 1251734 -> 471840 bytes .../static/img/paper_splash_large.jpg | Bin 0 -> 1251734 bytes .../script/bootstrap/bootstrap-toggle.js | 180 - .../static/script/bootstrap/bootstrap.min.js | 7 - .../static/script/inventree/delay.js | 12 - .../static/script/inventree}/inventree.js | 164 +- .../static/script/inventree/notification.js | 156 +- .../static/script/inventree/sidenav.js | 249 - .../select2/css/select2-bootstrap-5-theme.css | 514 + InvenTree/InvenTree/status.py | 12 +- InvenTree/InvenTree/status_codes.py | 54 +- InvenTree/InvenTree/tasks.py | 172 +- InvenTree/InvenTree/test_api.py | 12 +- InvenTree/InvenTree/test_urls.py | 10 +- InvenTree/InvenTree/test_views.py | 65 +- InvenTree/InvenTree/tests.py | 79 +- InvenTree/InvenTree/urls.py | 89 +- InvenTree/InvenTree/utils.py | 13 - InvenTree/InvenTree/validators.py | 47 +- InvenTree/InvenTree/version.py | 60 +- InvenTree/InvenTree/views.py | 157 +- InvenTree/barcodes/api.py | 53 +- InvenTree/barcodes/barcode.py | 166 +- InvenTree/barcodes/tests.py | 4 +- InvenTree/build/admin.py | 19 + InvenTree/build/api.py | 228 +- InvenTree/build/fixtures/build.yaml | 2 +- InvenTree/build/forms.py | 253 +- .../migrations/0013_auto_20200425_0507.py | 2 +- .../build/migrations/0018_build_reference.py | 5 +- .../migrations/0029_auto_20210601_1525.py | 6 +- .../migrations/0031_build_reference_int.py | 18 + .../migrations/0032_auto_20211014_0632.py | 50 + .../migrations/0033_auto_20211128_0151.py | 25 + .../0034_alter_build_reference_int.py | 18 + InvenTree/build/models.py | 348 +- InvenTree/build/serializers.py | 611 +- InvenTree/build/tasks.py | 102 + .../templates/build/allocation_card.html | 51 - .../build/templates/build/auto_allocate.html | 43 - .../build/templates/build/build_base.html | 223 +- .../templates/build/build_output_create.html | 20 - InvenTree/build/templates/build/complete.html | 26 - .../templates/build/complete_output.html | 53 - .../templates/build/create_build_item.html | 20 - .../templates/build/delete_build_item.html | 14 - InvenTree/build/templates/build/detail.html | 380 +- .../templates/build/edit_build_item.html | 10 - InvenTree/build/templates/build/index.html | 61 +- InvenTree/build/templates/build/navbar.html | 57 - InvenTree/build/templates/build/sidebar.html | 22 + .../build/templates/build/unallocate.html | 15 - InvenTree/build/test_api.py | 359 +- InvenTree/build/test_build.py | 54 +- InvenTree/build/tests.py | 98 +- InvenTree/build/urls.py | 13 - InvenTree/build/views.py | 803 +- InvenTree/common/admin.py | 39 +- InvenTree/common/api.py | 232 + InvenTree/common/apps.py | 22 +- InvenTree/common/files.py | 2 - .../migrations/0012_notificationentry.py | 25 + .../0013_webhookendpoint_webhookmessage.py | 40 + InvenTree/common/models.py | 744 +- InvenTree/common/serializers.py | 94 + InvenTree/common/tasks.py | 29 + .../templates/common/delete_currency.html | 7 - .../common/templates/common/edit_setting.html | 14 - InvenTree/common/test_views.py | 132 - InvenTree/common/tests.py | 158 +- InvenTree/common/views.py | 122 +- InvenTree/company/admin.py | 28 +- InvenTree/company/api.py | 2 +- InvenTree/company/apps.py | 39 +- InvenTree/company/fixtures/company.yaml | 1 + .../migrations/0019_auto_20200413_0642.py | 32 +- .../0024_unique_name_email_constraint.py | 2 +- .../migrations/0026_auto_20201110_1011.py | 4 +- .../migrations/0036_supplierpart_update_2.py | 6 +- InvenTree/company/models.py | 2 +- InvenTree/company/serializers.py | 7 +- .../templates/company/company_base.html | 130 +- .../company/templates/company/detail.html | 143 +- .../company/templates/company/index.html | 34 +- .../templates/company/manufacturer_part.html | 206 +- .../company/manufacturer_part_navbar.html | 41 - .../company/manufacturer_part_sidebar.html | 8 + .../company/templates/company/navbar.html | 70 - .../company/templates/company/sidebar.html | 26 + .../templates/company/supplier_part.html | 197 +- .../company/supplier_part_sidebar.html | 10 + InvenTree/company/test_api.py | 2 +- InvenTree/company/test_views.py | 24 - InvenTree/company/urls.py | 2 +- InvenTree/config_template.yaml | 32 +- InvenTree/label/api.py | 2 +- InvenTree/label/apps.py | 31 +- InvenTree/label/models.py | 4 +- InvenTree/label/test_api.py | 36 + InvenTree/label/tests.py | 11 +- InvenTree/locale/de/LC_MESSAGES/django.mo | Bin 125311 -> 128484 bytes InvenTree/locale/de/LC_MESSAGES/django.po | 8419 +++++++----- InvenTree/locale/el/LC_MESSAGES/django.mo | Bin 0 -> 523 bytes InvenTree/locale/el/LC_MESSAGES/django.po | 8301 +++++++----- InvenTree/locale/en/LC_MESSAGES/django.po | 6044 +++++---- InvenTree/locale/es/LC_MESSAGES/django.mo | Bin 327 -> 12870 bytes InvenTree/locale/es/LC_MESSAGES/django.po | 8615 +++++++----- InvenTree/locale/es_MX/LC_MESSAGES/django.mo | Bin 0 -> 380 bytes InvenTree/locale/es_MX/LC_MESSAGES/django.po | 8684 ++++++++++++ InvenTree/locale/fr/LC_MESSAGES/django.mo | Bin 325 -> 7801 bytes InvenTree/locale/fr/LC_MESSAGES/django.po | 9181 +++++++------ InvenTree/locale/he/LC_MESSAGES/django.mo | Bin 0 -> 574 bytes InvenTree/locale/he/LC_MESSAGES/django.po | 8473 +++++++----- InvenTree/locale/id/LC_MESSAGES/django.mo | Bin 0 -> 521 bytes InvenTree/locale/id/LC_MESSAGES/django.po | 8329 +++++++----- InvenTree/locale/it/LC_MESSAGES/django.mo | Bin 327 -> 6061 bytes InvenTree/locale/it/LC_MESSAGES/django.po | 9729 +++++++------ InvenTree/locale/ja/LC_MESSAGES/django.mo | Bin 321 -> 5973 bytes InvenTree/locale/ja/LC_MESSAGES/django.po | 8133 ++++++----- InvenTree/locale/ko/LC_MESSAGES/django.mo | Bin 0 -> 517 bytes InvenTree/locale/ko/LC_MESSAGES/django.po | 8607 +++++++----- InvenTree/locale/nl/LC_MESSAGES/django.mo | Bin 0 -> 19401 bytes InvenTree/locale/nl/LC_MESSAGES/django.po | 8469 +++++++----- InvenTree/locale/no/LC_MESSAGES/django.mo | Bin 0 -> 3436 bytes InvenTree/locale/no/LC_MESSAGES/django.po | 8707 ++++++------ InvenTree/locale/pl/LC_MESSAGES/django.mo | Bin 472 -> 27867 bytes InvenTree/locale/pl/LC_MESSAGES/django.po | 8215 ++++++----- InvenTree/locale/pt/LC_MESSAGES/django.mo | Bin 0 -> 380 bytes InvenTree/locale/pt/LC_MESSAGES/django.po | 9648 +++++++++++++ InvenTree/locale/ru/LC_MESSAGES/django.mo | Bin 492 -> 12057 bytes InvenTree/locale/ru/LC_MESSAGES/django.po | 8421 +++++++----- InvenTree/locale/sv/LC_MESSAGES/django.mo | Bin 0 -> 4084 bytes InvenTree/locale/sv/LC_MESSAGES/django.po | 8301 +++++++----- InvenTree/locale/th/LC_MESSAGES/django.mo | Bin 0 -> 515 bytes InvenTree/locale/th/LC_MESSAGES/django.po | 8301 +++++++----- InvenTree/locale/tr/LC_MESSAGES/django.mo | Bin 379 -> 48473 bytes InvenTree/locale/tr/LC_MESSAGES/django.po | 8287 +++++++----- InvenTree/locale/vi/LC_MESSAGES/django.mo | Bin 0 -> 521 bytes InvenTree/locale/vi/LC_MESSAGES/django.po | 8359 +++++++----- InvenTree/locale/zh/LC_MESSAGES/django.mo | Bin 331 -> 44746 bytes InvenTree/locale/zh/LC_MESSAGES/django.po | 8203 ++++++----- InvenTree/manage.py | 4 +- InvenTree/order/admin.py | 70 +- InvenTree/order/api.py | 514 +- InvenTree/order/forms.py | 109 +- .../migrations/0051_auto_20211014_0623.py | 23 + .../migrations/0052_auto_20211014_0631.py | 66 + .../migrations/0053_auto_20211128_0151.py | 35 + .../migrations/0053_salesordershipment.py | 31 + .../migrations/0054_auto_20211201_2139.py | 23 + .../0054_salesorderallocation_shipment.py | 19 + .../migrations/0055_auto_20211025_0645.py | 92 + ...056_alter_salesorderallocation_shipment.py | 19 + .../0057_salesorderlineitem_shipped.py | 20 + .../migrations/0058_auto_20211126_1210.py | 62 + ...0059_salesordershipment_tracking_number.py | 18 + .../migrations/0060_auto_20211129_1339.py | 22 + ...o_20211201_2139_0060_auto_20211129_1339.py | 14 + InvenTree/order/models.py | 423 +- InvenTree/order/serializers.py | 595 +- .../order/templates/order/order_base.html | 178 +- .../order/order_wizard/match_fields.html | 99 +- .../order/order_wizard/match_parts.html | 6 +- .../order/order_wizard/po_upload.html | 71 +- .../order/order_wizard/select_parts.html | 4 +- .../order/order_wizard/select_pos.html | 2 +- .../order/templates/order/po_navbar.html | 36 - .../order/templates/order/po_sidebar.html | 12 + .../order/purchase_order_detail.html | 135 +- .../templates/order/purchase_orders.html | 65 +- .../order/templates/order/receive_parts.html | 81 - .../templates/order/sales_order_base.html | 186 +- .../templates/order/sales_order_detail.html | 190 +- .../templates/order/sales_order_ship.html | 30 - .../order/templates/order/sales_orders.html | 69 +- .../order/so_allocate_by_serial.html | 12 - .../templates/order/so_allocation_delete.html | 14 - .../order/templates/order/so_navbar.html | 40 - .../order/templates/order/so_sidebar.html | 18 + InvenTree/order/test_api.py | 245 +- InvenTree/order/test_migrations.py | 124 + InvenTree/order/test_sales_order.py | 53 +- InvenTree/order/test_views.py | 85 +- InvenTree/order/urls.py | 13 +- InvenTree/order/views.py | 551 +- InvenTree/part/admin.py | 114 +- InvenTree/part/api.py | 550 +- InvenTree/part/apps.py | 40 +- InvenTree/part/bom.py | 258 +- InvenTree/part/fixtures/bom.yaml | 9 +- InvenTree/part/fixtures/part.yaml | 4 + InvenTree/part/forms.py | 111 +- InvenTree/part/migrations/0001_initial.py | 24 +- .../migrations/0039_auto_20200515_1127.py | 2 +- .../migrations/0056_auto_20201110_1125.py | 8 +- .../part/migrations/0072_bomitemsubstitute.py | 22 + .../migrations/0073_auto_20211013_1048.py | 21 + .../part/migrations/0074_partcategorystar.py | 27 + .../migrations/0075_auto_20211128_0151.py | 25 + InvenTree/part/models.py | 615 +- InvenTree/part/serializers.py | 339 +- InvenTree/part/tasks.py | 82 + InvenTree/part/templates/part/bom.html | 53 +- .../part/templates/part/bom_duplicate.html | 17 - .../part/bom_upload/match_parts.html | 127 - .../part/bom_upload/upload_file.html | 76 - .../part/templates/part/bom_validate.html | 12 - InvenTree/part/templates/part/cat_link.html | 16 +- InvenTree/part/templates/part/category.html | 378 +- .../part/templates/part/category_navbar.html | 45 - .../part/templates/part/category_sidebar.html | 19 + InvenTree/part/templates/part/detail.html | 523 +- .../part/import_wizard/ajax_match_fields.html | 4 +- .../import_wizard/ajax_match_references.html | 2 +- .../part/import_wizard/match_fields.html | 99 +- .../part/import_wizard/match_references.html | 6 +- .../part/import_wizard/part_upload.html | 72 +- InvenTree/part/templates/part/navbar.html | 114 - .../part/templates/part/part_app_base.html | 32 +- InvenTree/part/templates/part/part_base.html | 594 +- .../part/templates/part/part_sidebar.html | 58 + InvenTree/part/templates/part/part_thumb.html | 41 +- .../part/templates/part/set_category.html | 2 +- .../part/templates/part/stock_count.html | 4 +- InvenTree/part/templates/part/upload_bom.html | 119 + .../part/templatetags/inventree_extras.py | 126 +- InvenTree/part/test_api.py | 390 +- InvenTree/part/test_bom_export.py | 10 +- InvenTree/part/test_bom_import.py | 344 + InvenTree/part/test_bom_item.py | 83 +- InvenTree/part/test_category.py | 6 +- InvenTree/part/test_param.py | 2 +- InvenTree/part/test_part.py | 125 +- InvenTree/part/test_supplier_part.py | 7 - InvenTree/part/test_views.py | 42 +- InvenTree/part/urls.py | 12 +- InvenTree/part/views.py | 591 +- InvenTree/plugin/__init__.py | 19 + InvenTree/plugin/action.py | 23 + InvenTree/plugin/admin.py | 73 + InvenTree/plugin/api.py | 129 + InvenTree/plugin/apps.py | 36 + .../action => plugin/builtin}/__init__.py | 0 InvenTree/plugin/builtin/action/__init__.py | 0 InvenTree/plugin/builtin/action/mixins.py | 68 + .../builtin/action/simpleactionplugin.py | 25 + .../builtin/action/test_samples_action.py | 40 + InvenTree/plugin/builtin/barcode/__init__.py | 0 InvenTree/plugin/builtin/barcode/mixins.py | 146 + .../plugin/builtin/integration/__init__.py | 0 .../plugin/builtin/integration/mixins.py | 503 + InvenTree/plugin/events.py | 188 + InvenTree/plugin/helpers.py | 190 + InvenTree/plugin/integration.py | 253 + InvenTree/plugin/loader.py | 19 + InvenTree/plugin/migrations/0001_initial.py | 23 + .../0002_alter_pluginconfig_options.py | 17 + .../plugin/migrations/0003_pluginsetting.py | 26 + .../0004_alter_pluginsetting_key.py | 18 + InvenTree/plugin/migrations/__init__.py | 0 InvenTree/plugin/mixins/__init__.py | 19 + InvenTree/plugin/models.py | 201 + InvenTree/plugin/plugin.py | 96 + InvenTree/plugin/registry.py | 584 + InvenTree/plugin/samples/__init__.py | 0 .../plugin/samples/integration/__init__.py | 0 .../samples/integration/another_sample.py | 19 + .../plugin/samples/integration/api_caller.py | 32 + .../plugin/samples/integration/broken_file.py | 11 + .../samples/integration/broken_sample.py | 16 + .../samples/integration/event_sample.py | 23 + .../plugin/samples/integration/sample.py | 72 + .../samples/integration/scheduled_task.py | 60 + .../samples/integration/test_api_caller.py | 21 + .../integration/test_samples_integration.py | 21 + InvenTree/plugin/serializers.py | 150 + .../plugin/templatetags/plugin_extras.py | 75 + InvenTree/plugin/test_action.py | 61 + InvenTree/plugin/test_api.py | 124 + InvenTree/plugin/test_integration.py | 283 + InvenTree/plugin/test_plugin.py | 77 + InvenTree/plugin/urls.py | 24 + InvenTree/plugins/action/action.py | 92 - InvenTree/plugins/plugin.py | 16 - InvenTree/plugins/plugins.py | 69 - InvenTree/report/api.py | 56 +- InvenTree/report/apps.py | 4 +- InvenTree/report/models.py | 62 +- InvenTree/report/templatetags/report.py | 14 +- InvenTree/report/tests.py | 10 +- InvenTree/script/translation_stats.py | 24 +- InvenTree/stock/admin.py | 31 + InvenTree/stock/api.py | 578 +- InvenTree/stock/fixtures/stock.yaml | 13 - InvenTree/stock/forms.py | 155 +- .../migrations/0061_auto_20210511_0911.py | 8 +- .../migrations/0064_auto_20210621_1724.py | 10 +- .../migrations/0067_alter_stockitem_part.py | 20 + .../migrations/0068_stockitem_serial_int.py | 18 + .../migrations/0069_auto_20211109_2347.py | 54 + .../migrations/0070_auto_20211128_0151.py | 25 + .../migrations/0071_auto_20211205_1733.py | 47 + ...remove_stockitem_scheduled_for_deletion.py | 17 + .../0073_alter_stockitem_belongs_to.py | 19 + InvenTree/stock/models.py | 246 +- InvenTree/stock/serializers.py | 750 +- InvenTree/stock/tasks.py | 35 - InvenTree/stock/templates/stock/item.html | 264 +- .../stock/templates/stock/item_base.html | 533 +- .../stock/templates/stock/item_install.html | 33 - InvenTree/stock/templates/stock/loc_link.html | 13 +- InvenTree/stock/templates/stock/location.html | 359 +- .../templates/stock/location_delete.html | 2 +- .../templates/stock/location_navbar.html | 25 - .../templates/stock/location_sidebar.html | 8 + InvenTree/stock/templates/stock/navbar.html | 61 - .../stock/templates/stock/stock_adjust.html | 50 - .../stock/templates/stock/stock_app_base.html | 25 +- .../stock/templates/stock/stock_move.html | 1 - .../stock/templates/stock/stock_sidebar.html | 26 + InvenTree/stock/test_api.py | 456 +- InvenTree/stock/test_views.py | 226 +- InvenTree/stock/tests.py | 11 - InvenTree/stock/urls.py | 11 +- InvenTree/stock/views.py | 305 +- InvenTree/templates/500.html | 19 + InvenTree/templates/503.html | 73 + InvenTree/templates/InvenTree/index.html | 129 +- InvenTree/templates/InvenTree/search.html | 120 +- .../templates/InvenTree/settings/barcode.html | 1 - .../templates/InvenTree/settings/build.html | 1 - .../InvenTree/settings/category.html | 12 +- .../InvenTree/settings/currencies.html | 19 +- .../templates/InvenTree/settings/global.html | 1 - .../templates/InvenTree/settings/header.html | 12 - .../templates/InvenTree/settings/login.html | 32 + .../InvenTree/settings/mixins/settings.html | 16 + .../InvenTree/settings/mixins/urls.html | 27 + .../templates/InvenTree/settings/navbar.html | 121 - .../templates/InvenTree/settings/part.html | 42 +- .../templates/InvenTree/settings/plugin.html | 147 + .../InvenTree/settings/plugin_settings.html | 140 + .../templates/InvenTree/settings/po.html | 1 - .../templates/InvenTree/settings/report.html | 8 +- .../templates/InvenTree/settings/setting.html | 39 +- .../InvenTree/settings/settings.html | 114 +- .../templates/InvenTree/settings/sidebar.html | 63 + .../templates/InvenTree/settings/so.html | 1 - .../templates/InvenTree/settings/stock.html | 2 - .../templates/InvenTree/settings/user.html | 298 +- .../InvenTree/settings/user_display.html | 106 + .../InvenTree/settings/user_homepage.html | 4 +- .../InvenTree/settings/user_labels.html | 1 - .../InvenTree/settings/user_reports.html | 1 - .../InvenTree/settings/user_search.html | 3 +- .../InvenTree/settings/user_settings.html | 1 - InvenTree/templates/about.html | 18 +- InvenTree/templates/account/base.html | 133 + .../templates/account/email_confirm.html | 31 + InvenTree/templates/account/login.html | 62 + InvenTree/templates/account/logout.html | 25 + .../templates/account/password_reset.html | 30 + .../account/password_reset_from_key.html | 25 + InvenTree/templates/account/signup.html | 40 + InvenTree/templates/admin_button.html | 4 + .../templates/allauth_2fa/authenticate.html | 15 + .../templates/allauth_2fa/backup_tokens.html | 33 + InvenTree/templates/allauth_2fa/remove.html | 18 + InvenTree/templates/allauth_2fa/setup.html | 42 + InvenTree/templates/attachment_button.html | 8 + InvenTree/templates/attachment_delete.html | 7 - InvenTree/templates/attachment_table.html | 6 +- InvenTree/templates/base.html | 131 +- InvenTree/templates/clip.html | 2 +- InvenTree/templates/collapse.html | 23 - InvenTree/templates/collapse_index.html | 19 - .../email/build_order_required_stock.html | 39 + InvenTree/templates/email/email.html | 43 + .../email/low_stock_notification.html | 32 + InvenTree/templates/filter_list.html | 1 + InvenTree/templates/hover_image.html | 2 +- InvenTree/templates/js/dynamic/nav.js | 286 +- InvenTree/templates/js/dynamic/settings.js | 87 +- InvenTree/templates/js/translated/api.js | 40 +- .../templates/js/translated/attachment.js | 138 +- InvenTree/templates/js/translated/barcode.js | 20 +- InvenTree/templates/js/translated/bom.js | 780 +- InvenTree/templates/js/translated/build.js | 1628 ++- InvenTree/templates/js/translated/company.js | 24 +- InvenTree/templates/js/translated/filters.js | 39 +- InvenTree/templates/js/translated/forms.js | 746 +- InvenTree/templates/js/translated/helpers.js | 31 +- InvenTree/templates/js/translated/modals.js | 78 +- .../js/translated/model_renderers.js | 170 +- InvenTree/templates/js/translated/order.js | 1454 +- InvenTree/templates/js/translated/part.js | 622 +- InvenTree/templates/js/translated/plugin.js | 26 + InvenTree/templates/js/translated/stock.js | 1579 ++- .../templates/js/translated/table_filters.js | 91 +- InvenTree/templates/js/translated/tables.js | 20 +- InvenTree/templates/modals.html | 18 +- InvenTree/templates/navbar.html | 189 +- InvenTree/templates/navbar_demo.html | 12 + InvenTree/templates/notification.html | 18 - InvenTree/templates/page_base.html | 66 + InvenTree/templates/panel.html | 11 +- .../patterns/wizard}/match_fields.html | 11 +- .../templates/patterns/wizard/upload.html | 45 + InvenTree/templates/qr_button.html | 2 +- InvenTree/templates/qr_code.html | 2 +- .../templates/registration/logged_out.html | 59 +- InvenTree/templates/registration/login.html | 105 - .../registration/password_reset_complete.html | 59 - .../registration/password_reset_confirm.html | 69 - .../registration/password_reset_done.html | 65 - .../registration/password_reset_form.html | 68 - InvenTree/templates/required_part_table.html | 23 - InvenTree/templates/search_form.html | 12 +- InvenTree/templates/sidebar_header.html | 8 + InvenTree/templates/sidebar_item.html | 11 + InvenTree/templates/sidebar_link.html | 4 + InvenTree/templates/sidebar_toggle.html | 4 + InvenTree/templates/skeleton.html | 99 + InvenTree/templates/slide.html | 3 - .../socialaccount/snippets/provider_list.html | 17 + InvenTree/templates/spacer.html | 1 + InvenTree/templates/stats.html | 20 +- InvenTree/templates/status_codes.html | 7 +- InvenTree/templates/stock_table.html | 48 +- InvenTree/templates/two_column.html | 81 - InvenTree/templates/yesnolabel.html | 4 +- InvenTree/users/api.py | 56 +- InvenTree/users/apps.py | 2 +- InvenTree/users/models.py | 134 +- InvenTree/users/tests.py | 69 +- README.md | 33 +- ci/check_api_endpoint.py | 40 + ci/check_js_templates.py | 6 +- ci/check_locale_files.py | 6 +- ci/check_migration_files.py | 2 +- ci/check_version_number.py | 5 +- docker/Dockerfile | 22 +- docker/dev-config.env | 20 +- docker/docker-compose.dev.yml | 49 +- docker/docker-compose.sqlite.yml | 62 + docker/docker-compose.yml | 21 +- docker/init.sh | 5 +- docker/nginx.dev.conf | 57 + docker/prod-config.env | 5 +- docker/requirements.txt | 4 +- docker/sqlite-config.env | 10 + package-lock.json | 3788 ++++++ package.json | 7 + requirements.txt | 70 +- setup.cfg | 24 +- tasks.py | 128 +- 615 files changed, 213512 insertions(+), 86534 deletions(-) create mode 100644 .github/FUNDING.yml delete mode 100644 .github/ISSUE_TEMPLATE/app_issue.md create mode 100644 .github/workflows/check_translations.yaml delete mode 100644 .github/workflows/coverage.yaml create mode 100644 .github/workflows/docker_test.yaml delete mode 100644 .github/workflows/html.yaml delete mode 100644 .github/workflows/javascript.yaml delete mode 100644 .github/workflows/mysql.yaml delete mode 100644 .github/workflows/postgresql.yaml delete mode 100644 .github/workflows/python.yaml create mode 100644 .github/workflows/qc_checks.yaml create mode 100644 .github/workflows/stale.yml delete mode 100644 .github/workflows/style.yaml rename .github/workflows/{version.yaml => version.yml} (74%) create mode 100644 .github/workflows/welcome.yml create mode 100644 .gitpod.yml create mode 100644 InvenTree/InvenTree/config.py delete mode 100644 InvenTree/InvenTree/locale_stats.json create mode 100644 InvenTree/InvenTree/management/commands/rebuild_thumbnails.py create mode 100644 InvenTree/InvenTree/management/commands/remove_mfa.py delete mode 100644 InvenTree/InvenTree/plugins.py delete mode 100644 InvenTree/InvenTree/static/bootstrap-table/bootstrap-table-en-US.min.js delete mode 100644 InvenTree/InvenTree/static/bootstrap-table/extensions/accent-neutralise/bootstrap-table-accent-neutralise.js delete mode 100644 InvenTree/InvenTree/static/bootstrap-table/extensions/accent-neutralise/extension.json delete mode 100644 InvenTree/InvenTree/static/bootstrap-table/extensions/auto-refresh/extension.json delete mode 100644 InvenTree/InvenTree/static/bootstrap-table/extensions/cookie/extension.json delete mode 100644 InvenTree/InvenTree/static/bootstrap-table/extensions/copy-rows/extension.json delete mode 100644 InvenTree/InvenTree/static/bootstrap-table/extensions/defer-url/extension.json delete mode 100644 InvenTree/InvenTree/static/bootstrap-table/extensions/editable/extension.json delete mode 100644 InvenTree/InvenTree/static/bootstrap-table/extensions/export/extension.json delete mode 100644 InvenTree/InvenTree/static/bootstrap-table/extensions/filter-control/extension.json delete mode 100644 InvenTree/InvenTree/static/bootstrap-table/extensions/group-by-v2/extension.json delete mode 100644 InvenTree/InvenTree/static/bootstrap-table/extensions/group-by/bootstrap-table-group-by.css delete mode 100644 InvenTree/InvenTree/static/bootstrap-table/extensions/group-by/bootstrap-table-group-by.js delete mode 100644 InvenTree/InvenTree/static/bootstrap-table/extensions/group-by/extension.json delete mode 100644 InvenTree/InvenTree/static/bootstrap-table/extensions/i18n-enhance/extension.json delete mode 100644 InvenTree/InvenTree/static/bootstrap-table/extensions/key-events/extension.json delete mode 100644 InvenTree/InvenTree/static/bootstrap-table/extensions/mobile/extension.json delete mode 100644 InvenTree/InvenTree/static/bootstrap-table/extensions/multi-column-toggle/bootstrap-table-multi-toggle.js delete mode 100644 InvenTree/InvenTree/static/bootstrap-table/extensions/multi-column-toggle/extension.json delete mode 100644 InvenTree/InvenTree/static/bootstrap-table/extensions/multiple-search/bootstrap-table-multiple-search.js delete mode 100644 InvenTree/InvenTree/static/bootstrap-table/extensions/multiple-search/extension.json delete mode 100644 InvenTree/InvenTree/static/bootstrap-table/extensions/multiple-selection-row/bootstrap-table-multiple-selection-row.css delete mode 100644 InvenTree/InvenTree/static/bootstrap-table/extensions/multiple-selection-row/bootstrap-table-multiple-selection-row.js delete mode 100644 InvenTree/InvenTree/static/bootstrap-table/extensions/multiple-selection-row/extension.json delete mode 100644 InvenTree/InvenTree/static/bootstrap-table/extensions/multiple-sort/extension.json delete mode 100644 InvenTree/InvenTree/static/bootstrap-table/extensions/natural-sorting/bootstrap-table-natural-sorting.js delete mode 100644 InvenTree/InvenTree/static/bootstrap-table/extensions/natural-sorting/extension.json delete mode 100644 InvenTree/InvenTree/static/bootstrap-table/extensions/pipeline/LICENSE delete mode 100644 InvenTree/InvenTree/static/bootstrap-table/extensions/pipeline/README.md delete mode 100644 InvenTree/InvenTree/static/bootstrap-table/extensions/pipeline/extension.json delete mode 100644 InvenTree/InvenTree/static/bootstrap-table/extensions/reorder-columns/extension.json delete mode 100644 InvenTree/InvenTree/static/bootstrap-table/extensions/reorder-rows/extension.json delete mode 100644 InvenTree/InvenTree/static/bootstrap-table/extensions/resizable/extension.json delete mode 100644 InvenTree/InvenTree/static/bootstrap-table/extensions/select2-filter/bootstrap-table-select2-filter.js delete mode 100644 InvenTree/InvenTree/static/bootstrap-table/extensions/select2-filter/extension.json delete mode 100644 InvenTree/InvenTree/static/bootstrap-table/extensions/sticky-header/extension.json delete mode 100644 InvenTree/InvenTree/static/bootstrap-table/extensions/toolbar/extension.json delete mode 100644 InvenTree/InvenTree/static/bootstrap-table/extensions/tree-column/bootstrap-table-tree-column.css delete mode 100644 InvenTree/InvenTree/static/bootstrap-table/extensions/tree-column/bootstrap-table-tree-column.js delete mode 100644 InvenTree/InvenTree/static/bootstrap-table/extensions/tree-column/bootstrap-table-tree-column.less delete mode 100644 InvenTree/InvenTree/static/bootstrap-table/extensions/tree-column/extension.json delete mode 100644 InvenTree/InvenTree/static/bootstrap-table/extensions/tree-column/icon.png delete mode 100644 InvenTree/InvenTree/static/bootstrap-table/extensions/treegrid/extension.json create mode 100644 InvenTree/InvenTree/static/bootstrap-table/themes/bootstrap-table/bootstrap-table.css create mode 100644 InvenTree/InvenTree/static/bootstrap-table/themes/bootstrap-table/bootstrap-table.js create mode 100644 InvenTree/InvenTree/static/bootstrap-table/themes/bootstrap-table/bootstrap-table.min.css create mode 100644 InvenTree/InvenTree/static/bootstrap-table/themes/bootstrap-table/bootstrap-table.min.js create mode 100644 InvenTree/InvenTree/static/bootstrap-table/themes/bootstrap-table/fonts/bootstrap-table.eot create mode 100644 InvenTree/InvenTree/static/bootstrap-table/themes/bootstrap-table/fonts/bootstrap-table.svg create mode 100644 InvenTree/InvenTree/static/bootstrap-table/themes/bootstrap-table/fonts/bootstrap-table.ttf create mode 100644 InvenTree/InvenTree/static/bootstrap-table/themes/bootstrap-table/fonts/bootstrap-table.woff create mode 100644 InvenTree/InvenTree/static/bootstrap-table/themes/bulma/bootstrap-table-bulma.css create mode 100644 InvenTree/InvenTree/static/bootstrap-table/themes/bulma/bootstrap-table-bulma.js create mode 100644 InvenTree/InvenTree/static/bootstrap-table/themes/bulma/bootstrap-table-bulma.min.css create mode 100644 InvenTree/InvenTree/static/bootstrap-table/themes/bulma/bootstrap-table-bulma.min.js create mode 100644 InvenTree/InvenTree/static/bootstrap-table/themes/foundation/bootstrap-table-foundation.css create mode 100644 InvenTree/InvenTree/static/bootstrap-table/themes/foundation/bootstrap-table-foundation.js create mode 100644 InvenTree/InvenTree/static/bootstrap-table/themes/foundation/bootstrap-table-foundation.min.css create mode 100644 InvenTree/InvenTree/static/bootstrap-table/themes/foundation/bootstrap-table-foundation.min.js create mode 100644 InvenTree/InvenTree/static/bootstrap-table/themes/materialize/bootstrap-table-materialize.css create mode 100644 InvenTree/InvenTree/static/bootstrap-table/themes/materialize/bootstrap-table-materialize.js create mode 100644 InvenTree/InvenTree/static/bootstrap-table/themes/materialize/bootstrap-table-materialize.min.css create mode 100644 InvenTree/InvenTree/static/bootstrap-table/themes/materialize/bootstrap-table-materialize.min.js create mode 100644 InvenTree/InvenTree/static/bootstrap-table/themes/semantic/bootstrap-table-semantic.css create mode 100644 InvenTree/InvenTree/static/bootstrap-table/themes/semantic/bootstrap-table-semantic.js create mode 100644 InvenTree/InvenTree/static/bootstrap-table/themes/semantic/bootstrap-table-semantic.min.css create mode 100644 InvenTree/InvenTree/static/bootstrap-table/themes/semantic/bootstrap-table-semantic.min.js create mode 100644 InvenTree/InvenTree/static/bootstrap/css/bootstrap-grid.css create mode 100644 InvenTree/InvenTree/static/bootstrap/css/bootstrap-grid.css.map create mode 100644 InvenTree/InvenTree/static/bootstrap/css/bootstrap-grid.min.css create mode 100644 InvenTree/InvenTree/static/bootstrap/css/bootstrap-grid.min.css.map create mode 100644 InvenTree/InvenTree/static/bootstrap/css/bootstrap-grid.rtl.css create mode 100644 InvenTree/InvenTree/static/bootstrap/css/bootstrap-grid.rtl.css.map create mode 100644 InvenTree/InvenTree/static/bootstrap/css/bootstrap-grid.rtl.min.css create mode 100644 InvenTree/InvenTree/static/bootstrap/css/bootstrap-grid.rtl.min.css.map create mode 100644 InvenTree/InvenTree/static/bootstrap/css/bootstrap-reboot.css create mode 100644 InvenTree/InvenTree/static/bootstrap/css/bootstrap-reboot.css.map create mode 100644 InvenTree/InvenTree/static/bootstrap/css/bootstrap-reboot.min.css create mode 100644 InvenTree/InvenTree/static/bootstrap/css/bootstrap-reboot.min.css.map create mode 100644 InvenTree/InvenTree/static/bootstrap/css/bootstrap-reboot.rtl.css create mode 100644 InvenTree/InvenTree/static/bootstrap/css/bootstrap-reboot.rtl.css.map create mode 100644 InvenTree/InvenTree/static/bootstrap/css/bootstrap-reboot.rtl.min.css create mode 100644 InvenTree/InvenTree/static/bootstrap/css/bootstrap-reboot.rtl.min.css.map create mode 100644 InvenTree/InvenTree/static/bootstrap/css/bootstrap-utilities.css create mode 100644 InvenTree/InvenTree/static/bootstrap/css/bootstrap-utilities.css.map create mode 100644 InvenTree/InvenTree/static/bootstrap/css/bootstrap-utilities.min.css create mode 100644 InvenTree/InvenTree/static/bootstrap/css/bootstrap-utilities.min.css.map create mode 100644 InvenTree/InvenTree/static/bootstrap/css/bootstrap-utilities.rtl.css create mode 100644 InvenTree/InvenTree/static/bootstrap/css/bootstrap-utilities.rtl.css.map create mode 100644 InvenTree/InvenTree/static/bootstrap/css/bootstrap-utilities.rtl.min.css create mode 100644 InvenTree/InvenTree/static/bootstrap/css/bootstrap-utilities.rtl.min.css.map create mode 100644 InvenTree/InvenTree/static/bootstrap/css/bootstrap.css create mode 100644 InvenTree/InvenTree/static/bootstrap/css/bootstrap.css.map create mode 100644 InvenTree/InvenTree/static/bootstrap/css/bootstrap.min.css create mode 100644 InvenTree/InvenTree/static/bootstrap/css/bootstrap.min.css.map create mode 100644 InvenTree/InvenTree/static/bootstrap/css/bootstrap.rtl.css create mode 100644 InvenTree/InvenTree/static/bootstrap/css/bootstrap.rtl.css.map create mode 100644 InvenTree/InvenTree/static/bootstrap/css/bootstrap.rtl.min.css create mode 100644 InvenTree/InvenTree/static/bootstrap/css/bootstrap.rtl.min.css.map create mode 100644 InvenTree/InvenTree/static/bootstrap/js/bootstrap.bundle.js create mode 100644 InvenTree/InvenTree/static/bootstrap/js/bootstrap.bundle.js.map create mode 100644 InvenTree/InvenTree/static/bootstrap/js/bootstrap.bundle.min.js create mode 100644 InvenTree/InvenTree/static/bootstrap/js/bootstrap.bundle.min.js.map create mode 100644 InvenTree/InvenTree/static/bootstrap/js/bootstrap.esm.js create mode 100644 InvenTree/InvenTree/static/bootstrap/js/bootstrap.esm.js.map create mode 100644 InvenTree/InvenTree/static/bootstrap/js/bootstrap.esm.min.js create mode 100644 InvenTree/InvenTree/static/bootstrap/js/bootstrap.esm.min.js.map create mode 100644 InvenTree/InvenTree/static/bootstrap/js/bootstrap.js create mode 100644 InvenTree/InvenTree/static/bootstrap/js/bootstrap.js.map create mode 100644 InvenTree/InvenTree/static/bootstrap/js/bootstrap.min.js create mode 100644 InvenTree/InvenTree/static/bootstrap/js/bootstrap.min.js.map delete mode 100644 InvenTree/InvenTree/static/css/bootstrap-toggle.css delete mode 100644 InvenTree/InvenTree/static/css/bootstrap.min.css.map delete mode 100644 InvenTree/InvenTree/static/css/bootstrap_3.3.7_css_bootstrap.min.css create mode 100644 InvenTree/InvenTree/static/img/paper_splash_large.jpg delete mode 100644 InvenTree/InvenTree/static/script/bootstrap/bootstrap-toggle.js delete mode 100644 InvenTree/InvenTree/static/script/bootstrap/bootstrap.min.js delete mode 100644 InvenTree/InvenTree/static/script/inventree/delay.js rename InvenTree/{templates/js/dynamic => InvenTree/static/script/inventree}/inventree.js (59%) delete mode 100644 InvenTree/InvenTree/static/script/inventree/sidenav.js create mode 100644 InvenTree/InvenTree/static/select2/css/select2-bootstrap-5-theme.css delete mode 100644 InvenTree/InvenTree/utils.py create mode 100644 InvenTree/build/migrations/0031_build_reference_int.py create mode 100644 InvenTree/build/migrations/0032_auto_20211014_0632.py create mode 100644 InvenTree/build/migrations/0033_auto_20211128_0151.py create mode 100644 InvenTree/build/migrations/0034_alter_build_reference_int.py create mode 100644 InvenTree/build/tasks.py delete mode 100644 InvenTree/build/templates/build/allocation_card.html delete mode 100644 InvenTree/build/templates/build/auto_allocate.html delete mode 100644 InvenTree/build/templates/build/build_output_create.html delete mode 100644 InvenTree/build/templates/build/complete.html delete mode 100644 InvenTree/build/templates/build/complete_output.html delete mode 100644 InvenTree/build/templates/build/create_build_item.html delete mode 100644 InvenTree/build/templates/build/delete_build_item.html delete mode 100644 InvenTree/build/templates/build/edit_build_item.html delete mode 100644 InvenTree/build/templates/build/navbar.html create mode 100644 InvenTree/build/templates/build/sidebar.html delete mode 100644 InvenTree/build/templates/build/unallocate.html create mode 100644 InvenTree/common/migrations/0012_notificationentry.py create mode 100644 InvenTree/common/migrations/0013_webhookendpoint_webhookmessage.py create mode 100644 InvenTree/common/tasks.py delete mode 100644 InvenTree/common/templates/common/delete_currency.html delete mode 100644 InvenTree/common/templates/common/edit_setting.html delete mode 100644 InvenTree/company/templates/company/manufacturer_part_navbar.html create mode 100644 InvenTree/company/templates/company/manufacturer_part_sidebar.html delete mode 100644 InvenTree/company/templates/company/navbar.html create mode 100644 InvenTree/company/templates/company/sidebar.html create mode 100644 InvenTree/company/templates/company/supplier_part_sidebar.html create mode 100644 InvenTree/locale/el/LC_MESSAGES/django.mo create mode 100644 InvenTree/locale/es_MX/LC_MESSAGES/django.mo create mode 100644 InvenTree/locale/es_MX/LC_MESSAGES/django.po create mode 100644 InvenTree/locale/he/LC_MESSAGES/django.mo create mode 100644 InvenTree/locale/id/LC_MESSAGES/django.mo create mode 100644 InvenTree/locale/ko/LC_MESSAGES/django.mo create mode 100644 InvenTree/locale/nl/LC_MESSAGES/django.mo create mode 100644 InvenTree/locale/no/LC_MESSAGES/django.mo create mode 100644 InvenTree/locale/pt/LC_MESSAGES/django.mo create mode 100644 InvenTree/locale/pt/LC_MESSAGES/django.po create mode 100644 InvenTree/locale/sv/LC_MESSAGES/django.mo create mode 100644 InvenTree/locale/th/LC_MESSAGES/django.mo create mode 100644 InvenTree/locale/vi/LC_MESSAGES/django.mo create mode 100644 InvenTree/order/migrations/0051_auto_20211014_0623.py create mode 100644 InvenTree/order/migrations/0052_auto_20211014_0631.py create mode 100644 InvenTree/order/migrations/0053_auto_20211128_0151.py create mode 100644 InvenTree/order/migrations/0053_salesordershipment.py create mode 100644 InvenTree/order/migrations/0054_auto_20211201_2139.py create mode 100644 InvenTree/order/migrations/0054_salesorderallocation_shipment.py create mode 100644 InvenTree/order/migrations/0055_auto_20211025_0645.py create mode 100644 InvenTree/order/migrations/0056_alter_salesorderallocation_shipment.py create mode 100644 InvenTree/order/migrations/0057_salesorderlineitem_shipped.py create mode 100644 InvenTree/order/migrations/0058_auto_20211126_1210.py create mode 100644 InvenTree/order/migrations/0059_salesordershipment_tracking_number.py create mode 100644 InvenTree/order/migrations/0060_auto_20211129_1339.py create mode 100644 InvenTree/order/migrations/0061_merge_0054_auto_20211201_2139_0060_auto_20211129_1339.py delete mode 100644 InvenTree/order/templates/order/po_navbar.html create mode 100644 InvenTree/order/templates/order/po_sidebar.html delete mode 100644 InvenTree/order/templates/order/receive_parts.html delete mode 100644 InvenTree/order/templates/order/sales_order_ship.html delete mode 100644 InvenTree/order/templates/order/so_allocate_by_serial.html delete mode 100644 InvenTree/order/templates/order/so_allocation_delete.html delete mode 100644 InvenTree/order/templates/order/so_navbar.html create mode 100644 InvenTree/order/templates/order/so_sidebar.html create mode 100644 InvenTree/order/test_migrations.py create mode 100644 InvenTree/part/migrations/0072_bomitemsubstitute.py create mode 100644 InvenTree/part/migrations/0073_auto_20211013_1048.py create mode 100644 InvenTree/part/migrations/0074_partcategorystar.py create mode 100644 InvenTree/part/migrations/0075_auto_20211128_0151.py create mode 100644 InvenTree/part/tasks.py delete mode 100644 InvenTree/part/templates/part/bom_duplicate.html delete mode 100644 InvenTree/part/templates/part/bom_upload/match_parts.html delete mode 100644 InvenTree/part/templates/part/bom_upload/upload_file.html delete mode 100644 InvenTree/part/templates/part/bom_validate.html delete mode 100644 InvenTree/part/templates/part/category_navbar.html create mode 100644 InvenTree/part/templates/part/category_sidebar.html delete mode 100644 InvenTree/part/templates/part/navbar.html create mode 100644 InvenTree/part/templates/part/part_sidebar.html create mode 100644 InvenTree/part/templates/part/upload_bom.html create mode 100644 InvenTree/part/test_bom_import.py delete mode 100644 InvenTree/part/test_supplier_part.py create mode 100644 InvenTree/plugin/__init__.py create mode 100644 InvenTree/plugin/action.py create mode 100644 InvenTree/plugin/admin.py create mode 100644 InvenTree/plugin/api.py create mode 100644 InvenTree/plugin/apps.py rename InvenTree/{plugins/action => plugin/builtin}/__init__.py (100%) create mode 100644 InvenTree/plugin/builtin/action/__init__.py create mode 100644 InvenTree/plugin/builtin/action/mixins.py create mode 100644 InvenTree/plugin/builtin/action/simpleactionplugin.py create mode 100644 InvenTree/plugin/builtin/action/test_samples_action.py create mode 100644 InvenTree/plugin/builtin/barcode/__init__.py create mode 100644 InvenTree/plugin/builtin/barcode/mixins.py create mode 100644 InvenTree/plugin/builtin/integration/__init__.py create mode 100644 InvenTree/plugin/builtin/integration/mixins.py create mode 100644 InvenTree/plugin/events.py create mode 100644 InvenTree/plugin/helpers.py create mode 100644 InvenTree/plugin/integration.py create mode 100644 InvenTree/plugin/loader.py create mode 100644 InvenTree/plugin/migrations/0001_initial.py create mode 100644 InvenTree/plugin/migrations/0002_alter_pluginconfig_options.py create mode 100644 InvenTree/plugin/migrations/0003_pluginsetting.py create mode 100644 InvenTree/plugin/migrations/0004_alter_pluginsetting_key.py create mode 100644 InvenTree/plugin/migrations/__init__.py create mode 100644 InvenTree/plugin/mixins/__init__.py create mode 100644 InvenTree/plugin/models.py create mode 100644 InvenTree/plugin/plugin.py create mode 100644 InvenTree/plugin/registry.py create mode 100644 InvenTree/plugin/samples/__init__.py create mode 100644 InvenTree/plugin/samples/integration/__init__.py create mode 100644 InvenTree/plugin/samples/integration/another_sample.py create mode 100644 InvenTree/plugin/samples/integration/api_caller.py create mode 100644 InvenTree/plugin/samples/integration/broken_file.py create mode 100644 InvenTree/plugin/samples/integration/broken_sample.py create mode 100644 InvenTree/plugin/samples/integration/event_sample.py create mode 100644 InvenTree/plugin/samples/integration/sample.py create mode 100644 InvenTree/plugin/samples/integration/scheduled_task.py create mode 100644 InvenTree/plugin/samples/integration/test_api_caller.py create mode 100644 InvenTree/plugin/samples/integration/test_samples_integration.py create mode 100644 InvenTree/plugin/serializers.py create mode 100644 InvenTree/plugin/templatetags/plugin_extras.py create mode 100644 InvenTree/plugin/test_action.py create mode 100644 InvenTree/plugin/test_api.py create mode 100644 InvenTree/plugin/test_integration.py create mode 100644 InvenTree/plugin/test_plugin.py create mode 100644 InvenTree/plugin/urls.py delete mode 100644 InvenTree/plugins/action/action.py delete mode 100644 InvenTree/plugins/plugin.py delete mode 100644 InvenTree/plugins/plugins.py create mode 100644 InvenTree/stock/migrations/0067_alter_stockitem_part.py create mode 100644 InvenTree/stock/migrations/0068_stockitem_serial_int.py create mode 100644 InvenTree/stock/migrations/0069_auto_20211109_2347.py create mode 100644 InvenTree/stock/migrations/0070_auto_20211128_0151.py create mode 100644 InvenTree/stock/migrations/0071_auto_20211205_1733.py create mode 100644 InvenTree/stock/migrations/0072_remove_stockitem_scheduled_for_deletion.py create mode 100644 InvenTree/stock/migrations/0073_alter_stockitem_belongs_to.py delete mode 100644 InvenTree/stock/tasks.py delete mode 100644 InvenTree/stock/templates/stock/item_install.html delete mode 100644 InvenTree/stock/templates/stock/location_navbar.html create mode 100644 InvenTree/stock/templates/stock/location_sidebar.html delete mode 100644 InvenTree/stock/templates/stock/navbar.html delete mode 100644 InvenTree/stock/templates/stock/stock_adjust.html delete mode 100644 InvenTree/stock/templates/stock/stock_move.html create mode 100644 InvenTree/stock/templates/stock/stock_sidebar.html create mode 100644 InvenTree/templates/500.html create mode 100644 InvenTree/templates/503.html delete mode 100644 InvenTree/templates/InvenTree/settings/header.html create mode 100644 InvenTree/templates/InvenTree/settings/login.html create mode 100644 InvenTree/templates/InvenTree/settings/mixins/settings.html create mode 100644 InvenTree/templates/InvenTree/settings/mixins/urls.html delete mode 100644 InvenTree/templates/InvenTree/settings/navbar.html create mode 100644 InvenTree/templates/InvenTree/settings/plugin.html create mode 100644 InvenTree/templates/InvenTree/settings/plugin_settings.html create mode 100644 InvenTree/templates/InvenTree/settings/sidebar.html create mode 100644 InvenTree/templates/InvenTree/settings/user_display.html create mode 100644 InvenTree/templates/account/base.html create mode 100644 InvenTree/templates/account/email_confirm.html create mode 100644 InvenTree/templates/account/login.html create mode 100644 InvenTree/templates/account/logout.html create mode 100644 InvenTree/templates/account/password_reset.html create mode 100644 InvenTree/templates/account/password_reset_from_key.html create mode 100644 InvenTree/templates/account/signup.html create mode 100644 InvenTree/templates/admin_button.html create mode 100644 InvenTree/templates/allauth_2fa/authenticate.html create mode 100644 InvenTree/templates/allauth_2fa/backup_tokens.html create mode 100644 InvenTree/templates/allauth_2fa/remove.html create mode 100644 InvenTree/templates/allauth_2fa/setup.html create mode 100644 InvenTree/templates/attachment_button.html delete mode 100644 InvenTree/templates/attachment_delete.html delete mode 100644 InvenTree/templates/collapse.html delete mode 100644 InvenTree/templates/collapse_index.html create mode 100644 InvenTree/templates/email/build_order_required_stock.html create mode 100644 InvenTree/templates/email/email.html create mode 100644 InvenTree/templates/email/low_stock_notification.html create mode 100644 InvenTree/templates/filter_list.html create mode 100644 InvenTree/templates/js/translated/plugin.js create mode 100644 InvenTree/templates/navbar_demo.html delete mode 100644 InvenTree/templates/notification.html create mode 100644 InvenTree/templates/page_base.html rename InvenTree/{part/templates/part/bom_upload => templates/patterns/wizard}/match_fields.html (79%) create mode 100644 InvenTree/templates/patterns/wizard/upload.html delete mode 100644 InvenTree/templates/registration/login.html delete mode 100644 InvenTree/templates/registration/password_reset_complete.html delete mode 100644 InvenTree/templates/registration/password_reset_confirm.html delete mode 100644 InvenTree/templates/registration/password_reset_done.html delete mode 100644 InvenTree/templates/registration/password_reset_form.html delete mode 100644 InvenTree/templates/required_part_table.html create mode 100644 InvenTree/templates/sidebar_header.html create mode 100644 InvenTree/templates/sidebar_item.html create mode 100644 InvenTree/templates/sidebar_link.html create mode 100644 InvenTree/templates/sidebar_toggle.html create mode 100644 InvenTree/templates/skeleton.html delete mode 100644 InvenTree/templates/slide.html create mode 100644 InvenTree/templates/socialaccount/snippets/provider_list.html create mode 100644 InvenTree/templates/spacer.html delete mode 100644 InvenTree/templates/two_column.html create mode 100644 ci/check_api_endpoint.py create mode 100644 docker/docker-compose.sqlite.yml create mode 100644 docker/nginx.dev.conf create mode 100644 docker/sqlite-config.env create mode 100644 package-lock.json create mode 100644 package.json diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000000..5ebf729c54 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +patreon: inventree +ko_fi: inventree diff --git a/.github/ISSUE_TEMPLATE/app_issue.md b/.github/ISSUE_TEMPLATE/app_issue.md deleted file mode 100644 index e71861394c..0000000000 --- a/.github/ISSUE_TEMPLATE/app_issue.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -name: App issue -about: Report a bug or issue with the InvenTree app -title: "[APP] Enter bug description" -labels: bug, app -assignees: '' - ---- - -**Describe the bug** -A clear and concise description of the bug or issue - -**To Reproduce** -Steps to reproduce the behavior: - -1. Go to ... -2. Select ... -3. ... - -**Expected Behavior** -A clear and concise description of what you expected to happen - -**Screenshots** -If applicable, add screenshots to help explain your problem - -**Version Information** - -- App platform: *Select iOS or Android* -- App version: *Enter app version* -- Server version: *Enter server version* diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 1a75b97af0..55585c7670 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,31 +1,47 @@ --- -name: Bug report -about: Create a bug report to help us improve InvenTree +name: Bug +about: Create a bug report to help us improve InvenTree! title: "[BUG] Enter bug description" labels: bug, question assignees: '' --- -**Describe the bug** -A clear and concise description of what the bug is. + + + +**Describe the bug** + + +**Steps to Reproduce** -**To Reproduce** Steps to reproduce the behavior: + **Expected behavior** + + **Deployment Method** -Docker -Bare Metal +- [ ] Docker +- [ ] Bare Metal **Version Information** -You can get this by going to the "About InvenTree" section in the upper right corner and cicking on to the "copy version information" + diff --git a/.github/workflows/check_translations.yaml b/.github/workflows/check_translations.yaml new file mode 100644 index 0000000000..3407dc7fd8 --- /dev/null +++ b/.github/workflows/check_translations.yaml @@ -0,0 +1,37 @@ +name: Check Translations + +on: + push: + branches: + - l10 + pull_request: + branches: + - l10 + +jobs: + + check: + runs-on: ubuntu-latest + + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + INVENTREE_DB_NAME: './test_db.sqlite' + INVENTREE_DB_ENGINE: django.db.backends.sqlite3 + INVENTREE_DEBUG: info + INVENTREE_MEDIA_ROOT: ./media + INVENTREE_STATIC_ROOT: ./static + + + steps: + - name: Checkout Code + uses: actions/checkout@v2 + - name: Install Dependencies + run: | + sudo apt-get update + sudo apt-get install gettext + pip3 install invoke + invoke install + - name: Test Translations + run: invoke translate + - name: Check Migration Files + run: python3 ci/check_migration_files.py diff --git a/.github/workflows/coverage.yaml b/.github/workflows/coverage.yaml deleted file mode 100644 index 6d0334b804..0000000000 --- a/.github/workflows/coverage.yaml +++ /dev/null @@ -1,60 +0,0 @@ -# Perform CI checks, and calculate code coverage - -name: SQLite - -on: - push: - branches-ignore: - - l10* - - pull_request: - branches-ignore: - - l10* - -jobs: - - # Run tests on SQLite database - # These tests are used for code coverage analysis - coverage: - runs-on: ubuntu-latest - - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - INVENTREE_DB_NAME: './test_db.sqlite' - INVENTREE_DB_ENGINE: django.db.backends.sqlite3 - INVENTREE_DEBUG: info - INVENTREE_MEDIA_ROOT: ./media - INVENTREE_STATIC_ROOT: ./static - - steps: - - name: Checkout Code - uses: actions/checkout@v2 - - name: Setup Python - uses: actions/setup-python@v2 - with: - python-version: 3.7 - - name: Install Dependencies - run: | - sudo apt-get update - sudo apt-get install gettext - pip3 install invoke - invoke install - invoke static - - name: Coverage Tests - run: | - invoke coverage - - name: Data Import Export - run: | - invoke migrate - invoke import-fixtures - invoke export-records -f data.json - rm test_db.sqlite - invoke migrate - invoke import-records -f data.json - invoke import-records -f data.json - - name: Test Translations - run: invoke translate - - name: Check Migration Files - run: python3 ci/check_migration_files.py - - name: Upload Coverage Report - run: coveralls diff --git a/.github/workflows/docker_latest.yaml b/.github/workflows/docker_latest.yaml index 355afa5b87..6b248fe0b9 100644 --- a/.github/workflows/docker_latest.yaml +++ b/.github/workflows/docker_latest.yaml @@ -34,7 +34,6 @@ jobs: platforms: linux/amd64,linux/arm64,linux/arm/v7 push: true target: production - repository: inventree/inventree tags: inventree/inventree:latest - name: Image Digest run: echo ${{ steps.docker_build.outputs.digest }} diff --git a/.github/workflows/docker_stable.yaml b/.github/workflows/docker_stable.yaml index 99818507a8..65c31dd9dc 100644 --- a/.github/workflows/docker_stable.yaml +++ b/.github/workflows/docker_stable.yaml @@ -1,4 +1,5 @@ -# Build and push latest docker image on push to master branch +# Build and push docker image on push to 'stable' branch +# Docker build will be uploaded to dockerhub with the 'inventree:stable' tag name: Docker Build @@ -34,9 +35,8 @@ jobs: platforms: linux/amd64,linux/arm64,linux/arm/v7 push: true target: production - build-args: | - branch=stable - repository: inventree/inventree + build-args: + branch: stable tags: inventree/inventree:stable - name: Image Digest run: echo ${{ steps.docker_build.outputs.digest }} diff --git a/.github/workflows/docker_tag.yaml b/.github/workflows/docker_tag.yaml index e7a48b9c13..5de27f48be 100644 --- a/.github/workflows/docker_tag.yaml +++ b/.github/workflows/docker_tag.yaml @@ -1,4 +1,5 @@ -# Publish docker images to dockerhub +# Publish docker images to dockerhub on a tagged release +# Docker build will be uploaded to dockerhub with the 'invetree:' tag name: Docker Publish @@ -32,7 +33,6 @@ jobs: platforms: linux/amd64,linux/arm64,linux/arm/v7 push: true target: production - build-args: | - tag=${{ github.event.release.tag_name }} - repository: inventree/inventree + build-args: + tag: ${{ github.event.release.tag_name }} tags: inventree/inventree:${{ github.event.release.tag_name }} diff --git a/.github/workflows/docker_test.yaml b/.github/workflows/docker_test.yaml new file mode 100644 index 0000000000..d96621ee66 --- /dev/null +++ b/.github/workflows/docker_test.yaml @@ -0,0 +1,37 @@ +# Test that the InvenTree docker image compiles correctly + +# This CI action runs on pushes to either the master or stable branches + +# 1. Build the development docker image (as per the documentation) +# 2. Install requied python libs into the docker container +# 3. Launch the container +# 4. Check that the API endpoint is available + +name: Docker Test + +on: + push: + branches: + - 'master' + - 'stable' + +jobs: + + docker: + runs-on: ubuntu-latest + + steps: + - name: Checkout Code + uses: actions/checkout@v2 + - name: Build Docker Image + run: | + cd docker + docker-compose -f docker-compose.sqlite.yml build + docker-compose -f docker-compose.sqlite.yml run inventree-dev-server invoke update + docker-compose -f docker-compose.sqlite.yml up -d + - name: Sleepy Time + run: sleep 60 + - name: Test API + run: | + pip install requests + python3 ci/check_api_endpoint.py diff --git a/.github/workflows/html.yaml b/.github/workflows/html.yaml deleted file mode 100644 index 069da7cbb4..0000000000 --- a/.github/workflows/html.yaml +++ /dev/null @@ -1,54 +0,0 @@ -# Check javascript template files - -name: HTML Templates - -on: - push: - branches: - - master - - pull_request: - branches-ignore: - - l10* - -jobs: - - html: - runs-on: ubuntu-latest - - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - INVENTREE_DB_ENGINE: sqlite3 - INVENTREE_DB_NAME: inventree - INVENTREE_MEDIA_ROOT: ./media - INVENTREE_STATIC_ROOT: ./static - steps: - - name: Install node.js - uses: actions/setup-node@v2 - - run: npm install - - name: Checkout Code - uses: actions/checkout@v2 - - name: Setup Python - uses: actions/setup-python@v2 - with: - python-version: 3.7 - - name: Install Dependencies - run: | - sudo apt-get update - sudo apt-get install gettext - pip3 install invoke - invoke install - invoke static - - name: Check HTML Files - run: | - npm install markuplint - npx markuplint InvenTree/build/templates/build/*.html - npx markuplint InvenTree/common/templates/common/*.html - npx markuplint InvenTree/company/templates/company/*.html - npx markuplint InvenTree/order/templates/order/*.html - npx markuplint InvenTree/part/templates/part/*.html - npx markuplint InvenTree/stock/templates/stock/*.html - npx markuplint InvenTree/templates/*.html - npx markuplint InvenTree/templates/InvenTree/*.html - npx markuplint InvenTree/templates/InvenTree/settings/*.html - diff --git a/.github/workflows/javascript.yaml b/.github/workflows/javascript.yaml deleted file mode 100644 index a07b516ac6..0000000000 --- a/.github/workflows/javascript.yaml +++ /dev/null @@ -1,50 +0,0 @@ -# Check javascript template files - -name: Javascript Templates - -on: - push: - branches: - - master - - pull_request: - branches-ignore: - - l10* - -jobs: - - javascript: - runs-on: ubuntu-latest - - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - INVENTREE_DB_ENGINE: sqlite3 - INVENTREE_DB_NAME: inventree - INVENTREE_MEDIA_ROOT: ./media - INVENTREE_STATIC_ROOT: ./static - steps: - - name: Install node.js - uses: actions/setup-node@v2 - - run: npm install - - name: Checkout Code - uses: actions/checkout@v2 - - name: Setup Python - uses: actions/setup-python@v2 - with: - python-version: 3.7 - - name: Install Dependencies - run: | - sudo apt-get update - sudo apt-get install gettext - pip3 install invoke - invoke install - invoke static - - name: Check Templated Files - run: | - cd ci - python check_js_templates.py - - name: Lint Javascript Files - run: | - npm install eslint eslint-config-google - invoke render-js-files - npx eslint js_tmp/*.js \ No newline at end of file diff --git a/.github/workflows/mysql.yaml b/.github/workflows/mysql.yaml deleted file mode 100644 index f0eb0efd7f..0000000000 --- a/.github/workflows/mysql.yaml +++ /dev/null @@ -1,67 +0,0 @@ -# MySQL Unit Testing - -name: MySQL - -on: - push: - branches-ignore: - - l10* - - pull_request: - branches-ignore: - - l10* - -jobs: - - test: - runs-on: ubuntu-latest - - env: - # Database backend configuration - INVENTREE_DB_ENGINE: django.db.backends.mysql - INVENTREE_DB_NAME: inventree - INVENTREE_DB_USER: root - INVENTREE_DB_PASSWORD: password - INVENTREE_DB_HOST: '127.0.0.1' - INVENTREE_DB_PORT: 3306 - INVENTREE_DEBUG: info - INVENTREE_MEDIA_ROOT: ./media - INVENTREE_STATIC_ROOT: ./static - - services: - mysql: - image: mysql:latest - env: - MYSQL_ALLOW_EMPTY_PASSWORD: yes - MYSQL_DATABASE: inventree - MYSQL_USER: inventree - MYSQL_PASSWORD: password - MYSQL_ROOT_PASSWORD: password - options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3 - ports: - - 3306:3306 - - steps: - - name: Checkout Code - uses: actions/checkout@v2 - - name: Setup Python - uses: actions/setup-python@v2 - with: - python-version: 3.7 - - name: Install Dependencies - run: | - sudo apt-get install mysql-server libmysqlclient-dev - pip3 install invoke - pip3 install mysqlclient - invoke install - - name: Run Tests - run: invoke test - - name: Data Import Export - run: | - invoke migrate - python3 ./InvenTree/manage.py flush --noinput - invoke import-fixtures - invoke export-records -f data.json - python3 ./InvenTree/manage.py flush --noinput - invoke import-records -f data.json - invoke import-records -f data.json \ No newline at end of file diff --git a/.github/workflows/postgresql.yaml b/.github/workflows/postgresql.yaml deleted file mode 100644 index 9a56382c4e..0000000000 --- a/.github/workflows/postgresql.yaml +++ /dev/null @@ -1,63 +0,0 @@ -# PostgreSQL Unit Testing - -name: PostgreSQL - -on: - push: - branches-ignore: - - l10* - - pull_request: - branches-ignore: - - l10* - -jobs: - - test: - runs-on: ubuntu-latest - - env: - # Database backend configuration - INVENTREE_DB_ENGINE: django.db.backends.postgresql - INVENTREE_DB_NAME: inventree - INVENTREE_DB_USER: inventree - INVENTREE_DB_PASSWORD: password - INVENTREE_DB_HOST: '127.0.0.1' - INVENTREE_DB_PORT: 5432 - INVENTREE_DEBUG: info - INVENTREE_MEDIA_ROOT: ./media - INVENTREE_STATIC_ROOT: ./static - - services: - postgres: - image: postgres - env: - POSTGRES_USER: inventree - POSTGRES_PASSWORD: password - ports: - - 5432:5432 - - steps: - - name: Checkout Code - uses: actions/checkout@v2 - - name: Setup Python - uses: actions/setup-python@v2 - with: - python-version: 3.7 - - name: Install Dependencies - run: | - sudo apt-get install libpq-dev - pip3 install invoke - pip3 install psycopg2 - invoke install - - name: Run Tests - run: invoke test - - name: Data Import Export - run: | - invoke migrate - python3 ./InvenTree/manage.py flush --noinput - invoke import-fixtures - invoke export-records -f data.json - python3 ./InvenTree/manage.py flush --noinput - invoke import-records -f data.json - invoke import-records -f data.json \ No newline at end of file diff --git a/.github/workflows/python.yaml b/.github/workflows/python.yaml deleted file mode 100644 index 7e32685af1..0000000000 --- a/.github/workflows/python.yaml +++ /dev/null @@ -1,49 +0,0 @@ -# Run python library tests whenever code is pushed to master - -name: Python Bindings - -on: - push: - branches: - - master - - pull_request: - branches-ignore: - - l10* - -jobs: - - python: - runs-on: ubuntu-latest - - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - INVENTREE_DB_NAME: './test_db.sqlite' - INVENTREE_DB_ENGINE: 'sqlite3' - INVENTREE_DEBUG: info - INVENTREE_MEDIA_ROOT: ./media - INVENTREE_STATIC_ROOT: ./static - - steps: - - name: Checkout Code - uses: actions/checkout@v2 - - name: Install InvenTree - run: | - sudo apt-get update - sudo apt-get install python3-dev python3-pip python3-venv - pip3 install invoke - invoke install - invoke migrate - - name: Download Python Code - run: | - git clone --depth 1 https://github.com/inventree/inventree-python ./inventree-python - - name: Start Server - run: | - invoke import-records -f ./inventree-python/test/test_data.json - invoke server -a 127.0.0.1:8000 & - sleep 60 - - name: Run Tests - run: | - cd inventree-python - invoke test - \ No newline at end of file diff --git a/.github/workflows/qc_checks.yaml b/.github/workflows/qc_checks.yaml new file mode 100644 index 0000000000..b1067c4bbd --- /dev/null +++ b/.github/workflows/qc_checks.yaml @@ -0,0 +1,304 @@ +# Checks for each PR / push + +name: QC checks + +on: + push: + branches-ignore: + - l10* + + pull_request: + branches-ignore: + - l10* + +env: + python_version: 3.8 + node_version: 16 + + server_start_sleep: 60 + + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + INVENTREE_DB_ENGINE: sqlite3 + INVENTREE_DB_NAME: inventree + INVENTREE_MEDIA_ROOT: ./media + INVENTREE_STATIC_ROOT: ./static + + +jobs: + pep_style: + name: PEP style (python) + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v2 + - name: Set up Python ${{ env.python_version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ env.python_version }} + cache: 'pip' + - name: Install deps + run: | + pip install flake8==3.8.3 + pip install pep8-naming==0.11.1 + - name: flake8 + run: | + flake8 InvenTree + + javascript: + name: javascript template files + needs: pep_style + runs-on: ubuntu-latest + + steps: + - name: Checkout Code + uses: actions/checkout@v2 + - name: Install node.js ${{ env.node_version }} + uses: actions/setup-node@v2 + with: + node-version: ${{ env.node_version }} + cache: 'npm' + - run: npm install + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: ${{ env.python_version }} + cache: 'pip' + - name: Install Dependencies + run: | + sudo apt-get update + sudo apt-get install gettext + pip3 install invoke + invoke install + invoke static + - name: Check Templated Files + run: | + cd ci + python check_js_templates.py + - name: Lint Javascript Files + run: | + invoke render-js-files + npx eslint js_tmp/*.js + + html: + name: html template files + needs: pep_style + runs-on: ubuntu-latest + + steps: + - name: Checkout Code + uses: actions/checkout@v2 + - name: Install node.js ${{ env.node_version }} + uses: actions/setup-node@v2 + with: + node-version: ${{ env.node_version }} + cache: 'npm' + - run: npm install + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: ${{ env.python_version }} + cache: 'pip' + - name: Install Dependencies + run: | + sudo apt-get update + sudo apt-get install gettext + pip3 install invoke + invoke install + invoke static + - name: Check HTML Files + run: | + npx markuplint InvenTree/build/templates/build/*.html + npx markuplint InvenTree/company/templates/company/*.html + npx markuplint InvenTree/order/templates/order/*.html + npx markuplint InvenTree/part/templates/part/*.html + npx markuplint InvenTree/stock/templates/stock/*.html + npx markuplint InvenTree/templates/*.html + npx markuplint InvenTree/templates/InvenTree/*.html + npx markuplint InvenTree/templates/InvenTree/settings/*.html + + python: + name: python bindings + needs: pep_style + runs-on: ubuntu-latest + + env: + wrapper_name: inventree-python + + steps: + - name: Checkout Code + uses: actions/checkout@v2 + - name: Install InvenTree + run: | + sudo apt-get update + sudo apt-get install python3-dev python3-pip python3-venv + pip3 install invoke + invoke install + invoke migrate + - name: Download Python Code + run: | + git clone --depth 1 https://github.com/inventree/${{ env.wrapper_name }} ./${{ env.wrapper_name }} + - name: Start Server + run: | + invoke import-records -f ./${{ env.wrapper_name }}/test/test_data.json + invoke server -a 127.0.0.1:8000 & + sleep ${{ env.server_start_sleep }} + - name: Run Tests + run: | + cd ${{ env.wrapper_name }} + invoke test + + coverage: + name: Sqlite / coverage + needs: ['javascript', 'html'] + runs-on: ubuntu-latest + + env: + INVENTREE_DB_NAME: ./inventree.sqlite + INVENTREE_DB_ENGINE: sqlite3 + INVENTREE_PLUGINS_ENABLED: true + + steps: + - name: Checkout Code + uses: actions/checkout@v2 + - name: Setup Python ${{ env.python_version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ env.python_version }} + cache: 'pip' + - name: Install Dependencies + run: | + sudo apt-get update + sudo apt-get install gettext + pip3 install invoke + invoke install + invoke static + - name: Coverage Tests + run: | + invoke coverage + - name: Data Import Export + run: | + invoke migrate + invoke import-fixtures + invoke export-records -f data.json + rm inventree.sqlite + invoke migrate + invoke import-records -f data.json + invoke import-records -f data.json + - name: Test Translations + run: invoke translate + - name: Check Migration Files + run: python3 ci/check_migration_files.py + - name: Upload Coverage Report + run: coveralls + + postgres: + name: Postgres + needs: ['javascript', 'html'] + runs-on: ubuntu-latest + + env: + INVENTREE_DB_ENGINE: django.db.backends.postgresql + INVENTREE_DB_USER: inventree + INVENTREE_DB_PASSWORD: password + INVENTREE_DB_HOST: '127.0.0.1' + INVENTREE_DB_PORT: 5432 + INVENTREE_DEBUG: info + INVENTREE_CACHE_HOST: localhost + INVENTREE_PLUGINS_ENABLED: true + + services: + postgres: + image: postgres + env: + POSTGRES_USER: inventree + POSTGRES_PASSWORD: password + ports: + - 5432:5432 + + redis: + image: redis + ports: + - 6379:6379 + + steps: + - name: Checkout Code + uses: actions/checkout@v2 + - name: Setup Python ${{ env.python_version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ env.python_version }} + cache: 'pip' + - name: Install Dependencies + run: | + sudo apt-get update + sudo apt-get install libpq-dev + pip3 install invoke + pip3 install psycopg2 + pip3 install django-redis>=5.0.0 + invoke install + - name: Run Tests + run: invoke test + - name: Data Import Export + run: | + invoke migrate + python3 ./InvenTree/manage.py flush --noinput + invoke import-fixtures + invoke export-records -f data.json + python3 ./InvenTree/manage.py flush --noinput + invoke import-records -f data.json + invoke import-records -f data.json + + mysql: + name: MySql + needs: ['javascript', 'html'] + runs-on: ubuntu-latest + env: + # Database backend configuration + INVENTREE_DB_ENGINE: django.db.backends.mysql + INVENTREE_DB_USER: root + INVENTREE_DB_PASSWORD: password + INVENTREE_DB_HOST: '127.0.0.1' + INVENTREE_DB_PORT: 3306 + INVENTREE_DEBUG: info + INVENTREE_PLUGINS_ENABLED: true + + services: + mysql: + image: mysql:latest + env: + MYSQL_ALLOW_EMPTY_PASSWORD: yes + MYSQL_DATABASE: ${{ env.INVENTREE_DB_NAME }} + MYSQL_USER: inventree + MYSQL_PASSWORD: password + MYSQL_ROOT_PASSWORD: password + options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3 + ports: + - 3306:3306 + + steps: + - name: Checkout Code + uses: actions/checkout@v2 + - name: Setup Python ${{ env.python_version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ env.python_version }} + cache: 'pip' + - name: Install Dependencies + run: | + sudo apt-get update + sudo apt-get install libmysqlclient-dev + pip3 install invoke + pip3 install mysqlclient + invoke install + - name: Run Tests + run: invoke test + - name: Data Import Export + run: | + invoke migrate + python3 ./InvenTree/manage.py flush --noinput + invoke import-fixtures + invoke export-records -f data.json + python3 ./InvenTree/manage.py flush --noinput + invoke import-records -f data.json + invoke import-records -f data.json diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 0000000000..278e5139e5 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,25 @@ +# Marks all issues that do not receive activity stale starting 2022 +name: Mark stale issues and pull requests + +on: + schedule: + - cron: '24 11 * * *' + +jobs: + stale: + + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + + steps: + - uses: actions/stale@v3 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + stale-issue-message: 'This issue seems stale. Please react to show this is still important.' + stale-pr-message: 'This PR seems stale. Please react to show this is still important.' + stale-issue-label: 'no-activity' + stale-pr-label: 'no-activity' + start-date: '2022-01-01' + exempt-all-milestones: true diff --git a/.github/workflows/style.yaml b/.github/workflows/style.yaml deleted file mode 100644 index df52de1dcb..0000000000 --- a/.github/workflows/style.yaml +++ /dev/null @@ -1,34 +0,0 @@ -name: Style Checks - -on: - push: - branches-ignore: - - l10* - - pull_request: - branches-ignore: - - l10* - -jobs: - style: - runs-on: ubuntu-latest - - strategy: - max-parallel: 4 - matrix: - python-version: [3.7] - - steps: - - name: Checkout code - uses: actions/checkout@v2 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python-version }} - - name: Install deps - run: | - pip install flake8==3.8.3 - pip install pep8-naming==0.11.1 - - name: flake8 - run: | - flake8 InvenTree diff --git a/.github/workflows/version.yaml b/.github/workflows/version.yml similarity index 74% rename from .github/workflows/version.yaml rename to .github/workflows/version.yml index 6e32d9e148..73d5bd8a2c 100644 --- a/.github/workflows/version.yaml +++ b/.github/workflows/version.yml @@ -1,15 +1,16 @@ -# Check that the version number format matches the current branch - -name: Version Numbering +# Checks version number +name: version number on: pull_request: branches-ignore: - l10* + jobs: - check: + check_version: + name: version number runs-on: ubuntu-latest steps: diff --git a/.github/workflows/welcome.yml b/.github/workflows/welcome.yml new file mode 100644 index 0000000000..5b0e3a7256 --- /dev/null +++ b/.github/workflows/welcome.yml @@ -0,0 +1,17 @@ +# welcome new contributers +name: Welcome +on: + pull_request: + types: [opened] + issues: + types: [opened] + +jobs: + run: + runs-on: ubuntu-latest + steps: + - uses: actions/first-interaction@v1 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + issue-message: 'Welcome to InvenTree! Please check the [contributing docs](https://inventree.readthedocs.io/en/latest/contribute/) on how to help.\nIf you experience setup / install issues please read all [install docs]( https://inventree.readthedocs.io/en/latest/start/intro/).' + pr-message: 'This is your first PR, welcome!\nPlease check [Contributing](https://github.com/inventree/InvenTree/blob/master/CONTRIBUTING.md) to make sure your submission fits our general code-style and workflow.\nMake sure to document why this PR is needed and to link connected issues so we can review it faster.' diff --git a/.gitignore b/.gitignore index 420524d06f..6532442dc7 100644 --- a/.gitignore +++ b/.gitignore @@ -49,6 +49,7 @@ static_i18n # Local config file config.yaml +plugins.txt # Default data file data.json @@ -77,6 +78,10 @@ dev/ locale_stats.json # node.js -package-lock.json -package.json -node_modules/ \ No newline at end of file +node_modules/ + +# maintenance locker +maintenance_mode_state.txt + +# plugin dev directory +plugins/ diff --git a/.gitpod.yml b/.gitpod.yml new file mode 100644 index 0000000000..33389c5960 --- /dev/null +++ b/.gitpod.yml @@ -0,0 +1,47 @@ +tasks: + - name: Setup django + before: | + export INVENTREE_DB_ENGINE='sqlite3' + export INVENTREE_DB_NAME='/workspace/InvenTree/dev/database.sqlite3' + export INVENTREE_MEDIA_ROOT='/workspace/InvenTree/inventree-data/media' + export INVENTREE_STATIC_ROOT='/workspace/InvenTree/dev/static' + export PIP_USER='no' + + python3 -m venv venv + source venv/bin/activate + pip install invoke + inv install + mkdir dev + inv update + gp sync-done setup_server + + - name: Start server + init: gp sync-await setup_server + command: | + gp sync-await setup_server + export INVENTREE_DB_ENGINE='sqlite3' + export INVENTREE_DB_NAME='/workspace/InvenTree/dev/database.sqlite3' + export INVENTREE_MEDIA_ROOT='/workspace/InvenTree/inventree-data/media' + export INVENTREE_STATIC_ROOT='/workspace/InvenTree/dev/static' + + source venv/bin/activate + rm /workspace/InvenTree/inventree-data -r + git clone https://github.com/inventree/demo-dataset /workspace/InvenTree/inventree-data + invoke delete-data -f + invoke import-records -f /workspace/InvenTree/inventree-data/inventree_data.json + + inv server + +# List the ports to expose. Learn more https://www.gitpod.io/docs/config-ports/ +ports: + - port: 8000 + onOpen: open-preview + +github: + prebuilds: + master: true + pullRequests: false + pullRequestsFromForks: true + addBadge: true + addLabel: gitpod-ready + addCheck: false diff --git a/InvenTree/InvenTree/api.py b/InvenTree/InvenTree/api.py index a006050694..7c8f71ea9a 100644 --- a/InvenTree/InvenTree/api.py +++ b/InvenTree/InvenTree/api.py @@ -5,8 +5,6 @@ Main JSON interface views # -*- coding: utf-8 -*- from __future__ import unicode_literals -import logging - from django.utils.translation import ugettext_lazy as _ from django.http import JsonResponse @@ -21,14 +19,7 @@ from .views import AjaxView from .version import inventreeVersion, inventreeApiVersion, inventreeInstanceName from .status import is_worker_running -from plugins import plugins as inventree_plugins - - -logger = logging.getLogger("inventree") - - -logger.info("Loading action plugins...") -action_plugins = inventree_plugins.load_action_plugins() +from plugin import registry class InfoView(AjaxView): @@ -110,10 +101,11 @@ class ActionPluginView(APIView): 'error': _("No action specified") }) - for plugin_class in action_plugins: - if plugin_class.action_name() == action: - - plugin = plugin_class(request.user, data=data) + action_plugins = registry.with_mixin('action') + for plugin in action_plugins: + if plugin.action_name() == action: + # TODO @matmair use easier syntax once InvenTree 0.7.0 is released + plugin.init(request.user, data=data) plugin.perform_action() diff --git a/InvenTree/InvenTree/api_tester.py b/InvenTree/InvenTree/api_tester.py index e2bdae1d8f..fe2057b453 100644 --- a/InvenTree/InvenTree/api_tester.py +++ b/InvenTree/InvenTree/api_tester.py @@ -46,7 +46,7 @@ class InvenTreeAPITestCase(APITestCase): self.user.is_staff = True self.user.save() - + for role in self.roles: self.assignRole(role) @@ -106,12 +106,12 @@ class InvenTreeAPITestCase(APITestCase): return response - def post(self, url, data, expected_code=None): + def post(self, url, data, expected_code=None, format='json'): """ Issue a POST request """ - response = self.client.post(url, data=data, format='json') + response = self.client.post(url, data=data, format=format) if expected_code is not None: self.assertEqual(response.status_code, expected_code) @@ -130,12 +130,24 @@ class InvenTreeAPITestCase(APITestCase): return response - def patch(self, url, data, files=None, expected_code=None): + def patch(self, url, data, expected_code=None, format='json'): """ Issue a PATCH request """ - response = self.client.patch(url, data=data, files=files, format='json') + response = self.client.patch(url, data=data, format=format) + + if expected_code is not None: + self.assertEqual(response.status_code, expected_code) + + return response + + def options(self, url, expected_code=None): + """ + Issue an OPTIONS request + """ + + response = self.client.options(url, format='json') if expected_code is not None: self.assertEqual(response.status_code, expected_code) diff --git a/InvenTree/InvenTree/apps.py b/InvenTree/InvenTree/apps.py index 6456c5994f..76b918459c 100644 --- a/InvenTree/InvenTree/apps.py +++ b/InvenTree/InvenTree/apps.py @@ -18,16 +18,37 @@ class InvenTreeConfig(AppConfig): def ready(self): if canAppAccessDatabase(): + + self.remove_obsolete_tasks() + self.start_background_tasks() if not isInTestMode(): self.update_exchange_rates() + def remove_obsolete_tasks(self): + """ + Delete any obsolete scheduled tasks in the database + """ + + obsolete = [ + 'InvenTree.tasks.delete_expired_sessions', + 'stock.tasks.delete_old_stock_items', + ] + + try: + from django_q.models import Schedule + except AppRegistryNotReady: # pragma: no cover + return + + # Remove any existing obsolete tasks + Schedule.objects.filter(func__in=obsolete).delete() + def start_background_tasks(self): try: from django_q.models import Schedule - except (AppRegistryNotReady): + except AppRegistryNotReady: # pragma: no cover return logger.info("Starting background tasks...") @@ -57,17 +78,16 @@ class InvenTreeConfig(AppConfig): schedule_type=Schedule.DAILY, ) - # Remove expired sessions + # Delete old error messages InvenTree.tasks.schedule_task( - 'InvenTree.tasks.delete_expired_sessions', + 'InvenTree.tasks.delete_old_error_logs', schedule_type=Schedule.DAILY, ) - # Delete "old" stock items + # Delete old notification records InvenTree.tasks.schedule_task( - 'stock.tasks.delete_old_stock_items', - schedule_type=Schedule.MINUTES, - minutes=30, + 'common.tasks.delete_old_notifications', + schedule_type=Schedule.DAILY, ) def update_exchange_rates(self): @@ -80,10 +100,10 @@ class InvenTreeConfig(AppConfig): try: from djmoney.contrib.exchange.models import ExchangeBackend - from datetime import datetime, timedelta + from InvenTree.tasks import update_exchange_rates from common.settings import currency_code_default - except AppRegistryNotReady: + except AppRegistryNotReady: # pragma: no cover pass base_currency = currency_code_default() @@ -95,23 +115,18 @@ class InvenTreeConfig(AppConfig): last_update = backend.last_update - if last_update is not None: - delta = datetime.now().date() - last_update.date() - if delta > timedelta(days=1): - print(f"Last update was {last_update}") - update = True - else: + if last_update is None: # Never been updated - print("Exchange backend has never been updated") + logger.info("Exchange backend has never been updated") update = True # Backend currency has changed? if not base_currency == backend.base_currency: - print(f"Base currency changed from {backend.base_currency} to {base_currency}") + logger.info(f"Base currency changed from {backend.base_currency} to {base_currency}") update = True except (ExchangeBackend.DoesNotExist): - print("Exchange backend not found - updating") + logger.info("Exchange backend not found - updating") update = True except: @@ -119,4 +134,7 @@ class InvenTreeConfig(AppConfig): return if update: - update_exchange_rates() + try: + update_exchange_rates() + except Exception as e: + logger.error(f"Error updating exchange rates: {e}") diff --git a/InvenTree/InvenTree/ci_render_js.py b/InvenTree/InvenTree/ci_render_js.py index 62e3fc4667..e747f1a3c0 100644 --- a/InvenTree/InvenTree/ci_render_js.py +++ b/InvenTree/InvenTree/ci_render_js.py @@ -1,9 +1,9 @@ """ -Pull rendered copies of the templated +Pull rendered copies of the templated +only used for testing the js files! - This file is omited from coverage """ -from django.http import response -from django.test import TestCase, testcases +from django.test import TestCase from django.contrib.auth import get_user_model import os diff --git a/InvenTree/InvenTree/config.py b/InvenTree/InvenTree/config.py new file mode 100644 index 0000000000..35671c1b26 --- /dev/null +++ b/InvenTree/InvenTree/config.py @@ -0,0 +1,90 @@ +""" +Helper functions for loading InvenTree configuration options +""" + +import os +import shutil +import logging + + +logger = logging.getLogger('inventree') + + +def get_base_dir(): + """ Returns the base (top-level) InvenTree directory """ + return os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + +def get_config_file(): + """ + Returns the path of the InvenTree configuration file. + + Note: It will be created it if does not already exist! + """ + + base_dir = get_base_dir() + + cfg_filename = os.getenv('INVENTREE_CONFIG_FILE') + + if cfg_filename: + cfg_filename = cfg_filename.strip() + cfg_filename = os.path.abspath(cfg_filename) + else: + # Config file is *not* specified - use the default + cfg_filename = os.path.join(base_dir, 'config.yaml') + + if not os.path.exists(cfg_filename): + print("InvenTree configuration file 'config.yaml' not found - creating default file") + + cfg_template = os.path.join(base_dir, "config_template.yaml") + shutil.copyfile(cfg_template, cfg_filename) + print(f"Created config file {cfg_filename}") + + return cfg_filename + + +def get_plugin_file(): + """ + Returns the path of the InvenTree plugins specification file. + + Note: It will be created if it does not already exist! + """ + # Check if the plugin.txt file (specifying required plugins) is specified + PLUGIN_FILE = os.getenv('INVENTREE_PLUGIN_FILE') + + if not PLUGIN_FILE: + # If not specified, look in the same directory as the configuration file + + config_dir = os.path.dirname(get_config_file()) + + PLUGIN_FILE = os.path.join(config_dir, 'plugins.txt') + + if not os.path.exists(PLUGIN_FILE): + logger.warning("Plugin configuration file does not exist") + logger.info(f"Creating plugin file at '{PLUGIN_FILE}'") + + # If opening the file fails (no write permission, for example), then this will throw an error + with open(PLUGIN_FILE, 'w') as plugin_file: + plugin_file.write("# InvenTree Plugins (uses PIP framework to install)\n\n") + + return PLUGIN_FILE + + +def get_setting(environment_var, backup_val, default_value=None): + """ + Helper function for retrieving a configuration setting value + + - First preference is to look for the environment variable + - Second preference is to look for the value of the settings file + - Third preference is the default value + """ + + val = os.getenv(environment_var) + + if val is not None: + return val + + if backup_val is not None: + return backup_val + + return default_value diff --git a/InvenTree/InvenTree/context.py b/InvenTree/InvenTree/context.py index bd68a0182f..94665f9e07 100644 --- a/InvenTree/InvenTree/context.py +++ b/InvenTree/InvenTree/context.py @@ -23,7 +23,7 @@ def health_status(request): if request.path.endswith('.js'): # Do not provide to script requests - return {} + return {} # pragma: no cover if hasattr(request, '_inventree_health_status'): # Do not duplicate efforts diff --git a/InvenTree/InvenTree/exchange.py b/InvenTree/InvenTree/exchange.py index 9981e52ff7..a79239568d 100644 --- a/InvenTree/InvenTree/exchange.py +++ b/InvenTree/InvenTree/exchange.py @@ -1,7 +1,12 @@ +import certifi +import ssl +from urllib.request import urlopen + from common.settings import currency_code_default, currency_codes -from urllib.error import HTTPError, URLError +from urllib.error import URLError from djmoney.contrib.exchange.backends.base import SimpleExchangeBackend +from django.db.utils import OperationalError class InvenTreeExchange(SimpleExchangeBackend): @@ -23,6 +28,22 @@ class InvenTreeExchange(SimpleExchangeBackend): return { } + def get_response(self, **kwargs): + """ + Custom code to get response from server. + Note: Adds a 5-second timeout + """ + + url = self.get_url(**kwargs) + + try: + context = ssl.create_default_context(cafile=certifi.where()) + response = urlopen(url, timeout=5, context=context) + return response.read() + except: + # Returning None here will raise an error upstream + return None + def update_rates(self, base_currency=currency_code_default()): symbols = ','.join(currency_codes()) @@ -30,5 +51,14 @@ class InvenTreeExchange(SimpleExchangeBackend): try: super().update_rates(base=base_currency, symbols=symbols) # catch connection errors - except (HTTPError, URLError): + except URLError: print('Encountered connection error while updating') + except OperationalError as e: + if 'SerializationFailure' in e.__cause__.__class__.__name__: + print('Serialization Failure while updating exchange rates') + # We are just going to swallow this exception because the + # exchange rates will be updated later by the scheduled task + else: + # Other operational errors probably are still show stoppers + # so reraise them so that the log contains the stacktrace + raise diff --git a/InvenTree/InvenTree/fields.py b/InvenTree/InvenTree/fields.py index 26fddfa912..fa91831679 100644 --- a/InvenTree/InvenTree/fields.py +++ b/InvenTree/InvenTree/fields.py @@ -53,7 +53,7 @@ class InvenTreeModelMoneyField(ModelMoneyField): """ Custom MoneyField for clean migrations while using dynamic currency settings """ - + def __init__(self, **kwargs): # detect if creating migration if 'migrate' in sys.argv or 'makemigrations' in sys.argv: diff --git a/InvenTree/InvenTree/filters.py b/InvenTree/InvenTree/filters.py index cd1b769646..8272673fc3 100644 --- a/InvenTree/InvenTree/filters.py +++ b/InvenTree/InvenTree/filters.py @@ -34,18 +34,47 @@ class InvenTreeOrderingFilter(OrderingFilter): Ordering fields should be mapped to separate fields """ - for idx, field in enumerate(ordering): + ordering_initial = ordering + ordering = [] - reverse = False + for field in ordering_initial: - if field.startswith('-'): + reverse = field.startswith('-') + + if reverse: field = field[1:] - reverse = True + # Are aliases defined for this field? if field in aliases: - ordering[idx] = aliases[field] + alias = aliases[field] + else: + alias = field + """ + Potentially, a single field could be "aliased" to multiple field, + + (For example to enforce a particular ordering sequence) + + e.g. to filter first by the integer value... + + ordering_field_aliases = { + "reference": ["integer_ref", "reference"] + } + + """ + + if type(alias) is str: + alias = [alias] + elif type(alias) in [list, tuple]: + pass + else: + # Unsupported alias type + continue + + for a in alias: if reverse: - ordering[idx] = '-' + ordering[idx] + a = '-' + a + + ordering.append(a) return ordering diff --git a/InvenTree/InvenTree/forms.py b/InvenTree/InvenTree/forms.py index 7112c2a88b..02b993d31b 100644 --- a/InvenTree/InvenTree/forms.py +++ b/InvenTree/InvenTree/forms.py @@ -4,16 +4,31 @@ Helper forms which subclass Django forms to provide additional functionality # -*- coding: utf-8 -*- from __future__ import unicode_literals +from urllib.parse import urlencode +import logging from django.utils.translation import ugettext_lazy as _ from django import forms -from django.contrib.auth.models import User +from django.contrib.auth.models import User, Group +from django.conf import settings +from django.http import HttpResponseRedirect +from django.urls import reverse from crispy_forms.helper import FormHelper from crispy_forms.layout import Layout, Field from crispy_forms.bootstrap import PrependedText, AppendedText, PrependedAppendedText, StrictButton, Div +from allauth.account.forms import SignupForm, set_form_field_order +from allauth.account.adapter import DefaultAccountAdapter +from allauth.socialaccount.adapter import DefaultSocialAccountAdapter +from allauth.exceptions import ImmediateHttpResponse +from allauth_2fa.adapter import OTPAdapter +from allauth_2fa.utils import user_has_valid_totp_device + from part.models import PartCategory +from common.models import InvenTreeSetting + +logger = logging.getLogger('inventree') class HelperForm(forms.ModelForm): @@ -144,7 +159,6 @@ class EditUserForm(HelperForm): 'username', 'first_name', 'last_name', - 'email' ] @@ -204,3 +218,112 @@ class SettingCategorySelectForm(forms.ModelForm): css_class='row', ), ) + + +# override allauth +class CustomSignupForm(SignupForm): + """ + Override to use dynamic settings + """ + def __init__(self, *args, **kwargs): + kwargs['email_required'] = InvenTreeSetting.get_setting('LOGIN_MAIL_REQUIRED') + + super().__init__(*args, **kwargs) + + # check for two mail fields + if InvenTreeSetting.get_setting('LOGIN_SIGNUP_MAIL_TWICE'): + self.fields["email2"] = forms.EmailField( + label=_("Email (again)"), + widget=forms.TextInput( + attrs={ + "type": "email", + "placeholder": _("Email address confirmation"), + } + ), + ) + + # check for two password fields + if not InvenTreeSetting.get_setting('LOGIN_SIGNUP_PWD_TWICE'): + self.fields.pop("password2") + + # reorder fields + set_form_field_order(self, ["username", "email", "email2", "password1", "password2", ]) + + def clean(self): + cleaned_data = super().clean() + + # check for two mail fields + if InvenTreeSetting.get_setting('LOGIN_SIGNUP_MAIL_TWICE'): + email = cleaned_data.get("email") + email2 = cleaned_data.get("email2") + if (email and email2) and email != email2: + self.add_error("email2", _("You must type the same email each time.")) + + return cleaned_data + + +class RegistratonMixin: + """ + Mixin to check if registration should be enabled + """ + def is_open_for_signup(self, request, *args, **kwargs): + if settings.EMAIL_HOST and InvenTreeSetting.get_setting('LOGIN_ENABLE_REG', True): + return super().is_open_for_signup(request, *args, **kwargs) + return False + + def save_user(self, request, user, form, commit=True): + user = super().save_user(request, user, form) + start_group = InvenTreeSetting.get_setting('SIGNUP_GROUP') + if start_group: + try: + group = Group.objects.get(id=start_group) + user.groups.add(group) + except Group.DoesNotExist: + logger.error('The setting `SIGNUP_GROUP` contains an non existant group', start_group) + user.save() + return user + + +class CustomAccountAdapter(RegistratonMixin, OTPAdapter, DefaultAccountAdapter): + """ + Override of adapter to use dynamic settings + """ + def send_mail(self, template_prefix, email, context): + """only send mail if backend configured""" + if settings.EMAIL_HOST: + return super().send_mail(template_prefix, email, context) + return False + + +class CustomSocialAccountAdapter(RegistratonMixin, DefaultSocialAccountAdapter): + """ + Override of adapter to use dynamic settings + """ + def is_auto_signup_allowed(self, request, sociallogin): + if InvenTreeSetting.get_setting('LOGIN_SIGNUP_SSO_AUTO', True): + return super().is_auto_signup_allowed(request, sociallogin) + return False + + # from OTPAdapter + def has_2fa_enabled(self, user): + """Returns True if the user has 2FA configured.""" + return user_has_valid_totp_device(user) + + def login(self, request, user): + # Require two-factor authentication if it has been configured. + if self.has_2fa_enabled(user): + # Cast to string for the case when this is not a JSON serializable + # object, e.g. a UUID. + request.session['allauth_2fa_user_id'] = str(user.id) + + redirect_url = reverse('two-factor-authenticate') + # Add GET parameters to the URL if they exist. + if request.GET: + redirect_url += u'?' + urlencode(request.GET) + + raise ImmediateHttpResponse( + response=HttpResponseRedirect(redirect_url) + ) + + # Otherwise defer to the original allauth adapter. + return super().login(request, user) diff --git a/InvenTree/InvenTree/helpers.py b/InvenTree/InvenTree/helpers.py index 319b88cb09..2595f8b5c3 100644 --- a/InvenTree/InvenTree/helpers.py +++ b/InvenTree/InvenTree/helpers.py @@ -69,6 +69,35 @@ def getStaticUrl(filename): return os.path.join(STATIC_URL, str(filename)) +def construct_absolute_url(*arg): + """ + Construct (or attempt to construct) an absolute URL from a relative URL. + + This is useful when (for example) sending an email to a user with a link + to something in the InvenTree web framework. + + This requires the BASE_URL configuration option to be set! + """ + + base = str(InvenTreeSetting.get_setting('INVENTREE_BASE_URL')) + + url = '/'.join(arg) + + if not base: + return url + + # Strip trailing slash from base url + if base.endswith('/'): + base = base[:-1] + + if url.startswith('/'): + url = url[1:] + + url = f"{base}/{url}" + + return url + + def getBlankImage(): """ Return the qualified path for the 'blank image' placeholder. @@ -286,7 +315,7 @@ def WrapWithQuotes(text, quote='"'): return text -def MakeBarcode(object_name, object_pk, object_data={}, **kwargs): +def MakeBarcode(object_name, object_pk, object_data=None, **kwargs): """ Generate a string for a barcode. Adds some global InvenTree parameters. Args: @@ -298,6 +327,8 @@ def MakeBarcode(object_name, object_pk, object_data={}, **kwargs): Returns: json string of the supplied data plus some other data """ + if object_data is None: + object_data = {} url = kwargs.get('url', False) brief = kwargs.get('brief', True) @@ -375,21 +406,28 @@ def DownloadFile(data, filename, content_type='application/text', inline=False): return response -def extract_serial_numbers(serials, expected_quantity): +def extract_serial_numbers(serials, expected_quantity, next_number: int): """ Attempt to extract serial numbers from an input string. - Serial numbers must be integer values - Serial numbers must be positive - Serial numbers can be split by whitespace / newline / commma chars - Serial numbers can be supplied as an inclusive range using hyphen char e.g. 10-20 + - Serial numbers can be defined as ~ for getting the next available serial number - Serial numbers can be supplied as + for getting all expecteded numbers starting from - Serial numbers can be supplied as + for getting numbers starting from Args: + serials: input string with patterns expected_quantity: The number of (unique) serial numbers we expect + next_number(int): the next possible serial number """ serials = serials.strip() + # fill in the next serial number into the serial + if '~' in serials: + serials = serials.replace('~', str(next_number)) + groups = re.split("[\s,]+", serials) numbers = [] @@ -437,7 +475,6 @@ def extract_serial_numbers(serials, expected_quantity): continue else: errors.append(_("Invalid group: {g}").format(g=group)) - continue # plus signals either # 1: 'start+': expected number of serials, starting at start @@ -462,13 +499,21 @@ def extract_serial_numbers(serials, expected_quantity): # no case else: errors.append(_("Invalid group: {g}").format(g=group)) - continue + # Group should be a number + elif group: + # try conversion + try: + number = int(group) + except: + # seem like it is not a number + raise ValidationError(_(f"Invalid group {group}")) + + number_add(number) + + # No valid input group detected else: - if group in numbers: - errors.append(_("Duplicate serial: {g}".format(g=group))) - else: - numbers.append(group) + raise ValidationError(_(f"Invalid/no group {group}")) if len(errors) > 0: raise ValidationError(errors) @@ -667,3 +712,18 @@ def clean_decimal(number): return Decimal(0) return clean_number.quantize(Decimal(1)) if clean_number == clean_number.to_integral() else clean_number.normalize() + + +def inheritors(cls): + """ + Return all classes that are subclasses from the supplied cls + """ + subcls = set() + work = [cls] + while work: + parent = work.pop() + for child in parent.__subclasses__(): + if child not in subcls: + subcls.add(child) + work.append(child) + return subcls diff --git a/InvenTree/InvenTree/locale_stats.json b/InvenTree/InvenTree/locale_stats.json deleted file mode 100644 index 9f003895c5..0000000000 --- a/InvenTree/InvenTree/locale_stats.json +++ /dev/null @@ -1 +0,0 @@ -{"de": 95, "el": 0, "en": 0, "es": 4, "fr": 6, "he": 0, "id": 0, "it": 0, "ja": 4, "ko": 0, "nl": 0, "no": 0, "pl": 27, "ru": 6, "sv": 0, "th": 0, "tr": 32, "vi": 0, "zh": 1} \ No newline at end of file diff --git a/InvenTree/InvenTree/management/commands/clean_settings.py b/InvenTree/InvenTree/management/commands/clean_settings.py index e0fd09e6c7..283416de29 100644 --- a/InvenTree/InvenTree/management/commands/clean_settings.py +++ b/InvenTree/InvenTree/management/commands/clean_settings.py @@ -2,9 +2,14 @@ Custom management command to cleanup old settings that are not defined anymore """ +import logging + from django.core.management.base import BaseCommand +logger = logging.getLogger('inventree') + + class Command(BaseCommand): """ Cleanup old (undefined) settings in the database @@ -12,27 +17,27 @@ class Command(BaseCommand): def handle(self, *args, **kwargs): - print("Collecting settings") + logger.info("Collecting settings") from common.models import InvenTreeSetting, InvenTreeUserSetting # general settings db_settings = InvenTreeSetting.objects.all() - model_settings = InvenTreeSetting.GLOBAL_SETTINGS + model_settings = InvenTreeSetting.SETTINGS # check if key exist and delete if not for setting in db_settings: if setting.key not in model_settings: setting.delete() - print(f"deleted setting '{setting.key}'") + logger.info(f"deleted setting '{setting.key}'") # user settings db_settings = InvenTreeUserSetting.objects.all() - model_settings = InvenTreeUserSetting.GLOBAL_SETTINGS + model_settings = InvenTreeUserSetting.SETTINGS # check if key exist and delete if not for setting in db_settings: if setting.key not in model_settings: setting.delete() - print(f"deleted user setting '{setting.key}'") + logger.info(f"deleted user setting '{setting.key}'") - print("checked all settings") + logger.info("checked all settings") diff --git a/InvenTree/InvenTree/management/commands/rebuild_thumbnails.py b/InvenTree/InvenTree/management/commands/rebuild_thumbnails.py new file mode 100644 index 0000000000..1169112ce7 --- /dev/null +++ b/InvenTree/InvenTree/management/commands/rebuild_thumbnails.py @@ -0,0 +1,70 @@ +""" +Custom management command to rebuild thumbnail images + +- May be required after importing a new dataset, for example +""" + +import os +import logging + +from PIL import UnidentifiedImageError + +from django.core.management.base import BaseCommand +from django.conf import settings +from django.db.utils import OperationalError, ProgrammingError + +from company.models import Company +from part.models import Part + + +logger = logging.getLogger('inventree') + + +class Command(BaseCommand): + """ + Rebuild all thumbnail images + """ + + def rebuild_thumbnail(self, model): + """ + Rebuild the thumbnail specified by the "image" field of the provided model + """ + + if not model.image: + return + + img = model.image + url = img.thumbnail.name + loc = os.path.join(settings.MEDIA_ROOT, url) + + if not os.path.exists(loc): + logger.info(f"Generating thumbnail image for '{img}'") + + try: + model.image.render_variations(replace=False) + except FileNotFoundError: + logger.error(f"ERROR: Image file '{img}' is missing") + except UnidentifiedImageError: + logger.error(f"ERROR: Image file '{img}' is not a valid image") + + def handle(self, *args, **kwargs): + + logger.setLevel(logging.INFO) + + logger.info("Rebuilding Part thumbnails") + + for part in Part.objects.exclude(image=None): + try: + self.rebuild_thumbnail(part) + except (OperationalError, ProgrammingError): + logger.error("ERROR: Database read error.") + break + + logger.info("Rebuilding Company thumbnails") + + for company in Company.objects.exclude(image=None): + try: + self.rebuild_thumbnail(company) + except (OperationalError, ProgrammingError): + logger.error("ERROR: abase read error.") + break diff --git a/InvenTree/InvenTree/management/commands/remove_mfa.py b/InvenTree/InvenTree/management/commands/remove_mfa.py new file mode 100644 index 0000000000..8c84920cc3 --- /dev/null +++ b/InvenTree/InvenTree/management/commands/remove_mfa.py @@ -0,0 +1,36 @@ +""" +Custom management command to remove MFA for a user +""" + +from django.core.management.base import BaseCommand +from django.contrib.auth import get_user_model + + +class Command(BaseCommand): + """ + Remove MFA for a user + """ + + def add_arguments(self, parser): + parser.add_argument('mail', type=str) + + def handle(self, *args, **kwargs): + + # general settings + mail = kwargs.get('mail') + if not mail: + raise KeyError('A mail is required') + user = get_user_model() + mfa_user = [*set(user.objects.filter(email=mail) | user.objects.filter(emailaddress__email=mail))] + + if len(mfa_user) == 0: + print('No user with this mail associated') + elif len(mfa_user) > 1: + print('More than one user found with this mail') + else: + # and clean out all MFA methods + # backup codes + mfa_user[0].staticdevice_set.all().delete() + # TOTP tokens + mfa_user[0].totpdevice_set.all().delete() + print(f'Removed all MFA methods for user {str(mfa_user[0])}') diff --git a/InvenTree/InvenTree/metadata.py b/InvenTree/InvenTree/metadata.py index 613983fe94..e0f8a23322 100644 --- a/InvenTree/InvenTree/metadata.py +++ b/InvenTree/InvenTree/metadata.py @@ -31,7 +31,7 @@ class InvenTreeMetadata(SimpleMetadata): """ def determine_metadata(self, request, view): - + self.request = request self.view = view @@ -72,7 +72,10 @@ class InvenTreeMetadata(SimpleMetadata): # Remove any HTTP methods that the user does not have permission for for method, permission in rolemap.items(): - if method in actions and not check(user, table, permission): + + result = check(user, table, permission) + + if method in actions and not result: del actions[method] # Add a 'DELETE' action if we are allowed to delete @@ -105,20 +108,41 @@ class InvenTreeMetadata(SimpleMetadata): model_fields = model_meta.get_field_info(model_class) + model_default_func = getattr(model_class, 'api_defaults', None) + + if model_default_func: + model_default_values = model_class.api_defaults(self.request) + else: + model_default_values = {} + # Iterate through simple fields for name, field in model_fields.fields.items(): - if field.has_default() and name in serializer_info.keys(): + if name in serializer_info.keys(): - default = field.default + if field.has_default(): - if callable(default): - try: - default = default() - except: - continue + default = field.default - serializer_info[name]['default'] = default + if callable(default): + try: + default = default() + except: + continue + + serializer_info[name]['default'] = default + + elif name in model_default_values: + serializer_info[name]['default'] = model_default_values[name] + + # Attributes to copy from the model to the field (if they don't exist) + attributes = ['help_text'] + + for attr in attributes: + if attr not in serializer_info[name]: + + if hasattr(field, attr): + serializer_info[name][attr] = getattr(field, attr) # Iterate through relations for name, relation in model_fields.relations.items(): @@ -138,6 +162,9 @@ class InvenTreeMetadata(SimpleMetadata): if 'help_text' not in serializer_info[name] and hasattr(relation.model_field, 'help_text'): serializer_info[name]['help_text'] = relation.model_field.help_text + if name in model_default_values: + serializer_info[name]['default'] = model_default_values[name] + except AttributeError: pass @@ -147,7 +174,7 @@ class InvenTreeMetadata(SimpleMetadata): # Extract extra information if an instance is available if hasattr(serializer, 'instance'): instance = serializer.instance - + if instance is None and model_class is not None: # Attempt to find the instance based on kwargs lookup kwargs = getattr(self.view, 'kwargs', None) @@ -213,7 +240,7 @@ class InvenTreeMetadata(SimpleMetadata): # Introspect writable related fields if field_info['type'] == 'field' and not field_info['read_only']: - + # If the field is a PrimaryKeyRelatedField, we can extract the model from the queryset if isinstance(field, serializers.PrimaryKeyRelatedField): model = field.queryset.model diff --git a/InvenTree/InvenTree/middleware.py b/InvenTree/InvenTree/middleware.py index 3cd5aa74f7..a9c43c71b6 100644 --- a/InvenTree/InvenTree/middleware.py +++ b/InvenTree/InvenTree/middleware.py @@ -1,12 +1,18 @@ from django.shortcuts import HttpResponseRedirect -from django.urls import reverse_lazy +from django.urls import reverse_lazy, Resolver404 from django.db import connection from django.shortcuts import redirect +from django.conf.urls import include, url import logging import time import operator from rest_framework.authtoken.models import Token +from allauth_2fa.middleware import BaseRequire2FAMiddleware, AllauthTwoFactorMiddleware + +from InvenTree.urls import frontendpatterns +from common.models import InvenTreeSetting + logger = logging.getLogger("inventree") @@ -59,20 +65,19 @@ class AuthRequiredMiddleware(object): except Token.DoesNotExist: logger.warning(f"Access denied for unknown token {token_key}") - pass # No authorization was found for the request if not authorized: # A logout request will redirect the user to the login screen - if request.path_info == reverse_lazy('logout'): - return HttpResponseRedirect(reverse_lazy('login')) + if request.path_info == reverse_lazy('account_logout'): + return HttpResponseRedirect(reverse_lazy('account_login')) path = request.path_info # List of URL endpoints we *do not* want to redirect to urls = [ - reverse_lazy('login'), - reverse_lazy('logout'), + reverse_lazy('account_login'), + reverse_lazy('account_logout'), reverse_lazy('admin:login'), reverse_lazy('admin:logout'), ] @@ -80,7 +85,7 @@ class AuthRequiredMiddleware(object): if path not in urls and not path.startswith('/api/'): # Save the 'next' parameter to pass through to the login view - return redirect('%s?next=%s' % (reverse_lazy('login'), request.path)) + return redirect('%s?next=%s' % (reverse_lazy('account_login'), request.path)) response = self.get_response(request) @@ -146,3 +151,28 @@ class QueryCountMiddleware(object): print(x[0], ':', x[1]) return response + + +url_matcher = url('', include(frontendpatterns)) + + +class Check2FAMiddleware(BaseRequire2FAMiddleware): + """check if user is required to have MFA enabled""" + def require_2fa(self, request): + # Superusers are require to have 2FA. + try: + if url_matcher.resolve(request.path[1:]): + return InvenTreeSetting.get_setting('LOGIN_ENFORCE_MFA') + except Resolver404: + pass + return False + + +class CustomAllauthTwoFactorMiddleware(AllauthTwoFactorMiddleware): + """This function ensures only frontend code triggers the MFA auth cycle""" + def process_request(self, request): + try: + if not url_matcher.resolve(request.path[1:]): + super().process_request(request) + except Resolver404: + pass diff --git a/InvenTree/InvenTree/models.py b/InvenTree/InvenTree/models.py index 2ca179bb40..0fe3136871 100644 --- a/InvenTree/InvenTree/models.py +++ b/InvenTree/InvenTree/models.py @@ -4,6 +4,7 @@ Generic models which provide extra functionality over base Django model types. from __future__ import unicode_literals +import re import os import logging @@ -20,7 +21,8 @@ from django.dispatch import receiver from mptt.models import MPTTModel, TreeForeignKey from mptt.exceptions import InvalidMove -from .validators import validate_tree_name +from InvenTree.fields import InvenTreeURLField +from InvenTree.validators import validate_tree_name logger = logging.getLogger('inventree') @@ -43,15 +45,122 @@ def rename_attachment(instance, filename): return os.path.join(instance.getSubdir(), filename) +class DataImportMixin(object): + """ + Model mixin class which provides support for 'data import' functionality. + + Models which implement this mixin should provide information on the fields available for import + """ + + # Define a map of fields avaialble for import + IMPORT_FIELDS = {} + + @classmethod + def get_import_fields(cls): + """ + Return all available import fields + + Where information on a particular field is not explicitly provided, + introspect the base model to (attempt to) find that information. + + """ + fields = cls.IMPORT_FIELDS + + for name, field in fields.items(): + + # Attempt to extract base field information from the model + base_field = None + + for f in cls._meta.fields: + if f.name == name: + base_field = f + break + + if base_field: + if 'label' not in field: + field['label'] = base_field.verbose_name + + if 'help_text' not in field: + field['help_text'] = base_field.help_text + + fields[name] = field + + return fields + + @classmethod + def get_required_import_fields(cls): + """ Return all *required* import fields """ + fields = {} + + for name, field in cls.get_import_fields().items(): + required = field.get('required', False) + + if required: + fields[name] = field + + return fields + + +class ReferenceIndexingMixin(models.Model): + """ + A mixin for keeping track of numerical copies of the "reference" field. + + !!DANGER!! always add `ReferenceIndexingSerializerMixin`to all your models serializers to + ensure the reference field is not too big + + Here, we attempt to convert a "reference" field value (char) to an integer, + for performing fast natural sorting. + + This requires extra database space (due to the extra table column), + but is required as not all supported database backends provide equivalent casting. + + This mixin adds a field named 'reference_int'. + + - If the 'reference' field can be cast to an integer, it is stored here + - If the 'reference' field *starts* with an integer, it is stored here + - Otherwise, we store zero + """ + + class Meta: + abstract = True + + def rebuild_reference_field(self): + + reference = getattr(self, 'reference', '') + + self.reference_int = extract_int(reference) + + reference_int = models.BigIntegerField(default=0) + + +def extract_int(reference): + # Default value if we cannot convert to an integer + ref_int = 0 + + # Look at the start of the string - can it be "integerized"? + result = re.match(r"^(\d+)", reference) + + if result and len(result.groups()) == 1: + ref = result.groups()[0] + try: + ref_int = int(ref) + except: + ref_int = 0 + return ref_int + + class InvenTreeAttachment(models.Model): """ Provides an abstracted class for managing file attachments. + An attachment can be either an uploaded file, or an external URL + Attributes: attachment: File comment: String descriptor for the attachment user: User associated with file upload upload_date: Date the file was uploaded """ + def getSubdir(self): """ Return the subdirectory under which attachments should be stored. @@ -60,11 +169,32 @@ class InvenTreeAttachment(models.Model): return "attachments" + def save(self, *args, **kwargs): + # Either 'attachment' or 'link' must be specified! + if not self.attachment and not self.link: + raise ValidationError({ + 'attachment': _('Missing file'), + 'link': _('Missing external link'), + }) + + super().save(*args, **kwargs) + def __str__(self): - return os.path.basename(self.attachment.name) + if self.attachment is not None: + return os.path.basename(self.attachment.name) + else: + return str(self.link) attachment = models.FileField(upload_to=rename_attachment, verbose_name=_('Attachment'), - help_text=_('Select file to attach')) + help_text=_('Select file to attach'), + blank=True, null=True + ) + + link = InvenTreeURLField( + blank=True, null=True, + verbose_name=_('Link'), + help_text=_('Link to external URL') + ) comment = models.CharField(blank=True, max_length=100, verbose_name=_('Comment'), help_text=_('File comment')) @@ -80,7 +210,10 @@ class InvenTreeAttachment(models.Model): @property def basename(self): - return os.path.basename(self.attachment.name) + if self.attachment: + return os.path.basename(self.attachment.name) + else: + return None @basename.setter def basename(self, fn): diff --git a/InvenTree/InvenTree/plugins.py b/InvenTree/InvenTree/plugins.py deleted file mode 100644 index 8da725bf9c..0000000000 --- a/InvenTree/InvenTree/plugins.py +++ /dev/null @@ -1,43 +0,0 @@ -# -*- coding: utf-8 -*- - -import inspect -import importlib -import pkgutil - - -def iter_namespace(pkg): - - return pkgutil.iter_modules(pkg.__path__, pkg.__name__ + ".") - - -def get_modules(pkg): - # Return all modules in a given package - return [importlib.import_module(name) for finder, name, ispkg in iter_namespace(pkg)] - - -def get_classes(module): - # Return all classes in a given module - return inspect.getmembers(module, inspect.isclass) - - -def get_plugins(pkg, baseclass): - """ - Return a list of all modules under a given package. - - - Modules must be a subclass of the provided 'baseclass' - - Modules must have a non-empty PLUGIN_NAME parameter - """ - - plugins = [] - - modules = get_modules(pkg) - - # Iterate through each module in the package - for mod in modules: - # Iterate through each class in the module - for item in get_classes(mod): - plugin = item[1] - if issubclass(plugin, baseclass) and plugin.PLUGIN_NAME: - plugins.append(plugin) - - return plugins diff --git a/InvenTree/InvenTree/ready.py b/InvenTree/InvenTree/ready.py index 7d63861f4b..9f5ad0ea49 100644 --- a/InvenTree/InvenTree/ready.py +++ b/InvenTree/InvenTree/ready.py @@ -6,10 +6,16 @@ def isInTestMode(): Returns True if the database is in testing mode """ - if 'test' in sys.argv: - return True + return 'test' in sys.argv - return False + +def isImportingData(): + """ + Returns True if the database is currently importing data, + e.g. 'loaddata' command is performed + """ + + return 'loaddata' in sys.argv def canAppAccessDatabase(allow_test=False): diff --git a/InvenTree/InvenTree/serializers.py b/InvenTree/InvenTree/serializers.py index d2d00a932c..e21e2fb0fc 100644 --- a/InvenTree/InvenTree/serializers.py +++ b/InvenTree/InvenTree/serializers.py @@ -5,8 +5,8 @@ Serializers used in various InvenTree apps # -*- coding: utf-8 -*- from __future__ import unicode_literals - import os +import tablib from decimal import Decimal @@ -16,6 +16,7 @@ from django.conf import settings from django.contrib.auth.models import User from django.core.exceptions import ValidationError as DjangoValidationError from django.utils.translation import ugettext_lazy as _ +from django.db import models from djmoney.contrib.django_rest_framework.fields import MoneyField from djmoney.money import Money @@ -27,6 +28,8 @@ from rest_framework.fields import empty from rest_framework.exceptions import ValidationError from rest_framework.serializers import DecimalField +from .models import extract_int + class InvenTreeMoneySerializer(MoneyField): """ @@ -66,7 +69,7 @@ class InvenTreeMoneySerializer(MoneyField): if currency and amount is not None and not isinstance(amount, MONEY_CLASSES) and amount is not empty: return Money(amount, currency) - + return amount @@ -239,20 +242,15 @@ class InvenTreeModelSerializer(serializers.ModelSerializer): return data -class InvenTreeAttachmentSerializer(InvenTreeModelSerializer): +class ReferenceIndexingSerializerMixin(): """ - Special case of an InvenTreeModelSerializer, which handles an "attachment" model. - - The only real addition here is that we support "renaming" of the attachment file. + This serializer mixin ensures the the reference is not to big / small + for the BigIntegerField """ - - # The 'filename' field must be present in the serializer - filename = serializers.CharField( - label=_('Filename'), - required=False, - source='basename', - allow_blank=False, - ) + def validate_reference(self, value): + if extract_int(value) > models.BigIntegerField.MAX_BIGINT: + raise serializers.ValidationError('reference is to to big') + return value class InvenTreeAttachmentSerializerField(serializers.FileField): @@ -284,6 +282,27 @@ class InvenTreeAttachmentSerializerField(serializers.FileField): return os.path.join(str(settings.MEDIA_URL), str(value)) +class InvenTreeAttachmentSerializer(InvenTreeModelSerializer): + """ + Special case of an InvenTreeModelSerializer, which handles an "attachment" model. + + The only real addition here is that we support "renaming" of the attachment file. + """ + + attachment = InvenTreeAttachmentSerializerField( + required=False, + allow_null=False, + ) + + # The 'filename' field must be present in the serializer + filename = serializers.CharField( + label=_('Filename'), + required=False, + source='basename', + allow_blank=False, + ) + + class InvenTreeImageSerializerField(serializers.ImageField): """ Custom image serializer. @@ -296,3 +315,326 @@ class InvenTreeImageSerializerField(serializers.ImageField): return None return os.path.join(str(settings.MEDIA_URL), str(value)) + + +class InvenTreeDecimalField(serializers.FloatField): + """ + Custom serializer for decimal fields. Solves the following issues: + + - The normal DRF DecimalField renders values with trailing zeros + - Using a FloatField can result in rounding issues: https://code.djangoproject.com/ticket/30290 + """ + + def to_internal_value(self, data): + + # Convert the value to a string, and then a decimal + try: + return Decimal(str(data)) + except: + raise serializers.ValidationError(_("Invalid value")) + + +class DataFileUploadSerializer(serializers.Serializer): + """ + Generic serializer for uploading a data file, and extracting a dataset. + + - Validates uploaded file + - Extracts column names + - Extracts data rows + """ + + # Implementing class should register a target model (database model) to be used for import + TARGET_MODEL = None + + class Meta: + fields = [ + 'data_file', + ] + + data_file = serializers.FileField( + label=_("Data File"), + help_text=_("Select data file for upload"), + required=True, + allow_empty_file=False, + ) + + def validate_data_file(self, data_file): + """ + Perform validation checks on the uploaded data file. + """ + + self.filename = data_file.name + + name, ext = os.path.splitext(data_file.name) + + # Remove the leading . from the extension + ext = ext[1:] + + accepted_file_types = [ + 'xls', 'xlsx', + 'csv', 'tsv', + 'xml', + ] + + if ext not in accepted_file_types: + raise serializers.ValidationError(_("Unsupported file type")) + + # Impose a 50MB limit on uploaded BOM files + max_upload_file_size = 50 * 1024 * 1024 + + if data_file.size > max_upload_file_size: + raise serializers.ValidationError(_("File is too large")) + + # Read file data into memory (bytes object) + try: + data = data_file.read() + except Exception as e: + raise serializers.ValidationError(str(e)) + + if ext in ['csv', 'tsv', 'xml']: + try: + data = data.decode() + except Exception as e: + raise serializers.ValidationError(str(e)) + + # Convert to a tablib dataset (we expect headers) + try: + self.dataset = tablib.Dataset().load(data, ext, headers=True) + except Exception as e: + raise serializers.ValidationError(str(e)) + + if len(self.dataset.headers) == 0: + raise serializers.ValidationError(_("No columns found in file")) + + if len(self.dataset) == 0: + raise serializers.ValidationError(_("No data rows found in file")) + + return data_file + + def match_column(self, column_name, field_names, exact=False): + """ + Attempt to match a column name (from the file) to a field (defined in the model) + + Order of matching is: + - Direct match + - Case insensitive match + - Fuzzy match + """ + + column_name = column_name.strip() + + column_name_lower = column_name.lower() + + if column_name in field_names: + return column_name + + for field_name in field_names: + if field_name.lower() == column_name_lower: + return field_name + + if exact: + # Finished available 'exact' matches + return None + + # TODO: Fuzzy pattern matching for column names + + # No matches found + return None + + def extract_data(self): + """ + Returns dataset extracted from the file + """ + + # Provide a dict of available import fields for the model + model_fields = {} + + # Keep track of columns we have already extracted + matched_columns = set() + + if self.TARGET_MODEL: + try: + model_fields = self.TARGET_MODEL.get_import_fields() + except: + pass + + # Extract a list of valid model field names + model_field_names = [key for key in model_fields.keys()] + + # Provide a dict of available columns from the dataset + file_columns = {} + + for header in self.dataset.headers: + column = {} + + # Attempt to "match" file columns to model fields + match = self.match_column(header, model_field_names, exact=True) + + if match is not None and match not in matched_columns: + matched_columns.add(match) + column['value'] = match + else: + column['value'] = None + + file_columns[header] = column + + return { + 'file_fields': file_columns, + 'model_fields': model_fields, + 'rows': [row.values() for row in self.dataset.dict], + 'filename': self.filename, + } + + def save(self): + ... + + +class DataFileExtractSerializer(serializers.Serializer): + """ + Generic serializer for extracting data from an imported dataset. + + - User provides an array of matched headers + - User provides an array of raw data rows + """ + + # Implementing class should register a target model (database model) to be used for import + TARGET_MODEL = None + + class Meta: + fields = [ + 'columns', + 'rows', + ] + + # Mapping of columns + columns = serializers.ListField( + child=serializers.CharField( + allow_blank=True, + ), + ) + + rows = serializers.ListField( + child=serializers.ListField( + child=serializers.CharField( + allow_blank=True, + allow_null=True, + ), + ) + ) + + def validate(self, data): + + data = super().validate(data) + + self.columns = data.get('columns', []) + self.rows = data.get('rows', []) + + if len(self.rows) == 0: + raise serializers.ValidationError(_("No data rows provided")) + + if len(self.columns) == 0: + raise serializers.ValidationError(_("No data columns supplied")) + + self.validate_extracted_columns() + + return data + + @property + def data(self): + + if self.TARGET_MODEL: + try: + model_fields = self.TARGET_MODEL.get_import_fields() + except: + model_fields = {} + + rows = [] + + for row in self.rows: + """ + Optionally pre-process each row, before sending back to the client + """ + + processed_row = self.process_row(self.row_to_dict(row)) + + if processed_row: + rows.append({ + "original": row, + "data": processed_row, + }) + + return { + 'fields': model_fields, + 'columns': self.columns, + 'rows': rows, + } + + def process_row(self, row): + """ + Process a 'row' of data, which is a mapped column:value dict + + Returns either a mapped column:value dict, or None. + + If the function returns None, the column is ignored! + """ + + # Default implementation simply returns the original row data + return row + + def row_to_dict(self, row): + """ + Convert a "row" to a named data dict + """ + + row_dict = { + 'errors': {}, + } + + for idx, value in enumerate(row): + + if idx < len(self.columns): + col = self.columns[idx] + + if col: + row_dict[col] = value + + return row_dict + + def validate_extracted_columns(self): + """ + Perform custom validation of header mapping. + """ + + if self.TARGET_MODEL: + try: + model_fields = self.TARGET_MODEL.get_import_fields() + except: + model_fields = {} + + cols_seen = set() + + for name, field in model_fields.items(): + + required = field.get('required', False) + + # Check for missing required columns + if required: + if name not in self.columns: + raise serializers.ValidationError(_(f"Missing required column: '{name}'")) + + for col in self.columns: + + if not col: + continue + + # Check for duplicated columns + if col in cols_seen: + raise serializers.ValidationError(_(f"Duplicate column: '{col}'")) + + cols_seen.add(col) + + def save(self): + """ + No "save" action for this serializer + """ + ... diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index f3c166df88..4fb3dc61bb 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -15,8 +15,8 @@ import logging import os import random +import socket import string -import shutil import sys from datetime import datetime @@ -25,31 +25,14 @@ import moneyed import yaml from django.utils.translation import gettext_lazy as _ from django.contrib.messages import constants as messages +import django.conf.locale + +from .config import get_base_dir, get_config_file, get_plugin_file, get_setting def _is_true(x): # Shortcut function to determine if a value "looks" like a boolean - return str(x).lower() in ['1', 'y', 'yes', 't', 'true'] - - -def get_setting(environment_var, backup_val, default_value=None): - """ - Helper function for retrieving a configuration setting value - - - First preference is to look for the environment variable - - Second preference is to look for the value of the settings file - - Third preference is the default value - """ - - val = os.getenv(environment_var) - - if val is not None: - return val - - if backup_val is not None: - return backup_val - - return default_value + return str(x).strip().lower() in ['1', 'y', 'yes', 't', 'true'] # Determine if we are running in "test" mode e.g. "manage.py test" @@ -59,31 +42,16 @@ TESTING = 'test' in sys.argv DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' # Build paths inside the project like this: os.path.join(BASE_DIR, ...) -BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +BASE_DIR = get_base_dir() -# Specify where the "config file" is located. -# By default, this is 'config.yaml' - -cfg_filename = os.getenv('INVENTREE_CONFIG_FILE') - -if cfg_filename: - cfg_filename = cfg_filename.strip() - cfg_filename = os.path.abspath(cfg_filename) - -else: - # Config file is *not* specified - use the default - cfg_filename = os.path.join(BASE_DIR, 'config.yaml') - -if not os.path.exists(cfg_filename): - print("InvenTree configuration file 'config.yaml' not found - creating default file") - - cfg_template = os.path.join(BASE_DIR, "config_template.yaml") - shutil.copyfile(cfg_template, cfg_filename) - print(f"Created config file {cfg_filename}") +cfg_filename = get_config_file() with open(cfg_filename, 'r') as cfg: CONFIG = yaml.safe_load(cfg) +# We will place any config files in the same directory as the config file +config_dir = os.path.dirname(cfg_filename) + # Default action is to run the system in Debug mode # SECURITY WARNING: don't run with debug turned on in production! DEBUG = _is_true(get_setting( @@ -91,6 +59,12 @@ DEBUG = _is_true(get_setting( CONFIG.get('debug', True) )) +# Determine if we are running in "demo mode" +DEMO_MODE = _is_true(get_setting( + 'INVENTREE_DEMO', + CONFIG.get('demo', False) +)) + DOCKER = _is_true(get_setting( 'INVENTREE_DOCKER', False @@ -108,7 +82,7 @@ logging.basicConfig( ) if log_level not in ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']: - log_level = 'WARNING' + log_level = 'WARNING' # pragma: no cover LOGGING = { 'version': 1, @@ -122,6 +96,11 @@ LOGGING = { 'handlers': ['console'], 'level': log_level, }, + 'filters': { + 'require_not_maintenance_mode_503': { + '()': 'maintenance_mode.logging.RequireNotMaintenanceMode503', + }, + }, } # Get a logger instance for this setup file @@ -140,20 +119,20 @@ d) Create "secret_key.txt" if it does not exist if os.getenv("INVENTREE_SECRET_KEY"): # Secret key passed in directly - SECRET_KEY = os.getenv("INVENTREE_SECRET_KEY").strip() - logger.info("SECRET_KEY loaded by INVENTREE_SECRET_KEY") + SECRET_KEY = os.getenv("INVENTREE_SECRET_KEY").strip() # pragma: no cover + logger.info("SECRET_KEY loaded by INVENTREE_SECRET_KEY") # pragma: no cover else: # Secret key passed in by file location key_file = os.getenv("INVENTREE_SECRET_KEY_FILE") if key_file: - key_file = os.path.abspath(key_file) + key_file = os.path.abspath(key_file) # pragma: no cover else: # default secret key location key_file = os.path.join(BASE_DIR, "secret_key.txt") key_file = os.path.abspath(key_file) - if not os.path.exists(key_file): + if not os.path.exists(key_file): # pragma: no cover logger.info(f"Generating random key file at '{key_file}'") # Create a random key file with open(key_file, 'w') as f: @@ -165,7 +144,7 @@ else: try: SECRET_KEY = open(key_file, "r").read().strip() - except Exception: + except Exception: # pragma: no cover logger.exception(f"Couldn't load keyfile {key_file}") sys.exit(-1) @@ -177,7 +156,7 @@ STATIC_ROOT = os.path.abspath( ) ) -if STATIC_ROOT is None: +if STATIC_ROOT is None: # pragma: no cover print("ERROR: INVENTREE_STATIC_ROOT directory not defined") sys.exit(1) @@ -189,7 +168,7 @@ MEDIA_ROOT = os.path.abspath( ) ) -if MEDIA_ROOT is None: +if MEDIA_ROOT is None: # pragma: no cover print("ERROR: INVENTREE_MEDIA_ROOT directory is not defined") sys.exit(1) @@ -208,7 +187,7 @@ if cors_opt: CORS_ORIGIN_ALLOW_ALL = cors_opt.get('allow_all', False) if not CORS_ORIGIN_ALLOW_ALL: - CORS_ORIGIN_WHITELIST = cors_opt.get('whitelist', []) + CORS_ORIGIN_WHITELIST = cors_opt.get('whitelist', []) # pragma: no cover # Web URL endpoint for served static files STATIC_URL = '/static/' @@ -233,7 +212,10 @@ STATIC_COLOR_THEMES_DIR = os.path.join(STATIC_ROOT, 'css', 'color-themes') MEDIA_URL = '/media/' if DEBUG: - logger.info("InvenTree running in DEBUG mode") + logger.info("InvenTree running with DEBUG enabled") + +if DEMO_MODE: + logger.warning("InvenTree running in DEMO mode") # pragma: no cover logger.debug(f"MEDIA_ROOT: '{MEDIA_ROOT}'") logger.debug(f"STATIC_ROOT: '{STATIC_ROOT}'") @@ -246,9 +228,13 @@ INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', - 'django.contrib.sessions', + 'user_sessions', # db user sessions 'django.contrib.messages', 'django.contrib.staticfiles', + 'django.contrib.sites', + + # Maintenance + 'maintenance_mode', # InvenTree apps 'build.apps.BuildConfig', @@ -260,6 +246,7 @@ INSTALLED_APPS = [ 'report.apps.ReportConfig', 'stock.apps.StockConfig', 'users.apps.UsersConfig', + 'plugin.apps.PluginAppConfig', 'InvenTree.apps.InvenTreeConfig', # InvenTree app runs last # Third part add-ons @@ -279,30 +266,45 @@ INSTALLED_APPS = [ 'error_report', # Error reporting in the admin interface 'django_q', 'formtools', # Form wizard tools + + 'allauth', # Base app for SSO + 'allauth.account', # Extend user with accounts + 'allauth.socialaccount', # Use 'social' providers + + 'django_otp', # OTP is needed for MFA - base package + 'django_otp.plugins.otp_totp', # Time based OTP + 'django_otp.plugins.otp_static', # Backup codes + + 'allauth_2fa', # MFA flow for allauth ] MIDDLEWARE = CONFIG.get('middleware', [ 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', + 'user_sessions.middleware.SessionMiddleware', # db user sessions 'django.middleware.locale.LocaleMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'corsheaders.middleware.CorsMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django_otp.middleware.OTPMiddleware', # MFA support + 'InvenTree.middleware.CustomAllauthTwoFactorMiddleware', # Flow control for allauth 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', - 'InvenTree.middleware.AuthRequiredMiddleware' + 'InvenTree.middleware.AuthRequiredMiddleware', + 'InvenTree.middleware.Check2FAMiddleware', # Check if the user should be forced to use MFA + 'maintenance_mode.middleware.MaintenanceModeMiddleware', ]) # Error reporting middleware MIDDLEWARE.append('error_report.middleware.ExceptionProcessor') AUTHENTICATION_BACKENDS = CONFIG.get('authentication_backends', [ - 'django.contrib.auth.backends.ModelBackend' + 'django.contrib.auth.backends.ModelBackend', + 'allauth.account.auth_backends.AuthenticationBackend', # SSO login via external providers ]) # If the debug toolbar is enabled, add the modules -if DEBUG and CONFIG.get('debug_toolbar', False): +if DEBUG and CONFIG.get('debug_toolbar', False): # pragma: no cover logger.info("Running with DEBUG_TOOLBAR enabled") INSTALLED_APPS.append('debug_toolbar') MIDDLEWARE.append('debug_toolbar.middleware.DebugToolbarMiddleware') @@ -318,7 +320,6 @@ TEMPLATES = [ os.path.join(MEDIA_ROOT, 'report'), os.path.join(MEDIA_ROOT, 'label'), ], - 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', @@ -331,6 +332,13 @@ TEMPLATES = [ 'InvenTree.context.status_codes', 'InvenTree.context.user_roles', ], + 'loaders': [( + 'django.template.loaders.cached.Loader', [ + 'plugin.loader.PluginTemplateLoader', + 'django.template.loaders.filesystem.Loader', + 'django.template.loaders.app_directories.Loader', + ]) + ], }, }, ] @@ -355,63 +363,6 @@ REST_FRAMEWORK = { WSGI_APPLICATION = 'InvenTree.wsgi.application' -background_workers = os.environ.get('INVENTREE_BACKGROUND_WORKERS', None) - -if background_workers is not None: - try: - background_workers = int(background_workers) - except ValueError: - background_workers = None - -if background_workers is None: - # Sensible default? - background_workers = 4 - -# django-q configuration -Q_CLUSTER = { - 'name': 'InvenTree', - 'workers': background_workers, - 'timeout': 90, - 'retry': 120, - 'queue_limit': 50, - 'bulk': 10, - 'orm': 'default', - 'sync': False, -} - -# Markdownx configuration -# Ref: https://neutronx.github.io/django-markdownx/customization/ -MARKDOWNX_MEDIA_PATH = datetime.now().strftime('markdownx/%Y/%m/%d') - -# Markdownify configuration -# Ref: https://django-markdownify.readthedocs.io/en/latest/settings.html - -MARKDOWNIFY_WHITELIST_TAGS = [ - 'a', - 'abbr', - 'b', - 'blockquote', - 'em', - 'h1', 'h2', 'h3', - 'i', - 'img', - 'li', - 'ol', - 'p', - 'strong', - 'ul' -] - -MARKDOWNIFY_WHITELIST_ATTRS = [ - 'href', - 'src', - 'alt', -] - -MARKDOWNIFY_BLEACH = False - -DATABASES = {} - """ Configure the database backend based on the user-specified values. @@ -445,7 +396,7 @@ for key in db_keys: reqiured_keys = ['ENGINE', 'NAME'] for key in reqiured_keys: - if key not in db_config: + if key not in db_config: # pragma: no cover error_msg = f'Missing required database configuration value {key}' logger.error(error_msg) @@ -464,7 +415,7 @@ db_engine = db_config['ENGINE'].lower() # Correct common misspelling if db_engine == 'sqlite': - db_engine = 'sqlite3' + db_engine = 'sqlite3' # pragma: no cover if db_engine in ['sqlite3', 'postgresql', 'mysql']: # Prepend the required python module string @@ -478,14 +429,195 @@ logger.info(f"DB_ENGINE: {db_engine}") logger.info(f"DB_NAME: {db_name}") logger.info(f"DB_HOST: {db_host}") -DATABASES['default'] = db_config +""" +In addition to base-level database configuration, we may wish to specify specific options to the database backend +Ref: https://docs.djangoproject.com/en/3.2/ref/settings/#std:setting-OPTIONS +""" -CACHES = { - 'default': { - 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', - }, +# 'OPTIONS' or 'options' can be specified in config.yaml +# Set useful sensible timeouts for a transactional webserver to communicate +# with its database server, that is, if the webserver is having issues +# connecting to the database server (such as a replica failover) don't sit and +# wait for possibly an hour or more, just tell the client something went wrong +# and let the client retry when they want to. +db_options = db_config.get("OPTIONS", db_config.get("options", {})) + +# Specific options for postgres backend +if "postgres" in db_engine: # pragma: no cover + from psycopg2.extensions import ( + ISOLATION_LEVEL_READ_COMMITTED, + ISOLATION_LEVEL_SERIALIZABLE, + ) + + # Connection timeout + if "connect_timeout" not in db_options: + # The DB server is in the same data center, it should not take very + # long to connect to the database server + # # seconds, 2 is minium allowed by libpq + db_options["connect_timeout"] = int( + os.getenv("INVENTREE_DB_TIMEOUT", 2) + ) + + # Setup TCP keepalive + # DB server is in the same DC, it should not become unresponsive for + # very long. With the defaults below we wait 5 seconds for the network + # issue to resolve itself. It it that doesn't happen whatever happened + # is probably fatal and no amount of waiting is going to fix it. + # # 0 - TCP Keepalives disabled; 1 - enabled + if "keepalives" not in db_options: + db_options["keepalives"] = int( + os.getenv("INVENTREE_DB_TCP_KEEPALIVES", "1") + ) + # # Seconds after connection is idle to send keep alive + if "keepalives_idle" not in db_options: + db_options["keepalives_idle"] = int( + os.getenv("INVENTREE_DB_TCP_KEEPALIVES_IDLE", "1") + ) + # # Seconds after missing ACK to send another keep alive + if "keepalives_interval" not in db_options: + db_options["keepalives_interval"] = int( + os.getenv("INVENTREE_DB_TCP_KEEPALIVES_INTERVAL", "1") + ) + # # Number of missing ACKs before we close the connection + if "keepalives_count" not in db_options: + db_options["keepalives_count"] = int( + os.getenv("INVENTREE_DB_TCP_KEEPALIVES_COUNT", "5") + ) + # # Milliseconds for how long pending data should remain unacked + # by the remote server + # TODO: Supported starting in PSQL 11 + # "tcp_user_timeout": int(os.getenv("PGTCP_USER_TIMEOUT", "1000"), + + # Postgres's default isolation level is Read Committed which is + # normally fine, but most developers think the database server is + # actually going to do Serializable type checks on the queries to + # protect against simultaneous changes. + # https://www.postgresql.org/docs/devel/transaction-iso.html + # https://docs.djangoproject.com/en/3.2/ref/databases/#isolation-level + if "isolation_level" not in db_options: + serializable = _is_true( + os.getenv("INVENTREE_DB_ISOLATION_SERIALIZABLE", "true") + ) + db_options["isolation_level"] = ( + ISOLATION_LEVEL_SERIALIZABLE + if serializable + else ISOLATION_LEVEL_READ_COMMITTED + ) + +# Specific options for MySql / MariaDB backend +if "mysql" in db_engine: # pragma: no cover + # TODO TCP time outs and keepalives + + # MariaDB's default isolation level is Repeatable Read which is + # normally fine, but most developers think the database server is + # actually going to Serializable type checks on the queries to + # protect against siumltaneous changes. + # https://mariadb.com/kb/en/mariadb-transactions-and-isolation-levels-for-sql-server-users/#changing-the-isolation-level + # https://docs.djangoproject.com/en/3.2/ref/databases/#mysql-isolation-level + if "isolation_level" not in db_options: + serializable = _is_true( + os.getenv("INVENTREE_DB_ISOLATION_SERIALIZABLE", "true") + ) + db_options["isolation_level"] = ( + "serializable" if serializable else "read committed" + ) + +# Specific options for sqlite backend +if "sqlite" in db_engine: + # TODO: Verify timeouts are not an issue because no network is involved for SQLite + + # SQLite's default isolation level is Serializable due to SQLite's + # single writer implementation. Presumably as a result of this, it is + # not possible to implement any lower isolation levels in SQLite. + # https://www.sqlite.org/isolation.html + pass + +# Provide OPTIONS dict back to the database configuration dict +db_config['OPTIONS'] = db_options + +DATABASES = { + 'default': db_config } + +_cache_config = CONFIG.get("cache", {}) +_cache_host = _cache_config.get("host", os.getenv("INVENTREE_CACHE_HOST")) +_cache_port = _cache_config.get( + "port", os.getenv("INVENTREE_CACHE_PORT", "6379") +) + +if _cache_host: # pragma: no cover + # We are going to rely upon a possibly non-localhost for our cache, + # so don't wait too long for the cache as nothing in the cache should be + # irreplacable. + _cache_options = { + "CLIENT_CLASS": "django_redis.client.DefaultClient", + "SOCKET_CONNECT_TIMEOUT": int(os.getenv("CACHE_CONNECT_TIMEOUT", "2")), + "SOCKET_TIMEOUT": int(os.getenv("CACHE_SOCKET_TIMEOUT", "2")), + "CONNECTION_POOL_KWARGS": { + "socket_keepalive": _is_true( + os.getenv("CACHE_TCP_KEEPALIVE", "1") + ), + "socket_keepalive_options": { + socket.TCP_KEEPCNT: int( + os.getenv("CACHE_KEEPALIVES_COUNT", "5") + ), + socket.TCP_KEEPIDLE: int( + os.getenv("CACHE_KEEPALIVES_IDLE", "1") + ), + socket.TCP_KEEPINTVL: int( + os.getenv("CACHE_KEEPALIVES_INTERVAL", "1") + ), + socket.TCP_USER_TIMEOUT: int( + os.getenv("CACHE_TCP_USER_TIMEOUT", "1000") + ), + }, + }, + } + CACHES = { + "default": { + "BACKEND": "django_redis.cache.RedisCache", + "LOCATION": f"redis://{_cache_host}:{_cache_port}/0", + "OPTIONS": _cache_options, + }, + } +else: + CACHES = { + "default": { + "BACKEND": "django.core.cache.backends.locmem.LocMemCache", + }, + } + +try: + # 4 background workers seems like a sensible default + background_workers = int(os.environ.get('INVENTREE_BACKGROUND_WORKERS', 4)) +except ValueError: # pragma: no cover + background_workers = 4 + +# django-q configuration +Q_CLUSTER = { + 'name': 'InvenTree', + 'workers': background_workers, + 'timeout': 90, + 'retry': 120, + 'queue_limit': 50, + 'bulk': 10, + 'orm': 'default', + 'sync': False, +} + +if _cache_host: # pragma: no cover + # If using external redis cache, make the cache the broker for Django Q + # as well + Q_CLUSTER["django_redis"] = "worker" + +# database user sessions +SESSION_ENGINE = 'user_sessions.backends.db' +LOGOUT_REDIRECT_URL = 'index' +SILENCED_SYSTEM_CHECKS = [ + 'admin.E410', +] + # Password validation # https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators @@ -509,7 +641,7 @@ AUTH_PASSWORD_VALIDATORS = [ EXTRA_URL_SCHEMES = CONFIG.get('extra_url_schemes', []) -if not type(EXTRA_URL_SCHEMES) in [list]: +if not type(EXTRA_URL_SCHEMES) in [list]: # pragma: no cover logger.warning("extra_url_schemes not correctly formatted") EXTRA_URL_SCHEMES = [] @@ -524,6 +656,7 @@ LANGUAGES = [ ('el', _('Greek')), ('en', _('English')), ('es', _('Spanish')), + ('es-mx', _('Spanish (Mexican)')), ('fr', _('French')), ('he', _('Hebrew')), ('it', _('Italian')), @@ -532,6 +665,7 @@ LANGUAGES = [ ('nl', _('Dutch')), ('no', _('Norwegian')), ('pl', _('Polish')), + ('pt', _('Portugese')), ('ru', _('Russian')), ('sv', _('Swedish')), ('th', _('Thai')), @@ -540,6 +674,25 @@ LANGUAGES = [ ('zh-cn', _('Chinese')), ] +# Testing interface translations +if get_setting('TEST_TRANSLATIONS', False): # pragma: no cover + # Set default language + LANGUAGE_CODE = 'xx' + + # Add to language catalog + LANGUAGES.append(('xx', 'Test')) + + # Add custom languages not provided by Django + EXTRA_LANG_INFO = { + 'xx': { + 'code': 'xx', + 'name': 'Test', + 'name_local': 'Test' + }, + } + LANG_INFO = dict(django.conf.locale.LANG_INFO, **EXTRA_LANG_INFO) + django.conf.locale.LANG_INFO = LANG_INFO + # Currencies available for use CURRENCIES = CONFIG.get( 'currencies', @@ -550,7 +703,7 @@ CURRENCIES = CONFIG.get( # Check that each provided currency is supported for currency in CURRENCIES: - if currency not in moneyed.CURRENCIES: + if currency not in moneyed.CURRENCIES: # pragma: no cover print(f"Currency code '{currency}' is not supported") sys.exit(1) @@ -624,14 +777,14 @@ USE_L10N = True # Do not use native timezone support in "test" mode # It generates a *lot* of cruft in the logs if not TESTING: - USE_TZ = True + USE_TZ = True # pragma: no cover DATE_INPUT_FORMATS = [ "%Y-%m-%d", ] # crispy forms use the bootstrap templates -CRISPY_TEMPLATE_PACK = 'bootstrap3' +CRISPY_TEMPLATE_PACK = 'bootstrap4' # Use database transactions when importing / exporting data IMPORT_EXPORT_USE_TRANSACTIONS = True @@ -646,3 +799,93 @@ MESSAGE_TAGS = { messages.ERROR: 'alert alert-block alert-danger', messages.INFO: 'alert alert-block alert-info', } + +SITE_ID = 1 + +# Load the allauth social backends +SOCIAL_BACKENDS = CONFIG.get('social_backends', []) +for app in SOCIAL_BACKENDS: + INSTALLED_APPS.append(app) # pragma: no cover + +SOCIALACCOUNT_PROVIDERS = CONFIG.get('social_providers', []) + +# settings for allauth +ACCOUNT_EMAIL_CONFIRMATION_EXPIRE_DAYS = get_setting('INVENTREE_LOGIN_CONFIRM_DAYS', CONFIG.get('login_confirm_days', 3)) + +ACCOUNT_LOGIN_ATTEMPTS_LIMIT = get_setting('INVENTREE_LOGIN_ATTEMPTS', CONFIG.get('login_attempts', 5)) + +ACCOUNT_LOGOUT_ON_PASSWORD_CHANGE = True + +# override forms / adapters +ACCOUNT_FORMS = { + 'login': 'allauth.account.forms.LoginForm', + 'signup': 'InvenTree.forms.CustomSignupForm', + 'add_email': 'allauth.account.forms.AddEmailForm', + 'change_password': 'allauth.account.forms.ChangePasswordForm', + 'set_password': 'allauth.account.forms.SetPasswordForm', + 'reset_password': 'allauth.account.forms.ResetPasswordForm', + 'reset_password_from_key': 'allauth.account.forms.ResetPasswordKeyForm', + 'disconnect': 'allauth.socialaccount.forms.DisconnectForm', +} + +SOCIALACCOUNT_ADAPTER = 'InvenTree.forms.CustomSocialAccountAdapter' +ACCOUNT_ADAPTER = 'InvenTree.forms.CustomAccountAdapter' + +# Markdownx configuration +# Ref: https://neutronx.github.io/django-markdownx/customization/ +MARKDOWNX_MEDIA_PATH = datetime.now().strftime('markdownx/%Y/%m/%d') + +# Markdownify configuration +# Ref: https://django-markdownify.readthedocs.io/en/latest/settings.html + +MARKDOWNIFY_WHITELIST_TAGS = [ + 'a', + 'abbr', + 'b', + 'blockquote', + 'em', + 'h1', 'h2', 'h3', + 'i', + 'img', + 'li', + 'ol', + 'p', + 'strong', + 'ul' +] + +MARKDOWNIFY_WHITELIST_ATTRS = [ + 'href', + 'src', + 'alt', +] + +MARKDOWNIFY_BLEACH = False + +# Maintenance mode +MAINTENANCE_MODE_RETRY_AFTER = 60 +MAINTENANCE_MODE_STATE_BACKEND = 'maintenance_mode.backends.DefaultStorageBackend' + +# Are plugins enabled? +PLUGINS_ENABLED = _is_true(get_setting( + 'INVENTREE_PLUGINS_ENABLED', + CONFIG.get('plugins_enabled', False), +)) + +PLUGIN_FILE = get_plugin_file() + +# Plugin Directories (local plugins will be loaded from these directories) +PLUGIN_DIRS = ['plugin.builtin', 'barcodes.plugins', ] + +if not TESTING: + # load local deploy directory in prod + PLUGIN_DIRS.append('plugins') # pragma: no cover + +if DEBUG or TESTING: + # load samples in debug mode + PLUGIN_DIRS.append('plugin.samples') + +# Plugin test settings +PLUGIN_TESTING = get_setting('PLUGIN_TESTING', TESTING) # are plugins beeing tested? +PLUGIN_TESTING_SETUP = get_setting('PLUGIN_TESTING_SETUP', False) # load plugins from setup hooks in testing? +PLUGIN_RETRY = get_setting('PLUGIN_RETRY', 5) # how often should plugin loading be tried? diff --git a/InvenTree/InvenTree/static/bootstrap-table/bootstrap-table-en-US.min.js b/InvenTree/InvenTree/static/bootstrap-table/bootstrap-table-en-US.min.js deleted file mode 100644 index 87bef30086..0000000000 --- a/InvenTree/InvenTree/static/bootstrap-table/bootstrap-table-en-US.min.js +++ /dev/null @@ -1,10 +0,0 @@ -/** - * bootstrap-table - An extended table to integration with some of the most widely used CSS frameworks. (Supports Bootstrap, Semantic UI, Bulma, Material Design, Foundation) - * - * @version v1.18.3 - * @homepage https://bootstrap-table.com - * @author wenzhixin (http://wenzhixin.net.cn/) - * @license MIT - */ - -!function(t,n){"object"==typeof exports&&"undefined"!=typeof module?n(require("jquery")):"function"==typeof define&&define.amd?define(["jquery"],n):n((t="undefined"!=typeof globalThis?globalThis:t||self).jQuery)}(this,(function(t){"use strict";function n(t){return t&&"object"==typeof t&&"default"in t?t:{default:t}}var r=n(t),e="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{};function o(t,n){return t(n={exports:{}},n.exports),n.exports}var i=function(t){return t&&t.Math==Math&&t},u=i("object"==typeof globalThis&&globalThis)||i("object"==typeof window&&window)||i("object"==typeof self&&self)||i("object"==typeof e&&e)||function(){return this}()||Function("return this")(),f=function(t){try{return!!t()}catch(t){return!0}},c=!f((function(){return 7!=Object.defineProperty({},1,{get:function(){return 7}})[1]})),a={}.propertyIsEnumerable,l=Object.getOwnPropertyDescriptor,s={f:l&&!a.call({1:2},1)?function(t){var n=l(this,t);return!!n&&n.enumerable}:a},p=function(t,n){return{enumerable:!(1&t),configurable:!(2&t),writable:!(4&t),value:n}},g={}.toString,d=function(t){return g.call(t).slice(8,-1)},h="".split,y=f((function(){return!Object("z").propertyIsEnumerable(0)}))?function(t){return"String"==d(t)?h.call(t,""):Object(t)}:Object,m=function(t){if(null==t)throw TypeError("Can't call method on "+t);return t},v=function(t){return y(m(t))},w=function(t){return"object"==typeof t?null!==t:"function"==typeof t},b=function(t,n){if(!w(t))return t;var r,e;if(n&&"function"==typeof(r=t.toString)&&!w(e=r.call(t)))return e;if("function"==typeof(r=t.valueOf)&&!w(e=r.call(t)))return e;if(!n&&"function"==typeof(r=t.toString)&&!w(e=r.call(t)))return e;throw TypeError("Can't convert object to primitive value")},S={}.hasOwnProperty,T=function(t,n){return S.call(t,n)},O=u.document,P=w(O)&&w(O.createElement),j=!c&&!f((function(){return 7!=Object.defineProperty((t="div",P?O.createElement(t):{}),"a",{get:function(){return 7}}).a;var t})),x=Object.getOwnPropertyDescriptor,A={f:c?x:function(t,n){if(t=v(t),n=b(n,!0),j)try{return x(t,n)}catch(t){}if(T(t,n))return p(!s.f.call(t,n),t[n])}},C=function(t){if(!w(t))throw TypeError(String(t)+" is not an object");return t},E=Object.defineProperty,M={f:c?E:function(t,n,r){if(C(t),n=b(n,!0),C(r),j)try{return E(t,n,r)}catch(t){}if("get"in r||"set"in r)throw TypeError("Accessors not supported");return"value"in r&&(t[n]=r.value),t}},R=c?function(t,n,r){return M.f(t,n,p(1,r))}:function(t,n,r){return t[n]=r,t},F=function(t,n){try{R(u,t,n)}catch(r){u[t]=n}return n},N="__core-js_shared__",L=u[N]||F(N,{}),k=Function.toString;"function"!=typeof L.inspectSource&&(L.inspectSource=function(t){return k.call(t)});var H,I,_,D,q=L.inspectSource,z=u.WeakMap,G="function"==typeof z&&/native code/.test(q(z)),U=o((function(t){(t.exports=function(t,n){return L[t]||(L[t]=void 0!==n?n:{})})("versions",[]).push({version:"3.9.1",mode:"global",copyright:"© 2021 Denis Pushkarev (zloirock.ru)"})})),B=0,W=Math.random(),J=function(t){return"Symbol("+String(void 0===t?"":t)+")_"+(++B+W).toString(36)},K=U("keys"),Q={},V=u.WeakMap;if(G){var Y=L.state||(L.state=new V),X=Y.get,Z=Y.has,$=Y.set;H=function(t,n){return n.facade=t,$.call(Y,t,n),n},I=function(t){return X.call(Y,t)||{}},_=function(t){return Z.call(Y,t)}}else{var tt=K[D="state"]||(K[D]=J(D));Q[tt]=!0,H=function(t,n){return n.facade=t,R(t,tt,n),n},I=function(t){return T(t,tt)?t[tt]:{}},_=function(t){return T(t,tt)}}var nt,rt,et={set:H,get:I,has:_,enforce:function(t){return _(t)?I(t):H(t,{})},getterFor:function(t){return function(n){var r;if(!w(n)||(r=I(n)).type!==t)throw TypeError("Incompatible receiver, "+t+" required");return r}}},ot=o((function(t){var n=et.get,r=et.enforce,e=String(String).split("String");(t.exports=function(t,n,o,i){var f,c=!!i&&!!i.unsafe,a=!!i&&!!i.enumerable,l=!!i&&!!i.noTargetGet;"function"==typeof o&&("string"!=typeof n||T(o,"name")||R(o,"name",n),(f=r(o)).source||(f.source=e.join("string"==typeof n?n:""))),t!==u?(c?!l&&t[n]&&(a=!0):delete t[n],a?t[n]=o:R(t,n,o)):a?t[n]=o:F(n,o)})(Function.prototype,"toString",(function(){return"function"==typeof this&&n(this).source||q(this)}))})),it=u,ut=function(t){return"function"==typeof t?t:void 0},ft=function(t,n){return arguments.length<2?ut(it[t])||ut(u[t]):it[t]&&it[t][n]||u[t]&&u[t][n]},ct=Math.ceil,at=Math.floor,lt=function(t){return isNaN(t=+t)?0:(t>0?at:ct)(t)},st=Math.min,pt=function(t){return t>0?st(lt(t),9007199254740991):0},gt=Math.max,dt=Math.min,ht=function(t){return function(n,r,e){var o,i=v(n),u=pt(i.length),f=function(t,n){var r=lt(t);return r<0?gt(r+n,0):dt(r,n)}(e,u);if(t&&r!=r){for(;u>f;)if((o=i[f++])!=o)return!0}else for(;u>f;f++)if((t||f in i)&&i[f]===r)return t||f||0;return!t&&-1}},yt={includes:ht(!0),indexOf:ht(!1)}.indexOf,mt=["constructor","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","toLocaleString","toString","valueOf"].concat("length","prototype"),vt={f:Object.getOwnPropertyNames||function(t){return function(t,n){var r,e=v(t),o=0,i=[];for(r in e)!T(Q,r)&&T(e,r)&&i.push(r);for(;n.length>o;)T(e,r=n[o++])&&(~yt(i,r)||i.push(r));return i}(t,mt)}},wt={f:Object.getOwnPropertySymbols},bt=ft("Reflect","ownKeys")||function(t){var n=vt.f(C(t)),r=wt.f;return r?n.concat(r(t)):n},St=function(t,n){for(var r=bt(n),e=M.f,o=A.f,i=0;i=74)&&(nt=Lt.match(/Chrome\/(\d+)/))&&(rt=nt[1]);var _t,Dt=rt&&+rt,qt=!!Object.getOwnPropertySymbols&&!f((function(){return!Symbol.sham&&(Nt?38===Dt:Dt>37&&Dt<41)})),zt=qt&&!Symbol.sham&&"symbol"==typeof Symbol.iterator,Gt=U("wks"),Ut=u.Symbol,Bt=zt?Ut:Ut&&Ut.withoutSetter||J,Wt=function(t){return T(Gt,t)&&(qt||"string"==typeof Gt[t])||(qt&&T(Ut,t)?Gt[t]=Ut[t]:Gt[t]=Bt("Symbol."+t)),Gt[t]},Jt=Wt("species"),Kt=function(t,n){var r;return Mt(t)&&("function"!=typeof(r=t.constructor)||r!==Array&&!Mt(r.prototype)?w(r)&&null===(r=r[Jt])&&(r=void 0):r=void 0),new(void 0===r?Array:r)(0===n?0:n)},Qt=Wt("species"),Vt=Wt("isConcatSpreadable"),Yt=9007199254740991,Xt="Maximum allowed index exceeded",Zt=Dt>=51||!f((function(){var t=[];return t[Vt]=!1,t.concat()[0]!==t})),$t=(_t="concat",Dt>=51||!f((function(){var t=[];return(t.constructor={})[Qt]=function(){return{foo:1}},1!==t[_t](Boolean).foo}))),tn=function(t){if(!w(t))return!1;var n=t[Vt];return void 0!==n?!!n:Mt(t)};!function(t,n){var r,e,o,i,f,c=t.target,a=t.global,l=t.stat;if(r=a?u:l?u[c]||F(c,{}):(u[c]||{}).prototype)for(e in n){if(i=n[e],o=t.noTargetGet?(f=Et(r,e))&&f.value:r[e],!Ct(a?e:c+(l?".":"#")+e,t.forced)&&void 0!==o){if(typeof i==typeof o)continue;St(i,o)}(t.sham||o&&o.sham)&&R(i,"sham",!0),ot(r,e,i,t)}}({target:"Array",proto:!0,forced:!Zt||!$t},{concat:function(t){var n,r,e,o,i,u=Rt(this),f=Kt(u,0),c=0;for(n=-1,e=arguments.length;nYt)throw TypeError(Xt);for(r=0;r=Yt)throw TypeError(Xt);Ft(f,c++,i)}return f.length=c,f}}),r.default.fn.bootstrapTable.locales["en-US"]=r.default.fn.bootstrapTable.locales.en={formatCopyRows:function(){return"Copy Rows"},formatPrint:function(){return"Print"},formatLoadingMessage:function(){return"Loading, please wait"},formatRecordsPerPage:function(t){return"".concat(t," rows per page")},formatShowingRows:function(t,n,r,e){return void 0!==e&&e>0&&e>r?"Showing ".concat(t," to ").concat(n," of ").concat(r," rows (filtered from ").concat(e," total rows)"):"Showing ".concat(t," to ").concat(n," of ").concat(r," rows")},formatSRPaginationPreText:function(){return"previous page"},formatSRPaginationPageText:function(t){return"to page ".concat(t)},formatSRPaginationNextText:function(){return"next page"},formatDetailPagination:function(t){return"Showing ".concat(t," rows")},formatClearSearch:function(){return"Clear Search"},formatSearch:function(){return"Search"},formatNoMatches:function(){return"No matching records found"},formatPaginationSwitch:function(){return"Hide/Show pagination"},formatPaginationSwitchDown:function(){return"Show pagination"},formatPaginationSwitchUp:function(){return"Hide pagination"},formatRefresh:function(){return"Refresh"},formatToggle:function(){return"Toggle"},formatToggleOn:function(){return"Show card view"},formatToggleOff:function(){return"Hide card view"},formatColumns:function(){return"Columns"},formatColumnsToggleAll:function(){return"Toggle all"},formatFullscreen:function(){return"Fullscreen"},formatAllRows:function(){return"All"},formatAutoRefresh:function(){return"Auto Refresh"},formatExport:function(){return"Export data"},formatJumpTo:function(){return"GO"},formatAdvancedSearch:function(){return"Advanced search"},formatAdvancedCloseButton:function(){return"Close"},formatFilterControlSwitch:function(){return"Hide/Show controls"},formatFilterControlSwitchHide:function(){return"Hide controls"},formatFilterControlSwitchShow:function(){return"Show controls"}},r.default.extend(r.default.fn.bootstrapTable.defaults,r.default.fn.bootstrapTable.locales["en-US"])})); diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/accent-neutralise/bootstrap-table-accent-neutralise.js b/InvenTree/InvenTree/static/bootstrap-table/extensions/accent-neutralise/bootstrap-table-accent-neutralise.js deleted file mode 100644 index 42c8d2896f..0000000000 --- a/InvenTree/InvenTree/static/bootstrap-table/extensions/accent-neutralise/bootstrap-table-accent-neutralise.js +++ /dev/null @@ -1,170 +0,0 @@ -/** - * @author: Dennis Hernández - * @webSite: http://djhvscf.github.io/Blog - * @update: zhixin wen - */ - -!($ => { - const diacriticsMap = {} - const defaultAccentsDiacritics = [ - {base: 'A', letters: '\u0041\u24B6\uFF21\u00C0\u00C1\u00C2\u1EA6\u1EA4\u1EAA\u1EA8\u00C3\u0100\u0102\u1EB0\u1EAE\u1EB4\u1EB2\u0226\u01E0\u00C4\u01DE\u1EA2\u00C5\u01FA\u01CD\u0200\u0202\u1EA0\u1EAC\u1EB6\u1E00\u0104\u023A\u2C6F'}, - {base: 'AA',letters: '\uA732'}, - {base: 'AE',letters: '\u00C6\u01FC\u01E2'}, - {base: 'AO',letters: '\uA734'}, - {base: 'AU',letters: '\uA736'}, - {base: 'AV',letters: '\uA738\uA73A'}, - {base: 'AY',letters: '\uA73C'}, - {base: 'B', letters: '\u0042\u24B7\uFF22\u1E02\u1E04\u1E06\u0243\u0182\u0181'}, - {base: 'C', letters: '\u0043\u24B8\uFF23\u0106\u0108\u010A\u010C\u00C7\u1E08\u0187\u023B\uA73E'}, - {base: 'D', letters: '\u0044\u24B9\uFF24\u1E0A\u010E\u1E0C\u1E10\u1E12\u1E0E\u0110\u018B\u018A\u0189\uA779'}, - {base: 'DZ',letters: '\u01F1\u01C4'}, - {base: 'Dz',letters: '\u01F2\u01C5'}, - {base: 'E', letters: '\u0045\u24BA\uFF25\u00C8\u00C9\u00CA\u1EC0\u1EBE\u1EC4\u1EC2\u1EBC\u0112\u1E14\u1E16\u0114\u0116\u00CB\u1EBA\u011A\u0204\u0206\u1EB8\u1EC6\u0228\u1E1C\u0118\u1E18\u1E1A\u0190\u018E'}, - {base: 'F', letters: '\u0046\u24BB\uFF26\u1E1E\u0191\uA77B'}, - {base: 'G', letters: '\u0047\u24BC\uFF27\u01F4\u011C\u1E20\u011E\u0120\u01E6\u0122\u01E4\u0193\uA7A0\uA77D\uA77E'}, - {base: 'H', letters: '\u0048\u24BD\uFF28\u0124\u1E22\u1E26\u021E\u1E24\u1E28\u1E2A\u0126\u2C67\u2C75\uA78D'}, - {base: 'I', letters: '\u0049\u24BE\uFF29\u00CC\u00CD\u00CE\u0128\u012A\u012C\u0130\u00CF\u1E2E\u1EC8\u01CF\u0208\u020A\u1ECA\u012E\u1E2C\u0197'}, - {base: 'J', letters: '\u004A\u24BF\uFF2A\u0134\u0248'}, - {base: 'K', letters: '\u004B\u24C0\uFF2B\u1E30\u01E8\u1E32\u0136\u1E34\u0198\u2C69\uA740\uA742\uA744\uA7A2'}, - {base: 'L', letters: '\u004C\u24C1\uFF2C\u013F\u0139\u013D\u1E36\u1E38\u013B\u1E3C\u1E3A\u0141\u023D\u2C62\u2C60\uA748\uA746\uA780'}, - {base: 'LJ',letters: '\u01C7'}, - {base: 'Lj',letters: '\u01C8'}, - {base: 'M', letters: '\u004D\u24C2\uFF2D\u1E3E\u1E40\u1E42\u2C6E\u019C'}, - {base: 'N', letters: '\u004E\u24C3\uFF2E\u01F8\u0143\u00D1\u1E44\u0147\u1E46\u0145\u1E4A\u1E48\u0220\u019D\uA790\uA7A4'}, - {base: 'NJ',letters: '\u01CA'}, - {base: 'Nj',letters: '\u01CB'}, - {base: 'O', letters: '\u004F\u24C4\uFF2F\u00D2\u00D3\u00D4\u1ED2\u1ED0\u1ED6\u1ED4\u00D5\u1E4C\u022C\u1E4E\u014C\u1E50\u1E52\u014E\u022E\u0230\u00D6\u022A\u1ECE\u0150\u01D1\u020C\u020E\u01A0\u1EDC\u1EDA\u1EE0\u1EDE\u1EE2\u1ECC\u1ED8\u01EA\u01EC\u00D8\u01FE\u0186\u019F\uA74A\uA74C'}, - {base: 'OI',letters: '\u01A2'}, - {base: 'OO',letters: '\uA74E'}, - {base: 'OU',letters: '\u0222'}, - {base: 'OE',letters: '\u008C\u0152'}, - {base: 'oe',letters: '\u009C\u0153'}, - {base: 'P', letters: '\u0050\u24C5\uFF30\u1E54\u1E56\u01A4\u2C63\uA750\uA752\uA754'}, - {base: 'Q', letters: '\u0051\u24C6\uFF31\uA756\uA758\u024A'}, - {base: 'R', letters: '\u0052\u24C7\uFF32\u0154\u1E58\u0158\u0210\u0212\u1E5A\u1E5C\u0156\u1E5E\u024C\u2C64\uA75A\uA7A6\uA782'}, - {base: 'S', letters: '\u0053\u24C8\uFF33\u1E9E\u015A\u1E64\u015C\u1E60\u0160\u1E66\u1E62\u1E68\u0218\u015E\u2C7E\uA7A8\uA784'}, - {base: 'T', letters: '\u0054\u24C9\uFF34\u1E6A\u0164\u1E6C\u021A\u0162\u1E70\u1E6E\u0166\u01AC\u01AE\u023E\uA786'}, - {base: 'TZ',letters: '\uA728'}, - {base: 'U', letters: '\u0055\u24CA\uFF35\u00D9\u00DA\u00DB\u0168\u1E78\u016A\u1E7A\u016C\u00DC\u01DB\u01D7\u01D5\u01D9\u1EE6\u016E\u0170\u01D3\u0214\u0216\u01AF\u1EEA\u1EE8\u1EEE\u1EEC\u1EF0\u1EE4\u1E72\u0172\u1E76\u1E74\u0244'}, - {base: 'V', letters: '\u0056\u24CB\uFF36\u1E7C\u1E7E\u01B2\uA75E\u0245'}, - {base: 'VY',letters: '\uA760'}, - {base: 'W', letters: '\u0057\u24CC\uFF37\u1E80\u1E82\u0174\u1E86\u1E84\u1E88\u2C72'}, - {base: 'X', letters: '\u0058\u24CD\uFF38\u1E8A\u1E8C'}, - {base: 'Y', letters: '\u0059\u24CE\uFF39\u1EF2\u00DD\u0176\u1EF8\u0232\u1E8E\u0178\u1EF6\u1EF4\u01B3\u024E\u1EFE'}, - {base: 'Z', letters: '\u005A\u24CF\uFF3A\u0179\u1E90\u017B\u017D\u1E92\u1E94\u01B5\u0224\u2C7F\u2C6B\uA762'}, - {base: 'a', letters: '\u0061\u24D0\uFF41\u1E9A\u00E0\u00E1\u00E2\u1EA7\u1EA5\u1EAB\u1EA9\u00E3\u0101\u0103\u1EB1\u1EAF\u1EB5\u1EB3\u0227\u01E1\u00E4\u01DF\u1EA3\u00E5\u01FB\u01CE\u0201\u0203\u1EA1\u1EAD\u1EB7\u1E01\u0105\u2C65\u0250'}, - {base: 'aa',letters: '\uA733'}, - {base: 'ae',letters: '\u00E6\u01FD\u01E3'}, - {base: 'ao',letters: '\uA735'}, - {base: 'au',letters: '\uA737'}, - {base: 'av',letters: '\uA739\uA73B'}, - {base: 'ay',letters: '\uA73D'}, - {base: 'b', letters: '\u0062\u24D1\uFF42\u1E03\u1E05\u1E07\u0180\u0183\u0253'}, - {base: 'c', letters: '\u0063\u24D2\uFF43\u0107\u0109\u010B\u010D\u00E7\u1E09\u0188\u023C\uA73F\u2184'}, - {base: 'd', letters: '\u0064\u24D3\uFF44\u1E0B\u010F\u1E0D\u1E11\u1E13\u1E0F\u0111\u018C\u0256\u0257\uA77A'}, - {base: 'dz',letters: '\u01F3\u01C6'}, - {base: 'e', letters: '\u0065\u24D4\uFF45\u00E8\u00E9\u00EA\u1EC1\u1EBF\u1EC5\u1EC3\u1EBD\u0113\u1E15\u1E17\u0115\u0117\u00EB\u1EBB\u011B\u0205\u0207\u1EB9\u1EC7\u0229\u1E1D\u0119\u1E19\u1E1B\u0247\u025B\u01DD'}, - {base: 'f', letters: '\u0066\u24D5\uFF46\u1E1F\u0192\uA77C'}, - {base: 'g', letters: '\u0067\u24D6\uFF47\u01F5\u011D\u1E21\u011F\u0121\u01E7\u0123\u01E5\u0260\uA7A1\u1D79\uA77F'}, - {base: 'h', letters: '\u0068\u24D7\uFF48\u0125\u1E23\u1E27\u021F\u1E25\u1E29\u1E2B\u1E96\u0127\u2C68\u2C76\u0265'}, - {base: 'hv',letters: '\u0195'}, - {base: 'i', letters: '\u0069\u24D8\uFF49\u00EC\u00ED\u00EE\u0129\u012B\u012D\u00EF\u1E2F\u1EC9\u01D0\u0209\u020B\u1ECB\u012F\u1E2D\u0268\u0131'}, - {base: 'j', letters: '\u006A\u24D9\uFF4A\u0135\u01F0\u0249'}, - {base: 'k', letters: '\u006B\u24DA\uFF4B\u1E31\u01E9\u1E33\u0137\u1E35\u0199\u2C6A\uA741\uA743\uA745\uA7A3'}, - {base: 'l', letters: '\u006C\u24DB\uFF4C\u0140\u013A\u013E\u1E37\u1E39\u013C\u1E3D\u1E3B\u017F\u0142\u019A\u026B\u2C61\uA749\uA781\uA747'}, - {base: 'lj',letters: '\u01C9'}, - {base: 'm', letters: '\u006D\u24DC\uFF4D\u1E3F\u1E41\u1E43\u0271\u026F'}, - {base: 'n', letters: '\u006E\u24DD\uFF4E\u01F9\u0144\u00F1\u1E45\u0148\u1E47\u0146\u1E4B\u1E49\u019E\u0272\u0149\uA791\uA7A5'}, - {base: 'nj',letters: '\u01CC'}, - {base: 'o', letters: '\u006F\u24DE\uFF4F\u00F2\u00F3\u00F4\u1ED3\u1ED1\u1ED7\u1ED5\u00F5\u1E4D\u022D\u1E4F\u014D\u1E51\u1E53\u014F\u022F\u0231\u00F6\u022B\u1ECF\u0151\u01D2\u020D\u020F\u01A1\u1EDD\u1EDB\u1EE1\u1EDF\u1EE3\u1ECD\u1ED9\u01EB\u01ED\u00F8\u01FF\u0254\uA74B\uA74D\u0275'}, - {base: 'oi',letters: '\u01A3'}, - {base: 'ou',letters: '\u0223'}, - {base: 'oo',letters: '\uA74F'}, - {base: 'p',letters: '\u0070\u24DF\uFF50\u1E55\u1E57\u01A5\u1D7D\uA751\uA753\uA755'}, - {base: 'q',letters: '\u0071\u24E0\uFF51\u024B\uA757\uA759'}, - {base: 'r',letters: '\u0072\u24E1\uFF52\u0155\u1E59\u0159\u0211\u0213\u1E5B\u1E5D\u0157\u1E5F\u024D\u027D\uA75B\uA7A7\uA783'}, - {base: 's',letters: '\u0073\u24E2\uFF53\u00DF\u015B\u1E65\u015D\u1E61\u0161\u1E67\u1E63\u1E69\u0219\u015F\u023F\uA7A9\uA785\u1E9B'}, - {base: 't',letters: '\u0074\u24E3\uFF54\u1E6B\u1E97\u0165\u1E6D\u021B\u0163\u1E71\u1E6F\u0167\u01AD\u0288\u2C66\uA787'}, - {base: 'tz',letters: '\uA729'}, - {base: 'u',letters: '\u0075\u24E4\uFF55\u00F9\u00FA\u00FB\u0169\u1E79\u016B\u1E7B\u016D\u00FC\u01DC\u01D8\u01D6\u01DA\u1EE7\u016F\u0171\u01D4\u0215\u0217\u01B0\u1EEB\u1EE9\u1EEF\u1EED\u1EF1\u1EE5\u1E73\u0173\u1E77\u1E75\u0289'}, - {base: 'v',letters: '\u0076\u24E5\uFF56\u1E7D\u1E7F\u028B\uA75F\u028C'}, - {base: 'vy',letters: '\uA761'}, - {base: 'w',letters: '\u0077\u24E6\uFF57\u1E81\u1E83\u0175\u1E87\u1E85\u1E98\u1E89\u2C73'}, - {base: 'x',letters: '\u0078\u24E7\uFF58\u1E8B\u1E8D'}, - {base: 'y',letters: '\u0079\u24E8\uFF59\u1EF3\u00FD\u0177\u1EF9\u0233\u1E8F\u00FF\u1EF7\u1E99\u1EF5\u01B4\u024F\u1EFF'}, - {base: 'z',letters: '\u007A\u24E9\uFF5A\u017A\u1E91\u017C\u017E\u1E93\u1E95\u01B6\u0225\u0240\u2C6C\uA763'} - ] - - const initNeutraliser = () => { - for (const diacritic of defaultAccentsDiacritics) { - const letters = diacritic.letters - for (let i = 0; i < letters.length; i++) { - diacriticsMap[letters[i]] = diacritic.base - } - } - } - - /* eslint-disable no-control-regex */ - const removeDiacritics = str => str.replace(/[^\u0000-\u007E]/g, a => diacriticsMap[a] || a) - - $.extend($.fn.bootstrapTable.defaults, { - searchAccentNeutralise: false - }) - - $.BootstrapTable = class extends $.BootstrapTable { - init () { - if (this.options.searchAccentNeutralise) { - initNeutraliser() - } - super.init() - } - - initSearch () { - if (this.options.sidePagination !== 'server') { - let s = this.searchText && this.searchText.toLowerCase() - const f = $.isEmptyObject(this.filterColumns) ? null : this.filterColumns - - // Check filter - this.data = f ? this.options.data.filter((item, i) => { - for (const key in f) { - if (item[key] !== f[key]) { - return false - } - } - return true - }) : this.options.data - - this.data = s ? this.options.data.filter((item, i) => { - for (let [key, value] of Object.entries(item)) { - key = $.isNumeric(key) ? parseInt(key, 10) : key - const column = this.columns[this.fieldsColumnsIndex[key]] - const j = this.header.fields.indexOf(key) - - if (column && column.searchFormatter) { - value = $.fn.bootstrapTable.utils.calculateObjectValue(column, - this.header.formatters[j], [value, item, i], value) - } - - const index = this.header.fields.indexOf(key) - if (index !== -1 && this.header.searchables[index] && typeof value === 'string') { - if (this.options.searchAccentNeutralise) { - value = removeDiacritics(value) - s = removeDiacritics(s) - } - if (this.options.strictSearch) { - if ((`${value}`).toLowerCase() === s) { - return true - } - } else { - if ((`${value}`).toLowerCase().includes(s)) { - return true - } - } - } - } - return false - }) : this.data - } - } - } -})(jQuery) diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/accent-neutralise/extension.json b/InvenTree/InvenTree/static/bootstrap-table/extensions/accent-neutralise/extension.json deleted file mode 100644 index ce69cfdb5f..0000000000 --- a/InvenTree/InvenTree/static/bootstrap-table/extensions/accent-neutralise/extension.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "Accent Neutralise", - "version": "1.0.0", - "description": "Plugin to neutralise the words.", - "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/accent-neutralise", - "example": "#", - - "plugins": [{ - "name": "bootstrap-table-accent-neutralise", - "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/accent-neutralise" - }], - - "author": { - "name": "djhvscf", - "image": "https://avatars1.githubusercontent.com/u/4496763" - } -} \ No newline at end of file diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/auto-refresh/extension.json b/InvenTree/InvenTree/static/bootstrap-table/extensions/auto-refresh/extension.json deleted file mode 100644 index 182561bc1e..0000000000 --- a/InvenTree/InvenTree/static/bootstrap-table/extensions/auto-refresh/extension.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "Auto Refresh", - "version": "1.0.0", - "description": "Plugin to automatically refresh the table on an interval.", - "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/auto-refresh", - "example": "#", - - "plugins": [{ - "name": "bootstrap-table-auto-refresh", - "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/auto-refresh" - }], - - "author": { - "name": "fenichaler", - "image": "https://avatars.githubusercontent.com/u/3437075" - } -} diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/cookie/extension.json b/InvenTree/InvenTree/static/bootstrap-table/extensions/cookie/extension.json deleted file mode 100644 index 6422de402a..0000000000 --- a/InvenTree/InvenTree/static/bootstrap-table/extensions/cookie/extension.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "Cookie", - "version": "1.2.1", - "description": "Plugin to use the cookie of the browser.", - "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/cookie", - "example": "http://issues.wenzhixin.net.cn/bootstrap-table/#extensions/cookie.html", - - "plugins": [{ - "name": "bootstrap-table-cookie", - "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/cookie" - }], - - "author": { - "name": "djhvscf", - "image": "https://avatars1.githubusercontent.com/u/4496763" - } -} \ No newline at end of file diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/copy-rows/extension.json b/InvenTree/InvenTree/static/bootstrap-table/extensions/copy-rows/extension.json deleted file mode 100644 index 4deb78b288..0000000000 --- a/InvenTree/InvenTree/static/bootstrap-table/extensions/copy-rows/extension.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "Copy Rows", - "version": "1.0.0", - "description": "Allows pushing of selected column data to the clipboard.", - "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/copy-rows", - "example": "http://issues.wenzhixin.net.cn/bootstrap-table/#extensions/copy-rows.html", - - "plugins": [{ - "name": "copy-rows", - "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/copy-rows" - }], - - "author": { - "name": "Homer Glascock", - "image": "https://avatars1.githubusercontent.com/u/5546710" - } -} diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/defer-url/extension.json b/InvenTree/InvenTree/static/bootstrap-table/extensions/defer-url/extension.json deleted file mode 100644 index 4895fb21d0..0000000000 --- a/InvenTree/InvenTree/static/bootstrap-table/extensions/defer-url/extension.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "DeferURL", - "version": "1.0.0", - "description": "Plugin to defer server side processing.", - "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/defer-url", - "example": "http://issues.wenzhixin.net.cn/bootstrap-table/#extensions/defer-url.html", - - "plugins": [{ - "name": "bootstrap-table-defer-url", - "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/defer-url" - }], - - "author": { - "name": "rubensa", - "image": "https://avatars1.githubusercontent.com/u/1469340" - } -} \ No newline at end of file diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/editable/extension.json b/InvenTree/InvenTree/static/bootstrap-table/extensions/editable/extension.json deleted file mode 100644 index ea6106b693..0000000000 --- a/InvenTree/InvenTree/static/bootstrap-table/extensions/editable/extension.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "Table Editable", - "version": "1.1.0", - "description": "Use the x-editable to in-place editing your table.", - "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/editable", - "example": "http://issues.wenzhixin.net.cn/bootstrap-table/#extensions/editable.html", - - "plugins": [{ - "name": "x-editable", - "url": "https://github.com/vitalets/x-editable" - }], - - "author": { - "name": "wenzhixin", - "image": "https://avatars1.githubusercontent.com/u/2117018" - } -} \ No newline at end of file diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/export/extension.json b/InvenTree/InvenTree/static/bootstrap-table/extensions/export/extension.json deleted file mode 100644 index 5714a5fa0a..0000000000 --- a/InvenTree/InvenTree/static/bootstrap-table/extensions/export/extension.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "Table Export", - "version": "1.1.0", - "description": "Export your table data to JSON, XML, CSV, TXT, SQL, Word, Excel, PNG, PDF.", - "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/export", - "example": "http://issues.wenzhixin.net.cn/bootstrap-table/#extensions/export.html", - - "plugins": [{ - "name": "tableExport.jquery.plugin", - "url": "https://github.com/hhurz/tableExport.jquery.plugin" - }], - - "author": { - "name": "wenzhixin", - "image": "https://avatars1.githubusercontent.com/u/2117018" - } -} \ No newline at end of file diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/filter-control/extension.json b/InvenTree/InvenTree/static/bootstrap-table/extensions/filter-control/extension.json deleted file mode 100644 index b0e31694bb..0000000000 --- a/InvenTree/InvenTree/static/bootstrap-table/extensions/filter-control/extension.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "Filter Control", - "version": "2.1.0", - "description": "Plugin to add input/select element on the top of the columns in order to filter the data.", - "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/filter-control", - "example": "http://issues.wenzhixin.net.cn/bootstrap-table/#extensions/filter-control.html", - - "plugins": [{ - "name": "bootstrap-table-filter-control", - "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/filter-control" - }], - - "author": { - "name": "djhvscf", - "image": "https://avatars1.githubusercontent.com/u/4496763" - } -} \ No newline at end of file diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/group-by-v2/extension.json b/InvenTree/InvenTree/static/bootstrap-table/extensions/group-by-v2/extension.json deleted file mode 100644 index 2b948bbd30..0000000000 --- a/InvenTree/InvenTree/static/bootstrap-table/extensions/group-by-v2/extension.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "Group By V2", - "version": "1.0.0", - "description": "Group the data by field", - "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/group-by-v2", - "example": "", - "plugins": [], - "author": { - "name": "Knoxvillekm", - "image": "https://avatars3.githubusercontent.com/u/11072464" - } -} diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/group-by/bootstrap-table-group-by.css b/InvenTree/InvenTree/static/bootstrap-table/extensions/group-by/bootstrap-table-group-by.css deleted file mode 100644 index fce5a9a7b1..0000000000 --- a/InvenTree/InvenTree/static/bootstrap-table/extensions/group-by/bootstrap-table-group-by.css +++ /dev/null @@ -1,53 +0,0 @@ -table.treetable tbody tr td { - cursor: default; -} - -table.treetable span { - background-position: center left; - background-repeat: no-repeat; - padding: .2em 0 .2em 1.5em; -} - -table.treetable tr.collapsed span.indenter a { - background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAAsTAAALEwEAmpwYAAAKT2lDQ1BQaG90b3Nob3AgSUNDIHByb2ZpbGUAAHjanVNnVFPpFj333vRCS4iAlEtvUhUIIFJCi4AUkSYqIQkQSoghodkVUcERRUUEG8igiAOOjoCMFVEsDIoK2AfkIaKOg6OIisr74Xuja9a89+bN/rXXPues852zzwfACAyWSDNRNYAMqUIeEeCDx8TG4eQuQIEKJHAAEAizZCFz/SMBAPh+PDwrIsAHvgABeNMLCADATZvAMByH/w/qQplcAYCEAcB0kThLCIAUAEB6jkKmAEBGAYCdmCZTAKAEAGDLY2LjAFAtAGAnf+bTAICd+Jl7AQBblCEVAaCRACATZYhEAGg7AKzPVopFAFgwABRmS8Q5ANgtADBJV2ZIALC3AMDOEAuyAAgMADBRiIUpAAR7AGDIIyN4AISZABRG8lc88SuuEOcqAAB4mbI8uSQ5RYFbCC1xB1dXLh4ozkkXKxQ2YQJhmkAuwnmZGTKBNA/g88wAAKCRFRHgg/P9eM4Ors7ONo62Dl8t6r8G/yJiYuP+5c+rcEAAAOF0ftH+LC+zGoA7BoBt/qIl7gRoXgugdfeLZrIPQLUAoOnaV/Nw+H48PEWhkLnZ2eXk5NhKxEJbYcpXff5nwl/AV/1s+X48/Pf14L7iJIEyXYFHBPjgwsz0TKUcz5IJhGLc5o9H/LcL//wd0yLESWK5WCoU41EScY5EmozzMqUiiUKSKcUl0v9k4t8s+wM+3zUAsGo+AXuRLahdYwP2SycQWHTA4vcAAPK7b8HUKAgDgGiD4c93/+8//UegJQCAZkmScQAAXkQkLlTKsz/HCAAARKCBKrBBG/TBGCzABhzBBdzBC/xgNoRCJMTCQhBCCmSAHHJgKayCQiiGzbAdKmAv1EAdNMBRaIaTcA4uwlW4Dj1wD/phCJ7BKLyBCQRByAgTYSHaiAFiilgjjggXmYX4IcFIBBKLJCDJiBRRIkuRNUgxUopUIFVIHfI9cgI5h1xGupE7yAAygvyGvEcxlIGyUT3UDLVDuag3GoRGogvQZHQxmo8WoJvQcrQaPYw2oefQq2gP2o8+Q8cwwOgYBzPEbDAuxsNCsTgsCZNjy7EirAyrxhqwVqwDu4n1Y8+xdwQSgUXACTYEd0IgYR5BSFhMWE7YSKggHCQ0EdoJNwkDhFHCJyKTqEu0JroR+cQYYjIxh1hILCPWEo8TLxB7iEPENyQSiUMyJ7mQAkmxpFTSEtJG0m5SI+ksqZs0SBojk8naZGuyBzmULCAryIXkneTD5DPkG+Qh8lsKnWJAcaT4U+IoUspqShnlEOU05QZlmDJBVaOaUt2ooVQRNY9aQq2htlKvUYeoEzR1mjnNgxZJS6WtopXTGmgXaPdpr+h0uhHdlR5Ol9BX0svpR+iX6AP0dwwNhhWDx4hnKBmbGAcYZxl3GK+YTKYZ04sZx1QwNzHrmOeZD5lvVVgqtip8FZHKCpVKlSaVGyovVKmqpqreqgtV81XLVI+pXlN9rkZVM1PjqQnUlqtVqp1Q61MbU2epO6iHqmeob1Q/pH5Z/YkGWcNMw09DpFGgsV/jvMYgC2MZs3gsIWsNq4Z1gTXEJrHN2Xx2KruY/R27iz2qqaE5QzNKM1ezUvOUZj8H45hx+Jx0TgnnKKeX836K3hTvKeIpG6Y0TLkxZVxrqpaXllirSKtRq0frvTau7aedpr1Fu1n7gQ5Bx0onXCdHZ4/OBZ3nU9lT3acKpxZNPTr1ri6qa6UbobtEd79up+6Ynr5egJ5Mb6feeb3n+hx9L/1U/W36p/VHDFgGswwkBtsMzhg8xTVxbzwdL8fb8VFDXcNAQ6VhlWGX4YSRudE8o9VGjUYPjGnGXOMk423GbcajJgYmISZLTepN7ppSTbmmKaY7TDtMx83MzaLN1pk1mz0x1zLnm+eb15vft2BaeFostqi2uGVJsuRaplnutrxuhVo5WaVYVVpds0atna0l1rutu6cRp7lOk06rntZnw7Dxtsm2qbcZsOXYBtuutm22fWFnYhdnt8Wuw+6TvZN9un2N/T0HDYfZDqsdWh1+c7RyFDpWOt6azpzuP33F9JbpL2dYzxDP2DPjthPLKcRpnVOb00dnF2e5c4PziIuJS4LLLpc+Lpsbxt3IveRKdPVxXeF60vWdm7Obwu2o26/uNu5p7ofcn8w0nymeWTNz0MPIQ+BR5dE/C5+VMGvfrH5PQ0+BZ7XnIy9jL5FXrdewt6V3qvdh7xc+9j5yn+M+4zw33jLeWV/MN8C3yLfLT8Nvnl+F30N/I/9k/3r/0QCngCUBZwOJgUGBWwL7+Hp8Ib+OPzrbZfay2e1BjKC5QRVBj4KtguXBrSFoyOyQrSH355jOkc5pDoVQfujW0Adh5mGLw34MJ4WHhVeGP45wiFga0TGXNXfR3ENz30T6RJZE3ptnMU85ry1KNSo+qi5qPNo3ujS6P8YuZlnM1VidWElsSxw5LiquNm5svt/87fOH4p3iC+N7F5gvyF1weaHOwvSFpxapLhIsOpZATIhOOJTwQRAqqBaMJfITdyWOCnnCHcJnIi/RNtGI2ENcKh5O8kgqTXqS7JG8NXkkxTOlLOW5hCepkLxMDUzdmzqeFpp2IG0yPTq9MYOSkZBxQqohTZO2Z+pn5mZ2y6xlhbL+xW6Lty8elQfJa7OQrAVZLQq2QqboVFoo1yoHsmdlV2a/zYnKOZarnivN7cyzytuQN5zvn//tEsIS4ZK2pYZLVy0dWOa9rGo5sjxxedsK4xUFK4ZWBqw8uIq2Km3VT6vtV5eufr0mek1rgV7ByoLBtQFr6wtVCuWFfevc1+1dT1gvWd+1YfqGnRs+FYmKrhTbF5cVf9go3HjlG4dvyr+Z3JS0qavEuWTPZtJm6ebeLZ5bDpaql+aXDm4N2dq0Dd9WtO319kXbL5fNKNu7g7ZDuaO/PLi8ZafJzs07P1SkVPRU+lQ27tLdtWHX+G7R7ht7vPY07NXbW7z3/T7JvttVAVVN1WbVZftJ+7P3P66Jqun4lvttXa1ObXHtxwPSA/0HIw6217nU1R3SPVRSj9Yr60cOxx++/p3vdy0NNg1VjZzG4iNwRHnk6fcJ3/ceDTradox7rOEH0x92HWcdL2pCmvKaRptTmvtbYlu6T8w+0dbq3nr8R9sfD5w0PFl5SvNUyWna6YLTk2fyz4ydlZ19fi753GDborZ752PO32oPb++6EHTh0kX/i+c7vDvOXPK4dPKy2+UTV7hXmq86X23qdOo8/pPTT8e7nLuarrlca7nuer21e2b36RueN87d9L158Rb/1tWeOT3dvfN6b/fF9/XfFt1+cif9zsu72Xcn7q28T7xf9EDtQdlD3YfVP1v+3Njv3H9qwHeg89HcR/cGhYPP/pH1jw9DBY+Zj8uGDYbrnjg+OTniP3L96fynQ89kzyaeF/6i/suuFxYvfvjV69fO0ZjRoZfyl5O/bXyl/erA6xmv28bCxh6+yXgzMV70VvvtwXfcdx3vo98PT+R8IH8o/2j5sfVT0Kf7kxmTk/8EA5jz/GMzLdsAAAAgY0hSTQAAeiUAAICDAAD5/wAAgOkAAHUwAADqYAAAOpgAABdvkl/FRgAAAHlJREFUeNrcU1sNgDAQ6wgmcAM2MICGGlg1gJnNzWQcvwQGy1j4oUl/7tH0mpwzM7SgQyO+EZAUWh2MkkzSWhJwuRAlHYsJwEwyvs1gABDuzqoJcTw5qxaIJN0bgQRgIjnlmn1heSO5PE6Y2YXe+5Cr5+h++gs12AcAS6FS+7YOsj4AAAAASUVORK5CYII=); - padding-right: 12px; -} - -table.treetable tr.expanded span.indenter a { - background-image: url(data:image/png;base64,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); - padding-right: 12px; -} - -table.treetable tr.branch { - background-color: #f9f9f9; -} - -table.treetable tr.selected { - background-color: #3875d7; - color: #fff; -} - -table.treetable tr span.indenter a { - outline: none; /* Expander shows outline after upgrading to 3.0 (#141) */ -} - -table.treetable tr.collapsed.selected span.indenter a { - background-image: url(data:image/png;base64,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); -} - -table.treetable tr.expanded.selected span.indenter a { - background-image: url(data:image/png;base64,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); -} - -table.treetable tr.accept { - background-color: #a3bce4; - color: #fff -} - -table.treetable tr.collapsed.accept td span.indenter a { - background-image: url(data:image/x-png;base64,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); -} - -table.treetable tr.expanded.accept td span.indenter a { - background-image: url(data:image/x-png;base64,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); -} \ No newline at end of file diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/group-by/bootstrap-table-group-by.js b/InvenTree/InvenTree/static/bootstrap-table/extensions/group-by/bootstrap-table-group-by.js deleted file mode 100644 index 35c3b61a79..0000000000 --- a/InvenTree/InvenTree/static/bootstrap-table/extensions/group-by/bootstrap-table-group-by.js +++ /dev/null @@ -1,243 +0,0 @@ -/** - * @author: Dennis Hernández - * @webSite: http://djhvscf.github.io/Blog - * @version: v1.1.0 - */ - -!function ($) { - - 'use strict'; - - var originalRowAttr, - dataTTId = 'data-tt-id', - dataTTParentId = 'data-tt-parent-id', - obj = {}, - parentId = undefined; - - var getParentRowId = function (that, id) { - var parentRows = that.$body.find('tr').not('[' + 'data-tt-parent-id]'); - - for (var i = 0; i < parentRows.length; i++) { - if (i === id) { - return $(parentRows[i]).attr('data-tt-id'); - } - } - - return undefined; - }; - - var sumData = function (that, data) { - var sumRow = {}; - $.each(data, function (i, row) { - if (!row.IsParent) { - for (var prop in row) { - if (!isNaN(parseFloat(row[prop]))) { - if (that.columns[that.fieldsColumnsIndex[prop]].groupBySumGroup) { - if (sumRow[prop] === undefined) { - sumRow[prop] = 0; - } - sumRow[prop] += +row[prop]; - } - } - } - } - }); - return sumRow; - }; - - var rowAttr = function (row, index) { - //Call the User Defined Function - originalRowAttr.apply([row, index]); - - obj[dataTTId.toString()] = index; - - if (!row.IsParent) { - obj[dataTTParentId.toString()] = parentId === undefined ? index : parentId; - } else { - parentId = index; - delete obj[dataTTParentId.toString()]; - } - - return obj; - }; - - var setObjectKeys = function () { - // From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys - Object.keys = function (o) { - if (o !== Object(o)) { - throw new TypeError('Object.keys called on a non-object'); - } - var k = [], - p; - for (p in o) { - if (Object.prototype.hasOwnProperty.call(o, p)) { - k.push(p); - } - } - return k; - } - }; - - var getDataArrayFromItem = function (that, item) { - var itemDataArray = []; - for (var i = 0; i < that.options.groupByField.length; i++) { - itemDataArray.push(item[that.options.groupByField[i]]); - } - - return itemDataArray; - }; - - var getNewRow = function (that, result, index) { - var newRow = {}; - for (var i = 0; i < that.options.groupByField.length; i++) { - newRow[that.options.groupByField[i].toString()] = result[index][0][that.options.groupByField[i]]; - } - - newRow.IsParent = true; - - return newRow; - }; - - var groupBy = function (array, f) { - var groups = {}; - $.each(array, function (i, o) { - var group = JSON.stringify(f(o)); - groups[group] = groups[group] || []; - groups[group].push(o); - }); - return Object.keys(groups).map(function (group) { - return groups[group]; - }); - }; - - var makeGrouped = function (that, data) { - var newData = [], - sumRow = {}; - - var result = groupBy(data, function (item) { - return getDataArrayFromItem(that, item); - }); - - for (var i = 0; i < result.length; i++) { - result[i].unshift(getNewRow(that, result, i)); - if (that.options.groupBySumGroup) { - sumRow = sumData(that, result[i]); - if (!$.isEmptyObject(sumRow)) { - result[i].push(sumRow); - } - } - } - - newData = newData.concat.apply(newData, result); - - if (!that.options.loaded && newData.length > 0) { - that.options.loaded = true; - that.options.originalData = that.options.data; - that.options.data = newData; - } - - return newData; - }; - - $.extend($.fn.bootstrapTable.defaults, { - groupBy: false, - groupByField: [], - groupBySumGroup: false, - groupByInitExpanded: undefined, //node, 'all' - //internal variables - loaded: false, - originalData: undefined - }); - - $.fn.bootstrapTable.methods.push('collapseAll', 'expandAll', 'refreshGroupByField'); - - $.extend($.fn.bootstrapTable.COLUMN_DEFAULTS, { - groupBySumGroup: false - }); - - var BootstrapTable = $.fn.bootstrapTable.Constructor, - _init = BootstrapTable.prototype.init, - _initData = BootstrapTable.prototype.initData; - - BootstrapTable.prototype.init = function () { - //Temporal validation - if (!this.options.sortName) { - if ((this.options.groupBy) && (this.options.groupByField.length > 0)) { - var that = this; - - // Compatibility: IE < 9 and old browsers - if (!Object.keys) { - $.fn.bootstrapTable.utils.objectKeys(); - } - - //Make sure that the internal variables are set correctly - this.options.loaded = false; - this.options.originalData = undefined; - - originalRowAttr = this.options.rowAttributes; - this.options.rowAttributes = rowAttr; - this.$el.off('post-body.bs.table').on('post-body.bs.table', function () { - that.$el.treetable({ - expandable: true, - onNodeExpand: function () { - if (that.options.height) { - that.resetHeader(); - } - }, - onNodeCollapse: function () { - if (that.options.height) { - that.resetHeader(); - } - } - }, true); - - if (that.options.groupByInitExpanded !== undefined) { - if (typeof that.options.groupByInitExpanded === 'number') { - that.expandNode(that.options.groupByInitExpanded); - } else if (that.options.groupByInitExpanded.toLowerCase() === 'all') { - that.expandAll(); - } - } - }); - } - } - _init.apply(this, Array.prototype.slice.apply(arguments)); - }; - - BootstrapTable.prototype.initData = function (data, type) { - //Temporal validation - if (!this.options.sortName) { - if ((this.options.groupBy) && (this.options.groupByField.length > 0)) { - - this.options.groupByField = typeof this.options.groupByField === 'string' ? - this.options.groupByField.replace('[', '').replace(']', '') - .replace(/ /g, '').toLowerCase().split(',') : this.options.groupByField; - - data = makeGrouped(this, data ? data : this.options.data); - } - } - _initData.apply(this, [data, type]); - }; - - BootstrapTable.prototype.expandAll = function () { - this.$el.treetable('expandAll'); - }; - - BootstrapTable.prototype.collapseAll = function () { - this.$el.treetable('collapseAll'); - }; - - BootstrapTable.prototype.expandNode = function (id) { - id = getParentRowId(this, id); - if (id !== undefined) { - this.$el.treetable('expandNode', id); - } - }; - - BootstrapTable.prototype.refreshGroupByField = function (groupByFields) { - if (!$.fn.bootstrapTable.utils.compareObjects(this.options.groupByField, groupByFields)) { - this.options.groupByField = groupByFields; - this.load(this.options.originalData); - } - }; -}(jQuery); diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/group-by/extension.json b/InvenTree/InvenTree/static/bootstrap-table/extensions/group-by/extension.json deleted file mode 100644 index 1d6d838808..0000000000 --- a/InvenTree/InvenTree/static/bootstrap-table/extensions/group-by/extension.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "Group By", - "version": "1.1.0", - "description": "Plugin to group the data by fields.", - "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/group-by", - "example": "#", - - "plugins": [{ - "name": "bootstrap-table-group-by", - "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/group-by" - }], - - "author": { - "name": "djhvscf", - "image": "https://avatars1.githubusercontent.com/u/4496763" - } -} \ No newline at end of file diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/i18n-enhance/extension.json b/InvenTree/InvenTree/static/bootstrap-table/extensions/i18n-enhance/extension.json deleted file mode 100644 index a2b018029c..0000000000 --- a/InvenTree/InvenTree/static/bootstrap-table/extensions/i18n-enhance/extension.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "i18n Enhance", - "version": "1.0.0", - "description": "Plugin to add i18n API in order to change column's title and table locale.", - "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/i18n-enhance", - "example": "http://issues.wenzhixin.net.cn/bootstrap-table/#extensions/i18n-enhance.html", - - "plugins": [{ - "name": "bootstrap-table-i18n-enhance", - "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/i18n-enhance" - }], - - "author": { - "name": "Jewway", - "image": "https://avatars0.githubusercontent.com/u/3501899" - } -} \ No newline at end of file diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/key-events/extension.json b/InvenTree/InvenTree/static/bootstrap-table/extensions/key-events/extension.json deleted file mode 100644 index 966f6f8f29..0000000000 --- a/InvenTree/InvenTree/static/bootstrap-table/extensions/key-events/extension.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "Key Events", - "version": "1.0.0", - "description": "Plugin to support the key events in the bootstrap table.", - "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/key-events", - "example": "http://issues.wenzhixin.net.cn/bootstrap-table/#extensions/key-events.html", - - "plugins": [{ - "name": "bootstrap-table-key-events", - "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/key-events" - }], - - "author": { - "name": "djhvscf", - "image": "https://avatars1.githubusercontent.com/u/4496763" - } -} \ No newline at end of file diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/mobile/extension.json b/InvenTree/InvenTree/static/bootstrap-table/extensions/mobile/extension.json deleted file mode 100644 index 433eb77fe9..0000000000 --- a/InvenTree/InvenTree/static/bootstrap-table/extensions/mobile/extension.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "Mobile", - "version": "1.1.0", - "description": "Plugin to support the responsive feature.", - "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/mobile", - "example": "http://issues.wenzhixin.net.cn/bootstrap-table/#extensions/mobile.html", - - "plugins": [{ - "name": "bootstrap-table-mobile", - "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/mobile" - }], - - "author": { - "name": "djhvscf", - "image": "https://avatars1.githubusercontent.com/u/4496763" - } -} \ No newline at end of file diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/multi-column-toggle/bootstrap-table-multi-toggle.js b/InvenTree/InvenTree/static/bootstrap-table/extensions/multi-column-toggle/bootstrap-table-multi-toggle.js deleted file mode 100644 index 4cb110bd89..0000000000 --- a/InvenTree/InvenTree/static/bootstrap-table/extensions/multi-column-toggle/bootstrap-table-multi-toggle.js +++ /dev/null @@ -1,88 +0,0 @@ -/** - * @author Homer Glascock - * @version: v1.0.0 - */ - - !function ($) { - "use strict"; - - var sprintf = $.fn.bootstrapTable.utils.sprintf; - - var reInit = function (self) { - self.initHeader(); - self.initSearch(); - self.initPagination(); - self.initBody(); - }; - - $.extend($.fn.bootstrapTable.defaults, { - showToggleBtn: false, - multiToggleDefaults: [], //column names go here - }); - - $.fn.bootstrapTable.methods.push('hideAllColumns', 'showAllColumns'); - - var BootstrapTable = $.fn.bootstrapTable.Constructor, - _initToolbar = BootstrapTable.prototype.initToolbar; - - BootstrapTable.prototype.initToolbar = function () { - - _initToolbar.apply(this, Array.prototype.slice.apply(arguments)); - - var that = this, - $btnGroup = this.$toolbar.find('>.btn-group'); - - if (typeof this.options.multiToggleDefaults === 'string') { - this.options.multiToggleDefaults = JSON.parse(this.options.multiToggleDefaults); - } - - if (this.options.showToggleBtn && this.options.showColumns) { - var showbtn = "", - hidebtn = ""; - - $btnGroup.append(showbtn + hidebtn); - - $btnGroup.find('#showAllBtn').click(function () { that.showAllColumns(); - $btnGroup.find('#hideAllBtn').toggleClass('hidden'); - $btnGroup.find('#showAllBtn').toggleClass('hidden'); - }); - $btnGroup.find('#hideAllBtn').click(function () { that.hideAllColumns(); - $btnGroup.find('#hideAllBtn').toggleClass('hidden'); - $btnGroup.find('#showAllBtn').toggleClass('hidden'); - }); - } - }; - - BootstrapTable.prototype.hideAllColumns = function () { - var that = this, - defaults = that.options.multiToggleDefaults; - - $.each(this.columns, function (index, column) { - //if its one of the defaults dont touch it - if (defaults.indexOf(column.field) == -1 && column.switchable) { - column.visible = false; - var $items = that.$toolbar.find('.keep-open input').prop('disabled', false); - $items.filter(sprintf('[value="%s"]', index)).prop('checked', false); - } - }); - - reInit(that); - }; - - BootstrapTable.prototype.showAllColumns = function () { - var that = this; - $.each(this.columns, function (index, column) { - if (column.switchable) { - column.visible = true; - } - - var $items = that.$toolbar.find('.keep-open input').prop('disabled', false); - $items.filter(sprintf('[value="%s"]', index)).prop('checked', true); - }); - - reInit(that); - - that.toggleColumn(0, that.columns[0].visible, false); - }; - -}(jQuery); \ No newline at end of file diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/multi-column-toggle/extension.json b/InvenTree/InvenTree/static/bootstrap-table/extensions/multi-column-toggle/extension.json deleted file mode 100644 index e27efdc881..0000000000 --- a/InvenTree/InvenTree/static/bootstrap-table/extensions/multi-column-toggle/extension.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "Multi Column Toggle", - "version": "1.0.0", - "description": "Allows hiding and showing of multiple columns at once.", - "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/multi-column-toggle", - "example": "http://issues.wenzhixin.net.cn/bootstrap-table/#extensions/multi-column-toggle.html", - - "plugins": [{ - "name": "multi-column-toggle", - "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/multi-column-toggle" - }], - - "author": { - "name": "Homer Glascock", - "image": "https://avatars1.githubusercontent.com/u/5546710" - } -} diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/multiple-search/bootstrap-table-multiple-search.js b/InvenTree/InvenTree/static/bootstrap-table/extensions/multiple-search/bootstrap-table-multiple-search.js deleted file mode 100644 index a8e264e41f..0000000000 --- a/InvenTree/InvenTree/static/bootstrap-table/extensions/multiple-search/bootstrap-table-multiple-search.js +++ /dev/null @@ -1,71 +0,0 @@ -/** - * @author: Dennis Hernández - * @webSite: http://djhvscf.github.io/Blog - * @version: v1.0.0 - */ - -!function ($) { - - 'use strict'; - - $.extend($.fn.bootstrapTable.defaults, { - multipleSearch: false, - delimeter: " " - }); - - var BootstrapTable = $.fn.bootstrapTable.Constructor, - _initSearch = BootstrapTable.prototype.initSearch; - - BootstrapTable.prototype.initSearch = function () { - if (this.options.multipleSearch) { - if (this.searchText === undefined) { - return; - } - var strArray = this.searchText.split(this.options.delimeter), - that = this, - f = $.isEmptyObject(this.filterColumns) ? null : this.filterColumns, - dataFiltered = []; - - if (strArray.length === 1) { - _initSearch.apply(this, Array.prototype.slice.apply(arguments)); - } else { - for (var i = 0; i < strArray.length; i++) { - var str = strArray[i].trim(); - dataFiltered = str ? $.grep(dataFiltered.length === 0 ? this.options.data : dataFiltered, function (item, i) { - for (var key in item) { - key = $.isNumeric(key) ? parseInt(key, 10) : key; - var value = item[key], - column = that.columns[that.fieldsColumnsIndex[key]], - j = $.inArray(key, that.header.fields); - - // Fix #142: search use formated data - if (column && column.searchFormatter) { - value = $.fn.bootstrapTable.utils.calculateObjectValue(column, - that.header.formatters[j], [value, item, i], value); - } - - var index = $.inArray(key, that.header.fields); - if (index !== -1 && that.header.searchables[index] && (typeof value === 'string' || typeof value === 'number')) { - if (that.options.strictSearch) { - if ((value + '').toLowerCase() === str) { - return true; - } - } else { - if ((value + '').toLowerCase().indexOf(str) !== -1) { - return true; - } - } - } - } - return false; - }) : this.data; - } - - this.data = dataFiltered; - } - } else { - _initSearch.apply(this, Array.prototype.slice.apply(arguments)); - } - }; - -}(jQuery); diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/multiple-search/extension.json b/InvenTree/InvenTree/static/bootstrap-table/extensions/multiple-search/extension.json deleted file mode 100644 index 5160d1a98b..0000000000 --- a/InvenTree/InvenTree/static/bootstrap-table/extensions/multiple-search/extension.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "Multiple Search", - "version": "1.0.0", - "description": "Plugin to support the multiple search.", - "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/multiple-search", - "example": "#", - - "plugins": [{ - "name": "bootstrap-table-multiple-search", - "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/multiple-search" - }], - - "author": { - "name": "djhvscf", - "image": "https://avatars1.githubusercontent.com/u/4496763" - } -} \ No newline at end of file diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/multiple-selection-row/bootstrap-table-multiple-selection-row.css b/InvenTree/InvenTree/static/bootstrap-table/extensions/multiple-selection-row/bootstrap-table-multiple-selection-row.css deleted file mode 100644 index 81da76b5f3..0000000000 --- a/InvenTree/InvenTree/static/bootstrap-table/extensions/multiple-selection-row/bootstrap-table-multiple-selection-row.css +++ /dev/null @@ -1,17 +0,0 @@ -.multiple-select-row-selected { - background: lightBlue -} - -.table tbody tr:hover td, -.table tbody tr:hover th { - background-color: transparent; -} - - -.table-striped tbody tr:nth-child(odd):hover td { - background-color: #F9F9F9; -} - -.fixed-table-container tbody .selected td { - background: lightBlue; -} \ No newline at end of file diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/multiple-selection-row/bootstrap-table-multiple-selection-row.js b/InvenTree/InvenTree/static/bootstrap-table/extensions/multiple-selection-row/bootstrap-table-multiple-selection-row.js deleted file mode 100644 index 597b28b8f7..0000000000 --- a/InvenTree/InvenTree/static/bootstrap-table/extensions/multiple-selection-row/bootstrap-table-multiple-selection-row.js +++ /dev/null @@ -1,127 +0,0 @@ -/** - * @author: Dennis Hernández - * @webSite: http://djhvscf.github.io/Blog - * @version: v1.0.0 - */ - -!function ($) { - - 'use strict'; - - document.onselectstart = function() { - return false; - }; - - var getTableObjectFromCurrentTarget = function (currentTarget) { - currentTarget = $(currentTarget); - return currentTarget.is("table") ? currentTarget : currentTarget.parents().find(".table"); - }; - - var getRow = function (target) { - target = $(target); - return target.parent().parent(); - }; - - var onRowClick = function (e) { - var that = getTableObjectFromCurrentTarget(e.currentTarget); - - if (window.event.ctrlKey) { - toggleRow(e.currentTarget, that, false, false); - } - - if (window.event.button === 0) { - if (!window.event.ctrlKey && !window.event.shiftKey) { - clearAll(that); - toggleRow(e.currentTarget, that, false, false); - } - - if (window.event.shiftKey) { - selectRowsBetweenIndexes([that.bootstrapTable("getOptions").multipleSelectRowLastSelectedRow.rowIndex, e.currentTarget.rowIndex], that) - } - } - }; - - var onCheckboxChange = function (e) { - var that = getTableObjectFromCurrentTarget(e.currentTarget); - clearAll(that); - toggleRow(getRow(e.currentTarget), that, false, false); - }; - - var toggleRow = function (row, that, clearAll, useShift) { - if (clearAll) { - row = $(row); - that.bootstrapTable("getOptions").multipleSelectRowLastSelectedRow = undefined; - row.removeClass(that.bootstrapTable("getOptions").multipleSelectRowCssClass); - that.bootstrapTable("uncheck", row.data("index")); - } else { - that.bootstrapTable("getOptions").multipleSelectRowLastSelectedRow = row; - row = $(row); - if (useShift) { - row.addClass(that.bootstrapTable("getOptions").multipleSelectRowCssClass); - that.bootstrapTable("check", row.data("index")); - } else { - if(row.hasClass(that.bootstrapTable("getOptions").multipleSelectRowCssClass)) { - row.removeClass(that.bootstrapTable("getOptions").multipleSelectRowCssClass) - that.bootstrapTable("uncheck", row.data("index")); - } else { - row.addClass(that.bootstrapTable("getOptions").multipleSelectRowCssClass); - that.bootstrapTable("check", row.data("index")); - } - } - } - }; - - var selectRowsBetweenIndexes = function (indexes, that) { - indexes.sort(function(a, b) { - return a - b; - }); - - for (var i = indexes[0]; i <= indexes[1]; i++) { - toggleRow(that.bootstrapTable("getOptions").multipleSelectRowRows[i-1], that, false, true); - } - }; - - var clearAll = function (that) { - for (var i = 0; i < that.bootstrapTable("getOptions").multipleSelectRowRows.length; i++) { - toggleRow(that.bootstrapTable("getOptions").multipleSelectRowRows[i], that, true, false); - } - }; - - $.extend($.fn.bootstrapTable.defaults, { - multipleSelectRow: false, - multipleSelectRowCssClass: 'multiple-select-row-selected', - //internal variables used by the extension - multipleSelectRowLastSelectedRow: undefined, - multipleSelectRowRows: [] - }); - - var BootstrapTable = $.fn.bootstrapTable.Constructor, - _init = BootstrapTable.prototype.init, - _initBody = BootstrapTable.prototype.initBody; - - BootstrapTable.prototype.init = function () { - if (this.options.multipleSelectRow) { - var that = this; - - //Make sure that the internal variables have the correct value - this.options.multipleSelectRowLastSelectedRow = undefined; - this.options.multipleSelectRowRows = []; - - this.$el.on("post-body.bs.table", function (e) { - setTimeout(function () { - that.options.multipleSelectRowRows = that.$body.children(); - that.options.multipleSelectRowRows.click(onRowClick); - that.options.multipleSelectRowRows.find("input[type=checkbox]").change(onCheckboxChange); - }, 1); - }); - } - - _init.apply(this, Array.prototype.slice.apply(arguments)); - }; - - BootstrapTable.prototype.clearAllMultipleSelectionRow = function () { - clearAll(this); - }; - - $.fn.bootstrapTable.methods.push('clearAllMultipleSelectionRow'); -}(jQuery); diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/multiple-selection-row/extension.json b/InvenTree/InvenTree/static/bootstrap-table/extensions/multiple-selection-row/extension.json deleted file mode 100644 index 69d4a9effa..0000000000 --- a/InvenTree/InvenTree/static/bootstrap-table/extensions/multiple-selection-row/extension.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "Multiple Selection Row", - "version": "1.0.0", - "description": "Plugin to enable the multiple selection row. You can use the ctrl+click to select one row or use ctrl+shift+click to select a range of rows.", - "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/multiple-selection-row", - "example": "", - - "plugins": [{ - "name": "bootstrap-table-multiple-selection-row", - "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/multiple-selection-row" - }], - - "author": { - "name": "djhvscf", - "image": "https://avatars1.githubusercontent.com/u/4496763" - } -} \ No newline at end of file diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/multiple-sort/extension.json b/InvenTree/InvenTree/static/bootstrap-table/extensions/multiple-sort/extension.json deleted file mode 100644 index 580082a26b..0000000000 --- a/InvenTree/InvenTree/static/bootstrap-table/extensions/multiple-sort/extension.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "Multiple Sort", - "version": "1.1.0", - "description": "Plugin to support the multiple sort.", - "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/multiple-sort", - "example": "#", - - "plugins": [{ - "name": "bootstrap-table-multiple-sort", - "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/multiple-sort" - }], - - "author": { - "name": "dimbslmh", - "image": "https://avatars1.githubusercontent.com/u/745635" - } -} diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/natural-sorting/bootstrap-table-natural-sorting.js b/InvenTree/InvenTree/static/bootstrap-table/extensions/natural-sorting/bootstrap-table-natural-sorting.js deleted file mode 100644 index 30e6521760..0000000000 --- a/InvenTree/InvenTree/static/bootstrap-table/extensions/natural-sorting/bootstrap-table-natural-sorting.js +++ /dev/null @@ -1,67 +0,0 @@ -/** - * @author: Brian Huisman - * @webSite: http://www.greywyvern.com - * @version: v1.0.0 - * JS functions to allow natural sorting on bootstrap-table columns - * add data-sorter="alphanum" or data-sorter="numericOnly" to any th - * - * @update Dennis Hernández - * @update Duane May - */ - -function alphanum(a, b) { - function chunkify(t) { - var tz = [], - x = 0, - y = -1, - n = 0, - i, - j; - - while (i = (j = t.charAt(x++)).charCodeAt(0)) { - var m = (i === 46 || (i >= 48 && i <= 57)); - if (m !== n) { - tz[++y] = ""; - n = m; - } - tz[y] += j; - } - return tz; - } - - function stringfy(v) { - if (typeof(v) === "number") { - v = "" + v; - } - if (!v) { - v = ""; - } - return v; - } - - var aa = chunkify(stringfy(a)); - var bb = chunkify(stringfy(b)); - - for (x = 0; aa[x] && bb[x]; x++) { - if (aa[x] !== bb[x]) { - var c = Number(aa[x]), - d = Number(bb[x]); - - if (c == aa[x] && d == bb[x]) { - return c - d; - } else { - return (aa[x] > bb[x]) ? 1 : -1; - } - } - } - return aa.length - bb.length; -} - -function numericOnly(a, b) { - function stripNonNumber(s) { - s = s.replace(new RegExp(/[^0-9]/g), ""); - return parseInt(s, 10); - } - - return stripNonNumber(a) - stripNonNumber(b); -} \ No newline at end of file diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/natural-sorting/extension.json b/InvenTree/InvenTree/static/bootstrap-table/extensions/natural-sorting/extension.json deleted file mode 100644 index 06bf4e4ece..0000000000 --- a/InvenTree/InvenTree/static/bootstrap-table/extensions/natural-sorting/extension.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "Natural Sorting", - "version": "1.0.0", - "description": "Plugin to support the natural sorting.", - "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/natural-sorting", - "example": "#", - - "plugins": [{ - "name": "bootstrap-table-natural-sorting", - "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/natural-sorting" - }], - - "author": { - "name": "GreyWyvern", - "image": "https://avatars1.githubusercontent.com/u/137631" - } -} \ No newline at end of file diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/pipeline/LICENSE b/InvenTree/InvenTree/static/bootstrap-table/extensions/pipeline/LICENSE deleted file mode 100644 index 608dadfb85..0000000000 --- a/InvenTree/InvenTree/static/bootstrap-table/extensions/pipeline/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -(The MIT License) - -Copyright (c) 2019 doug-the-guy - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/pipeline/README.md b/InvenTree/InvenTree/static/bootstrap-table/extensions/pipeline/README.md deleted file mode 100644 index c5019431f2..0000000000 --- a/InvenTree/InvenTree/static/bootstrap-table/extensions/pipeline/README.md +++ /dev/null @@ -1,92 +0,0 @@ -# Bootstrap Table Pipelining - -Use Plugin: [bootstrap-table-pipeline] - -This plugin enables client side data caching for server side requests which will -eliminate the need to issue a new request every page change. This will allow -for a performance balance for a large data set between returning all data at once -(client side paging) and a new server side request (server side paging). - -There are two new options: -- usePipeline: enables this feature -- pipelineSize: the size of each cache window - -The size of the pipeline must be evenly divisible by the current page size. This is -assured by rounding up to the nearest evenly divisible value. For example, if -the pipeline size is 4990 and the current page size is 25, then pipeline size will -be dynamically set to 5000. - -The cache windows are computed based on the pipeline size and the total number of rows -returned by the server side query. For example, with pipeline size 500 and total rows -1300, the cache windows will be: - -[{'lower': 0, 'upper': 499}, {'lower': 500, 'upper': 999}, {'lower': 1000, 'upper': 1499}] - -Using the limit (i.e. the pipelineSize) and offset parameters, the server side request -**MUST** return only the data in the requested cache window **AND** the total number of rows. -To wit, the server side code must use the offset and limit parameters to prepare the response -data. - -On a page change, the new offset is checked if it is within the current cache window. If so, -the requested page data is returned from the cached data set. Otherwise, a new server side -request will be issued for the new cache window. - -The current cached data is only invalidated on these events: - - sorting - - searching - - page size change - - page change moves into a new cache window - -There are two new events: -- cached-data-hit.bs.table: issued when cached data is used on a page change -- cached-data-reset.bs.table: issued when the cached data is invalidated and new server side request is issued - -## Features - -* Created with Bootstrap 4 - -## Usage - -``` -# assumed import of bootstrap and bootstrap-table assets - -... - - - - - - -
TypeValueDate
-``` - -## Options - -### usePipeline - -* type: Boolean -* description: Set true to enable pipelining -* default: `false` - -## pipelineSize - -* type: Integer -* description: Size of each cache window. Must be greater than 0 -* default: `1000` - -## Events - -### onCachedDataHit(cached-data-hit.bs.table) - -* Fires when paging was able to use the locally cached data. - -### onCachedDataReset(cached-data-reset.bs.table) - -* Fires when the locally cached data needed to be reset (i.e. on sorting, searching, page size change or paged out of current cache window) diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/pipeline/extension.json b/InvenTree/InvenTree/static/bootstrap-table/extensions/pipeline/extension.json deleted file mode 100644 index de569c848c..0000000000 --- a/InvenTree/InvenTree/static/bootstrap-table/extensions/pipeline/extension.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "Pipeline", - "version": "1.0.0", - "description": "Plugin to support a hybrid approach to server/client side paging.", - "url": "", - "example": "#", - - "plugins": [{ - "name": "bootstrap-table-pipeline", - "url": "" - }], - - "author": { - "name": "doug-the-guy", - "image": "" - } -} - diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/reorder-columns/extension.json b/InvenTree/InvenTree/static/bootstrap-table/extensions/reorder-columns/extension.json deleted file mode 100644 index a3cc778318..0000000000 --- a/InvenTree/InvenTree/static/bootstrap-table/extensions/reorder-columns/extension.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "Reorder Columns", - "version": "1.1.0", - "description": "Plugin to support the reordering columns feature.", - "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/reorder-columns", - "example": "http://issues.wenzhixin.net.cn/bootstrap-table/#extensions/reorder-columns.html", - - "plugins": [{ - "name": "bootstrap-table-reorder-columns", - "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/reorder-columns" - }], - - "author": { - "name": "djhvscf", - "image": "https://avatars1.githubusercontent.com/u/4496763" - } -} \ No newline at end of file diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/reorder-rows/extension.json b/InvenTree/InvenTree/static/bootstrap-table/extensions/reorder-rows/extension.json deleted file mode 100644 index 69b978fb41..0000000000 --- a/InvenTree/InvenTree/static/bootstrap-table/extensions/reorder-rows/extension.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "Reorder Rows", - "version": "1.0.0", - "description": "Plugin to support the reordering rows feature.", - "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/reorder-rows", - "example": "http://issues.wenzhixin.net.cn/bootstrap-table/#extensions/reorder-rows.html", - - "plugins": [{ - "name": "bootstrap-table-reorder-rows", - "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/reorder-rows" - }], - - "author": { - "name": "djhvscf", - "image": "https://avatars1.githubusercontent.com/u/4496763" - } -} \ No newline at end of file diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/resizable/extension.json b/InvenTree/InvenTree/static/bootstrap-table/extensions/resizable/extension.json deleted file mode 100644 index f428c12beb..0000000000 --- a/InvenTree/InvenTree/static/bootstrap-table/extensions/resizable/extension.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "Resizable", - "version": "1.1.0", - "description": "Plugin to support the resizable feature.", - "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/resizable", - "example": "http://issues.wenzhixin.net.cn/bootstrap-table/#extensions/resizable.html", - - "plugins": [{ - "name": "bootstrap-table-resizable", - "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/resizable" - }], - - "author": { - "name": "djhvscf", - "image": "https://avatars1.githubusercontent.com/u/4496763" - } -} diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/select2-filter/bootstrap-table-select2-filter.js b/InvenTree/InvenTree/static/bootstrap-table/extensions/select2-filter/bootstrap-table-select2-filter.js deleted file mode 100644 index 6bfa915c21..0000000000 --- a/InvenTree/InvenTree/static/bootstrap-table/extensions/select2-filter/bootstrap-table-select2-filter.js +++ /dev/null @@ -1,332 +0,0 @@ -/** - * @author: Jewway - * @version: v1.1.1 - */ - -! function ($) { - 'use strict'; - - function getCurrentHeader(that) { - var header = that.$header; - if (that.options.height) { - header = that.$tableHeader; - } - - return header; - } - - function initFilterValues(that) { - if (!$.isEmptyObject(that.filterColumnsPartial)) { - var $header = getCurrentHeader(that); - - $.each(that.columns, function (idx, column) { - var value = that.filterColumnsPartial[column.field]; - - if (column.filter) { - if (column.filter.setFilterValue) { - var $filter = $header.find('[data-field=' + column.field + '] .filter'); - column.filter.setFilterValue($filter, column.field, value); - } else { - var $ele = $header.find('[data-filter-field=' + column.field + ']'); - switch (column.filter.type) { - case 'input': - $ele.val(value); - case 'select': - $ele.val(value).trigger('change'); - } - } - } - }); - } - } - - function createFilter(that, header) { - var enableFilter = false, - isVisible, - html, - timeoutId = 0; - - $.each(that.columns, function (i, column) { - isVisible = 'hidden'; - html = null; - - if (!column.visible) { - return; - } - - if (!column.filter) { - html = $('
'); - } else { - var filterClass = column.filter.class ? ' ' + column.filter.class : ''; - html = $('
'); - - if (column.searchable) { - enableFilter = true; - isVisible = 'visible' - } - - if (column.filter.template) { - html.append(column.filter.template(that, column, isVisible)); - } else { - var $filter = $(that.options.filterTemplate[column.filter.type.toLowerCase()](that, column, isVisible)); - - switch (column.filter.type) { - case 'input': - var cpLock = true; - $filter.off('compositionstart').on('compositionstart', function (event) { - cpLock = false; - }); - - $filter.off('compositionend').on('compositionend', function (event) { - cpLock = true; - var $input = $(this); - clearTimeout(timeoutId); - timeoutId = setTimeout(function () { - that.onColumnSearch(event, column.field, $input.val()); - }, that.options.searchTimeOut); - }); - - $filter.off('keyup').on('keyup', function (event) { - if (cpLock) { - var $input = $(this); - clearTimeout(timeoutId); - timeoutId = setTimeout(function () { - that.onColumnSearch(event, column.field, $input.val()); - }, that.options.searchTimeOut); - } - }); - - $filter.off('mouseup').on('mouseup', function (event) { - var $input = $(this), - oldValue = $input.val(); - - if (oldValue === "") { - return; - } - - setTimeout(function () { - var newValue = $input.val(); - - if (newValue === "") { - clearTimeout(timeoutId); - timeoutId = setTimeout(function () { - that.onColumnSearch(event, column.field, newValue); - }, that.options.searchTimeOut); - } - }, 1); - }); - break; - case 'select': - $filter.on('select2:select', function (event) { - that.onColumnSearch(event, column.field, $(this).val()); - }); - - $filter.on("select2:unselecting", function (event) { - var $select2 = $(this); - event.preventDefault(); - $select2.val(null).trigger('change'); - that.searchText = undefined; - that.onColumnSearch(event, column.field, $select2.val()); - }); - break; - } - - html.append($filter); - } - } - - $.each(header.children().children(), function (i, tr) { - tr = $(tr); - if (tr.data('field') === column.field) { - tr.find('.fht-cell').append(html); - return false; - } - }); - }); - - if (!enableFilter) { - header.find('.filter').hide(); - } - } - - function initSelect2(that) { - var $header = getCurrentHeader(that); - - $.each(that.columns, function (idx, column) { - if (column.filter && column.filter.type === 'select') { - var $selectEle = $header.find('select[data-filter-field="' + column.field + '"]'); - - if ($selectEle.length > 0 && !$selectEle.data().select2) { - var select2Opts = { - placeholder: "", - allowClear: true, - data: column.filter.data, - dropdownParent: that.$el.closest(".bootstrap-table") - }; - - $selectEle.select2(select2Opts); - } - } - }); - } - - $.extend($.fn.bootstrapTable.defaults, { - filter: false, - filterValues: {}, - filterTemplate: { - input: function (instance, column, isVisible) { - return ''; - }, - select: function (instance, column, isVisible) { - return ''; - } - }, - onColumnSearch: function (field, text) { - return false; - } - }); - - $.extend($.fn.bootstrapTable.COLUMN_DEFAULTS, { - filter: undefined - }); - - $.extend($.fn.bootstrapTable.Constructor.EVENTS, { - 'column-search.bs.table': 'onColumnSearch' - }); - - var BootstrapTable = $.fn.bootstrapTable.Constructor, - _init = BootstrapTable.prototype.init, - _initHeader = BootstrapTable.prototype.initHeader, - _initSearch = BootstrapTable.prototype.initSearch; - - BootstrapTable.prototype.init = function () { - //Make sure that the filtercontrol option is set - if (this.options.filter) { - var that = this; - - if (that.options.filterTemplate) { - that.options.filterTemplate = $.extend({}, $.fn.bootstrapTable.defaults.filterTemplate, that.options.filterTemplate); - } - - if (!$.isEmptyObject(that.options.filterValues)) { - that.filterColumnsPartial = that.options.filterValues; - that.options.filterValues = {}; - } - - this.$el.on('reset-view.bs.table', function () { - //Create controls on $tableHeader if the height is set - if (!that.options.height) { - return; - } - - //Avoid recreate the controls - if (that.$tableHeader.find('select').length > 0 || that.$tableHeader.find('input').length > 0) { - return; - } - - createFilter(that, that.$tableHeader); - }).on('post-header.bs.table', function () { - var timeoutId = 0; - - initSelect2(that); - clearTimeout(timeoutId); - timeoutId = setTimeout(function () { - initFilterValues(that); - }, that.options.searchTimeOut - 1000); - }).on('column-switch.bs.table', function (field, checked) { - initFilterValues(that); - }); - } - - _init.apply(this, Array.prototype.slice.apply(arguments)); - }; - - BootstrapTable.prototype.initHeader = function () { - _initHeader.apply(this, Array.prototype.slice.apply(arguments)); - if (this.options.filter) { - createFilter(this, this.$header); - } - }; - - BootstrapTable.prototype.initSearch = function () { - var that = this, - filterValues = that.filterColumnsPartial; - - // Filter for client - if (that.options.sidePagination === 'client') { - this.data = $.grep(this.data, function (row, idx) { - for (var field in filterValues) { - var column = that.columns[that.fieldsColumnsIndex[field]], - filterValue = filterValues[field].toLowerCase(), - rowValue = row[field]; - - rowValue = $.fn.bootstrapTable.utils.calculateObjectValue( - that.header, - that.header.formatters[$.inArray(field, that.header.fields)], [rowValue, row, idx], rowValue); - - if (column.filterStrictSearch) { - if (!($.inArray(field, that.header.fields) !== -1 && - (typeof rowValue === 'string' || typeof rowValue === 'number') && - rowValue.toString().toLowerCase() === filterValue.toString().toLowerCase())) { - return false; - } - } else { - if (!($.inArray(field, that.header.fields) !== -1 && - (typeof rowValue === 'string' || typeof rowValue === 'number') && - (rowValue + '').toLowerCase().indexOf(filterValue) !== -1)) { - return false; - } - } - } - - return true; - }); - } - - _initSearch.apply(this, Array.prototype.slice.apply(arguments)); - }; - - BootstrapTable.prototype.onColumnSearch = function (event, field, value) { - if ($.isEmptyObject(this.filterColumnsPartial)) { - this.filterColumnsPartial = {}; - } - - if (value) { - this.filterColumnsPartial[field] = value; - } else { - delete this.filterColumnsPartial[field]; - } - - this.options.pageNumber = 1; - this.onSearch(event); - this.trigger('column-search', field, value); - }; - - BootstrapTable.prototype.setSelect2Data = function (field, data) { - var that = this, - $header = getCurrentHeader(that), - $selectEle = $header.find('select[data-filter-field=\"' + field + '\"]'); - $selectEle.empty(); - $selectEle.select2({ - data: data, - placeholder: "", - allowClear: true, - dropdownParent: that.$el.closest(".bootstrap-table") - }); - - $.each(this.columns, function (idx, column) { - if (column.field === field) { - column.filter.data = data; - return false; - } - }); - }; - - BootstrapTable.prototype.setFilterValues = function (values) { - this.filterColumnsPartial = values; - }; - - $.fn.bootstrapTable.methods.push('setSelect2Data'); - $.fn.bootstrapTable.methods.push('setFilterValues'); - -}(jQuery); diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/select2-filter/extension.json b/InvenTree/InvenTree/static/bootstrap-table/extensions/select2-filter/extension.json deleted file mode 100644 index edbc589c0b..0000000000 --- a/InvenTree/InvenTree/static/bootstrap-table/extensions/select2-filter/extension.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "Select2 Filter", - "version": "1.1.0", - "description": "Plugin to add select2 filter on the top of the columns in order to filter the data.", - "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/select2-filter", - "example": "http://issues.wenzhixin.net.cn/bootstrap-table/#extensions/select2-filter.html", - - "plugins": [{ - "name": "bootstrap-table-select2-filter", - "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/select2-filter" - }], - - "author": { - "name": "Jewway", - "image": "https://avatars0.githubusercontent.com/u/3501899" - } -} \ No newline at end of file diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/sticky-header/extension.json b/InvenTree/InvenTree/static/bootstrap-table/extensions/sticky-header/extension.json deleted file mode 100644 index 70ae30ff8b..0000000000 --- a/InvenTree/InvenTree/static/bootstrap-table/extensions/sticky-header/extension.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "Sticky Header", - "version": "1.0.0", - "description": "An extension which provides a sticky header for table columns when scrolling on a long page and / or table. Works for tables with many columns and narrow width with horizontal scrollbars too.", - "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/sticky-header", - "example": "http://issues.wenzhixin.net.cn/bootstrap-table/#extensions/sticky-header.html", - - "plugins": [{ - "name": "bootstrap-table-sticky-header", - "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/sticky-header" - }], - - "author": { - "name": "vinzloh", - "image": "https://avatars0.githubusercontent.com/u/5501845" - } -} diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/toolbar/extension.json b/InvenTree/InvenTree/static/bootstrap-table/extensions/toolbar/extension.json deleted file mode 100644 index f33ae2c475..0000000000 --- a/InvenTree/InvenTree/static/bootstrap-table/extensions/toolbar/extension.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "Toolbar", - "version": "2.0.0", - "description": "Plugin to support the advanced search.", - "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/toolbar", - "example": "http://issues.wenzhixin.net.cn/bootstrap-table/#extensions/toolbar.html", - - "plugins": [{ - "name": "bootstrap-table-toolbar", - "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/toolbar" - }], - - "author": { - "name": "djhvscf", - "image": "https://avatars1.githubusercontent.com/u/4496763" - } -} \ No newline at end of file diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/tree-column/bootstrap-table-tree-column.css b/InvenTree/InvenTree/static/bootstrap-table/extensions/tree-column/bootstrap-table-tree-column.css deleted file mode 100644 index 481ca89cd8..0000000000 --- a/InvenTree/InvenTree/static/bootstrap-table/extensions/tree-column/bootstrap-table-tree-column.css +++ /dev/null @@ -1 +0,0 @@ -.table:not(.table-condensed)>tbody>tr>td.treenode{padding-top:0;padding-bottom:0;border-bottom:solid #fff 1px}.table:not(.table-condensed)>tbody>tr:last-child>td.treenode{border-bottom:none}.treenode .text{float:left;display:block;padding-top:6px;padding-bottom:6px}.treenode .vertical,.treenode .vertical.last{float:left;display:block;width:1px;border-left:dashed silver 1px;height:38px;margin-left:8px}.treenode .vertical.last{height:15px}.treenode .space,.treenode .node{float:left;display:block;width:15px;height:5px;margin-top:15px}.treenode .node{border-top:dashed silver 1px} \ No newline at end of file diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/tree-column/bootstrap-table-tree-column.js b/InvenTree/InvenTree/static/bootstrap-table/extensions/tree-column/bootstrap-table-tree-column.js deleted file mode 100644 index a8ff876d11..0000000000 --- a/InvenTree/InvenTree/static/bootstrap-table/extensions/tree-column/bootstrap-table-tree-column.js +++ /dev/null @@ -1,130 +0,0 @@ -/** - * @author: KingYang - * @webSite: https://github.com/kingyang - * @version: v1.0.0 - */ - -! function ($) { - - 'use strict'; - - $.extend($.fn.bootstrapTable.defaults, { - treeShowField: null, - idField: 'id', - parentIdField: 'pid', - treeVerticalcls: 'vertical', - treeVerticalLastcls: 'vertical last', - treeSpacecls: 'space', - treeNodecls: 'node', - treeCellcls: 'treenode', - treeTextcls: 'text', - onTreeFormatter: function (row) { - var that = this, - options = that.options, - level = row._level || 0, - plevel = row._parent && row._parent._level || 0, - paddings = []; - for (var i = 0; i < plevel; i++) { - paddings.push(''); - paddings.push(''); - } - - for (var i = plevel; i < level; i++) { - if (row._last && i === (level - 1)) { - paddings.push(''); - } else { - paddings.push(''); - } - paddings.push(''); - } - return paddings.join(''); - }, onGetNodes: function (row, data) { - var that = this; - var nodes = []; - $.each(data, function (i, item) { - if (row[that.options.idField] === item[that.options.parentIdField]) { - nodes.push(item); - } - }); - return nodes; - }, - onCheckLeaf: function (row, data) { - if (row.isLeaf !== undefined) { - return row.isLeaf; - } - return !row._nodes || !row._nodes.length; - }, onCheckRoot: function (row, data) { - var that = this; - return !row[that.options.parentIdField]; - } - }); - - var BootstrapTable = $.fn.bootstrapTable.Constructor, - _initRow = BootstrapTable.prototype.initRow, - _initHeader = BootstrapTable.prototype.initHeader; - - BootstrapTable.prototype.initHeader = function () { - var that = this; - _initHeader.apply(that, Array.prototype.slice.apply(arguments)); - var treeShowField = that.options.treeShowField; - if (treeShowField) { - $.each(this.header.fields, function (i, field) { - if (treeShowField === field) { - that.treeEnable = true; - var _formatter = that.header.formatters[i]; - var _class = [that.options.treeCellcls]; - if (that.header.classes[i]) { - _class.push(that.header.classes[i].split('"')[1] || ''); - } - that.header.classes[i] = ' class="' + _class.join(' ') + '"'; - that.header.formatters[i] = function (value, row, index) { - var colTree = [that.options.onTreeFormatter.apply(that, [row])]; - colTree.push(''); - if (_formatter) { - colTree.push(_formatter.apply(this, Array.prototype.slice.apply(arguments))); - } else { - colTree.push(value); - } - colTree.push(''); - return colTree.join(''); - }; - return false; - } - }); - } - }; - - var initNode = function (item, idx, data, parentDom) { - var that = this; - var nodes = that.options.onGetNodes.apply(that, [item, data]); - item._nodes = nodes; - parentDom.append(_initRow.apply(that, [item, idx, data, parentDom])); - var len = nodes.length - 1; - for (var i = 0; i <= len; i++) { - var node = nodes[i]; - node._level = item._level + 1; - node._parent = item; - if (i === len) - node._last = 1; - initNode.apply(that, [node, $.inArray(node, data), data, parentDom]); - } - }; - - - BootstrapTable.prototype.initRow = function (item, idx, data, parentDom) { - var that = this; - if (that.treeEnable) { - if (that.options.onCheckRoot.apply(that, [item, data])) { - if (item._level === undefined) { - item._level = 0; - } - initNode.apply(that, [item, idx, data, parentDom]); - return true; - } - return false; - - } - return _initRow.apply(that, Array.prototype.slice.apply(arguments)); - }; - -} (jQuery); diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/tree-column/bootstrap-table-tree-column.less b/InvenTree/InvenTree/static/bootstrap-table/extensions/tree-column/bootstrap-table-tree-column.less deleted file mode 100644 index fc8ceda3b9..0000000000 --- a/InvenTree/InvenTree/static/bootstrap-table/extensions/tree-column/bootstrap-table-tree-column.less +++ /dev/null @@ -1,43 +0,0 @@ - .table:not(.table-condensed) > tbody > tr > td.treenode { - padding-top: 0; - padding-bottom: 0; - border-bottom: solid #fff 1px; - } - - .table:not(.table-condensed) > tbody > tr:last-child > td.treenode { - border-bottom: none; - } - - .treenode { - .text { - float: left; - display: block; - padding-top: 6px; - padding-bottom: 6px; - } - .vertical, - .vertical.last { - float: left; - display: block; - width: 1px; - border-left: dashed silver 1px; - height: 38px; - margin-left: 8px; - } - .vertical.last { - height: 15px; - } - - .space, - .node { - float: left; - display: block; - width: 15px; - height: 5px; - margin-top: 15px; - } - - .node { - border-top: dashed silver 1px; - } - } \ No newline at end of file diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/tree-column/extension.json b/InvenTree/InvenTree/static/bootstrap-table/extensions/tree-column/extension.json deleted file mode 100644 index 72ddaa5a13..0000000000 --- a/InvenTree/InvenTree/static/bootstrap-table/extensions/tree-column/extension.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "Tree column", - "version": "1.0.0", - "description": "Plugin to support display tree data column.", - "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/tree-column", - "example": "http://issues.wenzhixin.net.cn/bootstrap-table/#extensions/tree-column.html", - - "plugins": [{ - "name": "bootstrap-table-reorder-rows", - "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/tree-column" - }], - - "author": { - "name": "KingYang", - "image": "https://avatars3.githubusercontent.com/u/1540211" - } -} \ No newline at end of file diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/tree-column/icon.png b/InvenTree/InvenTree/static/bootstrap-table/extensions/tree-column/icon.png deleted file mode 100644 index e7453922fe025d22db0ad736eb710b89b721042f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 346 zcmeAS@N?(olHy`uVBq!ia0vp^oIotZ!py+H$g}e1TOfzI*vT`5gM;JtL;nXru0eoL zh%1o3cH!8`gPSU5Y<=_c@rFZ}zkd1n@XnQ=KfYf&z5m(cJ9n;~{qyJdhj*|3|NnpX z=#I0yAH4$V;VkfoEM{Qf76M_$OLy!300qTLTq8=Hi&7IyGV}8kLNaqx84OJIjZF0o z%|15&e+X0|15y#3pH@5y-kU*?L7F-W!-efW>>xNt&GiU#kZ>jByWA9&fJ!eocx%9ZT2?p)4URA kBqSsn*>vw)$ZPR2Jl`Y3En=yu1aty}r>mdKI;Vst05Z*jEC2ui diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/treegrid/extension.json b/InvenTree/InvenTree/static/bootstrap-table/extensions/treegrid/extension.json deleted file mode 100644 index 2c07e1f487..0000000000 --- a/InvenTree/InvenTree/static/bootstrap-table/extensions/treegrid/extension.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "treegrid", - "version": "1.0.0", - "description": "Plugin to support the jquery treegrid.", - "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/treegrid", - "example": "https://github.com/wenzhixin/bootstrap-table-examples/blob/master/extensions/treegrid.html", - - "plugins": [{ - "name": "bootstrap-table-treegrid", - "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/treegrid" - }], - - "author": { - "name": "foreveryang321", - "image": "https://avatars0.githubusercontent.com/u/5868190" - } -} \ No newline at end of file diff --git a/InvenTree/InvenTree/static/bootstrap-table/themes/bootstrap-table/bootstrap-table.css b/InvenTree/InvenTree/static/bootstrap-table/themes/bootstrap-table/bootstrap-table.css new file mode 100644 index 0000000000..86ec3f217d --- /dev/null +++ b/InvenTree/InvenTree/static/bootstrap-table/themes/bootstrap-table/bootstrap-table.css @@ -0,0 +1,790 @@ +/** + * @author Dustin Utecht + * https://github.com/wenzhixin/bootstrap-table/ + */ +.bootstrap-table .fixed-table-toolbar::after { + content: ""; + display: block; + clear: both; +} + +.bootstrap-table .fixed-table-toolbar .bs-bars, +.bootstrap-table .fixed-table-toolbar .search, +.bootstrap-table .fixed-table-toolbar .columns { + position: relative; + margin-top: 10px; + margin-bottom: 10px; +} + +.bootstrap-table .fixed-table-toolbar .columns .btn-group > .btn-group { + display: inline-block; + margin-left: -1px !important; +} + +.bootstrap-table .fixed-table-toolbar .columns .btn-group > .btn-group > .btn { + border-radius: 0; +} + +.bootstrap-table .fixed-table-toolbar .columns .btn-group > .btn-group:first-child > .btn { + border-top-left-radius: 4px; + border-bottom-left-radius: 4px; +} + +.bootstrap-table .fixed-table-toolbar .columns .btn-group > .btn-group:last-child > .btn { + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; +} + +.bootstrap-table .fixed-table-toolbar .columns .dropdown-menu { + text-align: left; + max-height: 300px; + overflow: auto; + -ms-overflow-style: scrollbar; + z-index: 1001; +} + +.bootstrap-table .fixed-table-toolbar .columns label { + display: block; + padding: 3px 20px; + clear: both; + font-weight: normal; + line-height: 1.428571429; +} + +.bootstrap-table .fixed-table-toolbar .columns-left { + margin-right: 5px; +} + +.bootstrap-table .fixed-table-toolbar .columns-right { + margin-left: 5px; +} + +.bootstrap-table .fixed-table-toolbar .pull-right .dropdown-menu { + right: 0; + left: auto; +} + +.bootstrap-table .fixed-table-container { + position: relative; + clear: both; +} + +.bootstrap-table .fixed-table-container .table { + width: 100%; + margin-bottom: 0 !important; +} + +.bootstrap-table .fixed-table-container .table th, +.bootstrap-table .fixed-table-container .table td { + vertical-align: middle; + box-sizing: border-box; +} + +.bootstrap-table .fixed-table-container .table thead th { + vertical-align: bottom; + padding: 0; + margin: 0; +} + +.bootstrap-table .fixed-table-container .table thead th:focus { + outline: 0 solid transparent; +} + +.bootstrap-table .fixed-table-container .table thead th.detail { + width: 30px; +} + +.bootstrap-table .fixed-table-container .table thead th .th-inner { + padding: 0.75rem; + vertical-align: bottom; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.bootstrap-table .fixed-table-container .table thead th .sortable { + cursor: pointer; + background-position: right; + background-repeat: no-repeat; + padding-right: 30px !important; +} + +.bootstrap-table .fixed-table-container .table thead th .both { + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAQAAADYWf5HAAAAkElEQVQoz7X QMQ5AQBCF4dWQSJxC5wwax1Cq1e7BAdxD5SL+Tq/QCM1oNiJidwox0355mXnG/DrEtIQ6azioNZQxI0ykPhTQIwhCR+BmBYtlK7kLJYwWCcJA9M4qdrZrd8pPjZWPtOqdRQy320YSV17OatFC4euts6z39GYMKRPCTKY9UnPQ6P+GtMRfGtPnBCiqhAeJPmkqAAAAAElFTkSuQmCC"); +} + +.bootstrap-table .fixed-table-container .table thead th .asc { + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAYAAAByUDbMAAAAZ0lEQVQ4y2NgGLKgquEuFxBPAGI2ahhWCsS/gDibUoO0gPgxEP8H4ttArEyuQYxAPBdqEAxPBImTY5gjEL9DM+wTENuQahAvEO9DMwiGdwAxOymGJQLxTyD+jgWDxCMZRsEoGAVoAADeemwtPcZI2wAAAABJRU5ErkJggg=="); +} + +.bootstrap-table .fixed-table-container .table thead th .desc { + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAYAAAByUDbMAAAAZUlEQVQ4y2NgGAWjYBSggaqGu5FA/BOIv2PBIPFEUgxjB+IdQPwfC94HxLykus4GiD+hGfQOiB3J8SojEE9EM2wuSJzcsFMG4ttQgx4DsRalkZENxL+AuJQaMcsGxBOAmGvopk8AVz1sLZgg0bsAAAAASUVORK5CYII= "); +} + +.bootstrap-table .fixed-table-container .table tbody tr.selected td { + background-color: #fafafa; +} + +.bootstrap-table .fixed-table-container .table tbody tr.no-records-found td { + text-align: center; +} + +.bootstrap-table .fixed-table-container .table tbody tr .card-view { + display: flex; +} + +.bootstrap-table .fixed-table-container .table tbody tr .card-view .card-view-title { + font-weight: bold; + display: inline-block; + min-width: 30%; + width: auto !important; + text-align: left !important; +} + +.bootstrap-table .fixed-table-container .table tbody tr .card-view .card-view-value { + width: 100% !important; +} + +.bootstrap-table .fixed-table-container .table .bs-checkbox { + text-align: center; +} + +.bootstrap-table .fixed-table-container .table .bs-checkbox label { + margin-bottom: 0; +} + +.bootstrap-table .fixed-table-container .table .bs-checkbox label input[type="radio"], +.bootstrap-table .fixed-table-container .table .bs-checkbox label input[type="checkbox"] { + margin: 0 auto !important; +} + +.bootstrap-table .fixed-table-container .table.table-sm .th-inner { + padding: 0.3rem; +} + +.bootstrap-table .fixed-table-container.fixed-height:not(.has-footer) { + border-bottom: 1px solid #dbdbdb; +} + +.bootstrap-table .fixed-table-container.fixed-height.has-card-view { + border-top: 1px solid #dbdbdb; + border-bottom: 1px solid #dbdbdb; +} + +.bootstrap-table .fixed-table-container.fixed-height .fixed-table-border { + border-left: 1px solid #dbdbdb; + border-right: 1px solid #dbdbdb; +} + +.bootstrap-table .fixed-table-container.fixed-height .table thead th { + border-bottom: 1px solid #dbdbdb; +} + +.bootstrap-table .fixed-table-container.fixed-height .table-dark thead th { + border-bottom: 1px solid #32383e; +} + +.bootstrap-table .fixed-table-container .fixed-table-header { + overflow: hidden; +} + +.bootstrap-table .fixed-table-container .fixed-table-body { + overflow-x: auto; + overflow-y: auto; + height: 100%; +} + +.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading { + align-items: center; + background: #fff; + display: flex; + justify-content: center; + position: absolute; + bottom: 0; + width: 100%; + z-index: 1000; + transition: visibility 0s, opacity 0.15s ease-in-out; + opacity: 0; + visibility: hidden; +} + +.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.open { + visibility: visible; + opacity: 1; +} + +.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap { + align-items: baseline; + display: flex; + justify-content: center; +} + +.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .loading-text { + margin-right: 6px; +} + +.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-wrap { + align-items: center; + display: flex; + justify-content: center; +} + +.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-dot, +.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-wrap::after, +.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-wrap::before { + content: ""; + animation-duration: 1.5s; + animation-iteration-count: infinite; + animation-name: LOADING; + background: #363636; + border-radius: 50%; + display: block; + height: 5px; + margin: 0 4px; + opacity: 0; + width: 5px; +} + +.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-dot { + animation-delay: 0.3s; +} + +.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-wrap::after { + animation-delay: 0.6s; +} + +.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.table-dark { + background: #363636; +} + +.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.table-dark .animation-dot, +.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.table-dark .animation-wrap::after, +.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.table-dark .animation-wrap::before { + background: #fff; +} + +.bootstrap-table .fixed-table-container .fixed-table-footer { + overflow: hidden; +} + +.bootstrap-table .fixed-table-pagination::after { + content: ""; + display: block; + clear: both; +} + +.bootstrap-table .fixed-table-pagination > .pagination-detail, +.bootstrap-table .fixed-table-pagination > .pagination { + margin-top: 10px; + margin-bottom: 10px; +} + +.bootstrap-table .fixed-table-pagination > .pagination-detail .pagination-info { + line-height: 34px; + margin-right: 5px; +} + +.bootstrap-table .fixed-table-pagination > .pagination-detail .page-list { + display: inline-block; +} + +.bootstrap-table .fixed-table-pagination > .pagination-detail .page-list .btn-group { + position: relative; + display: inline-block; + vertical-align: middle; +} + +.bootstrap-table .fixed-table-pagination > .pagination-detail .page-list .btn-group .dropdown-menu { + margin-bottom: 0; +} + +.bootstrap-table .fixed-table-pagination > .pagination ul.pagination { + margin: 0; +} + +.bootstrap-table .fixed-table-pagination > .pagination ul.pagination li.page-intermediate a { + color: #c8c8c8; +} + +.bootstrap-table .fixed-table-pagination > .pagination ul.pagination li.page-intermediate a::before { + content: '\2B05'; +} + +.bootstrap-table .fixed-table-pagination > .pagination ul.pagination li.page-intermediate a::after { + content: '\27A1'; +} + +.bootstrap-table .fixed-table-pagination > .pagination ul.pagination li.disabled a { + pointer-events: none; + cursor: default; +} + +.bootstrap-table.fullscreen { + position: fixed; + top: 0; + left: 0; + z-index: 1050; + width: 100% !important; + background: #fff; + height: calc(100vh); + overflow-y: scroll; +} + +.bootstrap-table.bootstrap4 .pagination-lg .page-link, .bootstrap-table.bootstrap5 .pagination-lg .page-link { + padding: .5rem 1rem; +} + +.bootstrap-table.bootstrap5 .float-left { + float: left; +} + +.bootstrap-table.bootstrap5 .float-right { + float: right; +} + +/* calculate scrollbar width */ +div.fixed-table-scroll-inner { + width: 100%; + height: 200px; +} + +div.fixed-table-scroll-outer { + top: 0; + left: 0; + visibility: hidden; + width: 200px; + height: 150px; + overflow: hidden; +} + +@keyframes LOADING { + 0% { + opacity: 0; + } + 50% { + opacity: 1; + } + to { + opacity: 0; + } +} + +@font-face { + font-family: 'bootstrap-table'; + src: url("fonts/bootstrap-table.eot?gmdfsp"); + src: url("fonts/bootstrap-table.eot") format("embedded-opentype"), url("fonts/bootstrap-table.ttf") format("truetype"), url("fonts/bootstrap-table.woff") format("woff"), url("fonts/bootstrap-table.svg") format("svg"); + font-weight: normal; + font-style: normal; + font-display: block; +} + +[class^="icon-"], +[class*=" icon-"] { + /* use !important to prevent issues with browser extensions that change fonts */ + font-family: 'bootstrap-table', sans-serif !important; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.icon-arrow-down-circle:before { + content: "\e907"; +} + +.icon-arrow-up-circle:before { + content: "\e908"; +} + +.icon-chevron-left:before { + content: "\e900"; +} + +.icon-chevron-right:before { + content: "\e901"; +} + +.icon-clock:before { + content: "\e90c"; +} + +.icon-copy:before { + content: "\e909"; +} + +.icon-download:before { + content: "\e90d"; +} + +.icon-list:before { + content: "\e902"; +} + +.icon-maximize:before { + content: "\1f5ce"; +} + +.icon-minus:before { + content: "\e90f"; +} + +.icon-move:before { + content: "\e903"; +} + +.icon-plus:before { + content: "\e90e"; +} + +.icon-printer:before { + content: "\e90b"; +} + +.icon-refresh-cw:before { + content: "\e904"; +} + +.icon-search:before { + content: "\e90a"; +} + +.icon-toggle-right:before { + content: "\e905"; +} + +.icon-trash-2:before { + content: "\e906"; +} + +.icon-sort-amount-asc:before { + content: "\ea4c"; +} + +.bootstrap-table * { + box-sizing: border-box; +} + +.bootstrap-table input.form-control, +.bootstrap-table select.form-control, +.bootstrap-table .btn { + border-radius: 4px; + background-color: #fff; + border: 1px solid #ccc; + padding: 9px 12px; +} + +.bootstrap-table select.form-control { + height: 35px; +} + +.bootstrap-table .btn { + outline: none; + cursor: pointer; +} + +.bootstrap-table .btn.active { + background-color: #ebebeb; +} + +.bootstrap-table .btn:focus, .bootstrap-table .btn:hover { + background-color: whitesmoke; +} + +.bootstrap-table .caret { + display: inline-block; + width: 0; + height: 0; + margin-left: 2px; + vertical-align: middle; + border-top: 4px dashed; + border-top: 4px solid; + border-right: 4px solid transparent; + border-left: 4px solid transparent; +} + +.bootstrap-table .detail-icon { + text-decoration: none; + color: #3679e4; +} + +.bootstrap-table .detail-icon:hover { + color: #154a9f; +} + +.bootstrap-table .fixed-table-toolbar .columns, +.bootstrap-table .fixed-table-toolbar .columns .btn-group { + display: inline-block; +} + +.bootstrap-table .fixed-table-toolbar .columns > .btn:not(:first-child):not(:last-child), +.bootstrap-table .fixed-table-toolbar .columns > .btn:not(:first-child):not(:last-child) > .btn, +.bootstrap-table .fixed-table-toolbar .columns > .btn-group:not(:first-child):not(:last-child), +.bootstrap-table .fixed-table-toolbar .columns > .btn-group:not(:first-child):not(:last-child) > .btn { + border-radius: 0; +} + +.bootstrap-table .fixed-table-toolbar .columns > .btn:not(:last-child):not(.dropdown-toggle), +.bootstrap-table .fixed-table-toolbar .columns > .btn:not(:last-child) > .btn, +.bootstrap-table .fixed-table-toolbar .columns > .btn-group:not(:last-child):not(.dropdown-toggle), +.bootstrap-table .fixed-table-toolbar .columns > .btn-group:not(:last-child) > .btn { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-right: none; +} + +.bootstrap-table .fixed-table-toolbar .columns > .btn:not(:first-child):not(.dropdown-toggle), +.bootstrap-table .fixed-table-toolbar .columns > .btn:not(:first-child) > .btn, +.bootstrap-table .fixed-table-toolbar .columns > .btn-group:not(:first-child):not(.dropdown-toggle), +.bootstrap-table .fixed-table-toolbar .columns > .btn-group:not(:first-child) > .btn { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} + +.bootstrap-table .fixed-table-toolbar .columns label { + padding: 5px 12px; +} + +.bootstrap-table .fixed-table-toolbar .columns input[type="checkbox"] { + vertical-align: middle; +} + +.bootstrap-table .fixed-table-toolbar .columns .dropdown-divider { + border-bottom: 1px solid #dbdbdb; +} + +.bootstrap-table .fixed-table-toolbar .search .input-group .search-input { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-right: none; +} + +.bootstrap-table .fixed-table-toolbar .search .input-group button[name="search"], +.bootstrap-table .fixed-table-toolbar .search .input-group button[name="clearSearch"] { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} + +.bootstrap-table .fixed-table-toolbar .search .input-group button[name="search"]:not(:last-child), +.bootstrap-table .fixed-table-toolbar .search .input-group button[name="clearSearch"]:not(:last-child) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-right: none; +} + +.bootstrap-table .open.dropdown-menu { + display: block; +} + +.bootstrap-table .dropdown-menu-up .dropdown-menu { + top: auto; + bottom: 100%; +} + +.bootstrap-table .dropdown-menu { + display: none; + background-color: #fff; + position: absolute; + right: 0; + min-width: 120px; + margin-top: 2px; + border: 1px solid #ccc; + border-radius: 4px; + -webkit-box-shadow: 0 3px 12px rgba(0, 0, 0, 0.175); + box-shadow: 0 3px 12px rgba(0, 0, 0, 0.175); +} + +.bootstrap-table .dropdown-menu .dropdown-item { + color: #363636; + text-decoration: none; + display: block; + padding: 5px 12px; + white-space: nowrap; +} + +.bootstrap-table .dropdown-menu .dropdown-item:hover { + background-color: whitesmoke; +} + +.bootstrap-table .dropdown-menu .dropdown-item.active { + background-color: #3679e4; + color: #fff; +} + +.bootstrap-table .dropdown-menu .dropdown-item.active:hover { + background-color: #1b5fcc; +} + +.bootstrap-table .columns-left .dropdown-menu { + left: 0; + right: auto; +} + +.bootstrap-table .pagination-detail { + float: left; +} + +.bootstrap-table .pagination-detail .dropdown-item { + min-width: 45px; + text-align: center; +} + +.bootstrap-table table { + border-collapse: collapse; +} + +.bootstrap-table table th { + text-align: inherit; +} + +.bootstrap-table table.table-bordered thead tr th, +.bootstrap-table table.table-bordered tbody tr td { + border: 1px solid #dbdbdb; +} + +.bootstrap-table table.table-bordered tbody tr td { + padding: 0.75rem; +} + +.bootstrap-table table.table-hover tbody tr:hover { + background: #fafafa; +} + +.bootstrap-table .float-left { + float: left; +} + +.bootstrap-table .float-right { + float: right; +} + +.bootstrap-table .pagination { + padding: 0; + align-items: center; + display: flex; + justify-content: center; + text-align: center; + list-style: none; +} + +.bootstrap-table .pagination .page-item { + border: 1px solid #dbdbdb; + background-color: #fff; + border-radius: 4px; + margin: 2px; + padding: 5px 2px 5px 2px; +} + +.bootstrap-table .pagination .page-item:hover { + background-color: whitesmoke; +} + +.bootstrap-table .pagination .page-item .page-link { + padding: 6px 12px; + line-height: 1.428571429; + color: #363636; + text-decoration: none; + outline: none; +} + +.bootstrap-table .pagination .page-item.active { + background-color: #3679e4; + border: 1px solid #206ae1; +} + +.bootstrap-table .pagination .page-item.active .page-link { + color: #fff; +} + +.bootstrap-table .pagination .page-item.active:hover { + background-color: #1b5fcc; +} + +.bootstrap-table .pagination .btn-group { + display: inline-block; +} + +.bootstrap-table .pagination .btn-group .btn:not(:first-child):not(:last-child), +.bootstrap-table .pagination .btn-group input:not(:first-child):not(:last-child) { + border-radius: 0; +} + +.bootstrap-table .pagination .btn-group .btn:first-child:not(:last-child):not(.dropdown-toggle), +.bootstrap-table .pagination .btn-group input:first-child:not(:last-child):not(.dropdown-toggle) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +.bootstrap-table .pagination .btn-group .btn:last-child:not(:first-child), +.bootstrap-table .pagination .btn-group input:last-child:not(:first-child) { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} + +.bootstrap-table .pagination .btn-group > .btn-group:last-child:not(:first-child) > .btn:first-child { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} + +.bootstrap-table .filter-control { + display: flex; +} + +.bootstrap-table .page-jump-to input, +.bootstrap-table .page-jump-to .btn { + padding: 8px 12px; +} + +.modal { + position: fixed; + display: none; + top: 0; + left: 0; + bottom: 0; + right: 0; +} + +.modal.show { + display: flex; +} + +.modal .btn { + border-radius: 4px; + background-color: #fff; + border: 1px solid #ccc; + padding: 6px 12px; + outline: none; + cursor: pointer; +} + +.modal .btn.active { + border-color: black; +} + +.modal .modal-background { + position: fixed; + top: 0; + left: 0; + bottom: 0; + right: 0; + z-index: 998; + background-color: rgba(10, 10, 10, 0.86); +} + +.modal .modal-content { + position: relative; + width: 600px; + margin: 30px auto; + z-index: 999; +} + +.modal .modal-content .box { + background-color: #fff; + border-radius: 6px; + display: block; + padding: 1.25rem; +} diff --git a/InvenTree/InvenTree/static/bootstrap-table/themes/bootstrap-table/bootstrap-table.js b/InvenTree/InvenTree/static/bootstrap-table/themes/bootstrap-table/bootstrap-table.js new file mode 100644 index 0000000000..a8005559e7 --- /dev/null +++ b/InvenTree/InvenTree/static/bootstrap-table/themes/bootstrap-table/bootstrap-table.js @@ -0,0 +1,1124 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(require('jquery')) : + typeof define === 'function' && define.amd ? define(['jquery'], factory) : + (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.jQuery)); +}(this, (function ($) { 'use strict'; + + function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } + + var $__default = /*#__PURE__*/_interopDefaultLegacy($); + + function _classCallCheck(instance, Constructor) { + if (!(instance instanceof Constructor)) { + throw new TypeError("Cannot call a class as a function"); + } + } + + function _defineProperties(target, props) { + for (var i = 0; i < props.length; i++) { + var descriptor = props[i]; + descriptor.enumerable = descriptor.enumerable || false; + descriptor.configurable = true; + if ("value" in descriptor) descriptor.writable = true; + Object.defineProperty(target, descriptor.key, descriptor); + } + } + + function _createClass(Constructor, protoProps, staticProps) { + if (protoProps) _defineProperties(Constructor.prototype, protoProps); + if (staticProps) _defineProperties(Constructor, staticProps); + return Constructor; + } + + function _inherits(subClass, superClass) { + if (typeof superClass !== "function" && superClass !== null) { + throw new TypeError("Super expression must either be null or a function"); + } + + subClass.prototype = Object.create(superClass && superClass.prototype, { + constructor: { + value: subClass, + writable: true, + configurable: true + } + }); + if (superClass) _setPrototypeOf(subClass, superClass); + } + + function _getPrototypeOf(o) { + _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { + return o.__proto__ || Object.getPrototypeOf(o); + }; + return _getPrototypeOf(o); + } + + function _setPrototypeOf(o, p) { + _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { + o.__proto__ = p; + return o; + }; + + return _setPrototypeOf(o, p); + } + + function _isNativeReflectConstruct() { + if (typeof Reflect === "undefined" || !Reflect.construct) return false; + if (Reflect.construct.sham) return false; + if (typeof Proxy === "function") return true; + + try { + Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); + return true; + } catch (e) { + return false; + } + } + + function _assertThisInitialized(self) { + if (self === void 0) { + throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); + } + + return self; + } + + function _possibleConstructorReturn(self, call) { + if (call && (typeof call === "object" || typeof call === "function")) { + return call; + } + + return _assertThisInitialized(self); + } + + function _createSuper(Derived) { + var hasNativeReflectConstruct = _isNativeReflectConstruct(); + + return function _createSuperInternal() { + var Super = _getPrototypeOf(Derived), + result; + + if (hasNativeReflectConstruct) { + var NewTarget = _getPrototypeOf(this).constructor; + + result = Reflect.construct(Super, arguments, NewTarget); + } else { + result = Super.apply(this, arguments); + } + + return _possibleConstructorReturn(this, result); + }; + } + + function _superPropBase(object, property) { + while (!Object.prototype.hasOwnProperty.call(object, property)) { + object = _getPrototypeOf(object); + if (object === null) break; + } + + return object; + } + + function _get(target, property, receiver) { + if (typeof Reflect !== "undefined" && Reflect.get) { + _get = Reflect.get; + } else { + _get = function _get(target, property, receiver) { + var base = _superPropBase(target, property); + + if (!base) return; + var desc = Object.getOwnPropertyDescriptor(base, property); + + if (desc.get) { + return desc.get.call(receiver); + } + + return desc.value; + }; + } + + return _get(target, property, receiver || target); + } + + var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; + + function createCommonjsModule(fn, module) { + return module = { exports: {} }, fn(module, module.exports), module.exports; + } + + var check = function (it) { + return it && it.Math == Math && it; + }; + + // https://github.com/zloirock/core-js/issues/86#issuecomment-115759028 + var global_1 = + /* global globalThis -- safe */ + check(typeof globalThis == 'object' && globalThis) || + check(typeof window == 'object' && window) || + check(typeof self == 'object' && self) || + check(typeof commonjsGlobal == 'object' && commonjsGlobal) || + // eslint-disable-next-line no-new-func -- fallback + (function () { return this; })() || Function('return this')(); + + var fails = function (exec) { + try { + return !!exec(); + } catch (error) { + return true; + } + }; + + // Detect IE8's incomplete defineProperty implementation + var descriptors = !fails(function () { + return Object.defineProperty({}, 1, { get: function () { return 7; } })[1] != 7; + }); + + var nativePropertyIsEnumerable = {}.propertyIsEnumerable; + var getOwnPropertyDescriptor$1 = Object.getOwnPropertyDescriptor; + + // Nashorn ~ JDK8 bug + var NASHORN_BUG = getOwnPropertyDescriptor$1 && !nativePropertyIsEnumerable.call({ 1: 2 }, 1); + + // `Object.prototype.propertyIsEnumerable` method implementation + // https://tc39.es/ecma262/#sec-object.prototype.propertyisenumerable + var f$4 = NASHORN_BUG ? function propertyIsEnumerable(V) { + var descriptor = getOwnPropertyDescriptor$1(this, V); + return !!descriptor && descriptor.enumerable; + } : nativePropertyIsEnumerable; + + var objectPropertyIsEnumerable = { + f: f$4 + }; + + var createPropertyDescriptor = function (bitmap, value) { + return { + enumerable: !(bitmap & 1), + configurable: !(bitmap & 2), + writable: !(bitmap & 4), + value: value + }; + }; + + var toString = {}.toString; + + var classofRaw = function (it) { + return toString.call(it).slice(8, -1); + }; + + var split = ''.split; + + // fallback for non-array-like ES3 and non-enumerable old V8 strings + var indexedObject = fails(function () { + // throws an error in rhino, see https://github.com/mozilla/rhino/issues/346 + // eslint-disable-next-line no-prototype-builtins -- safe + return !Object('z').propertyIsEnumerable(0); + }) ? function (it) { + return classofRaw(it) == 'String' ? split.call(it, '') : Object(it); + } : Object; + + // `RequireObjectCoercible` abstract operation + // https://tc39.es/ecma262/#sec-requireobjectcoercible + var requireObjectCoercible = function (it) { + if (it == undefined) throw TypeError("Can't call method on " + it); + return it; + }; + + // toObject with fallback for non-array-like ES3 strings + + + + var toIndexedObject = function (it) { + return indexedObject(requireObjectCoercible(it)); + }; + + var isObject = function (it) { + return typeof it === 'object' ? it !== null : typeof it === 'function'; + }; + + // `ToPrimitive` abstract operation + // https://tc39.es/ecma262/#sec-toprimitive + // instead of the ES6 spec version, we didn't implement @@toPrimitive case + // and the second argument - flag - preferred type is a string + var toPrimitive = function (input, PREFERRED_STRING) { + if (!isObject(input)) return input; + var fn, val; + if (PREFERRED_STRING && typeof (fn = input.toString) == 'function' && !isObject(val = fn.call(input))) return val; + if (typeof (fn = input.valueOf) == 'function' && !isObject(val = fn.call(input))) return val; + if (!PREFERRED_STRING && typeof (fn = input.toString) == 'function' && !isObject(val = fn.call(input))) return val; + throw TypeError("Can't convert object to primitive value"); + }; + + var hasOwnProperty = {}.hasOwnProperty; + + var has$1 = function (it, key) { + return hasOwnProperty.call(it, key); + }; + + var document$1 = global_1.document; + // typeof document.createElement is 'object' in old IE + var EXISTS = isObject(document$1) && isObject(document$1.createElement); + + var documentCreateElement = function (it) { + return EXISTS ? document$1.createElement(it) : {}; + }; + + // Thank's IE8 for his funny defineProperty + var ie8DomDefine = !descriptors && !fails(function () { + return Object.defineProperty(documentCreateElement('div'), 'a', { + get: function () { return 7; } + }).a != 7; + }); + + var nativeGetOwnPropertyDescriptor = Object.getOwnPropertyDescriptor; + + // `Object.getOwnPropertyDescriptor` method + // https://tc39.es/ecma262/#sec-object.getownpropertydescriptor + var f$3 = descriptors ? nativeGetOwnPropertyDescriptor : function getOwnPropertyDescriptor(O, P) { + O = toIndexedObject(O); + P = toPrimitive(P, true); + if (ie8DomDefine) try { + return nativeGetOwnPropertyDescriptor(O, P); + } catch (error) { /* empty */ } + if (has$1(O, P)) return createPropertyDescriptor(!objectPropertyIsEnumerable.f.call(O, P), O[P]); + }; + + var objectGetOwnPropertyDescriptor = { + f: f$3 + }; + + var anObject = function (it) { + if (!isObject(it)) { + throw TypeError(String(it) + ' is not an object'); + } return it; + }; + + var nativeDefineProperty = Object.defineProperty; + + // `Object.defineProperty` method + // https://tc39.es/ecma262/#sec-object.defineproperty + var f$2 = descriptors ? nativeDefineProperty : function defineProperty(O, P, Attributes) { + anObject(O); + P = toPrimitive(P, true); + anObject(Attributes); + if (ie8DomDefine) try { + return nativeDefineProperty(O, P, Attributes); + } catch (error) { /* empty */ } + if ('get' in Attributes || 'set' in Attributes) throw TypeError('Accessors not supported'); + if ('value' in Attributes) O[P] = Attributes.value; + return O; + }; + + var objectDefineProperty = { + f: f$2 + }; + + var createNonEnumerableProperty = descriptors ? function (object, key, value) { + return objectDefineProperty.f(object, key, createPropertyDescriptor(1, value)); + } : function (object, key, value) { + object[key] = value; + return object; + }; + + var setGlobal = function (key, value) { + try { + createNonEnumerableProperty(global_1, key, value); + } catch (error) { + global_1[key] = value; + } return value; + }; + + var SHARED = '__core-js_shared__'; + var store$1 = global_1[SHARED] || setGlobal(SHARED, {}); + + var sharedStore = store$1; + + var functionToString = Function.toString; + + // this helper broken in `3.4.1-3.4.4`, so we can't use `shared` helper + if (typeof sharedStore.inspectSource != 'function') { + sharedStore.inspectSource = function (it) { + return functionToString.call(it); + }; + } + + var inspectSource = sharedStore.inspectSource; + + var WeakMap$1 = global_1.WeakMap; + + var nativeWeakMap = typeof WeakMap$1 === 'function' && /native code/.test(inspectSource(WeakMap$1)); + + var shared = createCommonjsModule(function (module) { + (module.exports = function (key, value) { + return sharedStore[key] || (sharedStore[key] = value !== undefined ? value : {}); + })('versions', []).push({ + version: '3.9.1', + mode: 'global', + copyright: '© 2021 Denis Pushkarev (zloirock.ru)' + }); + }); + + var id = 0; + var postfix = Math.random(); + + var uid = function (key) { + return 'Symbol(' + String(key === undefined ? '' : key) + ')_' + (++id + postfix).toString(36); + }; + + var keys = shared('keys'); + + var sharedKey = function (key) { + return keys[key] || (keys[key] = uid(key)); + }; + + var hiddenKeys$1 = {}; + + var WeakMap = global_1.WeakMap; + var set, get, has; + + var enforce = function (it) { + return has(it) ? get(it) : set(it, {}); + }; + + var getterFor = function (TYPE) { + return function (it) { + var state; + if (!isObject(it) || (state = get(it)).type !== TYPE) { + throw TypeError('Incompatible receiver, ' + TYPE + ' required'); + } return state; + }; + }; + + if (nativeWeakMap) { + var store = sharedStore.state || (sharedStore.state = new WeakMap()); + var wmget = store.get; + var wmhas = store.has; + var wmset = store.set; + set = function (it, metadata) { + metadata.facade = it; + wmset.call(store, it, metadata); + return metadata; + }; + get = function (it) { + return wmget.call(store, it) || {}; + }; + has = function (it) { + return wmhas.call(store, it); + }; + } else { + var STATE = sharedKey('state'); + hiddenKeys$1[STATE] = true; + set = function (it, metadata) { + metadata.facade = it; + createNonEnumerableProperty(it, STATE, metadata); + return metadata; + }; + get = function (it) { + return has$1(it, STATE) ? it[STATE] : {}; + }; + has = function (it) { + return has$1(it, STATE); + }; + } + + var internalState = { + set: set, + get: get, + has: has, + enforce: enforce, + getterFor: getterFor + }; + + var redefine = createCommonjsModule(function (module) { + var getInternalState = internalState.get; + var enforceInternalState = internalState.enforce; + var TEMPLATE = String(String).split('String'); + + (module.exports = function (O, key, value, options) { + var unsafe = options ? !!options.unsafe : false; + var simple = options ? !!options.enumerable : false; + var noTargetGet = options ? !!options.noTargetGet : false; + var state; + if (typeof value == 'function') { + if (typeof key == 'string' && !has$1(value, 'name')) { + createNonEnumerableProperty(value, 'name', key); + } + state = enforceInternalState(value); + if (!state.source) { + state.source = TEMPLATE.join(typeof key == 'string' ? key : ''); + } + } + if (O === global_1) { + if (simple) O[key] = value; + else setGlobal(key, value); + return; + } else if (!unsafe) { + delete O[key]; + } else if (!noTargetGet && O[key]) { + simple = true; + } + if (simple) O[key] = value; + else createNonEnumerableProperty(O, key, value); + // add fake Function#toString for correct work wrapped methods / constructors with methods like LoDash isNative + })(Function.prototype, 'toString', function toString() { + return typeof this == 'function' && getInternalState(this).source || inspectSource(this); + }); + }); + + var path = global_1; + + var aFunction$1 = function (variable) { + return typeof variable == 'function' ? variable : undefined; + }; + + var getBuiltIn = function (namespace, method) { + return arguments.length < 2 ? aFunction$1(path[namespace]) || aFunction$1(global_1[namespace]) + : path[namespace] && path[namespace][method] || global_1[namespace] && global_1[namespace][method]; + }; + + var ceil = Math.ceil; + var floor = Math.floor; + + // `ToInteger` abstract operation + // https://tc39.es/ecma262/#sec-tointeger + var toInteger = function (argument) { + return isNaN(argument = +argument) ? 0 : (argument > 0 ? floor : ceil)(argument); + }; + + var min$1 = Math.min; + + // `ToLength` abstract operation + // https://tc39.es/ecma262/#sec-tolength + var toLength = function (argument) { + return argument > 0 ? min$1(toInteger(argument), 0x1FFFFFFFFFFFFF) : 0; // 2 ** 53 - 1 == 9007199254740991 + }; + + var max = Math.max; + var min = Math.min; + + // Helper for a popular repeating case of the spec: + // Let integer be ? ToInteger(index). + // If integer < 0, let result be max((length + integer), 0); else let result be min(integer, length). + var toAbsoluteIndex = function (index, length) { + var integer = toInteger(index); + return integer < 0 ? max(integer + length, 0) : min(integer, length); + }; + + // `Array.prototype.{ indexOf, includes }` methods implementation + var createMethod$1 = function (IS_INCLUDES) { + return function ($this, el, fromIndex) { + var O = toIndexedObject($this); + var length = toLength(O.length); + var index = toAbsoluteIndex(fromIndex, length); + var value; + // Array#includes uses SameValueZero equality algorithm + // eslint-disable-next-line no-self-compare -- NaN check + if (IS_INCLUDES && el != el) while (length > index) { + value = O[index++]; + // eslint-disable-next-line no-self-compare -- NaN check + if (value != value) return true; + // Array#indexOf ignores holes, Array#includes - not + } else for (;length > index; index++) { + if ((IS_INCLUDES || index in O) && O[index] === el) return IS_INCLUDES || index || 0; + } return !IS_INCLUDES && -1; + }; + }; + + var arrayIncludes = { + // `Array.prototype.includes` method + // https://tc39.es/ecma262/#sec-array.prototype.includes + includes: createMethod$1(true), + // `Array.prototype.indexOf` method + // https://tc39.es/ecma262/#sec-array.prototype.indexof + indexOf: createMethod$1(false) + }; + + var indexOf = arrayIncludes.indexOf; + + + var objectKeysInternal = function (object, names) { + var O = toIndexedObject(object); + var i = 0; + var result = []; + var key; + for (key in O) !has$1(hiddenKeys$1, key) && has$1(O, key) && result.push(key); + // Don't enum bug & hidden keys + while (names.length > i) if (has$1(O, key = names[i++])) { + ~indexOf(result, key) || result.push(key); + } + return result; + }; + + // IE8- don't enum bug keys + var enumBugKeys = [ + 'constructor', + 'hasOwnProperty', + 'isPrototypeOf', + 'propertyIsEnumerable', + 'toLocaleString', + 'toString', + 'valueOf' + ]; + + var hiddenKeys = enumBugKeys.concat('length', 'prototype'); + + // `Object.getOwnPropertyNames` method + // https://tc39.es/ecma262/#sec-object.getownpropertynames + var f$1 = Object.getOwnPropertyNames || function getOwnPropertyNames(O) { + return objectKeysInternal(O, hiddenKeys); + }; + + var objectGetOwnPropertyNames = { + f: f$1 + }; + + var f = Object.getOwnPropertySymbols; + + var objectGetOwnPropertySymbols = { + f: f + }; + + // all object keys, includes non-enumerable and symbols + var ownKeys = getBuiltIn('Reflect', 'ownKeys') || function ownKeys(it) { + var keys = objectGetOwnPropertyNames.f(anObject(it)); + var getOwnPropertySymbols = objectGetOwnPropertySymbols.f; + return getOwnPropertySymbols ? keys.concat(getOwnPropertySymbols(it)) : keys; + }; + + var copyConstructorProperties = function (target, source) { + var keys = ownKeys(source); + var defineProperty = objectDefineProperty.f; + var getOwnPropertyDescriptor = objectGetOwnPropertyDescriptor.f; + for (var i = 0; i < keys.length; i++) { + var key = keys[i]; + if (!has$1(target, key)) defineProperty(target, key, getOwnPropertyDescriptor(source, key)); + } + }; + + var replacement = /#|\.prototype\./; + + var isForced = function (feature, detection) { + var value = data[normalize(feature)]; + return value == POLYFILL ? true + : value == NATIVE ? false + : typeof detection == 'function' ? fails(detection) + : !!detection; + }; + + var normalize = isForced.normalize = function (string) { + return String(string).replace(replacement, '.').toLowerCase(); + }; + + var data = isForced.data = {}; + var NATIVE = isForced.NATIVE = 'N'; + var POLYFILL = isForced.POLYFILL = 'P'; + + var isForced_1 = isForced; + + var getOwnPropertyDescriptor = objectGetOwnPropertyDescriptor.f; + + + + + + + /* + options.target - name of the target object + options.global - target is the global object + options.stat - export as static methods of target + options.proto - export as prototype methods of target + options.real - real prototype method for the `pure` version + options.forced - export even if the native feature is available + options.bind - bind methods to the target, required for the `pure` version + options.wrap - wrap constructors to preventing global pollution, required for the `pure` version + options.unsafe - use the simple assignment of property instead of delete + defineProperty + options.sham - add a flag to not completely full polyfills + options.enumerable - export as enumerable property + options.noTargetGet - prevent calling a getter on target + */ + var _export = function (options, source) { + var TARGET = options.target; + var GLOBAL = options.global; + var STATIC = options.stat; + var FORCED, target, key, targetProperty, sourceProperty, descriptor; + if (GLOBAL) { + target = global_1; + } else if (STATIC) { + target = global_1[TARGET] || setGlobal(TARGET, {}); + } else { + target = (global_1[TARGET] || {}).prototype; + } + if (target) for (key in source) { + sourceProperty = source[key]; + if (options.noTargetGet) { + descriptor = getOwnPropertyDescriptor(target, key); + targetProperty = descriptor && descriptor.value; + } else targetProperty = target[key]; + FORCED = isForced_1(GLOBAL ? key : TARGET + (STATIC ? '.' : '#') + key, options.forced); + // contained in target + if (!FORCED && targetProperty !== undefined) { + if (typeof sourceProperty === typeof targetProperty) continue; + copyConstructorProperties(sourceProperty, targetProperty); + } + // add a flag to not completely full polyfills + if (options.sham || (targetProperty && targetProperty.sham)) { + createNonEnumerableProperty(sourceProperty, 'sham', true); + } + // extend global + redefine(target, key, sourceProperty, options); + } + }; + + var aFunction = function (it) { + if (typeof it != 'function') { + throw TypeError(String(it) + ' is not a function'); + } return it; + }; + + // optional / simple context binding + var functionBindContext = function (fn, that, length) { + aFunction(fn); + if (that === undefined) return fn; + switch (length) { + case 0: return function () { + return fn.call(that); + }; + case 1: return function (a) { + return fn.call(that, a); + }; + case 2: return function (a, b) { + return fn.call(that, a, b); + }; + case 3: return function (a, b, c) { + return fn.call(that, a, b, c); + }; + } + return function (/* ...args */) { + return fn.apply(that, arguments); + }; + }; + + // `ToObject` abstract operation + // https://tc39.es/ecma262/#sec-toobject + var toObject = function (argument) { + return Object(requireObjectCoercible(argument)); + }; + + // `IsArray` abstract operation + // https://tc39.es/ecma262/#sec-isarray + var isArray = Array.isArray || function isArray(arg) { + return classofRaw(arg) == 'Array'; + }; + + var engineIsNode = classofRaw(global_1.process) == 'process'; + + var engineUserAgent = getBuiltIn('navigator', 'userAgent') || ''; + + var process = global_1.process; + var versions = process && process.versions; + var v8 = versions && versions.v8; + var match, version; + + if (v8) { + match = v8.split('.'); + version = match[0] + match[1]; + } else if (engineUserAgent) { + match = engineUserAgent.match(/Edge\/(\d+)/); + if (!match || match[1] >= 74) { + match = engineUserAgent.match(/Chrome\/(\d+)/); + if (match) version = match[1]; + } + } + + var engineV8Version = version && +version; + + var nativeSymbol = !!Object.getOwnPropertySymbols && !fails(function () { + /* global Symbol -- required for testing */ + return !Symbol.sham && + // Chrome 38 Symbol has incorrect toString conversion + // Chrome 38-40 symbols are not inherited from DOM collections prototypes to instances + (engineIsNode ? engineV8Version === 38 : engineV8Version > 37 && engineV8Version < 41); + }); + + var useSymbolAsUid = nativeSymbol + /* global Symbol -- safe */ + && !Symbol.sham + && typeof Symbol.iterator == 'symbol'; + + var WellKnownSymbolsStore = shared('wks'); + var Symbol$1 = global_1.Symbol; + var createWellKnownSymbol = useSymbolAsUid ? Symbol$1 : Symbol$1 && Symbol$1.withoutSetter || uid; + + var wellKnownSymbol = function (name) { + if (!has$1(WellKnownSymbolsStore, name) || !(nativeSymbol || typeof WellKnownSymbolsStore[name] == 'string')) { + if (nativeSymbol && has$1(Symbol$1, name)) { + WellKnownSymbolsStore[name] = Symbol$1[name]; + } else { + WellKnownSymbolsStore[name] = createWellKnownSymbol('Symbol.' + name); + } + } return WellKnownSymbolsStore[name]; + }; + + var SPECIES = wellKnownSymbol('species'); + + // `ArraySpeciesCreate` abstract operation + // https://tc39.es/ecma262/#sec-arrayspeciescreate + var arraySpeciesCreate = function (originalArray, length) { + var C; + if (isArray(originalArray)) { + C = originalArray.constructor; + // cross-realm fallback + if (typeof C == 'function' && (C === Array || isArray(C.prototype))) C = undefined; + else if (isObject(C)) { + C = C[SPECIES]; + if (C === null) C = undefined; + } + } return new (C === undefined ? Array : C)(length === 0 ? 0 : length); + }; + + var push = [].push; + + // `Array.prototype.{ forEach, map, filter, some, every, find, findIndex, filterOut }` methods implementation + var createMethod = function (TYPE) { + var IS_MAP = TYPE == 1; + var IS_FILTER = TYPE == 2; + var IS_SOME = TYPE == 3; + var IS_EVERY = TYPE == 4; + var IS_FIND_INDEX = TYPE == 6; + var IS_FILTER_OUT = TYPE == 7; + var NO_HOLES = TYPE == 5 || IS_FIND_INDEX; + return function ($this, callbackfn, that, specificCreate) { + var O = toObject($this); + var self = indexedObject(O); + var boundFunction = functionBindContext(callbackfn, that, 3); + var length = toLength(self.length); + var index = 0; + var create = specificCreate || arraySpeciesCreate; + var target = IS_MAP ? create($this, length) : IS_FILTER || IS_FILTER_OUT ? create($this, 0) : undefined; + var value, result; + for (;length > index; index++) if (NO_HOLES || index in self) { + value = self[index]; + result = boundFunction(value, index, O); + if (TYPE) { + if (IS_MAP) target[index] = result; // map + else if (result) switch (TYPE) { + case 3: return true; // some + case 5: return value; // find + case 6: return index; // findIndex + case 2: push.call(target, value); // filter + } else switch (TYPE) { + case 4: return false; // every + case 7: push.call(target, value); // filterOut + } + } + } + return IS_FIND_INDEX ? -1 : IS_SOME || IS_EVERY ? IS_EVERY : target; + }; + }; + + var arrayIteration = { + // `Array.prototype.forEach` method + // https://tc39.es/ecma262/#sec-array.prototype.foreach + forEach: createMethod(0), + // `Array.prototype.map` method + // https://tc39.es/ecma262/#sec-array.prototype.map + map: createMethod(1), + // `Array.prototype.filter` method + // https://tc39.es/ecma262/#sec-array.prototype.filter + filter: createMethod(2), + // `Array.prototype.some` method + // https://tc39.es/ecma262/#sec-array.prototype.some + some: createMethod(3), + // `Array.prototype.every` method + // https://tc39.es/ecma262/#sec-array.prototype.every + every: createMethod(4), + // `Array.prototype.find` method + // https://tc39.es/ecma262/#sec-array.prototype.find + find: createMethod(5), + // `Array.prototype.findIndex` method + // https://tc39.es/ecma262/#sec-array.prototype.findIndex + findIndex: createMethod(6), + // `Array.prototype.filterOut` method + // https://github.com/tc39/proposal-array-filtering + filterOut: createMethod(7) + }; + + // `Object.keys` method + // https://tc39.es/ecma262/#sec-object.keys + var objectKeys = Object.keys || function keys(O) { + return objectKeysInternal(O, enumBugKeys); + }; + + // `Object.defineProperties` method + // https://tc39.es/ecma262/#sec-object.defineproperties + var objectDefineProperties = descriptors ? Object.defineProperties : function defineProperties(O, Properties) { + anObject(O); + var keys = objectKeys(Properties); + var length = keys.length; + var index = 0; + var key; + while (length > index) objectDefineProperty.f(O, key = keys[index++], Properties[key]); + return O; + }; + + var html = getBuiltIn('document', 'documentElement'); + + var GT = '>'; + var LT = '<'; + var PROTOTYPE = 'prototype'; + var SCRIPT = 'script'; + var IE_PROTO = sharedKey('IE_PROTO'); + + var EmptyConstructor = function () { /* empty */ }; + + var scriptTag = function (content) { + return LT + SCRIPT + GT + content + LT + '/' + SCRIPT + GT; + }; + + // Create object with fake `null` prototype: use ActiveX Object with cleared prototype + var NullProtoObjectViaActiveX = function (activeXDocument) { + activeXDocument.write(scriptTag('')); + activeXDocument.close(); + var temp = activeXDocument.parentWindow.Object; + activeXDocument = null; // avoid memory leak + return temp; + }; + + // Create object with fake `null` prototype: use iframe Object with cleared prototype + var NullProtoObjectViaIFrame = function () { + // Thrash, waste and sodomy: IE GC bug + var iframe = documentCreateElement('iframe'); + var JS = 'java' + SCRIPT + ':'; + var iframeDocument; + iframe.style.display = 'none'; + html.appendChild(iframe); + // https://github.com/zloirock/core-js/issues/475 + iframe.src = String(JS); + iframeDocument = iframe.contentWindow.document; + iframeDocument.open(); + iframeDocument.write(scriptTag('document.F=Object')); + iframeDocument.close(); + return iframeDocument.F; + }; + + // Check for document.domain and active x support + // No need to use active x approach when document.domain is not set + // see https://github.com/es-shims/es5-shim/issues/150 + // variation of https://github.com/kitcambridge/es5-shim/commit/4f738ac066346 + // avoid IE GC bug + var activeXDocument; + var NullProtoObject = function () { + try { + /* global ActiveXObject -- old IE */ + activeXDocument = document.domain && new ActiveXObject('htmlfile'); + } catch (error) { /* ignore */ } + NullProtoObject = activeXDocument ? NullProtoObjectViaActiveX(activeXDocument) : NullProtoObjectViaIFrame(); + var length = enumBugKeys.length; + while (length--) delete NullProtoObject[PROTOTYPE][enumBugKeys[length]]; + return NullProtoObject(); + }; + + hiddenKeys$1[IE_PROTO] = true; + + // `Object.create` method + // https://tc39.es/ecma262/#sec-object.create + var objectCreate = Object.create || function create(O, Properties) { + var result; + if (O !== null) { + EmptyConstructor[PROTOTYPE] = anObject(O); + result = new EmptyConstructor(); + EmptyConstructor[PROTOTYPE] = null; + // add "__proto__" for Object.getPrototypeOf polyfill + result[IE_PROTO] = O; + } else result = NullProtoObject(); + return Properties === undefined ? result : objectDefineProperties(result, Properties); + }; + + var UNSCOPABLES = wellKnownSymbol('unscopables'); + var ArrayPrototype = Array.prototype; + + // Array.prototype[@@unscopables] + // https://tc39.es/ecma262/#sec-array.prototype-@@unscopables + if (ArrayPrototype[UNSCOPABLES] == undefined) { + objectDefineProperty.f(ArrayPrototype, UNSCOPABLES, { + configurable: true, + value: objectCreate(null) + }); + } + + // add a key to Array.prototype[@@unscopables] + var addToUnscopables = function (key) { + ArrayPrototype[UNSCOPABLES][key] = true; + }; + + var $find = arrayIteration.find; + + + var FIND = 'find'; + var SKIPS_HOLES = true; + + // Shouldn't skip holes + if (FIND in []) Array(1)[FIND](function () { SKIPS_HOLES = false; }); + + // `Array.prototype.find` method + // https://tc39.es/ecma262/#sec-array.prototype.find + _export({ target: 'Array', proto: true, forced: SKIPS_HOLES }, { + find: function find(callbackfn /* , that = undefined */) { + return $find(this, callbackfn, arguments.length > 1 ? arguments[1] : undefined); + } + }); + + // https://tc39.es/ecma262/#sec-array.prototype-@@unscopables + addToUnscopables(FIND); + + var $includes = arrayIncludes.includes; + + + // `Array.prototype.includes` method + // https://tc39.es/ecma262/#sec-array.prototype.includes + _export({ target: 'Array', proto: true }, { + includes: function includes(el /* , fromIndex = 0 */) { + return $includes(this, el, arguments.length > 1 ? arguments[1] : undefined); + } + }); + + // https://tc39.es/ecma262/#sec-array.prototype-@@unscopables + addToUnscopables('includes'); + + var MATCH$1 = wellKnownSymbol('match'); + + // `IsRegExp` abstract operation + // https://tc39.es/ecma262/#sec-isregexp + var isRegexp = function (it) { + var isRegExp; + return isObject(it) && ((isRegExp = it[MATCH$1]) !== undefined ? !!isRegExp : classofRaw(it) == 'RegExp'); + }; + + var notARegexp = function (it) { + if (isRegexp(it)) { + throw TypeError("The method doesn't accept regular expressions"); + } return it; + }; + + var MATCH = wellKnownSymbol('match'); + + var correctIsRegexpLogic = function (METHOD_NAME) { + var regexp = /./; + try { + '/./'[METHOD_NAME](regexp); + } catch (error1) { + try { + regexp[MATCH] = false; + return '/./'[METHOD_NAME](regexp); + } catch (error2) { /* empty */ } + } return false; + }; + + // `String.prototype.includes` method + // https://tc39.es/ecma262/#sec-string.prototype.includes + _export({ target: 'String', proto: true, forced: !correctIsRegexpLogic('includes') }, { + includes: function includes(searchString /* , position = 0 */) { + return !!~String(requireObjectCoercible(this)) + .indexOf(notARegexp(searchString), arguments.length > 1 ? arguments[1] : undefined); + } + }); + + /** + * @author Dustin Utecht + * https://github.com/wenzhixin/bootstrap-table/ + */ + + $__default['default'].fn.bootstrapTable.theme = 'bootstrap-table'; + $__default['default'].extend($__default['default'].fn.bootstrapTable.defaults, { + iconsPrefix: 'icon', + icons: { + paginationSwitchDown: 'icon-arrow-up-circle', + paginationSwitchUp: 'icon-arrow-down-circle', + refresh: 'icon-refresh-cw', + toggleOff: 'icon-toggle-right', + toggleOn: 'icon-toggle-right', + columns: 'icon-list', + detailOpen: 'icon-plus', + detailClose: 'icon-minus', + fullscreen: 'icon-maximize', + search: 'icon-search', + clearSearch: 'icon-trash-2' + } + }); + + $__default['default'].BootstrapTable = /*#__PURE__*/function (_$$BootstrapTable) { + _inherits(_class, _$$BootstrapTable); + + var _super = _createSuper(_class); + + function _class() { + _classCallCheck(this, _class); + + return _super.apply(this, arguments); + } + + _createClass(_class, [{ + key: "init", + value: function init() { + _get(_getPrototypeOf(_class.prototype), "init", this).call(this); + + this.constants.classes.dropup = 'dropdown-menu-up'; + $__default['default']('.modal').on('click', '[data-close]', function (e) { + $__default['default'](e.delegateTarget).removeClass('show'); + }); + } + }, { + key: "initConstants", + value: function initConstants() { + _get(_getPrototypeOf(_class.prototype), "initConstants", this).call(this); + + this.constants.html.inputGroup = '
%s%s
'; + } + }, { + key: "initToolbar", + value: function initToolbar() { + _get(_getPrototypeOf(_class.prototype), "initToolbar", this).call(this); + + this.handleToolbar(); + } + }, { + key: "handleToolbar", + value: function handleToolbar() { + if (this.$toolbar.find('.dropdown-toggle').length) { + this._initDropdown(); + } + } + }, { + key: "initPagination", + value: function initPagination() { + _get(_getPrototypeOf(_class.prototype), "initPagination", this).call(this); + + if (this.options.pagination && this.paginationParts.includes('pageSize')) { + this._initDropdown(); + } + } + }, { + key: "_initDropdown", + value: function _initDropdown() { + var $dropdownToggles = $__default['default']('.dropdown-toggle'); + $dropdownToggles.off('click').on('click', function (e) { + var $target = $__default['default'](e.currentTarget); + + if ($target.parents('.dropdown-toggle').length > 0) { + $target = $target.parents('.dropdown-toggle'); + } + + $target.next('.dropdown-menu').toggleClass('open'); + }); + $__default['default'](window).off('click').on('click', function (e) { + var $dropdownToggles = $__default['default']('.dropdown-toggle'); + + if ($__default['default'](e.target).parents('.dropdown-toggle, .dropdown-menu').length === 0 && !$__default['default'](e.target).hasClass('dropdown-toggle')) { + $dropdownToggles.next('.dropdown-menu').removeClass('open'); + } + }); + } + }]); + + return _class; + }($__default['default'].BootstrapTable); + +}))); diff --git a/InvenTree/InvenTree/static/bootstrap-table/themes/bootstrap-table/bootstrap-table.min.css b/InvenTree/InvenTree/static/bootstrap-table/themes/bootstrap-table/bootstrap-table.min.css new file mode 100644 index 0000000000..57df50bae3 --- /dev/null +++ b/InvenTree/InvenTree/static/bootstrap-table/themes/bootstrap-table/bootstrap-table.min.css @@ -0,0 +1,10 @@ +/** + * bootstrap-table - An extended table to integration with some of the most widely used CSS frameworks. (Supports Bootstrap, Semantic UI, Bulma, Material Design, Foundation) + * + * @version v1.18.3 + * @homepage https://bootstrap-table.com + * @author wenzhixin (http://wenzhixin.net.cn/) + * @license MIT + */ + +.bootstrap-table .fixed-table-toolbar::after{content:"";display:block;clear:both}.bootstrap-table .fixed-table-toolbar .bs-bars,.bootstrap-table .fixed-table-toolbar .columns,.bootstrap-table .fixed-table-toolbar .search{position:relative;margin-top:10px;margin-bottom:10px}.bootstrap-table .fixed-table-toolbar .columns .btn-group>.btn-group{display:inline-block;margin-left:-1px!important}.bootstrap-table .fixed-table-toolbar .columns .btn-group>.btn-group>.btn{border-radius:0}.bootstrap-table .fixed-table-toolbar .columns .btn-group>.btn-group:first-child>.btn{border-top-left-radius:4px;border-bottom-left-radius:4px}.bootstrap-table .fixed-table-toolbar .columns .btn-group>.btn-group:last-child>.btn{border-top-right-radius:4px;border-bottom-right-radius:4px}.bootstrap-table .fixed-table-toolbar .columns .dropdown-menu{text-align:left;max-height:300px;overflow:auto;-ms-overflow-style:scrollbar;z-index:1001}.bootstrap-table .fixed-table-toolbar .columns label{display:block;padding:3px 20px;clear:both;font-weight:400;line-height:1.428571429}.bootstrap-table .fixed-table-toolbar .columns-left{margin-right:5px}.bootstrap-table .fixed-table-toolbar .columns-right{margin-left:5px}.bootstrap-table .fixed-table-toolbar .pull-right .dropdown-menu{right:0;left:auto}.bootstrap-table .fixed-table-container{position:relative;clear:both}.bootstrap-table .fixed-table-container .table{width:100%;margin-bottom:0!important}.bootstrap-table .fixed-table-container .table td,.bootstrap-table .fixed-table-container .table th{vertical-align:middle;box-sizing:border-box}.bootstrap-table .fixed-table-container .table thead th{vertical-align:bottom;padding:0;margin:0}.bootstrap-table .fixed-table-container .table thead th:focus{outline:0 solid transparent}.bootstrap-table .fixed-table-container .table thead th.detail{width:30px}.bootstrap-table .fixed-table-container .table thead th .th-inner{padding:.75rem;vertical-align:bottom;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.bootstrap-table .fixed-table-container .table thead th .sortable{cursor:pointer;background-position:right;background-repeat:no-repeat;padding-right:30px!important}.bootstrap-table .fixed-table-container .table thead th .both{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAQAAADYWf5HAAAAkElEQVQoz7X QMQ5AQBCF4dWQSJxC5wwax1Cq1e7BAdxD5SL+Tq/QCM1oNiJidwox0355mXnG/DrEtIQ6azioNZQxI0ykPhTQIwhCR+BmBYtlK7kLJYwWCcJA9M4qdrZrd8pPjZWPtOqdRQy320YSV17OatFC4euts6z39GYMKRPCTKY9UnPQ6P+GtMRfGtPnBCiqhAeJPmkqAAAAAElFTkSuQmCC")}.bootstrap-table .fixed-table-container .table thead th .asc{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAYAAAByUDbMAAAAZ0lEQVQ4y2NgGLKgquEuFxBPAGI2ahhWCsS/gDibUoO0gPgxEP8H4ttArEyuQYxAPBdqEAxPBImTY5gjEL9DM+wTENuQahAvEO9DMwiGdwAxOymGJQLxTyD+jgWDxCMZRsEoGAVoAADeemwtPcZI2wAAAABJRU5ErkJggg==")}.bootstrap-table .fixed-table-container .table thead th .desc{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAYAAAByUDbMAAAAZUlEQVQ4y2NgGAWjYBSggaqGu5FA/BOIv2PBIPFEUgxjB+IdQPwfC94HxLykus4GiD+hGfQOiB3J8SojEE9EM2wuSJzcsFMG4ttQgx4DsRalkZENxL+AuJQaMcsGxBOAmGvopk8AVz1sLZgg0bsAAAAASUVORK5CYII= ")}.bootstrap-table .fixed-table-container .table tbody tr.selected td{background-color:#fafafa}.bootstrap-table .fixed-table-container .table tbody tr.no-records-found td{text-align:center}.bootstrap-table .fixed-table-container .table tbody tr .card-view{display:flex}.bootstrap-table .fixed-table-container .table tbody tr .card-view .card-view-title{font-weight:700;display:inline-block;min-width:30%;width:auto!important;text-align:left!important}.bootstrap-table .fixed-table-container .table tbody tr .card-view .card-view-value{width:100%!important}.bootstrap-table .fixed-table-container .table .bs-checkbox{text-align:center}.bootstrap-table .fixed-table-container .table .bs-checkbox label{margin-bottom:0}.bootstrap-table .fixed-table-container .table .bs-checkbox label input[type=checkbox],.bootstrap-table .fixed-table-container .table .bs-checkbox label input[type=radio]{margin:0 auto!important}.bootstrap-table .fixed-table-container .table.table-sm .th-inner{padding:.3rem}.bootstrap-table .fixed-table-container.fixed-height:not(.has-footer){border-bottom:1px solid #dbdbdb}.bootstrap-table .fixed-table-container.fixed-height.has-card-view{border-top:1px solid #dbdbdb;border-bottom:1px solid #dbdbdb}.bootstrap-table .fixed-table-container.fixed-height .fixed-table-border{border-left:1px solid #dbdbdb;border-right:1px solid #dbdbdb}.bootstrap-table .fixed-table-container.fixed-height .table thead th{border-bottom:1px solid #dbdbdb}.bootstrap-table .fixed-table-container.fixed-height .table-dark thead th{border-bottom:1px solid #32383e}.bootstrap-table .fixed-table-container .fixed-table-header{overflow:hidden}.bootstrap-table .fixed-table-container .fixed-table-body{overflow-x:auto;overflow-y:auto;height:100%}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading{align-items:center;background:#fff;display:flex;justify-content:center;position:absolute;bottom:0;width:100%;z-index:1000;transition:visibility 0s,opacity .15s ease-in-out;opacity:0;visibility:hidden}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.open{visibility:visible;opacity:1}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap{align-items:baseline;display:flex;justify-content:center}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .loading-text{margin-right:6px}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-wrap{align-items:center;display:flex;justify-content:center}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-dot,.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-wrap::after,.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-wrap::before{content:"";animation-duration:1.5s;animation-iteration-count:infinite;animation-name:LOADING;background:#363636;border-radius:50%;display:block;height:5px;margin:0 4px;opacity:0;width:5px}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-dot{animation-delay:.3s}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-wrap::after{animation-delay:.6s}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.table-dark{background:#363636}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.table-dark .animation-dot,.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.table-dark .animation-wrap::after,.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.table-dark .animation-wrap::before{background:#fff}.bootstrap-table .fixed-table-container .fixed-table-footer{overflow:hidden}.bootstrap-table .fixed-table-pagination::after{content:"";display:block;clear:both}.bootstrap-table .fixed-table-pagination>.pagination,.bootstrap-table .fixed-table-pagination>.pagination-detail{margin-top:10px;margin-bottom:10px}.bootstrap-table .fixed-table-pagination>.pagination-detail .pagination-info{line-height:34px;margin-right:5px}.bootstrap-table .fixed-table-pagination>.pagination-detail .page-list{display:inline-block}.bootstrap-table .fixed-table-pagination>.pagination-detail .page-list .btn-group{position:relative;display:inline-block;vertical-align:middle}.bootstrap-table .fixed-table-pagination>.pagination-detail .page-list .btn-group .dropdown-menu{margin-bottom:0}.bootstrap-table .fixed-table-pagination>.pagination ul.pagination{margin:0}.bootstrap-table .fixed-table-pagination>.pagination ul.pagination li.page-intermediate a{color:#c8c8c8}.bootstrap-table .fixed-table-pagination>.pagination ul.pagination li.page-intermediate a::before{content:'\2B05'}.bootstrap-table .fixed-table-pagination>.pagination ul.pagination li.page-intermediate a::after{content:'\27A1'}.bootstrap-table .fixed-table-pagination>.pagination ul.pagination li.disabled a{pointer-events:none;cursor:default}.bootstrap-table.fullscreen{position:fixed;top:0;left:0;z-index:1050;width:100%!important;background:#fff;height:calc(100vh);overflow-y:scroll}.bootstrap-table.bootstrap4 .pagination-lg .page-link,.bootstrap-table.bootstrap5 .pagination-lg .page-link{padding:.5rem 1rem}.bootstrap-table.bootstrap5 .float-left{float:left}.bootstrap-table.bootstrap5 .float-right{float:right}div.fixed-table-scroll-inner{width:100%;height:200px}div.fixed-table-scroll-outer{top:0;left:0;visibility:hidden;width:200px;height:150px;overflow:hidden}@keyframes LOADING{0%{opacity:0}50%{opacity:1}to{opacity:0}}@font-face{font-family:bootstrap-table;src:url("fonts/bootstrap-table.eot?gmdfsp");src:url("fonts/bootstrap-table.eot") format("embedded-opentype"),url("fonts/bootstrap-table.ttf") format("truetype"),url("fonts/bootstrap-table.woff") format("woff"),url("fonts/bootstrap-table.svg") format("svg");font-weight:400;font-style:normal;font-display:block}[class*=" icon-"],[class^=icon-]{font-family:bootstrap-table,sans-serif!important;speak:none;font-style:normal;font-weight:400;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.icon-arrow-down-circle:before{content:"\e907"}.icon-arrow-up-circle:before{content:"\e908"}.icon-chevron-left:before{content:"\e900"}.icon-chevron-right:before{content:"\e901"}.icon-clock:before{content:"\e90c"}.icon-copy:before{content:"\e909"}.icon-download:before{content:"\e90d"}.icon-list:before{content:"\e902"}.icon-maximize:before{content:"\1f5ce"}.icon-minus:before{content:"\e90f"}.icon-move:before{content:"\e903"}.icon-plus:before{content:"\e90e"}.icon-printer:before{content:"\e90b"}.icon-refresh-cw:before{content:"\e904"}.icon-search:before{content:"\e90a"}.icon-toggle-right:before{content:"\e905"}.icon-trash-2:before{content:"\e906"}.icon-sort-amount-asc:before{content:"\ea4c"}.bootstrap-table *{box-sizing:border-box}.bootstrap-table .btn,.bootstrap-table input.form-control,.bootstrap-table select.form-control{border-radius:4px;background-color:#fff;border:1px solid #ccc;padding:9px 12px}.bootstrap-table select.form-control{height:35px}.bootstrap-table .btn{outline:0;cursor:pointer}.bootstrap-table .btn.active{background-color:#ebebeb}.bootstrap-table .btn:focus,.bootstrap-table .btn:hover{background-color:#f5f5f5}.bootstrap-table .caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px dashed;border-top:4px solid;border-right:4px solid transparent;border-left:4px solid transparent}.bootstrap-table .detail-icon{text-decoration:none;color:#3679e4}.bootstrap-table .detail-icon:hover{color:#154a9f}.bootstrap-table .fixed-table-toolbar .columns,.bootstrap-table .fixed-table-toolbar .columns .btn-group{display:inline-block}.bootstrap-table .fixed-table-toolbar .columns>.btn-group:not(:first-child):not(:last-child),.bootstrap-table .fixed-table-toolbar .columns>.btn-group:not(:first-child):not(:last-child)>.btn,.bootstrap-table .fixed-table-toolbar .columns>.btn:not(:first-child):not(:last-child),.bootstrap-table .fixed-table-toolbar .columns>.btn:not(:first-child):not(:last-child)>.btn{border-radius:0}.bootstrap-table .fixed-table-toolbar .columns>.btn-group:not(:last-child):not(.dropdown-toggle),.bootstrap-table .fixed-table-toolbar .columns>.btn-group:not(:last-child)>.btn,.bootstrap-table .fixed-table-toolbar .columns>.btn:not(:last-child):not(.dropdown-toggle),.bootstrap-table .fixed-table-toolbar .columns>.btn:not(:last-child)>.btn{border-top-right-radius:0;border-bottom-right-radius:0;border-right:none}.bootstrap-table .fixed-table-toolbar .columns>.btn-group:not(:first-child):not(.dropdown-toggle),.bootstrap-table .fixed-table-toolbar .columns>.btn-group:not(:first-child)>.btn,.bootstrap-table .fixed-table-toolbar .columns>.btn:not(:first-child):not(.dropdown-toggle),.bootstrap-table .fixed-table-toolbar .columns>.btn:not(:first-child)>.btn{border-top-left-radius:0;border-bottom-left-radius:0}.bootstrap-table .fixed-table-toolbar .columns label{padding:5px 12px}.bootstrap-table .fixed-table-toolbar .columns input[type=checkbox]{vertical-align:middle}.bootstrap-table .fixed-table-toolbar .columns .dropdown-divider{border-bottom:1px solid #dbdbdb}.bootstrap-table .fixed-table-toolbar .search .input-group .search-input{border-top-right-radius:0;border-bottom-right-radius:0;border-right:none}.bootstrap-table .fixed-table-toolbar .search .input-group button[name=clearSearch],.bootstrap-table .fixed-table-toolbar .search .input-group button[name=search]{border-top-left-radius:0;border-bottom-left-radius:0}.bootstrap-table .fixed-table-toolbar .search .input-group button[name=clearSearch]:not(:last-child),.bootstrap-table .fixed-table-toolbar .search .input-group button[name=search]:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0;border-right:none}.bootstrap-table .open.dropdown-menu{display:block}.bootstrap-table .dropdown-menu-up .dropdown-menu{top:auto;bottom:100%}.bootstrap-table .dropdown-menu{display:none;background-color:#fff;position:absolute;right:0;min-width:120px;margin-top:2px;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:0 3px 12px rgba(0,0,0,.175);box-shadow:0 3px 12px rgba(0,0,0,.175)}.bootstrap-table .dropdown-menu .dropdown-item{color:#363636;text-decoration:none;display:block;padding:5px 12px;white-space:nowrap}.bootstrap-table .dropdown-menu .dropdown-item:hover{background-color:#f5f5f5}.bootstrap-table .dropdown-menu .dropdown-item.active{background-color:#3679e4;color:#fff}.bootstrap-table .dropdown-menu .dropdown-item.active:hover{background-color:#1b5fcc}.bootstrap-table .columns-left .dropdown-menu{left:0;right:auto}.bootstrap-table .pagination-detail{float:left}.bootstrap-table .pagination-detail .dropdown-item{min-width:45px;text-align:center}.bootstrap-table table{border-collapse:collapse}.bootstrap-table table th{text-align:inherit}.bootstrap-table table.table-bordered tbody tr td,.bootstrap-table table.table-bordered thead tr th{border:1px solid #dbdbdb}.bootstrap-table table.table-bordered tbody tr td{padding:.75rem}.bootstrap-table table.table-hover tbody tr:hover{background:#fafafa}.bootstrap-table .float-left{float:left}.bootstrap-table .float-right{float:right}.bootstrap-table .pagination{padding:0;align-items:center;display:flex;justify-content:center;text-align:center;list-style:none}.bootstrap-table .pagination .page-item{border:1px solid #dbdbdb;background-color:#fff;border-radius:4px;margin:2px;padding:5px 2px 5px 2px}.bootstrap-table .pagination .page-item:hover{background-color:#f5f5f5}.bootstrap-table .pagination .page-item .page-link{padding:6px 12px;line-height:1.428571429;color:#363636;text-decoration:none;outline:0}.bootstrap-table .pagination .page-item.active{background-color:#3679e4;border:1px solid #206ae1}.bootstrap-table .pagination .page-item.active .page-link{color:#fff}.bootstrap-table .pagination .page-item.active:hover{background-color:#1b5fcc}.bootstrap-table .pagination .btn-group{display:inline-block}.bootstrap-table .pagination .btn-group .btn:not(:first-child):not(:last-child),.bootstrap-table .pagination .btn-group input:not(:first-child):not(:last-child){border-radius:0}.bootstrap-table .pagination .btn-group .btn:first-child:not(:last-child):not(.dropdown-toggle),.bootstrap-table .pagination .btn-group input:first-child:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.bootstrap-table .pagination .btn-group .btn:last-child:not(:first-child),.bootstrap-table .pagination .btn-group input:last-child:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.bootstrap-table .pagination .btn-group>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-bottom-left-radius:0}.bootstrap-table .filter-control{display:flex}.bootstrap-table .page-jump-to .btn,.bootstrap-table .page-jump-to input{padding:8px 12px}.modal{position:fixed;display:none;top:0;left:0;bottom:0;right:0}.modal.show{display:flex}.modal .btn{border-radius:4px;background-color:#fff;border:1px solid #ccc;padding:6px 12px;outline:0;cursor:pointer}.modal .btn.active{border-color:#000}.modal .modal-background{position:fixed;top:0;left:0;bottom:0;right:0;z-index:998;background-color:rgba(10,10,10,.86)}.modal .modal-content{position:relative;width:600px;margin:30px auto;z-index:999}.modal .modal-content .box{background-color:#fff;border-radius:6px;display:block;padding:1.25rem} \ No newline at end of file diff --git a/InvenTree/InvenTree/static/bootstrap-table/themes/bootstrap-table/bootstrap-table.min.js b/InvenTree/InvenTree/static/bootstrap-table/themes/bootstrap-table/bootstrap-table.min.js new file mode 100644 index 0000000000..363d4ce3fd --- /dev/null +++ b/InvenTree/InvenTree/static/bootstrap-table/themes/bootstrap-table/bootstrap-table.min.js @@ -0,0 +1,10 @@ +/** + * bootstrap-table - An extended table to integration with some of the most widely used CSS frameworks. (Supports Bootstrap, Semantic UI, Bulma, Material Design, Foundation) + * + * @version v1.18.3 + * @homepage https://bootstrap-table.com + * @author wenzhixin (http://wenzhixin.net.cn/) + * @license MIT + */ + +!function(t,n){"object"==typeof exports&&"undefined"!=typeof module?n(require("jquery")):"function"==typeof define&&define.amd?define(["jquery"],n):n((t="undefined"!=typeof globalThis?globalThis:t||self).jQuery)}(this,(function(t){"use strict";function n(t){return t&&"object"==typeof t&&"default"in t?t:{default:t}}var e=n(t);function r(t,n){if(!(t instanceof n))throw new TypeError("Cannot call a class as a function")}function o(t,n){for(var e=0;e0?vt:gt)(t)},wt=Math.min,mt=function(t){return t>0?wt(bt(t),9007199254740991):0},Ot=Math.max,jt=Math.min,St=function(t){return function(n,e,r){var o,i=P(n),u=mt(i.length),c=function(t,n){var e=bt(t);return e<0?Ot(e+n,0):jt(e,n)}(r,u);if(t&&e!=e){for(;u>c;)if((o=i[c++])!=o)return!0}else for(;u>c;c++)if((t||c in i)&&i[c]===e)return t||c||0;return!t&&-1}},Tt={includes:St(!0),indexOf:St(!1)},Pt=Tt.indexOf,Et=function(t,n){var e,r=P(t),o=0,i=[];for(e in r)!_(et,e)&&_(r,e)&&i.push(e);for(;n.length>o;)_(r,e=n[o++])&&(~Pt(i,e)||i.push(e));return i},xt=["constructor","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","toLocaleString","toString","valueOf"],kt=xt.concat("length","prototype"),_t={f:Object.getOwnPropertyNames||function(t){return Et(t,kt)}},At={f:Object.getOwnPropertySymbols},Ct=ht("Reflect","ownKeys")||function(t){var n=_t.f(F(t)),e=At.f;return e?n.concat(e(t)):n},Rt=function(t,n){for(var e=Ct(n),r=L.f,o=I.f,i=0;i=74)&&(at=Qt.match(/Chrome\/(\d+)/))&&(lt=at[1]);var Yt,$t=lt&&+lt,Ht=!!Object.getOwnPropertySymbols&&!y((function(){return!Symbol.sham&&(Kt?38===$t:$t>37&&$t<41)})),Jt=Ht&&!Symbol.sham&&"symbol"==typeof Symbol.iterator,Zt=$("wks"),tn=d.Symbol,nn=Jt?tn:tn&&tn.withoutSetter||Z,en=function(t){return _(Zt,t)&&(Ht||"string"==typeof Zt[t])||(Ht&&_(tn,t)?Zt[t]=tn[t]:Zt[t]=nn("Symbol."+t)),Zt[t]},rn=en("species"),on=function(t,n){var e;return Gt(t)&&("function"!=typeof(e=t.constructor)||e!==Array&&!Gt(e.prototype)?E(e)&&null===(e=e[rn])&&(e=void 0):e=void 0),new(void 0===e?Array:e)(0===n?0:n)},un=[].push,cn=function(t){var n=1==t,e=2==t,r=3==t,o=4==t,i=6==t,u=7==t,c=5==t||i;return function(f,a,l,s){for(var p,d,y=Object(T(f)),h=S(y),g=Wt(a,l,3),v=mt(h.length),b=0,w=s||on,m=n?w(f,v):e||u?w(f,0):void 0;v>b;b++)if((c||b in h)&&(d=g(p=h[b],b,y),t))if(n)m[b]=d;else if(d)switch(t){case 3:return!0;case 5:return p;case 6:return b;case 2:un.call(m,p)}else switch(t){case 4:return!1;case 7:un.call(m,p)}return i?-1:r||o?o:m}},fn={forEach:cn(0),map:cn(1),filter:cn(2),some:cn(3),every:cn(4),find:cn(5),findIndex:cn(6),filterOut:cn(7)},an=Object.keys||function(t){return Et(t,xt)},ln=h?Object.defineProperties:function(t,n){F(t);for(var e,r=an(n),o=r.length,i=0;o>i;)L.f(t,e=r[i++],n[e]);return t},sn=ht("document","documentElement"),pn=nt("IE_PROTO"),dn=function(){},yn=function(t){return" +{% endblock %} + + diff --git a/InvenTree/templates/InvenTree/index.html b/InvenTree/templates/InvenTree/index.html index 14fc31531a..44bc70fc37 100644 --- a/InvenTree/templates/InvenTree/index.html +++ b/InvenTree/templates/InvenTree/index.html @@ -7,22 +7,16 @@ {% inventree_title %} | {% trans "Index" %} {% endblock %} -{% block content %} -

{% inventree_title %}

-
+{% block breadcrumb_list %} +{% endblock %} -
-
    -
-
-
-
    -
  • -
    - -
    -
  • -
+{% block sidebar %} + +{% endblock %} + +{% block content %} + +
{% endblock %} @@ -32,68 +26,57 @@ {{ block.super }} function addHeaderTitle(title) { - // Add a header block to the action list - $("#action-item-list").append( - `
  • ${title}
  • ` - ); + + addSidebarHeader({ + text: title, + }); } function addHeaderAction(label, title, icon, options) { - // Add an action block to the action list - $("#action-item-list").append( - `
  • - - - ${title} - - - - -
  • ` - ); + + // Construct a "badge" to add to the sidebar item + var badge = ` + + + + `; + + addSidebarItem({ + label: label, + text: title, + icon: icon, + content_after: badge + }); // Add a detail item to the detail item-panel - $("#detail-item-list").append( - `
  • -

    ${title}

    -
    -
  • ` + $("#detail-panels").append( + `
    +
    +

    ${title}

    +
    +
    +
    +
    +
    ` ); - $(`#detail-${label}`).hide(); - - $(`#action-${label}`).click(function() { - - // Hide all child elements - $('#detail-item-list').children('li').each(function() { - $(this).hide(); - }); - - // Show the one we want - $(`#detail-${label}`).fadeIn(); - - // Remove css class from all action items - $("#action-item-list").children('li').each(function() { - $(this).removeClass('index-action-selected'); - }); - - // Add css class to the action we are interested in - $(`#action-${label}`).addClass('index-action-selected'); - }); - // Connect a callback to the table $(`#table-${label}`).on('load-success.bs.table', function() { var count = $(`#table-${label}`).bootstrapTable('getData').length; - $(`#badge-${label}`).html(count); + var badge = $(`#sidebar-badge-${label}`); + + badge.html(count); if (count > 0) { - $(`#badge-${label}`).addClass('badge-orange'); + badge.removeClass('bg-dark'); + badge.addClass('bg-primary'); } }); } {% settings_value 'HOMEPAGE_PART_STARRED' user=request.user as setting_part_starred %} +{% settings_value 'HOMEPAGE_CATEGORY_STARRED' user=request.user as setting_category_starred %} {% settings_value 'HOMEPAGE_PART_LATEST' user=request.user as setting_part_latest %} {% settings_value 'HOMEPAGE_BOM_VALIDATION' user=request.user as setting_bom_validation %} {% to_list setting_part_starred setting_part_latest setting_bom_validation as settings_list_part %} @@ -102,15 +85,25 @@ function addHeaderAction(label, title, icon, options) { addHeaderTitle('{% trans "Parts" %}'); {% if setting_part_starred %} -addHeaderAction('starred-parts', '{% trans "Starred Parts" %}', 'fa-star'); +addHeaderAction('starred-parts', '{% trans "Subscribed Parts" %}', 'fa-bell'); loadSimplePartTable("#table-starred-parts", "{% url 'api-part-list' %}", { params: { - "starred": true, + starred: true, }, name: 'starred_parts', }); {% endif %} +{% if setting_category_starred %} +addHeaderAction('starred-categories', '{% trans "Subscribed Categories" %}', 'fa-bell'); +loadPartCategoryTable($('#table-starred-categories'), { + params: { + starred: true, + }, + name: 'starred_categories' +}); +{% endif %} + {% if setting_part_latest %} addHeaderAction('latest-parts', '{% trans "Latest Parts" %}', 'fa-newspaper'); loadSimplePartTable("#table-latest-parts", "{% url 'api-part-list' %}", { @@ -146,8 +139,7 @@ loadSimplePartTable("#table-bom-validation", "{% url 'api-part-list' %}", { {% to_list setting_stock_recent setting_stock_low setting_stock_depleted setting_stock_needed as settings_list_stock %} {% endif %} -{% if roles.stock.view and True in settings_list_stock %} -addHeaderTitle('{% trans "Stock" %}'); +{% if roles.stock.view %} {% if setting_stock_recent %} addHeaderAction('recently-updated-stock', '{% trans "Recently Updated" %}', 'fa-clock'); @@ -163,7 +155,7 @@ loadStockTable($('#table-recently-updated-stock'), { {% endif %} {% if setting_stock_low %} -addHeaderAction('low-stock', '{% trans "Low Stock" %}', 'fa-shopping-cart'); +addHeaderAction('low-stock', '{% trans "Low Stock" %}', 'fa-flag'); loadSimplePartTable("#table-low-stock", "{% url 'api-part-list' %}", { params: { low_stock: true, @@ -314,4 +306,11 @@ loadSalesOrderTable("#table-so-overdue", { {% endif %} -{% endblock %} \ No newline at end of file +enableSidebar( + 'index', + { + hide_toggle: true, + } +); + +{% endblock %} diff --git a/InvenTree/templates/InvenTree/search.html b/InvenTree/templates/InvenTree/search.html index bc1eb013bf..2759cb0b8b 100644 --- a/InvenTree/templates/InvenTree/search.html +++ b/InvenTree/templates/InvenTree/search.html @@ -8,35 +8,18 @@ {% inventree_title %} | {% trans "Search Results" %} {% endblock %} +{% block breadcrumb_list %} +{% endblock %} + {% block content %} -

    - {% trans "Search Results" %} -

    - -
    - {% include "search_form.html" with query_text=query %} +
    +
    + {% include "search_form.html" with query_text=query %} +
    -{% if query %} -{% else %} -
    -

    {% trans "Enter a search query" %}

    -
    -{% endif %} - -
    -
      -
    -
    -
    -
      -
    • -
      - -
      -
    • -
    +
    {% endblock %} @@ -45,65 +28,50 @@ {{ block.super }} function addItemTitle(title) { - // Add header block to the results list - $('#search-item-list').append( - `
  • ${title}
  • ` - ); + addSidebarHeader({ + text: title, + }); } function addItem(label, title, icon, options) { - // Add a search itme to the action list - $('#search-item-list').append( - `
  • - - - ${title} - - - - -
  • ` - ); + + // Construct a "badge" to add to the sidebar item + var badge = ` + + + + `; + + addSidebarItem({ + label: label, + text: title, + icon: icon, + content_after: badge + }); // Add a results table - $('#search-result-list').append( - `
  • -

    ${title}

    -
    -
  • ` + $('#detail-panels').append( + `
    +
    +

    ${title}

    +
    +
    +
    +
    +
    ` ); - // Hide the results table - $(`#search-result-${label}`).hide(); - - // Add callback when the action is clicked - $(`#search-item-${label}`).click(function() { - - // Hide all childs - $('#search-result-list').children('li').each(function() { - $(this).hide(); - }); - - // Show the one we want - $(`#search-result-${label}`).fadeIn(); - - // Remove css class from all action items - $("#search-item-list").children('li').each(function() { - $(this).removeClass('index-action-selected'); - }); - - // Add css class to the action we are interested in - $(`#search-item-${label}`).addClass('index-action-selected'); - }); - // Connect a callback to the table $(`#table-${label}`).on('load-success.bs.table', function() { var count = $(`#table-${label}`).bootstrapTable('getData').length; - $(`#badge-${label}`).html(count); + var badge = $(`#sidebar-badge-${label}`); + + badge.html(count); if (count > 0) { - $(`#badge-${label}`).addClass('badge-orange'); + badge.removeClass('bg-dark'); + badge.addClass('bg-primary'); } }); } @@ -258,7 +226,11 @@ {% endif %} + enableSidebar( + 'search', + { + hide_toggle: 'true', + } + ); - - -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/InvenTree/templates/InvenTree/settings/barcode.html b/InvenTree/templates/InvenTree/settings/barcode.html index 32b85151d7..8532476b75 100644 --- a/InvenTree/templates/InvenTree/settings/barcode.html +++ b/InvenTree/templates/InvenTree/settings/barcode.html @@ -11,7 +11,6 @@ {% block content %} - {% include "InvenTree/settings/header.html" %} {% include "InvenTree/settings/setting.html" with key="BARCODE_ENABLE" icon="fa-qrcode" %} diff --git a/InvenTree/templates/InvenTree/settings/build.html b/InvenTree/templates/InvenTree/settings/build.html index 6d16512a99..d567197959 100644 --- a/InvenTree/templates/InvenTree/settings/build.html +++ b/InvenTree/templates/InvenTree/settings/build.html @@ -11,7 +11,6 @@ {% block content %}
    - {% include "InvenTree/settings/header.html" %} {% include "InvenTree/settings/setting.html" with key="BUILDORDER_REFERENCE_PREFIX" %} {% include "InvenTree/settings/setting.html" with key="BUILDORDER_REFERENCE_REGEX" %} diff --git a/InvenTree/templates/InvenTree/settings/category.html b/InvenTree/templates/InvenTree/settings/category.html index 9eb595ddde..f90d1e8d11 100644 --- a/InvenTree/templates/InvenTree/settings/category.html +++ b/InvenTree/templates/InvenTree/settings/category.html @@ -7,6 +7,12 @@ {% trans "Category Settings" %} {% endblock %} +{% block actions %} + +{% endblock %} + {% block content %}
    @@ -21,12 +27,6 @@
    -
    - -
    -
    diff --git a/InvenTree/templates/InvenTree/settings/currencies.html b/InvenTree/templates/InvenTree/settings/currencies.html index 08434cc704..706f836317 100644 --- a/InvenTree/templates/InvenTree/settings/currencies.html +++ b/InvenTree/templates/InvenTree/settings/currencies.html @@ -11,32 +11,33 @@ {% block content %} - {% include "InvenTree/settings/header.html" %} {% include "InvenTree/settings/setting.html" with key="INVENTREE_DEFAULT_CURRENCY" icon="fa-globe" %} - -
    - - + - + + {% for rate in rates %} - + + + + {% endfor %} + - diff --git a/InvenTree/templates/InvenTree/settings/global.html b/InvenTree/templates/InvenTree/settings/global.html index 1723f36b72..60ae84f001 100644 --- a/InvenTree/templates/InvenTree/settings/global.html +++ b/InvenTree/templates/InvenTree/settings/global.html @@ -12,7 +12,6 @@ {% block content %}
    {% trans "Base Currency" %} {{ base_currency }}
    {% trans "Exchange Rates" %}{% trans "Exchange Rates" %}
    {{ rate.currency }} {{ rate.value }}{{ rate.currency }}
    {% trans "Last Update" %} + {% if rates_updated %} {{ rates_updated }} {% else %} @@ -45,7 +46,7 @@
    {% csrf_token %} - +
    - {% include "InvenTree/settings/header.html" %} {% include "InvenTree/settings/setting.html" with key="INVENTREE_INSTANCE" icon="fa-info-circle" %} {% include "InvenTree/settings/setting.html" with key="INVENTREE_INSTANCE_TITLE" icon="fa-info-circle" %} diff --git a/InvenTree/templates/InvenTree/settings/header.html b/InvenTree/templates/InvenTree/settings/header.html deleted file mode 100644 index d60a4dd784..0000000000 --- a/InvenTree/templates/InvenTree/settings/header.html +++ /dev/null @@ -1,12 +0,0 @@ -{% load i18n %} - -- - - - - - - - - diff --git a/InvenTree/templates/InvenTree/settings/login.html b/InvenTree/templates/InvenTree/settings/login.html new file mode 100644 index 0000000000..a52e35fb12 --- /dev/null +++ b/InvenTree/templates/InvenTree/settings/login.html @@ -0,0 +1,32 @@ +{% extends "panel.html" %} +{% load i18n %} +{% load inventree_extras %} + +{% block label %}login{% endblock %} + + +{% block heading %} +{% trans "Login Settings" %} +{% endblock %} + +{% block content %} + +
    {% trans "Setting" %}{% trans "Value" %}{% trans "Description" %}
    + + {% include "InvenTree/settings/setting.html" with key="LOGIN_ENABLE_SSO" icon="fa-user-shield" %} + {% include "InvenTree/settings/setting.html" with key="LOGIN_ENABLE_PWD_FORGOT" icon="fa-user-lock" %} + {% include "InvenTree/settings/setting.html" with key="LOGIN_MAIL_REQUIRED" icon="fa-at" %} + {% include "InvenTree/settings/setting.html" with key="LOGIN_ENFORCE_MFA" icon='fa-key' %} + + + + + {% include "InvenTree/settings/setting.html" with key="LOGIN_ENABLE_REG" icon="fa-user-plus" %} + {% include "InvenTree/settings/setting.html" with key="LOGIN_SIGNUP_MAIL_TWICE" icon="fa-at" %} + {% include "InvenTree/settings/setting.html" with key="LOGIN_SIGNUP_PWD_TWICE" icon="fa-user-lock" %} + {% include "InvenTree/settings/setting.html" with key="LOGIN_SIGNUP_SSO_AUTO" icon="fa-key" %} + {% include "InvenTree/settings/setting.html" with key="SIGNUP_GROUP" icon="fa-users" %} + +
    {% trans 'Signup' %}
    + +{% endblock %} diff --git a/InvenTree/templates/InvenTree/settings/mixins/settings.html b/InvenTree/templates/InvenTree/settings/mixins/settings.html new file mode 100644 index 0000000000..57218b3699 --- /dev/null +++ b/InvenTree/templates/InvenTree/settings/mixins/settings.html @@ -0,0 +1,16 @@ +{% load i18n %} +{% load plugin_extras %} + +
    +

    {% trans "Settings" %}

    +
    + +{% plugin_settings plugin_key as plugin_settings %} + + + + {% for setting in plugin_settings %} + {% include "InvenTree/settings/setting.html" with key=setting plugin=plugin %} + {% endfor %} + +
    \ No newline at end of file diff --git a/InvenTree/templates/InvenTree/settings/mixins/urls.html b/InvenTree/templates/InvenTree/settings/mixins/urls.html new file mode 100644 index 0000000000..0a7b029762 --- /dev/null +++ b/InvenTree/templates/InvenTree/settings/mixins/urls.html @@ -0,0 +1,27 @@ +{% load i18n %} +{% load inventree_extras %} + +
    +

    {% trans "URLs" %}

    +
    +{% define plugin.base_url as base %} +

    {% blocktrans %}The Base-URL for this plugin is {{ base }}.{% endblocktrans %}

    + + + + + + + + + + + {% for key, entry in plugin.urlpatterns.reverse_dict.items %}{% if key %} + + + + + + {% endif %}{% endfor %} + +
    {% trans "Name" %}{% trans "URL" %}
    {{key}}{{entry.1}}{% trans 'Open in new tab' %}
    diff --git a/InvenTree/templates/InvenTree/settings/navbar.html b/InvenTree/templates/InvenTree/settings/navbar.html deleted file mode 100644 index ebf24bffb1..0000000000 --- a/InvenTree/templates/InvenTree/settings/navbar.html +++ /dev/null @@ -1,121 +0,0 @@ -{% load i18n %} - - \ No newline at end of file diff --git a/InvenTree/templates/InvenTree/settings/part.html b/InvenTree/templates/InvenTree/settings/part.html index ee662d6905..4f9841aaaf 100644 --- a/InvenTree/templates/InvenTree/settings/part.html +++ b/InvenTree/templates/InvenTree/settings/part.html @@ -9,15 +9,13 @@ {% block content %} -

    {% trans "Part Options" %}

    - - {% include "InvenTree/settings/header.html" %} {% include "InvenTree/settings/setting.html" with key="PART_IPN_REGEX" %} {% include "InvenTree/settings/setting.html" with key="PART_ALLOW_DUPLICATE_IPN" %} {% include "InvenTree/settings/setting.html" with key="PART_ALLOW_EDIT_IPN" %} - {% include "InvenTree/settings/setting.html" with key="PART_SHOW_QUANTITY_IN_FORMS" icon="fa-hashtag" %} + {% include "InvenTree/settings/setting.html" with key="PART_NAME_FORMAT" %} + {% include "InvenTree/settings/setting.html" with key="PART_SHOW_PRICE_HISTORY" icon="fa-history" %} {% include "InvenTree/settings/setting.html" with key="PART_SHOW_PRICE_IN_FORMS" icon="fa-dollar-sign" %} {% include "InvenTree/settings/setting.html" with key="PART_SHOW_PRICE_IN_BOM" icon="fa-dollar-sign" %} {% include "InvenTree/settings/setting.html" with key="PART_SHOW_RELATED" icon="fa-random" %} @@ -41,28 +39,34 @@
    -

    {% trans "Part Import" %}

    - - - +
    +
    +

    {% trans "Part Import" %}

    + {% include "spacer.html" %} +
    + +
    +
    +
    - {% include "InvenTree/settings/header.html" %} {% include "InvenTree/settings/setting.html" with key="PART_SHOW_IMPORT" icon="fa-file-upload" %}
    - - -

    {% trans "Part Parameter Templates" %}

    - -
    - +
    + +

    {% trans "Part Parameter Templates" %}

    + {% include "spacer.html" %} +
    + +
    +
    diff --git a/InvenTree/templates/InvenTree/settings/plugin.html b/InvenTree/templates/InvenTree/settings/plugin.html new file mode 100644 index 0000000000..caee7c92bf --- /dev/null +++ b/InvenTree/templates/InvenTree/settings/plugin.html @@ -0,0 +1,147 @@ +{% extends "panel.html" %} +{% load i18n %} +{% load inventree_extras %} +{% load plugin_extras %} + +{% block label %}plugin{% endblock %} + + +{% block heading %} +{% trans "Plugin Settings" %} +{% endblock %} + +{% block content %} + +
    + {% trans "Changing the settings below require you to immediatly restart InvenTree. Do not change this while under active usage." %} +
    + +
    +
    + + {% include "InvenTree/settings/setting.html" with key="ENABLE_PLUGINS_SCHEDULE" icon="fa-calendar-alt" %} + {% include "InvenTree/settings/setting.html" with key="ENABLE_PLUGINS_EVENTS" icon="fa-reply-all" %} + {% include "InvenTree/settings/setting.html" with key="ENABLE_PLUGINS_URL" icon="fa-link" %} + {% include "InvenTree/settings/setting.html" with key="ENABLE_PLUGINS_NAVIGATION" icon="fa-sitemap" %} + {% include "InvenTree/settings/setting.html" with key="ENABLE_PLUGINS_APP" icon="fa-rocket" %} + +
    +
    + +
    +
    +

    {% trans "Plugins" %}

    + {% include "spacer.html" %} +
    + {% url 'admin:plugin_pluginconfig_changelist' as url %} + {% include "admin_button.html" with url=url %} + +
    +
    +
    + +
    + + + + + + + + + + + + + {% plugin_list as pl_list %} + {% for plugin_key, plugin in pl_list.items %} + {% mixin_enabled plugin 'urls' as urls %} + {% mixin_enabled plugin 'settings' as settings %} + + + + + + + + + {% endfor %} + + {% inactive_plugin_list as in_pl_list %} + {% if in_pl_list %} + + + {% for plugin_key, plugin in in_pl_list.items %} + + + + + + {% endfor %} + {% endif %} + +
    {% trans "Admin" %}{% trans "Name" %}{% trans "Author" %}{% trans "Date" %}{% trans "Version" %}
    + {% if user.is_staff and perms.plugin.change_pluginconfig %} + {% url 'admin:plugin_pluginconfig_change' plugin.pk as url %} + {% include "admin_button.html" with url=url %} + {% endif %} + {{ plugin.human_name }} - {{plugin_key}} + {% define plugin.registered_mixins as mixin_list %} + + {% if mixin_list %} + {% for mixin in mixin_list %} + + {{ mixin.human_name }} + + {% endfor %} + {% endif %} + + {% if plugin.website %} + + {% endif %} + {{ plugin.author }}{{ plugin.pub_date }}{% if plugin.version %}{{ plugin.version }}{% endif %}
    {% trans 'Inactive plugins' %}
    + {% if user.is_staff and perms.plugin.change_pluginconfig %} + {% url 'admin:plugin_pluginconfig_change' plugin.pk as url %} + {% include "admin_button.html" with url=url %} + {% endif %} + {{plugin.name}} - {{plugin.key}}
    +
    + + +{% plugin_errors as pl_errors %} +{% if pl_errors %} +
    +
    +

    {% trans "Plugin Error Stack" %}

    + {% include "spacer.html" %} +
    +
    + +
    + + + + + + + + + + + {% for stage, errors in pl_errors.items %} + {% for error_detail in errors %} + {% for name, message in error_detail.items %} + + + + + + {% endfor %} + {% endfor %} + {% endfor %} + +
    {% trans "Stage" %}{% trans "Name" %}{% trans "Message" %}
    {{ stage }}{{ name }}{{ message }}
    +
    +{% endif %} + +{% endblock %} diff --git a/InvenTree/templates/InvenTree/settings/plugin_settings.html b/InvenTree/templates/InvenTree/settings/plugin_settings.html new file mode 100644 index 0000000000..a670d71c34 --- /dev/null +++ b/InvenTree/templates/InvenTree/settings/plugin_settings.html @@ -0,0 +1,140 @@ +{% extends "panel.html" %} +{% load i18n %} +{% load inventree_extras %} +{% load plugin_extras %} + +{% block label %}plugin-{{plugin_key}}{% endblock %} + + +{% block heading %} +{% blocktrans with name=plugin.human_name %}Plugin details for {{name}}{% endblocktrans %} +{% endblock %} + +{% block content %} + +
    +
    +

    {% trans "Plugin information" %}

    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + {% if plugin.website %} + + + + + + {% endif %} + {% if plugin.license %} + + + + + + {% endif %} +
    {% trans "Name" %}{{ plugin.human_name }}{% include "clip.html" %}
    {% trans "Author" %}{{ plugin.author }}{% include "clip.html" %}
    {% trans "Description" %}{{ plugin.description }}{% include "clip.html" %}
    {% trans "Date" %}{{ plugin.pub_date }}{% include "clip.html" %}
    {% trans "Version" %} + {% if plugin.version %} + {{ plugin.version }}{% include "clip.html" %} + {% else %} + {% trans 'no version information supplied' %} + {% endif %} +
    {% trans "Website" %}{{ plugin.website }}{% include "clip.html" %}
    {% trans "License" %}{{ plugin.license }}{% include "clip.html" %}
    +
    + + {% if plugin.is_package == False %} +
    + {% trans 'The code information is pulled from the latest git commit for this plugin. It might not reflect official version numbers or information but the actual code running.' %} +
    + + {% endif %} +
    +
    +

    {% trans "Package information" %}

    +
    + + + + + + + + + + + + + {% if plugin.is_package == False %} + + + + + + + + + + + + + + + + + {% endif %} + + + + + + + + + + +
    {% trans "Installation method" %} + {% if plugin.is_package %} + {% trans "This plugin was installed as a package" %} + {% else %} + {% trans "This plugin was found in a local InvenTree path" %} + {% endif %} +
    {% trans "Installation path" %}{{ plugin.package_path }}
    {% trans "Commit Author" %}{{ plugin.package.author }} - {{ plugin.package.mail }}{% include "clip.html" %}
    {% trans "Commit Date" %}{{ plugin.package.date }}{% include "clip.html" %}
    {% trans "Commit Hash" %}{{ plugin.package.hash }}{% include "clip.html" %}
    {% trans "Commit Message" %}{{ plugin.package.message }}{% include "clip.html" %}
    {% trans "Sign Status" %}{% if plugin.package.verified %}{{ plugin.package.verified }}: {% endif%}{{ plugin.sign_state.msg }}
    {% trans "Sign Key" %}{{ plugin.package.key }}{% include "clip.html" %}
    +
    +
    +
    + +{% mixin_enabled plugin 'settings' as settings %} +{% if settings %} + {% include 'InvenTree/settings/mixins/settings.html' %} +{% endif %} + +{% mixin_enabled plugin 'urls' as urls %} +{% if urls %} + {% include 'InvenTree/settings/mixins/urls.html' %} +{% endif %} + +{% endblock %} diff --git a/InvenTree/templates/InvenTree/settings/po.html b/InvenTree/templates/InvenTree/settings/po.html index f8a114bb12..f1ad603db5 100644 --- a/InvenTree/templates/InvenTree/settings/po.html +++ b/InvenTree/templates/InvenTree/settings/po.html @@ -9,7 +9,6 @@ {% block content %} - {% include "InvenTree/settings/header.html" %} {% include "InvenTree/settings/setting.html" with key="PURCHASEORDER_REFERENCE_PREFIX" %} diff --git a/InvenTree/templates/InvenTree/settings/report.html b/InvenTree/templates/InvenTree/settings/report.html index b9a2bd6aec..a25ace23ce 100644 --- a/InvenTree/templates/InvenTree/settings/report.html +++ b/InvenTree/templates/InvenTree/settings/report.html @@ -11,11 +11,11 @@ {% block content %}
    - {% include "InvenTree/settings/header.html" %} - {% include "InvenTree/settings/setting.html" with key="REPORT_DEFAULT_PAGE_SIZE" %} - {% include "InvenTree/settings/setting.html" with key="REPORT_DEBUG_MODE" %} - {% include "InvenTree/settings/setting.html" with key="REPORT_ENABLE_TEST_REPORT" %} + {% include "InvenTree/settings/setting.html" with key="REPORT_ENABLE" icon="fa-file-pdf" %} + {% include "InvenTree/settings/setting.html" with key="REPORT_DEFAULT_PAGE_SIZE" icon="fa-print" %} + {% include "InvenTree/settings/setting.html" with key="REPORT_DEBUG_MODE" icon="fa-laptop-code" %} + {% include "InvenTree/settings/setting.html" with key="REPORT_ENABLE_TEST_REPORT" icon="fa-vial" %}
    diff --git a/InvenTree/templates/InvenTree/settings/setting.html b/InvenTree/templates/InvenTree/settings/setting.html index de3439a647..743e37bf8e 100644 --- a/InvenTree/templates/InvenTree/settings/setting.html +++ b/InvenTree/templates/InvenTree/settings/setting.html @@ -1,10 +1,12 @@ {% load inventree_extras %} {% load i18n %} -{% if user_setting %} - {% setting_object key user=request.user as setting %} +{% if plugin %} +{% setting_object key plugin=plugin as setting %} +{% elif user_setting %} +{% setting_object key user=request.user as setting %} {% else %} - {% setting_object key as setting %} +{% setting_object key as setting %} {% endif %} @@ -13,34 +15,31 @@ {% endif %} - {% trans setting.name %} + {{ setting.name }} + + {{ setting.description }} + {% if setting.is_bool %} -
    - +
    +
    {% else %}
    - {% if setting.value %} - {{ setting.value }} + {{ setting.value }} {% else %} - {% trans "No value set" %} + {% trans "No value set" %} {% endif %} - {{ setting.units }} +
    + +
    {% endif %} - - {% trans setting.description %} - -
    - -
    - - \ No newline at end of file + diff --git a/InvenTree/templates/InvenTree/settings/settings.html b/InvenTree/templates/InvenTree/settings/settings.html index beb7f5eb04..ba27187747 100644 --- a/InvenTree/templates/InvenTree/settings/settings.html +++ b/InvenTree/templates/InvenTree/settings/settings.html @@ -3,13 +3,17 @@ {% load i18n %} {% load static %} {% load inventree_extras %} +{% load plugin_extras %} + +{% block breadcrumb_list %} +{% endblock %} {% block page_title %} {% inventree_title %} | {% trans "Settings" %} {% endblock %} -{% block menubar %} -{% include "InvenTree/settings/navbar.html" %} +{% block sidebar %} +{% include "InvenTree/settings/sidebar.html" %} {% endblock %} {% block content %} @@ -20,10 +24,12 @@ {% include "InvenTree/settings/user_search.html" %} {% include "InvenTree/settings/user_labels.html" %} {% include "InvenTree/settings/user_reports.html" %} +{% include "InvenTree/settings/user_display.html" %} {% if user.is_staff %} {% include "InvenTree/settings/global.html" %} +{% include "InvenTree/settings/login.html" %} {% include "InvenTree/settings/barcode.html" %} {% include "InvenTree/settings/currencies.html" %} {% include "InvenTree/settings/report.html" %} @@ -34,6 +40,17 @@ {% include "InvenTree/settings/po.html" %} {% include "InvenTree/settings/so.html" %} +{% plugins_enabled as plug %} +{% if plug %} +{% include "InvenTree/settings/plugin.html" %} +{% plugin_list as pl_list %} +{% for plugin_key, plugin in pl_list.items %} + {% if plugin.registered_mixins %} + {% include "InvenTree/settings/plugin_settings.html" %} + {% endif %} +{% endfor %} +{% endif %} + {% endif %} {% endblock %} @@ -45,29 +62,68 @@ {% block js_ready %} {{ block.super }} -$('table').find('.btn-edit-setting').click(function() { +// Callback for when boolean settings are edited +$('table').find('.boolean-setting').change(function() { + var setting = $(this).attr('setting'); var pk = $(this).attr('pk'); - var url = `/settings/${pk}/edit/`; + var plugin = $(this).attr('plugin'); + var user = $(this).attr('user'); - if ($(this).attr('user')){ - url += `user/`; + var checked = this.checked; + + // Global setting by default + var url = `/api/settings/global/${pk}/`; + + if (plugin) { + url = `/api/plugin/settings/${pk}/`; + } else if (user) { + url = `/api/settings/user/${pk}/`; } - launchModalForm( + inventreePut( url, { - success: function(response) { - - if (response.is_bool) { - var enabled = response.value.toLowerCase() == 'true'; - $(`#setting-value-${setting}`).prop('checked', enabled); - } else { - $(`#setting-value-${setting}`).html(response.value); - } + value: checked.toString(), + }, + { + method: 'PATCH', + onSuccess: function(data) { + }, + error: function(xhr) { + showApiError(xhr, url); } } ); + +}); + +// Callback for when non-boolean settings are edited +$('table').find('.btn-edit-setting').click(function() { + var setting = $(this).attr('setting'); + var pk = $(this).attr('pk'); + var plugin = $(this).attr('plugin'); + var is_global = true; + + if ($(this).attr('user')){ + is_global = false; + } + + var title = ''; + + if (plugin != null) { + title = '{% trans "Edit Plugin Setting" %}'; + } else if (is_global) { + title = '{% trans "Edit Global Setting" %}'; + } else { + title = '{% trans "Edit User Setting" %}'; + } + + editSetting(pk, { + plugin: plugin, + global: is_global, + title: title, + }); }); $("#edit-user").on('click', function() { @@ -171,8 +227,8 @@ $('#cat-param-table').inventreeTable({ title: '{% trans "Default Value" %}', sortable: 'true', formatter: function(value, row, index, field) { - var bEdit = ""; - var bDel = ""; + var bEdit = ""; + var bDel = ""; var html = value html += "
    " + bEdit + bDel + "
    "; @@ -254,24 +310,24 @@ $("#param-table").inventreeTable({ columns: [ { field: 'pk', - title: 'ID', + title: '{% trans "ID" %}', visible: false, switchable: false, }, { field: 'name', - title: 'Name', + title: '{% trans "Name" %}', sortable: 'true', }, { field: 'units', - title: 'Units', + title: '{% trans "Units" %}', sortable: 'true', }, { formatter: function(value, row, index, field) { - var bEdit = ""; - var bDel = ""; + var bEdit = ""; + var bDel = ""; var html = "
    " + bEdit + bDel + "
    "; @@ -317,15 +373,13 @@ $("#import-part").click(function() { launchModalForm("{% url 'api-part-import' %}?reset", {}); }); - -enableNavbar({ - label: 'settings', - toggleId: '#item-menu-toggle', +{% plugins_enabled as plug %} +{% if plug %} +$("#install-plugin").click(function() { + installPlugin(); }); +{% endif %} -attachNavCallbacks({ - name: 'settings', - default: 'account' -}); +enableSidebar('settings'); {% endblock %} diff --git a/InvenTree/templates/InvenTree/settings/sidebar.html b/InvenTree/templates/InvenTree/settings/sidebar.html new file mode 100644 index 0000000000..85e0b4ce94 --- /dev/null +++ b/InvenTree/templates/InvenTree/settings/sidebar.html @@ -0,0 +1,63 @@ +{% load i18n %} +{% load static %} +{% load inventree_extras %} +{% load plugin_extras %} + +{% trans "User Settings" as text %} +{% include "sidebar_header.html" with text=text icon='fa-user-cog' %} + +{% trans "Account Settings" as text %} +{% include "sidebar_item.html" with label='account' text=text icon="fa-sign-in-alt" %} +{% trans "Display Settings" as text %} +{% include "sidebar_item.html" with label='user-display' text=text icon="fa-desktop" %} +{% trans "Home Page" as text %} +{% include "sidebar_item.html" with label='user-home' text=text icon="fa-home" %} +{% trans "Search Settings" as text %} +{% include "sidebar_item.html" with label='user-search' text=text icon="fa-search" %} +{% trans "Label Printing" as text %} +{% include "sidebar_item.html" with label='user-labels' text=text icon="fa-tag" %} +{% trans "Reporting" as text %} +{% include "sidebar_item.html" with label='user-reports' text=text icon="fa-file-pdf" %} + +{% if user.is_staff %} + +{% trans "Global Settings" as text %} +{% include "sidebar_header.html" with text=text icon='fa-cogs' %} + +{% trans "Server Configuration" as text %} +{% include "sidebar_item.html" with label='server' text=text icon="fa-server" %} +{% trans "Login Settings" as text %} +{% include "sidebar_item.html" with label='login' text=text icon="fa-fingerprint" %} +{% trans "Barcode Support" as text %} +{% include "sidebar_item.html" with label='barcodes' text=text icon="fa-qrcode" %} +{% trans "Currencies" as text %} +{% include "sidebar_item.html" with label='currencies' text=text icon="fa-dollar-sign" %} +{% trans "Reporting" as text %} +{% include "sidebar_item.html" with label='reporting' text=text icon="fa-file-pdf" %} +{% trans "Parts" as text %} +{% include "sidebar_item.html" with label='parts' text=text icon="fa-shapes" %} +{% trans "Categories" as text %} +{% include "sidebar_item.html" with label='category' text=text icon="fa-sitemap" %} +{% trans "Stock" as text %} +{% include "sidebar_item.html" with label='stock' text=text icon="fa-boxes" %} +{% trans "Build Orders" as text %} +{% include "sidebar_item.html" with label='build-order' text=text icon="fa-tools" %} +{% trans "Purchase Orders" as text %} +{% include "sidebar_item.html" with label='purchase-order' text=text icon="fa-shopping-cart" %} +{% trans "Sales Orders" as text %} +{% include "sidebar_item.html" with label='sales-order' text=text icon="fa-truck" %} + +{% plugins_enabled as plug %} +{% if plug %} +{% include "sidebar_header.html" with text="Plugin Settings" %} +{% include "sidebar_item.html" with label='plugin' text="Plugins" icon="fa-plug" %} + +{% plugin_list as pl_list %} +{% for plugin_key, plugin in pl_list.items %} +{% if plugin.registered_mixins %} +{% include "sidebar_item.html" with label='plugin-'|add:plugin_key text=plugin.human_name %} +{% endif %} +{% endfor %} +{% endif %} + +{% endif %} \ No newline at end of file diff --git a/InvenTree/templates/InvenTree/settings/so.html b/InvenTree/templates/InvenTree/settings/so.html index 2fe5680d71..e6fde3a093 100644 --- a/InvenTree/templates/InvenTree/settings/so.html +++ b/InvenTree/templates/InvenTree/settings/so.html @@ -10,7 +10,6 @@ {% block content %} - {% include "InvenTree/settings/header.html" %} {% include "InvenTree/settings/setting.html" with key="SALESORDER_REFERENCE_PREFIX" %} diff --git a/InvenTree/templates/InvenTree/settings/stock.html b/InvenTree/templates/InvenTree/settings/stock.html index e05def09a6..a3c0940c1f 100644 --- a/InvenTree/templates/InvenTree/settings/stock.html +++ b/InvenTree/templates/InvenTree/settings/stock.html @@ -10,9 +10,7 @@ {% block content %}
    - {% include "InvenTree/settings/header.html" %} - {% include "InvenTree/settings/setting.html" with key="STOCK_GROUP_BY_PART" icon="fa-layer-group" %} {% include "InvenTree/settings/setting.html" with key="STOCK_ENABLE_EXPIRY" icon="fa-stopwatch" %} {% include "InvenTree/settings/setting.html" with key="STOCK_STALE_DAYS" icon="fa-calendar" %} {% include "InvenTree/settings/setting.html" with key="STOCK_ALLOW_EXPIRED_SALE" icon="fa-truck" %} diff --git a/InvenTree/templates/InvenTree/settings/user.html b/InvenTree/templates/InvenTree/settings/user.html index 922e9ebc79..32bc4d43e7 100644 --- a/InvenTree/templates/InvenTree/settings/user.html +++ b/InvenTree/templates/InvenTree/settings/user.html @@ -2,6 +2,9 @@ {% load i18n %} {% load inventree_extras %} +{% load socialaccount %} +{% load crispy_forms_tags %} +{% load user_sessions i18n %} {% block label %}account{% endblock %} @@ -9,15 +12,20 @@ {% trans "Account Settings" %} {% endblock %} -{% block content %} -
    -
    - {% trans "Edit" %} -
    -
    - {% trans "Set Password" %} -
    +{% block actions %} +{% inventree_demo_mode as demo %} +{% if not demo %} +
    + {% trans "Set Password" %}
    +
    + {% trans "Edit" %} +
    +{% endif %} +{% endblock %} + +{% block content %} +{% mail_configured as mail_conf %}
    @@ -32,77 +40,237 @@ - - - -
    {% trans "Last Name" %} {{ user.last_name }}
    {% trans "Email Address" %}{{ user.email }}
    -
    -

    {% trans "Theme Settings" %}

    -
    +
    +
    +
    +

    {% trans "Email" %}

    +
    +
    -
    +
    + {% if user.emailaddress_set.all %} +

    {% trans 'The following email addresses are associated with your account:' %}

    -
    + {% csrf_token %} - -
    -
    -
    - -
    +
    + + {% for emailaddress in user.emailaddress_set.all %} +
    +
    + + + {% if emailaddress.verified %} + {% trans "Verified" %} + {% else %} + {% trans "Unverified" %} + {% endif %} + {% if emailaddress.primary %}{% trans "Primary" %}{% endif %}
    -
    -
    - -
    +
    + {% endfor %} + +
    + + + +
    + + -
    + {% else %} +
    + {% trans 'Warning:'%} + {% trans "You currently do not have any email address set up. You should really add an email address so you can receive notifications, reset your password, etc." %} +
    -
    -

    {% trans "Language Settings" %}

    + {% endif %} +
    + + {% if can_add_email %} +
    +
    {% trans "Add Email Address" %}
    + +
    + {% csrf_token %} + {{ add_email_form|crispy }} + +
    +
    + {% endif %}
    -
    -
    - {% csrf_token %} - -
    - -
    -
    - -
    -
    +
    +

    {% trans "Social Accounts" %}

    -
    -

    {% trans "Help the translation efforts!" %}

    -

    {% blocktrans with link="https://crowdin.com/project/inventree" %}Native language translation of the InvenTree web application is community contributed via crowdin. Contributions are welcomed and encouraged.{% endblocktrans %}

    + +
    + {% if social_form.accounts %} +

    {% blocktrans %}You can sign in to your account using any of the following third party accounts:{% endblocktrans %}

    + + +
    + {% csrf_token %} + +
    + {% if social_form.non_field_errors %} +
    {{ social_form.non_field_errors }}
    + {% endif %} + + {% for base_account in social_form.accounts %} + {% with base_account.get_provider_account as account %} +
    + +
    + {% endwith %} + {% endfor %} + +
    + +
    + +
    + +
    + + {% else %} +

    {% trans 'You currently have no social network accounts connected to this account.' %}

    + {% endif %} +
    + +
    +
    {% trans 'Add a 3rd Party Account' %}
    +
    + {% include "socialaccount/snippets/provider_list.html" with process="connect" %} +
    + {% include "socialaccount/snippets/login_extra.html" %}
    +
    +
    +

    {% trans "Multifactor" %}

    +
    + +
    + {% if user.staticdevice_set.all or user.totpdevice_set.all %} +

    {% trans 'You have these factors available:' %}

    + + + + + + + + {% for token in user.totpdevice_set.all %} + + + + + {% endfor %} + {% for token in user.staticdevice_set.all %} + + + + + {% endfor %} + +
    TypeName
    {% trans 'TOTP' %}{{ token.name }}
    {% trans 'Static' %}{{ token.name }}
    + + {% else %} +

    {% trans 'Warning:'%} + {% trans "You currently do not have any factors set up." %} +

    + + {% endif %} +
    + +
    +
    {% trans "Change factors" %}
    + {% trans "Setup multifactor" %} + {% if user.staticdevice_set.all or user.totpdevice_set.all %} + {% trans "Remove multifactor" %} + {% endif %} +
    +
    + +
    +
    +
    +

    {% trans "Active Sessions" %}

    + {% include "spacer.html" %} +
    + {% if session_list.count > 1 %} +
    + {% csrf_token %} + +
    + {% endif %} +
    +
    +
    + +
    + {% trans "unknown on unknown" as unknown_on_unknown %} + {% trans "unknown" as unknown %} + + + + + + + + + {% for object in session_list %} + + + + + + {% endfor %} +
    {% trans "IP Address" %}{% trans "Device" %}{% trans "Last Activity" %}
    {{ object.ip }}{{ object.user_agent|device|default_if_none:unknown_on_unknown|safe }} + {% if object.session_key == session_key %} + {% blocktrans with time=object.last_activity|timesince %}{{ time }} ago (this session){% endblocktrans %} + {% else %} + {% blocktrans with time=object.last_activity|timesince %}{{ time }} ago{% endblocktrans %} + {% endif %} +
    +
    +
    +{% endblock %} + +{% block js_ready %} +(function() { +var message = "{% trans 'Do you really want to remove the selected email address?' %}"; +var actions = document.getElementsByName('action_remove'); +if (actions.length) { +actions[0].addEventListener("click", function(e) { +if (! confirm(message)) { +e.preventDefault(); +} +}); +} +})(); {% endblock %} \ No newline at end of file diff --git a/InvenTree/templates/InvenTree/settings/user_display.html b/InvenTree/templates/InvenTree/settings/user_display.html new file mode 100644 index 0000000000..9f5c22f991 --- /dev/null +++ b/InvenTree/templates/InvenTree/settings/user_display.html @@ -0,0 +1,106 @@ +{% extends "panel.html" %} + +{% load i18n %} +{% load inventree_extras %} + +{% block label %}user-display{% endblock %} + +{% block heading %} +{% trans "Display Settings" %} +{% endblock %} + +{% block content %} + +
    + + + {% include "InvenTree/settings/setting.html" with key="STICKY_HEADER" icon="fa-bars" user_setting=True %} + {% include "InvenTree/settings/setting.html" with key="FORMS_CLOSE_USING_ESCAPE" icon="fa-window-close" user_setting=True %} + {% include "InvenTree/settings/setting.html" with key="PART_SHOW_QUANTITY_IN_FORMS" icon="fa-hashtag" user_setting=True %} + +
    +
    + +
    +

    {% trans "Theme Settings" %}

    +
    + +
    + +
    +
    + {% csrf_token %} + + +
    + +
    + +
    +
    +
    +
    +
    + +
    +

    {% trans "Language Settings" %}

    +
    + +
    +
    +
    + {% csrf_token %} + + +
    + +
    + +
    +
    +

    {% trans "Some languages are not complete" %} + {% if ALL_LANG %} + . {% trans "Show only sufficent" %} + {% else %} + {% trans "and hidden." %} {% trans "Show them too" %} + {% endif %} +

    +
    +
    +
    +

    {% trans "Help the translation efforts!" %}

    +

    {% blocktrans with link="https://crowdin.com/project/inventree" %}Native language translation of the InvenTree web application is community contributed via crowdin. Contributions are welcomed and encouraged.{% endblocktrans %}

    +
    +
    + +{% endblock %} \ No newline at end of file diff --git a/InvenTree/templates/InvenTree/settings/user_homepage.html b/InvenTree/templates/InvenTree/settings/user_homepage.html index ccaae6fa8a..54e3bdcefd 100644 --- a/InvenTree/templates/InvenTree/settings/user_homepage.html +++ b/InvenTree/templates/InvenTree/settings/user_homepage.html @@ -13,9 +13,9 @@
    - {% include "InvenTree/settings/header.html" %} - {% include "InvenTree/settings/setting.html" with key="HOMEPAGE_PART_STARRED" icon='fa-star' user_setting=True %} + {% include "InvenTree/settings/setting.html" with key="HOMEPAGE_PART_STARRED" icon='fa-bell' user_setting=True %} + {% include "InvenTree/settings/setting.html" with key="HOMEPAGE_CATEGORY_STARRED" icon='fa-bell' user_setting=True %} {% include "InvenTree/settings/setting.html" with key="HOMEPAGE_PART_LATEST" icon='fa-history' user_setting=True %} {% include "InvenTree/settings/setting.html" with key="PART_RECENT_COUNT" icon="fa-clock" user_setting=True %} {% include "InvenTree/settings/setting.html" with key="HOMEPAGE_BOM_VALIDATION" user_setting=True %} diff --git a/InvenTree/templates/InvenTree/settings/user_labels.html b/InvenTree/templates/InvenTree/settings/user_labels.html index bd3d041544..a2d9b7b89c 100644 --- a/InvenTree/templates/InvenTree/settings/user_labels.html +++ b/InvenTree/templates/InvenTree/settings/user_labels.html @@ -13,7 +13,6 @@
    - {% include "InvenTree/settings/header.html" %} {% include "InvenTree/settings/setting.html" with key="LABEL_INLINE" icon='fa-tag' user_setting=True %} diff --git a/InvenTree/templates/InvenTree/settings/user_reports.html b/InvenTree/templates/InvenTree/settings/user_reports.html index 8e93d41457..186adfe31c 100644 --- a/InvenTree/templates/InvenTree/settings/user_reports.html +++ b/InvenTree/templates/InvenTree/settings/user_reports.html @@ -13,7 +13,6 @@
    - {% include "InvenTree/settings/header.html" %} {% include "InvenTree/settings/setting.html" with key="REPORT_INLINE" icon='fa-file-pdf' user_setting=True %} diff --git a/InvenTree/templates/InvenTree/settings/user_search.html b/InvenTree/templates/InvenTree/settings/user_search.html index c06bfaec8d..51df53ee6b 100644 --- a/InvenTree/templates/InvenTree/settings/user_search.html +++ b/InvenTree/templates/InvenTree/settings/user_search.html @@ -13,9 +13,10 @@
    - {% include "InvenTree/settings/header.html" %} {% include "InvenTree/settings/setting.html" with key="SEARCH_PREVIEW_RESULTS" user_setting=True icon='fa-search' %} + {% include "InvenTree/settings/setting.html" with key="SEARCH_SHOW_STOCK_LEVELS" user_setting=True icon='fa-boxes' %} + {% include "InvenTree/settings/setting.html" with key="SEARCH_HIDE_INACTIVE_PARTS" user_setting=True icon='fa-eye-slash' %}
    diff --git a/InvenTree/templates/InvenTree/settings/user_settings.html b/InvenTree/templates/InvenTree/settings/user_settings.html index a9b9b0a82f..80c01f6919 100644 --- a/InvenTree/templates/InvenTree/settings/user_settings.html +++ b/InvenTree/templates/InvenTree/settings/user_settings.html @@ -13,7 +13,6 @@
    - {% include "InvenTree/settings/header.html" %}
    diff --git a/InvenTree/templates/about.html b/InvenTree/templates/about.html index 0744d6be8f..34884da9d1 100644 --- a/InvenTree/templates/about.html +++ b/InvenTree/templates/about.html @@ -6,11 +6,9 @@ diff --git a/InvenTree/templates/account/base.html b/InvenTree/templates/account/base.html new file mode 100644 index 0000000000..ea3795e87c --- /dev/null +++ b/InvenTree/templates/account/base.html @@ -0,0 +1,133 @@ +{% load static %} +{% load i18n %} +{% load inventree_extras %} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {% inventree_title %} | {% block head_title %}{% endblock %} + + +{% block extra_head %} +{% endblock %} + + + + +
    +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/InvenTree/templates/account/email_confirm.html b/InvenTree/templates/account/email_confirm.html new file mode 100644 index 0000000000..1bdd051fdc --- /dev/null +++ b/InvenTree/templates/account/email_confirm.html @@ -0,0 +1,31 @@ +{% extends "account/base.html" %} + +{% load i18n %} +{% load account %} + +{% block head_title %}{% trans "Confirm Email Address" %}{% endblock %} + + +{% block content %} +

    {% trans "Confirm Email Address" %}

    + +{% if confirmation %} + +{% user_display confirmation.email_address.user as user_display %} + +

    {% blocktrans with confirmation.email_address.email as email %}Please confirm that {{ email }} is an email address for user {{ user_display }}.{% endblocktrans %}

    + +
    +{% csrf_token %} + +
    + +{% else %} + +{% url 'account_email' as email_url %} + +

    {% blocktrans %}This email confirmation link expired or is invalid. Please issue a new email confirmation request.{% endblocktrans %}

    + +{% endif %} + +{% endblock %} diff --git a/InvenTree/templates/account/login.html b/InvenTree/templates/account/login.html new file mode 100644 index 0000000000..6e62560bfa --- /dev/null +++ b/InvenTree/templates/account/login.html @@ -0,0 +1,62 @@ +{% extends "account/base.html" %} + +{% load inventree_extras %} +{% load i18n account socialaccount crispy_forms_tags inventree_extras %} + +{% block head_title %}{% trans "Sign In" %}{% endblock %} + +{% block content %} + +{% settings_value 'LOGIN_ENABLE_REG' as enable_reg %} +{% settings_value 'LOGIN_ENABLE_PWD_FORGOT' as enable_pwd_forgot %} +{% settings_value 'LOGIN_ENABLE_SSO' as enable_sso %} +{% mail_configured as mail_conf %} +{% inventree_demo_mode as demo %} + +

    {% trans "Sign In" %}

    + +{% if enable_reg %} +{% get_providers as socialaccount_providers %} +{% if socialaccount_providers %} +

    {% blocktrans with site.name as site_name %}Please sign in with one +of your existing third party accounts or sign up +for a account and sign in below:{% endblocktrans %}

    +{% else %} +

    {% blocktrans %}If you have not created an account yet, then please +sign up first.{% endblocktrans %}

    +{% endif %} +{% endif %} + + + +{% if enable_sso %} +
    +

    {% trans 'or use SSO' %}

    +
    + {% include "socialaccount/snippets/provider_list.html" with process="login" %} +
    +{% include "socialaccount/snippets/login_extra.html" %} +{% endif %} + +{% endblock %} diff --git a/InvenTree/templates/account/logout.html b/InvenTree/templates/account/logout.html new file mode 100644 index 0000000000..df37c76be4 --- /dev/null +++ b/InvenTree/templates/account/logout.html @@ -0,0 +1,25 @@ +{% extends "account/base.html" %} + +{% load i18n %} + +{% block head_title %}{% trans "Sign Out" %}{% endblock %} + +{% block content %} +

    {% trans "Sign Out" %}

    + +

    {% trans 'Are you sure you want to sign out?' %}

    + +
    + {% csrf_token %} + {% if redirect_field_value %} + + {% endif %} +
    +
    + {% trans "Back to Site" %} + +
    +
    + + +{% endblock %} diff --git a/InvenTree/templates/account/password_reset.html b/InvenTree/templates/account/password_reset.html new file mode 100644 index 0000000000..1eeb5c6179 --- /dev/null +++ b/InvenTree/templates/account/password_reset.html @@ -0,0 +1,30 @@ +{% extends "account/base.html" %} + +{% load i18n account crispy_forms_tags inventree_extras %} + +{% block head_title %}{% trans "Password Reset" %}{% endblock %} + +{% block content %} + +{% settings_value 'LOGIN_ENABLE_PWD_FORGOT' as enable_pwd_forgot %} +{% mail_configured as mail_conf %} + +

    {% trans "Password Reset" %}

    + {% if user.is_authenticated %} + {% include "account/snippets/already_logged_in.html" %} + {% endif %} + + {% if mail_conf and enable_pwd_forgot %} +

    {% trans "Forgotten your password? Enter your email address below, and we'll send you an email allowing you to reset it." %}

    + +
    + {% csrf_token %} + {{ form|crispy }} + +
    + {% else %} +
    +

    {% trans "This function is currently disabled. Please contact an administrator." %}

    +
    + {% endif %} +{% endblock %} diff --git a/InvenTree/templates/account/password_reset_from_key.html b/InvenTree/templates/account/password_reset_from_key.html new file mode 100644 index 0000000000..c75543f67f --- /dev/null +++ b/InvenTree/templates/account/password_reset_from_key.html @@ -0,0 +1,25 @@ +{% extends "account/base.html" %} + +{% load i18n crispy_forms_tags %} +{% block head_title %}{% trans "Change Password" %}{% endblock %} + +{% block content %} +

    {% if token_fail %}{% trans "Bad Token" %}{% else %}{% trans "Change Password" %}{% endif %}

    + + {% if token_fail %} + {% url 'account_reset_password' as passwd_reset_url %} +

    {% blocktrans %}The password reset link was invalid, possibly because it has already been used. Please request a new password reset.{% endblocktrans %}

    + {% else %} + {% if form %} +
    + {% csrf_token %} + {{ form|crispy }} +
    + +
    +
    + {% else %} +

    {% trans 'Your password is now changed.' %}

    + {% endif %} + {% endif %} +{% endblock %} diff --git a/InvenTree/templates/account/signup.html b/InvenTree/templates/account/signup.html new file mode 100644 index 0000000000..f2972ba30b --- /dev/null +++ b/InvenTree/templates/account/signup.html @@ -0,0 +1,40 @@ +{% extends "account/base.html" %} + +{% load i18n crispy_forms_tags inventree_extras %} + +{% block head_title %}{% trans "Signup" %}{% endblock %} + +{% block content %} +{% settings_value 'LOGIN_ENABLE_REG' as enable_reg %} +{% settings_value 'LOGIN_ENABLE_SSO' as enable_sso %} + +

    {% trans "Sign Up" %}

    + +

    {% blocktrans %}Already have an account? Then please sign in.{% endblocktrans %}

    + +{% if enable_reg %} + + +{% if enable_sso %} +
    +

    {% trans 'Or use a SSO-provider for signup' %}

    +
    + {% include "socialaccount/snippets/provider_list.html" with process="login" %} +
    +{% include "socialaccount/snippets/login_extra.html" %} +{% endif %} + +{% else %} +
    +

    {% trans "This function is currently disabled. Please contact an administrator." %}

    +
    +{% endif %} + +{% endblock %} diff --git a/InvenTree/templates/admin_button.html b/InvenTree/templates/admin_button.html new file mode 100644 index 0000000000..ebe767f9ac --- /dev/null +++ b/InvenTree/templates/admin_button.html @@ -0,0 +1,4 @@ +{% load i18n %} + \ No newline at end of file diff --git a/InvenTree/templates/allauth_2fa/authenticate.html b/InvenTree/templates/allauth_2fa/authenticate.html new file mode 100644 index 0000000000..1a392fc44a --- /dev/null +++ b/InvenTree/templates/allauth_2fa/authenticate.html @@ -0,0 +1,15 @@ +{% extends "account/base.html" %} +{% load i18n crispy_forms_tags %} + +{% block content %} +

    {% trans "Two-Factor Authentication" %}

    + + +{% endblock %} diff --git a/InvenTree/templates/allauth_2fa/backup_tokens.html b/InvenTree/templates/allauth_2fa/backup_tokens.html new file mode 100644 index 0000000000..2257baecb3 --- /dev/null +++ b/InvenTree/templates/allauth_2fa/backup_tokens.html @@ -0,0 +1,33 @@ +{% extends "account/base.html" %} +{% load i18n %} + +{% block content %} +

    + {% trans "Two-Factor Authentication Backup Tokens" %} +

    + +{% if backup_tokens %} + {% if reveal_tokens %} +
      + {% for token in backup_tokens %} +
    • {{ token.token }}
    • + {% endfor %} +
    + {% else %} + {% trans 'Backup tokens have been generated, but are not revealed here for security reasons. Press the button below to generate new ones.' %} + {% endif %} +{% else %} + {% trans 'No tokens. Press the button below to generate some.' %} +{% endif %} + +
    +
    + {% csrf_token %} + +
    +
    +{% trans "Back to settings" %} + +{% endblock %} diff --git a/InvenTree/templates/allauth_2fa/remove.html b/InvenTree/templates/allauth_2fa/remove.html new file mode 100644 index 0000000000..fb9d15ae32 --- /dev/null +++ b/InvenTree/templates/allauth_2fa/remove.html @@ -0,0 +1,18 @@ +{% extends "account/base.html" %} +{% load i18n %} + +{% block content %} +

    + {% trans "Disable Two-Factor Authentication" %} +

    + +

    {% trans "Are you sure?" %}

    + +
    + {% csrf_token %} + +
    + +{% endblock %} diff --git a/InvenTree/templates/allauth_2fa/setup.html b/InvenTree/templates/allauth_2fa/setup.html new file mode 100644 index 0000000000..2b74ad9c47 --- /dev/null +++ b/InvenTree/templates/allauth_2fa/setup.html @@ -0,0 +1,42 @@ +{% extends "account/base.html" %} +{% load i18n crispy_forms_tags %} + +{% block content %} +

    + {% trans "Setup Two-Factor Authentication" %} +

    + +

    + {% trans 'Step 1' %}: +

    + +

    + {% trans 'Scan the QR code below with a token generator of your choice (for instance Google Authenticator).' %} +

    + +
    + +
    +
    + +

    + {% trans 'Step 2' %}: +

    + +

    + {% trans 'Input a token generated by the app:' %} +

    + +
    + {% csrf_token %} + {{ form|crispy }} + + +
    + + +{% endblock %} diff --git a/InvenTree/templates/attachment_button.html b/InvenTree/templates/attachment_button.html new file mode 100644 index 0000000000..d220f4829d --- /dev/null +++ b/InvenTree/templates/attachment_button.html @@ -0,0 +1,8 @@ +{% load i18n %} + + + \ No newline at end of file diff --git a/InvenTree/templates/attachment_delete.html b/InvenTree/templates/attachment_delete.html deleted file mode 100644 index 4ee7f03cb1..0000000000 --- a/InvenTree/templates/attachment_delete.html +++ /dev/null @@ -1,7 +0,0 @@ -{% extends "modal_delete_form.html" %} -{% load i18n %} - -{% block pre_form_content %} -{% trans "Are you sure you want to delete this attachment?" %} -
    -{% endblock %} \ No newline at end of file diff --git a/InvenTree/templates/attachment_table.html b/InvenTree/templates/attachment_table.html index 18a4da9acc..cd73e864a1 100644 --- a/InvenTree/templates/attachment_table.html +++ b/InvenTree/templates/attachment_table.html @@ -1,10 +1,8 @@ {% load i18n %}
    -
    - +
    + {% include "filter_list.html" with id="attachments" %}
    diff --git a/InvenTree/templates/base.html b/InvenTree/templates/base.html index c2316ce4b0..05d0712e6a 100644 --- a/InvenTree/templates/base.html +++ b/InvenTree/templates/base.html @@ -4,6 +4,9 @@ {% settings_value 'BARCODE_ENABLE' as barcodes %} {% settings_value 'REPORT_ENABLE_TEST_REPORT' as test_report_enabled %} +{% settings_value "REPORT_ENABLE" as report_enabled %} +{% settings_value "SERVER_RESTART_REQUIRED" as server_restart_required %} +{% inventree_demo_mode as demo_mode %} @@ -33,15 +36,15 @@ - - - - + + + + - + @@ -68,47 +71,63 @@ {% include "navbar.html" %} -
    +
    -
    +
    + +
    + + {% block alerts %} +
    + + {% if server_restart_required and not demo_mode %} + + {% endif %} +
    + {% endblock %} -
    - {% block pre_content %} - {% endblock %} -
    + {% block breadcrumb_list %} +
    - -
    - {% block menubar %} - - {% endblock %} -
    - -
    - {% block content %} - - {% endblock %} - -
    - - -{% block post_content %} -{% endblock %} - -
    - -{% include 'modals.html' %} -{% include 'about.html' %} -{% include 'notification.html' %} -
    - + {% include 'modals.html' %} + {% include 'about.html' %}
    @@ -116,11 +135,9 @@ - + - - @@ -143,10 +160,9 @@ - + - @@ -168,12 +184,13 @@ + - - - + + + {% block js_load %} {% endblock %} @@ -186,15 +203,25 @@ $(document).ready(function () { inventreeDocReady(); - showCachedAlerts(); - {% if barcodes %} $('#barcode-scan').click(function() { barcodeScanDialog(); }); {% endif %} - moment.locale('{{request.LANGUAGE_CODE}}'); + moment.locale('{{ request.LANGUAGE_CODE }}'); + + // Account notifications + {% if messages %} + {% for message in messages %} + showMessage( + '{{ message }}', + { + style: 'info', + } + ); + {% endfor %} + {% endif %} }); diff --git a/InvenTree/templates/clip.html b/InvenTree/templates/clip.html index b0edff2fb3..6051e439b4 100644 --- a/InvenTree/templates/clip.html +++ b/InvenTree/templates/clip.html @@ -1,5 +1,5 @@ {% load i18n %} - + \ No newline at end of file diff --git a/InvenTree/templates/collapse.html b/InvenTree/templates/collapse.html deleted file mode 100644 index 5624f34094..0000000000 --- a/InvenTree/templates/collapse.html +++ /dev/null @@ -1,23 +0,0 @@ -{% block collapse_preamble %} -{% endblock %} -
    -
    -
    -
    - - {% block collapse_heading %} - {% endblock %} -
    -
    -
    -
    - {% block collapse_content %} - {% endblock %} -
    -
    -
    -
    \ No newline at end of file diff --git a/InvenTree/templates/collapse_index.html b/InvenTree/templates/collapse_index.html deleted file mode 100644 index 6e918d7217..0000000000 --- a/InvenTree/templates/collapse_index.html +++ /dev/null @@ -1,19 +0,0 @@ -{% block collapse_preamble %} -{% endblock %} -
    -
    -
    - - {% block collapse_heading %} - {% endblock %} -
    -
    -
    - {% block collapse_content %} - {% endblock %} -
    -
    -
    -
    \ No newline at end of file diff --git a/InvenTree/templates/email/build_order_required_stock.html b/InvenTree/templates/email/build_order_required_stock.html new file mode 100644 index 0000000000..5f4015da27 --- /dev/null +++ b/InvenTree/templates/email/build_order_required_stock.html @@ -0,0 +1,39 @@ +{% extends "email/email.html" %} + +{% load i18n %} +{% load inventree_extras %} + +{% block title %} +{% trans "Stock is required for the following build order" %}
    +{% blocktrans with build=build.reference part=part.full_name quantity=build.quantity %}Build order {{ build }} - building {{ quantity }} x {{ part }}{% endblocktrans %} +
    +

    {% trans "Click on the following link to view this build order" %}: {{ link }}

    +{% endblock title %} + +{% block body %} +{% trans "The following parts are low on required stock" %} + + + {% trans "Part" %} + {% trans "Required Quantity" %} + {% trans "Available" %} + + +{% for line in lines %} + + + {{ line.part.full_name }}{% if part.description %} - {{ part.description }}{% endif %} + + + {% decimal line.required %} {% if line.part.units %}{{ line.part.units }}{% endif %} + + {% decimal line.available %} {% if line.part.units %}{{ line.part.units }}{% endif %} + + +{% endfor %} + +{% endblock body %} + +{% block footer_prefix %} +

    {% blocktrans with part=part.name %}You are receiving this email because you are subscribed to notifications for this part {% endblocktrans %}.

    +{% endblock footer_prefix %} diff --git a/InvenTree/templates/email/email.html b/InvenTree/templates/email/email.html new file mode 100644 index 0000000000..97e9a40f37 --- /dev/null +++ b/InvenTree/templates/email/email.html @@ -0,0 +1,43 @@ +{% load i18n %} +{% load static %} +{% load inventree_extras %} + + + + {% block header %} + + + + + {% endblock %} + + {% block body %} + + {% block body_row %} + + {% endblock %} + + {% endblock %} + + {% block footer %} + + + + {% endblock %} + +
    + {% block header_row %} +

    {% block title %}{% endblock %}

    + {% block subtitle %} + + {% endblock %} + {% endblock %} +
    + {% block footer_prefix %} + + {% endblock %} +

    {% trans "InvenTree version" %}: {% inventree_version %} - inventree.readthedocs.io

    + {% block footer_suffix %} + + {% endblock %} +
    diff --git a/InvenTree/templates/email/low_stock_notification.html b/InvenTree/templates/email/low_stock_notification.html new file mode 100644 index 0000000000..f922187f33 --- /dev/null +++ b/InvenTree/templates/email/low_stock_notification.html @@ -0,0 +1,32 @@ +{% extends "email/email.html" %} + +{% load i18n %} +{% load inventree_extras %} + +{% block title %} +{% blocktrans with part=part.name %} The available stock for {{ part }} has fallen below the configured minimum level{% endblocktrans %} +{% if link %} +

    {% trans "Click on the following link to view this part" %}: {{ link }}

    +{% endif %} +{% endblock title %} + + +{% block body %} + + {% trans "Part" %} + {% trans "Total Stock" %} + {% trans "Available" %} + {% trans "Minimum Quantity" %} + + + + {{ part.full_name }} + {% decimal part.total_stock %} + {% decimal part.available_stock %} + {% decimal part.minimum_stock %} + +{% endblock body %} + +{% block footer_prefix %} +

    {% blocktrans with part=part.name %}You are receiving this email because you are subscribed to notifications for this part {% endblocktrans %}.

    +{% endblock footer_prefix %} diff --git a/InvenTree/templates/filter_list.html b/InvenTree/templates/filter_list.html new file mode 100644 index 0000000000..ae29428924 --- /dev/null +++ b/InvenTree/templates/filter_list.html @@ -0,0 +1 @@ +
    \ No newline at end of file diff --git a/InvenTree/templates/hover_image.html b/InvenTree/templates/hover_image.html index 556a8db140..e9f781655b 100644 --- a/InvenTree/templates/hover_image.html +++ b/InvenTree/templates/hover_image.html @@ -1,6 +1,6 @@ {% load static %} -
    +
    {% if hover %} {% endif %} diff --git a/InvenTree/templates/js/dynamic/nav.js b/InvenTree/templates/js/dynamic/nav.js index cf652724ed..eed098f162 100644 --- a/InvenTree/templates/js/dynamic/nav.js +++ b/InvenTree/templates/js/dynamic/nav.js @@ -2,57 +2,27 @@ */ /* exported - attachNavCallbacks, + activatePanel, + addSidebarHeader, + addSidebarItem, + addSidebarLink, + enableBreadcrumbTree, + enableSidebar, onPanelLoad, */ /* -* Attach callbacks to navigation bar elements. -* -* Searches for elements with the class 'nav-toggle'. -* A callback is added to each element, -* to display the matching panel. -* -* The 'id' of the .nav-toggle element should be of the form "select-", -* and point to a matching "panel-" -*/ -function attachNavCallbacks(options={}) { - - $('.nav-toggle').click(function() { - var el = $(this); - - // Find the matching "panel" element - var panelName = el.attr('id').replace('select-', ''); - - activatePanel(panelName, options); - }); - - var panelClass = options.name || 'unknown'; - - /* Look for a default panel to initialize - * First preference = URL parameter e.g. ?display=part-stock - * Second preference = localStorage - * Third preference = default - */ - var defaultPanel = $.urlParam('display') || localStorage.getItem(`inventree-selected-panel-${panelClass}`) || options.default; - - if (defaultPanel) { - activatePanel(defaultPanel); - } -} - - -function activatePanel(panelName, options={}) { - - var panelClass = options.name || 'unknown'; + * Activate (display) the selected panel + */ +function activatePanel(label, panel_name, options={}) { // First, cause any other panels to "fade out" $('.panel-visible').hide(); $('.panel-visible').removeClass('panel-visible'); - + // Find the target panel - var panel = `#panel-${panelName}`; - var select = `#select-${panelName}`; + var panel = `#panel-${panel_name}`; + var select = `#select-${panel_name}`; // Check that the selected panel (and select) exist if ($(panel).length && $(select).length) { @@ -60,22 +30,22 @@ function activatePanel(panelName, options={}) { } else { // Either the select or the panel are not displayed! // Iterate through the available 'select' elements until one matches - panelName = null; + panel_name = null; - $('.nav-toggle').each(function() { - var panel_name = $(this).attr('id').replace('select-', ''); + $('.sidebar-selector').each(function() { + var name = $(this).attr('id').replace('select-', ''); - if ($(`#panel-${panel_name}`).length && (panelName == null)) { - panelName = panel_name; + if ($(`#panel-${name}`).length && (panel_name == null)) { + panel_name = name; } - panel = `#panel-${panelName}`; - select = `#select-${panelName}`; + panel = `#panel-${panel_name}`; + select = `#select-${panel_name}`; }); } // Save the selected panel - localStorage.setItem(`inventree-selected-panel-${panelClass}`, panelName); + localStorage.setItem(`inventree-selected-panel-${label}`, panel_name); // Display the panel $(panel).addClass('panel-visible'); @@ -90,9 +60,9 @@ function activatePanel(panelName, options={}) { $('.list-group-item').removeClass('active'); // Find the associated selector - var selectElement = `#select-${panelName}`; + var selector = `#select-${panel_name}`; - $(selectElement).parent('.list-group-item').addClass('active'); + $(selector).addClass('active'); } @@ -113,3 +83,215 @@ function onPanelLoad(panel, callback) { }); } + + +/** + * Enable support for sidebar on this page + */ +function enableSidebar(label, options={}) { + + // Enable callbacks for sidebar buttons + $('.sidebar-selector').click(function() { + var el = $(this); + + // Find the matching panel element to display + var panel_name = el.attr('id').replace('select-', ''); + + activatePanel(label, panel_name, options); + }); + + /* Look for a "default" panel to initialize for this page + * + * - First preference = URL parameter e.g. ?display=part-stock + * - Second preference = local storage + * - Third preference = default + */ + + var selected_panel = $.urlParam('display') || localStorage.getItem(`inventree-selected-panel-${label}`) || options.default; + + if (selected_panel) { + activatePanel(label, selected_panel); + } else { + // Find the "first" available panel (according to the sidebar) + var selector = $('.sidebar-selector').first(); + + if (selector.exists()) { + var panel_name = selector.attr('id').replace('select-', ''); + activatePanel(label, panel_name); + } + } + + if (options.hide_toggle) { + // Hide the toggle button if specified + $('#sidebar-toggle').remove(); + } else { + $('#sidebar-toggle').click(function() { + // Add callback to "collapse" and "expand" the sidebar + + // By default, the menu is "expanded" + var state = localStorage.getItem(`inventree-menu-state-${label}`) || 'expanded'; + + // We wish to "toggle" the state! + setSidebarState(label, state == 'expanded' ? 'collapsed' : 'expanded'); + }); + } + + // Set the initial state (default = expanded) + var state = localStorage.getItem(`inventree-menu-state-${label}`) || 'expanded'; + + setSidebarState(label, state); + + // Finally, show the sidebar + $('#sidebar').show(); + +} + +/** + * Enable support for breadcrumb tree navigation on this page + */ +function enableBreadcrumbTree(options) { + + var label = options.label; + + if (!label) { + console.log('ERROR: enableBreadcrumbTree called without supplying label'); + return; + } + + var filters = options.filters || {}; + + inventreeGet( + options.url, + filters, + { + success: function(data) { + + // Data are returned from the InvenTree server as a flattened list; + // We need to convert this into a tree structure + + var nodes = {}; + var roots = []; + var node = null; + + for (var i = 0; i < data.length; i++) { + node = data[i]; + nodes[node.pk] = node; + node.selectable = false; + + if (options.processNode) { + node = options.processNode(node); + } + + node.state = { + expanded: node.pk == options.selected, + selected: node.pk == options.selected, + }; + } + + for (var i = 0; i < data.length; i++) { + node = data[i]; + + if (node.parent != null) { + if (nodes[node.parent].nodes) { + nodes[node.parent].nodes.push(node); + } else { + nodes[node.parent].nodes = [node]; + } + + if (node.state.expanded) { + while (node.parent != null) { + nodes[node.parent].state.expanded = true; + node = nodes[node.parent]; + } + } + + } else { + roots.push(node); + } + } + + $('#breadcrumb-tree').treeview({ + data: roots, + showTags: true, + enableLinks: true, + expandIcon: 'fas fa-chevron-right', + collapseIcon: 'fa fa-chevron-down', + }); + + } + } + ); + + $('#breadcrumb-tree-toggle').click(function() { + // Add callback to "collapse" and "expand" the sidebar + + // Toggle treeview visibilty + $('#breadcrumb-tree-collapse').toggle(); + + }); + +} + +/* + * Set the "toggle" state of the sidebar + */ +function setSidebarState(label, state) { + + if (state == 'collapsed') { + $('.sidebar-item-text').animate({ + 'opacity': 0.0, + 'font-size': '0%', + }, 100, function() { + $('.sidebar-item-text').hide(); + $('#sidebar-toggle-icon').removeClass('fa-chevron-left').addClass('fa-chevron-right'); + }); + } else { + $('.sidebar-item-text').show(); + $('#sidebar-toggle-icon').removeClass('fa-chevron-right').addClass('fa-chevron-left'); + $('.sidebar-item-text').animate({ + 'opacity': 1.0, + 'font-size': '100%', + }, 100); + } + + // Save the state of this sidebar + localStorage.setItem(`inventree-menu-state-${label}`, state); +} + + +/* + * Dynamically construct and insert a sidebar item into the sidebar at runtime. + * This mirrors the templated code in "sidebar_item.html" + */ +function addSidebarItem(options={}) { + + var html = ` + + + ${options.content_before || ''} + + + ${options.content_after || ''} + + `; + + $('#sidebar-list-group').append(html); +} + +/* + * Dynamicall construct and insert a sidebar header into the sidebar at runtime + * This mirrors the templated code in "sidebar_header.html" + */ +function addSidebarHeader(options={}) { + + var html = ` + +
    + + +
    +
    + `; + + $('#sidebar-list-group').append(html); +} diff --git a/InvenTree/templates/js/dynamic/settings.js b/InvenTree/templates/js/dynamic/settings.js index cb21e1fefc..133edfba20 100644 --- a/InvenTree/templates/js/dynamic/settings.js +++ b/InvenTree/templates/js/dynamic/settings.js @@ -1,6 +1,7 @@ {% load inventree_extras %} /* exported + editSetting, user_settings, global_settings, */ @@ -12,9 +13,93 @@ const user_settings = { {% endfor %} }; -{% global_settings as GLOBAL_SETTINGS %} +{% visible_global_settings as GLOBAL_SETTINGS %} const global_settings = { {% for key, value in GLOBAL_SETTINGS.items %} {{ key }}: {% primitive_to_javascript value %}, {% endfor %} }; + +/* + * Edit a setting value + */ +function editSetting(pk, options={}) { + + // Is this a global setting or a user setting? + var global = options.global || false; + + var plugin = options.plugin; + + var url = ''; + + if (plugin) { + url = `/api/plugin/settings/${pk}/`; + } else if (global) { + url = `/api/settings/global/${pk}/`; + } else { + url = `/api/settings/user/${pk}/`; + } + + // First, read the settings object from the server + inventreeGet(url, {}, { + success: function(response) { + + if (response.choices && response.choices.length > 0) { + response.type = 'choice'; + } + + // Construct the field + var fields = { + value: { + label: response.name, + help_text: response.description, + type: response.type, + choices: response.choices, + } + }; + + constructChangeForm(fields, { + url: url, + method: 'PATCH', + title: options.title, + processResults: function(data, fields, opts) { + + switch (data.type) { + case 'boolean': + // Convert to boolean value + data.value = data.value.toString().toLowerCase() == 'true'; + break; + case 'integer': + // Convert to integer value + data.value = parseInt(data.value.toString()); + break; + default: + break; + } + + return data; + }, + processBeforeUpload: function(data) { + // Convert value to string + data.value = data.value.toString(); + + return data; + }, + onSuccess: function(response) { + + var setting = response.key; + + if (response.type == 'boolean') { + var enabled = response.value.toString().toLowerCase() == 'true'; + $(`#setting-value-${setting}`).prop('checked', enabled); + } else { + $(`#setting-value-${setting}`).html(response.value); + } + } + }); + }, + error: function(xhr) { + showApiError(xhr, url); + } + }); +} diff --git a/InvenTree/templates/js/translated/api.js b/InvenTree/templates/js/translated/api.js index 841cf467ba..eadf2e2afc 100644 --- a/InvenTree/templates/js/translated/api.js +++ b/InvenTree/templates/js/translated/api.js @@ -2,8 +2,6 @@ {% load inventree_extras %} /* globals - renderErrorMessage, - showAlertDialog, */ /* exported @@ -56,6 +54,7 @@ function inventreeGet(url, filters={}, options={}) { data: filters, dataType: 'json', contentType: 'application/json', + async: (options.async == false) ? false : true, success: function(response) { if (options.success) { options.success(response); @@ -63,11 +62,17 @@ function inventreeGet(url, filters={}, options={}) { }, error: function(xhr, ajaxOptions, thrownError) { console.error('Error on GET at ' + url); - console.error(thrownError); + + if (thrownError) { + console.error('Error: ' + thrownError); + } + if (options.error) { options.error({ error: thrownError }); + } else { + showApiError(xhr, url); } } }); @@ -104,6 +109,8 @@ function inventreeFormDataUpload(url, data, options={}) { if (options.error) { options.error(xhr, status, error); + } else { + showApiError(xhr, url); } } }); @@ -139,6 +146,8 @@ function inventreePut(url, data={}, options={}) { } else { console.error(`Error on ${method} to '${url}' - STATUS ${xhr.status}`); console.error(thrownError); + + showApiError(xhr, url); } }, complete: function(xhr, status) { @@ -162,13 +171,15 @@ function inventreeDelete(url, options={}) { return inventreePut(url, {}, options); } - -function showApiError(xhr) { +/* + * Display a notification with error information + */ +function showApiError(xhr, url) { var title = null; var message = null; - switch (xhr.status) { + switch (xhr.status || 0) { // No response case 0: title = '{% trans "No Response" %}'; @@ -196,6 +207,11 @@ function showApiError(xhr) { title = '{% trans "Error 404: Resource Not Found" %}'; message = '{% trans "The requested resource could not be located on the server" %}'; break; + // Method not allowed + case 405: + title = '{% trans "Error 405: Method Not Allowed" %}'; + message = '{% trans "HTTP method not allowed at URL" %}'; + break; // Timeout case 408: title = '{% trans "Error 408: Timeout" %}'; @@ -207,8 +223,14 @@ function showApiError(xhr) { break; } - message += '
    '; - message += renderErrorMessage(xhr); + if (url) { + message += '
    '; + message += `URL: ${url}`; + } - showAlertDialog(title, message); + showMessage(title, { + style: 'danger', + icon: 'fas fa-server icon-red', + details: message, + }); } diff --git a/InvenTree/templates/js/translated/attachment.js b/InvenTree/templates/js/translated/attachment.js index 88c73ed3e3..44061403fa 100644 --- a/InvenTree/templates/js/translated/attachment.js +++ b/InvenTree/templates/js/translated/attachment.js @@ -6,10 +6,57 @@ */ /* exported + addAttachmentButtonCallbacks, loadAttachmentTable, reloadAttachmentTable, */ + +/* + * Add callbacks to buttons for creating new attachments. + * + * Note: Attachments can also be external links! + */ +function addAttachmentButtonCallbacks(url, fields={}) { + + // Callback for 'new attachment' button + $('#new-attachment').click(function() { + + var file_fields = { + attachment: {}, + comment: {}, + }; + + Object.assign(file_fields, fields); + + constructForm(url, { + fields: file_fields, + method: 'POST', + onSuccess: reloadAttachmentTable, + title: '{% trans "Add Attachment" %}', + }); + }); + + // Callback for 'new link' button + $('#new-attachment-link').click(function() { + + var link_fields = { + link: {}, + comment: {}, + }; + + Object.assign(link_fields, fields); + + constructForm(url, { + fields: link_fields, + method: 'POST', + onSuccess: reloadAttachmentTable, + title: '{% trans "Add Link" %}', + }); + }); +} + + function reloadAttachmentTable() { $('#attachment-table').bootstrapTable('refresh'); @@ -20,6 +67,10 @@ function loadAttachmentTable(url, options) { var table = options.table || '#attachment-table'; + setupFilterList('attachments', $(table), '#filter-list-attachments'); + + addAttachmentButtonCallbacks(url, options.fields || {}); + $(table).inventreeTable({ url: url, name: options.name || 'attachments', @@ -27,59 +78,84 @@ function loadAttachmentTable(url, options) { return '{% trans "No attachments found" %}'; }, sortable: true, - search: false, + search: true, queryParams: options.filters || {}, onPostBody: function() { // Add callback for 'edit' button $(table).find('.button-attachment-edit').click(function() { var pk = $(this).attr('pk'); - if (options.onEdit) { - options.onEdit(pk); - } + constructForm(`${url}${pk}/`, { + fields: { + link: {}, + comment: {}, + }, + processResults: function(data, fields, opts) { + // Remove the "link" field if the attachment is a file! + if (data.attachment) { + delete opts.fields.link; + } + }, + onSuccess: reloadAttachmentTable, + title: '{% trans "Edit Attachment" %}', + }); }); // Add callback for 'delete' button $(table).find('.button-attachment-delete').click(function() { var pk = $(this).attr('pk'); - if (options.onDelete) { - options.onDelete(pk); - } + constructForm(`${url}${pk}/`, { + method: 'DELETE', + confirmMessage: '{% trans "Confirm Delete" %}', + title: '{% trans "Delete Attachment" %}', + onSuccess: reloadAttachmentTable, + }); }); }, columns: [ { field: 'attachment', - title: '{% trans "File" %}', - formatter: function(value) { + title: '{% trans "Attachment" %}', + formatter: function(value, row) { - var icon = 'fa-file-alt'; + if (row.attachment) { + var icon = 'fa-file-alt'; - var fn = value.toLowerCase(); + var fn = value.toLowerCase(); - if (fn.endsWith('.pdf')) { - icon = 'fa-file-pdf'; - } else if (fn.endsWith('.xls') || fn.endsWith('.xlsx')) { - icon = 'fa-file-excel'; - } else if (fn.endsWith('.doc') || fn.endsWith('.docx')) { - icon = 'fa-file-word'; + if (fn.endsWith('.csv')) { + icon = 'fa-file-csv'; + } else if (fn.endsWith('.pdf')) { + icon = 'fa-file-pdf'; + } else if (fn.endsWith('.xls') || fn.endsWith('.xlsx')) { + icon = 'fa-file-excel'; + } else if (fn.endsWith('.doc') || fn.endsWith('.docx')) { + icon = 'fa-file-word'; + } else if (fn.endsWith('.zip') || fn.endsWith('.7z')) { + icon = 'fa-file-archive'; + } else { + var images = ['.png', '.jpg', '.bmp', '.gif', '.svg', '.tif']; + + images.forEach(function(suffix) { + if (fn.endsWith(suffix)) { + icon = 'fa-file-image'; + } + }); + } + + var split = value.split('/'); + var filename = split[split.length - 1]; + + var html = ` ${filename}`; + + return renderLink(html, value); + } else if (row.link) { + var html = ` ${row.link}`; + return renderLink(html, row.link); } else { - var images = ['.png', '.jpg', '.bmp', '.gif', '.svg', '.tif']; - - images.forEach(function(suffix) { - if (fn.endsWith(suffix)) { - icon = 'fa-file-image'; - } - }); + return '-'; } - - var split = value.split('/'); - var filename = split[split.length - 1]; - - var html = ` ${filename}`; - - return renderLink(html, value); } }, { diff --git a/InvenTree/templates/js/translated/barcode.js b/InvenTree/templates/js/translated/barcode.js index a1d6fb7adf..6be56d14f1 100644 --- a/InvenTree/templates/js/translated/barcode.js +++ b/InvenTree/templates/js/translated/barcode.js @@ -10,7 +10,6 @@ modalSetSubmitText, modalShowSubmitButton, modalSubmit, - showAlertOrCache, showQuestionDialog, */ @@ -36,7 +35,7 @@ function makeBarcodeInput(placeholderText='', hintText='') {
    - + @@ -59,7 +58,7 @@ function makeNotesField(options={}) {
    - + @@ -258,7 +257,7 @@ function barcodeDialog(title, options={}) { $(modal).modal({ backdrop: 'static', - keyboard: false, + keyboard: user_settings.FORMS_CLOSE_USING_ESCAPE, }); if (options.preShow) { @@ -480,10 +479,13 @@ function barcodeCheckIn(location_id) { $(modal).modal('hide'); if (status == 'success' && 'success' in response) { - showAlertOrCache('alert-success', response.success, true); + addCachedAlert(response.success); location.reload(); } else { - showAlertOrCache('alert-success', '{% trans "Error transferring stock" %}', false); + showMessage('{% trans "Error transferring stock" %}', { + style: 'danger', + icon: 'fas fa-times-circle', + }); } } } @@ -604,10 +606,12 @@ function scanItemsIntoLocation(item_id_list, options={}) { $(modal).modal('hide'); if (status == 'success' && 'success' in response) { - showAlertOrCache('alert-success', response.success, true); + addCachedAlert(response.success); location.reload(); } else { - showAlertOrCache('alert-danger', '{% trans "Error transferring stock" %}', false); + showMessage('{% trans "Error transferring stock" %}', { + style: 'danger', + }); } } } diff --git a/InvenTree/templates/js/translated/bom.js b/InvenTree/templates/js/translated/bom.js index ec37f71d15..5c93c7c8f2 100644 --- a/InvenTree/templates/js/translated/bom.js +++ b/InvenTree/templates/js/translated/bom.js @@ -2,6 +2,7 @@ /* globals constructForm, + exportFormatOptions, imageHoverIcon, inventreeGet, inventreePut, @@ -14,18 +15,343 @@ */ /* exported + constructBomUploadTable, + downloadBomTemplate, + exportBom, newPartFromBomWizard, loadBomTable, + loadUsedInTable, removeRowFromBomWizard, removeColFromBomWizard, + submitBomTable */ -/* BOM management functions. - * Requires follwing files to be loaded first: - * - api.js - * - part.js - * - modals.js + +/* Construct a table of data extracted from a BOM file. + * This data is used to import a BOM interactively. */ +function constructBomUploadTable(data, options={}) { + + if (!data.rows) { + // TODO: Error message! + return; + } + + function constructRow(row, idx, fields) { + // Construct an individual row from the provided data + + var field_options = { + hideLabels: true, + hideClearButton: true, + form_classes: 'bom-form-group', + }; + + function constructRowField(field_name) { + + var field = fields[field_name] || null; + + if (!field) { + return `Cannot render field '${field_name}`; + } + + field.value = row.data[field_name]; + + return constructField(`items_${field_name}_${idx}`, field, field_options); + + } + + // Construct form inputs + var sub_part = constructRowField('sub_part'); + var quantity = constructRowField('quantity'); + var reference = constructRowField('reference'); + var overage = constructRowField('overage'); + var variants = constructRowField('allow_variants'); + var inherited = constructRowField('inherited'); + var optional = constructRowField('optional'); + var note = constructRowField('note'); + + var buttons = `
    `; + + buttons += makeIconButton('fa-info-circle', 'button-row-data', idx, '{% trans "Display row data" %}'); + buttons += makeIconButton('fa-times icon-red', 'button-row-remove', idx, '{% trans "Remove row" %}'); + + buttons += `
    `; + + var html = ` + + ${sub_part} + ${quantity} + ${reference} + ${overage} + ${variants} + ${inherited} + ${optional} + ${note} + ${buttons} + `; + + $('#bom-import-table tbody').append(html); + + // Handle any errors raised by initial data import + if (row.data.errors.part) { + addFieldErrorMessage(`items_sub_part_${idx}`, row.data.errors.part); + } + + if (row.data.errors.quantity) { + addFieldErrorMessage(`items_quantity_${idx}`, row.data.errors.quantity); + } + + // Initialize the "part" selector for this row + initializeRelatedField( + { + name: `items_sub_part_${idx}`, + value: row.data.part, + api_url: '{% url "api-part-list" %}', + filters: { + component: true, + }, + model: 'part', + required: true, + auto_fill: false, + onSelect: function(data, field, opts) { + // TODO? + }, + } + ); + + // Add callback for "remove row" button + $(`#button-row-remove-${idx}`).click(function() { + $(`#items_${idx}`).remove(); + }); + + // Add callback for "show data" button + $(`#button-row-data-${idx}`).click(function() { + + var modal = createNewModal({ + title: '{% trans "Row Data" %}', + cancelText: '{% trans "Close" %}', + hideSubmitButton: true + }); + + // Prettify the original import data + var pretty = JSON.stringify( + { + columns: data.columns, + row: row.original, + }, undefined, 4 + ); + + var html = ` +
    +
    ${pretty}
    +
    `; + + modalSetContent(modal, html); + + $(modal).modal('show'); + + }); + } + + // Request API endpoint options + getApiEndpointOptions('{% url "api-bom-list" %}', function(response) { + + var fields = response.actions.POST; + + data.rows.forEach(function(row, idx) { + constructRow(row, idx, fields); + }); + }); +} + + +/* Extract rows from the BOM upload table, + * and submit data to the server + */ +function submitBomTable(part_id, options={}) { + + // Extract rows from the form + var rows = []; + + var idx_values = []; + + var url = '{% url "api-bom-import-submit" %}'; + + $('.bom-import-row').each(function() { + var idx = $(this).attr('idx'); + + idx_values.push(idx); + + // Extract each field from the row + rows.push({ + part: part_id, + sub_part: getFormFieldValue(`items_sub_part_${idx}`, {}), + quantity: getFormFieldValue(`items_quantity_${idx}`, {}), + reference: getFormFieldValue(`items_reference_${idx}`, {}), + overage: getFormFieldValue(`items_overage_${idx}`, {}), + allow_variants: getFormFieldValue(`items_allow_variants_${idx}`, {type: 'boolean'}), + inherited: getFormFieldValue(`items_inherited_${idx}`, {type: 'boolean'}), + optional: getFormFieldValue(`items_optional_${idx}`, {type: 'boolean'}), + note: getFormFieldValue(`items_note_${idx}`, {}), + }); + }); + + var data = { + items: rows, + }; + + var options = { + nested: { + items: idx_values, + } + }; + + getApiEndpointOptions(url, function(response) { + var fields = response.actions.POST; + + // Disable the "Submit BOM" button + $('#bom-submit').prop('disabled', true); + $('#bom-submit-icon').show(); + + inventreePut(url, data, { + method: 'POST', + success: function(response) { + window.location.href = `/part/${part_id}/?display=bom`; + }, + error: function(xhr) { + switch (xhr.status) { + case 400: + handleFormErrors(xhr.responseJSON, fields, options); + break; + default: + showApiError(xhr, url); + break; + } + + // Re-enable the submit button + $('#bom-submit').prop('disabled', false); + $('#bom-submit-icon').hide(); + } + }); + }); +} + + +function downloadBomTemplate(options={}) { + + var format = options.format; + + if (!format) { + format = inventreeLoad('bom-export-format', 'csv'); + } + + constructFormBody({}, { + title: '{% trans "Download BOM Template" %}', + fields: { + format: { + label: '{% trans "Format" %}', + help_text: '{% trans "Select file format" %}', + required: true, + type: 'choice', + value: format, + choices: exportFormatOptions(), + } + }, + onSubmit: function(fields, opts) { + var format = getFormFieldValue('format', fields['format'], opts); + + // Save the format for next time + inventreeSave('bom-export-format', format); + + // Hide the modal + $(opts.modal).modal('hide'); + + // Download the file + location.href = `{% url "bom-upload-template" %}?format=${format}`; + + } + }); +} + + +/** + * Export BOM (Bill of Materials) for the specified Part instance + */ +function exportBom(part_id, options={}) { + + constructFormBody({}, { + title: '{% trans "Export BOM" %}', + fields: { + format: { + label: '{% trans "Format" %}', + help_text: '{% trans "Select file format" %}', + required: true, + type: 'choice', + value: inventreeLoad('bom-export-format', 'csv'), + choices: exportFormatOptions(), + }, + cascade: { + label: '{% trans "Cascading" %}', + help_text: '{% trans "Download cascading / multi-level BOM" %}', + type: 'boolean', + value: inventreeLoad('bom-export-cascading', true), + }, + levels: { + label: '{% trans "Levels" %}', + help_text: '{% trans "Select maximum number of BOM levels to export (0 = all levels)" %}', + type: 'integer', + value: 0, + min_value: 0, + }, + parameter_data: { + label: '{% trans "Include Parameter Data" %}', + help_text: '{% trans "Include part parameter data in exported BOM" %}', + type: 'boolean', + value: inventreeLoad('bom-export-parameter_data', false), + }, + stock_data: { + label: '{% trans "Include Stock Data" %}', + help_text: '{% trans "Include part stock data in exported BOM" %}', + type: 'boolean', + value: inventreeLoad('bom-export-stock_data', false), + }, + manufacturer_data: { + label: '{% trans "Include Manufacturer Data" %}', + help_text: '{% trans "Include part manufacturer data in exported BOM" %}', + type: 'boolean', + value: inventreeLoad('bom-export-manufacturer_data', false), + }, + supplier_data: { + label: '{% trans "Include Supplier Data" %}', + help_text: '{% trans "Include part supplier data in exported BOM" %}', + type: 'boolean', + value: inventreeLoad('bom-export-supplier_data', false), + } + }, + onSubmit: function(fields, opts) { + + // Extract values from the form + var field_names = ['format', 'cascade', 'levels', 'parameter_data', 'stock_data', 'manufacturer_data', 'supplier_data']; + + var url = `/part/${part_id}/bom-download/?`; + + field_names.forEach(function(fn) { + var val = getFormFieldValue(fn, fields[fn], opts); + + // Update user preferences + inventreeSave(`bom-export-${fn}`, val); + + url += `${fn}=${val}&`; + }); + + $(opts.modal).modal('hide'); + + // Redirect to the BOM file download + location.href = url; + } + }); + +} function bomItemFields() { @@ -35,6 +361,18 @@ function bomItemFields() { hidden: true, }, sub_part: { + secondary: { + title: '{% trans "New Part" %}', + fields: function() { + var fields = partFields(); + + // Set to a "component" part + fields.component.value = true; + + return fields; + }, + groups: partGroups(), + } }, quantity: {}, reference: {}, @@ -131,7 +469,189 @@ function newPartFromBomWizard(e) { } -function loadBomTable(table, options) { +/* + * Launch a modal dialog displaying the "substitute parts" for a particular BomItem + * + * If editable, allows substitutes to be added and deleted + */ +function bomSubstitutesDialog(bom_item_id, substitutes, options={}) { + + // Reload data for the parent table + function reloadParentTable() { + if (options.table) { + options.table.bootstrapTable('refresh'); + } + } + + // Extract a list of all existing "substitute" id values + function getSubstituteIdValues(modal) { + + var id_values = []; + + $(modal).find('.substitute-row').each(function(el) { + var part = $(this).attr('part'); + id_values.push(part); + }); + + return id_values; + } + + function renderSubstituteRow(substitute) { + + var pk = substitute.pk; + + var part = substitute.part_detail; + + var thumb = thumbnailImage(part.thumbnail || part.image); + + var buttons = ''; + + buttons += makeIconButton('fa-times icon-red', 'button-row-remove', pk, '{% trans "Remove substitute part" %}'); + + // Render a single row + var html = ` + + + + ${thumb} ${part.full_name} + + + ${part.description} + ${part.stock} + ${buttons} + + `; + + return html; + } + + // Construct a table to render the rows + var rows = ''; + + substitutes.forEach(function(sub) { + rows += renderSubstituteRow(sub); + }); + + var part_thumb = thumbnailImage(options.sub_part_detail.thumbnail || options.sub_part_detail.image); + var part_name = options.sub_part_detail.full_name; + var part_desc = options.sub_part_detail.description; + + var html = ` +
    + {% trans "Base Part" %}
    + ${part_thumb} ${part_name} - ${part_desc} +
    + `; + + // Add a table of individual rows + html += ` + + + + + + + + + + + ${rows} + +
    {% trans "Part" %}{% trans "Description" %}{% trans "Stock" %}
    + `; + + html += ` +
    + {% trans "Select and add a new substitute part using the input below" %} +
    + `; + + // Add a callback to remove a row from the table + function addRemoveCallback(modal, element) { + $(modal).find(element).click(function() { + var pk = $(this).attr('pk'); + + var pre = ` +
    + {% trans "Are you sure you wish to remove this substitute part link?" %} +
    + `; + + constructForm(`/api/bom/substitute/${pk}/`, { + method: 'DELETE', + title: '{% trans "Remove Substitute Part" %}', + preFormContent: pre, + confirm: true, + onSuccess: function() { + $(modal).find(`#substitute-row-${pk}`).remove(); + reloadParentTable(); + } + }); + }); + } + + constructForm('{% url "api-bom-substitute-list" %}', { + method: 'POST', + fields: { + bom_item: { + hidden: true, + value: bom_item_id, + }, + part: { + required: false, + adjustFilters: function(query, opts) { + + var subs = getSubstituteIdValues(opts.modal); + + // Also exclude the "master" part (if provided) + if (options.sub_part) { + subs.push(options.sub_part); + } + + if (subs.length > 0) { + query.exclude_id = subs; + } + + return query; + } + }, + }, + preFormContent: html, + cancelText: '{% trans "Close" %}', + submitText: '{% trans "Add Substitute" %}', + title: '{% trans "Edit BOM Item Substitutes" %}', + afterRender: function(fields, opts) { + addRemoveCallback(opts.modal, '.button-row-remove'); + }, + preventClose: true, + onSuccess: function(response, opts) { + + // Clear the form + var field = { + type: 'related field', + }; + + updateFieldValue('part', null, field, opts); + + // Add the new substitute to the table + var row = renderSubstituteRow(response); + $(opts.modal).find('#substitute-table > tbody:last-child').append(row); + + // Add a callback to the new button + addRemoveCallback(opts.modal, `#button-row-remove-${response.pk}`); + + // Re-enable the "submit" button + $(opts.modal).find('#modal-form-submit').prop('disabled', false); + + // Reload the parent BOM table + reloadParentTable(); + } + }); + +} + + +function loadBomTable(table, options={}) { /* Load a BOM table with some configurable options. * * Following options are available: @@ -215,11 +735,19 @@ function loadBomTable(table, options) { var sub_part = row.sub_part_detail; - html += makePartIcons(row.sub_part_detail); + html += makePartIcons(sub_part); + + if (row.substitutes && row.substitutes.length > 0) { + html += makeIconBadge('fa-exchange-alt', '{% trans "Substitutes Available" %}'); + } + + if (row.allow_variants) { + html += makeIconBadge('fa-sitemap', '{% trans "Variant stock allowed" %}'); + } // Display an extra icon if this part is an assembly if (sub_part.assembly) { - var text = ``; + var text = ``; html += renderLink(text, `/part/${row.sub_part}/bom/`); } @@ -282,12 +810,26 @@ function loadBomTable(table, options) { var text = value; if (value == null || value <= 0) { - text = `{% trans "No Stock" %}`; + text = `{% trans "No Stock" %}`; } return renderLink(text, url); } }); + + cols.push({ + field: 'substitutes', + title: '{% trans "Substitutes" %}', + searchable: false, + sortable: true, + formatter: function(value, row) { + if (row.substitutes && row.substitutes.length > 0) { + return row.substitutes.length; + } else { + return `-`; + } + } + }); if (show_pricing) { cols.push({ @@ -344,7 +886,7 @@ function loadBomTable(table, options) { if (!row.inherited) { return yesNoLabel(false); } else if (row.part == options.parent_id) { - return '{% trans "Inherited" %}'; + return yesNoLabel(true); } else { // If this BOM item is inherited from a parent part return renderLink( @@ -408,18 +950,17 @@ function loadBomTable(table, options) { if (row.part == options.parent_id) { - var bValidate = ``; + var bValidate = makeIconButton('fa-check-circle icon-green', 'bom-validate-button', row.pk, '{% trans "Validate BOM Item" %}'); - var bValid = ``; + var bValid = makeIconButton('fa-check-double icon-green', 'bom-valid-button', row.pk, '{% trans "This line has been validated" %}', {disabled: true}); - var bEdit = ``; + var bSubs = makeIconButton('fa-exchange-alt icon-blue', 'bom-substitutes-button', row.pk, '{% trans "Edit substitute parts" %}'); - var bDelt = ``; + var bEdit = makeIconButton('fa-edit icon-blue', 'bom-edit-button', row.pk, '{% trans "Edit BOM Item" %}'); - var html = `
    `; + var bDelt = makeIconButton('fa-trash-alt icon-red', 'bom-delete-button', row.pk, '{% trans "Delete BOM Item" %}'); - html += bEdit; - html += bDelt; + var html = `
    `; if (!row.validated) { html += bValidate; @@ -427,6 +968,10 @@ function loadBomTable(table, options) { html += bValid; } + html += bEdit; + html += bSubs; + html += bDelt; + html += `
    `; return html; @@ -446,6 +991,11 @@ function loadBomTable(table, options) { // This function may be called recursively for multi-level BOMs function requestSubItems(bom_pk, part_pk) { + // TODO: 2022-02-03 Currently, multi-level BOMs are not actually displayed. + + // Re-enable this function once multi-level display has been re-deployed + return; + inventreeGet( options.bom_url, { @@ -467,8 +1017,9 @@ function loadBomTable(table, options) { table.treegrid('collapseAll'); }, - error: function() { + error: function(xhr) { console.log('Error requesting BOM for part=' + part_pk); + showApiError(xhr); } } ); @@ -478,6 +1029,7 @@ function loadBomTable(table, options) { treeEnable: !options.editable, rootParentId: parent_id, idField: 'pk', + uniqueId: 'pk', parentIdField: 'parentId', treeShowField: 'sub_part', showColumns: true, @@ -554,19 +1106,27 @@ function loadBomTable(table, options) { // In editing mode, attached editables to the appropriate table elements if (options.editable) { + // Callback for "delete" button table.on('click', '.bom-delete-button', function() { var pk = $(this).attr('pk'); + var html = ` +
    + {% trans "Are you sure you want to delete this BOM item?" %} +
    `; + constructForm(`/api/bom/${pk}/`, { method: 'DELETE', title: '{% trans "Delete BOM Item" %}', + preFormContent: html, onSuccess: function() { reloadBomTable(table); } }); }); + // Callback for "edit" button table.on('click', '.bom-edit-button', function() { var pk = $(this).attr('pk'); @@ -576,12 +1136,14 @@ function loadBomTable(table, options) { constructForm(`/api/bom/${pk}/`, { fields: fields, title: '{% trans "Edit BOM Item" %}', + focus: 'sub_part', onSuccess: function() { reloadBomTable(table); } }); }); + // Callback for "validate" button table.on('click', '.bom-validate-button', function() { var pk = $(this).attr('pk'); @@ -600,5 +1162,187 @@ function loadBomTable(table, options) { } ); }); + + // Callback for "substitutes" button + table.on('click', '.bom-substitutes-button', function() { + var pk = $(this).attr('pk'); + + var row = table.bootstrapTable('getRowByUniqueId', pk); + var subs = row.substitutes || []; + + bomSubstitutesDialog( + pk, + subs, + { + table: table, + part: row.part, + sub_part: row.sub_part, + sub_part_detail: row.sub_part_detail, + } + ); + }); } } + + +/* + * Load a table which shows the assemblies which "require" a certain part. + * + * Arguments: + * - table: The ID string of the table element e.g. '#used-in-table' + * - part_id: The ID (PK) of the part we are interested in + * + * Options: + * - + * + * The following "options" are available. + */ +function loadUsedInTable(table, part_id, options={}) { + + var params = options.params || {}; + + params.uses = part_id; + params.part_detail = true; + params.sub_part_detail = true, + params.show_pricing = global_settings.PART_SHOW_PRICE_IN_BOM; + + var filters = {}; + + if (!options.disableFilters) { + filters = loadTableFilters('usedin'); + } + + for (var key in params) { + filters[key] = params[key]; + } + + setupFilterList('usedin', $(table), options.filterTarget || '#filter-list-usedin'); + + function loadVariantData(row) { + // Load variants information for inherited BOM rows + + inventreeGet( + '{% url "api-part-list" %}', + { + assembly: true, + ancestor: row.part, + }, + { + success: function(variantData) { + // Iterate through each variant item + for (var jj = 0; jj < variantData.length; jj++) { + variantData[jj].parent = row.pk; + + var variant = variantData[jj]; + + // Add this variant to the table, augmented + $(table).bootstrapTable('append', [{ + // Point the parent to the "master" assembly row + parent: row.pk, + part: variant.pk, + part_detail: variant, + sub_part: row.sub_part, + sub_part_detail: row.sub_part_detail, + quantity: row.quantity, + }]); + } + }, + error: function(xhr) { + showApiError(xhr); + } + } + ); + } + + $(table).inventreeTable({ + url: options.url || '{% url "api-bom-list" %}', + name: options.table_name || 'usedin', + sortable: true, + search: true, + showColumns: true, + queryParams: filters, + original: params, + rootParentId: 'top-level-item', + idField: 'pk', + uniqueId: 'pk', + parentIdField: 'parent', + treeShowField: 'part', + onLoadSuccess: function(tableData) { + // Once the initial data are loaded, check if there are any "inherited" BOM lines + for (var ii = 0; ii < tableData.length; ii++) { + var row = tableData[ii]; + + // This is a "top level" item in the table + row.parent = 'top-level-item'; + + // Ignore this row as it is not "inherited" by variant parts + if (!row.inherited) { + continue; + } + + loadVariantData(row); + } + }, + onPostBody: function() { + $(table).treegrid({ + treeColumn: 0, + }); + }, + columns: [ + { + field: 'pk', + title: 'ID', + visible: false, + switchable: false, + }, + { + field: 'part', + title: '{% trans "Assembly" %}', + switchable: false, + sortable: true, + formatter: function(value, row) { + var url = `/part/${value}/?display=bom`; + var html = ''; + + var part = row.part_detail; + + html += imageHoverIcon(part.thumbnail); + html += renderLink(part.full_name, url); + html += makePartIcons(part); + + return html; + } + }, + { + field: 'sub_part', + title: '{% trans "Required Part" %}', + sortable: true, + formatter: function(value, row) { + var url = `/part/${value}/`; + var html = ''; + + var sub_part = row.sub_part_detail; + + html += imageHoverIcon(sub_part.thumbnail); + html += renderLink(sub_part.full_name, url); + html += makePartIcons(sub_part); + + return html; + } + }, + { + field: 'quantity', + title: '{% trans "Required Quantity" %}', + formatter: function(value, row) { + var html = value; + + if (row.parent && row.parent != 'top-level-item') { + html += ` ({% trans "Inherited from parent BOM" %})`; + } + + return html; + } + } + ] + }); +} diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index ae7e439a59..fb8b870fad 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -4,7 +4,6 @@ /* globals buildStatusDisplay, constructForm, - getFieldByName, global_settings, imageHoverIcon, inventreeGet, @@ -20,11 +19,14 @@ */ /* exported + allocateStockToBuild, + completeBuildOrder, + createBuildOutput, editBuildOrder, loadAllocationTable, loadBuildOrderAllocationTable, loadBuildOutputAllocationTable, - loadBuildPartsTable, + loadBuildOutputTable, loadBuildTable, */ @@ -34,8 +36,13 @@ function buildFormFields() { reference: { prefix: global_settings.BUILDORDER_REFERENCE_PREFIX, }, + part: { + filters: { + assembly: true, + virtual: false, + } + }, title: {}, - part: {}, quantity: {}, parent: { filters: { @@ -43,12 +50,18 @@ function buildFormFields() { } }, sales_order: { - hidden: true, + icon: 'fa-truck', }, batch: {}, - target_date: {}, - take_from: {}, - destination: {}, + target_date: { + icon: 'fa-calendar-alt', + }, + take_from: { + icon: 'fa-sitemap', + }, + destination: { + icon: 'fa-sitemap', + }, link: { icon: 'fa-link', }, @@ -109,133 +122,524 @@ function newBuildOrder(options={}) { } -function makeBuildOutputActionButtons(output, buildInfo, lines) { - /* Generate action buttons for a build output. - */ +/* Construct a form to "complete" (finish) a build order */ +function completeBuildOrder(build_id, options={}) { - var buildId = buildInfo.pk; + var url = `/api/build/${build_id}/finish/`; - var outputId = 'untracked'; + var fields = { + accept_unallocated: {}, + accept_incomplete: {}, + }; - if (output) { - outputId = output.pk; + var html = ''; + + if (options.allocated && options.completed) { + html += ` +
    + {% trans "Build order is ready to be completed" %} +
    `; + } else { + html += ` +
    + {% trans "Build Order is incomplete" %} +
    + `; + + if (!options.allocated) { + html += `
    {% trans "Required stock has not been fully allocated" %}
    `; + } + + if (!options.completed) { + html += `
    {% trans "Required build quantity has not been completed" %}
    `; + } } - var panel = `#allocation-panel-${outputId}`; + // Hide particular fields if they are not required - function reloadTable() { - $(panel).find(`#allocation-table-${outputId}`).bootstrapTable('refresh'); + if (options.allocated) { + delete fields.accept_unallocated; } - // Find the div where the buttons will be displayed - var buildActions = $(panel).find(`#output-actions-${outputId}`); - - var html = `
    `; - - // "Auto" allocation only works for untracked stock items - if (!output && lines > 0) { - html += makeIconButton( - 'fa-magic icon-blue', 'button-output-auto', outputId, - '{% trans "Auto-allocate stock items to this output" %}', - ); + if (options.completed) { + delete fields.accept_incomplete; } - if (lines > 0) { - // Add a button to "cancel" the particular build output (unallocate) - html += makeIconButton( - 'fa-minus-circle icon-red', 'button-output-unallocate', outputId, - '{% trans "Unallocate stock from build output" %}', - ); - } - - - if (output) { - - // Add a button to "complete" the particular build output - html += makeIconButton( - 'fa-check icon-green', 'button-output-complete', outputId, - '{% trans "Complete build output" %}', - { - // disabled: true - } - ); - - // Add a button to "delete" the particular build output - html += makeIconButton( - 'fa-trash-alt icon-red', 'button-output-delete', outputId, - '{% trans "Delete build output" %}', - ); - - // TODO - Add a button to "destroy" the particular build output (mark as damaged, scrap) - } - - html += '
    '; - - buildActions.html(html); - - // Add callbacks for the buttons - $(panel).find(`#button-output-auto-${outputId}`).click(function() { - // Launch modal dialog to perform auto-allocation - launchModalForm(`/build/${buildId}/auto-allocate/`, - { - data: { - }, - success: reloadTable, - } - ); - }); - - $(panel).find(`#button-output-complete-${outputId}`).click(function() { - - var pk = $(this).attr('pk'); - - launchModalForm( - `/build/${buildId}/complete-output/`, - { - data: { - output: pk, - }, - reload: true, - } - ); - }); - - $(panel).find(`#button-output-unallocate-${outputId}`).click(function() { - - var pk = $(this).attr('pk'); - - launchModalForm( - `/build/${buildId}/unallocate/`, - { - success: reloadTable, - data: { - output: pk, - } - } - ); - }); - - $(panel).find(`#button-output-delete-${outputId}`).click(function() { - - var pk = $(this).attr('pk'); - - launchModalForm( - `/build/${buildId}/delete-output/`, - { - reload: true, - data: { - output: pk - } - } - ); + constructForm(url, { + fields: fields, + reload: true, + confirm: true, + method: 'POST', + title: '{% trans "Complete Build Order" %}', + preFormContent: html, }); } +/* + * Construct a new build output against the provided build + */ +function createBuildOutput(build_id, options) { + + // Request build order information from the server + inventreeGet( + `/api/build/${build_id}/`, + {}, + { + success: function(build) { + + var html = ''; + + var trackable = build.part_detail.trackable; + var remaining = Math.max(0, build.quantity - build.completed); + + var fields = { + quantity: { + value: remaining, + }, + serial_numbers: { + hidden: !trackable, + required: options.trackable_parts || trackable, + }, + batch_code: {}, + auto_allocate: { + hidden: !trackable, + }, + }; + + // Work out the next available serial numbers + inventreeGet(`/api/part/${build.part}/serial-numbers/`, {}, { + success: function(data) { + if (data.next) { + fields.serial_numbers.placeholder = `{% trans "Next available serial number" %}: ${data.next}`; + } else { + fields.serial_numbers.placeholder = `{% trans "Latest serial number" %}: ${data.latest}`; + } + }, + async: false, + }); + + if (options.trackable_parts) { + html += ` +
    + {% trans "The Bill of Materials contains trackable parts" %}.
    + {% trans "Build outputs must be generated individually" %}. +
    + `; + } + + if (trackable) { + html += ` +
    + {% trans "Trackable parts can have serial numbers specified" %}
    + {% trans "Enter serial numbers to generate multiple single build outputs" %} +
    + `; + } + + constructForm(`/api/build/${build_id}/create-output/`, { + method: 'POST', + title: '{% trans "Create Build Output" %}', + confirm: true, + fields: fields, + preFormContent: html, + onSuccess: function(response) { + location.reload(); + }, + }); + + } + } + ); + +} + + +/* + * Construct a set of output buttons for a particular build output + */ +function makeBuildOutputButtons(output_id, build_info, options={}) { + + var html = `
    `; + + // Tracked parts? Must be individually allocated + if (build_info.tracked_parts) { + + // Add a button to allocate stock against this build output + html += makeIconButton( + 'fa-sign-in-alt icon-blue', + 'button-output-allocate', + output_id, + '{% trans "Allocate stock items to this build output" %}', + { + disabled: true, + } + ); + + // Add a button to unallocate stock from this build output + html += makeIconButton( + 'fa-minus-circle icon-red', + 'button-output-unallocate', + output_id, + '{% trans "Unallocate stock from build output" %}', + ); + } + + // Add a button to "complete" this build output + html += makeIconButton( + 'fa-check-circle icon-green', + 'button-output-complete', + output_id, + '{% trans "Complete build output" %}', + ); + + // Add a button to "delete" this build output + html += makeIconButton( + 'fa-trash-alt icon-red', + 'button-output-delete', + output_id, + '{% trans "Delete build output" %}', + ); + + html += `
    `; + + return html; + +} + + +/* + * Unallocate stock against a particular build order + * + * Options: + * - output: pk value for a stock item "build output" + * - bom_item: pk value for a particular BOMItem (build item) + */ +function unallocateStock(build_id, options={}) { + + var url = `/api/build/${build_id}/unallocate/`; + + var html = ` +
    + {% trans "Are you sure you wish to unallocate stock items from this build?" %} + + `; + + constructForm(url, { + method: 'POST', + confirm: true, + preFormContent: html, + fields: { + output: { + hidden: true, + value: options.output, + }, + bom_item: { + hidden: true, + value: options.bom_item, + }, + }, + title: '{% trans "Unallocate Stock Items" %}', + onSuccess: function(response, opts) { + if (options.table) { + // Reload the parent table + $(options.table).bootstrapTable('refresh'); + } + } + }); +} + + +/** + * Launch a modal form to complete selected build outputs + */ +function completeBuildOutputs(build_id, outputs, options={}) { + + if (outputs.length == 0) { + showAlertDialog( + '{% trans "Select Build Outputs" %}', + '{% trans "At least one build output must be selected" %}', + ); + return; + } + + // Render a single build output (StockItem) + function renderBuildOutput(output, opts={}) { + var pk = output.pk; + + var output_html = imageHoverIcon(output.part_detail.thumbnail); + + if (output.quantity == 1 && output.serial) { + output_html += `{% trans "Serial Number" %}: ${output.serial}`; + } else { + output_html += `{% trans "Quantity" %}: ${output.quantity}`; + } + + var buttons = `
    `; + + buttons += makeIconButton('fa-times icon-red', 'button-row-remove', pk, '{% trans "Remove row" %}'); + + buttons += '
    '; + + var field = constructField( + `outputs_output_${pk}`, + { + type: 'raw', + html: output_html, + }, + { + hideLabels: true, + } + ); + + var html = ` + + ${field} + ${output.part_detail.full_name} + ${buttons} + `; + + return html; + } + + // Construct table entries + var table_entries = ''; + + outputs.forEach(function(output) { + table_entries += renderBuildOutput(output); + }); + + var html = ` + + + + + + + ${table_entries} + +
    {% trans "Output" %}
    `; + + constructForm(`/api/build/${build_id}/complete/`, { + method: 'POST', + preFormContent: html, + fields: { + status: {}, + location: {}, + }, + confirm: true, + title: '{% trans "Complete Build Outputs" %}', + afterRender: function(fields, opts) { + // Setup callbacks to remove outputs + $(opts.modal).find('.button-row-remove').click(function() { + var pk = $(this).attr('pk'); + + $(opts.modal).find(`#output_row_${pk}`).remove(); + }); + }, + onSubmit: function(fields, opts) { + + // Extract data elements from the form + var data = { + outputs: [], + status: getFormFieldValue('status', {}, opts), + location: getFormFieldValue('location', {}, opts), + }; + + var output_pk_values = []; + + outputs.forEach(function(output) { + var pk = output.pk; + + var row = $(opts.modal).find(`#output_row_${pk}`); + + if (row.exists()) { + data.outputs.push({ + output: pk, + }); + output_pk_values.push(pk); + } + }); + + // Provide list of nested values + opts.nested = { + 'outputs': output_pk_values, + }; + + inventreePut( + opts.url, + data, + { + method: 'POST', + success: function(response) { + // Hide the modal + $(opts.modal).modal('hide'); + + if (options.success) { + options.success(response); + } + }, + error: function(xhr) { + switch (xhr.status) { + case 400: + handleFormErrors(xhr.responseJSON, fields, opts); + break; + default: + $(opts.modal).modal('hide'); + showApiError(xhr, opts.url); + break; + } + } + } + ); + } + }); +} + + + +/** + * Launch a modal form to delete selected build outputs + */ +function deleteBuildOutputs(build_id, outputs, options={}) { + + if (outputs.length == 0) { + showAlertDialog( + '{% trans "Select Build Outputs" %}', + '{% trans "At least one build output must be selected" %}', + ); + return; + } + + // Render a single build output (StockItem) + function renderBuildOutput(output, opts={}) { + var pk = output.pk; + + var output_html = imageHoverIcon(output.part_detail.thumbnail); + + if (output.quantity == 1 && output.serial) { + output_html += `{% trans "Serial Number" %}: ${output.serial}`; + } else { + output_html += `{% trans "Quantity" %}: ${output.quantity}`; + } + + var buttons = `
    `; + + buttons += makeIconButton('fa-times icon-red', 'button-row-remove', pk, '{% trans "Remove row" %}'); + + buttons += '
    '; + + var field = constructField( + `outputs_output_${pk}`, + { + type: 'raw', + html: output_html, + }, + { + hideLabels: true, + } + ); + + var html = ` + + ${field} + ${output.part_detail.full_name} + ${buttons} + `; + + return html; + } + + // Construct table entries + var table_entries = ''; + + outputs.forEach(function(output) { + table_entries += renderBuildOutput(output); + }); + + var html = ` + + + + + + + ${table_entries} + +
    {% trans "Output" %}
    `; + + constructForm(`/api/build/${build_id}/delete-outputs/`, { + method: 'POST', + preFormContent: html, + fields: {}, + confirm: true, + title: '{% trans "Delete Build Outputs" %}', + afterRender: function(fields, opts) { + // Setup callbacks to remove outputs + $(opts.modal).find('.button-row-remove').click(function() { + var pk = $(this).attr('pk'); + + $(opts.modal).find(`#output_row_${pk}`).remove(); + }); + }, + onSubmit: function(fields, opts) { + var data = { + outputs: [], + }; + + var output_pk_values = []; + + outputs.forEach(function(output) { + var pk = output.pk; + + var row = $(opts.modal).find(`#output_row_${pk}`); + + if (row.exists()) { + data.outputs.push({ + output: pk + }); + output_pk_values.push(pk); + } + }); + + opts.nested = { + 'outputs': output_pk_values, + }; + + inventreePut( + opts.url, + data, + { + method: 'POST', + success: function(response) { + $(opts.modal).modal('hide'); + + if (options.success) { + options.success(response); + } + }, + error: function(xhr) { + switch (xhr.status) { + case 400: + handleFormErrors(xhr.responseJSON, fields, opts); + break; + default: + $(opts.modal).modal('hide'); + showApiError(xhr, opts.url); + break; + } + } + } + ); + } + }); +} + + +/** + * Load a table showing all the BuildOrder allocations for a given part + */ function loadBuildOrderAllocationTable(table, options={}) { - /** - * Load a table showing all the BuildOrder allocations for a given part - */ options.params['part_detail'] = true; options.params['build_detail'] = true; @@ -315,17 +719,259 @@ function loadBuildOrderAllocationTable(table, options={}) { } -function loadBuildOutputAllocationTable(buildInfo, output, options={}) { +/* + * Display a "build output" table for a particular build. + * + * This displays a list of "active" (i.e. "in production") build outputs for a given build + * + */ +function loadBuildOutputTable(build_info, options={}) { + + var table = options.table || '#build-output-table'; + + var params = options.params || {}; + + // Mandatory query filters + params.part_detail = true; + params.is_building = true; + params.build = build_info.pk; + + // Construct a list of "tracked" BOM items + var tracked_bom_items = []; + + var has_tracked_items = false; + + build_info.bom_items.forEach(function(bom_item) { + if (bom_item.sub_part_detail.trackable) { + tracked_bom_items.push(bom_item); + has_tracked_items = true; + }; + }); + + var filters = {}; + + for (var key in params) { + filters[key] = params[key]; + } + + // TODO: Initialize filter list + + function setupBuildOutputButtonCallbacks() { + + // Callback for the "allocate" button + $(table).find('.button-output-allocate').click(function() { + var pk = $(this).attr('pk'); + + // Find the "allocation" sub-table associated with this output + var subtable = $(`#output-sub-table-${pk}`); + + if (subtable.exists()) { + var rows = subtable.bootstrapTable('getSelections'); + + // None selected? Use all! + if (rows.length == 0) { + rows = subtable.bootstrapTable('getData'); + } + + allocateStockToBuild( + build_info.pk, + build_info.part, + rows, + { + output: pk, + success: function() { + $(table).bootstrapTable('refresh'); + } + } + ); + } else { + console.log(`WARNING: Could not locate sub-table for output ${pk}`); + } + }); + + // Callack for the "unallocate" button + $(table).find('.button-output-unallocate').click(function() { + var pk = $(this).attr('pk'); + + unallocateStock(build_info.pk, { + output: pk, + table: table + }); + }); + + // Callback for the "complete" button + $(table).find('.button-output-complete').click(function() { + var pk = $(this).attr('pk'); + + var output = $(table).bootstrapTable('getRowByUniqueId', pk); + + completeBuildOutputs( + build_info.pk, + [ + output, + ], + { + success: function() { + $(table).bootstrapTable('refresh'); + $('#build-stock-table').bootstrapTable('refresh'); + } + } + ); + }); + + // Callback for the "delete" button + $(table).find('.button-output-delete').click(function() { + var pk = $(this).attr('pk'); + + var output = $(table).bootstrapTable('getRowByUniqueId', pk); + + deleteBuildOutputs( + build_info.pk, + [ + output, + ], + { + success: function() { + $(table).bootstrapTable('refresh'); + $('#build-stock-table').bootstrapTable('refresh'); + } + } + ); + }); + } + /* - * Load the "allocation table" for a particular build output. - * - * Args: - * - buildId: The PK of the Build object - * - partId: The PK of the Part object - * - output: The StockItem object which is the "output" of the build - * - options: - * -- table: The #id of the table (will be auto-calculated if not provided) + * Construct a "sub table" showing the required BOM items */ + function constructBuildOutputSubTable(index, row, element) { + var sub_table_id = `output-sub-table-${row.pk}`; + + var html = ` +
    +
    +
    + `; + + element.html(html); + + loadBuildOutputAllocationTable( + build_info, + row, + { + table: `#${sub_table_id}`, + parent_table: table, + } + ); + } + + $(table).inventreeTable({ + url: '{% url "api-stock-list" %}', + queryParams: filters, + original: params, + showColumns: false, + uniqueId: 'pk', + name: 'build-outputs', + sortable: true, + search: false, + sidePagination: 'server', + detailView: has_tracked_items, + detailFilter: function(index, row) { + return true; + }, + detailFormatter: function(index, row, element) { + constructBuildOutputSubTable(index, row, element); + }, + formatNoMatches: function() { + return '{% trans "No active build outputs found" %}'; + }, + onPostBody: function() { + // Add callbacks for the buttons + setupBuildOutputButtonCallbacks(); + + $(table).bootstrapTable('expandAllRows'); + }, + columns: [ + { + title: '', + visible: true, + checkbox: true, + switchable: false, + }, + { + field: 'part', + title: '{% trans "Part" %}', + formatter: function(value, row) { + var thumb = row.part_detail.thumbnail; + + return imageHoverIcon(thumb) + row.part_detail.full_name + makePartIcons(row.part_detail); + } + }, + { + field: 'quantity', + title: '{% trans "Quantity" %}', + formatter: function(value, row) { + + var url = `/stock/item/${row.pk}/`; + + var text = ''; + + if (row.serial && row.quantity == 1) { + text = `{% trans "Serial Number" %}: ${row.serial}`; + } else { + text = `{% trans "Quantity" %}: ${row.quantity}`; + } + + return renderLink(text, url); + } + }, + { + field: 'allocated', + title: '{% trans "Allocated Parts" %}', + visible: has_tracked_items, + formatter: function(value, row) { + return `
    `; + } + }, + { + field: 'actions', + title: '', + switchable: false, + formatter: function(value, row) { + return makeBuildOutputButtons( + row.pk, + build_info, + ); + } + } + ] + }); + + // Enable the "allocate" button when the sub-table is exanded + $(table).on('expand-row.bs.table', function(detail, index, row) { + $(`#button-output-allocate-${row.pk}`).prop('disabled', false); + }); + + // Disable the "allocate" button when the sub-table is collapsed + $(table).on('collapse-row.bs.table', function(detail, index, row) { + $(`#button-output-allocate-${row.pk}`).prop('disabled', true); + }); +} + + +/* + * Display the "allocation table" for a particular build output. + * + * This displays a table of required allocations for a particular build output + * + * Args: + * - buildId: The PK of the Build object + * - partId: The PK of the Part object + * - output: The StockItem object which is the "output" of the build + * - options: + * -- table: The #id of the table (will be auto-calculated if not provided) + */ +function loadBuildOutputAllocationTable(buildInfo, output, options={}) { + var buildId = buildInfo.pk; var partId = buildInfo.part; @@ -344,6 +990,17 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { table = `#allocation-table-${outputId}`; } + // Filters + var filters = loadTableFilters('builditems'); + + var params = options.params || {}; + + for (var key in params) { + filters[key] = params[key]; + } + + setupFilterList('builditems', $(table), options.filterTarget || null); + // If an "output" is specified, then only "trackable" parts are allocated // Otherwise, only "untrackable" parts are allowed var trackable = ! !output; @@ -356,18 +1013,26 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { function requiredQuantity(row) { // Return the requied quantity for a given row + var quantity = 0; + if (output) { // "Tracked" parts are calculated against individual build outputs - return row.quantity * output.quantity; + quantity = row.quantity * output.quantity; } else { // "Untracked" parts are specified against the build itself - return row.quantity * buildInfo.quantity; + quantity = row.quantity * buildInfo.quantity; } + + // Store the required quantity in the row data + row.required = quantity; + + return quantity; } function sumAllocations(row) { // Calculat total allocations for a given row if (!row.allocations) { + row.allocated = 0; return 0; } @@ -377,6 +1042,8 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { quantity += item.quantity; }); + row.allocated = quantity; + return quantity; } @@ -389,52 +1056,28 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { // Primary key of the 'sub_part' var pk = $(this).attr('pk'); - // Launch form to allocate new stock against this output - launchModalForm('{% url "build-item-create" %}', { - success: reloadTable, - data: { - part: pk, - build: buildId, - install_into: outputId, - }, - secondary: [ - { - field: 'stock_item', - label: '{% trans "New Stock Item" %}', - title: '{% trans "Create new Stock Item" %}', - url: '{% url "stock-item-create" %}', - data: { - part: pk, - }, - }, + // Extract BomItem information from this row + var row = $(table).bootstrapTable('getRowByUniqueId', pk); + + if (!row) { + console.log('WARNING: getRowByUniqueId returned null'); + return; + } + + allocateStockToBuild( + buildId, + partId, + [ + row, ], - callback: [ - { - field: 'stock_item', - action: function(value) { - inventreeGet( - `/api/stock/${value}/`, {}, - { - success: function(response) { - - // How many items are actually available for the given stock item? - var available = response.quantity - response.allocated; - - var field = getFieldByName('#modal-form', 'quantity'); - - // Allocation quantity initial value - var initial = field.attr('value'); - - if (available < initial) { - field.val(available); - } - } - } - ); - } - } - ] - }); + { + source_location: buildInfo.source_location, + success: function(data) { + $(table).bootstrapTable('refresh'); + }, + output: output == null ? null : output.pk, + } + ); }); // Callback for 'buy' button @@ -468,17 +1111,16 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { // Callback for 'unallocate' button $(table).find('.button-unallocate').click(function() { - var pk = $(this).attr('pk'); - launchModalForm(`/build/${buildId}/unallocate/`, - { - success: reloadTable, - data: { - output: outputId, - part: pk, - } - } - ); + // Extract row data from the table + var idx = $(this).closest('tr').attr('data-index'); + var row = $(table).bootstrapTable('getData')[idx]; + + unallocateStock(buildId, { + bom_item: row.pk, + output: outputId == 'untracked' ? null : outputId, + table: table, + }); }); } @@ -496,7 +1138,11 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { }, name: 'build-allocation', uniqueId: 'sub_part', - onPostBody: setupCallbacks, + search: options.search || false, + onPostBody: function(data) { + // Setup button callbacks + setupCallbacks(); + }, onLoadSuccess: function(tableData) { // Once the BOM data are loaded, request allocation data for this build output @@ -572,31 +1218,34 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { $(table).bootstrapTable('updateByUniqueId', key, tableRow, true); } - // Update the total progress for this build output - var buildProgress = $(`#allocation-panel-${outputId}`).find($(`#output-progress-${outputId}`)); + // Update the progress bar for this build output + var build_progress = $(`#output-progress-${outputId}`); - if (totalLines > 0) { + if (build_progress.exists()) { + if (totalLines > 0) { - var progress = makeProgressBar( - allocatedLines, - totalLines - ); - - buildProgress.html(progress); + var progress = makeProgressBar( + allocatedLines, + totalLines, + { + max_width: '150px', + } + ); + + build_progress.html(progress); + } else { + build_progress.html(''); + } + } else { - buildProgress.html(''); + console.log(`WARNING: Could not find progress bar for output ${outputId}`); } - - // Update the available actions for this build output - - makeBuildOutputActionButtons(output, buildInfo, totalLines); } } ); }, sortable: true, showColumns: false, - detailViewByClick: true, detailView: true, detailFilter: function(index, row) { return row.allocations != null; @@ -648,11 +1297,9 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { text = `{% trans "Quantity" %}: ${row.quantity}`; } - {% if build.status == BuildStatus.COMPLETE %} - url = `/stock/item/${row.pk}/`; - {% else %} - url = `/stock/item/${row.stock_item}/`; - {% endif %} + var pk = row.stock_item || row.pk; + + url = `/stock/item/${pk}/`; return renderLink(text, url); } @@ -699,22 +1346,31 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { // Assign button callbacks to the newly created allocation buttons subTable.find('.button-allocation-edit').click(function() { var pk = $(this).attr('pk'); - launchModalForm(`/build/item/${pk}/edit/`, { - success: reloadTable, + + constructForm(`/api/build/item/${pk}/`, { + fields: { + quantity: {}, + }, + title: '{% trans "Edit Allocation" %}', + onSuccess: reloadTable, }); }); subTable.find('.button-allocation-delete').click(function() { var pk = $(this).attr('pk'); - launchModalForm(`/build/item/${pk}/delete/`, { - success: reloadTable, + + constructForm(`/api/build/item/${pk}/`, { + method: 'DELETE', + title: '{% trans "Remove Allocation" %}', + onSuccess: reloadTable, }); }); }, columns: [ { - field: 'pk', - visible: false, + visible: true, + switchable: false, + checkbox: true, }, { field: 'sub_part_detail.full_name', @@ -729,6 +1385,14 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { html += makePartIcons(row.sub_part_detail); + if (row.substitutes && row.substitutes.length > 0) { + html += makeIconBadge('fa-exchange-alt', '{% trans "Substitute parts available" %}'); + } + + if (row.allow_variants) { + html += makeIconBadge('fa-sitemap', '{% trans "Variant stock allowed" %}'); + } + return html; } }, @@ -830,14 +1494,360 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { }, ] }); - - // Initialize the action buttons - makeBuildOutputActionButtons(output, buildInfo, 0); } + +/** + * Allocate stock items to a build + * + * arguments: + * - buildId: ID / PK value for the build + * - partId: ID / PK value for the part being built + * - bom_items: A list of BomItem objects to be allocated + * + * options: + * - output: ID / PK of the associated build output (or null for untracked items) + * - source_location: ID / PK of the top-level StockLocation to source stock from (or null) + */ +function allocateStockToBuild(build_id, part_id, bom_items, options={}) { + + // ID of the associated "build output" (or null) + var output_id = options.output || null; + + var auto_fill_filters = {}; + + var source_location = options.source_location; + + if (output_id) { + // Request information on the particular build output (stock item) + inventreeGet(`/api/stock/${output_id}/`, {}, { + success: function(output) { + if (output.quantity == 1 && output.serial != null) { + auto_fill_filters.serial = output.serial; + } + }, + async: false, + }); + } + + function renderBomItemRow(bom_item, quantity) { + + var pk = bom_item.pk; + var sub_part = bom_item.sub_part_detail; + + var thumb = thumbnailImage(bom_item.sub_part_detail.thumbnail); + + var delete_button = `
    `; + + delete_button += makeIconButton( + 'fa-times icon-red', + 'button-row-remove', + pk, + '{% trans "Remove row" %}', + ); + + delete_button += `
    `; + + var quantity_input = constructField( + `items_quantity_${pk}`, + { + type: 'decimal', + min_value: 0, + value: quantity || 0, + title: '{% trans "Specify stock allocation quantity" %}', + required: true, + }, + { + hideLabels: true, + } + ); + + var allocated_display = makeProgressBar( + bom_item.allocated, + bom_item.required, + ); + + var stock_input = constructField( + `items_stock_item_${pk}`, + { + type: 'related field', + required: 'true', + }, + { + hideLabels: true, + } + ); + + // var stock_input = constructRelatedFieldInput(`items_stock_item_${pk}`); + + var html = ` + + + ${thumb} ${sub_part.full_name} + + + ${allocated_display} + + + ${stock_input} + + + ${quantity_input} + + + ${delete_button} + + + `; + + return html; + } + + var table_entries = ''; + + for (var idx = 0; idx < bom_items.length; idx++) { + var bom_item = bom_items[idx]; + + var required = bom_item.required || 0; + var allocated = bom_item.allocated || 0; + var remaining = required - allocated; + + if (remaining < 0) { + remaining = 0; + } + + // We only care about entries which are not yet fully allocated + if (remaining > 0) { + table_entries += renderBomItemRow(bom_item, remaining); + } + } + + if (table_entries.length == 0) { + + showAlertDialog( + '{% trans "Select Parts" %}', + '{% trans "You must select at least one part to allocate" %}', + ); + + return; + } + + var html = ``; + + // Render a "source location" input + html += constructField( + 'take_from', + { + type: 'related field', + label: '{% trans "Source Location" %}', + help_text: '{% trans "Select source location (leave blank to take from all locations)" %}', + required: false, + }, + {}, + ); + + // Create table of parts + html += ` + + + + + + + + + + + + ${table_entries} + +
    {% trans "Part" %}{% trans "Allocated" %}{% trans "Stock Item" %}{% trans "Quantity" %}
    + `; + + constructForm(`/api/build/${build_id}/allocate/`, { + method: 'POST', + fields: {}, + preFormContent: html, + confirm: true, + confirmMessage: '{% trans "Confirm stock allocation" %}', + title: '{% trans "Allocate Stock Items to Build Order" %}', + afterRender: function(fields, options) { + + var take_from_field = { + name: 'take_from', + model: 'stocklocation', + api_url: '{% url "api-location-list" %}', + required: false, + type: 'related field', + value: source_location, + noResults: function(query) { + return '{% trans "No matching stock locations" %}'; + }, + }; + + // Initialize "take from" field + initializeRelatedField( + take_from_field, + null, + options, + ); + + // Add callback to "clear" button for take_from field + addClearCallback( + 'take_from', + take_from_field, + options, + ); + + // Initialize stock item fields + bom_items.forEach(function(bom_item) { + initializeRelatedField( + { + name: `items_stock_item_${bom_item.pk}`, + api_url: '{% url "api-stock-list" %}', + filters: { + bom_item: bom_item.pk, + in_stock: true, + available: true, + part_detail: true, + location_detail: true, + }, + model: 'stockitem', + required: true, + render_part_detail: true, + render_location_detail: true, + render_stock_id: false, + auto_fill: true, + auto_fill_filters: auto_fill_filters, + onSelect: function(data, field, opts) { + // Adjust the 'quantity' field based on availability + + if (!('quantity' in data)) { + return; + } + + // Quantity remaining to be allocated + var remaining = Math.max((bom_item.required || 0) - (bom_item.allocated || 0), 0); + + // Calculate the available quantity + var available = Math.max((data.quantity || 0) - (data.allocated || 0), 0); + + // Maximum amount that we need + var desired = Math.min(available, remaining); + + updateFieldValue(`items_quantity_${bom_item.pk}`, desired, {}, opts); + }, + adjustFilters: function(filters) { + // Restrict query to the selected location + var location = getFormFieldValue( + 'take_from', + {}, + { + modal: options.modal, + } + ); + + filters.location = location; + filters.cascade = true; + + return filters; + }, + noResults: function(query) { + return '{% trans "No matching stock items" %}'; + } + }, + null, + options, + ); + }); + + // Add remove-row button callbacks + $(options.modal).find('.button-row-remove').click(function() { + var pk = $(this).attr('pk'); + + $(options.modal).find(`#allocation_row_${pk}`).remove(); + }); + }, + onSubmit: function(fields, opts) { + + // Extract elements from the form + var data = { + items: [] + }; + + var item_pk_values = []; + + bom_items.forEach(function(item) { + + var quantity = getFormFieldValue( + `items_quantity_${item.pk}`, + {}, + { + modal: opts.modal, + }, + ); + + var stock_item = getFormFieldValue( + `items_stock_item_${item.pk}`, + {}, + { + modal: opts.modal, + } + ); + + if (quantity != null) { + data.items.push({ + bom_item: item.pk, + stock_item: stock_item, + quantity: quantity, + output: output_id, + }); + + item_pk_values.push(item.pk); + } + }); + + // Provide nested values + opts.nested = { + 'items': item_pk_values + }; + + inventreePut( + opts.url, + data, + { + method: 'POST', + success: function(response) { + // Hide the modal + $(opts.modal).modal('hide'); + + if (options.success) { + options.success(response); + } + }, + error: function(xhr) { + switch (xhr.status) { + case 400: + handleFormErrors(xhr.responseJSON, fields, opts); + break; + default: + $(opts.modal).modal('hide'); + showApiError(xhr, opts.url); + break; + } + } + } + ); + }, + }); +} + + +/* + * Display a table of Build orders + */ function loadBuildTable(table, options) { - // Display a table of Build objects var params = options.params || {}; @@ -1104,190 +2114,4 @@ function loadAllocationTable(table, part_id, part, url, required, button) { } }); }); - -} - - -function loadBuildPartsTable(table, options={}) { - /** - * Display a "required parts" table for build view. - * - * This is a simplified BOM view: - * - Does not display sub-bom items - * - Does not allow editing of BOM items - * - * Options: - * - * part: Part ID - * build: Build ID - * build_quantity: Total build quantity - * build_remaining: Number of items remaining - */ - - // Query params - var params = { - sub_part_detail: true, - part: options.part, - }; - - var filters = {}; - - if (!options.disableFilters) { - filters = loadTableFilters('bom'); - } - - setupFilterList('bom', $(table)); - - for (var key in params) { - filters[key] = params[key]; - } - - function setupTableCallbacks() { - // Register button callbacks once the table data are loaded - - // Callback for 'buy' button - $(table).find('.button-buy').click(function() { - var pk = $(this).attr('pk'); - - launchModalForm('{% url "order-parts" %}', { - data: { - parts: [ - pk, - ] - } - }); - }); - - // Callback for 'build' button - $(table).find('.button-build').click(function() { - var pk = $(this).attr('pk'); - - newBuildOrder({ - part: pk, - parent: options.build, - }); - }); - } - - var columns = [ - { - field: 'sub_part', - title: '{% trans "Part" %}', - switchable: false, - sortable: true, - formatter: function(value, row) { - var url = `/part/${row.sub_part}/`; - var html = imageHoverIcon(row.sub_part_detail.thumbnail) + renderLink(row.sub_part_detail.full_name, url); - - var sub_part = row.sub_part_detail; - - html += makePartIcons(row.sub_part_detail); - - // Display an extra icon if this part is an assembly - if (sub_part.assembly) { - var text = ``; - - html += renderLink(text, `/part/${row.sub_part}/bom/`); - } - - return html; - } - }, - { - field: 'sub_part_detail.description', - title: '{% trans "Description" %}', - }, - { - field: 'reference', - title: '{% trans "Reference" %}', - searchable: true, - sortable: true, - }, - { - field: 'quantity', - title: '{% trans "Quantity" %}', - sortable: true - }, - { - sortable: true, - switchable: false, - field: 'sub_part_detail.stock', - title: '{% trans "Available" %}', - formatter: function(value, row) { - return makeProgressBar( - value, - row.quantity * options.build_remaining, - { - id: `part-progress-${row.part}` - } - ); - }, - sorter: function(valA, valB, rowA, rowB) { - if (rowA.received == 0 && rowB.received == 0) { - return (rowA.quantity > rowB.quantity) ? 1 : -1; - } - - var progressA = parseFloat(rowA.sub_part_detail.stock) / (rowA.quantity * options.build_remaining); - var progressB = parseFloat(rowB.sub_part_detail.stock) / (rowB.quantity * options.build_remaining); - - return (progressA < progressB) ? 1 : -1; - } - }, - { - field: 'actions', - title: '{% trans "Actions" %}', - switchable: false, - formatter: function(value, row) { - - // Generate action buttons against the part - var html = `
    `; - - if (row.sub_part_detail.assembly) { - html += makeIconButton('fa-tools icon-blue', 'button-build', row.sub_part, '{% trans "Build stock" %}'); - } - - if (row.sub_part_detail.purchaseable) { - html += makeIconButton('fa-shopping-cart icon-blue', 'button-buy', row.sub_part, '{% trans "Order stock" %}'); - } - - html += `
    `; - - return html; - } - } - ]; - - table.inventreeTable({ - url: '{% url "api-bom-list" %}', - showColumns: true, - name: 'build-parts', - sortable: true, - search: true, - onPostBody: setupTableCallbacks, - rowStyle: function(row) { - var classes = []; - - // Shade rows differently if they are for different parent parts - if (row.part != options.part) { - classes.push('rowinherited'); - } - - if (row.validated) { - classes.push('rowvalid'); - } else { - classes.push('rowinvalid'); - } - - return { - classes: classes.join(' '), - }; - }, - formatNoMatches: function() { - return '{% trans "No BOM items found" %}'; - }, - clickToSelect: true, - queryParams: filters, - original: params, - columns: columns, - }); } diff --git a/InvenTree/templates/js/translated/company.js b/InvenTree/templates/js/translated/company.js index 68b3079c3e..c92bb75d6f 100644 --- a/InvenTree/templates/js/translated/company.js +++ b/InvenTree/templates/js/translated/company.js @@ -124,6 +124,7 @@ function supplierPartFields() { part_detail: true, manufacturer_detail: true, }, + auto_fill: true, }, description: {}, link: { @@ -325,15 +326,15 @@ function loadCompanyTable(table, url, options={}) { var html = imageHoverIcon(row.image) + renderLink(value, row.url); if (row.is_customer) { - html += ``; + html += ``; } if (row.is_manufacturer) { - html += ``; + html += ``; } if (row.is_supplier) { - html += ``; + html += ``; } return html; @@ -379,6 +380,7 @@ function loadCompanyTable(table, url, options={}) { url: url, method: 'get', queryParams: filters, + original: params, groupBy: false, sidePagination: 'server', formatNoMatches: function() { @@ -462,7 +464,9 @@ function loadManufacturerPartTable(table, url, options) { filters[key] = params[key]; } - setupFilterList('manufacturer-part', $(table)); + var filterTarget = options.filterTarget || '#filter-list-manufacturer-part'; + + setupFilterList('manufacturer-part', $(table), filterTarget); $(table).inventreeTable({ url: url, @@ -493,15 +497,15 @@ function loadManufacturerPartTable(table, url, options) { var html = imageHoverIcon(row.part_detail.thumbnail) + renderLink(value, url); if (row.part_detail.is_template) { - html += ``; + html += ``; } if (row.part_detail.assembly) { - html += ``; + html += ``; } if (!row.part_detail.active) { - html += `{% trans "Inactive" %}`; + html += `{% trans "Inactive" %}`; } return html; @@ -750,15 +754,15 @@ function loadSupplierPartTable(table, url, options) { var html = imageHoverIcon(row.part_detail.thumbnail) + renderLink(value, url); if (row.part_detail.is_template) { - html += ``; + html += ``; } if (row.part_detail.assembly) { - html += ``; + html += ``; } if (!row.part_detail.active) { - html += `{% trans "Inactive" %}`; + html += `{% trans "Inactive" %}`; } return html; diff --git a/InvenTree/templates/js/translated/filters.js b/InvenTree/templates/js/translated/filters.js index d7e8f45ca5..78ed30eefa 100644 --- a/InvenTree/templates/js/translated/filters.js +++ b/InvenTree/templates/js/translated/filters.js @@ -273,23 +273,48 @@ function setupFilterList(tableKey, table, target) { var element = $(target); + if (!element || !element.exists()) { + console.log(`WARNING: setupFilterList could not find target '${target}'`); + return; + } + // One blank slate, please element.empty(); - element.append(``); + var buttons = ''; - element.append(``); + buttons += ``; - if (Object.keys(filters).length > 0) { - element.append(``); + // If there are filters defined for this table, add more buttons + if (!jQuery.isEmptyObject(getAvailableTableFilters(tableKey))) { + buttons += ``; + + if (Object.keys(filters).length > 0) { + buttons += ``; + } } + element.html(` +
    + ${buttons} +
    + `); + for (var key in filters) { var value = getFilterOptionValue(tableKey, key, filters[key]); var title = getFilterTitle(tableKey, key); var description = getFilterDescription(tableKey, key); - element.append(`
    ${title} = ${value}x
    `); + var filter_tag = ` +
    + ${title} = ${value} + + + +
    + `; + + element.append(filter_tag); } // Callback for reloading the table @@ -306,10 +331,12 @@ function setupFilterList(tableKey, table, target) { var html = ''; + html += `
    `; html += generateAvailableFilterList(tableKey); html += generateFilterInput(tableKey); - html += ``; + html += ``; + html += `
    `; element.append(html); diff --git a/InvenTree/templates/js/translated/forms.js b/InvenTree/templates/js/translated/forms.js index 802147e1df..9ee3dcace3 100644 --- a/InvenTree/templates/js/translated/forms.js +++ b/InvenTree/templates/js/translated/forms.js @@ -2,7 +2,6 @@ {% load inventree_extras %} /* globals - attachToggle, createNewModal, inventreeFormDataUpload, inventreeGet, @@ -20,13 +19,19 @@ renderStockLocation, renderSupplierPart, renderUser, - showAlertDialog, showAlertOrCache, showApiError, */ /* exported - setFormGroupVisibility + clearFormInput, + disableFormInput, + enableFormInput, + hideFormInput, + setFormInputPlaceholder, + setFormGroupVisibility, + showFormInput, + selectImportFields, */ /** @@ -49,6 +54,9 @@ * */ +// Set global default theme for select2 +$.fn.select2.defaults.set('theme', 'bootstrap-5'); + /* * Return true if the OPTIONS specify that the user * can perform a GET method at the endpoint. @@ -111,6 +119,10 @@ function canDelete(OPTIONS) { */ function getApiEndpointOptions(url, callback) { + if (!url) { + return; + } + // Return the ajax request object $.ajax({ url: url, @@ -121,9 +133,10 @@ function getApiEndpointOptions(url, callback) { json: 'application/json', }, success: callback, - error: function() { + error: function(xhr) { // TODO: Handle error console.log(`ERROR in getApiEndpointOptions at '${url}'`); + showApiError(xhr, url); } }); } @@ -179,6 +192,7 @@ function constructChangeForm(fields, options) { // Request existing data from the API endpoint $.ajax({ url: options.url, + data: options.params || {}, type: 'GET', contentType: 'application/json', dataType: 'json', @@ -186,6 +200,17 @@ function constructChangeForm(fields, options) { json: 'application/json', }, success: function(data) { + + // An optional function can be provided to process the returned results, + // before they are rendered to the form + if (options.processResults) { + var processed = options.processResults(data, fields, options); + + // If the processResults function returns data, it will be stored + if (processed) { + data = processed; + } + } // Push existing 'value' to each field for (const field in data) { @@ -197,12 +222,14 @@ function constructChangeForm(fields, options) { // Store the entire data object options.instance = data; - + constructFormBody(fields, options); }, - error: function() { + error: function(xhr) { // TODO: Handle error here console.log(`ERROR in constructChangeForm at '${options.url}'`); + + showApiError(xhr, options.url); } }); } @@ -239,9 +266,11 @@ function constructDeleteForm(fields, options) { constructFormBody(fields, options); }, - error: function() { + error: function(xhr) { // TODO: Handle error here console.log(`ERROR in constructDeleteForm at '${options.url}`); + + showApiError(xhr, options.url); } }); } @@ -319,10 +348,12 @@ function constructForm(url, options) { constructCreateForm(OPTIONS.actions.POST, options); } else { // User does not have permission to POST to the endpoint - showAlertDialog( - '{% trans "Action Prohibited" %}', - '{% trans "Create operation not allowed" %}' - ); + showMessage('{% trans "Action Prohibited" %}', { + style: 'danger', + details: '{% trans "Create operation not allowed" %}', + icon: 'fas fa-user-times', + }); + console.log(`'POST action unavailable at ${url}`); } break; @@ -332,10 +363,12 @@ function constructForm(url, options) { constructChangeForm(OPTIONS.actions.PUT, options); } else { // User does not have permission to PUT/PATCH to the endpoint - showAlertDialog( - '{% trans "Action Prohibited" %}', - '{% trans "Update operation not allowed" %}' - ); + showMessage('{% trans "Action Prohibited" %}', { + style: 'danger', + details: '{% trans "Update operation not allowed" %}', + icon: 'fas fa-user-times', + }); + console.log(`${options.method} action unavailable at ${url}`); } break; @@ -344,10 +377,12 @@ function constructForm(url, options) { constructDeleteForm(OPTIONS.actions.DELETE, options); } else { // User does not have permission to DELETE to the endpoint - showAlertDialog( - '{% trans "Action Prohibited" %}', - '{% trans "Delete operation not allowed" %}' - ); + showMessage('{% trans "Action Prohibited" %}', { + style: 'danger', + details: '{% trans "Delete operation not allowed" %}', + icon: 'fas fa-user-times', + }); + console.log(`DELETE action unavailable at ${url}`); } break; @@ -356,10 +391,12 @@ function constructForm(url, options) { // TODO? } else { // User does not have permission to GET to the endpoint - showAlertDialog( - '{% trans "Action Prohibited" %}', - '{% trans "View operation not allowed" %}' - ); + showMessage('{% trans "Action Prohibited" %}', { + style: 'danger', + details: '{% trans "View operation not allowed" %}', + icon: 'fas fa-user-times', + }); + console.log(`GET action unavailable at ${url}`); } break; @@ -519,11 +556,6 @@ function constructFormBody(fields, options) { // Attach clear callbacks (if required) addClearCallbacks(fields, options); - attachToggle(modal); - - $(modal + ' .select2-container').addClass('select-full-width'); - $(modal + ' .select2-container').css('width', '100%'); - modalShowSubmitButton(modal, true); $(modal).on('click', '#modal-form-submit', function() { @@ -563,13 +595,14 @@ function insertConfirmButton(options) { var message = options.confirmMessage || '{% trans "Confirm" %}'; - var confirm = ` - - ${message} - - `; + var html = ` +
    + + +
    + `; - $(options.modal).find('#modal-footer-buttons').append(confirm); + $(options.modal).find('#modal-footer-buttons').append(html); // Disable the 'submit' button $(options.modal).find('#modal-form-submit').prop('disabled', true); @@ -693,6 +726,14 @@ function submitFormData(fields, options) { data = form_data; } + // Optionally pre-process the data before uploading to the server + if (options.processBeforeUpload) { + data = options.processBeforeUpload(data); + } + + // Show the progress spinner + $(options.modal).find('#modal-progress-spinner').show(); + // Submit data upload_func( options.url, @@ -700,17 +741,22 @@ function submitFormData(fields, options) { { method: options.method, success: function(response) { + $(options.modal).find('#modal-progress-spinner').hide(); handleFormSuccess(response, options); }, error: function(xhr) { + $(options.modal).find('#modal-progress-spinner').hide(); + switch (xhr.status) { case 400: handleFormErrors(xhr.responseJSON, fields, options); break; default: $(options.modal).modal('hide'); - showApiError(xhr); + + console.log(`upload error at ${options.url}`); + showApiError(xhr, options.url); break; } } @@ -752,14 +798,23 @@ function updateFieldValues(fields, options) { } } - +/* + * Update the value of a named field + */ function updateFieldValue(name, value, field, options) { var el = getFormFieldElement(name, options); + if (!el) { + console.log(`WARNING: updateFieldValue could not find field '${name}'`); + return; + } + switch (field.type) { case 'boolean': - el.prop('checked', value); + if (value == true || value.toString().toLowerCase() == 'true') { + el.prop('checked'); + } break; case 'related field': // Clear? @@ -781,7 +836,17 @@ function updateFieldValue(name, value, field, options) { // Find the named field element in the modal DOM function getFormFieldElement(name, options) { - var el = $(options.modal).find(`#id_${name}`); + var field_name = getFieldName(name, options); + + var el = null; + + if (options && options.modal) { + // Field element is associated with a model? + el = $(options.modal).find(`#id_${field_name}`); + } else { + // Field element is top-level + el = $(`#id_${field_name}`); + } if (!el.exists) { console.log(`ERROR: Could not find form element for field '${name}'`); @@ -826,12 +891,13 @@ function validateFormField(name, options) { * - field: The field specification provided from the OPTIONS request * - options: The original options object provided by the client */ -function getFormFieldValue(name, field, options) { +function getFormFieldValue(name, field={}, options={}) { // Find the HTML element var el = getFormFieldElement(name, options); - if (!el) { + if (!el.exists()) { + console.log(`ERROR: getFormFieldValue could not locate field '${name}'`); return null; } @@ -879,20 +945,20 @@ function handleFormSuccess(response, options) { var cache = (options.follow && response.url) || options.redirect || options.reload; // Display any messages - if (response && response.success) { - showAlertOrCache('alert-success', response.success, cache); + if (response && (response.success || options.successMessage)) { + showAlertOrCache(response.success || options.successMessage, cache, {style: 'success'}); } if (response && response.info) { - showAlertOrCache('alert-info', response.info, cache); + showAlertOrCache(response.info, cache, {style: 'info'}); } if (response && response.warning) { - showAlertOrCache('alert-warning', response.warning, cache); + showAlertOrCache(response.warning, cache, {style: 'warning'}); } if (response && response.danger) { - showAlertOrCache('alert-danger', response.danger, cache); + showAlertOrCache(response.danger, cache, {style: 'danger'}); } if (options.onSuccess) { @@ -917,18 +983,119 @@ function handleFormSuccess(response, options) { /* * Remove all error text items from the form */ -function clearFormErrors(options) { +function clearFormErrors(options={}) { - // Remove the individual error messages - $(options.modal).find('.form-error-message').remove(); + if (options && options.modal) { + // Remove the individual error messages + $(options.modal).find('.form-error-message').remove(); - // Remove the "has error" class - $(options.modal).find('.has-error').removeClass('has-error'); + // Remove the "has error" class + $(options.modal).find('.form-field-error').removeClass('form-field-error'); - // Hide the 'non field errors' - $(options.modal).find('#non-field-errors').html(''); + // Hide the 'non field errors' + $(options.modal).find('#non-field-errors').html(''); + } else { + $('.form-error-message').remove(); + $('.form-field-errors').removeClass('form-field-error'); + $('#non-field-errors').html(''); + } } +/* + * Display form error messages as returned from the server, + * specifically for errors returned in an array. + * + * We need to know the unique ID of each item in the array, + * and the array length must equal the length of the array returned from the server + * + * arguments: + * - response: The JSON error response from the server + * - parent: The name of the parent field e.g. "items" + * - options: The global options struct + * + * options: + * - nested: A map of nested ID values for the "parent" field + * e.g. + * { + * "items": [ + * 1, + * 2, + * 12 + * ] + * } + * + */ + +function handleNestedErrors(errors, field_name, options={}) { + + var error_list = errors[field_name]; + + // Ignore null or empty list + if (!error_list) { + return; + } + + var nest_list = nest_list = options['nested'][field_name]; + + // Nest list must be provided! + if (!nest_list) { + console.log(`WARNING: handleNestedErrors missing nesting options for field '${fieldName}'`); + return; + } + + for (var idx = 0; idx < error_list.length; idx++) { + + var error_item = error_list[idx]; + + if (idx >= nest_list.length) { + console.log(`WARNING: handleNestedErrors returned greater number of errors (${error_list.length}) than could be handled (${nest_list.length})`); + break; + } + + // Extract the particular ID of the nested item + var nest_id = nest_list[idx]; + + // Here, error_item is a map of field names to error messages + for (sub_field_name in error_item) { + + var errors = error_item[sub_field_name]; + + if (sub_field_name == 'non_field_errors') { + + var row = null; + + if (options.modal) { + row = $(options.modal).find(`#items_${nest_id}`); + } else { + row = $(`#items_${nest_id}`); + } + + for (var ii = errors.length - 1; ii >= 0; ii--) { + + var html = ` +
    + ${errors[ii]} +
    `; + + row.after(html); + } + + } + + // Find the target (nested) field + var target = `${field_name}_${sub_field_name}_${nest_id}`; + + for (var ii = errors.length-1; ii >= 0; ii--) { + + var error_text = errors[ii]; + + addFieldErrorMessage(target, error_text, ii, options); + } + } + } +} + + /* * Display form error messages as returned from the server. @@ -938,15 +1105,23 @@ function clearFormErrors(options) { * - fields: The form data object * - options: Form options provided by the client */ -function handleFormErrors(errors, fields, options) { +function handleFormErrors(errors, fields={}, options={}) { // Reset the status of the "submit" button - $(options.modal).find('#modal-form-submit').prop('disabled', false); + if (options.modal) { + $(options.modal).find('#modal-form-submit').prop('disabled', false); + } // Remove any existing error messages from the form clearFormErrors(options); - var non_field_errors = $(options.modal).find('#non-field-errors'); + var non_field_errors = null; + + if (options.modal) { + non_field_errors = $(options.modal).find('#non-field-errors'); + } else { + non_field_errors = $('#non-field-errors'); + } // TODO: Display the JSON error text when hovering over the "info" icon non_field_errors.append( @@ -978,28 +1153,30 @@ function handleFormErrors(errors, fields, options) { for (var field_name in errors) { - // Add the 'has-error' class - $(options.modal).find(`#div_id_${field_name}`).addClass('has-error'); + if (field_name in fields) { - var field_dom = $(options.modal).find(`#errors-${field_name}`); // $(options.modal).find(`#id_${field_name}`); + var field = fields[field_name]; - var field_errors = errors[field_name]; + if ((field.type == 'field') && ('child' in field)) { + // This is a "nested" field + handleNestedErrors(errors, field_name, options); + } else { + // This is a "simple" field - if (field_errors && !first_error_field && isFieldVisible(field_name, options)) { - first_error_field = field_name; - } + var field_errors = errors[field_name]; - // Add an entry for each returned error message - for (var ii = field_errors.length-1; ii >= 0; ii--) { + if (field_errors && !first_error_field && isFieldVisible(field_name, options)) { + first_error_field = field_name; + } - var error_text = field_errors[ii]; + // Add an entry for each returned error message + for (var ii = field_errors.length-1; ii >= 0; ii--) { - var error_html = ` - - ${error_text} - `; + var error_text = field_errors[ii]; - field_dom.append(error_html); + addFieldErrorMessage(field_name, error_text, ii, options); + } + } } } @@ -1017,6 +1194,37 @@ function handleFormErrors(errors, fields, options) { } +/* + * Add a rendered error message to the provided field + */ +function addFieldErrorMessage(name, error_text, error_idx=0, options={}) { + + field_name = getFieldName(name, options); + + var field_dom = null; + + if (options && options.modal) { + $(options.modal).find(`#div_id_${field_name}`).addClass('form-field-error'); + field_dom = $(options.modal).find(`#errors-${field_name}`); + } else { + $(`#div_id_${field_name}`).addClass('form-field-error'); + field_dom = $(`#errors-${field_name}`); + } + + if (field_dom.exists()) { + + var error_html = ` + + ${error_text} + `; + + field_dom.append(error_html); + } else { + console.log(`WARNING: addFieldErrorMessage could not locate field '${field_name}'`); + } +} + + function isFieldVisible(field, options) { return $(options.modal).find(`#div_id_${field}`).is(':visible'); @@ -1072,9 +1280,24 @@ function addClearCallbacks(fields, options) { } -function addClearCallback(name, field, options) { +function addClearCallback(name, field, options={}) { - $(options.modal).find(`#clear_${name}`).click(function() { + var field_name = getFieldName(name, options); + + var el = null; + + if (options && options.modal) { + el = $(options.modal).find(`#clear_${field_name}`); + } else { + el = $(`#clear_${field_name}`); + } + + if (!el) { + console.log(`WARNING: addClearCallback could not find field '${name}'`); + return; + } + + el.click(function() { updateFieldValue(name, null, field, options); }); } @@ -1126,16 +1349,52 @@ function initializeGroups(fields, options) { } } +// Set the placeholder value for a field +function setFormInputPlaceholder(name, placeholder, options) { + $(options.modal).find(`#id_${name}`).attr('placeholder', placeholder); +} + +// Clear a form input +function clearFormInput(name, options) { + updateFieldValue(name, null, {}, options); +} + +// Disable a form input +function disableFormInput(name, options) { + $(options.modal).find(`#id_${name}`).prop('disabled', true); +} + + +// Enable a form input +function enableFormInput(name, options) { + $(options.modal).find(`#id_${name}`).prop('disabled', false); +} + + +// Hide a form input +function hideFormInput(name, options) { + $(options.modal).find(`#div_id_${name}`).hide(); +} + + +// Show a form input +function showFormInput(name, options) { + $(options.modal).find(`#div_id_${name}`).show(); +} + + // Hide a form group function hideFormGroup(group, options) { $(options.modal).find(`#form-panel-${group}`).hide(); } + // Show a form group function showFormGroup(group, options) { $(options.modal).find(`#form-panel-${group}`).show(); } + function setFormGroupVisibility(group, vis, options) { if (vis) { showFormGroup(group, options); @@ -1145,7 +1404,7 @@ function setFormGroupVisibility(group, vis, options) { } -function initializeRelatedFields(fields, options) { +function initializeRelatedFields(fields, options={}) { var field_names = options.field_names; @@ -1181,21 +1440,23 @@ function initializeRelatedFields(fields, options) { */ function addSecondaryModal(field, fields, options) { - var name = field.name; + var field_name = getFieldName(field.name, options); - var secondary = field.secondary; + var depth = options.depth || 0; var html = ` -
    - ${secondary.label || secondary.title} +
    + ${field.secondary.label || field.secondary.title}
    `; - $(options.modal).find(`label[for="id_${name}"]`).append(html); + $(options.modal).find(`label[for="id_${field_name}"]`).append(html); // Callback function when the secondary button is pressed - $(options.modal).find(`#btn-new-${name}`).click(function() { + $(options.modal).find(`#btn-new-${field_name}`).click(function() { + + var secondary = field.secondary; // Determine the API query URL var url = secondary.api_url || field.api_url; @@ -1216,16 +1477,24 @@ function addSecondaryModal(field, fields, options) { // Force refresh from the API, to get full detail inventreeGet(`${url}${data.pk}/`, {}, { success: function(responseData) { - - setRelatedFieldData(name, responseData, options); + setRelatedFieldData(field.name, responseData, options); } }); }; } + // Relinquish keyboard focus for this modal + $(options.modal).modal({ + keyboard: false, + }); + // Method should be "POST" for creation secondary.method = secondary.method || 'POST'; + secondary.modal = null; + + secondary.depth = depth + 1; + constructForm( url, secondary @@ -1235,7 +1504,7 @@ function addSecondaryModal(field, fields, options) { /* - * Initializea single related-field + * Initialize a single related-field * * argument: * - modal: DOM identifier for the modal window @@ -1243,13 +1512,12 @@ function addSecondaryModal(field, fields, options) { * - field: Field definition from the OPTIONS request * - options: Original options object provided by the client */ -function initializeRelatedField(field, fields, options) { +function initializeRelatedField(field, fields, options={}) { var name = field.name; if (!field.api_url) { - // TODO: Provide manual api_url option? - console.log(`Related field '${name}' missing 'api_url' parameter.`); + console.log(`WARNING: Related field '${name}' missing 'api_url' parameter.`); return; } @@ -1266,10 +1534,31 @@ function initializeRelatedField(field, fields, options) { // limit size for AJAX requests var pageSize = options.pageSize || 25; + var parent = null; + var auto_width = false; + var width = '100%'; + + // Special considerations if the select2 input is a child of a modal + if (options && options.modal) { + parent = $(options.modal); + auto_width = true; + width = null; + } + select.select2({ placeholder: '', - dropdownParent: $(options.modal), - dropdownAutoWidth: false, + dropdownParent: parent, + dropdownAutoWidth: auto_width, + width: width, + language: { + noResults: function(query) { + if (field.noResults) { + return field.noResults(query); + } else { + return '{% trans "No results found" %}'; + } + } + }, ajax: { url: field.api_url, dataType: 'json', @@ -1292,6 +1581,11 @@ function initializeRelatedField(field, fields, options) { query.search = params.term; query.offset = offset; query.limit = pageSize; + + // Allow custom run-time filter augmentation + if ('adjustFilters' in field) { + query = field.adjustFilters(query, options); + } return query; }, @@ -1367,6 +1661,11 @@ function initializeRelatedField(field, fields, options) { data = item.element.instance; } + // Run optional callback function + if (field.onSelect && data) { + field.onSelect(data, field, options); + } + if (!data.pk) { return field.placeholder || ''; } @@ -1386,6 +1685,7 @@ function initializeRelatedField(field, fields, options) { // If a 'value' is already defined, grab the model info from the server if (field.value) { + var pk = field.value; var url = `${field.api_url}/${pk}/`.replace('//', '/'); @@ -1394,6 +1694,30 @@ function initializeRelatedField(field, fields, options) { setRelatedFieldData(name, data, options); } }); + } else if (field.auto_fill) { + // Attempt to auto-fill the field + + var filters = {}; + + // Update with nominal field fields + Object.assign(filters, field.filters || {}); + + // Update with filters only used for initial filtering + Object.assign(filters, field.auto_fill_filters || {}); + + // Enforce pagination, limit to a single return (for fast query) + filters.limit = 1; + filters.offset = 0; + + inventreeGet(field.api_url, filters || {}, { + success: function(data) { + + // Only a single result is available, given the provided filters + if (data.count == 1) { + setRelatedFieldData(name, data.results[0], options); + } + } + }); } } @@ -1407,7 +1731,7 @@ function initializeRelatedField(field, fields, options) { * - data: JSON data representing the model instance * - options: The modal form specifications */ -function setRelatedFieldData(name, data, options) { +function setRelatedFieldData(name, data, options={}) { var select = getFormFieldElement(name, options); @@ -1487,6 +1811,15 @@ function renderModelData(name, model, data, parameters, options) { case 'partparametertemplate': renderer = renderPartParameterTemplate; break; + case 'purchaseorder': + renderer = renderPurchaseOrder; + break; + case 'salesorder': + renderer = renderSalesOrder; + break; + case 'salesordershipment': + renderer = renderSalesOrderShipment; + break; case 'manufacturerpart': renderer = renderManufacturerPart; break; @@ -1520,6 +1853,20 @@ function renderModelData(name, model, data, parameters, options) { } +/* + * Construct a field name for the given field + */ +function getFieldName(name, options={}) { + var field_name = name; + + if (options && options.depth) { + field_name += `_${options.depth}`; + } + + return field_name; +} + + /* * Construct a single form 'field' for rendering in a form. * @@ -1546,7 +1893,7 @@ function constructField(name, parameters, options) { return constructCandyInput(name, parameters, options); } - var field_name = `id_${name}`; + var field_name = getFieldName(name, options); // Hidden inputs are rendered without label / help text / etc if (parameters.hidden) { @@ -1566,6 +1913,8 @@ function constructField(name, parameters, options) { var group = parameters.group; + var group_id = getFieldName(group, options); + var group_options = options.groups[group] || {}; // Are we starting a new group? @@ -1573,12 +1922,12 @@ function constructField(name, parameters, options) { if (parameters.group != options.current_group) { html += ` -
    -
    `; +
    +
    `; if (group_options.collapsible) { html += ` -
    - + -
    +
    `; } @@ -1600,18 +1949,24 @@ function constructField(name, parameters, options) { options.current_group = group; } - var form_classes = 'form-group'; + var form_classes = options.form_classes || 'form-group'; if (parameters.errors) { - form_classes += ' has-error'; + form_classes += ' form-field-error'; } - + // Optional content to render before the field if (parameters.before) { html += parameters.before; } - html += `
    `; + var hover_title = ''; + + if (parameters.help_text) { + hover_title = ` title='${parameters.help_text}'`; + } + + html += `
    `; // Add a label if (!options.hideLabels) { @@ -1645,17 +2000,17 @@ function constructField(name, parameters, options) { html += `
    `; if (parameters.prefix) { - html += `${parameters.prefix}`; + html += `${parameters.prefix}`; } } - html += constructInput(name, parameters, options); + html += constructInput(field_name, parameters, options); if (extra) { - if (!parameters.required) { + if (!parameters.required && !options.hideClearButton) { html += ` - + `; } @@ -1664,12 +2019,15 @@ function constructField(name, parameters, options) { } if (parameters.help_text && !options.hideLabels) { - html += constructHelpText(name, parameters, options); + + // Boolean values are handled differently! + if (parameters.type != 'boolean' && !parameters.hidden) { + html += constructHelpText(name, parameters, options); + } } // Div for error messages - html += `
    `; - + html += `
    `; html += `
    `; // controls html += `
    `; // form-group @@ -1723,7 +2081,7 @@ function constructLabel(name, parameters) { * - parameters: Field parameters returned by the OPTIONS method * */ -function constructInput(name, parameters, options) { +function constructInput(name, parameters, options={}) { var html = ''; @@ -1759,6 +2117,8 @@ function constructInput(name, parameters, options) { case 'candy': func = constructCandyInput; break; + case 'raw': + func = constructRawInput; default: // Unsupported field type! break; @@ -1775,7 +2135,7 @@ function constructInput(name, parameters, options) { // Construct a set of default input options which apply to all input types -function constructInputOptions(name, classes, type, parameters) { +function constructInputOptions(name, classes, type, parameters, options={}) { var opts = []; @@ -1793,8 +2153,15 @@ function constructInputOptions(name, classes, type, parameters) { } if (parameters.value != null) { - // Existing value? - opts.push(`value='${parameters.value}'`); + if (parameters.type == 'boolean') { + // Special consideration of a boolean (checkbox) value + if (parameters.value == true || parameters.value.toString().toLowerCase() == 'true') { + opts.push('checked'); + } + } else { + // Existing value? + opts.push(`value='${parameters.value}'`); + } } else if (parameters.default != null) { // Otherwise, a defualt value? opts.push(`value='${parameters.default}'`); @@ -1837,7 +2204,6 @@ function constructInputOptions(name, classes, type, parameters) { switch (parameters.type) { case 'boolean': - opts.push(`style='display: inline-block; width: 20px; margin-right: 20px;'`); break; case 'integer': case 'float': @@ -1850,6 +2216,26 @@ function constructInputOptions(name, classes, type, parameters) { if (parameters.multiline) { return ``; + } else if (parameters.type == 'boolean') { + + if (parameters.hidden) { + return ''; + } + + var help_text = ''; + + if (!options.hideLabels && parameters.help_text) { + help_text = `${parameters.help_text}`; + } + + return ` +
    + + +
    + `; } else { return ``; } @@ -1869,13 +2255,14 @@ function constructHiddenInput(name, parameters) { // Construct a "checkbox" input -function constructCheckboxInput(name, parameters) { +function constructCheckboxInput(name, parameters, options={}) { return constructInputOptions( name, - 'checkboxinput', + 'form-check-input', 'checkbox', - parameters + parameters, + options ); } @@ -2011,6 +2398,17 @@ function constructCandyInput(name, parameters) { } +/* + * Construct a "raw" field input + * No actual field data! + */ +function constructRawInput(name, parameters) { + + return parameters.html; + +} + + /* * Construct a 'help text' div based on the field parameters * @@ -2021,13 +2419,121 @@ function constructCandyInput(name, parameters) { */ function constructHelpText(name, parameters) { - var style = ''; - - if (parameters.type == 'boolean') { - style = `style='display: inline-block; margin-left: 25px' `; - } - - var html = `
    ${parameters.help_text}
    `; + var html = `
    ${parameters.help_text}
    `; return html; } + + +/* + * Construct a dialog to select import fields + */ +function selectImportFields(url, data={}, options={}) { + + if (!data.model_fields) { + console.log(`WARNING: selectImportFields is missing 'model_fields'`); + return; + } + + if (!data.file_fields) { + console.log(`WARNING: selectImportFields is missing 'file_fields'`); + return; + } + + var choices = []; + + // Add an "empty" value + choices.push({ + value: '', + display_name: '-----', + }); + + for (const [name, field] of Object.entries(data.model_fields)) { + choices.push({ + value: name, + display_name: field.label || name, + }); + } + + var rows = ''; + + var field_names = Object.keys(data.file_fields); + + for (var idx = 0; idx < field_names.length; idx++) { + + var field_name = field_names[idx]; + + var choice_input = constructInput( + `column_${idx}`, + { + type: 'choice', + label: field_name, + value: data.file_fields[field_name].value, + choices: choices, + } + ); + + rows += `${field_name}${choice_input}`; + } + + var headers = `{% trans "File Column" %}{% trans "Field Name" %}`; + + var html = ''; + + if (options.preamble) { + html += options.preamble; + } + + html += `${headers}${rows}
    `; + + constructForm(url, { + method: 'POST', + title: '{% trans "Select Columns" %}', + fields: {}, + preFormContent: html, + onSubmit: function(fields, opts) { + + var columns = []; + + for (var idx = 0; idx < field_names.length; idx++) { + columns.push(getFormFieldValue(`column_${idx}`, {}, opts)); + } + + $(opts.modal).find('#modal-progress-spinner').show(); + + inventreePut( + opts.url, + { + columns: columns, + rows: data.rows, + }, + { + method: 'POST', + success: function(response) { + handleFormSuccess(response, opts); + + if (options.success) { + options.success(response); + } + }, + error: function(xhr) { + + $(opts.modal).find('#modal-progress-spinner').hide(); + + switch (xhr.status) { + case 400: + handleFormErrors(xhr.responseJSON, fields, opts); + break; + default: + $(opts.modal).modal('hide'); + + console.log(`upload error at ${opts.url}`); + showApiError(xhr, opts.url); + break; + } + } + } + ); + }, + }); +} diff --git a/InvenTree/templates/js/translated/helpers.js b/InvenTree/templates/js/translated/helpers.js index 6e3f7f0c95..2d35513e78 100644 --- a/InvenTree/templates/js/translated/helpers.js +++ b/InvenTree/templates/js/translated/helpers.js @@ -16,9 +16,9 @@ function yesNoLabel(value) { if (value) { - return `{% trans "YES" %}`; + return `{% trans "YES" %}`; } else { - return `{% trans "NO" %}`; + return `{% trans "NO" %}`; } } @@ -62,15 +62,16 @@ function imageHoverIcon(url) { * @param {String} url is the image URL * @returns html tag */ -function thumbnailImage(url) { +function thumbnailImage(url, options={}) { if (!url) { - url = '/static/img/blank_img.png'; + url = blankImage(); } // TODO: Support insertion of custom classes + var title = options.title || ''; - var html = ``; + var html = ``; return html; @@ -87,19 +88,23 @@ function select2Thumbnail(image) { } +/* + * Construct an 'icon badge' which floats to the right of an object + */ function makeIconBadge(icon, title) { - // Construct an 'icon badge' which floats to the right of an object - var html = ``; + var html = ``; return html; } +/* + * Construct an 'icon button' using the fontawesome set + */ function makeIconButton(icon, cls, pk, title, options={}) { - // Construct an 'icon button' using the fontawesome set - var classes = `btn btn-default btn-glyph ${cls}`; + var classes = `btn btn-outline-secondary ${cls}`; var id = `${cls}-${pk}`; @@ -178,8 +183,14 @@ function makeProgressBar(value, maximum, opts={}) { var id = options.id || 'progress-bar'; + var style = ''; + + if (opts.max_width) { + style += `max-width: ${options.max_width}; `; + } + return ` -
    +
    ${text}
    diff --git a/InvenTree/templates/js/translated/modals.js b/InvenTree/templates/js/translated/modals.js index 96e41fd6ec..f114f6f419 100644 --- a/InvenTree/templates/js/translated/modals.js +++ b/InvenTree/templates/js/translated/modals.js @@ -43,18 +43,16 @@ function createNewModal(options={}) { }); var html = ` -