Практики при разработке документов
Contents
Практики при разработке документов#
Odm документа#
Для документов создаем отдельную директорию (пакет), чтобы все коллекции были тоже в ней
У полей с датами в названии системного имени НЕ пишем слово Date
пример:
dReg
dDoc
dExec \Для поля с датой регистрации, ставим значение по умолчанию текущую дату
defaultValue="sysdate"
Для полей с номерами, датой рег., состоянием и номером состояния устанавливаем
isCopyInCopyObject="false"
, чтобы значения не копировались при копированииДля полей с номером, датой и типом объекта устанавливаем свойство
isHeadLine="true"
, так как они в большинстве случаем участвуют в вычислении заголовкаДобавляем поле с номером состояния, не видимое
<attr name="idStateMC" attribute-type="Number" caption="Номер состояния" order="80" type="basic"
isVisible="false" isCopyInCopyObject="false" isStateMC="true">
<mnemoCodeColumn/>
</attr>
Большинство полей можно взять из шаблона
bs\src\main\resources\META-INF\attribute-template.xml
Доп. поля связанные со складом и тмцstk\src\main\resources\META-INF\attribute-template.xml
Для денежных полей с суммами и ценами устанавливаем сразу денежный редактор
editorType="currency"
Для булевых полей ставим значение по умолчанию 0
defaultValue="0"
и тип редактора галкуeditorType="check"
Добавляем скрипты регистрации состояний, закладок и типов документа (вместо Demo_Doc напишите свой класс)
<dbData>
<script name="regObjectType" version="1">
<depends>
<dep on="Demo_Doc.regTab"/>
<dep on="Demo_Doc.regState"/>
</depends>
<install>Demo_DocApi.regObjectType(false);</install>
</script>
<script name="regState" version="1">
<install>Demo_DocApi.regState();</install>
</script>
<script name="regTab" version="1">
<install>Demo_DocApi.regTab();</install>
</script>
</dbData>
Если нужны прикрепленные файлы устанавливаем свойство в шапку
attachType="simple"
илиattachType="versioned"
для версионного режимаЕсли нужна настройка по счетам учета, то добавляем в коллекции настройку
<var-collection name="Bs_AccObjSetting" ref.attr="gidSrc" cascadeOnDelete="true"/>
Odm коллекции документа#
Наименование коллекций, как правило, содержит имя документа (может быть и сокращенное) и суффикс
Det
или какой то другой, напримерDemo_DocDet
Порядковый номер для позиций следует наименовать как
nRow - № п/п
и делать его числовым, наименованиеnOrder
путает сBs_Order
. Его не следует делать автонумерующимся, так как нумерация срабатывает на сохранение. Нумерацию можно сделать руками вApi.insertByParent
(ниже будет пример)
Api документа:#
Вместо
ListBuffer
следует использоватьArrayBuffer
, так как он меньше занимает места в памятиПереопределяем calcHeadLine, для вычисление заголовка с учетом типа документа, номера и даты
override def calcHeadLine(idp: NLong): NString = {
var svHeadLine = NString()
if (idp.isNotNull) {
load(idp) :> { aro =>
if (aro != null) {
svHeadLine = s"${Btk_ObjectTypeApi().getShortCaption(aro.idObjectType).nvl(Btk_ClassApi().getHeadLine(aro.idClass))} №" ++ aro.sNumDoc.nvl("") ++ " от " ++ aro.dDoc.toString("dd.MM.yyyy").nvl("")
}
}
}
svHeadLine
}
Переопределяем insert для установки типа объекта по умолчанию и других атрибутов (например валюта и тип курса) при создании
override def insert(): ApiRop = {
val rop = super.insert()
//установка типа объекта по умолчанию
setidObjectType(rop, Btk_ObjectTypeApi().getDefaultObjType(idClass))
//установки даты документа без времени
//setdDoc(rop, NDate.now().truncate())
//установка нац. валюты Cur_Currency
//setidCur(rop, Cur_CurrencySettingsApi().getidCurrencyMain)
//установка типа курса Cur_CurrencyRateType
//setidCurRateType(rop, Cur_CurrencySettingsApi().getidCurrencyRateType)
//установка типа налогообложения Tax_TaxType
//setidTaxType(rop, Tax_TaxTypeApi().getDefaults)
//установка ставки налога по умолчанию Tax_TaxRate
//setidVATRate(rop, Tax_TaxRateApi().getDefaults)
//установка текущего пользователя Btk_User
//setidUser(rop, Btk_UserApi().getCurrentUserID)
//установка текущего физ. лица Bs_Person
//setidPerson(rop, Bs_PersonApi().getCurrentPersonID)
//установка текущего сотрудника Bs_Employee
//setidEmployee(rop, Bs_EmployeeApi().getidCurrentEmployee)
rop
}
Переопределяем delete, чтобы нельзя было удалить документ, если он находится не в состоянии
Оформляется
override def delete(rop: ApiRop): Unit = {
if (rop.get(_.idStateMC).isDistinct(100.nn))
throw AppException("Ошибка. Удаление документа возможно только в состоянии \"Оформляется\".")
//Удаление из журнала с признаками проведения в учетах
//Bs_FlagAccKindBookDocApi().delByObj(rop.gid)
//Удаление из журнала связанных документов
//Bts_DocLinkApi().delByDoc(rop.gid)
super.delete(rop)
}
Если есть настройка по счетам учета, то на установку типа объекта и организации прописываем установку счетов учета для документа
override def setidDepOwner(rop: ApiRop, value: NLong): Unit = {
super.setidDepOwner(rop, value)
Bs_AccObjSettingApi().registerAccObjSetByObjectForDoc(
gidpObject = rop.gid
)
//если есть КПП организации, то устанавливаем его
//setidSelfKpp(rop, Bs_DepOwnerApi().getIdDefKpp(value))
}
override def setidObjectType(rop: ApiRop, value: NLong): Unit = {
super.setidObjectType(rop, value)
Bs_AccObjSettingApi().registerAccObjSetByObjectForDoc(
gidpObject = rop.gid
)
}
Для setidState в прикладной логике надо использовать номера состояний, а на их id.
Пример действий на перевод состояний:
//проверки для документа при выполнении
private def validateOnAccept(rop: ApiRop): Unit = {
}
//проверки для документа при откате
private def validateOnDecline(rop: ApiRop): Unit = {
//проверка на наличие проведения в бух., нал., упр. учетах
//Bs_FlagAccKindBookDocApi().validateObj(rop.gid)
}
//действия на выполнен
private def accept(rop: ApiRop): Unit = {
//если не заполнена дата исполнения, то заполняем ее текущей датой
//if (rop.get(_.dExec).isNull)
// setdExec(rop, NDate.now())
}
//действия на откат из выполнен
private def decline(rop: ApiRop): Unit = {
}
override def setidState(rop: ApiRop, value: NLong): Unit = {
//номера состояний
//из какого
val nvStateFrom = rop.get(_.idStateMC) //если нет поля можно использовать Btk_ClassStateApi().getOrder(rop.get(_.idState))
//в какое
val nvStateTo = Btk_ClassStateApi().getOrder(value)
//переход в выполнен (вверх по состоянию)
if (nvStateTo >= 300.nn && nvStateFrom < 300.nn) {
//проверки для документа при выполнении
validateOnAccept(rop)
//действия на выполнен
accept(rop)
} else if (nvStateTo < 300.nn && nvStateFrom >= 300.nn) {
//проверки для документа при откате
validateOnDecline(rop)
//действия на откат из выполнен
decline(rop)
}
super.setidState(rop, value)
//добавление в очередь отложенного проведения документов
//Bs_DocTransQueueApi().addToQueue(rop.gid, rop.get(_.idObjectType), nvStateFrom, nvStateTo)
}
Различные проверки должны выполняться, как правило, в начале перевода состояния, чтобы было как можно меньше логики выполнено, до получения ошибки
Если у документа есть коллекции, а у коллекций есть еще коллекции, то на перевод состояния сначала можно выполнить загрузку в кэш
/**
* прогрузка в кэш самого документа с коллекциями
*
* @param idap
*/
private def loadDocWithCollections(idp:NLong): Unit = {
new OQuery(entityAta.Type) {
where(t.id === idp)
batchIn(Demo_DocDetAta, Demo_DocDetItemAta, Stk_OperationAta)
}.foreach { rop => }
}
Создаем метод
regState
для регистрация состояний (здесь просто написан пример)
def regState(): Unit = {
session.commit()
Btk_Pkg().setRWSharedUOWEditType()
//первоначальное состояние
Btk_ClassStateApi().register(
idpMasterClass = idClass,
spSystemName = "Create",
spCaption = "Оформляется",
bpStartState = 1.nn,
npOrer = 100.nn
)
session.flush()
Btk_ClassStateApi().register(
idpMasterClass = idvClass,
spSystemName = "Annulled",
spCaption = "Аннулирован",
bpStartState = 0.nn,
npOrer = 50.nn)
Btk_ClassStateApi().register(
idpMasterClass = idClass,
spSystemName = "Agreed",
spCaption = "Согласуется",
bpStartState = 0.nn,
npOrer = 200.nn)
Btk_ClassStateApi().register(
idpMasterClass = idClass,
spSystemName = "Executed",
spCaption = "Выполнен",
bpStartState = 0.nn,
npOrer = 300.nn
)
session.commit()
}
Создаем метод
regTab
для регистрация закладок (здесь просто написаны примеры)
//регистрация закладок
def regTab(): Unit = {
session.commit()
Btk_Pkg().setRWSharedUOWEditType()
Btk_TabApi().register(
spCaption = "Позиции",
spSel = "gtk-Demo_DocDetAvi",
spRep = "List_idDoc",
idprefClass = idClass
)
Btk_TabApi().register(
spCaption = "Прикрепленные файлы"
, spSel = "gtk-Btk_AttachItemAvi"
, spRep = "List_SimpleAttach"
, idprefClass = idClass
)
Btk_TabApi().register(
spCaption = "Объектные характеристики",
spSel = "gtk-Demo_DocAvi",
spRep = "Card_ObjectAttr",
idprefClass = idClass
)
Btk_TabApi().register(
spCaption = "Связанные документы"
, spSel = "gtk-Bts_DocLinkAvi"
, spRep = "Tree"
, idprefClass = idClass
)
Btk_TabApi().register(
spCaption = "Проводки",
spSel = "gtk-Act_TransAvi",
spRep = "List_gidDoc",
idprefClass = idClass
)
Btk_TabApi().register(
spCaption = "Настройка счетов учета",
spSel = "gtk-Bs_AccObjSettingAvi",
spRep = "List_gidSrc",
idprefClass = idClass
)
session.commit()
}
Создаем метод
regObjectType
для регистрация типов объекта (здесь просто написан пример)
//регистрация типов
def regObjectType(bpNeedRefresh: Boolean = false): Unit = {
session.commit()
Btk_Pkg().setRWSharedUOWEditType()
//параметр bpNeedRefresh = false нужен, чтобы не затирать данные на проектных базах, если такого типа объекта нет, то он будет создан
//если хотите все перезаписать, то выполните в Jexl скрипте Demo_DocApi.regObjectType(true);
if (bpNeedRefresh || Btk_ObjectTypeApi().findByMnemoCodeAndClass("Demo_DocObjType", idClass).isNull) {
//регистрируем тип
val idvOT = Btk_ObjectTypeApi().register(spCode = "Demo_DocObjType"
, spCaption = "Тип А"
, spShortCaption = "Документ А"
, idpRefClass = idClass
, bpIsDefault = 1.nn)
//ищем закладку и привязываем к типу
var idvTab = Btk_TabApi().findByMnemoCode("Demo_DocAvi.List_idDoc")
if (idvTab.isNotNull)
Btk_ObjectTypeTabApi().registerTab(
idpObjectType = idvObjectType
, idpTab = idvTab
, spCaption = "Позиции"
, npOrder = 10.nn
)
idvTab = Btk_TabApi().findByMnemoCode("Demo_DocAvi.Card_ObjectAttr")
if (idvTab.isNotNull)
Btk_ObjectTypeTabApi().registerTab(
idpObjectType = idvObjectType
, idpTab = idvTab
, spCaption = "Характеристики"
, npOrder = 20.nn
)
//регистрируем переходы состояний для типа
Btk_StateChangeApi().registerForObjectType(idvOT, List(
"Create" -> "Agreed",
"Agreed" -> "Executed",
"Agreed" -> "Create",
"Executed" -> "Create",
"Executed" -> "Agreed"
))
}
session.commit()
}
Api коллекции#
Переопределяем insertByParent для установки порядкового номера при создании
override def insertByParent(ropParent: AnyRop): ApiRop = {
val nvRow = byParent(ropParent).map(_.get(_.nRow).nvl(0.nn)).reduceOption(_ max _).getOrElse(0.nn) + 1.nn
super.insertByParent(ropParent) :/ { rop =>
setnRow(rop, nvRow)
rop
}
}
Avi документа:#
для отображения List#
Переопределяем
onRefreshItem
, чтобы выполнялся запрос изselectStatement
override protected def onRefreshItem: Recs = {
prepareSelectStatement(s"t.$getPKFieldName = :$getPKFieldName")
}
Переопределять
onRefreshExt
в списках не нужно, так как весь запрос должен быть вselectStatement
Переопределяем CWA, чтобы дописать блокировку кнопки удаления, если документ находится не в состоянии оформляется
override def checkWorkability(): Unit = {
super.checkWorkability()
val bvRO = getSelfVar("idStateMC").asNNumber.isDistinct(100.nn)
opers().setEnabled("Delete", selection.canDelete && !getVar("ID").isNull && !bvRO)
}
Если нужно передать параметры в карточку при создании из списка, надо переопределить метод
insert_Params
, и затем обработать данный параметр в отображении для карточки в методеonInsertItem
.
Пример передачи организации из фильтра списка.
override def insert_Params(): Map[String, Any] = {
Map("idDepOwner#" -> getVar("flt_idDepOwner").asNLong)
}
Если нужно сделать фильтр по состоянию или типу, надо добавить на
beforeFirstOpen
переменные со значением id нашего класса, чтобы работали стандартные вып. списки для типа и состояния
override protected def beforeFirstOpen(): Unit = {
super.beforeFirstOpen()
addVar("idStateClass#", thisApi().idClass, FieldType.ftInteger) // для фильтра по состоянию
addVar("idObjTypeClass", thisApi().idClass, FieldType.ftInteger) // для фильтра по типу
}
Для установки значений по умолчанию при открытии для полей в фильтре c датами и организации, значения которых будут браться из глобального фильтра, надо использовать значения по умолчанию через Avm, а не через
beforeFirstOpen
для отображения Card - шапки#
Переопределяем
onInsertItem
для установки организации из глобального фильтра при создании
override protected def onInsertItem(): Unit = {
super.onInsertItem()
//если был передана организация через параметр
val idvDepOwner = getSelfVar("idDepOwner#").asNLong.nvl(getVar("super$idGlobalDepOwner").asNLong)
if (idvDepOwner.isNotNull) thisApi().setidDepOwner(thisRop(), idvDepOwner)
}
Если есть закладки или детальные формы, то переопределяем refresh, чтобы при нажатии обновить обновлялись детали тоже
override def refresh(): Unit = {
super.refresh()
selection.refreshDetails()
}
Переопределяем
saveForm
, чтобы прописать обновление карточки, если есть автонумерующиеся атрибуты
@Oper(refreshAfter = true)
override def saveForm(): Unit = super.saveForm()
//или так
override def saveForm(): Unit = {
super.saveForm()
selection.refreshItem()
}
Если нужна операция с молоточком и ключом, то устанавливаем для нее активность
@Oper(active = true)
override def extraOperations() = super.extraOperations()
Переопределяем
setidState
, чтобы дописать различные действия
override def setidState(event: SetterEvent): Unit = {
super.setidState(event)
//завершение транзакции
session.commit()
//снятие блокировки
Btk_FormSessionApi().closeLockUnit()
//обновление карточки
selection.refreshItem()
//вызов checkWorkability в карточке
selection.checkWorkability()
//вызов checkWorkability в деталях
selection.cwaDetails()
//если нужно обновление деталей вместо cwaDetails используем selection.refreshDetails
}
Переопределяем CWA, для блокировки полей и операций в зависимости от состояния
//для управлением редактируемостью атрибутов
private def setAttrReadOnly(): Unit = {
val avAttrsNotApply = Set("DREG", "SREGNUM", "IDSTATE", "IDSTATEHL")
val bvStNotForm = thisRop().get(_.idStateMC).isDistinct(100.nn)
attrs().filterNot(f => avAttrsNotApply.contains(f.name.toUpperCase))
.foreach(_.isReadOnly = bvStNotForm)
}
//для управлением видимостью атрибутов
private def setAttrVisible(): Unit = {
//(A.idObjectType + "HL").isVisible = false
}
//для управлением активность операций
private def setEnabledOpers(): Unit = {
val bvStNotForm = thisRop().get(_.idStateMC).isDistinct(100.nn)
val bvStAcc = thisRop().get(_.idStateMC) >= 300.nn
opers("fillDoc").isEnabled = !bvStNotForm
opers("clonePrint").isEnabled = bvStAcc
}
override def checkWorkability(): Unit = {
super.checkWorkability()
//для управлением редактируемостью атрибутов
setAttrReadOnly
//для управлением видимостью атрибутов
setAttrVisible
//для управлением активность операций
setEnabledOpers
}
реляционные запросы в onRefresh в карточках не следует использовать, так как если будет написан запрос, то перед выполнением произойдет сразу СОХРАНЕНИЕ. Все доп. поля пишем либо через onRefreshExt, additionalInfo или case class.
если переопределяете LookUp или пишите свои вып. списки, не забывайте дописывать
@FlushBefore(mode = FlushBeforeMode.Disabled)
, так как это тоже приводит к коммиту при вызове запроса
import ru.bitec.app.gtk.gl.{FlushBefore, FlushBeforeMode}
@FlushBefore(mode = FlushBeforeMode.Disabled)
override protected def onRefresh: Recs
Avi коллекции#
Переопределяем onRefresh, чтобы добавить сортировку по №п/п
override def onRefresh: Recs = {
thisApi().refreshByParent(getIdMaster).toList.sortBy(_.get(_.nRow))
}
реляционные запросы в onRefresh в коллекциях не следует использовать, так как если будет написан запрос, то перед выполнением произойдет сразу СОХРАНЕНИЕ. Все доп. поля пишем либо через onRefreshExt, additionalInfo или объектный
List[case class]
.Переопределяем CWA, для блокировки полей и операций, в зависимости от состояния
//для управлением редактируемостью атрибутов
private def setAttrReadOnly(): Unit = {
val bvStNotForm = getVar("super$idStateMC").asNNumber.isDistinct(100.nn)
attrs().foreach(_.isReadOnly = bvStNotForm)
}
//для управлением видимостью атрибутов
private def setAttrVisible(): Unit = {
//(A.idGds + "HL").isVisible = false
}
//для управлением активность операций
private def setEnabledOpers(): Unit = {
val bvStNotForm = getVar("super$idStateMC").asNNumber.isDistinct(100.nn)
opers("insert").isEnabled = getIdMaster.isNotNull && !bvStNotForm
opers("delete").isEnabled = !bvStNotForm && !A.id.isNull
opers("copyObject").isEnabled = !bvStNotForm && !A.id.isNull
}
override def checkWorkability(): Unit = {
super.checkWorkability()
//для управлением редактируемостью атрибутов
setAttrReadOnly
//для управлением видимостью атрибутов
setAttrVisible
//для управлением активность операций
setEnabledOpers
}
Avm документа#
Для атрибута тип документа в отображении Default устанавливаем тип редактора - выпадающий список, чтобы отображались типы только нашего класса
<attr name="idObjectTypeHL" caption="Тип" order="50.2" editorType="lookup" isLastInLine="false" isVisible="true">
<editor>
<lookup lookupQuery="gtk-Btk_ObjectTypeAvi#MainLookup" isLookupLazyLoad="true"
changeableAttr="idObjectType" lookupKeyAttr="id" lookupListAttr="sHeadLine"
isResetButtonVisible="true"/>
</editor>
</attr>
Для атрибута организация в отображении Default устанавливаем тип редактора - выпадающий список
<attr name="idDepOwnerHL" caption="Организация" order="70.2" isRequired="true">
<editor>
<lookup lookupQuery="gtk-Bs_DepOwnerAvi#Lookup"
changeableAttr="idDepOwner" isLookupLazyLoad="false"
lookupKeyAttr="id" lookupListAttr="sHeadLine" isResetButtonVisible="false"/>
</editor>
</attr>
Устанавливаем закладки от типа документа, по умолчанию видимые в карточке и не видимые в списке (
isVisible="false"
)
<tabItems isVisible="true" selection="gtk-Btk_ObjectTypeTabAvi"
representation="List_Tab"
selection.selectionAttr="SSELECTIONNAME"
selection.representationAttr="SREPRESENTATIONNAME"
selection.captionAttr="SCAPTION"
selection.imageIndexAttr="NIMAGE"
selection.paramsAttr="JSONPARAMS"
/>
Если есть поля в фильтре, значения которых должны браться из глобального фильтра (например по датам и организации), то прописываем значения по умолчанию через
defaultValue
<!--Период с-->
<condition id="filterFrom" logicalOperator="and" isExpression="true"
expression=":flt_dFrom <= t.dPeriod">
<filterAttr attribute-type="Date" name="flt_dFrom" editorType="datePick" order="10.0"
caption="Период с" isLastInLine="false" defaultValue="super$DGLOBALBEGINDATE">
<card controlWidth="24" isControlWidthFixed="true"/>
</filterAttr>
</condition>
<!--Период по-->
<condition id="filterTo" logicalOperator="and" isExpression="true"
expression=":flt_dTo >= t.dPeriod">
<filterAttr attribute-type="Date" name="flt_dTo" editorType="datePick" order="20.0" caption="по"
isLastInLine="false" defaultValue="super$DGLOBALENDDATE">
<card controlWidth="20" isControlWidthFixed="true"/>
</filterAttr>
</condition>
<!--План счетов-->
<condition logicalOperator="and" id="filterIdAdjustMethod" isExpression="true"
expression="t.idAdjustMethod = :flt_idAdjustMethod">
<filterAttr name="flt_idAdjustMethod" attribute-type="Long" isVisible="false" order="30"
defaultValue="super$idGlobalAdjustMethod"/>
<filterAttr name="flt_idAdjustMethodHL" attribute-type="Varchar" caption="План счетов" order="30.1"
editorType="lookup" isLastInLine="false">
<editor>
<lookup lookupQuery="gtk-Bs_AdjustMethodAvi#Lookup"
lookupKeyAttr="id" lookupListAttr="sHeadLine" changeableAttr="flt_idAdjustMethod"
isLookupLazyLoad="false" isResetButtonVisible="true"/>
</editor>
</filterAttr>
</condition>
<!--Организация-->
<condition logicalOperator="and" id="filterIdDepOwner" isExpression="true"
expression="t.idDepOwner = :flt_idDepOwner">
<filterAttr name="flt_idDepOwner" attribute-type="Long" caption="Организация" order="50"
defaultValue="super$idGlobalDepOwner" isVisible="false"/>
<filterAttr name="flt_idDepOwnerHL" attribute-type="Varchar" caption="Организация" order="50.2"
editorType="lookup" isLastInLine="false">
<editor>
<lookup lookupQuery="gtk-Bs_DepOwnerAvi#Lookup"
changeableAttr="flt_idDepOwner" isLookupLazyLoad="false"
lookupKeyAttr="id" lookupListAttr="sHeadLine" isResetButtonVisible="true"/>
</editor>
</filterAttr>
</condition>