summaryrefslogtreecommitdiff
path: root/FS
diff options
context:
space:
mode:
Diffstat (limited to 'FS')
-rw-r--r--FS/FS/Misc.pm3
-rw-r--r--FS/FS/Report/Tax.pm36
-rw-r--r--FS/FS/Schema.pm38
-rw-r--r--FS/FS/Template_Mixin.pm33
-rw-r--r--FS/FS/TicketSystem/RT_Internal.pm2
-rw-r--r--FS/FS/cdr/amcom.pm7
-rw-r--r--FS/FS/cust_bill_pkg.pm11
-rw-r--r--FS/FS/cust_bill_pkg_discount.pm30
-rw-r--r--FS/FS/cust_main/Billing_Batch.pm24
-rw-r--r--FS/FS/cust_main/Billing_Realtime.pm1
-rw-r--r--FS/FS/cust_main/Packages.pm2
-rw-r--r--FS/FS/cust_pkg.pm49
-rw-r--r--FS/FS/cust_pkg_discount.pm64
-rw-r--r--FS/FS/discount.pm12
-rw-r--r--FS/FS/log.pm35
-rw-r--r--FS/FS/log_email.pm108
-rw-r--r--FS/FS/msg_template.pm4
-rw-r--r--FS/FS/msg_template/email.pm6
-rw-r--r--FS/FS/part_pkg/agent.pm7
-rw-r--r--FS/FS/part_pkg/agent_invoice.pm225
-rw-r--r--FS/FS/part_pkg/discount_Mixin.pm53
-rw-r--r--FS/FS/pay_batch.pm18
-rw-r--r--FS/FS/quotation.pm3
-rw-r--r--FS/FS/quotation_pkg.pm88
-rw-r--r--FS/FS/quotation_pkg_detail.pm130
-rw-r--r--FS/FS/rate.pm35
-rw-r--r--FS/FS/rate_detail.pm5
27 files changed, 912 insertions, 117 deletions
diff --git a/FS/FS/Misc.pm b/FS/FS/Misc.pm
index d06653edd..18cb275bf 100644
--- a/FS/FS/Misc.pm
+++ b/FS/FS/Misc.pm
@@ -284,7 +284,8 @@ sub send_email {
'status' => ($error ? 'failed' : 'sent'),
'msgtype' => $options{'msgtype'},
});
- $cust_msg->insert; # ignore errors
+ my $log_error = $cust_msg->insert;
+ warn "Error logging message: $log_error\n" if $log_error; # at least warn
}
$error;
diff --git a/FS/FS/Report/Tax.pm b/FS/FS/Report/Tax.pm
index 2480a45b9..a892a6b87 100644
--- a/FS/FS/Report/Tax.pm
+++ b/FS/FS/Report/Tax.pm
@@ -240,6 +240,25 @@ sub report_internal {
$group
";
+ # also include the exempt-sales credit amount, for the credit report
+ $sql{exempt_credited} = "$select
+ SUM(COALESCE(exempt_credited, 0))
+ FROM cust_main_county
+ LEFT JOIN ($exempt_credit) AS exempt_credit USING (taxnum)
+ JOIN cust_bill_pkg USING (billpkgnum)
+ $join_cust_pkg $where AND $nottax
+ $group
+ ";
+
+ $all_sql{exempt_credited} = "$select_all
+ SUM(COALESCE(exempt_credited, 0))
+ FROM cust_main_county
+ LEFT JOIN ($exempt_credit) AS exempt_credit USING (taxnum)
+ JOIN cust_bill_pkg USING (billpkgnum)
+ $join_cust_pkg $where AND $nottax
+ $group
+ ";
+
# taxable sales
$sql{taxable} = "$select
SUM(cust_bill_pkg.setup + cust_bill_pkg.recur
@@ -339,12 +358,12 @@ sub report_internal {
my $istax = "cust_bill_pkg.pkgnum = 0 and cust_bill_pkg.feepart is null";
- $sql{tax} = "$select SUM(cust_bill_pkg_tax_location.amount)
+ $sql{tax} = "$select COALESCE(SUM(cust_bill_pkg_tax_location.amount),0)
$taxfrom
$where AND $istax
$group";
- $all_sql{tax} = "$select_all SUM(cust_bill_pkg_tax_location.amount)
+ $all_sql{tax} = "$select_all COALESCE(SUM(cust_bill_pkg_tax_location.amount),0)
$taxfrom
$where AND $istax
$group_all";
@@ -364,12 +383,12 @@ sub report_internal {
$creditwhere =~ s/cust_bill._date/cust_credit_bill._date/g;
}
- $sql{tax_credited} = "$select SUM(cust_credit_bill_pkg.amount)
+ $sql{tax_credited} = "$select COALESCE(SUM(cust_credit_bill_pkg.amount),0)
$creditfrom
$creditwhere AND $istax
$group";
- $all_sql{tax_credited} = "$select_all SUM(cust_credit_bill_pkg.amount)
+ $all_sql{tax_credited} = "$select_all COALESCE(SUM(cust_credit_bill_pkg.amount),0)
$creditfrom
$creditwhere AND $istax
$group_all";
@@ -385,12 +404,12 @@ sub report_internal {
' ON (cust_bill_pay_pkg.billpkgtaxlocationnum ='.
' cust_bill_pkg_tax_location.billpkgtaxlocationnum)';
- $sql{tax_paid} = "$select SUM(cust_bill_pay_pkg.amount)
+ $sql{tax_paid} = "$select COALESCE(SUM(cust_bill_pay_pkg.amount),0)
$paidfrom
$where AND $istax
$group";
- $all_sql{tax_paid} = "$select_all SUM(cust_bill_pay_pkg.amount)
+ $all_sql{tax_paid} = "$select_all COALESCE(SUM(cust_bill_pay_pkg.amount),0)
$paidfrom
$where AND $istax
$group_all";
@@ -562,6 +581,11 @@ sub table {
$this_row{exempt_pkg} +
$this_row{exempt_monthly}
);
+ $this_row{credits} = sprintf('%.2f',
+ $this_row{sales_credited} +
+ $this_row{exempt_credited} +
+ $this_row{tax_credited}
+ );
# and give it a label
if ( $this_row{total} ) {
$this_row{label} = 'Total';
diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm
index 486860ff6..7dc54f7c4 100644
--- a/FS/FS/Schema.pm
+++ b/FS/FS/Schema.pm
@@ -1962,6 +1962,24 @@ sub tables_hashref {
],
},
+ 'quotation_pkg_detail' => {
+ 'columns' => [
+ 'detailnum', 'serial', '', '', '', '',
+ 'billpkgnum', 'int', '', '', '', '', # actually links to quotationpkgnum
+ 'format', 'char', 'NULL', 1, '', '', # not used for anything
+ 'detail', 'varchar', '', 255, '', '',
+ ],
+ 'primary_key' => 'detailnum',
+ 'unique' => [],
+ 'index' => [ [ 'billpkgnum' ] ],
+ 'foreign_keys' => [
+ { columns => [ 'billpkgnum' ],
+ table => 'quotation_pkg',
+ references => [ 'quotationpkgnum' ],
+ },
+ ],
+ },
+
'quotation_pkg_discount' => {
'columns' => [
'quotationpkgdiscountnum', 'serial', '', '', '', '',
@@ -2888,6 +2906,7 @@ sub tables_hashref {
'otaker', 'varchar', 'NULL', 32, '', '',
'usernum', 'int', 'NULL', '', '', '',
'disabled', 'char', 'NULL', 1, '', '',
+ 'setuprecur', 'char', 'NULL', 5, '', '',
],
'primary_key' => 'pkgdiscountnum',
'unique' => [],
@@ -6544,6 +6563,25 @@ sub tables_hashref {
],
},
+ 'log_email' => {
+ 'columns' => [
+ 'logemailnum', 'serial', '', '', '', '',
+ 'context', 'varchar', 'NULL', $char_d, '', '',
+ 'min_level', 'int', 'NULL', '', '', '',
+ 'msgnum', 'int', '', '', '', '',
+ 'to_addr', 'varchar', 'NULL', 255, '', '',
+ ],
+ 'primary_key' => 'logemailnum',
+ 'unique' => [],
+ 'index' => [ ['context'], ['min_level'] ],
+ 'foreign_keys' => [
+ { columns => [ 'msgnum' ],
+ table => 'msg_template',
+ references => [ 'msgnum' ],
+ },
+ ],
+ },
+
'svc_alarm' => {
'columns' => [
# name type null length default local
diff --git a/FS/FS/Template_Mixin.pm b/FS/FS/Template_Mixin.pm
index 1a3217c44..ffaef9707 100644
--- a/FS/FS/Template_Mixin.pm
+++ b/FS/FS/Template_Mixin.pm
@@ -3050,6 +3050,9 @@ sub _items_cust_bill_pkg {
# if the current line item is waiting to go out, and the one we're about
# to start is not bundled, then push out the current one and start a new
# one.
+ if ( $d ) {
+ $d->{amount} = $d->{setup_amount} + $d->{recur_amount};
+ }
foreach ( $s, $r, ($opt{skip_usage} ? () : $u ), $d ) {
if ( $_ && !$cust_bill_pkg->hidden ) {
$_->{amount} = sprintf( "%.2f", $_->{amount} );
@@ -3485,7 +3488,8 @@ sub _items_cust_bill_pkg {
# $item_discount->{amount} is negative
if ( $d and $cust_bill_pkg->hidden ) {
- $d->{amount} += $item_discount->{amount};
+ $d->{setup_amount} += $item_discount->{setup_amount};
+ $d->{recur_amount} += $item_discount->{recur_amount};
} else {
$d = $item_discount;
$_ = &{$escape_function}($_) foreach @{ $d->{ext_description} };
@@ -3493,27 +3497,9 @@ sub _items_cust_bill_pkg {
# update the active line (before the discount) to show the
# original price (whether this is a hidden line or not)
- #
- # quotation discounts keep track of setup and recur; invoice
- # discounts currently don't
- if ( exists $item_discount->{setup_amount} ) {
-
- $s->{amount} -= $item_discount->{setup_amount} if $s;
- $r->{amount} -= $item_discount->{recur_amount} if $r;
- } else {
-
- # $active_line is the line item hashref for the line that will
- # show the original price
- # (use the recur or single line for the package, unless we're
- # showing a setup line for a package with no recurring fee)
- my $active_line = $r;
- if ( $type eq 'S' ) {
- $active_line = $s;
- }
- $active_line->{amount} -= $item_discount->{amount};
-
- }
+ $s->{amount} -= $item_discount->{setup_amount} if $s;
+ $r->{amount} -= $item_discount->{recur_amount} if $r;
} # if there are any discounts
} # if this is an appropriate place to show discounts
@@ -3522,6 +3508,11 @@ sub _items_cust_bill_pkg {
}
+ # discount amount is internally split up
+ if ( $d ) {
+ $d->{amount} = $d->{setup_amount} + $d->{recur_amount};
+ }
+
foreach ( $s, $r, ($opt{skip_usage} ? () : $u ), $d ) {
if ( $_ ) {
$_->{amount} = sprintf( "%.2f", $_->{amount} ),
diff --git a/FS/FS/TicketSystem/RT_Internal.pm b/FS/FS/TicketSystem/RT_Internal.pm
index b70ac5360..1c4513e6d 100644
--- a/FS/FS/TicketSystem/RT_Internal.pm
+++ b/FS/FS/TicketSystem/RT_Internal.pm
@@ -572,7 +572,7 @@ sub _web_external_auth {
# now get user specific information, to better create our user.
my $new_user_info
- = RT::Interface::Web::WebExternalAutoInfo($user);
+ = RT::Interface::Web::WebRemoteUserAutocreateInfo($user);
# set the attributes that have been defined.
# FIXME: this is a horrible kludge. I'm sure there's something cleaner
diff --git a/FS/FS/cdr/amcom.pm b/FS/FS/cdr/amcom.pm
index 43e6afd60..fee81f7df 100644
--- a/FS/FS/cdr/amcom.pm
+++ b/FS/FS/cdr/amcom.pm
@@ -27,8 +27,11 @@ my ($tmp_mday, $tmp_mon, $tmp_year);
'accountcode',# 2. BWGroupID (centrex group)
sub { # 3. BWGroupNumber
my ($cdr, $field) = @_; #, $conf, $hashref) = @_;
- $cdr->charged_party($field)
- if $cdr->accountcode eq '' && $field =~ /^(1800|1300)/;
+
+ if ($cdr->accountcode eq '' && $field =~ /^(1800|1300)/){
+ $cdr->charged_party($field);
+ $cdr->accountcode($field);
+ }
},
'uniqueid', # 4. Record ID
sub { # 5. Call Category (LOCAL, NATIONAL, FREECALL, MOBILE)
diff --git a/FS/FS/cust_bill_pkg.pm b/FS/FS/cust_bill_pkg.pm
index 178042666..5861ee47f 100644
--- a/FS/FS/cust_bill_pkg.pm
+++ b/FS/FS/cust_bill_pkg.pm
@@ -820,6 +820,8 @@ quantity.
sub _item_discount {
my $self = shift;
+ my %options = @_;
+
my @pkg_discounts = $self->pkg_discount;
return if @pkg_discounts == 0;
# special case: if there are old "discount details" on this line item, don't
@@ -832,7 +834,8 @@ sub _item_discount {
my $d = {
_is_discount => 1,
description => $self->mt('Discount'),
- amount => 0,
+ setup_amount => 0,
+ recur_amount => 0,
ext_description => \@ext,
pkgpart => $self->pkgpart,
feepart => $self->feepart,
@@ -840,9 +843,11 @@ sub _item_discount {
};
foreach my $pkg_discount (@pkg_discounts) {
push @ext, $pkg_discount->description;
- $d->{amount} -= $pkg_discount->amount;
+ my $setuprecur = $pkg_discount->cust_pkg_discount->setuprecur;
+ $d->{$setuprecur.'_amount'} -= $pkg_discount->amount;
}
- $d->{amount} *= $self->quantity || 1;
+ $d->{setup_amount} *= $self->quantity || 1; # ??
+ $d->{recur_amount} *= $self->quantity || 1; # ??
return $d;
}
diff --git a/FS/FS/cust_bill_pkg_discount.pm b/FS/FS/cust_bill_pkg_discount.pm
index 9e64d2076..616657a4f 100644
--- a/FS/FS/cust_bill_pkg_discount.pm
+++ b/FS/FS/cust_bill_pkg_discount.pm
@@ -135,10 +135,36 @@ Returns a string describing the discount (for use on an invoice).
sub description {
my $self = shift;
my $discount = $self->cust_pkg_discount->discount;
+
+ if ( $self->months == 0 ) {
+ # then this is a setup discount
+ my $desc = $discount->name;
+ if ( $desc ) {
+ $desc .= ': ';
+ } else {
+ $desc = $self->mt('Setup discount of ');
+ }
+ if ( (my $percent = $discount->percent) > 0 ) {
+ $percent = sprintf('%.1f', $percent) if $percent > int($percent);
+ $percent =~ s/\.0+$//;
+ $desc .= $percent . '%';
+ } else {
+ # note "$self->amount", not $discount->amount. if a flat discount
+ # is applied to the setup fee, show the amount actually discounted.
+ # we might do this for all types of discounts.
+ my $money_char = FS::Conf->new->config('money_char') || '$';
+ $desc .= $money_char . sprintf('%.2f', $self->amount);
+ }
+
+ # don't show "/month", months remaining or used, etc., as for setup
+ # discounts it doesn't matter.
+ return $desc;
+ }
+
my $desc = $discount->description_short;
$desc .= $self->mt(' each') if $self->cust_bill_pkg->quantity > 1;
- if ($discount->months) {
+ if ( $discount->months and $self->months > 0 ) {
# calculate months remaining on this cust_pkg_discount after this invoice
my $date = $self->cust_bill_pkg->cust_bill->_date;
my $used = FS::Record->scalar_sql(
@@ -152,7 +178,7 @@ sub description {
$used ||= 0;
my $remaining = sprintf('%.2f', $discount->months - $used);
$desc .= $self->mt(' for [quant,_1,month] ([quant,_2,month] remaining)',
- $self->months,
+ sprintf('%.2f', $self->months),
$remaining
);
}
diff --git a/FS/FS/cust_main/Billing_Batch.pm b/FS/FS/cust_main/Billing_Batch.pm
index c2416ab32..0cf2beeda 100644
--- a/FS/FS/cust_main/Billing_Batch.pm
+++ b/FS/FS/cust_main/Billing_Batch.pm
@@ -51,10 +51,22 @@ sub batch_card {
return '' unless $amount > 0;
my $invnum = delete $options{invnum};
- my $payby = $options{payby} || $self->payby; #still dubious
+
+ #false laziness with Billing_Realtime
+ my @cust_payby = qsearch({
+ 'table' => 'cust_payby',
+ 'hashref' => { 'custnum' => $self->custnum, },
+ 'extra_sql' => " AND payby IN ( 'CARD', 'CHEK' ) ",
+ 'order_by' => 'ORDER BY weight ASC',
+ });
+
+ # batch can't try out every one like realtime, just use first one
+ my $cust_payby = $cust_payby[0] || $self; # somewhat dubious
+
+ my $payby = $options{payby} || $cust_payby->payby;
if ($options{'realtime'}) {
- return $self->realtime_bop( FS::payby->payby2bop($self->payby),
+ return $self->realtime_bop( FS::payby->payby2bop($payby),
$amount,
%options,
);
@@ -114,10 +126,10 @@ sub batch_card {
'state' => $options{state} || $loc->state,
'zip' => $options{zip} || $loc->zip,
'country' => $options{country} || $loc->country,
- 'payby' => $options{payby} || $self->payby,
- 'payinfo' => $options{payinfo} || $self->payinfo,
- 'exp' => $options{paydate} || $self->paydate,
- 'payname' => $options{payname} || $self->payname,
+ 'payby' => $options{payby} || $cust_payby->payby,
+ 'payinfo' => $options{payinfo} || $cust_payby->payinfo,
+ 'exp' => $options{paydate} || $cust_payby->paydate,
+ 'payname' => $options{payname} || $cust_payby->payname,
'amount' => $amount, # consolidating
} );
diff --git a/FS/FS/cust_main/Billing_Realtime.pm b/FS/FS/cust_main/Billing_Realtime.pm
index 434815c16..c2ce680a1 100644
--- a/FS/FS/cust_main/Billing_Realtime.pm
+++ b/FS/FS/cust_main/Billing_Realtime.pm
@@ -888,6 +888,7 @@ sub _realtime_bop_result {
'_date' => '',
'payby' => $cust_pay_pending->payby,
'payinfo' => $options{'payinfo'},
+ 'paymask' => $options{'paymask'},
'paydate' => $cust_pay_pending->paydate,
'pkgnum' => $cust_pay_pending->pkgnum,
'discount_term' => $options{'discount_term'},
diff --git a/FS/FS/cust_main/Packages.pm b/FS/FS/cust_main/Packages.pm
index c147e551c..ead97f2c3 100644
--- a/FS/FS/cust_main/Packages.pm
+++ b/FS/FS/cust_main/Packages.pm
@@ -197,7 +197,7 @@ sub order_pkg {
map { $_ => $cust_pkg->$_() }
qw( pkgbatch
start_date order_date expire adjourn contract_end
- refnum discountnum waive_setup
+ refnum setup_discountnum recur_discountnum waive_setup
)
});
$error = $self->order_pkg('cust_pkg' => $pkg,
diff --git a/FS/FS/cust_pkg.pm b/FS/FS/cust_pkg.pm
index 279205b19..d7419078b 100644
--- a/FS/FS/cust_pkg.pm
+++ b/FS/FS/cust_pkg.pm
@@ -425,7 +425,7 @@ sub insert {
}
}
- if ( $self->discountnum ) {
+ if ( $self->setup_discountnum || $self->recur_discountnum ) {
my $error = $self->insert_discount();
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
@@ -4318,13 +4318,10 @@ sub insert_reason {
Associates this package with a discount (see L<FS::cust_pkg_discount>, possibly
inserting a new discount on the fly (see L<FS::discount>).
-Available options are:
-
-=over 4
-
-=item discountnum
-
-=back
+This will look at the cust_pkg for a pseudo-field named "setup_discountnum",
+and if present, will create a setup discount. If the discountnum is -1,
+a new discount definition will be inserted using the value in
+"setup_discountnum_amount" or "setup_discountnum_percent". Likewise for recur.
If there is an error, returns the error, otherwise returns false.
@@ -4334,21 +4331,29 @@ sub insert_discount {
#my ($self, %options) = @_;
my $self = shift;
- my $cust_pkg_discount = new FS::cust_pkg_discount {
- 'pkgnum' => $self->pkgnum,
- 'discountnum' => $self->discountnum,
- 'months_used' => 0,
- 'end_date' => '', #XXX
- #for the create a new discount case
- '_type' => $self->discountnum__type,
- 'amount' => $self->discountnum_amount,
- 'percent' => $self->discountnum_percent,
- 'months' => $self->discountnum_months,
- 'setup' => $self->discountnum_setup,
- #'disabled' => $self->discountnum_disabled,
- };
+ foreach my $x (qw(setup recur)) {
+ if ( my $discountnum = $self->get("${x}_discountnum") ) {
+ my $cust_pkg_discount = FS::cust_pkg_discount->new( {
+ 'pkgnum' => $self->pkgnum,
+ 'discountnum' => $discountnum,
+ 'setuprecur' => $x,
+ 'months_used' => 0,
+ 'end_date' => '', #XXX
+ #for the create a new discount case
+ 'amount' => $self->get("${x}_discountnum_amount"),
+ 'percent' => $self->get("${x}_discountnum_percent"),
+ 'months' => $self->get("${x}_discountnum_months"),
+ } );
+ if ( $x eq 'setup' ) {
+ $cust_pkg_discount->setup('Y');
+ $cust_pkg_discount->months('');
+ }
+ my $error = $cust_pkg_discount->insert;
+ return $error if $error;
+ }
+ }
- $cust_pkg_discount->insert;
+ '';
}
=item set_usage USAGE_VALUE_HASHREF
diff --git a/FS/FS/cust_pkg_discount.pm b/FS/FS/cust_pkg_discount.pm
index 5d0f85b5e..aa8981621 100644
--- a/FS/FS/cust_pkg_discount.pm
+++ b/FS/FS/cust_pkg_discount.pm
@@ -59,6 +59,9 @@ end_date
order taker, see L<FS::access_user>
+=item setuprecur
+
+whether this discount applies to setup fees or recurring fees
=back
@@ -125,11 +128,29 @@ sub check {
|| $self->ut_alphan('otaker')
|| $self->ut_numbern('usernum')
|| $self->ut_enum('disabled', [ '', 'Y' ] )
+ || $self->ut_enum('setuprecur', [ 'setup', 'recur' ] )
;
return $error if $error;
- return "Discount does not apply to setup fees, and package has no recurring"
- if ! $self->discount->setup && $self->cust_pkg->part_pkg->freq =~ /^0/;
+ my $cust_pkg = $self->cust_pkg;
+ my $discount = $self->discount;
+ if ( $self->setuprecur eq 'setup' ) {
+ if ( !$discount->setup ) {
+ # UI prevents this, and historical discounts should never have it either
+ return "Discount #".$self->discountnum." can't be applied to setup fees.";
+ } elsif ( $cust_pkg->base_setup == 0 ) {
+ # and this
+ return "Can't apply setup discount to a package with no setup fee.";
+ }
+ # else we're good. do NOT disallow applying setup discounts when the
+ # setup date is already set; upgrades use that.
+ } else {
+ if ( $self->cust_pkg->base_recur == 0 ) {
+ return "Can't apply recur discount to a package with no recurring fee.";
+ } elsif ( $cust_pkg->part_pkg->freq eq '0' ) {
+ return "Can't apply recur discount to a one-time charge.";
+ }
+ }
$self->usernum($FS::CurrentUser::CurrentUser->usernum) unless $self->usernum;
@@ -205,6 +226,45 @@ sub status {
sub _upgrade_data { # class method
my ($class, %opts) = @_;
$class->_upgrade_otaker(%opts);
+
+ # #14092: set setuprecur field on discounts. if we get one that applies to
+ # both setup and recur, split it into two discounts.
+ my $search = FS::Cursor->new({
+ table => 'cust_pkg_discount',
+ hashref => { setuprecur => '' }
+ });
+ while ( my $cust_pkg_discount = $search->fetch ) {
+ my $discount = $cust_pkg_discount->discount;
+ my $cust_pkg = $cust_pkg_discount->cust_pkg;
+ # 1. Does it apply to the setup fee?
+ # Yes, if: the discount applies to setup fees generally, and the package
+ # has a setup fee.
+ # No, if: the discount is a flat amount, and is not first-month only.
+ if ( $discount->setup
+ and $cust_pkg->base_setup > 0
+ and ($discount->amount == 0 or $discount->months == 1)
+ )
+ {
+ # then clone this discount into a new one
+ my $setup_discount = FS::cust_pkg_discount->new({
+ $cust_pkg_discount->hash,
+ setuprecur => 'setup',
+ pkgdiscountnum => ''
+ });
+ my $error = $setup_discount->insert;
+ die "$error (migrating cust_pkg_discount to setup discount)" if $error;
+ }
+ # 2. Does it apply to the recur fee?
+ # Yes, if: the package has a recur fee.
+ if ( $cust_pkg->base_recur > 0 ) {
+ # then modify this discount in place
+ $cust_pkg_discount->set('setuprecur' => 'recur');
+ my $error = $cust_pkg_discount->replace;
+ die "$error (migrating cust_pkg_discount)" if $error;
+ }
+ # not in here yet: splitting the cust_bill_pkg_discount records.
+ # (not really necessary)
+ }
}
=back
diff --git a/FS/FS/discount.pm b/FS/FS/discount.pm
index e11335741..13146a91b 100644
--- a/FS/FS/discount.pm
+++ b/FS/FS/discount.pm
@@ -119,12 +119,12 @@ sub check {
if ( $self->_type eq 'Select discount type' ) {
return 'Please select a discount type';
- } elsif ( $self->_type eq 'Amount' ) {
- $self->percent('0');
- return 'Amount must be greater than 0' unless $self->amount > 0;
- } elsif ( $self->_type eq 'Percentage' ) {
- $self->amount('0.00');
- return 'Percentage must be greater than 0' unless $self->percent > 0;
+ } elsif ( $self->amount > 0 ) {
+ $self->set('percent', '0');
+ } elsif ( $self->percent > 0 ) {
+ $self->set('amount', '0.00');
+ } else {
+ return "Discount amount or percentage must be > 0";
}
my $error =
diff --git a/FS/FS/log.pm b/FS/FS/log.pm
index a4ad214d0..b07910528 100644
--- a/FS/FS/log.pm
+++ b/FS/FS/log.pm
@@ -5,6 +5,7 @@ use base qw( FS::Record );
use FS::Record qw( qsearch qsearchs dbdef );
use FS::UID qw( dbh driver_name );
use FS::log_context;
+use FS::log_email;
=head1 NAME
@@ -71,6 +72,8 @@ otherwise returns false.
CONTEXT may be a list of context tags to attach to this record.
+Will send emails according to the conditions in L<FS::log_email>.
+
=cut
sub insert {
@@ -78,6 +81,7 @@ sub insert {
my $self = shift;
my $error = $self->SUPER::insert;
return $error if $error;
+ my $contexts = {}; #for quick checks when sending emails
foreach ( @_ ) {
my $context = FS::log_context->new({
'lognum' => $self->lognum,
@@ -85,11 +89,40 @@ sub insert {
});
$error = $context->insert;
return $error if $error;
+ $contexts->{$_} = 1;
+ }
+ foreach my $log_email (
+ qsearch('log_email',
+ {
+ 'disabled' => '',
+ 'min_level' => {
+ 'op' => '<=',
+ 'value' => $self->level,
+ },
+ }
+ )
+ ) {
+ # shouldn't be a lot of these, so not packing this into the qsearch
+ next if $log_email->context && !$contexts->{$log_email->context};
+ my $msg_template = qsearchs('msg_template',{ 'msgnum' => $log_email->msgnum });
+ unless ($msg_template) {
+ warn "Could not send email when logging, could not load message template for logemailnum " . $log_email->logemailnum;
+ next;
+ }
+ my $emailerror = $msg_template->send(
+ 'to' => $log_email->to_addr,
+ 'substitutions' => {
+ 'loglevel' => $FS::Log::LEVELS[$self->level], # which has hopefully been loaded...
+ 'logcontext' => $log_email->context, # use the one that triggered the email
+ 'logmessage' => $self->message,
+ },
+ );
+ warn "Could not send email when logging: $emailerror" if $emailerror;
}
'';
}
-# the insert method can be inherited from FS::Record
+# these methods can be inherited from FS::Record
sub delete { die "Log entries can't be modified." };
diff --git a/FS/FS/log_email.pm b/FS/FS/log_email.pm
new file mode 100644
index 000000000..9c53c230a
--- /dev/null
+++ b/FS/FS/log_email.pm
@@ -0,0 +1,108 @@
+package FS::log_email;
+
+use strict;
+use base qw( FS::Record );
+use FS::Record qw( qsearch qsearchs dbdef );
+use FS::UID qw( dbh driver_name );
+
+=head1 NAME
+
+FS::log_email - Object methods for log email records
+
+=head1 SYNOPSIS
+
+ use FS::log_email;
+
+ $record = new FS::log_email \%hash;
+ $record = new FS::log_email { 'column' => 'value' };
+
+ $error = $record->insert;
+
+ $error = $new_record->replace($old_record);
+
+ $error = $record->delete;
+
+ $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::log object represents the conditions for sending an email
+when a log entry is created. FS::log inherits from FS::Record.
+The following fields are currently supported:
+
+=over 4
+
+=item logemailnum - primary key
+
+=item context - the context that will trigger the email (all contexts if unspecified)
+
+=item min_level - the minimum log level that will trigger the email (all levels if unspecified)
+
+=item msgnum - the msg_template that will be used to send the email
+
+=item to_addr - who the email will be sent to (in addition to any bcc on the template)
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new log_email entry.
+
+=cut
+
+sub table { 'log_email'; }
+
+=item insert
+
+Adds this record to the database. If there is an error, returns the error,
+otherwise returns false.
+
+=item delete
+
+Delete this record from the database.
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database. If there is an error,
+returns the error, otherwise returns false.
+
+=item check
+
+Checks all fields to make sure this is a valid record. If there is
+an error, returns the error, otherwise returns false. Called by the insert
+and replace methods.
+
+=cut
+
+sub check {
+ my $self = shift;
+
+ my $error =
+ $self->ut_numbern('logemailnum')
+ || $self->ut_textn('context') # not validating against list of contexts in log_context,
+ # because not even log_context check currently does so
+ || $self->ut_number('min_level')
+ || $self->ut_foreign_key('msgnum', 'msg_template', 'msgnum')
+ || $self->ut_textn('to_addr')
+ ;
+ return $error if $error;
+
+ $self->SUPER::check;
+}
+
+=back
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/msg_template.pm b/FS/FS/msg_template.pm
index d17fd41cb..1d357b1a3 100644
--- a/FS/FS/msg_template.pm
+++ b/FS/FS/msg_template.pm
@@ -274,7 +274,7 @@ Options are passed as a list of name/value pairs:
=item cust_main
-Customer object (required).
+Customer object
=item object
@@ -324,7 +324,7 @@ sub prepare_substitutions {
my( $self, %opt ) = @_;
my $cust_main = $opt{'cust_main'}; # or die 'cust_main required';
- my $object = $opt{'object'} or die 'object required';
+ my $object = $opt{'object'}; # or die 'object required';
warn "preparing substitutions for '".$self->msgname."'\n"
if $DEBUG;
diff --git a/FS/FS/msg_template/email.pm b/FS/FS/msg_template/email.pm
index 377dbb17b..83ff18f19 100644
--- a/FS/FS/msg_template/email.pm
+++ b/FS/FS/msg_template/email.pm
@@ -164,7 +164,7 @@ Options are passed as a list of name/value pairs:
=item cust_main
-Customer object (required).
+Customer object
=item object
@@ -215,7 +215,7 @@ sub prepare {
my( $self, %opt ) = @_;
my $cust_main = $opt{'cust_main'}; # or die 'cust_main required';
- my $object = $opt{'object'} or die 'object required';
+ my $object = $opt{'object'}; # or die 'object required';
my $hashref = $self->prepare_substitutions(%opt);
@@ -365,7 +365,7 @@ sub prepare {
my $env_to = join(', ', @to);
my $cust_msg = FS::cust_msg->new({
- 'custnum' => $cust_main->custnum,
+ 'custnum' => $cust_main ? $cust_main->custnum : '',
'msgnum' => $self->msgnum,
'_date' => $time,
'env_from' => $env_from,
diff --git a/FS/FS/part_pkg/agent.pm b/FS/FS/part_pkg/agent.pm
index e1165c4c4..1a5b6154e 100644
--- a/FS/FS/part_pkg/agent.pm
+++ b/FS/FS/part_pkg/agent.pm
@@ -14,8 +14,8 @@ $DEBUG = 0;
$me = '[FS::part_pkg::agent]';
%info = (
- 'name' => 'Wholesale bulk billing, for master customers of an agent.',
- 'shortname' => 'Wholesale bulk billing for agent',
+ 'name' => 'Wholesale billing based on package prices, for master customers of an agent.',
+ 'shortname' => 'Wholesale billing for agent (package prices)',
'inherit_fields' => [qw( prorate global_Mixin)],
'fields' => {
#'recur_method' => { 'name' => 'Recurring fee method',
@@ -94,12 +94,13 @@ sub calc_recur {
if $DEBUG;
#make sure setup dates are filled in
- my $error = $cust_main->bill; #options don't propogate from freeside-daily
+ my $error = $cust_main->bill( time => $$sdate );
die "Error pre-billing agent customer: $error" if $error;
my @cust_pkg = grep { my $setup = $_->get('setup');
my $cancel = $_->get('cancel');
+ #$setup <= $$sdate # ?
$setup < $$sdate # END
&& ( ! $cancel || $cancel > $last_bill ) #START
}
diff --git a/FS/FS/part_pkg/agent_invoice.pm b/FS/FS/part_pkg/agent_invoice.pm
new file mode 100644
index 000000000..0e1e6fe1d
--- /dev/null
+++ b/FS/FS/part_pkg/agent_invoice.pm
@@ -0,0 +1,225 @@
+package FS::part_pkg::agent_invoice;
+use base qw(FS::part_pkg::recur_Common);
+
+use strict;
+use FS::Record qw( qsearch );
+use FS::agent;
+use FS::cust_main;
+use FS::cust_bill_pkg_detail;
+use Date::Format 'time2str';
+use Text::CSV;
+
+our $DEBUG = 0;
+
+our $me = '[FS::part_pkg::agent_invoice]';
+
+tie my %itemize_options, 'Tie::IxHash',
+ 'cust_bill' => 'one line per invoice',
+ 'cust_main' => 'one line per customer',
+ 'agent' => 'one line per agent',
+;
+
+# use detail_formats for this?
+my %itemize_header = (
+ 'cust_bill' => '"Inv #","Customer","Date","Charge"',
+ 'cust_main' => '"Cust #","Customer","Charge"',
+ 'agent' => '',
+);
+
+our %info = (
+ 'name' => 'Wholesale bulk billing based on actual invoice amounts, for master customers of an agent.',
+ 'shortname' => 'Wholesale billing for agent (invoice amounts)',
+ 'inherit_fields' => [qw( prorate_Mixin global_Mixin) ],
+ 'fields' => {
+ 'cutoff_day' => { 'name' => 'Billing Day (1 - 28) for prorating or '.
+ 'subscription',
+ 'default' => '1',
+ },
+
+ 'recur_method' => { 'name' => 'Recurring fee method',
+ #'type' => 'radio',
+ #'options' => \%recur_method,
+ 'type' => 'select',
+ 'select_options' => \%FS::part_pkg::recur_Common::recur_method,
+ },
+ 'multiplier' => { 'name' => 'Percentage of billed amount to charge' },
+ 'itemize' => { 'name' => 'Display on the wholesale invoice',
+ 'type' => 'select',
+ 'select_options' => \%itemize_options,
+ },
+ },
+ 'fieldorder' => [ qw( recur_method cutoff_day multiplier itemize ),
+ FS::part_pkg::prorate_Mixin::fieldorder,
+ ],
+
+ 'weight' => 53,
+
+);
+
+#some false laziness-ish w/ the other agent plan
+sub calc_recur {
+ my $self = shift;
+ my($cust_pkg, $sdate, $details, $param ) = @_;
+
+ my $csv = Text::CSV->new({ binary => 1 });
+
+ my $itemize = $self->option('itemize') || 'cust_bill';
+ my $last_bill = $cust_pkg->last_bill;
+
+ my $conf = new FS::Conf;
+# my $money_char = $conf->config('money_char') || '$';
+
+ warn "$me billing for agent packages from ". time2str('%x', $last_bill).
+ " to ". time2str('%x', $$sdate). "\n"
+ if $DEBUG;
+
+ # only invoices dated after $last_bill, but before or on $$sdate, will be
+ # included in this wholesale bundle.
+ # $last_bill is the last date the wholesale package was billed, unless
+ # it has never been billed before, in which case it's the current time.
+ # $$sdate is the date of the invoice we are now generating. It is one of:
+ # - the bill date we are now billing, if there is one.
+ # - or the wholesale package's setup date, if there is one
+ # - or the current time
+ # It will usually not be _after_ the current time. This can still happen
+ # if this package's bill date is later in the current day than right now,
+ # and next-bill-ignore-time is on.
+ my $date_range = " AND _date <= $$sdate";
+ if ( $last_bill ) {
+ $date_range .= " AND _date > $last_bill";
+ }
+
+ my $percent = $self->option('multiplier') || 100;
+
+ my $charged_cents = 0;
+ my $wholesale_cents = 0;
+ my $total = 0;
+
+ my @agents = qsearch('agent', { 'agent_custnum' => $cust_pkg->custnum } );
+
+ # The existing "agent" plan (based on package defined charges/costs) has to
+ # ensure that all the agent's customers are billed before calculating its
+ # fee, so that it can tell which packages were charged within the period.
+ # _This_ plan has to do it because it actually uses the invoices. Either way
+ # this behavior is not ideal, especially when using freeside-daily in
+ # multiprocess mode, since it can lead to billing all of the agent's
+ # customers within the master customer's billing job. If this becomes a
+ # problem, one option is to use "freeside-daily -a" to bill the agent's
+ # customers _first_, and master customers later.
+ foreach my $agent (@agents) {
+
+ warn "$me billing for agent ". $agent->agent. "\n"
+ if $DEBUG;
+
+ # cursor this if memory usage becomes a problem
+ my @cust_main = qsearch('cust_main', { 'agentnum' => $agent->agentnum } );
+
+ foreach my $cust_main (@cust_main) {
+
+ warn "$me billing agent charges for ". $cust_main->name_short. "\n"
+ if $DEBUG;
+
+ # this option at least should propagate, or we risk generating invoices
+ # in the apparent future and then leaving them out of this group
+ my $error = $cust_main->bill( 'time' => $$sdate );
+
+ die "Error pre-billing agent customer: $error" if $error;
+
+ my @cust_bill = qsearch({
+ table => 'cust_bill',
+ hashref => { 'custnum' => $cust_main->custnum },
+ extra_sql => $date_range,
+ order_by => ' ORDER BY _date ASC',
+ });
+
+ foreach my $cust_bill (@cust_bill) {
+
+ # do we want the itemize setting to be purely cosmetic, or to actually
+ # change how the calculation is done? for now let's make it purely
+ # cosmetic, and round at the level of the individual invoice. can
+ # change this if needed.
+ $charged_cents += $cust_bill->charged * 100;
+ $wholesale_cents += sprintf('%.0f', $cust_bill->charged * $percent);
+
+ if ( $itemize eq 'cust_bill' ) {
+ $csv->combine(
+ $cust_bill->invnum,
+ $cust_main->name_short,
+ $cust_main->time2str_local('short', $cust_bill->_date),
+ sprintf('%.2f', $wholesale_cents / 100),
+ );
+ my $detail = FS::cust_bill_pkg_detail->new({
+ format => 'C',
+ startdate => $cust_bill->_date,
+ amount => sprintf('%.2f', $wholesale_cents / 100),
+ detail => $csv->string,
+ });
+ push @$details, $detail;
+
+ $total += $wholesale_cents;
+ $charged_cents = $wholesale_cents = 0;
+ }
+
+ }
+
+ if ( $itemize eq 'cust_main' ) {
+
+ $csv->combine(
+ $cust_main->custnum,
+ $cust_main->name_short,
+ sprintf('%.2f', $wholesale_cents / 100),
+ );
+ my $detail = FS::cust_bill_pkg_detail->new({
+ format => 'C',
+ amount => sprintf('%.2f', $wholesale_cents / 100),
+ detail => $csv->string,
+ });
+ push @$details, $detail;
+
+ $total += $wholesale_cents;
+ $charged_cents = $wholesale_cents = 0;
+ }
+
+ } # foreach $cust_main
+
+ if ( $itemize eq 'agent' ) {
+ $csv->combine(
+ $cust_pkg->mt('[_1] customers', $agent->agent),
+ sprintf('%.2f', $wholesale_cents / 100),
+ );
+ my $detail = FS::cust_bill_pkg_detail->new({
+ format => 'C',
+ amount => sprintf('%.2f', $wholesale_cents / 100),
+ detail => $csv->string,
+ });
+ push @$details, $detail;
+
+ $total += $wholesale_cents;
+ $charged_cents = $wholesale_cents = 0;
+ }
+
+ } # foreach $agent
+
+ if ( @$details and $itemize_header{$itemize} ) {
+ unshift @$details, FS::cust_bill_pkg_detail->new({
+ format => 'C',
+ detail => $itemize_header{$itemize},
+ });
+ }
+
+ my $charges = ($total / 100) + $self->calc_recur_Common(@_);
+
+ sprintf('%.2f', $charges );
+
+}
+
+sub can_discount { 0; }
+
+sub hide_svc_detail { 1; }
+
+sub is_free { 0; }
+
+sub can_usageprice { 0; }
+
+1;
+
diff --git a/FS/FS/part_pkg/discount_Mixin.pm b/FS/FS/part_pkg/discount_Mixin.pm
index 5de7d8ea5..1e39f6aef 100644
--- a/FS/FS/part_pkg/discount_Mixin.pm
+++ b/FS/FS/part_pkg/discount_Mixin.pm
@@ -50,6 +50,9 @@ sub calc_discount {
my $tot_discount = 0;
#UI enforces just 1 for now, will need ordering when they can be stacked
+ # discount setup/recur splitting DOES NOT TOUCH THIS YET.
+ # we need some kind of monitoring to see who if anyone still uses term
+ # discounts.
if ( $param->{freq_override} ) {
# When a customer pays for more than one month at a time to receive a
# term discount, freq_override is set to the number of months.
@@ -80,6 +83,13 @@ sub calc_discount {
}
my @cust_pkg_discount = $cust_pkg->cust_pkg_discount_active;
+
+ if ( defined $param->{'setup_charge'} ) {
+ @cust_pkg_discount = grep { $_->setuprecur eq 'setup' } @cust_pkg_discount;
+ } else {
+ @cust_pkg_discount = grep { $_->setuprecur eq 'recur' } @cust_pkg_discount;
+ }
+
foreach my $cust_pkg_discount ( @cust_pkg_discount ) {
my $discount_left;
my $discount = $cust_pkg_discount->discount;
@@ -115,23 +125,17 @@ sub calc_discount {
# if it's a flat amount discount for other than one month:
# - skip the discount. unsure, leaving it alone for now.
- next unless $discount->setup;
-
$months = 0; # never count a setup discount as a month of discount
# (the recur discount in the same month should do it)
if ( $discount->percent > 0 ) {
$amount = $discount->percent * $param->{'setup_charge'} / 100;
- } elsif ( $discount->amount > 0 && ($discount->months || 0) == 1) {
+ } elsif ( $discount->amount > 0 ) {
# apply the discount amount, up to a maximum of the setup charge
$amount = min($discount->amount, $param->{'setup_charge'});
$discount_left = sprintf('%.2f', $discount->amount - $amount);
# transfer remainder of discount, if any, to recur
$param->{'discount_left_recur'}{$discount->discountnum} = $discount_left;
- } else {
- # I guess we don't allow multiple-month flat amount discounts to
- # apply to setup?
- next;
}
} else {
@@ -180,20 +184,27 @@ sub calc_discount {
# recur discount is zero.
#}
- # transfer remainder of discount, if any, to setup
- # this is used when the recur phase wants to add a setup fee
+ # Transfer remainder of discount, if any, to setup
+ # This is used when the recur phase wants to add a setup fee
# (prorate_defer_bill): the "discount_left_setup" amount will
- # be subtracted in _make_lines.
- if ( $discount->setup && $discount->amount > 0
- && ($discount->months || 0) != 1
- )
+ # be subtracted in _make_lines.
+ if ( $discount->amount > 0 && ($discount->months || 0) != 1 )
{
- # $amount is no longer permonth at this point! correct. very good.
- $discount_left = $amount - $recur_charge; # backward, as above
- if ( $discount_left > 0 ) {
- $amount = $recur_charge;
- $param->{'discount_left_setup'}{$discount->discountnum} =
- 0 - $discount_left;
+ # make sure there is a setup discount with this discountnum
+ # on the same package.
+ if ( qsearchs('cust_pkg_discount', {
+ pkgnum => $cust_pkg->pkgnum,
+ discountnum => $discount->discountnum,
+ setuprecur => 'setup'
+ }) )
+ {
+ # $amount is no longer permonth at this point! correct. very good.
+ $discount_left = $amount - $recur_charge; # backward, as above
+ if ( $discount_left > 0 ) {
+ $amount = $recur_charge;
+ $param->{'discount_left_setup'}{$discount->discountnum} =
+ 0 - $discount_left;
+ }
}
}
@@ -210,7 +221,7 @@ sub calc_discount {
};
}
- }
+ } # else not 'setup_charge'
$amount = sprintf('%.2f', $amount + 0.00000001 ); #so 1.005 rounds to 1.01
@@ -221,7 +232,7 @@ sub calc_discount {
'pkgdiscountnum' => $cust_pkg_discount->pkgdiscountnum,
'amount' => $amount,
'months' => $months,
- # XXX should have a 'setuprecur'
+ # 'setuprecur' is implied by the cust_pkg_discount link
};
push @{ $param->{'discounts'} }, $cust_bill_pkg_discount;
$tot_discount += $amount;
diff --git a/FS/FS/pay_batch.pm b/FS/FS/pay_batch.pm
index d7dd7bbe4..e299dd9c7 100644
--- a/FS/FS/pay_batch.pm
+++ b/FS/FS/pay_batch.pm
@@ -903,8 +903,6 @@ sub prepare_for_export {
my $status = $self->status;
if ($status eq 'O') {
$first_download = 1;
- my $error = $self->set_status('I');
- return "error updating pay_batch status: $error\n" if $error;
} elsif ($status eq 'I' && $curuser->access_right('Reprocess batches')) {
$first_download = 0;
} elsif ($status eq 'R' &&
@@ -938,7 +936,7 @@ sub prepare_for_export {
my $balance = $cust_pay_batch->cust_main->balance;
if ($balance <= 0) { # then don't charge this customer
- my $error = $cust_pay_batch->delete;
+ my $error = $cust_pay_batch->unbatch_and_delete;
return $error if $error;
} elsif ($balance < $cust_pay_batch->amount) {
# reduce the charge to the remaining balance
@@ -948,6 +946,20 @@ sub prepare_for_export {
}
# else $balance >= $cust_pay_batch->amount
}
+
+ # we might end up removing all cust_pay_batch above...
+ # probably the better way to handle this is to commit that removal,
+ # but no time to trace code & test that right now
+ #
+ # additionally, UI currently allows hand-deletion of all payments from a batch, meaning
+ # it's possible to try and process an empty batch...this is where we catch
+ # such an attempt, though it probably shouldn't be possible in the first place
+ return "Batch is empty" unless $self->cust_pay_batch;
+
+ #need to do this after unbatch_and_delete
+ my $error = $self->set_status('I');
+ return "error updating pay_batch status: $error\n" if $error;
+
} #if $first_download
'';
diff --git a/FS/FS/quotation.pm b/FS/FS/quotation.pm
index f82051066..d66b1b8e4 100644
--- a/FS/FS/quotation.pm
+++ b/FS/FS/quotation.pm
@@ -972,11 +972,12 @@ sub _items_pkg {
foreach my $quotation_pkg (@pkgs) {
my $part_pkg = $quotation_pkg->part_pkg;
+ my @details = $quotation_pkg->details;
my $setuprecur;
my $this_item = {
'pkgnum' => $quotation_pkg->quotationpkgnum,
'description' => $quotation_pkg->desc($locale),
- 'ext_description' => [],
+ 'ext_description' => \@details,
'quantity' => $quotation_pkg->quantity,
};
if ($freq eq '0') {
diff --git a/FS/FS/quotation_pkg.pm b/FS/FS/quotation_pkg.pm
index 4c78be75c..10bdc2efe 100644
--- a/FS/FS/quotation_pkg.pm
+++ b/FS/FS/quotation_pkg.pm
@@ -5,6 +5,7 @@ use strict;
use FS::Record qw( qsearchs qsearch dbh );
use FS::part_pkg;
use FS::quotation_pkg_discount; #so its loaded when TemplateItem_Mixin needs it
+use FS::quotation_pkg_detail;
use List::Util qw(sum);
=head1 NAME
@@ -101,6 +102,20 @@ sub display_table { 'quotation_pkg'; }
sub discount_table { 'quotation_pkg_discount'; }
+# detail table uses non-quotation fieldnames, see billpkgnum below
+sub detail_table { 'quotation_pkg_detail'; }
+
+=item billpkgnum
+
+Sets/returns quotationpkgnum, for ease of integration with TemplateItem_Mixin::details
+
+=cut
+
+sub billpkgnum {
+ my $self = shift;
+ $self->quotationpkgnum(@_);
+}
+
=item insert
Adds this record to the database. If there is an error, returns the error,
@@ -145,15 +160,21 @@ sub delete {
my $oldAutoCommit = $FS::UID::AutoCommit;
local $FS::UID::AutoCommit = 0;
+ my $error = $self->delete_details;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+
foreach ($self->quotation_pkg_discount, $self->quotation_pkg_tax) {
- my $error = $_->delete;
+ $error = $_->delete;
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
return $error . ' (deleting discount)';
}
}
- my $error = $self->SUPER::delete;
+ $error = $self->SUPER::delete;
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
return $error;
@@ -329,6 +350,69 @@ sub part_pkg_currency_option {
}
}
+=item delete_details
+
+Deletes all quotation_pkgs_details associated with this pkg (see L<FS::quotation_pkg_detail>).
+
+=cut
+
+sub delete_details {
+ my $self = shift;
+
+ my $oldAutoCommit = $FS::UID::AutoCommit;
+ local $FS::UID::AutoCommit = 0;
+ my $dbh = dbh;
+
+ foreach my $detail ( qsearch('quotation_pkg_detail',{ 'billpkgnum' => $self->quotationpkgnum }) ) {
+ my $error = $detail->delete;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "error removing old detail: $error";
+ }
+ }
+
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+ '';
+
+}
+
+=item set_details [ DETAIL, DETAIL, ... ]
+
+Sets quotation details for this package (see L<FS::quotation_pkg_detail>).
+
+If there is an error, returns the error, otherwise returns false.
+
+=cut
+
+sub set_details {
+ my( $self, @details ) = @_;
+
+ my $oldAutoCommit = $FS::UID::AutoCommit;
+ local $FS::UID::AutoCommit = 0;
+ my $dbh = dbh;
+
+ my $error = $self->delete_details;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+
+ foreach my $detail ( @details ) {
+ my $quotation_pkg_detail = new FS::quotation_pkg_detail {
+ 'billpkgnum' => $self->quotationpkgnum,
+ 'detail' => $detail,
+ };
+ $error = $quotation_pkg_detail->insert;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "error adding new detail: $error";
+ }
+ }
+
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+ '';
+
+}
=item cust_bill_pkg_display [ type => TYPE ]
diff --git a/FS/FS/quotation_pkg_detail.pm b/FS/FS/quotation_pkg_detail.pm
new file mode 100644
index 000000000..be3d81529
--- /dev/null
+++ b/FS/FS/quotation_pkg_detail.pm
@@ -0,0 +1,130 @@
+package FS::quotation_pkg_detail;
+use base qw(FS::Record);
+
+use strict;
+
+=head1 NAME
+
+FS::quotation_pkg_detail - Object methods for quotation_pkg_detail records
+
+=head1 SYNOPSIS
+
+ use FS::quotation_pkg_detail;
+
+ $record = new FS::quotation_pkg_detail \%hash;
+ $record = new FS::quotation_pkg_detail { 'column' => 'value' };
+
+ $error = $record->insert;
+
+ $error = $new_record->replace($old_record);
+
+ $error = $record->delete;
+
+ $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::quotation_pkg_detail object represents additional customer package details
+for a quotation. FS::quotation_pkg_detail inherits from FS::Record. The following fields are
+currently supported:
+
+=over 4
+
+=item detailnum
+
+primary key
+
+=item billpkgnum
+
+named thusly for quick compatability with L<FS::TemplateItem_Mixin>,
+actually the quotationpkgnum for the relevant L<FS::quotation_pkg>
+
+=item detail
+
+detail text
+
+=cut
+
+# 'format' field isn't used, there for TemplateItem_Mixin
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new record. To add the record to the database, see L<"insert">.
+
+Note that this stores the hash reference, not a distinct copy of the hash it
+points to. You can ask the object for a copy with the I<hash> method.
+
+=cut
+
+# the new method can be inherited from FS::Record, if a table method is defined
+
+sub table { 'quotation_pkg_detail'; }
+
+=item insert
+
+Adds this record to the database. If there is an error, returns the error,
+otherwise returns false.
+
+=cut
+
+# the insert method can be inherited from FS::Record
+
+=item delete
+
+Delete this record from the database.
+
+=cut
+
+# the delete method can be inherited from FS::Record
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database. If there is an error,
+returns the error, otherwise returns false.
+
+=cut
+
+# the replace method can be inherited from FS::Record
+
+=item check
+
+Checks all fields to make sure this is a valid record. If there is
+an error, returns the error, otherwise returns false. Called by the insert
+and replace methods.
+
+=cut
+
+# the check method should currently be supplied - FS::Record contains some
+# data checking routines
+
+sub check {
+ my $self = shift;
+
+ my $error =
+ $self->ut_numbern('detailnum')
+ || $self->ut_foreign_key('billpkgnum', 'quotation_pkg', 'quotationpkgnum')
+ || $self->ut_text('detail')
+ ;
+ return $error if $error;
+
+ $self->SUPER::check;
+}
+
+=back
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::quotation_pkg>, L<FS::Record>
+
+=cut
+
+1;
+
diff --git a/FS/FS/rate.pm b/FS/FS/rate.pm
index d26d11697..9b90dd28b 100644
--- a/FS/FS/rate.pm
+++ b/FS/FS/rate.pm
@@ -54,6 +54,8 @@ it may still be processed under a different pricing addon package), or be
marked as 'skipped', or throw a fatal error, depending on the setting of
the 'ignore_unrateable' package option.
+Deprecated; we now find the default detail by its lack of regionnum.
+
=item
=back
@@ -347,7 +349,11 @@ sub dest_detail {
});
}
- return $self->default_detail unless $rate_prefix;
+ if ( !$rate_prefix ) {
+ # then this call doesn't match any known region; just return the
+ # appropriate anywhere rate
+ return $self->default_detail($cdrtypenum) || $self->default_detail('');
+ }
$regionnum = $rate_prefix->regionnum;
@@ -365,7 +371,14 @@ sub dest_detail {
%hash,
'cdrtypenum' => $cdrtypenum
});
- # find all rates maching ratenum, regionnum and null cdrtypenum
+ # failing that, return the global default for this plan with the correct
+ # cdrtypenum (skips weektime processing)
+ if ( !@details and $cdrtypenum ) {
+ my $detail = $self->default_detail($cdrtypenum);
+ return $detail if $detail;
+ }
+ # failing that, find all rates maching ratenum, regionnum and null cdrtypenum
+ # (these can have weektime stuff)
if ( !@details and $cdrtypenum ) {
@details = qsearch( 'rate_detail', {
%hash,
@@ -391,7 +404,7 @@ sub dest_detail {
return $_ if $_->ratetimenum eq '';
}
# if still nothing, return the global default rate for this plan
- return $self->default_detail;
+ return $self->default_detail('');
}
=item rate_detail
@@ -400,16 +413,24 @@ Returns all region-specific details (see L<FS::rate_detail>) for this rate.
=back
-=item default_detail
+=item default_detail [ CDRTYPENUM ]
-Returns the default rate detail, if there is one.
+Returns the default rate detail for CDRTYPENUM (or for null CDR type, if not
+specified).
=cut
sub default_detail {
my $self = shift;
- $self->default_detailnum ?
- FS::rate_detail->by_key($self->default_detailnum) : ''
+ my $cdrtypenum = shift || '';
+# $self->default_detailnum ?
+# FS::rate_detail->by_key($self->default_detailnum) : ''
+ qsearchs( 'rate_detail', {
+ ratenum => $self->ratenum,
+ cdrtypenum => $cdrtypenum,
+ dest_regionnum => '',
+ orig_regionnum => '',
+ }) || '';
}
=head1 SUBROUTINES
diff --git a/FS/FS/rate_detail.pm b/FS/FS/rate_detail.pm
index fcd9f58b4..893335401 100644
--- a/FS/FS/rate_detail.pm
+++ b/FS/FS/rate_detail.pm
@@ -132,6 +132,7 @@ sub check {
|| $self->ut_foreign_key('ratenum', 'rate', 'ratenum')
|| $self->ut_foreign_keyn('orig_regionnum', 'rate_region', 'regionnum' )
|| $self->ut_foreign_keyn('dest_regionnum', 'rate_region', 'regionnum' )
+ || $self->ut_foreign_keyn('cdrtypenum', 'cdr_type', 'cdrtypenum' )
|| $self->ut_number('min_included')
#|| $self->ut_money('min_charge')
@@ -194,6 +195,8 @@ sub dest_regionname {
my $self = shift;
my $dest_region = $self->dest_region;
$dest_region ? $dest_region->regionname : 'Global default';
+ # should be 'Anywhere' or something, to indicate that it's the
+ # cross-region default
}
=item dest_prefixes_short
@@ -230,7 +233,7 @@ associated with this rate plan.
sub rate_time_name {
my $self = shift;
- $self->ratetimenum ? $self->rate_time->ratetimename : '(default)';
+ $self->ratetimenum ? $self->rate_time->ratetimename : '(any time)';
}
=item classname