与版本控制系统类似的业务应用程序中的参数管理

图片

在各种应用中,任务经常出现来支持对象相对于某个主题(或多个主题)的某些属性的时间变化逻辑。 例如,这可能是商店中商品零售价的变化或员工的KPI指标。

在本文中,我将展示可以构建哪些域逻辑和接口来解决此问题。 我将立即做出保留,它将涉及用户对属性的管理影响,而不是历史变化的反映。

该实现将在开放和免费的lsFusion平台的基础上进行介绍,但是在使用任何其他技术时也可以应用类似的方案。

引言


为了更简单地介绍和理解文章,我们将价格作为属性,将产品作为对象,而仓库将成为主题。 在这种情况下,用于设置属性的最小可能间隔将是日期。 因此,用户将能够确定特定日期的任何产品和仓库的价格是多少。

价格更改的用户输入方案将类似于经典版本控制系统中使用的方案。 从域逻辑的角度来看,任何更改都将是一次commit ,基于该commit可以计算特定日期的状态。 在许多主题领域,此类提交称为文档或交易。 在这种情况下,通过此提交,我们将得到所谓的价格表。 每个价目表都会指定其中包含的货物和仓库,以及有效期。

所描述的方案具有以下优点:

  • 原子性 。 每次更改均作为单独的文档发布。 因此,这些文档可以暂时保存,但不能发布。 输入错误时,很容易回滚整个更改。
  • 透明性 可以轻松确定谁进行了更改以及何时进行更改,并通过在文档上进行注释来指出原因。

与版本控制系统的主要区别在于,显式提交彼此独立。 因此,可以在任何时间相对轻松地删除所有提交。 另外,每个此类提交都可以设置为在其停止运行时结束,当然,这不在版本控制系统中。

实作


我们从仓库开始定义域逻辑。 让我们通过将仓库组合到一组动态深度的层次结构中,使解决方案更加复杂。 在相应的文章中将描述按照什么原理完成操作,因此,我只给出一段代码来声明组并创建用于编辑它们的表单:

仓库组广告
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);
我们认为,如果未设置日期 ,那么价目表将是无限的。
我们添加了一个事件,该事件在创建价格表时将自动放下它将开始运行的当前日期。
WHEN LOCAL SET (PriceList p IS PriceList) DO
fromDate(p) <- currentDate();
LOCAL关键字表示在将保存应用于数据库时不会触发该事件,而在进行更改时立即触发。

然后添加创建它的用户和创建时间:
createdTime ' ' = DATA DATETIME (PriceList);
createdUser = DATA User (PriceList);
nameCreatedUser '' (PriceList p) = name(createdUser(p));
现在创建一个将自动填充它们的事件:
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);
NONULL属性指示应该始终设置priceList属性,而DELETE指示当该属性值为零时(例如,删除价格列表时),应自动删除相应的行。

为了将来使用,我们将创建将确定价目表行有效期的属性:
fromDate ' ' (PriceListDetail d) = fromDate(priceList(d));
toDate ' ' (PriceListDetail d) = toDate(priceList(d));
现在,我们将价格表绑定到将要运行的仓库。 首先,添加主属性,如果整个仓库组都包含在价格清单中,则该属性为true:
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;
在计算此属性的逻辑中,各种变化都是可能的。 您可以更改击中行的过滤器(例如,在发布价格清单的WHERE中添加条件)和订单。 应当注意,对象本身,或者更确切地说是其内部标识符,已经由第二参数添加到选择顺序中。 这是必需的,以便始终以唯一的方式确定价格值。

根据收到的价格清单行,我们确定价格值及其有效期:
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)
;
使用Background属性突出显示确定表中价格的行。

另外,为了方便用户,我们将添加一个功能,可以从该故事立即在新会话中打开相应价格表的编辑表单:
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);
}
}
}
}
}
在这里,首先添加窗格容器,该窗格容器包含两个选项卡: priceListsprices 。 其中第一个只是添加价目表和价目表。 在第二个中,创建两个面板: leftPanerightPane 。 左侧面板包含日期和仓库,右侧面板包含货物和价格历史记录。

结果


考虑使用所得逻辑的主要选项。

假设我们针对不同的商品组有两个单独的价格表。 然后,根据所选仓库,在带有价格的标签中,将仅显示相应价格清单中的产品:

图片

现在,创建一个具有有限有效期的新价格清单,一个精简的仓库清单和一个新价格。 在第二个选项卡上,如果我们在新价格表的范围内选择一个日期,则将从中获得新价格。 有效期到期后,旧价格将再次从原始价格中恢复:

图片

使用相同的机制,您可以从某个日期“取消”特定价格的操作。 例如,如果您输入一个新价格而不指定价格,结果是该价格将被重置,商品将从过滤器中消失。 在这种情况下,删除输入的文档时,所有内容都会恢复到旧状态:

图片

具有仓库在日期的价格的价格所得的属性可以进一步用于各种事件或其他形式。 例如,您可以根据以下定价逻辑对订单进行自动定价:
WHEN LOCAL CHANGED (sku(UserOrderDetail d)) OR CHANGED (stock(d)) OR CHANGED (dateTime(d)) DO
price(d) <- price(sku(d), stock(d), dateTime(d));
这种逻辑的一个好处是,当您向组中添加新仓库时,已经创建的价格清单中的价格将自动应用于该仓库。 当您更改仓库组时,将会发生相同的事情。

如果您愿意,可以将选项卡上带有价格的列与当前价格进行编辑,并添加一个按钮,该按钮将为更改后的价格创建新的提交。

结论


在平台级别的解决方案中,既不使用参考书,也不使用带字符串的文档,寄存器,报告和其他不必要的抽象。 一切都只在类和属性的概念上完成。 请注意,在lsFusion上大约150行有意义的代码行中实现了这种相当复杂的逻辑。 在其他平台(例如1C)中以相同的方式实现它是一项艰巨的任务。

上述方案在基于lsFusion的ERP解决方案中得到了广泛的应用。 通过对其进行各种修改,可以使用它来支持供应商的价目表,管理零售价,库存和许多其他管理参数。

通过将多个实体添加到文档(例如,可以将供应商添加到仓库)以及一次在一个文档中定义多个属性,可以使模板变得复杂。 特别是,您可以添加实体价格类型,并在单据行中设置该行的元组的价格以及相应的价格类型。 在上述逻辑中,您只需要向一些属性添加一些其他参数即可。

借助其他几行代码,可以将所有变更记录归一化为一个表,以在该表上构建相应的索引。 然后,将在对数时间内选择任何日期的任何值。 当此表中有几亿条记录时,这种优化是必要的。

您可以网站的相应页面 (平台部分)上在线尝试构建的示例。 这是您需要粘贴到所需字段中的全部源代码:

源代码
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/zh-CN468553/


All Articles