Cap a Yearly Allowance per Employee on Payment Requests
This Groovy script turns the payment request into the tool for a yearly staff allowance, such as a medical allowance. Every claim is checked against what the same employee has already been paid this year, item by item and in total, and a claim that goes over the limit is refused.
Customer-specific example
This script works, but it is hard-wired to one setup:
- It applies only when Paid From is the record with code
health(the lineEntityCodeDF.fromString("health")). Change that code to your own cash box or bank account. - It uses the request lines' To, Ref 1, N1, N2 and N3, and the detailed remark's N1, for fixed purposes, as described below.
Adapt the script before you use it if any of these differ in your setup.
| Class name | EAYearlyLimitPerEmployeeInPVRequest |
| Script | EAYearlyLimitPerEmployeeInPVRequest.groovy |
| Runs on | Payment Request, on Validate On Save |
| Uses | Parameter 2. Leave Parameter 1 empty. |
| Shipped as | Source to paste. It is not built into the system. |
When to use it
Suppose a company covers its staff's medical bills up to a yearly limit: up to 300 a year for glasses, up to 500 for dental care, and never more than 1,000 a year in total for one employee. Claims are paid from a dedicated cash box with code health, through payment requests. Each line of a request is one claim: the employee in To, the kind of claim in Ref 1, and the amount.
Without help, somebody has to add up each employee's earlier claims by hand before approving a new one. This script does it on every save and refuses the request when a limit is crossed.
How it works
- It checks that this is an allowance request. If Paid From is not the record with code
health, the script does nothing. - It checks the lines. Every line must have an employee in To and a detailed remark in Ref 1. The detailed remark is the kind of claim, and its N1 is the yearly maximum per employee for that kind.
- It collects this year's earlier claims. It reads the committed payment requests dated in the same calendar year as this one, paid from the same Paid From, that have a line for one of this request's employees.
- It writes the running totals on each line:
- N1: the employee's total for that kind of claim this year, including this line and the lines above it.
- N2: the yearly maximum for that kind, from the detailed remark.
- N3: what is left, N2 minus N1. It is negative when the maximum is exceeded.
- It refuses a line over the maximum. A line whose N1 is more than N2 is refused, with the message on that line.
- It checks each employee's yearly total. Adding all kinds of claims together, an employee's total for the year, this request included, must not exceed Parameter 2.
Setting it up
- For each kind of claim, create a Detailed Remark and put its yearly maximum per employee in N1. A detailed remark with no N1 allows nothing.
- Open Administration → Display Customization → Entity Flow and create a new flow with Target Type set to Payment Request.
- Add a line to the Details grid with Target Action set to Validate On Save. Paste the whole script from the file above into Groovy Script, and leave Class Name empty: it is filled in when you save.
- Fill in the parameters:
| Parameter | Title shown after saving | What to enter |
|---|---|---|
| Parameter 1 | Not used - leave empty | Nothing. |
| Parameter 2 | Yearly Maximum | The most one employee may receive in a year, all kinds of claims together, for example 1000. Always fill it in. |
- Save the flow. Class Name shows the script's class name, the description appears in Description, and the parameter shows its title.
Example
The detailed remark GLASSES (Glasses) has N1 = 300 and DENTAL (Dental) has N1 = 500. A first request paid from health has three lines:
| To | Ref 1 | Amount | N1 | N2 | N3 |
|---|---|---|---|---|---|
| Employee E0001 | Glasses | 200 | 200 | 300 | 100 |
| Employee E0001 | Dental | 100 | 100 | 500 | 400 |
| Employee E0002 | Glasses | 250 | 250 | 300 | 50 |
It is saved. A second request then claims 150 for glasses for E0001. E0001's glasses total would become 350, more than 300, so the save is refused with:
The employee E0001 - Mohamed Ahmed El Shennawy total 350 for item GLASSES - Glasses exceeded the defined yearly maximum 300
Messages you may see
None of these messages has an Arabic translation, so they appear in English on Arabic screens as well.
| Message | Why | What to do |
|---|---|---|
| The employee {0} total {1} for item {2} exceeded the defined yearly maximum {3} | The employee's total this year for that kind of claim, this line included, is more than the detailed remark's N1. | Lower the amount, or raise the detailed remark's N1 if the limit has changed. |
| The yearly total {0} for employee {1} is more than configured value {2} | The employee's total for the year, all kinds of claims together, is more than Parameter 2. | Lower the amount, or raise Parameter 2 on the flow. |
| The ref {0} must be an employee at line {1} | To on that line is not an employee. | Choose the employee in To. |
| The ref {0} must be a detailed remark at line {1} | Ref 1 on that line is not a detailed remark. | Choose the kind of claim in Ref 1. |
An empty To or Ref 1 is refused with the system's usual message for a required field.
The script
package com.namasoft.modules.accounting.utils.entityactions
import com.namasoft.common.Pair
import com.namasoft.common.fieldids.newids.accounting.IdsOfPVRequest
import com.namasoft.common.utilities.ObjectChecker
import com.namasoft.infra.domainbase.common.CommonValidations
import com.namasoft.infra.domainbase.common.criteria.CriteriaBuilder
import com.namasoft.infra.domainbase.datafields.DecimalDF
import com.namasoft.infra.domainbase.datafields.EntityCodeDF
import com.namasoft.infra.domainbase.datafields.LongTextDF
import com.namasoft.infra.domainbase.datafields.UniqueIDDF
import com.namasoft.infra.domainbase.entity.base.EntityAction
import com.namasoft.infra.domainbase.modules.hr.Employee
import com.namasoft.infra.domainbase.persistence.repos.ListPageMatchingParameters
import com.namasoft.infra.domainbase.persistence.repos.Persister
import com.namasoft.infra.domainbase.util.Result
import com.namasoft.modules.accounting.domain.entities.PVRequest
import com.namasoft.modules.basic.domain.entities.DetailedRemark
import com.namasoft.modules.basic.utils.BSCEntityTypes
import java.util.stream.Collectors
class EAYearlyLimitPerEmployeeInPVRequest implements EntityAction<PVRequest> {
@Override
Result doAction(PVRequest object, LongTextDF... parameters) {
def from = object.getFrom();
def healthSafeCode = EntityCodeDF.fromString("health")
if (from == null || from.toReal().getCode() != healthSafeCode)
return Result.createSuccessResult();
Result result = Result.createAccumulatingResult();
validateDetails(object, result);
def emps = object.getDetails().stream().map { it.getTo() }.filter(ObjectChecker::isNotEmptyOrNull)
.filter { it.instanceOf(BSCEntityTypes.Employee()) }.collect(Collectors.toSet());
if (emps.isEmpty()) {
return result;
}
def allRequests = Persister.listPageMatching(PVRequest.class, ListPageMatchingParameters.create()
.criteria(CriteriaBuilder.create().field(IdsOfPVRequest.valueDate).greaterThanOrEqual(object.getValueDate().yearStart())
.and().field(IdsOfPVRequest.valueDate).lessThanOrEqual(object.getValueDate().yearEnd())
.and().commitedBefore().and().idNotEqual(object.getId())
.and().field(IdsOfPVRequest.details_to + ".id").in(emps.stream().map {it.getId()}.toList())
.and().field(IdsOfPVRequest.from + ".id").equal(from.getId())))
Map<Pair<Employee, DetailedRemark>, DecimalDF> totals = new HashMap<>();
for (def i = 0; i < object.getDetails().size(); i++) {
def d = object.getDetails().get(i);
if (d.getRef1() == null || d.getRef1().notInstanceOf(BSCEntityTypes.DetailedRemark()))
continue;
if (d.getTo() == null || d.getTo().notInstanceOf(BSCEntityTypes.Employee()))
continue;
def item = d.getRef1().<DetailedRemark> toReal()
def emp = d.getTo().<Employee> toReal()
def totalOfCurrent = totals.computeIfAbsent(new Pair<>(emp, item), { DecimalDF.zero() })
totalOfCurrent.mutateAdd(d.getAmount());
def totalOfOthers = DecimalDF.totalizeStream(allRequests.stream().flatMap { it.getDetails().stream() }.filter { ObjectChecker.arePairsEqual(d.getTo(), it.getTo(), d.getRef1(), it.getRef1()) }.map { it.getAmount() });
def grandTotal = totalOfOthers.add(totalOfCurrent)
if (grandTotal.isGreaterThan(item.getN1()))
Result.createDetailFieldValidationFailure(IdsOfPVRequest.details_to, i, "The employee {0} total {1} for item {2} exceeded the defined yearly maximum {3}",
emp, grandTotal, item, item.getN1()).addToAccumulatingResult(result)
d.setN1(grandTotal);
d.setN2(item.getN1());
d.setN3(item.getN1().subtract(grandTotal));
}
allRequests.add(object);
for (final def emp in emps) {
def yearTotal = DecimalDF.totalizeStream(allRequests.stream().flatMap { it.getDetails().stream() }
.filter { it.getTo() != null && it.getTo() == emp }
.map { it.getAmount() });
if (yearTotal.isGreaterThan(DecimalDF.fromString(parameters[1].getPrimitiveValue())))
Result.createFailureResult("The yearly total {0} for employee {1} is more than configured value {2}",
yearTotal, emp, parameters[1]).addToAccumulatingResult(result)
}
return result;
}
static void validateDetails(PVRequest object, Result result) {
for (int i = 0; i < object.getDetails().size(); i++) {
def detail = object.getDetails().get(i);
if (CommonValidations.checkColumnRequired(object.getEntityType(), IdsOfPVRequest.details_to, i, detail.getTo(), result)) {
if (detail.getTo().notInstanceOf(BSCEntityTypes.Employee()))
Result.createDetailFieldValidationFailure(IdsOfPVRequest.details_to, i, "The ref {0} must be an employee at line {1}", detail.getTo(), i + 1).addToAccumulatingResult(result);
}
if (CommonValidations.checkColumnRequired(object.getEntityType(), IdsOfPVRequest.details + ".ref1", i, detail.getRef1(), result)) {
if (detail.getRef1().notInstanceOf(BSCEntityTypes.DetailedRemark()))
Result.createDetailFieldValidationFailure(IdsOfPVRequest.details + ".ref1", i, "The ref {0} must be a detailed remark at line {1}", detail.getRef1(), i + 1).addToAccumulatingResult(result);
}
}
}
@Override
String describe() {
return "Payment request: a yearly allowance per employee, for example a medical allowance. Applies only when Paid From is the record with code health. " +
"Every line needs an employee in To and a detailed remark in Ref 1; the detailed remark's N1 is the yearly maximum per employee for that item. " +
"On each line the employee's total for the item this year (committed requests from the same Paid From, plus this one) is written to N1, " +
"the maximum to N2 and what remains to N3, and the line is refused when the maximum is exceeded. The request is also refused when an " +
"employee's total for the year, all items together, is more than parameter 2. Parameter 1 is not used."
}
@Override
List<String> columnNames() {
return Arrays.asList("Not used - leave empty", "Yearly Maximum")
}
}Related
- EAGroovyAction: how Groovy scripts are attached to entity flows and task schedules.
- Receipt & Payment Vouchers: payment requests and the vouchers made from them.