diff --git a/MekHQ/src/mekhq/campaign/Campaign.java b/MekHQ/src/mekhq/campaign/Campaign.java index 0fee50ab81..83a4007dce 100644 --- a/MekHQ/src/mekhq/campaign/Campaign.java +++ b/MekHQ/src/mekhq/campaign/Campaign.java @@ -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 individualPayouts) { + getFinances().debit(type, getLocalDate(), quantity, description, + individualPayouts, getCampaignOptions().isTrackTotalEarnings()); + String quantityString = quantity.toAmountAndSymbolString(); + addReport("Funds removed : " + quantityString + " (" + description + ')'); + + } + public CampaignOptions getCampaignOptions() { return campaignOptions; } @@ -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); } } } diff --git a/MekHQ/src/mekhq/campaign/finances/Accountant.java b/MekHQ/src/mekhq/campaign/finances/Accountant.java index f436fe7d50..4a2d8ac582 100644 --- a/MekHQ/src/mekhq/campaign/finances/Accountant.java +++ b/MekHQ/src/mekhq/campaign/finances/Accountant.java @@ -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; @@ -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 getPayRollSummary() { + Map 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; + } } diff --git a/MekHQ/src/mekhq/campaign/finances/Finances.java b/MekHQ/src/mekhq/campaign/finances/Finances.java index 715aca7f18..cc28f4f598 100644 --- a/MekHQ/src/mekhq/campaign/finances/Finances.java +++ b/MekHQ/src/mekhq/campaign/finances/Finances.java @@ -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; @@ -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) @@ -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)) { @@ -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 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); @@ -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("" @@ -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("" - + 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("" + + resourceMap.getString("InsufficientFunds.text"), resourceMap.getString("Shares.text"), ""); - 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 newLoans = new ArrayList<>(); Money overdueAmount = Money.zero(); diff --git a/MekHQ/src/mekhq/campaign/personnel/Person.java b/MekHQ/src/mekhq/campaign/personnel/Person.java index 3a581836ed..04edeeb1c6 100644 --- a/MekHQ/src/mekhq/campaign/personnel/Person.java +++ b/MekHQ/src/mekhq/campaign/personnel/Person.java @@ -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)); } } diff --git a/MekHQ/src/mekhq/gui/adapter/PersonnelTableMouseAdapter.java b/MekHQ/src/mekhq/gui/adapter/PersonnelTableMouseAdapter.java index ebec401b80..ffe77d5500 100644 --- a/MekHQ/src/mekhq/gui/adapter/PersonnelTableMouseAdapter.java +++ b/MekHQ/src/mekhq/gui/adapter/PersonnelTableMouseAdapter.java @@ -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 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; }