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