Parameter management in business applications similar to a version control system

image






In various applications, the task regularly arises of supporting the logic of the change in time of some attribute of an object relative to a certain subject (or subjects). For example, this may be a change in the retail price of goods in stores or KPI indicators for employees.



In this article I will show what domain logic and interfaces can be built to solve this problem. I must make a reservation right away that it will concern the managerial influence of the user on the attribute, and not the reflection of historical change.



The implementation will be presented on the basis of the open and free lsFusion platform, but a similar scheme can be applied when using any other technology.



Introduction



For a simpler presentation and understanding of the article, we take the price as an attribute, the product as the object, and the warehouse will be the subject. In this case, the minimum possible interval for setting the attribute will be the date. Thus, the user will be able to determine what the price for a particular date will be for any product and warehouse.



The user input scheme for price changes will be similar to that used in classic version control systems. Any change, from the point of view of domain logic, will be a single commit , on the basis of which the state for a certain date will be calculated. In many subject areas, such commits are called documents or transactions. In this case, by this commit we will mean the so-called price list. Each price list will specify the goods and warehouses that are included in it, as well as the validity period.



The described scheme has the following advantages:





The main difference from the version control system is that explicit commits are independent of each other. Thus, it is possible to delete all commits relatively painlessly at any time. In addition, each such commit can be set to end when it ceases to function, which of course is not in the version control system.



Implementation



We begin the definition of domain logic with warehouses. Let's complicate the solution a bit by combining the warehouses into a hierarchy of a dynamic depth group. By what principle this is done is described in the corresponding article , so Iโ€™ll just give a piece of code that declares groups and creates forms for editing them:



Warehouse Group Announcement
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;

}







Group hierarchy example
image








Next, declare warehouses that can be tied to any of the groups:



Warehouse Announcement
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;

}









Warehouse Example
image








And finally, declare the logic of the goods:



Product Announcement
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;

}







Product example
image








We proceed directly to creating the logic of price lists. First, we set the price list class itself, as well as its validity period:

CLASS PriceList '-' ;

fromDate ' ' = DATA DATE (PriceList);

toDate ' ' = DATA DATE (PriceList);




We believe that if no Date has been set, then the price list is endless.

We add an event that, when creating the price list, will automatically put down the current date from which it will begin to operate.

WHEN LOCAL SET (PriceList p IS PriceList) DO

fromDate(p) <- currentDate();





The LOCAL keyword means that the event will not fire when the save is applied to the database, but immediately at the time of the change.



Then add the user who created it and the creation time:

createdTime ' ' = DATA DATETIME (PriceList);

createdUser = DATA User (PriceList);

nameCreatedUser '' (PriceList p) = name(createdUser(p));





Now create an event that will automatically fill them:

WHEN SET (PriceList p IS PriceList) DO {

createdTime(p) <- currentDateTime();

createdUser(p) <- currentUser();

}





This event, unlike the previous one, will be triggered only when the Save button is clicked. That is, during a save transaction to the database.



Next, create the price list lines in which the goods and prices will be set:

CLASS PriceListDetail ' -' ;

priceList = DATA PriceList (PriceListDetail) NONULL DELETE ;



product = DATA Product (PriceListDetail);

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



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





The NONULL attribute indicates that the priceList property should always be set, and DELETE indicates that when the property value is zeroed (for example, when deleting the price list), the corresponding line should be automatically deleted.



For future use, create properties that will determine the period of validity of the price list lines:

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

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





Now we will bind the price list to the warehouses for which it will operate. First, add the primary property, which will be true if the entire group of warehouses is included in the price list:

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





We calculate the โ€œinclusionโ€ of the group taking into account the selected parents (as described in the article about hierarchies):

in ' ()' (PriceList p, Group child) =

GROUP LAST dataIn(p, Group parent) ORDER DESC level(child, parent) WHERE dataIn(p, parent);





Add the primary property, with which you can specify that the price list acts on a specific warehouse:

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





We calculate the final property, which will determine that the price list changes prices in the corresponding warehouse, taking into account the groups:

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





Create a property that will show the names of all selected groups and warehouses of the price list, for a more convenient user to view the list of price lists:

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 ;





The final step in the description of domain logic will directly calculate the current price of the goods in the warehouse. To do this, create a property that finds the last by date line of the price list with the desired goods, warehouse and validity period:

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;





In the logic of calculating this property, various variations are possible. You can change both the filter for hitting rows (for example, adding a condition in WHERE that the price list is posted) and the order. It should be noted that the object itself, or rather its internal identifier, has been added to the selection order as the second parameter. This is necessary so that the price value is always determined in a unique way.



Based on the received price list line, we determine the price value and its validity period:

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));





They will be further used in user interface tables.



Next, we move on to building the user interface. First, we draw a form for editing the price list. Create a form and add the โ€œheaderโ€ of the document there:

FORM priceList '-'

OBJECTS p = PriceList PANEL

PROPERTIES (p) fromDate, toDate



EDIT PriceList OBJECT p

;





Add the price list line to the form:

EXTEND FORM priceList

OBJECTS d = PriceListDetail

PROPERTIES (d) nameProduct, price

PROPERTIES (d) NEW , DELETE

FILTERS priceList(d) = p

;





Next, add a tree in which there will be both groups and warehouses:

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

;





Properties for groups and warehouses are added to the tree at the same time. The platform will, depending on the object, show this or that property in the order they are added to the form.



We customize the design of the form so that goods and warehouses are drawn in separate tabs:

DESIGN priceList {

OBJECTS {

NEW pane {

fill = 1 ;

type = TABBED ;

MOVE BOX (d) { caption = '' ; }

MOVE BOX ( TREE stocks) { caption = '' ; }

}

}

}





The edit form will look like this:



image






image






It remains to build the basic form of price management. It will consist of two tabs. The first one will show a list of all price lists (similar to the list of commits). The second tab will display the current prices for a particular warehouse for the selected date.



To implement the first tab, add to the form a list of price lists with lines for a quick preview:

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

;





For the second tab, we first add the date on which prices are displayed, the tree of warehouse groups, as well as the warehouses themselves:

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)

;





The list of warehouses will show all warehouses that are descendants of the group selected at the top.



Next, add to the form a list of goods for which there are valid prices for the warehouse on the selected date:

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)

;





Both the price itself and the validity period are added to the columns. You can also add the price list number - then this table will resemble the logic of annotations in version control systems.



In order for the user to understand where such a price came from, we add down the list of price list lines with suitable goods and warehouses:

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)

;





Using the BACKGROUND attribute , highlight the row that determined the price shown in the table.



Also, for the convenience of the user, we will add the ability to open the edit form of the corresponding price list in a new session immediately from this story:

edit (PriceListDetail d) + { edit(priceList(d)); }

EXTEND FORM managePrices

PROPERTIES (prd) NEWSESSION EDIT

;





To achieve this, you need to specify the action that will be performed when you try to edit a line by implementing the built-in edit action. Then, a standard button for editing an object through a dialog call is added to the form in the standard way.



And finally, we form the final form design:

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);

}

}

}

}

}





Here, the pane container is added first, which consists of two tabs: priceLists and prices . The first of them just adds a list of price lists and lines. In the second, two panels are created: leftPane and rightPane . The left panel contains the date and warehouses, and the right panel contains the goods and price history.



Result



Consider the main options for using the resulting logic.



Suppose we have two separate price lists for different groups of products. Then, depending on the selected warehouse, in the price tab, only goods from the corresponding price lists will be displayed:



image






Now create a new price list with a limited validity period, a stripped down list of warehouses and a new price. On the second tab, if we select a date in the range of the new price list, we will get a new price from it. As soon as the validity period expires, the old price will again return from the original price:



image






Using the same mechanism, you can โ€œcancelโ€ the action of specific prices from a certain date. For example, if you enter a new price without specifying a price, it turns out that the price will be reset and the goods will disappear from the filter. In this case, when deleting the entered document, everything returns to the old state:



image






The property obtained with the price of the goods by the warehouse on the date can be further used in various events or other forms. For example, you can make automatic pricing in an order based on this pricing logic:

WHEN LOCAL CHANGED (sku(UserOrderDetail d)) OR CHANGED (stock(d)) OR CHANGED (dateTime(d)) DO

price(d) <- price(sku(d), stock(d), dateTime(d));





A nice bonus in this logic will be that when you add a new warehouse to the group, prices from already created price lists will automatically apply to it. The same thing will happen when you change the group for the warehouse.



If you wish, you can make the column with the price on the tab with current prices editable and add a button that will create a new commit for the changed prices.



Conclusion



In the solution at the platform level, neither reference books, nor documents with strings, nor registers, nor reports and other unnecessary abstractions are used. Everything is done exclusively on the concepts of classes and properties. Note that this rather complex logic was implemented in approximately 150 significant lines of code on lsFusion. To implement it in the same setting on other platforms (for example, 1C) is a much more difficult task.



The scheme described above is widely used in the lsFusion-based ERP solution . With it, with various modifications, price lists of suppliers, management retail prices, stocks and many other management parameters are supported.



The template can be complicated by adding several entities to the document (for example, a supplier can be added to the warehouse), as well as defining several attributes in one document at once. In particular, you can add the entity Price Type, and in the document line set the price for the tuple of the line and the corresponding price type. In the logic described above, you just need to add a few additional parameters to some properties.



With the help of several additional lines of code, it is possible to denormalize all change records into one table on which to build the corresponding index. Then any value for any date will be sampled in a logarithmic time. Such optimization is necessary when there are several hundred million records in this table.



You can try the built example online on the corresponding page of the site (section Platform). Here is the whole source code that you need to paste into the desired field:



Source
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;

}








All Articles