Skip to content

Commit 08c45ff

Browse files
committed
Move reconciliation to Perl for easier customization
1 parent eed651b commit 08c45ff

File tree

4 files changed

+276
-10
lines changed

4 files changed

+276
-10
lines changed

lib/LedgerSMB/Workflow/Action/Reconciliation.pm

Lines changed: 237 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package LedgerSMB::Workflow::Action::Reconciliation;
33
use v5.36;
44
use warnings;
55
no warnings "experimental::for_list"; ## no critic -- accepted in 5.40
6+
no warnings "experimental::builtin"; ## no critic -- accepted in 5.40
67

78
use parent qw( LedgerSMB::Workflow::Action );
89

@@ -31,12 +32,16 @@ Available values:
3132
3233
=over 8
3334
35+
=item * add_pending_items
36+
3437
=item * approve
3538
3639
=item * delete
3740
3841
=item * submit
3942
43+
=item * reconcile
44+
4045
=item * reject
4146
4247
=back
@@ -67,12 +72,18 @@ Used by the workflow engine to dispatch work to the action instance.
6772
=cut
6873

6974
sub execute($self, $wf) {
70-
if ($self->entrypoint eq 'approve') {
75+
if ($self->entrypoint eq 'add_pending_items') {
76+
$self->_add_pending_items( $wf );
77+
}
78+
elsif ($self->entrypoint eq 'approve') {
7179
$self->_approve( $wf );
7280
}
7381
elsif ($self->entrypoint eq 'delete') {
7482
$self->_delete( $wf );
7583
}
84+
elsif ($self->entrypoint eq 'reconcile') {
85+
$self->_reconcile( $wf );
86+
}
7687
elsif ($self->entrypoint eq 'submit') {
7788
$self->_submit( $wf );
7889
}
@@ -81,23 +92,246 @@ sub execute($self, $wf) {
8192
}
8293
}
8394

95+
#####################################
96+
#
97+
# aadd_pending_items
98+
#
99+
#####################################
100+
101+
sub _add_pending_payments($recon_fx, $pending) {
102+
# group lines of a single payment
103+
my %payments = ( __NOPAYMENT__ => [] );
104+
for my $pl ($pending->@*) {
105+
if (defined $pl->{payment_id}) {
106+
$payments{$pl->{payment_id}} //= [];
107+
push $payments{$pl->{payment_id}}->@*, $pl;
108+
}
109+
else {
110+
push $payments{__NOPAYMENT__}->@*, $pl;
111+
}
112+
}
113+
114+
# add payment lines awaiting reconciliation
115+
my @new_recon;
116+
for my ($payment_id, $lines) (%payments) {
117+
next if $payment_id eq '__NOPAYMENT__';
118+
119+
push @new_recon, {
120+
amount => (sum0
121+
map {
122+
$recon_fx ? $_->{amount_tc} : $_->{amount_bc}
123+
} $lines->@*),
124+
post_date => $lines->[0]->{paymentdate},
125+
source => $lines->[0]->{source},
126+
links => $lines,
127+
};
128+
}
129+
130+
return ($payments{__NOPAYMENT__}, \@new_recon);
131+
}
132+
133+
sub _adjust_todo_lines($pending, $book_todo) {
134+
# add adjustment lines to existing payment lines
135+
my %existing_sources;
136+
for my $line ($book_todo->@*) {
137+
$existing_sources{$line->{source}} //= [];
138+
push $existing_sources{$line->{source}}->@*, $line;
139+
}
140+
for my ($index, $line) (indexed $pending->@*) {
141+
next unless exists $existing_sources{$line->{source}};
142+
my $existing = $existing_sources{$line->{source}};
143+
my @same_date = grep {
144+
$_->{post_date} eq $line->{post_date}
145+
} $existing->@*;
146+
next if scalar(@same_date) != 1;
147+
148+
splice $pending->@*, $index, 1;
149+
push $same_date[0]->{links}->@*, $line;
150+
}
151+
152+
return;
153+
}
154+
155+
sub _add_remaining_lines($recon_fx, $pending, $book_todo) {
156+
my %dates;
157+
for my $line ($pending->@*) {
158+
$dates{$line->{transdate}} //= {};
159+
$dates{$line->{transdate}}->{$line->{source} // ''} //= [];
160+
push $dates{$line->{transdate}}->{$line->{source} // ''}->@*, $line;
161+
}
162+
for my ($date, $sources) (%dates) {
163+
for my ($source, $lines) ($sources->%*) {
164+
if ($source) {
165+
my $amount = sum0
166+
map {
167+
$recon_fx ? $_->{amount_tc} : $_->{amount_bc}
168+
} $lines->@*;
169+
push $book_todo->@*, {
170+
amount => $amount,
171+
post_date => $date,
172+
source => $source,
173+
links => $lines,
174+
};
175+
}
176+
else {
177+
for my $line ($lines->@*) {
178+
my $amount =
179+
$recon_fx ? $_->{amount_tc} : $_->{amount_bc};
180+
push $book_todo->@*, {
181+
amount => $amount,
182+
post_date => $date,
183+
links => [ $line ],
184+
};
185+
}
186+
}
187+
}
188+
}
189+
return;
190+
}
191+
192+
sub _add_pending_items($self, $wf) {
193+
my ($pending, $new_recon) =
194+
_add_pending_payments(
195+
$wf->context->param( 'recon_fx' ),
196+
$wf->context->param( '_pending_items' ) );
197+
my $book_todo = $wf->context->param( '_book_todo');
198+
push $book_todo->@*, $new_recon->@*;
199+
200+
# modifies $pending->@* and $book_todo->@*
201+
_adjust_todo_lines( $pending, $book_todo );
202+
203+
# add the remaining lines grouped by source, if they have one
204+
# modifies $book_todo->@*
205+
_add_remaining_lines( $wf->context->param( 'recon_fx' ), $pending, $book_todo );
206+
207+
return;
208+
}
209+
210+
#####################################
211+
#
212+
# approve
213+
#
214+
#####################################
215+
84216
sub _approve($self, $wf) {
85217
$wf->context->param( 'approved', 1 );
86218
}
87219

220+
#####################################
221+
#
222+
# delete
223+
#
224+
#####################################
225+
88226
sub _delete($self, $wf) {
89227
$wf->context->param( 'deleted', 1 );
90228
}
91229

92-
sub _submit($self, $wf) {
93-
$wf->context->param( 'submitted', 1 );
230+
#####################################
231+
#
232+
# reconcile
233+
#
234+
#####################################
235+
236+
sub _reconcile_source_id( $stmt, $source_id, $book_todo, $book_done ) {
237+
my $lc_source_id = lc($source_id);
238+
my $candidates = [
239+
grep {
240+
lc($book_todo->[$_]->{source}) eq $lc_source_id
241+
and $book_todo->[$_]->{post_date} eq $stmt->{post_date}
242+
} 0..$book_todo->$#*
243+
];
244+
245+
return unless $candidates->@*;
246+
247+
if (scalar($candidates->@*) == 1) {
248+
my $found = splice $book_todo->@*, $candidates->[0], 1;
249+
$found->{recon_group} = $stmt->{id};
250+
$stmt->{recon_group} = $stmt->{id};
251+
push $book_done->@*, $found;
252+
return;
253+
}
254+
255+
$candidates = [
256+
grep {
257+
$book_todo->[$_]->{amount} == $stmt->{amount}
258+
and lc($book_todo->[$_]->{source}) eq $lc_source_id
259+
and $book_todo->[$_]->{post_date} eq $stmt->{post_date}
260+
} 0..$book_todo->$#*
261+
];
262+
263+
return unless $candidates->@*;
264+
265+
my $found = splice $book_todo->@*, $candidates->[0], 1;
266+
$found->{recon_group} = $stmt->{id};
267+
$stmt->{recon_group} = $stmt->{id};
268+
push $book_done->@*, $found;
269+
return;
94270
}
95271

272+
sub _reconcile_no_nource_id($stmt, $prefix, $book_todo, $book_done) {
273+
my $candidates = [
274+
grep {
275+
$book_todo->[$_]->{amount} == $stmt->{amount}
276+
and $book_todo->[$_]->{post_date} eq $stmt->{post_date}
277+
and $book_todo->[$_]->{source} !~ m/^\Q$prefix\E/
278+
} 0..$book_todo->$#*
279+
];
280+
281+
return unless $candidates->@*;
282+
283+
my $found = splice $book_todo->@*, $candidates->[0], 1;
284+
$found->{recon_group} = $stmt->{id};
285+
$stmt->{recon_group} = $stmt->{id};
286+
push $book_done->@*, $found;
287+
return;
288+
}
289+
290+
sub _reconcile($self, $wf) {
291+
my $stmt_todo = $wf->context->param( '_stmt_todo' );
292+
my $book_todo = $wf->context->param( '_book_todo' );
293+
my $book_done = $wf->context->param( '_book_done' );
294+
my $prefix = $wf->context->param( '_prefix' );
295+
296+
for my $stmt ($stmt_todo->@*) {
297+
my $source_id;
298+
if (defined $stmt->{source_id}) {
299+
if ($stmt->{source_id} =~ m/^[0-9]+$/) {
300+
$source_id = $prefix . $stmt->{source_id};
301+
}
302+
elsif ($stmt->{source_id} ne '') {
303+
$source_id = $stmt->{source_id};
304+
}
305+
}
306+
307+
if (defined $source_id) {
308+
_reconcile_source_id( $stmt, $source_id, $book_todo, $book_done );
309+
return;
310+
}
311+
312+
_reconcle_no_source_id( $stmt, $prefix, $book_todo, $book_done );
313+
}
314+
}
315+
316+
#####################################
317+
#
318+
# reject
319+
#
320+
#####################################
321+
96322
sub _reject($self, $wf) {
97323
$wf->context->param( 'rejected', 1 );
98324
}
99325

326+
#####################################
327+
#
328+
# submit
329+
#
330+
#####################################
100331

332+
sub _submit($self, $wf) {
333+
$wf->context->param( 'submitted', 1 );
334+
}
101335

102336
1;
103337

workflows/reconciliation.actions.xml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<actions type="reconciliation">
22
<action name="add_pending_items"
3-
class="LedgerSMB::Workflow::Action::Null" />
3+
class="LedgerSMB::Workflow::Action::Reconciliation"
4+
entrypoint="add_pending_items" />
45
<action name="approve"
56
class="LedgerSMB::Workflow::Action::Reconciliation"
67
entrypoint="approve" />
@@ -12,7 +13,9 @@
1213
<action name="submit"
1314
class="LedgerSMB::Workflow::Action::Reconciliation"
1415
entrypoint="submit" />
15-
<action name="reject"
16+
<action name="upload_statement"
17+
class="LedgerSMB::Workflow::Action::Null" />
18+
<action name="reconcile"
1619
class="LedgerSMB::Workflow::Action::Reconciliation"
17-
entrypoint="reject" />
20+
entrypoint="reconcile" />
1821
</actions>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<conditions>
2+
<type>reconciliation</type>
3+
<condition name="acl-approve"
4+
class="LedgerSMB::Workflow::Condition::ACL"
5+
role="reconciliation_approve" />
6+
<!--
7+
<condition name="reconciliation_gl_ok"
8+
class="LedgerSMB::Workflow::Condition::Reconciliation::GL_Ok" />
9+
<condition name="reconciliation_stmt_ok"
10+
class="LedgerSMB::Workflow::Condition::Reconciliation::StatementOk" />
11+
-->
12+
</conditions>

workflows/reconciliation.workflow.xml

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,33 @@
88
</state>
99

1010
<state name="SAVED">
11-
<action name="add_pending_items" resulting_state="SAVED" />
11+
<action name="add_pending_items" resulting_state="OUR_ITEMS_ADDED" />
1212
<action name="delete" resulting_state="DELETED" />
1313
<action name="save" resulting_state="SAVED" />
14-
<action name="submit" resulting_state="SUBMITTED" />
14+
<action name="submit" resulting_state="SUBMITTED" >
15+
<!--
16+
<condition name="reconciliation_gl_ok" />
17+
<condition name="reconciliation_stmt_ok" />
18+
-->
19+
</action>
1520
<action name="upload_statement" resulting_state="STMT_ITEMS_ADDED" />
1621
</state>
1722

23+
<state name="OUR_ITEMS_ADDED" autorun="yes">
24+
<action name="reconcile" resulting_state="SAVED" />
25+
</state>
26+
27+
<state name="STMT_ITEMS_ADDED" autorun="yes">
28+
<action name="reconcile" resulting_state="SAVED" />
29+
</state>
30+
1831
<state name="SUBMITTED">
19-
<action name="approve" resulting_state="APPROVED" />
20-
<action name="delete" resulting_state="DELETED" />
32+
<action name="approve" resulting_state="APPROVED">
33+
<condition name="acl-approve" />
34+
</action>
35+
<action name="delete" resulting_state="DELETED">
36+
<condition name="acl-approve" />
37+
</action>
2138
<action name="reject" resulting_state="SAVED" />
2239
</state>
2340

0 commit comments

Comments
 (0)