1 package SL::DB::Helper::TransNumberGenerator;
5 use parent qw(Exporter);
6 our @EXPORT = qw(get_next_trans_number create_trans_number);
9 use List::Util qw(max);
12 use SL::PrefixedNumber;
15 SL::DB::Manager::Order->type_filter($_[0]);
19 SL::DB::Manager::DeliveryOrder->type_filter($_[0]);
23 SL::DB::Manager::Part->type_filter($_[0]);
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 },
40 sub get_next_trans_number {
41 my ($self, %params) = @_;
43 my $spec_type = $specs{ $self->meta->table } ? $self->meta->table : $self->type;
44 my $spec = $specs{ $spec_type } || croak("Unsupported class " . ref($self));
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};
52 return $number if $self->id && $number;
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;
58 my $defaults = SL::DB::Default->get;
59 my $sequence = SL::PrefixedNumber->new(number => $defaults->$number_range_column);
61 $sequence->set_to_max(@numbers) if !$fill_holes_in_range;
63 my $new_number = $sequence->get_next;
64 $new_number = $sequence->get_next while $numbers_in_use{$new_number};
66 $defaults->update_attributes($number_range_column => $new_number) if $params{update_defaults};
67 $self->$number_column($new_number) if $params{update_record};
72 sub create_trans_number {
73 my ($self, %params) = @_;
75 return $self->get_next_trans_number(update_defaults => 1, update_record => 1, %params);
88 SL::DB::Helper::TransNumberGenerator - A mixin for creating unique record numbers
94 =item C<get_next_trans_number %params>
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
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.
107 The process of creating a unique number involves the following steps:
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>.
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.
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
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:
129 =item * C<update_record>
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.
135 =item * C<update_defaults>
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.
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.
146 =item C<create_trans_number %params>
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
156 This mixin exports all of its functions: L</get_next_trans_number> and
157 L</create_trans_number>. There are no optional exports.
165 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>