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