aa-ledger 0.9.9__py3-none-any.whl → 0.9.9.1__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.
Files changed (77) hide show
  1. {aa_ledger-0.9.9.dist-info → aa_ledger-0.9.9.1.dist-info}/METADATA +1 -1
  2. {aa_ledger-0.9.9.dist-info → aa_ledger-0.9.9.1.dist-info}/RECORD +77 -77
  3. ledger/__init__.py +9 -9
  4. ledger/api/api_helper/billboard_helper.py +277 -277
  5. ledger/api/ledger/admin.py +289 -289
  6. ledger/app_settings.py +50 -50
  7. ledger/constants.py +5 -0
  8. ledger/decorators.py +92 -11
  9. ledger/helpers/alliance.py +353 -334
  10. ledger/helpers/character.py +260 -260
  11. ledger/helpers/core.py +565 -565
  12. ledger/helpers/corporation.py +455 -421
  13. ledger/helpers/etag.py +237 -237
  14. ledger/helpers/ref_type.py +475 -475
  15. ledger/locale/cs_CZ/LC_MESSAGES/django.po +942 -942
  16. ledger/locale/de/LC_MESSAGES/django.po +961 -961
  17. ledger/locale/django.pot +942 -942
  18. ledger/locale/es/LC_MESSAGES/django.po +943 -943
  19. ledger/locale/fr_FR/LC_MESSAGES/django.po +942 -942
  20. ledger/locale/it_IT/LC_MESSAGES/django.po +942 -942
  21. ledger/locale/ja/LC_MESSAGES/django.po +943 -943
  22. ledger/locale/ko_KR/LC_MESSAGES/django.po +942 -942
  23. ledger/locale/nl_NL/LC_MESSAGES/django.po +942 -942
  24. ledger/locale/pl_PL/LC_MESSAGES/django.po +942 -942
  25. ledger/locale/ru/LC_MESSAGES/django.po +945 -945
  26. ledger/locale/sk/LC_MESSAGES/django.po +944 -944
  27. ledger/locale/uk/LC_MESSAGES/django.po +946 -946
  28. ledger/locale/zh_Hans/LC_MESSAGES/django.po +943 -943
  29. ledger/managers/character_mining_manager.py +239 -239
  30. ledger/managers/character_planetary_manager.py +1 -1
  31. ledger/migrations/0016_characterminingledger_price_per_unit.py +21 -21
  32. ledger/models/characteraudit.py +496 -496
  33. ledger/static/ledger/css/cards.css +1 -1
  34. ledger/static/ledger/css/table.css +1 -1
  35. ledger/static/ledger/js/charts.js +221 -221
  36. ledger/static/ledger/js/planetary.js +143 -143
  37. ledger/tasks.py +442 -449
  38. ledger/templates/ledger/allyledger/admin/alliance_administration.html +46 -46
  39. ledger/templates/ledger/allyledger/admin/alliance_overview.html +108 -108
  40. ledger/templates/ledger/allyledger/alliance_ledger.html +86 -86
  41. ledger/templates/ledger/bundles/ally-administration-bundles.html +59 -59
  42. ledger/templates/ledger/bundles/char-administration-bundles.html +66 -66
  43. ledger/templates/ledger/bundles/character-ledger-bundles.html +66 -66
  44. ledger/templates/ledger/bundles/corp-administration-bundles.html +68 -68
  45. ledger/templates/ledger/bundles/corporation-ledger-bundles.html +75 -75
  46. ledger/templates/ledger/charledger/admin/character_administration.html +39 -39
  47. ledger/templates/ledger/charledger/admin/character_overview.html +106 -106
  48. ledger/templates/ledger/charledger/character_ledger.html +94 -94
  49. ledger/templates/ledger/charledger/planetary/planetary_ledger.html +54 -54
  50. ledger/templates/ledger/corpledger/admin/corporation_administration.html +39 -39
  51. ledger/templates/ledger/corpledger/admin/corporation_overview.html +108 -108
  52. ledger/templates/ledger/corpledger/corporation_ledger.html +129 -86
  53. ledger/templates/ledger/partials/administration/alliance.html +37 -37
  54. ledger/templates/ledger/partials/administration/alliance_corporations.html +58 -58
  55. ledger/templates/ledger/partials/administration/corporation_characters.html +34 -34
  56. ledger/templates/ledger/partials/information/daily.html +56 -56
  57. ledger/templates/ledger/partials/information/day.html +48 -48
  58. ledger/templates/ledger/partials/information/hourly.html +53 -53
  59. ledger/templates/ledger/partials/information/summary.html +88 -88
  60. ledger/templates/ledger/partials/information/view_character_content.html +35 -35
  61. ledger/templates/ledger/partials/table/char-ledger.html +85 -85
  62. ledger/templates/ledger/partials/table/corp-ledger.html +66 -66
  63. ledger/templates/ledger/partials/view/card.html +160 -160
  64. ledger/tests/test_decarators.py +102 -17
  65. ledger/tests/test_helpers/test_etag.py +149 -149
  66. ledger/tests/test_managers/test_character_mining_manager.py +54 -54
  67. ledger/tests/test_models/test_characterminingledger.py +107 -106
  68. ledger/tests/test_tasks.py +282 -282
  69. ledger/tests/test_templatetags.py +5 -2
  70. ledger/tests/test_views/test_access.py +852 -852
  71. ledger/tests/testdata/esi.json +1 -2
  72. ledger/tests/testdata/eveuniverse.json +391 -391
  73. ledger/urls.py +66 -21
  74. ledger/views/alliance/alliance_ledger.py +203 -203
  75. ledger/views/corporation/corporation_ledger.py +25 -9
  76. {aa_ledger-0.9.9.dist-info → aa_ledger-0.9.9.1.dist-info}/WHEEL +0 -0
  77. {aa_ledger-0.9.9.dist-info → aa_ledger-0.9.9.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,421 +1,455 @@
1
- """PvE Views"""
2
-
3
- # Standard Library
4
- import json
5
- from decimal import Decimal
6
-
7
- # Django
8
- from django.core.cache import cache
9
- from django.core.handlers.wsgi import WSGIRequest
10
- from django.db.models import DecimalField, Q, Sum
11
- from django.utils.translation import gettext as _
12
-
13
- # Alliance Auth
14
- from allianceauth.eveonline.models import EveCharacter
15
- from allianceauth.services.hooks import get_extension_logger
16
-
17
- # Alliance Auth (External Libs)
18
- from app_utils.logging import LoggerAddTag
19
-
20
- # AA Ledger
21
- from ledger import __title__
22
- from ledger.app_settings import LEDGER_CACHE_ENABLED, LEDGER_CACHE_STALE
23
- from ledger.helpers.core import LedgerCore, LedgerEntity
24
- from ledger.helpers.ref_type import RefTypeManager
25
- from ledger.models.corporationaudit import (
26
- CorporationAudit,
27
- CorporationWalletJournalEntry,
28
- )
29
-
30
- logger = LoggerAddTag(get_extension_logger(__name__), __title__)
31
-
32
- NPC_ENTITIES = [
33
- 1000125, # Concord Bounties (Bounty Prizes, ESS
34
- 1000132, # Secure Commerce Commission (Market Fees)
35
- 1000413, # Air Laboratories (Daily Login Rewards, etc.)
36
- ]
37
-
38
-
39
- class CorporationData(LedgerCore):
40
- """Class to hold character data for the ledger."""
41
-
42
- # pylint: disable=too-many-positional-arguments
43
- def __init__(
44
- self,
45
- request: WSGIRequest,
46
- corporation: CorporationAudit,
47
- year=None,
48
- month=None,
49
- day=None,
50
- ):
51
- LedgerCore.__init__(self, year, month, day)
52
- self.request = request
53
- self.corporation = corporation
54
- self.auth_char_ids = self.auth_character_ids
55
-
56
- def setup_ledger(self, entity: LedgerEntity = None):
57
- """Setup the Ledger Data for the Corporation."""
58
- if entity is not None:
59
- if (
60
- self.request.GET.get("all", False)
61
- and entity.entity_id == self.corporation.corporation.corporation_id
62
- ):
63
- self.journal = CorporationWalletJournalEntry.objects.filter(
64
- self.filter_date,
65
- division__corporation=self.corporation,
66
- ).exclude( # Filter by date and division
67
- first_party_id=self.corporation.corporation.corporation_id,
68
- second_party_id=self.corporation.corporation.corporation_id,
69
- ) # exclude Transaction between the corporation itself
70
- else:
71
- character_ids = entity.get_alts_ids_or_self()
72
- self.journal = (
73
- CorporationWalletJournalEntry.objects.filter(
74
- self.filter_date,
75
- division__corporation=self.corporation,
76
- ) # Filter by date and division
77
- .filter(
78
- Q(first_party_id__in=character_ids)
79
- | Q(second_party_id__in=character_ids)
80
- ) # Filter only needed Character IDs
81
- .exclude(
82
- first_party_id=self.corporation.corporation.corporation_id,
83
- second_party_id=self.corporation.corporation.corporation_id,
84
- ) # exclude Transaction between the corporation itself
85
- )
86
-
87
- if entity.entity_id == self.corporation.corporation.corporation_id:
88
- self.journal = self.journal.filter(
89
- Q(first_party_id__in=NPC_ENTITIES)
90
- | Q(second_party_id__in=NPC_ENTITIES)
91
- )
92
-
93
- # If the entity is a corporation or alliance, we need to exclude the accounts Character IDs
94
- # from the journal to prevent double counting
95
- if entity.type in ["alliance", "corporation"]:
96
- exclude_ids = self.auth_char_ids - set(character_ids)
97
- self.journal = self.journal.exclude(
98
- Q(first_party_id__in=exclude_ids)
99
- | Q(second_party_id__in=exclude_ids)
100
- )
101
- # Get All Entities from the Journal
102
- self.entities = set(
103
- self.journal.values_list("second_party_id", flat=True)
104
- ) | set(self.journal.values_list("first_party_id", flat=True))
105
- else:
106
- self.journal = CorporationWalletJournalEntry.objects.filter(
107
- self.filter_date, division__corporation=self.corporation
108
- ).exclude( # Filter by date and division
109
- first_party_id=self.corporation.corporation.corporation_id,
110
- second_party_id=self.corporation.corporation.corporation_id,
111
- ) # exclude Transaction between the corporation itself
112
-
113
- # Evaluate the existing years for the view
114
- self.existing_years = (
115
- CorporationWalletJournalEntry.objects.filter(
116
- division__corporation=self.corporation
117
- )
118
- .exclude(date__year__isnull=True)
119
- .values_list("date__year", flat=True)
120
- .order_by("-date__year")
121
- .distinct()
122
- )
123
-
124
- # Get All Entities from the Journal
125
- self.entities = set(
126
- self.journal.values_list("second_party_id", flat=True)
127
- ) | set(self.journal.values_list("first_party_id", flat=True))
128
-
129
- def create_entity_data(
130
- self,
131
- entity: LedgerEntity,
132
- alts: EveCharacter = None,
133
- ) -> dict:
134
- """Create the URL for entity details based on the view type."""
135
- ids = (
136
- list(alts.values_list("character__character_id", flat=True))
137
- if alts is not None
138
- else [entity.entity_id]
139
- )
140
-
141
- # Create Alts Dictionary
142
- alts_dict = {}
143
- if alts is not None:
144
- for alt in alts:
145
- alts_dict[alt.character.character_id] = alt.character.character_name
146
-
147
- # Remove the main character from the alts dictionary only one entry
148
- if len(alts_dict) == 1:
149
- alts_dict.pop(entity.entity_id, None)
150
-
151
- used_pks = set()
152
- bounty = Decimal(0)
153
- ess = Decimal(0)
154
- miscellaneous = Decimal(0)
155
- costs = Decimal(0)
156
-
157
- for pk, rows in list(self.entries.items()):
158
- for row in rows:
159
- if row["first_party_id"] in ids or row["second_party_id"] in ids:
160
- if RefTypeManager.special_cases(
161
- row, ids=ids, account_char_ids=self.auth_char_ids
162
- ):
163
- continue
164
- bounty += row.get("bounty") or Decimal(0)
165
- ess += row.get("ess") or Decimal(0)
166
- miscellaneous += row.get("miscellaneous") or Decimal(0)
167
- costs += row.get("costs") or Decimal(0)
168
- used_pks.add(pk)
169
-
170
- # Remove Used Pks from Entries
171
- # This is to prevent the entries from being used in the future
172
- for pk in used_pks:
173
- self.entries.pop(pk, None)
174
-
175
- total = sum([bounty, ess, miscellaneous, costs])
176
-
177
- if total == 0:
178
- return None
179
-
180
- char_data = {
181
- "entity": entity,
182
- "alts": alts_dict,
183
- "ledger": {
184
- "bounty": bounty,
185
- "ess": ess,
186
- "miscellaneous": miscellaneous,
187
- "costs": costs,
188
- "total": total,
189
- },
190
- "type": entity.type,
191
- }
192
-
193
- return char_data
194
-
195
- def generate_ledger_data(self) -> dict:
196
- """Generate the ledger data for the corporation."""
197
- ledger = False
198
- finished_entities = False
199
-
200
- self.setup_ledger()
201
-
202
- journal = self.journal.values(
203
- "first_party_id", "second_party_id", "pk", "ref_type"
204
- ).annotate(
205
- bounty=Sum(
206
- "amount",
207
- filter=Q(ref_type__in=RefTypeManager.BOUNTY_PRIZES),
208
- output_field=DecimalField(),
209
- ),
210
- ess=Sum(
211
- "amount",
212
- filter=Q(ref_type__in=RefTypeManager.ESS_TRANSFER),
213
- output_field=DecimalField(),
214
- ),
215
- costs=Sum(
216
- "amount",
217
- filter=Q(ref_type__in=RefTypeManager.all_ref_types(), amount__lt=0),
218
- output_field=DecimalField(),
219
- ),
220
- miscellaneous=Sum(
221
- "amount",
222
- filter=Q(ref_type__in=RefTypeManager.all_ref_types(), amount__gt=0),
223
- output_field=DecimalField(),
224
- ),
225
- )
226
-
227
- # Get the journal hash and cache header
228
- ledger_hash = self._get_ledger_journal_hash(self.journal.values_list("pk"))
229
- ledger_header = self._get_ledger_header(
230
- self.corporation.corporation.corporation_id,
231
- self.year,
232
- self.month,
233
- self.day,
234
- )
235
- cache_header = cache.get(
236
- ledger_header,
237
- False,
238
- )
239
- logger.debug(
240
- f"Ledger Header: {ledger_header}, Cache Header: {cache_header}, Journal Hash: {ledger_hash}"
241
- )
242
-
243
- # Check if the journal is up to date
244
- journal_up_to_date = cache_header == ledger_hash
245
- ledger_key = self._build_ledger_cache_key(ledger_header)
246
-
247
- # Check if we have newest cached version of the ledger
248
- if journal_up_to_date and LEDGER_CACHE_ENABLED:
249
- ledger = cache.get(f"{ledger_key}-data", False)
250
- finished_entities = cache.get(f"{ledger_key}-finished_entities", False)
251
-
252
- if finished_entities is False or ledger is False:
253
- # Build the entries from the journal
254
- self.entries = {}
255
- for row in journal:
256
- self.entries.setdefault(row["pk"], []).append(row)
257
-
258
- ledger, finished_entities = self._process_accounts()
259
- self._process_remaining_entities(ledger, finished_entities)
260
- self._add_corporation_entity(ledger)
261
-
262
- # Finalize the billboard for the ledger.
263
- self.create_rattingbar(list(finished_entities))
264
- self.create_chord(ledger)
265
-
266
- context = self._build_context(ledger=ledger)
267
-
268
- # Create Cache
269
- cache.set(key=f"{ledger_key}-data", value=ledger, timeout=LEDGER_CACHE_STALE)
270
- cache.set(
271
- key=f"{ledger_key}-finished_entities",
272
- value=finished_entities,
273
- timeout=LEDGER_CACHE_STALE,
274
- )
275
- cache.set(
276
- key=self._get_ledger_header(
277
- self.corporation.corporation.corporation_id,
278
- self.year,
279
- self.month,
280
- self.day,
281
- ),
282
- value=ledger_hash,
283
- timeout=None, # Cache forever until the journal changes
284
- )
285
- return context
286
-
287
- def _process_accounts(self):
288
- """Process Auth Account information for the ledger."""
289
- ledger = []
290
- finished_entities = set()
291
- for account in self.auth_accounts:
292
- alts = account.user.character_ownerships.all()
293
- existing_alts = set(
294
- alts.values_list("character__character_id", flat=True)
295
- ).intersection(self.entities)
296
- alts = alts.filter(character__character_id__in=existing_alts)
297
- if not existing_alts:
298
- continue
299
- details_url = self.create_url(
300
- viewname="corporation_details",
301
- corporation_id=self.corporation.corporation.corporation_id,
302
- entity_id=account.main_character.character_id,
303
- )
304
- entity_obj = LedgerEntity(
305
- account.main_character.character_id,
306
- character_obj=account.main_character,
307
- details_url=details_url,
308
- )
309
- char_data = self.create_entity_data(
310
- entity=entity_obj,
311
- alts=alts,
312
- )
313
- if char_data is None:
314
- continue
315
- ledger.append(char_data)
316
- finished_entities.update(existing_alts)
317
- return ledger, finished_entities
318
-
319
- def _process_remaining_entities(self, ledger, finished_entities):
320
- """Process remaining entities for the ledger."""
321
- remaining_entities = self.entities - finished_entities
322
- if not remaining_entities:
323
- return
324
- for entity_id in remaining_entities:
325
- if entity_id in NPC_ENTITIES:
326
- continue
327
- if entity_id == self.corporation.corporation.corporation_id:
328
- continue
329
- details_url = self.create_url(
330
- viewname="corporation_details",
331
- corporation_id=self.corporation.corporation.corporation_id,
332
- entity_id=entity_id,
333
- )
334
- entity_obj = LedgerEntity(
335
- entity_id,
336
- details_url=details_url,
337
- )
338
- entity_data = self.create_entity_data(
339
- entity=entity_obj,
340
- )
341
- if entity_data is None:
342
- continue
343
- ledger.append(entity_data)
344
- finished_entities.add(entity_id)
345
-
346
- def _add_corporation_entity(self, ledger):
347
- """Add the corporation entity to the ledger."""
348
- corporation_entity = LedgerEntity(
349
- self.corporation.corporation.corporation_id,
350
- corporation_obj=self.corporation.corporation,
351
- details_url=self.create_url(
352
- viewname="corporation_details",
353
- corporation_id=self.corporation.corporation.corporation_id,
354
- entity_id=self.corporation.corporation.corporation_id,
355
- ),
356
- )
357
- corporation_data = self.create_entity_data(
358
- entity=corporation_entity,
359
- )
360
- if corporation_data is not None:
361
- ledger.append(corporation_data)
362
-
363
- def _build_context(self, ledger):
364
- """Build the context for the ledger view."""
365
- return {
366
- "title": f"Corporation Ledger - {self.corporation.corporation.corporation_name}",
367
- "corporation_id": self.corporation.corporation.corporation_id,
368
- "billboard": json.dumps(self.billboard.dict.asdict()),
369
- "ledger": ledger,
370
- "years": list(self.existing_years),
371
- "totals": self._calculate_totals(ledger),
372
- "view": self.create_view_data(
373
- viewname="corporation_details",
374
- corporation_id=self.corporation.corporation.corporation_id,
375
- entity_id=self.corporation.corporation.corporation_id,
376
- ),
377
- }
378
-
379
- def create_rattingbar(self, entities_ids: list = None):
380
- """Create the ratting bar for the view."""
381
- if not entities_ids:
382
- return
383
-
384
- rattingbar_timeline = self.billboard.create_timeline(self.journal)
385
- rattingbar = (
386
- rattingbar_timeline.annotate_bounty_income()
387
- .annotate_ess_income()
388
- .annotate_miscellaneous()
389
- )
390
- self.billboard.create_or_update_results(rattingbar)
391
- self._build_xy_chart(title=_("Ratting Bar"))
392
-
393
- def create_chord(self, ledger_data: list[dict]):
394
- """Create the chord chart for the view."""
395
- if not ledger_data:
396
- return
397
-
398
- for entry in ledger_data:
399
- entity_name = entry["entity"].entity_name
400
- ledger = entry["ledger"]
401
- self.billboard.chord_add_data(
402
- chord_from=entity_name,
403
- chord_to=_("Bounty (Wallet)"),
404
- value=ledger.get("bounty", 0),
405
- )
406
- self.billboard.chord_add_data(
407
- chord_from=entity_name,
408
- chord_to=_("ESS (Wallet)"),
409
- value=ledger.get("ess", 0),
410
- )
411
- self.billboard.chord_add_data(
412
- chord_from=entity_name,
413
- chord_to=_("Costs (Wallet)"),
414
- value=abs(ledger.get("costs", 0)),
415
- )
416
- self.billboard.chord_add_data(
417
- chord_from=entity_name,
418
- chord_to=_("Miscellaneous (Wallet)"),
419
- value=abs(ledger.get("miscellaneous", 0)),
420
- )
421
- self.billboard.chord_handle_overflow()
1
+ """PvE Views"""
2
+
3
+ # Standard Library
4
+ import json
5
+ from decimal import Decimal
6
+
7
+ # Django
8
+ from django.core.cache import cache
9
+ from django.core.handlers.wsgi import WSGIRequest
10
+ from django.db.models import DecimalField, Q, Sum
11
+ from django.utils.translation import gettext as _
12
+
13
+ # Alliance Auth
14
+ from allianceauth.eveonline.models import EveCharacter
15
+ from allianceauth.services.hooks import get_extension_logger
16
+
17
+ # Alliance Auth (External Libs)
18
+ from app_utils.logging import LoggerAddTag
19
+
20
+ # AA Ledger
21
+ from ledger import __title__
22
+ from ledger.app_settings import LEDGER_CACHE_ENABLED, LEDGER_CACHE_STALE
23
+ from ledger.constants import NPC_ENTITIES
24
+ from ledger.helpers.core import LedgerCore, LedgerEntity
25
+ from ledger.helpers.ref_type import RefTypeManager
26
+ from ledger.models.corporationaudit import (
27
+ CorporationAudit,
28
+ CorporationWalletDivision,
29
+ CorporationWalletJournalEntry,
30
+ )
31
+
32
+ logger = LoggerAddTag(get_extension_logger(__name__), __title__)
33
+
34
+
35
+ class CorporationData(LedgerCore):
36
+ """Class to hold character data for the ledger."""
37
+
38
+ # pylint: disable=too-many-positional-arguments
39
+ def __init__(
40
+ self,
41
+ request: WSGIRequest,
42
+ corporation: CorporationAudit,
43
+ division_id: int = None,
44
+ year: int = None,
45
+ month: int = None,
46
+ day: int = None,
47
+ ):
48
+ LedgerCore.__init__(self, year, month, day)
49
+ self.request = request
50
+ self.corporation = corporation
51
+ self.division_id = division_id
52
+ self.auth_char_ids = self.auth_character_ids
53
+
54
+ def setup_ledger(self, entity: LedgerEntity = None):
55
+ """Setup the Ledger Data for the Corporation."""
56
+ corporation_id = self.corporation.corporation.corporation_id
57
+
58
+ # Base queryset filtered by date and corporation division
59
+ base_qs = self._base_journal_queryset()
60
+
61
+ if entity is None:
62
+ # No entity specified: show all entries for the corporation (except self-transfers)
63
+ self.journal = base_qs.exclude(
64
+ first_party_id=corporation_id, second_party_id=corporation_id
65
+ )
66
+ # Prepare auxiliary data used by the view
67
+ self.existing_years = self._compute_existing_years()
68
+ self.entities = self._compute_entities()
69
+ return
70
+
71
+ # If the entity is the corporation itself and "all" is set, show all entries
72
+ if self.request.GET.get("all", False) and entity.entity_id == corporation_id:
73
+ self.journal = base_qs.exclude(
74
+ first_party_id=corporation_id, second_party_id=corporation_id
75
+ )
76
+ self.entities = self._compute_entities()
77
+ return
78
+
79
+ # Regular entity filtering: include any rows where the entity is a first or second party
80
+ character_ids = entity.get_alts_ids_or_self()
81
+ qs = base_qs.filter(
82
+ Q(first_party_id__in=character_ids) | Q(second_party_id__in=character_ids)
83
+ )
84
+ qs = qs.exclude(first_party_id=corporation_id, second_party_id=corporation_id)
85
+
86
+ # If the entity is the corporation itself, include NPC transactions too
87
+ if entity.entity_id == corporation_id:
88
+ qs = qs.filter(
89
+ Q(first_party_id__in=NPC_ENTITIES) | Q(second_party_id__in=NPC_ENTITIES)
90
+ )
91
+
92
+ # If entity represents a corporation or alliance, exclude auth account character IDs
93
+ # that are not part of the current entity to avoid double counting
94
+ if entity.type in ["alliance", "corporation"]:
95
+ exclude_ids = self.auth_char_ids - set(character_ids)
96
+ qs = qs.exclude(
97
+ Q(first_party_id__in=exclude_ids) | Q(second_party_id__in=exclude_ids)
98
+ )
99
+
100
+ self.journal = qs
101
+ self.entities = self._compute_entities()
102
+
103
+ def _base_journal_queryset(self):
104
+ """Return the base queryset filtered by the current date range and corporation division."""
105
+ if self.division_id is not None:
106
+ return CorporationWalletJournalEntry.objects.filter(
107
+ self.filter_date,
108
+ division__corporation=self.corporation,
109
+ division=self.division_id,
110
+ )
111
+ return CorporationWalletJournalEntry.objects.filter(
112
+ self.filter_date, division__corporation=self.corporation
113
+ )
114
+
115
+ def _compute_entities(self):
116
+ """Return a set of all entity IDs (first and second parties) present in the current journal."""
117
+ return set(self.journal.values_list("second_party_id", flat=True)) | set(
118
+ self.journal.values_list("first_party_id", flat=True)
119
+ )
120
+
121
+ def _compute_existing_years(self):
122
+ """Return the available years for journal entries for this corporation."""
123
+ return (
124
+ CorporationWalletJournalEntry.objects.filter(
125
+ division__corporation=self.corporation
126
+ )
127
+ .exclude(date__year__isnull=True)
128
+ .values_list("date__year", flat=True)
129
+ .order_by("-date__year")
130
+ .distinct()
131
+ )
132
+
133
+ def create_entity_data(
134
+ self,
135
+ entity: LedgerEntity,
136
+ alts: EveCharacter = None,
137
+ ) -> dict:
138
+ """Create the URL for entity details based on the view type."""
139
+ ids = (
140
+ list(alts.values_list("character__character_id", flat=True))
141
+ if alts is not None
142
+ else [entity.entity_id]
143
+ )
144
+
145
+ # Create Alts Dictionary
146
+ alts_dict = {}
147
+ if alts is not None:
148
+ for alt in alts:
149
+ alts_dict[alt.character.character_id] = alt.character.character_name
150
+
151
+ # Remove the main character from the alts dictionary only one entry
152
+ if len(alts_dict) == 1:
153
+ alts_dict.pop(entity.entity_id, None)
154
+
155
+ used_pks = set()
156
+ bounty = Decimal(0)
157
+ ess = Decimal(0)
158
+ miscellaneous = Decimal(0)
159
+ costs = Decimal(0)
160
+
161
+ for pk, rows in list(self.entries.items()):
162
+ for row in rows:
163
+ if row["first_party_id"] in ids or row["second_party_id"] in ids:
164
+ if RefTypeManager.special_cases(
165
+ row, ids=ids, account_char_ids=self.auth_char_ids
166
+ ):
167
+ continue
168
+ bounty += row.get("bounty") or Decimal(0)
169
+ ess += row.get("ess") or Decimal(0)
170
+ miscellaneous += row.get("miscellaneous") or Decimal(0)
171
+ costs += row.get("costs") or Decimal(0)
172
+ used_pks.add(pk)
173
+
174
+ # Remove Used Pks from Entries
175
+ # This is to prevent the entries from being used in the future
176
+ for pk in used_pks:
177
+ self.entries.pop(pk, None)
178
+
179
+ total = sum([bounty, ess, miscellaneous, costs])
180
+
181
+ if total == 0:
182
+ return None
183
+
184
+ char_data = {
185
+ "entity": entity,
186
+ "alts": alts_dict,
187
+ "ledger": {
188
+ "bounty": bounty,
189
+ "ess": ess,
190
+ "miscellaneous": miscellaneous,
191
+ "costs": costs,
192
+ "total": total,
193
+ },
194
+ "type": entity.type,
195
+ }
196
+
197
+ return char_data
198
+
199
+ def generate_ledger_data(self) -> dict:
200
+ """
201
+ Generate the ledger data for the corporation.
202
+
203
+ This method processes the journal entries, builds the ledger data,
204
+ and prepares the context for rendering the corporation ledger view.
205
+ """
206
+ ledger = False
207
+ finished_entities = False
208
+
209
+ self.setup_ledger()
210
+
211
+ journal = self.journal.values(
212
+ "first_party_id", "second_party_id", "pk", "ref_type"
213
+ ).annotate(
214
+ bounty=Sum(
215
+ "amount",
216
+ filter=Q(ref_type__in=RefTypeManager.BOUNTY_PRIZES),
217
+ output_field=DecimalField(),
218
+ ),
219
+ ess=Sum(
220
+ "amount",
221
+ filter=Q(ref_type__in=RefTypeManager.ESS_TRANSFER),
222
+ output_field=DecimalField(),
223
+ ),
224
+ costs=Sum(
225
+ "amount",
226
+ filter=Q(ref_type__in=RefTypeManager.all_ref_types(), amount__lt=0),
227
+ output_field=DecimalField(),
228
+ ),
229
+ miscellaneous=Sum(
230
+ "amount",
231
+ filter=Q(ref_type__in=RefTypeManager.all_ref_types(), amount__gt=0),
232
+ output_field=DecimalField(),
233
+ ),
234
+ )
235
+
236
+ # Get the journal hash and cache header
237
+ ledger_hash = self._get_ledger_journal_hash(self.journal.values_list("pk"))
238
+ ledger_header = self._get_ledger_header(
239
+ ledger_args=f"{self.corporation.corporation.corporation_id}_{self.division_id}",
240
+ year=self.year,
241
+ month=self.month,
242
+ day=self.day,
243
+ )
244
+ cache_header = cache.get(
245
+ ledger_header,
246
+ False,
247
+ )
248
+ logger.debug(
249
+ f"Ledger Header: {ledger_header}, Cache Header: {cache_header}, Journal Hash: {ledger_hash}"
250
+ )
251
+
252
+ # Check if the journal is up to date
253
+ journal_up_to_date = cache_header == ledger_hash
254
+ ledger_key = self._build_ledger_cache_key(ledger_header)
255
+
256
+ # Check if we have newest cached version of the ledger
257
+ if journal_up_to_date and LEDGER_CACHE_ENABLED:
258
+ ledger = cache.get(f"{ledger_key}-data", False)
259
+ finished_entities = cache.get(f"{ledger_key}-finished_entities", False)
260
+
261
+ if finished_entities is False or ledger is False:
262
+ # Build the entries from the journal
263
+ self.entries = {}
264
+ for row in journal:
265
+ self.entries.setdefault(row["pk"], []).append(row)
266
+
267
+ # Process Auth Accounts first
268
+ ledger, finished_entities = self._process_auth_accounts()
269
+ # Process remaining entities
270
+ self._process_remaining_entities(ledger, finished_entities)
271
+ # Process corporation entity last to ensure it's always included
272
+ self._handle_entity(
273
+ ledger=ledger,
274
+ entity_id=self.corporation.corporation.corporation_id,
275
+ corporation_obj=self.corporation.corporation,
276
+ )
277
+ # Finalize the billboard for the ledger.
278
+ self.create_rattingbar(list(finished_entities))
279
+ self.create_chord(ledger)
280
+
281
+ context = self._build_context(ledger=ledger)
282
+
283
+ # Create Cache
284
+ cache.set(key=f"{ledger_key}-data", value=ledger, timeout=LEDGER_CACHE_STALE)
285
+ cache.set(
286
+ key=f"{ledger_key}-finished_entities",
287
+ value=finished_entities,
288
+ timeout=LEDGER_CACHE_STALE,
289
+ )
290
+ cache.set(
291
+ key=self._get_ledger_header(
292
+ ledger_args=f"{self.corporation.corporation.corporation_id}_{self.division_id}",
293
+ year=self.year,
294
+ month=self.month,
295
+ day=self.day,
296
+ ),
297
+ value=ledger_hash,
298
+ timeout=None, # Cache forever until the journal changes
299
+ )
300
+ return context
301
+
302
+ def _build_view_data(self, entity_id: int):
303
+ details_kwargs = {
304
+ "viewname": "corporation_details",
305
+ "corporation_id": self.corporation.corporation.corporation_id,
306
+ "entity_id": entity_id,
307
+ }
308
+ if self.division_id is not None:
309
+ details_kwargs["division_id"] = self.division_id
310
+ return details_kwargs
311
+
312
+ def _build_view_url(self, entity_id: int):
313
+ """Return the full URL for a corporation view for the given entity id.
314
+
315
+ This wraps create_url(**self._build_view_data(...)) so callers can
316
+ request the URL in a single, readable call without losing ordering.
317
+ """
318
+ return self.create_url(**self._build_view_data(entity_id=entity_id))
319
+
320
+ # pylint: disable=too-many-arguments
321
+ def _handle_entity(
322
+ self,
323
+ ledger: list,
324
+ entity_id: int,
325
+ character_obj=None,
326
+ corporation_obj=None,
327
+ alts=None,
328
+ add_finished: bool = True,
329
+ finished_ids=None,
330
+ ) -> set:
331
+ """Create entity object, add to ledger if it has data and return IDs to mark finished.
332
+
333
+ - ledger: list to append to
334
+ - entity_id: numeric id
335
+ - character_obj / corporation_obj: optional objects to attach to LedgerEntity
336
+ - alts: optional alts queryset passed to create_entity_data
337
+ - add_finished: whether to return ids that should be added to finished_entities
338
+ - finished_ids: explicit IDs (set or iterable) to mark finished (used for accounts)
339
+ """
340
+ details_url = self._build_view_url(entity_id)
341
+ entity_obj = LedgerEntity(
342
+ entity_id,
343
+ character_obj=character_obj,
344
+ corporation_obj=corporation_obj,
345
+ details_url=details_url,
346
+ )
347
+ char_data = self.create_entity_data(entity=entity_obj, alts=alts)
348
+ if char_data is None:
349
+ return set()
350
+ ledger.append(char_data)
351
+ if not add_finished:
352
+ return set()
353
+ if finished_ids is not None:
354
+ return set(finished_ids)
355
+ return {entity_id}
356
+
357
+ def _process_auth_accounts(self):
358
+ """Process Auth Account information for the ledger."""
359
+ ledger = []
360
+ finished_entities = set()
361
+ for account in self.auth_accounts:
362
+ alts = account.user.character_ownerships.all()
363
+ existing_alts = set(
364
+ alts.values_list("character__character_id", flat=True)
365
+ ).intersection(self.entities)
366
+ alts = alts.filter(character__character_id__in=existing_alts)
367
+ if not existing_alts:
368
+ continue
369
+ finished_entities.update(
370
+ self._handle_entity(
371
+ ledger,
372
+ account.main_character.character_id,
373
+ character_obj=account.main_character,
374
+ alts=alts,
375
+ finished_ids=existing_alts,
376
+ )
377
+ )
378
+ return ledger, finished_entities
379
+
380
+ def _process_remaining_entities(self, ledger, finished_entities: set):
381
+ """Process remaining entities for the ledger."""
382
+ remaining_entities = self.entities - finished_entities
383
+ if not remaining_entities:
384
+ return
385
+ for entity_id in remaining_entities:
386
+ if entity_id in NPC_ENTITIES:
387
+ continue
388
+ if entity_id == self.corporation.corporation.corporation_id:
389
+ continue
390
+ finished_entities.update(self._handle_entity(ledger, entity_id))
391
+
392
+ def _build_context(self, ledger):
393
+ """Build the context for the ledger view."""
394
+ view = self._build_view_data(
395
+ entity_id=self.corporation.corporation.corporation_id
396
+ )
397
+
398
+ context = {
399
+ "title": f"Corporation Ledger - {self.corporation.corporation.corporation_name}",
400
+ "corporation_id": self.corporation.corporation.corporation_id,
401
+ "division_id": self.division_id,
402
+ "billboard": json.dumps(self.billboard.dict.asdict()),
403
+ "ledger": ledger,
404
+ "divisions": CorporationWalletDivision.objects.filter(
405
+ corporation=self.corporation
406
+ ).order_by("division_id"),
407
+ "years": list(self.existing_years),
408
+ "totals": self._calculate_totals(ledger),
409
+ "view": self.create_view_data(**view),
410
+ }
411
+ return context
412
+
413
+ def create_rattingbar(self, entities_ids: list = None):
414
+ """Create the ratting bar for the view."""
415
+ if not entities_ids:
416
+ return
417
+
418
+ rattingbar_timeline = self.billboard.create_timeline(self.journal)
419
+ rattingbar = (
420
+ rattingbar_timeline.annotate_bounty_income()
421
+ .annotate_ess_income()
422
+ .annotate_miscellaneous()
423
+ )
424
+ self.billboard.create_or_update_results(rattingbar)
425
+ self._build_xy_chart(title=_("Ratting Bar"))
426
+
427
+ def create_chord(self, ledger_data: list[dict]):
428
+ """Create the chord chart for the view."""
429
+ if not ledger_data:
430
+ return
431
+
432
+ for entry in ledger_data:
433
+ entity_name = entry["entity"].entity_name
434
+ ledger = entry["ledger"]
435
+ self.billboard.chord_add_data(
436
+ chord_from=entity_name,
437
+ chord_to=_("Bounty (Wallet)"),
438
+ value=ledger.get("bounty", 0),
439
+ )
440
+ self.billboard.chord_add_data(
441
+ chord_from=entity_name,
442
+ chord_to=_("ESS (Wallet)"),
443
+ value=ledger.get("ess", 0),
444
+ )
445
+ self.billboard.chord_add_data(
446
+ chord_from=entity_name,
447
+ chord_to=_("Costs (Wallet)"),
448
+ value=abs(ledger.get("costs", 0)),
449
+ )
450
+ self.billboard.chord_add_data(
451
+ chord_from=entity_name,
452
+ chord_to=_("Miscellaneous (Wallet)"),
453
+ value=abs(ledger.get("miscellaneous", 0)),
454
+ )
455
+ self.billboard.chord_handle_overflow()