1 --Upgrade Script for 2.5.6 to 2.5.7
2 \set eg_version '''2.5.7'''
4 INSERT INTO config.upgrade_log (version, applied_to) VALUES ('2.5.7', :eg_version);
6 SELECT evergreen.upgrade_deps_block_check('0890', :eg_version);
8 CREATE OR REPLACE FUNCTION acq.transfer_fund(
10 old_amount IN NUMERIC, -- in currency of old fund
12 new_amount IN NUMERIC, -- in currency of new fund
14 xfer_note IN TEXT -- to be recorded in acq.fund_transfer
15 -- ,funding_source_in IN INT -- if user wants to specify a funding source (see notes)
17 /* -------------------------------------------------------------------------------
19 Function to transfer money from one fund to another.
21 A transfer is represented as a pair of entries in acq.fund_allocation, with a
22 negative amount for the old (losing) fund and a positive amount for the new
23 (gaining) fund. In some cases there may be more than one such pair of entries
24 in order to pull the money from different funding sources, or more specifically
25 from different funding source credits. For each such pair there is also an
26 entry in acq.fund_transfer.
28 Since funding_source is a non-nullable column in acq.fund_allocation, we must
29 choose a funding source for the transferred money to come from. This choice
30 must meet two constraints, so far as possible:
32 1. The amount transferred from a given funding source must not exceed the
33 amount allocated to the old fund by the funding source. To that end we
34 compare the amount being transferred to the amount allocated.
36 2. We shouldn't transfer money that has already been spent or encumbered, as
37 defined by the funding attribution process. We attribute expenses to the
38 oldest funding source credits first. In order to avoid transferring that
39 attributed money, we reverse the priority, transferring from the newest funding
40 source credits first. There can be no guarantee that this approach will
41 avoid overcommitting a fund, but no other approach can do any better.
43 In this context the age of a funding source credit is defined by the
44 deadline_date for credits with deadline_dates, and by the effective_date for
45 credits without deadline_dates, with the proviso that credits with deadline_dates
46 are all considered "older" than those without.
50 In the signature for this function, there is one last parameter commented out,
51 named "funding_source_in". Correspondingly, the WHERE clause for the query
52 driving the main loop has an OR clause commented out, which references the
53 funding_source_in parameter.
55 If these lines are uncommented, this function will allow the user optionally to
56 restrict a fund transfer to a specified funding source. If the source
57 parameter is left NULL, then there will be no such restriction.
59 ------------------------------------------------------------------------------- */
61 same_currency BOOLEAN;
62 currency_ratio NUMERIC;
63 old_fund_currency TEXT;
64 old_remaining NUMERIC; -- in currency of old fund
65 new_fund_currency TEXT;
66 new_fund_active BOOLEAN;
67 new_remaining NUMERIC; -- in currency of new fund
68 curr_old_amt NUMERIC; -- in currency of old fund
69 curr_new_amt NUMERIC; -- in currency of new fund
70 source_addition NUMERIC; -- in currency of funding source
71 source_deduction NUMERIC; -- in currency of funding source
72 orig_allocated_amt NUMERIC; -- in currency of funding source
73 allocated_amt NUMERIC; -- in currency of fund
79 IF old_fund IS NULL THEN
80 RAISE EXCEPTION 'acq.transfer_fund: old fund id is NULL';
83 IF old_amount IS NULL THEN
84 RAISE EXCEPTION 'acq.transfer_fund: amount to transfer is NULL';
87 -- The new fund and its amount must be both NULL or both not NULL.
89 IF new_fund IS NOT NULL AND new_amount IS NULL THEN
90 RAISE EXCEPTION 'acq.transfer_fund: amount to transfer to receiving fund is NULL';
93 IF new_fund IS NULL AND new_amount IS NOT NULL THEN
94 RAISE EXCEPTION 'acq.transfer_fund: receiving fund is NULL, its amount is not NULL';
97 IF user_id IS NULL THEN
98 RAISE EXCEPTION 'acq.transfer_fund: user id is NULL';
101 -- Initialize the amounts to be transferred, each denominated
102 -- in the currency of its respective fund. They will be
103 -- reduced on each iteration of the loop.
105 old_remaining := old_amount;
106 new_remaining := new_amount;
108 -- RAISE NOTICE 'Transferring % in fund % to % in fund %',
109 -- old_amount, old_fund, new_amount, new_fund;
111 -- Get the currency types of the old and new funds.
122 IF old_fund_currency IS NULL THEN
123 RAISE EXCEPTION 'acq.transfer_fund: old fund id % is not defined', old_fund;
126 IF new_fund IS NOT NULL THEN
138 IF new_fund_currency IS NULL THEN
139 RAISE EXCEPTION 'acq.transfer_fund: new fund id % is not defined', new_fund;
140 ELSIF NOT new_fund_active THEN
142 -- No point in putting money into a fund from whence you can't spend it
144 RAISE EXCEPTION 'acq.transfer_fund: new fund id % is inactive', new_fund;
147 IF new_amount = old_amount THEN
148 same_currency := true;
152 -- We'll have to translate currency between funds. We presume that
153 -- the calling code has already applied an appropriate exchange rate,
154 -- so we'll apply the same conversion to each sub-transfer.
156 same_currency := false;
157 currency_ratio := new_amount / old_amount;
161 -- Identify the funding source(s) from which we want to transfer the money.
162 -- The principle is that we want to transfer the newest money first, because
163 -- we spend the oldest money first. The priority for spending is defined
164 -- by a sort of the view acq.ordered_funding_source_credit.
171 ofsc.amount * acq.exchange_ratio( fs.currency_type, old_fund_currency )
175 acq.ordered_funding_source_credit AS ofsc,
176 acq.funding_source fs
178 ofsc.funding_source = fs.id
179 and ofsc.funding_source IN
181 SELECT funding_source
182 FROM acq.fund_allocation
183 WHERE fund = old_fund
187 -- ofsc.funding_source = funding_source_in
188 -- OR funding_source_in IS NULL
191 ofsc.sort_priority desc,
196 -- Determine how much money the old fund got from this funding source,
197 -- denominated in the currency types of the source and of the fund.
198 -- This result may reflect transfers from previous iterations.
201 COALESCE( sum( amount ), 0 ),
202 COALESCE( sum( amount )
203 * acq.exchange_ratio( source.currency_type, old_fund_currency ), 0 )
205 orig_allocated_amt, -- in currency of the source
206 allocated_amt -- in currency of the old fund
211 and funding_source = source.funding_source;
213 -- Determine how much to transfer from this credit, in the currency
214 -- of the fund. Begin with the amount remaining to be attributed:
216 curr_old_amt := old_remaining;
218 -- Can't attribute more than was allocated from the fund:
220 IF curr_old_amt > allocated_amt THEN
221 curr_old_amt := allocated_amt;
224 -- Can't attribute more than the amount of the current credit:
226 IF curr_old_amt > source.converted_amt THEN
227 curr_old_amt := source.converted_amt;
230 curr_old_amt := trunc( curr_old_amt, 2 );
232 old_remaining := old_remaining - curr_old_amt;
234 -- Determine the amount to be deducted, if any,
235 -- from the old allocation.
237 IF old_remaining > 0 THEN
239 -- In this case we're using the whole allocation, so use that
240 -- amount directly instead of applying a currency translation
241 -- and thereby inviting round-off errors.
243 source_deduction := - curr_old_amt;
245 source_deduction := trunc(
247 acq.exchange_ratio( old_fund_currency, source.currency_type ),
251 IF source_deduction <> 0 THEN
253 -- Insert negative allocation for old fund in fund_allocation,
254 -- converted into the currency of the funding source
256 INSERT INTO acq.fund_allocation (
263 source.funding_source,
267 'Transfer to fund ' || new_fund
271 IF new_fund IS NOT NULL THEN
273 -- Determine how much to add to the new fund, in
274 -- its currency, and how much remains to be added:
276 IF same_currency THEN
277 curr_new_amt := curr_old_amt;
279 IF old_remaining = 0 THEN
281 -- This is the last iteration, so nothing should be left
283 curr_new_amt := new_remaining;
286 curr_new_amt := trunc( curr_old_amt * currency_ratio, 2 );
287 new_remaining := new_remaining - curr_new_amt;
291 -- Determine how much to add, if any,
292 -- to the new fund's allocation.
294 IF old_remaining > 0 THEN
296 -- In this case we're using the whole allocation, so use that amount
297 -- amount directly instead of applying a currency translation and
298 -- thereby inviting round-off errors.
300 source_addition := curr_new_amt;
301 ELSIF source.currency_type = old_fund_currency THEN
303 -- In this case we don't need a round trip currency translation,
304 -- thereby inviting round-off errors:
306 source_addition := curr_old_amt;
308 source_addition := trunc(
310 acq.exchange_ratio( new_fund_currency, source.currency_type ),
314 IF source_addition <> 0 THEN
316 -- Insert positive allocation for new fund in fund_allocation,
317 -- converted to the currency of the founding source
319 INSERT INTO acq.fund_allocation (
326 source.funding_source,
330 'Transfer from fund ' || old_fund
335 IF trunc( curr_old_amt, 2 ) <> 0
336 OR trunc( curr_new_amt, 2 ) <> 0 THEN
338 -- Insert row in fund_transfer, using amounts in the currency of the funds
340 INSERT INTO acq.fund_transfer (
347 funding_source_credit
350 trunc( curr_old_amt, 2 ),
352 trunc( curr_new_amt, 2 ),
359 if old_remaining <= 0 THEN
360 EXIT; -- Nothing more to be transferred
367 SELECT evergreen.upgrade_deps_block_check('0891', :eg_version);
369 UPDATE permission.perm_list
370 SET description = 'Allows a user to process and verify URLs'
371 WHERE code = 'URL_VERIFY';