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);
 
  14   SL::DB::Manager::Order->type_filter($_[0]);
 
  18   SL::DB::Manager::DeliveryOrder->type_filter($_[0]);
 
  22   SL::DB::Manager::Part->type_filter($_[0]);
 
  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 => 'articlenumber',  scoping => \&parts_scoping                        },
 
  39 sub get_next_trans_number {
 
  40   my ($self, %params) = @_;
 
  42   my $spec_type           = $specs{ $self->meta->table } ? $self->meta->table : $self->type;
 
  43   my $spec                = $specs{ $spec_type } || croak("Unsupported class " . ref($self));
 
  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};
 
  51   return $number if $self->id && $number;
 
  53   my $re              = '^(.*?)(\d+)$';
 
  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   @numbers            = grep { $_ } map { my @matches = m/$re/; @matches ? $matches[-1] * 1 : undef } @numbers;
 
  59   my $defaults        = SL::DB::Default->get;
 
  60   my $number_range    = $defaults->$number_range_column;
 
  61   my @matches         = $number_range =~ m/$re/;
 
  62   my $prefix          = (2 != scalar(@matches)) ? ''  : $matches[ 0];
 
  63   my $ref_number      = !@matches               ? '1' : $matches[-1];
 
  64   my $min_places      = length($ref_number);
 
  66   my $new_number      = $fill_holes_in_range ? $ref_number : max($ref_number, @numbers);
 
  67   my $new_number_full = undef;
 
  70     $new_number      =  $new_number + 1;
 
  71     my $new_number_s =  $new_number;
 
  72     $new_number_s    =~ s/\.\d+//g;
 
  73     $new_number_full =  $prefix . ('0' x max($min_places - length($new_number_s), 0)) . $new_number_s;
 
  74     last if !$numbers_in_use{$new_number_full};
 
  77   $defaults->update_attributes($number_range_column => $new_number_full) if $params{update_defaults};
 
  78   $self->$number_column($new_number_full)                                if $params{update_record};
 
  80   return $new_number_full;
 
  83 sub create_trans_number {
 
  84   my ($self, %params) = @_;
 
  86   return $self->get_next_trans_number(update_defaults => 1, update_record => 1, %params);
 
  99 SL::DB::Helper::TransNumberGenerator - A mixin for creating unique record numbers
 
 105 =item C<get_next_trans_number %params>
 
 107 Generates a new unique record number for the mixing class. Each record
 
 108 type (invoices, sales quotations, purchase orders etc) has its own
 
 109 number range. Within these ranges all numbers should be unique. The
 
 110 table C<defaults> contains the last record number assigned for all of
 
 113 This function contains hard-coded knowledge about the modules it can
 
 114 be mixed into. This way the models themselves don't have to contain
 
 115 boilerplate code for the details like the the number range column's
 
 116 name in the C<defaults> table.
 
 118 The process of creating a unique number involves the following steps:
 
 120 At first all existing record numbers for the current type are
 
 121 retrieved from the database as well as the last number assigned from
 
 122 the table C<defaults>.
 
 124 The next step is separating the number range from C<defaults> into two
 
 125 parts: an optional non-numeric prefix and its numeric suffix. The
 
 126 prefix, if present, will be kept intact.
 
 128 Now the number itself is increased as often as neccessary to create a
 
 129 unique one by comparing the generated numbers with the existing ones
 
 130 retrieved in the first step. In this step gaps in the assigned numbers
 
 131 are filled for some tables (e.g. invoices) but not for others
 
 134 After creating the unique record number this function can update
 
 135 C<$self> and the C<defaults> table if requested. This is controlled
 
 136 with the following parameters:
 
 140 =item * C<update_record>
 
 142 Determines whether or not C<$self>'s record number field is set to the
 
 143 newly generated number. C<$self> will not be saved even if this
 
 144 parameter is trueish. Defaults to false.
 
 146 =item * C<update_defaults>
 
 148 Determines whether or not the number range value in the C<defaults>
 
 149 table should be updated. Unlike C<$self> the C<defaults> table will be
 
 150 saved. Defaults to false.
 
 154 Always returns the newly generated number. This function cannot fail
 
 155 and return a value. If it fails then it is due to exceptions.
 
 157 =item C<create_trans_number %params>
 
 159 Calls and returns L</get_next_trans_number> with the parameters
 
 160 C<update_defaults = 1> and C<update_record = 1>. C<%params> is passed
 
 167 This mixin exports all of its functions: L</get_next_trans_number> and
 
 168 L</create_trans_number>. There are no optional exports.
 
 176 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>