🎉 Initialize module repository
This commit is contained in:
6
models/__init__.py
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
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
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
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
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
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
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.",
|
||||
)
|
||||
Reference in New Issue
Block a user