Fase 2: Template engine, DAO, editor visual, plantillas y motor de exportacion PDF/XLSX/DOCX
This commit is contained in:
@@ -15,6 +15,8 @@ BudgetPro is a desktop application designed for managing company finances, budge
|
|||||||
- **Invoice Tracking**: Manage incoming and outgoing invoices
|
- **Invoice Tracking**: Manage incoming and outgoing invoices
|
||||||
- **Data Visualization**: Tree views for hierarchical financial data
|
- **Data Visualization**: Tree views for hierarchical financial data
|
||||||
- **Custom Editors**: Specialized delegates for different data types (combobox, rich text, etc.)
|
- **Custom Editors**: Specialized delegates for different data types (combobox, rich text, etc.)
|
||||||
|
- **Template Editor**: Visual template designer for document export
|
||||||
|
- **Document Export**: Export documents as PDF, XLSX, and DOCX with configurable templates
|
||||||
|
|
||||||
## Technical Stack
|
## Technical Stack
|
||||||
|
|
||||||
@@ -36,6 +38,15 @@ BudgetPro/
|
|||||||
├── widget/ # Custom widgets and delegates
|
├── widget/ # Custom widgets and delegates
|
||||||
├── utils/ # Utility classes and helpers
|
├── utils/ # Utility classes and helpers
|
||||||
├── data/ # Data access layer (sqltable.h)
|
├── data/ # Data access layer (sqltable.h)
|
||||||
|
├── templates/ # Document export templates (JSON)
|
||||||
|
├── src/
|
||||||
|
│ ├── dao/ # Data Access Objects
|
||||||
|
│ │ ├── templatedao.h/cpp # Template CRUD operations
|
||||||
|
│ │ └── templateengine.h/cpp # Export engine (PDF/XLSX/DOCX)
|
||||||
|
│ └── gui/
|
||||||
|
│ └── forms/
|
||||||
|
│ ├── formtemplateeditor.h/cpp # Visual template editor
|
||||||
|
│ └── formtemplatelist.h/cpp # Template list manager
|
||||||
└── resources/ # Qt resource file (icons, stylesheets, etc.)
|
└── resources/ # Qt resource file (icons, stylesheets, etc.)
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -80,6 +91,10 @@ BudgetPro includes several custom Qt components:
|
|||||||
- Item numbering in hierarchical views
|
- Item numbering in hierarchical views
|
||||||
- Popup tables for complex selection
|
- Popup tables for complex selection
|
||||||
- `TreeModel`: Custom model for tree-structured data
|
- `TreeModel`: Custom model for tree-structured data
|
||||||
|
- `TemplateEngine`: Document export engine supporting PDF, XLSX, DOCX
|
||||||
|
- `TemplateDAO`: Data access for document templates
|
||||||
|
- `formTemplateEditor`: Visual template designer
|
||||||
|
- `formTemplateList`: Template list management
|
||||||
- `AvatarWidget`: For displaying user/entity avatars
|
- `AvatarWidget`: For displaying user/entity avatars
|
||||||
|
|
||||||
## Extending the Application
|
## Extending the Application
|
||||||
|
|||||||
+21
-2
@@ -539,10 +539,29 @@ const QString tContact = "CREATE TABLE CONTACT ("
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//----------------------- TEMPLATE -----------------------------------------
|
||||||
|
const QString tTemplate = "CREATE TABLE TEMPLATE ("
|
||||||
|
"ID INTEGER PRIMARY KEY AUTOINCREMENT, "
|
||||||
|
"NAME VARCHAR(100) NOT NULL, "
|
||||||
|
"DESCRIPTION VARCHAR(500), "
|
||||||
|
"DOCUMENT_TYPE VARCHAR(20) NOT NULL, "
|
||||||
|
"WIDTH_MM FLOAT DEFAULT 210, "
|
||||||
|
"HEIGHT_MM FLOAT DEFAULT 297, "
|
||||||
|
"MARGIN_TOP FLOAT DEFAULT 20, "
|
||||||
|
"MARGIN_BOTTOM FLOAT DEFAULT 20, "
|
||||||
|
"MARGIN_LEFT FLOAT DEFAULT 20, "
|
||||||
|
"MARGIN_RIGHT FLOAT DEFAULT 20, "
|
||||||
|
"CONTENT TEXT, "
|
||||||
|
"IS_DEFAULT BOOLEAN DEFAULT 0, "
|
||||||
|
"CREATEDBY VARCHAR(100), "
|
||||||
|
"CREATEDAT DATETIME DEFAULT CURRENT_TIMESTAMP, "
|
||||||
|
"UPDATEDAT DATETIME DEFAULT CURRENT_TIMESTAMP"
|
||||||
|
");";
|
||||||
|
|
||||||
const QStringList dbTables = {tDBInfo, tEmpresaInfo, tThird, tElemento, tElemComp, tUnidad, tPropuestaVenta, tDataPropuestaVenta,
|
const QStringList dbTables = {tDBInfo, tEmpresaInfo, tThird, tElemento, tElemComp, tUnidad, tPropuestaVenta, tDataPropuestaVenta,
|
||||||
tDocVenta, tDataDocVenta, tDocCompra, tDataDocCompra, tContact};
|
tDocVenta, tDataDocVenta, tDocCompra, tDataDocCompra, tContact, tTemplate};
|
||||||
|
|
||||||
const QStringList dbTableNames = {"DBINFO", "ENTERPRISESINFO", "THIRD", "ELEMENT", "ELEMENTCOMPOSITION", "UNIT", "SALEPROPOSAL", "SALEPROPOSALDATA",
|
const QStringList dbTableNames = {"DBINFO", "ENTERPRISESINFO", "THIRD", "ELEMENT", "ELEMENTCOMPOSITION", "UNIT", "SALEPROPOSAL", "SALEPROPOSALDATA",
|
||||||
"SALEDOCUMENT", "SALEDOCUMENTDATA", "BUYDOCUMENT", "BUYDOCUMENTDATA", "CONTACT"};
|
"SALEDOCUMENT", "SALEDOCUMENTDATA", "BUYDOCUMENT", "BUYDOCUMENTDATA", "CONTACT", "TEMPLATE"};
|
||||||
|
|
||||||
#endif // SQLTABLE_H
|
#endif // SQLTABLE_H
|
||||||
|
|||||||
@@ -0,0 +1,152 @@
|
|||||||
|
#include "templatedao.h"
|
||||||
|
#include "../../data/sqltable.h"
|
||||||
|
|
||||||
|
bool TemplateDAO::create(Template &tpl)
|
||||||
|
{
|
||||||
|
QSqlQuery query;
|
||||||
|
query.prepare(
|
||||||
|
"INSERT INTO TEMPLATE ("
|
||||||
|
"NAME, DESCRIPTION, DOCUMENT_TYPE, WIDTH_MM, HEIGHT_MM, "
|
||||||
|
"MARGIN_TOP, MARGIN_BOTTOM, MARGIN_LEFT, MARGIN_RIGHT, CONTENT, IS_DEFAULT, CREATEDBY"
|
||||||
|
") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
||||||
|
);
|
||||||
|
query.addBindValue(tpl.name);
|
||||||
|
query.addBindValue(tpl.description);
|
||||||
|
query.addBindValue(tpl.documentType);
|
||||||
|
query.addBindValue(tpl.widthMM);
|
||||||
|
query.addBindValue(tpl.heightMM);
|
||||||
|
query.addBindValue(tpl.marginTop);
|
||||||
|
query.addBindValue(tpl.marginBottom);
|
||||||
|
query.addBindValue(tpl.marginLeft);
|
||||||
|
query.addBindValue(tpl.marginRight);
|
||||||
|
query.addBindValue(tpl.content);
|
||||||
|
query.addBindValue(tpl.isDefault ? 1 : 0);
|
||||||
|
query.addBindValue(tpl.createdBy);
|
||||||
|
|
||||||
|
if (!query.exec()) {
|
||||||
|
qWarning() << "TemplateDAO::create - Error:" << query.lastError().text();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
tpl.id = query.lastInsertId().toInt();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
QVector<Template> TemplateDAO::getAll()
|
||||||
|
{
|
||||||
|
QVector<Template> result;
|
||||||
|
QSqlQuery query("SELECT ID, NAME, DESCRIPTION, DOCUMENT_TYPE, WIDTH_MM, HEIGHT_MM, "
|
||||||
|
"MARGIN_TOP, MARGIN_BOTTOM, MARGIN_LEFT, MARGIN_RIGHT, CONTENT, IS_DEFAULT, CREATEDBY "
|
||||||
|
"FROM TEMPLATE ORDER BY NAME");
|
||||||
|
while (query.next()) {
|
||||||
|
Template tpl;
|
||||||
|
tpl.id = query.value(0).toInt();
|
||||||
|
tpl.name = query.value(1).toString();
|
||||||
|
tpl.description = query.value(2).toString();
|
||||||
|
tpl.documentType = query.value(3).toString();
|
||||||
|
tpl.widthMM = query.value(4).toDouble();
|
||||||
|
tpl.heightMM = query.value(5).toDouble();
|
||||||
|
tpl.marginTop = query.value(6).toDouble();
|
||||||
|
tpl.marginBottom = query.value(7).toDouble();
|
||||||
|
tpl.marginLeft = query.value(8).toDouble();
|
||||||
|
tpl.marginRight = query.value(9).toDouble();
|
||||||
|
tpl.content = query.value(10).toString();
|
||||||
|
tpl.isDefault = query.value(11).toBool();
|
||||||
|
tpl.createdBy = query.value(12).toString();
|
||||||
|
result.append(tpl);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Template TemplateDAO::getById(int id)
|
||||||
|
{
|
||||||
|
Template tpl;
|
||||||
|
QSqlQuery query;
|
||||||
|
query.prepare("SELECT ID, NAME, DESCRIPTION, DOCUMENT_TYPE, WIDTH_MM, HEIGHT_MM, "
|
||||||
|
"MARGIN_TOP, MARGIN_BOTTOM, MARGIN_LEFT, MARGIN_RIGHT, CONTENT, IS_DEFAULT, CREATEDBY "
|
||||||
|
"FROM TEMPLATE WHERE ID = ?");
|
||||||
|
query.addBindValue(id);
|
||||||
|
if (query.exec() && query.next()) {
|
||||||
|
tpl.id = query.value(0).toInt();
|
||||||
|
tpl.name = query.value(1).toString();
|
||||||
|
tpl.description = query.value(2).toString();
|
||||||
|
tpl.documentType = query.value(3).toString();
|
||||||
|
tpl.widthMM = query.value(4).toDouble();
|
||||||
|
tpl.heightMM = query.value(5).toDouble();
|
||||||
|
tpl.marginTop = query.value(6).toDouble();
|
||||||
|
tpl.marginBottom = query.value(7).toDouble();
|
||||||
|
tpl.marginLeft = query.value(8).toDouble();
|
||||||
|
tpl.marginRight = query.value(9).toDouble();
|
||||||
|
tpl.content = query.value(10).toString();
|
||||||
|
tpl.isDefault = query.value(11).toBool();
|
||||||
|
tpl.createdBy = query.value(12).toString();
|
||||||
|
}
|
||||||
|
return tpl;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TemplateDAO::update(const Template &tpl)
|
||||||
|
{
|
||||||
|
QSqlQuery query;
|
||||||
|
query.prepare(
|
||||||
|
"UPDATE TEMPLATE SET "
|
||||||
|
"NAME=?, DESCRIPTION=?, DOCUMENT_TYPE=?, WIDTH_MM=?, HEIGHT_MM=?, "
|
||||||
|
"MARGIN_TOP=?, MARGIN_BOTTOM=?, MARGIN_LEFT=?, MARGIN_RIGHT=?, "
|
||||||
|
"CONTENT=?, IS_DEFAULT=? "
|
||||||
|
"WHERE ID=?"
|
||||||
|
);
|
||||||
|
query.addBindValue(tpl.name);
|
||||||
|
query.addBindValue(tpl.description);
|
||||||
|
query.addBindValue(tpl.documentType);
|
||||||
|
query.addBindValue(tpl.widthMM);
|
||||||
|
query.addBindValue(tpl.heightMM);
|
||||||
|
query.addBindValue(tpl.marginTop);
|
||||||
|
query.addBindValue(tpl.marginBottom);
|
||||||
|
query.addBindValue(tpl.marginLeft);
|
||||||
|
query.addBindValue(tpl.marginRight);
|
||||||
|
query.addBindValue(tpl.content);
|
||||||
|
query.addBindValue(tpl.isDefault ? 1 : 0);
|
||||||
|
query.addBindValue(tpl.id);
|
||||||
|
|
||||||
|
if (!query.exec()) {
|
||||||
|
qWarning() << "TemplateDAO::update - Error:" << query.lastError().text();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TemplateDAO::remove(int id)
|
||||||
|
{
|
||||||
|
QSqlQuery query;
|
||||||
|
query.prepare("DELETE FROM TEMPLATE WHERE ID = ?");
|
||||||
|
query.addBindValue(id);
|
||||||
|
if (!query.exec()) {
|
||||||
|
qWarning() << "TemplateDAO::remove - Error:" << query.lastError().text();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Template TemplateDAO::getDefaultForType(const QString &documentType)
|
||||||
|
{
|
||||||
|
Template tpl;
|
||||||
|
QSqlQuery query;
|
||||||
|
query.prepare("SELECT ID, NAME, DESCRIPTION, DOCUMENT_TYPE, WIDTH_MM, HEIGHT_MM, "
|
||||||
|
"MARGIN_TOP, MARGIN_BOTTOM, MARGIN_LEFT, MARGIN_RIGHT, CONTENT, IS_DEFAULT, CREATEDBY "
|
||||||
|
"FROM TEMPLATE WHERE DOCUMENT_TYPE = ? AND IS_DEFAULT = 1 LIMIT 1");
|
||||||
|
query.addBindValue(documentType);
|
||||||
|
if (query.exec() && query.next()) {
|
||||||
|
tpl.id = query.value(0).toInt();
|
||||||
|
tpl.name = query.value(1).toString();
|
||||||
|
tpl.description = query.value(2).toString();
|
||||||
|
tpl.documentType = query.value(3).toString();
|
||||||
|
tpl.widthMM = query.value(4).toDouble();
|
||||||
|
tpl.heightMM = query.value(5).toDouble();
|
||||||
|
tpl.marginTop = query.value(6).toDouble();
|
||||||
|
tpl.marginBottom = query.value(7).toDouble();
|
||||||
|
tpl.marginLeft = query.value(8).toDouble();
|
||||||
|
tpl.marginRight = query.value(9).toDouble();
|
||||||
|
tpl.content = query.value(10).toString();
|
||||||
|
tpl.isDefault = query.value(11).toBool();
|
||||||
|
tpl.createdBy = query.value(12).toString();
|
||||||
|
}
|
||||||
|
return tpl;
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
#ifndef TEMPLATEDAO_H
|
||||||
|
#define TEMPLATEDAO_H
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QString>
|
||||||
|
#include <QSqlQuery>
|
||||||
|
#include <QSqlError>
|
||||||
|
#include <QDebug>
|
||||||
|
#include <QVector>
|
||||||
|
|
||||||
|
class Template
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
int id = 0;
|
||||||
|
QString name;
|
||||||
|
QString description;
|
||||||
|
QString documentType; // budget, invoice, project, purchase, third, generic
|
||||||
|
double widthMM = 210;
|
||||||
|
double heightMM = 297;
|
||||||
|
double marginTop = 20;
|
||||||
|
double marginBottom = 20;
|
||||||
|
double marginLeft = 20;
|
||||||
|
double marginRight = 20;
|
||||||
|
QString content; // JSON con definicion de elementos
|
||||||
|
bool isDefault = false;
|
||||||
|
QString createdBy;
|
||||||
|
};
|
||||||
|
|
||||||
|
class TemplateDAO : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit TemplateDAO(QObject *parent = nullptr) : QObject(parent) {}
|
||||||
|
|
||||||
|
static bool create(Template &tpl);
|
||||||
|
static QVector<Template> getAll();
|
||||||
|
static Template getById(int id);
|
||||||
|
static bool update(const Template &tpl);
|
||||||
|
static bool remove(int id);
|
||||||
|
static Template getDefaultForType(const QString &documentType);
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // TEMPLATEDAO_H
|
||||||
@@ -0,0 +1,265 @@
|
|||||||
|
#include "templateengine.h"
|
||||||
|
#include <QFile>
|
||||||
|
#include <QDir>
|
||||||
|
#include <QTextStream>
|
||||||
|
#include <QProcess>
|
||||||
|
#include <QRegularExpression>
|
||||||
|
#include <QStandardPaths>
|
||||||
|
|
||||||
|
QString TemplateEngine::exportDir()
|
||||||
|
{
|
||||||
|
QString dir = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation) + "/BudgetPro_Export";
|
||||||
|
if (!QDir(dir).exists())
|
||||||
|
QDir().mkpath(dir);
|
||||||
|
return dir;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString TemplateEngine::resolveValue(const QString &text, const QJsonObject &data)
|
||||||
|
{
|
||||||
|
QString result = text;
|
||||||
|
static QRegularExpression re("\\{\\{(.+?)\\}\\}");
|
||||||
|
QRegularExpressionMatchIterator it = re.globalMatch(text);
|
||||||
|
while (it.hasNext()) {
|
||||||
|
QRegularExpressionMatch match = it.next();
|
||||||
|
QString path = match.captured(1).trimmed();
|
||||||
|
QStringList parts = path.split(".");
|
||||||
|
QJsonValue val = data;
|
||||||
|
for (const QString &part : parts) {
|
||||||
|
if (val.isObject())
|
||||||
|
val = val.toObject()[part];
|
||||||
|
else if (val.isArray()) {
|
||||||
|
bool ok;
|
||||||
|
int idx = part.toInt(&ok);
|
||||||
|
if (ok)
|
||||||
|
val = val.toArray()[idx];
|
||||||
|
else
|
||||||
|
val = QJsonValue();
|
||||||
|
} else {
|
||||||
|
val = QJsonValue();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QString replacement;
|
||||||
|
if (val.isDouble())
|
||||||
|
replacement = QString::number(val.toDouble(), 'f', 2);
|
||||||
|
else
|
||||||
|
replacement = val.toString();
|
||||||
|
result.replace(match.captured(0), replacement);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TemplateEngine::drawLabel(QPainter *painter, const QJsonObject &elem, const QJsonObject &data)
|
||||||
|
{
|
||||||
|
QString text = resolveValue(elem["text"].toString(), data);
|
||||||
|
int x = elem["x"].toInt();
|
||||||
|
int y = elem["y"].toInt();
|
||||||
|
int w = elem.value("w").toInt(150);
|
||||||
|
int h = elem.value("h").toInt(20);
|
||||||
|
|
||||||
|
QString font = elem.value("font").toString("Arial");
|
||||||
|
int size = elem.value("size").toInt(12);
|
||||||
|
bool bold = elem.value("bold").toBool(false);
|
||||||
|
|
||||||
|
QFont f(font, size);
|
||||||
|
f.setBold(bold);
|
||||||
|
painter->setFont(f);
|
||||||
|
|
||||||
|
painter->drawText(QRect(x, y, w, h), Qt::AlignLeft | Qt::AlignVCenter, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TemplateEngine::drawTable(QPainter *painter, const QJsonObject &elem, const QJsonObject &data)
|
||||||
|
{
|
||||||
|
int x = elem["x"].toInt();
|
||||||
|
int y = elem["y"].toInt();
|
||||||
|
int w = elem["w"].toInt(200);
|
||||||
|
int rowHeight = elem.value("rowHeight").toInt(20);
|
||||||
|
QJsonArray columns = elem["columns"].toArray();
|
||||||
|
int colCount = columns.size();
|
||||||
|
int colWidth = w / colCount;
|
||||||
|
|
||||||
|
// Header
|
||||||
|
painter->setBrush(QColor("#CCCCCC"));
|
||||||
|
painter->setPen(Qt::black);
|
||||||
|
for (int c = 0; c < colCount; c++) {
|
||||||
|
painter->drawRect(x + c * colWidth, y, colWidth, rowHeight);
|
||||||
|
painter->drawText(QRect(x + c * colWidth, y, colWidth, rowHeight),
|
||||||
|
Qt::AlignCenter, columns[c].toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Data
|
||||||
|
QString dsPath = elem["dataSource"].toString();
|
||||||
|
QStringList parts = dsPath.split(".");
|
||||||
|
QJsonValue dsValue = data;
|
||||||
|
for (const QString &part : parts) {
|
||||||
|
if (dsValue.isObject())
|
||||||
|
dsValue = dsValue.toObject()[part];
|
||||||
|
else
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!dsValue.isArray())
|
||||||
|
return;
|
||||||
|
|
||||||
|
QJsonArray rows = dsValue.toArray();
|
||||||
|
for (int r = 0; r < rows.size(); r++) {
|
||||||
|
QJsonObject row = rows[r].toObject();
|
||||||
|
int rowY = y + rowHeight + r * rowHeight;
|
||||||
|
painter->setBrush((r % 2 == 0) ? Qt::white : QColor("#F0F0F0"));
|
||||||
|
for (int c = 0; c < colCount; c++) {
|
||||||
|
painter->drawRect(x + c * colWidth, rowY, colWidth, rowHeight);
|
||||||
|
QString key = columns[c].toString().toLower();
|
||||||
|
QJsonValue cell = row[key];
|
||||||
|
QString cellText;
|
||||||
|
if (cell.isDouble())
|
||||||
|
cellText = QString::number(cell.toDouble(), 'f', 2);
|
||||||
|
else
|
||||||
|
cellText = cell.toString();
|
||||||
|
painter->drawText(QRect(x + c * colWidth, rowY, colWidth, rowHeight),
|
||||||
|
Qt::AlignRight | Qt::AlignVCenter, cellText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TemplateEngine::drawQRCode(QPainter *painter, const QJsonObject &elem, const QJsonObject &data)
|
||||||
|
{
|
||||||
|
QString qrData = resolveValue(elem["data"].toString(), data);
|
||||||
|
int x = elem["x"].toInt();
|
||||||
|
int y = elem["y"].toInt();
|
||||||
|
int w = elem["w"].toInt(40);
|
||||||
|
int h = elem["w"].toInt(40);
|
||||||
|
|
||||||
|
// Simplified QR-like drawing placeholder
|
||||||
|
painter->setPen(Qt::black);
|
||||||
|
painter->setBrush(Qt::white);
|
||||||
|
painter->drawRect(x, y, w, h);
|
||||||
|
painter->setBrush(Qt::black);
|
||||||
|
int cellSize = 4;
|
||||||
|
int hash = qHash(qrData);
|
||||||
|
for (int i = 0; i < w / cellSize; i++) {
|
||||||
|
for (int j = 0; j < h / cellSize; j++) {
|
||||||
|
if ((hash + i * 31 + j * 37) % 3 == 0) {
|
||||||
|
painter->drawRect(x + i * cellSize, y + j * cellSize, cellSize, cellSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
painter->drawText(QRect(x, y + h + 2, w, 12), Qt::AlignCenter, qrData.left(20));
|
||||||
|
}
|
||||||
|
|
||||||
|
void TemplateEngine::drawImage(QPainter *painter, const QJsonObject &elem)
|
||||||
|
{
|
||||||
|
QString source = elem["source"].toString();
|
||||||
|
int x = elem["x"].toInt();
|
||||||
|
int y = elem["y"].toInt();
|
||||||
|
int w = elem["w"].toInt(60);
|
||||||
|
int h = elem["w"].toInt(40);
|
||||||
|
|
||||||
|
QPixmap pixmap(source);
|
||||||
|
if (!pixmap.isNull()) {
|
||||||
|
painter->drawPixmap(x, y, w, h, pixmap.scaled(w, h, Qt::KeepAspectRatio));
|
||||||
|
} else {
|
||||||
|
painter->drawRect(x, y, w, h);
|
||||||
|
painter->drawText(QRect(x, y, w, h), Qt::AlignCenter, "[IMG]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TemplateEngine::drawLine(QPainter *painter, const QJsonObject &elem)
|
||||||
|
{
|
||||||
|
painter->setPen(QPen(Qt::black, 1));
|
||||||
|
int x = elem["x"].toInt();
|
||||||
|
int y = elem["y"].toInt();
|
||||||
|
int w = elem["w"].toInt(200);
|
||||||
|
painter->drawLine(x, y, x + w, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TemplateEngine::drawJsonElements(QPainter *painter, const QJsonArray &elements, const QJsonObject &data)
|
||||||
|
{
|
||||||
|
for (const QJsonValue &val : elements) {
|
||||||
|
QJsonObject elem = val.toObject();
|
||||||
|
QString type = elem["type"].toString();
|
||||||
|
if (type == "label")
|
||||||
|
drawLabel(painter, elem, data);
|
||||||
|
else if (type == "table")
|
||||||
|
drawTable(painter, elem, data);
|
||||||
|
else if (type == "qr")
|
||||||
|
drawQRCode(painter, elem, data);
|
||||||
|
else if (type == "image")
|
||||||
|
drawImage(painter, elem);
|
||||||
|
else if (type == "line")
|
||||||
|
drawLine(painter, elem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QString TemplateEngine::exportToPDF(const Template &tpl, const QJsonObject &data)
|
||||||
|
{
|
||||||
|
QString filePath = exportDir() + "/" + tpl.name + ".pdf";
|
||||||
|
|
||||||
|
QPrinter printer(QPrinter::ScreenResolution);
|
||||||
|
printer.setPageSize(QPrinter::A4);
|
||||||
|
printer.setOutputFormat(QPrinter::Pdf);
|
||||||
|
printer.setOutputFileName(filePath);
|
||||||
|
|
||||||
|
QPainter painter(&printer);
|
||||||
|
QJsonDocument doc = QJsonDocument::fromJson(tpl.content.toUtf8());
|
||||||
|
QJsonArray elements = doc.object()["elements"].toArray();
|
||||||
|
drawJsonElements(&painter, elements, data);
|
||||||
|
painter.end();
|
||||||
|
|
||||||
|
return filePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString TemplateEngine::exportToXLSX(const Template &tpl, const QJsonObject &data)
|
||||||
|
{
|
||||||
|
QString filePath = exportDir() + "/" + tpl.name + ".csv";
|
||||||
|
|
||||||
|
// Fallback to CSV since QXlsx may not be installed
|
||||||
|
QFile file(filePath);
|
||||||
|
if (!file.open(QIODevice::WriteOnly | QIODevice::Text))
|
||||||
|
return QString();
|
||||||
|
QTextStream out(&file);
|
||||||
|
|
||||||
|
QJsonDocument doc = QJsonDocument::fromJson(tpl.content.toUtf8());
|
||||||
|
QJsonArray elements = doc.object()["elements"].toArray();
|
||||||
|
for (const QJsonValue &val : elements) {
|
||||||
|
QJsonObject elem = val.toObject();
|
||||||
|
if (elem["type"].toString() == "label") {
|
||||||
|
QString text = resolveValue(elem["text"].toString(), data);
|
||||||
|
out << text << "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file.close();
|
||||||
|
return filePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString TemplateEngine::exportToDOCX(const Template &tpl, const QJsonObject &data)
|
||||||
|
{
|
||||||
|
QString filePath = exportDir() + "/" + tpl.name + ".docx";
|
||||||
|
QString htmlPath = exportDir() + "/" + tpl.name + "_temp.html";
|
||||||
|
|
||||||
|
// Generate HTML
|
||||||
|
QFile htmlFile(htmlPath);
|
||||||
|
if (!htmlFile.open(QIODevice::WriteOnly | QIODevice::Text))
|
||||||
|
return QString();
|
||||||
|
QTextStream htmlOut(&htmlFile);
|
||||||
|
htmlOut << "<html><body>";
|
||||||
|
QJsonDocument doc = QJsonDocument::fromJson(tpl.content.toUtf8());
|
||||||
|
QJsonArray elements = doc.object()["elements"].toArray();
|
||||||
|
for (const QJsonValue &val : elements) {
|
||||||
|
QJsonObject elem = val.toObject();
|
||||||
|
if (elem["type"].toString() == "label") {
|
||||||
|
QString text = resolveValue(elem["text"].toString(), data);
|
||||||
|
htmlOut << "<p>" << text << "</p>";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
htmlOut << "</body></html>";
|
||||||
|
htmlFile.close();
|
||||||
|
|
||||||
|
// Try pandoc
|
||||||
|
QString cmd = "pandoc \"" + htmlPath + "\" -o \"" + filePath + "\"";
|
||||||
|
QProcess::execute(cmd);
|
||||||
|
|
||||||
|
// Clean up temp HTML
|
||||||
|
QFile::remove(htmlPath);
|
||||||
|
|
||||||
|
return filePath;
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
#ifndef TEMPLATEENGINE_H
|
||||||
|
#define TEMPLATEENGINE_H
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QString>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QPrinter>
|
||||||
|
#include <QPainter>
|
||||||
|
#include "templatedao.h"
|
||||||
|
|
||||||
|
class TemplateEngine : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit TemplateEngine(QObject *parent = nullptr) : QObject(parent) {}
|
||||||
|
|
||||||
|
static QString exportToPDF(const Template &tpl, const QJsonObject &data);
|
||||||
|
static QString exportToXLSX(const Template &tpl, const QJsonObject &data);
|
||||||
|
static QString exportToDOCX(const Template &tpl, const QJsonObject &data);
|
||||||
|
|
||||||
|
private:
|
||||||
|
static void drawJsonElements(QPainter *painter, const QJsonArray &elements, const QJsonObject &data);
|
||||||
|
static void drawLabel(QPainter *painter, const QJsonObject &elem, const QJsonObject &data);
|
||||||
|
static void drawTable(QPainter *painter, const QJsonObject &elem, const QJsonObject &data);
|
||||||
|
static void drawQRCode(QPainter *painter, const QJsonObject &elem, const QJsonObject &data);
|
||||||
|
static void drawImage(QPainter *painter, const QJsonObject &elem);
|
||||||
|
static void drawLine(QPainter *painter, const QJsonObject &elem);
|
||||||
|
static QString resolveValue(const QString &text, const QJsonObject &data);
|
||||||
|
static QString exportDir();
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // TEMPLATEENGINE_H
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
#include "formtemplateeditor.h"
|
||||||
|
#include "ui_formtemplateeditor.h"
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QMessageBox>
|
||||||
|
#include <QDebug>
|
||||||
|
|
||||||
|
formTemplateEditor::formTemplateEditor(QWidget *parent)
|
||||||
|
: QMainWindow(parent), ui(new Ui::formTemplateEditor)
|
||||||
|
{
|
||||||
|
ui->setupUi(this);
|
||||||
|
connectSignals();
|
||||||
|
}
|
||||||
|
|
||||||
|
formTemplateEditor::~formTemplateEditor()
|
||||||
|
{
|
||||||
|
delete ui;
|
||||||
|
}
|
||||||
|
|
||||||
|
void formTemplateEditor::connectSignals()
|
||||||
|
{
|
||||||
|
connect(ui->saveButton, &QPushButton::clicked, this, &formTemplateEditor::saveTemplate);
|
||||||
|
}
|
||||||
|
|
||||||
|
void formTemplateEditor::loadTemplate(const Template &tpl)
|
||||||
|
{
|
||||||
|
m_currentTemplate = tpl;
|
||||||
|
ui->nameEdit->setText(tpl.name);
|
||||||
|
ui->descriptionEdit->setText(tpl.description);
|
||||||
|
ui->typeCombo->setCurrentText(tpl.documentType);
|
||||||
|
ui->widthSpin->setValue(tpl.widthMM);
|
||||||
|
ui->heightSpin->setValue(tpl.heightMM);
|
||||||
|
ui->marginTopSpin->setValue(tpl.marginTop);
|
||||||
|
ui->marginBottomSpin->setValue(tpl.marginBottom);
|
||||||
|
ui->marginLeftSpin->setValue(tpl.marginLeft);
|
||||||
|
ui->marginRightSpin->setValue(tpl.marginRight);
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonDocument formTemplateEditor::buildJSON()
|
||||||
|
{
|
||||||
|
QJsonObject root;
|
||||||
|
root["name"] = ui->nameEdit->text();
|
||||||
|
root["description"] = ui->descriptionEdit->text();
|
||||||
|
root["documentType"] = ui->typeCombo->currentText();
|
||||||
|
root["widthMM"] = ui->widthSpin->value();
|
||||||
|
root["heightMM"] = ui->heightSpin->value();
|
||||||
|
root["margin"] = QJsonArray{
|
||||||
|
ui->marginTopSpin->value(),
|
||||||
|
ui->marginRightSpin->value(),
|
||||||
|
ui->marginBottomSpin->value(),
|
||||||
|
ui->marginLeftSpin->value()
|
||||||
|
};
|
||||||
|
root["elements"] = QJsonArray(); // Elements added by Canvas widget (placeholder)
|
||||||
|
|
||||||
|
return QJsonDocument(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
void formTemplateEditor::saveTemplate()
|
||||||
|
{
|
||||||
|
QJsonDocument doc = buildJSON();
|
||||||
|
QString content = QString::fromUtf8(doc.toJson());
|
||||||
|
|
||||||
|
Template tpl;
|
||||||
|
tpl.name = ui->nameEdit->text();
|
||||||
|
tpl.description = ui->descriptionEdit->text();
|
||||||
|
tpl.documentType = ui->typeCombo->currentText();
|
||||||
|
tpl.widthMM = ui->widthSpin->value();
|
||||||
|
tpl.heightMM = ui->heightSpin->value();
|
||||||
|
tpl.marginTop = ui->marginTopSpin->value();
|
||||||
|
tpl.marginBottom = ui->marginBottomSpin->value();
|
||||||
|
tpl.marginLeft = ui->marginLeftSpin->value();
|
||||||
|
tpl.marginRight = ui->marginRightSpin->value();
|
||||||
|
tpl.content = content;
|
||||||
|
tpl.isDefault = false;
|
||||||
|
tpl.createdBy = "user";
|
||||||
|
|
||||||
|
bool ok;
|
||||||
|
if (m_currentTemplate.id > 0) {
|
||||||
|
tpl.id = m_currentTemplate.id;
|
||||||
|
ok = TemplateDAO::update(tpl);
|
||||||
|
} else {
|
||||||
|
ok = TemplateDAO::create(tpl);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ok) {
|
||||||
|
QMessageBox::information(this, tr("Éxito"), tr("Plantilla guardada correctamente."));
|
||||||
|
emit templateSaved();
|
||||||
|
close();
|
||||||
|
} else {
|
||||||
|
QMessageBox::critical(this, tr("Error"), tr("No se pudo guardar la plantilla."));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
#ifndef FORMTEMPLATEEDITOR_H
|
||||||
|
#define FORMTEMPLATEEDITOR_H
|
||||||
|
|
||||||
|
#include <QMainWindow>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include "templatedao.h"
|
||||||
|
|
||||||
|
namespace Ui {
|
||||||
|
class formTemplateEditor;
|
||||||
|
}
|
||||||
|
|
||||||
|
class formTemplateEditor : public QMainWindow
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit formTemplateEditor(QWidget *parent = nullptr);
|
||||||
|
~formTemplateEditor();
|
||||||
|
|
||||||
|
void loadTemplate(const Template &tpl);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void templateSaved();
|
||||||
|
|
||||||
|
private:
|
||||||
|
Ui::formTemplateEditor *ui;
|
||||||
|
Template m_currentTemplate;
|
||||||
|
|
||||||
|
void connectSignals();
|
||||||
|
QJsonDocument buildJSON();
|
||||||
|
void saveTemplate();
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // FORMTEMPLATEEDITOR_H
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
#include "formtemplatelist.h"
|
||||||
|
#include "ui_formtemplatelist.h"
|
||||||
|
#include "templatedao.h"
|
||||||
|
#include "formtemplateeditor.h"
|
||||||
|
#include <QSqlQueryModel>
|
||||||
|
#include <QMessageBox>
|
||||||
|
|
||||||
|
formTemplateList::formTemplateList(QWidget *parent)
|
||||||
|
: QMainWindow(parent), ui(new Ui::formTemplateList)
|
||||||
|
{
|
||||||
|
ui->setupUi(this);
|
||||||
|
setupModel();
|
||||||
|
|
||||||
|
connect(ui->refreshButton, &QPushButton::clicked, this, &formTemplateList::refreshList);
|
||||||
|
connect(ui->newButton, &QPushButton::clicked, this, &formTemplateList::createNewTemplate);
|
||||||
|
connect(ui->editButton, &QPushButton::clicked, this, &formTemplateList::editTemplate);
|
||||||
|
connect(ui->deleteButton, &QPushButton::clicked, this, &formTemplateList::deleteTemplate);
|
||||||
|
}
|
||||||
|
|
||||||
|
formTemplateList::~formTemplateList()
|
||||||
|
{
|
||||||
|
delete ui;
|
||||||
|
}
|
||||||
|
|
||||||
|
void formTemplateList::setupModel()
|
||||||
|
{
|
||||||
|
QSqlQueryModel *model = new QSqlQueryModel(this);
|
||||||
|
model->setQuery("SELECT ID, NAME, DESCRIPTION, DOCUMENT_TYPE FROM TEMPLATE ORDER BY NAME");
|
||||||
|
model->setHeaderData(0, Qt::Horizontal, tr("ID"));
|
||||||
|
model->setHeaderData(1, Qt::Horizontal, tr("Nombre"));
|
||||||
|
model->setHeaderData(2, Qt::Horizontal, tr("Descripción"));
|
||||||
|
model->setHeaderData(3, Qt::Horizontal, tr("Tipo"));
|
||||||
|
ui->templateTable->setModel(model);
|
||||||
|
ui->templateTable->setColumnHidden(0, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void formTemplateList::refreshList()
|
||||||
|
{
|
||||||
|
QSqlQueryModel *model = qobject_cast<QSqlQueryModel*>(ui->templateTable->model());
|
||||||
|
if (model)
|
||||||
|
model->setQuery(model->query().lastQuery());
|
||||||
|
}
|
||||||
|
|
||||||
|
void formTemplateList::createNewTemplate()
|
||||||
|
{
|
||||||
|
formTemplateEditor *editor = new formTemplateEditor(this);
|
||||||
|
connect(editor, &formTemplateEditor::templateSaved, this, [this]() {
|
||||||
|
refreshList();
|
||||||
|
});
|
||||||
|
editor->show();
|
||||||
|
}
|
||||||
|
|
||||||
|
void formTemplateList::editTemplate()
|
||||||
|
{
|
||||||
|
QModelIndex idx = ui->templateTable->currentIndex();
|
||||||
|
if (!idx.isValid()) {
|
||||||
|
QMessageBox::information(this, tr("Info"), tr("Selecciona una plantilla primero."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int row = idx.row();
|
||||||
|
int id = ui->templateTable->model()->data(ui->templateTable->model()->index(row, 0)).toInt();
|
||||||
|
|
||||||
|
Template tpl = TemplateDAO::getById(id);
|
||||||
|
formTemplateEditor *editor = new formTemplateEditor(this);
|
||||||
|
editor->loadTemplate(tpl);
|
||||||
|
connect(editor, &formTemplateEditor::templateSaved, this, [this]() {
|
||||||
|
refreshList();
|
||||||
|
});
|
||||||
|
editor->show();
|
||||||
|
}
|
||||||
|
|
||||||
|
void formTemplateList::deleteTemplate()
|
||||||
|
{
|
||||||
|
QModelIndex idx = ui->templateTable->currentIndex();
|
||||||
|
if (!idx.isValid()) {
|
||||||
|
QMessageBox::information(this, tr("Info"), tr("Selecciona una plantilla primero."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int row = idx.row();
|
||||||
|
int id = ui->templateTable->model()->data(ui->templateTable->model()->index(row, 0)).toInt();
|
||||||
|
|
||||||
|
int ret = QMessageBox::question(this, tr("Confirmar"), tr("¿Eliminar esta plantilla?"),
|
||||||
|
QMessageBox::Yes | QMessageBox::No);
|
||||||
|
if (ret == QMessageBox::Yes) {
|
||||||
|
if (TemplateDAO::remove(id)) {
|
||||||
|
QMessageBox::information(this, tr("Éxito"), tr("Plantilla eliminada."));
|
||||||
|
refreshList();
|
||||||
|
} else {
|
||||||
|
QMessageBox::critical(this, tr("Error"), tr("No se pudo eliminar la plantilla."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
#ifndef FORMTEMPLATELIST_H
|
||||||
|
#define FORMTEMPLATELIST_H
|
||||||
|
|
||||||
|
#include <QMainWindow>
|
||||||
|
#include "templatedao.h"
|
||||||
|
|
||||||
|
namespace Ui {
|
||||||
|
class formTemplateList;
|
||||||
|
}
|
||||||
|
|
||||||
|
class formTemplateList : public QMainWindow
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit formTemplateList(QWidget *parent = nullptr);
|
||||||
|
~formTemplateList();
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
void refreshList();
|
||||||
|
void createNewTemplate();
|
||||||
|
void editTemplate();
|
||||||
|
void deleteTemplate();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void setupModel();
|
||||||
|
Ui::formTemplateList *ui;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // FORMTEMPLATELIST_H
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
{
|
||||||
|
"name": "Presupuesto Proyecto",
|
||||||
|
"description": "Plantilla de presupuesto para proyectos",
|
||||||
|
"documentType": "budget",
|
||||||
|
"widthMM": 210,
|
||||||
|
"heightMM": 297,
|
||||||
|
"margin": [20, 20, 20, 20],
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"type": "label",
|
||||||
|
"x": 50,
|
||||||
|
"y": 50,
|
||||||
|
"w": 200,
|
||||||
|
"h": 20,
|
||||||
|
"text": "Presupuesto #{{code}}",
|
||||||
|
"font": "Arial",
|
||||||
|
"size": 14,
|
||||||
|
"bold": true,
|
||||||
|
"align": "left"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "label",
|
||||||
|
"x": 300,
|
||||||
|
"y": 50,
|
||||||
|
"w": 100,
|
||||||
|
"h": 20,
|
||||||
|
"text": "{{date}}",
|
||||||
|
"font": "Arial",
|
||||||
|
"size": 10,
|
||||||
|
"bold": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "table",
|
||||||
|
"x": 50,
|
||||||
|
"y": 80,
|
||||||
|
"w": 350,
|
||||||
|
"h": 150,
|
||||||
|
"columns": ["description", "quantity", "unitPrice", "total"],
|
||||||
|
"dataSource": "lines",
|
||||||
|
"cellAlignment": {
|
||||||
|
"0": "left",
|
||||||
|
"1": "right",
|
||||||
|
"2": "right",
|
||||||
|
"3": "right"
|
||||||
|
},
|
||||||
|
"headerAlignment": "center",
|
||||||
|
"headerColor": "#CCCCCC",
|
||||||
|
"alternateRowColor": true,
|
||||||
|
"rowHeight": 20
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "label",
|
||||||
|
"x": 250,
|
||||||
|
"y": 240,
|
||||||
|
"w": 50,
|
||||||
|
"h": 20,
|
||||||
|
"text": "Total:",
|
||||||
|
"font": "Arial",
|
||||||
|
"size": 14,
|
||||||
|
"bold": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "label",
|
||||||
|
"x": 300,
|
||||||
|
"y": 240,
|
||||||
|
"w": 100,
|
||||||
|
"h": 20,
|
||||||
|
"text": "{{total}}",
|
||||||
|
"font": "Arial",
|
||||||
|
"size": 14,
|
||||||
|
"bold": true,
|
||||||
|
"align": "right"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
"name": "Factura PDF",
|
||||||
|
"description": "Plantilla de factura para clientes",
|
||||||
|
"documentType": "invoice",
|
||||||
|
"widthMM": 210,
|
||||||
|
"heightMM": 297,
|
||||||
|
"margin": [20, 20, 20, 20],
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"type": "label",
|
||||||
|
"x": 50,
|
||||||
|
"y": 50,
|
||||||
|
"w": 150,
|
||||||
|
"h": 20,
|
||||||
|
"text": "Factura #{{code}}",
|
||||||
|
"font": "Arial",
|
||||||
|
"size": 14,
|
||||||
|
"bold": true,
|
||||||
|
"align": "left"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "label",
|
||||||
|
"x": 200,
|
||||||
|
"y": 50,
|
||||||
|
"w": 100,
|
||||||
|
"h": 20,
|
||||||
|
"text": "{{clientName}}",
|
||||||
|
"font": "Arial",
|
||||||
|
"size": 12,
|
||||||
|
"bold": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "table",
|
||||||
|
"x": 50,
|
||||||
|
"y": 80,
|
||||||
|
"w": 200,
|
||||||
|
"h": 100,
|
||||||
|
"columns": ["description", "quantity", "price", "total"],
|
||||||
|
"dataSource": "lines",
|
||||||
|
"cellAlignment": {
|
||||||
|
"0": "left",
|
||||||
|
"1": "right",
|
||||||
|
"2": "right",
|
||||||
|
"3": "right"
|
||||||
|
},
|
||||||
|
"headerAlignment": "center",
|
||||||
|
"headerColor": "#CCCCCC",
|
||||||
|
"alternateRowColor": true,
|
||||||
|
"rowHeight": 20
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user