🎉 Initialize module repository
8
.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
.DS_Store
|
||||||
|
.pytest_cache/
|
||||||
|
.ruff_cache/
|
||||||
|
*.log
|
||||||
|
*.swp
|
||||||
|
*~
|
||||||
1
__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from . import models
|
||||||
39
__manifest__.py
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"name": "MVD TCG MTG",
|
||||||
|
"summary": "Magic: The Gathering reference adapter for the MVD TCG suite",
|
||||||
|
"version": "19.0.9.5.1",
|
||||||
|
"description": """
|
||||||
|
Magic: The Gathering adapter built on top of MVD TCG Base.
|
||||||
|
|
||||||
|
This module adds MTG-specific reference data and presentation:
|
||||||
|
- sets, colors, rarities, finishes, formats, card types, and keywords
|
||||||
|
- MTG card fields such as mana cost, oracle text, collector number, and faces
|
||||||
|
- MTG-focused search, views, and symbol rendering in the Odoo backend
|
||||||
|
|
||||||
|
It does not import data on its own. External sources such as Scryfall are
|
||||||
|
handled by dedicated connector modules.
|
||||||
|
""",
|
||||||
|
"category": "Tools",
|
||||||
|
"author": "Mantjeverse Digital",
|
||||||
|
"license": "LGPL-3",
|
||||||
|
"depends": ["mvd_tcg_base", "web"],
|
||||||
|
"data": [
|
||||||
|
"security/ir.model.access.csv",
|
||||||
|
"data/mvd_tcg_game_data.xml",
|
||||||
|
"data/mvd_tcg_mtg_taxonomy_data.xml",
|
||||||
|
"views/mvd_tcg_mtg_set_views.xml",
|
||||||
|
"views/mvd_tcg_mtg_taxonomy_views.xml",
|
||||||
|
"views/mvd_tcg_mtg_card_views.xml",
|
||||||
|
"views/menu_views.xml",
|
||||||
|
],
|
||||||
|
"assets": {
|
||||||
|
"web.assets_backend": [
|
||||||
|
"mvd_tcg_mtg/static/src/js/fields/mtg_symbol_catalog.js",
|
||||||
|
"mvd_tcg_mtg/static/src/js/fields/mtg_symbols_field.js",
|
||||||
|
"mvd_tcg_mtg/static/src/xml/mtg_symbols_field.xml",
|
||||||
|
"mvd_tcg_mtg/static/src/scss/mtg_symbols_field.scss",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"application": False,
|
||||||
|
"installable": True,
|
||||||
|
}
|
||||||
9
data/mvd_tcg_game_data.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo noupdate="1">
|
||||||
|
<record id="mvd_tcg_game_mtg" model="mvd.tcg.game">
|
||||||
|
<field name="name">Magic: The Gathering</field>
|
||||||
|
<field name="code">mtg</field>
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
<field name="description">Magic: The Gathering reference root for MTG-specific adapters.</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
248
data/mvd_tcg_mtg_taxonomy_data.xml
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo noupdate="1">
|
||||||
|
<record id="mvd_tcg_mtg_rarity_common" model="mvd.tcg.mtg.rarity">
|
||||||
|
<field name="name">Common</field>
|
||||||
|
<field name="code">common</field>
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
</record>
|
||||||
|
<record id="mvd_tcg_mtg_rarity_uncommon" model="mvd.tcg.mtg.rarity">
|
||||||
|
<field name="name">Uncommon</field>
|
||||||
|
<field name="code">uncommon</field>
|
||||||
|
<field name="sequence">20</field>
|
||||||
|
</record>
|
||||||
|
<record id="mvd_tcg_mtg_rarity_rare" model="mvd.tcg.mtg.rarity">
|
||||||
|
<field name="name">Rare</field>
|
||||||
|
<field name="code">rare</field>
|
||||||
|
<field name="sequence">30</field>
|
||||||
|
</record>
|
||||||
|
<record id="mvd_tcg_mtg_rarity_mythic" model="mvd.tcg.mtg.rarity">
|
||||||
|
<field name="name">Mythic Rare</field>
|
||||||
|
<field name="code">mythic</field>
|
||||||
|
<field name="sequence">40</field>
|
||||||
|
</record>
|
||||||
|
<record id="mvd_tcg_mtg_rarity_special" model="mvd.tcg.mtg.rarity">
|
||||||
|
<field name="name">Special</field>
|
||||||
|
<field name="code">special</field>
|
||||||
|
<field name="sequence">50</field>
|
||||||
|
</record>
|
||||||
|
<record id="mvd_tcg_mtg_rarity_bonus" model="mvd.tcg.mtg.rarity">
|
||||||
|
<field name="name">Bonus</field>
|
||||||
|
<field name="code">bonus</field>
|
||||||
|
<field name="sequence">60</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="mvd_tcg_mtg_color_white" model="mvd.tcg.mtg.color">
|
||||||
|
<field name="name">White</field>
|
||||||
|
<field name="code">W</field>
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
</record>
|
||||||
|
<record id="mvd_tcg_mtg_color_blue" model="mvd.tcg.mtg.color">
|
||||||
|
<field name="name">Blue</field>
|
||||||
|
<field name="code">U</field>
|
||||||
|
<field name="sequence">20</field>
|
||||||
|
</record>
|
||||||
|
<record id="mvd_tcg_mtg_color_black" model="mvd.tcg.mtg.color">
|
||||||
|
<field name="name">Black</field>
|
||||||
|
<field name="code">B</field>
|
||||||
|
<field name="sequence">30</field>
|
||||||
|
</record>
|
||||||
|
<record id="mvd_tcg_mtg_color_red" model="mvd.tcg.mtg.color">
|
||||||
|
<field name="name">Red</field>
|
||||||
|
<field name="code">R</field>
|
||||||
|
<field name="sequence">40</field>
|
||||||
|
</record>
|
||||||
|
<record id="mvd_tcg_mtg_color_green" model="mvd.tcg.mtg.color">
|
||||||
|
<field name="name">Green</field>
|
||||||
|
<field name="code">G</field>
|
||||||
|
<field name="sequence">50</field>
|
||||||
|
</record>
|
||||||
|
<record id="mvd_tcg_mtg_color_colorless" model="mvd.tcg.mtg.color">
|
||||||
|
<field name="name">Colorless</field>
|
||||||
|
<field name="code">C</field>
|
||||||
|
<field name="sequence">60</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="mvd_tcg_mtg_card_type_artifact" model="mvd.tcg.mtg.card.type">
|
||||||
|
<field name="name">Artifact</field>
|
||||||
|
<field name="code">artifact</field>
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
</record>
|
||||||
|
<record id="mvd_tcg_mtg_card_type_battle" model="mvd.tcg.mtg.card.type">
|
||||||
|
<field name="name">Battle</field>
|
||||||
|
<field name="code">battle</field>
|
||||||
|
<field name="sequence">20</field>
|
||||||
|
</record>
|
||||||
|
<record id="mvd_tcg_mtg_card_type_creature" model="mvd.tcg.mtg.card.type">
|
||||||
|
<field name="name">Creature</field>
|
||||||
|
<field name="code">creature</field>
|
||||||
|
<field name="sequence">30</field>
|
||||||
|
</record>
|
||||||
|
<record id="mvd_tcg_mtg_card_type_enchantment" model="mvd.tcg.mtg.card.type">
|
||||||
|
<field name="name">Enchantment</field>
|
||||||
|
<field name="code">enchantment</field>
|
||||||
|
<field name="sequence">40</field>
|
||||||
|
</record>
|
||||||
|
<record id="mvd_tcg_mtg_card_type_instant" model="mvd.tcg.mtg.card.type">
|
||||||
|
<field name="name">Instant</field>
|
||||||
|
<field name="code">instant</field>
|
||||||
|
<field name="sequence">50</field>
|
||||||
|
</record>
|
||||||
|
<record id="mvd_tcg_mtg_card_type_kindred" model="mvd.tcg.mtg.card.type">
|
||||||
|
<field name="name">Kindred</field>
|
||||||
|
<field name="code">kindred</field>
|
||||||
|
<field name="sequence">60</field>
|
||||||
|
</record>
|
||||||
|
<record id="mvd_tcg_mtg_card_type_land" model="mvd.tcg.mtg.card.type">
|
||||||
|
<field name="name">Land</field>
|
||||||
|
<field name="code">land</field>
|
||||||
|
<field name="sequence">70</field>
|
||||||
|
</record>
|
||||||
|
<record id="mvd_tcg_mtg_card_type_planeswalker" model="mvd.tcg.mtg.card.type">
|
||||||
|
<field name="name">Planeswalker</field>
|
||||||
|
<field name="code">planeswalker</field>
|
||||||
|
<field name="sequence">80</field>
|
||||||
|
</record>
|
||||||
|
<record id="mvd_tcg_mtg_card_type_sorcery" model="mvd.tcg.mtg.card.type">
|
||||||
|
<field name="name">Sorcery</field>
|
||||||
|
<field name="code">sorcery</field>
|
||||||
|
<field name="sequence">90</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="mvd_tcg_mtg_finish_nonfoil" model="mvd.tcg.mtg.finish">
|
||||||
|
<field name="name">Nonfoil</field>
|
||||||
|
<field name="code">nonfoil</field>
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
</record>
|
||||||
|
<record id="mvd_tcg_mtg_finish_foil" model="mvd.tcg.mtg.finish">
|
||||||
|
<field name="name">Foil</field>
|
||||||
|
<field name="code">foil</field>
|
||||||
|
<field name="sequence">20</field>
|
||||||
|
</record>
|
||||||
|
<record id="mvd_tcg_mtg_finish_etched" model="mvd.tcg.mtg.finish">
|
||||||
|
<field name="name">Etched</field>
|
||||||
|
<field name="code">etched</field>
|
||||||
|
<field name="sequence">30</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="mvd_tcg_mtg_platform_paper" model="mvd.tcg.mtg.platform">
|
||||||
|
<field name="name">Paper</field>
|
||||||
|
<field name="code">paper</field>
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
</record>
|
||||||
|
<record id="mvd_tcg_mtg_platform_arena" model="mvd.tcg.mtg.platform">
|
||||||
|
<field name="name">Arena</field>
|
||||||
|
<field name="code">arena</field>
|
||||||
|
<field name="sequence">20</field>
|
||||||
|
</record>
|
||||||
|
<record id="mvd_tcg_mtg_platform_mtgo" model="mvd.tcg.mtg.platform">
|
||||||
|
<field name="name">MTGO</field>
|
||||||
|
<field name="code">mtgo</field>
|
||||||
|
<field name="sequence">30</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="mvd_tcg_mtg_format_standard" model="mvd.tcg.mtg.format">
|
||||||
|
<field name="name">Standard</field>
|
||||||
|
<field name="code">standard</field>
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
</record>
|
||||||
|
<record id="mvd_tcg_mtg_format_future" model="mvd.tcg.mtg.format">
|
||||||
|
<field name="name">Future</field>
|
||||||
|
<field name="code">future</field>
|
||||||
|
<field name="sequence">20</field>
|
||||||
|
</record>
|
||||||
|
<record id="mvd_tcg_mtg_format_historic" model="mvd.tcg.mtg.format">
|
||||||
|
<field name="name">Historic</field>
|
||||||
|
<field name="code">historic</field>
|
||||||
|
<field name="sequence">30</field>
|
||||||
|
</record>
|
||||||
|
<record id="mvd_tcg_mtg_format_timeless" model="mvd.tcg.mtg.format">
|
||||||
|
<field name="name">Timeless</field>
|
||||||
|
<field name="code">timeless</field>
|
||||||
|
<field name="sequence">40</field>
|
||||||
|
</record>
|
||||||
|
<record id="mvd_tcg_mtg_format_gladiator" model="mvd.tcg.mtg.format">
|
||||||
|
<field name="name">Gladiator</field>
|
||||||
|
<field name="code">gladiator</field>
|
||||||
|
<field name="sequence">50</field>
|
||||||
|
</record>
|
||||||
|
<record id="mvd_tcg_mtg_format_pioneer" model="mvd.tcg.mtg.format">
|
||||||
|
<field name="name">Pioneer</field>
|
||||||
|
<field name="code">pioneer</field>
|
||||||
|
<field name="sequence">60</field>
|
||||||
|
</record>
|
||||||
|
<record id="mvd_tcg_mtg_format_modern" model="mvd.tcg.mtg.format">
|
||||||
|
<field name="name">Modern</field>
|
||||||
|
<field name="code">modern</field>
|
||||||
|
<field name="sequence">70</field>
|
||||||
|
</record>
|
||||||
|
<record id="mvd_tcg_mtg_format_legacy" model="mvd.tcg.mtg.format">
|
||||||
|
<field name="name">Legacy</field>
|
||||||
|
<field name="code">legacy</field>
|
||||||
|
<field name="sequence">80</field>
|
||||||
|
</record>
|
||||||
|
<record id="mvd_tcg_mtg_format_pauper" model="mvd.tcg.mtg.format">
|
||||||
|
<field name="name">Pauper</field>
|
||||||
|
<field name="code">pauper</field>
|
||||||
|
<field name="sequence">90</field>
|
||||||
|
</record>
|
||||||
|
<record id="mvd_tcg_mtg_format_vintage" model="mvd.tcg.mtg.format">
|
||||||
|
<field name="name">Vintage</field>
|
||||||
|
<field name="code">vintage</field>
|
||||||
|
<field name="sequence">100</field>
|
||||||
|
</record>
|
||||||
|
<record id="mvd_tcg_mtg_format_penny" model="mvd.tcg.mtg.format">
|
||||||
|
<field name="name">Penny</field>
|
||||||
|
<field name="code">penny</field>
|
||||||
|
<field name="sequence">110</field>
|
||||||
|
</record>
|
||||||
|
<record id="mvd_tcg_mtg_format_commander" model="mvd.tcg.mtg.format">
|
||||||
|
<field name="name">Commander</field>
|
||||||
|
<field name="code">commander</field>
|
||||||
|
<field name="sequence">120</field>
|
||||||
|
</record>
|
||||||
|
<record id="mvd_tcg_mtg_format_oathbreaker" model="mvd.tcg.mtg.format">
|
||||||
|
<field name="name">Oathbreaker</field>
|
||||||
|
<field name="code">oathbreaker</field>
|
||||||
|
<field name="sequence">130</field>
|
||||||
|
</record>
|
||||||
|
<record id="mvd_tcg_mtg_format_standardbrawl" model="mvd.tcg.mtg.format">
|
||||||
|
<field name="name">Standard Brawl</field>
|
||||||
|
<field name="code">standardbrawl</field>
|
||||||
|
<field name="sequence">140</field>
|
||||||
|
</record>
|
||||||
|
<record id="mvd_tcg_mtg_format_brawl" model="mvd.tcg.mtg.format">
|
||||||
|
<field name="name">Brawl</field>
|
||||||
|
<field name="code">brawl</field>
|
||||||
|
<field name="sequence">150</field>
|
||||||
|
</record>
|
||||||
|
<record id="mvd_tcg_mtg_format_alchemy" model="mvd.tcg.mtg.format">
|
||||||
|
<field name="name">Alchemy</field>
|
||||||
|
<field name="code">alchemy</field>
|
||||||
|
<field name="sequence">160</field>
|
||||||
|
</record>
|
||||||
|
<record id="mvd_tcg_mtg_format_paupercommander" model="mvd.tcg.mtg.format">
|
||||||
|
<field name="name">Pauper Commander</field>
|
||||||
|
<field name="code">paupercommander</field>
|
||||||
|
<field name="sequence">170</field>
|
||||||
|
</record>
|
||||||
|
<record id="mvd_tcg_mtg_format_duel" model="mvd.tcg.mtg.format">
|
||||||
|
<field name="name">Duel Commander</field>
|
||||||
|
<field name="code">duel</field>
|
||||||
|
<field name="sequence">180</field>
|
||||||
|
</record>
|
||||||
|
<record id="mvd_tcg_mtg_format_oldschool" model="mvd.tcg.mtg.format">
|
||||||
|
<field name="name">Old School</field>
|
||||||
|
<field name="code">oldschool</field>
|
||||||
|
<field name="sequence">190</field>
|
||||||
|
</record>
|
||||||
|
<record id="mvd_tcg_mtg_format_premodern" model="mvd.tcg.mtg.format">
|
||||||
|
<field name="name">Premodern</field>
|
||||||
|
<field name="code">premodern</field>
|
||||||
|
<field name="sequence">200</field>
|
||||||
|
</record>
|
||||||
|
<record id="mvd_tcg_mtg_format_predh" model="mvd.tcg.mtg.format">
|
||||||
|
<field name="name">PreDH</field>
|
||||||
|
<field name="code">predh</field>
|
||||||
|
<field name="sequence">210</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
1144
i18n/de.po
Normal file
6
models/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
from . import mvd_tcg_game
|
||||||
|
from . import mvd_tcg_card
|
||||||
|
from . import mvd_tcg_mtg_card_face
|
||||||
|
from . import mvd_tcg_mtg_card_legality
|
||||||
|
from . import mvd_tcg_mtg_set
|
||||||
|
from . import mvd_tcg_mtg_taxonomy
|
||||||
499
models/mvd_tcg_card.py
Normal file
@@ -0,0 +1,499 @@
|
|||||||
|
"""Magic: The Gathering card extensions for the TCG suite."""
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
from odoo import api, fields, models
|
||||||
|
|
||||||
|
COLLECTOR_NUMBER_PATTERN = re.compile(r"^(\d+)(.*)$")
|
||||||
|
|
||||||
|
|
||||||
|
class MvdTcgCard(models.Model):
|
||||||
|
"""Extend neutral card references with MTG-specific metadata."""
|
||||||
|
|
||||||
|
_inherit = "mvd.tcg.card"
|
||||||
|
|
||||||
|
is_mtg_game = fields.Boolean(compute="_compute_is_mtg_game")
|
||||||
|
mtg_set_id = fields.Many2one(
|
||||||
|
"mvd.tcg.mtg.set",
|
||||||
|
string="Set",
|
||||||
|
index=True,
|
||||||
|
ondelete="restrict",
|
||||||
|
)
|
||||||
|
mtg_set_code = fields.Char(
|
||||||
|
related="mtg_set_id.code",
|
||||||
|
string="Set Code",
|
||||||
|
store=True,
|
||||||
|
index=True,
|
||||||
|
)
|
||||||
|
mtg_rarity_id = fields.Many2one(
|
||||||
|
"mvd.tcg.mtg.rarity",
|
||||||
|
string="Rarity",
|
||||||
|
index=True,
|
||||||
|
ondelete="restrict",
|
||||||
|
)
|
||||||
|
mtg_rarity_code = fields.Char(
|
||||||
|
related="mtg_rarity_id.code",
|
||||||
|
string="Rarity Code",
|
||||||
|
store=True,
|
||||||
|
index=True,
|
||||||
|
)
|
||||||
|
mtg_collector_number = fields.Char(string="Collector Number", index=True)
|
||||||
|
mtg_collector_sort_key = fields.Char(
|
||||||
|
string="Collector Sort Key",
|
||||||
|
compute="_compute_mtg_collector_sort_key",
|
||||||
|
store=True,
|
||||||
|
index=True,
|
||||||
|
)
|
||||||
|
mtg_oracle_id = fields.Char(string="Oracle ID", index=True)
|
||||||
|
mtg_layout = fields.Char(string="Layout", index=True)
|
||||||
|
mtg_mana_cost = fields.Char(string="Mana Cost")
|
||||||
|
mtg_mana_value = fields.Float(string="Mana Value")
|
||||||
|
mtg_type_line = fields.Char(string="Type Line", translate=True)
|
||||||
|
mtg_oracle_text = fields.Text(string="Oracle Text", translate=True)
|
||||||
|
mtg_flavor_text = fields.Text(string="Flavor Text", translate=True)
|
||||||
|
mtg_color_ids = fields.Many2many(
|
||||||
|
"mvd.tcg.mtg.color",
|
||||||
|
"mvd_tcg_card_mtg_color_rel",
|
||||||
|
"card_id",
|
||||||
|
"color_id",
|
||||||
|
string="Colors",
|
||||||
|
)
|
||||||
|
mtg_color_identity_ids = fields.Many2many(
|
||||||
|
"mvd.tcg.mtg.color",
|
||||||
|
"mvd_tcg_card_mtg_color_identity_rel",
|
||||||
|
"card_id",
|
||||||
|
"color_id",
|
||||||
|
string="Color Identity",
|
||||||
|
)
|
||||||
|
mtg_color_identity_signature = fields.Char(
|
||||||
|
string="Color Identity Signature",
|
||||||
|
compute="_compute_mtg_color_identity_signature",
|
||||||
|
store=True,
|
||||||
|
index=True,
|
||||||
|
help="Ordered MTG color identity signature, for example W, UB or WUBRG.",
|
||||||
|
)
|
||||||
|
mtg_card_type_ids = fields.Many2many(
|
||||||
|
"mvd.tcg.mtg.card.type",
|
||||||
|
"mvd_tcg_card_mtg_card_type_rel",
|
||||||
|
"card_id",
|
||||||
|
"type_id",
|
||||||
|
string="Card Types",
|
||||||
|
)
|
||||||
|
mtg_keyword_ids = fields.Many2many(
|
||||||
|
"mvd.tcg.mtg.keyword",
|
||||||
|
"mvd_tcg_card_mtg_keyword_rel",
|
||||||
|
"card_id",
|
||||||
|
"keyword_id",
|
||||||
|
string="Keywords",
|
||||||
|
)
|
||||||
|
mtg_game_platform_ids = fields.Many2many(
|
||||||
|
"mvd.tcg.mtg.platform",
|
||||||
|
"mvd_tcg_card_mtg_platform_rel",
|
||||||
|
"card_id",
|
||||||
|
"platform_id",
|
||||||
|
string="Games",
|
||||||
|
)
|
||||||
|
mtg_finish_ids = fields.Many2many(
|
||||||
|
"mvd.tcg.mtg.finish",
|
||||||
|
"mvd_tcg_card_mtg_finish_rel",
|
||||||
|
"card_id",
|
||||||
|
"finish_id",
|
||||||
|
string="Finishes",
|
||||||
|
)
|
||||||
|
mtg_power = fields.Char(string="Power")
|
||||||
|
mtg_toughness = fields.Char(string="Toughness")
|
||||||
|
mtg_loyalty = fields.Char(string="Loyalty")
|
||||||
|
mtg_artist = fields.Char(string="Artist", index="trigram")
|
||||||
|
mtg_color_signature = fields.Char(
|
||||||
|
string="Color Signature",
|
||||||
|
compute="_compute_mtg_color_signature",
|
||||||
|
store=True,
|
||||||
|
index=True,
|
||||||
|
help="Ordered MTG color signature, for example W, UB or WUBRG.",
|
||||||
|
)
|
||||||
|
mtg_face_ids = fields.One2many("mvd.tcg.mtg.card.face", "card_id", string="Faces")
|
||||||
|
mtg_face_count = fields.Integer(
|
||||||
|
string="Face Count",
|
||||||
|
compute="_compute_mtg_face_count",
|
||||||
|
store=True,
|
||||||
|
)
|
||||||
|
mtg_legality_ids = fields.One2many(
|
||||||
|
"mvd.tcg.mtg.card.legality",
|
||||||
|
"card_id",
|
||||||
|
string="Legalities",
|
||||||
|
)
|
||||||
|
mtg_is_token = fields.Boolean(string="Token")
|
||||||
|
mtg_is_reprint = fields.Boolean(string="Reprint")
|
||||||
|
mtg_is_promo = fields.Boolean(string="Promo")
|
||||||
|
mtg_is_digital = fields.Boolean(string="Digital")
|
||||||
|
mtg_is_full_art = fields.Boolean(string="Full Art")
|
||||||
|
mtg_is_textless = fields.Boolean(string="Textless")
|
||||||
|
mtg_set_released_on = fields.Date(
|
||||||
|
related="mtg_set_id.released_on",
|
||||||
|
string="Released On",
|
||||||
|
store=True,
|
||||||
|
index=True,
|
||||||
|
)
|
||||||
|
mtg_set_type = fields.Char(
|
||||||
|
related="mtg_set_id.set_type",
|
||||||
|
string="Set Type",
|
||||||
|
store=True,
|
||||||
|
index=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
_mtg_set_collector_unique = models.Constraint(
|
||||||
|
"UNIQUE (mtg_set_id, mtg_collector_number)",
|
||||||
|
"The collector number must be unique inside an MTG set.",
|
||||||
|
)
|
||||||
|
|
||||||
|
@api.depends("game_id.code")
|
||||||
|
def _compute_is_mtg_game(self):
|
||||||
|
"""Flag whether the current card belongs to the MTG adapter."""
|
||||||
|
for card in self:
|
||||||
|
card.is_mtg_game = card.game_id.code == "mtg"
|
||||||
|
|
||||||
|
@api.depends("mtg_collector_number")
|
||||||
|
def _compute_mtg_collector_sort_key(self):
|
||||||
|
"""Build a stable natural-sort key for MTG collector numbers.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None: The compute updates records in place.
|
||||||
|
"""
|
||||||
|
for card in self:
|
||||||
|
collector_number = (card.mtg_collector_number or "").strip().lower()
|
||||||
|
if not collector_number:
|
||||||
|
card.mtg_collector_sort_key = "99999999:"
|
||||||
|
continue
|
||||||
|
|
||||||
|
match = COLLECTOR_NUMBER_PATTERN.match(collector_number)
|
||||||
|
if match:
|
||||||
|
numeric_part, suffix = match.groups()
|
||||||
|
card.mtg_collector_sort_key = (
|
||||||
|
f"{int(numeric_part):08d}:{suffix.strip()}"
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
card.mtg_collector_sort_key = f"99999999:{collector_number}"
|
||||||
|
|
||||||
|
@api.depends("mtg_color_ids", "mtg_color_ids.sequence", "mtg_color_ids.code")
|
||||||
|
def _compute_mtg_color_signature(self):
|
||||||
|
"""Build the canonical MTG color signature from selected colors.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None: The compute updates records in place.
|
||||||
|
"""
|
||||||
|
for card in self:
|
||||||
|
colors = card.mtg_color_ids.sorted(
|
||||||
|
key=lambda color: (color.sequence, color.code or "", color.id)
|
||||||
|
)
|
||||||
|
card.mtg_color_signature = "".join(
|
||||||
|
(color.code or "").strip().upper() for color in colors
|
||||||
|
) or False
|
||||||
|
|
||||||
|
@api.depends(
|
||||||
|
"mtg_color_identity_ids",
|
||||||
|
"mtg_color_identity_ids.sequence",
|
||||||
|
"mtg_color_identity_ids.code",
|
||||||
|
)
|
||||||
|
def _compute_mtg_color_identity_signature(self):
|
||||||
|
"""Build the canonical MTG color identity signature.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None: The compute updates records in place.
|
||||||
|
"""
|
||||||
|
for card in self:
|
||||||
|
colors = card.mtg_color_identity_ids.sorted(
|
||||||
|
key=lambda color: (color.sequence, color.code or "", color.id)
|
||||||
|
)
|
||||||
|
card.mtg_color_identity_signature = "".join(
|
||||||
|
(color.code or "").strip().upper() for color in colors
|
||||||
|
) or False
|
||||||
|
|
||||||
|
@api.depends("mtg_face_ids")
|
||||||
|
def _compute_mtg_face_count(self):
|
||||||
|
"""Compute how many explicit faces are linked to each card.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None: The compute updates records in place.
|
||||||
|
"""
|
||||||
|
face_data = self.env["mvd.tcg.mtg.card.face"]._read_group(
|
||||||
|
[("card_id", "in", self.ids)],
|
||||||
|
["card_id"],
|
||||||
|
["__count"],
|
||||||
|
)
|
||||||
|
counts_by_card = {record.id: count for record, count in face_data}
|
||||||
|
for card in self:
|
||||||
|
card.mtg_face_count = counts_by_card.get(card.id, 0)
|
||||||
|
|
||||||
|
def action_open_mtg_set(self):
|
||||||
|
"""Open the linked MTG set in form view.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Window action for the linked MTG set.
|
||||||
|
"""
|
||||||
|
self.ensure_one()
|
||||||
|
if not self.mtg_set_id:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return {
|
||||||
|
"type": "ir.actions.act_window",
|
||||||
|
"name": self.mtg_set_id.display_name,
|
||||||
|
"res_model": "mvd.tcg.mtg.set",
|
||||||
|
"view_mode": "form",
|
||||||
|
"res_id": self.mtg_set_id.id,
|
||||||
|
"target": "current",
|
||||||
|
}
|
||||||
|
|
||||||
|
def mtg_get_rules_sections(self):
|
||||||
|
"""Return one face-aware MTG rules structure for the current card.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[dict[str, object]]: Ordered rules sections. Multi-face cards
|
||||||
|
return one section per face, while single-face cards return one
|
||||||
|
fallback section from the card header fields.
|
||||||
|
"""
|
||||||
|
self.ensure_one()
|
||||||
|
face_sections = []
|
||||||
|
for face in self.mtg_face_ids.sorted(lambda current_face: (current_face.sequence, current_face.id)):
|
||||||
|
section = {
|
||||||
|
"sequence": face.sequence,
|
||||||
|
"name": face.name or False,
|
||||||
|
"mana_cost": face.mana_cost or False,
|
||||||
|
"type_line": face.type_line or False,
|
||||||
|
"oracle_text": face.oracle_text or False,
|
||||||
|
"flavor_text": face.flavor_text or False,
|
||||||
|
"power": face.power or False,
|
||||||
|
"toughness": face.toughness or False,
|
||||||
|
"loyalty": face.loyalty or False,
|
||||||
|
}
|
||||||
|
if any(section.values()):
|
||||||
|
face_sections.append(section)
|
||||||
|
if face_sections:
|
||||||
|
return face_sections
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"sequence": 10,
|
||||||
|
"name": self.name or False,
|
||||||
|
"mana_cost": self.mtg_mana_cost or False,
|
||||||
|
"type_line": self.mtg_type_line or False,
|
||||||
|
"oracle_text": self.mtg_oracle_text or False,
|
||||||
|
"flavor_text": self.mtg_flavor_text or False,
|
||||||
|
"power": self.mtg_power or False,
|
||||||
|
"toughness": self.mtg_toughness or False,
|
||||||
|
"loyalty": self.mtg_loyalty or False,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
def mtg_get_rules_summary(self):
|
||||||
|
"""Return one analysis-friendly MTG rules summary.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str | bool: Single-face cards return their oracle text directly.
|
||||||
|
Multi-face cards return a combined face-aware rules summary.
|
||||||
|
"""
|
||||||
|
self.ensure_one()
|
||||||
|
rule_sections = self.mtg_get_rules_sections()
|
||||||
|
if len(rule_sections) <= 1:
|
||||||
|
return rule_sections and rule_sections[0]["oracle_text"] or False
|
||||||
|
|
||||||
|
rendered_sections = []
|
||||||
|
for section in rule_sections:
|
||||||
|
section_lines = []
|
||||||
|
heading_parts = [section["name"]]
|
||||||
|
if section["mana_cost"]:
|
||||||
|
heading_parts.append(section["mana_cost"])
|
||||||
|
heading = " ".join(part for part in heading_parts if part)
|
||||||
|
if heading:
|
||||||
|
section_lines.append(heading)
|
||||||
|
if section["type_line"]:
|
||||||
|
section_lines.append(section["type_line"])
|
||||||
|
if section["oracle_text"]:
|
||||||
|
section_lines.append(section["oracle_text"])
|
||||||
|
if section_lines:
|
||||||
|
rendered_sections.append("\n".join(section_lines))
|
||||||
|
return "\n\n//\n\n".join(rendered_sections) or False
|
||||||
|
|
||||||
|
def mtg_get_singleton_key_aliases(self):
|
||||||
|
"""Return stable MTG identity aliases across printings and styles.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple[str, ...]: Ordered identity aliases. Cards with a missing
|
||||||
|
Oracle ID still expose rules-based fallbacks that can match
|
||||||
|
styled variants of the same card.
|
||||||
|
"""
|
||||||
|
self.ensure_one()
|
||||||
|
english_card = self.with_context(lang="en_US")
|
||||||
|
aliases = []
|
||||||
|
oracle_id = (english_card.mtg_oracle_id or "").strip()
|
||||||
|
if oracle_id:
|
||||||
|
aliases.append(f"oracle:{oracle_id}")
|
||||||
|
|
||||||
|
rules_summary = re.sub(
|
||||||
|
r"\s+",
|
||||||
|
" ",
|
||||||
|
(english_card.mtg_get_rules_summary() or "").strip(),
|
||||||
|
).strip()
|
||||||
|
if rules_summary:
|
||||||
|
aliases.append(f"rules:{rules_summary}")
|
||||||
|
|
||||||
|
deduplicated_oracle_chunks = []
|
||||||
|
for section in english_card.mtg_get_rules_sections():
|
||||||
|
oracle_text = re.sub(
|
||||||
|
r"\s+",
|
||||||
|
" ",
|
||||||
|
(section["oracle_text"] or "").strip(),
|
||||||
|
).strip()
|
||||||
|
if oracle_text and oracle_text not in deduplicated_oracle_chunks:
|
||||||
|
deduplicated_oracle_chunks.append(oracle_text)
|
||||||
|
if deduplicated_oracle_chunks:
|
||||||
|
aliases.append(f"oracletext:{' // '.join(deduplicated_oracle_chunks)}")
|
||||||
|
|
||||||
|
face_names = " // ".join(
|
||||||
|
section["name"]
|
||||||
|
for section in english_card.mtg_get_rules_sections()
|
||||||
|
if section["name"]
|
||||||
|
)
|
||||||
|
fallback_name = face_names or (english_card.name or "").strip()
|
||||||
|
fallback_type = (english_card.mtg_type_line or "").strip()
|
||||||
|
fallback_cost = (english_card.mtg_mana_cost or "").strip()
|
||||||
|
if any((fallback_name, fallback_type, fallback_cost)):
|
||||||
|
aliases.append(f"fallback:{fallback_name}|{fallback_type}|{fallback_cost}")
|
||||||
|
|
||||||
|
deduplicated_aliases = []
|
||||||
|
for alias in aliases:
|
||||||
|
if alias and alias not in deduplicated_aliases:
|
||||||
|
deduplicated_aliases.append(alias)
|
||||||
|
return tuple(deduplicated_aliases)
|
||||||
|
|
||||||
|
def mtg_get_singleton_key(self):
|
||||||
|
"""Return the primary MTG singleton key for the current card.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str | bool: First stable singleton alias, if any.
|
||||||
|
"""
|
||||||
|
self.ensure_one()
|
||||||
|
aliases = self.mtg_get_singleton_key_aliases()
|
||||||
|
return aliases[0] if aliases else False
|
||||||
|
|
||||||
|
def mtg_allows_unlimited_copies(self):
|
||||||
|
"""Return whether one MTG card bypasses singleton restrictions.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: ``True`` for basic lands and cards with explicit unlimited-
|
||||||
|
copy rules text.
|
||||||
|
"""
|
||||||
|
self.ensure_one()
|
||||||
|
english_card = self.with_context(lang="en_US")
|
||||||
|
combined_type_line = " ".join(
|
||||||
|
section["type_line"]
|
||||||
|
for section in english_card.mtg_get_rules_sections()
|
||||||
|
if section["type_line"]
|
||||||
|
).lower()
|
||||||
|
rules_summary = (english_card.mtg_get_rules_summary() or "").lower()
|
||||||
|
is_basic_land = "land" in combined_type_line and "basic" in combined_type_line
|
||||||
|
allows_unlimited_copies = (
|
||||||
|
"a deck can have any number of cards named" in rules_summary
|
||||||
|
)
|
||||||
|
return is_basic_land or allows_unlimited_copies
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _mtg_exact_color_signature_from_ids(self, color_ids):
|
||||||
|
"""Build the canonical color signature for selected color records.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
color_ids: MTG color record ids from a filter domain.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str | bool: Canonical signature like ``WB`` or ``False`` when invalid.
|
||||||
|
"""
|
||||||
|
normalized_ids = [int(color_id) for color_id in color_ids or [] if color_id]
|
||||||
|
if not normalized_ids:
|
||||||
|
return False
|
||||||
|
|
||||||
|
colors = self.env["mvd.tcg.mtg.color"].browse(normalized_ids).exists().sorted(
|
||||||
|
key=lambda color: (color.sequence, color.code or "", color.id)
|
||||||
|
)
|
||||||
|
if len(colors) != len(set(normalized_ids)):
|
||||||
|
return False
|
||||||
|
return "".join((color.code or "").strip().upper() for color in colors)
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _mtg_transform_exact_color_domain(self, domain):
|
||||||
|
"""Rewrite MTG color filters into exact color-signature filters.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
domain: Original ORM domain.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: Domain with exact MTG color matching where applicable.
|
||||||
|
"""
|
||||||
|
domain_object = fields.Domain(domain or [])
|
||||||
|
selected_color_ids = []
|
||||||
|
|
||||||
|
for condition in domain_object.iter_conditions():
|
||||||
|
if condition.field_expr != "mtg_color_ids":
|
||||||
|
continue
|
||||||
|
if condition.operator == "=" and condition.value:
|
||||||
|
color_id = int(condition.value)
|
||||||
|
if color_id not in selected_color_ids:
|
||||||
|
selected_color_ids.append(color_id)
|
||||||
|
continue
|
||||||
|
if condition.operator == "in" and isinstance(condition.value, (list, tuple)):
|
||||||
|
for color_id in condition.value:
|
||||||
|
normalized_id = int(color_id)
|
||||||
|
if normalized_id not in selected_color_ids:
|
||||||
|
selected_color_ids.append(normalized_id)
|
||||||
|
|
||||||
|
if not selected_color_ids:
|
||||||
|
return domain
|
||||||
|
|
||||||
|
exact_signature = self._mtg_exact_color_signature_from_ids(selected_color_ids)
|
||||||
|
if not exact_signature:
|
||||||
|
return list(fields.Domain.FALSE)
|
||||||
|
|
||||||
|
remaining_domain = domain_object.map_conditions(
|
||||||
|
lambda condition: (
|
||||||
|
fields.Domain.TRUE
|
||||||
|
if condition.field_expr == "mtg_color_ids"
|
||||||
|
else fields.Domain(condition)
|
||||||
|
)
|
||||||
|
).optimize(self)
|
||||||
|
exact_domain = fields.Domain(
|
||||||
|
[("mtg_color_signature", "=", exact_signature)]
|
||||||
|
)
|
||||||
|
return list(fields.Domain.AND([remaining_domain, exact_domain]).optimize(self))
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _search(
|
||||||
|
self,
|
||||||
|
domain,
|
||||||
|
offset=0,
|
||||||
|
limit=None,
|
||||||
|
order=None,
|
||||||
|
*,
|
||||||
|
active_test=True,
|
||||||
|
bypass_access=False,
|
||||||
|
):
|
||||||
|
"""Apply exact MTG color matching for the Magic card action.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
domain: ORM domain for the current search.
|
||||||
|
offset: Search offset.
|
||||||
|
limit: Optional maximal number of records.
|
||||||
|
order: SQL order clause.
|
||||||
|
active_test: Whether active records should be filtered implicitly.
|
||||||
|
bypass_access: Whether access rules should be bypassed.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Query: Matching search query.
|
||||||
|
"""
|
||||||
|
if self.env.context.get("mvd_mtg_exact_color_filter"):
|
||||||
|
domain = self._mtg_transform_exact_color_domain(domain)
|
||||||
|
return super()._search(
|
||||||
|
domain,
|
||||||
|
offset=offset,
|
||||||
|
limit=limit,
|
||||||
|
order=order,
|
||||||
|
active_test=active_test,
|
||||||
|
bypass_access=bypass_access,
|
||||||
|
)
|
||||||
14
models/mvd_tcg_game.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
"""MTG-specific helpers on the neutral game model."""
|
||||||
|
|
||||||
|
from odoo import api, models
|
||||||
|
|
||||||
|
|
||||||
|
class MvdTcgGame(models.Model):
|
||||||
|
"""Provide stable access to the seeded MTG game record."""
|
||||||
|
|
||||||
|
_inherit = "mvd.tcg.game"
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _mvd_tcg_get_mtg_game(self):
|
||||||
|
"""Return the seeded MTG game record."""
|
||||||
|
return self.env.ref("mvd_tcg_mtg.mvd_tcg_game_mtg")
|
||||||
33
models/mvd_tcg_mtg_card_face.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
"""Magic: The Gathering face-level reference models."""
|
||||||
|
|
||||||
|
from odoo import fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class MvdTcgMtgCardFace(models.Model):
|
||||||
|
"""Represent one ordered printed face of an MTG card reference."""
|
||||||
|
|
||||||
|
_name = "mvd.tcg.mtg.card.face"
|
||||||
|
_description = "MTG Card Face"
|
||||||
|
_order = "card_id, sequence, id"
|
||||||
|
|
||||||
|
card_id = fields.Many2one(
|
||||||
|
"mvd.tcg.card",
|
||||||
|
required=True,
|
||||||
|
index=True,
|
||||||
|
ondelete="cascade",
|
||||||
|
)
|
||||||
|
sequence = fields.Integer(default=10, index=True)
|
||||||
|
name = fields.Char(required=True, translate=True, index="trigram")
|
||||||
|
mana_cost = fields.Char()
|
||||||
|
type_line = fields.Char(translate=True)
|
||||||
|
oracle_text = fields.Text(translate=True)
|
||||||
|
flavor_text = fields.Text(translate=True)
|
||||||
|
power = fields.Char()
|
||||||
|
toughness = fields.Char()
|
||||||
|
loyalty = fields.Char()
|
||||||
|
artist = fields.Char(index="trigram")
|
||||||
|
|
||||||
|
_card_sequence_unique = models.Constraint(
|
||||||
|
"UNIQUE (card_id, sequence)",
|
||||||
|
"The MTG face sequence must be unique per card.",
|
||||||
|
)
|
||||||
52
models/mvd_tcg_mtg_card_legality.py
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
"""Magic: The Gathering legality models."""
|
||||||
|
|
||||||
|
from odoo import fields, models
|
||||||
|
|
||||||
|
LEGALITY_SELECTION = [
|
||||||
|
("legal", "Legal"),
|
||||||
|
("not_legal", "Not Legal"),
|
||||||
|
("restricted", "Restricted"),
|
||||||
|
("banned", "Banned"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class MvdTcgMtgCardLegality(models.Model):
|
||||||
|
"""Store one legality status per MTG card and constructed format."""
|
||||||
|
|
||||||
|
_name = "mvd.tcg.mtg.card.legality"
|
||||||
|
_description = "MTG Card Legality"
|
||||||
|
_order = "format_sequence, id"
|
||||||
|
|
||||||
|
card_id = fields.Many2one(
|
||||||
|
"mvd.tcg.card",
|
||||||
|
required=True,
|
||||||
|
index=True,
|
||||||
|
ondelete="cascade",
|
||||||
|
)
|
||||||
|
format_id = fields.Many2one(
|
||||||
|
"mvd.tcg.mtg.format",
|
||||||
|
required=True,
|
||||||
|
index=True,
|
||||||
|
ondelete="restrict",
|
||||||
|
)
|
||||||
|
format_code = fields.Char(
|
||||||
|
related="format_id.code",
|
||||||
|
store=True,
|
||||||
|
index=True,
|
||||||
|
)
|
||||||
|
format_sequence = fields.Integer(
|
||||||
|
related="format_id.sequence",
|
||||||
|
store=True,
|
||||||
|
index=True,
|
||||||
|
)
|
||||||
|
status = fields.Selection(
|
||||||
|
selection=LEGALITY_SELECTION,
|
||||||
|
required=True,
|
||||||
|
default="not_legal",
|
||||||
|
index=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
_card_format_unique = models.Constraint(
|
||||||
|
"UNIQUE (card_id, format_id)",
|
||||||
|
"The MTG legality format must be unique per card.",
|
||||||
|
)
|
||||||
72
models/mvd_tcg_mtg_set.py
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
"""Magic: The Gathering set models for the TCG suite."""
|
||||||
|
|
||||||
|
from odoo import api, fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class MvdTcgMtgSet(models.Model):
|
||||||
|
"""Represent a Magic: The Gathering set within the MTG adapter."""
|
||||||
|
|
||||||
|
_name = "mvd.tcg.mtg.set"
|
||||||
|
_description = "MTG Set"
|
||||||
|
_order = "released_on desc, code, id"
|
||||||
|
|
||||||
|
name = fields.Char(required=True, translate=True, index="trigram")
|
||||||
|
code = fields.Char(required=True, index=True)
|
||||||
|
active = fields.Boolean(default=True)
|
||||||
|
sequence = fields.Integer(default=10)
|
||||||
|
game_id = fields.Many2one(
|
||||||
|
"mvd.tcg.game",
|
||||||
|
required=True,
|
||||||
|
index=True,
|
||||||
|
default=lambda self: self._default_game_id(),
|
||||||
|
ondelete="restrict",
|
||||||
|
)
|
||||||
|
released_on = fields.Date(index=True)
|
||||||
|
set_type = fields.Char(index=True)
|
||||||
|
official_card_count = fields.Integer()
|
||||||
|
icon_svg_uri = fields.Char()
|
||||||
|
note = fields.Text(translate=True)
|
||||||
|
mtg_card_ids = fields.One2many(
|
||||||
|
"mvd.tcg.card",
|
||||||
|
"mtg_set_id",
|
||||||
|
string="Cards",
|
||||||
|
)
|
||||||
|
mtg_card_count = fields.Integer(
|
||||||
|
string="Card Count",
|
||||||
|
compute="_compute_mtg_card_count",
|
||||||
|
)
|
||||||
|
|
||||||
|
_code_unique = models.Constraint(
|
||||||
|
"UNIQUE (game_id, code)",
|
||||||
|
"The MTG set code must be unique per game.",
|
||||||
|
)
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _default_game_id(self):
|
||||||
|
"""Return the seeded MTG game record."""
|
||||||
|
return self.env["mvd.tcg.game"]._mvd_tcg_get_mtg_game().id
|
||||||
|
|
||||||
|
@api.depends("mtg_card_ids")
|
||||||
|
def _compute_mtg_card_count(self):
|
||||||
|
"""Compute how many MTG cards are currently linked to each set."""
|
||||||
|
card_data = self.env["mvd.tcg.card"]._read_group(
|
||||||
|
[("mtg_set_id", "in", self.ids)],
|
||||||
|
["mtg_set_id"],
|
||||||
|
["__count"],
|
||||||
|
)
|
||||||
|
counts_by_set = {record.id: count for record, count in card_data}
|
||||||
|
for mtg_set in self:
|
||||||
|
mtg_set.mtg_card_count = counts_by_set.get(mtg_set.id, 0)
|
||||||
|
|
||||||
|
def action_open_cards(self):
|
||||||
|
"""Open the MTG cards catalog filtered on the selected set."""
|
||||||
|
self.ensure_one()
|
||||||
|
action = self.env["ir.actions.actions"]._for_xml_id(
|
||||||
|
"mvd_tcg_mtg.mvd_tcg_mtg_card_action"
|
||||||
|
)
|
||||||
|
action["domain"] = [("mtg_set_id", "=", self.id)]
|
||||||
|
action["context"] = {
|
||||||
|
"default_game_id": self.game_id.id,
|
||||||
|
"default_mtg_set_id": self.id,
|
||||||
|
}
|
||||||
|
return action
|
||||||
230
models/mvd_tcg_mtg_taxonomy.py
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
"""Magic: The Gathering taxonomy models for cards and faceting."""
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
from odoo import _, api, fields, models
|
||||||
|
from odoo.exceptions import UserError
|
||||||
|
|
||||||
|
|
||||||
|
class MvdTcgMtgTaxonomyMixin(models.AbstractModel):
|
||||||
|
"""Share technical code handling across MTG taxonomy models."""
|
||||||
|
|
||||||
|
_name = "mvd.tcg.mtg.taxonomy.mixin"
|
||||||
|
_description = "MTG Taxonomy Technical Code Mixin"
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _mvd_tcg_generate_code(self, name):
|
||||||
|
"""Return a slug-like technical code for one taxonomy name."""
|
||||||
|
return re.sub(r"[^a-z0-9]+", "_", (name or "").strip().lower()).strip("_") or "item"
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _mvd_tcg_get_unique_code(self, name):
|
||||||
|
"""Return a unique technical code for one taxonomy record name."""
|
||||||
|
base_code = self._mvd_tcg_generate_code(name)
|
||||||
|
existing_codes = set(self.search([]).mapped("code"))
|
||||||
|
if base_code not in existing_codes:
|
||||||
|
return base_code
|
||||||
|
suffix = 2
|
||||||
|
while f"{base_code}_{suffix}" in existing_codes:
|
||||||
|
suffix += 1
|
||||||
|
return f"{base_code}_{suffix}"
|
||||||
|
|
||||||
|
def _mvd_tcg_can_edit_code(self):
|
||||||
|
"""Return whether the current user may edit taxonomy codes."""
|
||||||
|
return self.env.is_superuser() or any(
|
||||||
|
self.env.user.has_group(xmlid)
|
||||||
|
for xmlid in (
|
||||||
|
"mvd_tcg_base.mvd_tcg_base_group_administrator",
|
||||||
|
"base.group_system",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
@api.model_create_multi
|
||||||
|
def create(self, vals_list):
|
||||||
|
"""Generate missing technical codes for manually created taxonomy records."""
|
||||||
|
prepared_vals_list = []
|
||||||
|
for vals in vals_list:
|
||||||
|
prepared_vals = dict(vals)
|
||||||
|
if not prepared_vals.get("code"):
|
||||||
|
prepared_vals["code"] = self._mvd_tcg_get_unique_code(
|
||||||
|
prepared_vals.get("name")
|
||||||
|
)
|
||||||
|
prepared_vals_list.append(prepared_vals)
|
||||||
|
return super().create(prepared_vals_list)
|
||||||
|
|
||||||
|
def write(self, vals):
|
||||||
|
"""Protect taxonomy codes from normal business edits."""
|
||||||
|
if "code" in vals and not self.env.context.get("mvd_tcg_bypass_taxonomy_code_write"):
|
||||||
|
if not self._mvd_tcg_can_edit_code():
|
||||||
|
raise UserError(
|
||||||
|
_(
|
||||||
|
"MTG taxonomy codes are technical identifiers and can only be "
|
||||||
|
"changed by TCG administrators."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return super().write(vals)
|
||||||
|
|
||||||
|
|
||||||
|
class MvdTcgMtgRarity(models.Model):
|
||||||
|
"""Store normalized MTG rarity records."""
|
||||||
|
|
||||||
|
_name = "mvd.tcg.mtg.rarity"
|
||||||
|
_inherit = "mvd.tcg.mtg.taxonomy.mixin"
|
||||||
|
_description = "MTG Rarity"
|
||||||
|
_order = "sequence, name, id"
|
||||||
|
|
||||||
|
name = fields.Char(required=True, translate=True, index="trigram")
|
||||||
|
code = fields.Char(required=True, index=True)
|
||||||
|
active = fields.Boolean(default=True)
|
||||||
|
sequence = fields.Integer(default=10)
|
||||||
|
card_ids = fields.One2many("mvd.tcg.card", "mtg_rarity_id")
|
||||||
|
|
||||||
|
_code_unique = models.Constraint(
|
||||||
|
"UNIQUE (code)",
|
||||||
|
"The MTG rarity code must be unique.",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MvdTcgMtgColor(models.Model):
|
||||||
|
"""Store normalized MTG colors for faceting and card metadata."""
|
||||||
|
|
||||||
|
_name = "mvd.tcg.mtg.color"
|
||||||
|
_inherit = "mvd.tcg.mtg.taxonomy.mixin"
|
||||||
|
_description = "MTG Color"
|
||||||
|
_order = "sequence, name, id"
|
||||||
|
|
||||||
|
name = fields.Char(required=True, translate=True, index="trigram")
|
||||||
|
code = fields.Char(required=True, index=True)
|
||||||
|
active = fields.Boolean(default=True)
|
||||||
|
sequence = fields.Integer(default=10)
|
||||||
|
card_ids = fields.Many2many(
|
||||||
|
"mvd.tcg.card",
|
||||||
|
"mvd_tcg_card_mtg_color_rel",
|
||||||
|
"color_id",
|
||||||
|
"card_id",
|
||||||
|
)
|
||||||
|
|
||||||
|
_code_unique = models.Constraint(
|
||||||
|
"UNIQUE (code)",
|
||||||
|
"The MTG color code must be unique.",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MvdTcgMtgCardType(models.Model):
|
||||||
|
"""Store normalized MTG card types for faceting and grouping."""
|
||||||
|
|
||||||
|
_name = "mvd.tcg.mtg.card.type"
|
||||||
|
_inherit = "mvd.tcg.mtg.taxonomy.mixin"
|
||||||
|
_description = "MTG Card Type"
|
||||||
|
_order = "sequence, name, id"
|
||||||
|
|
||||||
|
name = fields.Char(required=True, translate=True, index="trigram")
|
||||||
|
code = fields.Char(required=True, index=True)
|
||||||
|
active = fields.Boolean(default=True)
|
||||||
|
sequence = fields.Integer(default=10)
|
||||||
|
card_ids = fields.Many2many(
|
||||||
|
"mvd.tcg.card",
|
||||||
|
"mvd_tcg_card_mtg_card_type_rel",
|
||||||
|
"type_id",
|
||||||
|
"card_id",
|
||||||
|
)
|
||||||
|
|
||||||
|
_code_unique = models.Constraint(
|
||||||
|
"UNIQUE (code)",
|
||||||
|
"The MTG card type code must be unique.",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MvdTcgMtgKeyword(models.Model):
|
||||||
|
"""Store normalized MTG keywords imported from Scryfall."""
|
||||||
|
|
||||||
|
_name = "mvd.tcg.mtg.keyword"
|
||||||
|
_inherit = "mvd.tcg.mtg.taxonomy.mixin"
|
||||||
|
_description = "MTG Keyword"
|
||||||
|
_order = "sequence, name, id"
|
||||||
|
|
||||||
|
name = fields.Char(required=True, translate=True, index="trigram")
|
||||||
|
code = fields.Char(required=True, index=True)
|
||||||
|
active = fields.Boolean(default=True)
|
||||||
|
sequence = fields.Integer(default=10)
|
||||||
|
card_ids = fields.Many2many(
|
||||||
|
"mvd.tcg.card",
|
||||||
|
"mvd_tcg_card_mtg_keyword_rel",
|
||||||
|
"keyword_id",
|
||||||
|
"card_id",
|
||||||
|
)
|
||||||
|
|
||||||
|
_code_unique = models.Constraint(
|
||||||
|
"UNIQUE (code)",
|
||||||
|
"The MTG keyword code must be unique.",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MvdTcgMtgFormat(models.Model):
|
||||||
|
"""Store normalized MTG formats for legality information."""
|
||||||
|
|
||||||
|
_name = "mvd.tcg.mtg.format"
|
||||||
|
_inherit = "mvd.tcg.mtg.taxonomy.mixin"
|
||||||
|
_description = "MTG Format"
|
||||||
|
_order = "sequence, name, id"
|
||||||
|
|
||||||
|
name = fields.Char(required=True, translate=True, index="trigram")
|
||||||
|
code = fields.Char(required=True, index=True)
|
||||||
|
active = fields.Boolean(default=True)
|
||||||
|
sequence = fields.Integer(default=10)
|
||||||
|
legality_ids = fields.One2many("mvd.tcg.mtg.card.legality", "format_id")
|
||||||
|
|
||||||
|
_code_unique = models.Constraint(
|
||||||
|
"UNIQUE (code)",
|
||||||
|
"The MTG format code must be unique.",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MvdTcgMtgFinish(models.Model):
|
||||||
|
"""Store normalized MTG finishes such as foil and etched."""
|
||||||
|
|
||||||
|
_name = "mvd.tcg.mtg.finish"
|
||||||
|
_inherit = "mvd.tcg.mtg.taxonomy.mixin"
|
||||||
|
_description = "MTG Finish"
|
||||||
|
_order = "sequence, name, id"
|
||||||
|
|
||||||
|
name = fields.Char(required=True, translate=True, index="trigram")
|
||||||
|
code = fields.Char(required=True, index=True)
|
||||||
|
active = fields.Boolean(default=True)
|
||||||
|
sequence = fields.Integer(default=10)
|
||||||
|
card_ids = fields.Many2many(
|
||||||
|
"mvd.tcg.card",
|
||||||
|
"mvd_tcg_card_mtg_finish_rel",
|
||||||
|
"finish_id",
|
||||||
|
"card_id",
|
||||||
|
)
|
||||||
|
|
||||||
|
_code_unique = models.Constraint(
|
||||||
|
"UNIQUE (code)",
|
||||||
|
"The MTG finish code must be unique.",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MvdTcgMtgPlatform(models.Model):
|
||||||
|
"""Store normalized MTG game platforms such as paper and Arena."""
|
||||||
|
|
||||||
|
_name = "mvd.tcg.mtg.platform"
|
||||||
|
_inherit = "mvd.tcg.mtg.taxonomy.mixin"
|
||||||
|
_description = "MTG Platform"
|
||||||
|
_order = "sequence, name, id"
|
||||||
|
|
||||||
|
name = fields.Char(required=True, translate=True, index="trigram")
|
||||||
|
code = fields.Char(required=True, index=True)
|
||||||
|
active = fields.Boolean(default=True)
|
||||||
|
sequence = fields.Integer(default=10)
|
||||||
|
card_ids = fields.Many2many(
|
||||||
|
"mvd.tcg.card",
|
||||||
|
"mvd_tcg_card_mtg_platform_rel",
|
||||||
|
"platform_id",
|
||||||
|
"card_id",
|
||||||
|
)
|
||||||
|
|
||||||
|
_code_unique = models.Constraint(
|
||||||
|
"UNIQUE (code)",
|
||||||
|
"The MTG platform code must be unique.",
|
||||||
|
)
|
||||||
39
security/ir.model.access.csv
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||||
|
access_mvd_tcg_mtg_set_user,mvd.tcg.mtg.set.user,model_mvd_tcg_mtg_set,mvd_tcg_base.mvd_tcg_base_group_user,1,0,0,0
|
||||||
|
access_mvd_tcg_mtg_set_operator,mvd.tcg.mtg.set.operator,model_mvd_tcg_mtg_set,mvd_tcg_base.mvd_tcg_base_group_operator,1,0,0,0
|
||||||
|
access_mvd_tcg_mtg_set_manager,mvd.tcg.mtg.set.manager,model_mvd_tcg_mtg_set,mvd_tcg_base.mvd_tcg_base_group_manager,1,1,1,1
|
||||||
|
access_mvd_tcg_mtg_set_system,mvd.tcg.mtg.set.system,model_mvd_tcg_mtg_set,base.group_system,1,1,1,1
|
||||||
|
access_mvd_tcg_mtg_rarity_user,mvd.tcg.mtg.rarity.user,model_mvd_tcg_mtg_rarity,mvd_tcg_base.mvd_tcg_base_group_user,1,0,0,0
|
||||||
|
access_mvd_tcg_mtg_rarity_operator,mvd.tcg.mtg.rarity.operator,model_mvd_tcg_mtg_rarity,mvd_tcg_base.mvd_tcg_base_group_operator,1,0,0,0
|
||||||
|
access_mvd_tcg_mtg_rarity_manager,mvd.tcg.mtg.rarity.manager,model_mvd_tcg_mtg_rarity,mvd_tcg_base.mvd_tcg_base_group_manager,1,1,1,1
|
||||||
|
access_mvd_tcg_mtg_rarity_system,mvd.tcg.mtg.rarity.system,model_mvd_tcg_mtg_rarity,base.group_system,1,1,1,1
|
||||||
|
access_mvd_tcg_mtg_color_user,mvd.tcg.mtg.color.user,model_mvd_tcg_mtg_color,mvd_tcg_base.mvd_tcg_base_group_user,1,0,0,0
|
||||||
|
access_mvd_tcg_mtg_color_operator,mvd.tcg.mtg.color.operator,model_mvd_tcg_mtg_color,mvd_tcg_base.mvd_tcg_base_group_operator,1,0,0,0
|
||||||
|
access_mvd_tcg_mtg_color_manager,mvd.tcg.mtg.color.manager,model_mvd_tcg_mtg_color,mvd_tcg_base.mvd_tcg_base_group_manager,1,1,1,1
|
||||||
|
access_mvd_tcg_mtg_color_system,mvd.tcg.mtg.color.system,model_mvd_tcg_mtg_color,base.group_system,1,1,1,1
|
||||||
|
access_mvd_tcg_mtg_card_type_user,mvd.tcg.mtg.card.type.user,model_mvd_tcg_mtg_card_type,mvd_tcg_base.mvd_tcg_base_group_user,1,0,0,0
|
||||||
|
access_mvd_tcg_mtg_card_type_operator,mvd.tcg.mtg.card.type.operator,model_mvd_tcg_mtg_card_type,mvd_tcg_base.mvd_tcg_base_group_operator,1,0,0,0
|
||||||
|
access_mvd_tcg_mtg_card_type_manager,mvd.tcg.mtg.card.type.manager,model_mvd_tcg_mtg_card_type,mvd_tcg_base.mvd_tcg_base_group_manager,1,1,1,1
|
||||||
|
access_mvd_tcg_mtg_card_type_system,mvd.tcg.mtg.card.type.system,model_mvd_tcg_mtg_card_type,base.group_system,1,1,1,1
|
||||||
|
access_mvd_tcg_mtg_keyword_user,mvd.tcg.mtg.keyword.user,model_mvd_tcg_mtg_keyword,mvd_tcg_base.mvd_tcg_base_group_user,1,0,0,0
|
||||||
|
access_mvd_tcg_mtg_keyword_operator,mvd.tcg.mtg.keyword.operator,model_mvd_tcg_mtg_keyword,mvd_tcg_base.mvd_tcg_base_group_operator,1,0,0,0
|
||||||
|
access_mvd_tcg_mtg_keyword_manager,mvd.tcg.mtg.keyword.manager,model_mvd_tcg_mtg_keyword,mvd_tcg_base.mvd_tcg_base_group_manager,1,1,1,1
|
||||||
|
access_mvd_tcg_mtg_keyword_system,mvd.tcg.mtg.keyword.system,model_mvd_tcg_mtg_keyword,base.group_system,1,1,1,1
|
||||||
|
access_mvd_tcg_mtg_format_user,mvd.tcg.mtg.format.user,model_mvd_tcg_mtg_format,mvd_tcg_base.mvd_tcg_base_group_user,1,0,0,0
|
||||||
|
access_mvd_tcg_mtg_format_operator,mvd.tcg.mtg.format.operator,model_mvd_tcg_mtg_format,mvd_tcg_base.mvd_tcg_base_group_operator,1,0,0,0
|
||||||
|
access_mvd_tcg_mtg_format_manager,mvd.tcg.mtg.format.manager,model_mvd_tcg_mtg_format,mvd_tcg_base.mvd_tcg_base_group_manager,1,1,1,1
|
||||||
|
access_mvd_tcg_mtg_format_system,mvd.tcg.mtg.format.system,model_mvd_tcg_mtg_format,base.group_system,1,1,1,1
|
||||||
|
access_mvd_tcg_mtg_finish_user,mvd.tcg.mtg.finish.user,model_mvd_tcg_mtg_finish,mvd_tcg_base.mvd_tcg_base_group_user,1,0,0,0
|
||||||
|
access_mvd_tcg_mtg_finish_operator,mvd.tcg.mtg.finish.operator,model_mvd_tcg_mtg_finish,mvd_tcg_base.mvd_tcg_base_group_operator,1,0,0,0
|
||||||
|
access_mvd_tcg_mtg_finish_manager,mvd.tcg.mtg.finish.manager,model_mvd_tcg_mtg_finish,mvd_tcg_base.mvd_tcg_base_group_manager,1,1,1,1
|
||||||
|
access_mvd_tcg_mtg_finish_system,mvd.tcg.mtg.finish.system,model_mvd_tcg_mtg_finish,base.group_system,1,1,1,1
|
||||||
|
access_mvd_tcg_mtg_platform_user,mvd.tcg.mtg.platform.user,model_mvd_tcg_mtg_platform,mvd_tcg_base.mvd_tcg_base_group_user,1,0,0,0
|
||||||
|
access_mvd_tcg_mtg_platform_operator,mvd.tcg.mtg.platform.operator,model_mvd_tcg_mtg_platform,mvd_tcg_base.mvd_tcg_base_group_operator,1,0,0,0
|
||||||
|
access_mvd_tcg_mtg_platform_manager,mvd.tcg.mtg.platform.manager,model_mvd_tcg_mtg_platform,mvd_tcg_base.mvd_tcg_base_group_manager,1,1,1,1
|
||||||
|
access_mvd_tcg_mtg_platform_system,mvd.tcg.mtg.platform.system,model_mvd_tcg_mtg_platform,base.group_system,1,1,1,1
|
||||||
|
access_mvd_tcg_mtg_card_face_user,mvd.tcg.mtg.card.face.user,model_mvd_tcg_mtg_card_face,mvd_tcg_base.mvd_tcg_base_group_user,1,0,0,0
|
||||||
|
access_mvd_tcg_mtg_card_face_manager,mvd.tcg.mtg.card.face.manager,model_mvd_tcg_mtg_card_face,mvd_tcg_base.mvd_tcg_base_group_manager,1,1,1,1
|
||||||
|
access_mvd_tcg_mtg_card_face_system,mvd.tcg.mtg.card.face.system,model_mvd_tcg_mtg_card_face,base.group_system,1,1,1,1
|
||||||
|
access_mvd_tcg_mtg_card_legality_user,mvd.tcg.mtg.card.legality.user,model_mvd_tcg_mtg_card_legality,mvd_tcg_base.mvd_tcg_base_group_user,1,0,0,0
|
||||||
|
access_mvd_tcg_mtg_card_legality_manager,mvd.tcg.mtg.card.legality.manager,model_mvd_tcg_mtg_card_legality,mvd_tcg_base.mvd_tcg_base_group_manager,1,1,1,1
|
||||||
|
access_mvd_tcg_mtg_card_legality_system,mvd.tcg.mtg.card.legality.system,model_mvd_tcg_mtg_card_legality,base.group_system,1,1,1,1
|
||||||
|
BIN
static/src/img/card-symbols-report/0.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
static/src/img/card-symbols-report/1.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
static/src/img/card-symbols-report/10.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
static/src/img/card-symbols-report/100.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
static/src/img/card-symbols-report/1000000.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
static/src/img/card-symbols-report/11.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
static/src/img/card-symbols-report/12.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
static/src/img/card-symbols-report/13.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
static/src/img/card-symbols-report/14.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
static/src/img/card-symbols-report/15.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
static/src/img/card-symbols-report/16.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
static/src/img/card-symbols-report/17.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
static/src/img/card-symbols-report/18.png
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
static/src/img/card-symbols-report/19.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
static/src/img/card-symbols-report/2.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
static/src/img/card-symbols-report/20.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
static/src/img/card-symbols-report/2B.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
static/src/img/card-symbols-report/2G.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
static/src/img/card-symbols-report/2R.png
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
static/src/img/card-symbols-report/2U.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
static/src/img/card-symbols-report/2W.png
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
BIN
static/src/img/card-symbols-report/3.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
static/src/img/card-symbols-report/4.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
static/src/img/card-symbols-report/5.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
static/src/img/card-symbols-report/6.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
static/src/img/card-symbols-report/7.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
static/src/img/card-symbols-report/8.png
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
static/src/img/card-symbols-report/9.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
static/src/img/card-symbols-report/A.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
static/src/img/card-symbols-report/B.png
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
static/src/img/card-symbols-report/BG.png
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
static/src/img/card-symbols-report/BGP.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
static/src/img/card-symbols-report/BP.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
static/src/img/card-symbols-report/BR.png
Normal file
|
After Width: | Height: | Size: 6.0 KiB |
BIN
static/src/img/card-symbols-report/BRP.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
static/src/img/card-symbols-report/C.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
static/src/img/card-symbols-report/CB.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
static/src/img/card-symbols-report/CG.png
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
static/src/img/card-symbols-report/CHAOS.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
static/src/img/card-symbols-report/CP.png
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
static/src/img/card-symbols-report/CR.png
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
BIN
static/src/img/card-symbols-report/CU.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
static/src/img/card-symbols-report/CW.png
Normal file
|
After Width: | Height: | Size: 6.1 KiB |
BIN
static/src/img/card-symbols-report/D.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
static/src/img/card-symbols-report/E.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
static/src/img/card-symbols-report/G.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
static/src/img/card-symbols-report/GP.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
static/src/img/card-symbols-report/GU.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
static/src/img/card-symbols-report/GUP.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
static/src/img/card-symbols-report/GW.png
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
BIN
static/src/img/card-symbols-report/GWP.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
static/src/img/card-symbols-report/H.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
static/src/img/card-symbols-report/HALF.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
static/src/img/card-symbols-report/HR.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
static/src/img/card-symbols-report/HW.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
static/src/img/card-symbols-report/INFINITY.png
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
static/src/img/card-symbols-report/L.png
Normal file
|
After Width: | Height: | Size: 650 B |
BIN
static/src/img/card-symbols-report/P.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
static/src/img/card-symbols-report/PW.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
static/src/img/card-symbols-report/Q.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
static/src/img/card-symbols-report/R.png
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
static/src/img/card-symbols-report/RG.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
static/src/img/card-symbols-report/RGP.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
static/src/img/card-symbols-report/RP.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
static/src/img/card-symbols-report/RW.png
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
BIN
static/src/img/card-symbols-report/RWP.png
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
static/src/img/card-symbols-report/S.png
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
BIN
static/src/img/card-symbols-report/T.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
static/src/img/card-symbols-report/TK.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
static/src/img/card-symbols-report/U.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
static/src/img/card-symbols-report/UB.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
static/src/img/card-symbols-report/UBP.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
static/src/img/card-symbols-report/UP.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
static/src/img/card-symbols-report/UR.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
static/src/img/card-symbols-report/URP.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
static/src/img/card-symbols-report/W.png
Normal file
|
After Width: | Height: | Size: 6.6 KiB |
BIN
static/src/img/card-symbols-report/WB.png
Normal file
|
After Width: | Height: | Size: 6.1 KiB |
BIN
static/src/img/card-symbols-report/WBP.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
static/src/img/card-symbols-report/WP.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
static/src/img/card-symbols-report/WU.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
static/src/img/card-symbols-report/WUP.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
static/src/img/card-symbols-report/X.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
static/src/img/card-symbols-report/Y.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
static/src/img/card-symbols-report/Z.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
1
static/src/img/card-symbols/0.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><g fill='none'><circle fill='#CAC5C0' cx='50' cy='50' r='50'/><path d='M23 52.428c0-9.787 2.061-18.813 6.191-27.072 5.117-10.236 12.123-15.355 21.012-15.355 8.797 0 15.666 4.359 20.605 13.064 4.127 7.186 6.191 15.537 6.191 25.051 0 9.881-2.064 18.814-6.191 26.803-5.031 10.058-12.035 15.081-21.011 15.081-8.531 0-15.305-4.307-20.336-12.926-4.308-7.361-6.461-15.576-6.461-24.646zm11.309-4.444c0 12.926 1.93 22.984 5.795 30.168 2.691 5.025 6.146 7.541 10.367 7.541 10.146 0 15.221-10.506 15.221-31.518 0-9.244-.809-17.059-2.422-23.434-2.785-10.504-7.498-15.76-14.145-15.76-9.879 0-14.816 10.15-14.816 30.443v2.56z' fill='#0D0F0F'/></g></svg>
|
||||||
|
After Width: | Height: | Size: 702 B |
1
static/src/img/card-symbols/1.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><g transform='translate(0 -1)' fill='none'><circle fill='#CAC5C0' cx='50' cy='50.998' r='50'/><path d='M55.685 11.001v64.108c0 7.671 3.226 11.504 9.687 11.504h1.684v4.388h-34.111v-4.388h2.141c6.247 0 9.369-3.833 9.369-11.504v-42.054c0-7.758-2.697-11.643-8.081-11.643h-3.429v-4.247h1.237c6.66 0 12.691-2.057 18.08-6.165l3.423.001z' fill='#0D0F0F'/></g></svg>
|
||||||
|
After Width: | Height: | Size: 420 B |