private FinancialTransactionRow AddRow(int financialAccountId, Int64 amountCents, int personId) { // private function that actually executes the row adding FinancialAccount account = FinancialAccount.FromIdentity(financialAccountId); if (DateTime.Year <= account.Organization.Parameters.FiscalBooksClosedUntilYear) { // Recurse down into continuation transactions to write row in first nonclosed year FinancialTransactionRow newRow = null; FinancialTransaction transactionContinued = ContinuedTransaction; if (transactionContinued == null) { // No continuation; create one transactionContinued = Create(OrganizationId, DateTime.Now, "Continued Tx #" + Identity); newRow = transactionContinued.AddRow(financialAccountId, amountCents, personId); transactionContinued.Dependency = this; } else { // Recurse newRow = transactionContinued.AddRow(financialAccountId, amountCents, personId); } return(newRow); } FinancialTransactionRow addedRow = FinancialTransactionRow.FromIdentityAggressive(SwarmDb.GetDatabaseForWriting() .CreateFinancialTransactionRow(Identity, financialAccountId, amountCents, personId)); // If we're running from web, and this was a P&L account, then also notify the server that the P&L has changed // (doing this here means that the server can get pinged multiple times, but that's more defensive coding than // having to remember doing it everywhere at the UI level) if (account.AccountType == FinancialAccountType.Income || account.AccountType == FinancialAccountType.Cost) { if (SupportFunctions.OperatingTopology == OperatingTopology.FrontendWeb) { SocketMessage newMessage = new SocketMessage { MessageType = "ProfitLossChanged", OrganizationId = account.Organization.Identity, FinancialTransactionId = this.Identity }; newMessage.SendUpstream(); } } return(addedRow); }
public bool RecalculateTransaction(Dictionary <int, Int64> nominalTransaction, Person loggingPerson) { bool changedTransaction = false; // We need to create a delta. This is... somewhat complicated. // 1) Iterate over the rows to build a "current" transaction record. // 2) Create a "should-look-like" transaction record. (done in calling routine, already). // 3) Apply the delta, in two steps. Dictionary <int, Int64> currentTransaction = GetRecalculationBase(); FinancialTransaction continuedTransaction = ContinuedTransaction; if (continuedTransaction != null) { continuedTransaction.AddContinuedTransactionsToLookup(currentTransaction); // Recurses to all continued transactions } // Step 2: create an image of what the transaction SHOULD look like with changes. // now done in calling routine. // Step 3a: For all accounts existing in Current but not in Nominal, set them to 0, and // vice versa. foreach (int accountId in currentTransaction.Keys) { if (!nominalTransaction.ContainsKey(accountId)) { nominalTransaction[accountId] = 0; } } foreach (int accountId in nominalTransaction.Keys) { if (!currentTransaction.ContainsKey(accountId)) { currentTransaction[accountId] = 0; } } // Step 3b: Iterate over all accounts in the two sets -- which now has the same keys -- // and apply the delta to the transaction. foreach (int accountId in currentTransaction.Keys) { if (currentTransaction[accountId] != nominalTransaction[accountId]) { AddRow(accountId, nominalTransaction[accountId] - currentTransaction[accountId], loggingPerson == null ? 0 : loggingPerson.Identity); changedTransaction = true; } } return(changedTransaction); }
public void AddItem(FinancialTransaction transaction, Int64 turnoverCents, Int64 vatInboundCents, Int64 vatOutboundCents) { if (!UnderConstruction) { throw new InvalidOperationException("Cannot add items once the report is released"); } VatReportItem.Create(this, transaction, turnoverCents, vatInboundCents, vatOutboundCents); }
public void BindToTransactionAndClose(FinancialTransaction transaction, Person bindingPerson) { Dictionary <int, Int64> accountDebitLookup = new Dictionary <int, Int64>(); Organization organization = Organization; accountDebitLookup[organization.FinancialAccounts.DebtsExpenseClaims.Identity] = 0; accountDebitLookup[organization.FinancialAccounts.DebtsInboundInvoices.Identity] = 0; accountDebitLookup[organization.FinancialAccounts.DebtsSalary.Identity] = 0; accountDebitLookup[organization.FinancialAccounts.DebtsTax.Identity] = 0; accountDebitLookup[organization.FinancialAccounts.AssetsOutstandingCashAdvances.Identity] = 0; if (this.DependentExpenseClaims.Count > 0) { accountDebitLookup[organization.FinancialAccounts.DebtsExpenseClaims.Identity] += this.DependentExpenseClaims.TotalAmountCents; } if (this.DependentInvoices.Count > 0) { accountDebitLookup[organization.FinancialAccounts.DebtsInboundInvoices.Identity] += this.DependentInvoices.TotalAmountCents; } if (this.DependentSalariesNet.Count > 0) { accountDebitLookup[organization.FinancialAccounts.DebtsSalary.Identity] += this.DependentSalariesNet.TotalAmountCentsNet; } if (this.DependentSalariesTax.Count > 0) { accountDebitLookup[organization.FinancialAccounts.DebtsTax.Identity] += this.DependentSalariesTax.TotalAmountCentsTax; } if (this.DependentCashAdvancesPayout.Count > 0) { accountDebitLookup[organization.FinancialAccounts.AssetsOutstandingCashAdvances.Identity] += this.DependentCashAdvancesPayout.TotalAmountCents; } if (this.DependentCashAdvancesPayback.Count > 0) { accountDebitLookup[organization.FinancialAccounts.AssetsOutstandingCashAdvances.Identity] -= // observe the minus this.DependentCashAdvancesPayback.TotalAmountCents; } foreach (int financialAccountId in accountDebitLookup.Keys) { if (accountDebitLookup[financialAccountId] != 0) { transaction.AddRow(FinancialAccount.FromIdentity(financialAccountId), accountDebitLookup[financialAccountId], bindingPerson); } } transaction.Dependency = this; Open = false; }
public static FinancialTransaction Create(int organizationId, DateTime dateTime, string description) { int transactionId = SwarmDb.GetDatabaseForWriting() .CreateFinancialTransaction(organizationId, dateTime, description); FinancialTransaction newTx = FromIdentityAggressive(transactionId); newTx.SetOrganizationSequenceId(); return(newTx); }
public static FinancialTransactionTagTypes ForTransaction(FinancialTransaction transaction) { int[] tagTypeIdentities = SwarmDb.GetDatabaseForReading().GetFinancialTransactionTagTypes(transaction.Identity); if (tagTypeIdentities.Length == 0) { return(new FinancialTransactionTagTypes()); // none, so return empty set } return(FromIdentities(tagTypeIdentities)); }
public static InboundInvoice Create(Organization organization, DateTime dueDate, Int64 amountCents, Int64 vatCents, FinancialAccount budget, string supplier, string description, string payToAccount, string ocr, string invoiceReference, Person creatingPerson) { InboundInvoice newInvoice = FromIdentity(SwarmDb.GetDatabaseForWriting(). CreateInboundInvoice(organization.Identity, dueDate, budget.Identity, supplier, payToAccount, ocr, invoiceReference, amountCents, creatingPerson.Identity)); newInvoice.Description = description; // Not in original schema; not cause for schema update if (vatCents > 0) { newInvoice.VatCents = vatCents; } // Create a corresponding financial transaction with rows FinancialTransaction transaction = FinancialTransaction.Create(organization.Identity, DateTime.UtcNow, "Invoice #" + newInvoice.OrganizationSequenceId + " from " + supplier); transaction.AddRow(organization.FinancialAccounts.DebtsInboundInvoices, -amountCents, creatingPerson); if (vatCents > 0) { transaction.AddRow(organization.FinancialAccounts.AssetsVatInboundUnreported, vatCents, creatingPerson); transaction.AddRow(budget, amountCents - vatCents, creatingPerson); } else { transaction.AddRow(budget, amountCents, creatingPerson); } // Make the transaction dependent on the inbound invoice transaction.Dependency = newInvoice; // Create notification (slightly misplaced logic, but this is failsafest place) OutboundComm.CreateNotificationApprovalNeeded(budget, creatingPerson, supplier, (amountCents - vatCents) / 100.0, description, NotificationResource.InboundInvoice_Created); // Slightly misplaced logic, but failsafer here SwarmopsLogEntry.Create(creatingPerson, new InboundInvoiceCreatedLogEntry(creatingPerson, supplier, description, amountCents / 100.0, budget), newInvoice); // Clear a cache FinancialAccount.ClearApprovalAdjustmentsCache(organization); return(newInvoice); }
private void UpdateTransaction(Person updatingPerson) { Dictionary <int, Int64> nominalTransaction = new Dictionary <int, Int64>(); // Create an image of what the transaction SHOULD look like with changes. if (Attested || Open) { // ...only holds values if not closed as invalid... nominalTransaction[Organization.FinancialAccounts.DebtsInboundInvoices.Identity] = -AmountCents; nominalTransaction[BudgetId] = AmountCents; } FinancialTransaction.RecalculateTransaction(nominalTransaction, updatingPerson); }
public void DenyAttestation(Person denyingPerson, string reason) { this.Attested = false; this.Open = false; SwarmDb.GetDatabaseForWriting().CreateFinancialValidation(FinancialValidationType.Kill, FinancialDependencyType.ExpenseClaim, Identity, DateTime.UtcNow, denyingPerson.Identity, this.CostTotalCents); OutboundComm.CreateNotificationOfFinancialValidation(Budget, this.PayrollItem.Person, NetSalaryCents / 100.0, this.PayoutDate.ToString("MMMM yyyy"), NotificationResource.Salary_Denied, reason); FinancialTransaction transaction = FinancialTransaction.FromDependency(this); transaction.RecalculateTransaction(new Dictionary <int, long>(), denyingPerson); // zeroes out the tx }
public static FinancialTransaction ImportWithStub(int organizationId, DateTime dateTime, int financialAccountId, Int64 amountCents, string description, string importHash, int personId) { int transactionId = SwarmDb.GetDatabaseForWriting() .CreateFinancialTransactionStub(organizationId, dateTime, financialAccountId, amountCents, description, importHash, personId); if (transactionId == 0) { return(null); // This was a dupe -- already imported, as determined by ImportHash } FinancialTransaction newTx = FromIdentityAggressive(transactionId); newTx.SetOrganizationSequenceId(); return(newTx); }
private void AddContinuedTransactionsToLookup(Dictionary <int, Int64> currentTransactionData) { FinancialTransaction continuedTransaction = ContinuedTransaction; if (continuedTransaction != null) { continuedTransaction.AddContinuedTransactionsToLookup(currentTransactionData); } foreach (FinancialTransactionRow row in Rows) { if (!currentTransactionData.ContainsKey(row.FinancialAccountId)) { currentTransactionData[row.FinancialAccountId] = 0; } currentTransactionData[row.FinancialAccountId] += row.AmountCents; } }
public static VatReportItem Create(VatReport report, FinancialTransaction transaction, Int64 turnoverCents, Int64 vatInboundCents, Int64 vatOutboundCents) { // Assumes there's a dependency of some sort IHasIdentity foreignObject = transaction.Dependency; FinancialDependencyType dependencyType = (foreignObject != null ? FinancialTransaction.GetFinancialDependencyType(transaction.Dependency) : FinancialDependencyType.Unknown); // The transaction dependency is stored for quick lookup; it duplicates information in the database // to save an expensive query as a mere optimization. int newVatReportItemId = SwarmDb.GetDatabaseForWriting() .CreateVatReportItem(report.Identity, transaction.Identity, foreignObject?.Identity ?? 0, dependencyType, turnoverCents, vatInboundCents, vatOutboundCents); return(FromIdentityAggressive(newVatReportItemId)); }
private void UpdateFinancialTransaction(Person updatingPerson) { Dictionary <int, Int64> nominalTransaction = new Dictionary <int, Int64>(); int debtAccountId = Organization.FinancialAccounts.DebtsExpenseClaims.Identity; if (!Claimed) { debtAccountId = Organization.FinancialAccounts.CostsAllocatedFunds.Identity; } if (Validated || Open) { // ...only holds values if not closed as invalid... nominalTransaction[debtAccountId] = -AmountCents; nominalTransaction[BudgetId] = AmountCents; } FinancialTransaction.RecalculateTransaction(nominalTransaction, updatingPerson); }
public static FinancialTransaction FromBlockchainHash(Organization organization, string blockchainTransactionHash) { int[] transactionIds = SwarmDb.GetDatabaseForReading() .GetObjectsByOptionalData(ObjectType.FinancialTransaction, ObjectOptionalDataType.FinancialTransactionBlockchainHash, blockchainTransactionHash); // There may be multiple transactions in this Swarmops installation referring to this transaction on the blockchain, but only // one per organization. So find the transaction that matches the org we want. foreach (int transactionId in transactionIds) { FinancialTransaction potentialResult = FinancialTransaction.FromIdentity(transactionId); if (potentialResult.OrganizationId == organization.Identity) { return(potentialResult); } } throw new ArgumentException("No match for supplied blockchain tx hash and organization"); }
public static ExpenseClaim Create(Person claimer, Organization organization, FinancialAccount budget, DateTime expenseDate, string description, Int64 amountCents) { ExpenseClaim newClaim = FromIdentityAggressive(SwarmDb.GetDatabaseForWriting() .CreateExpenseClaim(claimer.Identity, organization.Identity, budget.Identity, expenseDate, description, amountCents)); // Create the financial transaction with rows string transactionDescription = "Expense #" + newClaim.Identity + ": " + description; // TODO: Localize if (transactionDescription.Length > 64) { transactionDescription = transactionDescription.Substring(0, 61) + "..."; } FinancialTransaction transaction = FinancialTransaction.Create(organization.Identity, DateTime.Now, transactionDescription); transaction.AddRow(organization.FinancialAccounts.DebtsExpenseClaims, -amountCents, claimer); transaction.AddRow(budget, amountCents, claimer); // Make the transaction dependent on the expense claim transaction.Dependency = newClaim; // Create notifications OutboundComm.CreateNotificationAttestationNeeded(budget, claimer, string.Empty, amountCents / 100.0, description, NotificationResource.ExpenseClaim_Created); // Slightly misplaced logic, but failsafer here OutboundComm.CreateNotificationFinancialValidationNeeded(organization, amountCents / 100.0, NotificationResource.Receipts_Filed); SwarmopsLogEntry.Create(claimer, new ExpenseClaimFiledLogEntry(claimer /*filing person*/, claimer /*beneficiary*/, amountCents / 100.0, budget, description), newClaim); return(newClaim); }
public void LogForexProfitLoss(Money accountBalanceInAnyCurrency) { Int64 currentNativeValueOfForeignCents = accountBalanceInAnyCurrency.ToCurrency(Organization.Currency).Cents; Int64 nativeCents = BalanceTotalCents; if (nativeCents - 100 > currentNativeValueOfForeignCents) { // log a loss Int64 lossCents = nativeCents - currentNativeValueOfForeignCents; FinancialTransaction lossTx = FinancialTransaction.Create(Organization, DateTime.UtcNow, "Forex Loss"); lossTx.AddRow(this, -lossCents, null); lossTx.AddRow(Organization.FinancialAccounts.CostsCurrencyFluctuations, lossCents, null); } else if (nativeCents + 100 < currentNativeValueOfForeignCents) { // log a profit Int64 profitCents = currentNativeValueOfForeignCents - nativeCents; FinancialTransaction profitTx = FinancialTransaction.Create(Organization, DateTime.UtcNow, "Forex Gains"); profitTx.AddRow(this, profitCents, null); profitTx.AddRow(Organization.FinancialAccounts.IncomeCurrencyFluctuations, -profitCents, null); } }
public void BindToTransactionAndClose (FinancialTransaction transaction, Person bindingPerson) { Dictionary<int, Int64> accountDebitLookup = new Dictionary<int, Int64>(); Organization organization = this.Organization; accountDebitLookup[organization.FinancialAccounts.DebtsExpenseClaims.Identity] = 0; accountDebitLookup[organization.FinancialAccounts.DebtsInboundInvoices.Identity] = 0; accountDebitLookup[organization.FinancialAccounts.DebtsSalary.Identity] = 0; accountDebitLookup[organization.FinancialAccounts.DebtsTax.Identity] = 0; accountDebitLookup[organization.FinancialAccounts.AssetsOutstandingCashAdvances.Identity] = 0; if (this.DependentExpenseClaims.Count > 0) { accountDebitLookup[organization.FinancialAccounts.DebtsExpenseClaims.Identity] += this.DependentExpenseClaims.TotalAmountCents; } if (this.DependentInvoices.Count > 0) { accountDebitLookup[organization.FinancialAccounts.DebtsInboundInvoices.Identity] += this.DependentInvoices.TotalAmountCents; } if (this.DependentSalariesNet.Count > 0) { accountDebitLookup[organization.FinancialAccounts.DebtsSalary.Identity] += this.DependentSalariesNet.TotalAmountCentsNet; } if (this.DependentSalariesTax.Count > 0) { accountDebitLookup[organization.FinancialAccounts.DebtsTax.Identity] += this.DependentSalariesTax.TotalAmountCentsTax; } if (this.DependentCashAdvancesPayout.Count > 0) { accountDebitLookup[organization.FinancialAccounts.AssetsOutstandingCashAdvances.Identity] += this.DependentCashAdvancesPayout.TotalAmountCents; } if (this.DependentCashAdvancesPayback.Count > 0) { accountDebitLookup[organization.FinancialAccounts.AssetsOutstandingCashAdvances.Identity] -= // observe the minus this.DependentCashAdvancesPayback.TotalAmountCents; } foreach (int financialAccountId in accountDebitLookup.Keys) { if (accountDebitLookup[financialAccountId] != 0) { transaction.AddRow(FinancialAccount.FromIdentity(financialAccountId), accountDebitLookup[financialAccountId], bindingPerson); } } transaction.Dependency = this; this.Open = false; }
public static void AutomatchAgainstUnbalancedTransactions(Organization organization) { // Matches unbalanced financial transactions against unclosed payouts // Should this be in bot? Payouts payouts = ForOrganization(organization); FinancialTransactions transactions = FinancialTransactions.GetUnbalanced(organization); foreach (FinancialTransaction transaction in transactions) { // Console.WriteLine("Looking at transaction #{0} ({1:yyyy-MM-dd}, {2:N2}).", transaction.Identity, transaction.DateTime, transaction.Rows.AmountTotal); // First, establish that there are no similar transactions within 7 days. N^2 search. DateTime timeLow = transaction.DateTime.AddDays(-7); DateTime timeHigh = transaction.DateTime.AddDays(7); bool foundCompeting = false; foreach (FinancialTransaction possiblyCompetingTransaction in transactions) { if (possiblyCompetingTransaction.Rows.AmountCentsTotal == transaction.Rows.AmountCentsTotal && possiblyCompetingTransaction.DateTime >= timeLow && possiblyCompetingTransaction.DateTime <= timeHigh && possiblyCompetingTransaction.Identity != transaction.Identity) { foundCompeting = true; // Console.WriteLine(" - Transaction #{0} ({1:yyyy-MM-dd} is competing, aborting", possiblyCompetingTransaction.Identity, possiblyCompetingTransaction.DateTime); } } if (foundCompeting) { continue; } // Console.WriteLine(" - no competing transactions...\r\n - transaction description is \"{0}\".", transaction.Description); // Console.WriteLine(" - looking for matching payouts"); int foundCount = 0; int payoutIdFound = 0; // As the amount of payouts grow, this becomes less efficient exponentially. foreach (Payout payout in payouts) { // Ugly hack to fix cash advance payouts DateTime payoutLowerTimeLimit = timeLow; DateTime payoutUpperTimeLimit = timeHigh; if (payout.AmountCents == -transaction.Rows.AmountCentsTotal && (payout.DependentCashAdvancesPayout.Count > 0 || payout.DependentCashAdvancesPayback.Count > 0)) { // HACK: While PW5 doesn't have a manual-debug interface, special case for cash advances payoutLowerTimeLimit = transaction.DateTime.AddDays(-60); payoutUpperTimeLimit = transaction.DateTime.AddDays(60); } // HACK: Allow for up to 20 days beyond scheduled payment to catch tax payments if (payout.DependentSalariesTax.Count > 0) { payoutLowerTimeLimit = transaction.DateTime.AddDays(-25); payoutUpperTimeLimit = transaction.DateTime.AddDays(3); // nobody pays taxes early... } if (payout.ExpectedTransactionDate >= payoutLowerTimeLimit && payout.ExpectedTransactionDate <= payoutUpperTimeLimit && payout.AmountCents == -transaction.Rows.AmountCentsTotal) { // Console.WriteLine(" - - payout #{0} matches ({1}, {2:yyyy-MM-dd})", payout.Identity, payout.Recipient, payout.ExpectedTransactionDate); try { // If this succeeds, there is a transaction already FinancialTransaction.FromDependency(payout); break; } catch (Exception) { // There isn't such a transaction, which is what we want } foundCount++; payoutIdFound = payout.Identity; } } if (foundCount == 0) { // Console.WriteLine(" - none found"); } else if (foundCount > 1) { // Console.WriteLine(" - multiple found, not autoprocessing"); } else { Payout payout = Payout.FromIdentity(payoutIdFound); payout.BindToTransactionAndClose(transaction, null); } } }
public static void PerformAutomated(BitcoinChain chain) { // Perform all waiting hot payouts for all orgs in the installation throw new NotImplementedException("Waiting for rewrite for Bitcoin Cash"); // TODO DateTime utcNow = DateTime.UtcNow; foreach (Organization organization in Organizations.GetAll()) { // If this org doesn't do hotwallet, continue if (organization.FinancialAccounts.AssetsBitcoinHot == null) { continue; } Payouts orgPayouts = Payouts.Construct(organization); Payouts bitcoinPayouts = new Payouts(); Dictionary <string, Int64> satoshiPayoutLookup = new Dictionary <string, long>(); Dictionary <string, Int64> nativeCentsPayoutLookup = new Dictionary <string, long>(); Dictionary <int, Int64> satoshiPersonLookup = new Dictionary <int, long>(); Dictionary <int, Int64> nativeCentsPersonLookup = new Dictionary <int, long>(); Int64 satoshisTotal = 0; string currencyCode = organization.Currency.Code; // For each ready payout that can automate, add an output to a constructed transaction TransactionBuilder txBuilder = null; // TODO TODO TODO TODO new TransactionBuilder(); foreach (Payout payout in orgPayouts) { if (payout.ExpectedTransactionDate > utcNow) { continue; // payout is not due yet } if (payout.RecipientPerson != null && payout.RecipientPerson.BitcoinPayoutAddress.Length > 2 && payout.Account.Length < 4) { // If the payout address is still in quarantine, don't pay out yet string addressSetTime = payout.RecipientPerson.BitcoinPayoutAddressTimeSet; if (addressSetTime.Length > 4 && DateTime.Parse(addressSetTime, CultureInfo.InvariantCulture).AddHours(48) > utcNow) { continue; // still in quarantine } // Test the payout address - is it valid and can we handle it? if (!BitcoinUtility.IsValidBitcoinAddress(payout.RecipientPerson.BitcoinPayoutAddress)) { // Notify person that address is invalid, then clear it NotificationStrings primaryStrings = new NotificationStrings(); NotificationCustomStrings secondaryStrings = new NotificationCustomStrings(); primaryStrings[NotificationString.OrganizationName] = organization.Name; secondaryStrings["BitcoinAddress"] = payout.RecipientPerson.BitcoinPayoutAddress; OutboundComm.CreateNotification(organization, NotificationResource.BitcoinPayoutAddress_Bad, primaryStrings, secondaryStrings, People.FromSingle(payout.RecipientPerson)); payout.RecipientPerson.BitcoinPayoutAddress = string.Empty; continue; // do not add this payout } // Ok, so it seems we're making this payout at this time. bitcoinPayouts.Add(payout); int recipientPersonId = payout.RecipientPerson.Identity; if (!satoshiPersonLookup.ContainsKey(recipientPersonId)) { satoshiPersonLookup[recipientPersonId] = 0; nativeCentsPersonLookup[recipientPersonId] = 0; } nativeCentsPersonLookup[recipientPersonId] += payout.AmountCents; // Find the amount of satoshis for this payout if (organization.Currency.IsBitcoinCore) { satoshiPayoutLookup[payout.ProtoIdentity] = payout.AmountCents; nativeCentsPayoutLookup[payout.ProtoIdentity] = payout.AmountCents; satoshisTotal += payout.AmountCents; satoshiPersonLookup[recipientPersonId] += payout.AmountCents; } else { // Convert currency Money payoutAmount = new Money(payout.AmountCents, organization.Currency); Int64 satoshis = payoutAmount.ToCurrency(Currency.BitcoinCore).Cents; satoshiPayoutLookup[payout.ProtoIdentity] = satoshis; nativeCentsPayoutLookup[payout.ProtoIdentity] = payout.AmountCents; satoshisTotal += satoshis; satoshiPersonLookup[recipientPersonId] += satoshis; } } else if (payout.RecipientPerson != null && payout.RecipientPerson.BitcoinPayoutAddress.Length < 3 && payout.Account.Length < 4) { // There is a payout for this person, but they don't have a bitcoin payout address set. Send notification to this effect once a day. if (utcNow.Minute != 0) { continue; } if (utcNow.Hour != 12) { continue; } NotificationStrings primaryStrings = new NotificationStrings(); primaryStrings[NotificationString.OrganizationName] = organization.Name; OutboundComm.CreateNotification(organization, NotificationResource.BitcoinPayoutAddress_PleaseSet, primaryStrings, People.FromSingle(payout.RecipientPerson)); } else if (payout.Account.StartsWith("bitcoin:")) { } } if (bitcoinPayouts.Count == 0) { // no automated payments pending for this organization, nothing more to do continue; } // We now have our desired payments. The next step is to find enough inputs to reach the required amount (plus fees; we're working a little blind here still). BitcoinTransactionInputs inputs = null; Int64 satoshisMaximumAnticipatedFees = BitcoinUtility.GetRecommendedFeePerThousandBytesSatoshis(chain) * 20; // assume max 20k transaction size try { inputs = BitcoinUtility.GetInputsForAmount(organization, satoshisTotal + satoshisMaximumAnticipatedFees); } catch (NotEnoughFundsException) { // If we're at the whole hour, send a notification to people responsible for refilling the hotwallet if (utcNow.Minute != 0) { continue; // we're not at the whole hour, so continue with next org instead } // Send urgent notification to top up the damn wallet so we can make payments NotificationStrings primaryStrings = new NotificationStrings(); primaryStrings[NotificationString.CurrencyCode] = organization.Currency.Code; primaryStrings[NotificationString.OrganizationName] = organization.Name; NotificationCustomStrings secondaryStrings = new NotificationCustomStrings(); Int64 satoshisAvailable = HotBitcoinAddresses.ForOrganization(organization).BalanceSatoshisTotal; secondaryStrings["AmountMissingMicrocoinsFloat"] = ((satoshisTotal - satoshisAvailable + satoshisMaximumAnticipatedFees) / 100.0).ToString("N2"); if (organization.Currency.IsBitcoinCore) { secondaryStrings["AmountNeededFloat"] = ((satoshisTotal + satoshisMaximumAnticipatedFees) / 100.0).ToString("N2"); secondaryStrings["AmountWalletFloat"] = (satoshisAvailable / 100.0).ToString("N2"); } else { // convert to org native currency secondaryStrings["AmountNeededFloat"] = (new Money(satoshisTotal, Currency.BitcoinCore).ToCurrency(organization.Currency).Cents / 100.0).ToString("N2"); secondaryStrings["AmountWalletFloat"] = (new Money(satoshisAvailable, Currency.BitcoinCore).ToCurrency(organization.Currency).Cents / 100.0).ToString("N2"); } OutboundComm.CreateNotification(organization, NotificationResource.Bitcoin_Shortage_Critical, primaryStrings, secondaryStrings, People.FromSingle(Person.FromIdentity(1))); continue; // with next organization } // If we arrive at this point, the previous function didn't throw, and we have enough money. // Ensure the existence of a cost account for bitcoin miner fees. organization.EnsureMinerFeeAccountExists(); // Add the inputs to the transaction. txBuilder = txBuilder.AddCoins(inputs.Coins); txBuilder = txBuilder.AddKeys(inputs.PrivateKeys); Int64 satoshisInput = inputs.AmountSatoshisTotal; // Add outputs and prepare notifications Int64 satoshisUsed = 0; Dictionary <int, List <string> > notificationSpecLookup = new Dictionary <int, List <string> >(); Dictionary <int, List <Int64> > notificationAmountLookup = new Dictionary <int, List <Int64> >(); Payout masterPayoutPrototype = Payout.Empty; HotBitcoinAddress changeAddress = HotBitcoinAddress.OrganizationWalletZero(organization, BitcoinChain.Core); // TODO: CHAIN! foreach (Payout payout in bitcoinPayouts) { int recipientPersonId = payout.RecipientPerson.Identity; if (!notificationSpecLookup.ContainsKey(recipientPersonId)) { notificationSpecLookup[recipientPersonId] = new List <string>(); notificationAmountLookup[recipientPersonId] = new List <Int64>(); } notificationSpecLookup[recipientPersonId].Add(payout.Specification); notificationAmountLookup[recipientPersonId].Add(payout.AmountCents); if (payout.RecipientPerson.BitcoinPayoutAddress.StartsWith("1")) // regular address { txBuilder = txBuilder.Send(new BitcoinPubKeyAddress(payout.RecipientPerson.BitcoinPayoutAddress), new Satoshis(satoshiPayoutLookup[payout.ProtoIdentity])); } else if (payout.RecipientPerson.BitcoinPayoutAddress.StartsWith("3")) // multisig { txBuilder = txBuilder.Send(new BitcoinScriptAddress(payout.RecipientPerson.BitcoinPayoutAddress, Network.Main), new Satoshis(satoshiPayoutLookup[payout.ProtoIdentity])); } else { throw new InvalidOperationException("Unhandled bitcoin address type in Payouts.PerformAutomated(): " + payout.RecipientPerson.BitcoinPayoutAddress); } satoshisUsed += satoshiPayoutLookup[payout.ProtoIdentity]; payout.MigrateDependenciesTo(masterPayoutPrototype); } // Set change address to wallet slush txBuilder.SetChange(new BitcoinPubKeyAddress(changeAddress.Address)); // Add fee int transactionSizeBytes = txBuilder.EstimateSize(txBuilder.BuildTransaction(false)) + inputs.Count; // +inputs.Count for size variability Int64 feeSatoshis = (transactionSizeBytes / 1000 + 1) * BitcoinUtility.GetRecommendedFeePerThousandBytesSatoshis(chain); txBuilder = txBuilder.SendFees(new Satoshis(feeSatoshis)); satoshisUsed += feeSatoshis; // Sign transaction - ready to execute Transaction txReady = txBuilder.BuildTransaction(true); // Verify that transaction is ready if (!txBuilder.Verify(txReady)) { // Transaction was not signed with the correct keys. This is a serious condition. NotificationStrings primaryStrings = new NotificationStrings(); primaryStrings[NotificationString.OrganizationName] = organization.Name; OutboundComm.CreateNotification(organization, NotificationResource.Bitcoin_PrivateKeyError, primaryStrings); throw new InvalidOperationException("Transaction is not signed enough"); } // Broadcast transaction BitcoinUtility.BroadcastTransaction(txReady, BitcoinChain.Cash); // Note the transaction hash string transactionHash = txReady.GetHash().ToString(); // Delete all old inputs, adjust balance for addresses (re-register unused inputs) inputs.AsUnspents.DeleteAll(); // Log the new unspent created by change (if there is any) if (satoshisInput - satoshisUsed > 0) { SwarmDb.GetDatabaseForWriting() .CreateHotBitcoinAddressUnspentConditional(changeAddress.Identity, transactionHash, +/* the change address seems to always get index 0? is this a safe assumption? */ 0, satoshisInput - satoshisUsed, /* confirmation count*/ 0); } // Register new balance of change address, should have increased by (satoshisInput-satoshisUsed) // TODO // Send notifications foreach (int personId in notificationSpecLookup.Keys) { Person person = Person.FromIdentity(personId); string spec = string.Empty; for (int index = 0; index < notificationSpecLookup[personId].Count; index++) { spec += String.Format(" * {0,-40} {1,14:N2} {2,-4}\r\n", notificationSpecLookup[personId][index], notificationAmountLookup[personId][index] / 100.0, currencyCode); } spec = spec.TrimEnd(); NotificationStrings primaryStrings = new NotificationStrings(); NotificationCustomStrings secondaryStrings = new NotificationCustomStrings(); primaryStrings[NotificationString.OrganizationName] = organization.Name; primaryStrings[NotificationString.CurrencyCode] = organization.Currency.DisplayCode; primaryStrings[NotificationString.EmbeddedPreformattedText] = spec; secondaryStrings["AmountFloat"] = (nativeCentsPersonLookup[personId] / 100.0).ToString("N2"); secondaryStrings["BitcoinAmountFloat"] = (satoshiPersonLookup[personId] / 100.0).ToString("N2"); secondaryStrings["BitcoinAddress"] = person.BitcoinPayoutAddress; // warn: potential rare race condition here OutboundComm.CreateNotification(organization, NotificationResource.Bitcoin_PaidOut, primaryStrings, secondaryStrings, People.FromSingle(person)); } // Create the master payout from its prototype Payout masterPayout = Payout.CreateBitcoinPayoutFromPrototype(organization, masterPayoutPrototype, txReady.GetHash().ToString()); // Finally, create ledger entries and notify NotificationStrings masterPrimaryStrings = new NotificationStrings(); NotificationCustomStrings masterSecondaryStrings = new NotificationCustomStrings(); masterPrimaryStrings[NotificationString.OrganizationName] = organization.Name; masterPrimaryStrings[NotificationString.CurrencyCode] = organization.Currency.DisplayCode; masterSecondaryStrings["AmountFloat"] = (new Swarmops.Logic.Financial.Money(satoshisUsed, Currency.BitcoinCore).ToCurrency( organization.Currency).Cents / 100.0).ToString("N2", CultureInfo.InvariantCulture); masterSecondaryStrings["BitcoinAmountFloat"] = (satoshisUsed / 100.0).ToString("N2", CultureInfo.InvariantCulture); masterSecondaryStrings["PaymentCount"] = bitcoinPayouts.Count.ToString("N0", CultureInfo.InvariantCulture); OutboundComm.CreateNotification(organization, NotificationResource.Bitcoin_Hotwallet_Outflow, masterPrimaryStrings, masterSecondaryStrings); // TODO: special case for native-bitcoin organizations vs. fiat-currency organizations FinancialTransaction ledgerTransaction = FinancialTransaction.Create(organization, utcNow, "Bitcoin automated payout"); if (organization.Currency.IsBitcoinCore) { ledgerTransaction.AddRow(organization.FinancialAccounts.AssetsBitcoinHot, -(masterPayoutPrototype.AmountCents + feeSatoshis), null); ledgerTransaction.AddRow(organization.FinancialAccounts.CostsBitcoinFees, feeSatoshis, null); } else { // If the ledger isn't using bitcoin natively, we need to translate the miner fee paid to ledger cents before entering it into ledger Int64 feeCentsLedger = new Money(feeSatoshis, Currency.BitcoinCore).ToCurrency(organization.Currency).Cents; ledgerTransaction.AddRow(organization.FinancialAccounts.AssetsBitcoinHot, -(masterPayoutPrototype.AmountCents + feeCentsLedger), null).AmountForeignCents = new Money(satoshisUsed, Currency.BitcoinCore); ledgerTransaction.AddRow(organization.FinancialAccounts.CostsBitcoinFees, feeCentsLedger, null); } ledgerTransaction.BlockchainHash = transactionHash; masterPayout.BindToTransactionAndClose(ledgerTransaction, null); } }
public static Salary Create(PayrollItem payrollItem, DateTime payoutDate) { // Load the existing adjustments. PayrollAdjustments adjustments = PayrollAdjustments.ForPayrollItem(payrollItem); Int64 payCents = payrollItem.BaseSalaryCents; // Apply all before-tax adjustments foreach (PayrollAdjustment adjustment in adjustments) { if (adjustment.Type == PayrollAdjustmentType.GrossAdjustment) { payCents += adjustment.AmountCents; } } Int64 subtractiveTaxCents = 0; Int64 additiveTaxCents = 0; if (!payrollItem.IsContractor) { // calculate tax Money grossInOrgCurrency = new Money { Cents = payCents, Currency = payrollItem.Organization.Currency, ValuationDateTime = DateTime.UtcNow }; Money grossInTaxCurrency = grossInOrgCurrency.ToCurrency(payrollItem.Country.Currency); Money subtractiveTax = TaxLevels.GetTax(payrollItem.Country, payrollItem.SubtractiveTaxLevelId, grossInTaxCurrency); Money subtractiveTaxInOrgCurrency = subtractiveTax.ToCurrency(payrollItem.Organization.Currency); subtractiveTaxCents = (Int64)(subtractiveTaxInOrgCurrency.Cents); additiveTaxCents = (Int64)(payCents * payrollItem.AdditiveTaxLevel); payCents -= subtractiveTaxCents; } // Apply all after-tax adjustments foreach (PayrollAdjustment adjustment in adjustments) { if (adjustment.Type == PayrollAdjustmentType.NetAdjustment) { payCents += adjustment.AmountCents; } } // If net is negative, create rollover adjustment PayrollAdjustment rolloverAdjustment = null; if (payCents < 0) { rolloverAdjustment = PayrollAdjustment.Create(payrollItem, PayrollAdjustmentType.NetAdjustment, -payCents, "Deficit rolls over to next salary"); PayrollAdjustment.Create(payrollItem, PayrollAdjustmentType.NetAdjustment, payCents, "Deficit rolled over from " + payoutDate.ToString("yyyy-MM-dd")); // keep second rollover open, so the deficit from this salary is carried to the next payCents = 0; } // Create salary, close adjustments Salary salary = Create(payrollItem, payoutDate, payCents, subtractiveTaxCents, additiveTaxCents); // For each adjustment, close and bind to salary foreach (PayrollAdjustment adjustment in adjustments) { adjustment.Close(salary); } if (rolloverAdjustment != null) { rolloverAdjustment.Close(salary); } // Add the financial transaction FinancialTransaction transaction = FinancialTransaction.Create(payrollItem.OrganizationId, DateTime.Now, "Salary #" + salary.Identity + ": " + payrollItem.PersonCanonical + " " + salary.PayoutDate.ToString("yyyy-MMM", CultureInfo.InvariantCulture)); transaction.AddRow(payrollItem.Budget, salary.CostTotalCents, null); transaction.AddRow(payrollItem.Organization.FinancialAccounts.DebtsSalary, -salary.NetSalaryCents, null); if (salary.TaxTotalCents != 0) { transaction.AddRow(payrollItem.Organization.FinancialAccounts.DebtsTax, -salary.TaxTotalCents, null); } transaction.Dependency = salary; // Finally, check if net and/or tax are zero, and if so, mark them as already-paid (i.e. not due for payment) if (salary.NetSalaryCents == 0) { salary.NetPaid = true; } if (salary.TaxTotalCents == 0) { salary.TaxPaid = true; } // Clear a cache FinancialAccount.ClearAttestationAdjustmentsCache(payrollItem.Organization); return(salary); }
public static Salary Create(PayrollItem payrollItem, DateTime payoutDate) { // Load the existing adjustments. PayrollAdjustments adjustments = PayrollAdjustments.ForPayrollItem(payrollItem); Int64 payCents = payrollItem.BaseSalaryCents; // Apply all before-tax adjustments foreach (PayrollAdjustment adjustment in adjustments) { if (adjustment.Type == PayrollAdjustmentType.GrossAdjustment) { payCents += adjustment.AmountCents; } } // calculate tax double subtractiveTax = TaxLevels.GetTax(payrollItem.Country, payrollItem.SubtractiveTaxLevelId, payCents / 100.0); if (subtractiveTax < 1.0) { // this is a percentage and not an absolute number subtractiveTax = payCents * subtractiveTax; } Int64 subtractiveTaxCents = (Int64)(subtractiveTax * 100); Int64 additiveTaxCents = (Int64)(payCents * payrollItem.AdditiveTaxLevel); payCents -= subtractiveTaxCents; // Apply all after-tax adjustments foreach (PayrollAdjustment adjustment in adjustments) { if (adjustment.Type == PayrollAdjustmentType.NetAdjustment) { payCents += adjustment.AmountCents; } } // Create salary, close adjustments Salary salary = Create(payrollItem, payoutDate, payCents, subtractiveTaxCents, additiveTaxCents); // For each adjustment, close and bind to salary foreach (PayrollAdjustment adjustment in adjustments) { adjustment.Close(salary); } // If net is negative, create rollover adjustment if (payCents < 0) { PayrollAdjustment rollover1 = PayrollAdjustment.Create(payrollItem, PayrollAdjustmentType.NetAdjustment, -payCents, "Deficit rolls over to next salary"); rollover1.Close(salary); PayrollAdjustment rollover2 = PayrollAdjustment.Create(payrollItem, PayrollAdjustmentType.NetAdjustment, payCents, "Deficit rolled over from " + payoutDate.ToString("yyyy-MM-dd")); // keep rollover2 open, so the deficit from this salary is carried to the next salary.NetSalaryCents = 0; } // Add the financial transaction FinancialTransaction transaction = FinancialTransaction.Create(payrollItem.OrganizationId, DateTime.Now, "Salary #" + salary.Identity + ": " + payrollItem.PersonCanonical + " " + salary.PayoutDate.ToString("yyyy-MMM", CultureInfo.InvariantCulture)); transaction.AddRow(payrollItem.Budget, salary.CostTotalCents, null); transaction.AddRow(payrollItem.Organization.FinancialAccounts.DebtsSalary, -salary.NetSalaryCents, null); transaction.AddRow(payrollItem.Organization.FinancialAccounts.DebtsTax, -salary.TaxTotalCents, null); transaction.Dependency = salary; // Finally, check if net and/or tax are zero, and if so, mark them as already-paid (i.e. not due for payment) if (salary.NetSalaryCents == 0) { salary.NetPaid = true; } if (salary.TaxTotalCents == 0) { salary.TaxPaid = true; } return(salary); }
public static FinancialTransaction Create(Organization organization, DateTime dateTime, string description) { return(FinancialTransaction.Create(organization.Identity, dateTime, description)); }
private static void CheckColdStorageRecurse(FinancialAccount parent, Dictionary <string, int> addressAccountLookup) { foreach (FinancialAccount child in parent.Children) { CheckColdStorageRecurse(child, addressAccountLookup); } // After recursing, get all transactions for this account and verify against our records // is the account name a valid bitcoin address on the main network? string address = parent.BitcoinAddress; if (string.IsNullOrEmpty(address)) { if (BitcoinUtility.IsValidBitcoinAddress(parent.Name.Trim())) { parent.BitcoinAddress = address = parent.Name.Trim(); } else { return; // not a bitcoin address but something else; do not process } } Organization organization = parent.Organization; bool organizationLedgerUsesBitcoin = organization.Currency.IsBitcoinCore; JObject addressData = JObject.Parse( new WebClient().DownloadString("https://blockchain.info/address/" + address + "?format=json&api_key=" + SystemSettings.BlockchainSwarmopsApiKey)); //int transactionCount = (int) (addressData["n_tx"]); foreach (JObject txJson in addressData["txs"]) { FinancialTransaction ourTx = null; Dictionary <Int64, Int64> satoshisLookup = new Dictionary <long, long>(); // map from ledgercents to satoshis BlockchainTransaction blockchainTx = BlockchainTransaction.FromBlockchainInfoJson(txJson); try { ourTx = FinancialTransaction.FromBlockchainHash(parent.Organization, blockchainTx.TransactionHash); // If the transaction was fetched fine, we have already seen this transaction, but need to re-check it } catch (ArgumentException) { // We didn't have this transaction, so we need to create it ourTx = FinancialTransaction.Create(parent.Organization, blockchainTx.TransactionDateTimeUtc, "Blockchain tx"); ourTx.BlockchainHash = blockchainTx.TransactionHash; // Did we lose or gain money? // Find all in- and outpoints, determine which are ours (hot and cold wallet) and which aren't } Dictionary <int, long> transactionReconstructedRows = new Dictionary <int, long>(); // Note the non-blockchain rows in this tx, keep them for reconstruction foreach (FinancialTransactionRow row in ourTx.Rows) { if (!addressAccountLookup.ContainsValue(row.FinancialAccountId)) // not optimal but n is small { // This is not a bitcoin address account, so note it for reconstruction if (!transactionReconstructedRows.ContainsKey(row.FinancialAccountId)) { transactionReconstructedRows[row.FinancialAccountId] = 0; // init } transactionReconstructedRows[row.FinancialAccountId] += row.AmountCents; } else { // this is a known blockchain row, note its ledgered value in satoshis if (!organizationLedgerUsesBitcoin) { Money nativeMoney = row.AmountForeignCents; if (nativeMoney != null && nativeMoney.Currency.IsBitcoinCore) // it damn well should be, but just checking { satoshisLookup[row.AmountCents] = row.AmountForeignCents.Cents; } } } } // Reconstruct the blockchain rows: input, output, fees, in that order // -- inputs foreach (BlockchainTransactionRow inputRow in blockchainTx.Inputs) { if (addressAccountLookup.ContainsKey(inputRow.Address)) { // this input is ours int financialAccountId = addressAccountLookup[inputRow.Address]; if (!transactionReconstructedRows.ContainsKey(financialAccountId)) { transactionReconstructedRows[financialAccountId] = 0; // initialize } if (organizationLedgerUsesBitcoin) { transactionReconstructedRows[financialAccountId] += -inputRow.ValueSatoshis; // note the negation! } else { Int64 ledgerCents = new Money(inputRow.ValueSatoshis, Currency.BitcoinCore, ourTx.DateTime).ToCurrency( organization.Currency).Cents; transactionReconstructedRows[financialAccountId] += -ledgerCents; // note the negation! satoshisLookup[ledgerCents] = inputRow.ValueSatoshis; } } } // -- outputs foreach (BlockchainTransactionRow outputRow in blockchainTx.Outputs) { if (addressAccountLookup.ContainsKey(outputRow.Address)) { // this output is ours int financialAccountId = addressAccountLookup[outputRow.Address]; if (!transactionReconstructedRows.ContainsKey(financialAccountId)) { transactionReconstructedRows[financialAccountId] = 0; // initialize } if (organizationLedgerUsesBitcoin) { transactionReconstructedRows[financialAccountId] += outputRow.ValueSatoshis; } else { Int64 ledgerCents = new Money(outputRow.ValueSatoshis, Currency.BitcoinCore, ourTx.DateTime).ToCurrency( organization.Currency).Cents; transactionReconstructedRows[financialAccountId] += ledgerCents; satoshisLookup[ledgerCents] = outputRow.ValueSatoshis; } } } // -- fees if (addressAccountLookup.ContainsKey(blockchainTx.Inputs[0].Address)) { // if the first input is ours, we're paying the fee (is there any case where this is not true?) if (organizationLedgerUsesBitcoin) { transactionReconstructedRows[organization.FinancialAccounts.CostsBitcoinFees.Identity] = blockchainTx.FeeSatoshis; } else { Int64 feeLedgerCents = new Money(blockchainTx.FeeSatoshis, Currency.BitcoinCore, blockchainTx.TransactionDateTimeUtc).ToCurrency(organization.Currency).Cents; transactionReconstructedRows[organization.FinancialAccounts.CostsBitcoinFees.Identity] = feeLedgerCents; } } // Rewrite the transaction (called always, but the function won't do anything if everything matches) ourTx.RecalculateTransaction(transactionReconstructedRows, /* loggingPerson*/ null); // Finally, add foreign cents, if any if (!organizationLedgerUsesBitcoin) { foreach (FinancialTransactionRow row in ourTx.Rows) { if (addressAccountLookup.ContainsValue(row.Account.Identity)) // "ContainsValue" is bad, but n is low { // Do we have a foreign amount for this row already? Money foreignMoney = row.AmountForeignCents; if (foreignMoney == null || foreignMoney.Cents == 0) { // no we didn't; create one if (satoshisLookup.ContainsKey(row.AmountCents)) { row.AmountForeignCents = new Money(satoshisLookup[row.AmountCents], Currency.BitcoinCore, ourTx.DateTime); } else if (satoshisLookup.ContainsKey(-row.AmountCents)) // the negative counterpart { row.AmountForeignCents = new Money(-satoshisLookup[-row.AmountCents], Currency.BitcoinCore, ourTx.DateTime); } else { // There's a last case which may happen if the row is an addition to a previous row; if so, calculate row.AmountForeignCents = new Money(row.AmountCents, organization.Currency, ourTx.DateTime).ToCurrency(Currency.BitcoinCore); } } } } } } }
public OutboundInvoice Credit(Person creditingPerson) { OutboundInvoice credit = Create(Organization, DateTime.Now, Budget, CustomerName, InvoiceAddressMail, InvoiceAddressPaper, Currency, Domestic, TheirReference, creditingPerson); if (Domestic) // TODO: LANGUAGE { credit.AddItem("Kredit för faktura " + Reference, -AmountCents); credit.AddItem("DETTA ÄR EN KREDITFAKTURA OCH SKA EJ BETALAS", 0.00); AddItem( String.Format("KREDITERAD {0:yyyy-MM-dd} i kreditfaktura {1}", DateTime.Today, credit.Reference), 0.00); } else { credit.AddItem("Credit for invoice " + Reference, -AmountCents); credit.AddItem("THIS IS A CREDIT. DO NOT PAY.", 0.00); AddItem( String.Format("CREDITED {0:yyyy-MM-dd} in credit invoice {1}", DateTime.Today, credit.Reference), 0.00); } CreditInvoice = credit; credit.CreditsInvoice = this; credit.Open = false; // Create the financial transaction with rows FinancialTransaction transaction = FinancialTransaction.Create(credit.OrganizationId, DateTime.Now, "Credit Invoice #" + credit.Identity + " to " + credit.CustomerName); transaction.AddRow( Organization.FromIdentity(credit.OrganizationId).FinancialAccounts.AssetsOutboundInvoices, credit.AmountCents, creditingPerson); transaction.AddRow(credit.Budget, -credit.AmountCents, creditingPerson); // Make the transaction dependent on the credit transaction.Dependency = credit; // Create the event for PirateBot-Mono to send off mails PWEvents.CreateEvent(EventSource.PirateWeb, EventType.OutboundInvoiceCreated, creditingPerson.Identity, OrganizationId, Geography.RootIdentity, 0, credit.Identity, string.Empty); // If this invoice was already closed, issue a credit. If not closed, close it. if (Open) { Open = false; } else { Payment payment = Payment; if (payment != null) { payment.Refund(creditingPerson); } } return(credit); }
public static ExpenseClaim Create(Person claimer, Organization organization, FinancialAccount budget, DateTime expenseDate, string description, Int64 amountCents, Int64 vatCents, ExpenseClaimGroup group = null) { ExpenseClaim newClaim = FromIdentityAggressive(SwarmDb.GetDatabaseForWriting() .CreateExpenseClaim(claimer.Identity, organization?.Identity ?? 0, budget?.Identity ?? 0, expenseDate, description, amountCents)); // budget can be 0 initially if created with a group if (vatCents > 0) { newClaim.VatCents = vatCents; } if (group != null) { newClaim.Group = group; } if (budget != null && organization != null) { // Create the financial transaction with rows string transactionDescription = "Expense #" + newClaim.OrganizationSequenceId + ": " + description; // TODO: Localize if (transactionDescription.Length > 64) { transactionDescription = transactionDescription.Substring(0, 61) + "..."; } DateTime expenseTxDate = expenseDate; int ledgersClosedUntil = organization.Parameters.FiscalBooksClosedUntilYear; if (ledgersClosedUntil >= expenseDate.Year) { expenseTxDate = DateTime.UtcNow; // If ledgers are closed for the actual expense time, account now } FinancialTransaction transaction = FinancialTransaction.Create(organization.Identity, expenseTxDate, transactionDescription); transaction.AddRow(organization.FinancialAccounts.DebtsExpenseClaims, -amountCents, claimer); if (vatCents > 0) { transaction.AddRow(budget, amountCents - vatCents, claimer); transaction.AddRow(organization.FinancialAccounts.AssetsVatInboundUnreported, vatCents, claimer); } else { transaction.AddRow(budget, amountCents, claimer); } // Make the transaction dependent on the expense claim transaction.Dependency = newClaim; // Create notifications OutboundComm.CreateNotificationAttestationNeeded(budget, claimer, string.Empty, newClaim.BudgetAmountCents / 100.0, description, NotificationResource.ExpenseClaim_Created); // Slightly misplaced logic, but failsafer here OutboundComm.CreateNotificationFinancialValidationNeeded(organization, newClaim.AmountCents / 100.0, NotificationResource.Receipts_Filed); SwarmopsLogEntry.Create(claimer, new ExpenseClaimFiledLogEntry(claimer /*filing person*/, claimer /*beneficiary*/, newClaim.BudgetAmountCents / 100.0, vatCents / 100.0, budget, description), newClaim); // Clear a cache FinancialAccount.ClearAttestationAdjustmentsCache(organization); } return(newClaim); }
public static VatReport Create(Organization organization, int year, int startMonth, int monthCount) { VatReport newReport = CreateDbRecord(organization, year, startMonth, monthCount); DateTime endDate = new DateTime(year, startMonth, 1).AddMonths(monthCount); FinancialAccount vatInbound = organization.FinancialAccounts.AssetsVatInboundUnreported; FinancialAccount vatOutbound = organization.FinancialAccounts.DebtsVatOutboundUnreported; FinancialAccount sales = organization.FinancialAccounts.IncomeSales; FinancialAccountRows inboundRows = RowsNotInVatReport(vatInbound, endDate); FinancialAccountRows outboundRows = RowsNotInVatReport(vatOutbound, endDate); FinancialAccountRows turnoverRows = RowsNotInVatReport(sales, endDate); Dictionary <int, bool> transactionsIncludedLookup = new Dictionary <int, bool>(); newReport.AddVatReportItemsFromAccountRows(inboundRows, transactionsIncludedLookup); newReport.AddVatReportItemsFromAccountRows(outboundRows, transactionsIncludedLookup); newReport.AddVatReportItemsFromAccountRows(turnoverRows, transactionsIncludedLookup); newReport.Release(); // Create financial TX that moves this VAT from unreported to reported Int64 differenceCents = newReport.VatInboundCents - newReport.VatOutboundCents; if (differenceCents != 0 && newReport.VatInboundCents > 0) { // if there's anything to report FinancialTransaction vatReportTransaction = FinancialTransaction.Create(organization, endDate.AddDays(4).AddHours(9), newReport.Description); if (newReport.VatInboundCents > 0) { vatReportTransaction.AddRow(organization.FinancialAccounts.AssetsVatInboundUnreported, -newReport.VatInboundCents, null); } if (newReport.VatOutboundCents > 0) { vatReportTransaction.AddRow(organization.FinancialAccounts.DebtsVatOutboundUnreported, newReport.VatOutboundCents, null); // not negative, because our number is sign-different from the bookkeeping's } if (differenceCents < 0) // outbound > inbound { vatReportTransaction.AddRow(organization.FinancialAccounts.DebtsVatOutboundReported, differenceCents, null); // debt, so negative as in our variable } else // inbound > outbound { vatReportTransaction.AddRow(organization.FinancialAccounts.AssetsVatInboundReported, differenceCents, null); // asset, so positive as in our variable } vatReportTransaction.Dependency = newReport; newReport.OpenTransaction = vatReportTransaction; } else { newReport.Open = false; // nothing to close, no tx created } return(newReport); }
private static FinancialDependencyType GetDependencyType(IHasIdentity foreignObject) { return(FinancialTransaction.GetFinancialDependencyType(foreignObject)); }
private void AddVatReportItemsFromAccountRows(FinancialAccountRows rows, Dictionary <int, bool> transactionsIncludedLookup) { if (rows.Count == 0) { return; } Organization organization = rows[0].Transaction.Organization; // there is always a rows[0] because check above int vatInboundAccountId = organization.FinancialAccounts.AssetsVatInboundUnreported.Identity; int vatOutboundAccountId = organization.FinancialAccounts.DebtsVatOutboundUnreported.Identity; Dictionary <int, bool> turnoverAccountLookup = new Dictionary <int, bool>(); FinancialAccounts turnoverAccounts = FinancialAccounts.ForOrganization(organization, FinancialAccountType.Income); foreach (FinancialAccount turnoverAccount in turnoverAccounts) { turnoverAccountLookup[turnoverAccount.Identity] = true; } foreach (FinancialAccountRow accountRow in rows) { FinancialTransaction tx = accountRow.Transaction; if (tx.Dependency is VatReport) { continue; // Never include previous VAT reports in new VAT reports } if (!transactionsIncludedLookup.ContainsKey(tx.Identity)) { Int64 vatInbound = 0; Int64 vatOutbound = 0; Int64 turnOver = 0; transactionsIncludedLookup[accountRow.FinancialTransactionId] = true; FinancialTransactionRows txRows = accountRow.Transaction.Rows; foreach (FinancialTransactionRow txRow in txRows) { if (txRow.FinancialAccountId == vatInboundAccountId) { vatInbound += txRow.AmountCents; } else if (txRow.FinancialAccountId == vatOutboundAccountId) { vatOutbound += -txRow.AmountCents; // this is a negative, so converting to positive } else if (turnoverAccountLookup.ContainsKey(txRow.FinancialAccountId)) { turnOver -= txRow.AmountCents; // turnover accounts are sign reversed, so convert to positive } } // Add new row to the VAT report AddItem(tx, turnOver, vatInbound, vatOutbound); } } }