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',                                                                           },
 
  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,    },
 
  32               purchase_delivery_order => { number_column => 'donumber',       number_range_column => 'pdonumber',      scoping => \&do_scoping,    },
 
  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               assortment              => { number_column => 'partnumber',     number_range_column => 'assortmentnumber', scoping => \&parts_scoping, },
 
  41 sub get_next_trans_number {
 
  42   my ($self, %params) = @_;
 
  44   my $spec_type           = $specs{ $self->meta->table } ? $self->meta->table : $self->type;
 
  45   my $spec                = $specs{ $spec_type } || croak("Unsupported class " . ref($self));
 
  47   my $number_column       = $spec->{number_column};
 
  48   my $number              = $self->$number_column;
 
  49   my $number_range_column = $spec->{number_range_column} || $number_column;
 
  50   my $scoping_conditions  = $spec->{scoping};
 
  51   my $fill_holes_in_range = !$spec->{keep_holes_in_range};
 
  53   return $number if $self->id && $number;
 
  55   require SL::DB::Default;
 
  56   require SL::DB::Business;
 
  58   my %conditions            = ( query => [ $scoping_conditions ? $scoping_conditions->($spec_type) : () ] );
 
  59   my %conditions_for_in_use = ( query => [ $scoping_conditions ? $scoping_conditions->($spec_type) : () ] );
 
  62   if ($spec_type =~ m{^(?:customer|vendor)$}) {
 
  63     $business = $self->business_id ? SL::DB::Business->new(id => $self->business_id)->load : $self->business;
 
  64     if ($business && (($business->customernumberinit // '') ne '')) {
 
  65       $number_range_column = 'customernumberinit';
 
  66       push @{ $conditions{query} }, ( business_id => $business->id );
 
  70       push @{ $conditions{query} }, ( business_id => undef );
 
  75   # Lock both the table where the new number is stored and the range
 
  76   # table. The storage table has to be locked first in order to
 
  77   # prevent deadlocks as the legacy code in SL/TransNumber.pm locks it
 
  80   # For the storage table we have to use a full lock in order to
 
  81   # prevent insertion of new entries while this routine is still
 
  82   # working. For the range table we only need a row-level lock,
 
  83   # therefore we're re-loading the row.
 
  84   $self->db->dbh->do("LOCK " . $self->meta->table) || die $self->db->dbh->errstr;
 
  86   my ($query_in_use, $bind_vals_in_use) = Rose::DB::Object::QueryBuilder::build_select(
 
  87     dbh                  => $self->db->dbh,
 
  88     select               => $number_column,
 
  89     tables               => [ $self->meta->table ],
 
  90     columns              => { $self->meta->table => [ $self->meta->column_names ] },
 
  92     %conditions_for_in_use,
 
  95   my @numbers        = do { no warnings 'once'; SL::DBUtils::selectall_array_query($::form, $self->db->dbh, $query_in_use, @{ $bind_vals_in_use || [] }) };
 
  96   my %numbers_in_use = map { ( $_ => 1 ) } @numbers;
 
  98   my $range_table    = ($business ? $business : SL::DB::Default->get)->load(for_update => 1);
 
 100   my $start_number   = $range_table->$number_range_column;
 
 101   $start_number      = $range_table->articlenumber if ($number_range_column =~ /^(assemblynumber|assortmentnumber)$/) && (length($start_number)//0 < 1);
 
 102   my $sequence       = SL::PrefixedNumber->new(number => $start_number // 0);
 
 104   if (!$fill_holes_in_range) {
 
 105     $sequence->set_to_max(@numbers) ;
 
 108   my $new_number = $sequence->get_next;
 
 109   $new_number    = $sequence->get_next while $numbers_in_use{$new_number};
 
 111   $range_table->update_attributes($number_range_column => $new_number) if $params{update_defaults};
 
 112   $self->$number_column($new_number)                                   if $params{update_record};
 
 117 sub create_trans_number {
 
 118   my ($self, %params) = @_;
 
 120   return $self->get_next_trans_number(update_defaults => 1, update_record => 1, %params);
 
 133 SL::DB::Helper::TransNumberGenerator - A mixin for creating unique record numbers
 
 139 =item C<get_next_trans_number %params>
 
 141 Generates a new unique record number for the mixing class. Each record
 
 142 type (invoices, sales quotations, purchase orders etc) has its own
 
 143 number range. Within these ranges all numbers should be unique. The
 
 144 table C<defaults> contains the last record number assigned for all of
 
 147 This function contains hard-coded knowledge about the modules it can
 
 148 be mixed into. This way the models themselves don't have to contain
 
 149 boilerplate code for the details like the the number range column's
 
 150 name in the C<defaults> table.
 
 152 The process of creating a unique number involves the following steps:
 
 154 At first all existing record numbers for the current type are
 
 155 retrieved from the database as well as the last number assigned from
 
 156 the table C<defaults>.
 
 158 The next step is separating the number range from C<defaults> into two
 
 159 parts: an optional non-numeric prefix and its numeric suffix. The
 
 160 prefix, if present, will be kept intact.
 
 162 Now the number itself is increased as often as neccessary to create a
 
 163 unique one by comparing the generated numbers with the existing ones
 
 164 retrieved in the first step. In this step gaps in the assigned numbers
 
 165 are filled for all currently supported tables.
 
 167 After creating the unique record number this function can update
 
 168 C<$self> and the C<defaults> table if requested. This is controlled
 
 169 with the following parameters:
 
 173 =item * C<update_record>
 
 175 Determines whether or not C<$self>'s record number field is set to the
 
 176 newly generated number. C<$self> will not be saved even if this
 
 177 parameter is trueish. Defaults to false.
 
 179 =item * C<update_defaults>
 
 181 Determines whether or not the number range value in the C<defaults>
 
 182 table should be updated. Unlike C<$self> the C<defaults> table will be
 
 183 saved. Defaults to false.
 
 187 Always returns the newly generated number. This function cannot fail
 
 188 and return a value. If it fails then it is due to exceptions.
 
 190 =item C<create_trans_number %params>
 
 192 Calls and returns L</get_next_trans_number> with the parameters
 
 193 C<update_defaults = 1> and C<update_record = 1>. C<%params> is passed
 
 200 This mixin exports all of its functions: L</get_next_trans_number> and
 
 201 L</create_trans_number>. There are no optional exports.
 
 209 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>