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