Экономия своими руками

Когда проводят презентацию новой системы, связанной с IT областью, часто рассказывают о таком параметре как «стоимость владения», сопровождая обязательным словом «низкая». Обычно если об этом говорят, то это значит, что стоимость покупки сильно завышена, а производитель не сомневается в том, что его технология не потребует больших затрат на сопровождение. По большому счету это не самый плохой выбор.

Не знаю, как по этому параметру оценивают Navision. В этой статье я дам несколько советов, как снизить стоимость владения или просто сэкономить на покупке объектов в Navision применяя вполне легальные методы. Хочу обратить внимание на то, что речь идет о методах, которые позволяют решить проблему экономии средств на закупку дополнительных объектов Navision (таблицы, формы, отчеты) и не всегда эти методы будут самыми производительными по скорости выполнения или будут простыми по способу реализации.

Таблицы

Временные данные

Как правило, для хранения временных данных используются временные таблицы. Стандартное и вполне разумное решение, для любой СУБД. Но в Navision нет возможности создать временную таблицу произвольной структуры. Часто приходится сталкиваться с тем, что временная таблица не имеет всех необходимых полей для хранения данных. Кроме того вводит в заблуждение то, что в поле временной таблицы с именем, например «Код Товара», хранится значение не имеющее ни какого отношения к этому коду т.к. в исходной таблице нет поля с подходящим названием, типом и что самое плохое нужной длины. Так что, для хранения временных данных нужно покупать новую таблицу? Нет, не наш метод.

Попробуйте использовать XML. Причем советую использовать не стандартне для Navision XMLPort, а COM объект ‘Microsoft XML, vXXX’. У вас открывается возможность создать объект произвольной структуры, не привязанной к табличным ограничениям Navision. Если Вы планируете хранить несколько тысяч значений во временной таблице, то работает это также быстро, как и временная таблица. Ключевые значения советую хранить как узлы XML. Тогда можно быстро обращаться к нужной записи.

Заманчивыми является следующие возможности. В одном XML можно хранить несколько временных таблиц одновременно. Можно реализовать массив/список с произвольной глубиной, это то, что вообще нельзя сделать в Navision. Можно сохранять временные данные в файл, что в частности позволяет упростить отладку. Можно создать замену постоянно глючащему fin.zup и сохранять необходимые настройки в XML. В приложении к статье приведен пример как, используя XML создать интерфейс хранения в стиле ini файла.

Долгосрочное хранение

Как быть в случае, если нам нужно создать таблицу для длительного хранения информации? Давайте купим таблицу в Navision? Нет, не наш метод.

Попробуйте использовать ADO. Создайте таблицу в SQL сервере нужной структуры и заполняйте ее данными, используя стандартные команды INSERT, UPDATE и т.д. Советую так поступать для тех случаев, когда Вам необходимо хранить логи доступа или логи использования тех или иных объектов или их состояний. Как правило, в таких случаях Вам нужно только добавлять в таблицу логов данные, что очень просто сделать. Но ни в коем случае не думайте, что я Вас пытаюсь ограничить только этими задачами, дерзайте и ищите другие возможности по использованию ADO.

Пример:

Создаем таблицу в SQL

CREATE TABLE [dbo].[Log](
    [TIMESTAMP] [TIMESTAMP] NOT NULL,
    [ID] [INT] IDENTITY(1,1) NOT NULL,
    [Date_Time] [datetime] NOT NULL,
    [Message] [VARCHAR](250) NOT NULL,
    [Message DATE] [datetime] NOT NULL,
    [Message TIME] [datetime] NOT NULL,
 CONSTRAINT [NAS Log$0] PRIMARY KEY CLUSTERED
(
    [ID] ASC
) WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF,
        ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON)
ON [DATA Filegroup 1]
) ON [DATA Filegroup 1]

В Navision создаем функцию для хранения логов
AddSQLLog(MessageText : Text[1024])

{
Name    DataType    Subtype Length
ADODB   Automation  'Microsoft ActiveX Data Objects 2.8 Library'.Connection
StringConnect   Text        1024
}
StringConnect:='driver={SQL Server};server='+Сервер+';uid='+IdUser+';pwd='+Password+';database='+ИмяБазы;

// инициализация библиотеки ADO
IF ISCLEAR(ADODB) THEN
   IF NOT CREATE(ADODB) THEN ERROR('Не могу инициализировать библиотеку ADO !');

ADODB.CommandTimeout(3600);
ADODB.ConnectionString :=StringConnect;
ADODB.Open();
=ADODB.Execute(
 'SET DATEFORMAT dmy ‘+
 ‘INSERT INTO [Log]'+
 '([Date_Time],[Message],[Message Date],[Message Time])'+
 'VALUES ('''+FORMAT(CREATEDATETIME(TODAY, TIME))+''','''+COPYSTR(MessageText,1,250)+''','''+
 FORMAT(TODAY,0,'<day, 2>/<month, 2>/<year, 2>')+''',''1754-01-01 '+FORMAT(TIME)+''')'
 );
ADODB.Close;

Еще  ADO

Пример необычного использования ADO из реальной практики. Всем известно что, при учете документов в Navision возможен учет только одного документа в один и тот же момент времени. При большом количестве пользователей/документов учет превращается в кошмар белых экранов и длительных блокировок. Радикально решать задачу нужно на уровне учетных функции, но специалисты Navision/Micrososoft на протяжении всех версий даже не пытаются этого делать. Давайте хотя бы уберем белые экраны и покажем пользователю счетчик попыток учета. При этом в реальной очереди блокировки будет находиться один пользователь.

Проверьте наличие представления [Session] в Вашей БД. Если его нет, то создайте:

SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

CREATE VIEW     [dbo].[SESSION] AS            
SELECT
CAST(SP.[spid] AS INTEGER) AS "Connection ID",
CAST(RTRIM(SP.[loginame]) AS NVARCHAR(64)) COLLATE Cyrillic_General_CI_AS AS "User ID",
CAST(CASE WHEN SP.[spid] = @@SPID THEN 1 ELSE 0 END AS TINYINT) AS "My Session",
CONVERT(DATETIME, '1754-01-01 '+CONVERT(CHAR(8), SP.[login_time], 108), 120) AS "Login Time",
CONVERT(DATETIME, CONVERT(CHAR(10), SP.[login_time], 120)+' 00:00:00:000', 121) AS "Login Date",
CAST (SD.[name] AS NVARCHAR(128)) COLLATE Cyrillic_General_CI_AS AS "Database Name",
CAST (RTRIM(SP.[program_name]) AS NVARCHAR(64)) COLLATE Cyrillic_General_CI_AS AS "Application Name",
CASE WHEN SP.[nt_domain] <> '' THEN 1 ELSE 0 END AS "Login Type",
CAST(RTRIM(SP.[hostname]) AS NVARCHAR(64)) COLLATE Cyrillic_General_CI_AS AS "Host Name",SP.[cpu] AS "CPU Time (ms)",
CASE WHEN SP.[memusage] < 0 THEN 0 ELSE SP.[memusage]*8 END AS "Memory Usage (KB)",
SP.[physical_io] AS "Physical I_O",
CAST(CASE WHEN SP.[blocked] <> 0 THEN 1 ELSE 0 END AS TINYINT) AS "Blocked",CAST(CASE WHEN SP.[blocked] <> 0 THEN SP.[waittime] ELSE 0 END AS INTEGER) AS "Wait Time (ms)",
CAST(SP.[blocked] AS INTEGER) AS "Blocking Connection ID",
CAST(ISNULL(RTRIM(SPB.[loginame]), '') AS NVARCHAR(64)) COLLATE Cyrillic_General_CI_AS AS "Blocking User ID",
CAST(ISNULL(RTRIM(SPB.[hostname]), '') AS NVARCHAR(64)) COLLATE Cyrillic_General_CI_AS AS "Blocking Host Name",
CAST('' AS NVARCHAR(64)) COLLATE Cyrillic_General_CI_AS AS "Blocking Object",
CASE WHEN SP.[cmd] = 'AWAITING COMMAND' THEN CAST(DATEDIFF(SECOND, SP.[last_batch], GETDATE()) AS BIGINT)*1000 ELSE 0 END AS "Idle Time"            
FROM  [master].[dbo].[sysprocesses] AS SP            
JOIN  [master].[dbo].[sysdatabases] AS SD
 ON (SP.[dbid] = SD.[dbid])            
LEFT OUTER JOIN [master].[dbo].[sysprocesses] AS SPB
 ON (SP.[blocked] = SPB.[spid])            
 WHERE  SP.[ecid] = 0 AND SP.[last_batch] >= CONVERT(DATETIME, '2000-01-01 00:00:00', 120)
GO

Предоставьте пользователю, через которого вы будете подключаться к SQL серверу право на просмотр этого представления.

Затем в Navision создайте функцию:

ПроверкаБлокировки(МаксПопыток : Integer) Result : Boolean

////////////////////////////////////////////////////////////
// Проверка блокировок пользователей
//   МаксПопыток  >0 - максимальное количество попыток
//                =0 - без повторных попыток, но блокировки проверит
//                <0 - пока не будет разблокирован
// Возврат
//   True - последняя проверка нашла блокировки
//   False - нет блокировки
///////////////////////////////////////////////////////////
//Либо создайте переменные либо измените в тексте программы
//Сервер
//IdUser
//ИмяБазы
//Password
////////////////////////////////////////////////////////////

{
Name    DataType    Subtype Length
ADODB   Automation  'Microsoft ActiveX Data Objects 2.8 Library'.Connection
ADOREC  Automation  'Microsoft ActiveX Data Objects Recordset 2.8 Library'.Recordset   
ФирмаИнфор    Record  Настройки 
StringConnect   Text        1024
isExit  Boolean    
КолПопыток    Integer    
DW  Dialog     
}
RANDOMIZE();

ФирмаИнфор.GET;
IF NOT ФирмаИнфор."Проверка Блокировки до Учета" THEN EXIT;
StringConnect:='driver={SQL Server};server='+Сервер+';uid='+IdUser+';pwd='+Password+';database='+ИмяБазы;

IF GUIALLOWED THEN DW.OPEN(
'Проверка возможности учета\'+
'Попытка: ##1##############',
КолПопыток
);

// инициализация библиоктеки ADO
IF ISCLEAR(ADODB) THEN
   IF NOT CREATE(ADODB) THEN ERROR('Не могу инициализировать библиотеку ADO 2.8!');
IF ISCLEAR(ADOREC) THEN
   IF NOT CREATE(ADOREC) THEN ERROR('Не могу инициализировать библиотеку ADO 2.8!');

ADODB.CommandTimeout(3600);
ADODB.ConnectionString :=StringConnect;
ADODB.Open();
isExit:=FALSE;
КолПопыток:=0;
WHILE NOT isExit DO
BEGIN
   КолПопыток:=КолПопыток+1;
   IF GUIALLOWED THEN DW.UPDATE();
   //получаем количество блокированных сессий
   ADOREC:=ADODB.Execute(
    ' select count([Connection Id]) [Block Count] '+
    ' from dbo.[Session] '+
    ' where '+
    ' [Database Name] in ('''+ИмяБазы+''') '+
    ' and [Application Name] = ''Microsoft Business Solutions-Navision Client'' '+
    ' and [Blocked]<>0 ');

    IF NOT ADOREC.BOF THEN
    IF NOT ADOREC.EOF THEN
    BEGIN
      ADOREC.MoveFirst;
    END;
    Result:=ConvertToInt(ADOREC.Fields.Item(0).Value)>0;
    IF МаксПопыток>=0 THEN
    BEGIN
      IF КолПопыток>=МаксПопыток THEN  isExit:=TRUE;
    END
    ELSE
    BEGIN
      IF NOT Result THEN isExit:=TRUE;
    END;
    ADOREC.Close();
    IF NOT isExit THEN SLEEP(RANDOM(1000));
END;
ADODB.Close();
IF GUIALLOWED THEN DW.CLOSE();

Данную функцию следует вызывать перед самой передачей документа в codeunit учета,  до того как возникнет  блокировка учета.  Как это работает:

Пользователь 1 запустил учет. Т.к. блокировки нет, он будет учитывать документ.

Пользователь 2 запустит учет. Т.к. блокировки нет, он запустит учет и будет ждать пользователя 1. Зависнет учет на первой строке документа. Количество блокировок 1.

Пользователь 3 видит окно с сообщением «Проверка возможности учета ХХХ» до тех пор, пока  пользователь 1 будет блокировать учет пользователя 2, либо не закончится счетчик попыток учета. Пауза между проверками случайным образом будет варьироваться в диапазоне от 0 до 1000 мсек. Когда пользователь 1  закончит учет пользователь 2 начнет учет, а пользователь 3 станет за ним в очередь учета. У меня количество попыток учета 500.

Замечу, что такой подход бережёт ресурсы сервера, т.к. сокращается количество блокировок, которые являются достаточно ресурсоемкими.

Универсальный справочник

У меня всегда вызывает раздражение тот факт, что в Navision считают, что для отображения бизнес информации достаточно тех справочников, которые изначально там присутствуют. Если Вам нужно классифицировать товар по способу хранения, то где хранить справочник способов хранения? Если клиенты делятся по каналу реализации, то где хранить справочник каналов реализации? Причем замечу, что  работа несложная, но нужно купить таблицу, форму для каждого справочника. Давайте Купим! Нет, не наш метод.

Как быть. Тут нам придется принять тот факт, что в справочники нужно вносить изменения, а значит, необходима форма. Когда будем разбираться с тем, как можно сэкономить на формах, то у Вас, возможно, она появиться без особых проблем. А вот таблицу нам придётся купить. Но не стоит покупать столько таблиц, сколько справочников нам нужно сейчас или возможно понадобиться в будущем. Давайте ограничимся одной таблицей и одной формой.

Начнем со структуры таблицы.

Универсальный справочник

Поле

Тип данных Описание

Тип строки

Option/Integer/Code[30]  Это тип справочника. Поле лучше не показывать в форме.

  • Option – имеет ограничение в 256 символов в описании. При большом количестве справочников будет являться ограничением. Более эффективно с точки зрения хранения информации и читабельности кода
  • Integer — Если вы использовали Option и надо снять ограничение то переходите в Integer. Все также эффективно хранение, но получается очень плохой код
  • CODE – Неэффективное хранение, но зато нормально читается код. Я предпочитаю этот тип.

Код

CODE[10] Ключ справочника

Наименование

Text[50] Описание ключа

Другие поля

Если нужны дополнительные поля, то можете их использовать.

Первичный ключ справочника «Тип строки», «Код». Это гарантирует нам уникальность введенных значений. Если значения справочника нужно сортировать каким-то уникальным способом, то в другие поля можно добавить поле «Сортировка» и создать еще один ключ «Тип строки», «Сортировка», «Код».

Теперь делаем форму.

Нужно учесть, что она должна показывать разные справочники.

Для передачи информации о том какой справочник нужно показать можно использовать фильтр который передаётся при вызове формы. Например, при вызове через lookup.

onLookup()

УниверсальныйСправочник.RESET;
УниверсальныйСправочник.FILTERGROUP(2);
УниверсальныйСправочник.SETRANGE(«Тип строки»,'БАЛАНС_АО');
УниверсальныйСправочник.FILTERGROUP(0);
IF FORM.RUNMODAL(0, УниверсальныйСправочник) = ACTION::LookupOK THEN
BEGIN
  …………..
END;

В форме на onOpenForm нужно добавить показ колонок:

ПарТип:=GETFILTER((«Тип строки»);

IF ПарТип = 'ОТДЕЛ' THEN
BEGIN
  CurrForm.Тип.VISIBLE:=FALSE;
  CurrForm.Код.VISIBLE:=FALSE;
  CurrForm.Параметр.VISIBLE:=FALSE;
  CurrForm.Сортировка.VISIBLE:=TRUE;
End

Else IF ПарТип = 'БАЛАНС_АО' THEN
BEGIN
  CurrForm.Тип.VISIBLE:=FALSE;
  CurrForm.Код.VISIBLE:=TRUE;
  CurrForm.Параметр.VISIBLE:=FALSE;
  CurrForm.Сортировка.VISIBLE:=FALSE;
End;

Либо можно в форме создать функцию, в которую нужно передать параметр с названием типа справочника. Я предпочитаю такой вызов. Хотя приходится добавлять переменные формы, но зато через функцию можно передать дополнительные параметры. Например, способ сортировки.

Формы

Показ одной формой данных из разных таблиц

Этот метод хорошо подойдет для справочников. Если у вас есть много таблиц с разной справочной информацией, то не обязательно создавать разные формы для разных справочников.  Можно использовать одну форму и временную таблицу, куда можно загружать разные данные. Этот метод подходит и для показа информации из разных источников (ADO,XML и т.д.). Только вот проблема будет с выбором временной таблицы. Она должна иметь достаточно большое количество полей для показа разнообразной информации.

Тема использования временной таблицы в форме  хорошо освещена в данном источнике http://naviart.ru/temp-subform-source.

Используя методы универсального справочника из предыдущей главы можно показывать информацию из разных таблиц/источников данных.

Разработка новых документов

Обязательно настанет день, когда у вас появиться задание на создание нового документа в Navision.  Давайте посчитаем, сколько таблиц и форм нужно использовать по стандарту:

  • Четыре таблицы – заголовок неучтенных документов, строки неучтенных документов, заголовок учтенных документов, строки учтенных документов
  • Шесть форм – список неучтенных документов, список учтенных документов, шапка неучтённого документа, шапка учтенного документа, субформа неучтенных строк , субформа учтенных строк.
  • Таблица движений – нужна если документы должны изменять состояние учета.

Открываем прайс и считаем, сколько стоят все эти объекты. Давайте купим! Нет не наш метод.

Зачем нам отдельные таблицы для учтенных и неучтенных документов?  Есть мнение что хранение в разных таблицах улучшает производительность . Вполне корректное утверждение. Но ведь есть еще такая замечательная возможность SQL сервера как секционирование, т.е. разделение большой таблицы на несколько частей. Если количество Ваших документов станет очень большим, то Вы можете разделить таблицу на секции, не изменяя код Navision отвечающий за заполнение таблицы или извлечение из нее данных. Причем сделать это можно именно тогда, когда большая таблица будет существенно влиять на скорость работы. Кстати советую секционировать такие таблицы как неучтенные документы продажи.

Итак, вместо четырех таблиц можно использовать две.  Предположим, нам нужно создать несколько разных документов в рамках одного проекта. Например, учет тары. Нужно ли создавать новые таблицы для каждого вида документа. Скорее всего, нет.

Заголовок

Поле Тип данных Описание
Тип документа Option Первичный ключ.Используйте только в том случае, если в одной таблице будет храниться несколько видов документов. Например: Приход тары, Расход тары, Списание тары и т.д.
Документ Но. Code[20] Первичный ключ
Учтен Boolean Если планируется секционирование по учтенным/неучтенным документам, то нужно включить это поле в первичный ключ.
Другие поля

Один совет, если в таблице будут храниться разные типы документов, то секционируйте таблицу по типам документов. Секционировать по учтенным/неученным  документам в таком случае нецелесообразно.  Соответственно поле «Учтен» не нужно включать в первичный ключ. Булевы поля в индексах не самое лучшее решение.

Строки

Поле Тип данных Описание
Тип документа Option Первичный ключ.Используйте только в том случае, если в одной таблице будет храниться несколько видов документов. Например: Приход тары, Расход тары, Списание тары и т.д.
Документ Но. Code[20] Первичный ключ
Строка Но. Integer Первичный ключ
Учтен Boolean Если планируется секционирование по учтенным/неучтенным документам, то нужно включить это поле в первичный ключ. В строках это поле нужно только при секционировании, иначе просто исключайте его из структуры таблицы.
Другие поля

Т.к. у нас нет разных таблиц для учтенных и неучтенных документов, то мы сразу же можем сократить количество форм до трех: Список документов, Заголовок документа, Субформа строк документа. Это же позволяет нам сократить количество печатных форм документа.

Давайте сначала посмотрим на список документов.

Разделение по типам документа лучше всего реализовать через передачу параметра в форму. Создайте в форме глобальную переменную:

DocType:integer;

Создайте функцию

SetDocType(pDocType:integer);

DocType : = pDocType;

Теперь нужно отфильтровать при открытии формы

onOpenForm()

FILTERGROUP(2);
Setrange(“Тип документа”,DocType);
FILTERGROUP(0);

Для корректного вызова формы с нужным типом документа нужно объявить переменные и вызов функции SetDocType()

onPush

{
ФормаСписка :Form «Спиок  документов»
ЗаголовокДокумента: Record “Заголовок”
}
Clear(ФормаСписка);
ФормаСписка.SetDocType(ЗаголовокДокумента.”Тип Документа”::”Приход тары”);
ФормаСписка.Run;

Если пользователю нужно отдельно показывать учтенные и неучтенные документы, то можно добавить параметр «Учтен» в функцию SetDocType() и переделать OpenForm.

Теперь давайте посмотрим на карточку документа.

Нам необходимо запретить редактирование учтенных документов

OpenForm()

EDITABLE:=not Учтен;

Как быть, если у разных типов документов должны быть показаны разные поля. Как правило, в рамках одной задачи поля одинаковые у разных типов документов. Но бывают поля, которые характерны только для одного типа документа. Например, в списании тары – тип списания.  Сначала в заголовок формы нужно добавить все поля и изменить OpenForm()

CurrForm.ТипСписания.Visible = (”Тип Документа” = ”Тип Документа”::”Списание тары”);

Не забудьте изменить свойство Name=”ТипСписания” у того объекта, который хотите скрыть/показать.

В строках документа Вам, возможно, нужно будет скрывать/показывать разные поля для разных типов документов. Делается это также как и в заголовке только для столбцов субформы.

Шапка документа и субформа. Две формы или одна?

Но вот тот вопрос, который у меня возник, когда я начал писать эту статью. Почему нам необходимо делить форму документа на шапку и строки, используя для этого форму и субформу? Нельзя ли обойтись одной формой? Оказалось можно.

Создайте форму для строк документа. Объявите глобальную переменную:

ЗаголовокДокумента:Record “Заголовок”;

Установить все необходимые фильтры в OpenForm() для показа одного документа, например:

OpenForm()

ЗаголовокДокмента.SETRANGE(“Документ Но.”,”ДОК000001”);
//тоже самое для строк
SETRANGE(“Документ Но.”,”ДОК000001”);

Если планируется редактирование заголовка, то в событии onCloseForm добавьте

ЗаголовокДокмента.MODIFY(True);

Добавьте в форму объект закладки и прижмите его к верхнему краю. Табличную часть разместите ниже. Добавьте в закладки поле для редактирования. В свойстве SourceExpr укажите ЗаголовокДокумента.ПолеДляРедактирования.  Откомпилируйте форму.

Теперь откройте форму. Успех? Вроде да. Однако при изменении в заголовке документа Вы увидите, что после повторного открытия формы содержимое заголовка и строк не изменилось.

Нужно вызвать Validate() поля. В событии onValidate() поля редактирования нужно написать

onValidate()

ЗаголовокДокумента.validate(ПолеДляРедактирования);

Компилируем. Редактирование приводит к желаемому результату. Теперь изменения вносятся в заголовок, и отрабатывает последующий код.

Но как же без ложки дегтя. Проблема в том, что у заголовка нет объекта xRec, т. к. транзакция не начата. Это означает что если в Validate поля, которое Вы редактируете, есть конструкция вида:

IF Rec.ПолеДляРедактирования<> xRec.ПолеДляРедактирования THEN …;

То система ничего не сделает т.к. Rec  и xRec одинаковы.

Что можно сделать? Есть два варианта:

  • Исключить сравнение Rec  и xRec в коде Validate  как таковое
  • Если, все таки, этого не избежать, то
    • Выделить сравнение в отдельную функцию
    • В заголовке формы создать временную таблицу «xЗаголовокДокумента:Record “Заголовок”;» в которую записывать копию заголовка при открытии формы
    • Передавать в функцию сравнения ЗаголовокДокумента и старую копию заголовка для корректного сравнения данных.

В любом случае данную методику можно как минимум использовать для показа учтенных документов, в которые не нужно вносить изменения.

Печать

Самый простой способ экономить в печати — использовать Excel. Я думаю каждый способен выбрать для себя как лучше всего его использовать . Кто-то будет писать прямой код, кто-то использует Query объект, кто-то шаблоны.  Я использую все три метода в зависимости от типа задачи.

С точки зрения экономии объектов Report  наиболее перспективной является печать документов. У меня в Navision для печати накладной документов продаж используется три отчета: Печать неучтенных продаж/кредит-нот,  Печать учтенных продаж, Печать учтенных кредит нот. Если бы в Navision не было деления на учтенные и неучтенные таблицы, то и проблем бы не было. А так с точки зрения администрирования это полный кошмар. Нужно добавить новую колонку, изменить текст, подвинуть поле — нужно вносить изменения в три отчета, в которых еще и некоторые переменные и поля названы по разному.

Зачем нам три отчета для печати одного документа, даже если источники данных разные? Нужно сразу делать один отчет. Отчет должен базироваться на временных таблицах.  Я использую временные таблицы на основе неучтенных документов и копирую в них печатаемые документы как учтенные, так и неучтенные. Таким образом, вместо трех отчетов я получаю один.

Однако все равно остается проблема  добавления/удаления колонки в секциях Navision.  В том же документе продажи у меня 21 секция для печати разных вариантов строк.  Поэтому следующим этапом становиться печать документа в Excel с использованием шаблона.Это уже не предмет изучения данной статьи.

В качестве примера как во временную таблицу скопировать разные типы документов приведу пример функции:

CopyDocToTempRec(ТипИсточникаДанных : Integer;СчетНо : Code[20];VAR TempHead : TEMPORARY Record "Продажа Заголовок";VAR TempLine : TEMPORARY Record " Продажа Строка"
//0 - Неучтенные заказ продажи или кредит нота
//1 - Учтенный заказ продажи
//2 - Учтенная кредит нота
{
Name    DataType    Subtype Length
ПЗ    Record  Продажа Заголовок  
ПС    Record  Продажа Строка
ПСЗ  Record  Продажа Счет Заголовок 
ПСС  Record  Продажа Счет Строка   
КНЗ  Record  Продажа Кредит Нота Заголовок
КНС  Record  Продажа Кредит Нота Строка  
}
IF ТипИсточникаДанных=0 THEN
BEGIN
  ПЗ.SETFILTER("Тип Документа",'%1|%2',ПЗ."Тип Документа"::Заказ,ПЗ."Тип Документа"::"Кредит Нота");
  ПЗ.SETRANGE("Счет Но.",СчетНо);
  IF ПЗ.FINDSET THEN
  BEGIN
    TempHead.INIT;
    TempHead.TRANSFERFIELDS(ПЗ);
    TempHead.INSERT;
  END
  ELSE ERROR('Не найден неучтенный документ %1',СчетНо);
  ПС.SETRANGE("Тип Документа",ПЗ."Тип Документа");
  ПС.SETRANGE("Документ Но.",ПЗ."Но.");
  IF ПС.FINDSET THEN
  REPEAT
    TempLine.INIT;
    TempLine.TRANSFERFIELDS(ПС);
    TempLine.INSERT;
  UNTIL ПС.NEXT=0;
END
ELSE IF ТипИсточникаДанных=1 THEN
BEGIN
  ПСЗ.SETRANGE("Но.",СчетНо);
  IF ПСЗ.FINDSET THEN
  BEGIN
    TempHead.INIT;
    TempHead.TRANSFERFIELDS(ПСЗ);
    TempHead."Тип Документа":=TempHead."Тип Документа"::Заказ;
    TempHead.INSERT;
  END
  ELSE ERROR('Не найдена учтенная продажа %1',СчетНо);
  ПСС.SETRANGE("Документ Но.",СчетНо);
  IF ПСС.FINDSET THEN
  REPEAT
    TempLine.INIT;
    TempLine.TRANSFERFIELDS(ПСС);
    TempLine."Тип Документа":=TempHead."Тип Документа";
    TempLine.INSERT;
  UNTIL ПСС.NEXT=0;
END
ELSE IF ТипИсточникаДанных=2 THEN
BEGIN
  КНЗ.SETRANGE("Но.",СчетНо);
  IF КНЗ.FINDSET THEN
  BEGIN
    TempHead.INIT;
    TempHead.TRANSFERFIELDS(КНЗ);
    TempHead."Тип Документа":=TempHead."Тип Документа"::"Кредит Нота";
    TempHead.INSERT;
  END
  ELSE ERROR('Не найдена учтенная кредит нота продажи %1',СчетНо);
  КНС.SETRANGE("Документ Но.",СчетНо);
  IF КНС.FINDSET THEN
  REPEAT
    TempLine.INIT;
    TempLine.TRANSFERFIELDS(КНС);
    TempLine."Тип Документа":=TempHead."Тип Документа";
    TempLine.INSERT;
  UNTIL КНС.NEXT=0;
END
ELSE ERROR('Неверный тип источника данных.');

Приложение

XML

Идея в том, чтобы имитировать работу в стиле ini файла как в Delphi используя  XML в качестве хранилища данных. В тему этой статьи: достаточно быстро данный код можно переделать для временной таблицы.

В любом доступном объекте создайте глобальную переменную

XMLDoc  Automation            'Microsoft XML, v2.6'.DOMDocument

Это объект для хранения XML файла. Его нужно правильно инициализировать и удалить с помощью следующих функций:

tXML_Init()

{
Name    DataType    Subtype Length
Node    Automation  'Microsoft XML, v2.6'.IXMLDOMNode  
Doc Automation  'Microsoft XML, v2.6'.IXMLDOMNode  
}
CREATE(XMLDoc,TRUE);
Node:=XMLDoc.createProcessingInstruction('xml', 'version=''1.0'' encoding=''UTF-8''');
XMLDoc.appendChild(Node);
Node := XMLDoc.createElement(‘INI’);
XMLDoc.appendChild(Node);

tXML_Free()

CLEAR(XMLDoc);

Добавляем функции для работы с ключами:

tXML_KeyCount() result : Integer

{
Name    DataType    Subtype Length
Doc Automation  'Microsoft XML, v2.6'.IXMLDOMNode  
NodeList    Automation  'Microsoft XML, v2.6'.IXMLDOMNodeList  
}
result:=0;
Doc := XMLDoc.documentElement;
IF NOT ISCLEAR(Doc) THEN
BEGIN
  NodeList := Doc.childNodes;
  result := NodeList.length;
END;

tXML_KeyExists(Key : Code[30]) result : Boolean

{
Name    DataType    Subtype Length
Doc Automation  'Microsoft XML, v2.6'.IXMLDOMNode  
Node    Automation  'Microsoft XML, v2.6'.IXMLDOMNode  
}
Doc := XMLDoc.documentElement;
result := NOT ISCLEAR(Doc);
IF result THEN
BEGIN
  Node := Doc.selectSingleNode(Key);
  result := NOT ISCLEAR(Node);
END;

tXML_AddKey(Key : Code[30])

{
Name    DataType    Subtype Length
Doc Automation  'Microsoft XML, v2.6'.IXMLDOMNode  
Node    Automation  'Microsoft XML, v2.6'.IXMLDOMNode  
}
IF NOT tXML_KeyExists(Key) THEN
BEGIN
  Doc := XMLDoc.documentElement;
  Node := XMLDoc.createElement(Key);
  Doc.appendChild(Node);
END;

tXML_DelKey(Key : Code[30])

{
Name    DataType    Subtype Length
Doc Automation  'Microsoft XML, v2.6'.IXMLDOMNode  
Node    Automation  'Microsoft XML, v2.6'.IXMLDOMNode  
}
IF tXML_KeyExists(Key) THEN
BEGIN
  Doc := XMLDoc.documentElement;
  Node := Doc.selectSingleNode(Key);
  Doc.removeChild(Node)
END;

tXML_GetKeyByIndex(Index : Integer) Key : Code[30]

{
Name    DataType    Subtype Length
Doc Automation  'Microsoft XML, v2.6'.IXMLDOMNode  
NodeList    Automation  'Microsoft XML, v2.6'.IXMLDOMNodeList  
Node    Automation  'Microsoft XML, v2.6'.IXMLDOMNode  
}
//Индекс начинается с 0
Key:='';
Doc := XMLDoc.documentElement;
IF NOT ISCLEAR(Doc) THEN
BEGIN
  NodeList := Doc.childNodes;
  Node := NodeList.item(Index);
  Key := Node.nodeName;
END;

tXML_FieldCount(Key : Code[30]) result : Integer

{
Name    DataType    Subtype Length
Doc Automation  'Microsoft XML, v2.6'.IXMLDOMNode  
KeyNode Automation  'Microsoft XML, v2.6'.IXMLDOMNode  
NodeList    Automation  'Microsoft XML, v2.6'.IXMLDOMNodeList  
}
result:=0;
Doc := XMLDoc.documentElement;
KeyNode := Doc.selectSingleNode(Key);
IF NOT ISCLEAR(KeyNode) THEN
BEGIN
  NodeList := KeyNode.childNodes;
  result := NodeList.length;
END;

И еще функции для работы со значениями:

tXML_FieldExists(Key : Code[30];Field : Code[30]) result : Boolean

{
Name    DataType    Subtype Length
Doc Automation  'Microsoft XML, v2.6'.IXMLDOMNode  
KeyNode Automation  'Microsoft XML, v2.6'.IXMLDOMNode  
FieldNode   Automation  'Microsoft XML, v2.6'.IXMLDOMNode  
}
result:=FALSE;
Doc := XMLDoc.documentElement;
KeyNode := Doc.selectSingleNode(Key);
IF NOT ISCLEAR(KeyNode) THEN
BEGIN
  FieldNode := KeyNode.selectSingleNode(Field);
  result:=NOT ISCLEAR(FieldNode);
END;

tXML_SetField(Key : Code[30];Field : Code[30];Value : Text[1024])

{
Name    DataType    Subtype Length
Doc Automation  'Microsoft XML, v2.6'.IXMLDOMNode  
KeyNode Automation  'Microsoft XML, v2.6'.IXMLDOMNode  
FieldNode   Automation  'Microsoft XML, v2.6'.IXMLDOMNode  
}
Doc := XMLDoc.documentElement;
KeyNode := Doc.selectSingleNode(Key);
IF ISCLEAR(KeyNode) THEN
BEGIN
  tXML_AddKey(Key);
  KeyNode := Doc.selectSingleNode(Key);
END;
FieldNode := KeyNode.selectSingleNode(Field);
IF ISCLEAR(FieldNode) THEN
BEGIN
  FieldNode := XMLDoc.createElement(Field);
  KeyNode.appendChild(FieldNode);
END;
FieldNode.text:=Value;

tXML_GetField(Key : Code[30];Field : Code[30]) result : Text[1024]

{
Name    DataType    Subtype Length
Doc Automation  'Microsoft XML, v2.6'.IXMLDOMNode  
KeyNode Automation  'Microsoft XML, v2.6'.IXMLDOMNode  
FieldNode   Automation  'Microsoft XML, v2.6'.IXMLDOMNode  
}
result:='';
Doc := XMLDoc.documentElement;
KeyNode := Doc.selectSingleNode(Key);
IF NOT ISCLEAR(KeyNode) THEN
BEGIN
  FieldNode := KeyNode.selectSingleNode(Field);
  IF NOT ISCLEAR(FieldNode)
  THEN result:=FieldNode.text;
END;

tXML_DelField(Key : Code[30];Field : Code[30])

{
Name    DataType    Subtype Length
Doc Automation  'Microsoft XML, v2.6'.IXMLDOMNode  
KeyNode Automation  'Microsoft XML, v2.6'.IXMLDOMNode  
FieldNode   Automation  'Microsoft XML, v2.6'.IXMLDOMNode  
}
Doc := XMLDoc.documentElement;
KeyNode := Doc.selectSingleNode(Key);
IF NOT ISCLEAR(KeyNode) THEN
BEGIN
  FieldNode := KeyNode.selectSingleNode(Field);
  IF NOT ISCLEAR(FieldNode)
  THEN KeyNode.removeChild(FieldNode);
END;

tXML_GetlFieldByIndex(Key : Code[30];Index : Integer) Field : Code[30]

{
Name    DataType    Subtype Length
Doc Automation  'Microsoft XML, v2.6'.IXMLDOMNode  
NodeList    Automation  'Microsoft XML, v2.6'.IXMLDOMNodeList  
Node    Automation  'Microsoft XML, v2.6'.IXMLDOMNode  
KeyNode Automation  'Microsoft XML, v2.6'.IXMLDOMNode  
}
//Index начинается с 0
Field:='';
Doc := XMLDoc.documentElement;
KeyNode := Doc.selectSingleNode(Key);
IF NOT ISCLEAR(KeyNode) THEN
BEGIN
  NodeList := KeyNode.childNodes;
  Node := NodeList.item(Index);
  Field := Node.nodeName;
END;

И функции для сохранения и чтения файлов:

tXML_SaveToFile(FileName : Text[512])

XMLDoc.save(FileName);

tXML_LoadFromFile(FileName : Text[512])

XMLDoc.load(FileName);

Функцианальность можно наращивать в зависимости от необходимости.
Пример использования:

{
Name    DataType    Subtype Length
tst_XMLTable    Record  Коды Резервов  
}
tst_XMLTable.tXML_Init;
if not tst_XMLTable.tXML_KeyExists(‘A’) then
begin
  tst_XMLTable.tXML_AddKey(‘A’);
  tst_XMLTable.tXML_SetField(‘A’,’F1’,’test’);
end;
tst_XMLTable.tXML_Free;

Автор:

Количество статей, опубликованных автором: 5. Дополнительная информация об авторе появится вскоре.

Комментарии (3 комментария)

  1. Xml вместо темповой таблицы — тема.

  2. kna

    >Но в Navision нет возможности создать временную таблицу произвольной структуры.
    Если рекод темповый — платформа не проверяет лицензию на него.
    Вполне рабочий юзкейс — создать таблицу с необходимой структурой вне лицензированного клиентом диапазона, и уже там где нужно объявить темповый рекорд на основе этой таблицы и успешно с ним работать.

  3. В каждом отдельном случае
    имплантат прочно вживается в
    кость от 3 до 6 месяцев, после чего
    начинается завершающий этап лечения — протезирование на имплантатах.
    Аллотрансплантация (кость берется у подходящего по иммунным
    параметрам донора). Аутотрансплантация (недостающий объем кости
    берется из другого участка).

    Обычно такая потребность возникает у
    тех пациентов, которым зуб удалили давно и за это время объем кости успел значительно уменьшиться.
    Если костная ткань имеет достаточный объем и десна
    в хорошем состоянии, то современная имплантология позволяет устанавливать имплантат сразу после удаления зуба.
    После потери верхних зубов
    костная ткань дна гайморовой пазухи атрофируется иногда до 1 мм кости.

Добавить комментарий