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