Party form-list with PartyRelationship

I am trying to create a screen that displays all Sales Accounts (role=Account) and the assigned sales person. Sales person is assigned using a PartyRelationship. This screen also needs the ability to filter on a particular sales person, so for example I can limit the results to only those Accounts assigned to me, or filter on another sales person to see all of their accounts. In the future I may also want to be able to enforce what the user can and cannot see, for example only allow reps to see their own accounts or the accounts of everyone that roles up to them (i.e, a sales manager).
I am using the FindParty and FindCustomer screens as examples. These work for building the initial screen but I cannot find an example of incorporating a PartyRelationship. If anyone knows of a good example of this, or more generally using any join table as part of a form list and search, please comment with the relevant screen xml file name(s).
FindParty and FindCustomer both call mantle.party.PartyServices.search#Party to retrieve the date for rendering. Is this approach only using Elastic, or is it possible to incorporate other entities that are not included in the data document definition? If not what are the possible approaches? 1) Add PartyRelationship to the data document definition, 2) and/or write a custom search service, 3) use entity-find? Other possible approaches? I want to use Elastic for search performance and I also want the ability to save searches.

You might take a look the moqui.security.EntityFilterSet and and EntityFilter.

MantleInstallData.xml has one defined for PartyRelationship (though either we’ve commented it out or it is commented out), that limits the PartyRelationship query whenever it is used. You could use the same concept if you want to bring the User ID into play I’d suspect…and it’d add those as conditions to your queries automatically.

I’m not sure about the ElasticSearch part of your question though…as to whether those honor the filter sets…we do have custom searches for most things that do not use ElasticSearch.

The filter sets can be a little hard to spot once you introduce them, as they just add the statements into the SQL for you, so if not written well, the filters can cause you grief. You can use the QueryStats in Tools to see your SQL query and find them…I’m not sure again on ElasticSearch.

Thanks @sbessire, the filter sets look interesting and is a feature I was not aware of. By the way, the file name is MantleSetupData.xml. You are correct, the record for PartyRelationship is commented out.
Not sure it directly addresses the problem i am trying to solve but I need to explore it more.
Simpler statements of my requirements are::

  1. Create a list view (form-list) of Sales Accounts and the associated Sales Representatives.
  2. Have the ability to search based on the sales rep.
    Other more complex requirements will come but right now I am just trying to learn how to do these two simple things. I have the first one working but not sure how I have done it is the best way.

Just updating this thread as my learning curve continues… I have read the documentation here. Moqui Documentation, and have come to the conclusion that I need to define a view entity. None of the existing view entities include both PartyRole AND PartyRelationship as member entities. It also seems that this is the best approach as I have not found any examples of defining a query with joins in entity-find (I think you could do this in OFBiz?) Maybe this is by design in Moqui. Is it possible to extend an existing view entity or do I need to create a new one?

There is an extend-entity element…e.g.:

    <extend-entity entity-name="UomConversion" package="moqui.basic">
        <field name="commodityProductCategoryId" type="id"/>
        <field name="sizeProductFeatureId" type="id"/>
        <relationship type="one" title="Commodity" related="mantle.product.category.ProductCategory" short-alias="commodityProductCategory">
            <key-map field-name="commodityProductCategoryId" related="productCategoryId"/></relationship>
        <relationship type="one" title="Size" related="mantle.product.feature.ProductFeature" short-alias="sizeProductFeature">
            <key-map field-name="sizeProductFeatureId" related="productFeatureId"/></relationship>
    </extend-entity>

If you are not adding new fields to one though…and just looking to do a join, then yes, the view-entity is your friend…it essentially allows you to save all your joins so they can be used later on.

For drop-downs, we have gone away from elastic-search and tried to make them custom throughout.

So, we have a PartyWidgetTemplates.xml…with things like this in it (with commented code for the transition-include and widget-template include, so a developer can just grab those two lines):



    <!-- Defaults to restricting active org if present -->
    <!--
        <transition-include name="getEmployeeList" location="component://YOURCOMPONENT/template/party/PartyTransitions.xml"/>
        <widget-template-include location="component://YOURCOMPONENT/template/party/PartyWidgetTemplates.xml#employeeDropDownInf"/>
    -->
    <widget-template name="employeeDropDownInf">
        <drop-down allow-empty="${allowEmpty ?: 'false'}" allow-multiple="${allowMultiple ?: 'false'}"
                   no-current-selected-key="${noCurrentSelectedKey?:''}" style="${style?:''}"
                   show-not="${showNot ?: 'false'}">
            <dynamic-options transition="getEmployeeList" server-search="true" min-length="2"/>
        </drop-down>
    </widget-template>

And a PartyTransitions.xml…with different transitions for how we might want a drop-down to work:

    <!--
        <transition-include name="getEmployeeList" location="component://YOURCOMPONENT/template/party/PartyTransitions.xml"/>
    -->
    <transition name="getEmployeeList">
        <parameter name="term"/>
        <parameter name="ignoreTerm"/>            <!-- 'true' ignores term; e.g. for a non-server search dependent dropdown -->
        <parameter name="fromDate"/>              <!-- empty -->
        <parameter name="includeDisabled"/>       <!-- 'true' includes disabled -->
        <parameter name="excludePartyId"/>        <!-- partyId or or comma separated list of party ids -->

        <parameter name="toPartyId"/>             <!-- If provided, pares down by employer party id -->
        <parameter name="partyRelationshipId"/>   <!-- If provided, pares down by partyRelationshipId or comma separated partyRelationshipIds -->
        <parameter name="activeOrgId"/>           <!-- only used if restrictActiveOrg -->
        <parameter name="restrictActiveOrg"/>     <!-- restricts by default; restricts if !'false' and activeOrgId -->

        <parameter name="timePeriodId"/>
        <parameter name="payrollBatchId"/>              <!-- Further restrict time period by payrollBatchId -->
        <parameter name="restrictToPeriodPrePayroll"/>  <!-- 'true' uses time period and payroll batch to pare down -->
        <parameter name="restrictToPeriodPayHistory"/>  <!-- 'true' uses time period to pare down by payroll history -->
        <parameter name="restrictToPeriodInvoice"/>     <!-- 'true' uses time period to pare down by payroll invoice -->

        <parameter name="paginate"/>              <!-- paginates by default; 'false' to skip-->
        <parameter name="overrideOrderBy"/>  <!-- if present, set orderByField to this value -->
        <actions>
            <set field="term" from="term ? (ignoreTerm == 'true' ? '': term) : ''"/>

            <set field="shouldPaginate" from="!(paginate == 'false')"/>
            <if condition="shouldPaginate"><then>
                <set field="pageSize" from="pageSize ?: 20"/>
            </then><else>
                <set field="pageSize" from="null"/> <!-- Necessary for DropDownAll Widget Templates to get all results -->
            </else></if>
            <if condition="overrideOrderBy">
                <set field="orderByField" from="overrideOrderBy"/>
            </if>

            <entity-find-one entity-name="mantle.party.PartyDetail" value-field="partyDetail">
                <field-map field-name="pseudoId" from="term"/>
            </entity-find-one>
            <if condition="partyDetail == null"><then>
                <!-- Check for an exact match on the actual partyId; this is what dropdowns start with -->
                <entity-find-one entity-name="mantle.party.PartyDetail" value-field="partyDetail">
                    <field-map field-name="partyId" from="term"/>
                </entity-find-one>
            </then><else-if condition="partyDetail.partyId != partyDetail.pseudoId">
                <!-- Sanity check the found pseudoId still matches the search criteria (or most of it). -->
                <if condition="!(includeDisabled == 'true') &amp;&amp; partyDetail.disabled == 'Y'"><then>
                    <set field="partyDetail" from="null"/>
                </then><else>
                    <entity-find entity-name="mantle.party.PartyRelationship" list="partyRelList" limit="1">
                        <search-form-inputs skip-fields="fromRoleTypeId,toRoleTypeId,toPartyId,partyRelationshipId,partyId,
                                                         fromDate,thruDate"/>
                        <econdition field-name="fromRoleTypeId" value="Employee"/>
                        <econdition field-name="toRoleTypeId" value="OrgEmployer"/>
                        <econdition field-name="toPartyId" ignore-if-empty="true"/>
                        <econdition field-name="toPartyId" from="activeOrgId" ignore="!activeOrgId || restrictActiveOrg == 'false'"/>
                        <econdition field-name="partyRelationshipId" operator="in" ignore-if-empty="true"/>
                        <econdition field-name="fromPartyId" from="partyDetail.partyId"/>
                        <econdition field-name="fromPartyId" operator="not-in" from="excludePartyId" ignore-if-empty="true"/>
                        <date-filter valid-date="fromDate" ignore-if-empty="true"/>
                        <select-field field-name="partyRelationshipId"/>
                    </entity-find>
                    <if condition="!partyRelList"><set field="partyDetail" from="null"/></if>
                </else></if>
            </else-if></if>

            <if condition="partyDetail != null"><then>
                <set field="partyDetailList" from="[partyDetail]"/>
            </then><else>

                <if condition="fromDate &amp;&amp; fromDate?.class != java.sql.Timestamp">
                    <set field="fromDate" from="ec.l10n.parseTimestamp(fromDate, 'yyyy-MM-dd HH:mm:ss.SSS', ec.user.locale, ec.user.getTimeZone())" type="Timestamp"/>
                </if>

                <!-- Split and possibly overwrite term. -->
                <service-call name="sssonline.YOURCOMPONENT.PartyServices.parse#PartyFindTerm" in-map="context" out-map="context"/>

                <!-- If it looks like a 9 digit number, attempt to treat it like it has SSN format too -->
                <if condition="term.size() == 9 &amp;&amp; term.isInteger()">
                    <set field="termAsDashedSsn" from="term.substring(0,3) + '-' + term.substring(3,5) + '-' + term.substring(5)"/>
                </if>

                <set field="entityName" value="YOURCOMPONENT.humanres.employment.EmploymentFromFindDetail"/>
                <if condition="(timePeriodId || payrollBatchId) &amp;&amp; (restrictToPeriodPayHistory == 'true' || restrictToPeriodInvoice == 'true')"><then>
                    <set field="entityName" value="YOURCOMPONENT.humanres.employment.EmploymentPayHistoryFromFindDetail"/>
                </then><else-if condition="payrollBatchId &amp;&amp; (restrictToPeriodPrePayroll == 'true')">
                    <set field="entityName" value="YOURCOMPONENT.humanres.employment.PayrollBatchFromFindDetail"/>
                </else-if></if>
                <entity-find entity-name="${entityName}" list="partyDetailList" limit="20" distinct="true">
                    <search-form-inputs default-order-by="lastName,firstName,middleName,suffix,organizationName" paginate="shouldPaginate"
                                        skip-fields="fromRoleTypeId,toRoleTypeId,toPartyId,partyRelationshipId,partyId,pseudoId,
                                                     firstName,firstAndLastName,combinedName,lastName,lastNameFirst
                                                     idValue,fromDate,thruDate"/>
                    <econdition field-name="fromRoleTypeId" value="Employee"/>
                    <econdition field-name="toRoleTypeId" value="OrgEmployer"/>
                    <econdition field-name="toPartyId" ignore-if-empty="true"/>
                    <econdition field-name="toPartyId" from="activeOrgId" ignore="!activeOrgId || restrictActiveOrg == 'false'"/>
                    <econdition field-name="partyRelationshipId" operator="in" ignore-if-empty="true"/>
                    <econdition field-name="partyId" operator="not-in" from="excludePartyId" ignore-if-empty="true"/>
                    <econdition field-name="invoiceId" operator="is-not-null" ignore="!((timePeriodId || payrollBatchId) &amp;&amp; restrictToPeriodInvoice == 'true')"/>
                    <econditions combine="or">
                        <econdition field-name="partyId" operator="like" value="%${term}%" ignore-case="true"/>
                        <econdition field-name="pseudoId" operator="like" value="%${term}%" ignore-case="true"/>
                        <econdition field-name="firstName" operator="like" value="%${term}%" ignore-case="true"/>
                        <econdition field-name="firstAndLastName" operator="like" value="%${term}%" ignore-case="true"/>
                        <econdition field-name="combinedName" operator="like" value="%${term}%" ignore-case="true"/>
                        <econdition field-name="lastName" operator="like" value="%${term}%" ignore-case="true"/>
                        <econdition field-name="lastNameFirst" operator="like" value="%${term}%" ignore-case="true"/>
                        <!--<econdition field-name="idValue" operator="like" value="%${term}%" or-null="true"/>-->
                        <econdition field-name="idValue" from="term"/>
                        <econdition field-name="idValue" from="termAsDashedSsn" ignore-if-empty="true"/>
                    </econditions>
                    <econditions>      <!-- These come from parse#PartyFindTerm -->
                        <econdition field-name="organizationName" operator="like" value="%${termOrganizationName}%" ignore-case="true" ignore="!termOrganizationName"/>
                        <econdition field-name="firstName" operator="like" value="%${termFirstName}%" ignore-case="true" ignore="!termFirstName"/>
                        <econdition field-name="middleName" operator="like" value="%${termMiddleName}%" ignore-case="true" ignore="!termMiddleName"/>
                        <econdition field-name="lastName" operator="like" value="%${termLastName}%" ignore-case="true" ignore="!termLastName"/>
                        <econdition field-name="suffix" operator="like" value="%${termSuffix}%" ignore-case="true" ignore="!termSuffix"/>
                        <econdition field-name="pseudoId" operator="like" value="%${termPseudoId}%" ignore-case="true" ignore="!termPseudoId"/>
                    </econditions>
                    <date-filter valid-date="fromDate" ignore-if-empty="true"/>
                    <select-field field-name="partyId,pseudoId,lastName,middleName,firstName,suffix,organizationName,idValue"/>
                </entity-find>
            </else></if>

            <!--<log level="warn" message="partyDetailList: ${partyDetailList}"/>-->
            <!--<log level="warn" message="find: ${partyDetailList_xafind}"/>-->

            <script>
                def outList = []
                for (party in partyDetailList) outList.add([value:party.partyId, label:ec.resource.expand("PartyNameTemplate", "", party)])
                ec.web.sendJsonResponse([options:outList, pageSize:pageSize, count:(partyDetailListCount?:partyDetailList.size())])
            </script>

        </actions>
        <default-response type="none"/>
    </transition>

You won’t find some of those view-entities…they’re joins we made.

Anyway, other than when you have to do a dependent drop-down…we do not let developers roll their own drop-downs…they have to use a widget-template-include…that way a fix is common to all of them.

In a screen form, the field definition might look like this:


                    <field name="fromPartyId">
                        <default-field title="" tooltip="Employee">
                            <widget-template-include location="component://YOURCOMPONENT/template/party/PartyWidgetTemplates.xml#employeeDropDownInf">
                                <set field="style" value="submit-on-close min-width-250"/>
                                <set field="includeDisabled" value="true"/>
                                <set field="allowEmpty" value="true"/>
                            </widget-template-include>
                        </default-field>
                    </field>

And yes, it is intentional that it passes the string true instead of a boolean…in hindsight, we’d have used a Y/N indicator instead, but…that ship has sailed.

Also, remember, even if you specify parameters at the top of a transition, it is just informational; transitions don’t use them like service-calls do, nor are they limited by the ones you list. Anything in the context is fair-game, which is why it is nice over time to be able to update the common drop-down logic…as you may find that it becomes necessary in the entity-finds to skip fields that sneak in from the context and become part of the where clause without that being the intent. In general, we exclude every field we intend to filter by in the skip fields section so that the only e-conditions that fire are the ones we intend…but, like I say…anything from the context is fair game if it matches a field in the entity or view-entity…so, over time you may have to exclude others.

1 Like