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