🎉 Initialize module repository
This commit is contained in:
2
wizards/__init__.py
Normal file
2
wizards/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from . import mvd_tcg_add_to_deck
|
||||
from . import mvd_tcg_deck_text_transfer
|
||||
114
wizards/mvd_tcg_add_to_deck.py
Normal file
114
wizards/mvd_tcg_add_to_deck.py
Normal file
@@ -0,0 +1,114 @@
|
||||
"""Transient helpers for adding cards to game-neutral decks."""
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class MvdTcgAddToDeck(models.TransientModel):
|
||||
"""Add one reference card to one deck board."""
|
||||
|
||||
_name = "mvd.tcg.add.to.deck"
|
||||
_description = "Add TCG Card To Deck"
|
||||
|
||||
card_id = fields.Many2one(
|
||||
"mvd.tcg.card",
|
||||
required=True,
|
||||
readonly=True,
|
||||
)
|
||||
game_id = fields.Many2one(
|
||||
"mvd.tcg.game",
|
||||
readonly=True,
|
||||
)
|
||||
deck_id = fields.Many2one(
|
||||
"mvd.tcg.deck",
|
||||
required=True,
|
||||
domain="[('active', '=', True), ('game_id', '=', game_id)]",
|
||||
)
|
||||
board_id = fields.Many2one(
|
||||
"mvd.tcg.deck.board",
|
||||
required=True,
|
||||
domain="[('deck_id', '=', deck_id)]",
|
||||
)
|
||||
quantity = fields.Integer(required=True, default=1)
|
||||
role_ids = fields.Many2many(
|
||||
"mvd.tcg.deck.role",
|
||||
string="Roles",
|
||||
)
|
||||
note = fields.Char()
|
||||
|
||||
@api.model
|
||||
def default_get(self, field_names):
|
||||
"""Prefill the wizard from the active card context.
|
||||
|
||||
Args:
|
||||
field_names: Requested wizard fields.
|
||||
|
||||
Returns:
|
||||
dict: Initial field values.
|
||||
"""
|
||||
defaults = super().default_get(field_names)
|
||||
card_id = defaults.get("card_id") or self.env.context.get("active_id")
|
||||
if self.env.context.get("active_model") == "mvd.tcg.card" and card_id:
|
||||
card = self.env["mvd.tcg.card"].browse(card_id).exists()
|
||||
if card:
|
||||
defaults["card_id"] = card.id
|
||||
defaults["game_id"] = card.game_id.id
|
||||
deck_id = defaults.get("deck_id") or self.env.context.get("default_deck_id")
|
||||
if deck_id:
|
||||
deck = self.env["mvd.tcg.deck"].browse(deck_id).exists()
|
||||
if deck:
|
||||
defaults["deck_id"] = deck.id
|
||||
defaults.setdefault("game_id", deck.game_id.id)
|
||||
board_id = defaults.get("board_id") or self.env.context.get("default_board_id")
|
||||
if board_id:
|
||||
board = self.env["mvd.tcg.deck.board"].browse(board_id).exists()
|
||||
if board:
|
||||
defaults["board_id"] = board.id
|
||||
defaults.setdefault("deck_id", board.deck_id.id)
|
||||
defaults.setdefault("game_id", board.deck_id.game_id.id)
|
||||
return defaults
|
||||
|
||||
@api.onchange("deck_id")
|
||||
def _onchange_deck_id(self):
|
||||
"""Preselect the most likely target board for the chosen deck.
|
||||
|
||||
Returns:
|
||||
None: The method updates wizard fields in place.
|
||||
"""
|
||||
if not self.deck_id:
|
||||
self.board_id = False
|
||||
return
|
||||
preferred_board = self.deck_id.board_ids.filtered(
|
||||
lambda board: board.code == "mainboard"
|
||||
)[:1]
|
||||
self.board_id = preferred_board or self.deck_id.board_ids[:1]
|
||||
|
||||
def action_add_to_deck(self):
|
||||
"""Create or increment a deck line for the selected card.
|
||||
|
||||
Returns:
|
||||
dict: Window action for the updated deck.
|
||||
|
||||
Raises:
|
||||
UserError: If the wizard lacks a valid card context.
|
||||
"""
|
||||
self.ensure_one()
|
||||
if not self.card_id:
|
||||
raise UserError(_("Select a card first."))
|
||||
|
||||
self.deck_id._mvd_tcg_add_card_to_board(
|
||||
self.card_id,
|
||||
self.board_id,
|
||||
quantity=self.quantity,
|
||||
role_ids=self.role_ids,
|
||||
note=self.note,
|
||||
)
|
||||
|
||||
return {
|
||||
"type": "ir.actions.act_window",
|
||||
"name": _("Deck"),
|
||||
"res_model": "mvd.tcg.deck",
|
||||
"view_mode": "form",
|
||||
"res_id": self.deck_id.id,
|
||||
"target": "current",
|
||||
}
|
||||
335
wizards/mvd_tcg_deck_text_transfer.py
Normal file
335
wizards/mvd_tcg_deck_text_transfer.py
Normal file
@@ -0,0 +1,335 @@
|
||||
"""Import and export decklists as plain text."""
|
||||
|
||||
import re
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class MvdTcgDeckTextTransfer(models.TransientModel):
|
||||
"""Round-trip decklists through a text-based import/export wizard."""
|
||||
|
||||
_name = "mvd.tcg.deck.text.transfer"
|
||||
_description = "TCG Deck Text Transfer"
|
||||
|
||||
_LINE_PATTERN = re.compile(
|
||||
r"^(?:(?P<qty>\d+)\s*x?\s+)?(?P<name>.+?)"
|
||||
r"(?:\s+\[(?P<bracket_ref>[^\]]+)\]"
|
||||
r"|\s+\((?P<print_set>[A-Za-z0-9]+)\s+(?P<print_collector>[^)]+)\))?$"
|
||||
)
|
||||
_BOARD_ALIASES = {
|
||||
"mainboard": "mainboard",
|
||||
"main": "mainboard",
|
||||
"command zone": "command_zone",
|
||||
"commandzone": "command_zone",
|
||||
"command": "command_zone",
|
||||
"commander": "command_zone",
|
||||
"sideboard": "sideboard",
|
||||
"side": "sideboard",
|
||||
"maybeboard": "maybeboard",
|
||||
"maybe": "maybeboard",
|
||||
}
|
||||
|
||||
deck_id = fields.Many2one(
|
||||
"mvd.tcg.deck",
|
||||
required=True,
|
||||
readonly=True,
|
||||
)
|
||||
game_id = fields.Many2one(
|
||||
"mvd.tcg.game",
|
||||
related="deck_id.game_id",
|
||||
readonly=True,
|
||||
)
|
||||
operation = fields.Selection(
|
||||
[
|
||||
("import", "Import"),
|
||||
("export", "Export"),
|
||||
],
|
||||
required=True,
|
||||
default="import",
|
||||
readonly=True,
|
||||
)
|
||||
replace_existing = fields.Boolean(
|
||||
string="Replace Existing Deck Lines",
|
||||
help="Clear all current deck entries before importing the provided deck list.",
|
||||
)
|
||||
line_text = fields.Text(
|
||||
string="Deck List",
|
||||
required=True,
|
||||
)
|
||||
|
||||
@api.model
|
||||
def default_get(self, field_names):
|
||||
"""Initialize the wizard from the current deck context.
|
||||
|
||||
Args:
|
||||
field_names: Requested wizard fields.
|
||||
|
||||
Returns:
|
||||
dict: Prefilled wizard values.
|
||||
"""
|
||||
defaults = super().default_get(field_names)
|
||||
deck_id = defaults.get("deck_id") or self.env.context.get("default_deck_id")
|
||||
if not deck_id and self.env.context.get("active_model") == "mvd.tcg.deck":
|
||||
deck_id = self.env.context.get("active_id")
|
||||
if deck_id:
|
||||
deck = self.env["mvd.tcg.deck"].browse(deck_id).exists()
|
||||
if deck:
|
||||
defaults["deck_id"] = deck.id
|
||||
if (
|
||||
defaults.get("operation") == "export"
|
||||
and defaults.get("deck_id")
|
||||
and "line_text" in field_names
|
||||
):
|
||||
deck = self.env["mvd.tcg.deck"].browse(defaults["deck_id"]).exists()
|
||||
if deck:
|
||||
defaults["line_text"] = self._build_export_text(deck)
|
||||
return defaults
|
||||
|
||||
def _normalize_board_heading(self, raw_heading):
|
||||
"""Resolve one free-form board heading to a canonical code.
|
||||
|
||||
Args:
|
||||
raw_heading: Raw heading text from the import payload.
|
||||
|
||||
Returns:
|
||||
str | bool: Canonical board code or ``False`` when unmatched.
|
||||
"""
|
||||
heading = (raw_heading or "").strip().lstrip("#").strip().lower()
|
||||
heading = re.sub(r"[^a-z\s_]", "", heading).replace("_", " ")
|
||||
heading = re.sub(r"\s+", " ", heading)
|
||||
return self.env["mvd.tcg.deck"]._mvd_tcg_normalize_board_code(
|
||||
self._BOARD_ALIASES.get(heading) or heading
|
||||
)
|
||||
|
||||
def _build_export_text(self, deck):
|
||||
"""Render one deck as a deterministic plain-text list.
|
||||
|
||||
Args:
|
||||
deck: Deck record to serialize.
|
||||
|
||||
Returns:
|
||||
str: Plain-text export payload with board headings.
|
||||
"""
|
||||
exported_sections = []
|
||||
for board in deck.board_ids.sorted(key=lambda current_board: (current_board.sequence, current_board.id)):
|
||||
if not board.line_ids:
|
||||
continue
|
||||
exported_sections.append(
|
||||
f"# {deck._mvd_tcg_get_board_export_heading(board)}"
|
||||
)
|
||||
for line in board.line_ids.sorted(
|
||||
key=lambda current_line: (
|
||||
current_line.primary_role_sequence,
|
||||
current_line.board_sequence,
|
||||
current_line.sequence,
|
||||
current_line.id,
|
||||
)
|
||||
):
|
||||
card_name = line.card_id.display_name
|
||||
print_hint = self._get_export_print_hint(line.card_id)
|
||||
exported_sections.append(
|
||||
f"{line.quantity} {card_name}{print_hint}"
|
||||
)
|
||||
exported_sections.append("")
|
||||
return "\n".join(exported_sections).strip()
|
||||
|
||||
@classmethod
|
||||
def _normalize_import_reference(cls, match):
|
||||
"""Convert one parsed optional print reference into the canonical key.
|
||||
|
||||
Args:
|
||||
match: Regex match produced by ``_LINE_PATTERN``.
|
||||
|
||||
Returns:
|
||||
str: Canonical import reference such as ``tdm:101``.
|
||||
"""
|
||||
bracket_reference = (match.group("bracket_ref") or "").strip().lower()
|
||||
if bracket_reference:
|
||||
return bracket_reference
|
||||
|
||||
print_set = (match.group("print_set") or "").strip().lower()
|
||||
print_collector = (match.group("print_collector") or "").strip().lower()
|
||||
if print_set and print_collector:
|
||||
return f"{print_set}:{print_collector}"
|
||||
return ""
|
||||
|
||||
@classmethod
|
||||
def _get_export_print_hint(cls, card):
|
||||
"""Return one human-readable print hint for export when available.
|
||||
|
||||
Args:
|
||||
card: Card record being exported.
|
||||
|
||||
Returns:
|
||||
str: Optional print hint such as `` (TDM 101)``.
|
||||
"""
|
||||
external_ref = (card.external_ref or "").strip()
|
||||
if ":" not in external_ref:
|
||||
return ""
|
||||
|
||||
set_code, collector_number = external_ref.split(":", 1)
|
||||
set_code = set_code.strip().upper()
|
||||
collector_number = collector_number.strip()
|
||||
if not set_code or not collector_number:
|
||||
return ""
|
||||
return f" ({set_code} {collector_number})"
|
||||
|
||||
def _find_card_for_import(self, deck, raw_name, raw_ref=False):
|
||||
"""Resolve one import line to one unique reference card.
|
||||
|
||||
Args:
|
||||
deck: Target deck record.
|
||||
raw_name: Human-readable imported card name.
|
||||
raw_ref: Optional explicit reference such as ``tdm:101``.
|
||||
|
||||
Returns:
|
||||
mvd.tcg.card: Unique matching card record.
|
||||
|
||||
Raises:
|
||||
UserError: If no unique card could be resolved.
|
||||
"""
|
||||
Card = self.env["mvd.tcg.card"]
|
||||
base_domain = [
|
||||
("game_id", "=", deck.game_id.id),
|
||||
("active", "=", True),
|
||||
]
|
||||
external_ref = (raw_ref or "").strip().lower()
|
||||
card_name = (raw_name or "").strip()
|
||||
|
||||
if external_ref:
|
||||
exact_by_ref = Card.search(base_domain + [("external_ref", "=", external_ref)])
|
||||
if len(exact_by_ref) == 1:
|
||||
return exact_by_ref
|
||||
if len(exact_by_ref) > 1:
|
||||
raise UserError(
|
||||
_(
|
||||
"The explicit reference '%(reference)s' matches multiple cards. Narrow the import source first."
|
||||
)
|
||||
% {"reference": external_ref}
|
||||
)
|
||||
|
||||
exact_current_lang = Card.search(base_domain + [("name", "=", card_name)])
|
||||
exact_english = Card.with_context(lang="en_US").search(
|
||||
base_domain + [("name", "=", card_name)]
|
||||
)
|
||||
exact_matches = (exact_current_lang | exact_english)
|
||||
if len(exact_matches) == 1:
|
||||
return exact_matches
|
||||
if len(exact_matches) > 1:
|
||||
raise UserError(
|
||||
_(
|
||||
"The card '%(name)s' matches multiple printings. Add a print hint such as '(TDM 101)' or pick the card directly in the deck builder."
|
||||
)
|
||||
% {"name": card_name}
|
||||
)
|
||||
|
||||
fuzzy_current_lang = Card.search(base_domain + [("name", "=ilike", card_name)])
|
||||
fuzzy_english = Card.with_context(lang="en_US").search(
|
||||
base_domain + [("name", "=ilike", card_name)]
|
||||
)
|
||||
fuzzy_matches = (fuzzy_current_lang | fuzzy_english)
|
||||
if len(fuzzy_matches) == 1:
|
||||
return fuzzy_matches
|
||||
if len(fuzzy_matches) > 1:
|
||||
raise UserError(
|
||||
_(
|
||||
"The card '%(name)s' matches multiple printings. Add a print hint such as '(TDM 101)' or use a more specific card name."
|
||||
)
|
||||
% {"name": card_name}
|
||||
)
|
||||
raise UserError(_("No active card named '%s' exists in the current game.") % card_name)
|
||||
|
||||
def _parse_import_payload(self):
|
||||
"""Parse the current text payload into structured import commands.
|
||||
|
||||
Returns:
|
||||
list[dict]: Parsed commands with board, quantity and card lookup data.
|
||||
|
||||
Raises:
|
||||
UserError: If the text payload contains invalid lines.
|
||||
"""
|
||||
self.ensure_one()
|
||||
default_board = self.deck_id._mvd_tcg_get_board_by_code("mainboard") or self.deck_id.board_ids[:1]
|
||||
current_board_code = default_board.code if default_board else False
|
||||
parsed_lines = []
|
||||
parsing_errors = []
|
||||
|
||||
for line_number, raw_line in enumerate((self.line_text or "").splitlines(), start=1):
|
||||
stripped_line = raw_line.strip()
|
||||
if not stripped_line:
|
||||
continue
|
||||
if stripped_line.startswith("//"):
|
||||
continue
|
||||
normalized_heading = self._normalize_board_heading(stripped_line)
|
||||
if normalized_heading:
|
||||
current_board_code = normalized_heading
|
||||
continue
|
||||
if stripped_line.startswith("#"):
|
||||
normalized_heading = self._normalize_board_heading(stripped_line[1:])
|
||||
if normalized_heading:
|
||||
current_board_code = normalized_heading
|
||||
continue
|
||||
match = self._LINE_PATTERN.match(stripped_line)
|
||||
if not match or not current_board_code:
|
||||
parsing_errors.append(_("Line %(line)s could not be parsed: %(value)s") % {
|
||||
"line": line_number,
|
||||
"value": stripped_line,
|
||||
})
|
||||
continue
|
||||
parsed_lines.append(
|
||||
{
|
||||
"line_number": line_number,
|
||||
"board_code": current_board_code,
|
||||
"quantity": int(match.group("qty") or 1),
|
||||
"name": (match.group("name") or "").strip(),
|
||||
"external_ref": self._normalize_import_reference(match),
|
||||
}
|
||||
)
|
||||
|
||||
if parsing_errors:
|
||||
raise UserError("\n".join(parsing_errors))
|
||||
return parsed_lines
|
||||
|
||||
def action_apply_text_transfer(self):
|
||||
"""Run the selected import or export operation.
|
||||
|
||||
Returns:
|
||||
dict: Window action returning to the current deck.
|
||||
"""
|
||||
self.ensure_one()
|
||||
if self.operation == "export":
|
||||
return {"type": "ir.actions.act_window_close"}
|
||||
|
||||
deck = self.deck_id
|
||||
deck._mvd_tcg_seed_default_boards()
|
||||
board_map = {board.code: board for board in deck.board_ids}
|
||||
parsed_lines = self._parse_import_payload()
|
||||
if self.replace_existing:
|
||||
deck.line_ids.unlink()
|
||||
|
||||
for parsed_line in parsed_lines:
|
||||
target_board = board_map.get(parsed_line["board_code"])
|
||||
if not target_board:
|
||||
raise UserError(
|
||||
_("No board exists for code '%s'.") % parsed_line["board_code"]
|
||||
)
|
||||
card = self._find_card_for_import(
|
||||
deck,
|
||||
parsed_line["name"],
|
||||
raw_ref=parsed_line["external_ref"],
|
||||
)
|
||||
deck._mvd_tcg_add_card_to_board(
|
||||
card,
|
||||
target_board,
|
||||
quantity=parsed_line["quantity"],
|
||||
)
|
||||
|
||||
return {
|
||||
"type": "ir.actions.act_window",
|
||||
"name": deck.display_name,
|
||||
"res_model": "mvd.tcg.deck",
|
||||
"view_mode": "form",
|
||||
"res_id": deck.id,
|
||||
"target": "current",
|
||||
}
|
||||
Reference in New Issue
Block a user