Files
crm.clientright.ru/TEMPLATE_SYSTEM_ARCHITECTURE.md
Fedor 01c4fe80b5 chore: snapshot current working tree changes
Save all currently accumulated repository changes as a backup snapshot for Gitea so no local work is lost.
2026-03-26 14:19:01 +03:00

17 KiB
Raw Blame History

🏗️ АРХИТЕКТУРА СИСТЕМЫ ГЕНЕРАЦИИ ДОКУМЕНТОВ ИЗ ШАБЛОНОВ

🎯 ЦЕЛЬ

Создать надежную систему генерации документов из шаблонов Nextcloud с автоматическим заполнением переменных из модулей CRM.


📋 КОНЦЕПЦИЯ

Основная идея:

  1. Шаблоны хранятся в Nextcloud /Templates/
  2. Маппинг переменных - конфигурация, которая связывает переменные шаблона с полями модулей CRM
  3. Генератор - получает данные из модуля, подставляет в шаблон, сохраняет документ
  4. UI - кнопка в детальном виде записи для выбора шаблона и генерации

🏛️ АРХИТЕКТУРА (3 СЛОЯ)

СЛОЙ 1: КОНФИГУРАЦИЯ (Маппинг переменных)

Файл: crm_extensions/file_storage/config/template_mappings.php

Структура:

return [
    'Project' => [
        'pretenziya.docx' => [
            // Прямые поля модуля
            'CLIENT_NAME' => 'projectname',  // поле projectname из vtiger_project
            'DATE' => 'createdtime',
            'AMOUNT' => 'cf_1885',
            
            // Связанные модули (через отношения)
            'CONTACT_NAME' => [
                'module' => 'Contacts',
                'relation' => 'Contacts',  // название связи
                'field' => 'lastname'
            ],
            
            // Вычисляемые поля
            'FULL_DATE' => [
                'type' => 'function',
                'function' => 'formatDate',
                'params' => ['createdtime', 'd.m.Y']
            ],
            
            // Константы
            'COMPANY_NAME' => [
                'type' => 'constant',
                'value' => 'ООО "Клиент Право"'
            ],
            
            // Кастомные функции
            'CLAIM_TEXT' => [
                'type' => 'custom',
                'handler' => 'getClaimTextFromProject'
            ]
        ],
        
        'iskovoe_zayavlenie.docx' => [
            // другой маппинг для другого шаблона
        ]
    ],
    
    'HelpDesk' => [
        'pretenziya.docx' => [
            'CLIENT_NAME' => 'ticket_title',
            'DESCRIPTION' => 'description',
            // ...
        ]
    ]
];

Преимущества:

  • Централизованная конфигурация
  • Легко добавлять новые шаблоны
  • Поддержка связанных модулей
  • Вычисляемые поля
  • Кастомные функции

СЛОЙ 2: ДВИЖОК ГЕНЕРАЦИИ

Файл: crm_extensions/file_storage/api/generate_from_template.php

Алгоритм:

1. Получить параметры:
   - module (Project, HelpDesk, etc.)
   - recordId (ID записи)
   - templateName (pretenziya.docx)

2. Загрузить маппинг для module + templateName

3. Получить данные записи:
   - vtws_retrieve() или Vtiger_Record_Model
   - Получить все поля модуля
   - Получить связанные записи (если нужно)

4. Обработать маппинг:
   - Прямые поля → взять из данных записи
   - Связанные модули → получить через отношения
   - Вычисляемые → вызвать функцию
   - Константы → подставить значение
   - Кастомные → вызвать handler

5. Скачать шаблон из Nextcloud (WebDAV)

6. Заполнить переменные (PHPWord для DOCX)

7. Сохранить в S3 в папку проекта/записи

8. Вернуть результат (JSON или редирект на OnlyOffice)

Обработка ошибок:

  • Если поле не найдено → подставить пустую строку или значение по умолчанию
  • Если связанная запись не найдена → пропустить или подставить "Не указано"
  • Если шаблон не найден → вернуть ошибку
  • Логирование всех операций

СЛОЙ 3: UI ИНТЕГРАЦИЯ

Вариант A: Кнопка в детальном виде

Файл: modules/{Module}/views/Detail.php или через JavaScript

JavaScript:

// Добавить кнопку "Создать из шаблона"
function showTemplateDialog(module, recordId) {
    // 1. Получить список шаблонов для модуля
    fetch(`/crm_extensions/file_storage/api/get_templates_for_module.php?module=${module}`)
        .then(r => r.json())
        .then(templates => {
            // 2. Показать диалог выбора шаблона
            // 3. При выборе → вызвать generate_from_template.php
        });
}

Вариант B: Отдельная страница/модальное окно

Файл: modules/{Module}/actions/TemplateGenerator.php

Преимущества:

  • Можно показать предпросмотр переменных
  • Можно редактировать значения перед генерацией
  • Можно выбрать несколько шаблонов

🔧 ДЕТАЛИ РЕАЛИЗАЦИИ

1. Получение данных модуля

// Вариант 1: Через vtws_retrieve (Webservice API)
$recordData = vtws_retrieve($recordId, $current_user);

// Вариант 2: Через Record Model (рекомендуется)
$recordModel = Vtiger_Record_Model::getInstanceById($recordId, $module);
$recordData = $recordModel->getData();

// Вариант 3: Прямой SQL (если нужны все поля включая кастомные)
$adb = PearDatabase::getInstance();
$result = $adb->pquery("SELECT * FROM vtiger_project WHERE projectid = ?", [$recordId]);
$recordData = $adb->fetchByAssoc($result);

Рекомендация: Использовать Record Model, т.к.:

  • Учитывает права доступа
  • Форматирует значения (даты, валюты)
  • Работает с кастомными полями
  • Поддерживает связанные модули

2. Обработка связанных модулей

// Получить связанную запись
function getRelatedRecord($recordModel, $relationName, $targetModule, $fieldName) {
    $relationModel = $recordModel->getRelation($relationName);
    if (!$relationModel) {
        return null;
    }
    
    $relatedRecords = $relationModel->getRelatedRecords();
    if (empty($relatedRecords)) {
        return null;
    }
    
    // Берем первую связанную запись
    $relatedRecord = $relatedRecords[0];
    $relatedModel = Vtiger_Record_Model::getInstanceById($relatedRecord['id'], $targetModule);
    
    return $relatedModel->get($fieldName);
}

// Использование:
$contactName = getRelatedRecord($projectModel, 'Contacts', 'Contacts', 'lastname');

3. Вычисляемые поля

// Функции-обработчики
class TemplateVariableProcessors {
    
    public static function formatDate($value, $format = 'd.m.Y') {
        if (empty($value)) return '';
        $timestamp = strtotime($value);
        return date($format, $timestamp);
    }
    
    public static function formatCurrency($value, $currency = 'RUB') {
        if (empty($value)) return '0,00 ₽';
        return number_format($value, 2, ',', ' ') . ' ₽';
    }
    
    public static function concatFields($recordData, $fields) {
        $parts = [];
        foreach ($fields as $field) {
            if (!empty($recordData[$field])) {
                $parts[] = $recordData[$field];
            }
        }
        return implode(' ', $parts);
    }
    
    // Кастомная функция для получения текста претензии из проекта
    public static function getClaimTextFromProject($recordId) {
        // Логика получения текста претензии
        // Например, из связанных тикетов HelpDesk
        return 'Текст претензии...';
    }
}

4. Обработка ошибок и валидация

class TemplateGenerator {
    
    public function generate($module, $recordId, $templateName) {
        try {
            // 1. Валидация параметров
            $this->validateParams($module, $recordId, $templateName);
            
            // 2. Проверка существования записи
            $recordModel = Vtiger_Record_Model::getInstanceById($recordId, $module);
            if (!$recordModel) {
                throw new Exception("Запись не найдена");
            }
            
            // 3. Проверка маппинга
            $mapping = $this->getMapping($module, $templateName);
            if (empty($mapping)) {
                throw new Exception("Маппинг не найден для {$module}/{$templateName}");
            }
            
            // 4. Получение данных
            $variables = $this->buildVariables($recordModel, $mapping);
            
            // 5. Генерация документа
            $result = $this->createDocument($templateName, $variables, $module, $recordId);
            
            return ['success' => true, 'fileUrl' => $result['url']];
            
        } catch (Exception $e) {
            error_log("Template generation error: " . $e->getMessage());
            return [
                'success' => false,
                'error' => $e->getMessage()
            ];
        }
    }
    
    private function buildVariables($recordModel, $mapping) {
        $variables = [];
        $recordData = $recordModel->getData();
        
        foreach ($mapping as $templateVar => $config) {
            try {
                if (is_string($config)) {
                    // Прямое поле
                    $variables[$templateVar] = $recordData[$config] ?? '';
                } elseif (is_array($config)) {
                    // Обработка сложных конфигураций
                    $variables[$templateVar] = $this->processComplexMapping($recordModel, $config);
                }
            } catch (Exception $e) {
                // Если ошибка - подставляем пустую строку или значение по умолчанию
                error_log("Variable {$templateVar} error: " . $e->getMessage());
                $variables[$templateVar] = $config['default'] ?? '';
            }
        }
        
        return $variables;
    }
}

📊 СТРУКТУРА ФАЙЛОВ

crm_extensions/file_storage/
├── config/
│   └── template_mappings.php          # Маппинг переменных
├── api/
│   ├── generate_from_template.php     # Основной endpoint генерации
│   ├── get_templates_for_module.php   # Список шаблонов для модуля
│   └── preview_template_variables.php # Предпросмотр переменных
├── lib/
│   ├── TemplateGenerator.php          # Класс генератора
│   ├── TemplateVariableProcessors.php  # Обработчики переменных
│   └── TemplateMappingLoader.php      # Загрузчик маппингов
└── js/
    └── template_generator.js           # UI компонент

🎨 UI КОМПОНЕНТ

Вариант 1: Модальное окно

// В детальном виде модуля
function showTemplateGenerator(module, recordId) {
    // 1. Загрузить список шаблонов
    fetch(`/crm_extensions/file_storage/api/get_templates_for_module.php?module=${module}`)
        .then(r => r.json())
        .then(data => {
            if (!data.success) {
                alert('Ошибка загрузки шаблонов');
                return;
            }
            
            // 2. Показать модальное окно с выбором шаблона
            const modal = new Vtiger_Modal({
                title: 'Создать документ из шаблона',
                content: buildTemplateSelector(data.templates),
                onSelect: (templateName) => {
                    // 3. Предпросмотр переменных (опционально)
                    showVariablePreview(module, recordId, templateName);
                },
                onGenerate: (templateName) => {
                    // 4. Генерация документа
                    generateDocument(module, recordId, templateName);
                }
            });
            modal.show();
        });
}

function generateDocument(module, recordId, templateName) {
    const url = `/crm_extensions/file_storage/api/generate_from_template.php?` +
        `module=${module}&` +
        `recordId=${recordId}&` +
        `templateName=${encodeURIComponent(templateName)}`;
    
    // Показать индикатор загрузки
    showLoadingIndicator();
    
    fetch(url)
        .then(r => r.json())
        .then(result => {
            hideLoadingIndicator();
            if (result.success) {
                // Открыть документ в OnlyOffice
                window.open(result.fileUrl, '_blank');
            } else {
                alert('Ошибка: ' + result.error);
            }
        });
}

🔒 БЕЗОПАСНОСТЬ И ПРОИЗВОДИТЕЛЬНОСТЬ

Безопасность:

  1. Проверка прав доступа к записи
  2. Валидация параметров (module, recordId, templateName)
  3. Санитизация переменных перед подстановкой
  4. Логирование всех операций

Производительность:

  1. Кеширование маппингов (Redis)
  2. Кеширование шаблонов (скачанных из Nextcloud)
  3. Асинхронная генерация для больших документов
  4. Оптимизация запросов к БД (один запрос вместо множества)

🚀 ПЛАН ВНЕДРЕНИЯ

Этап 1: Базовая функциональность

  1. Создать структуру файлов
  2. Реализовать TemplateGenerator с базовой логикой
  3. Создать простой маппинг для одного модуля (Project)
  4. Протестировать на одном шаблоне

Этап 2: Расширенная функциональность

  1. Добавить поддержку связанных модулей
  2. Добавить вычисляемые поля
  3. Добавить кастомные функции
  4. Создать UI компонент

Этап 3: Оптимизация и масштабирование

  1. Добавить кеширование
  2. Оптимизировать запросы
  3. Добавить предпросмотр переменных
  4. Добавить логирование и мониторинг

ВОПРОСЫ ДЛЯ УТОЧНЕНИЯ

  1. Какие модули приоритетны? (Project, HelpDesk, Contacts?)
  2. Какие шаблоны нужны в первую очередь? (претензии, иски, жалобы?)
  3. Нужен ли предпросмотр переменных перед генерацией?
  4. Нужна ли возможность редактировать переменные перед генерацией?
  5. Как обрабатывать ошибки? (показывать пользователю, логировать, отправлять уведомления?)

💡 РЕКОМЕНДАЦИИ

  1. Начать с простого:

    • Один модуль (Project)
    • Один шаблон (pretenziya.docx)
    • Прямые поля без связей
  2. Постепенно усложнять:

    • Добавить связанные модули
    • Добавить вычисляемые поля
    • Добавить больше шаблонов
  3. Тестировать на реальных данных:

    • Использовать реальные проекты
    • Проверять форматирование
    • Проверять производительность
  4. Документировать:

    • Маппинг переменных
    • Кастомные функции
    • Примеры использования

Готов начать реализацию! С чего начнем? 🚀