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