🎉 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

8
.gitignore vendored Normal file
View File

@@ -0,0 +1,8 @@
__pycache__/
*.py[cod]
.DS_Store
.pytest_cache/
.ruff_cache/
*.log
*.swp
*~

3
__init__.py Normal file
View File

@@ -0,0 +1,3 @@
from . import models
from . import report
from . import wizards

44
__manifest__.py Normal file
View File

@@ -0,0 +1,44 @@
{
"name": "MVD TCG Deck",
"summary": "Game-neutral deckbuilding, deck lines, and printable reports",
"version": "19.0.10.3.1",
"description": """
Game-neutral deckbuilding layer for the MVD TCG suite.
This module provides the shared deckbuilding primitives used across supported
games:
- decks, boards, deck lines, and reusable deck roles
- add-to-deck flows and text import/export
- generic board and line management
- printable deck reports
- backend image zoom support for deck line card thumbnails
Game-specific rules, analytics, and presentation are added by adapter modules
such as the MTG deck extension.
""",
"category": "Tools",
"author": "Mantjeverse Digital",
"license": "LGPL-3",
"depends": ["mvd_tcg_base", "web"],
"assets": {
"web.assets_backend": [
"mvd_tcg_deck/static/src/js/mvd_deck_zoom_image_field.js",
"mvd_tcg_deck/static/src/xml/mvd_deck_zoom_image_field.xml",
],
},
"data": [
"security/ir.model.access.csv",
"security/mvd_tcg_deck_security.xml",
"data/mvd_tcg_deck_role_data.xml",
"data/mvd_tcg_deck_role_sync.xml",
"views/mvd_tcg_deck_views.xml",
"views/mvd_tcg_add_to_deck_views.xml",
"views/mvd_tcg_deck_text_transfer_views.xml",
"views/mvd_tcg_card_views.xml",
"views/menu_views.xml",
"report/mvd_tcg_deck_report_templates.xml",
"report/mvd_tcg_deck_report_actions.xml",
],
"application": False,
"installable": True,
}

View File

@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo noupdate="1">
<record id="mvd_tcg_deck_role_ramp" model="mvd.tcg.deck.role">
<field name="sequence">10</field>
<field name="technical_key">ramp</field>
<field name="name">Ramp</field>
<field name="color">1</field>
</record>
<record id="mvd_tcg_deck_role_draw" model="mvd.tcg.deck.role">
<field name="sequence">20</field>
<field name="technical_key">draw</field>
<field name="name">Draw</field>
<field name="color">2</field>
</record>
<record id="mvd_tcg_deck_role_removal" model="mvd.tcg.deck.role">
<field name="sequence">30</field>
<field name="technical_key">removal</field>
<field name="name">Removal</field>
<field name="color">3</field>
</record>
<record id="mvd_tcg_deck_role_interaction" model="mvd.tcg.deck.role">
<field name="sequence">40</field>
<field name="technical_key">interaction</field>
<field name="name">Interaction</field>
<field name="color">4</field>
</record>
<record id="mvd_tcg_deck_role_protection" model="mvd.tcg.deck.role">
<field name="sequence">50</field>
<field name="technical_key">protection</field>
<field name="name">Protection</field>
<field name="color">5</field>
</record>
<record id="mvd_tcg_deck_role_wincon" model="mvd.tcg.deck.role">
<field name="sequence">60</field>
<field name="technical_key">wincon</field>
<field name="name">Win Condition</field>
<field name="color">6</field>
</record>
<record id="mvd_tcg_deck_role_value" model="mvd.tcg.deck.role">
<field name="sequence">70</field>
<field name="technical_key">value</field>
<field name="name">Value</field>
<field name="color">7</field>
</record>
<record id="mvd_tcg_deck_role_combo" model="mvd.tcg.deck.role">
<field name="sequence">80</field>
<field name="technical_key">combo</field>
<field name="name">Combo</field>
<field name="color">8</field>
</record>
</odoo>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<function model="mvd.tcg.deck.role" name="_mvd_tcg_sync_seed_role_keys"/>
</odoo>

947
i18n/de.po Normal file
View File

@@ -0,0 +1,947 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * mvd_tcg_deck
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 19.0+e-20260324\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-04-03 05:05+0000\n"
"PO-Revision-Date: 2026-04-03 05:05+0000\n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Language: de\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#. module: mvd_tcg_deck
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.mvd_tcg_deck_text_transfer_view_form
msgid ""
"# Mainboard\n"
"1 Card Name\n"
"1 Another Card (TDM 101)\n"
"# Sideboard\n"
"1 Third Card"
msgstr ""
"# Mainboard\n"
"1 Kartenname\n"
"1 Weitere Karte (TDM 101)\n"
"# Sideboard\n"
"1 Dritte Karte"
#. module: mvd_tcg_deck
#. odoo-python
#: code:addons/mvd_tcg_deck/models/mvd_tcg_deck.py:0
msgid "%s Lines"
msgstr "%s Zeilen"
#. module: mvd_tcg_deck
#: model:ir.actions.report,print_report_name:mvd_tcg_deck.action_report_mvd_tcg_deck
msgid "'Deck - %s' % (object.name)"
msgstr "'Deck - %s' % (object.name)"
#. module: mvd_tcg_deck
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.mvd_tcg_deck_view_form
msgid "<span class=\"o_stat_text\">Add Card</span>"
msgstr "<span class=\"o_stat_text\">Karte hinzufügen</span>"
#. module: mvd_tcg_deck
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.mvd_tcg_deck_view_form
msgid "<span class=\"o_stat_text\">Create Default Boards</span>"
msgstr "<span class=\"o_stat_text\">Standard-Boards anlegen</span>"
#. module: mvd_tcg_deck
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.mvd_tcg_deck_view_form
msgid "<span class=\"o_stat_text\">Export List</span>"
msgstr "<span class=\"o_stat_text\">Liste exportieren</span>"
#. module: mvd_tcg_deck
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.mvd_tcg_deck_view_form
msgid "<span class=\"o_stat_text\">Import List</span>"
msgstr "<span class=\"o_stat_text\">Liste importieren</span>"
#. module: mvd_tcg_deck
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.mvd_tcg_deck_board_view_form
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.mvd_tcg_deck_view_form
msgid "<span class=\"o_stat_text\">Manage Lines</span>"
msgstr "<span class=\"o_stat_text\">Zeilen verwalten</span>"
#. module: mvd_tcg_deck
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.mvd_tcg_deck_line_view_form
msgid "<span class=\"o_stat_text\">Open</span>"
msgstr "<span class=\"o_stat_text\">Öffnen</span>"
#. module: mvd_tcg_deck
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.report_mvd_tcg_deck_document
msgid "<span> · </span>"
msgstr ""
#. module: mvd_tcg_deck
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.report_mvd_tcg_deck_document
msgid ""
"<span> · </span>\n"
" <span class=\"page\"/> / <span class=\"topage\"/>"
msgstr ""
#. module: mvd_tcg_deck
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.report_mvd_tcg_deck_document
msgid ""
"<span> · </span>\n"
" <span class=\"page\"/> / <span class=\"topage\"/>"
msgstr ""
#. module: mvd_tcg_deck
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.mvd_tcg_deck_view_kanban
msgid "<span>No cover</span>"
msgstr "<span>Kein Cover</span>"
#. module: mvd_tcg_deck
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.mvd_tcg_deck_view_form
msgid "<strong class=\"me-1\">Boards</strong>"
msgstr "<strong class=\"me-1\">Boards</strong>"
#. module: mvd_tcg_deck
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.mvd_tcg_deck_view_form
msgid "<strong class=\"me-1\">Cards</strong>"
msgstr "<strong class=\"me-1\">Karten</strong>"
#. module: mvd_tcg_deck
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.mvd_tcg_deck_view_form
msgid "<strong class=\"me-1\">Distinct</strong>"
msgstr "<strong class=\"me-1\">Eindeutig</strong>"
#. module: mvd_tcg_deck
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.report_mvd_tcg_deck_document
msgid "<strong>Boards:</strong>"
msgstr "<strong>Boards:</strong>"
#. module: mvd_tcg_deck
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.report_mvd_tcg_deck_document
msgid "<strong>Game:</strong>"
msgstr "<strong>Spiel:</strong>"
#. module: mvd_tcg_deck
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.report_mvd_tcg_deck_document
msgid "<strong>Owner:</strong>"
msgstr "<strong>Besitzer:</strong>"
#. module: mvd_tcg_deck
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.report_mvd_tcg_deck_document
msgid "<strong>Updated:</strong>"
msgstr "<strong>Aktualisiert:</strong>"
#. module: mvd_tcg_deck
#: model:ir.model.constraint,message:mvd_tcg_deck.constraint_mvd_tcg_deck_line_board_card_unique
msgid "A card can appear only once per board."
msgstr "Eine Karte darf pro Board nur einmal vorkommen."
#. module: mvd_tcg_deck
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_deck__active
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_deck_role__active
msgid "Active"
msgstr "Aktiv"
#. module: mvd_tcg_deck
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.mvd_tcg_add_to_deck_view_form
msgid "Add"
msgstr "Hinzufügen"
#. module: mvd_tcg_deck
#: model:ir.model,name:mvd_tcg_deck.model_mvd_tcg_add_to_deck
msgid "Add TCG Card To Deck"
msgstr "TCG-Karte zum Deck hinzufügen"
#. module: mvd_tcg_deck
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.mvd_tcg_deck_line_view_form
msgid "Add an internal note for this deck entry."
msgstr "Füge eine interne Notiz für diesen Deckeintrag hinzu."
#. module: mvd_tcg_deck
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.mvd_tcg_deck_view_form
msgid "Add internal deck notes."
msgstr "Füge interne Decknotizen hinzu."
#. module: mvd_tcg_deck
#. odoo-python
#: code:addons/mvd_tcg_deck/models/mvd_tcg_card.py:0
#: code:addons/mvd_tcg_deck/models/mvd_tcg_deck.py:0
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.mvd_tcg_add_to_deck_view_form
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.mvd_tcg_card_view_form_deck_inherit
msgid "Add to Deck"
msgstr "Zum Deck hinzufügen"
#. module: mvd_tcg_deck
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.mvd_tcg_deck_view_form
msgid "All Cards"
msgstr "Alle Karten"
#. module: mvd_tcg_deck
#: model:ir.ui.menu,name:mvd_tcg_deck.mvd_tcg_decks_menu
msgid "All Decks"
msgstr "Alle Decks"
#. module: mvd_tcg_deck
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.mvd_tcg_deck_text_transfer_view_form
msgid "Apply"
msgstr "Anwenden"
#. module: mvd_tcg_deck
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.mvd_tcg_deck_view_form
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.mvd_tcg_deck_view_search
msgid "Archived"
msgstr "Archiviert"
#. module: mvd_tcg_deck
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_add_to_deck__board_id
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_deck_line__board_id
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.mvd_tcg_deck_board_view_form
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.mvd_tcg_deck_line_view_search
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.report_mvd_tcg_deck_document
msgid "Board"
msgstr "Board"
#. module: mvd_tcg_deck
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_deck__board_count
msgid "Board Count"
msgstr "Board-Anzahl"
#. module: mvd_tcg_deck
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_deck_board__line_ids
msgid "Board Lines"
msgstr "Board-Zeilen"
#. module: mvd_tcg_deck
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_deck_line__board_sequence
msgid "Board Sequence"
msgstr "Board-Reihenfolge"
#. module: mvd_tcg_deck
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.report_mvd_tcg_deck_document
msgid "Board Summary"
msgstr "Board-Zusammenfassung"
#. module: mvd_tcg_deck
#. odoo-python
#: code:addons/mvd_tcg_deck/models/mvd_tcg_deck.py:0
msgid ""
"Board codes are technical identifiers and can only be changed by TCG "
"administrators."
msgstr ""
#. module: mvd_tcg_deck
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_deck__board_ids
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.mvd_tcg_deck_view_form
msgid "Boards"
msgstr "Boards"
#. module: mvd_tcg_deck
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.report_mvd_tcg_deck_document
msgid "Build Participation"
msgstr "Build-Beteiligung"
#. module: mvd_tcg_deck
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.report_mvd_tcg_deck_document
msgid "Build Status"
msgstr "Build-Status"
#. module: mvd_tcg_deck
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.mvd_tcg_add_to_deck_view_form
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.mvd_tcg_deck_text_transfer_view_form
msgid "Cancel"
msgstr "Abbrechen"
#. module: mvd_tcg_deck
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_add_to_deck__card_id
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_deck_line__card_id
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.mvd_tcg_deck_line_view_form
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.report_mvd_tcg_deck_document
msgid "Card"
msgstr "Karte"
#. module: mvd_tcg_deck
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.report_mvd_tcg_deck_document
msgid "Card image"
msgstr ""
#. module: mvd_tcg_deck
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.mvd_tcg_deck_board_view_form
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.mvd_tcg_deck_board_view_list
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.mvd_tcg_deck_view_form
msgid "Cards"
msgstr "Karten"
#. module: mvd_tcg_deck
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.report_mvd_tcg_deck_document
msgid "Cards counted in the active build"
msgstr ""
#. module: mvd_tcg_deck
#: model:ir.model.fields,help:mvd_tcg_deck.field_mvd_tcg_deck_text_transfer__replace_existing
msgid ""
"Clear all current deck entries before importing the provided deck list."
msgstr ""
#. module: mvd_tcg_deck
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.mvd_tcg_deck_text_transfer_view_form
msgid "Close"
msgstr "Schließen"
#. module: mvd_tcg_deck
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_deck_board__code
msgid "Code"
msgstr "Code"
#. module: mvd_tcg_deck
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_deck_role__color
msgid "Color"
msgstr "Farbe"
#. module: mvd_tcg_deck
#: model:mvd.tcg.deck.role,name:mvd_tcg_deck.mvd_tcg_deck_role_combo
msgid "Combo"
msgstr ""
#. module: mvd_tcg_deck
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.report_mvd_tcg_deck_document
msgid "Compact deck overview for print and review."
msgstr ""
#. module: mvd_tcg_deck
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.report_mvd_tcg_deck_document
msgid "Copies"
msgstr "Kopien"
#. module: mvd_tcg_deck
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_deck__cover_card_id
msgid "Cover Card"
msgstr "Titelkarte"
#. module: mvd_tcg_deck
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_deck__cover_image
msgid "Cover Image"
msgstr "Titelbild"
#. module: mvd_tcg_deck
#: model_terms:ir.actions.act_window,help:mvd_tcg_deck.mvd_tcg_deck_action
msgid "Create a new deck"
msgstr "Neues Deck anlegen"
#. module: mvd_tcg_deck
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_add_to_deck__create_uid
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_deck__create_uid
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_deck_board__create_uid
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_deck_line__create_uid
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_deck_role__create_uid
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_deck_text_transfer__create_uid
msgid "Created by"
msgstr "Erstellt von"
#. module: mvd_tcg_deck
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_add_to_deck__create_date
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_deck__create_date
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_deck_board__create_date
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_deck_line__create_date
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_deck_role__create_date
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_deck_text_transfer__create_date
msgid "Created on"
msgstr "Erstellt am"
#. module: mvd_tcg_deck
#. odoo-python
#: code:addons/mvd_tcg_deck/wizards/mvd_tcg_add_to_deck.py:0
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_add_to_deck__deck_id
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_deck_board__deck_id
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_deck_line__deck_id
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_deck_text_transfer__deck_id
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.mvd_tcg_deck_line_view_search
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.mvd_tcg_deck_view_form
msgid "Deck"
msgstr "Deck"
#. module: mvd_tcg_deck
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.mvd_tcg_deck_board_view_form
msgid "Deck Board"
msgstr "Deck-Board"
#. module: mvd_tcg_deck
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_card__deck_count
msgid "Deck Count"
msgstr "Deck-Anzahl"
#. module: mvd_tcg_deck
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.mvd_tcg_deck_line_view_form
msgid "Deck Entry"
msgstr "Deckeintrag"
#. module: mvd_tcg_deck
#: model:ir.actions.act_window,name:mvd_tcg_deck.mvd_tcg_deck_line_action
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_card__deck_line_ids
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_deck__line_ids
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.mvd_tcg_deck_line_view_search
msgid "Deck Lines"
msgstr "Deckzeilen"
#. module: mvd_tcg_deck
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_deck_text_transfer__line_text
msgid "Deck List"
msgstr "Deckliste"
#. module: mvd_tcg_deck
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.mvd_tcg_deck_text_transfer_view_form
msgid "Deck List Transfer"
msgstr "Decklisten-Transfer"
#. module: mvd_tcg_deck
#: model:ir.actions.report,name:mvd_tcg_deck.action_report_mvd_tcg_deck
msgid "Deck Report"
msgstr "Deckbericht"
#. module: mvd_tcg_deck
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.mvd_tcg_deck_role_view_form
msgid "Deck Role"
msgstr "Deckrolle"
#. module: mvd_tcg_deck
#: model:ir.actions.act_window,name:mvd_tcg_deck.mvd_tcg_deck_role_action
#: model:ir.ui.menu,name:mvd_tcg_deck.mvd_tcg_deck_roles_menu
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.mvd_tcg_deck_role_view_list
msgid "Deck Roles"
msgstr "Deckrollen"
#. module: mvd_tcg_deck
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.report_mvd_tcg_deck_document
msgid "Deck Snapshot"
msgstr "Deck-Snapshot"
#. module: mvd_tcg_deck
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.report_mvd_tcg_deck_document
msgid "Deck cover"
msgstr ""
#. module: mvd_tcg_deck
#. odoo-python
#: code:addons/mvd_tcg_deck/models/mvd_tcg_deck.py:0
msgid "Deck lines can only reference cards from the same game as the deck."
msgstr ""
#. module: mvd_tcg_deck
#. odoo-python
#: code:addons/mvd_tcg_deck/models/mvd_tcg_deck.py:0
msgid "Deck role technical keys can only be changed by TCG administrators."
msgstr ""
#. module: mvd_tcg_deck
#: model:ir.actions.act_window,name:mvd_tcg_deck.mvd_tcg_deck_action
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.mvd_tcg_card_view_form_deck_inherit
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.mvd_tcg_deck_view_list
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.mvd_tcg_deck_view_search
msgid "Decks"
msgstr "Decks"
#. module: mvd_tcg_deck
#: model_terms:ir.actions.act_window,help:mvd_tcg_deck.mvd_tcg_deck_action
msgid ""
"Decks stay game-neutral. Boards organize the cards inside one deck,\n"
" while game adapters can later add format- and rules-specific behavior."
msgstr ""
#. module: mvd_tcg_deck
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.mvd_tcg_deck_view_form
msgid "Describe the deck plan, power level, win conditions or upgrade path."
msgstr ""
#. module: mvd_tcg_deck
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.mvd_tcg_deck_role_view_form
msgid "Describe what this role should represent in deck construction."
msgstr ""
#. module: mvd_tcg_deck
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_deck__description
msgid "Description"
msgstr "Beschreibung"
#. module: mvd_tcg_deck
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_add_to_deck__display_name
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_card__display_name
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_deck__display_name
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_deck_board__display_name
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_deck_line__display_name
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_deck_role__display_name
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_deck_text_transfer__display_name
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_report_mvd_tcg_deck_report_mvd_tcg_deck_document__display_name
msgid "Display Name"
msgstr "Anzeigename"
#. module: mvd_tcg_deck
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.mvd_tcg_deck_board_view_list
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.report_mvd_tcg_deck_document
msgid "Distinct"
msgstr "Eindeutig"
#. module: mvd_tcg_deck
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_deck__distinct_card_count
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_deck_board__distinct_card_count
msgid "Distinct Card Count"
msgstr "Anzahl eindeutiger Karten"
#. module: mvd_tcg_deck
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.report_mvd_tcg_deck_document
msgid "Distinct Cards"
msgstr "Eindeutige Karten"
#. module: mvd_tcg_deck
#: model:mvd.tcg.deck.role,name:mvd_tcg_deck.mvd_tcg_deck_role_draw
msgid "Draw"
msgstr "Kartenziehen"
#. module: mvd_tcg_deck
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.report_mvd_tcg_deck_document
msgid "Entries"
msgstr ""
#. module: mvd_tcg_deck
#: model:ir.model.fields.selection,name:mvd_tcg_deck.selection__mvd_tcg_deck_text_transfer__operation__export
msgid "Export"
msgstr ""
#. module: mvd_tcg_deck
#. odoo-python
#: code:addons/mvd_tcg_deck/models/mvd_tcg_deck.py:0
msgid "Export Deck List"
msgstr "Deckliste exportieren"
#. module: mvd_tcg_deck
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_add_to_deck__game_id
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_deck__game_id
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_deck_line__game_id
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_deck_text_transfer__game_id
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.mvd_tcg_deck_view_search
msgid "Game"
msgstr "Spiel"
#. module: mvd_tcg_deck
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_add_to_deck__id
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_card__id
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_deck__id
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_deck_board__id
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_deck_line__id
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_deck_role__id
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_deck_text_transfer__id
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_report_mvd_tcg_deck_report_mvd_tcg_deck_document__id
msgid "ID"
msgstr "ID"
#. module: mvd_tcg_deck
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_deck_line__card_image_1920
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.report_mvd_tcg_deck_document
msgid "Image"
msgstr "Bild"
#. module: mvd_tcg_deck
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_deck_line__card_image_128
msgid "Image 128"
msgstr ""
#. module: mvd_tcg_deck
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_deck_line__card_image_512
msgid "Image 512"
msgstr ""
#. module: mvd_tcg_deck
#: model:ir.model.fields.selection,name:mvd_tcg_deck.selection__mvd_tcg_deck_text_transfer__operation__import
msgid "Import"
msgstr "Import"
#. module: mvd_tcg_deck
#. odoo-python
#: code:addons/mvd_tcg_deck/models/mvd_tcg_deck.py:0
msgid "Import Deck List"
msgstr "Deckliste importieren"
#. module: mvd_tcg_deck
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_deck_board__include_in_total
msgid "Include In Total"
msgstr ""
#. module: mvd_tcg_deck
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.report_mvd_tcg_deck_document
msgid "Included"
msgstr ""
#. module: mvd_tcg_deck
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.report_mvd_tcg_deck_document
msgid "Included in build"
msgstr ""
#. module: mvd_tcg_deck
#: model:mvd.tcg.deck.role,name:mvd_tcg_deck.mvd_tcg_deck_role_interaction
msgid "Interaction"
msgstr ""
#. module: mvd_tcg_deck
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.report_mvd_tcg_deck_document
msgid "Internal Notes"
msgstr "Interne Notizen"
#. module: mvd_tcg_deck
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_add_to_deck__write_uid
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_deck__write_uid
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_deck_board__write_uid
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_deck_line__write_uid
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_deck_role__write_uid
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_deck_text_transfer__write_uid
msgid "Last Updated by"
msgstr "Zuletzt aktualisiert von"
#. module: mvd_tcg_deck
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_add_to_deck__write_date
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_deck__write_date
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_deck_board__write_date
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_deck_line__write_date
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_deck_role__write_date
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_deck_text_transfer__write_date
msgid "Last Updated on"
msgstr "Zuletzt aktualisiert am"
#. module: mvd_tcg_deck
#. odoo-python
#: code:addons/mvd_tcg_deck/wizards/mvd_tcg_deck_text_transfer.py:0
msgid "Line %(line)s could not be parsed: %(value)s"
msgstr ""
#. module: mvd_tcg_deck
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.report_mvd_tcg_deck_document
msgid "MVD TCG Decksheet"
msgstr ""
#. module: mvd_tcg_deck
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.mvd_tcg_deck_view_search
msgid "My Decks"
msgstr ""
#. module: mvd_tcg_deck
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_deck__name
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_deck_board__name
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_deck_role__name
msgid "Name"
msgstr "Name"
#. module: mvd_tcg_deck
#. odoo-python
#: code:addons/mvd_tcg_deck/wizards/mvd_tcg_deck_text_transfer.py:0
msgid "No active card named '%s' exists in the current game."
msgstr ""
#. module: mvd_tcg_deck
#. odoo-python
#: code:addons/mvd_tcg_deck/wizards/mvd_tcg_deck_text_transfer.py:0
msgid "No board exists for code '%s'."
msgstr ""
#. module: mvd_tcg_deck
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.report_mvd_tcg_deck_document
msgid "No cards in this board."
msgstr ""
#. module: mvd_tcg_deck
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.report_mvd_tcg_deck_document
msgid "No deck overview has been written yet."
msgstr ""
#. module: mvd_tcg_deck
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.report_mvd_tcg_deck_document
msgid "No internal notes recorded."
msgstr ""
#. module: mvd_tcg_deck
#. odoo-python
#: code:addons/mvd_tcg_deck/models/mvd_tcg_deck.py:0
msgid "No target board exists for code '%s'."
msgstr ""
#. module: mvd_tcg_deck
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_add_to_deck__note
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_deck__note
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_deck_board__note
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_deck_line__note
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_deck_role__note
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.report_mvd_tcg_deck_document
msgid "Note"
msgstr ""
#. module: mvd_tcg_deck
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.mvd_tcg_deck_board_view_form
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.mvd_tcg_deck_view_form
msgid "Notes"
msgstr ""
#. module: mvd_tcg_deck
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_deck_text_transfer__operation
msgid "Operation"
msgstr ""
#. module: mvd_tcg_deck
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.mvd_tcg_deck_view_form
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.report_mvd_tcg_deck_document
msgid "Overview"
msgstr "Übersicht"
#. module: mvd_tcg_deck
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_deck__user_id
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.mvd_tcg_deck_view_search
msgid "Owner"
msgstr ""
#. module: mvd_tcg_deck
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.report_mvd_tcg_deck_document
msgid "Per-board size, distinct cards, and build participation."
msgstr ""
#. module: mvd_tcg_deck
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_deck_line__primary_role_id
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.mvd_tcg_deck_line_view_search
msgid "Primary Role"
msgstr ""
#. module: mvd_tcg_deck
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_deck_line__primary_role_sequence
msgid "Primary Role Sequence"
msgstr ""
#. module: mvd_tcg_deck
#: model:mvd.tcg.deck.role,name:mvd_tcg_deck.mvd_tcg_deck_role_protection
msgid "Protection"
msgstr ""
#. module: mvd_tcg_deck
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.report_mvd_tcg_deck_document
msgid "Qty"
msgstr ""
#. module: mvd_tcg_deck
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_add_to_deck__quantity
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_deck_line__quantity
msgid "Quantity"
msgstr ""
#. module: mvd_tcg_deck
#: model:mvd.tcg.deck.role,name:mvd_tcg_deck.mvd_tcg_deck_role_ramp
msgid "Ramp"
msgstr ""
#. module: mvd_tcg_deck
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.report_mvd_tcg_deck_document
msgid "Reference"
msgstr ""
#. module: mvd_tcg_deck
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.report_mvd_tcg_deck_document
msgid "Reference Only"
msgstr ""
#. module: mvd_tcg_deck
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.report_mvd_tcg_deck_document
msgid "Reference board"
msgstr ""
#. module: mvd_tcg_deck
#: model:mvd.tcg.deck.role,name:mvd_tcg_deck.mvd_tcg_deck_role_removal
msgid "Removal"
msgstr ""
#. module: mvd_tcg_deck
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_deck_text_transfer__replace_existing
msgid "Replace Existing Deck Lines"
msgstr "Vorhandene Deckzeilen ersetzen"
#. module: mvd_tcg_deck
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.mvd_tcg_deck_role_view_form
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.report_mvd_tcg_deck_document
msgid "Role"
msgstr ""
#. module: mvd_tcg_deck
#: model:ir.ui.menu,name:mvd_tcg_deck.mvd_tcg_deck_roles_menu
msgid "Deck Roles"
msgstr "Deckrollen"
#. module: mvd_tcg_deck
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.report_mvd_tcg_deck_document
msgid "Role Coverage"
msgstr ""
#. module: mvd_tcg_deck
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_add_to_deck__role_ids
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_deck_line__role_ids
msgid "Roles"
msgstr ""
#. module: mvd_tcg_deck
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.report_mvd_tcg_deck_document
msgid "Roles summarize how the deck is currently tagged across all boards."
msgstr ""
#. module: mvd_tcg_deck
#. odoo-python
#: code:addons/mvd_tcg_deck/wizards/mvd_tcg_add_to_deck.py:0
msgid "Select a card first."
msgstr ""
#. module: mvd_tcg_deck
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_deck_board__sequence
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_deck_line__sequence
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_deck_role__sequence
msgid "Sequence"
msgstr "Reihenfolge"
#. module: mvd_tcg_deck
#: model:ir.model,name:mvd_tcg_deck.model_mvd_tcg_card
msgid "TCG Card"
msgstr ""
#. module: mvd_tcg_deck
#: model:ir.model,name:mvd_tcg_deck.model_mvd_tcg_deck
msgid "TCG Deck"
msgstr ""
#. module: mvd_tcg_deck
#: model:ir.model,name:mvd_tcg_deck.model_mvd_tcg_deck_board
msgid "TCG Deck Board"
msgstr ""
#. module: mvd_tcg_deck
#: model:ir.model,name:mvd_tcg_deck.model_mvd_tcg_deck_line
msgid "TCG Deck Line"
msgstr ""
#. module: mvd_tcg_deck
#: model:ir.model,name:mvd_tcg_deck.model_report_mvd_tcg_deck_report_mvd_tcg_deck_document
msgid "TCG Deck Report"
msgstr ""
#. module: mvd_tcg_deck
#: model:ir.model,name:mvd_tcg_deck.model_mvd_tcg_deck_role
msgid "TCG Deck Role"
msgstr ""
#. module: mvd_tcg_deck
#: model:ir.model,name:mvd_tcg_deck.model_mvd_tcg_deck_text_transfer
msgid "TCG Deck Text Transfer"
msgstr ""
#. module: mvd_tcg_deck
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.mvd_tcg_deck_board_view_form
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.mvd_tcg_deck_role_view_form
msgid "Technical"
msgstr "Technisch"
#. module: mvd_tcg_deck
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_deck_role__technical_key
msgid "Technical Key"
msgstr ""
#. module: mvd_tcg_deck
#: model:ir.model.constraint,message:mvd_tcg_deck.constraint_mvd_tcg_deck_board_board_code_unique
msgid "The board code must be unique inside a deck."
msgstr ""
#. module: mvd_tcg_deck
#. odoo-python
#: code:addons/mvd_tcg_deck/wizards/mvd_tcg_deck_text_transfer.py:0
msgid ""
"The card '%(name)s' matches multiple printings. Add a print hint such as "
"'(TDM 101)' or pick the card directly in the deck builder."
msgstr ""
#. module: mvd_tcg_deck
#. odoo-python
#: code:addons/mvd_tcg_deck/wizards/mvd_tcg_deck_text_transfer.py:0
msgid ""
"The card '%(name)s' matches multiple printings. Add a print hint such as "
"'(TDM 101)' or use a more specific card name."
msgstr ""
#. module: mvd_tcg_deck
#: model:ir.model.constraint,message:mvd_tcg_deck.constraint_mvd_tcg_deck_role_name_unique
msgid "The deck role name must be unique."
msgstr ""
#. module: mvd_tcg_deck
#. odoo-python
#: code:addons/mvd_tcg_deck/wizards/mvd_tcg_deck_text_transfer.py:0
msgid ""
"The explicit reference '%(reference)s' matches multiple cards. Narrow the "
"import source first."
msgstr ""
#. module: mvd_tcg_deck
#: model:ir.model.constraint,message:mvd_tcg_deck.constraint_mvd_tcg_deck_line_quantity_positive
msgid "The quantity must be greater than zero."
msgstr ""
#. module: mvd_tcg_deck
#. odoo-python
#: code:addons/mvd_tcg_deck/models/mvd_tcg_deck.py:0
msgid ""
"The selected card already exists in the target board. Merge or adjust that "
"line first."
msgstr ""
#. module: mvd_tcg_deck
#: model:ir.model.constraint,message:mvd_tcg_deck.constraint_mvd_tcg_deck_role_technical_key_unique
msgid "The technical role key must be unique."
msgstr ""
#. module: mvd_tcg_deck
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_deck__total_card_count
#: model:ir.model.fields,field_description:mvd_tcg_deck.field_mvd_tcg_deck_board__total_card_count
msgid "Total Card Count"
msgstr ""
#. module: mvd_tcg_deck
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.report_mvd_tcg_deck_document
msgid "Total Copies"
msgstr ""
#. module: mvd_tcg_deck
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.report_mvd_tcg_deck_document
msgid "Unique references across boards"
msgstr ""
#. module: mvd_tcg_deck
#: model:mvd.tcg.deck.role,name:mvd_tcg_deck.mvd_tcg_deck_role_value
msgid "Value"
msgstr ""
#. module: mvd_tcg_deck
#: model:mvd.tcg.deck.role,name:mvd_tcg_deck.mvd_tcg_deck_role_wincon
msgid "Win Condition"
msgstr ""
#. module: mvd_tcg_deck
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.mvd_tcg_deck_line_view_search
msgid "With Roles"
msgstr ""
#. module: mvd_tcg_deck
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.mvd_tcg_deck_view_kanban
msgid "boards"
msgstr ""
#. module: mvd_tcg_deck
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.mvd_tcg_deck_view_kanban
msgid "cards"
msgstr ""
#. module: mvd_tcg_deck
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.report_mvd_tcg_deck_document
msgid "copies ·"
msgstr ""
#. module: mvd_tcg_deck
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.mvd_tcg_deck_view_kanban
#: model_terms:ir.ui.view,arch_db:mvd_tcg_deck.report_mvd_tcg_deck_document
msgid "distinct"
msgstr ""

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})

1
report/__init__.py Normal file
View File

@@ -0,0 +1 @@
from . import mvd_tcg_deck_report

View File

@@ -0,0 +1,509 @@
"""QWeb report helpers for deck exports."""
import base64
from collections import defaultdict
from functools import lru_cache
from pathlib import Path
import re
from markupsafe import Markup, escape
from odoo import api, models
class ReportMvdTcgDeck(models.AbstractModel):
"""Prepare stable, wkhtmltopdf-friendly data for deck reports."""
_name = "report.mvd_tcg_deck.report_mvd_tcg_deck_document"
_description = "TCG Deck Report"
_MANA_SYMBOL_FILENAME_MAP = {
"": "INFINITY",
"½": "HALF",
}
_COLOR_BADGE_MAP = {
"W": {
"background": "#f8f2d4",
"border": "#d9c676",
"text": "#5c5020",
},
"U": {
"background": "#d8ebfb",
"border": "#8bb4dc",
"text": "#1d4874",
},
"B": {
"background": "#d9d7de",
"border": "#a39ba8",
"text": "#2f2a33",
},
"R": {
"background": "#f6d8d2",
"border": "#d78f81",
"text": "#742c1f",
},
"G": {
"background": "#dcebd8",
"border": "#9ebc91",
"text": "#274f2e",
},
"C": {
"background": "#eef1f4",
"border": "#c7cfd8",
"text": "#46525f",
},
}
_COLOR_ORDER = "WUBRGC"
@staticmethod
@lru_cache(maxsize=256)
def _get_symbol_data_uri(filename):
"""Return one local MTG symbol asset as an inline data URI.
Args:
filename: Symbol filename stem such as ``W`` or ``2W``.
Returns:
str: Base64-encoded SVG data URI for direct report embedding.
"""
addons_root = Path(__file__).resolve().parents[2]
symbol_path = (
addons_root
/ "mvd_tcg_mtg"
/ "static"
/ "src"
/ "img"
/ "card-symbols"
/ f"{filename}.svg"
)
svg_bytes = symbol_path.read_bytes()
encoded = base64.b64encode(svg_bytes).decode("ascii")
return f"data:image/svg+xml;base64,{encoded}"
@staticmethod
@lru_cache(maxsize=64)
def _get_raster_symbol_data_uri(filename):
"""Return one local raster MTG symbol asset as an inline data URI.
Args:
filename: Symbol filename stem such as ``W``.
Returns:
str: Base64-encoded PNG data URI for direct report embedding.
"""
addons_root = Path(__file__).resolve().parents[2]
symbol_path = (
addons_root
/ "mvd_tcg_mtg"
/ "static"
/ "src"
/ "img"
/ "card-symbols-report"
/ f"{filename}.png"
)
png_bytes = symbol_path.read_bytes()
encoded = base64.b64encode(png_bytes).decode("ascii")
return f"data:image/png;base64,{encoded}"
@staticmethod
@lru_cache(maxsize=256)
def _get_report_symbol_data_uri(filename):
"""Return the most stable report symbol asset as an inline data URI.
Args:
filename: Symbol filename stem such as ``W`` or ``T``.
Returns:
str: Prefer a raster PNG data URI when available, else fall back
to the SVG asset.
"""
addons_root = Path(__file__).resolve().parents[2]
raster_path = (
addons_root
/ "mvd_tcg_mtg"
/ "static"
/ "src"
/ "img"
/ "card-symbols-report"
/ f"{filename}.png"
)
if raster_path.exists():
return ReportMvdTcgDeck._get_raster_symbol_data_uri(filename)
return ReportMvdTcgDeck._get_symbol_data_uri(filename)
@api.model
def _get_board_sections(self, deck):
"""Return ordered board sections with role-grouped deck lines.
Args:
deck: Deck record that should be rendered.
Returns:
list[dict]: Section dictionaries for the report template.
"""
sections = []
for board in deck.board_ids.sorted(
lambda current_board: (current_board.sequence, current_board.id)
):
grouped_lines = self._get_board_line_groups(board)
sections.append(
{
"board": board,
"lines": board.line_ids.sorted(
lambda line: (line.sequence, line.card_id.display_name or "", line.id)
),
"line_groups": grouped_lines,
"include_in_total": board.include_in_total,
"note": board.note,
"total_card_count": board.total_card_count,
"distinct_card_count": board.distinct_card_count,
}
)
return sections
@api.model
def _get_board_line_groups(self, board):
"""Group one board's lines by primary role and sorted card order.
Args:
board: Deck board record that should be rendered.
Returns:
list[dict]: Ordered groups with ordered line records.
"""
grouped_lines = defaultdict(list)
role_labels = {}
role_order = {}
for line in board.line_ids.filtered("card_id"):
primary_role = line.role_ids.sorted(
key=lambda role: (role.sequence, role.name or "", role.id)
)[:1]
if primary_role:
role_key = primary_role.id
role_labels[role_key] = primary_role.name
role_order[role_key] = (0, primary_role.sequence, primary_role.name or "", primary_role.id)
else:
role_key = "unassigned"
role_labels[role_key] = "Unassigned"
role_order[role_key] = (1, 9999, "Unassigned", 0)
grouped_lines[role_key].append(line)
ordered_groups = []
for role_key in sorted(grouped_lines, key=lambda current_key: role_order[current_key]):
lines = sorted(
grouped_lines[role_key],
key=lambda line: (
getattr(line, "mtg_mana_value", 0.0),
line.card_id.display_name or "",
getattr(line, "mtg_collector_number", "") or "",
line.id,
),
)
ordered_groups.append(
{
"key": role_key,
"label": role_labels[role_key],
"lines": lines,
"total_card_count": sum(line.quantity for line in lines),
"distinct_card_count": len(lines),
}
)
return ordered_groups
@api.model
def _get_color_badges(self, signature):
"""Return MTG color symbol descriptors for one color identity signature.
Args:
signature: Compact MTG color signature such as ``WUB``.
Returns:
list[dict]: Symbol descriptors with local static asset paths.
"""
badges = []
for code in (signature or "").strip().upper():
if code not in {"W", "U", "B", "R", "G", "C"}:
continue
palette = self._COLOR_BADGE_MAP[code]
badges.append(
{
"label": code,
"src": self._get_raster_symbol_data_uri(code),
"background": palette["background"],
"border": palette["border"],
"text": palette["text"],
}
)
return badges
@api.model
def _get_report_overview_markup(self, deck):
"""Return one compact overview block for the report cover page.
Args:
deck: Deck record rendered in the report.
Returns:
Markup | bool: First paragraph of the deck description, or ``False``.
"""
html_value = (deck.description or "").strip()
if not html_value:
return False
paragraph_match = re.search(
r"<p\b[^>]*>.*?</p>",
html_value,
flags=re.IGNORECASE | re.DOTALL,
)
if paragraph_match:
plain_text = re.sub(r"<[^>]+>", " ", paragraph_match.group(0))
plain_text = " ".join(plain_text.split())
if not plain_text:
return False
return Markup(f"<p>{escape(plain_text)}</p>")
plain_text = re.sub(r"<[^>]+>", " ", html_value)
plain_text = " ".join(plain_text.split())
if not plain_text:
return False
return Markup(f"<p>{escape(plain_text)}</p>")
@api.model
def _get_mana_color_signature(self, mana_cost):
"""Return the ordered color signature implied by one mana cost.
Args:
mana_cost: Raw Scryfall mana string such as ``{2}{U}{R}``.
Returns:
str: Ordered distinct MTG color signature such as ``UR``.
"""
colors = []
for token in re.findall(r"\{([^}]+)\}", mana_cost or ""):
normalized_token = (token or "").strip().upper()
for color_code in "WUBRGC":
if color_code in normalized_token and color_code not in colors:
colors.append(color_code)
return self._normalize_color_signature("".join(colors))
@api.model
def _normalize_color_signature(self, signature):
"""Normalize one MTG color signature to the canonical WUBRGC order.
Args:
signature: Raw color signature such as ``URW``.
Returns:
str: Canonically ordered signature such as ``WUR``.
"""
normalized = []
for color_code in self._COLOR_ORDER:
if color_code in (signature or "").upper() and color_code not in normalized:
normalized.append(color_code)
return "".join(normalized)
@api.model
def _get_line_color_badges(self, line):
"""Return color badges only when they add information beyond mana cost.
Args:
line: Deck line record rendered in the report.
Returns:
list[dict]: Color badge descriptors for the current line.
"""
signature = (
line.card_id.mtg_color_signature
or line.card_id.mtg_color_identity_signature
or ""
).strip().upper()
signature = self._normalize_color_signature(signature)
if not signature or line.board_id.code == "command_zone":
return []
mana_signature = self._get_mana_color_signature(getattr(line, "mtg_mana_cost", False))
if mana_signature and mana_signature == signature:
return []
return self._get_color_badges(signature)
@api.model
def _get_mana_symbols(self, mana_cost):
"""Return static symbol asset descriptors for one mana cost.
Args:
mana_cost: Raw Scryfall mana string such as ``{1}{W}{U}``.
Returns:
list[dict]: Ordered symbol descriptors for report rendering.
"""
tokens = re.findall(r"\{([^}]+)\}", mana_cost or "")
symbols = []
for token in tokens:
normalized_token = (token or "").strip().upper()
filename = self._MANA_SYMBOL_FILENAME_MAP.get(
normalized_token,
normalized_token.replace("/", ""),
)
symbols.append(
{
"label": normalized_token,
"src": self._get_report_symbol_data_uri(filename),
}
)
return symbols
@api.model
def _render_mtg_rules_text(self, rules_text):
"""Render MTG rules text with inline mana and action symbols.
Args:
rules_text: Raw MTG oracle or rules text that may contain Scryfall
symbol tokens such as ``{W}`` or ``{T}``.
Returns:
Markup: Safe HTML with inline symbol images and line breaks.
"""
if not rules_text:
return Markup("")
rendered_chunks = []
for chunk in re.split(r"(\{[^}]+\})", rules_text):
if not chunk:
continue
if chunk.startswith("{") and chunk.endswith("}"):
symbol_markup = self._render_mtg_inline_symbols(chunk)
if symbol_markup:
rendered_chunks.append(symbol_markup)
continue
rendered_chunks.append(str(escape(chunk)).replace("\n", "<br/>"))
return Markup("".join(rendered_chunks))
@api.model
def _render_mtg_inline_symbols(self, mana_cost):
"""Render one or more MTG symbol tokens as inline report HTML.
Args:
mana_cost: Token string such as ``{1}{U}`` or ``{T}``.
Returns:
Markup | str: Inline HTML markup or an empty string if unresolved.
"""
symbols = self._get_mana_symbols(mana_cost)
if not symbols:
return ""
rendered = []
for symbol in symbols:
rendered.append(
(
'<span class="o_mvd_tcg_report_oracle_symbol_frame">'
'<img class="o_mvd_tcg_report_oracle_symbol_icon" '
f'src="{escape(symbol["src"])}" '
f'alt="{escape(symbol["label"])}" '
f'title="{escape(symbol["label"])}"/>'
"</span>"
)
)
return Markup("".join(rendered))
@api.model
def _get_line_rule_sections(self, line):
"""Return face-aware MTG rules sections for one report line.
Args:
line: Deck line record rendered in the report.
Returns:
list[dict[str, object]]: Ordered rules sections for the current
line's card. Non-MTG lines fall back to an empty list.
"""
card = line.card_id
if not card:
return []
get_sections = getattr(card, "mtg_get_rules_sections", False)
if not callable(get_sections):
return []
return get_sections()
@api.model
def _get_mtg_type_breakdown(self, deck):
"""Return a report-friendly MTG type breakdown.
Args:
deck: Deck record that should be rendered.
Returns:
list[dict]: Ordered MTG type rows with relative bar widths.
"""
metric_rows = [
("Creatures", deck.mtg_creature_count, "#2f855a"),
("Instants", deck.mtg_instant_count, "#3182ce"),
("Sorceries", deck.mtg_sorcery_count, "#805ad5"),
("Artifacts", deck.mtg_artifact_count, "#4a5568"),
("Enchantments", deck.mtg_enchantment_count, "#b7791f"),
("Planeswalkers", deck.mtg_planeswalker_count, "#c05621"),
("Lands", deck.mtg_land_count, "#718096"),
]
max_value = max((row[1] for row in metric_rows), default=0) or 1
return [
{
"label": label,
"value": value,
"accent": accent,
"bar_width": round((value / max_value) * 100, 2) if value else 0.0,
}
for label, value, accent in metric_rows
]
@api.model
def _get_role_summary(self, deck):
"""Aggregate role usage across the whole deck.
Args:
deck: Deck record that should be rendered.
Returns:
list[dict]: Ordered role usage counters.
"""
role_totals = defaultdict(lambda: {"line_count": 0, "quantity_total": 0})
for line in deck.line_ids:
for role in line.role_ids:
role_totals[role]["line_count"] += 1
role_totals[role]["quantity_total"] += line.quantity
return [
{
"role": role,
"line_count": counters["line_count"],
"quantity_total": counters["quantity_total"],
}
for role, counters in sorted(
role_totals.items(),
key=lambda item: (item[0].sequence, item[0].name or "", item[0].id),
)
]
@api.model
def _get_report_values(self, docids, data=None):
"""Build the report context for QWeb rendering.
Args:
docids: Selected deck identifiers.
data: Optional wizard payload passed by Odoo.
Returns:
dict: Context consumed by the QWeb template.
"""
docs = self.env["mvd.tcg.deck"].browse(docids)
return {
"doc_ids": docs.ids,
"doc_model": "mvd.tcg.deck",
"docs": docs,
"data": data or {},
"get_board_sections": self._get_board_sections,
"get_color_badges": self._get_color_badges,
"get_line_color_badges": self._get_line_color_badges,
"get_line_rule_sections": self._get_line_rule_sections,
"get_mana_symbols": self._get_mana_symbols,
"get_mtg_type_breakdown": self._get_mtg_type_breakdown,
"render_mtg_rules_text": self._render_mtg_rules_text,
"get_role_summary": self._get_role_summary,
"get_report_overview_markup": self._get_report_overview_markup,
}

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="paperformat_mvd_tcg_deck" model="report.paperformat">
<field name="name">MVD TCG Deck Report</field>
<field name="default" eval="False"/>
<field name="format">A4</field>
<field name="orientation">Portrait</field>
<field name="margin_top">8</field>
<field name="margin_bottom">8</field>
<field name="margin_left">7</field>
<field name="margin_right">7</field>
<field name="header_line" eval="False"/>
<field name="header_spacing">0</field>
<field name="dpi">96</field>
</record>
<record id="action_report_mvd_tcg_deck" model="ir.actions.report">
<field name="name">Deck Report</field>
<field name="model">mvd.tcg.deck</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">mvd_tcg_deck.report_mvd_tcg_deck_document</field>
<field name="report_file">mvd_tcg_deck.report_mvd_tcg_deck_document</field>
<field name="print_report_name">'Deck - %s' % (object.name)</field>
<field name="paperformat_id" ref="mvd_tcg_deck.paperformat_mvd_tcg_deck"/>
<field name="binding_model_id" ref="mvd_tcg_deck.model_mvd_tcg_deck"/>
<field name="binding_type">report</field>
</record>
</odoo>

View File

@@ -0,0 +1,763 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="report_mvd_tcg_deck_document">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="doc">
<t t-set="board_sections" t-value="get_board_sections(doc)"/>
<t t-set="role_summary" t-value="get_role_summary(doc)"/>
<div
class="article o_mvd_tcg_report_article"
t-att-data-oe-model="doc._name"
t-att-data-oe-id="doc.id"
t-att-data-oe-lang="doc.env.context.get('lang')"
>
<style type="text/css">
.o_mvd_tcg_report_article {
padding: 0;
}
.o_mvd_tcg_report {
color: #17212b;
font-size: 10.5px;
line-height: 1.42;
}
.o_mvd_tcg_report table {
width: 100%;
border-collapse: separate;
border-spacing: 0;
}
.o_mvd_tcg_report.page,
.o_mvd_tcg_report .page {
background: #ffffff;
}
.o_mvd_tcg_report .o_mvd_tcg_report_hero {
margin: 0 0 10px;
background: #ffffff;
color: #0f172a;
border-radius: 12px;
overflow: hidden;
border: 1px solid #d9e2ec;
border-top: 4px solid #4f46e5;
}
.o_mvd_tcg_report .o_mvd_tcg_report_hero td {
vertical-align: top;
padding: 0;
}
.o_mvd_tcg_report .o_mvd_tcg_report_hero_main {
padding: 12px 14px 10px;
}
.o_mvd_tcg_report .o_mvd_tcg_report_hero_cover {
width: 5.8cm;
padding: 10px 12px 10px 0;
text-align: right;
}
.o_mvd_tcg_report .o_mvd_tcg_report_eyebrow {
margin: 0 0 6px;
font-size: 9px;
letter-spacing: 0.16em;
text-transform: uppercase;
color: #4f46e5;
}
.o_mvd_tcg_report .o_mvd_tcg_report_title {
margin: 0 0 6px;
font-size: 24px;
line-height: 1.1;
font-weight: 700;
color: #0f172a;
}
.o_mvd_tcg_report .o_mvd_tcg_report_subtitle {
margin: 0 0 8px;
color: #52606d;
}
.o_mvd_tcg_report .o_mvd_tcg_report_chip {
display: inline-block;
margin: 0 6px 6px 0;
padding: 4px 10px;
border: 1px solid #d9e2ec;
border-radius: 999px;
background: #f8fafc;
color: #334e68;
white-space: nowrap;
}
.o_mvd_tcg_report .o_mvd_tcg_report_chip strong {
color: #102a43;
}
.o_mvd_tcg_report .o_mvd_tcg_report_cover {
max-width: 5.2cm;
max-height: 7.3cm;
border-radius: 10px;
border: 1px solid #cbd5e1;
box-shadow: 0 10px 20px rgba(15, 23, 42, 0.12);
}
.o_mvd_tcg_report .o_mvd_tcg_report_hero .o_mvd_tcg_report_kpi_grid {
margin-top: 8px;
}
.o_mvd_tcg_report .o_mvd_tcg_report_section {
margin: 0 0 8px;
padding: 8px 10px;
border: 1px solid #dde5f0;
border-radius: 10px;
background: #ffffff;
}
.o_mvd_tcg_report .o_mvd_tcg_report_section_emphasis {
border-color: #d9e2ec;
background: linear-gradient(180deg, #ffffff 0%, #f9fbff 100%);
}
.o_mvd_tcg_report .o_mvd_tcg_report_section_title {
margin: 0 0 6px;
font-size: 13px;
line-height: 1.2;
color: #0f172a;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.o_mvd_tcg_report .o_mvd_tcg_report_section_intro {
margin: 0 0 6px;
color: #5b6777;
font-size: 9px;
}
.o_mvd_tcg_report .o_mvd_tcg_report_kpi_grid td {
width: 25%;
padding: 0 8px 0 0;
vertical-align: top;
}
.o_mvd_tcg_report .o_mvd_tcg_report_kpi_grid td:last-child {
padding-right: 0;
}
.o_mvd_tcg_report .o_mvd_tcg_report_kpi {
min-height: 40px;
padding: 6px 8px;
border: 1px solid #dde5f0;
border-radius: 10px;
background: #ffffff;
}
.o_mvd_tcg_report .o_mvd_tcg_report_kpi_label {
margin: 0 0 4px;
font-size: 9px;
letter-spacing: 0.08em;
text-transform: uppercase;
color: #748195;
}
.o_mvd_tcg_report .o_mvd_tcg_report_kpi_value {
margin: 0;
font-size: 15px;
line-height: 1.1;
font-weight: 700;
color: #0f172a;
}
.o_mvd_tcg_report .o_mvd_tcg_report_kpi_hint {
margin: 2px 0 0;
color: #64748b;
font-size: 9px;
}
.o_mvd_tcg_report .o_mvd_tcg_report_snapshot td {
width: 50%;
padding: 0 10px 0 0;
vertical-align: top;
}
.o_mvd_tcg_report .o_mvd_tcg_report_snapshot td:last-child {
padding-right: 0;
}
.o_mvd_tcg_report .o_mvd_tcg_report_snapshot_block {
min-height: 56px;
padding: 8px 10px;
border-radius: 10px;
background: #fbfdff;
border: 1px solid #dde5f0;
}
.o_mvd_tcg_report .o_mvd_tcg_report_snapshot_block h4 {
margin: 0 0 6px;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.08em;
color: #334155;
}
.o_mvd_tcg_report .o_mvd_tcg_report_snapshot_block p {
margin: 0;
}
.o_mvd_tcg_report .o_mvd_tcg_report_snapshot_note {
white-space: pre-line;
color: #52606d;
}
.o_mvd_tcg_report .o_mvd_tcg_report_data_table th,
.o_mvd_tcg_report .o_mvd_tcg_report_data_table td {
padding: 6px 8px;
border-bottom: 1px solid #e5edf5;
vertical-align: top;
}
.o_mvd_tcg_report .o_mvd_tcg_report_data_table thead th {
background: #ecf2f9;
color: #334155;
font-size: 9px;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.o_mvd_tcg_report .o_mvd_tcg_report_data_table tbody tr:nth-child(even) td {
background: #fbfdff;
}
.o_mvd_tcg_report .o_mvd_tcg_report_data_table tbody tr:last-child td {
border-bottom: 0;
}
.o_mvd_tcg_report .o_mvd_tcg_report_roles_list span {
display: inline-block;
margin: 0 5px 5px 0;
padding: 3px 8px;
border: 1px solid #d9e2ec;
border-radius: 999px;
background: #ffffff;
white-space: nowrap;
}
.o_mvd_tcg_report .o_mvd_tcg_report_pill {
display: inline-block;
padding: 4px 10px;
border-radius: 999px;
font-size: 9px;
letter-spacing: 0.08em;
text-transform: uppercase;
font-weight: 700;
white-space: nowrap;
}
.o_mvd_tcg_report .o_mvd_tcg_report_pill_success {
background: #dcfce7;
color: #166534;
}
.o_mvd_tcg_report .o_mvd_tcg_report_pill_muted {
background: #e5edf5;
color: #486581;
}
.o_mvd_tcg_report .o_mvd_tcg_report_board_header {
margin: 0 0 12px;
padding: 14px 16px;
border-radius: 10px;
background: #ffffff;
color: #0f172a;
border: 1px solid #dde5f0;
border-left: 4px solid #4f46e5;
}
.o_mvd_tcg_report .o_mvd_tcg_report_board_header h2 {
margin: 0 0 8px;
font-size: 18px;
line-height: 1.1;
}
.o_mvd_tcg_report .o_mvd_tcg_report_board_header p {
margin: 0;
color: #52606d;
}
.o_mvd_tcg_report .o_mvd_tcg_report_board_meta_table td {
width: 33.33%;
padding: 0 10px 0 0;
vertical-align: top;
}
.o_mvd_tcg_report .o_mvd_tcg_report_board_meta_table td:last-child {
padding-right: 0;
}
.o_mvd_tcg_report .o_mvd_tcg_report_board_meta_card {
padding: 10px 12px;
border: 1px solid #dde5f0;
border-radius: 10px;
background: linear-gradient(180deg, #ffffff 0%, #f8fbff 100%);
}
.o_mvd_tcg_report .o_mvd_tcg_report_board_meta_card .o_mvd_tcg_report_kpi_value {
font-size: 18px;
}
.o_mvd_tcg_report .o_mvd_tcg_report_card_name {
font-weight: 700;
color: #102a43;
line-height: 1.15;
}
.o_mvd_tcg_report .o_mvd_tcg_report_card_name_text {
display: inline;
}
.o_mvd_tcg_report .o_mvd_tcg_report_inline_symbol_group {
display: inline-block;
margin-left: 4px;
vertical-align: text-bottom;
white-space: nowrap;
line-height: 0;
}
.o_mvd_tcg_report .o_mvd_tcg_report_inline_symbol_group + .o_mvd_tcg_report_inline_symbol_group {
margin-left: 6px;
}
.o_mvd_tcg_report .o_mvd_tcg_report_group_heading {
margin: 0 0 6px;
padding: 7px 10px;
border-left: 4px solid #4f46e5;
border-radius: 8px;
background: #f8fbff;
}
.o_mvd_tcg_report .o_mvd_tcg_report_group_heading strong {
color: #0f172a;
}
.o_mvd_tcg_report .o_mvd_tcg_report_group_heading span {
color: #64748b;
margin-left: 8px;
font-size: 9px;
}
.o_mvd_tcg_report .o_mvd_tcg_report_card_thumb {
width: 15mm;
text-align: center;
vertical-align: top;
}
.o_mvd_tcg_report .o_mvd_tcg_report_card_thumb img {
display: block;
width: 12mm;
height: auto;
border-radius: 4px;
border: 1px solid #dbe4ee;
}
.o_mvd_tcg_report .o_mvd_tcg_report_qty_cell {
width: 8mm;
text-align: center;
vertical-align: top;
font-weight: 700;
color: #102a43;
}
.o_mvd_tcg_report .o_mvd_tcg_report_card_table th,
.o_mvd_tcg_report .o_mvd_tcg_report_card_table td {
padding: 4px 6px;
}
.o_mvd_tcg_report .o_mvd_tcg_report_card_meta {
margin-top: 2px;
color: #52606d;
font-size: 8.6px;
line-height: 1.2;
}
.o_mvd_tcg_report .o_mvd_tcg_report_oracle {
margin-top: 2px;
color: #334155;
font-size: 8.2px;
line-height: 1.2;
white-space: pre-line;
}
.o_mvd_tcg_report .o_mvd_tcg_report_face_block {
margin-top: 4px;
padding-top: 4px;
border-top: 1px dashed #dbe4ee;
}
.o_mvd_tcg_report .o_mvd_tcg_report_face_block:first-child {
margin-top: 2px;
padding-top: 0;
border-top: 0;
}
.o_mvd_tcg_report .o_mvd_tcg_report_face_title {
font-weight: 700;
color: #102a43;
line-height: 1.15;
}
.o_mvd_tcg_report .o_mvd_tcg_report_oracle_symbol_frame {
display: inline-block;
width: 6.4px;
height: 6.4px;
margin: 0 1px;
vertical-align: 0;
line-height: 0;
}
.o_mvd_tcg_report .o_mvd_tcg_report_oracle_symbol_icon {
display: block !important;
width: 6.4px !important;
height: 6.4px !important;
min-width: 6.4px;
min-height: 6.4px;
max-width: 6.4px !important;
max-height: 6.4px !important;
vertical-align: top;
}
.o_mvd_tcg_report .o_mvd_tcg_report_callout {
padding: 12px 14px;
border-left: 4px solid #4f46e5;
border-radius: 10px;
background: #f5f7ff;
color: #3730a3;
}
.o_mvd_tcg_report .o_mvd_tcg_report_symbol_group {
display: inline-block;
vertical-align: middle;
white-space: nowrap;
line-height: 0;
}
.o_mvd_tcg_report .o_mvd_tcg_report_symbol_frame {
display: inline-block;
width: 15px;
height: 15px;
margin-right: 2px;
vertical-align: middle;
line-height: 0;
}
.o_mvd_tcg_report .o_mvd_tcg_report_symbol_icon {
display: block !important;
width: 15px !important;
height: 15px !important;
min-width: 15px;
min-height: 15px;
max-width: 15px !important;
max-height: 15px !important;
vertical-align: top;
}
.o_mvd_tcg_report .o_mvd_tcg_report_inline_symbol_frame {
width: 11px;
height: 11px;
margin-right: 1px;
}
.o_mvd_tcg_report .o_mvd_tcg_report_inline_symbol_icon {
width: 11px !important;
height: 11px !important;
min-width: 11px;
min-height: 11px;
max-width: 11px !important;
max-height: 11px !important;
}
.o_mvd_tcg_report .o_mvd_tcg_report_identity_frame {
width: 14px;
height: 14px;
margin-right: 2px;
}
.o_mvd_tcg_report .o_mvd_tcg_report_identity_icon {
width: 14px !important;
height: 14px !important;
min-width: 14px;
min-height: 14px;
max-width: 14px !important;
max-height: 14px !important;
}
.o_mvd_tcg_report .o_mvd_tcg_report_color_chip {
display: inline-block;
width: 18px;
height: 18px;
line-height: 18px;
text-align: center;
border-radius: 999px;
font-size: 9px;
font-weight: 700;
margin-right: 3px;
vertical-align: middle;
}
.o_mvd_tcg_report .o_mvd_tcg_report_muted {
color: #6b7280;
}
.o_mvd_tcg_report .o_mvd_tcg_report_empty {
padding: 14px 0;
color: #6b7280;
font-style: italic;
}
.o_mvd_tcg_report .o_mvd_tcg_report_page_meta {
display: none;
}
.o_mvd_tcg_report .o_mvd_tcg_report_footer {
margin-top: 10px;
padding-top: 8px;
border-top: 1px solid #dbe4ee;
font-size: 9px;
color: #64748b;
text-align: right;
}
.o_mvd_tcg_report .o_mvd_tcg_report_page_break {
page-break-before: always;
}
</style>
<div class="page o_mvd_tcg_report">
<table class="o_mvd_tcg_report_hero">
<tbody>
<tr>
<td class="o_mvd_tcg_report_hero_main">
<p class="o_mvd_tcg_report_eyebrow">MVD TCG Decksheet</p>
<h1 class="o_mvd_tcg_report_title">
<span t-field="doc.name"/>
</h1>
<p class="o_mvd_tcg_report_subtitle">
Compact deck overview for print and review.
</p>
<div>
<span class="o_mvd_tcg_report_chip">
<strong>Game:</strong>
<span t-field="doc.game_id"/>
</span>
<span class="o_mvd_tcg_report_chip">
<strong>Boards:</strong>
<span t-field="doc.board_count"/>
</span>
<span class="o_mvd_tcg_report_chip">
<strong>Owner:</strong>
<span t-field="doc.user_id"/>
</span>
<span class="o_mvd_tcg_report_chip">
<strong>Updated:</strong>
<span t-field="doc.write_date"/>
</span>
</div>
<table class="o_mvd_tcg_report_kpi_grid">
<tbody>
<tr>
<td>
<div class="o_mvd_tcg_report_kpi">
<p class="o_mvd_tcg_report_kpi_label">Total Copies</p>
<p class="o_mvd_tcg_report_kpi_value">
<span t-field="doc.total_card_count"/>
</p>
<p class="o_mvd_tcg_report_kpi_hint">Cards counted in the active build</p>
</div>
</td>
<td>
<div class="o_mvd_tcg_report_kpi">
<p class="o_mvd_tcg_report_kpi_label">Distinct Cards</p>
<p class="o_mvd_tcg_report_kpi_value">
<span t-field="doc.distinct_card_count"/>
</p>
<p class="o_mvd_tcg_report_kpi_hint">Unique references across boards</p>
</div>
</td>
</tr>
</tbody>
</table>
</td>
<td class="o_mvd_tcg_report_hero_cover">
<img
t-if="doc.cover_image"
t-att-src="image_data_uri(doc.cover_image)"
class="o_mvd_tcg_report_cover"
alt="Deck cover"
/>
</td>
</tr>
</tbody>
</table>
<div class="o_mvd_tcg_report_section">
<h3 class="o_mvd_tcg_report_section_title">Deck Snapshot</h3>
<table class="o_mvd_tcg_report_snapshot">
<tbody>
<tr>
<td>
<div class="o_mvd_tcg_report_snapshot_block">
<h4>Overview</h4>
<div t-if="get_report_overview_markup(doc)" t-out="get_report_overview_markup(doc)"/>
<p t-else="" class="o_mvd_tcg_report_muted">
No deck overview has been written yet.
</p>
</div>
</td>
</tr>
<tr>
<td>
<div class="o_mvd_tcg_report_snapshot_block">
<h4>Internal Notes</h4>
<p
t-if="doc.note"
class="o_mvd_tcg_report_snapshot_note"
t-out="doc.note"
/>
<p t-else="" class="o_mvd_tcg_report_muted">
No internal notes recorded.
</p>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div class="o_mvd_tcg_report_extension_hook"/>
<div class="o_mvd_tcg_report_page_break"/>
<div class="o_mvd_tcg_report_section">
<h3 class="o_mvd_tcg_report_section_title">Board Summary</h3>
<p class="o_mvd_tcg_report_section_intro">Per-board size, distinct cards, and build participation.</p>
<table class="o_mvd_tcg_report_data_table">
<thead>
<tr>
<th style="width: 28%;">Board</th>
<th style="width: 12%;">Copies</th>
<th style="width: 12%;">Distinct</th>
<th style="width: 18%;">Build Status</th>
<th style="width: 30%;">Note</th>
</tr>
</thead>
<tbody>
<t t-foreach="board_sections" t-as="section">
<tr>
<td>
<strong t-field="section['board'].name"/>
</td>
<td><t t-out="section['total_card_count']"/></td>
<td><t t-out="section['distinct_card_count']"/></td>
<td>
<span
t-if="section['include_in_total']"
class="o_mvd_tcg_report_pill o_mvd_tcg_report_pill_success"
>
Included
</span>
<span
t-else=""
class="o_mvd_tcg_report_pill o_mvd_tcg_report_pill_muted"
>
Reference Only
</span>
</td>
<td>
<span
t-if="section['note']"
t-out="section['note']"
/>
<span t-else="" class="o_mvd_tcg_report_muted"></span>
</td>
</tr>
</t>
</tbody>
</table>
</div>
<div t-if="role_summary" class="o_mvd_tcg_report_section">
<h3 class="o_mvd_tcg_report_section_title">Role Coverage</h3>
<p class="o_mvd_tcg_report_section_intro">
Roles summarize how the deck is currently tagged across all boards.
</p>
<table class="o_mvd_tcg_report_data_table">
<thead>
<tr>
<th style="width: 48%;">Role</th>
<th style="width: 26%;">Entries</th>
<th style="width: 26%;">Total Copies</th>
</tr>
</thead>
<tbody>
<t t-foreach="role_summary" t-as="role_row">
<tr>
<td><strong t-field="role_row['role'].name"/></td>
<td><t t-out="role_row['line_count']"/></td>
<td><t t-out="role_row['quantity_total']"/></td>
</tr>
</t>
</tbody>
</table>
</div>
<div class="o_mvd_tcg_report_footer">
<span t-field="doc.name"/>
<span> · </span>
<span t-field="doc.game_id"/>
<span> · </span>
<span t-field="doc.write_date"/>
<span> · </span>
<span class="page"/> / <span class="topage"/>
</div>
</div>
<t t-foreach="board_sections" t-as="section">
<div
class="page o_mvd_tcg_report o_mvd_tcg_report_page_break"
style="page-break-before: always;"
>
<div class="o_mvd_tcg_report_board_header">
<h2>
<span t-field="section['board'].name"/>
</h2>
<p>
<span
t-if="section['include_in_total']"
class="o_mvd_tcg_report_pill o_mvd_tcg_report_pill_success"
>
Included in build
</span>
<span
t-else=""
class="o_mvd_tcg_report_pill o_mvd_tcg_report_pill_muted"
>
Reference board
</span>
<t t-if="section['note']">
<span style="margin-left: 8px;" t-out="section['note']"/>
</t>
</p>
</div>
<table class="o_mvd_tcg_report_board_meta_table" style="margin-bottom: 10px;">
<tbody>
<tr>
<td>
<div class="o_mvd_tcg_report_board_meta_card">
<p class="o_mvd_tcg_report_kpi_label">Copies</p>
<p class="o_mvd_tcg_report_kpi_value">
<t t-out="section['total_card_count']"/>
</p>
</div>
</td>
<td>
<div class="o_mvd_tcg_report_board_meta_card">
<p class="o_mvd_tcg_report_kpi_label">Distinct Cards</p>
<p class="o_mvd_tcg_report_kpi_value">
<t t-out="section['distinct_card_count']"/>
</p>
</div>
</td>
<td>
<div class="o_mvd_tcg_report_board_meta_card">
<p class="o_mvd_tcg_report_kpi_label">Build Participation</p>
<p class="o_mvd_tcg_report_kpi_value" style="font-size: 15px;">
<t t-if="section['include_in_total']">Included</t>
<t t-else="">Reference</t>
</p>
</div>
</td>
</tr>
</tbody>
</table>
<t t-if="section['line_groups']">
<t t-foreach="section['line_groups']" t-as="group">
<div class="o_mvd_tcg_report_group_heading">
<strong t-out="group['label']"/>
<span>
<t t-out="group['total_card_count']"/> copies ·
<t t-out="group['distinct_card_count']"/> distinct
</span>
</div>
<table class="o_mvd_tcg_report_data_table o_mvd_tcg_report_card_table" style="margin-bottom: 8px;">
<thead>
<tr>
<th style="width: 10%;">Image</th>
<th style="width: 8%;">Qty</th>
<th style="width: 82%;">Card</th>
</tr>
</thead>
<tbody>
<t t-foreach="group['lines']" t-as="line">
<tr>
<td class="o_mvd_tcg_report_card_thumb">
<img
t-if="line.card_image_128"
t-att-src="image_data_uri(line.card_image_128)"
alt="Card image"
/>
</td>
<td class="o_mvd_tcg_report_qty_cell">
<span t-field="line.quantity"/>
</td>
<td class="o_mvd_tcg_report_card_cell">
<div class="o_mvd_tcg_report_card_name">
<span t-field="line.card_id"/>
</div>
</td>
</tr>
</t>
</tbody>
</table>
</t>
</t>
<p t-else="" class="o_mvd_tcg_report_empty">No cards in this board.</p>
<div class="o_mvd_tcg_report_footer">
<span t-field="doc.name"/>
<span> · </span>
<span t-field="doc.game_id"/>
<span> · </span>
<span class="page"/> / <span class="topage"/>
</div>
</div>
</t>
</div>
</t>
</t>
</template>
</odoo>

View File

@@ -0,0 +1,17 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_mvd_tcg_deck_user,mvd.tcg.deck.user,model_mvd_tcg_deck,mvd_tcg_base.mvd_tcg_base_group_user,1,1,1,1
access_mvd_tcg_deck_manager,mvd.tcg.deck.manager,model_mvd_tcg_deck,mvd_tcg_base.mvd_tcg_base_group_manager,1,1,1,1
access_mvd_tcg_deck_system,mvd.tcg.deck.system,model_mvd_tcg_deck,base.group_system,1,1,1,1
access_mvd_tcg_deck_board_user,mvd.tcg.deck.board.user,model_mvd_tcg_deck_board,mvd_tcg_base.mvd_tcg_base_group_user,1,1,1,1
access_mvd_tcg_deck_board_manager,mvd.tcg.deck.board.manager,model_mvd_tcg_deck_board,mvd_tcg_base.mvd_tcg_base_group_manager,1,1,1,1
access_mvd_tcg_deck_board_system,mvd.tcg.deck.board.system,model_mvd_tcg_deck_board,base.group_system,1,1,1,1
access_mvd_tcg_deck_line_user,mvd.tcg.deck.line.user,model_mvd_tcg_deck_line,mvd_tcg_base.mvd_tcg_base_group_user,1,1,1,1
access_mvd_tcg_deck_line_manager,mvd.tcg.deck.line.manager,model_mvd_tcg_deck_line,mvd_tcg_base.mvd_tcg_base_group_manager,1,1,1,1
access_mvd_tcg_deck_line_system,mvd.tcg.deck.line.system,model_mvd_tcg_deck_line,base.group_system,1,1,1,1
access_mvd_tcg_deck_role_user,mvd.tcg.deck.role.user,model_mvd_tcg_deck_role,mvd_tcg_base.mvd_tcg_base_group_user,1,0,0,0
access_mvd_tcg_deck_role_manager,mvd.tcg.deck.role.manager,model_mvd_tcg_deck_role,mvd_tcg_base.mvd_tcg_base_group_manager,1,1,1,1
access_mvd_tcg_deck_role_system,mvd.tcg.deck.role.system,model_mvd_tcg_deck_role,base.group_system,1,1,1,1
access_mvd_tcg_add_to_deck_user,mvd.tcg.add.to.deck.user,model_mvd_tcg_add_to_deck,mvd_tcg_base.mvd_tcg_base_group_user,1,1,1,1
access_mvd_tcg_add_to_deck_system,mvd.tcg.add.to.deck.system,model_mvd_tcg_add_to_deck,base.group_system,1,1,1,1
access_mvd_tcg_deck_text_transfer_user,mvd.tcg.deck.text.transfer.user,model_mvd_tcg_deck_text_transfer,mvd_tcg_base.mvd_tcg_base_group_user,1,1,1,1
access_mvd_tcg_deck_text_transfer_system,mvd.tcg.deck.text.transfer.system,model_mvd_tcg_deck_text_transfer,base.group_system,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_mvd_tcg_deck_user mvd.tcg.deck.user model_mvd_tcg_deck mvd_tcg_base.mvd_tcg_base_group_user 1 1 1 1
3 access_mvd_tcg_deck_manager mvd.tcg.deck.manager model_mvd_tcg_deck mvd_tcg_base.mvd_tcg_base_group_manager 1 1 1 1
4 access_mvd_tcg_deck_system mvd.tcg.deck.system model_mvd_tcg_deck base.group_system 1 1 1 1
5 access_mvd_tcg_deck_board_user mvd.tcg.deck.board.user model_mvd_tcg_deck_board mvd_tcg_base.mvd_tcg_base_group_user 1 1 1 1
6 access_mvd_tcg_deck_board_manager mvd.tcg.deck.board.manager model_mvd_tcg_deck_board mvd_tcg_base.mvd_tcg_base_group_manager 1 1 1 1
7 access_mvd_tcg_deck_board_system mvd.tcg.deck.board.system model_mvd_tcg_deck_board base.group_system 1 1 1 1
8 access_mvd_tcg_deck_line_user mvd.tcg.deck.line.user model_mvd_tcg_deck_line mvd_tcg_base.mvd_tcg_base_group_user 1 1 1 1
9 access_mvd_tcg_deck_line_manager mvd.tcg.deck.line.manager model_mvd_tcg_deck_line mvd_tcg_base.mvd_tcg_base_group_manager 1 1 1 1
10 access_mvd_tcg_deck_line_system mvd.tcg.deck.line.system model_mvd_tcg_deck_line base.group_system 1 1 1 1
11 access_mvd_tcg_deck_role_user mvd.tcg.deck.role.user model_mvd_tcg_deck_role mvd_tcg_base.mvd_tcg_base_group_user 1 0 0 0
12 access_mvd_tcg_deck_role_manager mvd.tcg.deck.role.manager model_mvd_tcg_deck_role mvd_tcg_base.mvd_tcg_base_group_manager 1 1 1 1
13 access_mvd_tcg_deck_role_system mvd.tcg.deck.role.system model_mvd_tcg_deck_role base.group_system 1 1 1 1
14 access_mvd_tcg_add_to_deck_user mvd.tcg.add.to.deck.user model_mvd_tcg_add_to_deck mvd_tcg_base.mvd_tcg_base_group_user 1 1 1 1
15 access_mvd_tcg_add_to_deck_system mvd.tcg.add.to.deck.system model_mvd_tcg_add_to_deck base.group_system 1 1 1 1
16 access_mvd_tcg_deck_text_transfer_user mvd.tcg.deck.text.transfer.user model_mvd_tcg_deck_text_transfer mvd_tcg_base.mvd_tcg_base_group_user 1 1 1 1
17 access_mvd_tcg_deck_text_transfer_system mvd.tcg.deck.text.transfer.system model_mvd_tcg_deck_text_transfer base.group_system 1 1 1 1

View File

@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="mvd_tcg_deck_rule_user_own_decks" model="ir.rule">
<field name="name">Own decks</field>
<field name="model_id" ref="mvd_tcg_deck.model_mvd_tcg_deck"/>
<field name="domain_force">[('user_id', '=', user.id)]</field>
<field name="groups" eval="[(4, ref('mvd_tcg_base.mvd_tcg_base_group_user'))]"/>
</record>
<record id="mvd_tcg_deck_rule_manager_all_decks" model="ir.rule">
<field name="name">All decks for managers</field>
<field name="model_id" ref="mvd_tcg_deck.model_mvd_tcg_deck"/>
<field name="domain_force">[(1, '=', 1)]</field>
<field name="groups" eval="[(4, ref('mvd_tcg_base.mvd_tcg_base_group_manager'))]"/>
</record>
<record id="mvd_tcg_deck_board_rule_user_own_decks" model="ir.rule">
<field name="name">Own deck boards</field>
<field name="model_id" ref="mvd_tcg_deck.model_mvd_tcg_deck_board"/>
<field name="domain_force">[('deck_id.user_id', '=', user.id)]</field>
<field name="groups" eval="[(4, ref('mvd_tcg_base.mvd_tcg_base_group_user'))]"/>
</record>
<record id="mvd_tcg_deck_board_rule_manager_all_decks" model="ir.rule">
<field name="name">All deck boards for managers</field>
<field name="model_id" ref="mvd_tcg_deck.model_mvd_tcg_deck_board"/>
<field name="domain_force">[(1, '=', 1)]</field>
<field name="groups" eval="[(4, ref('mvd_tcg_base.mvd_tcg_base_group_manager'))]"/>
</record>
<record id="mvd_tcg_deck_line_rule_user_own_decks" model="ir.rule">
<field name="name">Own deck lines</field>
<field name="model_id" ref="mvd_tcg_deck.model_mvd_tcg_deck_line"/>
<field name="domain_force">[('deck_id.user_id', '=', user.id)]</field>
<field name="groups" eval="[(4, ref('mvd_tcg_base.mvd_tcg_base_group_user'))]"/>
</record>
<record id="mvd_tcg_deck_line_rule_manager_all_decks" model="ir.rule">
<field name="name">All deck lines for managers</field>
<field name="model_id" ref="mvd_tcg_deck.model_mvd_tcg_deck_line"/>
<field name="domain_force">[(1, '=', 1)]</field>
<field name="groups" eval="[(4, ref('mvd_tcg_base.mvd_tcg_base_group_manager'))]"/>
</record>
</odoo>

View File

@@ -0,0 +1,50 @@
/** @odoo-module **/
import { registry } from "@web/core/registry";
import { useService } from "@web/core/utils/hooks";
import { ImageField, imageField } from "@web/views/fields/image/image_field";
class MvdDeckZoomImageField extends ImageField {
static template = "mvd_tcg_deck.MvdDeckZoomImageField";
setup() {
super.setup();
this.actionService = useService("action");
}
get isOpenable() {
return Boolean(this.props.record?.resId && this.props.record?.data?.card_id);
}
get tooltipAttributes() {
const fieldName = this.props.previewImage || this.props.name;
return {
template: "web.ImageZoomTooltip",
info: JSON.stringify({ url: this.getUrl(fieldName) }),
};
}
async onImageClick(ev) {
if (!this.isOpenable) {
return;
}
ev.preventDefault();
ev.stopPropagation();
const action = await this.orm.call(
this.props.record.resModel,
"action_open_card_preview",
[[this.props.record.resId]]
);
if (action) {
return this.actionService.doAction(action);
}
return undefined;
}
}
export const mvdDeckZoomImageField = {
...imageField,
component: MvdDeckZoomImageField,
};
registry.category("fields").add("mvd_deck_zoom_image", mvdDeckZoomImageField);

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t
t-name="mvd_tcg_deck.MvdDeckZoomImageField"
t-inherit="web.ImageField"
t-inherit-mode="primary"
>
<xpath expr="//img" position="attributes">
<attribute name="t-att-class">`${imgClass}${isOpenable ? ' cursor-pointer' : ''}`</attribute>
<attribute name="t-on-click">onImageClick</attribute>
</xpath>
</t>
</templates>

11
views/menu_views.xml Normal file
View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<menuitem
id="mvd_tcg_decks_menu"
name="All Decks"
parent="mvd_tcg_base.mvd_tcg_operations_menu"
sequence="10"
action="mvd_tcg_deck.mvd_tcg_deck_action"
groups="mvd_tcg_base.mvd_tcg_base_group_manager,base.group_system"
/>
</odoo>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="mvd_tcg_add_to_deck_view_form" model="ir.ui.view">
<field name="name">mvd.tcg.add.to.deck.view.form</field>
<field name="model">mvd.tcg.add.to.deck</field>
<field name="arch" type="xml">
<form string="Add to Deck">
<group>
<field name="card_id"/>
<field name="deck_id"/>
<field name="board_id"/>
<field name="quantity"/>
<field name="role_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
<field name="note"/>
</group>
<footer>
<button name="action_add_to_deck" type="object" string="Add" class="btn-primary"/>
<button string="Cancel" class="btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="mvd_tcg_card_view_form_deck_inherit" model="ir.ui.view">
<field name="name">mvd.tcg.card.view.form.deck.inherit</field>
<field name="model">mvd.tcg.card</field>
<field name="inherit_id" ref="mvd_tcg_base.mvd_tcg_card_view_form"/>
<field name="arch" type="xml">
<xpath expr="//div[@name='button_box']" position="inside">
<button
name="action_open_decks"
type="object"
class="oe_stat_button"
icon="fa-layer-group"
>
<field name="deck_count" string="Decks" widget="statinfo"/>
</button>
</xpath>
<xpath expr="//header" position="inside">
<button
name="action_open_add_to_deck_wizard"
string="Add to Deck"
type="object"
class="btn-secondary"
/>
</xpath>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="mvd_tcg_deck_text_transfer_view_form" model="ir.ui.view">
<field name="name">mvd.tcg.deck.text.transfer.view.form</field>
<field name="model">mvd.tcg.deck.text.transfer</field>
<field name="arch" type="xml">
<form string="Deck List Transfer">
<group>
<group>
<field name="operation"/>
<field name="deck_id"/>
<field name="game_id"/>
</group>
<group>
<field
name="replace_existing"
invisible="operation != 'import'"
/>
</group>
</group>
<group col="1">
<field
name="line_text"
nolabel="1"
readonly="operation == 'export'"
placeholder="# Mainboard&#10;1 Card Name&#10;1 Another Card (TDM 101)&#10;# Sideboard&#10;1 Third Card"
/>
</group>
<footer>
<button
name="action_apply_text_transfer"
type="object"
string="Apply"
class="btn-primary"
invisible="operation != 'import'"
/>
<button
string="Close"
class="btn-primary"
special="cancel"
invisible="operation != 'export'"
/>
<button
string="Cancel"
class="btn-secondary"
special="cancel"
invisible="operation == 'export'"
/>
</footer>
</form>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,499 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="mvd_tcg_deck_view_search" model="ir.ui.view">
<field name="name">mvd.tcg.deck.view.search</field>
<field name="model">mvd.tcg.deck</field>
<field name="arch" type="xml">
<search string="Decks">
<field name="name"/>
<field name="game_id"/>
<field name="user_id"/>
<separator/>
<filter name="filter_my_decks" string="My Decks" domain="[('user_id', '=', uid)]"/>
<filter name="filter_archived" string="Archived" domain="[('active', '=', False)]"/>
<group>
<filter name="group_by_game" string="Game" context="{'group_by': 'game_id'}"/>
<filter name="group_by_owner" string="Owner" context="{'group_by': 'user_id'}"/>
</group>
</search>
</field>
</record>
<record id="mvd_tcg_deck_line_view_search" model="ir.ui.view">
<field name="name">mvd.tcg.deck.line.view.search</field>
<field name="model">mvd.tcg.deck.line</field>
<field name="arch" type="xml">
<search string="Deck Lines">
<field name="card_id"/>
<field name="deck_id"/>
<field name="board_id"/>
<field name="role_ids"/>
<separator/>
<filter name="filter_with_roles" string="With Roles" domain="[('role_ids', '!=', False)]"/>
<group>
<filter name="group_by_deck" string="Deck" context="{'group_by': 'deck_id'}"/>
<filter name="group_by_board" string="Board" context="{'group_by': 'board_id'}"/>
<filter name="group_by_primary_role" string="Primary Role" context="{'group_by': 'primary_role_id'}"/>
</group>
</search>
</field>
</record>
<record id="mvd_tcg_deck_view_kanban" model="ir.ui.view">
<field name="name">mvd.tcg.deck.view.kanban</field>
<field name="model">mvd.tcg.deck</field>
<field name="arch" type="xml">
<kanban sample="1">
<field name="name"/>
<field name="cover_image"/>
<field name="game_id"/>
<field name="total_card_count"/>
<field name="distinct_card_count"/>
<field name="board_count"/>
<field name="user_id"/>
<templates>
<t t-name="card">
<div class="oe_kanban_global_click rounded overflow-hidden border">
<div class="row g-0">
<div class="col-4 bg-light">
<t t-if="record.cover_image.raw_value">
<field name="cover_image" widget="image" class="w-100 h-100"/>
</t>
<t t-else="">
<div class="d-flex align-items-center justify-content-center h-100 text-muted p-3">
<span>No cover</span>
</div>
</t>
</div>
<div class="col-8 p-3">
<div class="fw-bold mb-1">
<field name="name"/>
</div>
<div class="text-muted mb-2">
<field name="game_id"/> · <field name="user_id"/>
</div>
<div class="d-flex flex-wrap gap-1 small">
<span class="badge rounded-pill text-bg-light">
<field name="total_card_count"/> cards
</span>
<span class="badge rounded-pill text-bg-light">
<field name="distinct_card_count"/> distinct
</span>
<span class="badge rounded-pill text-bg-light">
<field name="board_count"/> boards
</span>
</div>
</div>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<record id="mvd_tcg_deck_view_list" model="ir.ui.view">
<field name="name">mvd.tcg.deck.view.list</field>
<field name="model">mvd.tcg.deck</field>
<field name="arch" type="xml">
<list string="Decks">
<field name="name"/>
<field name="game_id"/>
<field name="user_id" optional="show"/>
<field name="board_count" optional="show"/>
<field name="total_card_count"/>
<field name="distinct_card_count" optional="show"/>
<field name="write_date" optional="hide"/>
</list>
</field>
</record>
<record id="mvd_tcg_deck_board_view_list" model="ir.ui.view">
<field name="name">mvd.tcg.deck.board.view.list</field>
<field name="model">mvd.tcg.deck.board</field>
<field name="arch" type="xml">
<list editable="bottom">
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="include_in_total"/>
<field name="total_card_count" string="Cards"/>
<field name="distinct_card_count" string="Distinct" optional="show"/>
</list>
</field>
</record>
<record id="mvd_tcg_deck_board_view_form" model="ir.ui.view">
<field name="name">mvd.tcg.deck.board.view.form</field>
<field name="model">mvd.tcg.deck.board</field>
<field name="arch" type="xml">
<form string="Deck Board">
<sheet>
<div class="oe_button_box" name="button_box">
<button
name="action_open_line_manager"
type="object"
class="oe_stat_button"
icon="fa-list"
>
<div class="o_stat_info">
<span class="o_stat_text">Manage Lines</span>
</div>
</button>
</div>
<div class="oe_title">
<label for="name" string="Board"/>
<h1>
<field name="name"/>
</h1>
</div>
<group>
<group>
<field name="deck_id"/>
<field name="sequence"/>
</group>
<group>
<field name="include_in_total"/>
<field name="total_card_count" readonly="1"/>
<field name="distinct_card_count" readonly="1"/>
</group>
</group>
<notebook>
<page string="Cards" name="cards">
<field name="line_ids" nolabel="1" mode="list,form">
<list editable="bottom" default_order="sequence, id">
<field name="sequence" widget="handle"/>
<field
name="card_image_128"
widget="mvd_deck_zoom_image"
optional="show"
options="{'preview_image': 'card_image_1920', 'zoom': true, 'zoom_delay': 0, 'size': [0, 96]}"
/>
<field name="quantity"/>
<field name="card_id"/>
<field name="role_ids" widget="many2many_tags" options="{'color_field': 'color'}" optional="show"/>
<field name="note" optional="show"/>
</list>
</field>
</page>
<page string="Notes" name="notes">
<field name="note" nolabel="1"/>
</page>
<page
string="Technical"
name="technical"
groups="mvd_tcg_base.mvd_tcg_base_group_administrator,base.group_system"
>
<group>
<field name="code"/>
</group>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record id="mvd_tcg_deck_line_view_list" model="ir.ui.view">
<field name="name">mvd.tcg.deck.line.view.list</field>
<field name="model">mvd.tcg.deck.line</field>
<field name="arch" type="xml">
<list editable="bottom" default_order="primary_role_sequence, board_sequence, sequence, id">
<field name="board_sequence" column_invisible="True"/>
<field name="sequence" widget="handle"/>
<field name="board_id"/>
<field
name="card_image_128"
widget="mvd_deck_zoom_image"
optional="show"
options="{'preview_image': 'card_image_1920', 'zoom': true, 'zoom_delay': 0, 'size': [0, 96]}"
/>
<field name="quantity"/>
<field name="card_id"/>
<field name="primary_role_id" optional="show"/>
<field name="role_ids" widget="many2many_tags" options="{'color_field': 'color'}" optional="show"/>
<field name="note" optional="show"/>
</list>
</field>
</record>
<record id="mvd_tcg_deck_line_view_form" model="ir.ui.view">
<field name="name">mvd.tcg.deck.line.view.form</field>
<field name="model">mvd.tcg.deck.line</field>
<field name="arch" type="xml">
<form string="Deck Entry">
<sheet>
<div class="oe_button_box" name="button_box">
<button
name="action_open_line"
type="object"
class="oe_stat_button"
icon="fa-external-link"
>
<div class="o_stat_info">
<span class="o_stat_text">Open</span>
</div>
</button>
</div>
<div class="d-flex gap-3 align-items-start">
<field
name="card_image_512"
widget="mvd_deck_zoom_image"
class="oe_avatar"
nolabel="1"
options="{'preview_image': 'card_image_1920', 'zoom': true, 'zoom_delay': 0, 'size': [220, 306], 'img_class': 'rounded border'}"
readonly="1"
/>
<div class="oe_title flex-grow-1">
<label for="card_id" string="Card"/>
<h1>
<field name="card_id"/>
</h1>
<div class="d-flex gap-2 flex-wrap">
<field name="board_id" widget="badge"/>
<field name="quantity" widget="badge"/>
</div>
</div>
</div>
<group>
<group>
<field name="board_id"/>
<field name="quantity"/>
</group>
<group>
<field name="primary_role_id" readonly="1"/>
<field name="role_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
</group>
</group>
<group col="1">
<field name="note" placeholder="Add an internal note for this deck entry."/>
</group>
</sheet>
</form>
</field>
</record>
<record id="mvd_tcg_deck_role_view_list" model="ir.ui.view">
<field name="name">mvd.tcg.deck.role.view.list</field>
<field name="model">mvd.tcg.deck.role</field>
<field name="arch" type="xml">
<list string="Deck Roles">
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="color" widget="color_picker" optional="show"/>
<field name="active" optional="show"/>
</list>
</field>
</record>
<record id="mvd_tcg_deck_role_view_form" model="ir.ui.view">
<field name="name">mvd.tcg.deck.role.view.form</field>
<field name="model">mvd.tcg.deck.role</field>
<field name="arch" type="xml">
<form string="Deck Role">
<sheet>
<div class="oe_title">
<label for="name" string="Role"/>
<h1>
<field name="name"/>
</h1>
</div>
<group>
<group>
<field name="sequence"/>
<field name="active"/>
</group>
<group>
<field name="color" widget="color_picker"/>
</group>
</group>
<group col="1">
<field name="note" nolabel="1" placeholder="Describe what this role should represent in deck construction."/>
</group>
<group
string="Technical"
groups="mvd_tcg_base.mvd_tcg_base_group_administrator,base.group_system"
>
<field name="technical_key"/>
</group>
</sheet>
</form>
</field>
</record>
<record id="mvd_tcg_deck_view_form" model="ir.ui.view">
<field name="name">mvd.tcg.deck.view.form</field>
<field name="model">mvd.tcg.deck</field>
<field name="arch" type="xml">
<form string="Deck">
<sheet>
<widget name="web_ribbon" title="Archived" bg_color="text-bg-danger" invisible="active"/>
<div class="oe_button_box" name="button_box">
<button
name="action_open_cards"
type="object"
class="oe_stat_button"
icon="fa-clone"
>
<field name="distinct_card_count" string="Cards" widget="statinfo"/>
</button>
<button
name="action_open_add_to_deck_wizard"
type="object"
class="oe_stat_button"
icon="fa-plus-square"
>
<div class="o_stat_info">
<span class="o_stat_text">Add Card</span>
</div>
</button>
<button
name="action_open_line_manager"
type="object"
class="oe_stat_button"
icon="fa-list"
>
<div class="o_stat_info">
<span class="o_stat_text">Manage Lines</span>
</div>
</button>
<button
name="action_open_deck_import_wizard"
type="object"
class="oe_stat_button"
icon="fa-sign-in"
>
<div class="o_stat_info">
<span class="o_stat_text">Import List</span>
</div>
</button>
<button
name="action_open_deck_export_wizard"
type="object"
class="oe_stat_button"
icon="fa-sign-out"
>
<div class="o_stat_info">
<span class="o_stat_text">Export List</span>
</div>
</button>
<button
name="action_seed_default_boards"
type="object"
class="oe_stat_button"
icon="fa-columns"
invisible="board_count != 0"
>
<div class="o_stat_info">
<span class="o_stat_text">Create Default Boards</span>
</div>
</button>
</div>
<div class="d-flex gap-3 align-items-start mb-3">
<field
name="cover_image"
widget="image"
class="oe_avatar"
nolabel="1"
options="{'size': [220, 306], 'img_class': 'rounded border'}"
/>
<div class="oe_title flex-grow-1">
<h1 class="mb-1">
<field name="name"/>
</h1>
<div class="d-flex gap-2 flex-wrap align-items-center">
<field name="game_id" widget="badge"/>
<field name="user_id" widget="badge"/>
</div>
<div class="mt-3 d-flex flex-wrap gap-2">
<span class="badge rounded-pill text-bg-light">
<strong class="me-1">Boards</strong>
<field name="board_count"/>
</span>
<span class="badge rounded-pill text-bg-light">
<strong class="me-1">Cards</strong>
<field name="total_card_count"/>
</span>
<span class="badge rounded-pill text-bg-light">
<strong class="me-1">Distinct</strong>
<field name="distinct_card_count"/>
</span>
</div>
</div>
</div>
<notebook>
<page string="Overview" name="overview">
<group>
<group>
<field name="game_id"/>
<field name="user_id" readonly="1"/>
<field name="active"/>
</group>
<group>
<field name="board_count" readonly="1"/>
<field name="total_card_count" readonly="1"/>
<field name="distinct_card_count" readonly="1"/>
</group>
</group>
<group col="1">
<field
name="description"
widget="html"
nolabel="1"
placeholder="Describe the deck plan, power level, win conditions or upgrade path."
/>
</group>
</page>
<page string="Boards" name="boards">
<field name="board_ids" nolabel="1" mode="list,form"/>
</page>
<page string="All Cards" name="cards">
<field name="line_ids" nolabel="1" mode="list"/>
</page>
<page string="Notes" name="notes">
<field name="note" placeholder="Add internal deck notes."/>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record id="mvd_tcg_deck_line_action" model="ir.actions.act_window">
<field name="name">Deck Lines</field>
<field name="res_model">mvd.tcg.deck.line</field>
<field name="view_mode">list,form</field>
<field name="search_view_id" ref="mvd_tcg_deck.mvd_tcg_deck_line_view_search"/>
</record>
<record id="mvd_tcg_deck_role_action" model="ir.actions.act_window">
<field name="name">Deck Roles</field>
<field name="res_model">mvd.tcg.deck.role</field>
<field name="view_mode">list,form</field>
</record>
<record id="mvd_tcg_deck_action" model="ir.actions.act_window">
<field name="name">Decks</field>
<field name="res_model">mvd.tcg.deck</field>
<field name="view_mode">kanban,list,form</field>
<field name="view_id" ref="mvd_tcg_deck.mvd_tcg_deck_view_kanban"/>
<field name="search_view_id" ref="mvd_tcg_deck.mvd_tcg_deck_view_search"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create a new deck
</p>
<p>
Decks stay game-neutral. Boards organize the cards inside one deck,
while game adapters can later add format- and rules-specific behavior.
</p>
</field>
</record>
<menuitem
id="mvd_tcg_deck_roles_menu"
name="Deck Roles"
parent="mvd_tcg_base.mvd_tcg_operations_menu"
sequence="20"
action="mvd_tcg_deck.mvd_tcg_deck_role_action"
groups="mvd_tcg_base.mvd_tcg_base_group_manager,base.group_system"
/>
</odoo>

2
wizards/__init__.py Normal file
View File

@@ -0,0 +1,2 @@
from . import mvd_tcg_add_to_deck
from . import mvd_tcg_deck_text_transfer

View 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",
}

View 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",
}