🎉 Initialize module repository
This commit is contained in:
329
models/mvd_tcg_deck_line.py
Normal file
329
models/mvd_tcg_deck_line.py
Normal file
@@ -0,0 +1,329 @@
|
||||
"""MTG-specific deck line extensions."""
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
MTG_DECK_LEGALITY_SELECTION = [
|
||||
("unknown", "Unknown"),
|
||||
("legal", "Legal"),
|
||||
("restricted", "Restricted"),
|
||||
("not_legal", "Not Legal"),
|
||||
("banned", "Banned"),
|
||||
]
|
||||
|
||||
|
||||
class MvdTcgDeckLine(models.Model):
|
||||
"""Expose MTG card metadata directly on deck lines."""
|
||||
|
||||
_inherit = "mvd.tcg.deck.line"
|
||||
|
||||
mtg_set_id = fields.Many2one(
|
||||
related="card_id.mtg_set_id",
|
||||
string="Set",
|
||||
readonly=True,
|
||||
store=True,
|
||||
index=True,
|
||||
)
|
||||
mtg_collector_number = fields.Char(
|
||||
related="card_id.mtg_collector_number",
|
||||
string="Collector Number",
|
||||
readonly=True,
|
||||
store=True,
|
||||
index=True,
|
||||
)
|
||||
mtg_mana_cost = fields.Char(
|
||||
related="card_id.mtg_mana_cost",
|
||||
string="Mana Cost",
|
||||
readonly=True,
|
||||
store=True,
|
||||
)
|
||||
mtg_mana_value = fields.Float(
|
||||
related="card_id.mtg_mana_value",
|
||||
string="Mana Value",
|
||||
readonly=True,
|
||||
store=True,
|
||||
index=True,
|
||||
)
|
||||
mtg_type_line = fields.Char(
|
||||
related="card_id.mtg_type_line",
|
||||
string="Type Line",
|
||||
readonly=True,
|
||||
)
|
||||
mtg_primary_card_type_id = fields.Many2one(
|
||||
"mvd.tcg.mtg.card.type",
|
||||
string="Primary Card Type",
|
||||
compute="_compute_mtg_primary_card_type",
|
||||
store=True,
|
||||
readonly=True,
|
||||
index=True,
|
||||
)
|
||||
mtg_legality_status = fields.Selection(
|
||||
selection=MTG_DECK_LEGALITY_SELECTION,
|
||||
string="Legality Status",
|
||||
compute="_compute_mtg_rule_flags",
|
||||
store=True,
|
||||
readonly=True,
|
||||
index=True,
|
||||
)
|
||||
mtg_legality_ok = fields.Boolean(
|
||||
string="Legality OK",
|
||||
compute="_compute_mtg_rule_flags",
|
||||
store=True,
|
||||
readonly=True,
|
||||
)
|
||||
mtg_color_identity_violation = fields.Boolean(
|
||||
string="Color Identity Violation",
|
||||
compute="_compute_mtg_rule_flags",
|
||||
store=True,
|
||||
readonly=True,
|
||||
)
|
||||
mtg_singleton_violation = fields.Boolean(
|
||||
string="Singleton Violation",
|
||||
compute="_compute_mtg_rule_flags",
|
||||
store=True,
|
||||
readonly=True,
|
||||
)
|
||||
mtg_issue_count = fields.Integer(
|
||||
string="Issue Count",
|
||||
compute="_compute_mtg_rule_flags",
|
||||
store=True,
|
||||
readonly=True,
|
||||
)
|
||||
|
||||
@api.depends(
|
||||
"card_id.mtg_card_type_ids",
|
||||
"card_id.mtg_card_type_ids.sequence",
|
||||
"card_id.mtg_card_type_ids.name",
|
||||
)
|
||||
def _compute_mtg_primary_card_type(self):
|
||||
"""Expose one stable primary card type for grouping and sorting.
|
||||
|
||||
Returns:
|
||||
None: The compute updates records in place.
|
||||
"""
|
||||
for line in self:
|
||||
card_type = line.card_id.mtg_card_type_ids.sorted(
|
||||
key=lambda current_type: (
|
||||
current_type.sequence,
|
||||
current_type.name or "",
|
||||
current_type.id,
|
||||
)
|
||||
)[:1]
|
||||
line.mtg_primary_card_type_id = card_type
|
||||
|
||||
@api.depends(
|
||||
"board_id.code",
|
||||
"board_id.include_in_total",
|
||||
"deck_id.is_mtg_deck",
|
||||
"deck_id.mtg_format_id",
|
||||
"deck_id.mtg_format_id.code",
|
||||
"deck_id.mtg_color_identity_signature",
|
||||
"deck_id.line_ids.quantity",
|
||||
"deck_id.line_ids.board_id",
|
||||
"deck_id.line_ids.board_id.code",
|
||||
"deck_id.line_ids.board_id.include_in_total",
|
||||
"deck_id.line_ids.card_id",
|
||||
"deck_id.line_ids.card_id.mtg_oracle_id",
|
||||
"deck_id.line_ids.card_id.mtg_type_line",
|
||||
"deck_id.line_ids.card_id.mtg_oracle_text",
|
||||
"card_id.mtg_oracle_id",
|
||||
"card_id.mtg_color_identity_signature",
|
||||
"card_id.mtg_legality_ids.status",
|
||||
"card_id.mtg_legality_ids.format_code",
|
||||
)
|
||||
def _compute_mtg_rule_flags(self):
|
||||
"""Compute line-level MTG rule signals for the current deck context.
|
||||
|
||||
Returns:
|
||||
None: The compute updates records in place.
|
||||
"""
|
||||
singleton_violation_by_line = {}
|
||||
legality_status_by_line = {}
|
||||
legality_ok_by_line = {}
|
||||
color_violation_by_line = {}
|
||||
|
||||
mtg_decks = self.mapped("deck_id").filtered("is_mtg_deck")
|
||||
for deck in mtg_decks:
|
||||
format_code = deck.mtg_format_id.code or ""
|
||||
commander_identity = set(deck.mtg_color_identity_signature or "")
|
||||
singleton_groups = []
|
||||
candidate_lines = deck.line_ids.filtered(
|
||||
lambda line: line.card_id
|
||||
and line.card_id.game_id.code == "mtg"
|
||||
and line.board_id.include_in_total
|
||||
)
|
||||
candidate_entries = []
|
||||
for line in candidate_lines:
|
||||
card = line.card_id.with_context(lang="en_US")
|
||||
if self._mtg_is_singleton_exempt(card):
|
||||
continue
|
||||
singleton_aliases = set(card.mtg_get_singleton_key_aliases())
|
||||
if not singleton_aliases:
|
||||
singleton_aliases = {card.external_ref or str(card.id)}
|
||||
candidate_entries.append(
|
||||
{
|
||||
"line": line,
|
||||
"aliases": singleton_aliases,
|
||||
}
|
||||
)
|
||||
|
||||
remaining_entries = list(candidate_entries)
|
||||
while remaining_entries:
|
||||
seed_entry = remaining_entries.pop(0)
|
||||
grouped_lines = [seed_entry["line"]]
|
||||
grouped_aliases = set(seed_entry["aliases"])
|
||||
did_merge = True
|
||||
while did_merge:
|
||||
did_merge = False
|
||||
unmatched_entries = []
|
||||
for entry in remaining_entries:
|
||||
if grouped_aliases & entry["aliases"]:
|
||||
grouped_lines.append(entry["line"])
|
||||
grouped_aliases |= entry["aliases"]
|
||||
did_merge = True
|
||||
else:
|
||||
unmatched_entries.append(entry)
|
||||
remaining_entries = unmatched_entries
|
||||
singleton_groups.append(grouped_lines)
|
||||
|
||||
for grouped_lines in singleton_groups:
|
||||
total_quantity = sum(line.quantity for line in grouped_lines)
|
||||
has_violation = total_quantity > 1
|
||||
for line in grouped_lines:
|
||||
singleton_violation_by_line[line.id] = has_violation
|
||||
|
||||
for line in deck.line_ids:
|
||||
if not line.card_id or line.card_id.game_id.code != "mtg":
|
||||
legality_status_by_line[line.id] = "unknown"
|
||||
legality_ok_by_line[line.id] = True
|
||||
color_violation_by_line[line.id] = False
|
||||
continue
|
||||
|
||||
legality_status = self._mtg_get_card_legality_status(
|
||||
line.card_id,
|
||||
format_code,
|
||||
)
|
||||
legality_status_by_line[line.id] = legality_status
|
||||
legality_ok_by_line[line.id] = legality_status not in {
|
||||
"banned",
|
||||
"not_legal",
|
||||
}
|
||||
if legality_status == "restricted" and line.quantity > 1:
|
||||
legality_ok_by_line[line.id] = False
|
||||
|
||||
line_identity = set(line.card_id.mtg_color_identity_signature or "")
|
||||
color_violation_by_line[line.id] = bool(
|
||||
commander_identity
|
||||
and line.board_id.code != "command_zone"
|
||||
and line_identity - commander_identity
|
||||
)
|
||||
|
||||
for line in self:
|
||||
legality_status = legality_status_by_line.get(line.id, "unknown")
|
||||
legality_ok = legality_ok_by_line.get(line.id, True)
|
||||
color_violation = color_violation_by_line.get(line.id, False)
|
||||
singleton_violation = singleton_violation_by_line.get(line.id, False)
|
||||
|
||||
line.mtg_legality_status = legality_status
|
||||
line.mtg_legality_ok = legality_ok
|
||||
line.mtg_color_identity_violation = color_violation
|
||||
line.mtg_singleton_violation = singleton_violation
|
||||
line.mtg_issue_count = sum(
|
||||
bool(flag)
|
||||
for flag in (
|
||||
not legality_ok,
|
||||
color_violation,
|
||||
singleton_violation,
|
||||
)
|
||||
)
|
||||
|
||||
@api.model
|
||||
def _mtg_get_card_legality_status(self, card, format_code):
|
||||
"""Return the legality status of one card for one MTG format.
|
||||
|
||||
Args:
|
||||
card: MTG card record.
|
||||
format_code: Canonical MTG format code such as ``commander``.
|
||||
|
||||
Returns:
|
||||
str: One status from ``MTG_DECK_LEGALITY_SELECTION``.
|
||||
"""
|
||||
if not card or not format_code:
|
||||
return "unknown"
|
||||
legality = card.mtg_legality_ids.filtered(
|
||||
lambda current_legality: current_legality.format_code == format_code
|
||||
)[:1]
|
||||
return legality.status if legality else "unknown"
|
||||
|
||||
@api.model
|
||||
def _mtg_is_singleton_exempt(self, card):
|
||||
"""Return whether a card should bypass singleton checks.
|
||||
|
||||
Args:
|
||||
card: MTG card record in a stable language context.
|
||||
|
||||
Returns:
|
||||
bool: ``True`` when the card can legally appear multiple times.
|
||||
"""
|
||||
return bool(card and card.mtg_allows_unlimited_copies())
|
||||
|
||||
def _mvd_tcg_validate_move_to_board(self, target_board):
|
||||
"""Block duplicate MTG singleton cards during board moves."""
|
||||
self.ensure_one()
|
||||
result = super()._mvd_tcg_validate_move_to_board(target_board)
|
||||
deck = self.deck_id
|
||||
if (
|
||||
not deck.is_mtg_deck
|
||||
or not target_board.include_in_total
|
||||
or not deck._mtg_format_enforces_singleton()
|
||||
):
|
||||
return result
|
||||
|
||||
english_card = self.card_id.with_context(lang="en_US")
|
||||
conflict_lines = deck._mtg_get_singleton_conflict_lines(
|
||||
english_card,
|
||||
excluding_line=self,
|
||||
)
|
||||
total_quantity = self.quantity + sum(conflict_lines.mapped("quantity"))
|
||||
if total_quantity <= 1:
|
||||
return result
|
||||
|
||||
conflicting_names = ", ".join(conflict_lines.mapped("card_id.display_name")[:3])
|
||||
raise UserError(
|
||||
_(
|
||||
"Commander- and Brawl-style decks can include only one copy of the same card across all styles and printings. "
|
||||
"Moving this line would conflict with: %(cards)s"
|
||||
)
|
||||
% {"cards": conflicting_names or english_card.display_name}
|
||||
)
|
||||
|
||||
def action_move_to_mtg_command_zone(self):
|
||||
"""Move the current line to the MTG command zone."""
|
||||
self.ensure_one()
|
||||
return self._mvd_tcg_move_to_board("command_zone")
|
||||
|
||||
def action_move_to_mtg_mainboard(self):
|
||||
"""Move the current line to the MTG mainboard."""
|
||||
self.ensure_one()
|
||||
return self._mvd_tcg_move_to_board("mainboard")
|
||||
|
||||
def action_move_to_mtg_sideboard(self):
|
||||
"""Move the current line to the MTG sideboard."""
|
||||
self.ensure_one()
|
||||
return self._mvd_tcg_move_to_board("sideboard")
|
||||
|
||||
def action_move_to_mtg_maybeboard(self):
|
||||
"""Move the current line to the MTG maybeboard."""
|
||||
self.ensure_one()
|
||||
return self._mvd_tcg_move_to_board("maybeboard")
|
||||
|
||||
def _mvd_tcg_get_card_form_view_xmlid(self):
|
||||
"""Prefer the MTG-specific card form for MTG deck lines.
|
||||
|
||||
Returns:
|
||||
str: Stable XMLID for the preferred card form view.
|
||||
"""
|
||||
self.ensure_one()
|
||||
if self.game_id.code == "mtg":
|
||||
return "mvd_tcg_mtg.mvd_tcg_mtg_card_view_form"
|
||||
return super()._mvd_tcg_get_card_form_view_xmlid()
|
||||
Reference in New Issue
Block a user