🎉 Initialize module repository
This commit is contained in:
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>
|
||||
Reference in New Issue
Block a user