aa-shop 0.1.1__tar.gz

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 (75) hide show
  1. aa_shop-0.1.1/LICENSE +21 -0
  2. aa_shop-0.1.1/PKG-INFO +145 -0
  3. aa_shop-0.1.1/README.md +110 -0
  4. aa_shop-0.1.1/pyproject.toml +47 -0
  5. aa_shop-0.1.1/shop/__init__.py +3 -0
  6. aa_shop-0.1.1/shop/admin.py +12 -0
  7. aa_shop-0.1.1/shop/apps.py +7 -0
  8. aa_shop-0.1.1/shop/auth_hooks.py +48 -0
  9. aa_shop-0.1.1/shop/formatting.py +23 -0
  10. aa_shop-0.1.1/shop/forms.py +156 -0
  11. aa_shop-0.1.1/shop/management/__init__.py +0 -0
  12. aa_shop-0.1.1/shop/management/commands/__init__.py +0 -0
  13. aa_shop-0.1.1/shop/management/commands/shop_refresh_prices.py +45 -0
  14. aa_shop-0.1.1/shop/management/commands/shop_refresh_stock.py +42 -0
  15. aa_shop-0.1.1/shop/markdown.py +30 -0
  16. aa_shop-0.1.1/shop/market.py +72 -0
  17. aa_shop-0.1.1/shop/migrations/0001_initial.py +25 -0
  18. aa_shop-0.1.1/shop/migrations/0002_alter_general_options_shop.py +36 -0
  19. aa_shop-0.1.1/shop/migrations/0003_shop_enabled_sections.py +18 -0
  20. aa_shop-0.1.1/shop/migrations/0004_hulllisting_hulllistingsource_and_more.py +41 -0
  21. aa_shop-0.1.1/shop/migrations/0005_shopsource_delete_hulllistingsource.py +29 -0
  22. aa_shop-0.1.1/shop/migrations/0006_hulllisting_price_mode_hulllisting_price_pct_and_more.py +28 -0
  23. aa_shop-0.1.1/shop/migrations/0007_shopsource_price_mode_shopsource_price_pct.py +23 -0
  24. aa_shop-0.1.1/shop/migrations/0008_shopsource_container_name.py +18 -0
  25. aa_shop-0.1.1/shop/migrations/0009_hulllisting_persist.py +18 -0
  26. aa_shop-0.1.1/shop/migrations/0010_hulllisting_featured.py +18 -0
  27. aa_shop-0.1.1/shop/migrations/0011_shop_description.py +18 -0
  28. aa_shop-0.1.1/shop/migrations/0012_hulllisting_description.py +18 -0
  29. aa_shop-0.1.1/shop/migrations/0013_hulllisting_featured_at.py +18 -0
  30. aa_shop-0.1.1/shop/migrations/__init__.py +0 -0
  31. aa_shop-0.1.1/shop/models.py +210 -0
  32. aa_shop-0.1.1/shop/permissions.py +47 -0
  33. aa_shop-0.1.1/shop/sources.py +60 -0
  34. aa_shop-0.1.1/shop/static/shop/backoffice.css +66 -0
  35. aa_shop-0.1.1/shop/static/shop/storefront.css +2 -0
  36. aa_shop-0.1.1/shop/stock.py +47 -0
  37. aa_shop-0.1.1/shop/tasks.py +107 -0
  38. aa_shop-0.1.1/shop/templates/shop/_hull_listings.html +53 -0
  39. aa_shop-0.1.1/shop/templates/shop/_shop_table.html +46 -0
  40. aa_shop-0.1.1/shop/templates/shop/_source_picker.html +73 -0
  41. aa_shop-0.1.1/shop/templates/shop/base.html +8 -0
  42. aa_shop-0.1.1/shop/templates/shop/index.html +17 -0
  43. aa_shop-0.1.1/shop/templates/shop/manage.html +359 -0
  44. aa_shop-0.1.1/shop/templates/shop/shop_confirm_delete.html +12 -0
  45. aa_shop-0.1.1/shop/templates/shop/shop_form.html +23 -0
  46. aa_shop-0.1.1/shop/templates/storefront/_hulls_featured.html +32 -0
  47. aa_shop-0.1.1/shop/templates/storefront/_hulls_grid.html +32 -0
  48. aa_shop-0.1.1/shop/templates/storefront/_hulls_table.html +42 -0
  49. aa_shop-0.1.1/shop/templates/storefront/base.html +33 -0
  50. aa_shop-0.1.1/shop/templates/storefront/detail.html +71 -0
  51. aa_shop-0.1.1/shop/templates/storefront/item_detail.html +54 -0
  52. aa_shop-0.1.1/shop/tests/__init__.py +0 -0
  53. aa_shop-0.1.1/shop/tests/test_commands.py +68 -0
  54. aa_shop-0.1.1/shop/tests/test_formatting.py +24 -0
  55. aa_shop-0.1.1/shop/tests/test_forms.py +68 -0
  56. aa_shop-0.1.1/shop/tests/test_markdown.py +66 -0
  57. aa_shop-0.1.1/shop/tests/test_market.py +39 -0
  58. aa_shop-0.1.1/shop/tests/test_models.py +38 -0
  59. aa_shop-0.1.1/shop/tests/test_models_hulls.py +46 -0
  60. aa_shop-0.1.1/shop/tests/test_permissions.py +77 -0
  61. aa_shop-0.1.1/shop/tests/test_pricing.py +235 -0
  62. aa_shop-0.1.1/shop/tests/test_smoke.py +7 -0
  63. aa_shop-0.1.1/shop/tests/test_sources.py +57 -0
  64. aa_shop-0.1.1/shop/tests/test_stock.py +118 -0
  65. aa_shop-0.1.1/shop/tests/test_tasks.py +116 -0
  66. aa_shop-0.1.1/shop/tests/test_views_backoffice.py +99 -0
  67. aa_shop-0.1.1/shop/tests/test_views_manage.py +410 -0
  68. aa_shop-0.1.1/shop/tests/test_views_storefront.py +49 -0
  69. aa_shop-0.1.1/shop/tests/test_views_storefront_hulls.py +143 -0
  70. aa_shop-0.1.1/shop/urls/__init__.py +0 -0
  71. aa_shop-0.1.1/shop/urls/backoffice.py +22 -0
  72. aa_shop-0.1.1/shop/urls/storefront.py +12 -0
  73. aa_shop-0.1.1/shop/views/__init__.py +0 -0
  74. aa_shop-0.1.1/shop/views/backoffice.py +251 -0
  75. aa_shop-0.1.1/shop/views/storefront.py +52 -0
aa_shop-0.1.1/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Daniel Onisoru
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
aa_shop-0.1.1/PKG-INFO ADDED
@@ -0,0 +1,145 @@
1
+ Metadata-Version: 2.4
2
+ Name: aa-shop
3
+ Version: 0.1.1
4
+ Summary: A public asset shop plugin for Alliance Auth.
5
+ Keywords: allianceauth,eveonline,django
6
+ Author-email: Daniel Onisoru <daniel.onisoru@gmail.com>
7
+ Requires-Python: >=3.10
8
+ Description-Content-Type: text/markdown
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Environment :: Web Environment
11
+ Classifier: Framework :: Django
12
+ Classifier: Framework :: Django :: 4.2
13
+ Classifier: Framework :: Django :: 5.2
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Intended Audience :: End Users/Desktop
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
23
+ License-File: LICENSE
24
+ Requires-Dist: allianceauth>=5,<6
25
+ Requires-Dist: aa-memberaudit>=5
26
+ Requires-Dist: markdown
27
+ Requires-Dist: coverage ; extra == "test"
28
+ Requires-Dist: factory_boy ; extra == "test"
29
+ Project-URL: Changelog, https://gitlab.com/daniel.onisoru/aa-shop/-/blob/master/CHANGELOG.md
30
+ Project-URL: Homepage, https://gitlab.com/daniel.onisoru/aa-shop
31
+ Project-URL: Issues, https://gitlab.com/daniel.onisoru/aa-shop/-/issues
32
+ Project-URL: Source, https://gitlab.com/daniel.onisoru/aa-shop
33
+ Provides-Extra: test
34
+
35
+ # aa-shop
36
+
37
+ An [Alliance Auth](https://gitlab.com/allianceauth/allianceauth) plugin that lets registered
38
+ auth members run a publicly available shop stocked from their own (or their corporation's)
39
+ assets.
40
+
41
+ ## Features
42
+
43
+ - Members create and manage **personal** shops; authorized users manage **corporation** shops.
44
+ - A member **back-office** at `/shop-backoffice/` (lists your shops, create/edit/delete).
45
+ - Each published shop gets a **public storefront** at `/shop/<slug>/` — no login required.
46
+ Unpublished shops are visible only to their managers (preview).
47
+
48
+ ## Requirements
49
+
50
+ - Alliance Auth >= 5
51
+ - [aa-memberaudit](https://pypi.org/project/aa-memberaudit/) >= 5, installed and syncing
52
+
53
+ ## Installation
54
+
55
+ 1. Install into your Alliance Auth virtual environment:
56
+
57
+ ```bash
58
+ pip install aa-shop
59
+ ```
60
+
61
+ 2. Add `"shop"` to `INSTALLED_APPS` in your auth project's `local.py`:
62
+
63
+ ```python
64
+ INSTALLED_APPS += ["shop"]
65
+ ```
66
+
67
+ 3. Allow the storefront to expose its public (non-gated) views — **required**, or anonymous
68
+ visitors are redirected to login:
69
+
70
+ ```python
71
+ APPS_WITH_PUBLIC_VIEWS = ["storefront"]
72
+ ```
73
+
74
+ 4. Run migrations and collect static files, then restart your AA services:
75
+
76
+ ```bash
77
+ python manage.py migrate
78
+ python manage.py collectstatic
79
+ ```
80
+
81
+ 5. **EVE universe data:** the Hulls picker lists ship types from
82
+ [django-eveuniverse](https://gitlab.com/ErikKalkoken/django-eveuniverse), so make sure
83
+ your eveuniverse data is populated with ship types (load the full EVE universe, or at
84
+ least the Ship category, using eveuniverse's load tooling). Ships that aren't loaded
85
+ won't be selectable.
86
+
87
+ 6. Grant permissions (Django admin, or via groups/states):
88
+
89
+ | Permission | Grants |
90
+ |---|---|
91
+ | `shop.basic_access` | Access the back-office; create/manage your personal shops |
92
+ | `shop.manage_corporation_shops` | Create/manage your corporation's shops |
93
+
94
+ ### Periodic price refresh
95
+
96
+ Hulls priced as **% Jita Buy** / **% Jita Sell** are refreshed by a Celery task
97
+ every 6 hours. Add to your `local.py`:
98
+
99
+ ```python
100
+ from celery.schedules import crontab
101
+
102
+ CELERYBEAT_SCHEDULE["shop_refresh_pct_prices"] = {
103
+ "task": "shop.tasks.refresh_pct_prices",
104
+ "schedule": crontab(minute=0, hour="*/6"),
105
+ }
106
+ CELERYBEAT_SCHEDULE["shop_refresh_all_stock"] = {
107
+ "task": "shop.tasks.refresh_all_stock",
108
+ "schedule": crontab(minute=0), # hourly
109
+ }
110
+ ```
111
+
112
+ Stock is recomputed from memberaudit assets (no ESI). Refresh on demand with
113
+ `python manage.py shop_refresh_stock` (`--async` to enqueue the per-shop tasks).
114
+
115
+ The task uses the public ESI market endpoint (no token) at low priority
116
+ (override with `SHOP_TASKS_PRIORITY`, 1=urgent .. 9=idle; default 7). Prices are
117
+ best-of-book at Jita 4-4 (highest buy / lowest sell), the same as Janice.
118
+
119
+ To refresh on demand (e.g. after setting up %-pricing), run inline:
120
+
121
+ ```bash
122
+ python manage.py shop_refresh_prices # synchronous, prints results
123
+ python manage.py shop_refresh_prices --async # enqueue Celery tasks instead
124
+ ```
125
+
126
+ ## Usage
127
+
128
+ - **Members:** open **Shop** in the auth sidebar (`/shop-backoffice/`) to create a shop —
129
+ set a slug, name, type (personal/corporation), and publish it.
130
+ - **Public:** share `/shop/<slug>/`; published shops are browsable by anyone.
131
+
132
+ ## Contributing
133
+
134
+ Design docs and implementation plans live in `docs/superpowers/`. The storefront stylesheet
135
+ ships compiled (`shop/static/shop/storefront.css`); to rebuild it after changing storefront
136
+ templates, use the standalone Tailwind v4 CLI + daisyUI (no Node required):
137
+
138
+ ```bash
139
+ bash scripts/build-css.sh # add --watch during development
140
+ ```
141
+
142
+ ## License
143
+
144
+ MIT
145
+
@@ -0,0 +1,110 @@
1
+ # aa-shop
2
+
3
+ An [Alliance Auth](https://gitlab.com/allianceauth/allianceauth) plugin that lets registered
4
+ auth members run a publicly available shop stocked from their own (or their corporation's)
5
+ assets.
6
+
7
+ ## Features
8
+
9
+ - Members create and manage **personal** shops; authorized users manage **corporation** shops.
10
+ - A member **back-office** at `/shop-backoffice/` (lists your shops, create/edit/delete).
11
+ - Each published shop gets a **public storefront** at `/shop/<slug>/` — no login required.
12
+ Unpublished shops are visible only to their managers (preview).
13
+
14
+ ## Requirements
15
+
16
+ - Alliance Auth >= 5
17
+ - [aa-memberaudit](https://pypi.org/project/aa-memberaudit/) >= 5, installed and syncing
18
+
19
+ ## Installation
20
+
21
+ 1. Install into your Alliance Auth virtual environment:
22
+
23
+ ```bash
24
+ pip install aa-shop
25
+ ```
26
+
27
+ 2. Add `"shop"` to `INSTALLED_APPS` in your auth project's `local.py`:
28
+
29
+ ```python
30
+ INSTALLED_APPS += ["shop"]
31
+ ```
32
+
33
+ 3. Allow the storefront to expose its public (non-gated) views — **required**, or anonymous
34
+ visitors are redirected to login:
35
+
36
+ ```python
37
+ APPS_WITH_PUBLIC_VIEWS = ["storefront"]
38
+ ```
39
+
40
+ 4. Run migrations and collect static files, then restart your AA services:
41
+
42
+ ```bash
43
+ python manage.py migrate
44
+ python manage.py collectstatic
45
+ ```
46
+
47
+ 5. **EVE universe data:** the Hulls picker lists ship types from
48
+ [django-eveuniverse](https://gitlab.com/ErikKalkoken/django-eveuniverse), so make sure
49
+ your eveuniverse data is populated with ship types (load the full EVE universe, or at
50
+ least the Ship category, using eveuniverse's load tooling). Ships that aren't loaded
51
+ won't be selectable.
52
+
53
+ 6. Grant permissions (Django admin, or via groups/states):
54
+
55
+ | Permission | Grants |
56
+ |---|---|
57
+ | `shop.basic_access` | Access the back-office; create/manage your personal shops |
58
+ | `shop.manage_corporation_shops` | Create/manage your corporation's shops |
59
+
60
+ ### Periodic price refresh
61
+
62
+ Hulls priced as **% Jita Buy** / **% Jita Sell** are refreshed by a Celery task
63
+ every 6 hours. Add to your `local.py`:
64
+
65
+ ```python
66
+ from celery.schedules import crontab
67
+
68
+ CELERYBEAT_SCHEDULE["shop_refresh_pct_prices"] = {
69
+ "task": "shop.tasks.refresh_pct_prices",
70
+ "schedule": crontab(minute=0, hour="*/6"),
71
+ }
72
+ CELERYBEAT_SCHEDULE["shop_refresh_all_stock"] = {
73
+ "task": "shop.tasks.refresh_all_stock",
74
+ "schedule": crontab(minute=0), # hourly
75
+ }
76
+ ```
77
+
78
+ Stock is recomputed from memberaudit assets (no ESI). Refresh on demand with
79
+ `python manage.py shop_refresh_stock` (`--async` to enqueue the per-shop tasks).
80
+
81
+ The task uses the public ESI market endpoint (no token) at low priority
82
+ (override with `SHOP_TASKS_PRIORITY`, 1=urgent .. 9=idle; default 7). Prices are
83
+ best-of-book at Jita 4-4 (highest buy / lowest sell), the same as Janice.
84
+
85
+ To refresh on demand (e.g. after setting up %-pricing), run inline:
86
+
87
+ ```bash
88
+ python manage.py shop_refresh_prices # synchronous, prints results
89
+ python manage.py shop_refresh_prices --async # enqueue Celery tasks instead
90
+ ```
91
+
92
+ ## Usage
93
+
94
+ - **Members:** open **Shop** in the auth sidebar (`/shop-backoffice/`) to create a shop —
95
+ set a slug, name, type (personal/corporation), and publish it.
96
+ - **Public:** share `/shop/<slug>/`; published shops are browsable by anyone.
97
+
98
+ ## Contributing
99
+
100
+ Design docs and implementation plans live in `docs/superpowers/`. The storefront stylesheet
101
+ ships compiled (`shop/static/shop/storefront.css`); to rebuild it after changing storefront
102
+ templates, use the standalone Tailwind v4 CLI + daisyUI (no Node required):
103
+
104
+ ```bash
105
+ bash scripts/build-css.sh # add --watch during development
106
+ ```
107
+
108
+ ## License
109
+
110
+ MIT
@@ -0,0 +1,47 @@
1
+ [build-system]
2
+ requires = ["flit_core >=3.2,<4"]
3
+ build-backend = "flit_core.buildapi"
4
+
5
+ [project]
6
+ name = "aa-shop"
7
+ dynamic = ["version", "description"]
8
+ readme = "README.md"
9
+ license = {file = "LICENSE"}
10
+ requires-python = ">=3.10"
11
+ authors = [
12
+ { name = "Daniel Onisoru", email = "daniel.onisoru@gmail.com" },
13
+ ]
14
+ keywords = ["allianceauth", "eveonline", "django"]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Environment :: Web Environment",
18
+ "Framework :: Django",
19
+ "Framework :: Django :: 4.2",
20
+ "Framework :: Django :: 5.2",
21
+ "Intended Audience :: Developers",
22
+ "Intended Audience :: End Users/Desktop",
23
+ "License :: OSI Approved :: MIT License",
24
+ "Operating System :: OS Independent",
25
+ "Programming Language :: Python :: 3",
26
+ "Programming Language :: Python :: 3.10",
27
+ "Programming Language :: Python :: 3.11",
28
+ "Programming Language :: Python :: 3.12",
29
+ "Topic :: Internet :: WWW/HTTP :: Dynamic Content",
30
+ ]
31
+ dependencies = [
32
+ "allianceauth>=5,<6",
33
+ "aa-memberaudit>=5",
34
+ "markdown",
35
+ ]
36
+
37
+ [project.optional-dependencies]
38
+ test = ["coverage", "factory_boy"]
39
+
40
+ [project.urls]
41
+ Homepage = "https://gitlab.com/daniel.onisoru/aa-shop"
42
+ Source = "https://gitlab.com/daniel.onisoru/aa-shop"
43
+ Issues = "https://gitlab.com/daniel.onisoru/aa-shop/-/issues"
44
+ Changelog = "https://gitlab.com/daniel.onisoru/aa-shop/-/blob/master/CHANGELOG.md"
45
+
46
+ [tool.flit.module]
47
+ name = "shop"
@@ -0,0 +1,3 @@
1
+ """A public asset shop plugin for Alliance Auth."""
2
+
3
+ __version__ = "0.1.1"
@@ -0,0 +1,12 @@
1
+ """Admin registrations."""
2
+
3
+ from django.contrib import admin
4
+
5
+ from .models import Shop
6
+
7
+
8
+ @admin.register(Shop)
9
+ class ShopAdmin(admin.ModelAdmin):
10
+ list_display = ("name", "slug", "shop_type", "is_published", "corporation", "created_by")
11
+ list_filter = ("shop_type", "is_published")
12
+ search_fields = ("name", "slug")
@@ -0,0 +1,7 @@
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class ShopConfig(AppConfig):
5
+ name = "shop"
6
+ label = "shop"
7
+ verbose_name = "Shop"
@@ -0,0 +1,48 @@
1
+ from django.utils.translation import gettext_lazy as _
2
+
3
+ from allianceauth import hooks
4
+ from allianceauth.services.hooks import MenuItemHook, UrlHook
5
+
6
+ from .urls import backoffice as backoffice_urls
7
+ from .urls import storefront as storefront_urls
8
+
9
+
10
+ class ShopMenuItem(MenuItemHook):
11
+ """Sidebar entry into the back-office for authorized users."""
12
+
13
+ def __init__(self):
14
+ MenuItemHook.__init__(
15
+ self,
16
+ _("Shop"),
17
+ "fas fa-store fa-fw",
18
+ "backoffice:index",
19
+ navactive=["backoffice:"],
20
+ )
21
+
22
+ def render(self, request):
23
+ if request.user.has_perm("shop.basic_access"):
24
+ return MenuItemHook.render(self, request)
25
+ return ""
26
+
27
+
28
+ @hooks.register("menu_item_hook")
29
+ def register_menu():
30
+ return ShopMenuItem()
31
+
32
+
33
+ @hooks.register("url_hook")
34
+ def register_backoffice_urls():
35
+ return UrlHook(backoffice_urls, "backoffice", r"^shop-backoffice/")
36
+
37
+
38
+ @hooks.register("url_hook")
39
+ def register_storefront_urls():
40
+ return UrlHook(
41
+ storefront_urls,
42
+ "storefront",
43
+ r"^shop/",
44
+ excluded_views=[
45
+ "shop.views.storefront.detail",
46
+ "shop.views.storefront.item_detail",
47
+ ],
48
+ )
@@ -0,0 +1,23 @@
1
+ """Number formatting helpers."""
2
+
3
+
4
+ def format_isk(amount) -> str:
5
+ """Abbreviate an ISK amount EVE-style: K / M / B / T.
6
+
7
+ Up to 2 decimals, trailing zeros stripped. Examples:
8
+ 23_000_000 -> "23M ISK"
9
+ 1_500_000_000 -> "1.5B ISK"
10
+ 149_000_000_000 -> "149B ISK"
11
+ 750 -> "750 ISK"
12
+ """
13
+ amount = int(amount)
14
+ for unit, suffix in (
15
+ (1_000_000_000_000, "T"),
16
+ (1_000_000_000, "B"),
17
+ (1_000_000, "M"),
18
+ (1_000, "K"),
19
+ ):
20
+ if amount >= unit:
21
+ value = f"{amount / unit:.2f}".rstrip("0").rstrip(".")
22
+ return f"{value}{suffix} ISK"
23
+ return f"{amount:,} ISK"
@@ -0,0 +1,156 @@
1
+ """Forms."""
2
+
3
+ from django import forms
4
+
5
+ from allianceauth.eveonline.models import EveCorporationInfo
6
+ from eveuniverse.models import EveType
7
+
8
+ from .models import (
9
+ FIXED_PRICE, FIXED_STOCK, PCT_MODES, PRICE_MODE_CHOICES, SECTION_CHOICES,
10
+ STOCK_MODE_CHOICES, Shop,
11
+ )
12
+ from .sources import owner_characters
13
+
14
+ SHIP_CATEGORY_ID = 6
15
+
16
+
17
+ def ship_type_queryset():
18
+ return EveType.objects.filter(
19
+ eve_group__eve_category_id=SHIP_CATEGORY_ID, published=True
20
+ ).order_by("name")
21
+
22
+
23
+ def _resolve_corp(corporation_id):
24
+ try:
25
+ return EveCorporationInfo.objects.get(corporation_id=corporation_id)
26
+ except EveCorporationInfo.DoesNotExist:
27
+ return EveCorporationInfo.objects.create_corporation(corporation_id)
28
+
29
+
30
+ class ShopForm(forms.ModelForm):
31
+ enabled_sections = forms.MultipleChoiceField(
32
+ choices=SECTION_CHOICES,
33
+ required=False,
34
+ widget=forms.CheckboxSelectMultiple,
35
+ label="Sections",
36
+ )
37
+
38
+ class Meta:
39
+ model = Shop
40
+ fields = ["name", "slug", "shop_type", "is_published", "enabled_sections", "description"]
41
+ widgets = {
42
+ "description": forms.Textarea(attrs={"rows": 6}),
43
+ }
44
+ help_texts = {
45
+ "description": "Markdown supported — bold, italic, headings, lists, links.",
46
+ }
47
+
48
+ def __init__(self, *args, user=None, **kwargs):
49
+ self.user = user
50
+ super().__init__(*args, **kwargs)
51
+ # shop_type is fixed after creation
52
+ if self.instance and self.instance.pk:
53
+ self.fields["shop_type"].disabled = True
54
+
55
+ def clean_shop_type(self):
56
+ shop_type = self.cleaned_data["shop_type"]
57
+ if shop_type == Shop.ShopType.CORPORATION:
58
+ if not (self.user and self.user.has_perm("shop.manage_corporation_shops")):
59
+ raise forms.ValidationError(
60
+ "You do not have permission to create corporation shops."
61
+ )
62
+ main = getattr(getattr(self.user, "profile", None), "main_character", None)
63
+ if main is None or not main.corporation_id:
64
+ raise forms.ValidationError(
65
+ "Your main character has no corporation set."
66
+ )
67
+ return shop_type
68
+
69
+ def save(self, commit=True):
70
+ creating = self.instance.pk is None
71
+ shop = super().save(commit=False)
72
+ if creating:
73
+ shop.created_by = self.user
74
+ if shop.shop_type == Shop.ShopType.CORPORATION:
75
+ corp_id = self.user.profile.main_character.corporation_id
76
+ shop.corporation = _resolve_corp(corp_id)
77
+ if commit:
78
+ shop.save()
79
+ return shop
80
+
81
+
82
+ class AddStockroomForm(forms.Form):
83
+ character_id = forms.IntegerField()
84
+ location_id = forms.IntegerField()
85
+ container_item_id = forms.IntegerField(required=False)
86
+ price_mode = forms.ChoiceField(choices=PRICE_MODE_CHOICES)
87
+ price_pct = forms.DecimalField(
88
+ min_value=0, max_digits=5, decimal_places=2, required=False
89
+ )
90
+
91
+ def __init__(self, *args, owner=None, **kwargs):
92
+ self.owner = owner
93
+ super().__init__(*args, **kwargs)
94
+
95
+ def clean_character_id(self):
96
+ cid = self.cleaned_data["character_id"]
97
+ if not owner_characters(self.owner).filter(id=cid).exists():
98
+ raise forms.ValidationError("Not one of your characters.")
99
+ return cid
100
+
101
+ def clean(self):
102
+ cleaned = super().clean()
103
+ if cleaned.get("price_mode") in PCT_MODES and not cleaned.get("price_pct"):
104
+ self.add_error("price_pct", "Enter a percentage for %-based pricing.")
105
+ return cleaned
106
+
107
+
108
+ class EditStockroomForm(forms.Form):
109
+ price_mode = forms.ChoiceField(choices=PRICE_MODE_CHOICES)
110
+ price_pct = forms.DecimalField(
111
+ min_value=0, max_digits=5, decimal_places=2, required=False
112
+ )
113
+
114
+ def clean(self):
115
+ cleaned = super().clean()
116
+ if cleaned.get("price_mode") in PCT_MODES and not cleaned.get("price_pct"):
117
+ self.add_error("price_pct", "Enter a percentage for %-based pricing.")
118
+ return cleaned
119
+
120
+
121
+ class HullModeFieldsMixin(forms.Form):
122
+ stock_mode = forms.ChoiceField(choices=STOCK_MODE_CHOICES)
123
+ stock = forms.IntegerField(min_value=0, required=False)
124
+ price_mode = forms.ChoiceField(choices=PRICE_MODE_CHOICES)
125
+ price = forms.IntegerField(min_value=0, required=False) # ISK, fixed mode
126
+ price_pct = forms.DecimalField(
127
+ min_value=0, max_digits=5, decimal_places=2, required=False
128
+ )
129
+ persist = forms.BooleanField(required=False) # keep listing at 0 stock
130
+ description = forms.CharField(
131
+ required=False, widget=forms.Textarea(attrs={"rows": 4}),
132
+ help_text="Markdown supported.",
133
+ )
134
+
135
+ def clean(self):
136
+ cleaned = super().clean()
137
+ if cleaned.get("stock_mode") == FIXED_STOCK and cleaned.get("stock") is None:
138
+ self.add_error("stock", "Enter a stock quantity for fixed stock.")
139
+ if cleaned.get("price_mode") == FIXED_PRICE and cleaned.get("price") is None:
140
+ self.add_error("price", "Enter a price (0 = Ask) for fixed price.")
141
+ if cleaned.get("price_mode") in PCT_MODES and not cleaned.get("price_pct"):
142
+ self.add_error("price_pct", "Enter a percentage for %-based pricing.")
143
+ return cleaned
144
+
145
+
146
+ class EditHullForm(HullModeFieldsMixin):
147
+ pass
148
+
149
+
150
+ class AddHullForm(HullModeFieldsMixin):
151
+ eve_type = forms.ModelChoiceField(queryset=EveType.objects.none(), label="Ship")
152
+
153
+ def __init__(self, *args, **kwargs):
154
+ super().__init__(*args, **kwargs)
155
+ self.fields["eve_type"].queryset = ship_type_queryset()
156
+ self.fields["eve_type"].widget.attrs["class"] = "form-select"
File without changes
File without changes
@@ -0,0 +1,45 @@
1
+ """Refresh %-based hull prices from Jita on demand.
2
+
3
+ Runs inline (synchronous) by default so you see results immediately; pass
4
+ --async to enqueue the Celery per-type tasks instead (needs a worker).
5
+ """
6
+
7
+ from django.core.management.base import BaseCommand
8
+
9
+ from shop.tasks import _apply_prices_for_type, _enqueue_pct_refreshes, pct_type_ids
10
+
11
+
12
+ class Command(BaseCommand):
13
+ help = "Refresh %-priced hull listings from live Jita prices (Jita 4-4)."
14
+
15
+ def add_arguments(self, parser):
16
+ parser.add_argument(
17
+ "--async",
18
+ action="store_true",
19
+ dest="use_async",
20
+ help="Enqueue Celery per-type tasks instead of running inline.",
21
+ )
22
+
23
+ def handle(self, *args, **options):
24
+ if options["use_async"]:
25
+ type_ids = _enqueue_pct_refreshes()
26
+ self.stdout.write(
27
+ self.style.SUCCESS(f"Enqueued {len(type_ids)} per-type refresh task(s).")
28
+ )
29
+ return
30
+
31
+ type_ids = pct_type_ids()
32
+ if not type_ids:
33
+ self.stdout.write("No %-priced hull listings to refresh.")
34
+ return
35
+
36
+ total = 0
37
+ for type_id in type_ids:
38
+ changed = _apply_prices_for_type(type_id)
39
+ total += changed
40
+ self.stdout.write(f" type {type_id}: {changed} listing(s) updated")
41
+ self.stdout.write(
42
+ self.style.SUCCESS(
43
+ f"Done — {total} listing(s) updated across {len(type_ids)} type(s)."
44
+ )
45
+ )
@@ -0,0 +1,42 @@
1
+ """Recompute stored hull stock for shops with auto-stock listings.
2
+
3
+ Runs inline (synchronous) by default; pass --async to enqueue the Celery
4
+ per-shop tasks instead (needs a worker).
5
+ """
6
+
7
+ from django.core.management.base import BaseCommand
8
+
9
+ from shop.models import AUTO_STOCK, Shop
10
+ from shop.stock import recompute_shop_stock
11
+ from shop.tasks import _enqueue_stock_refreshes
12
+
13
+
14
+ class Command(BaseCommand):
15
+ help = "Recompute stored hull stock for shops with auto-stock listings."
16
+
17
+ def add_arguments(self, parser):
18
+ parser.add_argument(
19
+ "--async",
20
+ action="store_true",
21
+ dest="use_async",
22
+ help="Enqueue Celery per-shop tasks instead of running inline.",
23
+ )
24
+
25
+ def handle(self, *args, **options):
26
+ if options["use_async"]:
27
+ shop_ids = _enqueue_stock_refreshes()
28
+ self.stdout.write(
29
+ self.style.SUCCESS(f"Enqueued {len(shop_ids)} shop refresh task(s).")
30
+ )
31
+ return
32
+
33
+ shops = list(
34
+ Shop.objects.filter(hull_listings__stock_mode=AUTO_STOCK).distinct()
35
+ )
36
+ if not shops:
37
+ self.stdout.write("No shops with auto-stock listings.")
38
+ return
39
+ for shop in shops:
40
+ recompute_shop_stock(shop)
41
+ self.stdout.write(f" {shop.slug}: recomputed")
42
+ self.stdout.write(self.style.SUCCESS(f"Done — {len(shops)} shop(s)."))