Compare commits

...

298 Commits

Author SHA1 Message Date
Oliver
a7487ff842 Backport of https://github.com/inventree/InvenTree/pull/5632 (#5650)
Co-authored-by: Matthias Mair <code@mjmair.com>
2023-10-03 12:55:15 +11:00
Oliver
5725a9e271 Fix for readthedocs build config file (#5651) 2023-10-03 12:28:14 +11:00
Oliver
fe9a56a5c1 Allow stocktake quantity to be zero (#5644)
- Backport of https://github.com/inventree/InvenTree/pull/5627/

Co-authored-by: Oliver Lippert <oliver@allesit.de>
2023-10-02 22:49:10 +11:00
Oliver
39e682cd45 Correctly extract error information if plugin install fails (#5638) 2023-10-01 14:29:08 +11:00
github-actions[bot]
a36ab0c004 Fix bug in loading setting from cache (#5612) (#5616)
* Fix bug in loading setting from cache

- Need to bring out "cache" key first, otherwise it gets included in the calculated cache key
- Reference: https://github.com/inventree/InvenTree/issues/5586

* Unit test fixes

(cherry picked from commit 87a8755e1b)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2023-09-25 16:05:17 +10:00
github-actions[bot]
0b45d6f236 Use prefetch to improve query efficiency (#5613) (#5614)
- Significantly improves performance of users.models.update_group_roles() function
- Ref: https://github.com/inventree/InvenTree/pull/5612

(cherry picked from commit 200bc5bd3d)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2023-09-25 13:35:13 +10:00
github-actions[bot]
978e08f3a3 stop cascade update_pricing if prices didnt change (#5606) (#5607)
Co-authored-by: Oliver Lippert <oliver@lipperts-web.de>
(cherry picked from commit 2f0dbf9776)

Co-authored-by: Oliver Lippert <oliver@allesit.de>
2023-09-24 23:06:03 +10:00
github-actions[bot]
85b8157611 Primary address fix (#5592) (#5598)
* Improve management of primary address for a company

- Simplify approach (remove "confirm_primary" field)
- Remove @receiver hook
- Move all logic into Address.save() method

* Make address primary if it is the only one defined for a company

* Update frontend table

* Fix saving logic

* Actually fix it this time

* Fix for unit test

* Another test fix

(cherry picked from commit 324d5929b5)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2023-09-23 10:25:12 +10:00
github-actions[bot]
aaabce9873 Fix set password when no old password exists (#5562) (#5563)
(cherry picked from commit fe68598c1b)

Co-authored-by: Firas Ben Mefteh <ben.meft0@gmail.com>
2023-09-18 20:58:17 +10:00
Oliver
f5a36ce44e Update version.py (#5553)
Bump to 0.12.8
2023-09-18 13:35:33 +10:00
Oliver
6563b4c413 Pin setuptools version for docker CI (#5552)
* Pin setuptools version for docker CI

* Fix doc links
2023-09-16 12:17:43 +10:00
github-actions[bot]
abed9fb284 Update required parts for build orders (#5542) (#5543)
- When a BomItem is created or edited, update any active build orders which use it
- Runs as a background task
- Fixes https://github.com/inventree/InvenTree/issues/5541

(cherry picked from commit c8021ec319)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2023-09-15 10:06:39 +10:00
Lukas
09872eec8e [0.12.x] Fix missing filters for get settings validator (#5480) (#5522)
* Fix missing filters for get settings validator (#5480)

* Fix missing filters for get settings validator

* merge default model instance filters and kwargs

* Added tests for validators

* Give it a try without the kwargs passed to clean in save function

* Added string for identification for debug statement

* Added more debug comments

* Added more debug prints

* Fix test debug

* Modiefied workflow

* trigger ci

* Fix test and remove unused kwargs

* Added debug prints

* Only run one test in ci

* Added more debug code

* Remove all debug prints and reset workflow

* Reset overlooked file

(cherry picked from commit 9a6c2d2953)

# Conflicts:
#	InvenTree/plugin/samples/integration/test_sample.py

* Add missing import

* Added second missing import
2023-09-11 10:57:42 +10:00
github-actions[bot]
099b837a4e Login form fix (#5502) (#5504)
* Handle login without supplier user

- Use custom login form
- Redirect back to login page
- No longer throws error

* Fix method return

(cherry picked from commit 71ad4a1c99)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2023-09-05 12:43:39 +10:00
Oliver
cf977ad29a Update version.py (#5474)
Bump version to 0.12.7
2023-08-24 18:51:20 +10:00
github-actions[bot]
72464c50cc Url fix (#5472) (#5473)
* Use urljoin function to construct absolute URL

* Add unit test

(cherry picked from commit 8da5d62c69)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2023-08-24 16:04:42 +10:00
Oliver
942bc5350d Bump version to 0.12.6 (#5465)
- Skipping 0.12.5 due to an error on the last release
2023-08-23 12:22:32 +10:00
github-actions[bot]
7876676114 Fix plugin pickeling (#5412) (#5457)
(cherry picked from commit 1fe382e318)

Co-authored-by: Lukas <76838159+wolflu05@users.noreply.github.com>
2023-08-17 21:04:23 +10:00
Oliver
ea039645c3 Update unit tests (#5446)
- Remove failing test which no longer applies
2023-08-14 16:39:00 +10:00
github-actions[bot]
b5c7cf0779 Fix html tag in template (#5445) (#5448)
- Ensure <td> tag is closed correctly

(cherry picked from commit e7b5b145bf)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2023-08-14 16:28:58 +10:00
github-actions[bot]
89d8e47bab Disable "add rate" button in Admin interface (#5444) (#5447)
- Does not work with custom backend
- Throws error if the button is pressed
- So, remove the button

(cherry picked from commit a2f614ad41)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2023-08-14 16:19:13 +10:00
github-actions[bot]
b8e726d8a4 Catch IndexError when importing data (#5439) (#5443)
* Catch IndexError when importing data

* Also handle TypeError

(cherry picked from commit 93e4dadb49)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2023-08-14 15:39:07 +10:00
github-actions[bot]
3b238fdbba Fix for potential NoReverseMatch error (#5440) (#5442)
- Check that the database model really does exist in the template code

(cherry picked from commit a8118ed406)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2023-08-14 15:21:29 +10:00
github-actions[bot]
df8c2692a0 Fix build output unallocate button (#5426) (#5427)
(cherry picked from commit dce565b4a3)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2023-08-11 10:13:52 +10:00
Oliver
7391f33a97 Do not enforce units for part parameters (#5423)
Backport of #5160
2023-08-10 21:13:46 +10:00
Oliver
b1158f7083 Bump version number to 0.12.5 (#5424) 2023-08-10 21:13:37 +10:00
github-actions[bot]
4969628150 Purchase history graph fix (#5421) (#5422)
* Fix debug messages

* Fix bug in purchase history chart

- Use new pack_quantity_native attribute

(cherry picked from commit 86ca0b27a4)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2023-08-10 14:33:37 +10:00
Oliver
57eada1da1 backport email fix (#5409)
- Backport of https://github.com/inventree/InvenTree/pull/5396
2023-08-08 15:19:00 +10:00
Oliver
f526dcdeec fix cli on 22.04 (#5204) (#5395)
* fix cli on 22.04 (#5204)

(cherry picked from commit d4fad4f5c8)

* Update weasyprint docs link

* Another link fix

---------

Co-authored-by: Matthias Mair <code@mjmair.com>
2023-08-03 16:21:53 +10:00
github-actions[bot]
aacf35ed47 Improve sorting of part column for BOM table (#5386) (#5387)
(cherry picked from commit c39ae80a13)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2023-08-02 17:43:08 +10:00
github-actions[bot]
ca986cba01 Fix auto-allocation of build outputs (#5378) (#5379)
- Creation of BuildItem objects was using old model references

(cherry picked from commit 668dab4175)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2023-08-01 11:26:10 +10:00
github-actions[bot]
699fb83dd4 Fix SSO check comparing id against name and extend log output (#5340) (#5377)
* add error log on SSO check failure

* sso_check_provider: fix by comparing against id

the name is the pretty printed version which not necessarily is the same
as the provider id it is compared against. This fails e.g. for the
microsoft allauth extension where the id is microsoft, but the name is
"Microsoft Graph".

Closes: #5330
(cherry picked from commit ee5416719f)

Co-authored-by: Hendrik v. Raven <hendrik@consetetur.de>
2023-08-01 10:39:46 +10:00
Oliver
dd6e225cda Update version.py (#5374)
Bump version number to 0.12.4
2023-07-31 12:45:49 +10:00
github-actions[bot]
1f3a49b1ae Fix for migration - updating from old version (#5372) (#5373)
(cherry picked from commit 90383ccb53)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2023-07-31 12:43:20 +10:00
github-actions[bot]
385e7cb478 Return 404 on API requests other than GET (#5365) (#5366)
- Other request methods need love too!

(cherry picked from commit 59ffdcaa19)
(cherry picked from commit b89a120f9e)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2023-07-28 22:14:55 +10:00
github-actions[bot]
73768bfee1 Handle purchase price export for .xls files (#5362) (#5363)
(cherry picked from commit 87da286f2f)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2023-07-28 15:36:34 +10:00
github-actions[bot]
946fe2df29 Handle errors when printing reports (#5360) (#5361)
- Re-throw as a ValidationError
- Results in a 400 error, not a 500

(cherry picked from commit 5f3d3b28b3)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2023-07-28 14:23:43 +10:00
github-actions[bot]
afa7ed873f Exclude some common fields from django-import-export (#5349) (#5351)
- Add "get_fields()" method to InvenTreeResource
- Override default behaviour and exclude some common fields
- Will flow down to any inheriting classes

(cherry picked from commit 941451203a)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2023-07-26 17:22:21 +10:00
github-actions[bot]
46da332afe Allow duplicate BOM items when duplicating a part (#5347) (#5350)
(cherry picked from commit 6660508326)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2023-07-26 16:54:20 +10:00
Oliver
072b7b3146 Update version.py
Bump version number to 0.12.3
2023-07-25 11:46:41 +10:00
github-actions[bot]
1d51b2a058 Email config fix (#5336) (#5338)
* Change for DEFAULT_FROM_EMAIL

- Use USERNAME if not specified

(cherry picked from commit 487ac917c90e9fe3da4effaa9326b707ceecd321)

* Email configuration fails if DEFAULT_FROM_EMAIL not set

(cherry picked from commit 01e573c3a2702e7c21ed13b0cb44280c89d3dee1)

* Docs update

(cherry picked from commit bfedb9cf87)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2023-07-25 11:45:17 +10:00
github-actions[bot]
08f9bebdf0 Fix admin url to point to right model (#5319) (#5321)
(cherry picked from commit 9b377ccfbf)

Co-authored-by: Marcel Pörner <me@nerade.de>
2023-07-23 22:38:27 +10:00
github-actions[bot]
6d6629f11c Stock installed table fix (#5305) (#5306)
* Prevent installed items from being hidden

* Fix parent / child relationship

(cherry picked from commit f70294b247)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2023-07-21 23:57:00 +10:00
github-actions[bot]
db88fbda11 Fix company index page title (#5288) (#5291)
(cherry picked from commit 3baa640d70)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2023-07-20 10:07:39 +10:00
github-actions[bot]
49c9b5b1aa Docker build: Update python deps (#5270) (#5271)
* Update python deps

* Update requirements.in

* Fix requirements-dev.txt

(cherry picked from commit b717011f06)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2023-07-18 20:11:29 +10:00
github-actions[bot]
e1a0e79ead Fix settings function callback (#5259) (#5262)
* fix settings function callback

* merge instance filters and passed keys

(cherry picked from commit df77305d60)

Co-authored-by: Matthias Mair <code@mjmair.com>
2023-07-17 20:23:44 +10:00
github-actions[bot]
ab22f2a04d Fix language code for pt-br (#5256) (#5257)
- Has to be lowercase in settings.py to work correctly

(cherry picked from commit 20b59c3575)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2023-07-16 19:37:27 +10:00
github-actions[bot]
8a58bf5ffa Only update theme if value provided (#5240) (#5241)
- Handles case where null or invalid value provided

(cherry picked from commit 41167f22c9)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2023-07-13 20:39:28 +10:00
Oliver
6730098bac Update version.py (#5238)
Bump version number to 0.12.2
2023-07-13 15:13:47 +10:00
github-actions[bot]
93b44ad8e6 fix typo (#5236) (#5237)
(cherry picked from commit bd1689095d)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2023-07-13 11:03:28 +10:00
github-actions[bot]
9b5e828b87 Protected settings fix (#5229) (#5231)
* Hide protected setting in settings view

* Implement custom serializer for setting value

- Return '***' if the setting is protected

* Implement to_internal_value

* Stringify

* Add protected setting to sample plugin

* Unit tests for plugin settings API

* Update unit test

(cherry picked from commit 01f2aa5f74)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2023-07-12 16:31:28 +10:00
github-actions[bot]
cf5d637678 Add missing callback for attachment delete button (#5219) (#5220)
(cherry picked from commit b3dcc28bd9)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2023-07-11 11:30:57 +10:00
github-actions[bot]
feb2acf668 Fix link to SalesOrder in stock history table (#5210) (#5211)
(cherry picked from commit 8fb7612894)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2023-07-10 13:23:09 +10:00
Oliver
0017570dd3 Bump version number to 0.12.1 (#5201) 2023-07-07 14:25:30 +10:00
github-actions[bot]
4c41a50bb1 Fix allocation check for completing build order (#5199) (#5200)
- Allocation check only applies to untracked line items

(cherry picked from commit 1f81daadf6)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2023-07-07 13:48:18 +10:00
github-actions[bot]
eab3fdcf2c Fix quantity aggregation for stock table (#5188) (#5190)
* Fix quantity aggregation for stock table

- Stock quantity can only be added together if units are the same

* Add stock total footer to part table

(cherry picked from commit 773dd3b210)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2023-07-06 12:55:22 +10:00
github-actions[bot]
c59eee7359 Param fix (#5183) (#5184)
* Handle AttributeError in convert_physical_value

* Added new unit test

(cherry picked from commit 9abcc0ec34)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2023-07-06 11:11:27 +10:00
github-actions[bot]
4a5ebf8f01 Handle exception when creating default labels (#5163) (#5166)
* Handle exception when creating default labels

- Running workers in parallel may cause race conditions
- Catch any exception which is raised

* Prevent password from being logged

* Update default timeout for docker

(cherry picked from commit 8b730884d7)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2023-07-04 22:54:21 +10:00
github-actions[bot]
698798fee7 Order table improvements (#5151) (#5152)
- prevent "double loading" of order tables

(cherry picked from commit 17c2070503)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2023-07-04 16:23:43 +10:00
github-actions[bot]
2660889879 Rendering fix for build allocation table (#5145) (#5149)
- Fix link to part
- Fix link to stock item

(cherry picked from commit 5f61b5f120)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2023-07-04 13:40:40 +10:00
github-actions[bot]
01aaf95a0e fix: add missing build model property (#5127) (#5132)
* fix: add missing virtual build property

* chore: improve docstring

(cherry picked from commit 2e7c86ff92)

Co-authored-by: Mark Oude Elberink <mark@oude-elberink.de>
2023-07-03 10:04:02 +10:00
Oliver
1d18b487f3 Update version.py (#5123)
Bump version to 0.12.0
2023-06-30 14:22:43 +10:00
Oliver
7955d1f579 Get git log fix (#5113)
* Simplify extraction of git repo

- Replace Repo.discover() with Repo()
- Ensure provided path is directory

* Remove profiling code
2023-06-30 13:43:54 +10:00
Oliver
c8642bedcd remove old method (#5120)
- not used
2023-06-29 22:38:37 +10:00
Oliver
752fb97d2f Transfer stock location (#5117)
* Hide info messages if no data

* pre-fill location when transferring stock

* Change iteration variable

* Measure twice, cut once

* js improvements
2023-06-28 21:59:47 +10:00
Oliver
a78b26f93a Child items table (#5114)
* Template cleanup

- Remove "buttons" attribute (outdated)

* Fix filters for "child stock items" table

* Fix 'ancestor' stock filter
2023-06-28 15:22:23 +10:00
Oliver
53e120cdb3 Fix for index page badge (#5115)
- If table was not paginated, totalRows did not exist
- Handle this case by looking at length of actual data
2023-06-28 15:22:08 +10:00
Lavissa
940fa74365 Update docker dev docs (#4743)
* Change docker dev server install

* Added --rm to docker compose run commands
  This deletes the one-time-use container after exiting

* Added --dev to setup-test. This runs setup-dev as a part of
  setup-test.

* Revisions

* Updates

* Add context

* Update docs

* Tyops :)

* Remove reference to action that has not happened yet
2023-06-26 17:10:58 +10:00
Oliver
8b6abe1505 Fix for stock status change (#5105)
- Recent refactor introduced bug
- Update stock.js
2023-06-26 12:14:44 +10:00
Oliver
256c9cb751 New Crowdin updates (#5101)
* updated translation base

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-06-26 09:44:10 +10:00
Matthias Mair
73e0d03abf Merge workflows (#5103)
* merge workflows
from https://github.com/inventree/InvenTree/pull/5087

* syntax fix
2023-06-26 09:43:57 +10:00
Matthias Mair
a83a71b3a3 Add Notifications shortcut (#5104)
* add notify_users shortcut

* make html template optional
2023-06-26 09:43:07 +10:00
Oliver
0458b5c53a Variant low stock (#5102)
* Handle FileNotFoundError

* Adjust "low_stock" query filter

- Include variant stock
2023-06-24 22:31:52 +10:00
Oliver
5dfc389c06 New Crowdin updates (#5083)
* updated translation base

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-06-24 14:36:29 +10:00
Lavissa
a5c8d86530 Synergise label filter help texts (#5100)
* Synergise label filter help texts

* Fix QR code alt text

* Change label alt text from QC or QR
2023-06-24 08:40:36 +10:00
Oliver
24b554a8d2 PartParameter API improvements (#5094)
* PartParameter: Include template_detail by default

* PartParameter API updates

- Allow sorting by template name
- Refactor with mixin class

* Bug fixes

* Bump API version
2023-06-24 01:04:11 +10:00
Oliver
fab738cd75 Scheduling api fix (#5093)
* Fix query for part scheduling API

* Add unit test for scheduling endpoint

* Remove length check
2023-06-23 23:55:52 +10:00
Oliver
603aef5da9 Catch Part.DoesNotExist (#5092)
- Can be thrown when importing Part instances
- Fixes https://github.com/inventree/InvenTree/issues/5090
2023-06-23 21:30:51 +10:00
Oliver
198245d0d2 Pin mysqlclient lib for docker (#5091)
- Recent updates have caused breakage
2023-06-23 20:21:26 +10:00
Oliver
3b4e20b54a Unit Test Improvements (#5087)
* Disable migration testing

- Compare how long the unit tests take

* Change file

- To get unit tests to run

* Fix format

* Consolidate tasks.py

- Remove coverage task
- Add --coverage flag to test task

* Fix typo

* Run migration unit tests if migration files are updated

* Fix

* Touch migration file

- Should cause migration unit tests to be run

* Force migration checks for docker build

* Prevent default report creation in unit tests

- Should save some time

* Add simple profiling for plugin loading

- Display time taken to load each plugin

* Fix to invoke test

* Disable get_git_log (for testing)

* Disable get_git_path in CI

- Might remove this entirely?
- For now, bypass for unit testing

* Add debug for unit registry

- Display time taken to load registry

* Don't full-reload unit registry

* Adjust migration test workflow

- env var updates
- change paths-filter output

* Fix for migration_test.yaml

- Actually need to set the output

* env fix

* db name

* Prevent sleep if in test mode

* Reduce sleep time on wait_for_db
2023-06-23 17:25:59 +10:00
Oliver
693d24b4b6 Table filters refactor (#5086)
- Fix a couple of bugs
- Code cleanup
2023-06-22 10:29:14 +10:00
Oliver
deffcc2814 Update FUNDING.yml (#5085)
Fix paypal link
2023-06-21 22:35:25 +10:00
Oliver
3001471706 README Updates (#5084)
* Update funding.yml

* Cleanup README.md

* More cleanup
2023-06-21 22:30:30 +10:00
Oliver
7f5e844367 Fix for loadPartTable (#5082)
- Recent changes introduced a bug related to filters
- Only really visible on index page
2023-06-21 16:42:10 +10:00
Oliver
66ec82d4d1 Add migration test workflow (#4973)
* Add migration test workflow

* Adds dummy migration

- For testing only
- Will be removed

* Improvements to workflow

* Update workflow

* Use manual git step

* Don't install twice

* Let's try again

* Does v3 work?

* Try https repo

* Fix repo address

* Once more with feeling

* Fix location of test database

* Revert change to wrong file

* Fix DB location

* Hard-code database path

* debugging

* More updates grrr

* Adjust permissions on database file

* Manual git operation

* Updates for migration test workflow

* Fix database names

* Remove dummy migration file

* Fix filters

* Fix path filter

* Explicit pathing

* Fix formatting error

* Another formatting error
2023-06-20 22:37:30 +10:00
Oliver
e2800b19ef New Crowdin updates (#5061)
* updated translation base

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-06-20 22:27:16 +10:00
Oliver
303305e05f Fix constraint for address model (#5076)
* Fix constraint for address model

- Do not handle at database level
- Add a "validate_unique" method to the address model
- Fixes https://github.com/inventree/InvenTree/issues/5070

* Remove unique constraint rule

* Unit test fix

- Adjust unit test around new code

* Further unit test updates
2023-06-20 16:57:20 +10:00
Matthias Mair
b700b44c53 Add dj-rest-auth (#4187)
* Add dj-rest-auth
[FR] User registration via API
Fixes #3978

* add jwt support for API

* check for old password

* Add check if registration is allowed

* make email mandatory if selected

* lower postgres version?

* update req

* revert psql change

* move form options out

* Update reqs

* Add handlers for most OAuth2

* refactor and add logging

* make error message more actionable

* add handler for twitter

* add keycloak endpoint

* warning for legacy apps

* remove legacy twitter support

* rename file

* move url to sub

* make JWT optional (default off)

* Add var to config template

* Add API endpoint to list available providers

* fix url pattern
2023-06-20 16:26:02 +10:00
Oliver
09cc654530 Required for order fix (#5077)
* Fix part tables on index page

- Stop table filters overriding each other

* Refactor "needed for build" table

- Now shows amount actually needed

* Fix typo

* js fix

* linting

* Fix warning messages on index page

* js linting
2023-06-20 12:39:18 +10:00
Oliver
4c9d4add2c Table custom buttons (#5075)
* Add generic implementation for barcode actions

- Commonize code against tables
- Cleaner UI
- Better code
- Will make future react refactor easier

* Add permissions.js

- Separate .js file for dynamically checking permissions

* Update stock table to use client-side actions

* API endpoint for bulk category adjustment

* Bug fix for purchase_order.js

- Prevent some really strange API calls

* Refactor actions for part table

- Now done dynamically

* Refactor actions for the attachment tables

* Refactor actions for build output table

* Increment API version

* Cleanup janky button

* Refactor supplier part table

* Refactor manufacturer part table

* Remove linkButtonsToSelection

- no longer needed
- Cleanup, yay!

* Cleanup purchase order line table

* Refactor BOM table buttons

* JS linting

* Further cleanup

* Template cleanup

- remove extra div elements

* js linting

* js fix
2023-06-20 07:45:35 +10:00
Lavissa
13389845b1 Add Contacts to admin panel (#5065)
* Contact model added to admin panel
2023-06-19 22:42:02 +10:00
Oliver
021a5a4081 Fixes for purchase order receive buttons (#5072) 2023-06-19 20:01:00 +10:00
Matthias Mair
617ad6c233 decouble ruleset migration (#5067) 2023-06-19 16:56:26 +10:00
Oliver
2e8fb2a14a Stock status change API (#5064)
* Add API endpoint for changing stock item status

- Change status for multiple items simultaneously
- Reduce number of database queries required

* Perform bulk update in serializer

* Update 'updated' field

* Add front-end code

* Bump API version

* Bug fix and unit test
2023-06-18 07:40:47 +10:00
Oliver
f6420f98c2 Simplify release notes docs (#5063) 2023-06-17 21:56:26 +10:00
Lavissa
bf707766b6 [Feature] Company Addresses (#4732)
* Add initial model structure

* Initial Address model defined

* Add migration and unit tests

* Initial migration for Address model generated

* Unit tests for Address model added

* Move address field to new model

* Added migration to move address field to Address model

* Implement address feature to backend

* API endpoints for list and detail implemented

* Serializer class for Address implemented

* Final migration to delete old address field from company added

* Tests for API and migrations added

* Amend migration file names

* Fix migration names in test

* Add address property to company model

* Iinital view and JS code

* Fix indents

* Fix different things

* Pre-emptive change before merge

* Post-merge fixes

* dotdotdot...

* ...

* iDots

* .

* .

* .

* Add form functionality and model checks

* Forms require a confirmation slider to be checked to submit
  if address is selected as primary

* Backend resets primary address before saving if new address
  is designated as primary

* Fix pre-save logic to enforce primary uniqueness

* Fix typos

* Sort out migrations

* Forgot one

* Add admin entry and small fixes

* Fix migration file name and dependency

* Update InvenTree/company/models.py

Co-authored-by: Matthias Mair <code@mjmair.com>

* Update InvenTree/company/models.py

Co-authored-by: Matthias Mair <code@mjmair.com>

* Correct final issues

* .

---------

Co-authored-by: Matthias Mair <code@mjmair.com>
2023-06-17 21:55:25 +10:00
Oliver
61d2f452b2 Homepage Improvements (#5057)
* remove STOCK_RECENT_COUNT parameter

- Now "recent" is set by date
- Tables are paginated by the server

* Display total row count

* remove PART_RECENT_COUNT

- Replace with date filter
- Update Part.api

* Bump API version
2023-06-17 08:21:25 +10:00
Oliver
31ff3599eb Parameter by name (#5055)
* Add method get_parameter

- Return a parameter for a part, on name

* Add unit test for new method

* Adds template tag to retrieve parameter based on name

* Update docs
2023-06-16 12:14:17 +10:00
Oliver
51cece9e07 custom panel fix (#5049)
- Handle case where get_custom_panels returns None
2023-06-15 18:59:50 +10:00
Oliver
62faaf01c5 Stock installed items (#5030)
* Cleanup

* Add "installed_items" count to StockItem serializer

- Add queryset annotation
- Add API filter for list endpoint

* js code

* More js updates

* Load installed items on demand

* Make option configurable
2023-06-15 16:42:52 +10:00
Matthias Mair
013d206b91 Add metadata to plugin configs (#5019)
* add metadata to plugin

* Api version bump

* exclude metadata from admin to fix test
2023-06-15 16:17:05 +10:00
Matthias Mair
2322a98068 replace assertation with more targeted logging check (#5045) 2023-06-15 07:34:00 +10:00
Oliver
be6ab14c9b Improve StockItem API speed (#5042)
- Removes child detail fields which cannot be effectively annotated
- Prefetch required fields
- Add unit test method for checking query count
2023-06-14 18:33:49 +10:00
Oliver
8d16abcefb Build line labels (#5034)
* Adds BuildLineLabel model

- New type of label for printing against BuildLine objects

* Add serializer for new model

* Add API endpoints for new label type

* Add hooks to BuildLine table

* Create default label

- Create an example BuildLineLabel object

* Add admin integration

* Fix js code

* Use two-tiered template

- Allows base template to be updated

* Improve default label

* Add docs pages for labels

* Update nav

* Documentation for new label

* Add permission role

* Bump API version
2023-06-14 13:07:18 +10:00
Oliver
a3940cfc41 Improve error logging (#5039)
- Truncate path to < 200 chars
- Prevents exception when creating new Error object
2023-06-14 13:07:05 +10:00
Oliver
00bb740216 Add ProjectCode support to build orders (#4808)
* Add "project_code" field to Build model

* Add "project_code" field to Build model

* build javascript updates

(cherry picked from commit 3e27a3b739)

* Update table filters

(cherry picked from commit 196c675585)

* Adds API filtering

* Bump API version

* Hide project code field from build form if project codes not enabled

(cherry picked from commit 4e210e3dfa)

* refactoring to attempt to fix circular imports

* Upgrade django-test-migrations package

* Fix broken import

* Further fixes for unit tests

* Update unit tests for migration files

* Fix typo in build.js

* Migration test updates

- Need to specify MPTT stuff

* Fix build.js

* Fix migration order

* Update API version
2023-06-14 11:23:35 +10:00
Oliver
c8365ccd0c Update app docs (#5032) 2023-06-13 21:04:17 +10:00
Oliver
6ba777d363 Build Order Updates (#4855)
* Add new BuildLine model

- Represents an instance of a BOM item against a BuildOrder

* Create BuildLine instances automatically

When a new Build is created, automatically generate new BuildLine items

* Improve logic for handling exchange rate backends

* logic fixes

* Adds API endpoints

Add list and detail API endpoints for new BuildLine model

* update users/models.py

- Add new model to roles definition

* bulk-create on auto_allocate

Save database hits by performing a bulk-create

* Add skeleton data migration

* Create BuildLines for existing orders

* Working on building out BuildLine table

* Adds link for "BuildLine" to "BuildItem"

- A "BuildItem" will now be tracked against a BuildLine
- Not tracked directly against a build
- Not tracked directly against a BomItem
- Add schema migration
- Add data migration to update links

* Adjust migration 0045

- bom_item and build fields are about to be removed
- Set them to "nullable" so the data doesn't get removed

* Remove old fields from BuildItem model

- build fk
- bom_item fk
- A lot of other required changes too

* Update BuildLine.bom_item field

- Delete the BuildLine if the BomItem is removed
- This is closer to current behaviour

* Cleanup for Build model

- tracked_bom_items -> tracked_line_items
- untracked_bom_items -> tracked_bom_items
- remove build.can_complete
- move bom_item specific methods to the BuildLine model
- Cleanup / consolidation

* front-end work

- Update javascript
- Cleanup HTML templates

* Add serializer annotation and filtering

- Annotate 'allocated' quantity
- Filter by allocated / trackable / optional / consumable

* Make table sortable

* Add buttons

* Add callback for building new stock

* Fix Part annotation

* Adds callback to order parts

* Allocation works again

* template cleanup

* Fix allocate / unallocate actions

- Also turns out "unallocate" is not a word..

* auto-allocate works again

* Fix call to build.is_over_allocated

* Refactoring updates

* Bump API version

* Cleaner implementation of allocation sub-table

* Fix rendering in build output table

* Improvements to StockItem list API

- Refactor very old code
- Add option to include test results to queryset

* Add TODO for later me

* Fix for serializers.py

* Working on cleaner implementation of build output table

* Add function to determine if a single output is fully allocated

* Updates to build.js

- Button callbacks
- Table rendering

* Revert previous changes to build.serializers.py

* Fix for forms.js

* Rearrange code in build.js

* Rebuild "allocated lines" for output table

* Fix allocation calculation

* Show or hide column for tracked parts

* Improve debug messages

* Refactor "loadBuildLineTable"

- Allow it to also be used as output sub-table

* Refactor "completed tests" column

* Remove old javascript

- Cleans up a *lot* of crusty old code

* Annotate the available stock quantity to BuildLine serializer

- Similar pattern to BomItem serializer
- Needs refactoring in the future

* Update available column

* Fix build allocation table

- Bug fix
- Make pretty

* linting fixes

* Allow sorting by available stock

* Tweak for "required tests" column

* Bug fix for completing a build output

* Fix for consumable stock

* Fix for trim_allocated_stock

* Fix for creating new build

* Migration fix

- Ensure initial django_q migrations are applied
- Why on earth is this failing now?

* Catch exception

* Update for exception handling

* Update migrations

- Ensure inventreesetting is added

* Catch all exceptions when getting default currency code

* Bug fix for currency exchange rates update

* Working on unit tests

* Unit test fixes

* More work on unit tests

* Use bulk_create in unit test

* Update required quantity when a BuildOrder is saved

* Tweak overage display in BOM table

* Fix icon in BOM table

* Fix spelling error

* More unit test fixes

* Build reports

- Add line_items
- Update docs
- Cleanup

* Reimplement is_partially_allocated method

* Update docs about overage

* Unit testing for data migration

* Add "required_for_build_orders" annotation

- Makes API query *much* faster now
- remove old "required_parts_to_complete_build" method
- Cleanup part API filter code

* Adjust order of fixture loading

* Fix unit test

* Prevent "schedule_pricing_update" in unit tests

- Should cut down on DB hits significantly

* Unit test updates

* Improvements for unit test

- Don't hard-code pk values
- postgresql no likey

* Better unit test
2023-06-13 20:18:32 +10:00
Oliver
98bddd32d0 Skip pricing updates when importing data (#5026)
* Skip pricing updates when importing data

- Depending on migration state, pricing table might not exist
- post-save hooks can call update_pricing
- So, ignore if running data migration or import

* Typo fix
2023-06-13 12:40:21 +10:00
Oliver
9117c2234b Migration fixes (#5025)
* Catch exception on is_worker_running

- Exception may occur if table is not yet available
- If it *does* then we can assume the worker is no running

* General error catch in offload_task

* Pick an earlier migration to run from?

* Update initial common migration

- Handle error on table duplication

* Change target migration file

- Ensure that part MPTT migrations have been applied!

* Fix migration ref

- Need 0025
- Price field needs to be available
2023-06-13 07:34:56 +10:00
Oliver
89ad8312ce Fix migration order (#5027)
- Ensure stock.0094 runs before company.0059
- Ref https://github.com/inventree/InvenTree/pull/4984
- Ideally addresses historical migration issues
2023-06-13 07:34:41 +10:00
Oliver
8ca02cb105 Catch exception (#5008)
* Catch exception

* Update settings.py

Don't print out exception message, just log error

* Update settings.py

Style fixes

* Update settings.py

Remove error message

* Update settings.py

Remove logger
2023-06-12 21:03:08 +10:00
Matthias Mair
2f98ed7022 remove signatur checking (we do not use included git anymore) (#5018) 2023-06-12 20:10:13 +10:00
Matthias Mair
9f56ee1023 [FR] Add Feature flags (#4982)
* make currency choices independend

* Remove check for field, just try to get rid of it

* Add IF EXISTS to avoid error (works in postgres)

* Look for operational error, not programming error

* Use variants, depending on errors caused

* [FR] Add Feature flags
Fixes #4965

* Add option to define custom flags

* Revert "make currency choices independend"

This reverts commit ab84a7ff83.

* try fixing mysql

* more safeguards

* fix executioner call

* a fck

* use migrations. syntax

* and another round for mysql

* revert print change

* use UTC for datetime

* Update part.migrations.0112

- Add custom migration class which handles errors

* Add unit test for migration

- Ensure that the new fields are added to the model

* Update reference to PR

* fix ruleset for missing_models

* fix ruleset for flags_flagstate

* add API endpoints for flags

* add tests for new API endpoints

* fix tests

* fix merge

* fix tests

---------

Co-authored-by: martin <martin@iggland.com>
Co-authored-by: Oliver Walters <oliver.henry.walters@gmail.com>
2023-06-12 13:13:53 +10:00
Matthias Mair
15ab911da6 Add more version information (#5014)
* Add info for installer, branch, publisher

* include version info

* fix permissions

* fix typo

* add more info

* use GH url

* fix node

* remove publisher

* remove more logging

* Add info to version view

* enable branch discovery when not set

* fix ref to github version

* add branch to about dialog

* use precise plattform information - if no env is set

* fix ref names

* load VERSION info

* rename plattform to target

* extend installer var

* add generic plattform info

* add docs for version info

* add more info

* add installer codes

* Ammend navigation
2023-06-12 00:32:03 +10:00
Matthias Mair
f3a13fc625 Add SBOM to container (#5017)
* Add sbom to docker

* Enable manual release

* remove dispatch
2023-06-11 23:37:03 +10:00
Oliver
aebff26ad3 New Crowdin updates (#4976)
* updated translation base

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-06-10 19:56:44 +10:00
Matthias Mair
d710efb64b remove concurrency checks (#5002)
Fixes https://github.com/inventree/InvenTree/issues/4999
2023-06-09 10:30:03 +10:00
Matthias Mair
5d1d8ec889 Refactor states/status (#4857)
* add file for states

* move general definition out

* add some tests and docs

* add tests for invalid definitions

* make status_label tag generic

* move templatetags

* remove unused tag

* rename test file

* make status label a lookup

* rename tags

* move import structure

* add missing tag

* collect states dynamically

* fix context function

* move api function out

* add tests for tags

* rename tests

* refactor imports

* Add test for API function

* improve errors and add tests for imporved errors

* make test calls simpler

* refactor definitions to use enums

* switch to enum

* refactor definitions to use enums

* fix lookup

* fix tag name

* make _TAG lookup a function

* cleanup BaseEnum

* make _TAG definition simpler

* restructure status codes to enum

* reduce LoC

* type status codes as int

* add specific function for template context

* Add definition for lookups

* fix filter lookup

* TEST: "fix" action lookup

* Add missing migrations

* Make all group code references explict

* change default on models to value

* switch to IntEnum

* move groups into a seperate class

* only request _TAG if it exsists

* use value and list

* use dedicated groups

* fix stock assigment

* fix order code

* more fixes

* fix borked change

* fix render lookup

* add group

* fix import

* fix syntax

* clenup

* fix migrations

* fix typo

* fix wrong value usage

* fix test

* remove group section

* remove group section

* add more test cases

* Add more docstring

* move choices out of migrations

* change import ordeR?

* last try before I revert

* Update part.migrations.0112

- Add custom migration class which handles errors

* Add unit test for migration

- Ensure that the new fields are added to the model

* Update reference to PR

---------

Co-authored-by: Oliver Walters <oliver.henry.walters@gmail.com>
2023-06-09 10:27:26 +10:00
Matthias Mair
005c8341bf Improve devdocs (#4813)
* move devcontainer docs

* rename bare metal pages

* fix backlinks

* Add getting started for devs

* add mermaid

* include contrib in docs

* use another plugin

* include everything

* update doc checks

* fix install command

* remove mermaid

* remove inclusion tag

* remove empty list item

* readd include-markdown

* use non-conflicting syntax for include

* fix table rendering in mkdocs

* make controbuting embedable

* re-add mermaid

* remove empty section

* resturcture

* remove mermaid again
2023-06-09 10:16:30 +10:00
Oliver
280f6241dd Migration fix (#5006)
- Ensure initial django_q migrations are applied
- Why on earth is this failing now?
2023-06-09 10:09:02 +10:00
Oliver
f1031efa93 CSS fix for 2FA token (#5000)
- Enforce white background for the QR code
2023-06-09 01:09:12 +10:00
Matthias Mair
e807339c55 respect timezone when handeling git commit times (#4997) 2023-06-08 22:00:54 +10:00
Oliver
bae1c239e8 Fix for forms.js (#4996)
- Fixes subtle bug introduced in recent javascript linting fix
- Prevented error messages from displaying on modal forms
- The whole thing needs to be razed and rebuilt
2023-06-08 21:30:10 +10:00
Oliver
842d7a93d5 Fix for faulty data migrations (#4987)
* Update part.migrations.0112

- Add custom migration class which handles errors

* Add unit test for migration

- Ensure that the new fields are added to the model

* Update reference to PR
2023-06-08 21:12:57 +10:00
Matt Brown
a4b4df5ff4 Place uninstalled items back in stock (#4994)
reset the consumed_by field as well as belongs_to, so an uninstalled
item can be reused.

Fixes: #4992
2023-06-08 20:22:50 +10:00
Oliver
81413e02c4 Update CONTRIBUTING.md (#4988)
Add notes about "migration" tag
2023-06-07 22:15:59 +10:00
Matthias Mair
d7d3d8aa26 Currency migrations - stop migrations when defaults change (#4975)
* make currency choices independend

* replace hard coded default currency

* use function for psql?

* use callable default

* revert some fields

* also migrat all currency codes
2023-06-07 20:05:37 +10:00
Oliver
192c1ecb21 Add note on SSO behind proxy (#4985) 2023-06-07 11:55:02 +10:00
Oliver
a3150d9cb3 Refactor label API code (#4978)
* Refactor label API code

- Add common base class for serializers

* More import cleanup
2023-06-06 18:49:19 +10:00
Oliver
f65281c801 Migration bug fix 2 (#4977)
* Additional migration fix:

- In #4961 we did not notice that the migration files had been renamed
- There is still a chance that a production db (running from master) has a corrupted set of migrations
- Check if the duplicate columns already exist
- If they do, delete them first

* Typo fix

* Add PR reference
2023-06-06 15:45:24 +10:00
Oliver
ba24ff570a SSO bug fix (#4972)
* Catch SSO error

- If social application is not assigned to at least one site, errors happen
- Check if at least one site is enabled

* Docs updates

* Typo fix
2023-06-05 21:03:16 +10:00
Oliver
3ba1d10fc4 New Crowdin updates (#4950)
* updated translation base

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-06-05 20:04:16 +10:00
Oliver
a4be6bc90b New tags (#4971)
- feature
- experimental
2023-06-05 19:41:42 +10:00
Matthias Mair
58a33c2e67 [FR] Switch from git to dulwich (#4966)
* [FR] Switch from pure git to dulwich Fixes #4942

* fix lenght

* change length again

* change length again
2023-06-05 19:27:46 +10:00
dependabot[bot]
2ed7eefa27 Bump cryptography from 40.0.1 to 41.0.0 (#4955)
* Bump cryptography from 40.0.1 to 41.0.0

Bumps [cryptography](https://github.com/pyca/cryptography) from 40.0.1 to 41.0.0.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/40.0.1...41.0.0)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

* bump everything

* do not build for linux/arm/v7
see https://github.com/inventree/InvenTree/pull/4955#issuecomment-1575909025

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Matthias Mair <code@mjmair.com>
2023-06-05 19:27:27 +10:00
Oliver
d8965c6c2b Prevent div-by-zero error (#4967)
- Div-by-zero could occur when calculating how many items can be built for a part
- Might result if (somehow) the BomItem has a quantity of zero
2023-06-05 13:40:50 +10:00
Oliver
45ec7b9728 Enable and disable plugins via the API (#4964)
* Cleanup plugin settings page

- Template adjustments

* Activate plugin directly via API

* Update plugin activate endpoint

- Allow plugin to be deactivated also
- Default value = True if not provided

* Update front-end / js

- Allow same JS method to either enable or disable a plugin

* Hide info for plugins which are not active

* remove duplicated column

* Tweak serializer docstring

* Fix typo

* Add extra data to plugin serializer

- is_builtin
- is_sample

* Some backend cleanup

- Don't stringify null values
- Don't replace None with "Unavailable"

* front-end table for rendering plugins

* Change default sorting

- Show active plugins first

* Fix button callback

* Remove old template

* JS linting

* More linting
2023-06-05 12:19:56 +10:00
Matthias Mair
0c47552199 Add openssf badge (#4558) 2023-06-04 00:47:06 +10:00
Oliver
2ca9e0e574 Fix for improper migrations (#4961)
- Order of migrations had been changed
- Resulted in conflicting database state
- Never ever do this!
- Ref: https://github.com/inventree/InvenTree/pull/4898
2023-06-04 00:26:18 +10:00
Matthias Mair
21ed4b2081 Fix common spelling mistakes (#4956)
* add codespell

* first fixes

* doc fixes

* fix docstrings and comments

* functional changes

* docstrings again

* and docs again

* rename args

* add ignore

* use pre-commit for filtering instead

* ups

* fix typo in filter
2023-06-04 00:04:52 +10:00
Oliver
5e2bfaa43a Update CONTRIBUTING.md (#4960)
Add documentation on new PR tags
2023-06-03 23:16:39 +10:00
Oliver
b0338e181e Parameter validation via plugin (#4958)
* Expose part parameter validation to plugins

- Allow ValidationMixin plugins to validate part parameter values

* Update sample plugin

* Catch and re-throw error

* Update docs

* Improve plugin docs

* Simplify validation sample

* Calculate numeric value first
2023-06-03 21:27:31 +10:00
miggland
1d85b70313 Add Metadata to more models (#4898)
* Update models: add MetadataMixin

* Fix name of model in Metadata API definition

* Add API endpoints

* Update API version

* Fix syntax

* Add API endpoint for RO, RO line, RO line extra item

* Add Metadata to Contacts

* Fix link in API version

* Fix name of model

* Fix error?

* Fix error?

* Fix all errors, hopefully..

* Add tests for order, line, extraline metadata

Extend for PO, SO

* Add tests for metadata for Company-related models

* Fix spelling

* Consolidate metadata test for all part models into one test

* Add test for all Stock metadata

* Update stock test_api

* Add all metadata tests for orders

* Fix various errors in tests

* Fix model name

* Add migration files

* Update tests for metadata

* Resolve conflict around API version number

* Rename migration file

* Rename migration file

* Will Contact edit endpoint work better?

* Revert changes in URL definitions

* Remove test, duplicate

* Fix tests with fixed PK, not from fixtures, to use a dynamic PK

* Fix migration overlap
2023-06-02 19:26:20 +10:00
Oliver
c0dafe155f Fix for 'available' filter (#4952)
- Available filter also requires "in stock"
2023-06-02 16:37:16 +10:00
Lukas
1df97a7607 Add "can reproduce" checkbox to bug template (#4953)
* Added can-reproduce selection

* Update bug_report.yaml

* Update bug_report.yaml

* Update bug_report.yaml

* Update bug_report.yaml

* Update bug_report.yaml

* Update bug_report.yaml

* Update bug_report.yaml
2023-06-02 16:35:17 +10:00
Oliver
812b256e08 Shorten displayed linked by default (#4951) 2023-06-02 14:27:15 +10:00
Oliver
2c3ba6e528 Update action.md (#4949)
Fix path for action plugin docs
2023-06-02 09:35:56 +10:00
miggland
936c8ad7fc Metadata bugfixes (#4947)
* Fix name of model in Metadata API definition

* Add API endpoint for RO, RO line, RO line extra item

* Update InvenTree/order/api.py

Thanks @matmair - missed that I changed this later on

Co-authored-by: Matthias Mair <code@mjmair.com>

---------

Co-authored-by: Matthias Mair <code@mjmair.com>
2023-06-02 07:54:55 +10:00
Oliver
60f2f1ea86 Report files (#4946)
* Adds more report tags

- uploaded_file: Load qualified path for a media file
- encode_svg_image: Encode an svg image as base64 data

* Docs updates
2023-06-02 07:54:15 +10:00
miggland
11c5ce5f80 Test flexibility (#4945)
* Add option to specify which tests to run in invoke test

* Add information on testing in CONTRIBUTING.md
2023-06-01 23:54:06 +10:00
Lukas
4d9e92011e Fix/settings bugs and added model SettingKeyType typing (#4944)
* fix .gitignore and spelling issues

* Fix setting get not cached correctly

* Add model to settings key type

* Fix plugin setting slug url

* Fix typo

* Fix resetting of related setting field

* Improved model comment
2023-06-01 23:53:06 +10:00
Oliver
037654610e Better rendering for parameter table (#4943)
- Use trueFalseLabel
2023-06-01 20:30:53 +10:00
Miklós Márton
46a808c064 Add user settings to remember the last/favourite label template (#4938)
* Add user settings to remember the last/favourite label template

Fixes #4932

* Remove settings_value from translated templates

Thanks Oliver for the hint!
2023-06-01 20:01:48 +10:00
Oliver
18d9ecd0f4 Tweak svg data cleaning: (#4941)
- Decode data if passed as bytes
2023-06-01 16:47:47 +10:00
Oliver
cb0f0e34d9 Improvements for parameteric part table (#4940)
- Initially only display columns which have data
2023-06-01 16:09:13 +10:00
Oliver
2c58b2fd36 New Crowdin updates (#4939)
* updated translation base

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-06-01 15:39:39 +10:00
Oliver
e21a5e62b8 Parameter types (#4935)
* Add fields to PartParameterTemplateModel

- checkbox: Is the field a 'checkbox'
- choices: List of valid options

* Update javascript

* Adds unit test for PartParameterTemplate

- Checkbox cannot have units
- Checkbox cannot have choices
- Choices must be unique

* Improve API filtering

- Add "has_choices" filter
- Add "has_units" filter

* Prune dead code

* Update js functions for creating / editing parameters

* Update part parameter form

- Rebuild the "data" field based on the selected template
- Supports "string" / "boolean" / "select"

* Adjust data input based on parameter type

- Choice displays available options
- Checkbox displays boolean switch
- Otherwise displays text input
- Adds more unit testing
- Updates to forms.js for improved functionality

* Calculate numeric value for boolean parameters

* Update docs

* Bump API version
2023-06-01 07:20:11 +10:00
Oliver
2c05e3e74d Improve docs for report helpers (#4933) 2023-05-31 22:05:13 +10:00
Oliver
685cc1fd77 BOM table: remove quantity footer (#4930)
- Summing quantities of different parts does not make sense
- Especially when different parts have units
- i.e. "3 m + 22 litres = ??"
2023-05-31 17:25:51 +10:00
Lukas
99d122baa9 Refactor model helpers into own file (#4927)
* Refactor model helpers into own file to allow helper import when apps not loaded yet

* Import helper functions at module level

* Added missing imports where vscode couldnt help because its no explicit import
2023-05-31 09:18:42 +10:00
Matthias Mair
a196f443a1 Bump deps (#4911)
* bump deps

* Update requirements.txt

* fix pint

* fix reqs

* fix dev reqs
2023-05-31 09:18:02 +10:00
Oliver
92930d475c Fix JS linting workflow (#4915)
* Add bad .js code

- Call function which has not been defined
- Should throw JS lint error

* update eslint

- Move to newer version of eslint
- Change default rules

* Fixes for tables.js

* Fixes for table_filters.js

* Fixes for stock.js

* Fix for sales_order.js and search.js

* Fix return_order.js

* Fixes for purchase_order.js

* More updates

- part.js
- plugin.js
- pricing.js

* Updates

- order.js

* Even morerer updates

- label.js
- modals.js
- model_renderers.js
- news.js
- notification.js

* More, MORE!

- build.js
- company.js
- charts.js
- filters.js
- forms.js
- helpers.js

* Final?

- api.js
- attachment.js
- barcode.js
- bom.js

* Fix 'useless-escape'

* Disable no-useless-escape rule
2023-05-30 22:55:53 +10:00
Oliver
0808382d06 Fix ReturnOrder serializer (#4923)
- Add in missing fields
2023-05-30 21:00:03 +10:00
Oliver
eca2172624 Add Finnish language support (#4921) 2023-05-29 22:05:38 +10:00
Oliver
3205527ebe Change target of PurchaseOrder notification (#4905)
* Change target of PurchaseOrder notification

- Triggered when order is placed
- No longer when order is created

* unit test fixes
2023-05-29 21:04:31 +10:00
Ulices
32331875fe Fix Unink to Unlink (#4919) 2023-05-29 15:56:58 +10:00
Oliver
95755c5453 js fixes (#4918)
- Do not call formatDecimal on a string
2023-05-29 11:36:12 +10:00
Oliver
b842d5ea67 New Crowdin updates (#4910)
* updated translation base

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-05-28 22:13:15 +10:00
Oliver
637b195a68 Add name to backport workflow (#4914) 2023-05-28 20:26:39 +10:00
Oliver
60f79a0a23 Add workflow for backporting PRs (#4912)
* Add workflow for backporting PRs

- Time saving for backporting bug fixes to stable branches
- Apply to PRs before closing

* Cleanup .json file
2023-05-28 18:55:06 +10:00
Miklós Márton
0b4a06ae7e Fix completeSalesOrderShipment typo in sales_order.js (#4908) 2023-05-27 09:48:14 +10:00
Miklós Márton
21dafdee8e - Bump djLint to 1.29.0 (from 1.25.0) in the CI linting scripts (#4907)
- Add alt attribute to the img tags in the templates (required by the bump)
2023-05-27 08:52:10 +10:00
Oliver
11f816a787 Fix for migration file (#4901)
- Catch *all* exceptions when migrating

(cherry picked from commit bcd068fc23637d815d7899d549f83b8bd8a26057)
2023-05-26 19:44:32 +10:00
Oliver
01e2376748 New Crowdin updates (#4875)
* updated translation base

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-05-26 17:01:21 +10:00
miggland
4d76708bee Add API Calendar for Return Orders (#4899)
* Add RO to calendar output

* Update title and description to be more descriptive

* Add documentation

* Add simple test for RO calendar

* Lint/syntax

* Lint

* Changes to title, description
2023-05-26 17:01:04 +10:00
Oliver
5dd6f18495 Part units (#4854)
* Add validation to part units field

* Add "pack_units" field to the SupplierPart model

* Migrate old units to new units, and remove old field

* Table fix

* Fixture fix

* Update migration

* Improve "hook" for loading custom unit database

* Display part units column in part table

- Also allow ordering by part units
- Allow filtering to show parts which have defined units

* Adds data migration for converting units to valid values

* Add "pack_units_native" field to company.SupplierPart model

* Clean pack units when saving a SupplierPart

- Convert to native part units
- Handle empty units value
- Add unit tests

* Add background function to rebuild supplier parts when a part is saved

- Required to ensure that the "pack_size_native" is up to date

* Template updates

* Sort by native units first

* Bump API version

* Rename "pack_units" to "pack_quantity"

* Update migration file

- Allow reverse migration

* Fix for currency migration

- Handle case where no currencies are provided
- Handle case where base currency is not in provided options

* Adds unit test for data migration

* Add unit test for part.units data migration

- Check that units fields are updated correctly

* Add some extra "default units"

- each / piece
- dozen / hundred / thousand
- Add unit testing also

* Update references to "pack_size"

- Replace with "pack_quantity" or "pack_quantity_native" as appropriate

* Improvements based on unit testing

* catch error

* Docs updates

* Fixes for pricing tests

* Update unit tests for part migrations · 1b6b6d9d

* Bug fix for conversion code

* javascript updates

* JS formatting fix
2023-05-26 16:57:23 +10:00
Matthias Mair
717bb07dcf Possible fix for git messages (#4882)
* yank git stuff out

* fix tests

* Add check for git to reduce warning logs
Fixes #4428

* reverse git removal

* and more resetting
2023-05-26 08:17:32 +10:00
Oliver
433ea4d0de Documentation Improvements (#4894)
* Improvements for version banner

- Display at the bottom of the page
- Simplify text

* Use mkdocs version provider

* use 'navigation.instant'

* Support modern mkdocs features

- Much better navigation and improved features

* Content changes

* Add code to find previous versions

* Remove modern python syntax

* display output codes

* Hide version banner

* Extra debug message

* update terminology.md
2023-05-25 22:42:16 +10:00
Oliver
8268b9b105 Fix for data migration (#4892)
* Fix for data migration

- Catch *all* exceptions
- We don't want an unhandled exception to break data migration

* Add more exception handling

* Fix typo
2023-05-25 10:59:55 +10:00
Matthias Mair
fdd4169cd7 Improve cli experience (#4881)
* fix cli call to get a fully running invoke

* use relativ import
This is importend when imported from outside

* Add version command

* Add more information to version command

* make print easier to understand
2023-05-24 16:34:36 +10:00
Oliver
4079224658 Fix for PurchaseOrder template (#4885)
- Fixes bug which removes javascript incorrectly
2023-05-24 16:18:54 +10:00
Oliver
4d00c471e1 Change debug message for plugin events (#4884)
- Change from .info to .debug for less verbosity
2023-05-24 15:32:49 +10:00
Oliver
09e99e5f75 Remove debug message (#4883)
- Removes an old debug message which was left in by mistake
2023-05-24 13:48:38 +10:00
Matthias Mair
aa2f5e330a Make nginx more clear that changing the port is 'dangerous' (#4880)
* Make nginx more clear that changing the port is 'dangerous'
See https://github.com/inventree/InvenTree/issues/4859#issuecomment-1560025566

* also add note to prod docker-compose
2023-05-24 09:05:16 +10:00
Oliver
91d79dc3ed Improved error handling for email support (#4862)
* Improved error handling for email support

- Prevent email sending if email not configured
- Check for tx email address before sending

(cherry picked from commit de541f811ede030ea5eb3136132731e1dafccc31)

* Update InvenTree/email.py

Co-authored-by: Matthias Mair <code@mjmair.com>

* Update InvenTree/email.py

Co-authored-by: Matthias Mair <code@mjmair.com>

* Fix location of file email.py

* Allow dummy emails in testing

* Provide default email in testing mode

* Fix to get test working

---------

Co-authored-by: Matthias Mair <code@mjmair.com>
2023-05-24 07:33:31 +10:00
Oliver
96b7845d84 Fix for currency migration (#4873)
- Handle case where no currencies are provided
- Handle case where base currency is not in provided options

(cherry picked from commit b1bf086ae9)
2023-05-22 22:59:10 +10:00
Oliver
f76059b2b4 Reorganize FAQ section (#4872) 2023-05-22 20:02:42 +10:00
Oliver
59cbf17b02 Faq docker (#4871)
* Remove stat context variables

* Revert "Remove stat context variables"

This reverts commit 0989c308d0.

* Allow longer timeout for image download tests

* updated translation base

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* docs: Update FAQ section

- Add note regarding permissions for docker directories

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-05-22 17:17:59 +10:00
Oliver
5992dcdfda Handle ValidationError when creating a new setting (#4868)
- Duplicate keys can occur in some race conditions
- Catch and pass ValidationError
2023-05-22 12:12:02 +10:00
Oliver
63da2ae9f7 New Crowdin updates (#4852)
* updated translation base

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-05-22 00:36:23 +10:00
Oliver
8dc45e49cd Fix category filtering for parameteric part table (#4861) 2023-05-21 23:17:28 +10:00
Oliver
9e77b9fc56 Parameter filtering (#4823)
* adds new field 'parameter type' to PartParameterTemplate model

* Move part parameter settings onto their own page

* Add "choices" and "regex" template types

* Adds validation for PartParameter based on template type

* javascript cleanup

* Fix for serializers.py

* Add unit testing for parameter validation

* Add filters

* Rename "type" field to "param_type"

- Should have seen that one coming

* Coerce 'boolean' value to True/False

* table update

* js linting

* Add requirement for "pint" package

* Add validator for physical unit types

- Revert a previous migration which adds "parameter type" and "validator" fields
- These will get implemented later, too much scope creep for this PR
- Add unit test for validation of "units" field

* Update PartParameter model

- Add data_numeric field (will be used later)
- Add MinLengthValidator to data field

* Run validation for part parameter data

- Ensure it can be converted to internal units

* Update admin interface to display partparameter values inline for a part

* Adds validation of part parameter data value

- Also converts to base units, and stores as "numeric" value
- Display "numeric" value in tables
- Create new file conversion.py for data conversion

* Update unit tests and fix some bugs

* Update docstring

* Add units to parameter columns in parameteric part table

* Allow part list to be ordered by a particular parameter value

- Annotate queryset with new "order_by_parameter" method
- Skeleton method for future work

* Bump API version

* Adds unit testing for sorting parts by parameter value

* Update historical data migrations

- Turns out RunPython.noop is a thing?

* Cache the unit registry

- Creating the unit registry takes a significant amount of time
- Construct when first called, and then cache for subsequent hits
- Massive improvement in performance

* Throw error on empty values when converting between units

* Data migration for converting existing part parameter values

* Handle more error cases

* Show parameteric table on top-level part page too

* Unit test for data migration

* Update credits in docs

* Improved error checking

* WIP docs updates

* Fix parameteric table filtering

* remove zoom property

* Fix for import path

* Update parameter docs

* Run background task to rebuild parameters when template changes

* Make "data_numeric" field nullable

- Defaulting to zero is not appropriate, as the actual value may be zero
- Sorting still seems to work just fine

* Fixes for unit test

* More unit test fixes

* Further fixes for unit tests

---------

Co-authored-by: Matthias Mair <code@mjmair.com>
2023-05-19 13:24:55 +10:00
Lukas
cb8ae10280 Refactor: BaseInvenTreeSetting (#4834)
* Added typing for settings

* Refactored common.BaseInvenTreeSetting model to make it more generic

* Use older syntax for union types

* Added protected option to typing

* Remove now unused code

* Remove old 'get_kwargs' method as it is replaced by 'get_filters_for_instance'

* Trigger ci
2023-05-19 09:51:30 +10:00
Oliver
61481b4eb0 New Crowdin updates (#4842)
* updated translation base

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-05-18 20:51:44 +10:00
Oliver
327381357b Adds ability to partially scrap build outputs (#4846)
* BuildOrder updates:

- Use batch code generation when creating a new build output
- Allow partial scrapping of build outputs

* Fixes for stock table

* Bump API version

* Update unit tests
2023-05-18 14:04:57 +10:00
IvoLealMCS
120a710ad4 Add InvenTreePluginViewMixin to PartIndex (#4843) 2023-05-18 01:04:37 +10:00
Oliver
d5caa98936 Report helper docs (#4845)
* Add docs for more helper functions

* add docs about dot notation lookup
2023-05-18 01:04:19 +10:00
Oliver
b732b4ceb5 Add docs for more helper functions (#4844) 2023-05-18 00:07:19 +10:00
Oliver
4785f465e8 Cleanup / consolidate unit testing code (#4831)
- Move testing code out of helpers.py
- Create new file unit_test.py
2023-05-17 07:35:26 +10:00
Oliver
f85b378115 New Crowdin updates (#4810)
* updated translation base

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-05-17 00:38:05 +10:00
Oliver
5c7303fd53 Update mkdocs.yml (#4828)
Fix spelling error in docs
2023-05-16 22:56:07 +10:00
miggland
98d87c84e3 Shipment delivery date (#4825)
* Add delivery date setting to Shipment model

* Add delivery_date to serializer

* Correct test for is_delivered

* Add relevant fields to API and forms

* Add test

* Increment API version

* Move migration file because of conflict
2023-05-16 21:28:09 +10:00
Oliver
397419f365 Build consume stock (#4817)
* Adds "consumed_by" field to the StockItem model.

- Points to a BuildOrder instance which "consumed" this stock
- Marks item as unavailable
- Allows filtering against build order

* Allow API filtering

* Adds table of "consumed stock items" to build order page

* Update stock table to show "consumed by" stock status

* Add "consumed_by" link to stock item detail

* Optionally add 'buildorder' details to installStockItem method

* Update methodology for completing a build item

- Instead of deleting stock, mark as "consumed by"

* Fix history entry for splitting stock

* Bug fix

* track "consumed_by" field for tracked items also

* Update build docs

* Update allocation documentation

* Update terminology.md

* Unit test updates

* Fix conflicting migrations

* revert change
2023-05-16 21:25:02 +10:00
Oliver
368f615d71 Bug fix for improperly configured SSO provider (#4822)
* Add sso template tags

* Check if SSO provider is valid on login page

* Add warning if SSO method is not correctly configured

* Template tweaks
2023-05-15 15:09:14 +10:00
Oliver
065f3e2404 Small visual tweaks for notifications tab (#4819) 2023-05-15 12:15:01 +10:00
Oliver
3e0b57f10a Add "description" field to extra line items (#4815) 2023-05-15 11:13:38 +10:00
Oliver
caa7b84c3e Allow tag fields to be empty (#4816)
Without this change, editing items (e.g. via the admin interface) fails if the tag field is not empty
2023-05-15 10:57:12 +10:00
Oliver
e2505433a2 Docker build fix (#4811)
* Attempt to fix crypto build on raspbian

* Run CI if requirements files change
2023-05-15 08:27:26 +10:00
Matthias Mair
1b94a271b6 Add faq entry for https://github.com/inventree/InvenTree/issues/4802#issuecomment-1546767130 (#4812) 2023-05-15 07:47:36 +10:00
Oliver
b04053d9b5 New Crowdin updates (#4801)
* updated translation base

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-05-14 19:12:53 +10:00
Oliver
1d384572ec Update CSS for table filter tags (#4803) 2023-05-13 22:44:04 +10:00
Oliver
b2ceac2c4a [Feature] Scrap Build Outputs (#4800)
* Update docs for status codes

* Adds API endpoint for scrapping individual build outputs

* Support 'buildorder' reference in stock tracking history

* Add page for build output documentation

* Build docs

* Add example build order process to docs

* remove debug statement

* JS lint cleanup

* Add migration file for stock status

* Add unit tests for build output scrapping

* Increment API version

* bug fix
2023-05-13 22:19:35 +10:00
miggland
634daa2161 Add flag to API which allows using pack size (#4741)
* Add flag to API which allows using pack size when adding stock items manually

* Check for use_pack_size before pop

* Add test data and tests

* Improve data handling

* Add form field for use_pack_size when adding stock

* Add description of pack size to docs

* Don't check for supplier part if it is None

* Move form field to after supplier part, for better logic

* Fix wrong function

* Fix tests

* Adjust purchase price when using pack size

* Adjust help text for purchase price

* Adjust help text for purchase price some more

* Fix tests for purchase price of added stock

* Update api_version.py
2023-05-13 21:32:25 +10:00
Matthias Mair
017ccaa27a Cleanup plugin mixin registry (#4790)
* collect mixins dynamically

* remove unfinsihed option to reorder mixins

* clean up settings

* fix text

* fix mixin lookup

* stupid error

* fix assertations

* use regustered function instead of private dict

* switch to dict for reg

* fix test

* makke sure mixins also works with class

* cleanup

* fix reqs

* fix test assertations
2023-05-12 22:00:25 +10:00
Lukas
17057f4266 Docs/devcontainer (#4787)
* Added empty problemMatchers to prevent vscode from asking

* Added first draft for devcontainer docs

* Add 3rd space to tips

* Fix wording

* Add 4rd space to tips

* Refphased intro text

* Fixed spelling mistakes and added note

* Added dynamic variables for devcontainer

* Added missing containerWorkspaceFolder vars other devcontainer files

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>

* Added note for inventree core intelicense for plugin devs

* Added where is dev data stored question to faq

* Update docs/docs/start/devcontainer.md

* update toc

---------

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
Co-authored-by: Matthias Mair <code@mjmair.com>
2023-05-12 17:13:48 +10:00
Oliver
41cef1a190 Add reload functionality for build item table (#4799)
* Add reload functionality for build item table

* Update requirements.txt file

(cherry picked from commit c976d06ec75d17e4c2fd4e557eb0bda858ae391e)
2023-05-12 14:29:29 +10:00
dependabot[bot]
306f36bff8 Bump django from 3.2.18 to 3.2.19 (#4795)
Bumps [django](https://github.com/django/django) from 3.2.18 to 3.2.19.
- [Commits](https://github.com/django/django/compare/3.2.18...3.2.19)

---
updated-dependencies:
- dependency-name: django
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-10 08:21:59 +10:00
Oliver
57502a1ad8 New Crowdin updates (#4760)
* updated translation base

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-05-08 19:09:50 +10:00
miggland
84f8e33269 Add list of pending shipment to home page, with corresponding settings for user (#4775) 2023-05-08 00:25:51 +10:00
Jakob Haufe
89dfb6186f Restore shopping cart icon in BO/Allocate Parts (#4780)
The shopping cart icon for parts already on order was originally
implemented in 1b421fb59a but got broken in 27aa16d55d as the return
value of `makeIconBadge` is discarded.

Additionally, the FontAwesome JS renderer doesn't seem to like
non-empty content for this, so even when adding it back to `icons`,
it didn't get rendered properly. Instead, the count has to be added
to the title.
2023-05-07 22:47:10 +10:00
Lukas
404113d739 Fix metadatalookup (#4784) 2023-05-07 10:02:11 +10:00
Ulices
4510cf2dd6 fix strarting -> starting (#4786) 2023-05-07 10:01:04 +10:00
Oliver
3b3ce81d11 Sales order button fix (#4782)
* Add (empty) set of API filters for contact table

* Fix typo
2023-05-06 18:49:21 +10:00
Oliver
5886415aa7 Extend docs for report filename generation (#4781) 2023-05-06 09:49:32 +10:00
Oliver
09083d2de1 Build filters (#4773)
* Add optional callback to table filtering function

* Add custom filters to build item table
2023-05-06 08:39:10 +10:00
Ulices
434a00b55f fix caluclations->calculations (#4774) 2023-05-06 08:38:48 +10:00
Matthias Mair
1fc22359c7 add missing triage:not-checked to documentation issue (#4778) 2023-05-06 07:46:45 +10:00
Lukas
d416e57ee3 Add option to hide plugin setting from auto-generated plugin setting page (#4767)
* Add option to hide plugin setting from auto-generated plugin setting page

* Change hide to hidden

* Added small note to docs about hidden settings

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>

---------

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2023-05-06 01:14:39 +10:00
Oliver
35d04c0357 Fix for "used in" calculation (#4770)
* Simplify query filtering for determining list of parts which require a component to build

* Fix .devcontainer file

(cherry picked from commit d4bd8ea0a9)

* Catch ValueError
2023-05-06 00:44:49 +10:00
Oliver
d7f75d3ab3 Project code docs (#4772)
* Add page for project code docs

* Add details on project codes

* more docs
2023-05-05 23:59:30 +10:00
Oliver
a0f18d82cb Improve documentation on currency rendering (#4768)
* Improve documentation on currency rendering

* Fix .devcontainer file
2023-05-05 23:43:12 +10:00
Oliver
1c3d037baf Bug fix for zero quantity pricing (#4765)
- Fix default value for formatPriceRange method
- Display empty value in table
2023-05-05 23:08:33 +10:00
Lukas
7793b3505d Fix various devcontainer issues (#4761)
* Fix various devcontainer issues

- fix dubious git ownership during postCreateCommand
- fix gpg signing in container
- fix local gitconfig in container
- add gitlens extension to devcontainer
- enable plugins in devcontainer

* fix: spelling mistake
2023-05-05 22:34:42 +10:00
Oliver
9920c3fd9c Metadata fix (#4725)
* Add 'clean' method to MetadataMixin class

- Ensure that the "metadata" is a valid dict object

* Add "overwrite" option for set_metadata method

* Update unit tests

* full_clean -> clean

* Cleanup

* Fix for MetadataMixin

* Updates for unit tests

* Test
2023-05-05 14:06:19 +10:00
Oliver
c45e66935a Stock transfer same location (#4757)
* Allow stock items to be transferred into the same location

* Add new code when moving into same location

* Update unit test

* Further unit test fixes
2023-05-05 12:55:31 +10:00
Oliver
e7317522a6 New Crowdin updates (#4750)
* updated translation base

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-05-05 07:09:24 +10:00
Matthias Mair
f5c2591fd4 Add 'Tag' management (#4367)
* 'Tag' management
Fixes #83

* Add for ManufacturerPart, SupplierPart

* Add tags for StockLocation, StockItem

* fix serializer definition

* add migrations

* update pre-commit

* bump dependencies

* revert updates

* set version for bugbear

* remove bugbear

* readd bugbear remove isort

* and remove bugbear again

* remove bugbear

* make tag fields not required

* add ruleset

* Merge migrations

* fix migrations

* add unittest for detail

* test tag add

* order api

* reduce database access

* add tag modification test

* use overriden serializer to ensuer the manager is always available

* fix typo

* fix serializer

* increae query thershold by 1

* move tag serializer

* fix migrations

* content_types are changing between tests - removing them

* remove unneeded fixture

* Add basic docs

* bump API version

* add api access to the docs

* add python code

* Add tags to search and filters for all models
2023-05-04 09:02:48 +10:00
Oliver
baaa147fd0 Ignore variant stock when performing stocktake (#4752)
* Ignore variant stock when performing stocktake

- Including variant stock can cause duplicate counts of items
- Causes valuation numbers to be incorrect

* Fix unit tests

* Further unit test fixes
2023-05-02 23:57:40 +10:00
Oliver
6da108e031 Bug fix for label printing (#4751)
* Bug fix for label printing

- Do not use "debug mode" when printing using a plugin
- Fixes https://github.com/inventree/InvenTree/issues/4745

* Fix inverted logic
2023-05-02 23:09:41 +10:00
Oliver
3ff217d229 Fix for sales order tables (#4753)
- Allow line items to be allocated after partial shipment
- Fixes https://github.com/inventree/InvenTree/issues/4734
2023-05-02 22:55:53 +10:00
Oliver
be735e4568 Update FUNDING.yml (#4755)
Fix typo
2023-05-02 22:28:32 +10:00
Oliver
52321af962 Add github link to funding.yml (#4754) 2023-05-02 22:25:26 +10:00
Oliver
61d613ff34 Base URL configuration options (#4749)
* Improve construct_absolute_url method

- Look for hard-coded site URL if provided
- Otherwise look for specified site URL
- Otherwise look at the provided request object

* Refactor existing code which used base URL setting

* Update docs

* Validate that a provided base URL is valid
2023-05-02 22:14:57 +10:00
Matthias Mair
10c3d101e8 Replace mklint with djlint (#4746)
* remove markuplint

* remove dedicated html step - will be done by pre-commit

* add djlint for django template linting

* Fix T003: Endblock should have name

* Fix H013: Img tag should have an alt attribute

* Fix H014: Found extra blank lines

* Fix T003: Endblock should have name

* Fix H013: Img tag should have an alt attribute

* small fixes

* Fix T001: Variables should be wrapped in a single whitespace

* Fix T003: Endblock should have name

* small fixes

* fix form method

* add entry to contributing

* fix template changes

* another fix

* use current version
2023-05-02 20:03:52 +10:00
Oliver
09fabff551 Fix filters for part variant table (#4748)
- Slight adjustment to HTML tags
- Buttons were not displaying correctly inline
2023-05-02 14:47:02 +10:00
Oliver
149c4df231 Remove restriction on manual stock item installation (#4747)
- Allow stock item to be manually inside an item which is iteself installed in an item
- No practical reason for this limitation to exist
2023-05-02 14:46:54 +10:00
Oliver
d9864fce69 New Crowdin updates (#4737)
* updated translation base

* Fix: New translations django.po from Crowdin

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-05-02 14:46:46 +10:00
Oliver
20d8c2b4e6 Include error type when an error occurs updating exchange rates (#4728)
- Will allow us to further introspect errors received by sentry
2023-05-02 09:34:03 +10:00
Lavissa
6bd95f3b15 Link changes and in-table clipboards (#4697)
* Add clipboard to tables and external link changes

* Clipboard icon added to tables for screens >1200px wide. Enables
  copying of SKU/MPN/IPN from table cells where these otherwise are
  hyperlinks

* External links now open in new tabs with noreferrer

* Move external links into separate template

* All statically rendered external links have been moved out to a new
  template.
2023-05-02 08:13:50 +10:00
Miklós Márton
0b8feb2c4a Display proper printing target name in the label printing modal dialog (#4727)
Fix #4717
2023-05-02 07:59:54 +10:00
Matthias Mair
e5e1a09b45 CI cleanup (#4744)
* merge doc checks into main QC checks

* rename

* well both APIs are closed now so this would be useless

* make header more concise

* add check if server code changed - make run conditional

* reformat
2023-05-02 07:58:57 +10:00
Matthias Mair
21e0679cb9 [FR] Remove Gitpod integration (#4740)
Fixes #4729
2023-05-02 07:39:38 +10:00
Matthias Mair
5e99e54bbc Update README.md with digitalocean info (#4742)
* Update README.md with digitalocean info

* update link for DO in docs
2023-05-01 19:42:14 +02:00
Oliver
be856c3682 Scan in items fix (#4738)
* Clean up comments

* Bug fix

- Pass options.modal through if provided when creating new barcodeDialog

* Fix for scanning from item context into location
2023-05-01 23:50:20 +10:00
Oliver
ad4acef459 Plugin metadata docs (#4724)
* Add page for model metadata

* Add intro section

* Add info on direct access

* more updates
2023-05-01 23:33:44 +10:00
Matthias Mair
08c4aa4998 Make sure plugins are always on a new line (#4721)
* "Install Plugin" via GUI fails to add plugin to plugins.txt correctly for first plugin
Fixes #4719

* remove old change as it was not the root issue

* make sure plugins are always written on a new line
2023-05-01 23:26:23 +10:00
Oliver
c5ba632463 Use coveralls github action (#4735) 2023-05-01 22:13:35 +10:00
Matthias Mair
abee2cee88 Code style improvements (#4683)
* fix list comps

* mopre comp fixes

* reduce computing cost on any() calls

* add bugbear

* check for clean imports

* only allow limited relative imports

* fix notification method lookup

* fix notification method assigement

* rewrite assigment

* fix upstream changes to new style

* fix upstream change to new coding style
2023-04-28 20:49:53 +10:00
Matthias Mair
660a4f8e39 bump deps (#4692)
* bump deps

* add markuplint setting to disable character-reference
2023-04-28 20:12:13 +10:00
Oliver
f6831558a4 Fix export of order data (#4714)
* Adds extra unit test for exporting sales orders

- Exporting sales orders to .xls currently throws exception

* Fix 'total_price' field when exporting orders

* Fix for unit test
2023-04-28 06:54:50 +10:00
Oliver
f6021c4749 sentry.io improvements (#4712)
* Write function to catch sentry.io events before sending

- Will let us ignore certain types of errors which we are not interested in

* Cleanup

* Include release info

* Allow sentry reporting in debug mode

* Consolidate DRF sentry code into InvenTree/sentry.py

* Add more error types to ignore

* update docs
2023-04-28 06:54:31 +10:00
miggland
8d28fc06be Improve text when no sub-categories found (#4711)
Avoids confusion when looking for parts
2023-04-27 22:57:16 +10:00
Oliver
e2c3b28640 Company docs updates (#4710)
* Refactor order documentation into simpler consolidated pages

* Reorganize images

* Add documentation on "Contact"

* Add imgae
2023-04-27 22:27:21 +10:00
Oliver
bb860227c8 New Crowdin updates (#4705)
* updated translation base

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-27 14:43:54 +10:00
Oliver
82e98dffb8 Add custom template loader class (#4706)
- Specifically to ignore caching for generated reports and labels
- Fixes https://github.com/inventree/InvenTree/issues/4611
2023-04-27 13:28:00 +10:00
Matthias Mair
3975a85742 Check if doc links are broken (#4686)
* add check for links

* fix style

* call silently

* add dep

* add path

* ignore readme as it contains necesarry broken links

* fix syntax

* check recursevly

* run in sync

* use linkcheckmd instaead

* use sync

* add requests

* revert fix to check if checks still work

* fix lookup

* fix old link

* run check verbose

* run async

* remove recurse

* introduce error for test

* fix link
2023-04-27 07:24:26 +10:00
Oliver
608f96c723 Add python example for deleting a part (#4703)
* Add python example for deleting a part

* Ignore PRs which are just docs from release notes
2023-04-26 23:18:03 +10:00
Oliver
5fcab2aec3 Specify order currency (#4698)
* Add 'order_currency' to the various external order models

- By default will use the currency specified for the supplier (or customer)
- Can be specified per order, also

* Display order currency on order pgae

* Add 'order_currency' field

* Enable "blank" currency option (to default to the currency specified by the referenced company

* Fix default currency code when adding line items

* Remove 'total_price_currency' serializer field

- Now replaced with 'order_currency' for greater flexibility

* Bump api_version.py

* Update default order report templates

* Updated docs

* More docs updaes

* Adjust unit tests

* Use 'order_currency' in order tables

* Update purchase order api unit tests
2023-04-26 17:35:15 +10:00
Oliver
36d17c082b Homepage hide inactive (#4700)
* Add user setting for including "inactive" parts in results displayed on homepage

* Adds user setting to hide inactive parts on the homepage

Closes https://github.com/inventree/InvenTree/issues/4688
2023-04-26 15:45:14 +10:00
Oliver
f382d7ef21 Disable httpstat.us tests (#4699)
- Uptime of the eternal service is not great
- For now, disable tests
- Potentially reintroduce these at some point in the future
2023-04-26 14:27:13 +10:00
Oliver
eaa518852c Mark "supplier" field as required on PurchaseOrder serializer (#4693)
- Fixes https://github.com/inventree/InvenTree/issues/4687
2023-04-26 12:35:55 +10:00
Lavissa
011b5915e1 Fix missed translation of overallocation string (#4681)
* Fix missed translation of overallocation string

* Added missing translation prefix on string for Overallocation REJECT choice

* Fix missing translation in part admin panel

* Added missing translation prefix on string for "variant of" column in
  admin panel
2023-04-25 22:41:31 +10:00
Matthias Mair
d7bdcd95a6 Fix plugin meta lookup (#4684)
* fix website lookup

* use the raw data on error
2023-04-24 08:28:50 +10:00
Oliver
5411cf0878 Update README.md (#4679)
Add documentation badge
2023-04-23 00:20:41 +10:00
Jakob Haufe
7537fd1278 Fix sorting by location in stock item table (#4658)
Currently, when sorting the stock item table by location, it gets sorted
by location id.

This changes the sorting criterion to pathstring.
2023-04-23 00:12:03 +10:00
Oliver
8df207d8e1 Extend ReportMixin class to support labels (#4678)
* Extend ReportMixin context to shim label templates also

* Update docs
2023-04-22 23:46:43 +10:00
Oliver
50cbaff76d Fixes for docs building (#4659) 2023-04-22 23:35:25 +10:00
Oliver
2ffd2354eb Documentation integration (#4653)
* Add documentation under docs/ directory

* Add CI workflow for mkdocs configuration checking

* Add documentation issue template

* update pip-tools?

* Update .gitignore files

* Fix .gitignore rules

* Improve release notes page

* remove references to old repo
2023-04-22 22:40:29 +10:00
Oliver
20f01e8741 New Crowdin updates (#4648)
* updated translation base

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-22 09:11:33 +10:00
Oliver
ad545bad24 Update to report plugin API (#4649)
explicitly add the model instance when allowing plugins to add context data
2023-04-21 15:28:31 +10:00
Oliver
9198b52398 Add ordering and search to ProjectCodeList API endpoint (#4645) 2023-04-20 22:51:47 +10:00
Oliver
0f2fd2f678 New Crowdin updates (#4641)
* updated translation base

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-20 22:49:31 +10:00
Oliver
91189fbb77 Fix for "focus" field in forms (#4644)
- Reimplements ability to auto-focus fields when launching modal forms
- Can specify with the "focus" option
- Otherwise, will focus on the first available field
2023-04-20 22:40:08 +10:00
Oliver
7bc4de6a92 Fix persist buttons (#4640)
Fixes "persist form" buttons for the following models:

- Part
- PartCategory
- StockItem
- StockLocation

Closes https://github.com/inventree/InvenTree/issues/4491
2023-04-20 22:12:43 +10:00
Oliver
c64ff9d569 Refactor table buttons for printing (#4639)
- Make them to look like the other printing buttons
2023-04-20 22:12:33 +10:00
Oliver
68d1682000 Report Plugins (#4643)
* Add plugin mixin class for extending reports

* Expose report context to the plugin system

* Add an example mixin for adding context data to a report

* Add the 'request' object to the plugin code
2023-04-20 14:21:22 +10:00
Lavissa
a020548c8e Fix typo in Update Check Interval setting (#4642)
Change will create a new translation key
The following locales have added strings to the old key:

DE
Intervall für die Suche nach Updates

HU
Frissítés keresés gyakorisága

PT
Atualizar Verificação de Intervalo
2023-04-20 11:15:01 +10:00
Oliver
070e2afcea Project code support (#4636)
* Support image uploads in the "notes" markdown fields

- Implemented using the existing EasyMDE library
- Copy / paste support
- Drag / drop support

* Remove debug message

* Updated API version

* Better UX when saving notes

* Pin PIP version (for testing)

* Bug fixes

- Fix typo
- Use correct serializer type

* Add unit testing

* Update role permissions

* Typo fix

* Update migration file

* Adds a notes mixin class to be used for refactoring

* Refactor existing models with notes to use the new mixin

* Add helper function for finding all model types with a certain mixin

* Refactor barcode plugin to use new method

* Typo fix

* Add daily task to delete old / unused notes

* Add ProjectCode model

(cherry picked from commit 382a0a2fc32c930d46ed3fe0c6d2cae654c2209d)

* Adds IsStaffOrReadyOnly permissions

- Authenticated users get read-only access
- Staff users get read/write access

(cherry picked from commit 53d04da86c4c866fd9c909d147d93844186470b4)

* Adds API endpoints for project codes

(cherry picked from commit 5ae1da23b2eae4e1168bc6fe28a3544dedc4a1b4)

* Add migration file for projectcode model

(cherry picked from commit 5f8717712c65df853ea69907d33e185fd91df7ee)

* Add project code configuration page to the global settings view

* Add 'project code' field to orders

* Add ability to set / edit the project code for various order models

* Add project code info to order list tables

* Add configuration options for project code integration

* Allow orders to be filtered by project code

* Refactor table_filters.js

- Allow orders to be filtered dynamically by project code

* Bump API version

* Fixes

* Add resource mixin for exporting project code in order list

* Add "has_project_code" filter

* javascript fix

* Edit / delete project codes via API

- Also refactor some existing JS

* Move MetadataMixin to InvenTree.models

To prevent circular imports

(cherry picked from commit d23b013881eaffe612dfbfcdfc5dff6d729068c6)

* Fixes for circular imports

* Add metadata for ProjectCode model

* Add Metadata API endpoint for ProjectCode

* Add unit testing for ProjectCode API endpoints
2023-04-20 00:47:07 +10:00
Oliver
eafd2ac966 Catch error if notes directory does not exist (#4638) 2023-04-19 22:41:19 +10:00
Matthias Mair
1b8ad70fb6 [FR] Refactor plugin registry (#4340)
* add mixin order ref

* move import

* fix import order

* reorder import

* move activation/deactivation to mixins

* move loaded/unloaded mixins out into seperate modules

* fix deactivation sequence

* switch to classmethods for loading

* only run (de)activation if defined for mixin
Fixes #4184

* fix deactivating

* move reloading back to registry

* fix merge error

* move app mixin deactivation

* fix migration reloading

* reverse deactivation sequence

* Revert "reverse deactivation sequence"

This reverts commit aff17dd07d.
2023-04-19 20:54:42 +10:00
Oliver
5cd74c4190 Support image uploads in the "notes" markdown fields (#4615)
* Support image uploads in the "notes" markdown fields

- Implemented using the existing EasyMDE library
- Copy / paste support
- Drag / drop support

* Remove debug message

* Updated API version

* Better UX when saving notes

* Pin PIP version (for testing)

* Bug fixes

- Fix typo
- Use correct serializer type

* Add unit testing

* Update role permissions

* Typo fix

* Update migration file

* Adds a notes mixin class to be used for refactoring

* Refactor existing models with notes to use the new mixin

* Add helper function for finding all model types with a certain mixin

* Refactor barcode plugin to use new method

* Typo fix

* Add daily task to delete old / unused notes

* Bug fix for barcode refactoring

* Add unit testing for function
2023-04-19 13:08:26 +10:00
Matthias Mair
2623c22b7e Remove hashes from dev requirements (#4627)
* Remove hashes from dev requirements
Fixes #4616

* rollback https://github.com/inventree/InvenTree/pull/4617
2023-04-19 07:10:02 +10:00
Matthias Mair
9d5522c18c [FR] Update to OpenAPI from CoreAPI (#4178)
* [FR] Update to OpenAPI from CoreAPI
Fixes #3226

* factor request function out

* add schema export task

* add api-docs

* add action to check if diff occured

* also wait for docstyle

* use full command

* add envs for inventree

* update inventree before running

* use relative path

* remove schema action

* remove tags to fit 3.0 parsers

* fix url base name for reloads

* revert change in plugin resolver

* remove unused tags

* add rapidoc too

* declare api regex

* fix as suggested by @martonmiklos in
https://github.com/inventree/InvenTree/pull/4178#discussion_r1167279443

* set inventree logo

* remove Rapidoc
2023-04-18 23:08:36 +10:00
Oliver
b0f6021002 Implement pagination for stock history tracking API (#4629) 2023-04-18 22:40:31 +10:00
Oliver
ae05c68417 New Crowdin updates (#4624)
* updated translation base

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

* Fix: New translations django.po from Crowdin

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-18 09:40:59 +10:00
Oliver
3e53b60cac Mark as 0.12.0 dev (#4622) 2023-04-18 00:50:51 +10:00
Oliver
f6b9b12745 Translation fixes (#4621)
* Remove HTML tags from translateable string

* Improve another string

* Improve strings for plugin version check

* Cleanup duplicate serials msg

* JS translations

* fmt
2023-04-18 00:12:14 +10:00
1046 changed files with 204655 additions and 121506 deletions

8
.devcontainer/Dockerfile Normal file → Executable file
View File

@@ -4,6 +4,8 @@
ARG VARIANT="3.10-bullseye"
FROM mcr.microsoft.com/vscode/devcontainers/python:0-${VARIANT}
ARG WORKSPACE="/workspaces/InvenTree"
# [Choice] Node.js version: none, lts/*, 16, 14, 12, 10
ARG NODE_VERSION="none"
RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi
@@ -16,7 +18,7 @@ RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/
# [Optional] Uncomment this section to install additional OS packages.
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive && \
apt-get -y install --no-install-recommends \
git gcc g++ gettext gnupg libffi-dev \
git gcc g++ gettext gnupg2 libffi-dev \
# Weasyprint requirements : https://doc.courtbouillon.org/weasyprint/stable/first_steps.html#debian-11
poppler-utils libpango-1.0-0 libpangoft2-1.0-0 \
# Image format support
@@ -42,6 +44,6 @@ RUN pip install --disable-pip-version-check -U -r base_requirements.txt
# preserve command history between container starts
# Ref: https://code.visualstudio.com/remote/advancedcontainers/persist-bash-history
# Folder will be created in 'postCreateCommand' in devcontainer.json as it's not preserved due to the bind mount
RUN echo "export PROMPT_COMMAND='history -a' && export HISTFILE=/workspaces/InvenTree/dev/commandhistory/.bash_history" >> "/home/vscode/.bashrc"
RUN echo "export PROMPT_COMMAND='history -a' && export HISTFILE=${WORKSPACE}/dev/commandhistory/.bash_history" >> "/home/vscode/.bashrc"
WORKDIR /workspaces/InvenTree
WORKDIR ${WORKSPACE}

View File

@@ -11,7 +11,8 @@
// Use -bullseye variants on local on arm64/Apple Silicon.
"VARIANT": "3.10-bullseye",
// Options
"NODE_VERSION": "lts/*"
"NODE_VERSION": "lts/*",
"WORKSPACE": "${containerWorkspaceFolder}"
}
},
@@ -21,7 +22,7 @@
"vscode": {
// Set *default* container specific settings.json values on container create.
"settings": {
"python.defaultInterpreterPath": "/workspaces/InvenTree/dev/venv/bin/python",
"python.defaultInterpreterPath": "${containerWorkspaceFolder}/dev/venv/bin/python",
"python.linting.enabled": true,
"python.linting.pylintEnabled": false,
"python.linting.flake8Enabled": true,
@@ -40,7 +41,8 @@
"extensions": [
"ms-python.python",
"ms-python.vscode-pylance",
"batisteo.vscode-django"
"batisteo.vscode-django",
"eamodio.gitlens"
]
}
},
@@ -54,7 +56,7 @@
},
// Use 'postCreateCommand' to run commands after the container is created.
"postCreateCommand": "./.devcontainer/postCreateCommand.sh",
"postCreateCommand": "./.devcontainer/postCreateCommand.sh ${containerWorkspaceFolder}",
// Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
"remoteUser": "vscode",
@@ -68,21 +70,22 @@
"INVENTREE_DEBUG": "True",
"INVENTREE_DEBUG_LEVEL": "INFO",
"INVENTREE_DB_ENGINE": "sqlite3",
"INVENTREE_DB_NAME": "/workspaces/InvenTree/dev/database.sqlite3",
"INVENTREE_MEDIA_ROOT": "/workspaces/InvenTree/dev/media",
"INVENTREE_STATIC_ROOT": "/workspaces/InvenTree/dev/static",
"INVENTREE_BACKUP_DIR": "/workspaces/InvenTree/dev/backup",
"INVENTREE_CONFIG_FILE": "/workspaces/InvenTree/dev/config.yaml",
"INVENTREE_SECRET_KEY_FILE": "/workspaces/InvenTree/dev/secret_key.txt",
"INVENTREE_PLUGIN_DIR": "/workspaces/InvenTree/dev/plugins",
"INVENTREE_PLUGIN_FILE": "/workspaces/InvenTree/dev/plugins.txt",
"INVENTREE_DB_NAME": "${containerWorkspaceFolder}/dev/database.sqlite3",
"INVENTREE_MEDIA_ROOT": "${containerWorkspaceFolder}/dev/media",
"INVENTREE_STATIC_ROOT": "${containerWorkspaceFolder}/dev/static",
"INVENTREE_BACKUP_DIR": "${containerWorkspaceFolder}/dev/backup",
"INVENTREE_CONFIG_FILE": "${containerWorkspaceFolder}/dev/config.yaml",
"INVENTREE_SECRET_KEY_FILE": "${containerWorkspaceFolder}/dev/secret_key.txt",
"INVENTREE_PLUGINS_ENABLED": "True",
"INVENTREE_PLUGIN_DIR": "${containerWorkspaceFolder}/dev/plugins",
"INVENTREE_PLUGIN_FILE": "${containerWorkspaceFolder}/dev/plugins.txt",
// Python config
"PIP_USER": "no",
// used to load the venv into the PATH and avtivate it
// used to load the venv into the PATH and activate it
// Ref: https://stackoverflow.com/a/56286534
"VIRTUAL_ENV": "/workspaces/InvenTree/dev/venv",
"PATH": "/workspaces/InvenTree/dev/venv/bin:${containerEnv:PATH}"
"VIRTUAL_ENV": "${containerWorkspaceFolder}/dev/venv",
"PATH": "${containerWorkspaceFolder}/dev/venv/bin:${containerEnv:PATH}"
}
}

View File

@@ -1,8 +1,12 @@
#!/bin/bash
# Avoiding Dubious Ownership in Dev Containers for setup commands that use git
# Note that the local workspace directory is passed through as the first argument $1
git config --global --add safe.directory $1
# create folders
mkdir -p /workspaces/InvenTree/dev/{commandhistory,plugins}
cd /workspaces/InvenTree
mkdir -p $1/dev/{commandhistory,plugins}
cd $1
# create venv
python3 -m venv dev/venv
@@ -10,5 +14,10 @@ python3 -m venv dev/venv
# setup InvenTree server
pip install invoke
inv update
inv setup-dev
invoke update
invoke setup-dev
# remove existing gitconfig created by "Avoiding Dubious Ownership" step
# so that it gets copied from host to the container to have your global
# git config in container
rm -f /home/vscode/.gitconfig

3
.djlintrc Normal file
View File

@@ -0,0 +1,3 @@
{
"ignore": "D018,H006,H008,H020,H021,H023,H025,H030,H031,T002"
}

View File

@@ -4,7 +4,7 @@ env:
es2021: true
jquery: true
extends:
- google
- eslint:recommended
parserOptions:
ecmaVersion: 12
rules:
@@ -19,6 +19,8 @@ rules:
valid-jsdoc: off
no-multiple-empty-lines: off
comma-dangle: off
no-unused-vars: off
no-useless-escape: off
prefer-spread: off
indent:
- error

4
.github/FUNDING.yml vendored
View File

@@ -1,2 +1,4 @@
patreon: inventree
github: inventree
ko_fi: inventree
patreon: inventree
custom: [paypal.me/inventree]

View File

@@ -52,6 +52,13 @@ body:
label: "Version Information"
description: "The version info block."
placeholder: "You can get this by going to the `About InvenTree` section in the upper right corner and clicking on the `copy version information` button"
- type: checkboxes
id: can-reproduce
attributes:
label: "Please verify if you can reproduce this bug on the demo site."
description: "You can sign in at [InvenTree Demo](https://demo.inventree.org) with admin:inventree. Note that this instance runs on the latest dev version, so your bug may be fixed there."
options:
- label: "I can reproduce this bug on the demo site."
- type: textarea
id: logs
attributes:

View File

@@ -0,0 +1,15 @@
name: "Documentation"
description: "Create an issue to improve the documentation"
labels: ["documentation", "triage:not-checked"]
body:
- type: markdown
attributes:
value: |
Create a new issue regarding the InvenTree documentation
- type: textarea
id: repro
attributes:
label: Body of the issue
description: Please provide one distinct thing to fix or a clearly defined enhancement
validations:
required: true

View File

@@ -1,5 +1,5 @@
name: 'Migration test'
description: 'Run migration test sequenze'
description: 'Run migration test sequence'
author: 'InvenTree'
runs:

View File

@@ -1,5 +1,5 @@
name: 'Setup Enviroment'
description: 'Setup the enviroment for general InvenTree tests'
description: 'Setup the environment for general InvenTree tests'
author: 'InvenTree'
inputs:
python:
@@ -48,7 +48,7 @@ runs:
if: ${{ inputs.python == 'true' }}
shell: bash
run: |
python3 -m pip install pip==23.0.1
python3 -m pip install -U pip
pip3 install invoke wheel
- name: Install Specific Python Dependencies
if: ${{ inputs.pip-dependency }}
@@ -62,7 +62,7 @@ runs:
with:
node-version: ${{ env.node_version }}
cache: 'npm'
- name: Intall npm packages
- name: Install npm packages
if: ${{ inputs.npm == 'true' }}
shell: bash
run: npm install

5
.github/release.yml vendored
View File

@@ -4,6 +4,7 @@ changelog:
exclude:
labels:
- translation
- documentation
categories:
- title: Breaking Changes
labels:
@@ -15,7 +16,11 @@ changelog:
- title: New Features
labels:
- Semver-Minor
- feature
- enhancement
- title: Experimental Features
labels:
- experimental
- title: Bug Fixes
labels:
- Semver-Patch

37
.github/workflows/backport.yml vendored Normal file
View File

@@ -0,0 +1,37 @@
# Backport tagged issues to a stable branch.
#
# To enable backporting for a pullrequest, add the label "backport" to the PR.
# Additionally, add a label with the prefix "backport-to-" and the target branch
name: Backport
on:
pull_request_target:
types: ["labeled", "closed"]
jobs:
backport:
name: Backport PR
runs-on: ubuntu-latest
if: |
github.event.pull_request.merged == true
&& contains(github.event.pull_request.labels.*.name, 'backport')
&& (
(github.event.action == 'labeled' && github.event.label.name == 'backport')
|| (github.event.action == 'closed')
)
steps:
- name: Backport Action
uses: sqren/backport-github-action@v8.9.3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
auto_backport_label_prefix: backport-to-
add_original_reviewers: true
- name: Info log
if: ${{ success() }}
run: cat ~/.backport/backport.info.log
- name: Debug log
if: ${{ failure() }}
run: cat ~/.backport/backport.debug.log

View File

@@ -29,9 +29,6 @@ jobs:
# Build the docker image
build:
concurrency:
group: ${{ github.workflow }}-${{ github.event_name }}
cancel-in-progress: true
runs-on: ubuntu-latest
permissions:
contents: read
@@ -63,7 +60,7 @@ jobs:
docker-compose run inventree-dev-server invoke update
docker-compose run inventree-dev-server invoke setup-dev
docker-compose up -d
docker-compose run inventree-dev-server pip install --upgrade setuptools
docker-compose run inventree-dev-server pip install setuptools==68.1.2
docker-compose run inventree-dev-server invoke wait
- name: Check Data Directory
# The following file structure should have been created by the docker image
@@ -81,6 +78,7 @@ jobs:
run: |
echo "GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }}" >> docker.dev.env
docker-compose run inventree-dev-server invoke test --disable-pty
docker-compose run inventree-dev-server invoke test --migrations --disable-pty
docker-compose down
- name: Set up QEMU
if: github.event_name != 'pull_request'
@@ -121,8 +119,10 @@ jobs:
uses: docker/build-push-action@c56af957549030174b10d6867f20e78cfd7debc5 # pin@v3.2.0
with:
context: .
platforms: linux/amd64,linux/arm64,linux/arm/v7
platforms: linux/amd64,linux/arm64
push: true
sbom: true
provenance: false
target: production
tags: ${{ env.docker_tags }}
build-args: |

View File

@@ -1,15 +1,12 @@
# Checks for each PR / push
name: QC checks
name: QC
on:
push:
branches-ignore:
- l10*
branches-ignore: ['l10*']
pull_request:
branches-ignore:
- l10*
branches-ignore: ['l10*']
env:
python_version: 3.9
@@ -25,13 +22,38 @@ env:
INVENTREE_BACKUP_DIR: ../test_inventree_backup
jobs:
paths-filter:
name: Filter
runs-on: ubuntu-latest
outputs:
server: ${{ steps.filter.outputs.server }}
migrations: ${{ steps.filter.outputs.migrations }}
steps:
- uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # pin@v3.1.0
- uses: dorny/paths-filter@v2
id: filter
with:
filters: |
server:
- 'InvenTree/**'
- 'requirements.txt'
- 'requirements-dev.txt'
migrations:
- '**/migrations/**'
- '.github/workflows**'
pep_style:
name: Style [Python]
runs-on: ubuntu-20.04
needs: paths-filter
if: needs.paths-filter.outputs.server == 'true'
steps:
- uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # pin@v3.1.0
- name: Enviroment Setup
- name: Environment Setup
uses: ./.github/actions/setup
with:
dev-install: true
@@ -46,7 +68,7 @@ jobs:
steps:
- uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # pin@v3.1.0
- name: Enviroment Setup
- name: Environment Setup
uses: ./.github/actions/setup
with:
npm: true
@@ -60,22 +82,6 @@ jobs:
python InvenTree/manage.py prerender
npx eslint InvenTree/InvenTree/static_i18n/i18n/*.js
html:
name: Style [HTML]
runs-on: ubuntu-20.04
needs: pep_style
steps:
- uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # pin@v3.1.0
- name: Enviroment Setup
uses: ./.github/actions/setup
with:
npm: true
install: true
- name: Check HTML Files
run: npx markuplint **/templates/*.html
pre-commit:
name: Style [pre-commit]
runs-on: ubuntu-20.04
@@ -96,6 +102,28 @@ jobs:
pip install requests
python3 ci/version_check.py
mkdocs:
name: Style [Documentation]
runs-on: ubuntu-20.04
needs: paths-filter
steps:
- name: Checkout Code
uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # pin@v3.1.0
- name: Set up Python ${{ env.python_version }}
uses: actions/setup-python@13ae5bb136fac2878aff31522b9efb785519f984 # pin@v4.3.0
with:
python-version: ${{ env.python_version }}
- name: Check Config
run: |
pip install pyyaml
python docs/ci/check_mkdocs_config.py
- name: Check Links
run: |
pip install linkcheckmd requests
python -m linkcheckmd docs --recurse
python:
name: Tests - inventree-python
runs-on: ubuntu-20.04
@@ -115,7 +143,7 @@ jobs:
steps:
- uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # pin@v3.1.0
- name: Enviroment Setup
- name: Environment Setup
uses: ./.github/actions/setup
with:
apt-dependency: gettext poppler-utils
@@ -145,7 +173,7 @@ jobs:
steps:
- uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # pin@v3.1.0
- name: Enviroment Setup
- name: Environment Setup
uses: ./.github/actions/setup
with:
install: true
@@ -156,37 +184,40 @@ jobs:
name: Tests - DB [SQLite] + Coverage
runs-on: ubuntu-20.04
needs: [ 'javascript', 'html', 'pre-commit' ]
needs: [ 'javascript', 'pre-commit' ]
continue-on-error: true # continue if a step fails so that coverage gets pushed
env:
INVENTREE_DB_NAME: ./inventree.sqlite
INVENTREE_DB_ENGINE: sqlite3
INVENTREE_PLUGINS_ENABLED: true
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # pin@v3.1.0
- name: Enviroment Setup
- name: Environment Setup
uses: ./.github/actions/setup
with:
apt-dependency: gettext poppler-utils
dev-install: true
update: true
- name: Coverage Tests
run: invoke coverage
- name: Data Export Test
uses: ./.github/actions/migration
- name: Test Translations
run: invoke translate
- name: Check Migration Files
run: python3 ci/check_migration_files.py
- name: Coverage Tests
run: invoke test --coverage
- name: Upload Coverage Report
run: coveralls
uses: coverallsapp/github-action@v2
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
postgres:
name: Tests - DB [PostgreSQL]
runs-on: ubuntu-20.04
needs: [ 'javascript', 'html', 'pre-commit' ]
needs: [ 'javascript', 'pre-commit' ]
env:
INVENTREE_DB_ENGINE: django.db.backends.postgresql
@@ -214,7 +245,7 @@ jobs:
steps:
- uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # pin@v3.1.0
- name: Enviroment Setup
- name: Environment Setup
uses: ./.github/actions/setup
with:
apt-dependency: gettext poppler-utils libpq-dev
@@ -230,7 +261,7 @@ jobs:
name: Tests - DB [MySQL]
runs-on: ubuntu-20.04
needs: [ 'javascript', 'html', 'pre-commit' ]
needs: [ 'javascript', 'pre-commit' ]
if: github.event_name == 'push'
env:
@@ -259,7 +290,7 @@ jobs:
steps:
- uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # pin@v3.1.0
- name: Enviroment Setup
- name: Environment Setup
uses: ./.github/actions/setup
with:
apt-dependency: gettext poppler-utils libmysqlclient-dev
@@ -270,3 +301,82 @@ jobs:
run: invoke test
- name: Data Export Test
uses: ./.github/actions/migration
migration-tests:
name: Run Migration Unit Tests
runs-on: ubuntu-latest
needs: paths-filter
if: github.ref == 'refs/heads/master' && needs.paths-filter.outputs.migrations == 'true'
env:
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_PLUGINS_ENABLED: false
services:
postgres:
image: postgres:14
env:
POSTGRES_USER: inventree
POSTGRES_PASSWORD: password
ports:
- 5432:5432
steps:
- uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # pin@v3.1.0
- name: Environment Setup
uses: ./.github/actions/setup
with:
apt-dependency: gettext poppler-utils libpq-dev
pip-dependency: psycopg2
dev-install: true
update: true
- name: Run Tests
run: invoke test --migrations --report
migrations-checks:
name: Run Database Migrations
runs-on: ubuntu-latest
needs: paths-filter
if: github.ref == 'refs/heads/master' && needs.paths-filter.outputs.migrations == 'true'
env:
INVENTREE_DB_ENGINE: sqlite3
INVENTREE_DB_NAME: /home/runner/work/InvenTree/db.sqlite3
INVENTREE_DEBUG: info
INVENTREE_PLUGINS_ENABLED: false
steps:
- uses: actions/checkout@v3
name: Checkout Code
- name: Environment Setup
uses: ./.github/actions/setup
with:
install: true
- name: Fetch Database
run: git clone --depth 1 https://github.com/inventree/test-db ./test-db
- name: Latest Database
run: |
cp test-db/latest.sqlite3 /home/runner/work/InvenTree/db.sqlite3
chmod +rw /home/runner/work/InvenTree/db.sqlite3
invoke migrate
- name: 0.10.0 Database
run: |
rm /home/runner/work/InvenTree/db.sqlite3
cp test-db/stable_0.10.0.sqlite3 /home/runner/work/InvenTree/db.sqlite3
chmod +rw /home/runner/work/InvenTree/db.sqlite3
invoke migrate
- name: 0.11.0 Database
run: |
rm /home/runner/work/InvenTree/db.sqlite3
cp test-db/stable_0.11.0.sqlite3 /home/runner/work/InvenTree/db.sqlite3
chmod +rw /home/runner/work/InvenTree/db.sqlite3
invoke migrate

View File

@@ -1,35 +0,0 @@
# Runs on releases
name: Publish release notes
on:
release:
types: [ published ]
jobs:
tweet:
runs-on: ubuntu-latest
steps:
- uses: Eomm/why-don-t-you-tweet@5936bb1fd0096b1c2bbbb7518746638261bb4dae # pin@v1.0.1
with:
tweet-message: "InvenTree release ${{ github.event.release.tag_name }} is out
now! Release notes: ${{ github.event.release.html_url }} #opensource
#inventree"
env:
TWITTER_CONSUMER_API_KEY: ${{ secrets.TWITTER_CONSUMER_API_KEY }}
TWITTER_CONSUMER_API_SECRET: ${{ secrets.TWITTER_CONSUMER_API_SECRET }}
TWITTER_ACCESS_TOKEN: ${{ secrets.TWITTER_ACCESS_TOKEN }}
TWITTER_ACCESS_TOKEN_SECRET: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }}
reddit:
runs-on: ubuntu-latest
steps:
- uses: bluwy/release-for-reddit-action@4b2d034b5c86a24db24363f1064149a8c2db69b4 # pin@v1.2.0
with:
username: ${{ secrets.REDDIT_USERNAME }}
password: ${{ secrets.REDDIT_PASSWORD }}
app-id: ${{ secrets.REDDIT_APP_ID }}
app-secret: ${{ secrets.REDDIT_APP_SECRET }}
subreddit: InvenTree
title: "InvenTree version ${{ github.event.release.tag_name }} released"
comment: "${{ github.event.release.body }}"

5
.gitignore vendored
View File

@@ -45,9 +45,6 @@ inventree/label.png
inventree/my_special*
_tests*.txt
# Sphinx files
docs/_build
# Local static and media file storage (only when running in development mode)
inventree_media
inventree_static
@@ -100,7 +97,7 @@ node_modules/
maintenance_mode_state.txt
# plugin dev directory
plugins/
InvenTree/plugins/
# Compiled translation files
*.mo

View File

@@ -1,44 +0,0 @@
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 INVENTREE_BACKUP_DIR='/workspace/InvenTree/dev/backup'
export PIP_USER='no'
sudo apt install -y gettext
python3 -m venv venv
source venv/bin/activate
pip install invoke pyyaml
mkdir dev
invoke update
invoke setup-test --ignore-update --path inventree-data
gp sync-done start_server
- name: Start server
init: gp sync-await start_server
command: |
gp sync-await start_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'
export INVENTREE_BACKUP_DIR='/workspace/InvenTree/dev/backup'
source venv/bin/activate
inv server
ports:
- port: 8000
onOpen: open-preview
github:
prebuilds:
master: true
pullRequests: false
pullRequestsFromForks: true
addBadge: true
addLabel: gitpod-ready
addCheck: false

View File

@@ -15,6 +15,8 @@ env:
- INVENTREE_PLUGIN_FILE=/opt/inventree/plugins.txt
- INVENTREE_CONFIG_FILE=/opt/inventree/config.yaml
after_install: contrib/packager.io/postinstall.sh
before:
- contrib/packager.io/before.sh
dependencies:
- curl
- python3

View File

@@ -12,29 +12,45 @@ repos:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
- id: mixed-line-ending
- repo: https://github.com/pycqa/flake8
rev: '6.0.0'
hooks:
- id: flake8
additional_dependencies: [
'flake8-bugbear',
'flake8-comprehensions',
'flake8-docstrings',
'flake8-string-format',
'pep8-naming ',
'flake8-tidy-imports',
'pep8-naming'
]
- repo: https://github.com/pycqa/isort
rev: '5.12.0'
hooks:
- id: isort
- repo: https://github.com/jazzband/pip-tools
rev: 6.12.3
rev: 6.13.0
hooks:
- id: pip-compile
name: pip-compile requirements-dev.in
args: [--generate-hashes, requirements-dev.in, -o, requirements-dev.txt]
args: [requirements-dev.in, -o, requirements-dev.txt]
files: ^requirements-dev\.(in|txt)$
- id: pip-compile
name: pip-compile requirements.txt
args: [requirements.in, -o, requirements.txt]
files: ^requirements\.(in|txt)$
- repo: https://github.com/Riverside-Healthcare/djLint
rev: v1.30.2
hooks:
- id: djlint-django
- repo: https://github.com/codespell-project/codespell
rev: v2.2.4
hooks:
- id: codespell
exclude: >
(?x)^(
docs/docs/stylesheets/.*|
docs/docs/javascripts/.*|
docs/docs/webfonts/.*
)$

12
.vscode/tasks.json vendored
View File

@@ -1,52 +1,64 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
// the problemMatchers should prevent vscode from asking how it should check the output
"version": "2.0.0",
"tasks": [
{
"label": "clean-settings",
"type": "shell",
"command": "inv clean-settings",
"problemMatcher": [],
},
{
"label": "delete-data",
"type": "shell",
"command": "inv delete-data",
"problemMatcher": [],
},
{
"label": "migrate",
"type": "shell",
"command": "inv migrate",
"problemMatcher": [],
},
{
"label": "server",
"type": "shell",
"command": "inv server",
"problemMatcher": [],
},
{
"label": "setup-dev",
"type": "shell",
"command": "inv setup-dev",
"problemMatcher": [],
},
{
"label": "setup-test",
"type": "shell",
"command": "inv setup-test --path dev/inventree-demo-dataset",
"problemMatcher": [],
},
{
"label": "superuser",
"type": "shell",
"command": "inv superuser",
"problemMatcher": [],
},
{
"label": "test",
"type": "shell",
"command": "inv test",
"problemMatcher": [],
},
{
"label": "update",
"type": "shell",
"command": "inv update",
"problemMatcher": [],
},
]
}

View File

@@ -1,4 +1,4 @@
Hi there, thank you for your intrest in contributing!
Hi there, thank you for your interest in contributing!
Please read the contribution guidelines below, before submitting your first pull request to the InvenTree codebase.
## Quickstart
@@ -19,7 +19,7 @@ pip install invoke && invoke setup-dev --tests
```bash
git clone https://github.com/inventree/InvenTree.git && cd InvenTree
docker compose run inventree-dev-server invoke install
docker compose run inventree-dev-server invoke setup-test
docker compose run inventree-dev-server invoke setup-test --dev
docker compose up -d
```
@@ -33,7 +33,7 @@ Run the following command to set up all toolsets for development.
invoke setup-dev
```
*We recommend you run this command before starting to contribute. This will install and set up `pre-commit` to run some checks before each commit and help reduce the style errors.*
*We recommend you run this command before starting to contribute. This will install and set up `pre-commit` to run some checks before each commit and help reduce errors.*
## Branches and Versioning
@@ -50,7 +50,7 @@ The HEAD of the "main" or "master" branch of InvenTree represents the current "l
- All feature branches are merged into master
- All bug fixes are merged into master
**No pushing to master:** New featues must be submitted as a pull request from a separate branch (one branch per feature).
**No pushing to master:** New features must be submitted as a pull request from a separate branch (one branch per feature).
### Feature Branches
@@ -70,7 +70,7 @@ The HEAD of the "stable" branch represents the latest stable release code.
#### Release Candidate Branches
- Release candidate branches are made from master, and merged into stable.
- RC branches are targetted at a major/minor version e.g. "0.5"
- RC branches are targeted at a major/minor version e.g. "0.5"
- When a release candidate branch is merged into *stable*, the release is tagged
#### Bugfix Branches
@@ -101,7 +101,7 @@ django-upgrade --target-version 3.2 `find . -name "*.py"`
```
## Credits
If you add any new dependencies / libraries, they need to be added to [the docs](https://github.com/inventree/inventree-docs/blob/master/docs/credits.md). Please try to do that as timely as possible.
If you add any new dependencies / libraries, they need to be added to [the docs](https://github.com/inventree/inventree/blob/master/docs/docs/credits.md). Please try to do that as timely as possible.
## Migration Files
@@ -123,14 +123,44 @@ The InvenTree code base makes use of [GitHub actions](https://github.com/feature
The various github actions can be found in the `./github/workflows` directory
### Run tests locally
To run test locally, use:
```
invoke test
```
To run only partial tests, for example for a module use:
```
invoke test --runtest order
```
## Code Style
Sumbitted Python code is automatically checked against PEP style guidelines. Locally you can run `invoke style` to ensure the style checks will pass, before submitting the PR.
Submitted Python code is automatically checked against PEP style guidelines. Locally you can run `invoke style` to ensure the style checks will pass, before submitting the PR.
Please write docstrings for each function and class - we follow the [google doc-style](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) for python. Docstrings for general javascript code is encouraged! Docstyles are checked by `invoke style`.
### Django templates
Django are checked by [djlint](https://github.com/Riverside-Healthcare/djlint) through pre-commit.
The following rules out of the [default set](https://djlint.com/docs/linter/) are not applied:
```bash
D018: (Django) Internal links should use the { % url ... % } pattern
H006: Img tag should have height and width attributes
H008: Attributes should be double quoted
H021: Inline styles should be avoided
H023: Do not use entity references
H025: Tag seems to be an orphan
H030: Consider adding a meta description
H031: Consider adding meta keywords
T002: Double quotes should be used in tags
```
## Documentation
New features or updates to existing features should be accompanied by user documentation. A PR with associated documentation should link to the matching PR at https://github.com/inventree/inventree-docs/
New features or updates to existing features should be accompanied by user documentation.
## Translations
@@ -157,16 +187,16 @@ user_facing_string = _('This string will be exposed to the translation engine!')
HTML and javascript files are passed through the django templating engine. Translatable strings are implemented as follows:
```html
{% load i18n %}
{ % load i18n % }
<span>{% trans "This string will be translated" %} - this string will not!</span>
<span>{ % trans "This string will be translated" % } - this string will not!</span>
```
## Github use
### Tags
The tags describe issues and PRs in multiple areas:
| Area | Name | Description |
|---|---|---|
| --- | --- | --- |
| Triage Labels | | |
| | triage:not-checked | Item was not checked by the core team |
| | triage:not-approved | Item is not green-light by maintainer |
@@ -175,10 +205,13 @@ The tags describe issues and PRs in multiple areas:
| | bug | Identifies a bug which needs to be addressed |
| | dependency | Relates to a project dependency |
| | duplicate | Duplicate of another issue or PR |
| | enhancement | This is an suggested enhancement or new feature |
| | enhancement | This is an suggested enhancement, extending the functionality of an existing feature |
| | experimental | This is a new *experimental* feature which needs to be enabled manually |
| | feature | This is a new feature, introducing novel functionality |
| | help wanted | Assistance required |
| | invalid | This issue or PR is considered invalid |
| | inactive | Indicates lack of activity |
| | migration | Database migration, requires special attention |
| | question | This is a question |
| | roadmap | This is a roadmap feature with no immediate plans for implementation |
| | security | Relates to a security issue |
@@ -197,7 +230,9 @@ The tags describe issues and PRs in multiple areas:
| | stock | Stock item management |
| | user interface | User interface |
| Ecosystem Labels | | |
| | backport | Tags that the issue will be backported to a stable branch as a bug-fix |
| | demo | Relates to the InvenTree demo server or dataset |
| | docker | Docker / docker-compose |
| | CI | CI / unit testing ecosystem |
| | refactor | Refactoring existing code |
| | setup | Relates to the InvenTree setup / installation process |

View File

@@ -1,5 +1,10 @@
"""Admin classes"""
from django.contrib import admin
from django.http.request import HttpRequest
from djmoney.contrib.exchange.admin import RateAdmin
from djmoney.contrib.exchange.models import Rate
from import_export.resources import ModelResource
@@ -31,3 +36,27 @@ class InvenTreeResource(ModelResource):
row[idx] = val
return row
def get_fields(self, **kwargs):
"""Return fields, with some common exclusions"""
fields = super().get_fields(**kwargs)
fields_to_exclude = [
'metadata',
'lft', 'rght', 'tree_id', 'level',
]
return [f for f in fields if f.column_name not in fields_to_exclude]
class CustomRateAdmin(RateAdmin):
"""Admin interface for the Rate class"""
def has_add_permission(self, request: HttpRequest) -> bool:
"""Disable the 'add' permission for Rate objects"""
return False
admin.site.unregister(Rate)
admin.site.register(Rate, CustomRateAdmin)

View File

@@ -59,14 +59,39 @@ class NotFoundView(AjaxView):
permission_classes = [permissions.AllowAny]
def get(self, request, *args, **kwargs):
"""Proces an `not found` event on the API."""
data = {
'details': _('API endpoint not found'),
'url': request.build_absolute_uri(),
}
def not_found(self, request):
"""Return a 404 error"""
return JsonResponse(
{
'detail': _('API endpoint not found'),
'url': request.build_absolute_uri(),
},
status=404
)
return JsonResponse(data, status=404)
def options(self, request, *args, **kwargs):
"""Return 404"""
return self.not_found(request)
def get(self, request, *args, **kwargs):
"""Return 404"""
return self.not_found(request)
def post(self, request, *args, **kwargs):
"""Return 404"""
return self.not_found(request)
def patch(self, request, *args, **kwargs):
"""Return 404"""
return self.not_found(request)
def put(self, request, *args, **kwargs):
"""Return 404"""
return self.not_found(request)
def delete(self, request, *args, **kwargs):
"""Return 404"""
return self.not_found(request)
class BulkDeleteMixin:
@@ -306,47 +331,6 @@ class APISearchView(APIView):
return Response(results)
class StatusView(APIView):
"""Generic API endpoint for discovering information on 'status codes' for a particular model.
This class should be implemented as a subclass for each type of status.
For example, the API endpoint /stock/status/ will have information about
all available 'StockStatus' codes
"""
permission_classes = [
permissions.IsAuthenticated,
]
# Override status_class for implementing subclass
MODEL_REF = 'statusmodel'
def get_status_model(self, *args, **kwargs):
"""Return the StatusCode moedl based on extra parameters passed to the view"""
status_model = self.kwargs.get(self.MODEL_REF, None)
if status_model is None:
raise ValidationError(f"StatusView view called without '{self.MODEL_REF}' parameter")
return status_model
def get(self, request, *args, **kwargs):
"""Perform a GET request to learn information about status codes"""
status_class = self.get_status_model()
if not status_class:
raise NotImplementedError("status_class not defined for this endpoint")
data = {
'class': status_class.__name__,
'values': status_class.dict(),
}
return Response(data)
class MetadataView(RetrieveUpdateAPI):
"""Generic API endpoint for reading and editing metadata for a model"""

View File

@@ -2,11 +2,79 @@
# InvenTree API version
INVENTREE_API_VERSION = 107
INVENTREE_API_VERSION = 127
"""
Increment this API version number whenever there is a significant change to the API that any clients need to know about
v127 -> 2023-06-24 : https://github.com/inventree/InvenTree/pull/5094
- Enhancements for the PartParameter API endpoints
v126 -> 2023-06-19 : https://github.com/inventree/InvenTree/pull/5075
- Adds API endpoint for setting the "category" for multiple parts simultaneously
v125 -> 2023-06-17 : https://github.com/inventree/InvenTree/pull/5064
- Adds API endpoint for setting the "status" field for multiple stock items simultaneously
v124 -> 2023-06-17 : https://github.com/inventree/InvenTree/pull/5057
- Add "created_before" and "created_after" filters to the Part API
v123 -> 2023-06-15 : https://github.com/inventree/InvenTree/pull/5019
- Add Metadata to: Plugin Config
v122 -> 2023-06-14 : https://github.com/inventree/InvenTree/pull/5034
- Adds new BuildLineLabel label type
v121 -> 2023-06-14 : https://github.com/inventree/InvenTree/pull/4808
- Adds "ProjectCode" link to Build model
v120 -> 2023-06-07 : https://github.com/inventree/InvenTree/pull/4855
- Major overhaul of the build order API
- Adds new BuildLine model
v119 -> 2023-06-01 : https://github.com/inventree/InvenTree/pull/4898
- Add Metadata to: Part test templates, Part parameters, Part category parameter templates, BOM item substitute, Related Parts, Stock item test result
v118 -> 2023-06-01 : https://github.com/inventree/InvenTree/pull/4935
- Adds extra fields for the PartParameterTemplate model
v117 -> 2023-05-22 : https://github.com/inventree/InvenTree/pull/4854
- Part.units model now supports physical units (e.g. "kg", "m", "mm", etc)
- Replaces SupplierPart "pack_size" field with "pack_quantity"
- New field supports physical units, and allows for conversion between compatible units
v116 -> 2023-05-18 : https://github.com/inventree/InvenTree/pull/4823
- Updates to part parameter implementation, to use physical units
v115 - > 2023-05-18 : https://github.com/inventree/InvenTree/pull/4846
- Adds ability to partially scrap a build output
v114 -> 2023-05-16 : https://github.com/inventree/InvenTree/pull/4825
- Adds "delivery_date" to shipments
>>>>>>> inventree/master
v113 -> 2023-05-13 : https://github.com/inventree/InvenTree/pull/4800
- Adds API endpoints for scrapping a build output
v112 -> 2023-05-13: https://github.com/inventree/InvenTree/pull/4741
- Adds flag use_pack_size to the stock addition API, which allows addings packs
v111 -> 2023-05-02 : https://github.com/inventree/InvenTree/pull/4367
- Adds tags to the Part serializer
- Adds tags to the SupplierPart serializer
- Adds tags to the ManufacturerPart serializer
- Adds tags to the StockItem serializer
- Adds tags to the StockLocation serializer
v110 -> 2023-04-26 : https://github.com/inventree/InvenTree/pull/4698
- Adds 'order_currency' field for PurchaseOrder / SalesOrder endpoints
v109 -> 2023-04-19 : https://github.com/inventree/InvenTree/pull/4636
- Adds API endpoints for the "ProjectCode" model
v108 -> 2023-04-17 : https://github.com/inventree/InvenTree/pull/4615
- Adds functionality to upload images for rendering in markdown notes
v107 -> 2023-04-04 : https://github.com/inventree/InvenTree/pull/4575
- Adds barcode support for PurchaseOrder model
- Adds barcode support for ReturnOrder model
@@ -167,7 +235,7 @@ v64 -> 2022-07-08 : https://github.com/inventree/InvenTree/pull/3310
- Allow BOM List API endpoint to be filtered by "on_order" parameter
v63 -> 2022-07-06 : https://github.com/inventree/InvenTree/pull/3301
- Allow BOM List API endpoint to be filtered by "available_stock" paramater
- Allow BOM List API endpoint to be filtered by "available_stock" parameter
v62 -> 2022-07-05 : https://github.com/inventree/InvenTree/pull/3296
- Allows search on BOM List API endpoint

View File

@@ -11,6 +11,7 @@ from django.core.exceptions import AppRegistryNotReady
from django.db import transaction
from django.db.utils import IntegrityError
import InvenTree.conversion
import InvenTree.tasks
from InvenTree.config import get_setting
from InvenTree.ready import canAppAccessDatabase, isInTestMode
@@ -29,8 +30,8 @@ class InvenTreeConfig(AppConfig):
- Checking if migrations should be run
- Cleaning up tasks
- Starting regular tasks
- Updateing exchange rates
- Collecting notification mehods
- Updating exchange rates
- Collecting notification methods
- Adding users set in the current environment
"""
if canAppAccessDatabase() or settings.TESTING_ENV:
@@ -46,6 +47,9 @@ class InvenTreeConfig(AppConfig):
self.collect_notification_methods()
# Ensure the unit registry is loaded
InvenTree.conversion.get_unit_registry()
if canAppAccessDatabase() or settings.TESTING_ENV:
self.add_user_on_startup()
@@ -80,7 +84,7 @@ class InvenTreeConfig(AppConfig):
minutes=task.minutes,
)
# Put at least one task onto the backround worker stack,
# Put at least one task onto the background worker stack,
# which will be processed as soon as the worker comes online
InvenTree.tasks.offload_task(
InvenTree.tasks.heartbeat,
@@ -122,19 +126,22 @@ class InvenTreeConfig(AppConfig):
update = False
try:
backend = ExchangeBackend.objects.get(name='InvenTreeExchange')
backend = ExchangeBackend.objects.filter(name='InvenTreeExchange')
last_update = backend.last_update
if backend.exists():
backend = backend.first()
if last_update is None:
# Never been updated
logger.info("Exchange backend has never been updated")
update = True
last_update = backend.last_update
# Backend currency has changed?
if base_currency != backend.base_currency:
logger.info(f"Base currency changed from {backend.base_currency} to {base_currency}")
update = True
if last_update is None:
# Never been updated
logger.info("Exchange backend has never been updated")
update = True
# Backend currency has changed?
if base_currency != backend.base_currency:
logger.info(f"Base currency changed from {backend.base_currency} to {base_currency}")
update = True
except (ExchangeBackend.DoesNotExist):
logger.info("Exchange backend not found - updating")
@@ -148,7 +155,7 @@ class InvenTreeConfig(AppConfig):
try:
update_exchange_rates()
except Exception as e:
logger.error(f"Error updating exchange rates: {e}")
logger.error(f"Error updating exchange rates: {e} ({type(e)})")
def add_user_on_startup(self):
"""Add a user on startup."""
@@ -188,8 +195,8 @@ class InvenTreeConfig(AppConfig):
else:
new_user = user.objects.create_superuser(add_user, add_email, add_password)
logger.info(f'User {str(new_user)} was created!')
except IntegrityError as _e:
logger.warning(f'The user "{add_user}" could not be created due to the following error:\n{str(_e)}')
except IntegrityError:
logger.warning(f'The user "{add_user}" could not be created')
# do not try again
settings.USER_ADDED = True

View File

@@ -1,12 +1,12 @@
"""Pull rendered copies of the templated.
Only used for testing the js files! - This file is omited from coverage.
Only used for testing the js files! - This file is omitted from coverage.
"""
import os # pragma: no cover
import pathlib # pragma: no cover
from InvenTree.helpers import InvenTreeTestCase # pragma: no cover
from InvenTree.unit_test import InvenTreeTestCase # pragma: no cover
class RenderJavascriptFiles(InvenTreeTestCase): # pragma: no cover
@@ -17,7 +17,7 @@ class RenderJavascriptFiles(InvenTreeTestCase): # pragma: no cover
"""
def download_file(self, filename, prefix):
"""Function to `download`(copy) a file to a temporay firectory."""
"""Function to `download`(copy) a file to a temporary firectory."""
url = os.path.join(prefix, filename)
response = self.client.get(url)

View File

@@ -200,7 +200,7 @@ def get_setting(env_var=None, config_key=None, default_value=None, typecast=None
def get_boolean_setting(env_var=None, config_key=None, default_value=False):
"""Helper function for retreiving a boolean configuration setting"""
"""Helper function for retrieving a boolean configuration setting"""
return is_true(get_setting(env_var, config_key, default_value))

View File

@@ -2,11 +2,10 @@
"""Provides extra global data to all templates."""
import InvenTree.email
import InvenTree.status
from InvenTree.status_codes import (BuildStatus, PurchaseOrderStatus,
ReturnOrderLineStatus, ReturnOrderStatus,
SalesOrderStatus, StockHistoryCode,
StockStatus)
from generic.states import StatusCode
from InvenTree.helpers import inheritors
from users.models import RuleSet, check_user_role
@@ -28,7 +27,7 @@ def health_status(request):
status = {
'django_q_running': InvenTree.status.is_worker_running(),
'email_configured': InvenTree.status.is_email_configured(),
'email_configured': InvenTree.email.is_email_configured(),
}
# The following keys are required to denote system health
@@ -56,17 +55,7 @@ def status_codes(request):
return {}
request._inventree_status_codes = True
return {
# Expose the StatusCode classes to the templates
'ReturnOrderStatus': ReturnOrderStatus,
'ReturnOrderLineStatus': ReturnOrderLineStatus,
'SalesOrderStatus': SalesOrderStatus,
'PurchaseOrderStatus': PurchaseOrderStatus,
'BuildStatus': BuildStatus,
'StockStatus': StockStatus,
'StockHistoryCode': StockHistoryCode,
}
return {cls.__name__: cls.template_context() for cls in inheritors(StatusCode)}
def user_roles(request):

View File

@@ -0,0 +1,108 @@
"""Helper functions for converting between units."""
import logging
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
import pint
_unit_registry = None
logger = logging.getLogger('inventree')
def get_unit_registry():
"""Return a custom instance of the Pint UnitRegistry."""
global _unit_registry
# Cache the unit registry for speedier access
if _unit_registry is None:
reload_unit_registry()
return _unit_registry
def reload_unit_registry():
"""Reload the unit registry from the database.
This function is called at startup, and whenever the database is updated.
"""
import time
t_start = time.time()
global _unit_registry
_unit_registry = pint.UnitRegistry()
# Define some "standard" additional units
_unit_registry.define('piece = 1')
_unit_registry.define('each = 1 = ea')
_unit_registry.define('dozen = 12 = dz')
_unit_registry.define('hundred = 100')
_unit_registry.define('thousand = 1000')
# TODO: Allow for custom units to be defined in the database
dt = time.time() - t_start
logger.debug(f'Loaded unit registry in {dt:.3f}s')
def convert_physical_value(value: str, unit: str = None):
"""Validate that the provided value is a valid physical quantity.
Arguments:
value: Value to validate (str)
unit: Optional unit to convert to, and validate against
Raises:
ValidationError: If the value is invalid or cannot be converted to the specified unit
Returns:
The converted quantity, in the specified units
"""
# Ensure that the value is a string
value = str(value).strip()
# Error on blank values
if not value:
raise ValidationError(_('No value provided'))
ureg = get_unit_registry()
error = ''
try:
# Convert to a quantity
val = ureg.Quantity(value)
if unit:
if val.units == ureg.dimensionless:
# If the provided value is dimensionless, assume that the unit is correct
val = ureg.Quantity(value, unit)
else:
# Convert to the provided unit (may raise an exception)
val = val.to(unit)
# At this point we *should* have a valid pint value
# To double check, look at the maginitude
float(val.magnitude)
except (TypeError, ValueError, AttributeError):
error = _('Provided value is not a valid number')
except (pint.errors.UndefinedUnitError, pint.errors.DefinitionSyntaxError):
error = _('Provided value has an invalid unit')
except pint.errors.DimensionalityError:
error = _('Provided value could not be converted to the specified unit')
if error:
if unit:
error += f' ({unit})'
raise ValidationError(error)
# Return the converted value
return val

View File

@@ -0,0 +1,90 @@
"""Code for managing email functionality in InvenTree."""
import logging
from django.conf import settings
from django.core import mail as django_mail
import InvenTree.ready
import InvenTree.tasks
logger = logging.getLogger('inventree')
def is_email_configured():
"""Check if email backend is configured.
NOTE: This does not check if the configuration is valid!
"""
configured = True
testing = settings.TESTING
if InvenTree.ready.isInTestMode():
return False
if InvenTree.ready.isImportingData():
return False
if not settings.EMAIL_HOST:
configured = False
# Display warning unless in test mode
if not testing: # pragma: no cover
logger.debug("EMAIL_HOST is not configured")
# Display warning unless in test mode
if not settings.EMAIL_HOST_USER and not testing: # pragma: no cover
logger.debug("EMAIL_HOST_USER is not configured")
# Display warning unless in test mode
if not settings.EMAIL_HOST_PASSWORD and testing: # pragma: no cover
logger.debug("EMAIL_HOST_PASSWORD is not configured")
# Email sender must be configured
if not settings.DEFAULT_FROM_EMAIL:
configured = False
if not testing: # pragma: no cover
logger.warning("DEFAULT_FROM_EMAIL is not configured")
return configured
def send_email(subject, body, recipients, from_email=None, html_message=None):
"""Send an email with the specified subject and body, to the specified recipients list."""
if type(recipients) == str:
recipients = [recipients]
import InvenTree.ready
import InvenTree.status
if InvenTree.ready.isImportingData():
# If we are importing data, don't send emails
return
if not InvenTree.email.is_email_configured() and not settings.TESTING:
# Email is not configured / enabled
return
# If a *from_email* is not specified, ensure that the default is set
if not from_email:
from_email = settings.DEFAULT_FROM_EMAIL
# If we still don't have a valid from_email, then we can't send emails
if not from_email:
if settings.TESTING:
from_email = 'from@test.com'
else:
logger.error("send_email failed: DEFAULT_FROM_EMAIL not specified")
return
InvenTree.tasks.offload_task(
django_mail.send_mail,
subject,
body,
from_email,
recipients,
fail_silently=False,
html_message=html_message
)

View File

@@ -18,6 +18,8 @@ from rest_framework import serializers
from rest_framework.exceptions import ValidationError as DRFValidationError
from rest_framework.response import Response
import InvenTree.sentry
logger = logging.getLogger('inventree')
@@ -32,7 +34,7 @@ def log_error(path):
kind, info, data = sys.exc_info()
# Check if the eror is on the ignore list
# Check if the error is on the ignore list
if kind in settings.IGNORED_ERRORS:
return
@@ -61,18 +63,12 @@ def exception_handler(exc, context):
"""
response = None
if settings.SENTRY_ENABLED and settings.SENTRY_DSN and not settings.DEBUG:
# Report this exception to sentry.io
from sentry_sdk import capture_exception
# The following types of errors are ignored, they are "expected"
do_not_report = [
DjangoValidationError,
DRFValidationError,
]
if not any([isinstance(exc, err) for err in do_not_report]):
capture_exception(exc)
# Pass exception to sentry.io handler
try:
InvenTree.sentry.report_exception(exc)
except Exception:
# If sentry.io fails, we don't want to crash the server!
pass
# Catch any django validation error, and re-throw a DRF validation error
if isinstance(exc, DjangoValidationError):

View File

@@ -1,12 +1,11 @@
"""Exchangerate backend to use `exchangerate.host` to get rates."""
"""Exchangerate backend to use `frankfurter.app` to get rates."""
import ssl
from decimal import Decimal
from urllib.error import URLError
from urllib.request import urlopen
from django.db.utils import OperationalError
import certifi
import requests
from djmoney.contrib.exchange.backends.base import SimpleExchangeBackend
from common.settings import currency_code_default, currency_codes
@@ -15,19 +14,19 @@ from common.settings import currency_code_default, currency_codes
class InvenTreeExchange(SimpleExchangeBackend):
"""Backend for automatically updating currency exchange rates.
Uses the `exchangerate.host` service API
Uses the `frankfurter.app` service API
"""
name = "InvenTreeExchange"
def __init__(self):
"""Set API url."""
self.url = "https://api.exchangerate.host/latest"
self.url = "https://api.frankfurter.app/latest"
super().__init__()
def get_params(self):
"""Placeholder to set API key. Currently not required by `exchangerate.host`."""
"""Placeholder to set API key. Currently not required by `frankfurter.app`."""
# No API key is required
return {
}
@@ -40,14 +39,22 @@ class InvenTreeExchange(SimpleExchangeBackend):
url = self.get_url(**kwargs)
try:
context = ssl.create_default_context(cafile=certifi.where())
response = urlopen(url, timeout=5, context=context)
return response.read()
response = requests.get(url=url, timeout=5)
return response.content
except Exception:
# Something has gone wrong, but we can just try again next time
# Raise a TypeError so the outer function can handle this
raise TypeError
def get_rates(self, **params):
"""Intersect the requested currency codes with the available codes."""
rates = super().get_rates(**params)
# Add the base currency to the rates
rates[params["base_currency"]] = Decimal("1.0")
return rates
def update_rates(self, base_currency=None):
"""Set the requested currency codes and get rates."""
# Set default - see B008

View File

@@ -4,7 +4,7 @@ import sys
from decimal import Decimal
from django import forms
from django.db import models as models
from django.db import models
from django.utils.translation import gettext_lazy as _
from djmoney.forms.fields import MoneyField
@@ -19,6 +19,7 @@ from .validators import AllowedURLValidator, allowable_url_schemes
class InvenTreeRestURLField(RestURLField):
"""Custom field for DRF with custom scheme vaildators."""
def __init__(self, **kwargs):
"""Update schemes."""
@@ -111,6 +112,7 @@ class InvenTreeModelMoneyField(ModelMoneyField):
class InvenTreeMoneyField(MoneyField):
"""Custom MoneyField for clean migrations while using dynamic currency settings."""
def __init__(self, *args, **kwargs):
"""Override initial values with the real info from database."""
kwargs.update(money_kwargs())

View File

@@ -3,7 +3,7 @@
from django_filters import rest_framework as rest_filters
from rest_framework import filters
from InvenTree.helpers import str2bool
import InvenTree.helpers
class InvenTreeSearchFilter(filters.SearchFilter):
@@ -13,10 +13,10 @@ class InvenTreeSearchFilter(filters.SearchFilter):
"""Return a set of search fields for the request, adjusted based on request params.
The following query params are available to 'augment' the search (in decreasing order of priority)
- search_regex: If True, search is perfomed on 'regex' comparison
- search_regex: If True, search is performed on 'regex' comparison
"""
regex = str2bool(request.query_params.get('search_regex', False))
regex = InvenTree.helpers.str2bool(request.query_params.get('search_regex', False))
search_fields = super().get_search_fields(view, request)
@@ -37,7 +37,7 @@ class InvenTreeSearchFilter(filters.SearchFilter):
Depending on the request parameters, we may "augment" these somewhat
"""
whole = str2bool(request.query_params.get('search_whole', False))
whole = InvenTree.helpers.str2bool(request.query_params.get('search_whole', False))
terms = []

View File

@@ -91,7 +91,7 @@ def construct_format_regex(fmt_string: str) -> str:
# Add a named capture group for the format entry
if name:
# Check if integer values are requried
# Check if integer values are required
if format.endswith('d'):
chr = '\d'
else:

View File

@@ -12,7 +12,7 @@ from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from allauth.account.adapter import DefaultAccountAdapter
from allauth.account.forms import SignupForm, set_form_field_order
from allauth.account.forms import LoginForm, SignupForm, set_form_field_order
from allauth.exceptions import ImmediateHttpResponse
from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
from allauth_2fa.adapter import OTPAdapter
@@ -21,6 +21,8 @@ from crispy_forms.bootstrap import (AppendedText, PrependedAppendedText,
PrependedText)
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Field, Layout
from dj_rest_auth.registration.serializers import RegisterSerializer
from rest_framework import serializers
from common.models import InvenTreeSetting
from InvenTree.exceptions import log_error
@@ -159,11 +161,30 @@ class SetPasswordForm(HelperForm):
old_password = forms.CharField(
label=_("Old password"),
strip=False,
required=False,
widget=forms.PasswordInput(attrs={'autocomplete': 'current-password', 'autofocus': True}),
)
# override allauth
class CustomLoginForm(LoginForm):
"""Custom login form to override default allauth behaviour"""
def login(self, request, redirect_url=None):
"""Perform login action.
First check that:
- A valid user has been supplied
"""
if not self.user:
# No user supplied - redirect to the login page
return HttpResponseRedirect(reverse('account_login'))
# Now perform default login action
return super().login(request, redirect_url)
class CustomSignupForm(SignupForm):
"""Override to use dynamic settings."""
@@ -206,15 +227,20 @@ class CustomSignupForm(SignupForm):
return cleaned_data
def registration_enabled():
"""Determine whether user registration is enabled."""
return settings.EMAIL_HOST and (InvenTreeSetting.get_setting('LOGIN_ENABLE_REG') or InvenTreeSetting.get_setting('LOGIN_ENABLE_SSO_REG'))
class RegistratonMixin:
"""Mixin to check if registration should be enabled."""
def is_open_for_signup(self, request, *args, **kwargs):
"""Check if signup is enabled in settings.
Configure the class variable `REGISTRATION_SETTING` to set which setting should be used, defualt: `LOGIN_ENABLE_REG`.
Configure the class variable `REGISTRATION_SETTING` to set which setting should be used, default: `LOGIN_ENABLE_REG`.
"""
if settings.EMAIL_HOST and (InvenTreeSetting.get_setting('LOGIN_ENABLE_REG') or InvenTreeSetting.get_setting('LOGIN_ENABLE_SSO_REG')):
if registration_enabled():
return super().is_open_for_signup(request, *args, **kwargs)
return False
@@ -253,7 +279,7 @@ class RegistratonMixin:
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)
logger.error('The setting `SIGNUP_GROUP` contains an non existent group', start_group)
user.save()
return user
@@ -276,7 +302,7 @@ class CustomAccountAdapter(CustomUrlMixin, RegistratonMixin, OTPAdapter, Default
try:
result = super().send_mail(template_prefix, email, context)
except Exception:
# An exception ocurred while attempting to send email
# An exception occurred while attempting to send email
# Log it (for admin users) and return silently
log_error('account email')
result = False
@@ -285,6 +311,15 @@ class CustomAccountAdapter(CustomUrlMixin, RegistratonMixin, OTPAdapter, Default
return False
def get_email_confirmation_url(self, request, emailconfirmation):
"""Construct the email confirmation url"""
from InvenTree.helpers_model import construct_absolute_url
url = super().get_email_confirmation_url(request, emailconfirmation)
url = construct_absolute_url(url)
return url
class CustomSocialAccountAdapter(CustomUrlMixin, RegistratonMixin, DefaultSocialAccountAdapter):
"""Override of adapter to use dynamic settings."""
@@ -319,3 +354,20 @@ class CustomSocialAccountAdapter(CustomUrlMixin, RegistratonMixin, DefaultSocial
# Otherwise defer to the original allauth adapter.
return super().login(request, user)
# override dj-rest-auth
class CustomRegisterSerializer(RegisterSerializer):
"""Override of serializer to use dynamic settings."""
email = serializers.EmailField()
def __init__(self, instance=None, data=..., **kwargs):
"""Check settings to influence which fields are needed."""
kwargs['email_required'] = InvenTreeSetting.get_setting('LOGIN_MAIL_REQUIRED')
super().__init__(instance, data, **kwargs)
def save(self, request):
"""Override to check if registration is open."""
if registration_enabled():
return super().save(request)
raise forms.ValidationError(_('Registration is disabled.'))

View File

@@ -8,44 +8,28 @@ import os
import os.path
import re
from decimal import Decimal, InvalidOperation
from pathlib import Path
from wsgiref.util import FileWrapper
from django.conf import settings
from django.contrib.auth.models import Permission
from django.contrib.staticfiles.storage import StaticFilesStorage
from django.core.exceptions import FieldError, ValidationError
from django.core.files.storage import default_storage
from django.core.validators import URLValidator
from django.http import StreamingHttpResponse
from django.test import TestCase
from django.utils.translation import gettext_lazy as _
import moneyed.localization
import regex
import requests
from bleach import clean
from djmoney.contrib.exchange.models import convert_money
from djmoney.money import Money
from PIL import Image
import InvenTree.version
from common.models import InvenTreeSetting
from common.notifications import (InvenTreeNotificationBodies,
NotificationBody, trigger_notification)
from common.settings import currency_code_default
from .api_tester import ExchangeRateMixin, UserMixin
from .settings import MEDIA_URL, STATIC_URL
logger = logging.getLogger('inventree')
def getSetting(key, backup_value=None):
"""Shortcut for reading a setting value from the database."""
return InvenTreeSetting.get_setting(key, backup_value=backup_value)
def generateTestKey(test_name):
"""Generate a test 'key' for a given test name. This must not have illegal chars as it will be used for dict lookup in a template.
@@ -88,130 +72,6 @@ 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 download_image_from_url(remote_url, timeout=2.5):
"""Download an image file from a remote URL.
This is a potentially dangerous operation, so we must perform some checks:
- The remote URL is available
- The Content-Length is provided, and is not too large
- The file is a valid image file
Arguments:
remote_url: The remote URL to retrieve image
max_size: Maximum allowed image size (default = 1MB)
timeout: Connection timeout in seconds (default = 5)
Returns:
An in-memory PIL image file, if the download was successful
Raises:
requests.exceptions.ConnectionError: Connection could not be established
requests.exceptions.Timeout: Connection timed out
requests.exceptions.HTTPError: Server responded with invalid response code
ValueError: Server responded with invalid 'Content-Length' value
TypeError: Response is not a valid image
"""
# Check that the provided URL at least looks valid
validator = URLValidator()
validator(remote_url)
# Calculate maximum allowable image size (in bytes)
max_size = int(InvenTreeSetting.get_setting('INVENTREE_DOWNLOAD_IMAGE_MAX_SIZE')) * 1024 * 1024
# Add user specified user-agent to request (if specified)
user_agent = InvenTreeSetting.get_setting('INVENTREE_DOWNLOAD_FROM_URL_USER_AGENT')
if user_agent:
headers = {"User-Agent": user_agent}
else:
headers = None
try:
response = requests.get(
remote_url,
timeout=timeout,
allow_redirects=True,
stream=True,
headers=headers,
)
# Throw an error if anything goes wrong
response.raise_for_status()
except requests.exceptions.ConnectionError as exc:
raise Exception(_("Connection error") + f": {str(exc)}")
except requests.exceptions.Timeout as exc:
raise exc
except requests.exceptions.HTTPError:
raise requests.exceptions.HTTPError(_("Server responded with invalid status code") + f": {response.status_code}")
except Exception as exc:
raise Exception(_("Exception occurred") + f": {str(exc)}")
if response.status_code != 200:
raise Exception(_("Server responded with invalid status code") + f": {response.status_code}")
try:
content_length = int(response.headers.get('Content-Length', 0))
except ValueError:
raise ValueError(_("Server responded with invalid Content-Length value"))
if content_length > max_size:
raise ValueError(_("Image size is too large"))
# Download the file, ensuring we do not exceed the reported size
fo = io.BytesIO()
dl_size = 0
chunk_size = 64 * 1024
for chunk in response.iter_content(chunk_size=chunk_size):
dl_size += len(chunk)
if dl_size > max_size:
raise ValueError(_("Image download exceeded maximum size"))
fo.write(chunk)
if dl_size == 0:
raise ValueError(_("Remote server returned empty response"))
# Now, attempt to convert the downloaded data to a valid image file
# img.verify() will throw an exception if the image is not valid
try:
img = Image.open(fo).convert()
img.verify()
except Exception:
raise TypeError(_("Supplied URL is not a valid image file"))
return img
def TestIfImage(img):
"""Test if an image file is indeed an image."""
try:
@@ -632,7 +492,7 @@ def extract_serial_numbers(input_string, expected_quantity: int, starting_value=
serial = serial.strip()
# Ignore blank / emtpy serials
# Ignore blank / empty serials
if len(serial) == 0:
return
@@ -823,75 +683,6 @@ def validateFilterString(value, model=None):
return results
def addUserPermission(user, permission):
"""Shortcut function for adding a certain permission to a user."""
perm = Permission.objects.get(codename=permission)
user.user_permissions.add(perm)
def addUserPermissions(user, permissions):
"""Shortcut function for adding multiple permissions to a user."""
for permission in permissions:
addUserPermission(user, permission)
def getMigrationFileNames(app):
"""Return a list of all migration filenames for provided app."""
local_dir = Path(__file__).parent
files = local_dir.joinpath('..', app, 'migrations').iterdir()
# Regex pattern for migration files
regex = re.compile(r"^[\d]+_.*\.py$")
migration_files = []
for f in files:
if regex.match(f.name):
migration_files.append(f.name)
return migration_files
def getOldestMigrationFile(app, exclude_extension=True, ignore_initial=True):
"""Return the filename associated with the oldest migration."""
oldest_num = -1
oldest_file = None
for f in getMigrationFileNames(app):
if ignore_initial and f.startswith('0001_initial'):
continue
num = int(f.split('_')[0])
if oldest_file is None or num < oldest_num:
oldest_num = num
oldest_file = f
if exclude_extension:
oldest_file = oldest_file.replace('.py', '')
return oldest_file
def getNewestMigrationFile(app, exclude_extension=True):
"""Return the filename associated with the newest migration."""
newest_file = None
newest_num = -1
for f in getMigrationFileNames(app):
num = int(f.split('_')[0])
if newest_file is None or num > newest_num:
newest_num = num
newest_file = f
if exclude_extension:
newest_file = newest_file.replace('.py', '')
return newest_file
def clean_decimal(number):
"""Clean-up decimal value."""
# Check if empty
@@ -1062,102 +853,3 @@ def inheritors(cls):
subcls.add(child)
work.append(child)
return subcls
class InvenTreeTestCase(ExchangeRateMixin, UserMixin, TestCase):
"""Testcase with user setup buildin."""
pass
def notify_responsible(instance, sender, content: NotificationBody = InvenTreeNotificationBodies.NewOrder, exclude=None):
"""Notify all responsible parties of a change in an instance.
Parses the supplied content with the provided instance and sender and sends a notification to all responsible users,
excluding the optional excluded list.
Args:
instance: The newly created instance
sender: Sender model reference
content (NotificationBody, optional): _description_. Defaults to InvenTreeNotificationBodies.NewOrder.
exclude (User, optional): User instance that should be excluded. Defaults to None.
"""
if instance.responsible is not None:
# Setup context for notification parsing
content_context = {
'instance': str(instance),
'verbose_name': sender._meta.verbose_name,
'app_label': sender._meta.app_label,
'model_name': sender._meta.model_name,
}
# Setup notification context
context = {
'instance': instance,
'name': content.name.format(**content_context),
'message': content.message.format(**content_context),
'link': InvenTree.helpers.construct_absolute_url(instance.get_absolute_url()),
'template': {
'html': content.template.format(**content_context),
'subject': content.name.format(**content_context),
}
}
# Create notification
trigger_notification(
instance,
content.slug.format(**content_context),
targets=[instance.responsible],
target_exclude=[exclude],
context=context,
)
def render_currency(money, decimal_places=None, currency=None, include_symbol=True, min_decimal_places=None):
"""Render a currency / Money object to a formatted string (e.g. for reports)
Arguments:
money: The Money instance to be rendered
decimal_places: The number of decimal places to render to. If unspecified, uses the PRICING_DECIMAL_PLACES setting.
currency: Optionally convert to the specified currency
include_symbol: Render with the appropriate currency symbol
min_decimal_places: The minimum number of decimal places to render to. If unspecified, uses the PRICING_DECIMAL_PLACES_MIN setting.
"""
if money in [None, '']:
return '-'
if type(money) is not Money:
return '-'
if currency is not None:
# Attempt to convert to the provided currency
# If cannot be done, leave the original
try:
money = convert_money(money, currency)
except Exception:
pass
if decimal_places is None:
decimal_places = InvenTreeSetting.get_setting('PRICING_DECIMAL_PLACES', 6)
if min_decimal_places is None:
min_decimal_places = InvenTreeSetting.get_setting('PRICING_DECIMAL_PLACES_MIN', 0)
value = Decimal(str(money.amount)).normalize()
value = str(value)
if '.' in value:
decimals = len(value.split('.')[-1])
decimals = max(decimals, min_decimal_places)
decimals = min(decimals, decimal_places)
decimal_places = decimals
else:
decimal_places = max(decimal_places, 2)
return moneyed.localization.format_money(
money,
decimal_places=decimal_places,
include_symbol=include_symbol,
)

View File

@@ -0,0 +1,302 @@
"""Provides helper functions used throughout the InvenTree project that access the database."""
import io
import logging
from decimal import Decimal
from urllib.parse import urljoin
from django.conf import settings
from django.core.validators import URLValidator
from django.db.utils import OperationalError, ProgrammingError
from django.utils.translation import gettext_lazy as _
import moneyed.localization
import requests
from djmoney.contrib.exchange.models import convert_money
from djmoney.money import Money
from PIL import Image
import common.models
import InvenTree
import InvenTree.helpers_model
import InvenTree.version
from common.notifications import (InvenTreeNotificationBodies,
NotificationBody, trigger_notification)
logger = logging.getLogger('inventree')
def getSetting(key, backup_value=None):
"""Shortcut for reading a setting value from the database."""
return common.models.InvenTreeSetting.get_setting(key, backup_value=backup_value)
def construct_absolute_url(*arg, **kwargs):
"""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.
A URL is constructed in the following order:
1. If settings.SITE_URL is set (e.g. in the Django settings), use that
2. If the InvenTree setting INVENTREE_BASE_URL is set, use that
3. Otherwise, use the current request URL (if available)
"""
relative_url = '/'.join(arg)
# If a site URL is provided, use that
site_url = getattr(settings, 'SITE_URL', None)
if not site_url:
# Otherwise, try to use the InvenTree setting
try:
site_url = common.models.InvenTreeSetting.get_setting('INVENTREE_BASE_URL', create=False, cache=False)
except (ProgrammingError, OperationalError):
pass
if not site_url:
# Otherwise, try to use the current request
request = kwargs.get('request', None)
if request:
site_url = request.build_absolute_uri('/')
if not site_url:
# No site URL available, return the relative URL
return relative_url
return urljoin(site_url, relative_url)
def get_base_url(**kwargs):
"""Return the base URL for the InvenTree server"""
return construct_absolute_url('', **kwargs)
def download_image_from_url(remote_url, timeout=2.5):
"""Download an image file from a remote URL.
This is a potentially dangerous operation, so we must perform some checks:
- The remote URL is available
- The Content-Length is provided, and is not too large
- The file is a valid image file
Arguments:
remote_url: The remote URL to retrieve image
max_size: Maximum allowed image size (default = 1MB)
timeout: Connection timeout in seconds (default = 5)
Returns:
An in-memory PIL image file, if the download was successful
Raises:
requests.exceptions.ConnectionError: Connection could not be established
requests.exceptions.Timeout: Connection timed out
requests.exceptions.HTTPError: Server responded with invalid response code
ValueError: Server responded with invalid 'Content-Length' value
TypeError: Response is not a valid image
"""
# Check that the provided URL at least looks valid
validator = URLValidator()
validator(remote_url)
# Calculate maximum allowable image size (in bytes)
max_size = int(common.models.InvenTreeSetting.get_setting('INVENTREE_DOWNLOAD_IMAGE_MAX_SIZE')) * 1024 * 1024
# Add user specified user-agent to request (if specified)
user_agent = common.models.InvenTreeSetting.get_setting('INVENTREE_DOWNLOAD_FROM_URL_USER_AGENT')
if user_agent:
headers = {"User-Agent": user_agent}
else:
headers = None
try:
response = requests.get(
remote_url,
timeout=timeout,
allow_redirects=True,
stream=True,
headers=headers,
)
# Throw an error if anything goes wrong
response.raise_for_status()
except requests.exceptions.ConnectionError as exc:
raise Exception(_("Connection error") + f": {str(exc)}")
except requests.exceptions.Timeout as exc:
raise exc
except requests.exceptions.HTTPError:
raise requests.exceptions.HTTPError(_("Server responded with invalid status code") + f": {response.status_code}")
except Exception as exc:
raise Exception(_("Exception occurred") + f": {str(exc)}")
if response.status_code != 200:
raise Exception(_("Server responded with invalid status code") + f": {response.status_code}")
try:
content_length = int(response.headers.get('Content-Length', 0))
except ValueError:
raise ValueError(_("Server responded with invalid Content-Length value"))
if content_length > max_size:
raise ValueError(_("Image size is too large"))
# Download the file, ensuring we do not exceed the reported size
file = io.BytesIO()
dl_size = 0
chunk_size = 64 * 1024
for chunk in response.iter_content(chunk_size=chunk_size):
dl_size += len(chunk)
if dl_size > max_size:
raise ValueError(_("Image download exceeded maximum size"))
file.write(chunk)
if dl_size == 0:
raise ValueError(_("Remote server returned empty response"))
# Now, attempt to convert the downloaded data to a valid image file
# img.verify() will throw an exception if the image is not valid
try:
img = Image.open(file).convert()
img.verify()
except Exception:
raise TypeError(_("Supplied URL is not a valid image file"))
return img
def render_currency(money, decimal_places=None, currency=None, include_symbol=True, min_decimal_places=None, max_decimal_places=None):
"""Render a currency / Money object to a formatted string (e.g. for reports)
Arguments:
money: The Money instance to be rendered
decimal_places: The number of decimal places to render to. If unspecified, uses the PRICING_DECIMAL_PLACES setting.
currency: Optionally convert to the specified currency
include_symbol: Render with the appropriate currency symbol
min_decimal_places: The minimum number of decimal places to render to. If unspecified, uses the PRICING_DECIMAL_PLACES_MIN setting.
max_decimal_places: The maximum number of decimal places to render to. If unspecified, uses the PRICING_DECIMAL_PLACES setting.
"""
if money in [None, '']:
return '-'
if type(money) is not Money:
return '-'
if currency is not None:
# Attempt to convert to the provided currency
# If cannot be done, leave the original
try:
money = convert_money(money, currency)
except Exception:
pass
if decimal_places is None:
decimal_places = common.models.InvenTreeSetting.get_setting('PRICING_DECIMAL_PLACES', 6)
if min_decimal_places is None:
min_decimal_places = common.models.InvenTreeSetting.get_setting('PRICING_DECIMAL_PLACES_MIN', 0)
if max_decimal_places is None:
max_decimal_places = common.models.InvenTreeSetting.get_setting('PRICING_DECIMAL_PLACES', 6)
value = Decimal(str(money.amount)).normalize()
value = str(value)
if '.' in value:
decimals = len(value.split('.')[-1])
decimals = max(decimals, min_decimal_places)
decimals = min(decimals, decimal_places)
decimal_places = decimals
else:
decimal_places = max(decimal_places, 2)
decimal_places = max(decimal_places, max_decimal_places)
return moneyed.localization.format_money(
money,
decimal_places=decimal_places,
include_symbol=include_symbol,
)
def getModelsWithMixin(mixin_class) -> list:
"""Return a list of models that inherit from the given mixin class.
Args:
mixin_class: The mixin class to search for
Returns:
List of models that inherit from the given mixin class
"""
from django.contrib.contenttypes.models import ContentType
db_models = [x.model_class() for x in ContentType.objects.all() if x is not None]
return [x for x in db_models if x is not None and issubclass(x, mixin_class)]
def notify_responsible(instance, sender, content: NotificationBody = InvenTreeNotificationBodies.NewOrder, exclude=None):
"""Notify all responsible parties of a change in an instance.
Parses the supplied content with the provided instance and sender and sends a notification to all responsible users,
excluding the optional excluded list.
Args:
instance: The newly created instance
sender: Sender model reference
content (NotificationBody, optional): _description_. Defaults to InvenTreeNotificationBodies.NewOrder.
exclude (User, optional): User instance that should be excluded. Defaults to None.
"""
notify_users([instance.responsible], instance, sender, content=content, exclude=exclude)
def notify_users(users, instance, sender, content: NotificationBody = InvenTreeNotificationBodies.NewOrder, exclude=None):
"""Notify all passed users or groups.
Parses the supplied content with the provided instance and sender and sends a notification to all users,
excluding the optional excluded list.
Args:
users: List of users or groups to notify
instance: The newly created instance
sender: Sender model reference
content (NotificationBody, optional): _description_. Defaults to InvenTreeNotificationBodies.NewOrder.
exclude (User, optional): User instance that should be excluded. Defaults to None.
"""
# Setup context for notification parsing
content_context = {
'instance': str(instance),
'verbose_name': sender._meta.verbose_name,
'app_label': sender._meta.app_label,
'model_name': sender._meta.model_name,
}
# Setup notification context
context = {
'instance': instance,
'name': content.name.format(**content_context),
'message': content.message.format(**content_context),
'link': InvenTree.helpers_model.construct_absolute_url(instance.get_absolute_url()),
'template': {
'subject': content.name.format(**content_context),
}
}
if content.template:
context['template']['html'] = content.template.format(**content_context)
# Create notification
trigger_notification(
instance,
content.slug.format(**content_context),
targets=users,
target_exclude=[exclude],
context=context,
)

View File

@@ -23,8 +23,8 @@ def render_file(file_name, source, target, locales, ctx):
with open(target_file, 'w') as localised_file:
with lang_over(locale):
renderd = render_to_string(os.path.join(source, file_name), ctx)
localised_file.write(renderd)
rendered = render_to_string(os.path.join(source, file_name), ctx)
localised_file.write(rendered)
class Command(BaseCommand):

View File

@@ -18,7 +18,7 @@ class Command(BaseCommand):
while not connected:
time.sleep(5)
time.sleep(2)
try:
connection.ensure_connection()
@@ -34,4 +34,4 @@ class Command(BaseCommand):
self.stdout.write("Database configuration is not usable")
if connected:
self.stdout.write("Database connection sucessful!")
self.stdout.write("Database connection successful!")

View File

@@ -28,7 +28,7 @@ class InvenTreeMetadata(SimpleMetadata):
"""
def determine_metadata(self, request, view):
"""Overwrite the metadata to adapt to hte request user."""
"""Overwrite the metadata to adapt to the request user."""
self.request = request
self.view = view
@@ -36,7 +36,7 @@ class InvenTreeMetadata(SimpleMetadata):
"""
Custom context information to pass through to the OPTIONS endpoint,
if the "context=True" is supplied to the OPTIONS requst
if the "context=True" is supplied to the OPTIONS request
Serializer class can supply context data by defining a get_context_data() method (no arguments)
"""

View File

@@ -14,7 +14,6 @@ from allauth_2fa.middleware import (AllauthTwoFactorMiddleware,
from error_report.middleware import ExceptionProcessor
from rest_framework.authtoken.models import Token
from common.models import InvenTreeSetting
from InvenTree.urls import frontendpatterns
logger = logging.getLogger("inventree")
@@ -102,7 +101,7 @@ class AuthRequiredMiddleware(object):
'/static/',
]
if path not in urls and not any([path.startswith(p) for p in paths_ignore]):
if path not in urls and not any(path.startswith(p) for p in paths_ignore):
# Save the 'next' parameter to pass through to the login view
return redirect(f'{reverse_lazy("account_login")}?next={request.path}')
@@ -123,6 +122,9 @@ class Check2FAMiddleware(BaseRequire2FAMiddleware):
"""Check if user is required to have MFA enabled."""
def require_2fa(self, request):
"""Use setting to check if MFA should be enforced for frontend page."""
from common.models import InvenTreeSetting
try:
if url_matcher.resolve(request.path[1:]):
return InvenTreeSetting.get_setting('LOGIN_ENFORCE_MFA')
@@ -158,11 +160,38 @@ class InvenTreeExceptionProcessor(ExceptionProcessor):
"""Custom exception processor that respects blocked errors."""
def process_exception(self, request, exception):
"""Check if kind is ignored before procesing."""
"""Check if kind is ignored before processing."""
kind, info, data = sys.exc_info()
# Check if the eror is on the ignore list
# Check if the error is on the ignore list
if kind in settings.IGNORED_ERRORS:
return
return super().process_exception(request, exception)
import traceback
from django.views.debug import ExceptionReporter
from error_report.models import Error
from error_report.settings import ERROR_DETAIL_SETTINGS
# Error reporting is disabled
if not ERROR_DETAIL_SETTINGS.get('ERROR_DETAIL_ENABLE', True):
return
path = request.build_absolute_uri()
# Truncate the path to a reasonable length
# Otherwise we get a database error,
# because the path field is limited to 200 characters
if len(path) > 200:
path = path[:195] + '...'
error = Error.objects.create(
kind=kind.__name__,
html=ExceptionReporter(request, kind, info, data).get_traceback_html(),
path=path,
info=info,
data='\n'.join(traceback.format_exception(kind, info, data)),
)
error.save()

View File

@@ -125,7 +125,7 @@ class CreateAPI(CleanMixin, generics.CreateAPIView):
class RetrieveAPI(generics.RetrieveAPIView):
"""View for retreive API."""
"""View for retrieve API."""
pass

View File

@@ -21,10 +21,10 @@ from error_report.models import Error
from mptt.exceptions import InvalidMove
from mptt.models import MPTTModel, TreeForeignKey
import InvenTree.fields
import InvenTree.format
import InvenTree.helpers
from common.models import InvenTreeSetting
from InvenTree.fields import InvenTreeURLField
import InvenTree.helpers_model
from InvenTree.sanitizer import sanitize_svg
logger = logging.getLogger('inventree')
@@ -44,13 +44,88 @@ def rename_attachment(instance, filename):
return os.path.join(instance.getSubdir(), filename)
class MetadataMixin(models.Model):
"""Model mixin class which adds a JSON metadata field to a model, for use by any (and all) plugins.
The intent of this mixin is to provide a metadata field on a model instance,
for plugins to read / modify as required, to store any extra information.
The assumptions for models implementing this mixin are:
- The internal InvenTree business logic will make no use of this field
- Multiple plugins may read / write to this metadata field, and not assume they have sole rights
"""
class Meta:
"""Meta for MetadataMixin."""
abstract = True
def save(self, *args, **kwargs):
"""Save the model instance, and perform validation on the metadata field."""
self.validate_metadata()
super().save(*args, **kwargs)
def clean(self, *args, **kwargs):
"""Perform model validation on the metadata field."""
super().clean()
self.validate_metadata()
def validate_metadata(self):
"""Validate the metadata field."""
# Ensure that the 'metadata' field is a valid dict object
if self.metadata is None:
self.metadata = {}
if type(self.metadata) is not dict:
raise ValidationError({'metadata': _('Metadata must be a python dict object')})
metadata = models.JSONField(
blank=True, null=True,
verbose_name=_('Plugin Metadata'),
help_text=_('JSON metadata field, for use by external plugins'),
)
def get_metadata(self, key: str, backup_value=None):
"""Finds metadata for this model instance, using the provided key for lookup.
Args:
key: String key for requesting metadata. e.g. if a plugin is accessing the metadata, the plugin slug should be used
Returns:
Python dict object containing requested metadata. If no matching metadata is found, returns None
"""
if self.metadata is None:
return backup_value
return self.metadata.get(key, backup_value)
def set_metadata(self, key: str, data, commit: bool = True, overwrite: bool = False):
"""Save the provided metadata under the provided key.
Args:
key (str): Key for saving metadata
data (Any): Data object to save - must be able to be rendered as a JSON string
commit (bool, optional): If true, existing metadata with the provided key will be overwritten. If false, a merge will be attempted. Defaults to True.
overwrite (bool): If true, delete existing metadata before adding new value
"""
if overwrite or self.metadata is None:
self.metadata = {}
self.metadata[key] = data
if commit:
self.save()
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
# Define a map of fields available for import
IMPORT_FIELDS = {}
@classmethod
@@ -132,6 +207,8 @@ class ReferenceIndexingMixin(models.Model):
if cls.REFERENCE_PATTERN_SETTING is None:
return ''
# import at function level to prevent cyclic imports
from common.models import InvenTreeSetting
return InvenTreeSetting.get_setting(cls.REFERENCE_PATTERN_SETTING, create=False).strip()
@classmethod
@@ -411,7 +488,7 @@ class InvenTreeAttachment(models.Model):
blank=True, null=True
)
link = InvenTreeURLField(
link = InvenTree.fields.InvenTreeURLField(
blank=True, null=True,
verbose_name=_('Link'),
help_text=_('Link to external URL')
@@ -635,12 +712,12 @@ class InvenTreeTree(MPTTModel):
available = contents.get_all_objects_for_this_type()
# List of child IDs
childs = self.getUniqueChildren()
children = self.getUniqueChildren()
acceptable = [None]
for a in available:
if a.id not in childs:
if a.id not in children:
acceptable.append(a)
return acceptable
@@ -652,7 +729,7 @@ class InvenTreeTree(MPTTModel):
Returns:
List of category names from the top level to the parent of this category
"""
return [a for a in self.get_ancestors()]
return list(self.get_ancestors())
@property
def path(self):
@@ -670,6 +747,27 @@ class InvenTreeTree(MPTTModel):
return "{path} - {desc}".format(path=self.pathstring, desc=self.description)
class InvenTreeNotesMixin(models.Model):
"""A mixin class for adding notes functionality to a model class.
The following fields are added to any model which implements this mixin:
- notes : A text field for storing notes
"""
class Meta:
"""Metaclass options for this mixin.
Note: abstract must be true, as this is only a mixin, not a separate table
"""
abstract = True
notes = InvenTree.fields.InvenTreeNotesField(
verbose_name=_('Notes'),
help_text=_('Markdown notes (optional)'),
)
class InvenTreeBarcodeMixin(models.Model):
"""A mixin class for adding barcode functionality to a model class.
@@ -793,7 +891,7 @@ def after_error_logged(sender, instance: Error, created: bool, **kwargs):
users = get_user_model().objects.filter(is_staff=True)
link = InvenTree.helpers.construct_absolute_url(
link = InvenTree.helpers_model.construct_absolute_url(
reverse('admin:error_report_error_change', kwargs={'object_id': instance.pk})
)
@@ -809,7 +907,7 @@ def after_error_logged(sender, instance: Error, created: bool, **kwargs):
'inventree.error_log',
context=context,
targets=users,
delivery_methods=set([common.notifications.UIMessageNotification]),
delivery_methods={common.notifications.UIMessageNotification, },
)
except Exception as exc:

View File

@@ -92,6 +92,14 @@ class IsSuperuser(permissions.IsAdminUser):
return bool(request.user and request.user.is_superuser)
class IsStaffOrReadOnly(permissions.IsAdminUser):
"""Allows read-only access to any user, but write access is restricted to staff users."""
def has_permission(self, request, view):
"""Check if the user is a superuser."""
return bool(request.user and request.user.is_staff or request.method in permissions.SAFE_METHODS)
def auth_exempt(view_func):
"""Mark a view function as being exempt from auth requirements."""
def wrapped_view(*args, **kwargs):

View File

@@ -13,6 +13,11 @@ def isImportingData():
return 'loaddata' in sys.argv
def isRunningMigrations():
"""Return True if the database is currently running migrations."""
return 'migrate' in sys.argv or 'makemigrations' in sys.argv
def canAppAccessDatabase(allow_test: bool = False, allow_plugins: bool = False, allow_shell: bool = False):
"""Returns True if the apps.py file can access database records.

View File

@@ -43,7 +43,7 @@ ALLOWED_ATTRIBUTES_SVG = [
]
def sanitize_svg(file_data: str, strip: bool = True, elements: str = ALLOWED_ELEMENTS_SVG, attributes: str = ALLOWED_ATTRIBUTES_SVG) -> str:
def sanitize_svg(file_data, strip: bool = True, elements: str = ALLOWED_ELEMENTS_SVG, attributes: str = ALLOWED_ATTRIBUTES_SVG) -> str:
"""Sanatize a SVG file.
Args:
@@ -56,6 +56,10 @@ def sanitize_svg(file_data: str, strip: bool = True, elements: str = ALLOWED_ELE
str: Sanitzied SVG file.
"""
# Handle byte-encoded data
if type(file_data) == bytes:
file_data = file_data.decode('utf-8')
cleaned = clean(
file_data,
tags=elements,
@@ -64,4 +68,5 @@ def sanitize_svg(file_data: str, strip: bool = True, elements: str = ALLOWED_ELE
strip_comments=strip,
css_sanitizer=CSSSanitizer()
)
return cleaned

View File

@@ -0,0 +1,68 @@
"""Configuration for Sentry.io error reporting."""
import logging
from django.conf import settings
from django.core.exceptions import ValidationError
from django.http import Http404
import rest_framework.exceptions
import sentry_sdk
from sentry_sdk.integrations.django import DjangoIntegration
from InvenTree.version import INVENTREE_SW_VERSION
logger = logging.getLogger('inventree')
def default_sentry_dsn():
"""Return the default Sentry.io DSN for InvenTree"""
return 'https://3928ccdba1d34895abde28031fd00100@o378676.ingest.sentry.io/6494600'
def sentry_ignore_errors():
"""Return a list of error types to ignore.
These error types will *not* be reported to sentry.io.
"""
return [
Http404,
ValidationError,
rest_framework.exceptions.AuthenticationFailed,
rest_framework.exceptions.PermissionDenied,
rest_framework.exceptions.ValidationError,
]
def init_sentry(dsn, sample_rate, tags):
"""Initialize sentry.io error reporting"""
logger.info("Initializing sentry.io integration")
sentry_sdk.init(
dsn=dsn,
integrations=[DjangoIntegration()],
traces_sample_rate=sample_rate,
send_default_pii=True,
ignore_errors=sentry_ignore_errors(),
release=INVENTREE_SW_VERSION,
)
for key, val in tags.items():
sentry_sdk.set_tag(f'inventree_{key}', val)
def report_exception(exc):
"""Report an exception to sentry.io"""
if settings.SENTRY_ENABLED and settings.SENTRY_DSN:
if not any(isinstance(exc, e) for e in sentry_ignore_errors()):
logger.info(f"Reporting exception to sentry.io: {exc}")
try:
sentry_sdk.capture_exception(exc)
except Exception:
logger.warning("Failed to report exception to sentry.io")

View File

@@ -19,11 +19,12 @@ from rest_framework.exceptions import ValidationError
from rest_framework.fields import empty
from rest_framework.serializers import DecimalField
from rest_framework.utils import model_meta
from taggit.serializers import TaggitSerializer
from common.models import InvenTreeSetting
import common.models as common_models
from common.settings import currency_code_default, currency_code_mappings
from InvenTree.fields import InvenTreeRestURLField, InvenTreeURLField
from InvenTree.helpers import download_image_from_url
from InvenTree.helpers_model import download_image_from_url
class InvenTreeMoneySerializer(MoneyField):
@@ -33,7 +34,7 @@ class InvenTreeMoneySerializer(MoneyField):
"""
def __init__(self, *args, **kwargs):
"""Overrite default values."""
"""Override default values."""
kwargs["max_digits"] = kwargs.get("max_digits", 19)
self.decimal_places = kwargs["decimal_places"] = kwargs.get("decimal_places", 6)
kwargs["required"] = kwargs.get("required", False)
@@ -73,10 +74,17 @@ class InvenTreeCurrencySerializer(serializers.ChoiceField):
def __init__(self, *args, **kwargs):
"""Initialize the currency serializer"""
kwargs['choices'] = currency_code_mappings()
choices = currency_code_mappings()
allow_blank = kwargs.get('allow_blank', False) or kwargs.get('allow_null', False)
if allow_blank:
choices = [('', '---------')] + choices
kwargs['choices'] = choices
if 'default' not in kwargs and 'required' not in kwargs:
kwargs['default'] = currency_code_default
kwargs['default'] = '' if allow_blank else currency_code_default
if 'label' not in kwargs:
kwargs['label'] = _('Currency')
@@ -257,6 +265,28 @@ class InvenTreeModelSerializer(serializers.ModelSerializer):
return data
class InvenTreeTaggitSerializer(TaggitSerializer):
"""Updated from https://github.com/glemmaPaul/django-taggit-serializer."""
def update(self, instance, validated_data):
"""Overridden update method to re-add the tagmanager."""
to_be_tagged, validated_data = self._pop_tags(validated_data)
tag_object = super().update(instance, validated_data)
for key in to_be_tagged.keys():
# re-add the tagmanager
new_tagobject = tag_object.__class__.objects.get(id=tag_object.id)
setattr(tag_object, key, getattr(new_tagobject, key))
return self._save_tags(tag_object, to_be_tagged)
class InvenTreeTagModelSerializer(InvenTreeTaggitSerializer, InvenTreeModelSerializer):
"""Combination of InvenTreeTaggitSerializer and InvenTreeModelSerializer."""
pass
class UserSerializer(InvenTreeModelSerializer):
"""Serializer for a User."""
@@ -492,7 +522,7 @@ class DataFileUploadSerializer(serializers.Serializer):
pass
# Extract a list of valid model field names
model_field_names = [key for key in model_fields.keys()]
model_field_names = list(model_fields.keys())
# Provide a dict of available columns from the dataset
file_columns = {}
@@ -694,7 +724,7 @@ class RemoteImageMixin(metaclass=serializers.SerializerMetaclass):
if not url:
return
if not InvenTreeSetting.get_setting('INVENTREE_DOWNLOAD_FROM_URL'):
if not common_models.InvenTreeSetting.get_setting('INVENTREE_DOWNLOAD_FROM_URL'):
raise ValidationError(_("Downloading images from remote URL is not enabled"))
try:

View File

@@ -17,15 +17,18 @@ from pathlib import Path
import django.conf.locale
import django.core.exceptions
from django.core.validators import URLValidator
from django.http import Http404
from django.utils.translation import gettext_lazy as _
import moneyed
import sentry_sdk
from sentry_sdk.integrations.django import DjangoIntegration
from dotenv import load_dotenv
from InvenTree.config import get_boolean_setting, get_custom_file, get_setting
from InvenTree.sentry import default_sentry_dsn, init_sentry
from InvenTree.version import inventreeApiVersion
from . import config
from .config import get_boolean_setting, get_custom_file, get_setting
INVENTREE_NEWS_URL = 'https://inventree.org/news/feed.atom'
@@ -63,6 +66,12 @@ BASE_DIR = config.get_base_dir()
# Load configuration data
CONFIG = config.load_config_data(set_cache=True)
# Load VERSION data if it exists
version_file = BASE_DIR.parent.joinpath('VERSION')
if version_file.exists():
print('load version from file')
load_dotenv(version_file)
# Default action is to run the system in Debug mode
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = get_boolean_setting('INVENTREE_DEBUG', 'debug', True)
@@ -194,6 +203,7 @@ INSTALLED_APPS = [
'stock.apps.StockConfig',
'users.apps.UsersConfig',
'plugin.apps.PluginAppConfig',
'generic',
'InvenTree.apps.InvenTreeConfig', # InvenTree app runs last
# Core django modules
@@ -223,6 +233,8 @@ INSTALLED_APPS = [
'django_q',
'formtools', # Form wizard tools
'dbbackup', # Backups - django-dbbackup
'taggit', # Tagging
'flags', # Flagging - django-flags
'allauth', # Base app for SSO
'allauth.account', # Extend user with accounts
@@ -233,6 +245,9 @@ INSTALLED_APPS = [
'django_otp.plugins.otp_static', # Backup codes
'allauth_2fa', # MFA flow for allauth
'dj_rest_auth', # Authentication APIs - dj-rest-auth
'dj_rest_auth.registration', # Registration APIs - dj-rest-auth'
'drf_spectacular', # API documentation
'django_ical', # For exporting calendars
]
@@ -326,7 +341,7 @@ TEMPLATES = [
'InvenTree.context.user_roles',
],
'loaders': [(
'django.template.loaders.cached.Loader', [
'InvenTree.template.InvenTreeTemplateLoader', [
'plugin.template.PluginTemplateLoader',
'django.template.loaders.filesystem.Loader',
'django.template.loaders.app_directories.Loader',
@@ -356,7 +371,7 @@ REST_FRAMEWORK = {
'rest_framework.permissions.DjangoModelPermissions',
'InvenTree.permissions.RolePermission',
),
'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema',
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
'DEFAULT_METADATA_CLASS': 'InvenTree.metadata.InvenTreeMetadata',
'DEFAULT_RENDERER_CLASSES': [
'rest_framework.renderers.JSONRenderer',
@@ -367,6 +382,32 @@ if DEBUG:
# Enable browsable API if in DEBUG mode
REST_FRAMEWORK['DEFAULT_RENDERER_CLASSES'].append('rest_framework.renderers.BrowsableAPIRenderer')
# dj-rest-auth
# JWT switch
USE_JWT = get_boolean_setting('INVENTREE_USE_JWT', 'use_jwt', False)
REST_USE_JWT = USE_JWT
OLD_PASSWORD_FIELD_ENABLED = True
REST_AUTH_REGISTER_SERIALIZERS = {'REGISTER_SERIALIZER': 'InvenTree.forms.CustomRegisterSerializer'}
# JWT settings - rest_framework_simplejwt
if USE_JWT:
JWT_AUTH_COOKIE = 'inventree-auth'
JWT_AUTH_REFRESH_COOKIE = 'inventree-token'
REST_FRAMEWORK['DEFAULT_AUTHENTICATION_CLASSES'] + (
'dj_rest_auth.jwt_auth.JWTCookieAuthentication',
)
INSTALLED_APPS.append('rest_framework_simplejwt')
# WSGI default setting
SPECTACULAR_SETTINGS = {
'TITLE': 'InvenTree API',
'DESCRIPTION': 'API for InvenTree - the intuitive open source inventory management system',
'LICENSE': {'MIT': 'https://github.com/inventree/InvenTree/blob/master/LICENSE'},
'EXTERNAL_DOCS': {'docs': 'https://docs.inventree.org', 'web': 'https://inventree.org'},
'VERSION': inventreeApiVersion(),
'SERVE_INCLUDE_SCHEMA': False,
}
WSGI_APPLICATION = 'InvenTree.wsgi.application'
"""
@@ -467,7 +508,7 @@ if "postgres" in db_engine: # pragma: no cover
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
# # seconds, 2 is minimum allowed by libpq
db_options["connect_timeout"] = int(
get_setting('INVENTREE_DB_TIMEOUT', 'database.timeout', 2)
)
@@ -560,31 +601,25 @@ DATABASES = {
REMOTE_LOGIN = get_boolean_setting('INVENTREE_REMOTE_LOGIN', 'remote_login_enabled', False)
REMOTE_LOGIN_HEADER = get_setting('INVENTREE_REMOTE_LOGIN_HEADER', 'remote_login_header', 'REMOTE_USER')
LOGIN_REDIRECT_URL = "/index/"
# sentry.io integration for error reporting
SENTRY_ENABLED = get_boolean_setting('INVENTREE_SENTRY_ENABLED', 'sentry_enabled', False)
# Default Sentry DSN (can be overriden if user wants custom sentry integration)
INVENTREE_DSN = 'https://3928ccdba1d34895abde28031fd00100@o378676.ingest.sentry.io/6494600'
SENTRY_DSN = get_setting('INVENTREE_SENTRY_DSN', 'sentry_dsn', INVENTREE_DSN)
# Default Sentry DSN (can be overridden if user wants custom sentry integration)
SENTRY_DSN = get_setting('INVENTREE_SENTRY_DSN', 'sentry_dsn', default_sentry_dsn())
SENTRY_SAMPLE_RATE = float(get_setting('INVENTREE_SENTRY_SAMPLE_RATE', 'sentry_sample_rate', 0.1))
if SENTRY_ENABLED and SENTRY_DSN: # pragma: no cover
logger.info("Running with sentry.io integration enabled")
sentry_sdk.init(
dsn=SENTRY_DSN,
integrations=[DjangoIntegration(), ],
traces_sample_rate=1.0 if DEBUG else SENTRY_SAMPLE_RATE,
send_default_pii=True
)
inventree_tags = {
'testing': TESTING,
'docker': DOCKER,
'debug': DEBUG,
'remote': REMOTE_LOGIN,
}
for key, val in inventree_tags.items():
sentry_sdk.set_tag(f'inventree_{key}', val)
init_sentry(SENTRY_DSN, SENTRY_SAMPLE_RATE, inventree_tags)
# Cache configuration
cache_host = get_setting('INVENTREE_CACHE_HOST', 'cache.host', None)
@@ -593,7 +628,7 @@ cache_port = get_setting('INVENTREE_CACHE_PORT', 'cache.port', '6379', typecast=
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.
# irreplaceable.
_cache_options = {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
"SOCKET_CONNECT_TIMEOUT": int(os.getenv("CACHE_CONNECT_TIMEOUT", "2")),
@@ -713,6 +748,7 @@ LANGUAGES = [
('es', _('Spanish')),
('es-mx', _('Spanish (Mexican)')),
('fa', _('Farsi / Persian')),
('fi', _('Finnish')),
('fr', _('French')),
('he', _('Hebrew')),
('hu', _('Hungarian')),
@@ -723,14 +759,14 @@ LANGUAGES = [
('no', _('Norwegian')),
('pl', _('Polish')),
('pt', _('Portuguese')),
('pt-BR', _('Portuguese (Brazilian)')),
('pt-br', _('Portuguese (Brazilian)')),
('ru', _('Russian')),
('sl', _('Slovenian')),
('sv', _('Swedish')),
('th', _('Thai')),
('tr', _('Turkish')),
('vi', _('Vietnamese')),
('zh-hans', _('Chinese')),
('zh-hans', _('Chinese (Simplified)')),
]
# Testing interface translations
@@ -759,6 +795,11 @@ CURRENCIES = get_setting(
typecast=list,
)
# Ensure that at least one currency value is available
if len(CURRENCIES) == 0: # pragma: no cover
logger.warning("No currencies selected: Defaulting to USD")
CURRENCIES = ['USD']
# Maximum number of decimal places for currency rendering
CURRENCY_DECIMAL_PLACES = 6
@@ -783,6 +824,10 @@ EMAIL_USE_SSL = get_boolean_setting('INVENTREE_EMAIL_SSL', 'email.ssl', False)
DEFAULT_FROM_EMAIL = get_setting('INVENTREE_EMAIL_SENDER', 'email.sender', '')
# If "from" email not specified, default to the username
if not DEFAULT_FROM_EMAIL:
DEFAULT_FROM_EMAIL = get_setting('INVENTREE_EMAIL_USERNAME', 'email.username', '')
EMAIL_USE_LOCALTIME = False
EMAIL_TIMEOUT = 60
@@ -832,7 +877,7 @@ ACCOUNT_PREVENT_ENUMERATION = True
# override forms / adapters
ACCOUNT_FORMS = {
'login': 'allauth.account.forms.LoginForm',
'login': 'InvenTree.forms.CustomLoginForm',
'signup': 'InvenTree.forms.CustomSignupForm',
'add_email': 'allauth.account.forms.AddEmailForm',
'change_password': 'allauth.account.forms.ChangePasswordForm',
@@ -899,12 +944,22 @@ PLUGINS_ENABLED = get_boolean_setting('INVENTREE_PLUGINS_ENABLED', 'plugins_enab
PLUGIN_FILE = config.get_plugin_file()
# Plugin test settings
PLUGIN_TESTING = get_setting('INVENTREE_PLUGIN_TESTING', 'PLUGIN_TESTING', TESTING) # Are plugins beeing tested?
PLUGIN_TESTING = get_setting('INVENTREE_PLUGIN_TESTING', 'PLUGIN_TESTING', TESTING) # Are plugins being tested?
PLUGIN_TESTING_SETUP = get_setting('INVENTREE_PLUGIN_TESTING_SETUP', 'PLUGIN_TESTING_SETUP', False) # Load plugins from setup hooks in testing?
PLUGIN_TESTING_EVENTS = False # Flag if events are tested right now
PLUGIN_RETRY = get_setting('INVENTREE_PLUGIN_RETRY', 'PLUGIN_RETRY', 5) # How often should plugin loading be tried?
PLUGIN_FILE_CHECKED = False # Was the plugin file checked?
# Site URL can be specified statically, or via a run-time setting
SITE_URL = get_setting('INVENTREE_SITE_URL', 'site_url', None)
if SITE_URL:
logger.info(f"Site URL: {SITE_URL}")
# Check that the site URL is valid
validator = URLValidator()
validator(SITE_URL)
# User interface customization values
CUSTOM_LOGO = get_custom_file('INVENTREE_CUSTOM_LOGO', 'customize.logo', 'custom logo', lookup_media=True)
CUSTOM_SPLASH = get_custom_file('INVENTREE_CUSTOM_SPLASH', 'customize.splash', 'custom splash')
@@ -915,3 +970,23 @@ if DEBUG:
logger.info(f"MEDIA_ROOT: '{MEDIA_ROOT}'")
logger.info(f"STATIC_ROOT: '{STATIC_ROOT}'")
# Flags
FLAGS = {
'EXPERIMENTAL': [
{'condition': 'boolean', 'value': DEBUG},
{'condition': 'parameter', 'value': 'experimental='},
], # Should experimental features be turned on?
'NEXT_GEN': [
{'condition': 'parameter', 'value': 'ngen='},
], # Should next-gen features be turned on?
}
# Get custom flags from environment/yaml
CUSTOM_FLAGS = get_setting('INVENTREE_FLAGS', 'flags', None, typecast=dict)
if CUSTOM_FLAGS:
if not isinstance(CUSTOM_FLAGS, dict):
logger.error(f"Invalid custom flags, must be valid dict: {CUSTOM_FLAGS}")
else:
logger.info(f"Custom flags: {CUSTOM_FLAGS}")
FLAGS.update(CUSTOM_FLAGS)

View File

@@ -0,0 +1,127 @@
"""API endpoints for social authentication with allauth."""
import logging
from importlib import import_module
from django.urls import include, path, reverse
from allauth.socialaccount import providers
from allauth.socialaccount.models import SocialApp
from allauth.socialaccount.providers.keycloak.views import \
KeycloakOAuth2Adapter
from allauth.socialaccount.providers.oauth2.views import (OAuth2Adapter,
OAuth2LoginView)
from rest_framework.generics import ListAPIView
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from common.models import InvenTreeSetting
logger = logging.getLogger('inventree')
class GenericOAuth2ApiLoginView(OAuth2LoginView):
"""Api view to login a user with a social account"""
def dispatch(self, request, *args, **kwargs):
"""Dispatch the regular login view directly."""
return self.login(request, *args, **kwargs)
class GenericOAuth2ApiConnectView(GenericOAuth2ApiLoginView):
"""Api view to connect a social account to the current user"""
def dispatch(self, request, *args, **kwargs):
"""Dispatch the connect request directly."""
# Override the request method be in connection mode
request.GET = request.GET.copy()
request.GET['process'] = 'connect'
# Resume the dispatch
return super().dispatch(request, *args, **kwargs)
def handle_oauth2(adapter: OAuth2Adapter):
"""Define urls for oauth2 endpoints."""
return [
path('login/', GenericOAuth2ApiLoginView.adapter_view(adapter), name=f'{provider.id}_api_login'),
path('connect/', GenericOAuth2ApiConnectView.adapter_view(adapter), name=f'{provider.id}_api_connect'),
]
def handle_keycloak():
"""Define urls for keycloak."""
return [
path('login/', GenericOAuth2ApiLoginView.adapter_view(KeycloakOAuth2Adapter), name='keycloak_api_login'),
path('connect/', GenericOAuth2ApiConnectView.adapter_view(KeycloakOAuth2Adapter), name='keycloak_api_connet'),
]
legacy = {
'twitter': 'twitter_oauth2',
'bitbucket': 'bitbucket_oauth2',
'linkedin': 'linkedin_oauth2',
'vimeo': 'vimeo_oauth2',
'openid': 'openid_connect',
} # legacy connectors
# Collect urls for all loaded providers
social_auth_urlpatterns = []
provider_urlpatterns = []
for provider in providers.registry.get_list():
try:
prov_mod = import_module(provider.get_package() + ".views")
except ImportError:
continue
# Try to extract the adapter class
adapters = [cls for cls in prov_mod.__dict__.values() if isinstance(cls, type) and not cls == OAuth2Adapter and issubclass(cls, OAuth2Adapter)]
# Get urls
urls = []
if len(adapters) == 1:
urls = handle_oauth2(adapter=adapters[0])
else:
if provider.id in legacy:
logger.warning(f'`{provider.id}` is not supported on platform UI. Use `{legacy[provider.id]}` instead.')
continue
elif provider.id == 'keycloak':
urls = handle_keycloak()
else:
logger.error(f'Found handler that is not yet ready for platform UI: `{provider.id}`. Open an feature request on GitHub if you need it implemented.')
continue
provider_urlpatterns += [path(f'{provider.id}/', include(urls))]
social_auth_urlpatterns += provider_urlpatterns
class SocialProvierListView(ListAPIView):
"""List of available social providers."""
permission_classes = (AllowAny,)
def get(self, request, *args, **kwargs):
"""Get the list of providers."""
provider_list = []
for provider in providers.registry.get_list():
provider_data = {
'id': provider.id,
'name': provider.name,
'login': request.build_absolute_uri(reverse(f'{provider.id}_api_login')),
'connect': request.build_absolute_uri(reverse(f'{provider.id}_api_connect')),
}
try:
provider_data['display_name'] = provider.get_app(request).name
except SocialApp.DoesNotExist:
provider_data['display_name'] = provider.name
provider_list.append(provider_data)
data = {
'sso_enabled': InvenTreeSetting.get_setting('LOGIN_ENABLE_SSO'),
'sso_registration': InvenTreeSetting.get_setting('LOGIN_ENABLE_SSO_REG'),
'mfa_required': InvenTreeSetting.get_setting('LOGIN_ENFORCE_MFA'),
'providers': provider_list
}
return Response(data)

View File

@@ -105,6 +105,10 @@ main {
font-size: 110%;
}
.bg-qr-code {
background-color: #FFF !important;
}
.qr-code {
max-width: 400px;
max-height: 400px;
@@ -219,8 +223,7 @@ main {
}
.sub-table {
margin-left: 45px;
margin-right: 45px;
margin-left: 60px;
}
.detail-icon .glyphicon {
@@ -266,10 +269,6 @@ main {
}
/* Styles for table buttons and filtering */
.button-toolbar .btn {
margin-left: 1px;
margin-right: 1px;
}
.filter-list {
display: inline-block;
@@ -301,16 +300,14 @@ main {
.filter-tag {
display: inline-block;
*display: inline;
zoom: 1;
padding-top: 3px;
padding-left: 3px;
padding-right: 3px;
border: 1px solid #aaa;
border-radius: 3px;
background: #eee;
margin: 1px;
margin-left: 5px;
margin-right: 5px;
margin: 5px;
padding: 5px;
padding-top: 1px;
padding-bottom: 1px;
color: var(--bs-body-color);
border: 1px solid var(--border-color);
border-radius: 10px;
background: var(--secondary-color);
white-space: nowrap;
}
@@ -321,7 +318,6 @@ main {
.filter-input {
display: inline-block;
*display: inline;
zoom: 1;
}
.filter-tag:hover {
@@ -1094,4 +1090,10 @@ a {
.sso-provider-link a {
width: 100%;
text-align: left;
}
}
.flex-cell {
display: flex;
align-items: center;
justify-content: space-between;
}

View File

@@ -4,13 +4,13 @@
import logging
from datetime import timedelta
from django.conf import settings
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from django_q.models import Success
from django_q.monitor import Stat
import InvenTree.email
import InvenTree.ready
logger = logging.getLogger("inventree")
@@ -38,38 +38,14 @@ def is_worker_running(**kwargs):
)
# If any results are returned, then the background worker is running!
return results.exists()
try:
result = results.exists()
except Exception:
# We may throw an exception if the database is not ready,
# or if the django_q table is not yet created (i.e. in CI testing)
result = False
def is_email_configured():
"""Check if email backend is configured.
NOTE: This does not check if the configuration is valid!
"""
configured = True
if InvenTree.ready.isInTestMode():
return False
if InvenTree.ready.isImportingData():
return False
if not settings.EMAIL_HOST:
configured = False
# Display warning unless in test mode
if not settings.TESTING: # pragma: no cover
logger.debug("EMAIL_HOST is not configured")
# Display warning unless in test mode
if not settings.TESTING: # pragma: no cover
logger.debug("EMAIL_HOST_USER is not configured")
# Display warning unless in test mode
if not settings.TESTING: # pragma: no cover
logger.debug("EMAIL_HOST_PASSWORD is not configured")
return configured
return result
def check_system_health(**kwargs):
@@ -91,7 +67,7 @@ def check_system_health(**kwargs):
result = False
logger.warning(_("Background worker check failed"))
if not is_email_configured(): # pragma: no cover
if not InvenTree.email.is_email_configured(): # pragma: no cover
result = False
logger.warning(_("Email backend not configured"))

View File

@@ -2,374 +2,161 @@
from django.utils.translation import gettext_lazy as _
class StatusCode:
"""Base class for representing a set of StatusCodes.
This is used to map a set of integer values to text.
"""
colors = {}
@classmethod
def render(cls, key, large=False):
"""Render the value as a HTML label."""
# If the key cannot be found, pass it back
if key not in cls.options.keys():
return key
value = cls.options.get(key, key)
color = cls.colors.get(key, 'secondary')
span_class = f'badge rounded-pill bg-{color}'
return "<span class='{cl}'>{value}</span>".format(
cl=span_class,
value=value
)
@classmethod
def list(cls):
"""Return the StatusCode options as a list of mapped key / value items."""
return list(cls.dict().values())
@classmethod
def text(cls, key):
"""Text for supplied status code."""
return cls.options.get(key, None)
@classmethod
def items(cls):
"""All status code items."""
return cls.options.items()
@classmethod
def keys(cls):
"""All status code keys."""
return cls.options.keys()
@classmethod
def labels(cls):
"""All status code labels."""
return cls.options.values()
@classmethod
def names(cls):
"""Return a map of all 'names' of status codes in this class
Will return a dict object, with the attribute name indexed to the integer value.
e.g.
{
'PENDING': 10,
'IN_PROGRESS': 20,
}
"""
keys = cls.keys()
status_names = {}
for d in dir(cls):
if d.startswith('_'):
continue
if d != d.upper():
continue
value = getattr(cls, d, None)
if value is None:
continue
if callable(value):
continue
if type(value) != int:
continue
if value not in keys:
continue
status_names[d] = value
return status_names
@classmethod
def dict(cls):
"""Return a dict representation containing all required information"""
values = {}
for name, value, in cls.names().items():
entry = {
'key': value,
'name': name,
'label': cls.label(value),
}
if hasattr(cls, 'colors'):
if color := cls.colors.get(value, None):
entry['color'] = color
values[name] = entry
return values
@classmethod
def label(cls, value):
"""Return the status code label associated with the provided value."""
return cls.options.get(value, value)
@classmethod
def value(cls, label):
"""Return the value associated with the provided label."""
for k in cls.options.keys():
if cls.options[k].lower() == label.lower():
return k
raise ValueError("Label not found")
from generic.states import StatusCode
class PurchaseOrderStatus(StatusCode):
"""Defines a set of status codes for a PurchaseOrder."""
# Order status codes
PENDING = 10 # Order is pending (not yet placed)
PLACED = 20 # Order has been placed with supplier
COMPLETE = 30 # Order has been completed
CANCELLED = 40 # Order was cancelled
LOST = 50 # Order was lost
RETURNED = 60 # Order was returned
PENDING = 10, _("Pending"), 'secondary' # Order is pending (not yet placed)
PLACED = 20, _("Placed"), 'primary' # Order has been placed with supplier
COMPLETE = 30, _("Complete"), 'success' # Order has been completed
CANCELLED = 40, _("Cancelled"), 'danger' # Order was cancelled
LOST = 50, _("Lost"), 'warning' # Order was lost
RETURNED = 60, _("Returned"), 'warning' # Order was returned
options = {
PENDING: _("Pending"),
PLACED: _("Placed"),
COMPLETE: _("Complete"),
CANCELLED: _("Cancelled"),
LOST: _("Lost"),
RETURNED: _("Returned"),
}
colors = {
PENDING: 'secondary',
PLACED: 'primary',
COMPLETE: 'success',
CANCELLED: 'danger',
LOST: 'warning',
RETURNED: 'warning',
}
class PurchaseOrderStatusGroups:
"""Groups for PurchaseOrderStatus codes."""
# Open orders
OPEN = [
PENDING,
PLACED,
PurchaseOrderStatus.PENDING.value,
PurchaseOrderStatus.PLACED.value,
]
# Failed orders
FAILED = [
CANCELLED,
LOST,
RETURNED
PurchaseOrderStatus.CANCELLED.value,
PurchaseOrderStatus.LOST.value,
PurchaseOrderStatus.RETURNED.value
]
class SalesOrderStatus(StatusCode):
"""Defines a set of status codes for a SalesOrder."""
PENDING = 10 # Order is pending
IN_PROGRESS = 15 # Order has been issued, and is in progress
SHIPPED = 20 # Order has been shipped to customer
CANCELLED = 40 # Order has been cancelled
LOST = 50 # Order was lost
RETURNED = 60 # Order was returned
PENDING = 10, _("Pending"), 'secondary' # Order is pending
IN_PROGRESS = 15, _("In Progress"), 'primary' # Order has been issued, and is in progress
SHIPPED = 20, _("Shipped"), 'success' # Order has been shipped to customer
CANCELLED = 40, _("Cancelled"), 'danger' # Order has been cancelled
LOST = 50, _("Lost"), 'warning' # Order was lost
RETURNED = 60, _("Returned"), 'warning' # Order was returned
options = {
PENDING: _("Pending"),
IN_PROGRESS: _("In Progress"),
SHIPPED: _("Shipped"),
CANCELLED: _("Cancelled"),
LOST: _("Lost"),
RETURNED: _("Returned"),
}
colors = {
PENDING: 'secondary',
IN_PROGRESS: 'primary',
SHIPPED: 'success',
CANCELLED: 'danger',
LOST: 'warning',
RETURNED: 'warning',
}
class SalesOrderStatusGroups:
"""Groups for SalesOrderStatus codes."""
# Open orders
OPEN = [
PENDING,
IN_PROGRESS,
SalesOrderStatus.PENDING.value,
SalesOrderStatus.IN_PROGRESS.value,
]
# Completed orders
COMPLETE = [
SHIPPED,
SalesOrderStatus.SHIPPED.value,
]
class StockStatus(StatusCode):
"""Status codes for Stock."""
OK = 10 # Item is OK
ATTENTION = 50 # Item requires attention
DAMAGED = 55 # Item is damaged
DESTROYED = 60 # Item is destroyed
REJECTED = 65 # Item is rejected
LOST = 70 # Item has been lost
QUARANTINED = 75 # Item has been quarantined and is unavailable
RETURNED = 85 # Item has been returned from a customer
OK = 10, _("OK"), 'success' # Item is OK
ATTENTION = 50, _("Attention needed"), 'warning' # Item requires attention
DAMAGED = 55, _("Damaged"), 'warning' # Item is damaged
DESTROYED = 60, _("Destroyed"), 'danger' # Item is destroyed
REJECTED = 65, _("Rejected"), 'danger' # Item is rejected
LOST = 70, _("Lost"), 'dark' # Item has been lost
QUARANTINED = 75, _("Quarantined"), 'info' # Item has been quarantined and is unavailable
RETURNED = 85, _("Returned"), 'warning' # Item has been returned from a customer
options = {
OK: _("OK"),
ATTENTION: _("Attention needed"),
DAMAGED: _("Damaged"),
DESTROYED: _("Destroyed"),
LOST: _("Lost"),
REJECTED: _("Rejected"),
QUARANTINED: _("Quarantined"),
RETURNED: _("Returned"),
}
colors = {
OK: 'success',
ATTENTION: 'warning',
DAMAGED: 'danger',
DESTROYED: 'danger',
LOST: 'dark',
REJECTED: 'danger',
QUARANTINED: 'info'
}
class StockStatusGroups:
"""Groups for StockStatus codes."""
# The following codes correspond to parts that are 'available' or 'in stock'
AVAILABLE_CODES = [
OK,
ATTENTION,
DAMAGED,
RETURNED,
StockStatus.OK.value,
StockStatus.ATTENTION.value,
StockStatus.DAMAGED.value,
StockStatus.RETURNED.value,
]
class StockHistoryCode(StatusCode):
"""Status codes for StockHistory."""
LEGACY = 0
LEGACY = 0, _('Legacy stock tracking entry')
CREATED = 1
CREATED = 1, _('Stock item created')
# Manual editing operations
EDITED = 5
ASSIGNED_SERIAL = 6
EDITED = 5, _('Edited stock item')
ASSIGNED_SERIAL = 6, _('Assigned serial number')
# Manual stock operations
STOCK_COUNT = 10
STOCK_ADD = 11
STOCK_REMOVE = 12
STOCK_COUNT = 10, _('Stock counted')
STOCK_ADD = 11, _('Stock manually added')
STOCK_REMOVE = 12, _('Stock manually removed')
# Location operations
STOCK_MOVE = 20
STOCK_MOVE = 20, _('Location changed')
STOCK_UPDATE = 25, _('Stock updated')
# Installation operations
INSTALLED_INTO_ASSEMBLY = 30
REMOVED_FROM_ASSEMBLY = 31
INSTALLED_INTO_ASSEMBLY = 30, _('Installed into assembly')
REMOVED_FROM_ASSEMBLY = 31, _('Removed from assembly')
INSTALLED_CHILD_ITEM = 35
REMOVED_CHILD_ITEM = 36
INSTALLED_CHILD_ITEM = 35, _('Installed component item')
REMOVED_CHILD_ITEM = 36, _('Removed component item')
# Stock splitting operations
SPLIT_FROM_PARENT = 40
SPLIT_CHILD_ITEM = 42
SPLIT_FROM_PARENT = 40, _('Split from parent item')
SPLIT_CHILD_ITEM = 42, _('Split child item')
# Stock merging operations
MERGED_STOCK_ITEMS = 45
MERGED_STOCK_ITEMS = 45, _('Merged stock items')
# Convert stock item to variant
CONVERTED_TO_VARIANT = 48
CONVERTED_TO_VARIANT = 48, _('Converted to variant')
# Build order codes
BUILD_OUTPUT_CREATED = 50
BUILD_OUTPUT_COMPLETED = 55
BUILD_CONSUMED = 57
BUILD_OUTPUT_CREATED = 50, _('Build order output created')
BUILD_OUTPUT_COMPLETED = 55, _('Build order output completed')
BUILD_OUTPUT_REJECTED = 56, _('Build order output rejected')
BUILD_CONSUMED = 57, _('Consumed by build order')
# Sales order codes
SHIPPED_AGAINST_SALES_ORDER = 60
SHIPPED_AGAINST_SALES_ORDER = 60, _("Shipped against Sales Order")
# Purchase order codes
RECEIVED_AGAINST_PURCHASE_ORDER = 70
RECEIVED_AGAINST_PURCHASE_ORDER = 70, _('Received against Purchase Order')
# Return order codes
RETURNED_AGAINST_RETURN_ORDER = 80
RETURNED_AGAINST_RETURN_ORDER = 80, _('Returned against Return Order')
# Customer actions
SENT_TO_CUSTOMER = 100
RETURNED_FROM_CUSTOMER = 105
options = {
LEGACY: _('Legacy stock tracking entry'),
CREATED: _('Stock item created'),
EDITED: _('Edited stock item'),
ASSIGNED_SERIAL: _('Assigned serial number'),
STOCK_COUNT: _('Stock counted'),
STOCK_ADD: _('Stock manually added'),
STOCK_REMOVE: _('Stock manually removed'),
STOCK_MOVE: _('Location changed'),
INSTALLED_INTO_ASSEMBLY: _('Installed into assembly'),
REMOVED_FROM_ASSEMBLY: _('Removed from assembly'),
INSTALLED_CHILD_ITEM: _('Installed component item'),
REMOVED_CHILD_ITEM: _('Removed component item'),
SPLIT_FROM_PARENT: _('Split from parent item'),
SPLIT_CHILD_ITEM: _('Split child item'),
MERGED_STOCK_ITEMS: _('Merged stock items'),
CONVERTED_TO_VARIANT: _('Converted to variant'),
SENT_TO_CUSTOMER: _('Sent to customer'),
RETURNED_FROM_CUSTOMER: _('Returned from customer'),
BUILD_OUTPUT_CREATED: _('Build order output created'),
BUILD_OUTPUT_COMPLETED: _('Build order output completed'),
BUILD_CONSUMED: _('Consumed by build order'),
SHIPPED_AGAINST_SALES_ORDER: _("Shipped against Sales Order"),
RECEIVED_AGAINST_PURCHASE_ORDER: _('Received against Purchase Order'),
RETURNED_AGAINST_RETURN_ORDER: _('Returned against Return Order'),
}
SENT_TO_CUSTOMER = 100, _('Sent to customer')
RETURNED_FROM_CUSTOMER = 105, _('Returned from customer')
class BuildStatus(StatusCode):
"""Build status codes."""
PENDING = 10 # Build is pending / active
PRODUCTION = 20 # BuildOrder is in production
CANCELLED = 30 # Build was cancelled
COMPLETE = 40 # Build is complete
PENDING = 10, _("Pending"), 'secondary' # Build is pending / active
PRODUCTION = 20, _("Production"), 'primary' # BuildOrder is in production
CANCELLED = 30, _("Cancelled"), 'danger' # Build was cancelled
COMPLETE = 40, _("Complete"), 'success' # Build is complete
options = {
PENDING: _("Pending"),
PRODUCTION: _("Production"),
CANCELLED: _("Cancelled"),
COMPLETE: _("Complete"),
}
colors = {
PENDING: 'secondary',
PRODUCTION: 'primary',
COMPLETE: 'success',
CANCELLED: 'danger',
}
class BuildStatusGroups:
"""Groups for BuildStatus codes."""
ACTIVE_CODES = [
PENDING,
PRODUCTION,
BuildStatus.PENDING.value,
BuildStatus.PRODUCTION.value,
]
@@ -377,68 +164,40 @@ class ReturnOrderStatus(StatusCode):
"""Defines a set of status codes for a ReturnOrder"""
# Order is pending, waiting for receipt of items
PENDING = 10
PENDING = 10, _("Pending"), 'secondary'
# Items have been received, and are being inspected
IN_PROGRESS = 20
IN_PROGRESS = 20, _("In Progress"), 'primary'
COMPLETE = 30
CANCELLED = 40
COMPLETE = 30, _("Complete"), 'success'
CANCELLED = 40, _("Cancelled"), 'danger'
class ReturnOrderStatusGroups:
"""Groups for ReturnOrderStatus codes."""
OPEN = [
PENDING,
IN_PROGRESS,
ReturnOrderStatus.PENDING.value,
ReturnOrderStatus.IN_PROGRESS.value,
]
options = {
PENDING: _("Pending"),
IN_PROGRESS: _("In Progress"),
COMPLETE: _("Complete"),
CANCELLED: _("Cancelled"),
}
colors = {
PENDING: 'secondary',
IN_PROGRESS: 'primary',
COMPLETE: 'success',
CANCELLED: 'danger',
}
class ReturnOrderLineStatus(StatusCode):
"""Defines a set of status codes for a ReturnOrderLineItem"""
PENDING = 10
PENDING = 10, _("Pending"), 'secondary'
# Item is to be returned to customer, no other action
RETURN = 20
RETURN = 20, _("Return"), 'success'
# Item is to be repaired, and returned to customer
REPAIR = 30
REPAIR = 30, _("Repair"), 'primary'
# Item is to be replaced (new item shipped)
REPLACE = 40
REPLACE = 40, _("Replace"), 'warning'
# Item is to be refunded (cannot be repaired)
REFUND = 50
REFUND = 50, _("Refund"), 'info'
# Item is rejected
REJECT = 60
options = {
PENDING: _('Pending'),
RETURN: _('Return'),
REPAIR: _('Repair'),
REFUND: _('Refund'),
REPLACE: _('Replace'),
REJECT: _('Reject')
}
colors = {
PENDING: 'secondary',
RETURN: 'success',
REPAIR: 'primary',
REFUND: 'info',
REPLACE: 'warning',
REJECT: 'danger',
}
REJECT = 60, _("Reject"), 'danger'

View File

@@ -12,7 +12,6 @@ from datetime import datetime, timedelta
from typing import Callable, List
from django.conf import settings
from django.core import mail as django_mail
from django.core.exceptions import AppRegistryNotReady
from django.core.management import call_command
from django.db import DEFAULT_DB_ALIAS, connections
@@ -71,7 +70,7 @@ def raise_warning(msg):
# If testing is running raise a warning that can be asserted
if settings.TESTING:
warnings.warn(msg)
warnings.warn(msg, stacklevel=2)
def check_daily_holdoff(task_name: str, n_days: int = 1) -> bool:
@@ -92,13 +91,15 @@ def check_daily_holdoff(task_name: str, n_days: int = 1) -> bool:
"""
from common.models import InvenTreeSetting
from InvenTree.ready import isInTestMode
if n_days <= 0:
logger.info(f"Specified interval for task '{task_name}' < 1 - task will not run")
return False
# Sleep a random number of seconds to prevent worker conflict
time.sleep(random.randint(1, 5))
if not isInTestMode():
time.sleep(random.randint(1, 5))
attempt_key = f'_{task_name}_ATTEMPT'
success_key = f'_{task_name}_SUCCESS'
@@ -167,6 +168,7 @@ def offload_task(taskname, *args, force_async=False, force_sync=False, **kwargs)
If workers are not running or force_sync flag
is set then the task is ran synchronously.
"""
try:
import importlib
@@ -186,6 +188,8 @@ def offload_task(taskname, *args, force_async=False, force_sync=False, **kwargs)
task.run()
except ImportError:
raise_warning(f"WARNING: '{taskname}' not started - Function not found")
except Exception as exc:
raise_warning(f"WARNING: '{taskname}' not started due to {type(exc)}")
else:
if callable(taskname):
@@ -249,7 +253,7 @@ class ScheduledTask:
class TaskRegister:
"""Registery for periodicall tasks."""
"""Registry for periodicall tasks."""
task_list: List[ScheduledTask] = []
def register(self, task, schedule, minutes: int = None):
@@ -495,7 +499,7 @@ def check_for_updates():
def update_exchange_rates():
"""Update currency exchange rates."""
try:
from djmoney.contrib.exchange.models import ExchangeBackend, Rate
from djmoney.contrib.exchange.models import Rate
from common.settings import currency_code_default, currency_codes
from InvenTree.exchange import InvenTreeExchange
@@ -507,22 +511,9 @@ def update_exchange_rates():
# Other error?
return
# Test to see if the database is ready yet
try:
backend = ExchangeBackend.objects.get(name='InvenTreeExchange')
except ExchangeBackend.DoesNotExist:
pass
except Exception: # pragma: no cover
# Some other error
logger.warning("update_exchange_rates: Database not ready")
return
backend = InvenTreeExchange()
logger.info(f"Updating exchange rates from {backend.url}")
base = currency_code_default()
logger.info(f"Using base currency '{base}'")
logger.info(f"Updating exchange rates using base currency '{base}'")
try:
backend.update_rates(base_currency=base)
@@ -530,7 +521,7 @@ def update_exchange_rates():
# Remove any exchange rates which are not in the provided currencies
Rate.objects.filter(backend="InvenTreeExchange").exclude(currency__in=currency_codes()).delete()
except Exception as e: # pragma: no cover
logger.error(f"Error updating exchange rates: {e}")
logger.error(f"Error updating exchange rates: {e} ({type(e)})")
@scheduled_task(ScheduledTask.DAILY)
@@ -558,27 +549,11 @@ def run_backup():
record_task_success('run_backup')
def send_email(subject, body, recipients, from_email=None, html_message=None):
"""Send an email with the specified subject and body, to the specified recipients list."""
if type(recipients) == str:
recipients = [recipients]
offload_task(
django_mail.send_mail,
subject,
body,
from_email,
recipients,
fail_silently=False,
html_message=html_message
)
@scheduled_task(ScheduledTask.DAILY)
def check_for_migrations(worker: bool = True):
"""Checks if migrations are needed.
If the setting auto_update is enabled we will start updateing.
If the setting auto_update is enabled we will start updating.
"""
# Test if auto-updates are enabled
if not get_setting('INVENTREE_AUTO_UPDATE', 'auto_update'):

View File

@@ -0,0 +1,36 @@
"""Custom template loader for InvenTree"""
import os
from django.conf import settings
from django.template.loaders.base import Loader as BaseLoader
from django.template.loaders.cached import Loader as CachedLoader
class InvenTreeTemplateLoader(CachedLoader):
"""Custom template loader which bypasses cache for PDF export"""
def get_template(self, template_name, skip=None):
"""Return a template object for the given template name.
Any custom report or label templates will be forced to reload (without cache).
This ensures that generated PDF reports / labels are always up-to-date.
"""
# List of template patterns to skip cache for
skip_cache_dirs = [
os.path.abspath(os.path.join(settings.MEDIA_ROOT, 'report')),
os.path.abspath(os.path.join(settings.MEDIA_ROOT, 'label')),
'snippets/',
]
# Initially load the template using the cached loader
template = CachedLoader.get_template(self, template_name, skip)
template_path = str(template.name)
# If the template matches any of the skip patterns, reload it without cache
if any(template_path.startswith(d) for d in skip_cache_dirs):
template = BaseLoader.get_template(self, template_name, skip)
return template

View File

@@ -6,8 +6,7 @@ from django.urls import reverse
from rest_framework import status
from InvenTree.api_tester import InvenTreeAPITestCase
from InvenTree.helpers import InvenTreeTestCase
from InvenTree.unit_test import InvenTreeAPITestCase, InvenTreeTestCase
from users.models import RuleSet, update_group_roles

View File

@@ -7,7 +7,7 @@ from django.urls import reverse
from error_report.models import Error
from InvenTree.exceptions import log_error
from InvenTree.helpers import InvenTreeTestCase
from InvenTree.unit_test import InvenTreeTestCase
class MiddlewareTests(InvenTreeTestCase):
@@ -28,13 +28,13 @@ class MiddlewareTests(InvenTreeTestCase):
self.client.logout()
# check that static files go through
# TODO @matmair reenable this check
# TODO @matmair re-enable this check
# self.check_path('/static/css/inventree.css', 302)
# check that account things go through
self.check_path(reverse('account_login'))
# logout goes diretly to login
# logout goes directly to login
self.check_path(reverse('account_logout'))
# check that frontend code is redirected to login

View File

@@ -70,11 +70,11 @@ class InvenTreeTaskTests(TestCase):
with self.assertWarnsMessage(UserWarning, "WARNING: 'InvenTree' not started - Malformed function path"):
InvenTree.tasks.offload_task('InvenTree')
# Non exsistent app
# Non existent app
with self.assertWarnsMessage(UserWarning, "WARNING: 'InvenTreeABC.test_tasks.doesnotmatter' not started - No module named 'InvenTreeABC.test_tasks'"):
InvenTree.tasks.offload_task('InvenTreeABC.test_tasks.doesnotmatter')
# Non exsistent function
# Non existent function
with self.assertWarnsMessage(UserWarning, "WARNING: 'InvenTree.test_tasks.doesnotexsist' not started - No function named 'doesnotexsist'"):
InvenTree.tasks.offload_task('InvenTree.test_tasks.doesnotexsist')

View File

@@ -14,18 +14,18 @@ class URLTest(TestCase):
# Need fixture data in the database
fixtures = [
'settings',
'build',
'company',
'manufacturer_part',
'price_breaks',
'supplier_part',
'order',
'sales_order',
'bom',
'category',
'params',
'part_pricebreaks',
'part',
'bom',
'build',
'test_templates',
'location',
'stock_tests',

View File

@@ -5,7 +5,7 @@ import os
from django.contrib.auth import get_user_model
from django.urls import reverse
from InvenTree.helpers import InvenTreeTestCase
from InvenTree.unit_test import InvenTreeTestCase
class ViewTests(InvenTreeTestCase):

View File

@@ -14,17 +14,19 @@ from django.contrib.sites.models import Site
from django.core.exceptions import ValidationError
from django.test import TestCase, override_settings
import requests
from djmoney.contrib.exchange.exceptions import MissingRate
from djmoney.contrib.exchange.models import Rate, convert_money
from djmoney.money import Money
import InvenTree.conversion
import InvenTree.format
import InvenTree.helpers
import InvenTree.helpers_model
import InvenTree.tasks
from common.models import InvenTreeSetting
from common.settings import currency_codes
from InvenTree.sanitizer import sanitize_svg
from InvenTree.unit_test import InvenTreeTestCase
from part.models import Part, PartCategory
from stock.models import StockItem, StockLocation
@@ -33,6 +35,45 @@ from .tasks import offload_task
from .validators import validate_overage
class ConversionTest(TestCase):
"""Tests for conversion of physical units"""
def test_dimensionless_units(self):
"""Tests for 'dimensonless' unit quantities"""
# Test some dimensionless units
tests = {
'ea': 1,
'each': 1,
'3 piece': 3,
'5 dozen': 60,
'3 hundred': 300,
'2 thousand': 2000,
'12 pieces': 12,
}
for val, expected in tests.items():
q = InvenTree.conversion.convert_physical_value(val).to_base_units()
self.assertEqual(q.magnitude, expected)
def test_invalid_values(self):
"""Test conversion of invalid inputs"""
inputs = [
'-',
';;',
'-x',
'?',
'--',
'+',
'++',
]
for val in inputs:
with self.assertRaises(ValidationError):
InvenTree.conversion.convert_physical_value(val)
class ValidatorTest(TestCase):
"""Simple tests for custom field validators."""
@@ -192,6 +233,34 @@ class FormatTest(TestCase):
class TestHelpers(TestCase):
"""Tests for InvenTree helper functions."""
def test_absolute_url(self):
"""Test helper function for generating an absolute URL"""
base = "https://demo.inventree.org:12345"
InvenTreeSetting.set_setting('INVENTREE_BASE_URL', base, change_user=None)
tests = {
"": base,
"api/": base + "/api/",
"/api/": base + "/api/",
"api": base + "/api",
"media/label/output/": base + "/media/label/output/",
"static/logo.png": base + "/static/logo.png",
"https://www.google.com": "https://www.google.com",
"https://demo.inventree.org:12345/out.html": "https://demo.inventree.org:12345/out.html",
"https://demo.inventree.org/test.html": "https://demo.inventree.org/test.html",
"http://www.cwi.nl:80/%7Eguido/Python.html": "http://www.cwi.nl:80/%7Eguido/Python.html",
"test.org": base + "/test.org",
}
for url, expected in tests.items():
# Test with supplied base URL
self.assertEqual(InvenTree.helpers_model.construct_absolute_url(url, site_url=base), expected)
# Test without supplied base URL
self.assertEqual(InvenTree.helpers_model.construct_absolute_url(url), expected)
def test_image_url(self):
"""Test if a filename looks like an image."""
for name in ['ape.png', 'bat.GiF', 'apple.WeBP', 'BiTMap.Bmp']:
@@ -259,12 +328,12 @@ class TestHelpers(TestCase):
"\\invalid-url"
]:
with self.assertRaises(django_exceptions.ValidationError):
helpers.download_image_from_url(url)
InvenTree.helpers_model.download_image_from_url(url)
def dl_helper(url, expected_error, timeout=2.5, retries=3):
"""Helper function for unit testing downloads.
As the httpstat.us service occassionaly refuses a connection,
As the httpstat.us service occasionally refuses a connection,
we will simply try multiple times
"""
@@ -274,7 +343,7 @@ class TestHelpers(TestCase):
while tries < retries:
try:
helpers.download_image_from_url(url, timeout=timeout)
InvenTree.helpers_model.download_image_from_url(url, timeout=timeout)
break
except Exception as exc:
if type(exc) is expected_error:
@@ -287,10 +356,12 @@ class TestHelpers(TestCase):
time.sleep(10 * tries)
# Attempt to download an image which throws a 404
dl_helper("https://httpstat.us/404", requests.exceptions.HTTPError, timeout=10)
# TODO: Re-implement this test when we are happier with the external service
# dl_helper("https://httpstat.us/404", requests.exceptions.HTTPError, timeout=10)
# Attempt to download, but timeout
dl_helper("https://httpstat.us/200?sleep=5000", requests.exceptions.ReadTimeout, timeout=1)
# TODO: Re-implement this test when we are happier with the external service
# dl_helper("https://httpstat.us/200?sleep=5000", requests.exceptions.ReadTimeout, timeout=1)
large_img = "https://github.com/inventree/InvenTree/raw/master/InvenTree/InvenTree/static/img/paper_splash_large.jpg"
@@ -298,13 +369,27 @@ class TestHelpers(TestCase):
# Attempt to download an image which is too large
with self.assertRaises(ValueError):
helpers.download_image_from_url(large_img, timeout=10)
InvenTree.helpers_model.download_image_from_url(large_img, timeout=10)
# Increase allowable download size
InvenTreeSetting.set_setting('INVENTREE_DOWNLOAD_IMAGE_MAX_SIZE', 5, change_user=None)
# Download a valid image (should not throw an error)
helpers.download_image_from_url(large_img, timeout=10)
InvenTree.helpers_model.download_image_from_url(large_img, timeout=10)
def test_model_mixin(self):
"""Test the getModelsWithMixin function"""
from InvenTree.models import InvenTreeBarcodeMixin
models = InvenTree.helpers_model.getModelsWithMixin(InvenTreeBarcodeMixin)
self.assertIn(Part, models)
self.assertIn(StockLocation, models)
self.assertIn(StockItem, models)
self.assertNotIn(PartCategory, models)
self.assertNotIn(InvenTreeSetting, models)
class TestQuoteWrap(TestCase):
@@ -502,7 +587,7 @@ class TestSerialNumberExtraction(TestCase):
self.assertEqual(sn, ['5', '6', '7', '8'])
def test_failures(self):
"""Test wron serial numbers."""
"""Test wrong serial numbers."""
e = helpers.extract_serial_numbers
# Test duplicates
@@ -660,6 +745,7 @@ class CurrencyTests(TestCase):
else: # pragma: no cover
print("Exchange rate update failed - retrying")
print(f'Expected {currency_codes()}, got {[a.currency for a in rates]}')
time.sleep(1)
self.assertTrue(update_successful)
@@ -696,7 +782,7 @@ class TestStatus(TestCase):
self.assertEqual(ready.isImportingData(), False)
class TestSettings(helpers.InvenTreeTestCase):
class TestSettings(InvenTreeTestCase):
"""Unit tests for settings."""
superuser = True
@@ -792,7 +878,7 @@ class TestSettings(helpers.InvenTreeTestCase):
'inventree/data/config.yaml',
]
self.assertTrue(any([opt in str(config.get_config_file()).lower() for opt in valid]))
self.assertTrue(any(opt in str(config.get_config_file()).lower() for opt in valid))
# with env set
with self.in_env_context({'INVENTREE_CONFIG_FILE': 'my_special_conf.yaml'}):
@@ -807,7 +893,7 @@ class TestSettings(helpers.InvenTreeTestCase):
'inventree/data/plugins.txt',
]
self.assertTrue(any([opt in str(config.get_plugin_file()).lower() for opt in valid]))
self.assertTrue(any(opt in str(config.get_plugin_file()).lower() for opt in valid))
# with env set
with self.in_env_context({'INVENTREE_PLUGIN_FILE': 'my_special_plugins.txt'}):
@@ -835,7 +921,7 @@ class TestSettings(helpers.InvenTreeTestCase):
self.assertEqual(config.get_setting(TEST_ENV_NAME, None, typecast=dict), {})
class TestInstanceName(helpers.InvenTreeTestCase):
class TestInstanceName(InvenTreeTestCase):
"""Unit tests for instance name."""
def test_instance_name(self):
@@ -863,7 +949,7 @@ class TestInstanceName(helpers.InvenTreeTestCase):
self.assertEqual(site_obj.domain, 'http://127.1.2.3')
class TestOffloadTask(helpers.InvenTreeTestCase):
class TestOffloadTask(InvenTreeTestCase):
"""Tests for offloading tasks to the background worker"""
fixtures = [
@@ -960,7 +1046,7 @@ class TestOffloadTask(helpers.InvenTreeTestCase):
self.assertTrue(result)
class BarcodeMixinTest(helpers.InvenTreeTestCase):
class BarcodeMixinTest(InvenTreeTestCase):
"""Tests for the InvenTreeBarcodeMixin mixin class"""
def test_barcode_model_type(self):
@@ -993,7 +1079,7 @@ class SanitizerTest(TestCase):
"""Simple tests for sanitizer functions."""
def test_svg_sanitizer(self):
"""Test that SVGs are sanitized acordingly."""
"""Test that SVGs are sanitized accordingly."""
valid_string = """<svg xmlns="http://www.w3.org/2000/svg" version="1.1" id="svg2" height="400" width="400">{0}
<path id="path1" d="m -151.78571,359.62883 v 112.76373 l 97.068507,-56.04253 V 303.14815 Z" style="fill:#ddbc91;"></path>
</svg>"""

View File

@@ -1,12 +1,18 @@
"""Helper functions for performing API unit tests."""
"""Helper functions for unit testing / CI"""
import csv
import io
import json
import re
from contextlib import contextmanager
from pathlib import Path
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group
from django.contrib.auth.models import Group, Permission
from django.db import connections
from django.http.response import StreamingHttpResponse
from django.test import TestCase
from django.test.utils import CaptureQueriesContext
from djmoney.contrib.exchange.models import ExchangeBackend, Rate
from rest_framework.test import APITestCase
@@ -15,6 +21,75 @@ from plugin import registry
from plugin.models import PluginConfig
def addUserPermission(user, permission):
"""Shortcut function for adding a certain permission to a user."""
perm = Permission.objects.get(codename=permission)
user.user_permissions.add(perm)
def addUserPermissions(user, permissions):
"""Shortcut function for adding multiple permissions to a user."""
for permission in permissions:
addUserPermission(user, permission)
def getMigrationFileNames(app):
"""Return a list of all migration filenames for provided app."""
local_dir = Path(__file__).parent
files = local_dir.joinpath('..', app, 'migrations').iterdir()
# Regex pattern for migration files
regex = re.compile(r"^[\d]+_.*\.py$")
migration_files = []
for f in files:
if regex.match(f.name):
migration_files.append(f.name)
return migration_files
def getOldestMigrationFile(app, exclude_extension=True, ignore_initial=True):
"""Return the filename associated with the oldest migration."""
oldest_num = -1
oldest_file = None
for f in getMigrationFileNames(app):
if ignore_initial and f.startswith('0001_initial'):
continue
num = int(f.split('_')[0])
if oldest_file is None or num < oldest_num:
oldest_num = num
oldest_file = f
if exclude_extension:
oldest_file = oldest_file.replace('.py', '')
return oldest_file
def getNewestMigrationFile(app, exclude_extension=True):
"""Return the filename associated with the newest migration."""
newest_file = None
newest_num = -1
for f in getMigrationFileNames(app):
num = int(f.split('_')[0])
if newest_file is None or num > newest_num:
newest_num = num
newest_file = f
if exclude_extension:
newest_file = newest_file.replace('.py', '')
return newest_file
class UserMixin:
"""Mixin to setup a user and login for tests.
@@ -162,9 +237,38 @@ class ExchangeRateMixin:
Rate.objects.bulk_create(items)
class InvenTreeTestCase(ExchangeRateMixin, UserMixin, TestCase):
"""Testcase with user setup buildin."""
pass
class InvenTreeAPITestCase(ExchangeRateMixin, UserMixin, APITestCase):
"""Base class for running InvenTree API tests."""
@contextmanager
def assertNumQueriesLessThan(self, value, using='default', verbose=False, debug=False):
"""Context manager to check that the number of queries is less than a certain value.
Example:
with self.assertNumQueriesLessThan(10):
# Do some stuff
Ref: https://stackoverflow.com/questions/1254170/django-is-there-a-way-to-count-sql-queries-from-an-unit-test/59089020#59089020
"""
with CaptureQueriesContext(connections[using]) as context:
yield # your test will be run here
if verbose:
msg = "\r\n%s" % json.dumps(context.captured_queries, indent=4)
else:
msg = None
n = len(context.captured_queries)
if debug:
print(f"Expected less than {value} queries, got {n} queries")
self.assertLess(n, value, msg=msg)
def checkResponse(self, url, method, expected_code, response):
"""Debug output for an unexpected response"""
@@ -265,8 +369,7 @@ class InvenTreeAPITestCase(ExchangeRateMixin, UserMixin, APITestCase):
"""Download a file from the server, and return an in-memory file."""
response = self.client.get(url, data=data, format='json')
if expected_code is not None:
self.assertEqual(response.status_code, expected_code)
self.checkResponse(url, 'DOWNLOAD_FILE', expected_code, response)
# Check that the response is of the correct type
if not isinstance(response, StreamingHttpResponse):
@@ -284,27 +387,27 @@ class InvenTreeAPITestCase(ExchangeRateMixin, UserMixin, APITestCase):
if decode:
# Decode data and return as StringIO file object
fo = io.StringIO()
fo.name = fo
fo.write(response.getvalue().decode('UTF-8'))
file = io.StringIO()
file.name = file
file.write(response.getvalue().decode('UTF-8'))
else:
# Return a a BytesIO file object
fo = io.BytesIO()
fo.name = fn
fo.write(response.getvalue())
file = io.BytesIO()
file.name = fn
file.write(response.getvalue())
fo.seek(0)
file.seek(0)
return fo
return file
def process_csv(self, fo, delimiter=',', required_cols=None, excluded_cols=None, required_rows=None):
def process_csv(self, file_object, delimiter=',', required_cols=None, excluded_cols=None, required_rows=None):
"""Helper function to process and validate a downloaded csv file."""
# Check that the correct object type has been passed
self.assertTrue(isinstance(fo, io.StringIO))
self.assertTrue(isinstance(file_object, io.StringIO))
fo.seek(0)
file_object.seek(0)
reader = csv.reader(fo, delimiter=delimiter)
reader = csv.reader(file_object, delimiter=delimiter)
headers = []
rows = []

View File

@@ -9,7 +9,10 @@ from django.contrib import admin
from django.urls import include, path, re_path
from django.views.generic.base import RedirectView
from rest_framework.documentation import include_docs_urls
from dj_rest_auth.registration.views import (ConfirmEmailView,
SocialAccountDisconnectView,
SocialAccountListView)
from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView
from build.api import build_api_urls
from build.urls import build_urls
@@ -31,6 +34,7 @@ from stock.urls import stock_urls
from users.api import user_urls
from .api import APISearchView, InfoView, NotFoundView
from .social_auth_urls import SocialProvierListView, social_auth_urlpatterns
from .views import (AboutView, AppearanceSelectView, CustomConnectionsView,
CustomEmailView, CustomLoginView,
CustomPasswordResetFromKeyView,
@@ -62,12 +66,26 @@ apipatterns = [
# Plugin endpoints
path('', include(plugin_api_urls)),
# Webhook endpoints
# Common endpoints endpoint
path('', include(common_api_urls)),
# OpenAPI Schema
re_path('schema/', SpectacularAPIView.as_view(custom_settings={'SCHEMA_PATH_PREFIX': '/api/'}), name='schema'),
# InvenTree information endpoint
path('', InfoView.as_view(), name='api-inventree-info'),
# Auth API endpoints
path('auth/', include([
re_path(r'^registration/account-confirm-email/(?P<key>[-:\w]+)/$', ConfirmEmailView.as_view(), name='account_confirm_email'),
path('registration/', include('dj_rest_auth.registration.urls')),
path('providers/', SocialProvierListView.as_view(), name='social_providers'),
path('social/', include(social_auth_urlpatterns)),
path('social/', SocialAccountListView.as_view(), name='social_account_list'),
path('social/<int:pk>/disconnect/', SocialAccountDisconnectView.as_view(), name='social_account_disconnect'),
path('', include('dj_rest_auth.urls')),
])),
# Unknown endpoint
re_path(r'^.*$', NotFoundView.as_view(), name='api-404'),
]
@@ -92,10 +110,11 @@ notifications_urls = [
dynamic_javascript_urls = [
re_path(r'^calendar.js', DynamicJsView.as_view(template_name='js/dynamic/calendar.js'), name='calendar.js'),
re_path(r'^nav.js', DynamicJsView.as_view(template_name='js/dynamic/nav.js'), name='nav.js'),
re_path(r'^permissions.js', DynamicJsView.as_view(template_name='js/dynamic/permissions.js'), name='permissions.js'),
re_path(r'^settings.js', DynamicJsView.as_view(template_name='js/dynamic/settings.js'), name='settings.js'),
]
# These javascript files are pased through the Django translation layer
# These javascript files are passed through the Django translation layer
translated_javascript_urls = [
re_path(r'^api.js', DynamicJsView.as_view(template_name='js/translated/api.js'), name='api.js'),
re_path(r'^attachment.js', DynamicJsView.as_view(template_name='js/translated/attachment.js'), name='attachment.js'),
@@ -107,6 +126,7 @@ translated_javascript_urls = [
re_path(r'^filters.js', DynamicJsView.as_view(template_name='js/translated/filters.js'), name='filters.js'),
re_path(r'^forms.js', DynamicJsView.as_view(template_name='js/translated/forms.js'), name='forms.js'),
re_path(r'^helpers.js', DynamicJsView.as_view(template_name='js/translated/helpers.js'), name='helpers.js'),
re_path(r'^index.js', DynamicJsView.as_view(template_name='js/translated/index.js'), name='index.js'),
re_path(r'^label.js', DynamicJsView.as_view(template_name='js/translated/label.js'), name='label.js'),
re_path(r'^model_renderers.js', DynamicJsView.as_view(template_name='js/translated/model_renderers.js'), name='model_renderers.js'),
re_path(r'^modals.js', DynamicJsView.as_view(template_name='js/translated/modals.js'), name='modals.js'),
@@ -136,7 +156,7 @@ backendpatterns = [
re_path(r'^auth/?', auth_request),
re_path(r'^api/', include(apipatterns)),
re_path(r'^api-doc/', include_docs_urls(title='InvenTree API')),
re_path(r'^api-doc/', SpectacularRedocView.as_view(url_name='schema'), name='api-doc'),
]
frontendpatterns = [

View File

@@ -8,9 +8,31 @@ from django.core import validators
from django.core.exceptions import FieldDoesNotExist, ValidationError
from django.utils.translation import gettext_lazy as _
import pint
from jinja2 import Template
from moneyed import CURRENCIES
import InvenTree.conversion
def validate_physical_units(unit):
"""Ensure that a given unit is a valid physical unit."""
unit = unit.strip()
# Ignore blank units
if not unit:
return
ureg = InvenTree.conversion.get_unit_registry()
try:
ureg(unit)
except AttributeError:
raise ValidationError(_('Invalid physical unit'))
except pint.errors.UndefinedUnitError:
raise ValidationError(_('Invalid physical unit'))
def validate_currency_code(code):
"""Check that a given code is a valid currency code."""

View File

@@ -4,25 +4,41 @@ Provides information on the current InvenTree version
"""
import os
import pathlib
import platform
import re
import subprocess
from datetime import datetime as dt
from datetime import timedelta as td
import django
from django.conf import settings
import common.models
from InvenTree.api_version import INVENTREE_API_VERSION
from dulwich.repo import NotGitRepository, Repo
from .api_version import INVENTREE_API_VERSION
# InvenTree software version
INVENTREE_SW_VERSION = "0.11.0"
INVENTREE_SW_VERSION = "0.12.8"
# Discover git
try:
main_repo = Repo(pathlib.Path(__file__).parent.parent.parent)
main_commit = main_repo[main_repo.head()]
except (NotGitRepository, FileNotFoundError):
main_commit = None
def inventreeInstanceName():
"""Returns the InstanceName settings for the current database."""
import common.models
return common.models.InvenTreeSetting.get_setting("INVENTREE_INSTANCE", "")
def inventreeInstanceTitle():
"""Returns the InstanceTitle for the current database."""
import common.models
if common.models.InvenTreeSetting.get_setting("INVENTREE_INSTANCE_TITLE", False):
return common.models.InvenTreeSetting.get_setting("INVENTREE_INSTANCE", "")
else:
@@ -66,6 +82,7 @@ def isInvenTreeUpToDate():
A background task periodically queries GitHub for latest version, and stores it to the database as "_INVENTREE_LATEST_VERSION"
"""
import common.models
latest = common.models.InvenTreeSetting.get_setting('_INVENTREE_LATEST_VERSION', backup_value=None, create=False)
# No record for "latest" version - we must assume we are up to date!
@@ -97,10 +114,9 @@ def inventreeCommitHash():
if commit_hash:
return commit_hash
try:
return str(subprocess.check_output('git rev-parse --short HEAD'.split()), 'utf-8').strip()
except Exception: # pragma: no cover
if main_commit is None:
return None
return main_commit.sha().hexdigest()[0:7]
def inventreeCommitDate():
@@ -111,8 +127,53 @@ def inventreeCommitDate():
if commit_date:
return commit_date.split(' ')[0]
try:
d = str(subprocess.check_output('git show -s --format=%ci'.split()), 'utf-8').strip()
return d.split(' ')[0]
except Exception: # pragma: no cover
if main_commit is None:
return None
commit_dt = dt.fromtimestamp(main_commit.commit_time) + td(seconds=main_commit.commit_timezone)
return str(commit_dt.date())
def inventreeInstaller():
"""Returns the installer for the running codebase - if set."""
# First look in the environment variables, e.g. if running in docker
installer = os.environ.get('INVENTREE_PKG_INSTALLER', '')
if installer:
return installer
elif settings.DOCKER:
return 'DOC'
elif main_commit is not None:
return 'GIT'
return None
def inventreeBranch():
"""Returns the branch for the running codebase - if set."""
# First look in the environment variables, e.g. if running in docker
branch = os.environ.get('INVENTREE_PKG_BRANCH', '')
if branch:
return branch
if main_commit is None:
return None
branch = main_repo.refs.follow(b'HEAD')[0][1].decode()
return branch.removeprefix('refs/heads/')
def inventreeTarget():
"""Returns the target platform for the running codebase - if set."""
# First look in the environment variables, e.g. if running in docker
return os.environ.get('INVENTREE_PKG_TARGET', None)
def inventreePlatform():
"""Returns the platform for the instance."""
return platform.platform(aliased=True)

View File

@@ -31,8 +31,8 @@ from allauth_2fa.views import TwoFactorRemove
from djmoney.contrib.exchange.models import ExchangeBackend, Rate
from user_sessions.views import SessionDeleteOtherView, SessionDeleteView
from common.models import ColorTheme, InvenTreeSetting
from common.settings import currency_code_default, currency_codes
import common.models as common_models
import common.settings as common_settings
from part.models import PartCategory
from users.models import RuleSet, check_user_role
@@ -357,7 +357,7 @@ class AjaxUpdateView(AjaxMixin, UpdateView):
- Updates model with POST field data
- Performs form and object validation
- If errors exist, re-render the form
- Otherwise, return sucess status
- Otherwise, return success status
"""
self.request = request
@@ -386,7 +386,7 @@ class AjaxUpdateView(AjaxMixin, UpdateView):
if valid:
# Save the updated objec to the database
# Save the updated object to the database
self.save(self.object, form)
self.object = self.get_object()
@@ -447,8 +447,7 @@ class SetPasswordView(AjaxUpdateView):
if valid:
# Old password must be correct
if not user.check_password(old_password):
if user.has_usable_password() and not user.check_password(old_password):
form.add_error('old_password', _('Wrong password provided'))
valid = False
@@ -514,10 +513,10 @@ class SettingsView(TemplateView):
"""Add data for template."""
ctx = super().get_context_data(**kwargs).copy()
ctx['settings'] = InvenTreeSetting.objects.all().order_by('key')
ctx['settings'] = common_models.InvenTreeSetting.objects.all().order_by('key')
ctx["base_currency"] = currency_code_default()
ctx["currencies"] = currency_codes
ctx["base_currency"] = common_settings.currency_code_default()
ctx["currencies"] = common_settings.currency_codes
ctx["rates"] = Rate.objects.filter(backend="InvenTreeExchange")
@@ -525,8 +524,10 @@ class SettingsView(TemplateView):
# When were the rates last updated?
try:
backend = ExchangeBackend.objects.get(name='InvenTreeExchange')
ctx["rates_updated"] = backend.last_update
backend = ExchangeBackend.objects.filter(name='InvenTreeExchange')
if backend.exists():
backend = backend.first()
ctx["rates_updated"] = backend.last_update
except Exception:
ctx["rates_updated"] = None
@@ -620,8 +621,8 @@ class AppearanceSelectView(RedirectView):
def get_user_theme(self):
"""Get current user color theme."""
try:
user_theme = ColorTheme.objects.filter(user=self.request.user).get()
except ColorTheme.DoesNotExist:
user_theme = common_models.ColorTheme.objects.filter(user=self.request.user).get()
except common_models.ColorTheme.DoesNotExist:
user_theme = None
return user_theme
@@ -635,11 +636,15 @@ class AppearanceSelectView(RedirectView):
# Create theme entry if user did not select one yet
if not user_theme:
user_theme = ColorTheme()
user_theme = common_models.ColorTheme()
user_theme.user = request.user
user_theme.name = theme
user_theme.save()
if theme:
try:
user_theme.name = theme
user_theme.save()
except Exception:
pass
return redirect(reverse_lazy('settings'))

View File

@@ -4,9 +4,9 @@ from django.contrib import admin
from import_export.admin import ImportExportModelAdmin
from import_export.fields import Field
import import_export.widgets as widgets
from import_export import widgets
from build.models import Build, BuildItem
from build.models import Build, BuildLine, BuildItem
from InvenTree.admin import InvenTreeResource
import part.models
@@ -87,18 +87,33 @@ class BuildItemAdmin(admin.ModelAdmin):
"""Class for managing the BuildItem model via the admin interface"""
list_display = (
'build',
'stock_item',
'quantity'
)
autocomplete_fields = [
'build',
'bom_item',
'build_line',
'stock_item',
'install_into',
]
class BuildLineAdmin(admin.ModelAdmin):
"""Class for managing the BuildLine model via the admin interface"""
list_display = (
'build',
'bom_item',
'quantity',
)
search_fields = [
'build__title',
'build__reference',
'bom_item__sub_part__name',
]
admin.site.register(Build, BuildAdmin)
admin.site.register(BuildItem, BuildItemAdmin)
admin.site.register(BuildLine, BuildLineAdmin)

View File

@@ -1,5 +1,6 @@
"""JSON API for the Build app."""
from django.db.models import F
from django.urls import include, path, re_path
from django.utils.translation import gettext_lazy as _
from django.contrib.auth.models import User
@@ -9,14 +10,16 @@ from rest_framework.exceptions import ValidationError
from django_filters.rest_framework import DjangoFilterBackend
from django_filters import rest_framework as rest_filters
from InvenTree.api import AttachmentMixin, APIDownloadMixin, ListCreateDestroyAPIView, MetadataView, StatusView
from InvenTree.api import AttachmentMixin, APIDownloadMixin, ListCreateDestroyAPIView, MetadataView
from generic.states import StatusView
from InvenTree.helpers import str2bool, isNull, DownloadFile
from InvenTree.status_codes import BuildStatus
from InvenTree.status_codes import BuildStatus, BuildStatusGroups
from InvenTree.mixins import CreateAPI, RetrieveUpdateDestroyAPI, ListCreateAPI
import common.models
import build.admin
import build.serializers
from build.models import Build, BuildItem, BuildOrderAttachment
from build.models import Build, BuildLine, BuildItem, BuildOrderAttachment
import part.models
from users.models import Owner
from InvenTree.filters import SEARCH_ORDER_FILTER_ALIAS
@@ -41,9 +44,9 @@ class BuildFilter(rest_filters.FilterSet):
def filter_active(self, queryset, name, value):
"""Filter the queryset to either include or exclude orders which are active."""
if str2bool(value):
return queryset.filter(status__in=BuildStatus.ACTIVE_CODES)
return queryset.filter(status__in=BuildStatusGroups.ACTIVE_CODES)
else:
return queryset.exclude(status__in=BuildStatus.ACTIVE_CODES)
return queryset.exclude(status__in=BuildStatusGroups.ACTIVE_CODES)
overdue = rest_filters.BooleanFilter(label='Build is overdue', method='filter_overdue')
@@ -87,6 +90,21 @@ class BuildFilter(rest_filters.FilterSet):
lookup_expr="iexact"
)
project_code = rest_filters.ModelChoiceFilter(
queryset=common.models.ProjectCode.objects.all(),
field_name='project_code'
)
has_project_code = rest_filters.BooleanFilter(label='has_project_code', method='filter_has_project_code')
def filter_has_project_code(self, queryset, name, value):
"""Filter by whether or not the order has a project code"""
if str2bool(value):
return queryset.exclude(project_code=None)
else:
return queryset.filter(project_code=None)
class BuildList(APIDownloadMixin, ListCreateAPI):
"""API endpoint for accessing a list of Build objects.
@@ -112,11 +130,13 @@ class BuildList(APIDownloadMixin, ListCreateAPI):
'completed',
'issued_by',
'responsible',
'project_code',
'priority',
]
ordering_field_aliases = {
'reference': ['reference_int', 'reference'],
'project_code': ['project_code__code'],
}
ordering = '-reference'
@@ -127,6 +147,7 @@ class BuildList(APIDownloadMixin, ListCreateAPI):
'part__name',
'part__IPN',
'part__description',
'project_code__code',
'priority',
]
@@ -250,6 +271,88 @@ class BuildUnallocate(CreateAPI):
return ctx
class BuildLineFilter(rest_filters.FilterSet):
"""Custom filterset for the BuildLine API endpoint."""
class Meta:
"""Meta information for the BuildLineFilter class."""
model = BuildLine
fields = [
'build',
'bom_item',
]
# Fields on related models
consumable = rest_filters.BooleanFilter(label=_('Consumable'), field_name='bom_item__consumable')
optional = rest_filters.BooleanFilter(label=_('Optional'), field_name='bom_item__optional')
tracked = rest_filters.BooleanFilter(label=_('Tracked'), field_name='bom_item__sub_part__trackable')
allocated = rest_filters.BooleanFilter(label=_('Allocated'), method='filter_allocated')
def filter_allocated(self, queryset, name, value):
"""Filter by whether each BuildLine is fully allocated"""
if str2bool(value):
return queryset.filter(allocated__gte=F('quantity'))
else:
return queryset.filter(allocated__lt=F('quantity'))
class BuildLineEndpoint:
"""Mixin class for BuildLine API endpoints."""
queryset = BuildLine.objects.all()
serializer_class = build.serializers.BuildLineSerializer
def get_queryset(self):
"""Override queryset to select-related and annotate"""
queryset = super().get_queryset()
queryset = queryset.select_related(
'build', 'bom_item',
)
queryset = build.serializers.BuildLineSerializer.annotate_queryset(queryset)
return queryset
class BuildLineList(BuildLineEndpoint, ListCreateAPI):
"""API endpoint for accessing a list of BuildLine objects"""
filterset_class = BuildLineFilter
filter_backends = SEARCH_ORDER_FILTER_ALIAS
ordering_fields = [
'part',
'allocated',
'reference',
'quantity',
'consumable',
'optional',
'unit_quantity',
'available_stock',
]
ordering_field_aliases = {
'part': 'bom_item__sub_part__name',
'reference': 'bom_item__reference',
'unit_quantity': 'bom_item__quantity',
'consumable': 'bom_item__consumable',
'optional': 'bom_item__optional',
}
search_fields = [
'bom_item__sub_part__name',
'bom_item__reference',
]
class BuildLineDetail(BuildLineEndpoint, RetrieveUpdateDestroyAPI):
"""API endpoint for detail view of a BuildLine object."""
pass
class BuildOrderContextMixin:
"""Mixin class which adds build order as serializer context variable."""
@@ -276,6 +379,19 @@ class BuildOutputCreate(BuildOrderContextMixin, CreateAPI):
serializer_class = build.serializers.BuildOutputCreateSerializer
class BuildOutputScrap(BuildOrderContextMixin, CreateAPI):
"""API endpoint for scrapping build output(s)."""
queryset = Build.objects.none()
serializer_class = build.serializers.BuildOutputScrapSerializer
def get_serializer_context(self):
"""Add extra context information to the endpoint serializer."""
ctx = super().get_serializer_context()
ctx['to_complete'] = False
return ctx
class BuildOutputComplete(BuildOrderContextMixin, CreateAPI):
"""API endpoint for completing build outputs."""
@@ -359,9 +475,8 @@ class BuildItemFilter(rest_filters.FilterSet):
"""Metaclass option"""
model = BuildItem
fields = [
'build',
'build_line',
'stock_item',
'bom_item',
'install_into',
]
@@ -370,6 +485,11 @@ class BuildItemFilter(rest_filters.FilterSet):
field_name='stock_item__part',
)
build = rest_filters.ModelChoiceFilter(
queryset=build.models.Build.objects.all(),
field_name='build_line__build',
)
tracked = rest_filters.BooleanFilter(label='Tracked', method='filter_tracked')
def filter_tracked(self, queryset, name, value):
@@ -395,10 +515,9 @@ class BuildItemList(ListCreateAPI):
try:
params = self.request.query_params
kwargs['part_detail'] = str2bool(params.get('part_detail', False))
kwargs['build_detail'] = str2bool(params.get('build_detail', False))
kwargs['location_detail'] = str2bool(params.get('location_detail', False))
kwargs['stock_detail'] = str2bool(params.get('stock_detail', True))
for key in ['part_detail', 'location_detail', 'stock_detail', 'build_detail']:
if key in params:
kwargs[key] = str2bool(params.get(key, False))
except AttributeError:
pass
@@ -409,9 +528,8 @@ class BuildItemList(ListCreateAPI):
queryset = BuildItem.objects.all()
queryset = queryset.select_related(
'bom_item',
'bom_item__sub_part',
'build',
'build_line',
'build_line__build',
'install_into',
'stock_item',
'stock_item__location',
@@ -421,7 +539,7 @@ class BuildItemList(ListCreateAPI):
return queryset
def filter_queryset(self, queryset):
"""Customm query filtering for the BuildItem list."""
"""Custom query filtering for the BuildItem list."""
queryset = super().filter_queryset(queryset)
params = self.request.query_params
@@ -473,6 +591,12 @@ build_api_urls = [
re_path(r'^.*$', BuildAttachmentList.as_view(), name='api-build-attachment-list'),
])),
# Build lines
re_path(r'^line/', include([
path(r'<int:pk>/', BuildLineDetail.as_view(), name='api-build-line-detail'),
re_path(r'^.*$', BuildLineList.as_view(), name='api-build-line-list'),
])),
# Build Items
re_path(r'^item/', include([
path(r'<int:pk>/', include([
@@ -489,6 +613,7 @@ build_api_urls = [
re_path(r'^complete/', BuildOutputComplete.as_view(), name='api-build-output-complete'),
re_path(r'^create-output/', BuildOutputCreate.as_view(), name='api-build-output-create'),
re_path(r'^delete-outputs/', BuildOutputDelete.as_view(), name='api-build-output-delete'),
re_path(r'^scrap-outputs/', BuildOutputScrap.as_view(), name='api-build-output-scrap'),
re_path(r'^finish/', BuildFinish.as_view(), name='api-build-finish'),
re_path(r'^cancel/', BuildCancel.as_view(), name='api-build-cancel'),
re_path(r'^unallocate/', BuildUnallocate.as_view(), name='api-build-unallocate'),

View File

@@ -11,10 +11,6 @@ def update_tree(apps, schema_editor):
Build.objects.rebuild()
def nupdate_tree(apps, schema_editor): # pragma: no cover
pass
class Migration(migrations.Migration):
atomic = False
@@ -53,5 +49,5 @@ class Migration(migrations.Migration):
field=models.PositiveIntegerField(db_index=True, default=0, editable=False),
preserve_default=False,
),
migrations.RunPython(update_tree, reverse_code=nupdate_tree),
migrations.RunPython(update_tree, reverse_code=migrations.RunPython.noop),
]

View File

@@ -23,13 +23,6 @@ def add_default_reference(apps, schema_editor):
print(f"\nUpdated build reference for {count} existing BuildOrder objects")
def reverse_default_reference(apps, schema_editor): # pragma: no cover
"""
Do nothing! But we need to have a function here so the whole process is reversible.
"""
pass
class Migration(migrations.Migration):
atomic = False
@@ -49,7 +42,7 @@ class Migration(migrations.Migration):
# Auto-populate the new reference field for any existing build order objects
migrations.RunPython(
add_default_reference,
reverse_code=reverse_default_reference
reverse_code=migrations.RunPython.noop
),
# Now that each build has a non-empty, unique reference, update the field requirements!

View File

@@ -51,14 +51,6 @@ def assign_bom_items(apps, schema_editor):
logger.info(f"Assigned BomItem for {count_valid}/{count_total} entries")
def unassign_bom_items(apps, schema_editor): # pragma: no cover
"""
Reverse migration does not do anything.
Function here to preserve ability to reverse migration
"""
pass
class Migration(migrations.Migration):
dependencies = [
@@ -66,5 +58,5 @@ class Migration(migrations.Migration):
]
operations = [
migrations.RunPython(assign_bom_items, reverse_code=unassign_bom_items),
migrations.RunPython(assign_bom_items, reverse_code=migrations.RunPython.noop),
]

View File

@@ -31,12 +31,6 @@ def build_refs(apps, schema_editor):
build.reference_int = ref
build.save()
def unbuild_refs(apps, schema_editor): # pragma: no cover
"""
Provided only for reverse migration compatibility
"""
pass
class Migration(migrations.Migration):
@@ -49,6 +43,6 @@ class Migration(migrations.Migration):
operations = [
migrations.RunPython(
build_refs,
reverse_code=unbuild_refs
reverse_code=migrations.RunPython.noop
)
]

View File

@@ -50,11 +50,6 @@ def update_build_reference(apps, schema_editor):
print(f"Updated reference field for {n} BuildOrder objects")
def nupdate_build_reference(apps, schema_editor):
"""Reverse migration code. Does nothing."""
pass
class Migration(migrations.Migration):
dependencies = [
@@ -64,6 +59,6 @@ class Migration(migrations.Migration):
operations = [
migrations.RunPython(
update_build_reference,
reverse_code=nupdate_build_reference,
reverse_code=migrations.RunPython.noop,
)
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 3.2.18 on 2023-04-19 00:37
import InvenTree.fields
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('build', '0041_alter_build_title'),
]
operations = [
migrations.AlterField(
model_name='build',
name='notes',
field=InvenTree.fields.InvenTreeNotesField(blank=True, help_text='Markdown notes (optional)', max_length=50000, null=True, verbose_name='Notes'),
),
]

View File

@@ -0,0 +1,28 @@
# Generated by Django 3.2.19 on 2023-05-19 06:04
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('part', '0109_auto_20230517_1048'),
('build', '0042_alter_build_notes'),
]
operations = [
migrations.CreateModel(
name='BuildLine',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('quantity', models.DecimalField(decimal_places=5, default=1, help_text='Required quantity for build order', max_digits=15, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Quantity')),
('bom_item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='build_lines', to='part.bomitem')),
('build', models.ForeignKey(help_text='Build object', on_delete=django.db.models.deletion.CASCADE, related_name='build_lines', to='build.build')),
],
options={
'unique_together': {('build', 'bom_item')},
},
),
]

View File

@@ -0,0 +1,97 @@
# Generated by Django 3.2.19 on 2023-05-28 14:10
from django.db import migrations
def get_bom_items_for_part(part, Part, BomItem):
""" Return a list of all BOM items for a given part.
Note that we cannot use the ORM here (as we are inside a data migration),
so we *copy* the logic from the Part class.
This is a snapshot of the Part.get_bom_items() method as of 2023-05-29
"""
bom_items = set()
# Get all BOM items which directly reference the part
for bom_item in BomItem.objects.filter(part=part):
bom_items.add(bom_item)
# Get all BOM items which are inherited by the part
parents = Part.objects.filter(
tree_id=part.tree_id,
level__lt=part.level,
lft__lt=part.lft,
rght__gt=part.rght
)
for bom_item in BomItem.objects.filter(part__in=parents, inherited=True):
bom_items.add(bom_item)
return list(bom_items)
def add_lines_to_builds(apps, schema_editor):
"""Create BuildOrderLine objects for existing build orders"""
# Get database models
Build = apps.get_model("build", "Build")
BuildLine = apps.get_model("build", "BuildLine")
Part = apps.get_model("part", "Part")
BomItem = apps.get_model("part", "BomItem")
build_lines = []
builds = Build.objects.all()
if builds.count() > 0:
print(f"Creating BuildOrderLine objects for {builds.count()} existing builds")
for build in builds:
# Create a BuildOrderLine for each BuildItem
bom_items = get_bom_items_for_part(build.part, Part, BomItem)
for item in bom_items:
build_lines.append(
BuildLine(
build=build,
bom_item=item,
quantity=item.quantity * build.quantity,
)
)
if len(build_lines) > 0:
# Construct the new BuildLine objects
BuildLine.objects.bulk_create(build_lines)
print(f"Created {len(build_lines)} BuildOrderLine objects for existing builds")
def remove_build_lines(apps, schema_editor):
"""Remove BuildOrderLine objects from the database"""
# Get database models
BuildLine = apps.get_model("build", "BuildLine")
n = BuildLine.objects.all().count()
BuildLine.objects.all().delete()
if n > 0:
print(f"Removed {n} BuildOrderLine objects")
class Migration(migrations.Migration):
dependencies = [
('build', '0043_buildline'),
]
operations = [
migrations.RunPython(
add_lines_to_builds,
reverse_code=remove_build_lines,
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 3.2.19 on 2023-06-06 10:30
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('build', '0044_auto_20230528_1410'),
]
operations = [
migrations.AddField(
model_name='builditem',
name='build_line',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='allocations', to='build.buildline'),
),
]

View File

@@ -0,0 +1,95 @@
# Generated by Django 3.2.19 on 2023-06-06 10:33
import logging
from django.db import migrations
logger = logging.getLogger('inventree')
def add_build_line_links(apps, schema_editor):
"""Data migration to add links between BuildLine and BuildItem objects.
Associated model types:
Build: A "Build Order"
BomItem: An individual line in the BOM for Build.part
BuildItem: An individual stock allocation against the Build Order
BuildLine: (new model) an individual line in the Build Order
Goals:
- Find all BuildItem objects which are associated with a Build
- Link them against the relevant BuildLine object
- The BuildLine objects should have been created in 0044_auto_20230528_1410.py
"""
BuildItem = apps.get_model("build", "BuildItem")
BuildLine = apps.get_model("build", "BuildLine")
# Find any existing BuildItem objects
build_items = BuildItem.objects.all()
n_missing = 0
for item in build_items:
# Find the relevant BuildLine object
line = BuildLine.objects.filter(
build=item.build,
bom_item=item.bom_item
).first()
if line is None:
logger.warning(f"BuildLine does not exist for BuildItem {item.pk}")
n_missing += 1
if item.build is None or item.bom_item is None:
continue
# Create one!
line = BuildLine.objects.create(
build=item.build,
bom_item=item.bom_item,
quantity=item.bom_item.quantity * item.build.quantity
)
# Link the BuildItem to the BuildLine
# In the next data migration, we remove the 'build' and 'bom_item' fields from BuildItem
item.build_line = line
item.save()
if build_items.count() > 0:
logger.info(f"add_build_line_links: Updated {build_items.count()} BuildItem objects (added {n_missing})")
def reverse_build_links(apps, schema_editor):
"""Reverse data migration from add_build_line_links
Basically, iterate through each BuildItem and update the links based on the BuildLine
"""
BuildItem = apps.get_model("build", "BuildItem")
items = BuildItem.objects.all()
for item in items:
item.build = item.build_line.build
item.bom_item = item.build_line.bom_item
item.save()
if items.count() > 0:
logger.info(f"reverse_build_links: Updated {items.count()} BuildItem objects")
class Migration(migrations.Migration):
dependencies = [
('build', '0045_builditem_build_line'),
]
operations = [
migrations.RunPython(
add_build_line_links,
reverse_code=reverse_build_links,
)
]

View File

@@ -0,0 +1,26 @@
# Generated by Django 3.2.19 on 2023-06-06 10:58
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('stock', '0101_stockitemtestresult_metadata'),
('build', '0046_auto_20230606_1033'),
]
operations = [
migrations.AlterUniqueTogether(
name='builditem',
unique_together={('build_line', 'stock_item', 'install_into')},
),
migrations.RemoveField(
model_name='builditem',
name='bom_item',
),
migrations.RemoveField(
model_name='builditem',
name='build',
),
]

View File

@@ -0,0 +1,20 @@
# Generated by Django 3.2.19 on 2023-05-14 09:22
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('common', '0019_projectcode_metadata'),
('build', '0047_auto_20230606_1058'),
]
operations = [
migrations.AddField(
model_name='build',
name='project_code',
field=models.ForeignKey(blank=True, help_text='Project code for this build order', null=True, on_delete=django.db.models.deletion.SET_NULL, to='common.projectcode', verbose_name='Project Code'),
),
]

File diff suppressed because it is too large Load Diff

View File

@@ -4,8 +4,11 @@ from django.db import transaction
from django.core.exceptions import ValidationError as DjangoValidationError
from django.utils.translation import gettext_lazy as _
from django.db.models import Case, When, Value
from django.db import models
from django.db.models import ExpressionWrapper, F, FloatField
from django.db.models import Case, Sum, When, Value
from django.db.models import BooleanField
from django.db.models.functions import Coalesce
from rest_framework import serializers
from rest_framework.serializers import ValidationError
@@ -17,14 +20,15 @@ import InvenTree.helpers
from InvenTree.serializers import InvenTreeDecimalField
from InvenTree.status_codes import StockStatus
from stock.models import StockItem, StockLocation
from stock.models import generate_batch_code, StockItem, StockLocation
from stock.serializers import StockItemSerializerBrief, LocationSerializer
from part.models import BomItem
from part.serializers import PartSerializer, PartBriefSerializer
from common.serializers import ProjectCodeSerializer
import part.filters
from part.serializers import BomItemSerializer, PartSerializer, PartBriefSerializer
from users.serializers import OwnerSerializer
from .models import Build, BuildItem, BuildOrderAttachment
from .models import Build, BuildLine, BuildItem, BuildOrderAttachment
class BuildSerializer(InvenTreeModelSerializer):
@@ -46,6 +50,8 @@ class BuildSerializer(InvenTreeModelSerializer):
'parent',
'part',
'part_detail',
'project_code',
'project_code_detail',
'overdue',
'reference',
'sales_order',
@@ -87,11 +93,13 @@ class BuildSerializer(InvenTreeModelSerializer):
barcode_hash = serializers.CharField(read_only=True)
project_code_detail = ProjectCodeSerializer(source='project_code', many=False, read_only=True)
@staticmethod
def annotate_queryset(queryset):
"""Add custom annotations to the BuildSerializer queryset, performing database queries as efficiently as possible.
The following annoted fields are added:
The following annotated fields are added:
- overdue: True if the build is outstanding *and* the completion date has past
"""
@@ -170,7 +178,7 @@ class BuildOutputSerializer(serializers.Serializer):
if to_complete:
# The build output must have all tracked parts allocated
if not build.is_fully_allocated(output):
if not build.is_output_fully_allocated(output):
# Check if the user has specified that incomplete allocations are ok
accept_incomplete = InvenTree.helpers.str2bool(self.context['request'].data.get('accept_incomplete_allocation', False))
@@ -181,6 +189,45 @@ class BuildOutputSerializer(serializers.Serializer):
return output
class BuildOutputQuantitySerializer(BuildOutputSerializer):
"""Serializer for a single build output, with additional quantity field"""
class Meta:
"""Serializer metaclass"""
fields = BuildOutputSerializer.Meta.fields + [
'quantity',
]
quantity = serializers.DecimalField(
max_digits=15,
decimal_places=5,
min_value=0,
required=True,
label=_('Quantity'),
help_text=_('Enter quantity for build output'),
)
def validate(self, data):
"""Validate the serializer data"""
data = super().validate(data)
output = data.get('output')
quantity = data.get('quantity')
if quantity <= 0:
raise ValidationError({
'quantity': _('Quantity must be greater than zero')
})
if quantity > output.quantity:
raise ValidationError({
'quantity': _("Quantity cannot be greater than the output quantity")
})
return data
class BuildOutputCreateSerializer(serializers.Serializer):
"""Serializer for creating a new BuildOutput against a BuildOrder.
@@ -226,6 +273,7 @@ class BuildOutputCreateSerializer(serializers.Serializer):
batch_code = serializers.CharField(
required=False,
allow_blank=True,
default=generate_batch_code,
label=_('Batch Code'),
help_text=_('Batch code for this build output'),
)
@@ -302,12 +350,14 @@ class BuildOutputCreateSerializer(serializers.Serializer):
auto_allocate = data.get('auto_allocate', False)
build = self.get_build()
user = self.context['request'].user
build.create_build_output(
quantity,
serials=self.serials,
batch=batch_code,
auto_allocate=auto_allocate,
user=user,
)
@@ -349,6 +399,78 @@ class BuildOutputDeleteSerializer(serializers.Serializer):
build.delete_output(output)
class BuildOutputScrapSerializer(serializers.Serializer):
"""DRF serializer for scrapping one or more build outputs"""
class Meta:
"""Serializer metaclass"""
fields = [
'outputs',
'location',
'notes',
]
outputs = BuildOutputQuantitySerializer(
many=True,
required=True,
)
location = serializers.PrimaryKeyRelatedField(
queryset=StockLocation.objects.all(),
many=False,
allow_null=False,
required=True,
label=_('Location'),
help_text=_('Stock location for scrapped outputs'),
)
discard_allocations = serializers.BooleanField(
required=False,
default=False,
label=_('Discard Allocations'),
help_text=_('Discard any stock allocations for scrapped outputs'),
)
notes = serializers.CharField(
label=_('Notes'),
help_text=_('Reason for scrapping build output(s)'),
required=True,
allow_blank=False,
)
def validate(self, data):
"""Perform validation on the serializer data"""
super().validate(data)
outputs = data.get('outputs', [])
if len(outputs) == 0:
raise ValidationError(_("A list of build outputs must be provided"))
return data
def save(self):
"""Save the serializer to scrap the build outputs"""
build = self.context['build']
request = self.context['request']
data = self.validated_data
outputs = data.get('outputs', [])
# Scrap the build outputs
with transaction.atomic():
for item in outputs:
output = item['output']
quantity = item['quantity']
build.scrap_build_output(
output,
quantity,
data.get('location', None),
user=request.user,
notes=data.get('notes', ''),
discard_allocations=data.get('discard_allocations', False)
)
class BuildOutputCompleteSerializer(serializers.Serializer):
"""DRF serializer for completing one or more build outputs."""
@@ -376,8 +498,8 @@ class BuildOutputCompleteSerializer(serializers.Serializer):
)
status = serializers.ChoiceField(
choices=list(StockStatus.items()),
default=StockStatus.OK,
choices=StockStatus.items(),
default=StockStatus.OK.value,
label=_("Status"),
)
@@ -448,7 +570,7 @@ class BuildCancelSerializer(serializers.Serializer):
build = self.context['build']
return {
'has_allocated_stock': build.is_partially_allocated(None),
'has_allocated_stock': build.is_partially_allocated(),
'incomplete_outputs': build.incomplete_count,
'completed_outputs': build.complete_count,
}
@@ -489,7 +611,7 @@ class OverallocationChoice():
TRIM = 'trim'
OPTIONS = {
REJECT: ('Not permitted'),
REJECT: _('Not permitted'),
ACCEPT: _('Accept as consumed by this build order'),
TRIM: _('Deallocate before completing this build order'),
}
@@ -507,8 +629,8 @@ class BuildCompleteSerializer(serializers.Serializer):
build = self.context['build']
return {
'overallocated': build.has_overallocated_parts(),
'allocated': build.are_untracked_parts_allocated(),
'overallocated': build.is_overallocated(),
'allocated': build.are_untracked_parts_allocated,
'remaining': build.remaining,
'incomplete': build.incomplete_count,
}
@@ -525,7 +647,7 @@ class BuildCompleteSerializer(serializers.Serializer):
"""Check if the 'accept_overallocated' field is required"""
build = self.context['build']
if build.has_overallocated_parts(output=None) and value == OverallocationChoice.REJECT:
if build.is_overallocated() and value == OverallocationChoice.REJECT:
raise ValidationError(_('Some stock items have been overallocated'))
return value
@@ -541,7 +663,7 @@ class BuildCompleteSerializer(serializers.Serializer):
"""Check if the 'accept_unallocated' field is required"""
build = self.context['build']
if not build.are_untracked_parts_allocated() and not value:
if not build.are_untracked_parts_allocated and not value:
raise ValidationError(_('Required stock has not been fully allocated'))
return value
@@ -592,12 +714,12 @@ class BuildUnallocationSerializer(serializers.Serializer):
- bom_item: Filter against a particular BOM line item
"""
bom_item = serializers.PrimaryKeyRelatedField(
queryset=BomItem.objects.all(),
build_line = serializers.PrimaryKeyRelatedField(
queryset=BuildLine.objects.all(),
many=False,
allow_null=True,
required=False,
label=_('BOM Item'),
label=_('Build Line'),
)
output = serializers.PrimaryKeyRelatedField(
@@ -628,8 +750,8 @@ class BuildUnallocationSerializer(serializers.Serializer):
data = self.validated_data
build.unallocateStock(
bom_item=data['bom_item'],
build.deallocate_stock(
build_line=data['build_line'],
output=data['output']
)
@@ -640,34 +762,34 @@ class BuildAllocationItemSerializer(serializers.Serializer):
class Meta:
"""Serializer metaclass"""
fields = [
'bom_item',
'build_item',
'stock_item',
'quantity',
'output',
]
bom_item = serializers.PrimaryKeyRelatedField(
queryset=BomItem.objects.all(),
build_line = serializers.PrimaryKeyRelatedField(
queryset=BuildLine.objects.all(),
many=False,
allow_null=False,
required=True,
label=_('BOM Item'),
label=_('Build Line Item'),
)
def validate_bom_item(self, bom_item):
def validate_build_line(self, build_line):
"""Check if the parts match"""
build = self.context['build']
# BomItem should point to the same 'part' as the parent build
if build.part != bom_item.part:
if build.part != build_line.bom_item.part:
# If not, it may be marked as "inherited" from a parent part
if bom_item.inherited and build.part in bom_item.part.get_descendants(include_self=False):
if build_line.bom_item.inherited and build.part in build_line.bom_item.part.get_descendants(include_self=False):
pass
else:
raise ValidationError(_("bom_item.part must point to the same part as the build order"))
return bom_item
return build_line
stock_item = serializers.PrimaryKeyRelatedField(
queryset=StockItem.objects.all(),
@@ -710,8 +832,7 @@ class BuildAllocationItemSerializer(serializers.Serializer):
"""Perform data validation for this item"""
super().validate(data)
build = self.context['build']
bom_item = data['bom_item']
build_line = data['build_line']
stock_item = data['stock_item']
quantity = data['quantity']
output = data.get('output', None)
@@ -733,20 +854,20 @@ class BuildAllocationItemSerializer(serializers.Serializer):
})
# Output *must* be set for trackable parts
if output is None and bom_item.sub_part.trackable:
if output is None and build_line.bom_item.sub_part.trackable:
raise ValidationError({
'output': _('Build output must be specified for allocation of tracked parts'),
})
# Output *cannot* be set for un-tracked parts
if output is not None and not bom_item.sub_part.trackable:
if output is not None and not build_line.bom_item.sub_part.trackable:
raise ValidationError({
'output': _('Build output cannot be specified for allocation of untracked parts'),
})
# Check if this allocation would be unique
if BuildItem.objects.filter(build=build, stock_item=stock_item, install_into=output).exists():
if BuildItem.objects.filter(build_line=build_line, stock_item=stock_item, install_into=output).exists():
raise ValidationError(_('This stock item has already been allocated to this build output'))
return data
@@ -780,24 +901,21 @@ class BuildAllocationSerializer(serializers.Serializer):
items = data.get('items', [])
build = self.context['build']
with transaction.atomic():
for item in items:
bom_item = item['bom_item']
build_line = item['build_line']
stock_item = item['stock_item']
quantity = item['quantity']
output = item.get('output', None)
# Ignore allocation for consumable BOM items
if bom_item.consumable:
if build_line.bom_item.consumable:
continue
try:
# Create a new BuildItem to allocate stock
BuildItem.objects.create(
build=build,
bom_item=bom_item,
build_line=build_line,
stock_item=stock_item,
quantity=quantity,
install_into=output
@@ -879,43 +997,37 @@ class BuildItemSerializer(InvenTreeModelSerializer):
model = BuildItem
fields = [
'pk',
'bom_part',
'build',
'build_detail',
'build_line',
'install_into',
'location',
'location_detail',
'part',
'part_detail',
'stock_item',
'quantity',
'location_detail',
'part_detail',
'stock_item_detail',
'quantity'
'build_detail',
]
bom_part = serializers.IntegerField(source='bom_item.sub_part.pk', read_only=True)
part = serializers.IntegerField(source='stock_item.part.pk', read_only=True)
location = serializers.IntegerField(source='stock_item.location.pk', read_only=True)
# Annotated fields
build = serializers.PrimaryKeyRelatedField(source='build_line.build', many=False, read_only=True)
# Extra (optional) detail fields
part_detail = PartSerializer(source='stock_item.part', many=False, read_only=True)
build_detail = BuildSerializer(source='build', many=False, read_only=True)
part_detail = PartBriefSerializer(source='stock_item.part', many=False, read_only=True)
stock_item_detail = StockItemSerializerBrief(source='stock_item', read_only=True)
location_detail = LocationSerializer(source='stock_item.location', read_only=True)
build_detail = BuildSerializer(source='build_line.build', many=False, read_only=True)
quantity = InvenTreeDecimalField()
def __init__(self, *args, **kwargs):
"""Determine which extra details fields should be included"""
build_detail = kwargs.pop('build_detail', False)
part_detail = kwargs.pop('part_detail', False)
location_detail = kwargs.pop('location_detail', False)
part_detail = kwargs.pop('part_detail', True)
location_detail = kwargs.pop('location_detail', True)
stock_detail = kwargs.pop('stock_detail', False)
build_detail = kwargs.pop('build_detail', False)
super().__init__(*args, **kwargs)
if not build_detail:
self.fields.pop('build_detail')
if not part_detail:
self.fields.pop('part_detail')
@@ -925,6 +1037,144 @@ class BuildItemSerializer(InvenTreeModelSerializer):
if not stock_detail:
self.fields.pop('stock_item_detail')
if not build_detail:
self.fields.pop('build_detail')
class BuildLineSerializer(InvenTreeModelSerializer):
"""Serializer for a BuildItem object."""
class Meta:
"""Serializer metaclass"""
model = BuildLine
fields = [
'pk',
'build',
'bom_item',
'bom_item_detail',
'part_detail',
'quantity',
'allocations',
# Annotated fields
'allocated',
'on_order',
'available_stock',
'available_substitute_stock',
'available_variant_stock',
]
read_only_fields = [
'build',
'bom_item',
'allocations',
]
quantity = serializers.FloatField()
# Foreign key fields
bom_item_detail = BomItemSerializer(source='bom_item', many=False, read_only=True)
part_detail = PartSerializer(source='bom_item.sub_part', many=False, read_only=True)
allocations = BuildItemSerializer(many=True, read_only=True)
# Annotated (calculated) fields
allocated = serializers.FloatField(read_only=True)
on_order = serializers.FloatField(read_only=True)
available_stock = serializers.FloatField(read_only=True)
available_substitute_stock = serializers.FloatField(read_only=True)
available_variant_stock = serializers.FloatField(read_only=True)
@staticmethod
def annotate_queryset(queryset):
"""Add extra annotations to the queryset:
- allocated: Total stock quantity allocated against this build line
- available: Total stock available for allocation against this build line
- on_order: Total stock on order for this build line
"""
# Pre-fetch related fields
queryset = queryset.prefetch_related(
'bom_item__sub_part__stock_items',
'bom_item__sub_part__stock_items__allocations',
'bom_item__sub_part__stock_items__sales_order_allocations',
'bom_item__substitutes',
'bom_item__substitutes__part__stock_items',
'bom_item__substitutes__part__stock_items__allocations',
'bom_item__substitutes__part__stock_items__sales_order_allocations',
)
# Annotate the "allocated" quantity
# Difficulty: Easy
queryset = queryset.annotate(
allocated=Coalesce(
Sum('allocations__quantity'), 0,
output_field=models.DecimalField()
),
)
ref = 'bom_item__sub_part__'
# Annotate the "on_order" quantity
# Difficulty: Medium
queryset = queryset.annotate(
on_order=part.filters.annotate_on_order_quantity(reference=ref),
)
# Annotate the "available" quantity
# TODO: In the future, this should be refactored.
# TODO: Note that part.serializers.BomItemSerializer also has a similar annotation
queryset = queryset.alias(
total_stock=part.filters.annotate_total_stock(reference=ref),
allocated_to_sales_orders=part.filters.annotate_sales_order_allocations(reference=ref),
allocated_to_build_orders=part.filters.annotate_build_order_allocations(reference=ref),
)
# Calculate 'available_stock' based on previously annotated fields
queryset = queryset.annotate(
available_stock=ExpressionWrapper(
F('total_stock') - F('allocated_to_sales_orders') - F('allocated_to_build_orders'),
output_field=models.DecimalField(),
)
)
ref = 'bom_item__substitutes__part__'
# Extract similar information for any 'substitute' parts
queryset = queryset.alias(
substitute_stock=part.filters.annotate_total_stock(reference=ref),
substitute_build_allocations=part.filters.annotate_build_order_allocations(reference=ref),
substitute_sales_allocations=part.filters.annotate_sales_order_allocations(reference=ref)
)
# Calculate 'available_substitute_stock' field
queryset = queryset.annotate(
available_substitute_stock=ExpressionWrapper(
F('substitute_stock') - F('substitute_build_allocations') - F('substitute_sales_allocations'),
output_field=models.DecimalField(),
)
)
# Annotate the queryset with 'available variant stock' information
variant_stock_query = part.filters.variant_stock_query(reference='bom_item__sub_part__')
queryset = queryset.alias(
variant_stock_total=part.filters.annotate_variant_quantity(variant_stock_query, reference='quantity'),
variant_bo_allocations=part.filters.annotate_variant_quantity(variant_stock_query, reference='sales_order_allocations__quantity'),
variant_so_allocations=part.filters.annotate_variant_quantity(variant_stock_query, reference='allocations__quantity'),
)
queryset = queryset.annotate(
available_variant_stock=ExpressionWrapper(
F('variant_stock_total') - F('variant_bo_allocations') - F('variant_so_allocations'),
output_field=FloatField(),
)
)
return queryset
class BuildAttachmentSerializer(InvenTreeAttachmentSerializer):
"""Serializer for a BuildAttachment."""

View File

@@ -12,9 +12,10 @@ from allauth.account.models import EmailAddress
from plugin.events import trigger_event
import common.notifications
import build.models
import InvenTree.helpers
import InvenTree.email
import InvenTree.helpers_model
import InvenTree.tasks
from InvenTree.status_codes import BuildStatus
from InvenTree.status_codes import BuildStatusGroups
from InvenTree.ready import isImportingData
import part.models as part_models
@@ -23,6 +24,55 @@ import part.models as part_models
logger = logging.getLogger('inventree')
def update_build_order_lines(bom_item_pk: int):
"""Update all BuildOrderLineItem objects which reference a particular BomItem.
This task is triggered when a BomItem is created or updated.
"""
logger.info(f"Updating build order lines for BomItem {bom_item_pk}")
bom_item = part_models.BomItem.objects.filter(pk=bom_item_pk).first()
# If the BomItem has been deleted, there is nothing to do
if not bom_item:
return
assemblies = bom_item.get_assemblies()
# Find all active builds which reference any of the parts
builds = build.models.Build.objects.filter(
part__in=list(assemblies),
status__in=BuildStatusGroups.ACTIVE_CODES
)
# Iterate through each build, and update the relevant line items
for bo in builds:
# Try to find a matching build order line
line = build.models.BuildLine.objects.filter(
build=bo,
bom_item=bom_item,
).first()
q = bom_item.get_required_quantity(bo.quantity)
if line:
# Ensure quantity is correct
if line.quantity != q:
line.quantity = q
line.save()
else:
# Create a new line item
build.models.BuildLine.objects.create(
build=bo,
bom_item=bom_item,
quantity=q,
)
if builds.count() > 0:
logger.info(f"Updated {builds.count()} build orders for part {bom_item.part}")
def check_build_stock(build: build.models.Build):
"""Check the required stock for a newly created build order.
@@ -64,7 +114,7 @@ def check_build_stock(build: build.models.Build):
# There is not sufficient stock for this part
lines.append({
'link': InvenTree.helpers.construct_absolute_url(sub_part.get_absolute_url()),
'link': InvenTree.helpers_model.construct_absolute_url(sub_part.get_absolute_url()),
'part': sub_part,
'in_stock': in_stock,
'allocated': allocated,
@@ -88,7 +138,7 @@ def check_build_stock(build: build.models.Build):
logger.info(f"Notifying users of stock required for build {build.pk}")
context = {
'link': InvenTree.helpers.construct_absolute_url(build.get_absolute_url()),
'link': InvenTree.helpers_model.construct_absolute_url(build.get_absolute_url()),
'build': build,
'part': build.part,
'lines': lines,
@@ -101,7 +151,7 @@ def check_build_stock(build: build.models.Build):
recipients = emails.values_list('email', flat=True)
InvenTree.tasks.send_email(subject, '', recipients, html_message=html_message)
InvenTree.email.send_email(subject, '', recipients, html_message=html_message)
def notify_overdue_build_order(bo: build.models.Build):
@@ -121,7 +171,7 @@ def notify_overdue_build_order(bo: build.models.Build):
'order': bo,
'name': name,
'message': _(f"Build order {bo} is now overdue"),
'link': InvenTree.helpers.construct_absolute_url(
'link': InvenTree.helpers_model.construct_absolute_url(
bo.get_absolute_url(),
),
'template': {
@@ -157,7 +207,7 @@ def check_overdue_build_orders():
overdue_orders = build.models.Build.objects.filter(
target_date=yesterday,
status__in=BuildStatus.ACTIVE_CODES
status__in=BuildStatusGroups.ACTIVE_CODES
)
for bo in overdue_orders:

View File

@@ -2,12 +2,12 @@
{% load static %}
{% load i18n %}
{% load status_codes %}
{% load generic %}
{% load inventree_extras %}
{% block page_title %}
{% inventree_title %} | {% trans "Build Order" %} - {{ build }}
{% endblock %}
{% endblock page_title %}
{% block breadcrumbs %}
<li class='breadcrumb-item'><a href='{% url "build-index" %}'>{% trans "Build Orders" %}</a></li>
@@ -15,7 +15,7 @@
{% endblock breadcrumbs %}
{% block thumbnail %}
<img class="part-thumb"
<img alt="{% trans "Part thumbnail" %}" class="part-thumb"
{% if build.part.image %}
src="{{ build.part.image.preview.url }}"
{% else %}
@@ -25,7 +25,7 @@ src="{% static 'img/blank_image.png' %}"
{% block heading %}
{% trans "Build Order" %} {{ build }}
{% endblock %}
{% endblock heading %}
{% block actions %}
<!-- Admin Display -->
@@ -108,6 +108,7 @@ src="{% static 'img/blank_image.png' %}"
<td>{% trans "Build Description" %}</td>
<td>{{ build.title }}</td>
</tr>
{% include "project_code_data.html" with instance=build %}
{% include "barcode_data.html" with instance=build %}
</table>
@@ -117,12 +118,6 @@ src="{% static 'img/blank_image.png' %}"
{% trans "No build outputs have been created for this build order" %}<br>
</div>
{% endif %}
{% if build.parent %}
<div class='alert alert-block alert-info'>
{% object_link 'build-detail' build.parent.id build.parent as link %}
{% blocktrans %}This Build Order is a child of Build Order {{link}}{% endblocktrans %}
</div>
{% endif %}
{% if build.active %}
{% if build.can_complete %}
@@ -147,7 +142,7 @@ src="{% static 'img/blank_image.png' %}"
{% endif %}
{% endif %}
</div>
{% endblock %}
{% endblock details %}
{% block details_right %}
<table class='table table-striped table-condensed'>
@@ -156,7 +151,7 @@ src="{% static 'img/blank_image.png' %}"
<td><span class='fas fa-info'></span></td>
<td>{% trans "Status" %}</td>
<td>
{% build_status_label build.status %}
{% status_label 'build' build.status %}
</td>
</tr>
{% if build.target_date %}
@@ -180,7 +175,7 @@ src="{% static 'img/blank_image.png' %}"
{% else %}
<span class='fa fa-times-circle icon-red'></span>
{% endif %}
<td>{% trans "Completed" %}</td>
<td>{% trans "Completed Outputs" %}</td>
<td>{% progress_bar build.completed build.quantity id='build-completed' max_width='150px' %}</td>
</tr>
{% if build.parent %}
@@ -219,11 +214,11 @@ src="{% static 'img/blank_image.png' %}"
</tr>
{% endif %}
</table>
{% endblock %}
{% endblock details_right %}
{% block page_data %}
<h3>
{% build_status_label build.status large=True %}
{% status_label 'build' build.status large=True %}
{% if build.is_overdue %}
<span class='badge rounded-pill bg-danger'>{% trans "Overdue" %}</span>
{% endif %}
@@ -231,8 +226,7 @@ src="{% static 'img/blank_image.png' %}"
<hr>
<p>{{ build.title }}</p>
{% endblock %}
{% endblock page_data %}
{% block js_ready %}
@@ -288,7 +282,7 @@ src="{% static 'img/blank_image.png' %}"
$('#show-qr-code').click(function() {
showQRDialog(
'{% trans "Build Order QR Code" %}',
'{"build": {{ build.pk }}}'
'{"build": {{ build.pk }} }'
);
});
@@ -312,4 +306,4 @@ src="{% static 'img/blank_image.png' %}"
{% endif %}
{% endif %}
{% endblock %}
{% endblock js_ready %}

View File

@@ -2,11 +2,11 @@
{% load static %}
{% load i18n %}
{% load inventree_extras %}
{% load status_codes %}
{% load generic %}
{% block sidebar %}
{% include "build/sidebar.html" %}
{% endblock %}
{% endblock sidebar %}
{% block page_content %}
@@ -22,12 +22,12 @@
<tr>
<td><span class='fas fa-info'></span></td>
<td>{% trans "Description" %}</td>
<td>{{ build.title }}{% include "clip.html"%}</td>
<td>{{ build.title }}{% include "clip.html" %}</td>
</tr>
<tr>
<td><span class='fas fa-shapes'></span></td>
<td>{% trans "Part" %}</td>
<td><a href="{% url 'part-detail' build.part.id %}?display=build-orders">{{ build.part.full_name }}</a>{% include "clip.html"%}</td>
<td><a href="{% url 'part-detail' build.part.id %}?display=build-orders">{{ build.part.full_name }}</a>{% include "clip.html" %}</td>
</tr>
<tr>
<td></td>
@@ -38,7 +38,7 @@
<td>{% trans "Stock Source" %}</td>
<td>
{% if build.take_from %}
<a href="{% url 'stock-location-detail' build.take_from.id %}">{{ build.take_from }}</a>{% include "clip.html"%}
<a href="{% url 'stock-location-detail' build.take_from.id %}">{{ build.take_from }}</a>{% include "clip.html" %}
{% else %}
<em>{% trans "Stock can be taken from any available location." %}</em>
{% endif %}
@@ -51,7 +51,7 @@
{% if build.destination %}
<a href="{% url 'stock-location-detail' build.destination.id %}">
{{ build.destination }}
</a>{% include "clip.html"%}
</a>{% include "clip.html" %}
{% else %}
<em>{% trans "Destination location not specified" %}</em>
{% endif %}
@@ -60,14 +60,14 @@
<tr>
<td><span class='fas fa-info'></span></td>
<td>{% trans "Status" %}</td>
<td>{% build_status_label build.status %}</td>
<td>{% status_label 'build' build.status %}</td>
</tr>
<tr>
<td><span class='fas fa-check-circle'></span></td>
<td>{% trans "Completed" %}</td>
<td>{% trans "Completed Outputs" %}</td>
<td>{% progress_bar build.completed build.quantity id='build-completed-2' max_width='150px' %}</td>
</tr>
{% if build.active and has_untracked_bom_items %}
{% if build.active %}
<tr>
<td><span class='fas fa-list'></span></td>
<td>{% trans "Allocated Parts" %}</td>
@@ -78,14 +78,14 @@
<tr>
<td><span class='fas fa-layer-group'></span></td>
<td>{% trans "Batch" %}</td>
<td>{{ build.batch }}{% include "clip.html"%}</td>
<td>{{ build.batch }}{% include "clip.html" %}</td>
</tr>
{% endif %}
{% if build.parent %}
<tr>
<td><span class='fas fa-sitemap'></span></td>
<td>{% trans "Parent Build" %}</td>
<td><a href="{% url 'build-detail' build.parent.id %}">{{ build.parent }}</a>{% include "clip.html"%}</td>
<td><a href="{% url 'build-detail' build.parent.id %}">{{ build.parent }}</a>{% include "clip.html" %}</td>
</tr>
{% endif %}
{% if build.priority != 0 %}
@@ -99,14 +99,14 @@
<tr>
<td><span class='fas fa-dolly'></span></td>
<td>{% trans "Sales Order" %}</td>
<td><a href="{% url 'so-detail' build.sales_order.id %}">{{ build.sales_order }}</a>{% include "clip.html"%}</td>
<td><a href="{% url 'so-detail' build.sales_order.id %}">{{ build.sales_order }}</a>{% include "clip.html" %}</td>
</tr>
{% endif %}
{% if build.link %}
<tr>
<td><span class='fas fa-link'></span></td>
<td>{% trans "External Link" %}</td>
<td><a href="{{ build.link }}">{{ build.link }}</a>{% include "clip.html"%}</td>
<td>{% include 'clip_link.html' with link=build.link %}</td>
</tr>
{% endif %}
{% if build.issued_by %}
@@ -165,9 +165,7 @@
</div>
<div class='panel-content'>
<div id='child-button-toolbar'>
<div class='button-toolbar container-fluid float-right'>
{% include "filter_list.html" with id='sub-build' %}
</div>
{% include "filter_list.html" with id='sub-build' %}
</div>
<table class='table table-striped table-condensed' id='sub-build-table' data-toolbar='#child-button-toolbar'></table>
</div>
@@ -179,9 +177,9 @@
<h4>{% trans "Allocate Stock to Build" %}</h4>
{% include "spacer.html" %}
<div class='btn-group' role='group'>
{% if roles.build.add and build.active and has_untracked_bom_items %}
<button class='btn btn-danger' type='button' id='btn-unallocate' title='{% trans "Unallocate stock" %}'>
<span class='fas fa-minus-circle'></span> {% trans "Unallocate Stock" %}
{% if roles.build.add and build.active %}
<button class='btn btn-danger' type='button' id='btn-unallocate' title='{% trans "Deallocate stock" %}'>
<span class='fas fa-minus-circle'></span> {% trans "Deallocate Stock" %}
</button>
<button class='btn btn-primary' type='button' id='btn-auto-allocate' title='{% trans "Automatically allocate stock to build" %}'>
<span class='fas fa-magic'></span> {% trans "Auto Allocate" %}
@@ -199,34 +197,10 @@
</div>
</div>
<div class='panel-content'>
{% if has_untracked_bom_items %}
{% if build.active %}
{% if build.are_untracked_parts_allocated %}
<div class='alert alert-block alert-success'>
{% trans "Untracked stock has been fully allocated for this Build Order" %}
<div id='build-lines-toolbar'>
{% include "filter_list.html" with id='buildlines' %}
</div>
{% else %}
<div class='alert alert-block alert-danger'>
{% trans "Untracked stock has not been fully allocated for this Build Order" %}
</div>
{% endif %}
{% endif %}
<div id='unallocated-toolbar'>
<div class='button-toolbar container-fluid' style='float: right;'>
<div class='btn-group'>
<button id='allocate-selected-items' class='btn btn-success' title='{% trans "Allocate selected items" %}'>
<span class='fas fa-sign-in-alt'></span>
</button>
{% include "filter_list.html" with id='builditems' %}
</div>
</div>
</div>
<table class='table table-striped table-condensed' id='allocation-table-untracked' data-toolbar='#unallocated-toolbar'></table>
{% else %}
<div class='alert alert-block alert-info'>
{% trans "This Build Order does not have any associated untracked BOM items" %}
</div>
{% endif %}
<table class='table table-striped table-condensed' id='build-lines-table' data-toolbar='#build-lines-toolbar'></table>
</div>
</div>
@@ -246,37 +220,24 @@
</div>
<div class='panel-content'>
<div id='build-output-toolbar'>
<div class='button-toolbar container-fluid'>
{% if build.active %}
<div class='btn-group'>
<!-- Build output actions -->
<div class='btn-group'>
<button id='output-options' class='btn btn-primary dropdown-toggle' type='button' data-bs-toggle='dropdown' title='{% trans "Output Actions" %}'>
<span class='fas fa-tools'></span> <span class='caret'></span>
</button>
<ul class='dropdown-menu'>
{% if roles.build.add %}
<li><a class='dropdown-item' href='#' id='multi-output-complete' title='{% trans "Complete selected build outputs" %}'>
<span class='fas fa-check-circle icon-green'></span> {% trans "Complete outputs" %}
</a></li>
{% endif %}
{% if roles.build.delete %}
<li><a class='dropdown-item' href='#' id='multi-output-delete' title='{% trans "Delete selected build outputs" %}'>
<span class='fas fa-trash-alt icon-red'></span> {% trans "Delete outputs" %}
</a></li>
{% endif %}
</ul>
</div>
{% include "filter_list.html" with id='incompletebuilditems' %}
</div>
{% endif %}
</div>
{% include "filter_list.html" with id='incompletebuilditems' %}
</div>
<table class='table table-striped table-condensed' id='build-output-table' data-toolbar='#build-output-toolbar'></table>
</div>
</div>
<div class='panel panel-hidden' id='panel-consumed'>
<div class='panel-heading'>
<h4>
{% trans "Consumed Stock" %}
</h4>
</div>
<div class='panel-content'>
{% include "stock_table.html" with read_only=True prefix="consumed-" %}
</div>
</div>
<div class='panel panel-hidden' id='panel-completed'>
<div class='panel-heading'>
<h4>
@@ -285,7 +246,7 @@
</div>
<div class='panel-content'>
{% include "stock_table.html" with read_only=True prefix="build-" %}
{% include "stock_table.html" with prefix="build-" %}
</div>
</div>
@@ -319,25 +280,32 @@
</div>
</div>
{% endblock %}
{% endblock page_content %}
{% block js_ready %}
{{ block.super }}
onPanelLoad('consumed', function() {
loadStockTable($('#consumed-stock-table'), {
filterTarget: '#filter-list-consumed-stock',
params: {
location_detail: true,
part_detail: true,
consumed_by: {{ build.pk }},
in_stock: false,
},
});
});
onPanelLoad('completed', function() {
loadStockTable($("#build-stock-table"), {
filterTarget: '#filter-list-build-stock',
params: {
location_detail: true,
part_detail: true,
build: {{ build.id }},
is_building: false,
},
groupByField: 'location',
buttons: [
'#stock-options',
],
url: "{% url 'api-stock-list' %}",
});
});
@@ -400,38 +368,15 @@ onPanelLoad('outputs', function() {
{% endif %}
});
{% if build.active and has_untracked_bom_items %}
function loadUntrackedStockTable() {
var build_info = {
pk: {{ build.pk }},
part: {{ build.part.pk }},
quantity: {{ build.quantity }},
{% if build.take_from %}
source_location: {{ build.take_from.pk }},
{% endif %}
tracked_parts: false,
};
$('#allocation-table-untracked').bootstrapTable('destroy');
// Load allocation table for un-tracked parts
loadBuildOutputAllocationTable(
build_info,
null,
{
search: true,
}
);
}
onPanelLoad('allocate', function() {
loadUntrackedStockTable();
// Load the table of line items for this build order
loadBuildLineTable(
"#build-lines-table",
{{ build.pk }},
{}
);
});
{% endif %}
$('#btn-create-output').click(function() {
createBuildOutput(
@@ -453,70 +398,62 @@ $("#btn-auto-allocate").on('click', function() {
{% if build.take_from %}
location: {{ build.take_from.pk }},
{% endif %}
onSuccess: loadUntrackedStockTable,
onSuccess: function() {
$('#build-lines-table').bootstrapTable('refresh');
},
}
);
});
$("#btn-allocate").on('click', function() {
function allocateSelectedLines() {
var bom_items = $("#allocation-table-untracked").bootstrapTable("getData");
let data = getTableData('#build-lines-table');
var incomplete_bom_items = [];
let unallocated_lines = [];
bom_items.forEach(function(bom_item) {
if (bom_item.required > bom_item.allocated) {
incomplete_bom_items.push(bom_item);
data.forEach(function(line) {
if (line.allocated < line.quantity) {
unallocated_lines.push(line);
}
});
if (incomplete_bom_items.length == 0) {
if (unallocated_lines.length == 0) {
showAlertDialog(
'{% trans "Allocation Complete" %}',
'{% trans "All untracked stock items have been allocated" %}',
'{% trans "All lines have been fully allocated" %}',
);
} else {
allocateStockToBuild(
{{ build.pk }},
{{ build.part.pk }},
incomplete_bom_items,
unallocated_lines,
{
{% if build.take_from %}
source_location: {{ build.take_from.pk }},
{% endif %}
success: loadUntrackedStockTable,
success: function() {
$('#build-lines-table').bootstrapTable('refresh');
},
}
);
}
});
}
$('#btn-unallocate').on('click', function() {
unallocateStock({{ build.id }}, {
deallocateStock({{ build.id }}, {
table: '#allocation-table-untracked',
onSuccess: loadUntrackedStockTable,
onSuccess: function() {
$('#build-lines-table').bootstrapTable('refresh');
},
});
});
$('#allocate-selected-items').click(function() {
var bom_items = getTableData('#allocation-table-untracked');
allocateStockToBuild(
{{ build.pk }},
{{ build.part.pk }},
bom_items,
{
{% if build.take_from %}
source_location: {{ build.take_from.pk }},
{% endif %}
success: loadUntrackedStockTable,
}
);
$("#btn-allocate").on('click', function() {
allocateSelectedLines();
});
{% endif %}
enableSidebar('buildorder');
{% endblock %}
{% endblock js_ready %}

View File

@@ -6,11 +6,11 @@
{% block page_title %}
{% inventree_title %} | {% trans "Build Orders" %}
{% endblock %}
{% endblock page_title %}
{% block heading %}
{% trans "Build Orders" %}
{% endblock %}
{% endblock heading %}
{% block actions %}
{% if roles.build.add %}
@@ -18,24 +18,20 @@
<span class='fas fa-tools'></span> {% trans "New Build Order" %}
</button>
{% endif %}
{% endblock %}
{% endblock actions %}
{% block page_info %}
<div class='panel-content'>
<div id='button-toolbar'>
<div class='button-toolbar container-fluid' style='float: right;'>
<div class='btn-group' role='group'>
{% include "filter_list.html" with id="build" %}
</div>
</div>
{% include "filter_list.html" with id="build" %}
</div>
<table class='table table-striped table-condensed' id='build-table' data-toolbar='#button-toolbar'>
</table>
</div>
{% endblock %}
{% endblock page_info %}
{% block js_ready %}
{{ block.super }}
@@ -48,4 +44,4 @@ loadBuildTable($("#build-table"), {
locale: '{{ request.LANGUAGE_CODE }}',
});
{% endblock %}
{% endblock js_ready %}

View File

@@ -4,16 +4,16 @@
{% trans "Build Order Details" as text %}
{% include "sidebar_item.html" with label='details' text=text icon="fa-info-circle" %}
{% if build.active %}
{% if build.is_active %}
{% trans "Allocate Stock" as text %}
{% include "sidebar_item.html" with label='allocate' text=text icon="fa-tasks" %}
{% endif %}
{% if not build.is_complete %}
{% trans "Incomplete Outputs" as text %}
{% include "sidebar_item.html" with label='outputs' text=text icon="fa-tools" %}
{% endif %}
{% trans "Completed Outputs" as text %}
{% include "sidebar_item.html" with label='completed' text=text icon="fa-boxes" %}
{% trans "Consumed Stock" as text %}
{% include "sidebar_item.html" with label='consumed' text=text icon="fa-list" %}
{% trans "Child Build Orders" as text %}
{% include "sidebar_item.html" with label='children' text=text icon="fa-sitemap" %}
{% trans "Attachments" as text %}

View File

@@ -10,8 +10,8 @@ from part.models import Part
from build.models import Build, BuildItem
from stock.models import StockItem
from InvenTree.status_codes import BuildStatus
from InvenTree.api_tester import InvenTreeAPITestCase
from InvenTree.status_codes import BuildStatus, StockStatus
from InvenTree.unit_test import InvenTreeAPITestCase
class TestBuildAPI(InvenTreeAPITestCase):
@@ -298,7 +298,7 @@ class BuildTest(BuildAPITest):
expected_code=400,
)
bo.status = BuildStatus.CANCELLED
bo.status = BuildStatus.CANCELLED.value
bo.save()
# Now, we should be able to delete
@@ -541,10 +541,10 @@ class BuildTest(BuildAPITest):
{
'export': 'csv',
}
) as fo:
) as file:
data = self.process_csv(
fo,
file,
required_cols=required_cols,
excluded_cols=excluded_cols,
required_rows=Build.objects.count()
@@ -582,6 +582,9 @@ class BuildAllocationTest(BuildAPITest):
self.build = Build.objects.get(pk=1)
# Regenerate BuildLine objects
self.build.create_build_line_items()
# Record number of build items which exist at the start of each test
self.n = BuildItem.objects.count()
@@ -593,7 +596,7 @@ class BuildAllocationTest(BuildAPITest):
self.assertEqual(self.build.part.bom_items.count(), 4)
# No items yet allocated to this build
self.assertEqual(self.build.allocated_stock.count(), 0)
self.assertEqual(BuildItem.objects.filter(build_line__build=self.build).count(), 0)
def test_get(self):
"""A GET request to the endpoint should return an error."""
@@ -634,7 +637,7 @@ class BuildAllocationTest(BuildAPITest):
{
"items": [
{
"bom_item": 1, # M2x4 LPHS
"build_line": 1, # M2x4 LPHS
"stock_item": 2, # 5,000 screws available
}
]
@@ -658,7 +661,7 @@ class BuildAllocationTest(BuildAPITest):
expected_code=400
).data
self.assertIn("This field is required", str(data["items"][0]["bom_item"]))
self.assertIn("This field is required", str(data["items"][0]["build_line"]))
# Missing stock_item
data = self.post(
@@ -666,7 +669,7 @@ class BuildAllocationTest(BuildAPITest):
{
"items": [
{
"bom_item": 1,
"build_line": 1,
"quantity": 5000,
}
]
@@ -681,12 +684,25 @@ class BuildAllocationTest(BuildAPITest):
def test_invalid_bom_item(self):
"""Test by passing an invalid BOM item."""
# Find the right (in this case, wrong) BuildLine instance
si = StockItem.objects.get(pk=11)
lines = self.build.build_lines.all()
wrong_line = None
for line in lines:
if line.bom_item.sub_part.pk != si.pk:
wrong_line = line
break
data = self.post(
self.url,
{
"items": [
{
"bom_item": 5,
"build_line": wrong_line.pk,
"stock_item": 11,
"quantity": 500,
}
@@ -695,19 +711,31 @@ class BuildAllocationTest(BuildAPITest):
expected_code=400
).data
self.assertIn('must point to the same part', str(data))
self.assertIn('Selected stock item does not match BOM line', str(data))
def test_valid_data(self):
"""Test with valid data.
This should result in creation of a new BuildItem object
"""
# Find the correct BuildLine
si = StockItem.objects.get(pk=2)
right_line = None
for line in self.build.build_lines.all():
if line.bom_item.sub_part.pk == si.part.pk:
right_line = line
break
self.post(
self.url,
{
"items": [
{
"bom_item": 1,
"build_line": right_line.pk,
"stock_item": 2,
"quantity": 5000,
}
@@ -749,16 +777,22 @@ class BuildOverallocationTest(BuildAPITest):
cls.state = {}
cls.allocation = {}
for i, bi in enumerate(cls.build.part.bom_items.all()):
rq = cls.build.required_quantity(bi, None) + i + 1
si = StockItem.objects.filter(part=bi.sub_part, quantity__gte=rq).first()
items_to_create = []
cls.state[bi.sub_part] = (si, si.quantity, rq)
BuildItem.objects.create(
build=cls.build,
for idx, build_line in enumerate(cls.build.build_lines.all()):
required = build_line.quantity + idx + 1
sub_part = build_line.bom_item.sub_part
si = StockItem.objects.filter(part=sub_part, quantity__gte=required).first()
cls.state[sub_part] = (si, si.quantity, required)
items_to_create.append(BuildItem(
build_line=build_line,
stock_item=si,
quantity=rq,
)
quantity=required,
))
BuildItem.objects.bulk_create(items_to_create)
# create and complete outputs
cls.build.create_build_output(cls.build.quantity)
@@ -822,9 +856,10 @@ class BuildOverallocationTest(BuildAPITest):
self.assertTrue(self.build.is_complete)
# Check stock items have reduced only by bom requirement (overallocation trimmed)
for bi in self.build.part.bom_items.all():
si, oq, _ = self.state[bi.sub_part]
rq = self.build.required_quantity(bi, None)
for line in self.build.build_lines.all():
si, oq, _ = self.state[line.bom_item.sub_part]
rq = line.quantity
si.refresh_from_db()
self.assertEqual(si.quantity, oq - rq)
@@ -843,7 +878,7 @@ class BuildListTest(BuildAPITest):
builds = self.get(self.url, data={'active': True})
self.assertEqual(len(builds.data), 1)
builds = self.get(self.url, data={'status': BuildStatus.COMPLETE})
builds = self.get(self.url, data={'status': BuildStatus.COMPLETE.value})
self.assertEqual(len(builds.data), 4)
builds = self.get(self.url, data={'overdue': False})
@@ -863,7 +898,7 @@ class BuildListTest(BuildAPITest):
reference="BO-0006",
quantity=10,
title='Just some thing',
status=BuildStatus.PRODUCTION,
status=BuildStatus.PRODUCTION.value,
target_date=in_the_past
)
@@ -924,3 +959,130 @@ class BuildListTest(BuildAPITest):
builds = response.data
self.assertEqual(len(builds), 20)
class BuildOutputScrapTest(BuildAPITest):
"""Unit tests for scrapping build outputs"""
def scrap(self, build_id, data, expected_code=None):
"""Helper method to POST to the scrap API"""
url = reverse('api-build-output-scrap', kwargs={'pk': build_id})
response = self.post(url, data, expected_code=expected_code)
return response.data
def test_invalid_scraps(self):
"""Test that invalid scrap attempts are rejected"""
# Test with missing required fields
response = self.scrap(1, {}, expected_code=400)
for field in ['outputs', 'location', 'notes']:
self.assertIn('This field is required', str(response[field]))
# Scrap with no outputs specified
response = self.scrap(
1,
{
'outputs': [],
'location': 1,
'notes': 'Should fail',
}
)
self.assertIn('A list of build outputs must be provided', str(response))
# Scrap with an invalid output ID
response = self.scrap(
1,
{
'outputs': [
{
'output': 9999,
}
],
'location': 1,
'notes': 'Should fail',
},
expected_code=400
)
self.assertIn('object does not exist', str(response['outputs']))
# Create a build output, for a different build
build = Build.objects.get(pk=2)
output = StockItem.objects.create(
part=build.part,
quantity=10,
batch='BATCH-TEST',
is_building=True,
build=build,
)
response = self.scrap(
1,
{
'outputs': [
{
'output': output.pk,
},
],
'location': 1,
'notes': 'Should fail',
},
expected_code=400
)
self.assertIn("Build output does not match the parent build", str(response['outputs']))
def test_valid_scraps(self):
"""Test that valid scrap attempts succeed"""
# Create a build output
build = Build.objects.get(pk=1)
for _ in range(3):
build.create_build_output(2)
outputs = build.build_outputs.all()
self.assertEqual(outputs.count(), 3)
self.assertEqual(StockItem.objects.filter(build=build).count(), 3)
for output in outputs:
self.assertEqual(output.status, StockStatus.OK)
self.assertTrue(output.is_building)
# Scrap all three outputs
self.scrap(
1,
{
'outputs': [
{
'output': outputs[0].pk,
'quantity': outputs[0].quantity,
},
{
'output': outputs[1].pk,
'quantity': outputs[1].quantity,
},
{
'output': outputs[2].pk,
'quantity': outputs[2].quantity,
},
],
'location': 1,
'notes': 'Should succeed',
},
expected_code=201
)
# There should still be three outputs associated with this build
self.assertEqual(StockItem.objects.filter(build=build).count(), 3)
for output in outputs:
output.refresh_from_db()
self.assertEqual(output.status, StockStatus.REJECTED)
self.assertFalse(output.is_building)

View File

@@ -13,7 +13,7 @@ from InvenTree import status_codes as status
import common.models
import build.tasks
from build.models import Build, BuildItem, generate_next_build_reference
from build.models import Build, BuildItem, BuildLine, generate_next_build_reference
from part.models import Part, BomItem, BomItemSubstitute
from stock.models import StockItem
from users.models import Owner
@@ -107,6 +107,11 @@ class BuildTestBase(TestCase):
issued_by=get_user_model().objects.get(pk=1),
)
# Create some BuildLine items we can use later on
cls.line_1 = BuildLine.objects.get(build=cls.build, bom_item=cls.bom_item_1)
cls.line_2 = BuildLine.objects.get(build=cls.build, bom_item=cls.bom_item_2)
cls.line_3 = BuildLine.objects.get(build=cls.build, bom_item=cls.bom_item_3)
# Create some build output (StockItem) objects
cls.output_1 = StockItem.objects.create(
part=cls.assembly,
@@ -141,6 +146,7 @@ class BuildTest(BuildTestBase):
def test_ref_int(self):
"""Test the "integer reference" field used for natural sorting"""
# Set build reference to new value
common.models.InvenTreeSetting.set_setting('BUILDORDER_REFERENCE_PATTERN', 'BO-{ref}-???', change_user=None)
refs = {
@@ -163,6 +169,9 @@ class BuildTest(BuildTestBase):
build.save()
self.assertEqual(build.reference_int, ref_int)
# Set build reference back to default value
common.models.InvenTreeSetting.set_setting('BUILDORDER_REFERENCE_PATTERN', 'BO-{ref:04d}', change_user=None)
def test_ref_validation(self):
"""Test that the reference field validation works as expected"""
@@ -209,6 +218,9 @@ class BuildTest(BuildTestBase):
title='Valid reference',
)
# Set build reference back to default value
common.models.InvenTreeSetting.set_setting('BUILDORDER_REFERENCE_PATTERN', 'BO-{ref:04d}', change_user=None)
def test_next_ref(self):
"""Test that the next reference is automatically generated"""
@@ -233,6 +245,9 @@ class BuildTest(BuildTestBase):
self.assertEqual(build.reference, 'XYZ-000988')
self.assertEqual(build.reference_int, 988)
# Set build reference back to default value
common.models.InvenTreeSetting.set_setting('BUILDORDER_REFERENCE_PATTERN', 'BO-{ref:04d}', change_user=None)
def test_init(self):
"""Perform some basic tests before we start the ball rolling"""
@@ -248,13 +263,10 @@ class BuildTest(BuildTestBase):
for output in self.build.get_build_outputs().all():
self.assertFalse(self.build.is_fully_allocated(output))
self.assertFalse(self.build.is_bom_item_allocated(self.bom_item_1, self.output_1))
self.assertFalse(self.build.is_bom_item_allocated(self.bom_item_2, self.output_2))
self.assertFalse(self.line_1.is_fully_allocated())
self.assertFalse(self.line_2.is_overallocated())
self.assertEqual(self.build.unallocated_quantity(self.bom_item_1, self.output_1), 15)
self.assertEqual(self.build.unallocated_quantity(self.bom_item_1, self.output_2), 35)
self.assertEqual(self.build.unallocated_quantity(self.bom_item_2, self.output_1), 9)
self.assertEqual(self.build.unallocated_quantity(self.bom_item_2, self.output_2), 21)
self.assertEqual(self.line_1.allocated_quantity(), 0)
self.assertFalse(self.build.is_complete)
@@ -264,25 +276,25 @@ class BuildTest(BuildTestBase):
stock = StockItem.objects.create(part=self.assembly, quantity=99)
# Create a BuiltItem which points to an invalid StockItem
b = BuildItem(stock_item=stock, build=self.build, quantity=10)
b = BuildItem(stock_item=stock, build_line=self.line_2, quantity=10)
with self.assertRaises(ValidationError):
b.save()
# Create a BuildItem which has too much stock assigned
b = BuildItem(stock_item=self.stock_1_1, build=self.build, quantity=9999999)
b = BuildItem(stock_item=self.stock_1_1, build_line=self.line_1, quantity=9999999)
with self.assertRaises(ValidationError):
b.clean()
# Negative stock? Not on my watch!
b = BuildItem(stock_item=self.stock_1_1, build=self.build, quantity=-99)
b = BuildItem(stock_item=self.stock_1_1, build_line=self.line_1, quantity=-99)
with self.assertRaises(ValidationError):
b.clean()
# Ok, what about we make one that does *not* fail?
b = BuildItem(stock_item=self.stock_1_2, build=self.build, install_into=self.output_1, quantity=10)
b = BuildItem(stock_item=self.stock_1_2, build_line=self.line_1, install_into=self.output_1, quantity=10)
b.save()
def test_duplicate_bom_line(self):
@@ -302,13 +314,24 @@ class BuildTest(BuildTestBase):
allocations: Map of {StockItem: quantity}
"""
items_to_create = []
for item, quantity in allocations.items():
BuildItem.objects.create(
# Find an appropriate BuildLine to allocate against
line = BuildLine.objects.filter(
build=self.build,
bom_item__sub_part=item.part
).first()
items_to_create.append(BuildItem(
build_line=line,
stock_item=item,
quantity=quantity,
install_into=output
)
))
BuildItem.objects.bulk_create(items_to_create)
def test_partial_allocation(self):
"""Test partial allocation of stock"""
@@ -321,7 +344,7 @@ class BuildTest(BuildTestBase):
}
)
self.assertTrue(self.build.is_fully_allocated(self.output_1))
self.assertTrue(self.build.is_output_fully_allocated(self.output_1))
# Partially allocate tracked stock against build output 2
self.allocate_stock(
@@ -331,7 +354,7 @@ class BuildTest(BuildTestBase):
}
)
self.assertFalse(self.build.is_fully_allocated(self.output_2))
self.assertFalse(self.build.is_output_fully_allocated(self.output_2))
# Partially allocate untracked stock against build
self.allocate_stock(
@@ -342,11 +365,12 @@ class BuildTest(BuildTestBase):
}
)
self.assertFalse(self.build.is_fully_allocated(None))
self.assertFalse(self.build.is_output_fully_allocated(None))
unallocated = self.build.unallocated_bom_items(None)
# Find lines which are *not* fully allocated
unallocated = self.build.unallocated_lines()
self.assertEqual(len(unallocated), 2)
self.assertEqual(len(unallocated), 3)
self.allocate_stock(
None,
@@ -357,17 +381,17 @@ class BuildTest(BuildTestBase):
self.assertFalse(self.build.is_fully_allocated(None))
unallocated = self.build.unallocated_bom_items(None)
self.assertEqual(len(unallocated), 1)
self.build.unallocateStock()
unallocated = self.build.unallocated_bom_items(None)
unallocated = self.build.unallocated_lines()
self.assertEqual(len(unallocated), 2)
self.assertFalse(self.build.are_untracked_parts_allocated())
self.build.deallocate_stock()
unallocated = self.build.unallocated_lines(None)
self.assertEqual(len(unallocated), 3)
self.assertFalse(self.build.is_fully_allocated(tracked=False))
self.stock_2_1.quantity = 500
self.stock_2_1.save()
@@ -381,7 +405,7 @@ class BuildTest(BuildTestBase):
}
)
self.assertTrue(self.build.are_untracked_parts_allocated())
self.assertTrue(self.build.is_fully_allocated(tracked=False))
def test_overallocation_and_trim(self):
"""Test overallocation of stock and trim function"""
@@ -424,24 +448,40 @@ class BuildTest(BuildTestBase):
extra_2_2: 4, # 35
}
)
self.assertTrue(self.build.has_overallocated_parts(None))
self.assertTrue(self.build.is_overallocated())
self.build.trim_allocated_stock()
self.assertFalse(self.build.has_overallocated_parts(None))
self.assertFalse(self.build.is_overallocated())
self.build.complete_build_output(self.output_1, None)
self.build.complete_build_output(self.output_2, None)
self.assertTrue(self.build.can_complete)
n = StockItem.objects.filter(consumed_by=self.build).count()
self.build.complete_build(None)
self.assertEqual(self.build.status, status.BuildStatus.COMPLETE)
# Check stock items are in expected state.
self.assertEqual(StockItem.objects.get(pk=self.stock_1_2.pk).quantity, 53)
self.assertEqual(StockItem.objects.filter(part=self.sub_part_2).aggregate(Sum('quantity'))['quantity__sum'], 5)
# Total stock quantity has not been decreased
items = StockItem.objects.filter(part=self.sub_part_2)
self.assertEqual(items.aggregate(Sum('quantity'))['quantity__sum'], 35)
# However, the "available" stock quantity has been decreased
self.assertEqual(items.filter(consumed_by=None).aggregate(Sum('quantity'))['quantity__sum'], 5)
# And the "consumed_by" quantity has been increased
self.assertEqual(items.filter(consumed_by=self.build).aggregate(Sum('quantity'))['quantity__sum'], 30)
self.assertEqual(StockItem.objects.get(pk=self.stock_3_1.pk).quantity, 980)
# Check that the "consumed_by" item count has increased
self.assertEqual(StockItem.objects.filter(consumed_by=self.build).count(), n + 8)
def test_cancel(self):
"""Test cancellation of the build"""
@@ -510,15 +550,12 @@ class BuildTest(BuildTestBase):
self.assertEqual(BuildItem.objects.count(), 0)
# New stock items should have been created!
self.assertEqual(StockItem.objects.count(), 10)
self.assertEqual(StockItem.objects.count(), 13)
# This stock item has been depleted!
with self.assertRaises(StockItem.DoesNotExist):
StockItem.objects.get(pk=self.stock_1_1.pk)
# This stock item has also been depleted
with self.assertRaises(StockItem.DoesNotExist):
StockItem.objects.get(pk=self.stock_2_1.pk)
# This stock item has been marked as "consumed"
item = StockItem.objects.get(pk=self.stock_1_1.pk)
self.assertIsNotNone(item.consumed_by)
self.assertFalse(item.in_stock)
# And 10 new stock items created for the build output
outputs = StockItem.objects.filter(build=self.build)
@@ -574,12 +611,12 @@ class BuildTest(BuildTestBase):
"""Unit tests for the metadata field."""
# Make sure a BuildItem exists before trying to run this test
b = BuildItem(stock_item=self.stock_1_2, build=self.build, install_into=self.output_1, quantity=10)
b = BuildItem(stock_item=self.stock_1_2, build_line=self.line_1, install_into=self.output_1, quantity=10)
b.save()
for model in [Build, BuildItem]:
p = model.objects.first()
self.assertIsNone(p.metadata)
self.assertEqual(len(p.metadata.keys()), 0)
self.assertIsNone(p.get_metadata('test'))
self.assertEqual(p.get_metadata('test', backup_value=123), 123)
@@ -631,7 +668,7 @@ class AutoAllocationTests(BuildTestBase):
# No build item allocations have been made against the build
self.assertEqual(self.build.allocated_stock.count(), 0)
self.assertFalse(self.build.are_untracked_parts_allocated())
self.assertFalse(self.build.is_fully_allocated(tracked=False))
# Stock is not interchangeable, nothing will happen
self.build.auto_allocate_stock(
@@ -639,15 +676,15 @@ class AutoAllocationTests(BuildTestBase):
substitutes=False,
)
self.assertFalse(self.build.are_untracked_parts_allocated())
self.assertFalse(self.build.is_fully_allocated(tracked=False))
self.assertEqual(self.build.allocated_stock.count(), 0)
self.assertFalse(self.build.is_bom_item_allocated(self.bom_item_1))
self.assertFalse(self.build.is_bom_item_allocated(self.bom_item_2))
self.assertFalse(self.line_1.is_fully_allocated())
self.assertFalse(self.line_2.is_fully_allocated())
self.assertEqual(self.build.unallocated_quantity(self.bom_item_1), 50)
self.assertEqual(self.build.unallocated_quantity(self.bom_item_2), 30)
self.assertEqual(self.line_1.unallocated_quantity(), 50)
self.assertEqual(self.line_2.unallocated_quantity(), 30)
# This time we expect stock to be allocated!
self.build.auto_allocate_stock(
@@ -656,28 +693,27 @@ class AutoAllocationTests(BuildTestBase):
optional_items=True,
)
self.assertFalse(self.build.are_untracked_parts_allocated())
self.assertFalse(self.build.is_fully_allocated(tracked=False))
self.assertEqual(self.build.allocated_stock.count(), 7)
self.assertTrue(self.build.is_bom_item_allocated(self.bom_item_1))
self.assertFalse(self.build.is_bom_item_allocated(self.bom_item_2))
self.assertTrue(self.line_1.is_fully_allocated())
self.assertFalse(self.line_2.is_fully_allocated())
self.assertEqual(self.build.unallocated_quantity(self.bom_item_1), 0)
self.assertEqual(self.build.unallocated_quantity(self.bom_item_2), 5)
self.assertEqual(self.line_1.unallocated_quantity(), 0)
self.assertEqual(self.line_2.unallocated_quantity(), 5)
# This time, allow substitue parts to be used!
# This time, allow substitute parts to be used!
self.build.auto_allocate_stock(
interchangeable=True,
substitutes=True,
)
# self.assertEqual(self.build.allocated_stock.count(), 8)
self.assertEqual(self.build.unallocated_quantity(self.bom_item_1), 0)
self.assertEqual(self.build.unallocated_quantity(self.bom_item_2), 5.0)
self.assertEqual(self.line_1.unallocated_quantity(), 0)
self.assertEqual(self.line_2.unallocated_quantity(), 5)
self.assertTrue(self.build.is_bom_item_allocated(self.bom_item_1))
self.assertFalse(self.build.is_bom_item_allocated(self.bom_item_2))
self.assertTrue(self.line_1.is_fully_allocated())
self.assertFalse(self.line_2.is_fully_allocated())
def test_fully_auto(self):
"""We should be able to auto-allocate against a build in a single go"""
@@ -688,7 +724,7 @@ class AutoAllocationTests(BuildTestBase):
optional_items=True,
)
self.assertTrue(self.build.are_untracked_parts_allocated())
self.assertTrue(self.build.is_fully_allocated(tracked=False))
self.assertEqual(self.build.unallocated_quantity(self.bom_item_1), 0)
self.assertEqual(self.build.unallocated_quantity(self.bom_item_2), 0)
self.assertEqual(self.line_1.unallocated_quantity(), 0)
self.assertEqual(self.line_2.unallocated_quantity(), 0)

View File

@@ -2,14 +2,14 @@
from django_test_migrations.contrib.unittest_case import MigratorTestCase
from InvenTree import helpers
from InvenTree import unit_test
class TestForwardMigrations(MigratorTestCase):
"""Test entire schema migration sequence for the build app."""
migrate_from = ('build', helpers.getOldestMigrationFile('build'))
migrate_to = ('build', helpers.getNewestMigrationFile('build'))
migrate_from = ('build', unit_test.getOldestMigrationFile('build'))
migrate_to = ('build', unit_test.getNewestMigrationFile('build'))
def prepare(self):
"""Create initial data!"""
@@ -19,22 +19,15 @@ class TestForwardMigrations(MigratorTestCase):
name='Widget',
description='Buildable Part',
active=True,
level=0, lft=0, rght=0, tree_id=0,
)
with self.assertRaises(TypeError):
# Cannot set the 'assembly' field as it hasn't been added to the db schema
Part.objects.create(
name='Blorb',
description='ABCDE',
assembly=True
)
Build = self.old_state.apps.get_model('build', 'build')
Build.objects.create(
part=buildable_part,
title='A build of some stuff',
quantity=50
quantity=50,
)
def test_items_exist(self):
@@ -58,7 +51,7 @@ class TestForwardMigrations(MigratorTestCase):
class TestReferenceMigration(MigratorTestCase):
"""Test custom migration which adds 'reference' field to Build model."""
migrate_from = ('build', helpers.getOldestMigrationFile('build'))
migrate_from = ('build', unit_test.getOldestMigrationFile('build'))
migrate_to = ('build', '0018_build_reference')
def prepare(self):
@@ -67,7 +60,8 @@ class TestReferenceMigration(MigratorTestCase):
part = Part.objects.create(
name='Part',
description='A test part'
description='A test part',
level=0, lft=0, rght=0, tree_id=0,
)
Build = self.old_state.apps.get_model('build', 'build')
@@ -113,7 +107,7 @@ class TestReferencePatternMigration(MigratorTestCase):
"""
migrate_from = ('build', '0019_auto_20201019_1302')
migrate_to = ('build', helpers.getNewestMigrationFile('build'))
migrate_to = ('build', unit_test.getNewestMigrationFile('build'))
def prepare(self):
"""Create some initial data prior to migration"""
@@ -158,3 +152,139 @@ class TestReferencePatternMigration(MigratorTestCase):
pattern = Setting.objects.get(key='BUILDORDER_REFERENCE_PATTERN')
self.assertEqual(pattern.value, 'BuildOrder-{ref:04d}')
class TestBuildLineCreation(MigratorTestCase):
"""Test that build lines are correctly created for existing builds.
Ref: https://github.com/inventree/InvenTree/pull/4855
This PR added the 'BuildLine' model, which acts as a link between a Build and a BomItem.
- Migration 0044 creates BuildLine objects for existing builds.
- Migration 0046 links any existing BuildItem objects to corresponding BuildLine
"""
migrate_from = ('build', '0041_alter_build_title')
migrate_to = ('build', '0047_auto_20230606_1058')
def prepare(self):
"""Create data to work with"""
# Model references
Part = self.old_state.apps.get_model('part', 'part')
BomItem = self.old_state.apps.get_model('part', 'bomitem')
Build = self.old_state.apps.get_model('build', 'build')
BuildItem = self.old_state.apps.get_model('build', 'builditem')
StockItem = self.old_state.apps.get_model('stock', 'stockitem')
# The "BuildLine" model does not exist yet
with self.assertRaises(LookupError):
self.old_state.apps.get_model('build', 'buildline')
# Create a part
assembly = Part.objects.create(
name='Assembly',
description='An assembly',
assembly=True,
level=0, lft=0, rght=0, tree_id=0,
)
# Create components
for idx in range(1, 11):
part = Part.objects.create(
name=f"Part {idx}",
description=f"Part {idx}",
level=0, lft=0, rght=0, tree_id=0,
)
# Create plentiful stock
StockItem.objects.create(
part=part,
quantity=1000,
level=0, lft=0, rght=0, tree_id=0,
)
# Create a BOM item
BomItem.objects.create(
part=assembly,
sub_part=part,
quantity=idx,
reference=f"REF-{idx}",
)
# Create some builds
for idx in range(1, 4):
build = Build.objects.create(
part=assembly,
title=f"Build {idx}",
quantity=idx * 10,
reference=f"REF-{idx}",
level=0, lft=0, rght=0, tree_id=0,
)
# Allocate stock to the build
for bom_item in BomItem.objects.all():
stock_item = StockItem.objects.get(part=bom_item.sub_part)
BuildItem.objects.create(
build=build,
bom_item=bom_item,
stock_item=stock_item,
quantity=bom_item.quantity,
)
def test_build_line_creation(self):
"""Test that the BuildLine objects have been created correctly"""
Build = self.new_state.apps.get_model('build', 'build')
BomItem = self.new_state.apps.get_model('part', 'bomitem')
BuildLine = self.new_state.apps.get_model('build', 'buildline')
BuildItem = self.new_state.apps.get_model('build', 'builditem')
StockItem = self.new_state.apps.get_model('stock', 'stockitem')
# There should be 3x builds
self.assertEqual(Build.objects.count(), 3)
# 10x BOMItem objects
self.assertEqual(BomItem.objects.count(), 10)
# 10x StockItem objects
self.assertEqual(StockItem.objects.count(), 10)
# And 30x BuildLine items (1 for each BomItem for each Build)
self.assertEqual(BuildLine.objects.count(), 30)
# And 30x BuildItem objects (1 for each BomItem for each Build)
self.assertEqual(BuildItem.objects.count(), 30)
# Check that each BuildItem has been linked to a BuildLine
for item in BuildItem.objects.all():
self.assertIsNotNone(item.build_line)
self.assertEqual(
item.stock_item.part,
item.build_line.bom_item.sub_part,
)
item = BuildItem.objects.first()
# Check that the "build" field has been removed
with self.assertRaises(AttributeError):
item.build
# Check that the "bom_item" field has been removed
with self.assertRaises(AttributeError):
item.bom_item
# Check that each BuildLine is correctly configured
for line in BuildLine.objects.all():
# Check that the quantity is correct
self.assertEqual(
line.quantity,
line.build.quantity * line.bom_item.quantity,
)
# Check that the linked parts are correct
self.assertEqual(
line.build.part,
line.bom_item.part,
)

View File

@@ -4,7 +4,7 @@ from django.urls import reverse
from datetime import datetime, timedelta
from InvenTree.helpers import InvenTreeTestCase
from InvenTree.unit_test import InvenTreeTestCase
from .models import Build
from stock.models import StockItem

View File

@@ -39,7 +39,5 @@ class BuildDetail(InvenTreeRoleMixin, InvenTreePluginViewMixin, DetailView):
part = build.part
ctx['part'] = part
ctx['has_tracked_bom_items'] = build.has_tracked_bom_items()
ctx['has_untracked_bom_items'] = build.has_untracked_bom_items()
return ctx

View File

@@ -2,6 +2,7 @@
import json
from django.conf import settings
from django.http.response import HttpResponse
from django.urls import include, path, re_path
from django.utils.decorators import method_decorator
@@ -17,13 +18,13 @@ from rest_framework.views import APIView
import common.models
import common.serializers
from InvenTree.api import BulkDeleteMixin
from InvenTree.api import BulkDeleteMixin, MetadataView
from InvenTree.config import CONFIG_LOOKUPS
from InvenTree.filters import ORDER_FILTER, SEARCH_ORDER_FILTER
from InvenTree.helpers import inheritors
from InvenTree.mixins import (ListAPI, RetrieveAPI, RetrieveUpdateAPI,
RetrieveUpdateDestroyAPI)
from InvenTree.permissions import IsSuperuser
from InvenTree.mixins import (ListAPI, ListCreateAPI, RetrieveAPI,
RetrieveUpdateAPI, RetrieveUpdateDestroyAPI)
from InvenTree.permissions import IsStaffOrReadOnly, IsSuperuser
from plugin.models import NotificationUserSetting
from plugin.serializers import NotificationUserSettingSerializer
@@ -45,7 +46,7 @@ class WebhookView(CsrfExemptMixin, APIView):
run_async = False
def post(self, request, endpoint, *args, **kwargs):
"""Process incomming webhook."""
"""Process incoming webhook."""
# get webhook definition
self._get_webhook(endpoint, request, *args, **kwargs)
@@ -121,8 +122,13 @@ class CurrencyExchangeView(APIView):
# Information on last update
try:
backend = ExchangeBackend.objects.get(name='InvenTreeExchange')
updated = backend.last_update
backend = ExchangeBackend.objects.filter(name='InvenTreeExchange')
if backend.exists():
backend = backend.first()
updated = backend.last_update
else:
updated = None
except Exception:
updated = None
@@ -164,7 +170,7 @@ class CurrencyRefreshView(APIView):
class SettingsList(ListAPI):
"""Generic ListView for settings.
This is inheritted by all list views for settings.
This is inherited by all list views for settings.
"""
filter_backends = SEARCH_ORDER_FILTER
@@ -440,6 +446,69 @@ class ConfigDetail(RetrieveAPI):
return {key: value}
class NotesImageList(ListCreateAPI):
"""List view for all notes images."""
queryset = common.models.NotesImage.objects.all()
serializer_class = common.serializers.NotesImageSerializer
permission_classes = [permissions.IsAuthenticated, ]
def perform_create(self, serializer):
"""Create (upload) a new notes image"""
image = serializer.save()
image.user = self.request.user
image.save()
class ProjectCodeList(ListCreateAPI):
"""List view for all project codes."""
queryset = common.models.ProjectCode.objects.all()
serializer_class = common.serializers.ProjectCodeSerializer
permission_classes = [permissions.IsAuthenticated, IsStaffOrReadOnly]
filter_backends = SEARCH_ORDER_FILTER
ordering_fields = [
'code',
]
search_fields = [
'code',
'description',
]
class ProjectCodeDetail(RetrieveUpdateDestroyAPI):
"""Detail view for a particular project code"""
queryset = common.models.ProjectCode.objects.all()
serializer_class = common.serializers.ProjectCodeSerializer
permission_classes = [permissions.IsAuthenticated, IsStaffOrReadOnly]
class FlagList(ListAPI):
"""List view for feature flags."""
queryset = settings.FLAGS
serializer_class = common.serializers.FlagSerializer
permission_classes = [permissions.AllowAny, ]
class FlagDetail(RetrieveAPI):
"""Detail view for an individual feature flag."""
serializer_class = common.serializers.FlagSerializer
permission_classes = [permissions.AllowAny, ]
def get_object(self):
"""Attempt to find a config object with the provided key."""
key = self.kwargs['key']
value = settings.FLAGS.get(key, None)
if not value:
raise NotFound()
return {key: value}
settings_api_urls = [
# User settings
re_path(r'^user/', include([
@@ -456,7 +525,7 @@ settings_api_urls = [
path(r'<int:pk>/', NotificationUserSettingsDetail.as_view(), name='api-notification-setting-detail'),
# Notification Settings List
re_path(r'^.*$', NotificationUserSettingsList.as_view(), name='api-notifcation-setting-list'),
re_path(r'^.*$', NotificationUserSettingsList.as_view(), name='api-notification-setting-list'),
])),
# Global settings
@@ -473,6 +542,18 @@ common_api_urls = [
# Webhooks
path('webhook/<slug:endpoint>/', WebhookView.as_view(), name='api-webhook'),
# Uploaded images for notes
re_path(r'^notes-image-upload/', NotesImageList.as_view(), name='api-notes-image-list'),
# Project codes
re_path(r'^project-code/', include([
path(r'<int:pk>/', include([
re_path(r'^metadata/', MetadataView.as_view(), {'model': common.models.ProjectCode}, name='api-project-code-metadata'),
re_path(r'^.*$', ProjectCodeDetail.as_view(), name='api-project-code-detail'),
])),
re_path(r'^.*$', ProjectCodeList.as_view(), name='api-project-code-list'),
])),
# Currencies
re_path(r'^currency/', include([
re_path(r'^exchange/', CurrencyExchangeView.as_view(), name='api-currency-exchange'),
@@ -500,6 +581,11 @@ common_api_urls = [
re_path(r'^.*$', NewsFeedEntryList.as_view(), name='api-news-list'),
])),
# Flags
path('flags/', include([
path('<str:key>/', FlagDetail.as_view(), name='api-flag-detail'),
re_path(r'^.*$', FlagList.as_view(), name='api-flag-list'),
])),
]
admin_api_urls = [

View File

@@ -59,7 +59,8 @@ class FileManager:
# Reset stream position to beginning of file
file.seek(0)
else:
raise ValidationError(_(f'Unsupported file format: {ext.upper()}'))
fmt = ext.upper()
raise ValidationError(_(f'Unsupported file format: {fmt}'))
except UnicodeEncodeError:
raise ValidationError(_('Error reading file (invalid encoding)'))
@@ -83,7 +84,7 @@ class FileManager:
self.HEADERS = self.REQUIRED_HEADERS + self.ITEM_MATCH_HEADERS + self.OPTIONAL_MATCH_HEADERS + self.OPTIONAL_HEADERS
def setup(self):
"""Setup headers should be overriden in usage to set the Different Headers."""
"""Setup headers should be overridden in usage to set the Different Headers."""
if not self.name:
return
@@ -180,7 +181,7 @@ class FileManager:
for i in range(self.row_count()):
data = [item for item in self.get_row_data(i)]
data = list(self.get_row_data(i))
# Is the row completely empty? Skip!
empty = True

View File

@@ -46,7 +46,7 @@ class MatchFieldForm(forms.Form):
"""Step 2 of FileManagementFormView."""
def __init__(self, *args, **kwargs):
"""Setup filemanager and check columsn."""
"""Setup filemanager and check columns."""
# Get FileManager
file_manager = None
if 'file_manager' in kwargs:
@@ -106,7 +106,7 @@ class MatchItemForm(forms.Form):
# Set field name
field_name = col_guess.lower() + '-' + str(row['index'])
# check if field def was overriden
# check if field def was overridden
overriden_field = self.get_special_field(col_guess, row, file_manager)
if overriden_field:
self.fields[field_name] = overriden_field
@@ -174,5 +174,5 @@ class MatchItemForm(forms.Form):
)
def get_special_field(self, col_guess, row, file_manager):
"""Function to be overriden in inherited forms to add specific form settings."""
"""Function to be overridden in inherited forms to add specific form settings."""
return None

View File

@@ -4,15 +4,40 @@ import django.core.validators
from django.db import migrations, models
class CreateModelOrSkip(migrations.CreateModel):
"""Custom migration operation to create a model if it does not already exist.
- If the model already exists, the migration is skipped
- This class has been added to deal with some errors being thrown in CI tests
- The 'common_currency' table doesn't exist anymore anyway!
- In the future, these migrations will be squashed
"""
def database_forwards(self, app_label, schema_editor, from_state, to_state) -> None:
"""Forwards migration *attempts* to create the model, but will fail gracefully if it already exists"""
try:
super().database_forwards(app_label, schema_editor, from_state, to_state)
except Exception:
pass
def state_forwards(self, app_label, state) -> None:
try:
super().state_forwards(app_label, state)
except Exception:
pass
class Migration(migrations.Migration):
initial = True
atomic = False
dependencies = [
]
operations = [
migrations.CreateModel(
CreateModelOrSkip(
name='Currency',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),

View File

@@ -8,6 +8,16 @@ def set_default_currency(apps, schema_editor):
""" migrate the currency setting from config.yml to db """
# get value from settings-file
base_currency = get_setting('INVENTREE_BASE_CURRENCY', 'base_currency', 'USD')
from common.settings import currency_codes
# check if value is valid
if base_currency not in currency_codes():
if len (currency_codes()) > 0:
base_currency = currency_codes()[0]
else:
base_currency = 'USD'
# write to database
InvenTreeSetting.set_setting('INVENTREE_DEFAULT_CURRENCY', base_currency, None, create=True)

View File

@@ -0,0 +1,27 @@
# Generated by Django 3.2.18 on 2023-04-17 05:55
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import common.models
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('common', '0016_alter_notificationentry_updated'),
]
operations = [
migrations.CreateModel(
name='NotesImage',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('image', models.ImageField(help_text='Image file', upload_to=common.models.rename_notes_image, verbose_name='Image')),
('date', models.DateTimeField(auto_now_add=True)),
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
],
),
]

View File

@@ -0,0 +1,21 @@
# Generated by Django 3.2.18 on 2023-04-19 02:06
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('common', '0017_notesimage'),
]
operations = [
migrations.CreateModel(
name='ProjectCode',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('code', models.CharField(help_text='Unique project code', max_length=50, unique=True, verbose_name='Project Code')),
('description', models.CharField(blank=True, help_text='Project description', max_length=200, verbose_name='Description')),
],
),
]

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