Практики при разработке документов#

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 &lt;= 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 &gt;= 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>