Compare commits

...

381 Commits

Author SHA1 Message Date
Oliver
7c09c20725 Update version.py
Bump version number
2020-04-05 00:56:16 +11:00
Oliver
cffb921fb1 Merge pull request #694 from SchrodingersGat/thumbnail-image
Thumbnail image
2020-04-05 00:55:32 +11:00
Oliver Walters
2d3e7e35af Fix middleware due to failing tests 2020-04-05 00:46:15 +11:00
Oliver Walters
8b61acb048 PEP fixes 2020-04-05 00:38:25 +11:00
Oliver Walters
0cfb293ca9 List API now uses the thumbnail image 2020-04-05 00:19:37 +11:00
Oliver Walters
afa31b3415 Generate thumbnails for any part images existing in the database 2020-04-05 00:19:05 +11:00
Oliver Walters
d505e79be8 Allow token-based access to /media/ and /static/
- The InvenTree app needs to access the images, but currently token auth only works for the /api/ endpoint
- The app cannot use csrf tokens!
- So, borrow the tokens which are already created per-user in the DRF framework
- If a user is not authed, then check for a token!
- See InvenTree/middleware.py for further documentation
2020-04-04 23:29:05 +11:00
Oliver Walters
aee0970e49 Part image field now uses django-stdimage 2020-04-04 15:47:05 +11:00
Oliver
7ee94f3574 Merge pull request #692 from SchrodingersGat/api-improvements
Api improvements
2020-04-03 12:37:03 +11:00
Oliver Walters
b25df586cd Fix API tets 2020-04-03 12:30:58 +11:00
Oliver Walters
92f5648656 Fix API endpoints for Stock app 2020-04-03 12:20:43 +11:00
Oliver Walters
ccb637773f Add item count to StockLocation serializer 2020-04-03 11:41:51 +11:00
Oliver Walters
d4da6211be StockItem: filtering improvements
- Optional 'cacade' param
- Filter by null parent
2020-04-03 11:40:37 +11:00
Oliver Walters
fb94949538 Allow StockLocation filtering of null parent 2020-04-03 11:34:42 +11:00
Oliver Walters
f5150f549a Part API changes
- Allow filtering parts with null parent (top-level category parts)
- Option to include sub-category parts or not
2020-04-03 09:37:03 +11:00
Oliver Walters
6e65a736e7 Add isNull function to query against null keys 2020-04-03 09:31:26 +11:00
Oliver Walters
d17056820b Allow PartCategory filtering by null parent 2020-04-03 09:25:58 +11:00
Oliver Walters
7e8664a4dd Include "parts" count in Category API 2020-04-03 09:15:09 +11:00
Oliver
40822a93df Merge pull request #691 from SchrodingersGat/default-currency
auto-fill price break with default quantity
2020-03-31 22:14:10 +11:00
Oliver Walters
67a73c1fbf auto-fill price break with default quantity 2020-03-31 22:11:16 +11:00
Oliver
9d19029ba9 Merge pull request #690 from SchrodingersGat/bug-fixes
Bug fixes
2020-03-31 21:43:42 +11:00
Oliver Walters
c31b72bde2 type checking 2020-03-31 21:40:23 +11:00
Oliver Walters
6919eaa1e1 Update translations 2020-03-31 21:33:50 +11:00
Oliver Walters
124967ed31 Remove trailing zeros in part order form 2020-03-31 21:30:34 +11:00
Oliver Walters
570010b99c Change POLineItem quantity to a rounding decimal field 2020-03-31 21:23:57 +11:00
Oliver Walters
4c96b34c7c Override prepare_value method of RoundingDecimalFormField
- Remove trailing zeros in form field display
2020-03-31 21:21:39 +11:00
Oliver Walters
f07f3b99cf Remove 'notes' field from PurchaseOrder edit / create form 2020-03-31 20:48:44 +11:00
Oliver
cdc4c5d6d5 Merge pull request #684 from SchrodingersGat/build-status-label-fix
Display proper build status label
2020-03-30 17:18:46 +11:00
Oliver Walters
34c097c46a Display proper build status label 2020-03-30 16:48:14 +11:00
Oliver
e28fe6df6a Merge pull request #682 from SchrodingersGat/low-stock
"Low Stock" badge
2020-03-30 15:35:53 +11:00
Oliver Walters
0dc6d9d37e Improved visual layout 2020-03-30 15:04:56 +11:00
Oliver Walters
5aec3df7c9 Add stock-info labels to Part info page 2020-03-30 13:37:34 +11:00
Oliver Walters
06f28898a0 separate display for "no stock" and "low stock" in list view 2020-03-30 13:31:14 +11:00
Oliver Walters
e8e0ab8416 Include 'minimum_stock' information in part list api 2020-03-30 13:21:33 +11:00
Oliver
00eada2c3c Merge pull request #679 from SchrodingersGat/on-order-fix
Better filtering of annotations for Part-list API
2020-03-26 18:07:53 +11:00
Oliver Walters
c0650ba7f4 Add "buiding" icon in part list if no stock and none on order 2020-03-26 17:57:49 +11:00
Oliver Walters
713d7960a8 Fix on_order calculation
- Take into account the number "received"
- Also fix unit tests
2020-03-26 17:56:44 +11:00
Oliver Walters
6a78f6d451 Include quantity currently being build in Part API 2020-03-26 17:43:02 +11:00
Oliver Walters
41bbbdcd43 Improve query speed when calculating how many parts are on order 2020-03-26 17:31:59 +11:00
Oliver Walters
57123283f4 Better filtering of annotations for Part-list API 2020-03-26 17:08:01 +11:00
Oliver
864a21ac85 Merge pull request #678 from SchrodingersGat/on-order
Add "On Order" badge
2020-03-26 14:53:54 +11:00
Oliver Walters
99efbd4c40 If a part has no stock but is on order, display an "on-order" badge 2020-03-26 14:46:40 +11:00
Oliver Walters
dae45875fb Add 'on_order' quantity to the part list API 2020-03-26 14:46:23 +11:00
Oliver
d0f71ea6de Merge pull request #676 from maxbachmann/master
use rapidfuzz instead of fuzzywuzzy
2020-03-23 09:18:38 +11:00
maxbachmann
b162c97226 use rapidfuzz instead of fuzzywuzzy 2020-03-22 22:31:15 +01:00
Oliver
c6f069028c Update version.py
bump version number
2020-03-22 20:19:16 +11:00
Oliver
b6bc5e3bff Merge pull request #675 from SchrodingersGat/po-attachment
Po attachment
2020-03-22 20:18:09 +11:00
Oliver Walters
25caec4c53 Fix unit testings 2020-03-22 20:13:38 +11:00
Oliver
7d095213a5 Merge pull request #646 from inventree/dependabot/pip/django-2.2.10
Bump django from 2.2.9 to 2.2.10
2020-03-22 20:13:18 +11:00
Oliver Walters
672119fb92 Update README.md 2020-03-22 20:07:36 +11:00
Oliver Walters
7f269898c4 Flake - ignore media and static dirs 2020-03-22 20:02:13 +11:00
Oliver Walters
82be9db3df Make a fancy badge 2020-03-22 19:57:51 +11:00
Oliver Walters
90aa205057 Update translations 2020-03-22 19:57:37 +11:00
Oliver Walters
4a259dc146 Can now successfully edit or delete a purchase-order attachment 2020-03-22 19:55:46 +11:00
Oliver Walters
5af2fae120 Simplify URLs for purchase orders 2020-03-22 19:47:08 +11:00
Oliver Walters
834f80698b Create a new attachment against a PurchaseOrder 2020-03-22 18:41:41 +11:00
Oliver Walters
56a6943438 Add an 'attachment' page for the PurchaseOrder view 2020-03-22 18:13:34 +11:00
Oliver Walters
cc41752f9f Add PurchaseOrderAttachment model
- File attachment against PurchaseOrder
2020-03-22 18:02:53 +11:00
Oliver Walters
a661d7e1a6 Abstract the PartAttachment class
Now "Attachments" are much easier to implement for different models
2020-03-22 17:59:23 +11:00
Oliver
3b6ed585ab Merge pull request #671 from SchrodingersGat/allocation-add
Bugfix: Build Allocation
2020-03-19 10:29:00 +11:00
Oliver Walters
01f1ac49e3 Improve SupplierPart detail page 2020-03-19 10:23:41 +11:00
Oliver Walters
5207b2ba21 Add build status label to part detail 2020-03-19 10:20:09 +11:00
Oliver Walters
6fd0380196 Display item overage in the allocation list 2020-03-19 10:16:58 +11:00
Oliver Walters
15bc457714 Improve calculation of BOM item overage 2020-03-19 10:15:43 +11:00
Oliver Walters
3fd0cf67b6 Fix summation of build allocation items 2020-03-19 09:01:22 +11:00
Oliver
daa8496157 Merge pull request #669 from SchrodingersGat/round-fix
Implement auto-rounding decimal field
2020-03-19 08:47:29 +11:00
Oliver
1259cea2c3 Merge pull request #668 from SchrodingersGat/part-creation-details
Part creation details
2020-03-18 22:23:35 +11:00
Oliver Walters
6731bc1b06 Implement auto-rounding decimal field
Ref: https://stackoverflow.com/questions/37958130/automatically-round-djangos-decimalfield-according-to-the-max-digits-and-decima
2020-03-18 22:22:40 +11:00
Oliver Walters
d51ac2f5c2 Save creation user when making a new part via the API 2020-03-18 22:00:32 +11:00
Oliver Walters
a147ce4284 Save the current user when creating a new part 2020-03-18 21:53:02 +11:00
Oliver Walters
8186e4bab0 Display creation information in part detail page 2020-03-18 21:50:38 +11:00
Oliver Walters
2b08b0f2b9 Add new fields for Part object
- Creation date
- Creation user
- Responsible user
2020-03-18 21:50:18 +11:00
Oliver
febd1ad4a7 Merge pull request #667 from SchrodingersGat/hide-template-parts
Limit choices for Part selection
2020-03-18 21:32:29 +11:00
Oliver Walters
8eaaf62eda Limit choices for Part selection
Based on is_template / virtual / active status
2020-03-18 21:28:11 +11:00
Oliver
b2cb41f879 Merge pull request #666 from SchrodingersGat/build-allocation
Build allocation fix
2020-03-18 21:07:44 +11:00
Oliver Walters
c04aa1bff7 Increase unit testing for BOM item model 2020-03-18 21:04:37 +11:00
Oliver Walters
3b9f57fc80 Bug fix - Multiplying float by decimal
- Overage percentage now uses explicit decimal multiplication
2020-03-18 20:44:45 +11:00
Oliver Walters
33ffa2f75f Add option to make stock-table read-only
- Default table is not read-only
2020-03-18 20:37:25 +11:00
Oliver
1fa4e7f5fb Merge pull request #658 from SchrodingersGat/part-notes-fix
Allow 'notes' field in Part object to be blank
2020-02-23 20:06:26 +11:00
Oliver Walters
4a2fa36e30 Allow 'notes' field in Part object to be blank 2020-02-23 20:02:33 +11:00
Oliver
6c415bc922 Merge pull request #653 from SchrodingersGat/stock-item-tree
Stock item tree
2020-02-19 00:02:59 +11:00
Oliver Walters
068c237c6e remove failing test 2020-02-18 23:59:37 +11:00
Oliver
826102e10e Merge pull request #654 from SchrodingersGat/error-fix
Catch a ProgrammingError if table does not exist
2020-02-18 11:13:44 +11:00
Oliver Walters
066d69215f Catch a ProgrammingError if table does not exist 2020-02-18 10:44:01 +11:00
Oliver Walters
49118d8083 Do not let a StockItem be deleted if child items exist 2020-02-18 10:41:06 +11:00
Oliver Walters
49d5573f8b Bug fix: Update child/parent relationship when a StockItem is deleted
- Pass the child items up to the parent of the deleted item
- Fix unit tests
2020-02-18 08:42:55 +11:00
Oliver Walters
9e456f5a11 Flake fix 2020-02-18 08:15:05 +11:00
Oliver Walters
0f4d60dceb StockItem LIST API can now be filtered by StocKItem status 2020-02-17 23:32:43 +11:00
Oliver Walters
23aebab6d0 Display list of build outputs in the Build tab
- Allow StockList api to be filtered by Build id
2020-02-17 23:31:23 +11:00
Oliver Walters
e483b42df6 Logic fix for StockItem splitting
- The original is left in place
- The new item is moved
2020-02-17 22:56:54 +11:00
Oliver Walters
3715c5d637 Set the parent relationship when serializing StockItem object
- Keep track of which StockItem is came from
2020-02-17 22:44:41 +11:00
Oliver Walters
ae4ebab957 Display table of StockItems which have been split from the current item
- The StockItem list api now allows filtering by 'ancestor'
- Add 'children' tab for StockItem
- Needed to tweak the unit testing fixtures (yay thanks MPTT)
2020-02-17 22:37:55 +11:00
Oliver Walters
4f266958e3 Add custom migration
- Required to initialize the MPTT fields for the StockItem model
2020-02-17 22:11:44 +11:00
Oliver Walters
750dfcda07 Add 'parent' field for StockItem
- Allows StockItem to be tracked when it is split into multiple items
- Uses MPTT field
2020-02-17 21:52:31 +11:00
Oliver
0071a29af7 Merge pull request #649 from SchrodingersGat/doc-fix
Doc fix
2020-02-16 09:32:56 +11:00
Oliver Walters
64c567474a Doc fix 2020-02-16 09:25:28 +11:00
dependabot[bot]
8a42d9f2fa Bump django from 2.2.9 to 2.2.10
Bumps [django](https://github.com/django/django) from 2.2.9 to 2.2.10.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/2.2.9...2.2.10)

Signed-off-by: dependabot[bot] <support@github.com>
2020-02-12 03:32:31 +00:00
Oliver
5261e96c8a Revert to django 2.2.9
2.2.10 causes issues

```ModuleNotFoundError: No module named 'django.utils'```
2020-02-12 14:32:09 +11:00
Oliver
f03f6c4386 Merge pull request #644 from SchrodingersGat/supplier-part-tabs
Supplier part tabs
2020-02-12 12:55:54 +11:00
Oliver
0a9f2b37cf Merge pull request #645 from inventree/dependabot/pip/django-2.2.10
Bump django from 2.2.9 to 2.2.10
2020-02-12 12:47:03 +11:00
Oliver Walters
7dcc94b106 Filter purchaseorder API by supplier part 2020-02-12 12:44:52 +11:00
Oliver Walters
33d21594da Create new stock item from supplierpart stock page
- Allow stock-item-create form to have supplierpart passed as initial data
- Add some translations too
2020-02-12 11:32:01 +11:00
Oliver Walters
6d80788618 Allow stock-filtering and export using SupplierPart ID 2020-02-12 11:16:00 +11:00
Oliver Walters
8dd8505a2c More tab updates
- Add Stock tab
- Add Stock table for supplier part
- Allow stock API to be filtered by supplier-part ID
- Add Orders tab
2020-02-12 11:09:37 +11:00
dependabot[bot]
05f7b30ab0 Bump django from 2.2.9 to 2.2.10
Bumps [django](https://github.com/django/django) from 2.2.9 to 2.2.10.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/2.2.9...2.2.10)

Signed-off-by: dependabot[bot] <support@github.com>
2020-02-12 00:09:16 +00:00
Oliver Walters
d9d21395d9 Add a 'pricing' tab for SupplierPart 2020-02-12 10:48:25 +11:00
Oliver Walters
63b70614b6 Create 'tabs' for SupplierPart view 2020-02-12 10:42:45 +11:00
Oliver
34c3320cd5 Merge pull request #642 from SchrodingersGat/rounding-fixes
Rounding fixes
2020-02-12 10:31:41 +11:00
Oliver Walters
50fee1bfe5 More work on translations 2020-02-12 10:25:46 +11:00
Oliver Walters
28e9d842bf Small tweaks for BOM export
- Steps towards preventing circular BOMs
- Improve formatting of exported BOM
2020-02-12 10:18:20 +11:00
Oliver Walters
d04fb0d826 Use decimal2string instead of normalize 2020-02-12 10:08:35 +11:00
Oliver Walters
520b8d7b2b Fix for BOM pricing weirdness 2020-02-12 10:06:17 +11:00
Oliver Walters
c287a0a0b9 More rounding improvements 2020-02-12 08:22:55 +11:00
Oliver Walters
265ed5115a Float parsing on StockItem tracking page 2020-02-12 08:19:08 +11:00
Oliver Walters
53d1040875 Fix allocation count 2020-02-12 08:14:04 +11:00
Oliver Walters
5ae7ca71d7 Improve number rendering of build allocation page 2020-02-12 08:12:26 +11:00
Oliver Walters
564635c368 Add some translatable strings 2020-02-12 08:11:59 +11:00
Oliver
31d6c77143 Merge pull request #641 from SchrodingersGat/cascading-bom
Provide form for user to select export options
2020-02-11 22:47:11 +11:00
Oliver Walters
882bda46b4 Fix tests 2020-02-11 22:38:50 +11:00
Oliver Walters
f865573e48 Implement cascading export of BOM 2020-02-11 22:32:36 +11:00
Oliver Walters
434d084371 Provide form for user to select export options 2020-02-11 21:43:17 +11:00
Oliver
eecc435c02 Merge pull request #639 from SchrodingersGat/use-same-img
Use same thumbnail for multiple parts
2020-02-11 20:30:08 +11:00
Oliver Walters
55aa63dab4 Override save() method for Part model
- Delete old thumbnails if they are no longer being used
2020-02-11 20:27:06 +11:00
Oliver Walters
77c950a729 Fixed unit tests 2020-02-11 00:39:02 +11:00
Oliver Walters
dee47bdea8 Prevent django_cleanup from deleting part thumbs that are used elsewhere
- Will need to implement a method for automatically deleting part thumbs...
2020-02-11 00:29:29 +11:00
Oliver Walters
8ea1086b03 Make thumb buttons only visible on mouseover 2020-02-11 00:28:46 +11:00
Oliver Walters
e0e996a6c3 Add buttons to select or upload part images 2020-02-11 00:00:03 +11:00
Oliver Walters
d4fe83170f Select existing image and upload successfully 2020-02-10 23:48:45 +11:00
Oliver Walters
534b60d4b8 Print out MEDIA_ROOT directory if in debug mode 2020-02-10 23:43:41 +11:00
Oliver Walters
725eb3c538 Do not duplicate images when copying a part
- Simply reference the existing image
2020-02-10 23:04:58 +11:00
Oliver Walters
17c10da10e Display existing images in a form 2020-02-10 22:57:36 +11:00
Oliver Walters
a82e219336 Add translatable strings for part views 2020-02-10 22:10:06 +11:00
Oliver Walters
1327c1d3b1 Add API endpoint for querying part images 2020-02-10 22:03:06 +11:00
Oliver
356624a8fb Merge pull request #638 from SchrodingersGat/build-date-fix
Build date fix
2020-02-10 21:42:12 +11:00
Oliver Walters
66c1a2ef57 test fix 2020-02-10 21:36:57 +11:00
Oliver Walters
cb29ff14e0 Change auto_now field to auto_now_add
- Build creation date should no longer get erroneously updated
2020-02-10 21:34:41 +11:00
Oliver
3e977834c5 Merge pull request #637 from SchrodingersGat/serial-fix
Serial fix
2020-02-07 08:42:29 +11:00
Oliver Walters
4bd4f2a0a3 Fix for bug b)
- Don't attempt to save if there are duplicates
- Fix overwritten variable name
- Provide correct return data to the form
2020-02-06 23:22:55 +11:00
Oliver Walters
2949289fab Fix for bug a)
- Would not create new StockItem for trackable part if Serial Numbers not provided
2020-02-06 23:11:47 +11:00
Oliver
0939ffeb76 Merge pull request #633 from SchrodingersGat/inventree-settings
Inventree settings
2020-02-03 21:33:34 +11:00
Oliver Walters
41336bd549 Fixes 2020-02-03 21:28:47 +11:00
Oliver Walters
d059aff4f8 Use the part_deep_copy setting to set the default deep_copy value when duplicating a part 2020-02-03 21:14:06 +11:00
Oliver Walters
9cef038d6a IPN must match regex validator (if one is provided) 2020-02-03 21:09:24 +11:00
Oliver Walters
356b6cf15b Load default settings on InvenTree launch 2020-02-03 20:51:53 +11:00
Oliver
4b8e44bc4a Merge pull request #632 from SchrodingersGat/database-stats
Database stats
2020-02-02 22:18:32 +11:00
Oliver Walters
ef7fca5633 PEP fixes 2020-02-02 22:15:46 +11:00
Oliver Walters
244d364575 Display some basic stats 2020-02-02 22:13:10 +11:00
Oliver Walters
aa210efad6 Simple skelton for database stats view 2020-02-02 22:03:31 +11:00
Oliver Walters
91ca37c84b Add stats link to navbac
- Also add translation layer for the navbar
2020-02-02 21:51:23 +11:00
Oliver
d62d4c9355 Merge pull request #631 from SchrodingersGat/allow-dupe-names
Allow PartCategory and StocKLocation names to be non-unique
2020-02-02 21:46:07 +11:00
Oliver Walters
d0a7a24649 Add translatable strings 2020-02-02 21:43:10 +11:00
Oliver Walters
5264f816f1 Allow PartCategory and StocKLocation names to be non-unique
- As long as they are unique in the current tree level
2020-02-02 21:40:03 +11:00
Oliver
2a7bf94793 Merge pull request #629 from SchrodingersGat/doc-fix
Fix docs
2020-02-02 21:11:25 +11:00
Oliver
868e005445 Merge pull request #630 from chschlue/updloc
Update German translation
2020-02-02 21:10:33 +11:00
Oliver Walters
d827070585 Fix docs 2020-02-02 18:11:29 +11:00
Christian Schlüter
e379b44606 Update German translation 2020-02-02 08:10:48 +01:00
Oliver
d71fd1aad4 Merge pull request #628 from SchrodingersGat/backup-fix
Slight tweak to makefile
2020-02-02 18:09:41 +11:00
Oliver Walters
38be1fc696 Don't force backup step as part of **make update 2020-02-02 18:05:50 +11:00
Oliver
a022b8223e Update version.py 2020-02-02 12:46:42 +11:00
Oliver
23c0d68330 Merge pull request #626 from SchrodingersGat/markdown-notes
Markdown notes
2020-02-02 12:45:24 +11:00
Oliver Walters
1a32e441b7 Add //TODO entry in stock tabs 2020-02-02 12:42:35 +11:00
Oliver Walters
20273f1541 Add commit date information to about window 2020-02-02 12:39:35 +11:00
Oliver Walters
f88f5a39f8 Visual fix for allocation tab 2020-02-02 12:16:31 +11:00
Oliver Walters
1bdcbd1974 Markdownify the 'notes' field for StockItem
- New tab interface for the StockItem page
- Display / editing of notes field with markdown
2020-02-02 12:11:18 +11:00
Oliver Walters
908e2ef8bc Add glyphicon for company notes 2020-02-02 11:54:09 +11:00
Oliver Walters
b46151b406 travis fix (hopefully?) 2020-02-02 11:52:22 +11:00
Oliver Walters
0f92468462 Add icon to signify if notes exist 2020-02-02 11:48:43 +11:00
Oliver Walters
7ec194a14a Markdownify the notes field for PurchaseOrder
- Update model field
- Create tab view for PO page
- Add 'notes' tab
2020-02-02 11:44:44 +11:00
Oliver Walters
a7846940c4 Markdownify the notes field for Build model 2020-02-02 00:00:19 +11:00
Oliver Walters
51fab36074 Display / editing for Company notes field
- Also includes some translation updates
2020-02-01 23:45:28 +11:00
Oliver Walters
ca9f9e047c Make company notes field markdownable 2020-02-01 23:31:45 +11:00
Oliver Walters
1f71a93d88 Fix page formatting 2020-02-01 23:26:54 +11:00
Oliver Walters
88ec40e454 Fix success_url for notes form 2020-02-01 22:25:35 +11:00
Oliver Walters
f0933f216c PEP fixes 2020-02-01 17:29:58 +11:00
Oliver Walters
919662054c Alter markdownify settings to properly render images, headings 2020-02-01 14:49:28 +11:00
Oliver Walters
b9dda51378 Side-by-side live editing for markdown 2020-02-01 14:40:11 +11:00
Oliver Walters
3c3ae43c18 Add special view for displaying / editing notes field for part 2020-02-01 13:36:09 +11:00
Oliver Walters
c546ed5dcd Update requirements
- Use markdownify for rendering
- Use markdownx for editing
2020-01-31 21:42:30 +11:00
Oliver Walters
da01177d23 Blank 'notes' page for Part model 2020-01-31 20:38:29 +11:00
Oliver Walters
aa2f63830e Translation template for part attachments page 2020-01-31 20:37:54 +11:00
Oliver Walters
596d06cf1a Add a markdown editor for the 'Notes" field of Part model
https://github.com/timmyomahony/django-pagedown
2020-01-31 20:28:54 +11:00
Oliver
b5f8635794 Merge pull request #618 from SchrodingersGat/security-fix
Update django version
2020-01-19 21:38:51 +11:00
Oliver Walters
442c6e8b27 Update django version 2020-01-19 21:33:24 +11:00
Oliver
8275271ea0 Merge pull request #613 from SchrodingersGat/more-unit-testing
Some more unit tests
2020-01-07 21:22:00 +11:00
Oliver Walters
bd653f2c49 Some more unit tests 2020-01-07 21:16:01 +11:00
Oliver
561cc7a1dd Merge pull request #609 from SchrodingersGat/order-unit-testing
Write unit test for PurchaseOrder receive views
2020-01-06 20:53:55 +11:00
Oliver Walters
a064ce13fc PEP fixes 2020-01-06 20:51:12 +11:00
Oliver Walters
e103bd8880 Write unit test for PurchaseOrder receive views 2020-01-06 20:50:16 +11:00
Oliver
3def3e1e89 Merge pull request #605 from SchrodingersGat/order-receive-fix
Bug fix - receiving lines against a PO
2020-01-06 10:09:31 +11:00
Oliver Walters
61897cb0fc Updated translation files 2020-01-06 09:24:29 +11:00
Oliver Walters
067d2be1f0 Bug fix - receiving lines against a PO caused issues due to integer/Decimal conversion 2020-01-06 09:23:13 +11:00
Oliver
3098f6045f Merge pull request #604 from SchrodingersGat/add-quantity-field
Add quantity field
2020-01-06 09:02:08 +11:00
Oliver Walters
740d7678d7 Update translation files 2020-01-06 08:57:13 +11:00
Oliver Walters
b3ec748123 Display current stock item quantity in stock-adjust modal form 2020-01-06 08:52:28 +11:00
Oliver
d2d5909701 Merge pull request #601 from SchrodingersGat/trailing-fix
Trailing fix
2020-01-02 20:32:16 +11:00
Oliver Walters
43d47686c5 Style fixes 2020-01-02 20:27:07 +11:00
Oliver Walters
dae74a19d3 Fix logic for decimal string helper 2020-01-02 20:25:59 +11:00
Oliver
84f8bab418 Merge pull request #593 from olagino/germanTranslation
Added german translation
2019-12-21 19:22:47 +11:00
Leon Schnieber
720485709b Applied suggestions and corrected typos 2019-12-20 13:33:34 +01:00
Oliver
fa480c0558 Update README.md
Add info on Inventree-Docker
2019-12-20 14:23:06 +11:00
Leon Schnieber
17c048e8dd added german translation 2019-12-19 17:51:42 +01:00
Leon Schnieber
a0534dafec added german translation 2019-12-19 17:47:15 +01:00
Oliver
89e60d28b0 Merge pull request #592 from SchrodingersGat/order-filters
Various bug fixes
2019-12-09 22:24:09 +11:00
Oliver Walters
6cd3b3176c Regenerate translation files
- Also fix documentation for making a virtual envirtonment
2019-12-09 22:17:21 +11:00
Oliver Walters
fafd0397bc remove defunct file 2019-12-09 21:56:26 +11:00
Oliver Walters
71c1faf9ff Use the client-side PO table in more places 2019-12-09 21:55:00 +11:00
Oliver Walters
a257f94ac0 Use client-side rendering for list of purchase orders 2019-12-09 21:33:27 +11:00
Oliver Walters
25e5a64cee Improve filtering / ordering / sorting for purchase-order API 2019-12-09 21:19:35 +11:00
Oliver Walters
5e9b012031 Bug fix for static lookup of blank image 2019-12-09 20:40:04 +11:00
Oliver
0ef033f800 Merge pull request #591 from SchrodingersGat/bug-fixes
normalize decimal fields
2019-12-06 22:43:32 +11:00
Oliver Walters
1a6f06cceb normalize decimal fields 2019-12-06 22:40:27 +11:00
Oliver
d5d4cbaec1 Merge pull request #589 from SchrodingersGat/flake-fix
Ignore venv files for PEP checking
2019-12-05 23:10:43 +11:00
Oliver Walters
ef37eada2f Tweak docs 2019-12-05 23:07:36 +11:00
Oliver Walters
000229fcbb Ignore venv files for PEP checking 2019-12-05 23:05:28 +11:00
Oliver
ee063f7508 Merge pull request #588 from SchrodingersGat/venv
Add documentation for creating a virtual environment
2019-12-05 23:01:30 +11:00
Oliver Walters
f648433e53 Add documentation for creating a virtual environment 2019-12-05 22:56:20 +11:00
Oliver
bd2a2b5b26 Merge pull request #587 from inventree/dependabot/pip/pillow-6.2.0
Bump pillow from 5.0.0 to 6.2.0
2019-12-05 14:44:51 +11:00
Oliver
9946fbda17 Merge pull request #585 from SchrodingersGat/tweaks
Reload page after ordering parts
2019-12-05 14:28:34 +11:00
dependabot[bot]
bacdb7776b Bump pillow from 5.0.0 to 6.2.0
Bumps [pillow](https://github.com/python-pillow/Pillow) from 5.0.0 to 6.2.0.
- [Release notes](https://github.com/python-pillow/Pillow/releases)
- [Changelog](https://github.com/python-pillow/Pillow/blob/master/CHANGES.rst)
- [Commits](https://github.com/python-pillow/Pillow/compare/5.0.0...6.2.0)

Signed-off-by: dependabot[bot] <support@github.com>
2019-12-05 03:19:21 +00:00
Oliver
6afb657acb Merge pull request #586 from SchrodingersGat/po-tweaks
Po tweaks
2019-12-05 14:18:55 +11:00
Oliver Walters
98374ca466 Update to more recent libraries
- Specify exact module versions
2019-12-05 14:12:05 +11:00
Oliver Walters
173e1311d4 Hard code PIP requirements 2019-12-05 10:29:33 +11:00
Oliver Walters
2152cb14b4 Add translation files 2019-12-05 10:29:23 +11:00
Oliver Walters
7f2804dff3 Add button to mark a purchase order as complete, even if not all line items are received 2019-12-05 10:29:16 +11:00
Oliver Walters
3f172cb065 Add 'new location' button when receiving parts by individual line 2019-12-05 09:12:37 +11:00
Oliver Walters
cbdea9f18c Reload page after ordering parts 2019-11-29 20:37:34 +11:00
Oliver
4b599b7cdb Merge pull request #580 from SchrodingersGat/decimal-quantity
Decimal quantity
2019-11-19 21:46:51 +11:00
Oliver Walters
7e6c5fae62 Display units in part table 2019-11-19 10:39:40 +11:00
Oliver Walters
8b2f1b9313 Better rendering of aggregated stock count in stock table
https://stackoverflow.com/questions/11832914/round-to-at-most-2-decimal-places-only-if-necessary#12830454
2019-11-19 10:36:01 +11:00
Oliver Walters
64db28be67 Fix __str__ function to get unit tests to pass 2019-11-19 10:31:49 +11:00
Oliver Walters
381becef79 Convert some more fields to decimal
- purchase order line item quantity
- purchase order line item received
2019-11-19 10:30:04 +11:00
Oliver Walters
a1f33c4084 Change build allocation quantity to decimal field 2019-11-19 10:22:46 +11:00
Oliver Walters
dbdbe69f7f Bug fix for test cases 2019-11-19 10:19:52 +11:00
Oliver Walters
9da8189899 Allow non-integer stock movement 2019-11-19 10:17:20 +11:00
Oliver Walters
e4bfe43c04 More rendering improvements 2019-11-19 10:10:23 +11:00
Oliver Walters
003a2d9f3c Allow creation of stockitem with non-integer quantity
-  Also provided more translation strings
2019-11-19 10:00:08 +11:00
Oliver Walters
0ea8ade26c Better rendering for build allocation page
- Added translations too
2019-11-19 09:51:35 +11:00
Oliver Walters
75774771dc Changes to StockItem model
- Stock adjustments need to accept decimal values
2019-11-19 09:18:41 +11:00
Oliver Walters
20755a6dac Rendering of decimal value on stockitem page 2019-11-19 09:10:47 +11:00
Oliver Walters
4e1b9efe93 Fix javascript rendering of decimal quantity in BOM table 2019-11-19 09:08:17 +11:00
Oliver Walters
6e90ac367e Massaging unit tests
- Decimal fields are useful but VERY ANNOYING to use
- Needed to fix some test cases
2019-11-19 08:59:56 +11:00
Oliver Walters
400941c10f Change item quantity field from PositiveInteger to Decimal
- Allow 'partial' quantity e.g. '0.45kg'
- Need to change some maths functions as Decimal type is pernickity
2019-11-19 08:49:54 +11:00
Oliver Walters
81a226c760 Added translation strings for stock.models 2019-11-19 08:46:25 +11:00
Oliver Walters
5ffbfe8eb8 Add translation strings for part models 2019-11-19 08:42:10 +11:00
Oliver
39f9aa6141 Merge pull request #579 from SchrodingersGat/multi-stock-delete
Multi stock delete
2019-11-16 20:44:45 +11:00
Oliver Walters
16f3dfb678 Removed old migration file 2019-11-16 20:39:10 +11:00
Oliver Walters
b7473be8ef Update stock adjustment dialog 2019-11-16 20:29:05 +11:00
Oliver Walters
2261973331 Ability to delete multiple stock items 2019-11-16 20:19:10 +11:00
Oliver Walters
789515e39d Add translations for the StockItem detail page 2019-11-16 20:14:08 +11:00
Oliver Walters
0effb584b9 Remove 'active' field
- Will work this change in at a later date
2019-11-16 20:13:51 +11:00
Oliver Walters
339126b27a Add new field "active" to StockItem model
- True by default
- Set to 'false' to mark a stockitem as 'deleted'
2019-11-16 19:41:36 +11:00
Oliver Walters
56255a98d8 Add a menu item to delete multiple stock items 2019-11-16 19:28:47 +11:00
Oliver
d66c20ca94 Merge pull request #576 from SchrodingersGat/req-fix
Lock specific version of coverage
2019-11-05 20:56:26 +11:00
Oliver Walters
ce6f54aeaa Lock specific version of coverage 2019-11-05 20:23:15 +11:00
Oliver
b92b7dc825 Merge pull request #575 from SchrodingersGat/stock-table-notes-fix
Properly display 'notes' field in grouped rows for stock table
2019-11-05 11:13:17 +11:00
Oliver Walters
1887463f7f Properly display 'notes' field in grouped rows for stock table 2019-11-04 21:55:48 +11:00
Oliver
54855da522 Merge pull request #565 from SchrodingersGat/supplier-part-delete
Single form to delete single or multiple SupplierPart objects
2019-09-30 13:48:10 +10:00
Oliver Walters
d1c7877713 Add unit test for CompanyIndex 2019-09-30 13:44:19 +10:00
Oliver Walters
be96a2f7e3 Add some unit tests 2019-09-30 13:39:56 +10:00
Oliver Walters
871b853b9f Single form to delete single or multiple SupplierPart objects 2019-09-30 13:28:51 +10:00
Oliver
f49130862e Merge pull request #564 from SchrodingersGat/secondary-fix
Bug fix - secondary modals not working correctly
2019-09-27 21:07:32 +10:00
Oliver Walters
ab25a199ce Bug fix - secondary modals not working correctly 2019-09-27 21:00:27 +10:00
Oliver
bed2cec5e7 Merge pull request #562 from SchrodingersGat/stock-top-detail
Stock top detail
2019-09-27 10:18:50 +10:00
Oliver Walters
b870728125 Add translation hooks for part detail template 2019-09-27 10:12:46 +10:00
Oliver Walters
225ad0ffa6 Add note in contribution file regarding translations 2019-09-27 10:07:34 +10:00
Oliver Walters
427f47310b Add detail pane to top-level part view 2019-09-27 10:04:20 +10:00
Oliver Walters
3e2a5263a5 Add stock information for top-level stock page 2019-09-27 09:59:14 +10:00
Oliver
b876d31d09 Merge pull request #561 from SchrodingersGat/translation
Translation
2019-09-26 11:03:05 +10:00
Oliver Walters
30de734dfa Remove test as it is not currently working 2019-09-26 10:55:23 +10:00
Oliver Walters
f2eb66d854 Update readme file and scripts 2019-09-26 10:37:39 +10:00
Oliver Walters
fcba00bc69 Check for altered translation files that have not been compiled 2019-09-26 10:32:44 +10:00
Oliver Walters
b56a1ade24 Make translation compilation a separate step
- Must be run after a translation file is updated
2019-09-26 10:21:14 +10:00
Oliver Walters
fcb47fce09 Make gettext a prerequisite rather than polluting the make file with apt commands that require sudo 2019-09-26 10:16:45 +10:00
Oliver Walters
eaf910d263 Try enforcing sudo 2019-09-26 10:12:10 +10:00
Oliver Walters
9be528a3fb Add (brief) documentation page regarding translations 2019-09-26 10:07:18 +10:00
Oliver Walters
40acf90efe Add some initial (empty) translation files 2019-09-26 09:59:50 +10:00
Oliver Walters
0ae2fd9246 Update migration script to re-compile translation strings 2019-09-26 09:58:50 +10:00
Oliver Walters
37e9bd8d9b Add install requirement for gettext (required package for creating translation files) 2019-09-26 09:57:09 +10:00
Oliver Walters
3cc79d6def Add default language option to config.yaml 2019-09-26 09:56:41 +10:00
Oliver Walters
0e0405f337 Update settings.py to enable translations 2019-09-26 09:55:45 +10:00
Oliver
fb75617807 Update version.py 2019-09-24 08:08:42 +10:00
Oliver
d247ea7589 Merge pull request #557 from SchrodingersGat/save-user-data
Save user data
2019-09-24 08:04:24 +10:00
Oliver Walters
7c1615a2b6 Fix user recording when serializing stock 2019-09-24 07:59:59 +10:00
Oliver Walters
41c07fc423 Save user who created a stock item
- Handled differently for batch or serialized parts
2019-09-24 07:54:18 +10:00
Oliver Walters
52ec213a28 Save user information when creating a new purchase order 2019-09-24 07:43:14 +10:00
Oliver
cc1e580538 Merge pull request #555 from SchrodingersGat/new-loc
PO Features
2019-09-23 19:52:44 +10:00
Oliver Walters
b1380687e6 PEP 2019-09-23 19:31:50 +10:00
Oliver Walters
8d92960f10 Ability to receive PO lines items individually 2019-09-23 19:31:18 +10:00
Oliver Walters
0d68dbcfa7 Display which lines have been received against a PO 2019-09-23 19:05:22 +10:00
Oliver Walters
21e369e6cc Update ReceivePurchaseOrder form
- Location field is now a proper MPTT field
- Ability to create a new location
2019-09-23 19:02:36 +10:00
Oliver
2a31820abe Merge pull request #550 from SchrodingersGat/table-improvements
Table improvements
2019-09-22 22:28:37 +10:00
Oliver Walters
ae2e2f36e4 Update a bunch more tables 2019-09-22 22:18:53 +10:00
Oliver Walters
2046c12600 Use a jQuerified function 2019-09-22 21:56:57 +10:00
Oliver Walters
df41fafefb Update page table 2019-09-22 21:42:51 +10:00
Oliver Walters
8eaff6a353 Add wrapper function around bootstrapTable 2019-09-22 21:37:20 +10:00
Oliver Walters
fcbf0e6e93 Create UI elements to cancel an order
- View
- Form
- Template
- Button
- Javascript
2019-09-20 11:52:38 +10:00
Oliver Walters
6f54091354 Improve display of stock location 2019-09-20 00:03:59 +10:00
Oliver Walters
5a9e5dea20 Add sub-category and part count 2019-09-20 00:00:34 +10:00
Oliver Walters
508a3fc35c Improve display of part category page 2019-09-19 23:59:01 +10:00
Oliver Walters
b3ea2bfb9a Update badges 2019-09-19 23:38:15 +10:00
Oliver Walters
3c98cd87a7 Use localStorage rather than sessionStorage for storing user prefs
- Also create some helper functions
2019-09-19 23:29:03 +10:00
Oliver Walters
cf2abb4130 Add option to display ALL results in a paginated table
- Commonize the number of pages allowed
2019-09-19 23:20:42 +10:00
Oliver
45a321694b Merge pull request #549 from SchrodingersGat/edit-username
Ability to edit username
2019-09-19 14:40:20 +10:00
Oliver Walters
8a995cc193 Ability to edit username 2019-09-19 14:36:14 +10:00
Oliver
600eac7f1d Merge pull request #542 from SchrodingersGat/fixes
Fixes
2019-09-17 20:31:53 +10:00
Oliver Walters
a77fd23fcf Add a reminder for future-self 2019-09-17 20:19:27 +10:00
Oliver Walters
cb77506111 Simplify 2019-09-17 20:19:05 +10:00
Oliver Walters
c5a82f4b6e Simplifty PartPriceInfo field 2019-09-17 20:17:25 +10:00
Oliver Walters
4a0be0dfb8 Simplify 2019-09-17 20:15:50 +10:00
Oliver
7c328969c9 Merge pull request #539 from SchrodingersGat/ui-tweaks
Ui tweaks
2019-09-17 14:21:46 +10:00
Oliver Walters
774872e6a6 Make function atomic 2019-09-17 14:17:49 +10:00
Oliver Walters
08f958dd72 Add form for setting part category 2019-09-17 14:06:11 +10:00
Oliver Walters
94cd28ecb9 Add ability so set category for multiple parts at once 2019-09-17 13:49:57 +10:00
Oliver Walters
b5b7dc0fbf Fix tests 2019-09-17 12:29:18 +10:00
Oliver Walters
f90aa1d2cf Make purchase-order table sortable 2019-09-17 11:44:50 +10:00
Oliver Walters
1cffd41c07 Fix broken price-break buttons
- Did not work!
2019-09-17 10:54:28 +10:00
Oliver Walters
d40fc59616 Reload page after ordering part 2019-09-17 10:34:41 +10:00
Oliver Walters
a9d1cadc12 Add link to documentation 2019-09-17 00:41:28 +10:00
Oliver Walters
55ebf48684 Add more export fields for SupplierPart and SupplierPriceBreak 2019-09-16 09:43:57 +10:00
Oliver Walters
628a58e8fc Show which parts are short in build view 2019-09-16 08:23:40 +10:00
Oliver Walters
fedbb834ee Add mouse-over text to build-cancel button 2019-09-16 08:17:39 +10:00
Oliver
b46d1c2286 Merge pull request #534 from SchrodingersGat/ci-migrations
Script to check for unstaged migrations
2019-09-16 00:07:38 +10:00
Oliver Walters
5107cf5694 Restore model file 2019-09-16 00:03:19 +10:00
Oliver Walters
9af9158c10 Remove migration file 2019-09-15 23:56:12 +10:00
Oliver Walters
19522dfb92 Commit the migation file 2019-09-15 23:50:47 +10:00
Oliver Walters
b9155bbde9 Fix model file so that CI can run 2019-09-15 23:45:55 +10:00
Oliver Walters
2986e995d1 Make migrations before running script 2019-09-15 23:43:39 +10:00
Oliver Walters
e781202daa Add script 2019-09-15 23:42:36 +10:00
Oliver Walters
56dda5eff4 Script to check for unstaged migrations 2019-09-15 23:41:32 +10:00
Oliver
180df8f110 Merge pull request #533 from SchrodingersGat/key-value-settings
Key value settings
2019-09-15 23:28:37 +10:00
Oliver Walters
4746a3ccff Bootstrapify the table 2019-09-15 23:11:06 +10:00
Oliver Walters
2c1a744c2d Display singleton settings in the settings tab
- Only visible to 'staff' user
2019-09-15 23:09:58 +10:00
Oliver Walters
098cd0ec44 Add description field 2019-09-15 23:07:45 +10:00
Oliver Walters
02e71bd2ce Template for displaying other settings 2019-09-15 22:50:47 +10:00
Oliver Walters
3e33326120 Add the InvenTreeSetting model
- Storage of singleton settings in key:value pairs
2019-09-15 22:46:24 +10:00
Oliver
8d4e2ce498 Update CONTRIBUTING.md
Include notes about migration files
2019-09-15 22:34:22 +10:00
Oliver
ee6c922fad Merge pull request #530 from SchrodingersGat/export-consolidation
Export consolidation
2019-09-15 22:27:57 +10:00
Oliver Walters
194ae49914 Export full_name for parts 2019-09-15 22:23:34 +10:00
Oliver Walters
7f5aba423a Export full_name for parts 2019-09-15 22:23:28 +10:00
Oliver Walters
db04f399c1 Simplify exporting of BOM for a part 2019-09-15 22:21:12 +10:00
Oliver Walters
ed20e9d4a1 Simplify code for exporting PurchaseOrder
- New resource for managing import/export of POLineItem model
2019-09-15 22:04:52 +10:00
Oliver Walters
204cd967aa Include status label text when exporting stocktake data 2019-09-15 20:14:27 +10:00
Oliver Walters
03043e67c7 Perform full validation when importing data 2019-09-15 19:58:05 +10:00
Oliver Walters
2d17f957f1 Remove code duplication for part data export 2019-09-15 19:52:28 +10:00
Oliver Walters
2bc97764c7 Allow more file formats for BOM import 2019-09-15 19:45:59 +10:00
Oliver Walters
9c84e9076f Consolidate stock export code
- Now defined in stock.admin as StockItemResource
- Much more control over format of exported data
- Exported data can be re-imported!
2019-09-15 19:29:18 +10:00
Oliver
66e439a836 Merge pull request #526 from linucks/purchase-order-btn-fix
Fixes problem with 'New Purchase Order' button from Suppliers page.
2019-09-15 19:20:33 +10:00
jmht
ce099f43f3 Fixes problem with 'New Purchase Order' button not working from Suppliers page. 2019-09-14 21:40:09 +01:00
Oliver
fa789036e0 Merge pull request #525 from SchrodingersGat/url-validation
Extra URL validation
2019-09-14 00:14:45 +10:00
Oliver Walters
70e07470db Custom URL validators for more fields 2019-09-14 00:08:49 +10:00
Oliver Walters
4ac8353099 Create a custom URL field, which allows the user-specified validators
- Ref: https://stackoverflow.com/questions/41756572/django-urlfield-with-custom-scheme
- Apply this to the URL field in the Part model
2019-09-14 00:04:08 +10:00
Oliver Walters
ee17d5d3c3 Allow for custom url schemes to be specified in the config file 2019-09-14 00:03:13 +10:00
Oliver
0846daf1f6 Merge pull request #524 from SchrodingersGat/import-export
Customization of django-import-export plugin
2019-09-13 23:35:57 +10:00
Oliver Walters
8578a3b8d1 Add searching to other admin views 2019-09-13 23:32:49 +10:00
Oliver Walters
9b1d0bee3b Add filtering and searching to Part admin 2019-09-13 23:27:22 +10:00
Oliver Walters
28d49bdd47 PEP 2019-09-13 23:19:12 +10:00
Oliver Walters
6a19e94feb Include some extra calculated fields for Part export (readonly) 2019-09-13 23:15:34 +10:00
Oliver Walters
52eeffc2c4 Change more models to use ImportExportModelAdmin 2019-09-13 23:05:16 +10:00
Oliver Walters
f707dd3430 Currency model admin now supports import / export 2019-09-13 23:02:54 +10:00
Oliver Walters
cb5db332d3 Manager for import/export of StockItem data 2019-09-13 23:00:21 +10:00
Oliver Walters
23b814569a Manager for importing StockLocation data 2019-09-13 22:44:50 +10:00
Oliver Walters
37ab3d214d Import/export management for the Company app
- Company
- SupplierPart
- SupplierPriceBreak
2019-09-13 22:39:15 +10:00
Oliver Walters
c579854e89 Export 'default_supplier' field 2019-09-13 22:29:11 +10:00
Oliver Walters
2bc34853e2 import/export manager for PartParameter 2019-09-13 22:27:32 +10:00
Oliver Walters
c469e48f26 Data manager for BomItem 2019-09-13 22:23:40 +10:00
Oliver Walters
bacd70687d Management class for PartCategory import / export 2019-09-13 22:20:08 +10:00
Oliver Walters
89acc778f5 Skip unchanged lines for matching ID values 2019-09-13 22:11:31 +10:00
Oliver Walters
ac36048230 Improve import/export of Part
- Can now import part data
- Either UPDATE existing rows, or CREATE new ones
2019-09-13 22:08:31 +10:00
Oliver Walters
8a68313e5e Customize admin export of Part object 2019-09-13 21:39:37 +10:00
Oliver
9e1f56cdb8 Merge pull request #522 from SchrodingersGat/order-improvements
Order improvements
2019-09-13 21:16:47 +10:00
Oliver Walters
7e9c095edb Ok, fixed now 2019-09-13 21:14:00 +10:00
Oliver Walters
588713467d Fixed unit tests 2019-09-13 21:07:32 +10:00
Oliver
03a42fa360 Merge pull request #523 from SchrodingersGat/template-badge
Display template badge in part table
2019-09-13 21:04:19 +10:00
Oliver Walters
c8be9cb90c Display template badge in part table 2019-09-13 20:58:17 +10:00
Oliver Walters
36ec5e41b0 Cleanup 2019-09-13 20:53:04 +10:00
Oliver Walters
59f102af3c Database filtering beats list comprehension! 2019-09-13 20:15:34 +10:00
Oliver Walters
6854190ff9 Simple test for POLineItemedit view 2019-09-13 20:10:17 +10:00
Oliver Walters
d515e2d968 Tests for POLineItem creation form 2019-09-13 20:01:41 +10:00
Oliver Walters
7c6901f445 Tests for purchas order issue form 2019-09-13 18:15:05 +10:00
Oliver
5672c37c9f Merge pull request #520 from SchrodingersGat/bom-item-edit
Improve BomItem editing form
2019-09-13 16:29:54 +10:00
Oliver Walters
567826165c Improve BomItem editing form
- Don't allow duplication of an item already in the BOM
- Remove the parent part from the BOM
2019-09-13 16:26:44 +10:00
214 changed files with 13516 additions and 1989 deletions

6
.gitignore vendored
View File

@@ -9,6 +9,10 @@ env/
./build/
develop-eggs/
dist/
bin/
lib64
pyvenv.cfg
share/
downloads/
eggs/
.eggs/
@@ -36,6 +40,8 @@ InvenTree/media
InvenTree/static
media
static
inventree_media
inventree_static
# Local config file
config.yaml

View File

@@ -7,16 +7,20 @@ python:
addons:
apt-packages:
-sqlite3
- sqlite3
before_install:
- sudo apt-get update
- sudo apt-get install gettext
- make install
- make migrate
- cd InvenTree && python3 manage.py createsuperuser --username InvenTreeAdmin --email admin@inventree.com --noinput && cd ..
script:
- git ls-files --exclude-standard --others
- cd InvenTree && python3 manage.py makemigrations && cd ..
- python3 ci/check_migration_files.py
- make coverage
- make translate
- make style
after_success:

View File

@@ -4,6 +4,18 @@ Contributions to InvenTree are welcomed - please follow the guidelines below.
No pushing to master! New featues must be submitted in a separate branch (one branch per feature).
## Include Migration Files
Any required migration files **must** be included in the commit, or the pull-request will be rejected. If you change the underlying database schema, make sure you run `make migrate` and commit the migration files before submitting the PR.
## Update Translation Files
Any PRs which update translatable strings (i.e. text strings that will appear in the web-front UI) must also update the translation (locale) files to include hooks for the translated strings.
*This does not mean that all translations must be provided, but that the translation files must include locations for the translated strings to be written.*
To perform this step, simply run `make_translate` from the top level directory before submitting the PR.
## Testing
Any new code should be covered by unit tests - a submitted PR may not be accepted if the code coverage is decreased.

View File

@@ -0,0 +1,71 @@
""" Custom fields used in InvenTree """
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from .validators import allowable_url_schemes
from django.forms.fields import URLField as FormURLField
from django.db import models as models
from django.core import validators
from django import forms
from decimal import Decimal
class InvenTreeURLFormField(FormURLField):
""" Custom URL form field with custom scheme validators """
default_validators = [validators.URLValidator(schemes=allowable_url_schemes())]
class InvenTreeURLField(models.URLField):
""" Custom URL field which has custom scheme validators """
default_validators = [validators.URLValidator(schemes=allowable_url_schemes())]
def formfield(self, **kwargs):
return super().formfield(**{
'form_class': InvenTreeURLFormField
})
def round_decimal(value, places):
"""
Round value to the specified number of places.
"""
if value is not None:
# see https://docs.python.org/2/library/decimal.html#decimal.Decimal.quantize for options
return value.quantize(Decimal(10) ** -places)
return value
class RoundingDecimalFormField(forms.DecimalField):
def to_python(self, value):
value = super(RoundingDecimalFormField, self).to_python(value)
value = round_decimal(value, self.decimal_places)
return value
def prepare_value(self, value):
"""
Override the 'prepare_value' method, to remove trailing zeros when displaying.
Why? It looks nice!
"""
if type(value) == Decimal:
return value.normalize()
else:
return value
class RoundingDecimalField(models.DecimalField):
def to_python(self, value):
value = super(RoundingDecimalField, self).to_python(value)
return round_decimal(value, self.decimal_places)
def formfield(self, **kwargs):
defaults = {
'form_class': RoundingDecimalFormField
}
defaults.update(kwargs)
return super(RoundingDecimalField, self).formfield(**kwargs)

View File

@@ -43,6 +43,7 @@ class EditUserForm(HelperForm):
class Meta:
model = User
fields = [
'username',
'first_name',
'last_name',
'email'

View File

@@ -52,6 +52,50 @@ def str2bool(text, test=True):
return str(text).lower() in ['0', 'n', 'no', 'none', 'f', 'false', 'off', ]
def isNull(text):
"""
Test if a string 'looks' like a null value.
This is useful for querying the API against a null key.
Args:
text: Input text
Returns:
True if the text looks like a null value
"""
return str(text).strip().lower() in ['top', 'null', 'none', 'empty', 'false', '-1']
def decimal2string(d):
"""
Format a Decimal number as a string,
stripping out any trailing zeroes or decimal points.
Essentially make it look like a whole number if it is one.
Args:
d: A python Decimal object
Returns:
A string representation of the input number
"""
try:
# Ensure that the provided string can actually be converted to a float
float(d)
except ValueError:
# Not a number
return str(d)
s = str(d)
# Return entire number if there is no decimal place
if '.' not in s:
return s
return s.rstrip("0").rstrip(".")
def WrapWithQuotes(text, quote='"'):
""" Wrap the supplied text with quotes
@@ -103,6 +147,7 @@ def GetExportFormats():
'xls',
'xlsx',
'json',
'yaml',
]

View File

@@ -5,6 +5,8 @@ import logging
import time
import operator
from rest_framework.authtoken.models import Token
logger = logging.getLogger(__name__)
@@ -20,10 +22,49 @@ class AuthRequiredMiddleware(object):
response = self.get_response(request)
# Redirect any unauthorized HTTP requests to the login page
if not request.user.is_authenticated:
if not request.path_info == reverse_lazy('login') and not request.path_info.startswith('/api/'):
return HttpResponseRedirect(reverse_lazy('login'))
"""
Normally, a web-based session would use csrftoken based authentication.
However when running an external application (e.g. the InvenTree app),
we wish to use token-based auth to grab media files.
So, we will allow token-based authentication but ONLY for the /media/ directory.
What problem is this solving?
- The InvenTree mobile app does not use csrf token auth
- Token auth is used by the Django REST framework, but that is under the /api/ endpoint
- Media files (e.g. Part images) are required to be served to the app
- We do not want to make /media/ files accessible without login!
There is PROBABLY a better way of going about this?
a) Allow token-based authentication against a user?
b) Serve /media/ files in a duplicate location e.g. /api/media/ ?
c) Is there a "standard" way of solving this problem?
My [google|stackoverflow]-fu has failed me. So this hack has been created.
"""
authorized = False
if 'Authorization' in request.headers.keys():
auth = request.headers['Authorization'].strip()
if auth.startswith('Token') and len(auth.split()) == 2:
token = auth.split()[1]
# Does the provided token match a valid user?
if Token.objects.filter(key=token).exists():
allowed = ['/media/', '/static/']
# Only allow token-auth for /media/ or /static/ dirs!
if any([request.path_info.startswith(a) for a in allowed]):
authorized = True
# No authorization was found for the request
if not authorized:
if not request.path_info == reverse_lazy('login') and not request.path_info.startswith('/api/'):
return HttpResponseRedirect(reverse_lazy('login'))
# Code to be executed for each request/response after
# the view is called.
@@ -38,6 +79,8 @@ class QueryCountMiddleware(object):
status code of 200). It does not currently support
multi-db setups.
To enable this middleware, set 'log_queries: True' in the local InvenTree config file.
Reference: https://www.dabapps.com/blog/logging-sql-queries-django-13/
"""

View File

@@ -4,8 +4,11 @@ Generic models which provide extra functionality over base Django model types.
from __future__ import unicode_literals
import os
from django.db import models
from django.contrib.contenttypes.models import ContentType
from django.utils.translation import gettext_lazy as _
from django.db.models.signals import pre_delete
from django.dispatch import receiver
@@ -15,6 +18,51 @@ from mptt.models import MPTTModel, TreeForeignKey
from .validators import validate_tree_name
def rename_attachment(instance, filename):
"""
Function for renaming an attachment file.
The subdirectory for the uploaded file is determined by the implementing class.
Args:
instance: Instance of a PartAttachment object
filename: name of uploaded file
Returns:
path to store file, format: '<subdir>/<id>/filename'
"""
# Construct a path to store a file attachment for a given model type
return os.path.join(instance.getSubdir(), filename)
class InvenTreeAttachment(models.Model):
""" Provides an abstracted class for managing file attachments.
Attributes:
attachment: File
comment: String descriptor for the attachment
"""
def getSubdir(self):
"""
Return the subdirectory under which attachments should be stored.
Note: Re-implement this for each subclass of InvenTreeAttachment
"""
return "attachments"
attachment = models.FileField(upload_to=rename_attachment,
help_text=_('Select file to attach'))
comment = models.CharField(max_length=100, help_text=_('File comment'))
@property
def basename(self):
return os.path.basename(self.attachment.name)
class Meta:
abstract = True
class InvenTreeTree(MPTTModel):
""" Provides an abstracted self-referencing tree model for data categories.
@@ -29,6 +77,8 @@ class InvenTreeTree(MPTTModel):
class Meta:
abstract = True
# Names must be unique at any given level in the tree
unique_together = ('name', 'parent')
class MPTTMeta:
@@ -37,7 +87,6 @@ class InvenTreeTree(MPTTModel):
name = models.CharField(
blank=False,
max_length=100,
unique=True,
validators=[validate_tree_name]
)

View File

@@ -17,6 +17,10 @@ import logging
import tempfile
import yaml
from datetime import datetime
from django.utils.translation import gettext_lazy as _
def eprint(*args, **kwargs):
""" Print a warning message to stderr """
@@ -99,6 +103,8 @@ INSTALLED_APPS = [
'django_cleanup', # Automatically delete orphaned MEDIA files
'qr_code', # Generate QR codes
'mptt', # Modified Preorder Tree Traversal
'markdownx', # Markdown editing
'markdownify', # Markdown template rendering
]
LOGGING = {
@@ -115,6 +121,7 @@ LOGGING = {
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.locale.LocaleMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'corsheaders.middleware.CorsMiddleware',
@@ -158,6 +165,37 @@ REST_FRAMEWORK = {
WSGI_APPLICATION = 'InvenTree.wsgi.application'
# Markdownx configuration
# Ref: https://neutronx.github.io/django-markdownx/customization/
MARKDOWNX_MEDIA_PATH = datetime.now().strftime('markdownx/%Y/%m/%d')
# Markdownify configuration
# Ref: https://django-markdownify.readthedocs.io/en/latest/settings.html
MARKDOWNIFY_WHITELIST_TAGS = [
'a',
'abbr',
'b',
'blockquote',
'em',
'h1', 'h2', 'h3',
'i',
'img',
'li',
'ol',
'p',
'strong',
'ul'
]
MARKDOWNIFY_WHITELIST_ATTRS = [
'href',
'src',
'alt',
]
MARKDOWNIFY_BLEACH = True
DATABASES = {}
"""
@@ -213,11 +251,32 @@ AUTH_PASSWORD_VALIDATORS = [
},
]
# Extra (optional) URL validators
# See https://docs.djangoproject.com/en/2.2/ref/validators/#django.core.validators.URLValidator
EXTRA_URL_SCHEMES = CONFIG.get('extra_url_schemes', [])
if not type(EXTRA_URL_SCHEMES) in [list]:
eprint("Warning: extra_url_schemes not correctly formatted")
EXTRA_URL_SCHEMES = []
# Internationalization
# https://docs.djangoproject.com/en/1.10/topics/i18n/
LANGUAGE_CODE = 'en-us'
LANGUAGE_CODE = CONFIG.get('language', 'en-us')
# If a new language translation is supported, it must be added here
LANGUAGES = [
('en', _('English')),
('de', _('German')),
('fr', _('French')),
('pk', _('Polish')),
]
LOCALE_PATHS = (
os.path.join(BASE_DIR, 'locale/'),
)
TIME_ZONE = 'UTC'
@@ -227,6 +286,10 @@ USE_L10N = True
USE_TZ = True
DATE_INPUT_FORMATS = [
"%Y-%m-%d",
]
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.10/howto/static-files/
@@ -245,7 +308,10 @@ STATICFILES_DIRS = [
MEDIA_URL = '/media/'
# The filesystem location for served static files
MEDIA_ROOT = CONFIG.get('media_root', os.path.join(BASE_DIR, 'media'))
MEDIA_ROOT = os.path.abspath(CONFIG.get('media_root', os.path.join(BASE_DIR, 'media')))
if DEBUG:
print("MEDIA_ROOT:", MEDIA_ROOT)
# crispy forms use the bootstrap templates
CRISPY_TEMPLATE_PACK = 'bootstrap'

View File

@@ -1,3 +1,34 @@
:root {
--primary-color: #335d88;
--secondary-color: #b69c80;
--highlight-color: #f5efe8;
--basic-color: #333;
}
.markdownx .row {
margin: 5px;
padding: 5px;
border: 1px solid #cce;
border-radius: 4px;
}
.markdownx-editor {
width: 100%;
border: 1px solid #cce;
border-radius: 3px;
padding: 10px;
}
.panel-content {
padding: 10px;
}
.markdownx-preview {
border: 1px solid #cce;
border-radius: 3px;
padding: 10px;
}
.qr-code {
max-width: 400px;
max-height: 400px;
@@ -21,6 +52,10 @@
font-size: 12px;
}
.glyphicon-right {
float: right;
}
.starred-part {
color: #ffbb00;
}
@@ -77,6 +112,10 @@
font-size: 100%;
}
.label-right {
float: right;
}
/* Bootstrap table overrides */
.stock-sub-group td {
@@ -98,11 +137,11 @@
max-width: 250px;
}
.bomrowvalid {
.rowvalid {
color: #050;
}
.bomrowinvalid {
.rowinvalid {
color: #A00;
font-style: italic;
}
@@ -152,6 +191,24 @@
-webkit-opacity: 10%;
}
/* grid display for part images */
.table-img-grid tr {
display: inline;
}
.table-img-grid td {
padding: 10px;
margin: 10px;
}
.table-img-grid .grid-image {
height: 128px;
width: 128px;
object-fit: contain;
background: #eee;
}
.btn-glyph {
padding-left: 6px;
@@ -180,6 +237,20 @@
object-fit: contain;
}
.part-thumb-container:hover .part-thumb-overlay {
opacity: 1;
}
.part-thumb-overlay {
position: absolute;
top: 0;
left: 0;
opacity: 0;
transition: .25s ease;
padding: 15px;
margin: 5px;
}
.checkbox {
margin-left: 20px;
}
@@ -297,6 +368,12 @@
width: 100%;
}
input[type="submit"] {
color: #333;
background-color: #e6e6e6;
border-color: #adadad;
}
.modal textarea {
width: 100%;
}

View File

@@ -133,7 +133,14 @@ function loadBomTable(table, options) {
title: 'Part',
sortable: true,
formatter: function(value, row, index, field) {
return imageHoverIcon(row.sub_part_detail.image_url) + renderLink(row.sub_part_detail.full_name, row.sub_part_detail.url);
var html = imageHoverIcon(row.sub_part_detail.image_url) + renderLink(row.sub_part_detail.full_name, row.sub_part_detail.url);
// Display an extra icon if this part is an assembly
if (row.sub_part_detail.assembly) {
html += "<a href='" + row.sub_part_detail.url + "bom'><span class='glyphicon-right glyphicon glyphicon-th-list'></span></a>";
}
return html;
}
}
);
@@ -163,21 +170,16 @@ function loadBomTable(table, options) {
formatter: function(value, row, index, field) {
var text = value;
// The 'value' is a text string with (potentially) multiple trailing zeros
// Let's make it a bit more pretty
text = parseFloat(text);
if (row.overage) {
text += "<small> (+" + row.overage + ") </small>";
}
return text;
},
footerFormatter: function(data) {
var quantity = 0;
data.forEach(function(item) {
quantity += item.quantity;
});
return quantity;
},
});
if (!options.editable) {
@@ -280,14 +282,13 @@ function loadBomTable(table, options) {
search: true,
rowStyle: function(row, index) {
if (row.validated) {
return {classes: 'bomrowvalid'};
return {classes: 'rowvalid'};
} else {
return {classes: 'bomrowinvalid'};
return {classes: 'rowinvalid'};
}
},
formatNoMatches: function() { return "No BOM items found"; },
clickToSelect: true,
showFooter: true,
queryParams: function(p) {
return params;
},

View File

@@ -1,5 +1,6 @@
function updateAllocationTotal(id, count, required) {
count = parseFloat(count);
$('#allocation-total-'+id).html(count);
@@ -27,21 +28,24 @@ function loadAllocationTable(table, part_id, part, url, required, button) {
field: 'stock_item_detail',
title: 'Stock Item',
formatter: function(value, row, index, field) {
return '' + value.quantity + ' x ' + value.part_name + ' @ ' + value.location_name;
return '' + parseFloat(value.quantity) + ' x ' + value.part_name + ' @ ' + value.location_name;
}
},
{
field: 'stock_item_detail.quantity',
title: 'Available',
formatter: function(value, row, index, field) {
return parseFloat(value);
}
},
{
field: 'quantity',
title: 'Allocated',
formatter: function(value, row, index, field) {
var html = value;
var html = parseFloat(value);
var bEdit = "<button class='btn btn-primary item-edit-button btn-sm' type='button' title='Edit stock allocation' url='/build/item/" + row.pk + "/edit/'><span class='glyphicon glyphicon-small glyphicon-edit'></span></button>";
var bDel = "<button class='btn btn-danger item-del-button btn-sm' type='button' title='Delete stock allocation' url='/build/item/" + row.pk + "/delete/'><span class='glyphicon glyphicon-small glyphicon-trash'></span></button>";
var bEdit = "<button class='btn item-edit-button btn-sm' type='button' title='Edit stock allocation' url='/build/item/" + row.pk + "/edit/'><span class='glyphicon glyphicon-small glyphicon-edit'></span></button>";
var bDel = "<button class='btn item-del-button btn-sm' type='button' title='Delete stock allocation' url='/build/item/" + row.pk + "/delete/'><span class='glyphicon glyphicon-small glyphicon-trash'></span></button>";
html += "<div class='btn-group' style='float: right;'>" + bEdit + bDel + "</div>";
@@ -67,7 +71,7 @@ function loadAllocationTable(table, part_id, part, url, required, button) {
var count = 0;
for (var i = 0; i < results.length; i++) {
count += results[i].quantity;
count += parseFloat(results[i].quantity);
}
updateAllocationTotal(part_id, count, required);

View File

@@ -41,6 +41,13 @@ function inventreeDocReady() {
modal.modal('show');
});
// Callback to launch the 'Database Stats' window
$('#launch-stats').click(function() {
launchModalForm("/stats/", {
no_post: true,
});
});
}
function isFileTransfer(transfer) {
@@ -136,4 +143,53 @@ function imageHoverIcon(url) {
`;
return html;
}
function inventreeSave(name, value) {
/*
* Save a key:value pair to local storage
*/
var key = "inventree-" + name;
localStorage.setItem(key, value);
}
function inventreeLoad(name, defaultValue) {
/*
* Retrieve a key:value pair from local storage
*/
var key = "inventree-" + name;
var value = localStorage.getItem(key);
if (value == null) {
return defaultValue;
} else {
return value;
}
}
function inventreeLoadInt(name) {
/*
* Retrieve a value from local storage, and attempt to cast to integer
*/
var data = inventreeLoad(name);
return parseInt(data, 10);
}
function inventreeLoadFloat(name) {
var data = inventreeLoad(name);
return parseFloat(data);
}
function inventreeDel(name) {
var key = 'inventree-' + name;
localStorage.removeItem(key);
}

View File

@@ -98,4 +98,96 @@ function removePurchaseOrderLineItem(e) {
launchModalForm(url, {
reload: true,
});
}
function loadPurchaseOrderTable(table, options) {
/* Create a purchase-order table */
table.inventreeTable({
url: options.url,
formatNoMatches: function() { return "No purchase orders found"; },
columns: [
{
field: 'pk',
title: 'ID',
visible: false,
},
{
sortable: true,
field: 'supplier',
title: 'Supplier',
formatter: function(value, row, index, field) {
return imageHoverIcon(row.supplier__image) + renderLink(row.supplier__name, '/company/' + value + '/purchase-orders/');
}
},
{
sortable: true,
field: 'reference',
title: 'Reference',
formatter: function(value, row, index, field) {
return renderLink(value, "/order/purchase-order/" + row.pk + "/");
}
},
{
sortable: true,
field: 'creation_date',
title: 'Date',
},
{
sortable: true,
field: 'description',
title: 'Description',
},
{
sortable: true,
field: 'status',
title: 'Status',
formatter: function(value, row, index, field) {
return orderStatusLabel(row.status, row.status_text);
}
},
{
sortable: true,
field: 'lines',
title: 'Items'
},
],
});
}
function orderStatusLabel(code, label) {
/* Render a purchase-order status label. */
var html = "<span class='label";
switch (code) {
case 10: // pending
html += " label-info";
break;
case 20: // placed
html += " label-primary";
break;
case 30: // complete
html += " label-success";
break;
case 40: // cancelled
case 50: // lost
html += " label-warning";
break;
case 60: // returned
html += " label-danger";
break;
default:
break;
}
html += "'>";
html += label;
html += "</span>";
console.log(html);
return html;
}

View File

@@ -95,6 +95,10 @@ function loadPartTable(table, url, options={}) {
query.active = true;
}
// Include sub-category search
// TODO - Make this user-configurable!
query.cascade = true;
var columns = [
{
field: 'pk',
@@ -135,8 +139,12 @@ function loadPartTable(table, url, options={}) {
name = '<i>' + name + '</i>';
}
var display = imageHoverIcon(row.image) + renderLink(name, '/part/' + row.pk + '/');
var display = imageHoverIcon(row.thumbnail) + renderLink(name, '/part/' + row.pk + '/');
if (row.is_template) {
display = display + "<span class='label label-info' style='float: right;'>TEMPLATE</span>";
}
if (!row.active) {
display = display + "<span class='label label-warning' style='float: right;'>INACTIVE</span>";
}
@@ -177,25 +185,38 @@ function loadPartTable(table, url, options={}) {
title: 'Stock',
searchable: false,
sortable: true,
formatter: function(value, row, index, field) {
formatter: function(value, row, index, field) {
var link = "stock";
if (value) {
return renderLink(value, '/part/' + row.pk + '/stock/');
}
else {
return "<span class='label label-warning'>No Stock</span>";
// There IS stock available for this part
// Is stock "low" (below the 'minimum_stock' quantity)?
if (row.minimum_stock && row.minimum_stock > value) {
value += "<span class='label label-right label-warning'>Low stock</span>";
}
} else if (row.on_order) {
// There is no stock available, but stock is on order
value = "0<span class='label label-right label-primary'>On Order : " + row.on_order + "</span>";
link = "orders";
} else if (row.building) {
// There is no stock available, but stock is being built
value = "0<span class='label label-right label-info'>Building : " + row.building + "</span>";
link = "builds";
} else {
// There is no stock available
value = "0<span class='label label-right label-danger'>No Stock</span>";
}
return renderLink(value, '/part/' + row.pk + "/" + link + "/");
}
});
$(table).bootstrapTable({
$(table).inventreeTable({
url: url,
sortable: true,
search: true,
sortName: 'name',
method: 'get',
pagination: true,
pageSize: 25,
rememberOrder: true,
formatNoMatches: function() { return "No parts found"; },
queryParams: function(p) {
return query;

View File

@@ -42,13 +42,14 @@ function loadStockTable(table, options) {
var params = options.params || {};
table.bootstrapTable({
sortable: true,
search: true,
// Enforce 'cascade' option
// TODO - Make this user-configurable?
params.cascade = true;
console.log('load stock table');
table.inventreeTable({
method: 'get',
pagination: true,
pageSize: 25,
rememberOrder: true,
formatNoMatches: function() {
return 'No stock items matching query';
},
@@ -69,7 +70,7 @@ function loadStockTable(table, options) {
name += row.part__name;
return imageHoverIcon(row.part__image) + name + ' <i>(' + data.length + ' items)</i>';
return imageHoverIcon(row.part__thumbnail) + name + ' <i>(' + data.length + ' items)</i>';
}
else if (field == 'part__description') {
return row.part__description;
@@ -83,6 +84,8 @@ function loadStockTable(table, options) {
items += 1;
});
stock = +stock.toFixed(5);
return stock + " (" + items + " items)";
} else if (field == 'batch') {
var batches = [];
@@ -128,6 +131,28 @@ function loadStockTable(table, options) {
// A single location!
return renderLink(row.location__path, '/stock/location/' + row.location + '/')
}
} else if (field == 'notes') {
var notes = [];
data.forEach(function(item) {
var note = item.notes;
if (!note || note == '') {
note = '-';
}
if (!notes.includes(note)) {
notes.push(note);
}
});
if (notes.length > 1) {
return '...';
} else if (notes.length == 1) {
return notes[0] || '-';
} else {
return '-';
}
}
else {
return '';
@@ -163,7 +188,7 @@ function loadStockTable(table, options) {
name += row.part__revision;
}
return imageHoverIcon(row.part__image) + renderLink(name, '/part/' + row.part + '/stock/');
return imageHoverIcon(row.part__thumbnail) + renderLink(name, '/part/' + row.part + '/stock/');
}
},
{
@@ -291,6 +316,18 @@ function loadStockTable(table, options) {
},
});
});
$("#multi-item-delete").click(function() {
var selections = $("#stock-table").bootstrapTable("getSelections");
var stock = [];
selections.forEach(function(item) {
stock.push(item.pk);
});
stockAdjustment('delete');
});
}
@@ -352,6 +389,9 @@ function loadStockTrackingTable(table, options) {
cols.push({
field: 'quantity',
title: 'Quantity',
formatter: function(value, row, index, field) {
return parseFloat(value);
},
});
cols.push({
@@ -386,15 +426,10 @@ function loadStockTrackingTable(table, options) {
}
});
table.bootstrapTable({
sortable: true,
search: true,
table.inventreeTable({
method: 'get',
rememberOrder: true,
queryParams: options.params,
columns: cols,
pagination: true,
pageSize: 50,
url: options.url,
});

View File

@@ -44,6 +44,31 @@ function isNumeric(n) {
}
/* Wrapper function for bootstrapTable.
* Sets some useful defaults, and manage persistent settings.
*/
$.fn.inventreeTable = function(options) {
var tableName = options.name || 'table';
var varName = tableName + '-pagesize';
options.pagination = true;
options.pageSize = inventreeLoad(varName, 25);
options.pageList = [25, 50, 100, 250, 'all'];
options.rememberOrder = true;
options.sortable = true;
options.search = true;
// Callback to save pagination data
options.onPageChange = function(number, size) {
inventreeSave(varName, size);
};
// Standard options for all tables
this.bootstrapTable(options);
}
function customGroupSorter(sortName, sortOrder, sortData) {
console.log('got here');

View File

@@ -12,6 +12,15 @@ class StatusCode:
""" 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")
class OrderStatus(StatusCode):
@@ -62,11 +71,17 @@ class StockStatus(StatusCode):
LOST: _("Lost"),
}
# The following codes correspond to parts that are 'available'
# The following codes correspond to parts that are 'available' or 'in stock'
AVAILABLE_CODES = [
OK,
ATTENTION,
DAMAGED
DAMAGED,
]
# The following codes correspond to parts that are 'unavailable'
UNAVAILABLE_CODES = [
DESTROYED,
LOST,
]

View File

@@ -33,7 +33,8 @@ from django.conf.urls.static import static
from django.views.generic.base import RedirectView
from rest_framework.documentation import include_docs_urls
from .views import IndexView, SearchView, SettingsView, EditUserView, SetPasswordView
from .views import IndexView, SearchView, DatabaseStatsView
from .views import SettingsView, EditUserView, SetPasswordView
from .views import InfoView
from users.urls import user_urls
@@ -61,6 +62,7 @@ settings_urls = [
url(r'^user/?', SettingsView.as_view(template_name='InvenTree/settings/user.html'), name='settings-user'),
url(r'^currency/?', SettingsView.as_view(template_name='InvenTree/settings/currency.html'), name='settings-currency'),
url(r'^part/?', SettingsView.as_view(template_name='InvenTree/settings/part.html'), name='settings-part'),
url(r'^other/?', SettingsView.as_view(template_name='InvenTree/settings/other.html'), name='settings-other'),
# Catch any other urls
url(r'^.*$', SettingsView.as_view(template_name='InvenTree/settings/user.html'), name='settings'),
@@ -96,17 +98,19 @@ urlpatterns = [
url(r'^index/', IndexView.as_view(), name='index'),
url(r'^search/', SearchView.as_view(), name='search'),
url(r'^stats/', DatabaseStatsView.as_view(), name='stats'),
url(r'^api/', include(apipatterns)),
url(r'^api-doc/', include_docs_urls(title='InvenTree API')),
url(r'^markdownx/', include('markdownx.urls')),
]
# Static file access
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
if settings.DEBUG:
# Media file access
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
# Media file access
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
# Send any unknown URLs to the parts page
urlpatterns += [url(r'^.*$', RedirectView.as_view(url='/index/', permanent=False), name='index')]

View File

@@ -2,9 +2,32 @@
Custom field validators for InvenTree
"""
from django.conf import settings
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
from common.models import InvenTreeSetting
import re
def allowable_url_schemes():
""" Return the list of allowable URL schemes.
In addition to the default schemes allowed by Django,
the install configuration file (config.yaml) can specify
extra schemas """
# Default schemes
schemes = ['http', 'https', 'ftp', 'ftps']
extra = settings.EXTRA_URL_SCHEMES
for e in extra:
if e.lower() not in schemes:
schemes.append(e.lower())
return schemes
def validate_part_name(value):
""" Prevent some illegal characters in part names.
@@ -17,6 +40,18 @@ def validate_part_name(value):
)
def validate_part_ipn(value):
""" Validate the Part IPN against regex rule """
pattern = InvenTreeSetting.get_setting('part_ipn_regex')
if pattern:
match = re.search(pattern, value)
if match is None:
raise ValidationError(_('IPN must match regex pattern') + " '{pat}'".format(pat=pattern))
def validate_tree_name(value):
""" Prevent illegal characters in tree item names """

View File

@@ -4,7 +4,7 @@ Provides information on the current InvenTree version
import subprocess
INVENTREE_SW_VERSION = "0.0.6"
INVENTREE_SW_VERSION = "0.0.10"
def inventreeVersion():
@@ -15,6 +15,12 @@ def inventreeVersion():
def inventreeCommitHash():
""" Returns the git commit hash for the running codebase """
commit = str(subprocess.check_output('git rev-parse --short HEAD'.split()), 'utf-8').strip()
return str(subprocess.check_output('git rev-parse --short HEAD'.split()), 'utf-8').strip()
return commit
def inventreeCommitDate():
""" Returns the git commit date for the running codebase """
d = str(subprocess.check_output('git show -s --format=%ci'.split()), 'utf-8').strip()
return d.split(' ')[0]

View File

@@ -8,6 +8,7 @@ as JSON objects and passing them to modal forms (using jQuery / bootstrap).
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.utils.translation import gettext_lazy as _
from django.template.loader import render_to_string
from django.http import JsonResponse, HttpResponseRedirect
@@ -15,7 +16,9 @@ from django.views import View
from django.views.generic import UpdateView, CreateView
from django.views.generic.base import TemplateView
from part.models import Part
from part.models import Part, PartCategory
from stock.models import StockLocation, StockItem
from common.models import InvenTreeSetting
from .forms import DeleteForm, EditUserForm, SetPasswordForm
from .helpers import str2bool
@@ -163,6 +166,8 @@ class AjaxMixin(object):
if form:
context['form'] = form
else:
context['form'] = None
data['title'] = self.ajax_form_title
@@ -237,6 +242,18 @@ class AjaxCreateView(AjaxMixin, CreateView):
- Handles form validation via AJAX POST requests
"""
def pre_save(self, **kwargs):
"""
Hook for doing something before the form is validated
"""
pass
def post_save(self, **kwargs):
"""
Hook for doing something with the created object after it is saved
"""
pass
def get(self, request, *args, **kwargs):
""" Creates form with initial data, and renders JSON response """
@@ -254,26 +271,29 @@ class AjaxCreateView(AjaxMixin, CreateView):
- Return status info (success / failure)
"""
self.request = request
form = self.get_form()
self.form = self.get_form()
# Extra JSON data sent alongside form
data = {
'form_valid': form.is_valid(),
'form_valid': self.form.is_valid(),
}
if form.is_valid():
obj = form.save()
if self.form.is_valid():
self.pre_save()
self.object = self.form.save()
self.post_save()
# Return the PK of the newly-created object
data['pk'] = obj.pk
data['text'] = str(obj)
data['pk'] = self.object.pk
data['text'] = str(self.object)
try:
data['url'] = obj.get_absolute_url()
data['url'] = self.object.get_absolute_url()
except AttributeError:
pass
return self.renderJsonResponse(request, form, data)
return self.renderJsonResponse(request, self.form, data)
class AjaxUpdateView(AjaxMixin, UpdateView):
@@ -511,3 +531,41 @@ class SettingsView(TemplateView):
"""
template_name = "InvenTree/settings.html"
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs).copy()
ctx['settings'] = InvenTreeSetting.objects.all().order_by('key')
return ctx
class DatabaseStatsView(AjaxView):
""" View for displaying database statistics """
ajax_template_name = "stats.html"
ajax_form_title = _("Database Statistics")
def get_context_data(self, **kwargs):
ctx = {}
# Part stats
ctx['part_count'] = Part.objects.count()
ctx['part_cat_count'] = PartCategory.objects.count()
# Stock stats
ctx['stock_item_count'] = StockItem.objects.count()
ctx['stock_loc_count'] = StockLocation.objects.count()
"""
TODO: Other ideas for database metrics
- "Popular" parts (used to make other parts?)
- Most ordered part
- Most sold part
- etc etc etc
"""
return ctx

View File

@@ -26,7 +26,6 @@ class EditBuildForm(HelperForm):
'take_from',
'batch',
'URL',
'notes',
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 2.2.5 on 2019-09-13 14:07
import InvenTree.fields
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('build', '0005_auto_20190604_2217'),
]
operations = [
migrations.AlterField(
model_name='build',
name='URL',
field=InvenTree.fields.InvenTreeURLField(blank=True, help_text='Link to external URL'),
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 2.2.5 on 2019-11-18 23:21
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('build', '0006_auto_20190913_1407'),
]
operations = [
migrations.AlterField(
model_name='builditem',
name='quantity',
field=models.DecimalField(decimal_places=5, default=1, help_text='Stock quantity to allocate to build', max_digits=15, validators=[django.core.validators.MinValueValidator(1)]),
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 2.2.9 on 2020-02-01 12:47
from django.db import migrations
import markdownx.models
class Migration(migrations.Migration):
dependencies = [
('build', '0007_auto_20191118_2321'),
]
operations = [
migrations.AlterField(
model_name='build',
name='notes',
field=markdownx.models.MarkdownxField(blank=True, help_text='Extra build notes'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 2.2.9 on 2020-02-10 10:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('build', '0008_auto_20200201_1247'),
]
operations = [
migrations.AlterField(
model_name='build',
name='creation_date',
field=models.DateField(auto_now_add=True),
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 2.2.9 on 2020-03-18 10:27
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('build', '0009_auto_20200210_1032'),
]
operations = [
migrations.AlterField(
model_name='build',
name='part',
field=models.ForeignKey(help_text='Select part to build', limit_choices_to={'active': True, 'assembly': True, 'is_template': False, 'virtual': False}, on_delete=django.db.models.deletion.CASCADE, related_name='builds', to='part.Part'),
),
]

View File

@@ -16,7 +16,11 @@ from django.db import models, transaction
from django.db.models import Sum
from django.core.validators import MinValueValidator
from markdownx.models import MarkdownxField
from InvenTree.status_codes import BuildStatus
from InvenTree.fields import InvenTreeURLField
from InvenTree.helpers import decimal2string
from stock.models import StockItem
from part.models import Part, BomItem
@@ -39,7 +43,7 @@ class Build(models.Model):
"""
def __str__(self):
return "Build {q} x {part}".format(q=self.quantity, part=str(self.part))
return "Build {q} x {part}".format(q=decimal2string(self.quantity), part=str(self.part))
def get_absolute_url(self):
return reverse('build-detail', kwargs={'pk': self.id})
@@ -47,39 +51,40 @@ class Build(models.Model):
title = models.CharField(
blank=False,
max_length=100,
help_text='Brief description of the build')
help_text=_('Brief description of the build'))
part = models.ForeignKey('part.Part', on_delete=models.CASCADE,
related_name='builds',
limit_choices_to={
'is_template': False,
'assembly': True,
'active': True
'active': True,
'virtual': False,
},
help_text='Select part to build',
help_text=_('Select part to build'),
)
take_from = models.ForeignKey('stock.StockLocation', on_delete=models.SET_NULL,
related_name='sourcing_builds',
null=True, blank=True,
help_text='Select location to take stock from for this build (leave blank to take from any stock location)'
help_text=_('Select location to take stock from for this build (leave blank to take from any stock location)')
)
quantity = models.PositiveIntegerField(
default=1,
validators=[MinValueValidator(1)],
help_text='Number of parts to build'
help_text=_('Number of parts to build')
)
status = models.PositiveIntegerField(default=BuildStatus.PENDING,
choices=BuildStatus.items(),
validators=[MinValueValidator(0)],
help_text='Build status')
help_text=_('Build status'))
batch = models.CharField(max_length=100, blank=True, null=True,
help_text='Batch code for this build output')
help_text=_('Batch code for this build output'))
creation_date = models.DateField(auto_now=True, editable=False)
creation_date = models.DateField(auto_now_add=True, editable=False)
completion_date = models.DateField(null=True, blank=True)
@@ -89,10 +94,13 @@ class Build(models.Model):
related_name='builds_completed'
)
URL = models.URLField(blank=True, help_text='Link to external URL')
URL = InvenTreeURLField(blank=True, help_text=_('Link to external URL'))
notes = models.TextField(blank=True, help_text='Extra build notes')
""" Notes attached to each build output """
notes = MarkdownxField(blank=True, help_text=_('Extra build notes'))
@property
def output_count(self):
return self.build_outputs.count()
@transaction.atomic
def cancelBuild(self, user):
@@ -232,7 +240,7 @@ class Build(models.Model):
now=str(datetime.now().date())
)
if self.part.trackable:
if self.part.trackable and serial_numbers:
# Add new serial numbers
for serial in serial_numbers:
item = StockItem.objects.create(
@@ -398,18 +406,20 @@ class BuildItem(models.Model):
Build,
on_delete=models.CASCADE,
related_name='allocated_stock',
help_text='Build to allocate parts'
help_text=_('Build to allocate parts')
)
stock_item = models.ForeignKey(
'stock.StockItem',
on_delete=models.CASCADE,
related_name='allocations',
help_text='Stock Item to allocate to build',
help_text=_('Stock Item to allocate to build'),
)
quantity = models.PositiveIntegerField(
quantity = models.DecimalField(
decimal_places=5,
max_digits=15,
default=1,
validators=[MinValueValidator(1)],
help_text='Stock quantity to allocate to build'
help_text=_('Stock quantity to allocate to build')
)

View File

@@ -62,9 +62,7 @@ InvenTree | Allocate Parts
{% else %}
$("#build-list").bootstrapTable({
search: true,
sortable: true,
$("#build-list").inventreeTable({
});
$("#btn-allocate").click(function() {

View File

@@ -1,12 +1,14 @@
{% load i18n %}
{% load inventree_extras %}
<div class='row'>
<h4>Allocate Stock to Build</h4>
<h4>{% trans "Allocate Stock to Build" %}</h4>
<div class='col-sm-6'>
</div>
<div class='col-sm-6'>
<div class='btn-group' style='float: right;'>
<button class='btn btn-primary' type='button' title='Automatic allocation' id='auto-allocate-build'>Auto Allocate</button>
<button class='btn btn-warning' type='button' title='Unallocate build stock' id='unallocate-build'>Unallocate</button>
<button class='btn btn-primary' type='button' title='Automatic allocation' id='auto-allocate-build'>{% trans "Auto Allocate" %}</button>
<button class='btn btn-warning' type='button' title='Unallocate build stock' id='unallocate-build'>{% trans "Unallocate" %}</button>
</div>
</div>
</div>
@@ -14,16 +16,16 @@
<div class='row'>
<div class='col-sm-6'>
<h4>Part</h4>
<h4>{% trans "Part" %}</h4>
</div>
<div class='col-sm-2'>
<h4>Available</h4>
<h4>{% trans "Available" %}</h4>
</div>
<div class='col-sm-2'>
<h4>Required</h4>
<h4>{% trans "Required" %}</h4>
</div>
<div class='col-sm-2'>
<h4>Allocated</h4>
<h4>{% trans "Allocated" %}</h4>
</div>
</div>

View File

@@ -1,36 +1,39 @@
<h4>Required Parts</h4>
{% load i18n %}
{% load inventree_extras %}
<h4>{% trans "Required Parts" %}</h4>
<hr>
<div id='build-item-toolbar'>
<div class='btn-group'>
<button class='btn btn-primary' type='button' id='btn-allocate' title='Allocate Stock'>Allocate</button>
<button class='btn btn-primary' type='button' id='btn-order-parts' title='Order Parts'>Order Parts</button>
<button class='btn btn-primary' type='button' id='btn-allocate' title='Allocate Stock'>{% trans "Allocate" %}</button>
<button class='btn btn-primary' type='button' id='btn-order-parts' title='Order Parts'>{% trans "Order Parts" %}</button>
</div>
</div>
<table class='table table-striped table-condensed' id='build-list' data-sorting='true' data-toolbar='#build-item-toolbar'>
<thead>
<tr>
<th data-sortable='true'>Part</th>
<th>Description</th>
<th data-sortable='true'>Available</th>
<th data-sortable='true'>Required</th>
<th data-sortable='true'>Allocated</th>
<th data-sortable='true'>On Order</th>
<th data-sortable='true'>{% trans "Part" %}</th>
<th>{% trans "Description" %}</th>
<th data-sortable='true'>{% trans "Available" %}</th>
<th data-sortable='true'>{% trans "Required" %}</th>
<th data-sortable='true'>{% trans "Allocated" %}</th>
<th data-sortable='true'>{% trans "On Order" %}</th>
</tr>
</thead>
<tbody>
{% for item in build.required_parts %}
<tr>
<tr {% if build.status == BuildStatus.PENDING %}class='{% if item.part.total_stock > item.quantity %}rowvalid{% else %}rowinvalid{% endif %}'{% endif %}>
<td>
{% include "hover_image.html" with image=item.part.image hover=True %}
<a class='hover-icon'a href="{% url 'part-detail' item.part.id %}">{{ item.part.full_name }}</a>
</td>
<td>{{ item.part.description }}</td>
<td>{{ item.part.total_stock }}</td>
<td>{{ item.quantity }}</td>
<td>{% decimal item.part.total_stock %}</td>
<td>{% decimal item.quantity %}</td>
<td>{{ item.allocated }}</td>
<td>{{ item.part.on_order }}</td>
<td>{% decimal item.part.on_order %}</td>
</tr>
{% endfor %}
</tbody>

View File

@@ -15,10 +15,10 @@
{% block collapse_heading %}
<div class='col-sm-2'>
<b>{{ item.sub_part.total_stock }}</b>
<b>{% decimal item.sub_part.total_stock %}</b>
</div>
<div class='col-sm-2'>
<b>{% multiply build.quantity item.quantity %}</b>
<b>{% multiply build.quantity item.quantity %}{% if item.overage %} (+ {{ item.overage }}){% endif %}</b>
</div>
<div class='col-sm-2'>
<b><span id='allocation-total-{{ item.sub_part.id }}'>{% part_allocation_count build item.sub_part %}</span></b>

View File

@@ -34,7 +34,7 @@ InvenTree | Build - {{ build }}
<button type='button' class='btn btn-default btn-glyph' id='build-complete' title="Complete Build">
<span class='glyphicon glyphicon-send'/>
</button>
<button type='button' class='btn btn-default btn-glyph' id='build-cancel'>
<button type='button' class='btn btn-default btn-glyph' id='build-cancel' title='Cancel Build'>
<span class='glyphicon glyphicon-remove'/>
</button>
{% endif %}
@@ -90,6 +90,10 @@ InvenTree | Build - {{ build }}
{% endblock %}
{% block js_load %}
<script type='text/javascript' src="{% static 'script/inventree/stock.js' %}"></script>
{% endblock %}
{% block js_ready %}
$("#build-edit").click(function () {

View File

@@ -0,0 +1,32 @@
{% extends "build/build_base.html" %}
{% load static %}
{% load i18n %}
{% block details %}
{% include "build/tabs.html" with tab='output' %}
<h4>{% trans "Build Outputs" %}</h4>
<hr>
{% include "stock_table.html" with read_only=True %}
{% endblock %}
{% block js_ready %}
{{ block.super }}
loadStockTable($("#stock-table"), {
params: {
location_detail: true,
part_details: true,
build: {{ build.id }},
},
groupByField: 'location',
buttons: [
'#stock-options',
],
url: "{% url 'api-stock-list' %}",
});
{% endblock %}

View File

@@ -1,7 +1,9 @@
{% extends "modal_delete_form.html" %}
{% load i18n %}
{% load inventree_extras %}
{% block pre_form_content %}
Are you sure you want to unallocate these parts?
{% trans "Are you sure you want to unallocate these parts?" %}
<br>
This will remove {{ item.quantity }} parts from build '{{ item.build.title }}'.
This will remove {% decimal item.quantity %} parts from build '{{ item.build.title }}'.
{% endblock %}

View File

@@ -1,74 +1,67 @@
{% extends "build/build_base.html" %}
{% load static %}
{% load i18n %}
{% block details %}
{% include "build/tabs.html" with tab='details' %}
<h4>Build Details</h4>
<h4>{% trans "Build Details" %}</h4>
<hr>
<table class='table table-striped'>
<tr>
<td>Title</td><td>{{ build.title }}</td>
<td>{% trans "Title" %}</td><td>{{ build.title }}</td>
</tr>
<tr>
<td>Part</td><td><a href="{% url 'part-build' build.part.id %}">{{ build.part.full_name }}</a></td>
<td>{% trans "Part" %}</td><td><a href="{% url 'part-build' build.part.id %}">{{ build.part.full_name }}</a></td>
</tr>
<tr>
<td>Quantity</td><td>{{ build.quantity }}</td>
<td>{% trans "Quantity" %}</td><td>{{ build.quantity }}</td>
</tr>
<tr>
<td>Stock Source</td>
<td>{% trans "Stock Source" %}</td>
<td>
{% if build.take_from %}
<a href="{% url 'stock-location-detail' build.take_from.id %}">{{ build.take_from }}</a>
{% else %}
Stock can be taken from any available location.
{% trans "Stock can be taken from any available location." %}
{% endif %}
</td>
</tr>
<tr>
<td>Status</td><td>{% include "build_status.html" with build=build %}</td>
<td>{% trans "Status" %}</td><td>{% include "build_status.html" with build=build %}</td>
</tr>
{% if build.batch %}
<tr>
<td>Batch</td><td>{{ build.batch }}</td>
<td>{% trans "Batch" %}</td><td>{{ build.batch }}</td>
</tr>
{% endif %}
{% if build.URL %}
<tr>
<td>URL</td><td><a href="{{ build.URL }}">{{ build.URL }}</a></td>
<td>{% trans "URL" %}</td><td><a href="{{ build.URL }}">{{ build.URL }}</a></td>
</tr>
{% endif %}
<tr>
<td>Created</td><td>{{ build.creation_date }}</td>
<td>{% trans "Created" %}</td><td>{{ build.creation_date }}</td>
</tr>
{% if build.is_active %}
<tr>
<td>Enough Parts?</td>
<td>{% trans "Enough Parts?" %}</td>
<td>
{% if build.can_build %}
Yes
{% trans "Yes" %}
{% else %}
No
{% trans "No" %}
{% endif %}
</td>
</tr>
{% endif %}
{% if build.completion_date %}
<tr>
<td>Completed</td><td>{{ build.completion_date }}{% if build.completed_by %}<span class='badge'>{{ build.completed_by }}</span>{% endif %}</td>
<td>{% trans "Completed" %}</td><td>{{ build.completion_date }}{% if build.completed_by %}<span class='badge'>{{ build.completed_by }}</span>{% endif %}</td>
</tr>
{% endif %}
</table>
{% if build.notes %}
<div class="panel panel-default">
<div class="panel-heading"><b>Notes</b></div>
<div class="panel-body">{{ build.notes }}</div>
</div>
{% endif %}
{% endblock %}

View File

@@ -44,9 +44,7 @@ InvenTree | Build List
});
});
$(".build-table").bootstrapTable({
sortable: true,
search: true,
$(".build-table").inventreeTable({
formatNoMatches: function() { return 'No builds found'; },
columns: [
{

View File

@@ -0,0 +1,56 @@
{% extends "build/build_base.html" %}
{% load static %}
{% load i18n %}
{% load markdownify %}
{% block details %}
{% include "build/tabs.html" with tab='notes' %}
{% if editing %}
<h4>{% trans "Build Notes" %}</h4>
<hr>
<form method='POST'>
{% csrf_token %}
{{ form }}
<hr>
<input type="submit" value='{% trans "Save" %}'/>
</form>
{{ form.media }}
{% else %}
<div class='row'>
<div class='col-sm-6'>
<h4>{% trans "Build Notes" %}</h4>
</div>
<div class='col-sm-6'>
<button title='{% trans "Edit notes" %}' class='btn btn-default btn-glyph float-right' id='edit-notes'><span class='glyphicon glyphicon-edit'></span></button>
</div>
</div>
<hr>
<div class='panel panel-default'>
<div class='panel-content'>
{{ build.notes | markdownify }}
</div>
</div>
{% endif %}
{% endblock %}
{% block js_ready %}
{{ block.super }}
{% if editing %}
{% else %}
$("#edit-notes").click(function() {
location.href = "{% url 'build-notes' build.id %}?edit=1";
});
{% endif %}
{% endblock %}

View File

@@ -1,8 +1,16 @@
{% load i18n %}
<ul class='nav nav-tabs'>
<li{% if tab == 'details' %} class='active'{% endif %}>
<a href="{% url 'build-detail' build.id %}">Details</a>
<a href="{% url 'build-detail' build.id %}">{% trans "Details" %}</a>
</li>
<li{% if tab == 'output' %} class='active'{% endif %}>
<a href="{% url 'build-output' build.id %}">{% trans "Outputs" %}{% if build.output_count > 0%}<span class='badge'>{{ build.output_count }}</span>{% endif %}</a>
</li>
<li{% if tab == 'notes' %} class='active'{% endif %}>
<a href="{% url 'build-notes' build.id %}">{% trans "Notes" %}{% if build.notes %} <span class='glyphicon glyphicon-small glyphicon-info-sign'></span>{% endif %}</a>
</li>
<li{% if tab == 'allocate' %} class='active'{% endif %}>
<a href="{% url 'build-allocate' build.id %}">Assign Parts</a>
<a href="{% url 'build-allocate' build.id %}">{% trans "Assign Parts" %}</a>
</li>
</ul>

View File

@@ -1,9 +1,10 @@
{% extends "modal_form.html" %}
{% load i18n %}
{% load inventree_extras %}
{% block pre_form_content %}
{{ block.super }}
Are you sure you wish to unallocate all stock for this build?
{% trans "Are you sure you wish to unallocate all stock for this build?" %}
{% endblock %}

View File

@@ -25,6 +25,10 @@ build_detail_urls = [
url(r'^auto-allocate/?', views.BuildAutoAllocate.as_view(), name='build-auto-allocate'),
url(r'^unallocate/', views.BuildUnallocate.as_view(), name='build-unallocate'),
url(r'^notes/', views.BuildNotes.as_view(), name='build-notes'),
url(r'^output/', views.BuildDetail.as_view(template_name='build/build_output.html'), name='build-output'),
url(r'^.*$', views.BuildDetail.as_view(), name='build-detail'),
]

View File

@@ -7,8 +7,9 @@ from __future__ import unicode_literals
from django.utils.translation import ugettext as _
from django.core.exceptions import ValidationError
from django.views.generic import DetailView, ListView
from django.views.generic import DetailView, ListView, UpdateView
from django.forms import HiddenInput
from django.urls import reverse
from part.models import Part
from .models import Build, BuildItem
@@ -52,7 +53,7 @@ class BuildCancel(AjaxUpdateView):
model = Build
ajax_template_name = 'build/cancel.html'
ajax_form_title = 'Cancel Build'
ajax_form_title = _('Cancel Build')
context_object_name = 'build'
form_class = forms.CancelBuildForm
@@ -70,12 +71,12 @@ class BuildCancel(AjaxUpdateView):
if confirm:
build.cancelBuild(request.user)
else:
form.errors['confirm_cancel'] = ['Confirm build cancellation']
form.errors['confirm_cancel'] = [_('Confirm build cancellation')]
valid = False
data = {
'form_valid': valid,
'danger': 'Build was cancelled'
'danger': _('Build was cancelled')
}
return self.renderJsonResponse(request, form, data=data)
@@ -91,7 +92,7 @@ class BuildAutoAllocate(AjaxUpdateView):
model = Build
form_class = forms.ConfirmBuildForm
context_object_name = 'build'
ajax_form_title = 'Allocate Stock'
ajax_form_title = _('Allocate Stock')
ajax_template_name = 'build/auto_allocate.html'
def get_context_data(self, *args, **kwargs):
@@ -104,7 +105,7 @@ class BuildAutoAllocate(AjaxUpdateView):
context['build'] = build
context['allocations'] = build.getAutoAllocations()
except Build.DoesNotExist:
context['error'] = 'No matching build found'
context['error'] = _('No matching build found')
return context
@@ -123,8 +124,8 @@ class BuildAutoAllocate(AjaxUpdateView):
valid = False
if confirm is False:
form.errors['confirm'] = ['Confirm stock allocation']
form.non_field_errors = 'Check the confirmation box at the bottom of the list'
form.errors['confirm'] = [_('Confirm stock allocation')]
form.non_field_errors = _('Check the confirmation box at the bottom of the list')
else:
build.autoAllocate()
valid = True
@@ -144,7 +145,7 @@ class BuildUnallocate(AjaxUpdateView):
model = Build
form_class = forms.ConfirmBuildForm
ajax_form_title = "Unallocate Stock"
ajax_form_title = _("Unallocate Stock")
ajax_template_name = "build/unallocate.html"
def post(self, request, *args, **kwargs):
@@ -157,8 +158,8 @@ class BuildUnallocate(AjaxUpdateView):
valid = False
if confirm is False:
form.errors['confirm'] = ['Confirm unallocation of build stock']
form.non_field_errors = 'Check the confirmation box'
form.errors['confirm'] = [_('Confirm unallocation of build stock')]
form.non_field_errors = _('Check the confirmation box')
else:
build.unallocateStock()
valid = True
@@ -181,7 +182,7 @@ class BuildComplete(AjaxUpdateView):
model = Build
form_class = forms.CompleteBuildForm
context_object_name = "build"
ajax_form_title = "Complete Build"
ajax_form_title = _("Complete Build")
ajax_template_name = "build/complete.html"
def get_form(self):
@@ -254,14 +255,14 @@ class BuildComplete(AjaxUpdateView):
if confirm is False:
form.errors['confirm'] = [
'Confirm completion of build',
_('Confirm completion of build'),
]
else:
try:
location = StockLocation.objects.get(id=loc_id)
valid = True
except StockLocation.DoesNotExist:
form.errors['location'] = ['Invalid location selected']
form.errors['location'] = [_('Invalid location selected')]
serials = []
@@ -305,10 +306,32 @@ class BuildComplete(AjaxUpdateView):
def get_data(self):
""" Provide feedback data back to the form """
return {
'info': 'Build marked as COMPLETE'
'info': _('Build marked as COMPLETE')
}
class BuildNotes(UpdateView):
""" View for editing the 'notes' field of a Build object.
"""
context_object_name = 'build'
template_name = 'build/notes.html'
model = Build
fields = ['notes']
def get_success_url(self):
return reverse('build-notes', kwargs={'pk': self.get_object().id})
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['editing'] = str2bool(self.request.GET.get('edit', ''))
return ctx
class BuildDetail(DetailView):
""" Detail view of a single Build object. """
model = Build
@@ -359,7 +382,7 @@ class BuildCreate(AjaxCreateView):
model = Build
context_object_name = 'build'
form_class = forms.EditBuildForm
ajax_form_title = 'Start new Build'
ajax_form_title = _('Start new Build')
ajax_template_name = 'modal_form.html'
def get_initial(self):
@@ -382,7 +405,7 @@ class BuildCreate(AjaxCreateView):
def get_data(self):
return {
'success': 'Created new build',
'success': _('Created new build'),
}
@@ -392,12 +415,12 @@ class BuildUpdate(AjaxUpdateView):
model = Build
form_class = forms.EditBuildForm
context_object_name = 'build'
ajax_form_title = 'Edit Build Details'
ajax_form_title = _('Edit Build Details')
ajax_template_name = 'modal_form.html'
def get_data(self):
return {
'info': 'Edited build',
'info': _('Edited build'),
}
@@ -406,7 +429,7 @@ class BuildDelete(AjaxDeleteView):
model = Build
ajax_template_name = 'build/delete_build.html'
ajax_form_title = 'Delete Build'
ajax_form_title = _('Delete Build')
class BuildItemDelete(AjaxDeleteView):
@@ -416,12 +439,12 @@ class BuildItemDelete(AjaxDeleteView):
model = BuildItem
ajax_template_name = 'build/delete_build_item.html'
ajax_form_title = 'Unallocate Stock'
ajax_form_title = _('Unallocate Stock')
context_object_name = 'item'
def get_data(self):
return {
'danger': 'Removed parts from build allocation'
'danger': _('Removed parts from build allocation')
}
@@ -431,7 +454,7 @@ class BuildItemCreate(AjaxCreateView):
model = BuildItem
form_class = forms.EditBuildItemForm
ajax_template_name = 'build/create_build_item.html'
ajax_form_title = 'Allocate new Part'
ajax_form_title = _('Allocate new Part')
part = None
available_stock = None
@@ -547,11 +570,11 @@ class BuildItemEdit(AjaxUpdateView):
model = BuildItem
ajax_template_name = 'modal_form.html'
form_class = forms.EditBuildItemForm
ajax_form_title = 'Edit Stock Allocation'
ajax_form_title = _('Edit Stock Allocation')
def get_data(self):
return {
'info': 'Updated Build Item',
'info': _('Updated Build Item'),
}
def get_form(self):

View File

@@ -1,10 +1,21 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.contrib import admin
from .models import Currency
from import_export.admin import ImportExportModelAdmin
from .models import Currency, InvenTreeSetting
class CurrencyAdmin(admin.ModelAdmin):
class CurrencyAdmin(ImportExportModelAdmin):
list_display = ('symbol', 'suffix', 'description', 'value', 'base')
class SettingsAdmin(ImportExportModelAdmin):
list_display = ('key', 'value', 'description')
admin.site.register(Currency, CurrencyAdmin)
admin.site.register(InvenTreeSetting, SettingsAdmin)

View File

@@ -1,5 +1,48 @@
from django.apps import AppConfig
from django.db.utils import OperationalError, ProgrammingError
import os
import yaml
class CommonConfig(AppConfig):
name = 'common'
def ready(self):
""" Will be called when the Common app is first loaded """
self.populate_default_settings()
def populate_default_settings(self):
""" Populate the default values for InvenTree key:value pairs.
If a setting does not exist, it will be created.
"""
from .models import InvenTreeSetting
here = os.path.dirname(os.path.abspath(__file__))
settings_file = os.path.join(here, 'kvp.yaml')
with open(settings_file) as kvp:
values = yaml.safe_load(kvp)
for value in values:
key = value['key']
default = value['default']
description = value['description']
try:
# If a particular setting does not exist in the database, create it now
if not InvenTreeSetting.objects.filter(key=key).exists():
setting = InvenTreeSetting(
key=key,
value=default,
description=description
)
setting.save()
print("Creating new key: '{k}' = '{v}'".format(k=key, v=default))
except (OperationalError, ProgrammingError):
# Migrations have not yet been applied - table does not exist
break

13
InvenTree/common/kvp.yaml Normal file
View File

@@ -0,0 +1,13 @@
# This file contains the default values for the key:value settings available in InvenTree
# This file should not be edited locally.
# Note: The description strings provided here will be translatable,
# so ensure that any translations are provided as appropriate.
- key: 'part_ipn_regex'
default: ''
description: 'Format string for internal part number'
- key: part_deep_copy
default: True
description: 'Parts are deep-copied by default'

View File

@@ -0,0 +1,21 @@
# Generated by Django 2.2.5 on 2019-09-15 12:44
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('common', '0003_auto_20190902_2310'),
]
operations = [
migrations.CreateModel(
name='InvenTreeSetting',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('key', models.CharField(help_text='Settings key', max_length=50, unique=True)),
('value', models.CharField(blank=True, help_text='Settings value', max_length=200)),
],
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 2.2.5 on 2019-09-15 12:56
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('common', '0004_inventreesetting'),
]
operations = [
migrations.AddField(
model_name='inventreesetting',
name='description',
field=models.CharField(blank=True, help_text='Settings description', max_length=200),
),
migrations.AlterField(
model_name='inventreesetting',
name='key',
field=models.CharField(help_text='Settings key (must be unique - case insensitive', max_length=50, unique=True),
),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 2.2.9 on 2020-02-03 09:51
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('common', '0005_auto_20190915_1256'),
]
operations = [
migrations.AlterModelOptions(
name='inventreesetting',
options={'verbose_name': 'InvenTree Setting', 'verbose_name_plural': 'InvenTree Settings'},
),
]

View File

@@ -9,6 +9,83 @@ from __future__ import unicode_literals
from django.db import models
from django.utils.translation import ugettext as _
from django.core.validators import MinValueValidator, MaxValueValidator
from django.core.exceptions import ValidationError
class InvenTreeSetting(models.Model):
"""
An InvenTreeSetting object is a key:value pair used for storing
single values (e.g. one-off settings values).
The class provides a way of retrieving the value for a particular key,
even if that key does not exist.
"""
class Meta:
verbose_name = "InvenTree Setting"
verbose_name_plural = "InvenTree Settings"
@classmethod
def get_setting(cls, key, backup_value=None):
"""
Get the value of a particular setting.
If it does not exist, return the backup value (default = None)
"""
try:
setting = InvenTreeSetting.objects.get(key__iexact=key)
return setting.value
except InvenTreeSetting.DoesNotExist:
return backup_value
@classmethod
def set_setting(cls, key, value, user, create=True):
"""
Set the value of a particular setting.
If it does not exist, option to create it.
Args:
key: settings key
value: New value
user: User object (must be staff member to update a core setting)
create: If True, create a new setting if the specified key does not exist.
"""
if not user.is_staff:
return
try:
setting = InvenTreeSetting.objects.get(key__iexact=key)
except InvenTreeSetting.DoesNotExist:
if create:
setting = InvenTreeSetting(key=key)
else:
return
setting.value = value
setting.save()
key = models.CharField(max_length=50, blank=False, unique=True, help_text=_('Settings key (must be unique - case insensitive'))
value = models.CharField(max_length=200, blank=True, unique=False, help_text=_('Settings value'))
description = models.CharField(max_length=200, blank=True, unique=False, help_text=_('Settings description'))
def validate_unique(self, exclude=None):
""" Ensure that the key:value pair is unique.
In addition to the base validators, this ensures that the 'key'
is unique, using a case-insensitive comparison.
"""
super().validate_unique(exclude)
try:
setting = InvenTreeSetting.objects.exclude(id=self.id).filter(key__iexact=self.key)
if setting.exists():
raise ValidationError({'key': _('Key string must be unique')})
except InvenTreeSetting.DoesNotExist:
pass
class Currency(models.Model):

View File

@@ -5,6 +5,8 @@ Django views for interacting with common models
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.utils.translation import ugettext as _
from InvenTree.views import AjaxCreateView, AjaxUpdateView, AjaxDeleteView
from . import models
@@ -16,7 +18,7 @@ class CurrencyCreate(AjaxCreateView):
model = models.Currency
form_class = forms.CurrencyEditForm
ajax_form_title = 'Create new Currency'
ajax_form_title = _('Create new Currency')
class CurrencyEdit(AjaxUpdateView):
@@ -24,12 +26,12 @@ class CurrencyEdit(AjaxUpdateView):
model = models.Currency
form_class = forms.CurrencyEditForm
ajax_form_title = 'Edit Currency'
ajax_form_title = _('Edit Currency')
class CurrencyDelete(AjaxDeleteView):
""" View for deleting an existing Currency object """
model = models.Currency
ajax_form_title = 'Delete Currency'
ajax_form_title = _('Delete Currency')
ajax_template_name = "common/delete_currency.html"

View File

@@ -1,20 +1,91 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.contrib import admin
from import_export.admin import ImportExportModelAdmin
from import_export.resources import ModelResource
from import_export.fields import Field
import import_export.widgets as widgets
from .models import Company
from .models import SupplierPart
from .models import SupplierPriceBreak
from part.models import Part
from common.models import Currency
class CompanyResource(ModelResource):
""" Class for managing Company data import/export """
class Meta:
model = Company
skip_unchanged = True
report_skipped = False
clean_model_instances = True
class CompanyAdmin(ImportExportModelAdmin):
resource_class = CompanyResource
list_display = ('name', 'website', 'contact')
class SupplierPartResource(ModelResource):
""" Class for managing SupplierPart data import/export """
part = Field(attribute='part', widget=widgets.ForeignKeyWidget(Part))
part_name = Field(attribute='part__full_name', readonly=True)
supplier = Field(attribute='supplier', widget=widgets.ForeignKeyWidget(Company))
supplier_name = Field(attribute='supplier__name', readonly=True)
class Meta:
model = SupplierPart
skip_unchanged = True
report_skipped = False
clean_model_instances = True
class SupplierPartAdmin(ImportExportModelAdmin):
resource_class = SupplierPartResource
list_display = ('part', 'supplier', 'SKU')
class SupplierPriceBreakResource(ModelResource):
""" Class for managing SupplierPriceBreak data import/export """
part = Field(attribute='part', widget=widgets.ForeignKeyWidget(SupplierPart))
currency = Field(attribute='currency', widget=widgets.ForeignKeyWidget(Currency))
supplier_id = Field(attribute='part__supplier__pk', readonly=True)
supplier_name = Field(attribute='part__supplier__name', readonly=True)
part_name = Field(attribute='part__part__full_name', readonly=True)
SKU = Field(attribute='part__SKU', readonly=True)
MPN = Field(attribute='part__MPN', readonly=True)
class Meta:
model = SupplierPriceBreak
skip_unchanged = True
report_skipped = False
clean_model_instances = True
class SupplierPriceBreakAdmin(ImportExportModelAdmin):
resource_class = SupplierPriceBreakResource
list_display = ('part', 'quantity', 'cost')

View File

@@ -31,6 +31,7 @@ class CompanyList(generics.ListCreateAPIView):
serializer_class = CompanySerializer
queryset = Company.objects.all()
permission_classes = [
permissions.IsAuthenticated,
]

View File

@@ -15,6 +15,13 @@
supplier: 1
SKU: 'ACME0002'
- model: company.supplierpart
pk: 3
fields:
part: 1
supplier: 1
SKU: 'ACME0003'
# Widget purchaseable from ACME
- model: company.supplierpart
pk: 100
@@ -33,7 +40,7 @@
# M2x4 LPHS from Zerg Corp
- model: company.supplierpart
pk: 3
pk: 7
fields:
part: 1
supplier: 3

View File

@@ -6,6 +6,7 @@ Django Forms for interacting with Company app
from __future__ import unicode_literals
from InvenTree.forms import HelperForm
from InvenTree.fields import RoundingDecimalFormField
from .models import Company
from .models import SupplierPart
@@ -27,7 +28,6 @@ class EditCompanyForm(HelperForm):
'contact',
'is_customer',
'is_supplier',
'notes'
]
@@ -65,6 +65,10 @@ class EditSupplierPartForm(HelperForm):
class EditPriceBreakForm(HelperForm):
""" Form for creating / editing a supplier price break """
quantity = RoundingDecimalFormField(max_digits=10, decimal_places=5)
cost = RoundingDecimalFormField(max_digits=10, decimal_places=5)
class Meta:
model = SupplierPriceBreak
fields = [

View File

@@ -0,0 +1,24 @@
# Generated by Django 2.2.5 on 2019-09-13 14:07
import InvenTree.fields
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('company', '0007_remove_supplierpart_lead_time'),
]
operations = [
migrations.AlterField(
model_name='company',
name='URL',
field=InvenTree.fields.InvenTreeURLField(blank=True, help_text='Link to external company information'),
),
migrations.AlterField(
model_name='supplierpart',
name='URL',
field=InvenTree.fields.InvenTreeURLField(blank=True, help_text='URL for external supplier part link'),
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 2.2.5 on 2019-11-18 23:23
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('company', '0008_auto_20190913_1407'),
]
operations = [
migrations.AlterField(
model_name='supplierpricebreak',
name='quantity',
field=models.DecimalField(decimal_places=5, default=1, max_digits=15, validators=[django.core.validators.MinValueValidator(1)]),
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 2.2.9 on 2020-02-01 12:31
from django.db import migrations
import markdownx.models
class Migration(migrations.Migration):
dependencies = [
('company', '0009_auto_20191118_2323'),
]
operations = [
migrations.AlterField(
model_name='company',
name='notes',
field=markdownx.models.MarkdownxField(blank=True),
),
]

View File

@@ -0,0 +1,20 @@
# Generated by Django 2.2.9 on 2020-03-18 11:14
import InvenTree.fields
import django.core.validators
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('company', '0010_auto_20200201_1231'),
]
operations = [
migrations.AlterField(
model_name='supplierpricebreak',
name='cost',
field=InvenTree.fields.RoundingDecimalField(decimal_places=5, max_digits=10, validators=[django.core.validators.MinValueValidator(0)]),
),
]

View File

@@ -0,0 +1,20 @@
# Generated by Django 2.2.9 on 2020-03-18 11:14
import InvenTree.fields
import django.core.validators
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('company', '0011_auto_20200318_1114'),
]
operations = [
migrations.AlterField(
model_name='supplierpricebreak',
name='quantity',
field=InvenTree.fields.RoundingDecimalField(decimal_places=5, default=1, max_digits=15, validators=[django.core.validators.MinValueValidator(1)]),
),
]

View File

@@ -10,6 +10,7 @@ import os
import math
from decimal import Decimal
from django.utils.translation import gettext_lazy as _
from django.core.validators import MinValueValidator
from django.db import models
from django.db.models import Sum
@@ -17,8 +18,10 @@ from django.db.models import Sum
from django.apps import apps
from django.urls import reverse
from django.conf import settings
from django.contrib.staticfiles.templatetags.staticfiles import static
from markdownx.models import MarkdownxField
from InvenTree.fields import InvenTreeURLField, RoundingDecimalField
from InvenTree.status_codes import OrderStatus
from common.models import Currency
@@ -68,32 +71,32 @@ class Company(models.Model):
"""
name = models.CharField(max_length=100, blank=False, unique=True,
help_text='Company name')
help_text=_('Company name'))
description = models.CharField(max_length=500, help_text='Description of the company')
description = models.CharField(max_length=500, help_text=_('Description of the company'))
website = models.URLField(blank=True, help_text='Company website URL')
website = models.URLField(blank=True, help_text=_('Company website URL'))
address = models.CharField(max_length=200,
blank=True, help_text='Company address')
blank=True, help_text=_('Company address'))
phone = models.CharField(max_length=50,
blank=True, help_text='Contact phone number')
blank=True, help_text=_('Contact phone number'))
email = models.EmailField(blank=True, help_text='Contact email address')
email = models.EmailField(blank=True, help_text=_('Contact email address'))
contact = models.CharField(max_length=100,
blank=True, help_text='Point of contact')
blank=True, help_text=_('Point of contact'))
URL = models.URLField(blank=True, help_text='Link to external company information')
URL = InvenTreeURLField(blank=True, help_text=_('Link to external company information'))
image = models.ImageField(upload_to=rename_company_image, max_length=255, null=True, blank=True)
notes = models.TextField(blank=True)
notes = MarkdownxField(blank=True)
is_customer = models.BooleanField(default=False, help_text='Do you sell items to this company?')
is_customer = models.BooleanField(default=False, help_text=_('Do you sell items to this company?'))
is_supplier = models.BooleanField(default=True, help_text='Do you purchase items from this company?')
is_supplier = models.BooleanField(default=True, help_text=_('Do you purchase items from this company?'))
def __str__(self):
""" Get string representation of a Company """
@@ -109,7 +112,7 @@ class Company(models.Model):
if self.image:
return os.path.join(settings.MEDIA_URL, str(self.image.url))
else:
return static('/img/blank_image.png')
return os.path.join(settings.STATIC_URL, 'img/blank_image.png')
@property
def part_count(self):
@@ -223,32 +226,32 @@ class SupplierPart(models.Model):
'purchaseable': True,
'is_template': False,
},
help_text='Select part',
help_text=_('Select part'),
)
supplier = models.ForeignKey(Company, on_delete=models.CASCADE,
related_name='parts',
limit_choices_to={'is_supplier': True},
help_text='Select supplier',
help_text=_('Select supplier'),
)
SKU = models.CharField(max_length=100, help_text='Supplier stock keeping unit')
SKU = models.CharField(max_length=100, help_text=_('Supplier stock keeping unit'))
manufacturer = models.CharField(max_length=100, blank=True, help_text='Manufacturer')
manufacturer = models.CharField(max_length=100, blank=True, help_text=_('Manufacturer'))
MPN = models.CharField(max_length=100, blank=True, help_text='Manufacturer part number')
MPN = models.CharField(max_length=100, blank=True, help_text=_('Manufacturer part number'))
URL = models.URLField(blank=True, help_text='URL for external supplier part link')
URL = InvenTreeURLField(blank=True, help_text=_('URL for external supplier part link'))
description = models.CharField(max_length=250, blank=True, help_text='Supplier part description')
description = models.CharField(max_length=250, blank=True, help_text=_('Supplier part description'))
note = models.CharField(max_length=100, blank=True, help_text='Notes')
note = models.CharField(max_length=100, blank=True, help_text=_('Notes'))
base_cost = models.DecimalField(max_digits=10, decimal_places=3, default=0, validators=[MinValueValidator(0)], help_text='Minimum charge (e.g. stocking fee)')
base_cost = models.DecimalField(max_digits=10, decimal_places=3, default=0, validators=[MinValueValidator(0)], help_text=_('Minimum charge (e.g. stocking fee)'))
packaging = models.CharField(max_length=50, blank=True, help_text='Part packaging')
packaging = models.CharField(max_length=50, blank=True, help_text=_('Part packaging'))
multiple = models.PositiveIntegerField(default=1, validators=[MinValueValidator(1)], help_text='Order multiple')
multiple = models.PositiveIntegerField(default=1, validators=[MinValueValidator(1)], help_text=('Order multiple'))
# TODO - Reimplement lead-time as a charfield with special validation (pattern matching).
# lead_time = models.DurationField(blank=True, null=True)
@@ -378,9 +381,9 @@ class SupplierPriceBreak(models.Model):
part = models.ForeignKey(SupplierPart, on_delete=models.CASCADE, related_name='pricebreaks')
quantity = models.PositiveIntegerField(default=1, validators=[MinValueValidator(1)])
quantity = RoundingDecimalField(max_digits=15, decimal_places=5, default=1, validators=[MinValueValidator(1)])
cost = models.DecimalField(max_digits=10, decimal_places=5, validators=[MinValueValidator(0)])
cost = RoundingDecimalField(max_digits=10, decimal_places=5, validators=[MinValueValidator(0)])
currency = models.ForeignKey(Currency, blank=True, null=True, on_delete=models.SET_NULL)

View File

@@ -1,9 +1,10 @@
{% extends "base.html" %}
{% load static %}
{% load i18n %}
{% block page_title %}
InvenTree | Company - {{ company.name }}
InvenTree | {% trans "Company" %} - {{ company.name }}
{% endblock %}
{% block content %}
@@ -44,27 +45,27 @@ InvenTree | Company - {{ company.name }}
<table class="table">
{% if company.website %}
<tr>
<td>Website</td><td><a href="{{ company.website }}">{{ company.website }}</a></td>
<td>{% trans "Website" %}</td><td><a href="{{ company.website }}">{{ company.website }}</a></td>
</tr>
{% endif %}
{% if company.address %}
<tr>
<td>Address</td><td>{{ company.address }}</td>
<td>{% trans "Address" %}</td><td>{{ company.address }}</td>
</tr>
{% endif %}
{% if company.phone %}
<tr>
<td>Phone</td><td>{{ company.phone }}</td>
<td>{% trans "Phone" %}</td><td>{{ company.phone }}</td>
</tr>
{% endif %}
{% if company.email %}
<tr>
<td>Email</td><td>{{ company.email }}</td>
<td>{% trans "Email" %}</td><td>{{ company.email }}</td>
</tr>
{% endif %}
{% if company.contact %}
<tr>
<td>Contact</td><td>{{ company.contact }}</td>
<td>{% trans "Contact" %}</td><td>{{ company.contact }}</td>
</tr>
{% endif %}
</table>
@@ -99,7 +100,7 @@ InvenTree | Company - {{ company.name }}
});
$("#company-order-2").click(function() {
launchModalForm("{% url 'purchase-order-create' %}",
launchModalForm("{% url 'po-create' %}",
{
data: {
supplier: {{ company.id }},

View File

@@ -1,30 +1,24 @@
{% extends "company/company_base.html" %}
{% load static %}
{% load i18n %}}
{% block details %}
{% include 'company/tabs.html' with tab='details' %}
<h4>Company Details</h4>
<h4>{% trans "Company Details" %}</h4>
<hr>
<table class='table table-striped'>
<tr>
<td>Customer</td>
<td>{% trans "Customer" %}</td>
<td>{% include 'yesnolabel.html' with value=company.is_customer %}</td>
</tr>
<tr>
<td>Supplier</td>
<td>{% trans "Supplier" %}</td>
<td>{% include 'yesnolabel.html' with value=company.is_supplier %}</td>
</tr>
</table>
{% if company.notes %}
<div class="panel panel-default">
<div class="panel-heading"><b>Notes</b></div>
<div class="panel-body">{{ company.notes }}</div>
</div>
{% endif %}
{% endblock %}
{% block js_ready %}
{{ block.super }}

View File

@@ -1,20 +1,22 @@
{% extends "company/company_base.html" %}
{% load static %}
{% block details %}
{% load i18n %}
{% include 'company/tabs.html' with tab='parts' %}
<h4>Supplier Parts</h4>
<h4>{% trans "Supplier Parts" %}</h4>
<hr>
<div id='button-toolbar'>
<button class="btn btn-success" id='part-create'>New Supplier Part</button>
<button class="btn btn-success" id='part-create'>{% trans "New Supplier Part" %}</button>
<div class="dropdown" style="float: right;">
<button class="btn btn-primary dropdown-toggle" type="button" data-toggle="dropdown">Options
<button class="btn btn-primary dropdown-toggle" type="button" data-toggle="dropdown">{% trans "Options" %}
<span class="caret"></span></button>
<ul class="dropdown-menu">
<li><a href='#' id='multi-part-order' title='Order parts'>Order Parts</a></li>
<li><a href='#' id='multi-part-order' title='Order parts'>{% trans "Order Parts" %}</a></li>
<li><a href='#' id='multi-part-delete' title='Delete parts'>{% trans "Delete Parts" %}</a></li>
</ul>
</div>
</div>
@@ -45,11 +47,7 @@
});
});
$("#part-table").bootstrapTable({
sortable: true,
search: true,
pagination: true,
pageSize: 50,
$("#part-table").inventreeTable({
formatNoMatches: function() { return "No supplier parts found for {{ company.name }}"; },
queryParams: function(p) {
return {
@@ -64,7 +62,7 @@
{
sortable: true,
field: 'part_detail.full_name',
title: 'Part',
title: '{% trans "Part" %}',
formatter: function(value, row, index, field) {
return imageHoverIcon(row.part_detail.image_url) + renderLink(value, '/part/' + row.part + '/suppliers/');
}
@@ -72,7 +70,7 @@
{
sortable: true,
field: 'SKU',
title: 'SKU',
title: '{% trans "SKU" %}',
formatter: function(value, row, index, field) {
return renderLink(value, row.url);
}
@@ -80,7 +78,7 @@
{
sortable: true,
field: 'manufacturer',
title: 'Manufacturer',
title: '{% trans "Manufacturer" %}',
},
{
sortable: true,
@@ -89,7 +87,7 @@
},
{
field: 'URL',
title: 'URL',
title: '{% trans "URL" %}',
formatter: function(value, row, index, field) {
if (value) {
return renderLink(value, value);
@@ -102,6 +100,23 @@
url: "{% url 'api-part-supplier-list' %}"
});
$("#multi-part-delete").click(function() {
var selections = $("#part-table").bootstrapTable("getSelections");
var parts = [];
selections.forEach(function(item) {
parts.push(item.pk);
});
launchModalForm("{% url 'supplier-part-delete' %}", {
data: {
parts: parts,
},
reload: true,
});
});
$("#multi-part-order").click(function() {
var selections = $("#part-table").bootstrapTable("getSelections");

View File

@@ -1,31 +1,34 @@
{% extends "company/company_base.html" %}
{% load static %}
{% block details %}
{% load i18n %}
{% include 'company/tabs.html' with tab='po' %}
<h4>Open Purchase Orders</h4>
<h4>{% trans "Purchase Orders" %}</h4>
<hr>
<div id='button-bar'>
<div class='btn-group'>
<button class='btn btn-primary' type='button' id='company-order-2' title='Create new purchase order'>New Purchase Order</button>
<button class='btn btn-primary' type='button' id='company-order2' title='Create new purchase order'>{% trans "New Purchase Order" %}</button>
</div>
</div>
{% include "order/po_table.html" with orders=company.outstanding_purchase_orders.all toolbar='#button-bar' %}
{% if company.closed_purchase_orders.count > 0 %}
{% include "order/po_table_collapse.html" with title="Closed Orders" orders=company.closed_purchase_orders.all %}
{% endif %}
<table class='table table-striped table-condensed po-table' id='purchase-order-table' data-toolbar='#button-bar'>
</table>
{% endblock %}
{% block js_ready %}
{{ block.super }}
loadPurchaseOrderTable($("#purchase-order-table"), {
url: "{% url 'api-po-list' %}?supplier={{ company.id }}",
});
function newOrder() {
launchModalForm("{% url 'purchase-order-create' %}",
launchModalForm("{% url 'po-create' %}",
{
data: {
supplier: {{ company.id }},
@@ -38,13 +41,11 @@
newOrder();
});
$("#company-order-2").click(function() {
$("#company-order2").click(function() {
newOrder();
});
$("#po-table").bootstrapTable({
search: true,
sortable: true,
$(".po-table").inventreeTable({
});
{% endblock %}
{% endblock %}

View File

@@ -1,11 +1,12 @@
{% extends "company/company_base.html" %}
{% load static %}
{% load i18n %}
{% block details %}
{% include "company/tabs.html" with tab='stock' %}
<h4>Supplier Stock</h4>
<h4>{% trans "Supplier Stock" %}</h4>
<hr>
@@ -29,7 +30,7 @@
$("#stock-export").click(function() {
launchModalForm("{% url 'stock-export-options' %}", {
submit_text: "Export",
submit_text: '{% trans "Export" %}',
success: function(response) {
var url = "{% url 'stock-export' %}";

View File

@@ -1,19 +1,20 @@
{% extends "base.html" %}
{% load static %}
{% load i18n %}
{% block page_title %}
InvenTree | Supplier List
InvenTree | {% trans "Supplier List" %}
{% endblock %}
{% block content %}
<h3>Supplier List</h3>
<h3>{% trans "Supplier List" %}</h3>
<hr>
<div id='button-toolbar'>
<div class='btn-group'>
<button type='button' class="btn btn-success" id='new-company' title='Add new supplier'>New Supplier</button>
<button type='button' class="btn btn-success" id='new-company' title='Add new supplier'>{% trans "New Supplier" %}</button>
</div>
</div>
@@ -32,21 +33,17 @@ InvenTree | Supplier List
});
});
$("#company-table").bootstrapTable({
sortable: true,
search: true,
pagination: true,
pageSize: 50,
$("#company-table").inventreeTable({
formatNoMatches: function() { return "No company information found"; },
columns: [
{
field: 'pk',
title: 'ID',
title: '{% trans "ID" %}',
visible: false,
},
{
field: 'name',
title: 'Supplier',
title: '{% trans "Supplier" %}',
sortable: true,
formatter: function(value, row, index, field) {
return imageHoverIcon(row.image) + renderLink(value, row.url);
@@ -54,12 +51,12 @@ InvenTree | Supplier List
},
{
field: 'description',
title: 'Description',
title: '{% trans "Description" %}',
sortable: true,
},
{
field: 'website',
title: 'Website',
title: '{% trans "Website" %}',
formatter: function(value, row, index, field) {
if (value) {
return renderLink(value, value);
@@ -69,7 +66,7 @@ InvenTree | Supplier List
},
{
field: 'part_count',
title: 'Parts',
title: '{% trans "Parts" %}',
sortable: true,
formatter: function(value, row, index, field) {
return renderLink(value, row.url + 'parts/');

View File

@@ -0,0 +1,53 @@
{% extends "company/company_base.html" %}
{% load static %}
{% load i18n %}
{% block details %}
{% load markdownify %}
{% include 'company/tabs.html' with tab='notes' %}
{% if editing %}
<h4>{% trans "Company Notes" %}</h4>
<hr>
<form method='POST'>
{% csrf_token %}
{{ form }}
<hr>
<input type="submit" value='{% trans "Save" %}'/>
</form>
{{ form.media }}
{% else %}
<div class='row'>
<div class='col-sm-6'>
<h4>{% trans "Company Notes" %}</h4>
</div>
<div class='col-sm-6'>
<button title='{% trans "Edit notes" %}' class='btn btn-default btn-glyph float-right' id='edit-notes'><span class='glyphicon glyphicon-edit'></span></button>
</div>
</div>
<hr>
<div class='panel panel-default'>
<div class='panel-content'>
{{ company.notes | markdownify }}
</div>
</div>
{% endif %}
{% endblock %}
{% block js_ready %}
{{ block.super }}
{% if editing %}
{% else %}
$("#edit-notes").click(function() {
location.href = "{% url 'company-notes' company.id %}?edit=1";
});
{% endif %}
{% endblock %}

View File

@@ -1,49 +0,0 @@
{% extends "base.html" %}
{% block content %}
<h3>Supplier Order Details</h3>
<table class='table table-striped'>
<tr>
<td>Reference</td>
<td>{{ order.internal_ref }}</td>
</tr>
<tr>
<td>Supplier</td>
<td>
{% if order.supplier %}
<a href="{% url 'supplier-detail-orders' order.supplier.id %}">{{ order.supplier.name }}</a>
{% endif %}
</td>
</tr>
<tr>
<td>Status</td>
<td>{% include "supplier/order_status.html" with order=order %}</td>
</tr>
<tr>
<td>Created Date</td>
<td>{{ order.created_date }}</td>
</tr>
<tr>
<td>Issued Date</td>
<td>{{ order.issued_date }}</td>
</tr>
<tr>
<td>Delivered Date</td>
<td>{{ order.delivery_date }}</td>
</tr>
</table>
{% if order.notes %}
<div class="panel panel-default">
<div class="panel-heading"><b>Notes</b></div>
<div class="panel-body">{{ order.notes }}</div>
</div>
{% endif %}
<h2>TODO</h2>
Here we list all the line ites which exist under this order...
{% endblock %}

View File

@@ -1,5 +1,28 @@
{% extends "modal_delete_form.html" %}
{% load i18n %}
{% block pre_form_content %}
Are you sure you want to delete this supplier part?
{% trans "Are you sure you want to delete the following Supplier Parts?" %}
<hr>
{% endblock %}
{% block form_data %}
<table class='table table-striped table-condensed'>
{% for part in parts %}
<tr>
<input type='hidden' name='supplier-part-{{ part.id}}' value='supplier-part-{{ part.id }}'/>
<td>
{% include "hover_image.html" with image=part.supplier.image %}
{{ part.supplier.name }}
</td>
<td>
{% include "hover_image.html" with image=part.part.image %}
{{ part.part.full_name }}
</td>
</tr>
{% endfor %}
</table>
{% endblock %}

View File

@@ -1,171 +0,0 @@
{% extends "base.html" %}
{% load static %}
{% block page_title %}
InvenTree | {{ company.name }} - Parts
{% endblock %}
{% block content %}
<div class='row'>
<div class='col-sm-6'>
<h3>Supplier Part</h3>
<div class='btn-row'>
<div class='btn-group'>
<button type='button' class='btn btn-default btn-glyph' id='edit-part' title='Edit supplier part'>
<span class='glyphicon glyphicon-edit'/>
</button>
<button type='button' class='btn btn-default btn-glyph' id='delete-part' title='Delete supplier part'>
<span class='glyphicon glyphicon-trash'/>
</button>
</div>
</div>
</div>
<div class='col-sm-6'>
<div class='media-left'>
<img class='part-thumb'
{% if part.part.image %}
src='{{ part.part.image.url }}'
{% else %}
src="{% static 'img/blank_image.png' %}"
{% endif %}/>
</div>
</div>
</div>
<hr>
<div class='row'>
<div class='col-sm-6'>
<h4>Supplier Part Details</h4>
<table class="table table-striped table-condensed">
<tr>
<td>Internal Part</td>
<td>
{% if part.part %}
<a href="{% url 'part-suppliers' part.part.id %}">{{ part.part.full_name }}</a>
{% endif %}
</td>
</tr>
<tr><td>Supplier</td><td><a href="{% url 'company-detail-parts' part.supplier.id %}">{{ part.supplier.name }}</a></td></tr>
<tr><td>SKU</td><td>{{ part.SKU }}</tr></tr>
{% if part.URL %}
<tr><td>URL</td><td><a href="{{ part.URL }}">{{ part.URL }}</a></td></tr>
{% endif %}
{% if part.description %}
<tr><td>Description</td><td>{{ part.description }}</td></tr>
{% endif %}
{% if part.manufacturer %}
<tr><td>Manufacturer</td><td>{{ part.manufacturer }}</td></tr>
<tr><td>MPN</td><td>{{ part.MPN }}</td></tr>
{% endif %}
{% if part.note %}
<tr><td>Note</td><td>{{ part.note }}</td></tr>
{% endif %}
</table>
</div>
<div class='col-sm-6'>
<h4>Pricing Information</h4>
<table class="table table-striped table-condensed">
<tr><td>Order Multiple</td><td>{{ part.multiple }}</td></tr>
{% if part.base_cost > 0 %}
<tr><td>Base Price (Flat Fee)</td><td>{{ part.base_cost }}</td></tr>
{% endif %}
<tr>
<th>Price Breaks</th>
<th>
<div style='float: right;'>
<button class='btn btn-primary' id='new-price-break' type='button'>New Price Break</button>
</div>
</th>
</tr>
<tr>
<th>Quantity</th>
<th>Price</th>
</tr>
{% if part.price_breaks.all %}
{% for pb in part.price_breaks.all %}
<tr>
<td>{{ pb.quantity }}</td>
<td>
{% if pb.currency %}{{ pb.currency.symbol }}{% endif %}
{{ pb.cost }}
{% if pb.currency %}{{ pb.currency.suffix }}{% endif %}
<div class='btn-group' style='float: right;'>
<button title='Edit Price Break' class='btn btn-default btn-sm' type='button' url="{% url 'price-break-edit' pb.id %}"><span class='glyphicon glyphicon-edit'></span></button>
<button title='Delete Price Break' class='btn btn-default btn-sm' type='button' url="{% url 'price-break-delete' pb.id %}"><span class='glyphicon glyphicon-trash'></span></button>
</div>
</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan='2'>
<span class='warning-msg'><i>No price breaks have been added for this part</i></span>
</td>
</tr>
{% endif %}
</table>
</div>
</div>
<hr>
<h4>Purchase Orders</h4>
{% include "order/po_table.html" with orders=part.purchase_orders %}
{% endblock %}
{% block js_ready %}
{{ block.super }}
$('#edit-part').click(function () {
launchModalForm(
"{% url 'supplier-part-edit' part.id %}",
{
reload: true
}
);
});
$('#delete-part').click(function() {
launchModalForm(
"{% url 'supplier-part-delete' part.id %}",
{
redirect: "{% url 'company-index' %}"
}
);
});
$('#new-price-break').click(function() {
launchModalForm("{% url 'price-break-create' %}",
{
reload: true,
data: {
part: {{ part.id }},
}
}
);
});
$('.pb-edit-button').click(function() {
var button = $(this);
launchModalForm(button.attr('url'),
{
reload: true,
}
);
});
$('.pb-delete-button').click(function() {
var button = $(this);
launchModalForm(button.attr('url'),
{
reload: true,
}
);
});
{% endblock %}

View File

@@ -0,0 +1,98 @@
{% extends "base.html" %}
{% load static %}
{% load i18n %}
{% block page_title %}
InvenTree | {% trans "Supplier Part" %}
{% endblock %}
{% block content %}
<div class='row'>
<div class='col-sm-6'>
<h3>{% trans "Supplier Part" %}</h3>
<div class='btn-row'>
<div class='btn-group'>
<button type='button' class='btn btn-default btn-glyph' id='edit-part' title='Edit supplier part'>
<span class='glyphicon glyphicon-edit'/>
</button>
<button type='button' class='btn btn-default btn-glyph' id='delete-part' title='Delete supplier part'>
<span class='glyphicon glyphicon-trash'/>
</button>
</div>
</div>
<div class='media-left'>
<img class='part-thumb'
{% if part.part.image %}
src='{{ part.part.image.url }}'
{% else %}
src="{% static 'img/blank_image.png' %}"
{% endif %}/>
</div>
</div>
<div class='col-sm-6'>
<h4>{% trans "Supplier Part Details" %}</h4>
<table class="table table-striped table-condensed">
<tr>
<td>{% trans "Internal Part" %}</td>
<td>
{% if part.part %}
<a href="{% url 'part-suppliers' part.part.id %}">{{ part.part.full_name }}</a>
{% endif %}
</td>
</tr>
<tr><td>{% trans "Supplier" %}</td><td><a href="{% url 'company-detail-parts' part.supplier.id %}">{{ part.supplier.name }}</a></td></tr>
<tr><td>{% trans "SKU" %}</td><td>{{ part.SKU }}</tr></tr>
{% if part.URL %}
<tr><td>{% trans "URL" %}</td><td><a href="{{ part.URL }}">{{ part.URL }}</a></td></tr>
{% endif %}
{% if part.description %}
<tr><td>{% trans "Description" %}</td><td>{{ part.description }}</td></tr>
{% endif %}
{% if part.manufacturer %}
<tr><td>{% trans "Manufacturer" %}</td><td>{{ part.manufacturer }}</td></tr>
<tr><td>{% trans "MPN" %}</td><td>{{ part.MPN }}</td></tr>
{% endif %}
{% if part.note %}
<tr><td>{% trans "Note" %}</td><td>{{ part.note }}</td></tr>
{% endif %}
</table>
</div>
</div>
<hr>
<div class='container-fluid'>
{% block details %}
<!-- Particular SupplierPart page goes here ... -->
{% endblock %}
</div>
{% endblock %}
{% block js_ready %}
{{ block.super }}
$('#edit-part').click(function () {
launchModalForm(
"{% url 'supplier-part-edit' part.id %}",
{
reload: true
}
);
});
$('#delete-part').click(function() {
launchModalForm(
"{% url 'supplier-part-delete' %}?part={{ part.id }}",
{
redirect: "{% url 'company-detail-parts' part.supplier.id %}"
}
);
});
{% endblock %}

View File

@@ -0,0 +1,44 @@
{% extends "company/supplier_part_base.html" %}
{% load static %}
{% load i18n %}
{% block details %}
{% include "company/supplier_part_tabs.html" with tab='details' %}
<hr>
<h4>{% trans "Supplier Part Details" %}</h4>
<table class="table table-striped table-condensed">
<tr>
<td>{% trans "Internal Part" %}</td>
<td>
{% if part.part %}
<a href="{% url 'part-suppliers' part.part.id %}">{{ part.part.full_name }}</a>
{% endif %}
</td>
</tr>
<tr><td>{% trans "Supplier" %}</td><td><a href="{% url 'company-detail-parts' part.supplier.id %}">{{ part.supplier.name }}</a></td></tr>
<tr><td>{% trans "SKU" %}</td><td>{{ part.SKU }}</tr></tr>
{% if part.URL %}
<tr><td>{% trans "URL" %}</td><td><a href="{{ part.URL }}">{{ part.URL }}</a></td></tr>
{% endif %}
{% if part.description %}
<tr><td>{% trans "Description" %}</td><td>{{ part.description }}</td></tr>
{% endif %}
{% if part.manufacturer %}
<tr><td>{% trans "Manufacturer" %}</td><td>{{ part.manufacturer }}</td></tr>
<tr><td>{% trans "MPN" %}</td><td>{{ part.MPN }}</td></tr>
{% endif %}
{% if part.note %}
<tr><td>{% trans "Note" %}</td><td>{{ part.note }}</td></tr>
{% endif %}
</table>
{% endblock %}
{% block js_ready %}
{{ block.super }}
{% endblock %}

View File

@@ -0,0 +1,31 @@
{% extends "company/supplier_part_base.html" %}
{% load static %}
{% load i18n %}
{% block details %}
{% include "company/supplier_part_tabs.html" with tab='orders' %}
<hr>
<h4>{% trans "Supplier Part Orders" %}</h4>
<div id='button-bar'>
<div class='btn-group'>
<button class='btn btn-primary' type='button' id='part-order2' title='Order part'>Order Part</button>
</div>
</div>
<table class='table table-striped table-condensed po-table' id='purchase-order-table' data-toolbar='#button-bar'>
</table>
{% endblock %}
{% block js_ready %}
{{ block.super }}
loadPurchaseOrderTable($("#purchase-order-table"), {
url: "{% url 'api-po-list' %}?supplier_part={{ part.id }}",
});
{% endblock %}

View File

@@ -0,0 +1,92 @@
{% extends "company/supplier_part_base.html" %}
{% load static %}
{% load i18n %}
{% load inventree_extras %}
{% block details %}
{% include "company/supplier_part_tabs.html" with tab='pricing' %}
<hr>
<h4>{% trans "Pricing Information" %}</h4>
<table class="table table-striped table-condensed">
<tr><td>{% trans "Order Multiple" %}</td><td>{{ part.multiple }}</td></tr>
{% if part.base_cost > 0 %}
<tr><td>{% trans "Base Price (Flat Fee)" %}</td><td>{{ part.base_cost }}</td></tr>
{% endif %}
<tr>
<th>{% trans "Price Breaks" %}</th>
<th>
<div style='float: right;'>
<button class='btn btn-primary' id='new-price-break' type='button'>{% trans "New Price Break" %}</button>
</div>
</th>
</tr>
<tr>
<th>{% trans "Quantity" %}</th>
<th>{% trans "Price" %}</th>
</tr>
{% if part.price_breaks.all %}
{% for pb in part.price_breaks.all %}
<tr>
<td>{% decimal pb.quantity %}</td>
<td>
{% if pb.currency %}{{ pb.currency.symbol }}{% endif %}
{% decimal pb.cost %}
{% if pb.currency %}{{ pb.currency.suffix }}{% endif %}
<div class='btn-group' style='float: right;'>
<button title='Edit Price Break' class='btn btn-default btn-sm pb-edit-button' type='button' url="{% url 'price-break-edit' pb.id %}"><span class='glyphicon glyphicon-edit'></span></button>
<button title='Delete Price Break' class='btn btn-default btn-sm pb-delete-button' type='button' url="{% url 'price-break-delete' pb.id %}"><span class='glyphicon glyphicon-trash'></span></button>
</div>
</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan='2'>
<span class='warning-msg'><i>{% trans "No price breaks have been added for this part" %}</i></span>
</td>
</tr>
{% endif %}
</table>
{% endblock %}
{% block js_ready %}
{{ block.super }}
$('#new-price-break').click(function() {
launchModalForm("{% url 'price-break-create' %}",
{
reload: true,
data: {
part: {{ part.id }},
}
}
);
});
$('.pb-edit-button').click(function() {
var button = $(this);
launchModalForm(button.attr('url'),
{
reload: true,
}
);
});
$('.pb-delete-button').click(function() {
var button = $(this);
launchModalForm(button.attr('url'),
{
reload: true,
}
);
});
{% endblock %}

View File

@@ -0,0 +1,72 @@
{% extends "company/supplier_part_base.html" %}
{% load static %}
{% load i18n %}
{% block details %}
{% include "company/supplier_part_tabs.html" with tab='stock' %}
<hr>
<h4>{% trans "Supplier Part Stock" %}</h4>
{% include "stock_table.html" %}
{% endblock %}
{% block js_load %}
{{ block.super }}
<script type='text/javascript' src="{% static 'script/inventree/stock.js' %}"></script>
{% endblock %}
{% block js_ready %}
{{ block.super }}
loadStockTable($("#stock-table"), {
params: {
supplier_part: {{ part.id }},
location_detail: true,
part_detail: true,
},
groupByField: 'location',
buttons: ['#stock-options'],
url: "{% url 'api-stock-list' %}",
});
$("#stock-export").click(function() {
launchModalForm("{% url 'stock-export-options' %}", {
submit_text: '{% trans "Export" %}',
success: function(response) {
var url = "{% url 'stock-export' %}";
url += "?format=" + response.format;
url += "&cascade=" + response.cascade;
url += "&supplier_part={{ part.id }}";
location.href = url;
},
});
});
$("#item-create").click(function() {
launchModalForm("{% url 'stock-item-create' %}", {
reload: true,
data: {
part: {{ part.part.id }},
supplier_part: {{ part.id }},
},
secondary: [
{
field: 'location',
label: '{% trans "New Location" %}',
title: '{% trans "Create New Location" %}',
url: "{% url 'stock-location-create' %}",
}
]
});
return false;
});
{% endblock %}

View File

@@ -0,0 +1,13 @@
{% load i18n %}
<ul class='nav nav-tabs'>
<li{% if tab == 'pricing' %} class='active'{% endif %}>
<a href="{% url 'supplier-part-pricing' part.id %}">{% trans "Pricing" %}</a>
</li>
<li{% if tab == 'stock' %} class='active'{% endif %}>
<a href="{% url 'supplier-part-stock' part.id %}">{% trans "Stock" %}</a>
</li>
<li {% if tab == 'orders' %} class='active'{% endif %}>
<a href="{% url 'supplier-part-orders' part.id %}">{% trans "Orders" %}</a>
</li>
</ul>

View File

@@ -1,23 +1,28 @@
{% load i18n %}
<ul class='nav nav-tabs'>
<li{% if tab == 'details' %} class='active'{% endif %}>
<a href="{% url 'company-detail' company.id %}">Details</a>
<a href="{% url 'company-detail' company.id %}">{% trans "Details" %}</a>
</li>
{% if company.is_supplier %}
<li{% if tab == 'parts' %} class='active'{% endif %}>
<a href="{% url 'company-detail-parts' company.id %}">Supplier Parts <span class='badge'>{{ company.part_count }}</span></a>
<a href="{% url 'company-detail-parts' company.id %}">{% trans "Supplier Parts" %} <span class='badge'>{{ company.part_count }}</span></a>
</li>
<li{% if tab == 'stock' %} class='active'{% endif %}>
<a href="{% url 'company-detail-stock' company.id %}">Stock <span class='badge'>{{ company.stock_count }}</a>
<a href="{% url 'company-detail-stock' company.id %}">{% trans "Stock" %} <span class='badge'>{{ company.stock_count }}</a>
</li>
<li{% if tab == 'po' %} class='active'{% endif %}>
<a href="{% url 'company-detail-purchase-orders' company.id %}">Purchase Orders <span class='badge'>{{ company.purchase_orders.count }}</span></a>
<a href="{% url 'company-detail-purchase-orders' company.id %}">{% trans "Purchase Orders" %} <span class='badge'>{{ company.purchase_orders.count }}</span></a>
</li>
{% endif %}
{% if company.is_customer %}
{% if 0 %}
<li{% if tab == 'co' %} class='active'{% endif %}>
<a href="#">Sales Orders</a>
<a href="#">{% trans "Sales Orders" %}</a>
</li>
{% endif %}
{% endif %}
<li{% if tab == 'notes' %} class='active'{% endif %}>
<a href="{% url 'company-notes' company.id %}">{% trans "Notes" %}{% if company.notes %} <span class='glyphicon glyphicon-small glyphicon-info-sign'></span>{% endif %}</a>
</li>
</ul>

View File

@@ -0,0 +1,64 @@
""" Unit tests for Company views (see views.py) """
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.test import TestCase
from django.urls import reverse
from django.contrib.auth import get_user_model
from .models import SupplierPart
class CompanyViewTest(TestCase):
fixtures = [
'category',
'part',
'location',
'company',
'supplier_part',
]
def setUp(self):
super().setUp()
# Create a user
User = get_user_model()
User.objects.create_user('username', 'user@email.com', 'password')
self.client.login(username='username', password='password')
def test_company_index(self):
""" Test the company index """
response = self.client.get(reverse('company-index'))
self.assertEqual(response.status_code, 200)
def test_supplier_part_delete(self):
""" Test the SupplierPartDelete view """
url = reverse('supplier-part-delete')
# Get form using 'part' argument
response = self.client.get(url, {'part': '1'}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(response.status_code, 200)
# Get form using 'parts' argument
response = self.client.get(url + '?parts[]=1&parts[]=2', HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(response.status_code, 200)
# POST to delete two parts
n = SupplierPart.objects.count()
response = self.client.post(
url,
{
'supplier-part-2': 'supplier-part-2',
'supplier-part-3': 'supplier-part-3',
'confirm_delete': True
},
HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(response.status_code, 200)
self.assertEqual(n - 2, SupplierPart.objects.count())

View File

@@ -56,7 +56,7 @@ class CompanySimpleTest(TestCase):
zerg = Company.objects.get(pk=3)
self.assertTrue(acme.has_parts)
self.assertEqual(acme.part_count, 3)
self.assertEqual(acme.part_count, 4)
self.assertTrue(appel.has_parts)
self.assertEqual(appel.part_count, 2)

View File

@@ -18,6 +18,7 @@ company_detail_urls = [
url(r'parts/?', views.CompanyDetail.as_view(template_name='company/detail_part.html'), name='company-detail-parts'),
url(r'stock/?', views.CompanyDetail.as_view(template_name='company/detail_stock.html'), name='company-detail-stock'),
url(r'purchase-orders/?', views.CompanyDetail.as_view(template_name='company/detail_purchase_orders.html'), name='company-detail-purchase-orders'),
url(r'notes/?', views.CompanyNotes.as_view(), name='company-notes'),
url(r'thumbnail/?', views.CompanyImage.as_view(), name='company-image'),
@@ -46,14 +47,19 @@ price_break_urls = [
]
supplier_part_detail_urls = [
url(r'edit/?', views.SupplierPartEdit.as_view(), name='supplier-part-edit'),
url(r'delete/?', views.SupplierPartDelete.as_view(), name='supplier-part-delete'),
url(r'^edit/?', views.SupplierPartEdit.as_view(), name='supplier-part-edit'),
url('^.*$', views.SupplierPartDetail.as_view(), name='supplier-part-detail'),
url(r'^pricing/', views.SupplierPartDetail.as_view(template_name='company/supplier_part_pricing.html'), name='supplier-part-pricing'),
url(r'^orders/', views.SupplierPartDetail.as_view(template_name='company/supplier_part_orders.html'), name='supplier-part-orders'),
url(r'^stock/', views.SupplierPartDetail.as_view(template_name='company/supplier_part_stock.html'), name='supplier-part-stock'),
url('^.*$', views.SupplierPartDetail.as_view(template_name='company/supplier_part_pricing.html'), name='supplier-part-detail'),
]
supplier_part_urls = [
url(r'^new/?', views.SupplierPartCreate.as_view(), name='supplier-part-create'),
url(r'delete/', views.SupplierPartDelete.as_view(), name='supplier-part-delete'),
url(r'^(?P<pk>\d+)/', include(supplier_part_detail_urls)),
]

View File

@@ -6,12 +6,17 @@ Django views for interacting with Company app
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.views.generic import DetailView, ListView
from django.utils.translation import ugettext as _
from django.views.generic import DetailView, ListView, UpdateView
from django.urls import reverse
from django.forms import HiddenInput
from InvenTree.views import AjaxCreateView, AjaxUpdateView, AjaxDeleteView
from InvenTree.status_codes import OrderStatus
from InvenTree.helpers import str2bool
from common.models import Currency
from .models import Company
from .models import SupplierPart
@@ -51,6 +56,28 @@ class CompanyIndex(ListView):
return queryset
class CompanyNotes(UpdateView):
""" View for editing the 'notes' field of a Company object.
"""
context_object_name = 'company'
template_name = 'company/notes.html'
model = Company
fields = ['notes']
def get_success_url(self):
return reverse('company-notes', kwargs={'pk': self.get_object().id})
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['editing'] = str2bool(self.request.GET.get('edit', ''))
return ctx
class CompanyDetail(DetailView):
""" Detail view for Company object """
context_obect_name = 'company'
@@ -69,12 +96,12 @@ class CompanyImage(AjaxUpdateView):
""" View for uploading an image for the Company """
model = Company
ajax_template_name = 'modal_form.html'
ajax_form_title = 'Update Company Image'
ajax_form_title = _('Update Company Image')
form_class = CompanyImageForm
def get_data(self):
return {
'success': 'Updated company image',
'success': _('Updated company image'),
}
@@ -84,11 +111,11 @@ class CompanyEdit(AjaxUpdateView):
form_class = EditCompanyForm
context_object_name = 'company'
ajax_template_name = 'modal_form.html'
ajax_form_title = 'Edit Company'
ajax_form_title = _('Edit Company')
def get_data(self):
return {
'info': 'Edited company information',
'info': _('Edited company information'),
}
@@ -98,11 +125,11 @@ class CompanyCreate(AjaxCreateView):
context_object_name = 'company'
form_class = EditCompanyForm
ajax_template_name = 'modal_form.html'
ajax_form_title = "Create new Company"
ajax_form_title = _("Create new Company")
def get_data(self):
return {
'success': "Created new company",
'success': _("Created new company"),
}
@@ -112,19 +139,19 @@ class CompanyDelete(AjaxDeleteView):
model = Company
success_url = '/company/'
ajax_template_name = 'company/delete.html'
ajax_form_title = 'Delete Company'
ajax_form_title = _('Delete Company')
context_object_name = 'company'
def get_data(self):
return {
'danger': 'Company was deleted',
'danger': _('Company was deleted'),
}
class SupplierPartDetail(DetailView):
""" Detail view for SupplierPart """
model = SupplierPart
template_name = 'company/partdetail.html'
template_name = 'company/supplier_part_detail.html'
context_object_name = 'part'
queryset = SupplierPart.objects.all()
@@ -142,7 +169,7 @@ class SupplierPartEdit(AjaxUpdateView):
context_object_name = 'part'
form_class = EditSupplierPartForm
ajax_template_name = 'modal_form.html'
ajax_form_title = 'Edit Supplier Part'
ajax_form_title = _('Edit Supplier Part')
class SupplierPartCreate(AjaxCreateView):
@@ -151,7 +178,7 @@ class SupplierPartCreate(AjaxCreateView):
model = SupplierPart
form_class = EditSupplierPartForm
ajax_template_name = 'modal_form.html'
ajax_form_title = 'Create new Supplier Part'
ajax_form_title = _('Create new Supplier Part')
context_object_name = 'part'
def get_form(self):
@@ -197,12 +224,80 @@ class SupplierPartCreate(AjaxCreateView):
class SupplierPartDelete(AjaxDeleteView):
""" Delete view for removing a SupplierPart """
model = SupplierPart
""" Delete view for removing a SupplierPart.
SupplierParts can be deleted using a variety of 'selectors'.
- ?part=<pk> -> Delete a single SupplierPart object
- ?parts=[] -> Delete a list of SupplierPart objects
"""
success_url = '/supplier/'
ajax_template_name = 'company/partdelete.html'
ajax_form_title = 'Delete Supplier Part'
context_object_name = 'supplier_part'
ajax_form_title = _('Delete Supplier Part')
parts = []
def get_context_data(self):
ctx = {}
ctx['parts'] = self.parts
return ctx
def get_parts(self):
""" Determine which SupplierPart object(s) the user wishes to delete.
"""
self.parts = []
# User passes a single SupplierPart ID
if 'part' in self.request.GET:
try:
self.parts.append(SupplierPart.objects.get(pk=self.request.GET.get('part')))
except (ValueError, SupplierPart.DoesNotExist):
pass
elif 'parts[]' in self.request.GET:
part_id_list = self.request.GET.getlist('parts[]')
self.parts = SupplierPart.objects.filter(id__in=part_id_list)
def get(self, request, *args, **kwargs):
self.request = request
self.get_parts()
return self.renderJsonResponse(request, form=self.get_form())
def post(self, request, *args, **kwargs):
""" Handle the POST action for deleting supplier parts.
"""
self.request = request
self.parts = []
for item in self.request.POST:
if item.startswith('supplier-part-'):
pk = item.replace('supplier-part-', '')
try:
self.parts.append(SupplierPart.objects.get(pk=pk))
except (ValueError, SupplierPart.DoesNotExist):
pass
confirm = str2bool(self.request.POST.get('confirm_delete', False))
data = {
'form_valid': confirm,
}
if confirm:
for part in self.parts:
part.delete()
return self.renderJsonResponse(self.request, data=data, form=self.get_form())
class PriceBreakCreate(AjaxCreateView):
@@ -210,7 +305,7 @@ class PriceBreakCreate(AjaxCreateView):
model = SupplierPriceBreak
form_class = EditPriceBreakForm
ajax_form_title = 'Add Price Break'
ajax_form_title = _('Add Price Break')
ajax_template_name = 'modal_form.html'
def get_data(self):
@@ -237,6 +332,13 @@ class PriceBreakCreate(AjaxCreateView):
initials['part'] = self.get_part()
# Pre-select the default currency
try:
base = Currency.objects.get(base=True)
initials['currency'] = base
except Currency.DoesNotExist:
pass
return initials
@@ -245,7 +347,7 @@ class PriceBreakEdit(AjaxUpdateView):
model = SupplierPriceBreak
form_class = EditPriceBreakForm
ajax_form_title = 'Edit Price Break'
ajax_form_title = _('Edit Price Break')
ajax_template_name = 'modal_form.html'
def get_form(self):
@@ -260,5 +362,5 @@ class PriceBreakDelete(AjaxDeleteView):
""" View for deleting a supplier price break """
model = SupplierPriceBreak
ajax_form_title = "Delete Price Break"
ajax_form_title = _("Delete Price Break")
ajax_template_name = 'modal_delete_form.html'

View File

@@ -18,6 +18,9 @@ database:
#HOST: ''
#PORT: ''
# Select default system language (default is 'en-us')
language: en-us
# Set debug to False to run in production mode
debug: True
@@ -47,7 +50,16 @@ media_root: '../inventree_media'
# By default it is stored in a directory named 'static' local to the InvenTree directory
static_root: '../inventree_static'
# Optional URL schemes to allow in URL fields
# By default, only the following schemes are allowed: ['http', 'https', 'ftp', 'ftps']
# Uncomment the lines below to allow extra schemes
#extra_url_schemes:
# - mailto
# - git
# - ssh
# Logging options
# If debug mode is enabled, set log_queries to True to show aggregate database queries in the debug console
log_queries: False
# Backup options

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -3,10 +3,15 @@ from __future__ import unicode_literals
from django.contrib import admin
from import_export.admin import ImportExportModelAdmin
from import_export.resources import ModelResource
from import_export.fields import Field
from .models import PurchaseOrder, PurchaseOrderLineItem
class PurchaseOrderAdmin(admin.ModelAdmin):
class PurchaseOrderAdmin(ImportExportModelAdmin):
list_display = (
'reference',
@@ -17,7 +22,27 @@ class PurchaseOrderAdmin(admin.ModelAdmin):
)
class PurchaseOrderLineItemAdmin(admin.ModelAdmin):
class POLineItemResource(ModelResource):
""" Class for managing import / export of POLineItem data """
part_name = Field(attribute='part__part__name', readonly=True)
manufacturer = Field(attribute='part__manufacturer', readonly=True)
MPN = Field(attribute='part__MPN', readonly=True)
SKU = Field(attribute='part__SKU', readonly=True)
class Meta:
model = PurchaseOrderLineItem
skip_unchanged = True
report_skipped = False
clean_model_instances = True
class PurchaseOrderLineItemAdmin(ImportExportModelAdmin):
resource_class = POLineItemResource
list_display = (
'order',

View File

@@ -7,9 +7,19 @@ from __future__ import unicode_literals
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import generics, permissions
from rest_framework import filters
from rest_framework.response import Response
from django.conf import settings
from django.conf.urls import url
from InvenTree.status_codes import OrderStatus
import os
from part.models import Part
from company.models import SupplierPart
from .models import PurchaseOrder, PurchaseOrderLineItem
from .serializers import POSerializer, POLineItemSerializer
@@ -24,18 +34,87 @@ class POList(generics.ListCreateAPIView):
queryset = PurchaseOrder.objects.all()
serializer_class = POSerializer
def list(self, request, *args, **kwargs):
queryset = self.get_queryset().prefetch_related('supplier', 'lines')
queryset = self.filter_queryset(queryset)
# Special filtering for 'status' field
if 'status' in request.GET:
status = request.GET['status']
# First attempt to filter by integer value
try:
status = int(status)
queryset = queryset.filter(status=status)
except ValueError:
try:
value = OrderStatus.value(status)
queryset = queryset.filter(status=value)
except ValueError:
pass
# Attempt to filter by part
if 'part' in request.GET:
try:
part = Part.objects.get(pk=request.GET['part'])
queryset = queryset.filter(id__in=[p.id for p in part.purchase_orders()])
except (Part.DoesNotExist, ValueError):
pass
# Attempt to filter by supplier part
if 'supplier_part' in request.GET:
try:
supplier_part = SupplierPart.objects.get(pk=request.GET['supplier_part'])
queryset = queryset.filter(id__in=[p.id for p in supplier_part.purchase_orders()])
except (ValueError, SupplierPart.DoesNotExist):
pass
data = queryset.values(
'pk',
'supplier',
'supplier__name',
'supplier__image',
'reference',
'description',
'URL',
'status',
'notes',
'creation_date',
)
for item in data:
order = queryset.get(pk=item['pk'])
item['supplier__image'] = os.path.join(settings.MEDIA_URL, item['supplier__image'])
item['status_text'] = OrderStatus.label(item['status'])
item['lines'] = order.lines.count()
return Response(data)
permission_classes = [
permissions.IsAuthenticated,
]
filter_backends = [
DjangoFilterBackend,
filters.SearchFilter,
filters.OrderingFilter,
]
filter_fields = [
'supplier',
]
ordering_fields = [
'creation_date',
'reference',
]
ordering = '-creation_date'
class PODetail(generics.RetrieveUpdateAPIView):
""" API endpoint for detail view of a PurchaseOrder object """

View File

@@ -35,8 +35,16 @@
quantity: 250
received: 50
# 1000 x ACME0003
- model: order.purchaseorderlineitem
fields:
order: 1
part: 3
quantity: 1000
# 100 x ZERGLPHS (M2x4 LPHS)
- model: order.purchaseorderlineitem
pk: 22
fields:
order: 2
part: 3

View File

@@ -6,15 +6,20 @@ Django Forms for interacting with Order objects
from __future__ import unicode_literals
from django import forms
from django.utils.translation import ugettext as _
from mptt.fields import TreeNodeChoiceField
from InvenTree.forms import HelperForm
from InvenTree.fields import RoundingDecimalFormField
from .models import PurchaseOrder, PurchaseOrderLineItem
from stock.models import StockLocation
from .models import PurchaseOrder, PurchaseOrderLineItem, PurchaseOrderAttachment
class IssuePurchaseOrderForm(HelperForm):
confirm = forms.BooleanField(required=False, help_text='Place order')
confirm = forms.BooleanField(required=False, help_text=_('Place order'))
class Meta:
model = PurchaseOrder
@@ -23,6 +28,39 @@ class IssuePurchaseOrderForm(HelperForm):
]
class CompletePurchaseOrderForm(HelperForm):
confirm = forms.BooleanField(required=False, help_text=_("Mark order as complete"))
class Meta:
model = PurchaseOrder
fields = [
'confirm',
]
class CancelPurchaseOrderForm(HelperForm):
confirm = forms.BooleanField(required=False, help_text=_('Cancel order'))
class Meta:
model = PurchaseOrder
fields = [
'confirm',
]
class ReceivePurchaseOrderForm(HelperForm):
location = TreeNodeChoiceField(queryset=StockLocation.objects.all(), required=True, help_text=_('Receive parts to this location'))
class Meta:
model = PurchaseOrder
fields = [
'location',
]
class EditPurchaseOrderForm(HelperForm):
""" Form for editing a PurchaseOrder object """
@@ -33,13 +71,26 @@ class EditPurchaseOrderForm(HelperForm):
'supplier',
'description',
'URL',
'notes'
]
class EditPurchaseOrderAttachmentForm(HelperForm):
""" Form for editing a PurchaseOrderAttachment object """
class Meta:
model = PurchaseOrderAttachment
fields = [
'order',
'attachment',
'comment'
]
class EditPurchaseOrderLineItemForm(HelperForm):
""" Form for editing a PurchaseOrderLineItem object """
quantity = RoundingDecimalFormField(max_digits=10, decimal_places=5)
class Meta:
model = PurchaseOrderLineItem
fields = [

View File

@@ -0,0 +1,19 @@
# Generated by Django 2.2.5 on 2019-11-18 23:23
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('order', '0012_auto_20190617_1943'),
]
operations = [
migrations.AlterField(
model_name='purchaseorderlineitem',
name='quantity',
field=models.DecimalField(decimal_places=5, default=1, help_text='Item quantity', max_digits=15, validators=[django.core.validators.MinValueValidator(0)]),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 2.2.5 on 2019-11-18 23:28
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('order', '0013_auto_20191118_2323'),
]
operations = [
migrations.AlterField(
model_name='purchaseorderlineitem',
name='received',
field=models.DecimalField(decimal_places=5, default=0, help_text='Number of items received', max_digits=15),
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 2.2.9 on 2020-02-01 23:46
from django.db import migrations
import markdownx.models
class Migration(migrations.Migration):
dependencies = [
('order', '0014_auto_20191118_2328'),
]
operations = [
migrations.AlterField(
model_name='purchaseorder',
name='notes',
field=markdownx.models.MarkdownxField(blank=True, help_text='Order notes'),
),
]

View File

@@ -0,0 +1,27 @@
# Generated by Django 2.2.9 on 2020-03-22 07:01
import InvenTree.models
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('order', '0015_auto_20200201_2346'),
]
operations = [
migrations.CreateModel(
name='PurchaseOrderAttachment',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('attachment', models.FileField(help_text='Select file to attach', upload_to=InvenTree.models.rename_attachment)),
('comment', models.CharField(help_text='File comment', max_length=100)),
('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='attachments', to='order.PurchaseOrder')),
],
options={
'abstract': False,
},
),
]

View File

@@ -0,0 +1,20 @@
# Generated by Django 2.2.10 on 2020-03-31 10:00
import InvenTree.fields
import django.core.validators
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('order', '0016_purchaseorderattachment'),
]
operations = [
migrations.AlterField(
model_name='purchaseorderlineitem',
name='quantity',
field=InvenTree.fields.RoundingDecimalField(decimal_places=5, default=1, help_text='Item quantity', max_digits=15, validators=[django.core.validators.MinValueValidator(0)]),
),
]

View File

@@ -5,19 +5,25 @@ Order model definitions
# -*- coding: utf-8 -*-
from django.db import models, transaction
from django.db.models import F
from django.core.validators import MinValueValidator
from django.core.exceptions import ValidationError
from django.contrib.auth.models import User
from django.urls import reverse
from django.utils.translation import ugettext as _
import tablib
from markdownx.models import MarkdownxField
import os
from datetime import datetime
from stock.models import StockItem
from company.models import Company, SupplierPart
from InvenTree.fields import RoundingDecimalField
from InvenTree.helpers import decimal2string
from InvenTree.status_codes import OrderStatus
from InvenTree.models import InvenTreeAttachment
class Order(models.Model):
@@ -80,7 +86,7 @@ class Order(models.Model):
complete_date = models.DateField(blank=True, null=True)
notes = models.TextField(blank=True, help_text=_('Order notes'))
notes = MarkdownxField(blank=True, help_text=_('Order notes'))
def place_order(self):
""" Marks the order as PLACED. Order must be currently PENDING. """
@@ -98,6 +104,13 @@ class Order(models.Model):
self.complete_date = datetime.now().date()
self.save()
def cancel_order(self):
""" Marks the order as CANCELLED. """
if self.status in [OrderStatus.PLACED, OrderStatus.PENDING]:
self.status = OrderStatus.CANCELLED
self.save()
class PurchaseOrder(Order):
""" A PurchaseOrder represents goods shipped inwards from an external supplier.
@@ -125,56 +138,8 @@ class PurchaseOrder(Order):
related_name='+'
)
def export_to_file(self, **kwargs):
""" Export order information to external file """
file_format = kwargs.get('format', 'csv').lower()
data = tablib.Dataset(headers=[
'Line',
'Part',
'Description',
'Manufacturer',
'MPN',
'Order Code',
'Quantity',
'Received',
'Reference',
'Notes',
])
idx = 0
for item in self.lines.all():
line = []
line.append(idx)
if item.part:
line.append(item.part.part.name)
line.append(item.part.part.description)
line.append(item.part.manufacturer)
line.append(item.part.MPN)
line.append(item.part.SKU)
else:
line += [[] * 5]
line.append(item.quantity)
line.append(item.received)
line.append(item.reference)
line.append(item.notes)
idx += 1
data.append(line)
return data.export(file_format)
def get_absolute_url(self):
return reverse('purchase-order-detail', kwargs={'pk': self.id})
return reverse('po-detail', kwargs={'pk': self.id})
@transaction.atomic
def add_line_item(self, supplier_part, quantity, group=True, reference=''):
@@ -226,7 +191,13 @@ class PurchaseOrder(Order):
Any line item where 'received' < 'quantity' will be returned.
"""
return [line for line in self.lines.all() if line.quantity > line.received]
return self.lines.filter(quantity__gt=F('received'))
@property
def is_complete(self):
""" Return True if all line items have been received """
return self.pending_line_items().count() == 0
@transaction.atomic
def receive_line_item(self, line, location, quantity, user):
@@ -271,6 +242,17 @@ class PurchaseOrder(Order):
self.complete_order() # This will save the model
class PurchaseOrderAttachment(InvenTreeAttachment):
"""
Model for storing file attachments against a PurchaseOrder object
"""
def getSubdir(self):
return os.path.join("po_files", str(self.order.id))
order = models.ForeignKey(PurchaseOrder, on_delete=models.CASCADE, related_name="attachments")
class OrderLineItem(models.Model):
""" Abstract model for an order line item
@@ -283,7 +265,7 @@ class OrderLineItem(models.Model):
class Meta:
abstract = True
quantity = models.PositiveIntegerField(validators=[MinValueValidator(0)], default=1, help_text=_('Item quantity'))
quantity = RoundingDecimalField(max_digits=15, decimal_places=5, validators=[MinValueValidator(0)], default=1, help_text=_('Item quantity'))
reference = models.CharField(max_length=100, blank=True, help_text=_('Line item reference'))
@@ -305,7 +287,7 @@ class PurchaseOrderLineItem(OrderLineItem):
def __str__(self):
return "{n} x {part} from {supplier} (for {po})".format(
n=self.quantity,
n=decimal2string(self.quantity),
part=self.part.SKU if self.part else 'unknown part',
supplier=self.order.supplier.name,
po=self.order)
@@ -325,7 +307,7 @@ class PurchaseOrderLineItem(OrderLineItem):
help_text=_("Supplier part"),
)
received = models.PositiveIntegerField(default=0, help_text=_('Number of items received'))
received = models.DecimalField(decimal_places=5, max_digits=15, default=0, help_text=_('Number of items received'))
def remaining(self):
""" Calculate the number of items remaining to be received """

View File

@@ -0,0 +1,132 @@
{% extends "base.html" %}
{% load i18n %}
{% load static %}
{% load inventree_extras %}
{% block page_title %}
InvenTree | {{ order }}
{% endblock %}
{% block content %}
<div class='row'>
<div class='col-sm-6'>
<div class='media'>
<div class='media-left'>
<img class='part-thumb'
{% if order.supplier.image %}
src="{{ order.supplier.image.url }}"
{% else %}
src="{% static 'img/blank_image.png' %}"
{% endif %}
/>
</div>
<div class='media-body'>
<h4>{{ order }}</h4>
<p>{{ order.description }}</p>
{% if order.URL %}
<a href="{{ order.URL }}">{{ order.URL }}</a>
{% endif %}
<p>
<div class='btn-row'>
<div class='btn-group'>
<button type='button' class='btn btn-default btn-glyph' id='edit-order' title='Edit order information'>
<span class='glyphicon glyphicon-edit'></span>
</button>
<button type='button' class='btn btn-default btn-glyph' id='export-order' title='Export order to file'>
<span class='glyphicon glyphicon-download-alt'></span>
</button>
{% if order.status == OrderStatus.PENDING and order.lines.count > 0 %}
<button type='button' class='btn btn-default btn-glyph' id='place-order' title='Place order'>
<span class='glyphicon glyphicon-send'></span>
</button>
{% elif order.status == OrderStatus.PLACED %}
<button type='button' class='btn btn-default btn-glyph' id='receive-order' title='Receive items'>
<span class='glyphicon glyphicon-check'></span>
</button>
<button type='button' class='btn btn-default btn-glyph' id='complete-order' title='Mark order as complete'>
<span class='glyphicon glyphicon-ok'></span>
</button>
{% endif %}
{% if order.status == OrderStatus.PENDING or order.status == OrderStatus.PLACED %}
<button type='button' class='btn btn-default btn-glyph' id='cancel-order' title='Cancel order'>
<span class='glyphicon glyphicon-remove'></span>
</button>
{% endif %}
</div>
</div>
</p>
</div>
</div>
</div>
<div class='col-sm-6'>
<h4>{% trans "Purchase Order Details" %}</h4>
<table class='table'>
<tr>
<td>{% trans "Supplier" %}</td>
<td><a href="{% url 'company-detail' order.supplier.id %}">{{ order.supplier }}</a></td>
</tr>
<tr>
<td>{% trans "Status" %}</td>
<td>{% include "order/order_status.html" %}</td>
</tr>
<tr>
<td>{% trans "Created" %}</td>
<td>{{ order.creation_date }}<span class='badge'>{{ order.created_by }}</span></td>
</tr>
{% if order.issue_date %}
<tr>
<td>{% trans "Issued" %}</td>
<td>{{ order.issue_date }}</td>
</tr>
{% endif %}
{% if order.status == OrderStatus.COMPLETE %}
<tr>
<td>{% trans "Received" %}</td>
<td>{{ order.complete_date }}<span class='badge'>{{ order.received_by }}</span></td>
</tr>
{% endif %}
</table>
</div>
</div>
<hr>
<div class='container-fluid'>
{% block details %}
<!-- Specific order details to go here -->
{% endblock %}
</div>
{% endblock %}
{% block js_ready %}
{{ block.super }}
{% if order.status == OrderStatus.PENDING and order.lines.count > 0 %}
$("#place-order").click(function() {
launchModalForm("{% url 'po-issue' order.id %}",
{
reload: true,
});
});
{% endif %}
$("#edit-order").click(function() {
launchModalForm("{% url 'po-edit' order.id %}",
{
reload: true,
}
);
});
$("#cancel-order").click(function() {
launchModalForm("{% url 'po-cancel' order.id %}", {
reload: true,
});
});
{% endblock %}

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