# Практики при разработке документов ## 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 ```