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