# Практики при разработке документов
## Odm документа
- Для документов создаем отдельную директорию (пакет), чтобы все коллекции были тоже в ней
- У полей с датами в названии системного имени НЕ пишем слово Date \
пример: \
dReg \
dDoc \
dExec \
- Для поля с датой регистрации, ставим значение по умолчанию текущую дату `defaultValue="sysdate"`
- Для полей с номерами, датой рег., состоянием и номером состояния устанавливаем `isCopyInCopyObject="false"`, чтобы значения не копировались при копировании
- Для полей с номером, датой и типом объекта устанавливаем свойство `isHeadLine="true"`, так как они в большинстве случаем участвуют в вычислении заголовка
- Добавляем поле с номером состояния, не видимое
```xml
```
- Большинство полей можно взять из шаблона `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 напишите свой класс)
```xml
```
- Если нужны прикрепленные файлы устанавливаем свойство в шапку `attachType="simple"` или `attachType="versioned"` для версионного режима
- Если нужна настройка по счетам учета, то добавляем в коллекции настройку ``
## Odm коллекции документа
- Наименование коллекций, как правило, содержит имя документа (может быть и сокращенное) и суффикс `Det` или какой то другой, например `Demo_DocDet`
- Порядковый номер для позиций следует наименовать как `nRow - № п/п` и делать его числовым, наименование `nOrder` путает с `Bs_Order`. Его не следует делать автонумерующимся, так как нумерация срабатывает на сохранение. Нумерацию можно сделать руками в `Api.insertByParent` (ниже будет пример)
## Api документа:
- Вместо `ListBuffer` следует использовать `ArrayBuffer`, так как он меньше занимает места в памяти
- Переопределяем calcHeadLine, для вычисление заголовка с учетом типа документа, номера и даты
```scala
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 для установки типа объекта по умолчанию и других атрибутов (например валюта и тип курса) при создании
```scala
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, чтобы нельзя было удалить документ, если он находится не в состоянии `Оформляется`
```scala
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)
}
```
- Если есть настройка по счетам учета, то на установку типа объекта и организации прописываем установку счетов учета для документа
```scala
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. \
Пример действий на перевод состояний:
```scala
//проверки для документа при выполнении
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)
}
```
- Различные проверки должны выполняться, как правило, в начале перевода состояния, чтобы было как можно меньше логики выполнено, до получения ошибки
- Если у документа есть коллекции, а у коллекций есть еще коллекции, то на перевод состояния сначала можно выполнить загрузку в кэш
```scala
/**
* прогрузка в кэш самого документа с коллекциями
*
* @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` для регистрация состояний (здесь просто написан пример)
```scala
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` для регистрация закладок (здесь просто написаны примеры)
```scala
//регистрация закладок
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` для регистрация типов объекта (здесь просто написан пример)
```scala
//регистрация типов
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 для установки порядкового номера при создании
```scala
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`
```scala
override protected def onRefreshItem: Recs = {
prepareSelectStatement(s"t.$getPKFieldName = :$getPKFieldName")
}
```
- Переопределять `onRefreshExt` в списках не нужно, так как весь запрос должен быть в `selectStatement`
- Переопределяем CWA, чтобы дописать блокировку кнопки удаления, если документ находится не в состоянии оформляется
```scala
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`. \
Пример передачи организации из фильтра списка.
```scala
override def insert_Params(): Map[String, Any] = {
Map("idDepOwner#" -> getVar("flt_idDepOwner").asNLong)
}
```
- Если нужно сделать фильтр по состоянию или типу, надо добавить на `beforeFirstOpen` переменные со значением id нашего класса, чтобы работали стандартные вып. списки для типа и состояния
```scala
override protected def beforeFirstOpen(): Unit = {
super.beforeFirstOpen()
addVar("idStateClass#", thisApi().idClass, FieldType.ftInteger) // для фильтра по состоянию
addVar("idObjTypeClass", thisApi().idClass, FieldType.ftInteger) // для фильтра по типу
}
```
- Для установки значений по умолчанию при открытии для полей в фильтре c датами и организации, значения которых будут браться из глобального фильтра, надо использовать значения по умолчанию через Avm, а не через `beforeFirstOpen`
### для отображения Card - шапки
- Переопределяем `onInsertItem` для установки организации из глобального фильтра при создании
```scala
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, чтобы при нажатии обновить обновлялись детали тоже
```scala
override def refresh(): Unit = {
super.refresh()
selection.refreshDetails()
}
```
- Переопределяем `saveForm`, чтобы прописать обновление карточки, если есть автонумерующиеся атрибуты
```scala
@Oper(refreshAfter = true)
override def saveForm(): Unit = super.saveForm()
//или так
override def saveForm(): Unit = {
super.saveForm()
selection.refreshItem()
}
```
- Если нужна операция с молоточком и ключом, то устанавливаем для нее активность
```scala
@Oper(active = true)
override def extraOperations() = super.extraOperations()
```
- Переопределяем `setidState`, чтобы дописать различные действия
```scala
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, для блокировки полей и операций в зависимости от состояния
```scala
//для управлением редактируемостью атрибутов
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)`, так как это тоже приводит к коммиту при вызове запроса
```scala
import ru.bitec.app.gtk.gl.{FlushBefore, FlushBeforeMode}
@FlushBefore(mode = FlushBeforeMode.Disabled)
override protected def onRefresh: Recs
```
## Avi коллекции
- Переопределяем onRefresh, чтобы добавить сортировку по №п/п
```scala
override def onRefresh: Recs = {
thisApi().refreshByParent(getIdMaster).toList.sortBy(_.get(_.nRow))
}
```
- реляционные запросы в onRefresh в коллекциях не следует использовать, так как если будет написан запрос, то перед выполнением произойдет сразу СОХРАНЕНИЕ.
Все доп. поля пишем либо через onRefreshExt, additionalInfo или объектный `List[case class]`.
- Переопределяем CWA, для блокировки полей и операций, в зависимости от состояния
```scala
//для управлением редактируемостью атрибутов
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 устанавливаем тип редактора - выпадающий список, чтобы отображались типы только нашего класса
```xml
```
- Для атрибута организация в отображении Default устанавливаем тип редактора - выпадающий список
```xml
```
- Устанавливаем закладки от типа документа, по умолчанию видимые в карточке и не видимые в списке (`isVisible="false"`)
```xml
```
- Если есть поля в фильтре, значения которых должны браться из глобального фильтра (например по датам и организации), то прописываем значения по умолчанию через `defaultValue`
```xml
```