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.

Files changed (191) hide show
  1. bom/__init__.py +1 -0
  2. bom/admin.py +207 -0
  3. bom/apps.py +8 -0
  4. bom/auth_backends.py +47 -0
  5. bom/base_classes.py +31 -0
  6. bom/constants.py +217 -0
  7. bom/context_processors.py +9 -0
  8. bom/csv_headers.py +252 -0
  9. bom/decorators.py +32 -0
  10. bom/form_fields.py +59 -0
  11. bom/forms.py +1328 -0
  12. bom/helpers.py +367 -0
  13. bom/local_settings.py +35 -0
  14. bom/migrations/0001_initial.py +135 -0
  15. bom/migrations/0002_auto_20180908_2151.py +24 -0
  16. bom/migrations/0003_sellerpart_data_source.py +18 -0
  17. bom/migrations/0004_auto_20180911_0011.py +18 -0
  18. bom/migrations/0005_auto_20181007_1934.py +56 -0
  19. bom/migrations/0006_auto_20181007_1949.py +41 -0
  20. bom/migrations/0007_auto_20181009_0256.py +19 -0
  21. bom/migrations/0008_auto_20181030_0427.py +19 -0
  22. bom/migrations/0009_subpart_reference.py +18 -0
  23. bom/migrations/0010_auto_20181202_0733.py +23 -0
  24. bom/migrations/0011_auto_20181202_2113.py +22 -0
  25. bom/migrations/0012_partchangehistory.py +30 -0
  26. bom/migrations/0013_auto_20190222_1631.py +19 -0
  27. bom/migrations/0014_auto_20190223_2353.py +18 -0
  28. bom/migrations/0015_auto_20190303_1915.py +136 -0
  29. bom/migrations/0016_auto_20190405_2308.py +58 -0
  30. bom/migrations/0017_auto_20190616_1912.py +19 -0
  31. bom/migrations/0018_auto_20190616_2143.py +24 -0
  32. bom/migrations/0019_auto_20190624_1246.py +45 -0
  33. bom/migrations/0020_auto_20190627_0207.py +38 -0
  34. bom/migrations/0021_auto_20190627_0428.py +23 -0
  35. bom/migrations/0022_auto_20190811_2140.py +35 -0
  36. bom/migrations/0023_auto_20191205_2351.py +255 -0
  37. bom/migrations/0024_auto_20191214_1342.py +89 -0
  38. bom/migrations/0025_auto_20191221_1907.py +38 -0
  39. bom/migrations/0026_auto_20191222_2258.py +22 -0
  40. bom/migrations/0027_auto_20191222_2347.py +17 -0
  41. bom/migrations/0028_partrevision_displayable_synopsis.py +74 -0
  42. bom/migrations/0029_auto_20191231_1630.py +23 -0
  43. bom/migrations/0030_auto_20200101_2253.py +22 -0
  44. bom/migrations/0031_auto_20200104_1352.py +38 -0
  45. bom/migrations/0032_auto_20200126_1806.py +27 -0
  46. bom/migrations/0033_auto_20200203_0618.py +29 -0
  47. bom/migrations/0034_auto_20200222_0359.py +30 -0
  48. bom/migrations/0035_auto_20200303_0111.py +34 -0
  49. bom/migrations/0036_auto_20200303_0538.py +17 -0
  50. bom/migrations/0037_auto_20200405_1642.py +44 -0
  51. bom/migrations/0038_auto_20200422_0504.py +19 -0
  52. bom/migrations/0039_auto_20200929_2315.py +41 -0
  53. bom/migrations/0040_alter_organization_currency.py +19 -0
  54. bom/migrations/0041_organization_subscription_quantity.py +18 -0
  55. bom/migrations/0042_auto_20210720_2137.py +23 -0
  56. bom/migrations/0043_auto_20211123_0157.py +24 -0
  57. bom/migrations/0044_auto_20220831_1241.py +23 -0
  58. bom/migrations/0045_sellerpart_link.py +18 -0
  59. bom/migrations/0046_alter_sellerpart_unique_together.py +17 -0
  60. bom/migrations/0047_sellerpart_seller_part_number.py +18 -0
  61. bom/migrations/0048_rename_part_organization_number_class_bom_part_organiz_b333d6_idx_and_more.py +1017 -0
  62. bom/migrations/0049_alter_assembly_id_alter_assemblysubparts_id_and_more.py +99 -0
  63. bom/migrations/0050_alter_organization_options.py +17 -0
  64. bom/migrations/0051_alter_manufacturer_organization_and_more.py +41 -0
  65. bom/migrations/0052_remove_partrevision_attribute_and_more.py +584 -0
  66. bom/migrations/__init__.py +0 -0
  67. bom/models.py +886 -0
  68. bom/part_bom.py +192 -0
  69. bom/settings.py +262 -0
  70. bom/static/bom/css/dashboard.css +17 -0
  71. bom/static/bom/css/jquery.treetable.css +28 -0
  72. bom/static/bom/css/materialize.min.css +13 -0
  73. bom/static/bom/css/part-info.css +15 -0
  74. bom/static/bom/css/style.css +482 -0
  75. bom/static/bom/css/tablesorter-theme.materialize.css +176 -0
  76. bom/static/bom/css/treetable-theme.css +42 -0
  77. bom/static/bom/doc/sample_part_classes.csv +38 -0
  78. bom/static/bom/doc/test_bom.csv +6 -0
  79. bom/static/bom/doc/test_bom_5_intelligent.csv +4 -0
  80. bom/static/bom/doc/test_full_bom.csv +37 -0
  81. bom/static/bom/doc/test_new_parts.csv +5 -0
  82. bom/static/bom/doc/test_new_parts_5_intelligent.csv +5 -0
  83. bom/static/bom/img/_ionicons_svg_md-arrow-dropdown.svg +1 -0
  84. bom/static/bom/img/_ionicons_svg_md-arrow-dropright.svg +1 -0
  85. bom/static/bom/img/favicon.ico +0 -0
  86. bom/static/bom/img/google/web/1x/btn_google_signin_dark_disabled_web.png +0 -0
  87. bom/static/bom/img/google/web/1x/btn_google_signin_dark_focus_web.png +0 -0
  88. bom/static/bom/img/google/web/1x/btn_google_signin_dark_normal_web.png +0 -0
  89. bom/static/bom/img/google/web/1x/btn_google_signin_dark_pressed_web.png +0 -0
  90. bom/static/bom/img/google/web/1x/btn_google_signin_light_disabled_web.png +0 -0
  91. bom/static/bom/img/google/web/1x/btn_google_signin_light_focus_web.png +0 -0
  92. bom/static/bom/img/google/web/1x/btn_google_signin_light_normal_web.png +0 -0
  93. bom/static/bom/img/google/web/1x/btn_google_signin_light_pressed_web.png +0 -0
  94. bom/static/bom/img/google/web/2x/btn_google_signin_dark_disabled_web@2x.png +0 -0
  95. bom/static/bom/img/google/web/2x/btn_google_signin_dark_focus_web@2x.png +0 -0
  96. bom/static/bom/img/google/web/2x/btn_google_signin_dark_normal_web@2x.png +0 -0
  97. bom/static/bom/img/google/web/2x/btn_google_signin_dark_pressed_web@2x.png +0 -0
  98. bom/static/bom/img/google/web/2x/btn_google_signin_light_disabled_web@2x.png +0 -0
  99. bom/static/bom/img/google/web/2x/btn_google_signin_light_focus_web@2x.png +0 -0
  100. bom/static/bom/img/google/web/2x/btn_google_signin_light_normal_web@2x.png +0 -0
  101. bom/static/bom/img/google/web/2x/btn_google_signin_light_pressed_web@2x.png +0 -0
  102. bom/static/bom/img/google/web/vector/btn_google_dark_disabled_ios.eps +814 -0
  103. bom/static/bom/img/google/web/vector/btn_google_dark_disabled_ios.svg +24 -0
  104. bom/static/bom/img/google/web/vector/btn_google_dark_focus_ios.eps +1866 -0
  105. bom/static/bom/img/google/web/vector/btn_google_dark_focus_ios.svg +51 -0
  106. bom/static/bom/img/google/web/vector/btn_google_dark_normal_ios.eps +1031 -0
  107. bom/static/bom/img/google/web/vector/btn_google_dark_normal_ios.svg +50 -0
  108. bom/static/bom/img/google/web/vector/btn_google_dark_pressed_ios.eps +1031 -0
  109. bom/static/bom/img/google/web/vector/btn_google_dark_pressed_ios.svg +50 -0
  110. bom/static/bom/img/google/web/vector/btn_google_light_disabled_ios.eps +814 -0
  111. bom/static/bom/img/google/web/vector/btn_google_light_disabled_ios.svg +24 -0
  112. bom/static/bom/img/google/web/vector/btn_google_light_focus_ios.eps +1837 -0
  113. bom/static/bom/img/google/web/vector/btn_google_light_focus_ios.svg +44 -0
  114. bom/static/bom/img/google/web/vector/btn_google_light_normal_ios.eps +1002 -0
  115. bom/static/bom/img/google/web/vector/btn_google_light_normal_ios.svg +43 -0
  116. bom/static/bom/img/google/web/vector/btn_google_light_pressed_ios.eps +1002 -0
  117. bom/static/bom/img/google/web/vector/btn_google_light_pressed_ios.svg +43 -0
  118. bom/static/bom/img/google_drive_logo.svg +1 -0
  119. bom/static/bom/img/indabom.png +0 -0
  120. bom/static/bom/img/mouser.png +0 -0
  121. bom/static/bom/img/octopart_blue.svg +19 -0
  122. bom/static/bom/js/formset-handler.js +65 -0
  123. bom/static/bom/js/jquery-3.4.1.min.js +2 -0
  124. bom/static/bom/js/jquery.ba-floatingscrollbar.min.js +10 -0
  125. bom/static/bom/js/jquery.treetable.js +629 -0
  126. bom/static/bom/js/materialize.min.js +6 -0
  127. bom/templates/bom/account-delete.html +23 -0
  128. bom/templates/bom/add-manufacturer-part.html +66 -0
  129. bom/templates/bom/add-sellerpart.html +93 -0
  130. bom/templates/bom/base-menu.html +16 -0
  131. bom/templates/bom/base.html +129 -0
  132. bom/templates/bom/bom-action-btn.html +23 -0
  133. bom/templates/bom/bom-action-table.html +57 -0
  134. bom/templates/bom/bom-base-menu.html +6 -0
  135. bom/templates/bom/bom-base.html +24 -0
  136. bom/templates/bom/bom-form-modal.html +36 -0
  137. bom/templates/bom/bom-form.html +30 -0
  138. bom/templates/bom/bom-modal-add-users.html +49 -0
  139. bom/templates/bom/bom-signup.html +12 -0
  140. bom/templates/bom/components/bom-flat.html +131 -0
  141. bom/templates/bom/components/bom-indented.html +237 -0
  142. bom/templates/bom/components/manufacturer-part-list.html +270 -0
  143. bom/templates/bom/components/seller-part-list.html +62 -0
  144. bom/templates/bom/create-part.html +65 -0
  145. bom/templates/bom/dashboard-menu.html +15 -0
  146. bom/templates/bom/dashboard.html +303 -0
  147. bom/templates/bom/edit-manufacturer-part.html +72 -0
  148. bom/templates/bom/edit-part-class.html +120 -0
  149. bom/templates/bom/edit-part.html +67 -0
  150. bom/templates/bom/edit-quantity-of-measure.html +119 -0
  151. bom/templates/bom/edit-user-meta.html +70 -0
  152. bom/templates/bom/help.html +1356 -0
  153. bom/templates/bom/manufacturer-info.html +82 -0
  154. bom/templates/bom/manufacturers.html +97 -0
  155. bom/templates/bom/nothing-to-see.html +15 -0
  156. bom/templates/bom/organization-create.html +135 -0
  157. bom/templates/bom/part-info.html +448 -0
  158. bom/templates/bom/part-revision-display.html +50 -0
  159. bom/templates/bom/part-revision-edit.html +39 -0
  160. bom/templates/bom/part-revision-manage-bom.html +115 -0
  161. bom/templates/bom/part-revision-new.html +57 -0
  162. bom/templates/bom/part-revision-release.html +41 -0
  163. bom/templates/bom/search-help.html +101 -0
  164. bom/templates/bom/seller-info.html +82 -0
  165. bom/templates/bom/sellers.html +97 -0
  166. bom/templates/bom/settings.html +734 -0
  167. bom/templates/bom/signup.html +28 -0
  168. bom/templates/bom/subscription_panel.html +16 -0
  169. bom/templates/bom/table_of_contents.html +47 -0
  170. bom/templates/bom/upload-bom.html +111 -0
  171. bom/templates/bom/upload-parts-help.html +103 -0
  172. bom/templates/bom/upload-parts.html +50 -0
  173. bom/templates/registration/login.html +39 -0
  174. bom/tests.py +1592 -0
  175. bom/third_party_apis/__init__.py +0 -0
  176. bom/third_party_apis/base_api.py +51 -0
  177. bom/third_party_apis/google_drive.py +166 -0
  178. bom/third_party_apis/mouser.py +132 -0
  179. bom/third_party_apis/test_apis.py +24 -0
  180. bom/urls.py +100 -0
  181. bom/utils.py +228 -0
  182. bom/validators.py +23 -0
  183. bom/views/__init__.py +0 -0
  184. bom/views/json_views.py +55 -0
  185. bom/views/views.py +1773 -0
  186. bom/wsgi.py +16 -0
  187. django_bom-1.262.dist-info/METADATA +206 -0
  188. django_bom-1.262.dist-info/RECORD +191 -0
  189. django_bom-1.262.dist-info/WHEEL +5 -0
  190. django_bom-1.262.dist-info/licenses/LICENSE +674 -0
  191. 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,17 @@
1
+ @media print {
2
+ #searchForm {
3
+ display: none;
4
+ }
5
+
6
+ .actions-row {
7
+ display: none;
8
+ }
9
+
10
+ .fixed-action-btn {
11
+ display: none;
12
+ }
13
+
14
+ .responsive-table-wrapper {
15
+ overflow-x: visible !important;
16
+ }
17
+ }
@@ -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
+ }