🎉 Initialize module repository

This commit is contained in:
Marc Wempe
2026-04-03 23:08:57 +02:00
commit d81e8a87e3
25 changed files with 4584 additions and 0 deletions

2
models/__init__.py Normal file
View File

@@ -0,0 +1,2 @@
from . import mvd_tcg_card
from . import mvd_tcg_deck

67
models/mvd_tcg_card.py Normal file
View File

@@ -0,0 +1,67 @@
"""Deck integration on top of game-neutral card references."""
from odoo import _, api, fields, models
class MvdTcgCard(models.Model):
"""Extend cards with deck usage helpers."""
_inherit = "mvd.tcg.card"
deck_line_ids = fields.One2many(
"mvd.tcg.deck.line",
"card_id",
string="Deck Lines",
readonly=True,
)
deck_count = fields.Integer(
compute="_compute_deck_count",
readonly=True,
)
@api.depends("deck_line_ids.deck_id")
def _compute_deck_count(self):
"""Compute how many distinct decks reference each card.
Returns:
None: The method updates records in place.
"""
for card in self:
card.deck_count = len(card.deck_line_ids.mapped("deck_id"))
def action_open_decks(self):
"""Open all decks that reference the current card.
Returns:
dict: Window action filtered to linked decks.
"""
self.ensure_one()
deck_ids = self.deck_line_ids.mapped("deck_id").ids
action = self.env["ir.actions.actions"]._for_xml_id(
"mvd_tcg_deck.mvd_tcg_deck_action"
)
action["domain"] = [("id", "in", deck_ids or [0])]
if self.game_id:
action["context"] = {"default_game_id": self.game_id.id}
return action
def action_open_add_to_deck_wizard(self):
"""Open the deck-assignment wizard for the current card.
Returns:
dict: Modal wizard action.
"""
self.ensure_one()
return {
"type": "ir.actions.act_window",
"name": _("Add to Deck"),
"res_model": "mvd.tcg.add.to.deck",
"view_mode": "form",
"target": "new",
"context": {
"default_card_id": self.id,
"default_game_id": self.game_id.id,
"active_model": self._name,
"active_id": self.id,
},
}

959
models/mvd_tcg_deck.py Normal file
View File

@@ -0,0 +1,959 @@
"""Game-neutral deck builder models."""
import re
from odoo import _, api, fields, models
from odoo.exceptions import UserError, ValidationError
class MvdTcgDeck(models.Model):
"""Store one curated deck for one supported TCG."""
_name = "mvd.tcg.deck"
_description = "TCG Deck"
_order = "write_date desc, id desc"
_MVD_TCG_BOARD_CODE_ALIASES = {
"mainboard": {"main", "mainboard"},
"sideboard": {"side", "sideboard"},
"maybeboard": {"maybe", "maybeboard"},
"command_zone": {"command", "command_zone", "commander", "command zone"},
}
_MVD_TCG_BOARD_EXPORT_HEADINGS = {
"mainboard": "Mainboard",
"sideboard": "Sideboard",
"maybeboard": "Maybeboard",
"command_zone": "Command Zone",
}
active = fields.Boolean(default=True)
name = fields.Char(required=True, index="trigram")
game_id = fields.Many2one(
"mvd.tcg.game",
string="Game",
required=True,
index=True,
ondelete="restrict",
)
user_id = fields.Many2one(
"res.users",
string="Owner",
required=True,
default=lambda self: self.env.user,
index=True,
)
description = fields.Html()
note = fields.Text()
board_ids = fields.One2many(
"mvd.tcg.deck.board",
"deck_id",
string="Boards",
copy=True,
)
line_ids = fields.One2many(
"mvd.tcg.deck.line",
"deck_id",
string="Deck Lines",
readonly=True,
)
board_count = fields.Integer(
compute="_compute_counts",
readonly=True,
)
total_card_count = fields.Integer(
compute="_compute_counts",
readonly=True,
)
distinct_card_count = fields.Integer(
compute="_compute_counts",
readonly=True,
)
cover_card_id = fields.Many2one(
"mvd.tcg.card",
compute="_compute_cover",
readonly=True,
)
cover_image = fields.Image(
compute="_compute_cover",
readonly=True,
)
def _mvd_tcg_can_manage_deck_owner(self):
"""Return whether the current user may assign deck ownership.
Returns:
bool: ``True`` for TCG managers, administrators and system users.
"""
return self.env.is_superuser() or any(
self.env.user.has_group(xmlid)
for xmlid in (
"mvd_tcg_base.mvd_tcg_base_group_manager",
"base.group_system",
)
)
@api.model_create_multi
def create(self, vals_list):
"""Create decks and seed their default boards when needed.
Args:
vals_list: Standard ORM payloads for new decks.
Returns:
Model: Created deck records.
"""
prepared_vals_list = []
for vals in vals_list:
prepared_vals = dict(vals)
requested_owner_id = prepared_vals.get("user_id")
if (
requested_owner_id
and requested_owner_id != self.env.user.id
and not self._mvd_tcg_can_manage_deck_owner()
):
raise UserError(
_(
"Only TCG managers can assign decks to another owner."
)
)
prepared_vals_list.append(prepared_vals)
decks = super().create(prepared_vals_list)
for deck, vals in zip(decks, vals_list):
if vals.get("board_ids"):
continue
deck._mvd_tcg_seed_default_boards()
return decks
def write(self, vals):
"""Protect deck ownership from normal user reassignment.
Args:
vals: Field values to update.
Returns:
bool: Result of the underlying ORM write.
"""
if "user_id" in vals:
requested_owner_id = vals.get("user_id")
if (
requested_owner_id
and requested_owner_id != self.env.user.id
and not self._mvd_tcg_can_manage_deck_owner()
):
raise UserError(
_(
"Only TCG managers can reassign a deck to another owner."
)
)
return super().write(vals)
@api.depends("board_ids", "board_ids.total_card_count", "line_ids.card_id")
def _compute_counts(self):
"""Compute aggregate counters for each deck.
Returns:
None: The method updates records in place.
"""
for deck in self:
deck.board_count = len(deck.board_ids)
deck.total_card_count = sum(
deck.board_ids.filtered("include_in_total").mapped("total_card_count")
)
deck.distinct_card_count = len(deck.line_ids.mapped("card_id"))
@api.depends(
"line_ids.sequence",
"line_ids.board_sequence",
"line_ids.card_id",
"line_ids.card_id.image_1920",
)
def _compute_cover(self):
"""Pick a stable cover card and image for each deck.
Returns:
None: The method updates records in place.
"""
for deck in self:
ordered_lines = sorted(
deck.line_ids.filtered("card_id"),
key=lambda line: (line.board_sequence, line.sequence, line.id),
)
cover_card = ordered_lines[0].card_id if ordered_lines else False
deck.cover_card_id = cover_card
deck.cover_image = (
cover_card._mvd_tcg_get_deck_image_binary() if cover_card else False
)
def _mvd_tcg_seed_default_boards(self):
"""Create default boards for decks that do not have any yet.
Returns:
None: The method creates child board records in place.
"""
board_model = self.env["mvd.tcg.deck.board"]
for deck in self.filtered(lambda current_deck: not current_deck.board_ids):
board_values = deck.game_id._mvd_tcg_get_default_deck_board_templates()
board_model.with_context(
mvd_tcg_bypass_board_code_write=True
).create(
[
{
"deck_id": deck.id,
"name": values["name"],
"code": values["code"],
"sequence": values.get("sequence", 10),
"include_in_total": values.get("include_in_total", True),
}
for values in board_values
]
)
def action_seed_default_boards(self):
"""Recreate default boards when a deck has none.
Returns:
bool: ``True`` when the operation completed.
"""
self._mvd_tcg_seed_default_boards()
return True
def action_open_cards(self):
"""Open all cards that currently belong to the deck.
Returns:
dict: Window action filtered to linked cards.
"""
self.ensure_one()
action = self.env["ir.actions.actions"]._for_xml_id(
"mvd_tcg_base.mvd_tcg_card_action"
)
action["domain"] = [("id", "in", self.line_ids.mapped("card_id").ids or [0])]
action["context"] = {"default_game_id": self.game_id.id}
return action
def action_open_line_manager(self):
"""Open the standalone line manager for the current deck.
Returns:
dict: Window action filtered to the current deck lines.
"""
self.ensure_one()
action = self.env["ir.actions.actions"]._for_xml_id(
"mvd_tcg_deck.mvd_tcg_deck_line_action"
)
action["name"] = _("%s Lines") % self.display_name
action["domain"] = [("deck_id", "=", self.id)]
action["context"] = {
"default_deck_id": self.id,
"search_default_group_by_board": 1,
}
return action
def action_open_add_to_deck_wizard(self):
"""Open the generic add-to-deck wizard for the current deck.
Returns:
dict: Window action for the add-to-deck wizard.
"""
self.ensure_one()
return {
"type": "ir.actions.act_window",
"name": _("Add to Deck"),
"res_model": "mvd.tcg.add.to.deck",
"view_mode": "form",
"target": "new",
"context": {
"default_game_id": self.game_id.id,
"default_deck_id": self.id,
},
}
def action_open_deck_import_wizard(self):
"""Open the text import wizard for the current deck.
Returns:
dict: Window action for the deck text transfer wizard.
"""
self.ensure_one()
return {
"type": "ir.actions.act_window",
"name": _("Import Deck List"),
"res_model": "mvd.tcg.deck.text.transfer",
"view_mode": "form",
"target": "new",
"context": {
"default_deck_id": self.id,
"default_operation": "import",
},
}
def action_open_deck_export_wizard(self):
"""Open the text export wizard for the current deck.
Returns:
dict: Window action for the deck text transfer wizard.
"""
self.ensure_one()
return {
"type": "ir.actions.act_window",
"name": _("Export Deck List"),
"res_model": "mvd.tcg.deck.text.transfer",
"view_mode": "form",
"target": "new",
"context": {
"default_deck_id": self.id,
"default_operation": "export",
},
}
@classmethod
def _mvd_tcg_normalize_board_code(cls, raw_code):
"""Resolve one raw board label to its stable technical board code.
Args:
raw_code: Free-form board name or code.
Returns:
str | bool: Stable board code or ``False`` when no alias matches.
"""
normalized_code = (raw_code or "").strip().lower()
for board_code, aliases in cls._MVD_TCG_BOARD_CODE_ALIASES.items():
if normalized_code in aliases or normalized_code == board_code:
return board_code
return False
@classmethod
def _mvd_tcg_get_board_export_heading(cls, board):
"""Return one stable, human-readable board heading for exports.
Args:
board: Deck board record.
Returns:
str: Export heading that remains round-trippable.
"""
normalized_code = cls._mvd_tcg_normalize_board_code(board.code)
return cls._MVD_TCG_BOARD_EXPORT_HEADINGS.get(
normalized_code,
board.name,
)
def _mvd_tcg_get_board_by_code(self, board_code):
"""Return the first board matching the requested code.
Args:
board_code: Stable technical board code such as ``mainboard``.
Returns:
mvd.tcg.deck.board: Matching board record, if any.
"""
self.ensure_one()
normalized_code = self._mvd_tcg_normalize_board_code(board_code) or board_code
accepted_codes = self._MVD_TCG_BOARD_CODE_ALIASES.get(
normalized_code,
{normalized_code},
)
return self.board_ids.filtered(lambda board: board.code in accepted_codes)[:1]
def _mvd_tcg_add_card_to_board(
self,
card,
board,
quantity=1,
role_ids=False,
note=False,
):
"""Create or increment one deck line through the standard add rules.
Args:
card: Card record that should be added.
board: Target board inside the current deck.
quantity: Quantity delta to add.
role_ids: Optional deck roles to merge onto the target line.
note: Optional note to apply to the resulting line.
Returns:
mvd.tcg.deck.line: Created or updated deck line.
"""
self.ensure_one()
line_model = self.env["mvd.tcg.deck.line"]
existing_line = line_model.search(
[
("board_id", "=", board.id),
("card_id", "=", card.id),
],
limit=1,
)
self._mvd_tcg_validate_add_to_board(
card,
board,
quantity=quantity,
existing_line=existing_line,
)
if existing_line:
values = {"quantity": existing_line.quantity + quantity}
if role_ids:
merged_roles = existing_line.role_ids | role_ids
values["role_ids"] = [(6, 0, merged_roles.ids)]
if note:
values["note"] = note
existing_line.write(values)
return existing_line
return line_model.create(
{
"board_id": board.id,
"quantity": quantity,
"card_id": card.id,
"role_ids": [(6, 0, role_ids.ids)] if role_ids else False,
"note": note,
}
)
def _mvd_tcg_action_open_board(self, board_code):
"""Open one logical board of the current deck.
Args:
board_code: Stable technical board code such as ``mainboard``.
Returns:
dict | bool: Window action for the board form, or ``False``.
"""
self.ensure_one()
board = self._mvd_tcg_get_board_by_code(board_code)
if not board:
self._mvd_tcg_seed_default_boards()
board = self._mvd_tcg_get_board_by_code(board_code)
return board.action_open_board() if board else False
def _mvd_tcg_action_open_add_to_board_wizard(self, board_code):
"""Open the add-to-deck wizard with a preselected board.
Args:
board_code: Stable technical board code such as ``mainboard``.
Returns:
dict: Window action for the add-to-deck wizard.
"""
self.ensure_one()
board = self._mvd_tcg_get_board_by_code(board_code)
if not board:
self._mvd_tcg_seed_default_boards()
board = self._mvd_tcg_get_board_by_code(board_code)
return {
"type": "ir.actions.act_window",
"name": _("Add to Deck"),
"res_model": "mvd.tcg.add.to.deck",
"view_mode": "form",
"target": "new",
"context": {
"default_game_id": self.game_id.id,
"default_deck_id": self.id,
"default_board_id": board.id if board else False,
},
}
def _mvd_tcg_validate_add_to_board(
self,
card,
board,
quantity=1,
existing_line=False,
):
"""Validate whether one card can be added to one target board.
Args:
card: Card record that should be added.
board: Target board record.
quantity: Quantity delta requested by the user.
existing_line: Existing matching board line when the add operation
would increment an already present card.
Returns:
bool: ``True`` when no game-specific rule blocks the add flow.
"""
self.ensure_one()
return True
class MvdTcgDeckBoard(models.Model):
"""Store one logical board inside a deck."""
_name = "mvd.tcg.deck.board"
_description = "TCG Deck Board"
_order = "deck_id, sequence, id"
sequence = fields.Integer(default=10)
deck_id = fields.Many2one(
"mvd.tcg.deck",
required=True,
ondelete="cascade",
index=True,
)
name = fields.Char(required=True, translate=True)
code = fields.Char(required=True, index=True)
include_in_total = fields.Boolean(default=True)
note = fields.Text()
line_ids = fields.One2many(
"mvd.tcg.deck.line",
"board_id",
string="Board Lines",
copy=True,
)
total_card_count = fields.Integer(
compute="_compute_counts",
readonly=True,
)
distinct_card_count = fields.Integer(
compute="_compute_counts",
readonly=True,
)
_board_code_unique = models.Constraint(
"UNIQUE(deck_id, code)",
"The board code must be unique inside a deck.",
)
@api.model
def _mvd_tcg_generate_board_code(self, name):
"""Return a slug-like technical code for one board name."""
return re.sub(r"[^a-z0-9]+", "_", (name or "").strip().lower()).strip("_") or "board"
@api.model
def _mvd_tcg_get_unique_board_code(self, deck_id, name):
"""Return a unique board code inside one deck."""
base_code = self._mvd_tcg_generate_board_code(name)
if not deck_id:
return base_code
existing_codes = set(self.search([("deck_id", "=", deck_id)]).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_board_code(self):
"""Return whether the current user may edit board 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 technical codes for manually created boards."""
prepared_vals_list = []
for vals in vals_list:
prepared_vals = dict(vals)
if (
prepared_vals.get("code")
and not self.env.context.get("mvd_tcg_bypass_board_code_write")
and not self._mvd_tcg_can_edit_board_code()
):
raise UserError(
_(
"Board codes are technical identifiers and can only be set "
"by TCG administrators."
)
)
if not prepared_vals.get("code"):
prepared_vals["code"] = self._mvd_tcg_get_unique_board_code(
prepared_vals.get("deck_id"),
prepared_vals.get("name"),
)
prepared_vals_list.append(prepared_vals)
return super().create(prepared_vals_list)
def write(self, vals):
"""Protect technical board codes from normal business edits."""
if "code" in vals and not self.env.context.get("mvd_tcg_bypass_board_code_write"):
if not self._mvd_tcg_can_edit_board_code():
raise UserError(
_(
"Board codes are technical identifiers and can only be changed "
"by TCG administrators."
)
)
return super().write(vals)
@api.depends("line_ids.quantity", "line_ids.card_id")
def _compute_counts(self):
"""Compute per-board card counters.
Returns:
None: The method updates records in place.
"""
for board in self:
board.total_card_count = sum(board.line_ids.mapped("quantity"))
board.distinct_card_count = len(board.line_ids.mapped("card_id"))
def action_open_board(self):
"""Open the current board in form view.
Returns:
dict: Window action for the current board.
"""
self.ensure_one()
return {
"type": "ir.actions.act_window",
"name": self.display_name,
"res_model": "mvd.tcg.deck.board",
"view_mode": "form",
"res_id": self.id,
"target": "current",
}
def action_open_line_manager(self):
"""Open the standalone line manager for the current board.
Returns:
dict: Window action filtered to the current board lines.
"""
self.ensure_one()
action = self.env["ir.actions.actions"]._for_xml_id(
"mvd_tcg_deck.mvd_tcg_deck_line_action"
)
action["name"] = _("%s Lines") % self.display_name
action["domain"] = [("board_id", "=", self.id)]
action["context"] = {
"default_deck_id": self.deck_id.id,
"default_board_id": self.id,
"search_default_group_by_primary_role": 1,
}
return action
class MvdTcgDeckLine(models.Model):
"""Store one card entry inside one deck board."""
_name = "mvd.tcg.deck.line"
_description = "TCG Deck Line"
_order = "deck_id, board_sequence, sequence, id"
sequence = fields.Integer(default=10)
board_id = fields.Many2one(
"mvd.tcg.deck.board",
required=True,
ondelete="cascade",
index=True,
)
deck_id = fields.Many2one(
"mvd.tcg.deck",
related="board_id.deck_id",
store=True,
readonly=True,
index=True,
)
board_sequence = fields.Integer(
related="board_id.sequence",
string="Board Sequence",
store=True,
readonly=True,
index=True,
)
quantity = fields.Integer(required=True, default=1)
card_id = fields.Many2one(
"mvd.tcg.card",
string="Card",
required=True,
index=True,
domain=[("active", "=", True)],
)
game_id = fields.Many2one(
"mvd.tcg.game",
related="deck_id.game_id",
store=True,
readonly=True,
index=True,
)
card_image_128 = fields.Image(
related="card_id.image_128",
readonly=True,
)
card_image_512 = fields.Image(
related="card_id.image_512",
readonly=True,
)
card_image_1920 = fields.Image(
related="card_id.image_1920",
readonly=True,
)
role_ids = fields.Many2many(
"mvd.tcg.deck.role",
"mvd_tcg_deck_line_role_rel",
"line_id",
"role_id",
string="Roles",
)
primary_role_id = fields.Many2one(
"mvd.tcg.deck.role",
compute="_compute_primary_role",
store=True,
readonly=True,
index=True,
)
primary_role_sequence = fields.Integer(
compute="_compute_primary_role",
store=True,
readonly=True,
index=True,
)
note = fields.Char()
_board_card_unique = models.Constraint(
"UNIQUE(board_id, card_id)",
"A card can appear only once per board.",
)
_quantity_positive = models.Constraint(
"CHECK(quantity > 0)",
"The quantity must be greater than zero.",
)
@api.constrains("card_id", "game_id")
def _check_card_game_matches_deck(self):
"""Ensure cards only enter decks of the same game.
Returns:
None: The method validates records in place.
Raises:
ValidationError: If the card and deck game differ.
"""
for line in self:
if (
line.card_id
and line.game_id
and line.card_id.game_id
and line.card_id.game_id != line.game_id
):
raise ValidationError(
_(
"Deck lines can only reference cards from the same game as "
"the deck."
)
)
@api.depends("role_ids", "role_ids.sequence", "role_ids.name")
def _compute_primary_role(self):
"""Expose one stable primary role for sorting and grouping.
Returns:
None: The compute updates records in place.
"""
for line in self:
role = line.role_ids.sorted(
key=lambda current_role: (
current_role.sequence,
current_role.name or "",
current_role.id,
)
)[:1]
line.primary_role_id = role
line.primary_role_sequence = role.sequence if role else 9999
def action_open_line(self):
"""Open the current deck line in form view.
Returns:
dict: Window action for the current deck line.
"""
self.ensure_one()
return {
"type": "ir.actions.act_window",
"name": self.display_name,
"res_model": "mvd.tcg.deck.line",
"view_mode": "form",
"res_id": self.id,
"target": "current",
}
def action_open_card_preview(self):
"""Open the linked card in a modal preview form.
Returns:
dict: Window action for the linked card form.
"""
self.ensure_one()
return self._mvd_tcg_get_card_preview_action()
def _mvd_tcg_get_card_form_view_xmlid(self):
"""Return the preferred form view XMLID for linked card previews.
Returns:
str: Stable XMLID for the target card form view.
"""
return "mvd_tcg_base.mvd_tcg_card_view_form"
def _mvd_tcg_get_card_preview_action(self):
"""Build the window action that opens the linked card preview.
Returns:
dict: Window action for the linked card form.
"""
self.ensure_one()
return {
"type": "ir.actions.act_window",
"name": self.card_id.display_name,
"res_model": "mvd.tcg.card",
"view_mode": "form",
"res_id": self.card_id.id,
"views": [
(
self.env.ref(self._mvd_tcg_get_card_form_view_xmlid()).id,
"form",
)
],
"target": "new",
}
def _mvd_tcg_move_to_board(self, board_code):
"""Move the current line to another logical board of the same deck.
Args:
board_code: Canonical board code such as ``mainboard``.
Returns:
bool: ``True`` when the move completed.
Raises:
UserError: If no matching board exists or a duplicate would be created.
"""
self.ensure_one()
target_board = self.deck_id._mvd_tcg_get_board_by_code(board_code)
if not target_board:
raise UserError(_("No target board exists for code '%s'.") % board_code)
if target_board == self.board_id:
return True
self._mvd_tcg_validate_move_to_board(target_board)
conflicting_line = self.search(
[
("board_id", "=", target_board.id),
("card_id", "=", self.card_id.id),
("id", "!=", self.id),
],
limit=1,
)
if conflicting_line:
raise UserError(
_(
"The selected card already exists in the target board. Merge or "
"adjust that line first."
)
)
self.board_id = target_board
return True
def _mvd_tcg_validate_move_to_board(self, target_board):
"""Validate whether the current line can move to another board.
Args:
target_board: Destination board record.
Returns:
bool: ``True`` when no game-specific rule blocks the move.
"""
self.ensure_one()
return True
class MvdTcgDeckRole(models.Model):
"""Store reusable deck line roles such as ramp or removal."""
_name = "mvd.tcg.deck.role"
_description = "TCG Deck Role"
_order = "sequence, name, id"
active = fields.Boolean(default=True)
sequence = fields.Integer(default=10)
name = fields.Char(required=True, translate=True, index="trigram")
technical_key = fields.Char(
index=True,
copy=False,
groups="mvd_tcg_base.mvd_tcg_base_group_administrator,base.group_system",
)
color = fields.Integer(default=0)
note = fields.Text(translate=True)
_name_unique = models.Constraint(
"UNIQUE(name)",
"The deck role name must be unique.",
)
_technical_key_unique = models.Constraint(
"UNIQUE(technical_key)",
"The technical role key must be unique.",
)
@api.model
def _mvd_tcg_generate_technical_key(self, name):
"""Return a slug-like technical key for one deck role name."""
return re.sub(r"[^a-z0-9]+", "_", (name or "").strip().lower()).strip("_") or "role"
@api.model
def _mvd_tcg_get_unique_technical_key(self, name):
"""Return a globally unique technical key for one deck role name."""
base_key = self._mvd_tcg_generate_technical_key(name)
existing_keys = set(self.search([]).mapped("technical_key"))
if base_key not in existing_keys:
return base_key
suffix = 2
while f"{base_key}_{suffix}" in existing_keys:
suffix += 1
return f"{base_key}_{suffix}"
def _mvd_tcg_can_edit_technical_key(self):
"""Return whether the current user may edit technical role keys."""
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 keys for manually created deck roles."""
prepared_vals_list = []
for vals in vals_list:
prepared_vals = dict(vals)
if not prepared_vals.get("technical_key"):
prepared_vals["technical_key"] = self._mvd_tcg_get_unique_technical_key(
prepared_vals.get("name")
)
prepared_vals_list.append(prepared_vals)
return super().create(prepared_vals_list)
def write(self, vals):
"""Protect technical role keys from normal business edits."""
if "technical_key" in vals and not self.env.context.get(
"mvd_tcg_bypass_role_key_write"
):
if not self._mvd_tcg_can_edit_technical_key():
raise UserError(
_(
"Deck role technical keys can only be changed by TCG "
"administrators."
)
)
return super().write(vals)
@api.model
def _mvd_tcg_sync_seed_role_keys(self):
"""Backfill technical keys for seeded deck roles on module updates."""
role_xmlids = {
"ramp": "mvd_tcg_deck.mvd_tcg_deck_role_ramp",
"draw": "mvd_tcg_deck.mvd_tcg_deck_role_draw",
"removal": "mvd_tcg_deck.mvd_tcg_deck_role_removal",
"interaction": "mvd_tcg_deck.mvd_tcg_deck_role_interaction",
"protection": "mvd_tcg_deck.mvd_tcg_deck_role_protection",
"wincon": "mvd_tcg_deck.mvd_tcg_deck_role_wincon",
"value": "mvd_tcg_deck.mvd_tcg_deck_role_value",
"combo": "mvd_tcg_deck.mvd_tcg_deck_role_combo",
}
for technical_key, xmlid in role_xmlids.items():
role = self.env.ref(xmlid, raise_if_not_found=False)
if role and role.technical_key != technical_key:
role.sudo().write({"technical_key": technical_key})