django-bom 1.262__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of django-bom might be problematic. Click here for more details.
- bom/__init__.py +1 -0
- bom/admin.py +207 -0
- bom/apps.py +8 -0
- bom/auth_backends.py +47 -0
- bom/base_classes.py +31 -0
- bom/constants.py +217 -0
- bom/context_processors.py +9 -0
- bom/csv_headers.py +252 -0
- bom/decorators.py +32 -0
- bom/form_fields.py +59 -0
- bom/forms.py +1328 -0
- bom/helpers.py +367 -0
- bom/local_settings.py +35 -0
- bom/migrations/0001_initial.py +135 -0
- bom/migrations/0002_auto_20180908_2151.py +24 -0
- bom/migrations/0003_sellerpart_data_source.py +18 -0
- bom/migrations/0004_auto_20180911_0011.py +18 -0
- bom/migrations/0005_auto_20181007_1934.py +56 -0
- bom/migrations/0006_auto_20181007_1949.py +41 -0
- bom/migrations/0007_auto_20181009_0256.py +19 -0
- bom/migrations/0008_auto_20181030_0427.py +19 -0
- bom/migrations/0009_subpart_reference.py +18 -0
- bom/migrations/0010_auto_20181202_0733.py +23 -0
- bom/migrations/0011_auto_20181202_2113.py +22 -0
- bom/migrations/0012_partchangehistory.py +30 -0
- bom/migrations/0013_auto_20190222_1631.py +19 -0
- bom/migrations/0014_auto_20190223_2353.py +18 -0
- bom/migrations/0015_auto_20190303_1915.py +136 -0
- bom/migrations/0016_auto_20190405_2308.py +58 -0
- bom/migrations/0017_auto_20190616_1912.py +19 -0
- bom/migrations/0018_auto_20190616_2143.py +24 -0
- bom/migrations/0019_auto_20190624_1246.py +45 -0
- bom/migrations/0020_auto_20190627_0207.py +38 -0
- bom/migrations/0021_auto_20190627_0428.py +23 -0
- bom/migrations/0022_auto_20190811_2140.py +35 -0
- bom/migrations/0023_auto_20191205_2351.py +255 -0
- bom/migrations/0024_auto_20191214_1342.py +89 -0
- bom/migrations/0025_auto_20191221_1907.py +38 -0
- bom/migrations/0026_auto_20191222_2258.py +22 -0
- bom/migrations/0027_auto_20191222_2347.py +17 -0
- bom/migrations/0028_partrevision_displayable_synopsis.py +74 -0
- bom/migrations/0029_auto_20191231_1630.py +23 -0
- bom/migrations/0030_auto_20200101_2253.py +22 -0
- bom/migrations/0031_auto_20200104_1352.py +38 -0
- bom/migrations/0032_auto_20200126_1806.py +27 -0
- bom/migrations/0033_auto_20200203_0618.py +29 -0
- bom/migrations/0034_auto_20200222_0359.py +30 -0
- bom/migrations/0035_auto_20200303_0111.py +34 -0
- bom/migrations/0036_auto_20200303_0538.py +17 -0
- bom/migrations/0037_auto_20200405_1642.py +44 -0
- bom/migrations/0038_auto_20200422_0504.py +19 -0
- bom/migrations/0039_auto_20200929_2315.py +41 -0
- bom/migrations/0040_alter_organization_currency.py +19 -0
- bom/migrations/0041_organization_subscription_quantity.py +18 -0
- bom/migrations/0042_auto_20210720_2137.py +23 -0
- bom/migrations/0043_auto_20211123_0157.py +24 -0
- bom/migrations/0044_auto_20220831_1241.py +23 -0
- bom/migrations/0045_sellerpart_link.py +18 -0
- bom/migrations/0046_alter_sellerpart_unique_together.py +17 -0
- bom/migrations/0047_sellerpart_seller_part_number.py +18 -0
- bom/migrations/0048_rename_part_organization_number_class_bom_part_organiz_b333d6_idx_and_more.py +1017 -0
- bom/migrations/0049_alter_assembly_id_alter_assemblysubparts_id_and_more.py +99 -0
- bom/migrations/0050_alter_organization_options.py +17 -0
- bom/migrations/0051_alter_manufacturer_organization_and_more.py +41 -0
- bom/migrations/0052_remove_partrevision_attribute_and_more.py +584 -0
- bom/migrations/__init__.py +0 -0
- bom/models.py +886 -0
- bom/part_bom.py +192 -0
- bom/settings.py +262 -0
- bom/static/bom/css/dashboard.css +17 -0
- bom/static/bom/css/jquery.treetable.css +28 -0
- bom/static/bom/css/materialize.min.css +13 -0
- bom/static/bom/css/part-info.css +15 -0
- bom/static/bom/css/style.css +482 -0
- bom/static/bom/css/tablesorter-theme.materialize.css +176 -0
- bom/static/bom/css/treetable-theme.css +42 -0
- bom/static/bom/doc/sample_part_classes.csv +38 -0
- bom/static/bom/doc/test_bom.csv +6 -0
- bom/static/bom/doc/test_bom_5_intelligent.csv +4 -0
- bom/static/bom/doc/test_full_bom.csv +37 -0
- bom/static/bom/doc/test_new_parts.csv +5 -0
- bom/static/bom/doc/test_new_parts_5_intelligent.csv +5 -0
- bom/static/bom/img/_ionicons_svg_md-arrow-dropdown.svg +1 -0
- bom/static/bom/img/_ionicons_svg_md-arrow-dropright.svg +1 -0
- bom/static/bom/img/favicon.ico +0 -0
- bom/static/bom/img/google/web/1x/btn_google_signin_dark_disabled_web.png +0 -0
- bom/static/bom/img/google/web/1x/btn_google_signin_dark_focus_web.png +0 -0
- bom/static/bom/img/google/web/1x/btn_google_signin_dark_normal_web.png +0 -0
- bom/static/bom/img/google/web/1x/btn_google_signin_dark_pressed_web.png +0 -0
- bom/static/bom/img/google/web/1x/btn_google_signin_light_disabled_web.png +0 -0
- bom/static/bom/img/google/web/1x/btn_google_signin_light_focus_web.png +0 -0
- bom/static/bom/img/google/web/1x/btn_google_signin_light_normal_web.png +0 -0
- bom/static/bom/img/google/web/1x/btn_google_signin_light_pressed_web.png +0 -0
- bom/static/bom/img/google/web/2x/btn_google_signin_dark_disabled_web@2x.png +0 -0
- bom/static/bom/img/google/web/2x/btn_google_signin_dark_focus_web@2x.png +0 -0
- bom/static/bom/img/google/web/2x/btn_google_signin_dark_normal_web@2x.png +0 -0
- bom/static/bom/img/google/web/2x/btn_google_signin_dark_pressed_web@2x.png +0 -0
- bom/static/bom/img/google/web/2x/btn_google_signin_light_disabled_web@2x.png +0 -0
- bom/static/bom/img/google/web/2x/btn_google_signin_light_focus_web@2x.png +0 -0
- bom/static/bom/img/google/web/2x/btn_google_signin_light_normal_web@2x.png +0 -0
- bom/static/bom/img/google/web/2x/btn_google_signin_light_pressed_web@2x.png +0 -0
- bom/static/bom/img/google/web/vector/btn_google_dark_disabled_ios.eps +814 -0
- bom/static/bom/img/google/web/vector/btn_google_dark_disabled_ios.svg +24 -0
- bom/static/bom/img/google/web/vector/btn_google_dark_focus_ios.eps +1866 -0
- bom/static/bom/img/google/web/vector/btn_google_dark_focus_ios.svg +51 -0
- bom/static/bom/img/google/web/vector/btn_google_dark_normal_ios.eps +1031 -0
- bom/static/bom/img/google/web/vector/btn_google_dark_normal_ios.svg +50 -0
- bom/static/bom/img/google/web/vector/btn_google_dark_pressed_ios.eps +1031 -0
- bom/static/bom/img/google/web/vector/btn_google_dark_pressed_ios.svg +50 -0
- bom/static/bom/img/google/web/vector/btn_google_light_disabled_ios.eps +814 -0
- bom/static/bom/img/google/web/vector/btn_google_light_disabled_ios.svg +24 -0
- bom/static/bom/img/google/web/vector/btn_google_light_focus_ios.eps +1837 -0
- bom/static/bom/img/google/web/vector/btn_google_light_focus_ios.svg +44 -0
- bom/static/bom/img/google/web/vector/btn_google_light_normal_ios.eps +1002 -0
- bom/static/bom/img/google/web/vector/btn_google_light_normal_ios.svg +43 -0
- bom/static/bom/img/google/web/vector/btn_google_light_pressed_ios.eps +1002 -0
- bom/static/bom/img/google/web/vector/btn_google_light_pressed_ios.svg +43 -0
- bom/static/bom/img/google_drive_logo.svg +1 -0
- bom/static/bom/img/indabom.png +0 -0
- bom/static/bom/img/mouser.png +0 -0
- bom/static/bom/img/octopart_blue.svg +19 -0
- bom/static/bom/js/formset-handler.js +65 -0
- bom/static/bom/js/jquery-3.4.1.min.js +2 -0
- bom/static/bom/js/jquery.ba-floatingscrollbar.min.js +10 -0
- bom/static/bom/js/jquery.treetable.js +629 -0
- bom/static/bom/js/materialize.min.js +6 -0
- bom/templates/bom/account-delete.html +23 -0
- bom/templates/bom/add-manufacturer-part.html +66 -0
- bom/templates/bom/add-sellerpart.html +93 -0
- bom/templates/bom/base-menu.html +16 -0
- bom/templates/bom/base.html +129 -0
- bom/templates/bom/bom-action-btn.html +23 -0
- bom/templates/bom/bom-action-table.html +57 -0
- bom/templates/bom/bom-base-menu.html +6 -0
- bom/templates/bom/bom-base.html +24 -0
- bom/templates/bom/bom-form-modal.html +36 -0
- bom/templates/bom/bom-form.html +30 -0
- bom/templates/bom/bom-modal-add-users.html +49 -0
- bom/templates/bom/bom-signup.html +12 -0
- bom/templates/bom/components/bom-flat.html +131 -0
- bom/templates/bom/components/bom-indented.html +237 -0
- bom/templates/bom/components/manufacturer-part-list.html +270 -0
- bom/templates/bom/components/seller-part-list.html +62 -0
- bom/templates/bom/create-part.html +65 -0
- bom/templates/bom/dashboard-menu.html +15 -0
- bom/templates/bom/dashboard.html +303 -0
- bom/templates/bom/edit-manufacturer-part.html +72 -0
- bom/templates/bom/edit-part-class.html +120 -0
- bom/templates/bom/edit-part.html +67 -0
- bom/templates/bom/edit-quantity-of-measure.html +119 -0
- bom/templates/bom/edit-user-meta.html +70 -0
- bom/templates/bom/help.html +1356 -0
- bom/templates/bom/manufacturer-info.html +82 -0
- bom/templates/bom/manufacturers.html +97 -0
- bom/templates/bom/nothing-to-see.html +15 -0
- bom/templates/bom/organization-create.html +135 -0
- bom/templates/bom/part-info.html +448 -0
- bom/templates/bom/part-revision-display.html +50 -0
- bom/templates/bom/part-revision-edit.html +39 -0
- bom/templates/bom/part-revision-manage-bom.html +115 -0
- bom/templates/bom/part-revision-new.html +57 -0
- bom/templates/bom/part-revision-release.html +41 -0
- bom/templates/bom/search-help.html +101 -0
- bom/templates/bom/seller-info.html +82 -0
- bom/templates/bom/sellers.html +97 -0
- bom/templates/bom/settings.html +734 -0
- bom/templates/bom/signup.html +28 -0
- bom/templates/bom/subscription_panel.html +16 -0
- bom/templates/bom/table_of_contents.html +47 -0
- bom/templates/bom/upload-bom.html +111 -0
- bom/templates/bom/upload-parts-help.html +103 -0
- bom/templates/bom/upload-parts.html +50 -0
- bom/templates/registration/login.html +39 -0
- bom/tests.py +1592 -0
- bom/third_party_apis/__init__.py +0 -0
- bom/third_party_apis/base_api.py +51 -0
- bom/third_party_apis/google_drive.py +166 -0
- bom/third_party_apis/mouser.py +132 -0
- bom/third_party_apis/test_apis.py +24 -0
- bom/urls.py +100 -0
- bom/utils.py +228 -0
- bom/validators.py +23 -0
- bom/views/__init__.py +0 -0
- bom/views/json_views.py +55 -0
- bom/views/views.py +1773 -0
- bom/wsgi.py +16 -0
- django_bom-1.262.dist-info/METADATA +206 -0
- django_bom-1.262.dist-info/RECORD +191 -0
- django_bom-1.262.dist-info/WHEEL +5 -0
- django_bom-1.262.dist-info/licenses/LICENSE +674 -0
- django_bom-1.262.dist-info/top_level.txt +1 -0
bom/part_bom.py
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from collections import OrderedDict
|
|
3
|
+
|
|
4
|
+
from djmoney.money import Money
|
|
5
|
+
|
|
6
|
+
from .base_classes import AsDictModel
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class PartBom(AsDictModel):
|
|
13
|
+
def __init__(self, part_revision, quantity, unit_cost=None, missing_item_costs=0, nre_cost=None, out_of_pocket_cost=None):
|
|
14
|
+
self.part_revision = part_revision
|
|
15
|
+
self.parts = OrderedDict()
|
|
16
|
+
self.quantity = quantity
|
|
17
|
+
self._currency = self.part_revision.part.organization.currency
|
|
18
|
+
if unit_cost is None:
|
|
19
|
+
unit_cost = Money(0, self._currency)
|
|
20
|
+
if nre_cost is None:
|
|
21
|
+
nre_cost = Money(0, self._currency)
|
|
22
|
+
if out_of_pocket_cost is None:
|
|
23
|
+
out_of_pocket_cost = Money(0, self._currency)
|
|
24
|
+
|
|
25
|
+
self.unit_cost = unit_cost
|
|
26
|
+
self.missing_item_costs = missing_item_costs # count of items that have no cost
|
|
27
|
+
self.nre_cost = nre_cost
|
|
28
|
+
self.out_of_pocket_cost = out_of_pocket_cost # cost of buying self.quantity with MOQs
|
|
29
|
+
|
|
30
|
+
def cost(self):
|
|
31
|
+
return self.unit_cost * self.quantity
|
|
32
|
+
|
|
33
|
+
def total_out_of_pocket_cost(self):
|
|
34
|
+
return self.out_of_pocket_cost + self.nre_cost
|
|
35
|
+
|
|
36
|
+
def append_item_and_update(self, item):
|
|
37
|
+
if item.bom_id in self.parts:
|
|
38
|
+
self.parts[item.bom_id].extended_quantity += item.extended_quantity
|
|
39
|
+
ref = ', ' + item.references
|
|
40
|
+
self.parts[item.bom_id].references += ref
|
|
41
|
+
else:
|
|
42
|
+
self.parts[item.bom_id] = item
|
|
43
|
+
|
|
44
|
+
item.total_extended_quantity = int(self.quantity) * item.extended_quantity
|
|
45
|
+
self.update_bom_for_part(item)
|
|
46
|
+
|
|
47
|
+
def update_bom_for_part(self, bom_part):
|
|
48
|
+
if bom_part.do_not_load:
|
|
49
|
+
bom_part.order_quantity = 0
|
|
50
|
+
bom_part.order_cost = 0
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
if bom_part.seller_part:
|
|
54
|
+
try:
|
|
55
|
+
bom_part.order_quantity = bom_part.seller_part.order_quantity(bom_part.total_extended_quantity)
|
|
56
|
+
bom_part.order_cost = bom_part.total_extended_quantity * bom_part.seller_part.unit_cost
|
|
57
|
+
except AttributeError:
|
|
58
|
+
pass
|
|
59
|
+
self.unit_cost = (self.unit_cost + bom_part.seller_part.unit_cost * bom_part.extended_quantity) if bom_part.seller_part.unit_cost is not None else self.unit_cost
|
|
60
|
+
self.out_of_pocket_cost = self.out_of_pocket_cost + bom_part.out_of_pocket_cost()
|
|
61
|
+
self.nre_cost = (self.nre_cost + bom_part.seller_part.nre_cost) if bom_part.seller_part.nre_cost is not None else self.nre_cost
|
|
62
|
+
else:
|
|
63
|
+
self.missing_item_costs += 1
|
|
64
|
+
|
|
65
|
+
def update(self):
|
|
66
|
+
self.missing_item_costs = 0
|
|
67
|
+
self.unit_cost = Money(0, self._currency)
|
|
68
|
+
self.out_of_pocket_cost = Money(0, self._currency)
|
|
69
|
+
self.nre_cost = Money(0, self._currency)
|
|
70
|
+
for _, bom_part in self.parts.items():
|
|
71
|
+
self.update_bom_for_part(bom_part)
|
|
72
|
+
|
|
73
|
+
def mouser_parts(self):
|
|
74
|
+
mouser_items = {}
|
|
75
|
+
for bom_id, item in self.parts.items():
|
|
76
|
+
if item.part.id not in mouser_items and item.part.number_class.mouser_enabled:
|
|
77
|
+
for manufacturer_part in item.part.manufacturer_parts():
|
|
78
|
+
mouser_items.update({bom_id: manufacturer_part})
|
|
79
|
+
if not manufacturer_part.mouser_disable:
|
|
80
|
+
mouser_items.update({bom_id: manufacturer_part})
|
|
81
|
+
return mouser_items
|
|
82
|
+
|
|
83
|
+
def manufacturer_parts(self, source_mouser=False):
|
|
84
|
+
# TODO: optimize this query to not hit the DB in a for loop
|
|
85
|
+
if source_mouser:
|
|
86
|
+
mps = []
|
|
87
|
+
for item in self.parts:
|
|
88
|
+
if item.part.manufacturer_part.source_mouser:
|
|
89
|
+
mps.append(item.part.manufacturer_part)
|
|
90
|
+
return mps
|
|
91
|
+
return [item.part.manufacturer_part for item in self.parts]
|
|
92
|
+
|
|
93
|
+
def as_dict(self, include_id=False):
|
|
94
|
+
d = super().as_dict()
|
|
95
|
+
d['unit_cost'] = self.unit_cost.amount
|
|
96
|
+
d['nre'] = self.nre_cost.amount
|
|
97
|
+
d['out_of_pocket_cost'] = self.out_of_pocket_cost.amount
|
|
98
|
+
return d
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class PartBomItem(AsDictModel):
|
|
102
|
+
def __init__(self, bom_id, part, part_revision, do_not_load, references, quantity, extended_quantity, seller_part=None):
|
|
103
|
+
# top_level_quantity is the highest quantity, typically a order quantity for the highest assembly level in a BOM
|
|
104
|
+
# A bom item should not care about its parent quantity
|
|
105
|
+
self.bom_id = bom_id
|
|
106
|
+
self.part = part
|
|
107
|
+
self.part_revision = part_revision
|
|
108
|
+
self.do_not_load = do_not_load
|
|
109
|
+
self.references = references
|
|
110
|
+
|
|
111
|
+
self.quantity = quantity # quantity is the quantity per each direct parent assembly
|
|
112
|
+
self.extended_quantity = extended_quantity # extended_quantity, is the item quantity used in the top level assembly (e.g. assuming PartBom.quantity = 1)
|
|
113
|
+
self.total_extended_quantity = None # extended_quantity * top_level_quantity (PartBom.quantity) - Set when appending to PartBom
|
|
114
|
+
self.order_quantity = None # order quantity taking into MOQ/MPQ constraints - Set when appending to PartBom
|
|
115
|
+
|
|
116
|
+
self._currency = self.part.organization.currency
|
|
117
|
+
|
|
118
|
+
self.order_cost = Money(0, self._currency) # order_cost is updated similar to above order_quantity - Set when appending to PartBom
|
|
119
|
+
self.seller_part = seller_part
|
|
120
|
+
|
|
121
|
+
self.api_info = None
|
|
122
|
+
|
|
123
|
+
def extended_cost(self):
|
|
124
|
+
try:
|
|
125
|
+
return self.extended_quantity * self.seller_part.unit_cost
|
|
126
|
+
except (AttributeError, TypeError) as err:
|
|
127
|
+
logger.log(logging.INFO, '[part_bom.py] ' + str(err))
|
|
128
|
+
return Money(0, self._currency)
|
|
129
|
+
|
|
130
|
+
def out_of_pocket_cost(self):
|
|
131
|
+
try:
|
|
132
|
+
return self.order_quantity * self.seller_part.unit_cost
|
|
133
|
+
except (AttributeError, TypeError) as err:
|
|
134
|
+
logger.log(logging.INFO, '[part_bom.py] ' + str(err))
|
|
135
|
+
return Money(0, self._currency)
|
|
136
|
+
|
|
137
|
+
def as_dict(self, include_id=False):
|
|
138
|
+
dict = super().as_dict()
|
|
139
|
+
del dict['bom_id']
|
|
140
|
+
return dict
|
|
141
|
+
|
|
142
|
+
def as_dict_for_export(self):
|
|
143
|
+
return {
|
|
144
|
+
'part_number': self.part.full_part_number(),
|
|
145
|
+
'quantity': self.quantity,
|
|
146
|
+
'do_not_load': self.do_not_load,
|
|
147
|
+
'part_class': self.part.number_class.name if self.part.number_class else '',
|
|
148
|
+
'references': self.references,
|
|
149
|
+
'part_synopsis': self.part_revision.synopsis(),
|
|
150
|
+
'part_description': self.part_revision.description,
|
|
151
|
+
'part_revision': self.part_revision.revision,
|
|
152
|
+
'part_manufacturer': self.part.primary_manufacturer_part.manufacturer.name if self.part.primary_manufacturer_part is not None and self.part.primary_manufacturer_part.manufacturer is not None else '',
|
|
153
|
+
'part_manufacturer_part_number': self.part.primary_manufacturer_part.manufacturer_part_number if self.part.primary_manufacturer_part is not None else '',
|
|
154
|
+
'part_ext_qty': self.extended_quantity,
|
|
155
|
+
'part_order_qty': self.order_quantity,
|
|
156
|
+
'part_seller': self.seller_part.seller.name if self.seller_part is not None else '',
|
|
157
|
+
'part_seller_part_number': self.seller_part.seller_part_number if self.seller_part is not None else '',
|
|
158
|
+
'part_cost': self.seller_part.unit_cost if self.seller_part is not None else '',
|
|
159
|
+
'part_moq': self.seller_part.minimum_order_quantity if self.seller_part is not None else 0,
|
|
160
|
+
'part_nre': self.seller_part.nre_cost if self.seller_part is not None else 0,
|
|
161
|
+
'part_ext_cost': self.extended_cost(),
|
|
162
|
+
'part_out_of_pocket_cost': self.out_of_pocket_cost(),
|
|
163
|
+
'part_lead_time_days': self.seller_part.lead_time_days if self.seller_part is not None else 0,
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
def manufacturer_parts_for_export(self):
|
|
167
|
+
return [mp.as_dict_for_export() for mp in self.part.manufacturer_parts(exclude_primary=True)]
|
|
168
|
+
|
|
169
|
+
def seller_parts_for_export(self):
|
|
170
|
+
return [sp.as_dict_for_export() for sp in self.part.seller_parts(exclude_primary=True)]
|
|
171
|
+
|
|
172
|
+
def __str__(self):
|
|
173
|
+
return f'{self.part.full_part_number()}, qty: {self.quantity}'
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class PartIndentedBomItem(PartBomItem, AsDictModel):
|
|
177
|
+
def __init__(self, indent_level, parent_id, subpart, parent_quantity, *args, **kwargs):
|
|
178
|
+
super().__init__(*args, **kwargs)
|
|
179
|
+
self.indent_level = indent_level
|
|
180
|
+
self.parent_id = parent_id
|
|
181
|
+
self.subpart = subpart
|
|
182
|
+
self.parent_quantity = parent_quantity
|
|
183
|
+
|
|
184
|
+
def as_dict_for_export(self):
|
|
185
|
+
dict = super().as_dict_for_export()
|
|
186
|
+
dict.update({
|
|
187
|
+
'level': self.indent_level,
|
|
188
|
+
})
|
|
189
|
+
return dict
|
|
190
|
+
|
|
191
|
+
def __str__(self):
|
|
192
|
+
return f'level: {self.indent_level}, {super().__str__()}'
|
bom/settings.py
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from django.utils.log import DEFAULT_LOGGING
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
BASE_DIR = Path(__file__).resolve().parent.parent
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
from .local_settings import *
|
|
12
|
+
except ImportError:
|
|
13
|
+
logger.warning("local_settings.py not found. Using default settings.")
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
BOM_CONFIG = {}
|
|
17
|
+
BOM_CONFIG_DEFAULT = {
|
|
18
|
+
'base_template': 'base.html',
|
|
19
|
+
'mouser_api_key': None,
|
|
20
|
+
'standalone_mode': True,
|
|
21
|
+
'admin_dashboard': {
|
|
22
|
+
'enable_autocomplete': True,
|
|
23
|
+
'page_size': 50,
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
BOM_ORGANIZATION_MODEL = 'bom.Organization'
|
|
27
|
+
BOM_USER_META_MODEL = 'bom.UserMeta'
|
|
28
|
+
|
|
29
|
+
# Apply custom settings over defaults
|
|
30
|
+
bom_config_new = BOM_CONFIG_DEFAULT.copy()
|
|
31
|
+
bom_config_new.update(BOM_CONFIG)
|
|
32
|
+
BOM_CONFIG = bom_config_new
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# --------------------------------------------------------------------------
|
|
36
|
+
# APPLICATION DEFINITION
|
|
37
|
+
# --------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
INSTALLED_APPS = [
|
|
40
|
+
# Custom Apps first
|
|
41
|
+
'bom.apps.BomConfig',
|
|
42
|
+
|
|
43
|
+
# Django contrib apps
|
|
44
|
+
'django.contrib.admin',
|
|
45
|
+
'django.contrib.auth',
|
|
46
|
+
'django.contrib.contenttypes',
|
|
47
|
+
'django.contrib.sessions',
|
|
48
|
+
'django.contrib.messages',
|
|
49
|
+
'django.contrib.staticfiles',
|
|
50
|
+
|
|
51
|
+
# Third-party apps
|
|
52
|
+
'materializecssform',
|
|
53
|
+
'social_django',
|
|
54
|
+
'djmoney',
|
|
55
|
+
'djmoney.contrib.exchange',
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
MIDDLEWARE = [
|
|
59
|
+
'django.middleware.security.SecurityMiddleware',
|
|
60
|
+
'django.contrib.sessions.middleware.SessionMiddleware',
|
|
61
|
+
'django.middleware.common.CommonMiddleware',
|
|
62
|
+
'django.middleware.csrf.CsrfViewMiddleware',
|
|
63
|
+
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
|
64
|
+
'django.contrib.messages.middleware.MessageMiddleware',
|
|
65
|
+
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
|
66
|
+
'social_django.middleware.SocialAuthExceptionMiddleware',
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
ROOT_URLCONF = 'bom.urls'
|
|
70
|
+
WSGI_APPLICATION = 'bom.wsgi.application'
|
|
71
|
+
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# --------------------------------------------------------------------------
|
|
75
|
+
# TEMPLATES
|
|
76
|
+
# --------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
TEMPLATES = [
|
|
79
|
+
{
|
|
80
|
+
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
|
81
|
+
# Use pathlib syntax for cleaner path joining
|
|
82
|
+
'DIRS': [BASE_DIR / 'bom' / 'templates' / 'bom'],
|
|
83
|
+
'APP_DIRS': True,
|
|
84
|
+
'OPTIONS': {
|
|
85
|
+
'context_processors': [
|
|
86
|
+
'django.template.context_processors.debug',
|
|
87
|
+
'django.template.context_processors.request',
|
|
88
|
+
'django.contrib.auth.context_processors.auth',
|
|
89
|
+
'django.contrib.messages.context_processors.messages',
|
|
90
|
+
'django.template.context_processors.media',
|
|
91
|
+
'social_django.context_processors.backends',
|
|
92
|
+
'social_django.context_processors.login_redirect',
|
|
93
|
+
'bom.context_processors.bom_config',
|
|
94
|
+
],
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# --------------------------------------------------------------------------
|
|
101
|
+
# AUTHENTICATION & SECURITY
|
|
102
|
+
# --------------------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
AUTHENTICATION_BACKENDS = (
|
|
105
|
+
'social_core.backends.google.GoogleOAuth2',
|
|
106
|
+
'django.contrib.auth.backends.ModelBackend',
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# Password validation - kept as is
|
|
110
|
+
|
|
111
|
+
AUTH_PASSWORD_VALIDATORS = [
|
|
112
|
+
{'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',},
|
|
113
|
+
{'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',},
|
|
114
|
+
{'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',},
|
|
115
|
+
{'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',},
|
|
116
|
+
]
|
|
117
|
+
|
|
118
|
+
# Social Auth Settings - kept as is
|
|
119
|
+
SOCIAL_AUTH_GOOGLE_OAUTH2_SCOPE = ['email', 'profile', 'https://www.googleapis.com/auth/drive', ]
|
|
120
|
+
SOCIAL_AUTH_GOOGLE_OAUTH2_AUTH_EXTRA_ARGUMENTS = {
|
|
121
|
+
'access_type': 'offline',
|
|
122
|
+
'approval_prompt': 'force'
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
SOCIAL_AUTH_PIPELINE = (
|
|
126
|
+
'social_core.pipeline.social_auth.social_details',
|
|
127
|
+
'social_core.pipeline.social_auth.social_uid',
|
|
128
|
+
'social_core.pipeline.social_auth.social_user',
|
|
129
|
+
'social_core.pipeline.user.get_username',
|
|
130
|
+
'social_core.pipeline.social_auth.associate_by_email',
|
|
131
|
+
'social_core.pipeline.user.create_user',
|
|
132
|
+
'social_core.pipeline.social_auth.associate_user',
|
|
133
|
+
'social_core.pipeline.social_auth.load_extra_data',
|
|
134
|
+
'social_core.pipeline.user.user_details',
|
|
135
|
+
'bom.third_party_apis.google_drive.initialize_parent',
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
SOCIAL_AUTH_DISCONNECT_PIPELINE = (
|
|
139
|
+
'social_core.pipeline.disconnect.allowed_to_disconnect',
|
|
140
|
+
'bom.third_party_apis.google_drive.uninitialize_parent',
|
|
141
|
+
'social_core.pipeline.disconnect.get_entries',
|
|
142
|
+
'social_core.pipeline.disconnect.revoke_tokens',
|
|
143
|
+
'social_core.pipeline.disconnect.disconnect',
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
# --------------------------------------------------------------------------
|
|
148
|
+
# I18N & TIME
|
|
149
|
+
# --------------------------------------------------------------------------
|
|
150
|
+
|
|
151
|
+
LANGUAGE_CODE = 'en-us'
|
|
152
|
+
TIME_ZONE = 'UTC'
|
|
153
|
+
USE_I18N = True
|
|
154
|
+
USE_L10N = True # Deprecated in Django 4.0, but harmless for now
|
|
155
|
+
USE_TZ = True
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
# --------------------------------------------------------------------------
|
|
159
|
+
# FILE STORAGE (Static and Media)
|
|
160
|
+
# --------------------------------------------------------------------------
|
|
161
|
+
|
|
162
|
+
STATIC_URL = '/static/'
|
|
163
|
+
STATIC_ROOT = BASE_DIR / 'static'
|
|
164
|
+
|
|
165
|
+
MEDIA_URL = '/media/'
|
|
166
|
+
MEDIA_ROOT = BASE_DIR / 'media'
|
|
167
|
+
|
|
168
|
+
# Use the Django 4.2+ STORAGES setting
|
|
169
|
+
STORAGES = {
|
|
170
|
+
# Default is FileSystemStorage, configured by STATIC_ROOT/MEDIA_ROOT
|
|
171
|
+
"default": {
|
|
172
|
+
"BACKEND": "django.core.files.storage.FileSystemStorage",
|
|
173
|
+
},
|
|
174
|
+
"staticfiles": {
|
|
175
|
+
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
|
|
176
|
+
},
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
# --------------------------------------------------------------------------
|
|
181
|
+
# URLS & REDIRECTS
|
|
182
|
+
# --------------------------------------------------------------------------
|
|
183
|
+
|
|
184
|
+
LOGIN_URL = '/login/'
|
|
185
|
+
LOGOUT_URL = '/logout/'
|
|
186
|
+
|
|
187
|
+
LOGIN_REDIRECT_URL = '/'
|
|
188
|
+
LOGOUT_REDIRECT_URL = '/'
|
|
189
|
+
|
|
190
|
+
SOCIAL_AUTH_LOGIN_REDIRECT_URL = '/settings?tab_anchor=file'
|
|
191
|
+
SOCIAL_AUTH_DISCONNECT_REDIRECT_URL = '/settings?tab_anchor=file'
|
|
192
|
+
SOCIAL_AUTH_LOGIN_ERROR_URL = '/'
|
|
193
|
+
|
|
194
|
+
# Custom login url for BOM_LOGIN (kept for compatibility)
|
|
195
|
+
BOM_LOGIN_URL = None
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
# --------------------------------------------------------------------------
|
|
199
|
+
# DJMONEY CONFIG
|
|
200
|
+
# --------------------------------------------------------------------------
|
|
201
|
+
|
|
202
|
+
CURRENCY_DECIMAL_PLACES = 4
|
|
203
|
+
EXCHANGE_BACKEND = 'djmoney.contrib.exchange.backends.FixerBackend'
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
# --------------------------------------------------------------------------
|
|
207
|
+
# LOGGING
|
|
208
|
+
# --------------------------------------------------------------------------
|
|
209
|
+
|
|
210
|
+
# Set DEBUG to False here if not defined in local_settings
|
|
211
|
+
DEBUG = locals().get('DEBUG', False)
|
|
212
|
+
LOG_FILE_PATH = '/var/log/indabom/django.log' if not DEBUG else BASE_DIR / 'bom.log'
|
|
213
|
+
|
|
214
|
+
LOGGING = {
|
|
215
|
+
'version': 1,
|
|
216
|
+
'disable_existing_loggers': False,
|
|
217
|
+
'handlers': {
|
|
218
|
+
'mail_admins': {
|
|
219
|
+
'class': 'django.utils.log.AdminEmailHandler',
|
|
220
|
+
'level': 'ERROR',
|
|
221
|
+
'include_html': True,
|
|
222
|
+
},
|
|
223
|
+
'logfile': {
|
|
224
|
+
'class': 'logging.handlers.WatchedFileHandler',
|
|
225
|
+
'filename': LOG_FILE_PATH,
|
|
226
|
+
'formatter': 'timestamp', # Define a simple formatter
|
|
227
|
+
},
|
|
228
|
+
'console': {
|
|
229
|
+
'class': 'logging.StreamHandler',
|
|
230
|
+
'formatter': 'timestamp',
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
'formatters': {
|
|
234
|
+
'timestamp': {
|
|
235
|
+
'format': "[%(asctime)s] %(levelname)s [%(name)s.%(funcName)s:%(lineno)d] %(message)s"
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
'loggers': {
|
|
239
|
+
# Catchall logger
|
|
240
|
+
'': {
|
|
241
|
+
'handlers': ['console', 'logfile'],
|
|
242
|
+
'level': 'INFO',
|
|
243
|
+
},
|
|
244
|
+
# Django logging
|
|
245
|
+
'django': {
|
|
246
|
+
'handlers': ['logfile'],
|
|
247
|
+
'level': 'ERROR',
|
|
248
|
+
'propagate': False,
|
|
249
|
+
},
|
|
250
|
+
'django.request': {
|
|
251
|
+
'handlers': ['mail_admins', 'logfile'],
|
|
252
|
+
'level': 'ERROR',
|
|
253
|
+
'propagate': False,
|
|
254
|
+
},
|
|
255
|
+
# django-bom app
|
|
256
|
+
'bom': {
|
|
257
|
+
'handlers': ['logfile', 'console'],
|
|
258
|
+
'level': 'INFO',
|
|
259
|
+
'propagate': False
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
table.treetable span.indenter {
|
|
2
|
+
display: inline-block;
|
|
3
|
+
margin: 0;
|
|
4
|
+
padding: 0;
|
|
5
|
+
text-align: right;
|
|
6
|
+
|
|
7
|
+
/* Disable text selection of nodes (for better D&D UX) */
|
|
8
|
+
user-select: none;
|
|
9
|
+
-khtml-user-select: none;
|
|
10
|
+
-moz-user-select: none;
|
|
11
|
+
-o-user-select: none;
|
|
12
|
+
-webkit-user-select: none;
|
|
13
|
+
|
|
14
|
+
/* Force content-box box model for indenter (Bootstrap compatibility) */
|
|
15
|
+
-webkit-box-sizing: content-box;
|
|
16
|
+
-moz-box-sizing: content-box;
|
|
17
|
+
box-sizing: content-box;
|
|
18
|
+
|
|
19
|
+
width: 19px;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
table.treetable span.indenter a {
|
|
23
|
+
background-position: left center;
|
|
24
|
+
background-repeat: no-repeat;
|
|
25
|
+
display: inline-block;
|
|
26
|
+
text-decoration: none;
|
|
27
|
+
width: 19px;
|
|
28
|
+
}
|