Gestión de parámetros en aplicaciones empresariales similares a un sistema de control de versiones.

imagen

En diversas aplicaciones, la tarea surge regularmente de apoyar la lógica del cambio en el tiempo de algún atributo de un objeto en relación con un determinado sujeto (o sujetos). Por ejemplo, esto puede ser un cambio en el precio minorista de los bienes en las tiendas o indicadores KPI para los empleados.

En este artículo, mostraré qué lógica de dominio e interfaces se pueden construir para resolver este problema. Inmediatamente haré una reserva de que se referirá a la influencia administrativa del usuario en el atributo, y no al reflejo del cambio histórico.

La implementación se presentará sobre la base de la plataforma lsFusion abierta y gratuita, pero se puede aplicar un esquema similar al usar cualquier otra tecnología.

Introduccion


Para una presentación y comprensión más simples del artículo, tomamos el precio como un atributo, el producto como el objeto y el almacén será el tema. En este caso, el intervalo mínimo posible para establecer el atributo será la fecha. Por lo tanto, el usuario podrá determinar cuál será el precio para una fecha en particular para cualquier producto y almacén.

El esquema de entrada del usuario para los cambios de precios será similar al utilizado en los sistemas de control de versiones clásicos. Cualquier cambio, desde el punto de vista de la lógica de dominio, será una confirmación única, en función de la cual se calculará el estado de una fecha determinada. En muchas áreas temáticas, estos compromisos se denominan documentos o transacciones. En este caso, por este compromiso nos referiremos a la llamada lista de precios. Cada lista de precios especificará los bienes y almacenes que están incluidos en ella, así como el período de validez.

El esquema descrito tiene las siguientes ventajas:

  • Atomicidad Cada cambio se emite como un documento separado. Por lo tanto, estos documentos pueden guardarse temporalmente, pero no publicarse. Con una entrada errónea, es fácil revertir todo el cambio.
  • Transparencia Es fácil determinar quién realizó el cambio y cuándo, así como indicar el motivo haciendo un comentario sobre el documento.

La principal diferencia con el sistema de control de versiones es que las confirmaciones explícitas son independientes entre sí. Por lo tanto, es posible eliminar todas las confirmaciones relativamente sin dolor en cualquier momento. Además, cada uno de estos commits puede configurarse para finalizar cuando deja de funcionar, lo que por supuesto no está en el sistema de control de versiones.

Implementación


Comenzamos la definición de lógica de dominio con almacenes. Vamos a complicar un poco la solución combinando almacenes en una jerarquía de un grupo de profundidad dinámica. Según qué principio se hace esto, se describe en el artículo correspondiente, por lo que solo daré un fragmento de código que declara grupos y crea formularios para editarlos:

Anuncio de grupo de almacenes
CLASS Group ' ' ;
name '' = DATA ISTRING [ 50 ] (Group);

parent = DATA Group (Group);
nameParent ' ' (Group g) = name(parent(g));

level '' (Group child, Group parent) =
RECURSION 1l IF child IS Group AND parent = child
STEP 2l IF parent = parent($parent) MATERIALIZED ;

FORM group ' '
OBJECTS g = Group PANEL
PROPERTIES (g) name, nameParent

EDIT Group OBJECT g
;

FORM groups ' '
OBJECTS g = Group
PROPERTIES (g) READONLY name, nameParent
PROPERTIES (g) NEWSESSION NEW , EDIT , DELETE

LIST Group OBJECT g
;

NAVIGATOR {
NEW groups;
}

Ejemplo de jerarquía grupal
imagen


A continuación, declare los almacenes que se pueden vincular a cualquiera de los grupos:

Anuncio de almacén
CLASS Stock '' ;
name '' = DATA ISTRING [ 50 ] (Stock);

group '' = DATA Group (Stock);
nameGroup '' (Stock st) = name(group(st));

FORM stock ''
OBJECTS s = Stock PANEL
PROPERTIES (s) name, nameGroup

EDIT Stock OBJECT s
;

FORM stocks ''
OBJECTS s = Stock
PROPERTIES (s) READONLY name, nameGroup
PROPERTIES (s) NEWSESSION NEW , EDIT , DELETE

LIST Stock OBJECT s
;

NAVIGATOR {
NEW stocks;
}


Ejemplo de almacén
imagen


Y finalmente, declare la lógica de los bienes:

Anuncio de producto
CLASS Product '' ;
name '' = DATA ISTRING [ 50 ] (Product);

FORM product ''
OBJECTS p = Product PANEL
PROPERTIES (p) name

EDIT Product OBJECT p
;

FORM products ''
OBJECTS p = Product
PROPERTIES (p) READONLY name
PROPERTIES (p) NEWSESSION NEW , EDIT , DELETE

LIST Product OBJECT p
;

NAVIGATOR {
NEW products;
}

Ejemplo de producto
imagen


Procedemos directamente a crear la lógica de las listas de precios. Primero, establecemos la clase de lista de precios en sí, así como su período de validez:
CLASS PriceList '-' ;
fromDate ' ' = DATA DATE (PriceList);
toDate ' ' = DATA DATE (PriceList);
Creemos que si no se ha establecido una Fecha , entonces la lista de precios es interminable.
Agregamos un evento que, al crear la lista de precios, anotará automáticamente la fecha actual a partir de la cual comenzará a funcionar.
WHEN LOCAL SET (PriceList p IS PriceList) DO
fromDate(p) <- currentDate();
La palabra clave LOCAL significa que el evento no se activará cuando se aplique el guardado a la base de datos, sino inmediatamente cuando se realice el cambio.

Luego agregue el usuario que lo creó y el tiempo de creación:
createdTime ' ' = DATA DATETIME (PriceList);
createdUser = DATA User (PriceList);
nameCreatedUser '' (PriceList p) = name(createdUser(p));
Ahora cree un evento que los llene automáticamente:
WHEN SET (PriceList p IS PriceList) DO {
createdTime(p) <- currentDateTime();
createdUser(p) <- currentUser();
}
Este evento, a diferencia del anterior, se activará solo cuando se haga clic en el botón Guardar. Es decir, durante una transacción de guardar en la base de datos.

A continuación, cree las líneas de la lista de precios en las que se establecerán los bienes y precios:
CLASS PriceListDetail ' -' ;
priceList = DATA PriceList (PriceListDetail) NONULL DELETE ;

product = DATA Product (PriceListDetail);
nameProduct '' (PriceListDetail d) = name(product(d));

price '' = DATA NUMERIC [ 10 , 2 ] (PriceListDetail);
El atributo NONULL indica que la propiedad priceList siempre debe establecerse, y DELETE indica que cuando el valor de la propiedad se pone a cero (por ejemplo, al eliminar la lista de precios), la línea correspondiente se debe eliminar automáticamente.

Para uso futuro, crearemos propiedades que determinarán el período de validez de las líneas de la lista de precios:
fromDate ' ' (PriceListDetail d) = fromDate(priceList(d));
toDate ' ' (PriceListDetail d) = toDate(priceList(d));
Ahora vincularemos la lista de precios a los almacenes para los que operará. Primero, agregue la propiedad principal, que será verdadera si todo el grupo de almacenes está incluido en la lista de precios:
dataIn '' = DATA BOOLEAN (PriceList, Group);
Calculamos la "inclusión" del grupo teniendo en cuenta a los padres seleccionados (como se describe en el artículo sobre jerarquías):
in ' ()' (PriceList p, Group child) =
GROUP LAST dataIn(p, Group parent) ORDER DESC level(child, parent) WHERE dataIn(p, parent);
Agregue la propiedad principal, con la que puede especificar que la lista de precios actúe en un almacén específico:
dataIn '' = DATA BOOLEAN (PriceList, Stock);
Calculamos la propiedad final, que determinará que la lista de precios cambia los precios en el almacén correspondiente, teniendo en cuenta los grupos:
in '' (PriceList p, Stock s) = dataIn(p, s) OR in(p, group(s));
Cree una propiedad que muestre los nombres de todos los grupos y almacenes seleccionados de la lista de precios, para que un usuario más conveniente vea la lista de listas de precios:
stocks '' (PriceList p) = CONCAT ' / ' ,
GROUP CONCAT name(Group g) IF dataIn(p, g), ',' ORDER g,
GROUP CONCAT name(Stock s) IF dataIn(p, s), ',' ORDER s
CHARWIDTH 30 ;
El paso final en la descripción de la lógica de dominio calculará directamente el precio actual de las mercancías en el almacén. Para hacer esto, cree una propiedad que encuentre la última línea de fecha de la lista de precios con los bienes, el almacén y el período de validez deseados:
priceListDetail (Product p, Stock s, DATE dt) =
GROUP LAST PriceListDetail d
ORDER fromDate(d), d
WHERE product(d) = p AND in(priceList(d), s) AND
fromDate(d) <= dt AND NOT toDate(d) < dt;
En la lógica de calcular esta propiedad, son posibles varias variaciones. Puede cambiar tanto el filtro para golpear filas (por ejemplo, agregar una condición en DONDE se publica la lista de precios) como el pedido. Cabe señalar que el objeto mismo, o más bien su identificador interno, se ha agregado al orden de selección mediante el segundo parámetro. Esto es necesario para que el valor del precio siempre se determine de una manera única.

Según la línea de lista de precios recibida, determinamos el valor del precio y su período de validez:
price '' (Product p, Stock s, DATE dt) = price(priceListDetail(p, s, dt));
fromDate ' ' (Product p, Stock s, DATE dt) = fromDate(priceListDetail(p, s, dt));
toDate ' ' (Product p, Stock s, DATE dt) = toDate(priceListDetail(p, s, dt));
Se seguirán utilizando en las tablas de la interfaz de usuario.

A continuación, pasamos a construir la interfaz de usuario. Primero, dibujamos un formulario para editar la lista de precios. Cree un formulario y agregue el "encabezado" del documento allí:
FORM priceList '-'
OBJECTS p = PriceList PANEL
PROPERTIES (p) fromDate, toDate

EDIT PriceList OBJECT p
;
Agregue la línea de la lista de precios al formulario:
EXTEND FORM priceList
OBJECTS d = PriceListDetail
PROPERTIES (d) nameProduct, price
PROPERTIES (d) NEW , DELETE
FILTERS priceList(d) = p
;
A continuación, agregue un árbol en el que habrá grupos y almacenes:
EXTEND FORM priceList
TREE stocks g = Group PARENT parent, s = Stock
PROPERTIES READONLY name(g), name(s)
PROPERTIES dataIn(p, g), in(p, g)
PROPERTIES dataIn(p, s), in(p, s)
FILTERS group(s) = g
;
Las propiedades para grupos y almacenes se agregan al árbol al mismo tiempo. La plataforma, según el objeto, mostrará esta o aquella propiedad en el orden en que se agregan al formulario.

Personalizamos el diseño del formulario para que los bienes y almacenes se dibujen en pestañas separadas:
DESIGN priceList {
OBJECTS {
NEW pane {
fill = 1 ;
type = TABBED ;
MOVE BOX (d) { caption = '' ; }
MOVE BOX ( TREE stocks) { caption = '' ; }
}
}
}
El formulario de edición se verá así:

imagen

imagen

Queda por construir la forma básica de gestión de precios. Consistirá en dos pestañas. El primero mostrará una lista de todas las listas de precios (similar a la lista de confirmaciones). La segunda pestaña mostrará los precios actuales de un almacén específico para la fecha seleccionada.

Para implementar la primera pestaña, agregue al formulario una lista de listas de precios con líneas para una vista previa rápida:
FORM managePrices ' '
OBJECTS p = PriceList
PROPERTIES (p) READONLY fromDate, toDate, stocks, createdTime, nameCreatedUser
PROPERTIES (p) NEWSESSION NEW , EDIT , DELETE

OBJECTS d = PriceListDetail
PROPERTIES (d) READONLY nameProduct, price
FILTERS priceList(d) = p
;
Para la segunda pestaña, primero agregamos la fecha en la que se muestran los precios, el árbol de los grupos de almacenes, así como los propios almacenes:
EXTEND FORM managePrices
OBJECTS dt = DATE PANEL
PROPERTIES VALUE (dt)

TREE groups g = Group PARENT parent
PROPERTIES READONLY name(g)

OBJECTS s = Stock
PROPERTIES (s) READONLY name, nameGroup
FILTERS level(group(s), g)
;
La lista de almacenes mostrará todos los almacenes que son descendientes del grupo seleccionado en la parte superior.

A continuación, agregue al formulario una lista de productos para los que hay precios válidos para el almacén en la fecha seleccionada:
EXTEND FORM managePrices
OBJECTS pr = Product
PROPERTIES READONLY name(pr), price(pr, s, dt), fromDate(pr, s, dt), toDate(pr, s, dt)
FILTERS price(pr, s, dt)
;
Tanto el precio en sí como el período de validez se agregan a las columnas. También puede agregar el número de la lista de precios; luego, esta tabla se parecerá a la lógica de las anotaciones en los sistemas de control de versiones.

Para que el usuario entienda de dónde proviene ese precio, agregamos la lista de líneas de lista de precios con productos y almacenes adecuados:
EXTEND FORM managePrices
OBJECTS prd = PriceListDetail
PROPERTIES READONLY BACKGROUND (priceListDetail(pr, s, dt) = prd)
fromDate(prd), toDate(prd), '' = stocks(priceList(prd)), price(prd)
FILTERS product(prd) = pr AND in(priceList(prd), s)
;
Usando el atributo BACKGROUND, resalte la fila que determinó el precio que se muestra en la tabla.

Además, para la comodidad del usuario, agregaremos la posibilidad de abrir el formulario de edición de la lista de precios correspondiente en una nueva sesión inmediatamente a partir de esta historia:
edit (PriceListDetail d) + { edit(priceList(d)); }
EXTEND FORM managePrices
PROPERTIES (prd) NEWSESSION EDIT
;
Para lograr esto, debe especificar la acción que se realizará cuando intente editar una línea implementando la acción de edición incorporada. Luego, un botón estándar para editar un objeto a través de una llamada de diálogo se agrega al formulario de la manera estándar.

Y finalmente, formamos el diseño final del formulario:
DESIGN managePrices {
OBJECTS {
NEW pane {
fill = 1 ;
type = TABBED ;
NEW priceLists {
caption = '-' ;
MOVE BOX (p);
MOVE BOX (d);
}
NEW prices {
caption = '' ;
fill = 1 ;
type = SPLITH ;
NEW leftPane {
MOVE BOX (dt);
MOVE BOX ( TREE groups);
MOVE BOX (s);
}
NEW rightPane {
fill = 3 ;
type = SPLITV ;
MOVE BOX (pr) { fill = 3 ; }
MOVE BOX (prd);
}
}
}
}
}
Aquí, el contenedor del panel se agrega primero, que consta de dos pestañas: listas de precios y precios . El primero de ellos solo agrega una lista de listas de precios y líneas. En el segundo, se crean dos paneles: leftPane y rightPane . El panel izquierdo contiene la fecha y los almacenes, y el panel derecho contiene los bienes y el historial de precios.

Resultado


Considere las principales opciones para usar la lógica resultante.

Supongamos que tenemos dos listas de precios separadas para diferentes grupos de bienes. Luego, dependiendo del almacén seleccionado, en la pestaña con precios, solo se mostrarán los productos de las listas de precios correspondientes:

imagen

Ahora cree una nueva lista de precios con un período de validez limitado, una lista reducida de almacenes y un nuevo precio. En la segunda pestaña, si seleccionamos una fecha en el rango de la nueva lista de precios, obtendremos un nuevo precio. Tan pronto como expire el período de validez, el precio anterior volverá nuevamente del precio original:

imagen

Usando el mismo mecanismo, puede "cancelar" la acción de precios específicos desde una fecha determinada. Por ejemplo, si ingresa un nuevo precio sin especificar un precio, resulta que el precio se restablecerá y los productos desaparecerán del filtro. En este caso, al eliminar el documento ingresado, todo vuelve al estado anterior:

imagen

La propiedad resultante con el precio de los bienes por el almacén en la fecha se puede utilizar en varios eventos u otras formas. Por ejemplo, puede realizar precios automáticos en un orden basado en esta lógica de precios:
WHEN LOCAL CHANGED (sku(UserOrderDetail d)) OR CHANGED (stock(d)) OR CHANGED (dateTime(d)) DO
price(d) <- price(sku(d), stock(d), dateTime(d));
Una buena ventaja en esta lógica será que cuando agregue un nuevo almacén al grupo, los precios de las listas de precios ya creadas se aplicarán automáticamente. Lo mismo sucederá cuando cambie el grupo para el almacén.

Si lo desea, puede editar la columna con el precio en la pestaña con los precios actuales y agregar un botón que creará un nuevo compromiso para los precios modificados.

Conclusión


En la solución a nivel de plataforma, no se utilizan libros de referencia, ni documentos con cadenas, ni registros, ni informes y otras abstracciones innecesarias. Todo se hace exclusivamente en los conceptos de clases y propiedades. Tenga en cuenta que esta lógica bastante compleja se implementó en aproximadamente 150 líneas de código significativas en lsFusion. Implementarlo en la misma formulación en otras plataformas (por ejemplo, 1C) es una tarea mucho más difícil.

El esquema descrito anteriormente se usa ampliamente en la solución ERP basada en lsFusion. Utilizándolo, con varias modificaciones, se admiten listas de precios de proveedores, precios minoristas de gestión, existencias y muchos otros parámetros de gestión.

La plantilla puede complicarse agregando varias entidades al documento (por ejemplo, se puede agregar un proveedor al almacén), así como definiendo varios atributos en un documento a la vez. En particular, puede agregar el Tipo de precio de la entidad y, en la línea del documento, establecer el precio de la tupla de la línea y el tipo de precio correspondiente. En la lógica descrita anteriormente, solo necesita agregar algunos parámetros adicionales a algunas propiedades.

Con la ayuda de varias líneas de código adicionales, es posible desnormalizar todos los registros de cambios en una tabla sobre la cual construir el índice correspondiente. Luego, la selección de cualquier valor para cualquier fecha se realizará en un tiempo logarítmico. Dicha optimización es necesaria cuando hay varios cientos de millones de registros en esta tabla.

Puede probar el ejemplo creado en línea en la página correspondiente del sitio (sección Plataforma). Aquí está todo el código fuente que necesita pegar en el campo deseado:

Código fuente
REQUIRE Authentication, Time;

CLASS Group ' ' ;
name '' = DATA ISTRING [ 50 ] (Group);

parent = DATA Group (Group);
nameParent ' ' (Group g) = name(parent(g));

level '' (Group child, Group parent) =
RECURSION 1l IF child IS Group AND parent = child
STEP 2l IF parent = parent($parent) MATERIALIZED ;

FORM group ' '
OBJECTS g = Group PANEL
PROPERTIES (g) name, nameParent

EDIT Group OBJECT g
;

FORM groups ' '
OBJECTS g = Group
PROPERTIES (g) READONLY name, nameParent
PROPERTIES (g) NEWSESSION NEW , EDIT , DELETE

LIST Group OBJECT g
;

NAVIGATOR {
NEW groups;
}

CLASS Stock '' ;
name '' = DATA ISTRING [ 50 ] (Stock);

group '' = DATA Group (Stock);
nameGroup '' (Stock st) = name(group(st));

FORM stock ''
OBJECTS s = Stock PANEL
PROPERTIES (s) name, nameGroup

EDIT Stock OBJECT s
;

FORM stocks ''
OBJECTS s = Stock
PROPERTIES (s) READONLY name, nameGroup
PROPERTIES (s) NEWSESSION NEW , EDIT , DELETE

LIST Stock OBJECT s
;

NAVIGATOR {
NEW stocks;
}

CLASS Product '' ;
name '' = DATA ISTRING [ 50 ] (Product);

FORM product ''
OBJECTS p = Product PANEL
PROPERTIES (p) name

EDIT Product OBJECT p
;

FORM products ''
OBJECTS p = Product
PROPERTIES (p) READONLY name
PROPERTIES (p) NEWSESSION NEW , EDIT , DELETE

LIST Product OBJECT p
;

NAVIGATOR {
NEW products;
}

CLASS PriceList '-' ;
fromDate ' ' = DATA DATE (PriceList);
toDate ' ' = DATA DATE (PriceList);

createdTime ' ' = DATA DATETIME (PriceList);
createdUser = DATA User (PriceList);
nameCreatedUser '' (PriceList p) = name(createdUser(p));

WHEN LOCAL SET (PriceList p IS PriceList) DO
fromDate(p) <- currentDate();

WHEN SET (PriceList p IS PriceList) DO {
createdTime(p) <- currentDateTime();
createdUser(p) <- currentUser();
}

CLASS PriceListDetail ' -' ;
priceList = DATA PriceList (PriceListDetail) NONULL DELETE ;

product = DATA Product (PriceListDetail);
nameProduct '' (PriceListDetail d) = name(product(d));

price '' = DATA NUMERIC [ 10 , 2 ] (PriceListDetail);

fromDate ' ' (PriceListDetail d) = fromDate(priceList(d));
toDate ' ' (PriceListDetail d) = toDate(priceList(d));

dataIn '' = DATA BOOLEAN (PriceList, Group);

in ' ()' (PriceList p, Group child) =
GROUP LAST dataIn(p, Group parent) ORDER DESC level(child, parent) WHERE dataIn(p, parent);

dataIn '' = DATA BOOLEAN (PriceList, Stock);
in '' (PriceList p, Stock s) = dataIn(p, s) OR in(p, group(s));

stocks '' (PriceList p) = CONCAT ' / ' ,
GROUP CONCAT name(Group g) IF dataIn(p, g), ',' ORDER g,
GROUP CONCAT name(Stock s) IF dataIn(p, s), ',' ORDER s
CHARWIDTH 30 ;

priceListDetail (Product p, Stock s, DATE dt) =
GROUP LAST PriceListDetail d
ORDER fromDate(d), d
WHERE product(d) = p AND in(priceList(d), s) AND
fromDate(d) <= dt AND NOT toDate(d) < dt;

price '' (Product p, Stock s, DATE dt) = price(priceListDetail(p, s, dt));
fromDate ' ' (Product p, Stock s, DATE dt) = fromDate(priceListDetail(p, s, dt));
toDate ' ' (Product p, Stock s, DATE dt) = toDate(priceListDetail(p, s, dt));

FORM priceList '-'
OBJECTS p = PriceList PANEL
PROPERTIES (p) fromDate, toDate

EDIT PriceList OBJECT p
;

EXTEND FORM priceList
OBJECTS d = PriceListDetail
PROPERTIES (d) nameProduct, price
PROPERTIES (d) NEW , DELETE
FILTERS priceList(d) = p
;

EXTEND FORM priceList
TREE stocks g = Group PARENT parent, s = Stock
PROPERTIES READONLY name(g), name(s)
PROPERTIES dataIn(p, g), in(p, g)
PROPERTIES dataIn(p, s), in(p, s)
FILTERS group(s) = g
;

DESIGN priceList {
OBJECTS {
NEW pane {
fill = 1 ;
type = TABBED ;
MOVE BOX (d) { caption = '' ; }
MOVE BOX ( TREE stocks) { caption = '' ; }
}
}
}

FORM managePrices ' '
OBJECTS p = PriceList
PROPERTIES (p) READONLY fromDate, toDate, stocks, createdTime, nameCreatedUser
PROPERTIES (p) NEWSESSION NEW , EDIT , DELETE

OBJECTS d = PriceListDetail
PROPERTIES (d) READONLY nameProduct, price
FILTERS priceList(d) = p
;

EXTEND FORM managePrices
OBJECTS dt = DATE PANEL
PROPERTIES VALUE (dt)

TREE groups g = Group PARENT parent
PROPERTIES READONLY name(g)

OBJECTS s = Stock
PROPERTIES (s) READONLY name, nameGroup
FILTERS level(group(s), g)
;

EXTEND FORM managePrices
OBJECTS pr = Product
PROPERTIES READONLY name(pr), price(pr, s, dt), fromDate(pr, s, dt), toDate(pr, s, dt)
FILTERS price(pr, s, dt)
;

EXTEND FORM managePrices
OBJECTS prd = PriceListDetail
PROPERTIES READONLY BACKGROUND (priceListDetail(pr, s, dt) = prd)
fromDate(prd), toDate(prd), '' = stocks(priceList(prd)), price(prd)
FILTERS product(prd) = pr AND in(priceList(prd), s)
;

edit (PriceListDetail d) + { edit(priceList(d)); }
EXTEND FORM managePrices
PROPERTIES (prd) NEWSESSION EDIT
;

DESIGN managePrices {
OBJECTS {
NEW pane {
fill = 1 ;
type = TABBED ;
NEW priceLists {
caption = '-' ;
MOVE BOX (p);
MOVE BOX (d);
}
NEW prices {
caption = '' ;
fill = 1 ;
type = SPLITH ;
NEW leftPane {
MOVE BOX (dt);
MOVE BOX ( TREE groups);
MOVE BOX (s);
}
NEW rightPane {
fill = 3 ;
type = SPLITV ;
MOVE BOX (pr) { fill = 3 ; }
MOVE BOX (prd);
}
}
}
}
}

NAVIGATOR {
NEW managePrices;
}

Source: https://habr.com/ru/post/468553/


All Articles