Skip to content

Commit

Permalink
Merge pull request #5752 from psikomonkie/issue-5736-negative-personn…
Browse files Browse the repository at this point in the history
…el-income

Issue 5736: Preventing Negative Personnel Income
  • Loading branch information
HammerGS authored Jan 15, 2025
2 parents 4e1a69e + 5e9e2e7 commit 5a064ba
Show file tree
Hide file tree
Showing 5 changed files with 176 additions and 89 deletions.
42 changes: 25 additions & 17 deletions MekHQ/src/mekhq/campaign/Campaign.java
Original file line number Diff line number Diff line change
Expand Up @@ -5584,6 +5584,24 @@ public void removeFunds(final TransactionType type, final Money quantity,
addReport("Funds removed : " + quantityString + " (" + description + ')');
}


/**
* Generic method for paying Personnel (Person) in the company.
* Debits money from the campaign and if the campaign tracks
* total earnings it will account for that.
* @param type TransactionType being debited
* @param quantity total money - it's usually displayed outside of this method
* @param description String displayed in the ledger and report
* @param individualPayouts Map of Person to the Money they're owed
*/
public void payPersonnel(TransactionType type, Money quantity, String description, Map<Person, Money> individualPayouts) {
getFinances().debit(type, getLocalDate(), quantity, description,
individualPayouts, getCampaignOptions().isTrackTotalEarnings());
String quantityString = quantity.toAmountAndSymbolString();
addReport("Funds removed : " + quantityString + " (" + description + ')');

}

public CampaignOptions getCampaignOptions() {
return campaignOptions;
}
Expand Down Expand Up @@ -7812,27 +7830,17 @@ public void completeMission(@Nullable Mission mission, MissionStatus status) {

if (getCampaignOptions().isUseShareSystem()) {
ResourceBundle financeResources = ResourceBundle.getBundle("mekhq.resources.Finances",
MekHQ.getMHQOptions().getLocale());
MekHQ.getMHQOptions().getLocale());

Money shares = remainingMoney.multipliedBy(contract.getSharesPercent()).dividedBy(100);
remainingMoney = remainingMoney.minus(shares);
if (remainingMoney.isGreaterThan(Money.zero())) {
Money shares = remainingMoney.multipliedBy(contract.getSharesPercent()).dividedBy(100);
remainingMoney = remainingMoney.minus(shares);

if (getFinances().debit(TransactionType.SALARIES, getLocalDate(), shares,
if (getFinances().debit(TransactionType.SALARIES, getLocalDate(), shares,
String.format(financeResources.getString("ContractSharePayment.text"), contract.getName()))) {
addReport(financeResources.getString("DistributedShares.text"), shares.toAmountAndSymbolString());

if (getCampaignOptions().isTrackTotalEarnings()) {
boolean sharesForAll = getCampaignOptions().isSharesForAll();
addReport(financeResources.getString("DistributedShares.text"), shares.toAmountAndSymbolString());

int numberOfShares = getActivePersonnel().stream()
.mapToInt(person -> person.getNumShares(this, sharesForAll))
.sum();

Money singleShare = shares.dividedBy(numberOfShares);

for (Person person : getActivePersonnel()) {
person.payPersonShares(this, singleShare, sharesForAll);
}
getFinances().payOutSharesToPersonnel(this, shares);
}
}
}
Expand Down
27 changes: 27 additions & 0 deletions MekHQ/src/mekhq/campaign/finances/Accountant.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,16 @@
*/
package mekhq.campaign.finances;

import java.time.LocalDate;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

import megamek.common.Entity;
import mekhq.campaign.Campaign;
import mekhq.campaign.CampaignOptions;
import mekhq.campaign.Hangar;
import mekhq.campaign.finances.enums.TransactionType;
import mekhq.campaign.parts.Part;
import mekhq.campaign.personnel.Person;
import mekhq.campaign.personnel.enums.PersonnelRole;
Expand Down Expand Up @@ -228,4 +232,27 @@ public Money getContractBase() {
return getTheoreticalPayroll(getCampaignOptions().isInfantryDontCount());
}
}

/**
* Returns a map of every Person and their salary.
*
* @see Finances#debit(TransactionType, LocalDate, Money, String, Map, boolean)
* @return map of personnel to their pay, including pool as a null key
*/
public Map<Person, Money> getPayRollSummary() {
Map<Person, Money> payRollSummary = new HashMap<>();
for (Person p : getCampaign().getActivePersonnel()) {
payRollSummary.put(p, p.getSalary(getCampaign()));
}
// And pay our pool
payRollSummary.put(null, Money.of(
(getCampaign().getCampaignOptions().getRoleBaseSalaries()
[PersonnelRole.ASTECH.ordinal()].getAmount().doubleValue()
* getCampaign().getAstechPool())
+ (getCampaign().getCampaignOptions().getRoleBaseSalaries()
[PersonnelRole.MEDIC.ordinal()].getAmount().doubleValue()
* getCampaign().getMedicPool())));

return payRollSummary;
}
}
160 changes: 109 additions & 51 deletions MekHQ/src/mekhq/campaign/finances/Finances.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,25 +19,6 @@
*/
package mekhq.campaign.finances;

import java.io.BufferedWriter;
import java.io.File;
import java.io.PrintWriter;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.time.LocalDate;
import java.time.Period;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
import java.util.ResourceBundle;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVPrinter;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import megamek.common.annotations.Nullable;
import megamek.logging.MMLogger;
import mekhq.MekHQ;
Expand All @@ -52,6 +33,25 @@
import mekhq.io.FileType;
import mekhq.utilities.MHQXMLUtility;
import mekhq.utilities.ReportingUtilities;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVPrinter;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import java.io.BufferedWriter;
import java.io.File;
import java.io.PrintWriter;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.time.LocalDate;
import java.time.Period;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

/**
* @author Jay Lawson (jaylawson39 at yahoo.com)
Expand Down Expand Up @@ -159,6 +159,17 @@ public int getPartialYearsInDebt(LocalDate date) {
return 0;
}

/**
* Debits (removes) money from the campaign's balance.
* Consider the debit method that takes a Map of Person to Money
* if this debit is for paying your crew.
* @param type TransactionType being debited
* @param date when the transaction occurred
* @param amount Money to remove from the campaign's balanace
* @param reason String displayed in the ledger
* @return true if the transaction succeeds, false if it doesn't,
* such as from insufficient balance
*/
public boolean debit(final TransactionType type, final LocalDate date, final Money amount,
final String reason) {
if (getBalance().isLessThan(amount)) {
Expand All @@ -173,6 +184,41 @@ public boolean debit(final TransactionType type, final LocalDate date, final Mon
return true;
}


/**
* Debits (removes) money from the campaign's balance.
* When debiting money to people in the Campaign,
* if TrackTotalEarnings is true we'll want to pay each
* Person what they're owed. Use this method to debit (remove)
* money from your Campaign's balance while paying it to
* the provided people (Person) in the individualPayoutsMap.
* @param type TransactionType being debited
* @param date when the transaction occurred
* @param amount total money - it's usually displayed outside of this method
* @param reason String displayed in the ledger
* @param individualPayouts Map of Person to the Money they're owed
* @param isTrackTotalEarnings true if we want to apply earnings to individual people (Person)
* @return true if the transaction succeeds, false if it doesn't,
* such as from insufficient balance
*/
public boolean debit(final TransactionType type, final LocalDate date, Money amount, String reason, Map<Person, Money> individualPayouts, boolean isTrackTotalEarnings) {
if (debit(type, date, amount, reason)) {
if (isTrackTotalEarnings && !individualPayouts.isEmpty()) {
for (Person person : individualPayouts.keySet()) {
Money payout = individualPayouts.get(person);
if (person != null) { // Null person will be used for temp personnel
person.payPerson(payout);
}
}
} else {
logger.error(String.format("Individual Payouts is Empty! Transaction Type: %s Date: %s Reason: %s",type, date, reason));
}
return true;
}

return false;
}

public void credit(final TransactionType type, final LocalDate date, final Money amount,
final String reason) {
Transaction t = new Transaction(type, date, amount, reason);
Expand Down Expand Up @@ -312,19 +358,17 @@ public void newDay(final Campaign campaign, final LocalDate yesterday, final Loc
}

if (campaign.getCampaignOptions().isPayForSalaries()) {

Money payRollCost = campaign.getAccountant().getPayRoll();

if (debit(TransactionType.SALARIES, today, payRollCost,
resourceMap.getString("Salaries.title"))) {
resourceMap.getString("Salaries.title"),
campaign.getAccountant().getPayRollSummary(),
campaign.getCampaignOptions().isTrackTotalEarnings())) {
campaign.addReport(
String.format(resourceMap.getString("Salaries.text"),
payRollCost.toAmountAndSymbolString()));
String.format(resourceMap.getString("Salaries.text"),
payRollCost.toAmountAndSymbolString()));

if (campaign.getCampaignOptions().isTrackTotalEarnings()) {
for (Person person : campaign.getActivePersonnel()) {
person.payPersonSalary(campaign);
}
}
} else {
campaign.addReport(
String.format("<font color='" + MekHQ.getMHQOptions().getFontColorNegativeHexColor() + "'>"
Expand Down Expand Up @@ -445,37 +489,51 @@ private void payoutShares(Campaign campaign, Contract contract, LocalDate date)
if (campaign.getCampaignOptions().isUseAtB() && campaign.getCampaignOptions().isUseShareSystem()
&& (contract instanceof AtBContract)) {
Money shares = contract.getMonthlyPayOut().multipliedBy(contract.getSharesPercent())
.dividedBy(100);
if (debit(TransactionType.SALARIES, date, shares,
.dividedBy(100);
if (shares.isGreaterThan(Money.zero())) {
if (debit(TransactionType.SALARIES, date, shares,
String.format(resourceMap.getString("ContractSharePayment.text"), contract.getName()))) {
campaign.addReport(resourceMap.getString("DistributedShares.text"), shares.toAmountAndSymbolString());
campaign.addReport(resourceMap.getString("DistributedShares.text"), shares.toAmountAndSymbolString());

if (campaign.getCampaignOptions().isTrackTotalEarnings()) {
int numberOfShares = 0;
boolean sharesForAll = campaign.getCampaignOptions().isSharesForAll();
for (Person person : campaign.getActivePersonnel()) {
numberOfShares += person.getNumShares(campaign, sharesForAll);
}

Money singleShare = shares.dividedBy(numberOfShares);
for (Person person : campaign.getActivePersonnel()) {
person.payPersonShares(campaign, singleShare, sharesForAll);
}
}
} else {
/*
* This should not happen, as the shares payment should be less than the
* contract
* payment that has just been made.
*/
campaign.addReport("<font color='" + MekHQ.getMHQOptions().getFontColorNegativeHexColor() + "'>"
+ resourceMap.getString("InsufficientFunds.text"), resourceMap.getString("Shares.text"),
payOutSharesToPersonnel(campaign, shares);
} else {
/*
* This should not happen, as the shares payment should be less than the
* contract payment that has just been made.
*/
campaign.addReport("<font color='" + MekHQ.getMHQOptions().getFontColorNegativeHexColor() + "'>"
+ resourceMap.getString("InsufficientFunds.text"), resourceMap.getString("Shares.text"),
"</font>");
logger.error("Attempted to payout share amount larger than the payment of the contract");
logger.error("Attempted to payout share amount larger than the payment of the contract");
}
}
}
}

/**
* Shares calculate the amount debited without iterating
* through all the personnel, so it's not more efficient
* to provide that information to debit. Pay out shares
* manually for now.
* @param campaign where to pull personnel from
* @param shares total value of the shares to pay out
*/
public void payOutSharesToPersonnel(Campaign campaign, Money shares) {
if (campaign.getCampaignOptions().isTrackTotalEarnings()) {
boolean sharesForAll = campaign.getCampaignOptions().isSharesForAll();

int numberOfShares = campaign.getActivePersonnel().stream()
.mapToInt(person -> person.getNumShares(campaign, sharesForAll))
.sum();

Money singleShare = shares.dividedBy(numberOfShares);

for (Person person : campaign.getActivePersonnel()) {
person.payPersonShares(campaign, singleShare, sharesForAll);
}
}
}

public Money checkOverdueLoanPayments(Campaign campaign) {
List<Loan> newLoans = new ArrayList<>();
Money overdueAmount = Money.zero();
Expand Down
19 changes: 7 additions & 12 deletions MekHQ/src/mekhq/campaign/personnel/Person.java
Original file line number Diff line number Diff line change
Expand Up @@ -2879,22 +2879,17 @@ public Money getTotalEarnings() {
}

/**
* This is used to pay a person
* This is used to pay a person. Preventing negative payments
* is intentional to ensure we don't accidentally
* change someone when trying to give them money.
* To charge a person, implement a new method.
* (And then add a @see here)
*
* @param money the amount of money to add to their total earnings
*/
public void payPerson(final Money money) {
totalEarnings = getTotalEarnings().plus(money);
}

/**
* This is used to pay a person their salary
*
* @param campaign the campaign the person is a part of
*/
public void payPersonSalary(final Campaign campaign) {
if (getStatus().isActive()) {
payPerson(getSalary(campaign));
if (money.isPositiveOrZero()) {
totalEarnings = getTotalEarnings().plus((money));
}
}

Expand Down
17 changes: 8 additions & 9 deletions MekHQ/src/mekhq/gui/adapter/PersonnelTableMouseAdapter.java
Original file line number Diff line number Diff line change
Expand Up @@ -1103,16 +1103,15 @@ public void actionPerformed(ActionEvent action) {
return;
}

// pay person
for (Person person : people) {
person.payPerson(Money.of(payment));
MekHQ.triggerEvent(new PersonChangedEvent(person));
}
// pay person & add expense
Map<Person, Money> personMoneyMap = new HashMap<>();
personMoneyMap.put(selectedPerson, Money.of(payment));
gui.getCampaign().payPersonnel(TransactionType.MISCELLANEOUS,
Money.of(payment),
String.format(resources.getString("givePayment.format"),selectedPerson.getFullName()),
personMoneyMap);
MekHQ.triggerEvent(new PersonChangedEvent(selectedPerson));

// add expense
gui.getCampaign().removeFunds(TransactionType.MISCELLANEOUS, Money.of(payment),
String.format(resources.getString("givePayment.format"),
selectedPerson.getFullName()));

break;
}
Expand Down

0 comments on commit 5a064ba

Please sign in to comment.