Merge branch 'kunden-lieferantennummernkreise-in-transnumbergenerator-2138'
[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',                                                                        fill_holes_in_range => 1 },
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, fill_holes_in_range => 1 },
31               purchase_delivery_order => { number_column => 'donumber',       number_range_column => 'pdonumber',      scoping => \&do_scoping, fill_holes_in_range => 1 },
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->{fill_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
58   my $business;
59   if ($spec_type =~ m{^(?:customer|vendor)$}) {
60     $business = $self->business_id ? SL::DB::Business->new(id => $self->business_id)->load : $self->business;
61     if ($business && (($business->customernumberinit // '') ne '')) {
62       $number_range_column = 'customernumberinit';
63       push @{ $conditions{query} }, ( business_id => $business->id );
64
65     } else {
66       undef $business;
67       push @{ $conditions{query} }, ( business_id => undef );
68
69     }
70   }
71
72   my @numbers        = map { $_->$number_column } @{ $self->_get_manager_class->get_all(%conditions) };
73   my %numbers_in_use = map { ( $_ => 1 )        } @numbers;
74
75   my $range_table    = $business ? $business : SL::DB::Default->get;
76   my $start_number   = $range_table->$number_range_column;
77   $start_number      = $range_table->articlenumber if ($number_range_column eq 'assemblynumber') && (length($start_number) < 1);
78   my $sequence       = SL::PrefixedNumber->new(number => $start_number);
79
80   $sequence->set_to_max(@numbers) if !$fill_holes_in_range;
81
82   my $new_number = $sequence->get_next;
83   $new_number    = $sequence->get_next while $numbers_in_use{$new_number};
84
85   $range_table->update_attributes($number_range_column => $new_number) if $params{update_defaults};
86   $self->$number_column($new_number)                                   if $params{update_record};
87
88   return $new_number;
89 }
90
91 sub create_trans_number {
92   my ($self, %params) = @_;
93
94   return $self->get_next_trans_number(update_defaults => 1, update_record => 1, %params);
95 }
96
97 1;
98
99 __END__
100
101 =pod
102
103 =encoding utf8
104
105 =head1 NAME
106
107 SL::DB::Helper::TransNumberGenerator - A mixin for creating unique record numbers
108
109 =head1 FUNCTIONS
110
111 =over 4
112
113 =item C<get_next_trans_number %params>
114
115 Generates a new unique record number for the mixing class. Each record
116 type (invoices, sales quotations, purchase orders etc) has its own
117 number range. Within these ranges all numbers should be unique. The
118 table C<defaults> contains the last record number assigned for all of
119 the number ranges.
120
121 This function contains hard-coded knowledge about the modules it can
122 be mixed into. This way the models themselves don't have to contain
123 boilerplate code for the details like the the number range column's
124 name in the C<defaults> table.
125
126 The process of creating a unique number involves the following steps:
127
128 At first all existing record numbers for the current type are
129 retrieved from the database as well as the last number assigned from
130 the table C<defaults>.
131
132 The next step is separating the number range from C<defaults> into two
133 parts: an optional non-numeric prefix and its numeric suffix. The
134 prefix, if present, will be kept intact.
135
136 Now the number itself is increased as often as neccessary to create a
137 unique one by comparing the generated numbers with the existing ones
138 retrieved in the first step. In this step gaps in the assigned numbers
139 are filled for some tables (e.g. invoices) but not for others
140 (e.g. sales orders).
141
142 After creating the unique record number this function can update
143 C<$self> and the C<defaults> table if requested. This is controlled
144 with the following parameters:
145
146 =over 2
147
148 =item * C<update_record>
149
150 Determines whether or not C<$self>'s record number field is set to the
151 newly generated number. C<$self> will not be saved even if this
152 parameter is trueish. Defaults to false.
153
154 =item * C<update_defaults>
155
156 Determines whether or not the number range value in the C<defaults>
157 table should be updated. Unlike C<$self> the C<defaults> table will be
158 saved. Defaults to false.
159
160 =back
161
162 Always returns the newly generated number. This function cannot fail
163 and return a value. If it fails then it is due to exceptions.
164
165 =item C<create_trans_number %params>
166
167 Calls and returns L</get_next_trans_number> with the parameters
168 C<update_defaults = 1> and C<update_record = 1>. C<%params> is passed
169 to it as well.
170
171 =back
172
173 =head1 EXPORTS
174
175 This mixin exports all of its functions: L</get_next_trans_number> and
176 L</create_trans_number>. There are no optional exports.
177
178 =head1 BUGS
179
180 Nothing here yet.
181
182 =head1 AUTHOR
183
184 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
185
186 =cut