]> wagnertech.de Git - mfinanz.git/blob - SL/DB/Helper/TransNumberGenerator.pm
kivitendo 3.9.2-0.2
[mfinanz.git] / SL / DB / Helper / TransNumberGenerator.pm
1 package SL::DB::Helper::TransNumberGenerator;
2
3 use strict;
4
5 use parent qw(Exporter);
6 our @EXPORT = qw(get_next_trans_number create_trans_number);
7
8 use Carp;
9 use List::Util qw(max);
10
11 use SL::DBUtils ();
12 use SL::PrefixedNumber;
13
14 sub oe_scoping {
15   SL::DB::Manager::Order->type_filter($_[0]);
16 }
17
18 sub do_scoping {
19   SL::DB::Manager::DeliveryOrder->type_filter($_[0]);
20 }
21
22 sub rec_scoping {
23   SL::DB::Manager::Reclamation->type_filter($_[0]);
24 }
25
26 sub parts_scoping {
27  # SL::DB::Manager::Part->type_filter($_[0]);
28 }
29
30 my %specs = ( ar                          => { number_column => 'invnumber',                                                                           },
31               sales_quotation             => { number_column => 'quonumber',      number_range_column => 'sqnumber',       scoping => \&oe_scoping,    },
32               sales_order_intake          => { number_column => 'ordnumber',      number_range_column => 'soinumber',      scoping => \&oe_scoping,    },
33               sales_order                 => { number_column => 'ordnumber',      number_range_column => 'sonumber',       scoping => \&oe_scoping,    },
34               request_quotation           => { number_column => 'quonumber',      number_range_column => 'rfqnumber',      scoping => \&oe_scoping,    },
35               purchase_quotation_intake   => { number_column => 'quonumber',      number_range_column => 'pqinumber',      scoping => \&oe_scoping,    },
36               purchase_order              => { number_column => 'ordnumber',      number_range_column => 'ponumber',       scoping => \&oe_scoping,    },
37               purchase_order_confirmation => { number_column => 'ordnumber',      number_range_column => 'pocnumber',      scoping => \&oe_scoping,    },
38               sales_delivery_order        => { number_column => 'donumber',       number_range_column => 'sdonumber',      scoping => \&do_scoping,    },
39               purchase_delivery_order     => { number_column => 'donumber',       number_range_column => 'pdonumber',      scoping => \&do_scoping,    },
40               supplier_delivery_order     => { number_column => 'donumber',       number_range_column => 'sudonumber',     scoping => \&do_scoping,    },
41               rma_delivery_order          => { number_column => 'donumber',       number_range_column => 'rdonumber',      scoping => \&do_scoping,    },
42               sales_reclamation           => { number_column => 'record_number',  number_range_column => 's_reclamation_record_number',scoping => \&rec_scoping,   },
43               purchase_reclamation        => { number_column => 'record_number',  number_range_column => 'p_reclamation_record_number',scoping => \&rec_scoping,   },
44               customer                    => { number_column => 'customernumber', number_range_column => 'customernumber',                             },
45               vendor                      => { number_column => 'vendornumber',   number_range_column => 'vendornumber',                               },
46               part                        => { number_column => 'partnumber',     number_range_column => 'articlenumber',                              },
47               service                     => { number_column => 'partnumber',     number_range_column => 'servicenumber',                              },
48               assembly                    => { number_column => 'partnumber',     number_range_column => 'assemblynumber',                             },
49               assortment                  => { number_column => 'partnumber',     number_range_column => 'assortmentnumber',                           },
50             );
51
52 sub get_next_trans_number {
53   my ($self, %params) = @_;
54
55   my $spec_type           = $specs{ $self->meta->table } ? $self->meta->table : $self->type;
56   my $spec                = $specs{ $spec_type } || croak("Unsupported class " . ref($self));
57
58   my $number_column       = $spec->{number_column};
59   my $number              = $self->$number_column;
60   my $number_range_column = $spec->{number_range_column} || $number_column;
61   my $scoping_conditions  = $spec->{scoping};
62   my $fill_holes_in_range = !$spec->{keep_holes_in_range};
63
64   return $number if $self->id && $number;
65
66   require SL::DB::Default;
67   require SL::DB::Business;
68
69   my %conditions            = ( query => [ $scoping_conditions ? $scoping_conditions->($spec_type) : () ] );
70   my %conditions_for_in_use = ( query => [ $scoping_conditions ? $scoping_conditions->($spec_type) : () ] );
71
72   my $business;
73   if ($spec_type =~ m{^(?:customer|vendor)$}) {
74     $business = $self->business_id ? SL::DB::Business->new(id => $self->business_id)->load : $self->business;
75     if ($business && (($business->customernumberinit // '') ne '')) {
76       $number_range_column = 'customernumberinit';
77       push @{ $conditions{query} }, ( business_id => $business->id );
78
79     } else {
80       undef $business;
81       push @{ $conditions{query} }, ( business_id => undef );
82
83     }
84   }
85
86   # Lock both the table where the new number is stored and the range
87   # table. The storage table has to be locked first in order to
88   # prevent deadlocks as the legacy code in SL/TransNumber.pm locks it
89   # first, too.
90
91   # For the storage table we have to use a full lock in order to
92   # prevent insertion of new entries while this routine is still
93   # working. For the range table we only need a row-level lock,
94   # therefore we're re-loading the row.
95   $self->db->dbh->do("LOCK " . $self->meta->table) || die $self->db->dbh->errstr;
96
97   my ($query_in_use, $bind_vals_in_use) = Rose::DB::Object::QueryBuilder::build_select(
98     dbh                  => $self->db->dbh,
99     select               => $number_column,
100     tables               => [ $self->meta->table ],
101     columns              => { $self->meta->table => [ $self->meta->column_names ] },
102     query_is_sql         => 1,
103     %conditions_for_in_use,
104   );
105
106   my @numbers        = do { no warnings 'once'; SL::DBUtils::selectall_array_query($::form, $self->db->dbh, $query_in_use, @{ $bind_vals_in_use || [] }) };
107   my %numbers_in_use = map { ( $_ => 1 ) } @numbers;
108
109   my $range_table    = ($business ? $business : SL::DB::Default->get)->load(for_update => 1);
110
111   my $start_number   = $range_table->$number_range_column;
112   my $sequence       = SL::PrefixedNumber->new(number => $start_number // 0);
113
114   if (!$fill_holes_in_range) {
115     $sequence->set_to_max(@numbers) ;
116   }
117
118   my $new_number = $sequence->get_next;
119   $new_number    = $sequence->get_next while $numbers_in_use{$new_number};
120
121   $range_table->update_attributes($number_range_column => $new_number) if $params{update_defaults};
122   $self->$number_column($new_number)                                   if $params{update_record};
123
124   return $new_number;
125 }
126
127 sub create_trans_number {
128   my ($self, %params) = @_;
129
130   return $self->get_next_trans_number(update_defaults => 1, update_record => 1, %params);
131 }
132
133 1;
134
135 __END__
136
137 =pod
138
139 =encoding utf8
140
141 =head1 NAME
142
143 SL::DB::Helper::TransNumberGenerator - A mixin for creating unique record numbers
144
145 =head1 FUNCTIONS
146
147 =over 4
148
149 =item C<get_next_trans_number %params>
150
151 Generates a new unique record number for the mixing class. Each record
152 type (invoices, sales quotations, purchase orders etc) has its own
153 number range. Within these ranges all numbers should be unique. The
154 table C<defaults> contains the last record number assigned for all of
155 the number ranges.
156
157 This function contains hard-coded knowledge about the modules it can
158 be mixed into. This way the models themselves don't have to contain
159 boilerplate code for the details like the the number range column's
160 name in the C<defaults> table.
161
162 The process of creating a unique number involves the following steps:
163
164 At first all existing record numbers for the current type are
165 retrieved from the database as well as the last number assigned from
166 the table C<defaults>.
167
168 The next step is separating the number range from C<defaults> into two
169 parts: an optional non-numeric prefix and its numeric suffix. The
170 prefix, if present, will be kept intact.
171
172 Now the number itself is increased as often as neccessary to create a
173 unique one by comparing the generated numbers with the existing ones
174 retrieved in the first step. In this step gaps in the assigned numbers
175 are filled for all currently supported tables.
176
177 After creating the unique record number this function can update
178 C<$self> and the C<defaults> table if requested. This is controlled
179 with the following parameters:
180
181 =over 2
182
183 =item * C<update_record>
184
185 Determines whether or not C<$self>'s record number field is set to the
186 newly generated number. C<$self> will not be saved even if this
187 parameter is trueish. Defaults to false.
188
189 =item * C<update_defaults>
190
191 Determines whether or not the number range value in the C<defaults>
192 table should be updated. Unlike C<$self> the C<defaults> table will be
193 saved. Defaults to false.
194
195 =back
196
197 Always returns the newly generated number. This function cannot fail
198 and return a value. If it fails then it is due to exceptions.
199
200 =item C<create_trans_number %params>
201
202 Calls and returns L</get_next_trans_number> with the parameters
203 C<update_defaults = 1> and C<update_record = 1>. C<%params> is passed
204 to it as well.
205
206 =back
207
208 =head1 EXPORTS
209
210 This mixin exports all of its functions: L</get_next_trans_number> and
211 L</create_trans_number>. There are no optional exports.
212
213 =head1 BUGS
214
215 Nothing here yet.
216
217 =head1 AUTHOR
218
219 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
220
221 =cut