воскресенье, 6 ноября 2011 г.

BIEE 11g: остатки на произвольную дату

Сегодня я расскажу о подходе к отображению остатков в Oracle BI, который я использую в проектах.
Подход позволяет отображать остатки на произвольную дату. Он прост в реализации.
Рассмотрим его на примере репозитория SH (ссылка на скачивание готового примера) и схемы SH базы данных Oracle.
Предполагаем, что нашей задачей является создание оборотно-сальдовой ведомости по истории продаж.

Нам потребуется поддержка (с помощью ETL) таблицы остатков на текущую дату.

create table sales_current_balance as
select t.prod_id,
t.cust_id,
t.channel_id,
t.promo_id,
sum(t.quantity_sold) as quantity_sold,
sum(t.amount_sold) as amount_sold
from sales t
group by t.prod_id, t.cust_id, t.channel_id, t.promo_id

Импортируем в репозиторий OBIEE следующие таблицы:


Создадим альясы:


Создадим связи между физическими объектами-альясами:


Обратите внимание, что таблица _F.02_SALES_CURRENT_BALANCE не содержит внешнего ключа TIMES_ID.
А также таблица _F.03_SALES_DELTA связана комплексно с таблицей календаря.

Далее «протянем» используемые таблицы на логический и презентационный слой.




Заметьте, что для показателей всех логических таблиц фактов установлен тип агрегации – SUM.
А также, что для показателей таблицы _F.02_SALES_CURRENT_BALANCE установлен уровень агрегации по измерению календаря – TOTAL (Это важно. Установка уровня агрегации на Logical Tables Source лишь позволяет BIServer’у выбрать какой источник использовать для конкретного логического запроса, в то время как установка уровня агрегации на конкретном столбце – относится непосредственно к логике обработки).
Ну и самое интересное – это создание отдельной логической таблицы (фиктивное). Которая содержит вычисляемые логические показатели – сальдо на начало и на конец по сумме и по количеству продаж. Функции их формирования говорят сами за себя.
Для AMT_BALANCE_OPENING:
"SH with balances"."_F.02_SALES_CURRENT_BALANCE"."AMOUNT_SOLD" - IFNULL("SH with balances"."_F.03_SALES_DELTA"."AMOUNT_SOLD",0)
Для AMT_BALANCE_CLOSING:
"SH with balances"."_F.04_SALES_BALANCE"."AMT_BALANCE_OPENING" + IFNULL("SH with balances"."_F.01_SALES_TURNOVER"."AMOUNT_SOLD" ,0)

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


В режиме просмотра результатов (PivotTable) видно, что мы очень близки к нашей цели: в несколько кликов мышью сформирован оборотно-сальдовый отчет по суммам продаж в разрезе городов. При желании можно детализировать отчет до клиентов конкретного города. Причем обороты + сальдо будут отображаться корректно уже в разрезе клиентов.




Отлично!
Правда, мы забыли настроить важнейшее измерение – календарь. С ним не все очевидно в используемом подходе (при желании можно проанализировать физические запросы, генерируемые к БД).
Нам потребуется создать (и поддерживать, конечно) отдельные таблицы на каждый уровень иерархии календаря.
Допустим, нам хватит лишь 3 уровней иерархии календаря – год, месяц, день.
Значит нам достаточно создать 2 новые таблицы:

create table times_year as
select *
from times t
where t.time_id = trunc(t.time_id,'Y')

create table times_month as
select *
from times t
where t.time_id = trunc(t.time_id,'MM')


Существующая уже таблица TIMES – это и есть таблица под уровень иерархии «Day».

Импортируем 2 новые таблицы в наш репозиторий. Создадим альясы. Настроим физические связи.


Перенесем новые таблицы как дополнительные Logical Table Source в логическую таблицу календаря - _D.06_TIMES.


Не забываем корректно настроить отображение логических столбцов на LTS.
А также создать нужные 3 уровня иерархии календаря (с корректными ключами).

Теперь можно возвращаться к нашему анализу:




Вот и все!
Теперь наш отчет поддерживает как детализацию по измерениям, так и детализацию по календарю.
Несложно, не правда ли!
Читать дальше...

вторник, 27 сентября 2011 г.

BIEE 11g: writeback + Pivot Table

Предыдущая часть.

Механизм обратной записи – writeback – очень удобный инструмент в арсенале OBIEE. И одним из самых естественных примеров его использования является ввод плановых показателей. Но у OBIEE writeback есть существенный минус – он работает только с представлением данных "Таблица". А зачастую просто необходимо видеть не только форму для ввода планов, но и аналогичные плановые данные за прошлые/будущие периоды, т.е. осуществлять ввод плановых показателей через представление "Таблица среза", где столбцы будут элементами измерения календаря.
На примере данных предыдущей части:



В режиме правки видно, что это действительно PivotTable, а не обходные реализации (как например, представление таблицы среза обычной таблицей с заранее определенным количеством столбцов на используемые элементы измерения календаря).


Создать подобную форму совершенно не сложно.
1. Для начала преобразуем тип столбцов для ввода. Нам нужен символьный тип (character).

cast( ifnull("Plan Salesfacts"."Dummy Input", 0) + ifnull("Plan Salesfacts"."Amount Sold", 0) as character)



2. Теперь (так как тип нужных столбцов символьный) мы можем задать формат данных как HTML-конструкцию, обеспечивающую возможность ввода данных.

@[html]<input class="WBInput" type="text" novalue="false" size="10" onchange="obipswb.Grid.Change(event)" onfocus="obipswb.Grid.Focus(event)" value=@H origvalue=@H>



3. Также сделаем небольшой трюк с XML-представлением отчета, который позволит использовать в представлении PivotTable стандартную функциональность writeback (а имеено – создаст кнопки «Apply»/”Revert”). Для этого на вкладке «Advanced» отчета в XML-представлении пропишем в узле <saw:view xsi:type="saw:pivotTableView" name="pivotTableView!1">
Подузел

<saw:displayFormat>
<saw:formatSpec>
<sawwb:writeBack xmlns:sawwb="com.siebel.analytics.web/writeback/v1" xsi:type="sawwb:tableProperties" enabled="true" toggleTableMode="false" templateName="SetPlanSalesProdCatPivot" useTemplate="true" buttonPos="left"/>
</saw:formatSpec>
</saw:displayFormat>

Особую роль тут играет атрибут templateName – задание его значения позволяет использовать XML-шаблон операций insert/update при обратной записи.


4. До сих пор мы не меняли исходных кодов OBIEE. Но сделать это придется: в файлике
Middleware\user_projects\domains\bifoundation_domain\servers\bi_server1\tmp\_WL_user\analytics_11.1.1\7dezjl\war\res\b_mozilla\views\pivot\wbpivotview.js
Требуется внести изменения в функцию obipswb.Grid.UpdateWriteBackActionForEdge

obipswb.Grid.UpdateWriteBackActionForEdge = function (c, f, h) {
var g = false;
var b = c.getWriteBackRows(f);
//jack carver start
if (f == obips.EdgeDefinition.DATA_EDGE) {
for (var d = 0; d < b.length; d++) {
var a = b[d];
if (!a || a.length == 0) {
continue
}
for (var dd = 0; dd < a.length; dd++) {
var aa = a[dd];
if (!aa) {
continue
}
var e = obipswb.Grid.getRecordValues(c, a, f, aa.getCoordinate().getLayer(), aa.getCoordinate().getSlice());

if (obipswb.Grid.shouldDeleteRow(aa)) {
h.deleteRecord(e)
} else {
h.updateRecord(e)
}
g = true
}
}
return g
}
//jack carver end
for (var d = 0; d < b.length; d++) {
var a = b[d];
if (!a || a.length == 0) {
continue
}
var e = obipswb.Grid.getRecordValues(c, a, f, a[0].getCoordinate().getLayer(), a[0].getCoordinate().getSlice());
if (obipswb.Grid.shouldDeleteRow(a)) {
h.deleteRecord(e)
} else {
h.updateRecord(e)
}
g = true
}
return g
};


5. Последний штрих – создание XML-сообщения, сохраняющего изменения в БД.
У меня это сообщение находится в файле Middleware\instances\instance19\bifoundation\OracleBIPresentationServicesComponent\coreapplication_obips1\msgdb\customMessages\MyWritebackTest.xml

<?xml version="1.0" encoding="utf-8"?>
<WebMessageTables xmlns:sawm="com.siebel.analytics.web/message/v1">
<WebMessageTable lang="en-us" system="WriteBack" table="Messages">
<WebMessage name="SetPlanSalesProdCatPivot">
<XML>
<writeBack connectionPool="WriteBack_Connection_Pool">
<insert>MERGE INTO PLAN_SALES_PROD_CAT dst
USING (SELECT ( select min(t.time_id) from TIMES t where t.calendar_month_desc = '@{c_MONTH_DESC}' ) AS time_id,
( select pc.prod_category_id from PROD_CATEGORIES pc where pc.prod_category = '@{c_PROD_CAT}' ) AS prod_category_id
FROM DUAL) src
ON ( src.time_id = dst.time_id AND src.prod_category_id = dst.prod_category_id )
WHEN MATCHED THEN
UPDATE SET dst.amount_sold = nvl('@{c_AMOUNT_SOLD}',dst.amount_sold)
WHEN NOT MATCHED THEN
INSERT
( time_id, prod_category_id, amount_sold )
VALUES
( src.time_id, src.prod_category_id, '@{c_AMOUNT_SOLD}' ) </insert>
<update>MERGE INTO PLAN_SALES_PROD_CAT dst
USING (SELECT ( select min(t.time_id) from TIMES t where t.calendar_month_desc = '@{c_MONTH_DESC}' ) AS time_id,
( select pc.prod_category_id from PROD_CATEGORIES pc where pc.prod_category = '@{c_PROD_CAT}' ) AS prod_category_id
FROM DUAL) src
ON ( src.time_id = dst.time_id AND src.prod_category_id = dst.prod_category_id )
WHEN MATCHED THEN
UPDATE SET dst.amount_sold = nvl('@{c_AMOUNT_SOLD}',dst.amount_sold)
WHEN NOT MATCHED THEN
INSERT
( time_id, prod_category_id, amount_sold )
VALUES
( src.time_id, src.prod_category_id, '@{c_AMOUNT_SOLD}' ) </update>
</writeBack>
</XML>
</WebMessage>
</WebMessageTable>
</WebMessageTables>


Вот и все. Просто, не правда ли!
Читать дальше...

воскресенье, 12 июня 2011 г.

BIEE 11g: writeback + ввод плановых показателей

Сегодня я расскажу о своем опыте использования механизма обратной записи – writeback – в OracleBI 11g.
Представим, что вам нужно реализовать в OBIEE возможность ввода плановых показателей – сумма и количество проданного товара – еженедельно на неделю вперед. Причем товарный справочник у вас немаленький, где-то нужно вводить планы по каждой конкретной товарной позиции, где-то достаточно задать плановое значение на категорию/подкатегорию товара, а уже это значение разобьется на показатели потоварно (пропорционально стоимости товарной позиции).
Далее я расскажу как добиться подобного на примере схемы SH.

За основу возьмем репозиторий SH под OracleBI 11g.
Создадим 3 таблицы с плановыми показателями – каждая на основании таблицы SH.SALES.
Первая таблица будет содержать плановые показатели в разрезе категорий товара, вторая – в разрезе подкатегорий товара, третья – в разрезе товарных позиций.
Для простоты примера будем считать, что плановые показатели нам нужны лишь в разрезах календаря и товарного справочника.
Создадим таблицу категорий товара и таблицу плановых показателей в разрезе календаря и категорий товара.

create table PROD_CATEGORIES as
select t.prod_category_id, t.prod_category, t.prod_category_desc
from PRODUCTS t
group by t.prod_category_id, t.prod_category, t.prod_category_desc;
alter table PROD_CATEGORIES add primary key (prod_category_id);

create table PLAN_SALES_PROD_CAT
(
prod_category_id NUMBER not null,
time_id DATE not null,
quantity_sold NUMBER(10,2) not null,
amount_sold NUMBER(10,2) not null
);
alter table PLAN_SALES_PROD_CAT
add constraint PLAN_SALES_PROD_CAT_PROD_FK foreign key (PROD_CATEGORY_ID)
references PROD_CATEGORIES (PROD_CATEGORY_ID);
alter table PLAN_SALES_PROD_CAT
add constraint PLAN_SALES_PROD_CAT_TIME_FK foreign key (TIME_ID)
references TIMES (TIME_ID);

create index PLAN_SALES_PROD_CAT_PROD_NIX on PLAN_SALES_PROD_CAT (PROD_CATEGORY_ID);
create index PLAN_SALES_PROD_CAT_TIME_NIX on PLAN_SALES_PROD_CAT (TIME_ID);


Создадим таблицу подкатегорий товара и таблицу плановых показателей в разрезе календаря и подкатегорий товара.

create table PROD_SUBCATEGORIES as
select t.prod_category_id, t.prod_category, t.prod_category_desc,
t.prod_subcategory_id, t.prod_subcategory, t.prod_subcategory_desc
from PRODUCTS t
group by t.prod_category_id, t.prod_category, t.prod_category_desc,
t.prod_subcategory_id, t.prod_subcategory, t.prod_subcategory_desc;
alter table PROD_SUBCATEGORIES add primary key (prod_subcategory_id);

create table PLAN_SALES_PROD_SUBCAT
(
prod_subcategory_id NUMBER not null,
time_id DATE not null,
quantity_sold NUMBER(10,2) not null,
amount_sold NUMBER(10,2) not null
);
alter table PLAN_SALES_PROD_SUBCAT
add constraint PLAN_SALES_PROD_SUBCAT_PROD_FK foreign key (PROD_SUBCATEGORY_ID)
references PROD_SUBCATEGORIES (PROD_SUBCATEGORY_ID);
alter table PLAN_SALES_PROD_SUBCAT
add constraint PLAN_SALES_PROD_SUBCAT_TIME_FK foreign key (TIME_ID)
references TIMES (TIME_ID);

create index PLN_SALES_PROD_SUBCAT_PROD_NIX on PLAN_SALES_PROD_SUBCAT (PROD_SUBCATEGORY_ID);
create index PLN_SALES_PROD_SUBCAT_TIME_NIX on PLAN_SALES_PROD_SUBCAT (TIME_ID);


Создадим таблицу плановых показателей в разрезе календаря и товарных позиций.

create table PLAN_SALES_PROD
(
prod_id NUMBER not null,
time_id DATE not null,
quantity_sold NUMBER(10,2) not null,
amount_sold NUMBER(10,2) not null
);
alter table PLAN_SALES_PROD
add constraint PLAN_SALES_PROD_PROD_FK foreign key (PROD_ID)
references PRODUCTS (PROD_ID);
alter table PLAN_SALES_PROD
add constraint PLAN_SALES_PROD_TIME_FK foreign key (TIME_ID)
references TIMES (TIME_ID);

create index PLN_SALES_PROD_PROD_NIX on PLAN_SALES_PROD (PROD_ID);
create index PLN_SALES_PROD_TIME_NIX on PLAN_SALES_PROD (TIME_ID);


Импортируем эти таблицы в наш репозиторий.

С помощью созданных таблиц мы будем ОТОБРАЖАТЬ в BI плановые показатели. Но чтобы позволить пользователям ЗАДАВАТЬ данные для плановых показателей – создадим еще одну таблицу на физ. слое репозитория

В отличие от таблиц просмотра плановых показателей, которые связаны с таблицам измерений строго через внешние ключи

Таблицу ввода показателей мы свяжем с измерениями через кросс-джойн:


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


Теперь настроим бизнес слой репозитория.
Рассмотрим логическую таблицу Products



Аналогично настроим таблицу фактов – Plan Salesfacts


Для таблицы фактов Input Table зададим свойство ее единственного столбца – Dummy Input – writeable.
Также не забудем задать тип агрегации – сумма – для всех наших столбцов-показателей: как для просмотра, так и для ввода.


Дополним презентационный слой.


И зададим для столбцов, используемых для ввода, настройку доступа.


Напоследок создадим новый экземпляр ConnectionPool’а, который будет применяться только для writeback’а. Назовем его WriteBack_Connection_Pool.
На этом настройка репозитория завершена.

Укажем, что все пользователи могут использовать writeback:
В меню BI – Administration - Manage Privileges - Write Back


Создадим XML шаблон, определяющий insert/update введенных пользователями показателей в таблицы БД. Так как у нас используется кросс-джойн между всеми таблицами измерений и таблицей ввода показателей (по сути – dual-таблица с одной строкой), то на любую комбинацию измерений мы получим строку во writeback-запросе, а это значит, что нам потребуется создать update-инструкцию. Также следует учесть, что данных в настоящей таблице с плановыми показателями может и не быть, поэтому в update-инструкции следует применять оператор MERGE.
На практике мне пришлось указать и insert-конструкцию, думаю это баг.

Итак, создадим в папке
Middleware\instances\instance1\bifoundation\OracleBIPresentationServicesComponent\coreapplication_obips1\msgdb\customMessages
Файл MyWritebackTest.xml со следующим содержимым:

<?xml version="1.0" encoding="utf-8"?>
<WebMessageTables xmlns:sawm="com.siebel.analytics.web/message/v1">
<WebMessageTable lang="en-us" system="WriteBack" table="Messages">
<WebMessage name="SetPlanSalesProdCat">
<XML>
<writeBack connectionPool="WriteBack_Connection_Pool">
<insert>MERGE INTO PLAN_SALES_PROD_CAT dst
USING (SELECT ( select min(t.time_id) from TIMES t where t.calendar_month_desc = '@{c_MONTH_DESC}' ) AS time_id,
( select pc.prod_category_id from PROD_CATEGORIES pc where pc.prod_category = '@{c_PROD_CAT}' ) AS prod_category_id
FROM DUAL) src
ON ( src.time_id = dst.time_id AND src.prod_category_id = dst.prod_category_id )
WHEN MATCHED THEN
UPDATE SET dst.quantity_sold = @{c_QNT_SOLD},
dst.amount_sold = @{c_AMOUNT_SOLD}
WHEN NOT MATCHED THEN
INSERT
( time_id, prod_category_id, quantity_sold, amount_sold )
VALUES
( src.time_id, src.prod_category_id, @{c_QNT_SOLD}, @{c_AMOUNT_SOLD} ) </insert>
<update>MERGE INTO PLAN_SALES_PROD_CAT dst
USING (SELECT ( select min(t.time_id) from TIMES t where t.calendar_month_desc = '@{c_MONTH_DESC}' ) AS time_id,
( select pc.prod_category_id from PROD_CATEGORIES pc where pc.prod_category = '@{c_PROD_CAT}' ) AS prod_category_id
FROM DUAL) src
ON ( src.time_id = dst.time_id AND src.prod_category_id = dst.prod_category_id )
WHEN MATCHED THEN
UPDATE SET dst.quantity_sold = @{c_QNT_SOLD},
dst.amount_sold = @{c_AMOUNT_SOLD}
WHEN NOT MATCHED THEN
INSERT
( time_id, prod_category_id, quantity_sold, amount_sold )
VALUES
( src.time_id, src.prod_category_id, @{c_QNT_SOLD}, @{c_AMOUNT_SOLD} ) </update>
</writeBack>
</XML>
</WebMessage>
<WebMessage name="SetPlanSalesProdSubcat">
<XML>
<writeBack connectionPool="WriteBack_Connection_Pool">
<insert>MERGE INTO PLAN_SALES_PROD_SUBCAT dst
USING (SELECT ( select min(t.time_id) from TIMES t where t.calendar_month_desc = '@{c_MONTH_DESC}' ) AS time_id,
( select pc.prod_subcategory_id from PROD_SUBCATEGORIES pc where pc.prod_subcategory = '@{c_PROD_SUBCAT}' and pc.prod_category = '@{c_PROD_CAT}' ) AS prod_subcategory_id
FROM DUAL) src
ON ( src.time_id = dst.time_id AND src.prod_subcategory_id = dst.prod_subcategory_id )
WHEN MATCHED THEN
UPDATE SET dst.quantity_sold = @{c_QNT_SOLD},
dst.amount_sold = @{c_AMOUNT_SOLD}
WHEN NOT MATCHED THEN
INSERT
( time_id, prod_subcategory_id, quantity_sold, amount_sold )
VALUES
( src.time_id, src.prod_subcategory_id, @{c_QNT_SOLD}, @{c_AMOUNT_SOLD} ) </insert>
<update>MERGE INTO PLAN_SALES_PROD_SUBCAT dst
USING (SELECT ( select min(t.time_id) from TIMES t where t.calendar_month_desc = '@{c_MONTH_DESC}' ) AS time_id,
( select pc.prod_subcategory_id from PROD_SUBCATEGORIES pc where pc.prod_subcategory = '@{c_PROD_SUBCAT}' and pc.prod_category = '@{c_PROD_CAT}' ) AS prod_subcategory_id
FROM DUAL) src
ON ( src.time_id = dst.time_id AND src.prod_subcategory_id = dst.prod_subcategory_id )
WHEN MATCHED THEN
UPDATE SET dst.quantity_sold = @{c_QNT_SOLD},
dst.amount_sold = @{c_AMOUNT_SOLD}
WHEN NOT MATCHED THEN
INSERT
( time_id, prod_subcategory_id, quantity_sold, amount_sold )
VALUES
( src.time_id, src.prod_subcategory_id, @{c_QNT_SOLD}, @{c_AMOUNT_SOLD} ) </update>
</writeBack>
</XML>
</WebMessage>
<WebMessage name="SetPlanSalesProdDtl">
<XML>
<writeBack connectionPool="WriteBack_Connection_Pool">
<insert>MERGE INTO PLAN_SALES_PROD dst
USING (SELECT ( select min(t.time_id) from TIMES t where t.calendar_month_desc = '@{c_MONTH_DESC}' ) AS time_id,
( select p.prod_id from PRODUCTS p where p.prod_name = '@{c_PROD_NAME}' and p.prod_subcategory = '@{c_PROD_SUBCAT}' and p.prod_category = '@{c_PROD_CAT}' ) AS prod_id
FROM DUAL) src
ON ( src.time_id = dst.time_id AND src.prod_id = dst.prod_id )
WHEN MATCHED THEN
UPDATE SET dst.quantity_sold = @{c_QNT_SOLD},
dst.amount_sold = @{c_AMOUNT_SOLD}
WHEN NOT MATCHED THEN
INSERT
( time_id, prod_id, quantity_sold, amount_sold )
VALUES
( src.time_id, src.prod_id, @{c_QNT_SOLD}, @{c_AMOUNT_SOLD} ) </insert>
<update>MERGE INTO PLAN_SALES_PROD dst
USING (SELECT ( select min(t.time_id) from TIMES t where t.calendar_month_desc = '@{c_MONTH_DESC}' ) AS time_id,
( select p.prod_id from PRODUCTS p where p.prod_name = '@{c_PROD_NAME}' and p.prod_subcategory = '@{c_PROD_SUBCAT}' and p.prod_category = '@{c_PROD_CAT}' ) AS prod_id
FROM DUAL) src
ON ( src.time_id = dst.time_id AND src.prod_id = dst.prod_id )
WHEN MATCHED THEN
UPDATE SET dst.quantity_sold = @{c_QNT_SOLD},
dst.amount_sold = @{c_AMOUNT_SOLD}
WHEN NOT MATCHED THEN
INSERT
( time_id, prod_id, quantity_sold, amount_sold )
VALUES
( src.time_id, src.prod_id, @{c_QNT_SOLD}, @{c_AMOUNT_SOLD} ) </update>
</writeBack>
</XML>
</WebMessage>
</WebMessageTable>
</WebMessageTables>


Рестартуем Presentation Server.

Далее создадим ансвер, который позволит вводить плановые показатели в разрезе товарных позиций:
Включим в ансвер столбцы месяца календаря, категории, подкатегории и наименования товара, а также столбцы плановых показателей (сначала следует вывести столбец Dummy Input, поскольку именно у него задано свойство Writeable на логическом уровне репозитория; указать в свойствах столбца в ансвере, что он является writeable, а затем следует изменить его формулу – ifnull("Plan Salesfacts"."Dummy Input", 0)+"Plan Salesfacts"."Amount Sold" – тем самым мы добъемся одновременного отображения существующих данных в плановых таблицах с возможностью получить в ансвере набор всех возможных пересечений таблиц-измерений ).
Добавим фильтры по месяцу, по категории и подкатегории.


Перейдем на вкладку результатов ансвера, и в режиме редактирования табличного представления зададим свойства перезаписи – укажем, что данная таблица используется для writeback, и при этом применяется XML шаблон с именем SetPlanSalesProdDtl.


Наконец перейдем на вкладку «Advanced». Где зададим свойства columnID для каждого столбца ансвера в соответствии с нашим XML шаблоном обратной записи.

Не забудьте нажать кнопку «Apply XML» после внесения изменений.
У меня получился вот такой XML-код данного ансвера:

<saw:report xmlns:saw="com.siebel.analytics.web/report/v1.1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlVersion="201008230" xmlns:sawx="com.siebel.analytics.web/expression/v1.1">
<saw:criteria xsi:type="saw:simpleCriteria" subjectArea=""SH"" withinHierarchy="true">
<saw:columns>
<saw:column xsi:type="saw:regularColumn" columnID="c_MONTH_DESC">
<saw:columnFormula>
<sawx:expr xsi:type="sawx:sqlExpression">"Calendar"."Calendar Month Desc"</sawx:expr></saw:columnFormula>
<saw:displayFormat>
<saw:formatSpec suppress="suppress" wrapText="true" interaction="none"/></saw:displayFormat>
<saw:columnHeading>
<saw:displayFormat>
<saw:formatSpec interaction="none"/></saw:displayFormat></saw:columnHeading></saw:column>
<saw:column xsi:type="saw:regularColumn" columnID="c_PROD_CAT">
<saw:columnFormula>
<sawx:expr xsi:type="sawx:sqlExpression">"Products"."Prod Category"</sawx:expr></saw:columnFormula></saw:column>
<saw:column xsi:type="saw:regularColumn" columnID="c_PROD_SUBCAT">
<saw:columnFormula>
<sawx:expr xsi:type="sawx:sqlExpression">"Products"."Prod Subcategory"</sawx:expr></saw:columnFormula></saw:column>
<saw:column xsi:type="saw:regularColumn" columnID="c_PROD_NAME">
<saw:columnFormula>
<sawx:expr xsi:type="sawx:sqlExpression">"Products"."Prod Name"</sawx:expr></saw:columnFormula></saw:column>
<saw:column xsi:type="saw:regularColumn" columnID="c_AMOUNT_SOLD">
<saw:columnFormula>
<sawx:expr xsi:type="sawx:sqlExpression">ifnull("Plan Salesfacts"."Dummy Input", 0) + "Plan Salesfacts"."Amount Sold"</sawx:expr></saw:columnFormula>
<saw:displayFormat>
<saw:formatSpec suppress="repeat" wrapText="true">
<sawwb:writeBack xmlns:sawwb="com.siebel.analytics.web/writeback/v1" required="false" xsi:type="sawwb:editBox" inputSize="10" inputHeight="1"/></saw:formatSpec></saw:displayFormat>
<saw:tableHeading>
<saw:caption fmt="text">
<saw:text>Plan Salesfacts</saw:text></saw:caption></saw:tableHeading>
<saw:columnHeading>
<saw:displayFormat>
<saw:formatSpec/></saw:displayFormat>
<saw:caption fmt="text">
<saw:text>Amount Sold</saw:text></saw:caption></saw:columnHeading></saw:column>
<saw:column xsi:type="saw:regularColumn" columnID="c_QNT_SOLD">
<saw:columnFormula>
<sawx:expr xsi:type="sawx:sqlExpression">ifnull("Plan Salesfacts"."Dummy Input", 0) + "Plan Salesfacts"."Qnt Sold"</sawx:expr></saw:columnFormula>
<saw:displayFormat>
<saw:formatSpec suppress="repeat" wrapText="true">
<sawwb:writeBack xmlns:sawwb="com.siebel.analytics.web/writeback/v1" required="false" xsi:type="sawwb:editBox" inputSize="10" inputHeight="1"/></saw:formatSpec></saw:displayFormat>
<saw:tableHeading>
<saw:caption fmt="text">
<saw:text>Plan Salesfacts</saw:text></saw:caption></saw:tableHeading>
<saw:columnHeading>
<saw:displayFormat>
<saw:formatSpec/></saw:displayFormat>
<saw:caption fmt="text">
<saw:text>Qnt Sold</saw:text></saw:caption></saw:columnHeading></saw:column></saw:columns>
<saw:filter>
<sawx:expr xsi:type="sawx:logical" op="and">
<sawx:expr xsi:type="sawx:comparison" op="equal">
<sawx:expr xsi:type="sawx:sqlExpression">"Calendar"."Calendar Month Desc"</sawx:expr>
<sawx:expr xsi:type="xsd:string">2002-12</sawx:expr></sawx:expr>
<sawx:expr xsi:type="sawx:special" op="prompted">
<sawx:expr xsi:type="sawx:sqlExpression">"Products"."Prod Category"</sawx:expr></sawx:expr>
<sawx:expr xsi:type="sawx:special" op="prompted">
<sawx:expr xsi:type="sawx:sqlExpression">"Products"."Prod Subcategory"</sawx:expr></sawx:expr></sawx:expr></saw:filter></saw:criteria>
<saw:views currentView="2">
<saw:view xsi:type="saw:compoundView" name="compoundView!1">
<saw:cvTable>
<saw:cvRow>
<saw:cvCell viewName="titleView!1">
<saw:displayFormat>
<saw:formatSpec/></saw:displayFormat></saw:cvCell></saw:cvRow>
<saw:cvRow>
<saw:cvCell viewName="tableView!1">
<saw:displayFormat>
<saw:formatSpec/></saw:displayFormat></saw:cvCell></saw:cvRow></saw:cvTable></saw:view>
<saw:view xsi:type="saw:titleView" name="titleView!1"/>
<saw:view xsi:type="saw:tableView" name="tableView!1">
<saw:displayFormat>
<saw:formatSpec>
<sawwb:writeBack xmlns:sawwb="com.siebel.analytics.web/writeback/v1" xsi:type="sawwb:tableProperties" enabled="true" toggleTableMode="true" templateName="SetPlanSalesProdDtl" useTemplate="true" buttonPos="right"/></saw:formatSpec></saw:displayFormat>
<saw:edges>
<saw:edge axis="page" showColumnHeader="true"/>
<saw:edge axis="section"/>
<saw:edge axis="row" showColumnHeader="true">
<saw:edgeLayers>
<saw:edgeLayer type="column" columnID="c_MONTH_DESC"/>
<saw:edgeLayer type="column" columnID="c_PROD_CAT"/>
<saw:edgeLayer type="column" columnID="c_PROD_SUBCAT"/>
<saw:edgeLayer type="column" columnID="c_PROD_NAME"/>
<saw:edgeLayer type="column" columnID="c_AMOUNT_SOLD"/>
<saw:edgeLayer type="column" columnID="c_QNT_SOLD"/></saw:edgeLayers></saw:edge>
<saw:edge axis="column"/></saw:edges></saw:view></saw:views></saw:report>


Опробуем его.



Похожим образом создаем ансверы для ввода плановых показателей в разрезе подкатегорий товара и в разрезе категорий товара. У каждого из них указываем свой XML шаблон обратной записи, а также переопределяем на столбцах иерархии товара свойства Interaction: вместо свойство Drill (по умолчанию) задаем переход к сохраненному содержимому BI. Таким образом эмулируем дрилл-даун с возможностью writeback на каждом его уровне детализации.



Надеюсь, эта статья сэкономит вам не один час времени! Также вы можете скачать созданные в рамках этого примера RPD-файл и веб-каталог (созданные в примере ансверы находятся в папке /Shared Folders/Writeback Sample)

P.S. Ввиду очевидности реализации не стал рассматривать здесь механизм пропорционального разбиения суммы планового показателя верхнего уровня: например, вы задали плановую сумму продаж по категории товара и хотите, чтобы эта сумма автоматически распределилась на все подкатегории и затем на все входящие в нее товары пропорционально цене. Реализовать подобное можно с помощью триггеров на таблицы PLAN… и небольшого количества PL/SQL кода.
Читать дальше...

пятница, 10 июня 2011 г.

BIP: XSLT шаблон таблицы среза

Пример реализации в XSL шаблона разметки, позволяющего отобразить таблицу среза (pivot table)

Не стал подробно расписывать все конструкции, использованные в шаблоне. Думаю, приведенных в них комментариев будет достаточно.

Пример состоит их 2 файлов: XML и XSL.
XML файл данных

<?xml version="1.0" encoding="UTF-8"?>
<ROWSET>
<ROW>
<CALC_PERIOD>2011-01-01</CALC_PERIOD>
<CALC_PERIOD_MONTH>Январь</CALC_PERIOD_MONTH>
<PERIOD>2011-01-01</PERIOD>
<PERIOD_MONTH>Январь</PERIOD_MONTH>
<LIAB_VOLUME>1000</LIAB_VOLUME>
</ROW>
<ROW>
<CALC_PERIOD>2011-02-01</CALC_PERIOD>
<CALC_PERIOD_MONTH>Февраль</CALC_PERIOD_MONTH>
<PERIOD>2011-01-01</PERIOD>
<PERIOD_MONTH>Январь</PERIOD_MONTH>
<LIAB_VOLUME>2000</LIAB_VOLUME>
</ROW>
<ROW>
<CALC_PERIOD>2011-02-01</CALC_PERIOD>
<CALC_PERIOD_MONTH>Февраль</CALC_PERIOD_MONTH>
<PERIOD>2011-02-01</PERIOD>
<PERIOD_MONTH>Февраль</PERIOD_MONTH>
<LIAB_VOLUME>2000</LIAB_VOLUME>
</ROW>
<ROW>
<CALC_PERIOD>2011-03-01</CALC_PERIOD>
<CALC_PERIOD_MONTH>Март</CALC_PERIOD_MONTH>
<PERIOD>2011-01-01</PERIOD>
<PERIOD_MONTH>Январь</PERIOD_MONTH>
<LIAB_VOLUME>2000</LIAB_VOLUME>
</ROW>
<ROW>
<CALC_PERIOD>2011-03-01</CALC_PERIOD>
<CALC_PERIOD_MONTH>Март</CALC_PERIOD_MONTH>
<PERIOD>2011-02-01</PERIOD>
<PERIOD_MONTH>Февраль</PERIOD_MONTH>
<LIAB_VOLUME>2000</LIAB_VOLUME>
</ROW>
<ROW>
<CALC_PERIOD>2011-03-01</CALC_PERIOD>
<CALC_PERIOD_MONTH>Март</CALC_PERIOD_MONTH>
<PERIOD>2011-03-01</PERIOD>
<PERIOD_MONTH>Март</PERIOD_MONTH>
<LIAB_VOLUME>2000</LIAB_VOLUME>
</ROW>
<ROW>
<CALC_PERIOD>2011-04-01</CALC_PERIOD>
<CALC_PERIOD_MONTH>Апрель</CALC_PERIOD_MONTH>
<PERIOD>2011-01-01</PERIOD>
<PERIOD_MONTH>Январь</PERIOD_MONTH>
<LIAB_VOLUME>4000</LIAB_VOLUME>
</ROW>
<ROW>
<CALC_PERIOD>2011-04-01</CALC_PERIOD>
<CALC_PERIOD_MONTH>Апрель</CALC_PERIOD_MONTH>
<PERIOD>2011-02-01</PERIOD>
<PERIOD_MONTH>Февраль</PERIOD_MONTH>
<LIAB_VOLUME>1000</LIAB_VOLUME>
</ROW>
<ROW>
<CALC_PERIOD>2011-04-01</CALC_PERIOD>
<CALC_PERIOD_MONTH>Апрель</CALC_PERIOD_MONTH>
<PERIOD>2011-03-01</PERIOD>
<PERIOD_MONTH>Март</PERIOD_MONTH>
<LIAB_VOLUME>3000</LIAB_VOLUME>
</ROW>
<ROW>
<CALC_PERIOD>2011-04-01</CALC_PERIOD>
<CALC_PERIOD_MONTH>Апрель</CALC_PERIOD_MONTH>
<PERIOD>2011-04-01</PERIOD>
<PERIOD_MONTH>Апрель</PERIOD_MONTH>
<LIAB_VOLUME>5000</LIAB_VOLUME>
</ROW>
</ROWSET>



XSL шаблон разметки

<?xml version="1.0"?>
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:fo="http://www.w3.org/1999/XSL/Format" xmlns="urn:schemas-microsoft-com:office:spreadsheet" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:office:excel" xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet" xmlns:ora="http://www.oracle.com/XSL/Transform/java/" xmlns:xdofo="http://xmlns.oracle.com/oxp/fo/extensions" xmlns:xdoxslt="http://www.oracle.com/XSL/Transform/java/oracle.apps.xdo.template.rtf.XSLTFunctions" xmlns:xdoxliff="urn:oasis:names:tc:xliff:document:1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:msxsl="urn:schemas-microsoft-com:xslt">
<xsl:decimal-format name="myFormat" decimal-separator="." grouping-separator=" "/>
<!--ключ-индекс по расчетным периодам -->
<xsl:key name="key_col_1" match="CALC_PERIOD" use="."/>
<!--ключ-индекс по учетным периодам -->
<xsl:key name="key_row_1" match="PERIOD" use="."/>
<!--главная функция-шаблон -->
<xsl:template match="/">
<xsl:processing-instruction name="mso-application">
<xsl:text>progid="Excel.Sheet"</xsl:text>
</xsl:processing-instruction>
<Workbook xmlns="urn:schemas-microsoft-com:office:spreadsheet" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:office:excel" xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet" xmlns:html="http://www.w3.org/TR/REC-html40">
<DocumentProperties xmlns="urn:schemas-microsoft-com:office:office">
<Author/>
<LastAuthor/>
<Created/>
<Company/>
<Version>11.9999</Version>
</DocumentProperties>
<ExcelWorkbook xmlns="urn:schemas-microsoft-com:office:excel">
<WindowHeight>11895</WindowHeight>
<WindowWidth>15180</WindowWidth>
<WindowTopX>480</WindowTopX>
<WindowTopY>135</WindowTopY>
<ProtectStructure>False</ProtectStructure>
<ProtectWindows>False</ProtectWindows>
</ExcelWorkbook>
<Styles>
<Style ss:ID="Default" ss:Name="Normal">
<Alignment ss:Vertical="Bottom"/>
<Borders/>
<Font ss:FontName="Arial Cyr" x:CharSet="204"/>
<Interior/>
<NumberFormat/>
<Protection/>
</Style>
<Style ss:ID="s1">
<Alignment ss:Vertical="Bottom" ss:Horizontal="Center"/>
<Borders>
<Border ss:Position="Bottom" ss:LineStyle="Continuous" ss:Weight="1"/>
<Border ss:Position="Left" ss:LineStyle="Continuous" ss:Weight="1"/>
<Border ss:Position="Right" ss:LineStyle="Continuous" ss:Weight="1"/>
<Border ss:Position="Top" ss:LineStyle="Continuous" ss:Weight="1"/>
</Borders>
<Font ss:FontName="Arial Cyr" x:CharSet="204"/>
<Interior/>
<NumberFormat/>
<Protection/>
</Style>
<Style ss:ID="s2">
<Alignment ss:Vertical="Bottom" ss:WrapText="1"/>
<Borders>
<Border ss:Position="Bottom" ss:LineStyle="Continuous" ss:Weight="1"/>
<Border ss:Position="Left" ss:LineStyle="Continuous" ss:Weight="2"/>
<Border ss:Position="Right" ss:LineStyle="Continuous" ss:Weight="1"/>
</Borders>
<Font ss:FontName="Times New Roman" x:CharSet="204" x:Family="Roman" ss:Color="#000000"/>
<Interior ss:Color="#FFFFFF" ss:Pattern="Solid"/>
</Style>
<Style ss:ID="s3">
<Alignment ss:Horizontal="Center" ss:Vertical="Center" ss:WrapText="1"/>
<Borders>
<Border ss:Position="Bottom" ss:LineStyle="Continuous" ss:Weight="2"/>
<Border ss:Position="Left" ss:LineStyle="Continuous" ss:Weight="2"/>
<Border ss:Position="Right" ss:LineStyle="Continuous" ss:Weight="2"/>
<Border ss:Position="Top" ss:LineStyle="Continuous" ss:Weight="2"/>
</Borders>
<Font ss:FontName="Times New Roman" x:CharSet="204" x:Family="Roman" ss:Bold="1"/>
<Interior ss:Color="#CCCCCC" ss:Pattern="Solid"/>
</Style>
<Style ss:ID="s4">
<Alignment ss:Horizontal="Right" ss:Vertical="Center" ss:WrapText="1"/>
<Borders>
<Border ss:Position="Bottom" ss:LineStyle="Continuous" ss:Weight="1"/>
<Border ss:Position="Left" ss:LineStyle="Continuous" ss:Weight="1"/>
<Border ss:Position="Right" ss:LineStyle="Continuous" ss:Weight="1"/>
<Border ss:Position="Top" ss:LineStyle="Continuous" ss:Weight="1"/>
</Borders>
<Font ss:FontName="Times New Roman" x:CharSet="204" x:Family="Roman" ss:Bold="0"/>
<NumberFormat ss:Format="#,##0.000"/>
</Style>
<Style ss:ID="s41">
<Alignment ss:Horizontal="Right" ss:Vertical="Center" ss:WrapText="1"/>
<Borders>
<Border ss:Position="Bottom" ss:LineStyle="Continuous" ss:Weight="1"/>
<Border ss:Position="Left" ss:LineStyle="Continuous" ss:Weight="1"/>
<Border ss:Position="Right" ss:LineStyle="Continuous" ss:Weight="1"/>
<Border ss:Position="Top" ss:LineStyle="Continuous" ss:Weight="1"/>
</Borders>
<Font ss:FontName="Times New Roman" x:CharSet="204" x:Family="Roman" ss:Bold="0"/>
<Interior ss:Color="#CCCCCC" ss:Pattern="Solid"/>
<NumberFormat ss:Format="#,##0.000"/>
</Style>
<Style ss:ID="s5">
<Alignment ss:Horizontal="Right" ss:Vertical="Center" ss:WrapText="1"/>
<Borders>
<Border ss:Position="Bottom" ss:LineStyle="Continuous" ss:Weight="1"/>
<Border ss:Position="Left" ss:LineStyle="Continuous" ss:Weight="1"/>
<Border ss:Position="Right" ss:LineStyle="Continuous" ss:Weight="2"/>
<Border ss:Position="Top" ss:LineStyle="Continuous" ss:Weight="1"/>
</Borders>
<Font ss:FontName="Times New Roman" x:CharSet="204" x:Family="Roman" ss:Bold="1"/>
<Interior ss:Color="#CCCCCC" ss:Pattern="Solid"/>
</Style>
<Style ss:ID="s6">
<Alignment ss:Horizontal="Right" ss:Vertical="Center" ss:WrapText="1"/>
<Borders>
<Border ss:Position="Left" ss:LineStyle="Continuous" ss:Weight="2"/>
</Borders>
<Font ss:FontName="Times New Roman" x:CharSet="204" x:Family="Roman" ss:Bold="0"/>
</Style>
<Style ss:ID="s7">
<Alignment ss:Horizontal="Right" ss:Vertical="Center" ss:WrapText="1"/>
<Borders>
<Border ss:Position="Top" ss:LineStyle="Continuous" ss:Weight="2"/>
</Borders>
<Font ss:FontName="Times New Roman" x:CharSet="204" x:Family="Roman" ss:Bold="0"/>
</Style>
</Styles>
<Worksheet ss:Name="Sheet 1">
<!--матрица с данными -->
<xsl:variable name="rowset" select="/ROWSET/ROW"/>
<!--список расчетных периодов -->
<xsl:variable name="col_1" select="$rowset/CALC_PERIOD"/>
<!--список учетных периодов -->
<xsl:variable name="row_1" select="$rowset/PERIOD"/>
<!--кол-во distinct-элементов в списках расчетных и учетных периодов -->
<xsl:variable name="col_1_cnt" select="count($col_1[generate-id(.)=generate-id(key('key_col_1',.)[1])])"/>
<xsl:variable name="row_1_cnt" select="count($row_1[generate-id(.)=generate-id(key('key_row_1',.)[1])])"/>
<Table ss:DefaultColumnWidth="75">
<!--заголовочная строка таблицы: Период | Расчетный период , где вторая ячейка объединяет все ячейки доступных расчетных периодов -->
<Row>
<Cell ss:StyleID="s3">
<Data ss:Type="String">Период</Data>
</Cell>
<Cell ss:StyleID="s3" ss:MergeAcross="{($col_1_cnt)-1}">
<Data ss:Type="String">Расчетный период</Data>
</Cell>
</Row>
<!--выводим заголовочную строку с ячейками по каждому расчетному периоду
==================================
| Период | Расчетный период |
==================================
| | Январь | Февраль | Март |
==================================
-->
<!--если есть хоть один расчетный период - столбец кросс-таблицы, то ... -->
<xsl:if test="count($col_1[generate-id(.)=generate-id(key('key_col_1',.)[1])])!=0">
<!--в цикле по отсортированному списку (по дате в маске YYYY-MM-DD) расчетных периодов находим для каждого периода его описание - название месяца -->
<Row>
<xsl:for-each select="$col_1[generate-id(.)=generate-id(key('key_col_1',.)[1])]">
<xsl:sort select="substring-before(.,'-')" data-type="number" order="ascending"/>
<xsl:sort select="substring-before(substring-after(.,'-'),'-')" data-type="number" order="ascending"/>
<xsl:variable name="cur" select="."/>
<Cell ss:StyleID="s3" ss:Index="{1+position()}">
<Data ss:Type="String">
<xsl:value-of select="$rowset[CALC_PERIOD = $cur][1]/CALC_PERIOD_MONTH"/>
</Data>
</Cell>
</xsl:for-each>
</Row>
</xsl:if>

<!--в distinct-цикле по учетным периодам ... -->
<xsl:for-each select="$row_1[generate-id(.)=generate-id(key('key_row_1',.)[1])]">
<!--сортируем набор периодов (по дате в маске YYYY-MM-DD) -->
<xsl:sort select="substring-before(.,'-')" data-type="number" order="ascending"/>
<xsl:sort select="substring-before(substring-after(.,'-'),'-')" data-type="number" order="ascending"/>
<xsl:variable name="cur" select="."/>
<Row>
<!--выводим описание учетного периода - название месяца -->
<Cell ss:StyleID="s5">
<Data ss:Type="String">
<xsl:value-of select="$rowset[PERIOD = $cur][1]/PERIOD_MONTH"/>
</Data>
</Cell>
<xsl:variable name="cr" select="."/>
<xsl:variable name="CrPs" select="position()"/>
<!--в отсортированном distinct-цикле по расчетным периодам -->
<xsl:for-each select="$col_1[generate-id(.)=generate-id(key('key_col_1',.)[1])]">
<xsl:sort select="substring-before(.,'-')" data-type="number" order="ascending"/>
<xsl:sort select="substring-before(substring-after(.,'-'),'-')" data-type="number" order="ascending"/>
<!--проверяем на индексы i/j ячейки матрицы - если равны, то закрашиваем ячейку цветом -->
<xsl:variable name="mn" select="."/>
<xsl:variable name="CrStl">
<xsl:choose>
<xsl:when test="$CrPs=position()">
<xsl:value-of select="'s41'"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="'s4'"/>
</xsl:otherwise>
</xsl:choose>
</xsl:variable>
<!--выводим значение суммы в текущем разрезе учетного и расчетного периода -->
<Cell ss:StyleID="{$CrStl}">
<Data ss:Type="Number">
<xsl:value-of select="sum($rowset[PERIOD = $cr and CALC_PERIOD = $mn ]/LIAB_VOLUME)"/>
</Data>
</Cell>
</xsl:for-each>
<Cell ss:StyleID="s6"/>
</Row>
</xsl:for-each>
<!--выводим строку, которая верхней bold-границей визуально закрывает кросс-таблицу -->
<Row>
<Cell ss:StyleID="s7"/>
<xsl:for-each select="$col_1[generate-id(.)=generate-id(key('key_col_1',.)[1])]">
<Cell ss:StyleID="s7"/>
</xsl:for-each>
</Row>
</Table>
<WorksheetOptions xmlns="urn:schemas-microsoft-com:office:excel">
<PageSetup>
<PageMargins x:Bottom="0.984251969" x:Left="0.78740157499999996" x:Right="0.78740157499999996" x:Top="0.984251969"/>
</PageSetup>
<Selected/>
<ProtectObjects>False</ProtectObjects>
<ProtectScenarios>False</ProtectScenarios>
<FreezePanes/>
<FrozenNoSplit/>
<SplitHorizontal>2</SplitHorizontal>
<SplitVertical>1</SplitVertical>
<!--закрепляем верхние строки -->
<TopRowBottomPane>2</TopRowBottomPane>
<LeftColumnRightPane>1</LeftColumnRightPane>
<ActivePane>2</ActivePane>
</WorksheetOptions>
</Worksheet>
</Workbook>
</xsl:template>
</xsl:stylesheet>


Использование подобной пары - XML + XSL - позволяет получить вот такую симпатичную таблицу среза.

Читать дальше...

BIP: XSLT шаблоны

Oracle BIPublisher позволяет генерировать отчеты в формате Excel разными способами:
1) RTF шаблон разметки -> XSL-FO шаблон + XML данных -> FO шаблон -> MHT/HTML файл, который открывается в Excel

2) Excel шаблон -> XSL шаблон + XML данных -> XML с данными и правилами их отображения на конкретный Excel шаблон -> native Excel

3) XSL-шаблон -> XSL шаблон + XML данных -> XML с данными и с учетом схемы, понимаемой Excel – "Таблица XML"

Сегодня я опишу процесс создания 3-го вида шаблонов.

В качестве примера я использую отчет "Quarterly Income Statement" из стандартной поставки BIPublisher 10.1.3.4.1.
Сформирую отчет в формате Excel используя готовый RTF шаблон.


Внешний вид отчета оставляет желать лучшего. Немного доработаю его (Вообще, уверен, что чаще всего, когда перед вами стоит задача создать отчет в формате Excel, пример вам уже предоставлен, со всеми "рюшечками и бантиками" - закреплениями областей, выделениями цветом, автофильтрами, с условными форматированиями, с применением всевозможных формул и т.д.).


Измененный отчет я сохраню в формате "Таблица XML".


Теперь полученный файл beauty_template.xml я переименую в beauty_template_from.xsl.
То есть создам исходную копию, а также определю расширением файла, что он действительно является XSL шаблоном.

Далее я покажу как из файла beauty_template_from.xsl создать beauty_template_to.xsl (все файлы, используемые в этом примере, приведены в конце поста).
Сразу оговорюсь, что приведенный метод крайне упрощен и направлен лишь на то, чтобы максимально быстро начать разрабатывать шаблоны данного типа. Для оптимизации создаваемых шаблонов да и для более глубокого понимания принципов их действия, настоятельно советую почитать какой-либо учебник по языкам XSLT и XPath.

Открываем файл beauty_template_from.xsl в своем любимом XML редакторе (я вот обожаю Altova XMLSpy – считайте это рекламой ;) ), сохраняем его как beauty_template_to.xsl и начинаем править.

Удаляем первые 2 строки:

<?xml version="1.0"?>
<?mso-application progid="Excel.Sheet"?>

Остается узел

<Workbook xmlns="urn:schemas-microsoft-com:office:spreadsheet"
xmlns:o="urn:schemas-microsoft-com:office:office"
xmlns:x="urn:schemas-microsoft-com:office:excel"
xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet"
xmlns:html="http://www.w3.org/TR/REC-html40">
….
….
….
</Workbook>

Помещаем ПЕРЕД узлом Workbook следующий код:

<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:fo="http://www.w3.org/1999/XSL/Format" xmlns="urn:schemas-microsoft-com:office:spreadsheet" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:office:excel" xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet" xmlns:ora="http://www.oracle.com/XSL/Transform/java/" xmlns:xdofo="http://xmlns.oracle.com/oxp/fo/extensions" xmlns:xdoxslt="http://www.oracle.com/XSL/Transform/java/oracle.apps.xdo.template.rtf.XSLTFunctions" xmlns:xdoxliff="urn:oasis:names:tc:xliff:document:1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:msxsl="urn:schemas-microsoft-com:xslt">

<xsl:template match="/">
<xsl:processing-instruction name="mso-application">
<xsl:text>progid="Excel.Sheet"</xsl:text>
</xsl:processing-instruction>

И помещаем ПОСЛЕ узла Workbook код:

</xsl:template>
</xsl:stylesheet>

Этим мы добились:
1) Наш файл шаблона стал полноценным XSLT-документом (за счет корневого узла "xsl:stylesheet"; кстати, пусть версия 2.0 вас не обманывает – далеко не все конструкции спецификации XSL 2.0 поддерживаются используемым в BIPublisher XSL-трансформатором)
2) Создана "точка входа" для работы XSL-трансформатора – базовая функция "/".
3) Получившийся после XSL-трансформации документ будет содержать в начале инструкцию <?mso-application progid="Excel.Sheet"?>, что позволит автоматически открывать XML файл в MS Excel.

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

Начнем править основной узел нашего шаблона – Workbook.
Оставьте без изменений узлы DocumentProperties, ExcelWorkbook, Styles

А вот узел Worksheet (или узлы, если у вас несколько Листов документа) рассмотрим детальнее.

Узел Names содержит список все именованных диапазонов в рамках текущего листа. Так как мы добавили автофильтр, то можем видеть именованный диапазон _FilterDatabase. Оставим его без изменений.

Узел Table содержит непосредственно описание табличной части листа. Важно удалить атрибут ss:ExpandedRowCount данного узла – иначе впоследствии, если заданное атрибутом значение будет меньше реального кол-ва строк отчета, Excel не позволит открыть сформированный отчет.
Далее, внутри таблицы видим множество узлов Column и Row. Column оставляем без изменения (пока не рассматриваем случай кросс-таблицы с динамически формируемым кол-вом столбцов).
А вот часть узлов Row мы будем удалять:
Определите с какого узла Row начинается вывод непосредственно данных (то, что мы будем извлекать из XML-файла, формируемого BIPublisher’ом). Оставьте только один такой узел, остальные удаляйте.
Добавьте в оставшийся узел Row атрибут ss:AutoFitHeight="1" , это задаст автовыравнивание строк по высоте.
Также удалите из всех подузлов Cell строки Row с данными вхождение элемента <NamedCell ss:Name="_FilterDatabase"/> - он определяет именованный диапазон автофильтра, в нашем примере его лучше убрать для экономии объема формируемого отчета (Но оставьте этот элемент для узла Row заголовочной строки).

Узел Table теперь должен выглядеть так:

<Table ss:ExpandedColumnCount="5" x:FullColumns="1" x:FullRows="1">
<Column ss:Width="148.5"/>
<Column ss:AutoFitWidth="0" ss:Width="86.25"/>
<Column ss:AutoFitWidth="0" ss:Width="83.25"/>
<Column ss:AutoFitWidth="0" ss:Width="84"/>
<Column ss:AutoFitWidth="0" ss:Width="80.25"/>
<Row ss:Height="36">
<Cell ss:StyleID="s60"><Data ss:Type="String">Quarterly Income Statement</Data></Cell>
<Cell ss:StyleID="s60"/>
<Cell ss:StyleID="s61"><Data ss:Type="String">Date:27-NOV-2007 22:48:33</Data></Cell>
<Cell ss:StyleID="s62"/>
<Cell ss:StyleID="s62"/>
</Row>
<Row ss:AutoFitHeight="0" ss:Height="14.625">
<Cell ss:StyleID="s63"><Data ss:Type="String">Vision Operations (USA)</Data></Cell>
<Cell ss:StyleID="s63"/>
<Cell ss:StyleID="s61"><Data ss:Type="String">Page:1 </Data></Cell>
<Cell ss:StyleID="s62"/>
<Cell ss:StyleID="s62"/>
</Row>
<Row ss:AutoFitHeight="0" ss:Height="14.625">
<Cell ss:StyleID="s63"><Data ss:Type="String">Current Period: Nov-07</Data></Cell>
<Cell ss:StyleID="s63"/>
<Cell ss:StyleID="s61"/>
<Cell ss:StyleID="s62"/>
<Cell ss:StyleID="s62"/>
</Row>
<Row ss:AutoFitHeight="0" ss:Height="14.625">
<Cell ss:StyleID="s63"/>
<Cell ss:StyleID="s63"/>
<Cell ss:StyleID="s59"/>
<Cell ss:StyleID="s62"/>
<Cell ss:StyleID="s62"/>
</Row>
<Row ss:AutoFitHeight="0" ss:Height="16.875">
<Cell ss:StyleID="s64"><ss:Data ss:Type="String" xmlns="http://www.w3.org/TR/REC-html40"><Font html:Color="#000000">Currency: </Font><B><Font html:Color="#000000">USD</Font></B></ss:Data></Cell>
<Cell ss:StyleID="s64"/>
<Cell ss:StyleID="s64"/>
<Cell ss:StyleID="s64"/>
<Cell ss:StyleID="s64"/>
</Row>
<Row ss:Height="24.75">
<Cell ss:StyleID="s67"><NamedCell ss:Name="_FilterDatabase"/></Cell>
<Cell ss:StyleID="s68"><Data ss:Type="String">Same Quarter Ending Nov-06</Data><NamedCell ss:Name="_FilterDatabase"/></Cell>
<Cell ss:StyleID="s68"><Data ss:Type="String">Current Quarter Ending Nov-07</Data><NamedCell ss:Name="_FilterDatabase"/></Cell>
<Cell ss:StyleID="s68"><Data ss:Type="String">Year-to-Date  Ending Nov-06</Data><NamedCell ss:Name="_FilterDatabase"/></Cell>
<Cell ss:StyleID="s68"><Data ss:Type="String">Year-to-Date Ending Nov-07</Data><NamedCell ss:Name="_FilterDatabase"/></Cell>
</Row>
<Row ss:AutoFitHeight="1">
<Cell ss:StyleID="s65"><Data ss:Type="String">Revenue</Data></Cell>
<Cell ss:StyleID="s66"></Cell>
<Cell ss:StyleID="s66"></Cell>
<Cell ss:StyleID="s66"></Cell>
<Cell ss:StyleID="s66"></Cell>
</Row>
</Table>

Посмотрим на наши XML-данные:

<MasterReport xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:fsg="http://www.oracle.com/fsg/2002-03-20/"
xsi:schemaLocation="http://www.oracle.com/2002-03-20/fsg.xsd">
….
….
….
<fsg:RptDef RptId="p1001" RptDetName="No specific Company requested">
<fsg:RptLine RptCnt="p1001" RowCnt="r100001" LinCnt="l100001">
<fsg:RptCell ColCnt="c1000">Revenue</fsg:RptCell>
</fsg:RptLine>
<fsg:RptLine RptCnt="p1001" RowCnt="r100002" LinCnt="l100002">
<fsg:RptCell ColCnt="c1000"> Hardware Revenue </fsg:RptCell>
<fsg:RptCell ColCnt="c1001">15,398,120</fsg:RptCell>
<fsg:RptCell ColCnt="c1002">24,150,192</fsg:RptCell>
<fsg:RptCell ColCnt="c1003">39,628,954</fsg:RptCell>
<fsg:RptCell ColCnt="c1004">73,614,532</fsg:RptCell>
</fsg:RptLine>
<fsg:RptLine RptCnt="p1001" RowCnt="r100003" LinCnt="l100003">
<fsg:RptCell ColCnt="c1000"> Support Revenue </fsg:RptCell>
<fsg:RptCell ColCnt="c1001">7,321,697</fsg:RptCell>
<fsg:RptCell ColCnt="c1002">1,034,440</fsg:RptCell>
<fsg:RptCell ColCnt="c1003">17,490,142</fsg:RptCell>
<fsg:RptCell ColCnt="c1004">3,695,656</fsg:RptCell>
</fsg:RptLine>
….
….
….
</fsg:RptDef>
</MasterReport>

Мы будем выводить в строках таблицы Excel данные по каждому узлу fsg:RptLine формируемого BIPublisher файла данных.
Для этого нужно узел Row нашего шаблона "обернуть" в конструкцию

<xsl:for-each select="MasterReport/fsg:RptDef/fsg:RptLine">

</xsl:for-each>

Так как наш XML файл данных содержит пространство имен в именах узлов, то следует его определить в рамках нашего шаблона: добавьте в верхний узел - xsl:stylesheet – новый атрибут, определяющий пространство имен fsg
xmlns:fsg=http://www.oracle.com/fsg/2002-03-20/


Сделанным мы добились того, что шаблон будет формировать строку таблицы столько раз, сколько узлов fsg:RptLine в файле данных.
Теперь определим подстановку значений в ячейки строк из соответствующих атрибутов/подузлов XML файла данных.
Для этого достаточно внутри узла Cell прописать следующую конструкцию:
<Data ss:Type="String"><xsl:value-of select="fsg:RptCell[1]"/></Data>

Где ss:Type задает тип данных ячейки, для простоты воспользуемся Текстовым типом;
xsl:value-of select – конструкция XSL для обращения к конкретным узлам XML файла, по отношению к которому применяется данный шаблон, а значение "fsg:RptCell[1]" (по сути – Xpath-инструкция) говорит что нужно взять значение первого по порядку узла fsg:RptCell, вложенного в узел текущей итерации xsl:for-each.
Таким образом, узел Row с данными будет выглядеть так:

<xsl:for-each select="MasterReport/fsg:RptDef/fsg:RptLine">
<Row ss:AutoFitHeight="1">
<Cell ss:StyleID="s65"><Data ss:Type="String"><xsl:value-of select="fsg:RptCell[1]"/></Data></Cell>
<Cell ss:StyleID="s66"><Data ss:Type="String"><xsl:value-of select="fsg:RptCell[2]"/></Data></Cell>
<Cell ss:StyleID="s66"><Data ss:Type="String"><xsl:value-of select="fsg:RptCell[3]"/></Data></Cell>
<Cell ss:StyleID="s66"><Data ss:Type="String"><xsl:value-of select="fsg:RptCell[4]"/></Data></Cell>
<Cell ss:StyleID="s66"><Data ss:Type="String"><xsl:value-of select="fsg:RptCell[5]"/></Data></Cell>
</Row>
</xsl:for-each>

На этом шаблон отчета можно считать завершенным.

Выложим созданный XSL шаблон в BIPublisher.


Важно указать верный тип шаблона – "Таблица стилей XSL (XML)". "(XML)" говорит о том, что после XSL-трансформации будет создан XML-документ.

Запустим отчет на выполнение.


Укажем, что сформированный файл нужно открывать в MS Excel (если у вас IE, то файл откроется сразу в MS Excel; я пользуюсь браузером Firefox, который полученный XML несмотря на инструкции mso-progid открывает как обычный XML, чтобы это изменить – попробуйте это решение)


Вуаля, симпатичный экселевский файл.

Из минусов такого подхода:
1) При большом количестве строк с данными отчет сильно "разбухает" в объеме – помогает после открытия отчета сохранить его как "Книга MS Excel".
2) Нет возможности применять ряд специфичных функций Экселя (макросы, графики, диаграммы, таблицы среза).

Из плюсов:
1) Полная управляемость логикой формирования разметки отчета – так как используется открытая спецификация XSL (преимущества перед native Excel шаблонами BIPublisher, которые являются закрытым стандартом).
2) Значительные преимущества перед RTF шаблонами BIPublisher, за счет использования "родного" для Экселя типа документа – "Таблица XML", в отличие от MHT/HTML-формата, получаемого из RTF шаблона и неявно преобразуемого в формат книги Экселя.

P.S. Ссылка на архив с файлами примера.

Читать дальше...