summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--FS/FS/Conf.pm26
-rw-r--r--FS/FS/ConfDefaults.pm22
-rw-r--r--FS/FS/Mason.pm3
-rw-r--r--FS/FS/Misc/Geo.pm150
-rw-r--r--FS/FS/Record.pm9
-rw-r--r--FS/FS/Schema.pm59
-rw-r--r--FS/FS/Template_Mixin.pm135
-rw-r--r--FS/FS/TicketSystem.pm15
-rw-r--r--FS/FS/UI/Web.pm23
-rw-r--r--FS/FS/agent.pm17
-rw-r--r--FS/FS/cable_provider.pm112
-rw-r--r--FS/FS/cust_bill.pm348
-rw-r--r--FS/FS/cust_location.pm70
-rw-r--r--FS/FS/cust_main.pm25
-rw-r--r--FS/FS/cust_main/Search.pm26
-rw-r--r--FS/FS/cust_main_Mixin.pm9
-rw-r--r--FS/FS/cust_main_county.pm76
-rw-r--r--FS/FS/cust_payby.pm10
-rw-r--r--FS/FS/cust_pkg.pm24
-rw-r--r--FS/FS/cust_svc.pm14
-rw-r--r--FS/FS/invoice_conf.pm274
-rw-r--r--FS/FS/invoice_mode.pm157
-rw-r--r--FS/FS/part_event/Action/cust_bill_email.pm10
-rw-r--r--FS/FS/part_event/Action/cust_bill_print.pm9
-rw-r--r--FS/FS/part_event/Action/cust_bill_print_pdf.pm9
-rw-r--r--FS/FS/part_event/Action/cust_bill_send.pm9
-rw-r--r--FS/FS/part_event/Action/cust_bill_send_agent.pm17
-rw-r--r--FS/FS/part_event/Action/cust_bill_send_alternate.pm5
-rw-r--r--FS/FS/part_event/Action/cust_bill_send_if_newest.pm18
-rw-r--r--FS/FS/part_event/Action/cust_bill_send_reminder.pm12
-rw-r--r--FS/FS/part_event/Action/cust_statement_send.pm3
-rw-r--r--FS/FS/part_event/Action/fee.pm43
-rw-r--r--FS/FS/part_event/Condition/pkg_age_Common.pm2
-rw-r--r--FS/FS/part_export/domain_shellcommands.pm3
-rw-r--r--FS/FS/part_export/shellcommands_withdomain.pm27
-rw-r--r--FS/FS/svc_cable.pm60
-rw-r--r--FS/MANIFEST6
-rw-r--r--FS/t/cable_provider.t5
-rw-r--r--FS/t/invoice_conf.t5
-rw-r--r--FS/t/invoice_mode.t5
-rwxr-xr-xbin/generate-table-module4
-rwxr-xr-xbin/standardize-locations25
-rwxr-xr-x[-rw-r--r--]bin/test-event5
-rw-r--r--httemplate/browse/cable_provider.html32
-rw-r--r--httemplate/browse/invoice_conf.html70
-rw-r--r--httemplate/docs/about.html2
-rw-r--r--httemplate/edit/cable_provider.html20
-rw-r--r--httemplate/edit/cust_main/bottomfixup.js21
-rw-r--r--httemplate/edit/cust_main/top_misc.html7
-rw-r--r--httemplate/edit/elements/edit.html1
-rw-r--r--httemplate/edit/invoice_conf.html296
-rw-r--r--httemplate/edit/process/cable_provider.html10
-rw-r--r--httemplate/edit/process/invoice_conf.html21
-rw-r--r--httemplate/elements/columnstart.html77
-rw-r--r--httemplate/elements/menu.html6
-rw-r--r--httemplate/elements/select-cable_provider.html7
-rw-r--r--httemplate/elements/tr-select-cable_provider.html12
-rw-r--r--httemplate/elements/tr-select-invoice_mode.html10
-rw-r--r--httemplate/misc/delete-invoice_conf.html19
-rwxr-xr-xhttemplate/misc/email-invoice.cgi2
-rwxr-xr-xhttemplate/misc/fax-invoice.cgi2
-rwxr-xr-xhttemplate/misc/print-invoice.cgi2
-rw-r--r--httemplate/misc/send-invoice.cgi5
-rw-r--r--httemplate/misc/xmlhttp-cust_main-duplicates.html4
-rw-r--r--httemplate/search/phone_state.html167
-rwxr-xr-xhttemplate/view/cust_bill.cgi35
-rwxr-xr-xhttemplate/view/cust_main.cgi1
-rwxr-xr-xhttemplate/view/cust_statement.html4
-rw-r--r--httemplate/view/elements/cust_bill-typeset7
-rw-r--r--ng_selfservice/.freeside.class.php.swpbin12288 -> 0 bytes
-rw-r--r--ng_selfservice/.index.php.swpbin12288 -> 0 bytes
-rw-r--r--ng_selfservice/.logout.php.swpbin12288 -> 0 bytes
-rw-r--r--ng_selfservice/.main.php.swpbin12288 -> 0 bytes
-rw-r--r--ng_selfservice/.password.php.swpbin12288 -> 0 bytes
-rw-r--r--ng_selfservice/.payment.php.swpbin12288 -> 0 bytes
-rw-r--r--ng_selfservice/.payment_ach.php.swpbin12288 -> 0 bytes
-rw-r--r--ng_selfservice/.payment_cc.php.swpbin12288 -> 0 bytes
-rw-r--r--ng_selfservice/.payment_paypal.php.swpbin12288 -> 0 bytes
-rw-r--r--ng_selfservice/.payment_webpay.php.swpbin12288 -> 0 bytes
-rw-r--r--ng_selfservice/.personal.php.swpbin12288 -> 0 bytes
-rw-r--r--ng_selfservice/.process_login.php.swpbin12288 -> 0 bytes
-rw-r--r--ng_selfservice/.process_ticket_create.php.swpbin12288 -> 0 bytes
-rw-r--r--ng_selfservice/.services.php.swpbin12288 -> 0 bytes
-rw-r--r--ng_selfservice/.services_new.php.swpbin20480 -> 0 bytes
-rw-r--r--ng_selfservice/.ticket.php.swpbin12288 -> 0 bytes
-rw-r--r--ng_selfservice/.ticket_create.php.swpbin12288 -> 0 bytes
-rw-r--r--ng_selfservice/.tickets.php.swpbin12288 -> 0 bytes
-rw-r--r--ng_selfservice/.tickets_resolved.php.swpbin12288 -> 0 bytes
-rw-r--r--ng_selfservice/.usage.php.swpbin12288 -> 0 bytes
-rw-r--r--ng_selfservice/.usage_cdr.php.swpbin12288 -> 0 bytes
-rw-r--r--ng_selfservice/.usage_data.php.swpbin12288 -> 0 bytes
-rw-r--r--ng_selfservice/elements/.card.php.swpbin12288 -> 0 bytes
-rw-r--r--ng_selfservice/elements/.check.php.swpbin12288 -> 0 bytes
-rw-r--r--ng_selfservice/elements/.error.php.swpbin12288 -> 0 bytes
-rw-r--r--ng_selfservice/elements/.header.php.swpbin12288 -> 0 bytes
-rw-r--r--ng_selfservice/elements/.menu.php.swpbin12288 -> 0 bytes
-rw-r--r--ng_selfservice/elements/.menu_footer.php.swpbin12288 -> 0 bytes
-rw-r--r--ng_selfservice/elements/.session.php.swpbin12288 -> 0 bytes
-rw-r--r--ng_selfservice/elements/.ticketlist.php.swpbin12288 -> 0 bytes
-rw-r--r--ng_selfservice/js/.menu.js.swpbin12288 -> 0 bytes
-rw-r--r--rt/lib/RT/Interface/Web.pm65
-rw-r--r--rt/lib/RT/URI/freeside/Internal.pm35
-rw-r--r--rt/share/html/Ticket/Elements/Customers10
-rwxr-xr-xrt/share/html/Ticket/Update.html2
104 files changed, 2378 insertions, 460 deletions
diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm
index 03280c484..16bbaad60 100644
--- a/FS/FS/Conf.pm
+++ b/FS/FS/Conf.pm
@@ -4044,7 +4044,7 @@ and customer address. Include units.',
'type' => 'select',
'multiple' => 1,
'select_hash' => [
- #'address1' => 'Billing address',
+ 'address' => 'Billing or service address',
],
},
@@ -4163,6 +4163,13 @@ and customer address. Include units.',
},
{
+ 'key' => 'previous_balance-payments_since',
+ 'section' => 'invoicing',
+ 'description' => 'Instead of showing payments (and credits) applied to the invoice, show those received since the previous invoice date.',
+ 'type' => 'checkbox',
+ },
+
+ {
'key' => 'balance_due_below_line',
'section' => 'invoicing',
'description' => 'Place the balance due message below a line. Only meaningful when when invoice_sections is false.',
@@ -4182,8 +4189,9 @@ and customer address. Include units.',
'description' => 'Method for standardizing customer addresses.',
'type' => 'select',
'select_hash' => [ '' => '',
- 'usps' => 'U.S. Postal Service',
+ 'usps' => 'U.S. Postal Service',
'ezlocate' => 'EZLocate',
+ 'tomtom' => 'TomTom',
],
},
@@ -4202,6 +4210,13 @@ and customer address. Include units.',
},
{
+ 'key' => 'tomtom-userid',
+ 'section' => 'UI',
+ 'description' => 'TomTom geocoding service API key. See <a href="http://www.tomtom.com/">the TomTom website</a> to obtain a key. This is recommended for addresses in the United States only.',
+ 'type' => 'text',
+ },
+
+ {
'key' => 'ezlocate-userid',
'section' => 'UI',
'description' => 'User ID for EZ-Locate service. See <a href="http://www.geocode.com/">the TomTom website</a> for access and pricing information.',
@@ -5505,6 +5520,13 @@ and customer address. Include units.',
'type' => 'text',
},
+ {
+ 'key' => 'allow_invalid_cards',
+ 'section' => '',
+ 'description' => 'Accept invalid credit card numbers. Useful for testing with fictitious customers. There is no good reason to enable this in production.',
+ 'type' => 'checkbox',
+ },
+
{ key => "apacheroot", section => "deprecated", description => "<b>DEPRECATED</b>", type => "text" },
{ key => "apachemachine", section => "deprecated", description => "<b>DEPRECATED</b>", type => "text" },
{ key => "apachemachines", section => "deprecated", description => "<b>DEPRECATED</b>", type => "text" },
diff --git a/FS/FS/ConfDefaults.pm b/FS/FS/ConfDefaults.pm
index de65b44a9..191ff8537 100644
--- a/FS/FS/ConfDefaults.pm
+++ b/FS/FS/ConfDefaults.pm
@@ -38,15 +38,15 @@ sub cust_fields_avail { (
'Cust# | Cust. Status | Name | Company' =>
'custnum | Status | Last, First | Company',
- 'Cust. Status | (bill) Customer | (service) Customer' =>
- 'Status | Last, First or Company (Last, First) | (same for service contact if present)',
- 'Cust# | Cust. Status | (bill) Customer | (service) Customer' =>
- 'custnum | Status | Last, First or Company (Last, First) | (same for service contact if present)',
+ 'Cust. Status | Customer' =>
+ 'Status | Last, First or Company (Last, First)',
+ 'Cust# | Cust. Status | Customer' =>
+ 'custnum | Status | Last, First or Company (Last, First)',
- 'Cust. Status | (bill) Name | (bill) Company | (service) Name | (service) Company' =>
- 'Status | Last, First | Company | (same for service contact if present)',
- 'Cust# | Cust. Status | (bill) Name | (bill) Company | (service) Name | (service) Company' =>
- 'custnum | Status | Last, First | Company | (same for service contact if present)',
+ 'Cust. Status | Name | Company' =>
+ 'Status | Last, First | Company',
+ 'Cust# | Cust. Status | Name | Company' =>
+ 'custnum | Status | Last, First | Company',
'Cust# | Cust. Status | Name | Company | Address 1 | Address 2 | City | State | Zip | Country | Day phone | Night phone | Invoicing email(s)' =>
'custnum | Status | Last, First | Company | (address) | Day phone | Night phone | Invoicing email(s)',
@@ -57,13 +57,13 @@ sub cust_fields_avail { (
'Cust# | Cust. Status | Name | Company | Address 1 | Address 2 | City | State | Zip | Country | Day phone | Night phone | Fax number | Invoicing email(s) | Payment Type | Current Balance' =>
'custnum | Status | Last, First | Company | (address) | (all phones) | Invoicing email(s) | Payment Type | Current Balance',
- 'Cust# | Cust. Status | (bill) Name | (bill) Company | (bill) Address 1 | (bill) Address 2 | (bill) City | (bill) State | (bill) Zip | (bill) Country | (bill) Day phone | (bill) Night phone | (service) Name | (service) Company | (service) Address 1 | (service) Address 2 | (service) City | (service) State | (service) Zip | (service) Country | (service) Day phone | (service) Night phone | Invoicing email(s)' =>
+ 'Cust# | Cust. Status | Name | Company | (bill) Address 1 | (bill) Address 2 | (bill) City | (bill) State | (bill) Zip | (bill) Country | Day phone | Night phone | (service) Address 1 | (service) Address 2 | (service) City | (service) State | (service) Zip | (service) Country | Day phone | Night phone | Invoicing email(s)' =>
'custnum | Status | Last, First | Company | (address) | Day phone | Night phone | (service address) | Invoicing email(s)',
- 'Cust# | Cust. Status | (bill) Name | (bill) Company | (bill) Address 1 | (bill) Address 2 | (bill) City | (bill) State | (bill) Zip | (bill) Country | (bill) Day phone | (bill) Night phone | (bill) Fax number | (service) Name | (service) Company | (service) Address 1 | (service) Address 2 | (service) City | (service) State | (service) Zip | (service) Country | (service) Day phone | (service) Night phone | (service) Fax number | Invoicing email(s) | Payment Type' =>
+ 'Cust# | Cust. Status | Name | Company | (bill) Address 1 | (bill) Address 2 | (bill) City | (bill) State | (bill) Zip | (bill) Country | Day phone | Night phone | Fax number | (service) Address 1 | (service) Address 2 | (service) City | (service) State | (service) Zip | (service) Country | Invoicing email(s) | Payment Type' =>
'custnum | Status | Last, First | Company | (address) | (all phones) | (service address) | Invoicing email(s) | Payment Type',
- 'Cust# | Cust. Status | (bill) Name | (bill) Company | (bill) Address 1 | (bill) Address 2 | (bill) City | (bill) State | (bill) Zip | (bill) Country | (bill) Day phone | (bill) Night phone | (bill) Fax number | (service) Name | (service) Company | (service) Address 1 | (service) Address 2 | (service) City | (service) State | (service) Zip | (service) Country | (service) Day phone | (service) Night phone | (service) Fax number | Invoicing email(s) | Payment Type | Current Balance' =>
+ 'Cust# | Cust. Status | Name | Company | (bill) Address 1 | (bill) Address 2 | (bill) City | (bill) State | (bill) Zip | (bill) Country | Day phone | Night phone | Fax number | (service) Address 1 | (service) Address 2 | (service) City | (service) State | (service) Zip | (service) Country | Day phone | Night phone | Fax number | Invoicing email(s) | Payment Type | Current Balance' =>
'custnum | Status | Last, First | Company | (address) | (all phones) | (service address) | Invoicing email(s) | Payment Type | Current Balance',
'Invoicing email(s)' => 'Invoicing email(s)',
diff --git a/FS/FS/Mason.pm b/FS/FS/Mason.pm
index 780e3ffaf..1215ca414 100644
--- a/FS/FS/Mason.pm
+++ b/FS/FS/Mason.pm
@@ -353,6 +353,9 @@ if ( -e $addl_handler_use_file ) {
use FS::sales_pkg_class;
use FS::svc_alarm;
use FS::cable_model;
+ use FS::invoice_mode;
+ use FS::invoice_conf;
+ use FS::cable_provider;
# Sammath Naur
if ( $FS::Mason::addl_handler_use ) {
diff --git a/FS/FS/Misc/Geo.pm b/FS/FS/Misc/Geo.pm
index a93d98f93..b5cc325d1 100644
--- a/FS/FS/Misc/Geo.pm
+++ b/FS/FS/Misc/Geo.pm
@@ -10,6 +10,7 @@ use HTML::TokeParser;
use URI::Escape 3.31;
use Data::Dumper;
use FS::Conf;
+use Locale::Country;
FS::UID->install_callback( sub {
$conf = new FS::Conf;
@@ -410,6 +411,155 @@ sub standardize_ezlocate {
\%result;
}
+sub standardize_tomtom {
+ # post-2013 TomTom API
+ # much better, but incompatible with ezlocate
+ my $self = shift;
+ my $location = shift;
+ my $class = 'Geo::TomTom::Geocoding';
+ eval "use $class";
+ die $@ if $@;
+
+ my $key = $conf->config('tomtom-userid')
+ or die "no tomtom-userid configured\n";
+
+ my $country = code2country($location->{country});
+ my ($address1, $address2) = ($location->{address1}, $location->{address2});
+ # try to fix some cases of the address fields being switched
+ if ( $address2 =~ /^\d/ and $address1 !~ /^\d/ ) {
+ $address2 = $address1;
+ $address1 = $location->{address2};
+ }
+ my $result = $class->query(
+ key => $key,
+ T => $address1,
+ L => $location->{city},
+ AA => $location->{state},
+ PC => $location->{zip},
+ CC => country2code($country, LOCALE_CODE_ALPHA_3),
+ );
+ unless ( $result->is_success ) {
+ die "TomTom geocoding error: ".$result->message."\n";
+ }
+ my ($match) = $result->locations;
+ if (!$match) {
+ die "Location not found.\n";
+ }
+ my $type = $match->{type};
+ warn "tomtom returned $type match\n" if $DEBUG;
+ warn Dumper($match) if $DEBUG > 1;
+ my $tract = '';
+ if ( defined $match->{censusTract} ) {
+ $tract = $match->{censusStateCode}. $match->{censusFipsCountyCode}.
+ join('.', $match->{censusTract} =~ /(....)(..)/);
+ }
+ # match levels below "intersection" should not be considered clean
+ my $clean = ($type eq 'addresspoint' ||
+ $type eq 'poi' ||
+ $type eq 'house' ||
+ $type eq 'intersection'
+ ) ? 'Y' : '';
+
+ $address2 = normalize_address2($address2, $location->{country});
+
+ $address1 = '';
+ $address1 = $match->{houseNumber} . ' ' if length($match->{houseNumber});
+ $address1 .= $match->{street} if $match->{street};
+
+ return +{
+ address1 => $address1,
+ address2 => $address2,
+ city => $match->{city},
+ state => $location->{state}, # this will never change
+ country => $location->{country}, # ditto
+ zip => ($match->{standardPostalCode} || $match->{postcode}),
+ latitude => $match->{latitude},
+ longitude => $match->{longitude},
+ censustract => $tract,
+ addr_clean => $clean,
+ };
+}
+
+=iten normalize_address2 STRING, COUNTRY
+
+Given an 'address2' STRING, normalize it for COUNTRY postal standards.
+Currently only works for US and CA.
+
+=cut
+
+# XXX really ought to be a separate module
+my %address2_forms = (
+ # Postal Addressing Standards, Appendix C
+ # (plus correction of "hanger" to "hangar")
+ US => {qw(
+ APARTMENT APT
+ BASEMENT BSMT
+ BUILDING BLDG
+ DEPARTMENT DEPT
+ FLOOR FL
+ FRONT FRNT
+ HANGAR HNGR
+ HANGER HNGR
+ KEY KEY
+ LOBBY LBBY
+ LOT LOT
+ LOWER LOWR
+ OFFICE OFC
+ PENTHOUSE PH
+ PIER PIER
+ REAR REAR
+ ROOM RM
+ SIDE SIDE
+ SLIP SLIP
+ SPACE SPC
+ STOP STOP
+ SUITE STE
+ TRAILER TRLR
+ UNIT UNIT
+ UPPER UPPR
+ )},
+ # Canada Post Addressing Guidelines 4.3
+ CA => {qw(
+ APARTMENT APT
+ APPARTEMENT APP
+ BUREAU BUREAU
+ SUITE SUITE
+ UNIT UNIT
+ UNITÉ UNITÉ
+ )},
+);
+
+sub normalize_address2 {
+ # Some things seen in the address2 field:
+ # Whitespace
+ # The complete address (with address1 containing part of the company name,
+ # or an ATTN or DBA line, or P.O. Box, or department name, or building/suite
+ # number, etc.)
+ my ($addr2, $country) = @_;
+ $addr2 = uc($addr2);
+ if ( exists($address2_forms{$country}) ) {
+ my $dict = $address2_forms{$country};
+ # protect this
+ $addr2 =~ s/#\s*(\d)/NUMBER$1/; # /g?
+ my @words;
+ # remove all punctuation and spaces
+ foreach my $w (split(/\W+/, $addr2)) {
+ if ( exists($dict->{$w}) ) {
+ push @words, $dict->{$w};
+ } else {
+ push @words, $w;
+ }
+ }
+ my $result = join(' ', @words);
+ # correct spacing of pound sign + number
+ $result =~ s/NUMBER(\d)/# $1/;
+ warn "normalizing '$addr2' to '$result'\n" if $DEBUG > 1;
+ $addr2 = $result;
+ }
+ $addr2;
+}
+
+
=back
=cut
diff --git a/FS/FS/Record.pm b/FS/FS/Record.pm
index fd035249b..71eddc1eb 100644
--- a/FS/FS/Record.pm
+++ b/FS/FS/Record.pm
@@ -3038,13 +3038,8 @@ Checks to see if the string is encrypted and returns true or false (1/0) to indi
sub is_encrypted {
my ($self, $value) = @_;
- # Possible Bug - Some work may be required here....
-
- if ($value =~ /^M/ && length($value) > 80) {
- return 1;
- } else {
- return 0;
- }
+ # could be more precise about it, but this will do for now
+ $value =~ /^M/ && length($value) > 80;
}
=item decrypt($value)
diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm
index ed3790452..e44b74edc 100644
--- a/FS/FS/Schema.pm
+++ b/FS/FS/Schema.pm
@@ -4272,6 +4272,8 @@ sub tables_hashref {
'svc_cable' => {
'columns' => [
'svcnum', 'int', '', '', '', '',
+ 'providernum', 'int', 'NULL', '', '', '',
+ # XXX "Circuit ID/Order number"
'modelnum', 'int', 'NULL', '', '', '',
'serialnum', 'varchar', 'NULL', $char_d, '', '',
'mac_addr', 'varchar', 'NULL', 12, '', '',
@@ -4292,6 +4294,17 @@ sub tables_hashref {
'index' => [],
},
+ 'cable_provider' => {
+ 'columns' => [
+ 'providernum', 'serial', '', '', '', '',
+ 'provider', 'varchar', '', $char_d, '', '',
+ 'disabled', 'char', 'NULL', 1, '', '',
+ ],
+ 'primary_key' => 'providernum',
+ 'unique' => [ [ 'provider' ], ],
+ 'index' => [],
+ },
+
'vend_main' => {
'columns' => [
'vendnum', 'serial', '', '', '', '',
@@ -4377,6 +4390,52 @@ sub tables_hashref {
'index' => [ [ 'derivenum', ], ],
},
+ 'invoice_mode' => {
+ 'columns' => [
+ 'modenum', 'serial', '', '', '', '',
+ 'agentnum', 'int', 'NULL', '', '', '',
+ 'modename', 'varchar', '', 32, '', '',
+ ],
+ 'primary_key' => 'modenum',
+ 'unique' => [ ],
+ 'index' => [ ],
+ },
+
+ 'invoice_conf' => {
+ 'columns' => [
+ 'confnum', 'serial', '', '', '', '',
+ 'modenum', 'int', '', '', '', '',
+ 'locale', 'varchar', 'NULL', 16, '', '',
+ 'notice_name', 'varchar', 'NULL', 64, '', '',
+ 'subject', 'varchar', 'NULL', 64, '', '',
+ 'htmlnotes', 'text', 'NULL', '', '', '',
+ 'htmlfooter', 'text', 'NULL', '', '', '',
+ 'htmlsummary', 'text', 'NULL', '', '', '',
+ 'htmlreturnaddress', 'text', 'NULL', '', '', '',
+ 'latexnotes', 'text', 'NULL', '', '', '',
+ 'latexfooter', 'text', 'NULL', '', '', '',
+ 'latexsummary', 'text', 'NULL', '', '', '',
+ 'latexcoupon', 'text', 'NULL', '', '', '',
+ 'latexsmallfooter', 'text', 'NULL', '', '', '',
+ 'latexreturnaddress', 'text', 'NULL', '', '', '',
+ 'latextopmargin', 'varchar', 'NULL', 16, '', '',
+ 'latexheadsep', 'varchar', 'NULL', 16, '', '',
+ 'latexaddresssep', 'varchar', 'NULL', 16, '', '',
+ 'latextextheight', 'varchar', 'NULL', 16, '', '',
+ 'latexextracouponspace','varchar', 'NULL', 16, '', '',
+ 'latexcouponfootsep', 'varchar', 'NULL', 16, '', '',
+ 'latexcouponamountenclosedsep', 'varchar', 'NULL', 16, '', '',
+ 'latexcoupontoaddresssep', 'varchar', 'NULL', 16, '', '',
+ 'latexverticalreturnaddress', 'char', 'NULL', 1, '', '',
+ 'latexcouponaddcompanytoaddress', 'char', 'NULL', 1, '', '',
+ 'logo_png', 'blob', 'NULL', '', '', '',
+ 'logo_eps', 'blob', 'NULL', '', '', '',
+ 'lpr', 'varchar', 'NULL', $char_d, '', '',
+ ],
+ 'primary_key' => 'confnum',
+ 'unique' => [ [ 'modenum', 'locale' ] ],
+ 'index' => [ ],
+ },
# name type nullability length default local
diff --git a/FS/FS/Template_Mixin.pm b/FS/FS/Template_Mixin.pm
index db3885443..840df7558 100644
--- a/FS/FS/Template_Mixin.pm
+++ b/FS/FS/Template_Mixin.pm
@@ -18,6 +18,7 @@ use FS::Record qw( qsearch qsearchs );
use FS::Misc qw( generate_ps generate_pdf );
use FS::pkg_category;
use FS::pkg_class;
+use FS::invoice_mode;
use FS::L10N;
$DEBUG = 0;
@@ -30,12 +31,51 @@ FS::UID->install_callback( sub {
$date_format_long = $conf->config('date_format_long') || '%b %o, %Y';
} );
-=item print_text HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
+=item conf [ MODE ]
+
+Returns a configuration handle (L<FS::Conf>) set to the customer's locale.
+
+If the "mode" pseudo-field is set on the object, the configuration handle
+will be an L<FS::invoice_conf> for that invoice mode (and the customer's
+locale).
+
+=cut
+
+sub conf {
+ my $self = shift;
+ my $mode = $self->get('mode');
+ if ($self->{_conf} and !defined($mode)) {
+ return $self->{_conf};
+ }
+
+ my $cust_main = $self->cust_main;
+ my $locale = $cust_main ? $cust_main->locale : '';
+ my $conf;
+ if ( $mode ) {
+ if ( ref $mode and $mode->isa('FS::invoice_mode') ) {
+ $mode = $mode->modenum;
+ } elsif ( $mode =~ /\D/ ) {
+ die "invalid invoice mode $mode";
+ }
+ $conf = qsearchs('invoice_conf', { modenum => $mode, locale => $locale });
+ if (!$conf) {
+ $conf = qsearchs('invoice_conf', { modenum => $mode, locale => '' });
+ # it doesn't have a locale, but system conf still might
+ $conf->set('locale' => $locale) if $conf;
+ }
+ }
+ # if $mode is unspecified, or if there is no invoice_conf matching this mode
+ # and locale, then use the system config only (but with the locale)
+ $conf ||= FS::Conf->new({ 'locale' => $locale });
+ # cache it
+ return $self->{_conf} = $conf;
+}
+
+=item print_text OPTIONS
Returns an text invoice, as a list of lines.
-Options can be passed as a hashref (recommended) or as a list of time, template
-and then any key/value pairs for any other options.
+Options can be passed as a hash.
I<time>, if specified, is used to control the printing of overdue messages. The
default is now. It isn't the date of the invoice; that's the `_date' field.
@@ -50,25 +90,19 @@ I<notice_name>, if specified, overrides "Invoice" as the name of the sent docume
sub print_text {
my $self = shift;
- my( $today, $template, %opt );
+ my %params;
if ( ref($_[0]) ) {
- %opt = %{ shift() };
- $today = delete($opt{'time'}) || '';
- $template = delete($opt{template}) || '';
+ %params = %{ shift() };
} else {
- ( $today, $template, %opt ) = @_;
+ %params = @_;
}
- my %params = ( 'format' => 'template' );
- $params{'time'} = $today if $today;
- $params{'template'} = $template if $template;
- $params{$_} = $opt{$_}
- foreach grep $opt{$_}, qw( unsquelch_cdr notice_name );
+ $params{'format'} = 'template'; # for some reason
$self->print_generic( %params );
}
-=item print_latex HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
+=item print_latex HASHREF
Internal method - returns a filename of a filled-in LaTeX template for this
invoice (Note: add ".tex" to get the actual filename), and a filename of
@@ -76,15 +110,16 @@ an associated logo (with the .eps extension included).
See print_ps and print_pdf for methods that return PostScript and PDF output.
-Options can be passed as a hashref (recommended) or as a list of time, template
-and then any key/value pairs for any other options.
+Options can be passed as a hash.
I<time>, if specified, is used to control the printing of overdue messages. The
default is now. It isn't the date of the invoice; that's the `_date' field.
It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
L<Time::Local> and L<Date::Parse> for conversion functions.
-I<template>, if specified, is the name of a suffix for alternate invoices.
+I<template>, if specified, is the name of a suffix for alternate invoices.
+This is strongly deprecated; see L<FS::invoice_conf> for the right way to
+customize invoice templates for different purposes.
I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
@@ -92,22 +127,20 @@ I<notice_name>, if specified, overrides "Invoice" as the name of the sent docume
sub print_latex {
my $self = shift;
- my $conf = $self->conf;
- my( $today, $template, %opt );
+ my %params;
+
if ( ref($_[0]) ) {
- %opt = %{ shift() };
- $today = delete($opt{'time'}) || '';
- $template = delete($opt{template}) || '';
+ %params = %{ shift() };
} else {
- ( $today, $template, %opt ) = @_;
+ %params = @_;
}
- my %params = ( 'format' => 'latex' );
- $params{'time'} = $today if $today;
- $params{'template'} = $template if $template;
- $params{$_} = $opt{$_}
- foreach grep $opt{$_}, qw( unsquelch_cdr notice_name no_date no_number );
+ $params{'format'} = 'latex';
+ my $conf = $self->conf;
+ # this needs to go away
+ my $template = $params{'template'};
+ # and this especially
$template ||= $self->_agent_template
if $self->can('_agent_template');
@@ -191,7 +224,8 @@ Non optional options include
Optional options include
-template - a value used as a suffix for a configuration template
+template - a value used as a suffix for a configuration template. Please
+don't use this.
time - a value used to control the printing of overdue messages. The
default is now. It isn't the date of the invoice; that's the `_date' field.
@@ -214,6 +248,7 @@ locale - override customer's locale
sub print_generic {
my( $self, %params ) = @_;
my $conf = $self->conf;
+
my $today = $params{today} ? $params{today} : time;
warn "$me print_generic called on $self with suffix $params{template}\n"
if $DEBUG;
@@ -227,6 +262,8 @@ sub print_generic {
unless $cust_main->payname
&& $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
+ my $locale = $params{'locale'} || $cust_main->locale;
+
my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
'html' => [ '<%=', '%>' ],
'template' => [ '{', '}' ],
@@ -235,11 +272,18 @@ sub print_generic {
warn "$me print_generic creating template\n"
if $DEBUG > 1;
+ # set the notice name here, and nowhere else.
+ my $notice_name = $params{notice_name}
+ || $conf->config('notice_name')
+ || $self->notice_name;
+
#create the template
my $template = $params{template} ? $params{template} : $self->_agent_template;
my $templatefile = $self->template_conf. $format;
$templatefile .= "_$template"
if length($template) && $conf->exists($templatefile."_$template");
+
+ # the base template
my @invoice_template = map "$_\n", $conf->config($templatefile)
or die "cannot load config data $templatefile";
@@ -380,6 +424,7 @@ sub print_generic {
# generate template variables
my $returnaddress;
+
if (
defined( $conf->config_orbase( "invoice_${format}returnaddress",
$template
@@ -457,7 +502,7 @@ sub print_generic {
'today' => time2str($date_format_long, $today),
'terms' => $self->terms,
'template' => $template, #params{'template'},
- 'notice_name' => ($params{'notice_name'} || $self->notice_name),#escape_function?
+ 'notice_name' => $notice_name, # escape?
'current_charges' => sprintf("%.2f", $self->charged),
'duedate' => $self->due_date2str($rdate_format), #date_format?
@@ -499,7 +544,7 @@ sub print_generic {
);
#localization
- my $lh = FS::L10N->get_handle( $params{'locale'} || $cust_main->locale );
+ my $lh = FS::L10N->get_handle( $locale );
$invoice_data{'emt'} = sub { &$escape_function($self->mt(@_)) };
my %info = FS::Locales->locale_info($cust_main->locale || 'en_US');
# eval to avoid death for unimplemented languages
@@ -608,23 +653,12 @@ sub print_generic {
# summary formats
$invoice_data{'last_bill'} = {};
- # returns the last unpaid bill, not the last bill
- #my $last_bill = $pr_cust_bill[-1];
-
if ( $self->custnum && $self->invnum ) {
- # THIS returns the customer's last bill before this one
- my $last_bill = qsearchs({
- 'table' => 'cust_bill',
- 'hashref' => { 'custnum' => $self->custnum,
- 'invnum' => { op => '<', value => $self->invnum },
- },
- 'order_by' => ' ORDER BY invnum DESC LIMIT 1'
- });
- if ( $last_bill ) {
+ if ( $self->previous_bill ) {
+ my $last_bill = $self->previous_bill;
$invoice_data{'last_bill'} = {
'_date' => $last_bill->_date, #unformatted
- # all we need for now
};
my (@payments, @credits);
# for formats that itemize previous payments
@@ -766,6 +800,7 @@ sub print_generic {
my $taxtotal = 0;
my $tax_section = { 'description' => $self->mt('Taxes, Surcharges, and Fees'),
'subtotal' => $taxtotal, # adjusted below
+ 'tax_section' => 1,
};
my $tax_weight = _pkg_category($tax_section->{description})
? _pkg_category($tax_section->{description})->weight
@@ -952,9 +987,6 @@ sub print_generic {
foreach my $section (@sections, @$late_sections) {
- warn "$me adding section \n". Dumper($section)
- if $DEBUG > 1;
-
# begin some normalization
$section->{'subtotal'} = $section->{'amount'}
if $multisection
@@ -1167,7 +1199,7 @@ sub print_generic {
$adjust_section->{'pretotal'} = $self->mt('New charges total').' '.
$other_money_char. sprintf('%.2f', $self->charged );
}
- }else{
+ } else {
push @total_items, $total;
}
push @buf,['','-----------'];
@@ -1554,12 +1586,9 @@ sub print_html {
my %params;
if ( ref($_[0]) ) {
%params = %{ shift() };
- }else{
- $params{'time'} = shift;
- $params{'template'} = shift;
- $params{'cid'} = shift;
+ } else {
+ %params = @_;
}
-
$params{'format'} = 'html';
$self->print_generic( %params );
diff --git a/FS/FS/TicketSystem.pm b/FS/FS/TicketSystem.pm
index c1c69fa3f..fa54e0bbd 100644
--- a/FS/FS/TicketSystem.pm
+++ b/FS/FS/TicketSystem.pm
@@ -342,6 +342,21 @@ sub _upgrade_data {
or die $dbh->errstr;
$cve_2013_3373_sth->execute or die $cve_2013_3373_sth->errstr;
+ # Remove dangling customer links, if any
+ my %target_pkey = ('cust_main' => 'custnum', 'cust_svc' => 'svcnum');
+ for my $table (keys %target_pkey) {
+ my $pkey = $target_pkey{$table};
+ my $rows = $dbh->do(
+ "DELETE FROM links WHERE id IN(".
+ "SELECT links.id FROM links LEFT JOIN $table ON (links.target = ".
+ "'freeside://freeside/$table/' || $table.$pkey) ".
+ "WHERE links.target like 'freeside://freeside/$table/%' ".
+ "AND $table.$pkey IS NULL".
+ ")"
+ ) or die $dbh->errstr;
+ warn "Removed $rows dangling ticket-$table links\n" if $rows > 0;
+ }
+
return;
}
diff --git a/FS/FS/UI/Web.pm b/FS/FS/UI/Web.pm
index ccba1de3a..d7f998bdf 100644
--- a/FS/FS/UI/Web.pm
+++ b/FS/FS/UI/Web.pm
@@ -229,18 +229,25 @@ sub cust_header {
'Cust#' => 'custnum',
'Name' => 'contact',
'Company' => 'company',
+
+ # obsolete but might still be referenced in configuration
'(bill) Customer' => 'name',
'(service) Customer' => 'ship_name',
'(bill) Name' => 'contact',
'(service) Name' => 'ship_contact',
'(bill) Company' => 'company',
'(service) Company' => 'ship_company',
+ '(bill) Day phone' => 'daytime',
+ '(bill) Night phone' => 'night',
+ '(bill) Fax number' => 'fax',
+
+ 'Customer' => 'name',
'Address 1' => 'bill_address1',
'Address 2' => 'bill_address2',
'City' => 'bill_city',
'State' => 'bill_state',
'Zip' => 'bill_zip',
- 'Country' => 'country_full',
+ 'Country' => 'bill_country_full',
'Day phone' => 'daytime', # XXX should use msgcat, but how?
'Night phone' => 'night', # XXX should use msgcat, but how?
'Fax number' => 'fax',
@@ -249,19 +256,13 @@ sub cust_header {
'(bill) City' => 'bill_city',
'(bill) State' => 'bill_state',
'(bill) Zip' => 'bill_zip',
- '(bill) Country' => 'country_full',
- '(bill) Day phone' => 'daytime', # XXX should use msgcat, but how?
- '(bill) Night phone' => 'night', # XXX should use msgcat, but how?
- '(bill) Fax number' => 'fax',
+ '(bill) Country' => 'bill_country_full',
'(service) Address 1' => 'ship_address1',
'(service) Address 2' => 'ship_address2',
'(service) City' => 'ship_city',
'(service) State' => 'ship_state',
'(service) Zip' => 'ship_zip',
'(service) Country' => 'ship_country_full',
- '(service) Day phone' => 'ship_daytime', # XXX should use msgcat, how?
- '(service) Night phone' => 'ship_night', # XXX should use msgcat, how?
- '(service) Fax number' => 'ship_fax',
'Invoicing email(s)' => 'invoicing_list_emailonly_scalar',
'Payment Type' => 'payby',
'Current Balance' => 'current_balance',
@@ -348,8 +349,10 @@ sub cust_sql_fields {
}
}
}
-
- push @fields, 'payby' if grep { $_ eq 'payby'} @cust_fields;
+
+ foreach my $field (qw(daytime night fax payby)) {
+ push @fields, $field if (grep { $_ eq $field } @cust_fields);
+ }
push @fields, 'agent_custid';
my @extra_fields = ();
diff --git a/FS/FS/agent.pm b/FS/FS/agent.pm
index 0cd07ef15..d70ff18b4 100644
--- a/FS/FS/agent.pm
+++ b/FS/FS/agent.pm
@@ -378,6 +378,23 @@ sub payment_gateway {
$payment_gateway;
}
+=item invoice_modes
+
+Returns all L<FS::invoice_mode> objects that are valid for this agent (i.e.
+those with this agentnum or null agentnum).
+
+=cut
+
+sub invoice_modes {
+ my $self = shift;
+ qsearch( {
+ table => 'invoice_mode',
+ hashref => { agentnum => $self->agentnum },
+ extra_sql => ' OR agentnum IS NULL',
+ order_by => ' ORDER BY modename',
+ } );
+}
+
=item num_prospect_cust_main
Returns the number of prospects (customers with no packages ever ordered) for
diff --git a/FS/FS/cable_provider.pm b/FS/FS/cable_provider.pm
new file mode 100644
index 000000000..e988192f4
--- /dev/null
+++ b/FS/FS/cable_provider.pm
@@ -0,0 +1,112 @@
+package FS::cable_provider;
+
+use strict;
+use base qw( FS::Record );
+use FS::Record qw( qsearch qsearchs );
+
+=head1 NAME
+
+FS::cable_provider - Object methods for cable_provider records
+
+=head1 SYNOPSIS
+
+ use FS::cable_provider;
+
+ $record = new FS::cable_provider \%hash;
+ $record = new FS::cable_provider { 'column' => 'value' };
+
+ $error = $record->insert;
+
+ $error = $new_record->replace($old_record);
+
+ $error = $record->delete;
+
+ $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::cable_provider object represents a cable service provider.
+FS::cable_provider inherits from FS::Record. The following fields are
+currently supported:
+
+=over 4
+
+=item providernum
+
+primary key
+
+=item provider
+
+provider
+
+=item disabled
+
+disabled
+
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new provider. To add the provider 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 { 'cable_provider'; }
+
+=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 provider. 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('providernum')
+ || $self->ut_text('provider')
+ || $self->ut_enum('disabled', [ '', 'Y' ] )
+ ;
+ 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/cust_bill.pm b/FS/FS/cust_bill.pm
index fc6a7ddbe..a747a782d 100644
--- a/FS/FS/cust_bill.pm
+++ b/FS/FS/cust_bill.pm
@@ -80,7 +80,7 @@ FS::cust_bill - Object methods for cust_bill records
$tax_amount = $record->tax;
@lines = $cust_bill->print_text;
- @lines = $cust_bill->print_text $time;
+ @lines = $cust_bill->print_text('time' => $time);
=head1 DESCRIPTION
@@ -153,7 +153,13 @@ Invoices are normally created by calling the bill method of a customer object
=cut
sub table { 'cust_bill'; }
-sub notice_name { 'Invoice'; }
+
+# should be the ONLY occurrence of "Invoice" in invoice rendering code.
+# (except email_subject and invnum_date_pretty)
+sub notice_name {
+ my $self = shift;
+ $self->conf->config('notice_name') || 'Invoice'
+}
sub cust_linked { $_[0]->cust_main_custnum; }
sub cust_unlinked_msg {
@@ -422,6 +428,25 @@ sub display_invnum {
}
}
+=item previous_bill
+
+Returns the customer's last invoice before this one.
+
+=cut
+
+sub previous_bill {
+ my $self = shift;
+ if ( !$self->get('previous_bill') ) {
+ $self->set('previous_bill', qsearchs({
+ 'table' => 'cust_bill',
+ 'hashref' => { 'custnum' => $self->custnum,
+ '_date' => { op=>'<', value=>$self->_date } },
+ 'order_by' => 'ORDER BY _date DESC LIMIT 1',
+ }) );
+ }
+ $self->get('previous_bill');
+}
+
=item previous
Returns a list consisting of the total previous balance for this customer,
@@ -1024,7 +1049,7 @@ Options:
sender address, required
-=item tempate
+=item template
alternate template name, optional
@@ -1058,15 +1083,10 @@ sub generate_email {
my %return = (
'from' => $args{'from'},
- 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
+ 'subject' => ($args{'subject'} || $self->email_subject),
);
- my %opt = (
- 'unsquelch_cdr' => $conf->exists('voip-cdr_email'),
- 'template' => $args{'template'},
- 'notice_name' => ( $args{'notice_name'} || 'Invoice' ),
- 'no_coupon' => $args{'no_coupon'},
- );
+ $args{'unsquelch_cdr'} = $conf->exists('voip-cdr_email');
my $cust_main = $self->cust_main;
@@ -1108,7 +1128,7 @@ sub generate_email {
if ( ref($args{'print_text'}) eq 'ARRAY' ) {
$data = $args{'print_text'};
} else {
- $data = [ $self->print_text(\%opt) ];
+ $data = [ $self->print_text(\%args) ];
}
}
@@ -1165,10 +1185,10 @@ sub generate_email {
'Filename' => 'barcode.png',
'Content-ID' => "<$barcode_content_id>",
;
- $opt{'barcode_cid'} = $barcode_content_id;
+ $args{'barcode_cid'} = $barcode_content_id;
}
- $htmldata = $self->print_html({ 'cid'=>$content_id, %opt });
+ $htmldata = $self->print_html({ 'cid'=>$content_id, %args });
}
$alternative->attach(
@@ -1230,7 +1250,7 @@ sub generate_email {
$related->add_part($image) if $image;
- my $pdf = build MIME::Entity $self->mimebuild_pdf(\%opt);
+ my $pdf = build MIME::Entity $self->mimebuild_pdf(\%args);
$return{'mimeparts'} = [ $related, $pdf, @otherparts ];
@@ -1262,7 +1282,7 @@ sub generate_email {
#mime parts arguments a la MIME::Entity->build().
$return{'mimeparts'} = [
- { $self->mimebuild_pdf(\%opt) }
+ { $self->mimebuild_pdf(\%args) }
];
}
@@ -1282,7 +1302,7 @@ sub generate_email {
if ( ref($args{'print_text'}) eq 'ARRAY' ) {
$return{'body'} = $args{'print_text'};
} else {
- $return{'body'} = [ $self->print_text(\%opt) ];
+ $return{'body'} = [ $self->print_text(\%args) ];
}
}
@@ -1311,105 +1331,48 @@ sub mimebuild_pdf {
);
}
-=item send HASHREF | [ TEMPLATE [ , AGENTNUM [ , INVOICE_FROM [ , AMOUNT ] ] ] ]
+=item send HASHREF
Sends this invoice to the destinations configured for this customer: sends
email, prints and/or faxes. See L<FS::cust_main_invoice>.
-Options can be passed as a hashref (recommended) or as a list of up to
-four values for templatename, agentnum, invoice_from and amount.
+Options can be passed as a hashref. Positional parameters are no longer
+allowed.
-I<template>, if specified, is the name of a suffix for alternate invoices.
+I<template>: a suffix for alternate invoices
-I<agentnum>, if specified, means that this invoice will only be sent for customers
-of the specified agent or agent(s). AGENTNUM can be a scalar agentnum (for a
-single agent) or an arrayref of agentnums.
+I<agentnum>: obsolete, now does nothing.
-I<invoice_from>, if specified, overrides the default email invoice From: address.
+I<invoice_from> overrides the default email invoice From: address.
-I<amount>, if specified, only sends the invoice if the total amount owed on this
-invoice and all older invoices is greater than the specified amount.
+I<amount>: obsolete, does nothing
-I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
+I<notice_name> overrides "Invoice" as the name of the sent document
+(templates from 10/2009 or newer required).
-I<lpr>, if specified, is passed to
+I<lpr> overrides the system 'lpr' option as the command to print a document
+from standard input.
=cut
-sub queueable_send {
- my %opt = @_;
-
- my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
- or die "invalid invoice number: " . $opt{invnum};
-
- my @args = ( $opt{template}, $opt{agentnum} );
- push @args, $opt{invoice_from}
- if exists($opt{invoice_from}) && $opt{invoice_from};
-
- my $error = $self->send( @args );
- die $error if $error;
-
-}
-
sub send {
my $self = shift;
+ my $opt = ref($_[0]) ? $_[0] : +{ @_ };
my $conf = $self->conf;
- my( $template, $invoice_from, $notice_name );
- my $agentnums = '';
- my $balance_over = 0;
- my $lpr = '';
-
- if ( ref($_[0]) ) {
- my $opt = shift;
- $template = $opt->{'template'} || '';
- if ( $agentnums = $opt->{'agentnum'} ) {
- $agentnums = [ $agentnums ] unless ref($agentnums);
- }
- $invoice_from = $opt->{'invoice_from'};
- $balance_over = $opt->{'balance_over'} if $opt->{'balance_over'};
- $notice_name = $opt->{'notice_name'};
- $lpr = $opt->{'lpr'}
- } else {
- $template = scalar(@_) ? shift : '';
- if ( scalar(@_) && $_[0] ) {
- $agentnums = ref($_[0]) ? shift : [ shift ];
- }
- $invoice_from = shift if scalar(@_);
- $balance_over = shift if scalar(@_) && $_[0] !~ /^\s*$/;
- }
-
my $cust_main = $self->cust_main;
- return 'N/A' unless ! $agentnums
- or grep { $_ == $cust_main->agentnum } @$agentnums;
-
- return ''
- unless $cust_main->total_owed_date($self->_date) > $balance_over;
-
- $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
- $conf->config('invoice_from', $cust_main->agentnum );
-
- my %opt = (
- 'template' => $template,
- 'invoice_from' => $invoice_from,
- 'notice_name' => ( $notice_name || 'Invoice' ),
- );
-
my @invoicing_list = $cust_main->invoicing_list;
- #$self->email_invoice(\%opt)
- $self->email(\%opt)
+ $self->email($opt)
if ( grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list )
&& ! $self->invoice_noemail;
- $opt{'lpr'} = $lpr;
- #$self->print_invoice(\%opt)
- $self->print(\%opt)
+ $self->print($opt)
if grep { $_ eq 'POST' } @invoicing_list; #postal
#this has never been used post-$ORIGINAL_ISP afaik
- $self->fax_invoice(\%opt)
+ $self->fax_invoice($opt)
if grep { $_ eq 'FAX' } @invoicing_list; #fax
'';
@@ -1418,16 +1381,17 @@ sub send {
=item email HASHREF | [ TEMPLATE [ , INVOICE_FROM ] ]
-Emails this invoice.
+Sends this invoice to the customer's email destination(s).
-Options can be passed as a hashref (recommended) or as a list of up to
-two values for templatename and invoice_from.
+Options must be passed as a hashref. Positional parameters are no longer
+allowed.
I<template>, if specified, is the name of a suffix for alternate invoices.
-I<invoice_from>, if specified, overrides the default email invoice From: address.
+I<invoice_from>, if specified, overrides the default email invoice From:
+address.
-I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
+I<notice_name> is the name of the sent document.
=cut
@@ -1437,38 +1401,30 @@ sub queueable_email {
my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
or die "invalid invoice number: " . $opt{invnum};
- my %args = ( 'template' => $opt{template} );
- $args{$_} = $opt{$_}
- foreach grep { exists($opt{$_}) && $opt{$_} }
- qw( invoice_from notice_name no_coupon );
+ my %args = map {$_ => $opt{$_}}
+ grep { $opt{$_} }
+ qw( invoice_from notice_name no_coupon template );
my $error = $self->email( \%args );
die $error if $error;
}
-#sub email_invoice {
sub email {
my $self = shift;
return if $self->hide;
my $conf = $self->conf;
-
- my( $template, $invoice_from, $notice_name, $no_coupon );
- if ( ref($_[0]) ) {
- my $opt = shift;
- $template = $opt->{'template'} || '';
- $invoice_from = $opt->{'invoice_from'};
- $notice_name = $opt->{'notice_name'} || 'Invoice';
- $no_coupon = $opt->{'no_coupon'} || 0;
- } else {
- $template = scalar(@_) ? shift : '';
- $invoice_from = shift if scalar(@_);
- $notice_name = 'Invoice';
- $no_coupon = 0;
+ my $opt = shift;
+ if ($opt and !ref($opt)) {
+ die "FS::cust_bill::email called with positional parameters";
}
- $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
- $conf->config('invoice_from', $self->cust_main->agentnum );
+ my $template = $opt->{template};
+ my $from = delete $opt->{invoice_from};
+
+ # this is where we set the From: address
+ $from ||= $self->_agent_invoice_from || #XXX should go away
+ $conf->config('invoice_from', $self->cust_main->agentnum );
my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
$self->cust_main->invoicing_list;
@@ -1478,20 +1434,19 @@ sub email {
die 'No recipients for customer #'. $self->custnum;
} else {
#default: better to notify this person than silence
- @invoicing_list = ($invoice_from);
+ @invoicing_list = ($from);
}
}
+ # this is where we set the Subject:
my $subject = $self->email_subject($template);
my $error = send_email(
$self->generate_email(
- 'from' => $invoice_from,
+ 'from' => $from,
'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
'subject' => $subject,
- 'template' => $template,
- 'notice_name' => $notice_name,
- 'no_coupon' => $no_coupon,
+ %$opt, # template, etc.
)
);
die "can't email invoice: $error\n" if $error;
@@ -1518,12 +1473,12 @@ sub email_subject {
eval qq("$subject");
}
-=item lpr_data HASHREF | [ TEMPLATE ]
+=item lpr_data HASHREF
Returns the postscript or plaintext for this invoice as an arrayref.
-Options can be passed as a hashref (recommended) or as a single optional value
-for template.
+Options must be passed as a hashref. Positional parameters are no longer
+allowed.
I<template>, if specified, is the name of a suffix for alternate invoices.
@@ -1534,31 +1489,21 @@ I<notice_name>, if specified, overrides "Invoice" as the name of the sent docume
sub lpr_data {
my $self = shift;
my $conf = $self->conf;
- my( $template, $notice_name );
- if ( ref($_[0]) ) {
- my $opt = shift;
- $template = $opt->{'template'} || '';
- $notice_name = $opt->{'notice_name'} || 'Invoice';
- } else {
- $template = scalar(@_) ? shift : '';
- $notice_name = 'Invoice';
+ my $opt = shift;
+ if ($opt and !ref($opt)) {
+ # nobody does this anyway
+ die "FS::cust_bill::lpr_data called with positional parameters";
}
- my %opt = (
- 'template' => $template,
- 'notice_name' => $notice_name,
- );
-
my $method = $conf->exists('invoice_latex') ? 'print_ps' : 'print_text';
- [ $self->$method( \%opt ) ];
+ [ $self->$method( $opt ) ];
}
-=item print HASHREF | [ TEMPLATE ]
+=item print HASHREF
Prints this invoice.
-Options can be passed as a hashref (recommended) or as a single optional
-value for template.
+Options must be passed as a hashref.
I<template>, if specified, is the name of a suffix for alternate invoices.
@@ -1566,48 +1511,34 @@ I<notice_name>, if specified, overrides "Invoice" as the name of the sent docume
=cut
-#sub print_invoice {
sub print {
my $self = shift;
return if $self->hide;
my $conf = $self->conf;
-
- my( $template, $notice_name, $lpr );
- if ( ref($_[0]) ) {
- my $opt = shift;
- $template = $opt->{'template'} || '';
- $notice_name = $opt->{'notice_name'} || 'Invoice';
- $lpr = $opt->{'lpr'}
- } else {
- $template = scalar(@_) ? shift : '';
- $notice_name = 'Invoice';
- $lpr = '';
+ my $opt = shift;
+ if ($opt and !ref($opt)) {
+ die "FS::cust_bill::print called with positional parameters";
}
- my %opt = (
- 'template' => $template,
- 'notice_name' => $notice_name,
- );
-
+ my $lpr = delete $opt->{lpr};
if($conf->exists('invoice_print_pdf')) {
# Add the invoice to the current batch.
- $self->batch_invoice(\%opt);
+ $self->batch_invoice($opt);
}
else {
do_print(
- $self->lpr_data(\%opt),
+ $self->lpr_data($opt),
'agentnum' => $self->cust_main->agentnum,
'lpr' => $lpr,
);
}
}
-=item fax_invoice HASHREF | [ TEMPLATE ]
+=item fax_invoice HASHREF
Faxes this invoice.
-Options can be passed as a hashref (recommended) or as a single optional
-value for template.
+Options must be passed as a hashref.
I<template>, if specified, is the name of a suffix for alternate invoices.
@@ -1619,15 +1550,9 @@ sub fax_invoice {
my $self = shift;
return if $self->hide;
my $conf = $self->conf;
-
- my( $template, $notice_name );
- if ( ref($_[0]) ) {
- my $opt = shift;
- $template = $opt->{'template'} || '';
- $notice_name = $opt->{'notice_name'} || 'Invoice';
- } else {
- $template = scalar(@_) ? shift : '';
- $notice_name = 'Invoice';
+ my $opt = shift;
+ if ($opt and !ref($opt)) {
+ die "FS::cust_bill::fax_invoice called with positional parameters";
}
die 'FAX invoice destination not (yet?) supported with plain text invoices.'
@@ -1636,12 +1561,7 @@ sub fax_invoice {
my $dialstring = $self->cust_main->getfield('fax');
#Check $dialstring?
- my %opt = (
- 'template' => $template,
- 'notice_name' => $notice_name,
- );
-
- my $error = send_fax( 'docdata' => $self->lpr_data(\%opt),
+ my $error = send_fax( 'docdata' => $self->lpr_data($opt),
'dialstring' => $dialstring,
);
die $error if $error;
@@ -1730,29 +1650,6 @@ sub spool_invoice {
);
}
-=item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
-
-Like B<send>, but only sends the invoice if it is the newest open invoice for
-this customer.
-
-=cut
-
-sub send_if_newest {
- my $self = shift;
-
- return ''
- if scalar(
- grep { $_->owed > 0 }
- qsearch('cust_bill', {
- 'custnum' => $self->custnum,
- #'_date' => { op=>'>', value=>$self->_date },
- 'invnum' => { op=>'>', value=>$self->invnum },
- } )
- );
-
- $self->send(@_);
-}
-
=item send_csv OPTION => VALUE, ...
Sends invoice as a CSV data-file to a remote host with the specified protocol.
@@ -3109,12 +3006,25 @@ sub _items_credits {
my @b;
#credits
- foreach ( $self->cust_credited ) {
+ my @objects;
+ if ( $self->conf->exists('previous_balance-payments_since') ) {
+ my $date = 0;
+ $date = $self->previous_bill->_date if $self->previous_bill;
+ @objects = qsearch('cust_credit', {
+ 'custnum' => $self->custnum,
+ '_date' => {op => '>=', value => $date},
+ });
+ # hard to do this in the qsearch...
+ @objects = grep { $_->_date < $self->_date } @objects;
+ } else {
+ @objects = $self->cust_credited;
+ }
- #something more elaborate if $_->amount ne $_->cust_credit->credited ?
+ foreach my $obj ( @objects ) {
+ my $cust_credit = $obj->isa('FS::cust_credit') ? $obj : $obj->cust_credit;
- my $reason = substr($_->cust_credit->reason, 0, $trim_len);
- $reason .= '...' if length($reason) < length($_->cust_credit->reason);
+ my $reason = substr($cust_credit->reason, 0, $trim_len);
+ $reason .= '...' if length($reason) < length($cust_credit->reason);
$reason = " ($reason) " if $reason;
push @b, {
@@ -3122,8 +3032,8 @@ sub _items_credits {
# " (". time2str("%x",$_->cust_credit->_date) .")".
# $reason,
'description' => $self->mt('Credit applied').' '.
- time2str($date_format,$_->cust_credit->_date). $reason,
- 'amount' => sprintf("%.2f",$_->amount),
+ time2str($date_format,$obj->_date). $reason,
+ 'amount' => sprintf("%.2f",$obj->amount),
};
}
@@ -3135,21 +3045,31 @@ sub _items_payments {
my $self = shift;
my @b;
- #get & print payments
- foreach ( $self->cust_bill_pay ) {
-
- #something more elaborate if $_->amount ne ->cust_pay->paid ?
+ my $detailed = $self->conf->exists('invoice_payment_details');
+ my @objects;
+ if ( $self->conf->exists('previous_balance-payments_since') ) {
+ my $date = 0;
+ $date = $self->previous_bill->_date if $self->previous_bill;
+ @objects = qsearch('cust_pay', {
+ 'custnum' => $self->custnum,
+ '_date' => {op => '>=', value => $date},
+ });
+ @objects = grep { $_->_date < $self->_date } @objects;
+ } else {
+ @objects = $self->cust_bill_pay;
+ }
+ foreach my $obj (@objects) {
+ my $cust_pay = $obj->isa('FS::cust_pay') ? $obj : $obj->cust_pay;
my $desc = $self->mt('Payment received').' '.
- time2str($date_format,$_->cust_pay->_date );
- $desc .= $self->mt(' via ' . $_->cust_pay->payby_payinfo_pretty)
- if ( $self->conf->exists('invoice_payment_details') );
-
+ time2str($date_format, $cust_pay->_date );
+ $desc .= $self->mt(' via ' . $cust_pay->payby_payinfo_pretty)
+ if $detailed;
+
push @b, {
'description' => $desc,
- 'amount' => sprintf("%.2f", $_->amount )
+ 'amount' => sprintf("%.2f", $obj->amount )
};
-
}
@b;
diff --git a/FS/FS/cust_location.pm b/FS/FS/cust_location.pm
index b98ade157..11e97ecfe 100644
--- a/FS/FS/cust_location.pm
+++ b/FS/FS/cust_location.pm
@@ -10,6 +10,8 @@ use FS::Conf;
use FS::prospect_main;
use FS::cust_main;
use FS::cust_main_county;
+use FS::GeocodeCache;
+use Date::Format qw( time2str );
$import = 0;
@@ -677,6 +679,13 @@ sub process_censustract_update {
return;
}
+=item process_set_coord
+
+Queueable function to find and fill in coordinates for all locations that
+lack them. Because this uses the Google Maps API, it's internally rate
+limited and must run in a single process.
+
+=cut
sub process_set_coord {
my $job = shift;
@@ -716,6 +725,67 @@ sub process_set_coord {
return;
}
+=item process_standardize [ LOCATIONNUMS ]
+
+Performs address standardization on locations with unclean addresses,
+using whatever method you have configured. If the standardize_* method
+returns a I<clean> address match, the location will be updated. This is
+always an in-place update (because the physical location is the same,
+and is just being referred to by a more accurate name).
+
+Disabled locations will be skipped, as nobody cares.
+
+If any LOCATIONNUMS are provided, only those locations will be updated.
+
+=cut
+
+sub process_standardize {
+ my $job = shift;
+ my @others = qsearch('queue', {
+ 'status' => 'locked',
+ 'job' => $job->job,
+ 'jobnum' => {op=>'!=', value=>$job->jobnum},
+ });
+ return if @others;
+ my @locationnums = grep /^\d+$/, @_;
+ my $where = "AND locationnum IN(".join(',',@locationnums).")"
+ if scalar(@locationnums);
+ my @locations = qsearch({
+ table => 'cust_location',
+ hashref => { addr_clean => '', disabled => '' },
+ extra_sql => $where,
+ });
+ my $n_todo = scalar(@locations);
+ my $n_done = 0;
+
+ # special: log this
+ my $log;
+ eval "use Text::CSV";
+ open $log, '>', "$FS::UID::cache_dir/process_standardize-" .
+ time2str('%Y%m%d',time) .
+ ".csv";
+ my $csv = Text::CSV->new({binary => 1, eol => "\n"});
+
+ foreach my $cust_location (@locations) {
+ $job->update_statustext( int(100 * $n_done/$n_todo) . ",$n_done / $n_todo locations" ) if $job;
+ my $result = FS::GeocodeCache->standardize($cust_location);
+ if ( $result->{addr_clean} and !$result->{error} ) {
+ my @cols = ($cust_location->locationnum);
+ foreach (keys %$result) {
+ push @cols, $cust_location->get($_), $result->{$_};
+ $cust_location->set($_, $result->{$_});
+ }
+ # bypass immutable field restrictions
+ my $error = $cust_location->FS::Record::replace;
+ warn "location ".$cust_location->locationnum.": $error\n" if $error;
+ $csv->print($log, \@cols);
+ }
+ $n_done++;
+ dbh->commit; # so that we can resume if interrupted
+ }
+ close $log;
+}
+
=head1 BUGS
=head1 SEE ALSO
diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm
index 641d54a30..a9a4cb0ef 100644
--- a/FS/FS/cust_main.pm
+++ b/FS/FS/cust_main.pm
@@ -17,6 +17,7 @@ use vars qw( $DEBUG $me $conf
@encrypted_fields
$import
$ignore_expired_card $ignore_banned_card $ignore_illegal_zip
+ $ignore_invalid_card
$skip_fuzzyfiles
@paytypes
);
@@ -89,6 +90,7 @@ $me = '[FS::cust_main]';
$import = 0;
$ignore_expired_card = 0;
$ignore_banned_card = 0;
+$ignore_invalid_card = 0;
$skip_fuzzyfiles = 0;
@@ -102,6 +104,7 @@ sub nohistory_fields { ('payinfo', 'paycvv'); }
install_callback FS::UID sub {
$conf = new FS::Conf;
#yes, need it for stuff below (prolly should be cached)
+ $ignore_invalid_card = $conf->exists('allow_invalid_cards');
};
sub _cache {
@@ -1826,7 +1829,8 @@ sub check {
# Need some kind of global flag to accept invalid cards, for testing
# on scrubbed data.
- if ( !$import && $check_payinfo && $self->payby =~ /^(CARD|DCRD)$/ ) {
+ if ( !$import && !$ignore_invalid_card && $check_payinfo &&
+ $self->payby =~ /^(CARD|DCRD)$/ ) {
my $payinfo = $self->payinfo;
$payinfo =~ s/\D//g;
@@ -1898,7 +1902,8 @@ sub check {
$self->payissue('');
}
- } elsif ( $check_payinfo && $self->payby =~ /^(CHEK|DCHK)$/ ) {
+ } elsif ( !$ignore_invalid_card && $check_payinfo &&
+ $self->payby =~ /^(CHEK|DCHK)$/ ) {
my $payinfo = $self->payinfo;
$payinfo =~ s/[^\d\@\.]//g;
@@ -4071,6 +4076,16 @@ sub ship_contact_firstlast {
# code2country($self->country);
#}
+sub bill_country_full {
+ my $self = shift;
+ code2country($self->bill_location->country);
+}
+
+sub ship_country_full {
+ my $self = shift;
+ code2country($self->ship_location->country);
+}
+
=item county_state_county [ PREFIX ]
Returns a string consisting of just the county, state and country.
@@ -4910,9 +4925,9 @@ sub queueable_print {
my %opt = @_;
my $self = qsearchs('cust_main', { 'custnum' => $opt{custnum} } )
- or die "invalid customer number: " . $opt{custvnum};
+ or die "invalid customer number: " . $opt{custnum};
- my $error = $self->print( $opt{template} );
+ my $error = $self->print( { 'template' => $opt{template} } );
die $error if $error;
}
@@ -5145,7 +5160,7 @@ sub _upgrade_data { #class method
};
- FS::upgrade_journal->set_done('cust_main__trimspaces');
+ FS::upgrade_journal->set_done('cust_main__cust_payby');
}
$class->_upgrade_otaker(%opts);
diff --git a/FS/FS/cust_main/Search.pm b/FS/FS/cust_main/Search.pm
index f14f897ea..182527ff9 100644
--- a/FS/FS/cust_main/Search.pm
+++ b/FS/FS/cust_main/Search.pm
@@ -610,14 +610,24 @@ sub search {
##
# address
##
- if ( $params->{'address'} =~ /\S/ ) {
- my $address = dbh->quote('%'. lc($params->{'address'}). '%');
- push @where, "EXISTS(
- SELECT 1 FROM cust_location
- WHERE cust_location.custnum = cust_main.custnum
- AND (LOWER(cust_location.address1) LIKE $address OR
- LOWER(cust_location.address2) LIKE $address)
- )";
+ if ( $params->{'address'} ) {
+ # allow this to be an arrayref
+ my @values = ($params->{'address'});
+ @values = @{$values[0]} if ref($values[0]);
+ my @orwhere;
+ foreach (grep /\S/, @values) {
+ my $address = dbh->quote('%'. lc($_). '%');
+ push @orwhere,
+ "LOWER(cust_location.address1) LIKE $address",
+ "LOWER(cust_location.address2) LIKE $address";
+ }
+ if (@orwhere) {
+ push @where, "EXISTS(
+ SELECT 1 FROM cust_location
+ WHERE cust_location.custnum = cust_main.custnum
+ AND (".join(' OR ',@orwhere).")
+ )";
+ }
}
##
diff --git a/FS/FS/cust_main_Mixin.pm b/FS/FS/cust_main_Mixin.pm
index 212c04e0f..f584b415e 100644
--- a/FS/FS/cust_main_Mixin.pm
+++ b/FS/FS/cust_main_Mixin.pm
@@ -131,9 +131,12 @@ linked to a customer.
sub country_full {
my $self = shift;
- $self->cust_linked
- ? FS::cust_main::country_full($self)
- : $self->cust_unlinked_msg;
+ if ( $self->locationnum ) { # cust_pkg has this
+ my $location = FS::cust_location->by_key($self->locationnum);
+ $location ? $location->country_full : '';
+ } elsif ( $self->cust_linked ) {
+ $self->cust_main->bill_country_full;
+ }
}
=item invoicing_list_emailonly
diff --git a/FS/FS/cust_main_county.pm b/FS/FS/cust_main_county.pm
index a61d67e11..10a007c57 100644
--- a/FS/FS/cust_main_county.pm
+++ b/FS/FS/cust_main_county.pm
@@ -244,7 +244,7 @@ are inserted.
In addition to calculating the tax for the line items, this will calculate
any appropriate tax exemptions and attach them to the line items.
-Options may include 'custnum' and 'invoice_date' in case the cust_bill_pkg
+Options may include 'custnum' and 'invoice_time' in case the cust_bill_pkg
objects belong to an invoice that hasn't been inserted yet.
Options may include 'exemptions', an arrayref of L<FS::cust_tax_exempt_pkg>
@@ -276,7 +276,7 @@ sub taxline {
my $cust_bill = $taxables->[0]->cust_bill;
my $custnum = $cust_bill ? $cust_bill->custnum : $opt{'custnum'};
- my $invoice_date = $cust_bill ? $cust_bill->_date : $opt{'invoice_date'};
+ my $invoice_time = $cust_bill ? $cust_bill->_date : $opt{'invoice_time'};
my $cust_main = FS::cust_main->by_key($custnum) if $custnum > 0;
if (!$cust_main) {
# better way to handle this? should we just assume that it's taxable?
@@ -364,22 +364,36 @@ sub taxline {
if ( $self->exempt_amount && $self->exempt_amount > 0
and $taxable_charged > 0 ) {
- #my ($mon,$year) = (localtime($cust_bill_pkg->sdate) )[4,5];
- my ($mon,$year) =
- (localtime( $cust_bill_pkg->sdate || $invoice_date ) )[4,5];
- $mon++;
- $year += 1900;
- my $freq = $cust_bill_pkg->freq;
- unless ($freq) {
- $freq = $part_pkg->freq || 1; # less trustworthy fallback
- }
- if ( $freq !~ /(\d+)$/ ) {
- $dbh->rollback if $oldAutoCommit;
- return "daily/weekly package definitions not (yet?)".
- " compatible with monthly tax exemptions";
+ # If the billing period extends across multiple calendar months,
+ # there may be several months of exemption available.
+ my $sdate = $cust_bill_pkg->sdate || $invoice_time;
+ my $start_month = (localtime($sdate))[4] + 1;
+ my $start_year = (localtime($sdate))[5] + 1900;
+ my $edate = $cust_bill_pkg->edate || $invoice_time;
+ my $end_month = (localtime($edate))[4] + 1;
+ my $end_year = (localtime($edate))[5] + 1900;
+
+ # If the partial last month + partial first month <= one month,
+ # don't use the exemption in the last month
+ # (unless the last month is also the first month, e.g. one-time
+ # charges)
+ if ( (localtime($sdate))[3] >= (localtime($edate))[3]
+ and ($start_month != $end_month or $start_year != $end_year)
+ ) {
+ $end_month--;
+ if ( $end_month == 0 ) {
+ $end_year--;
+ $end_month = 12;
+ }
}
- my $taxable_per_month =
- sprintf("%.2f", $taxable_charged / $freq );
+
+ # number of months of exemption available
+ my $freq = ($end_month - $start_month) +
+ ($end_year - $start_year) * 12 +
+ 1;
+
+ # divide equally among all of them
+ my $permonth = sprintf('%.2f', $taxable_charged / $freq);
#call the whole thing off if this customer has any old
#exemption records...
@@ -392,9 +406,15 @@ sub taxline {
'run bin/fs-migrate-cust_tax_exempt?';
}
- foreach my $which_month ( 1 .. $freq ) {
-
- #maintain the new exemption table now
+ my ($mon, $year) = ($start_month, $start_year);
+ while ($taxable_charged > 0.005 and
+ ($year < $end_year or
+ ($year == $end_year and $mon <= $end_month)
+ )
+ ) {
+
+ # find the sum of the exemption used by this customer, for this tax,
+ # in this month
my $sql = "
SELECT SUM(amount)
FROM cust_tax_exempt_pkg
@@ -408,7 +428,7 @@ sub taxline {
";
my $sth = dbh->prepare($sql) or do {
$dbh->rollback if $oldAutoCommit;
- return "fatal: can't lookup exising exemption: ". dbh->errstr;
+ return "fatal: can't lookup existing exemption: ". dbh->errstr;
};
$sth->execute(
$custnum,
@@ -417,10 +437,11 @@ sub taxline {
$mon,
) or do {
$dbh->rollback if $oldAutoCommit;
- return "fatal: can't lookup exising exemption: ". dbh->errstr;
+ return "fatal: can't lookup existing exemption: ". dbh->errstr;
};
my $existing_exemption = $sth->fetchrow_arrayref->[0] || 0;
+ # add any exemption we're already using for another line item
foreach ( grep { $_->taxnum == $self->taxnum &&
$_->exempt_monthly eq 'Y' &&
$_->month == $mon &&
@@ -430,13 +451,15 @@ sub taxline {
{
$existing_exemption += $_->amount;
}
-
+
my $remaining_exemption =
$self->exempt_amount - $existing_exemption;
if ( $remaining_exemption > 0 ) {
- my $addl = $remaining_exemption > $taxable_per_month
- ? $taxable_per_month
+ my $addl = $remaining_exemption > $permonth
+ ? $permonth
: $remaining_exemption;
+ $addl = $taxable_charged if $addl > $taxable_charged;
+
push @new_exemptions, FS::cust_tax_exempt_pkg->new({
amount => sprintf('%.2f', $addl),
exempt_monthly => 'Y',
@@ -445,7 +468,6 @@ sub taxline {
});
$taxable_charged -= $addl;
}
- last if $taxable_charged < 0.005;
# if they're using multiple months of exemption for a multi-month
# package, then record the exemptions in separate months
$mon++;
@@ -454,7 +476,7 @@ sub taxline {
$year++;
}
- } #foreach $which_month
+ }
} # if exempt_amount
$_->taxnum($self->taxnum) foreach @new_exemptions;
diff --git a/FS/FS/cust_payby.pm b/FS/FS/cust_payby.pm
index 5914ab5c5..d78c574e4 100644
--- a/FS/FS/cust_payby.pm
+++ b/FS/FS/cust_payby.pm
@@ -7,9 +7,11 @@ use FS::Record qw( qsearchs ); #qsearch;
use FS::payby;
use FS::cust_main;
use Business::CreditCard qw( validate cardtype );
+use FS::Msgcat qw( gettext );
use vars qw( $conf @encrypted_fields
$ignore_expired_card $ignore_banned_card
+ $ignore_invalid_card
);
@encrypted_fields = ('payinfo', 'paycvv');
@@ -17,10 +19,12 @@ sub nohistory_fields { ('payinfo', 'paycvv'); }
$ignore_expired_card = 0;
$ignore_banned_card = 0;
+$ignore_invalid_card = 0;
install_callback FS::UID sub {
$conf = new FS::Conf;
#yes, need it for stuff below (prolly should be cached)
+ $ignore_invalid_card = $conf->exists('allow_invalid_cards');
};
=head1 NAME
@@ -197,7 +201,8 @@ sub check {
# Need some kind of global flag to accept invalid cards, for testing
# on scrubbed data.
#XXX if ( !$import && $check_payinfo && $self->payby =~ /^(CARD|DCRD)$/ ) {
- if ( $check_payinfo && $self->payby =~ /^(CARD|DCRD)$/ ) {
+ if ( !$ignore_invalid_card &&
+ $check_payinfo && $self->payby =~ /^(CARD|DCRD)$/ ) {
my $payinfo = $self->payinfo;
$payinfo =~ s/\D//g;
@@ -269,7 +274,8 @@ sub check {
$self->payissue('');
}
- } elsif ( $check_payinfo && $self->payby =~ /^(CHEK|DCHK)$/ ) {
+ } elsif ( !$ignore_invalid_card &&
+ $check_payinfo && $self->payby =~ /^(CHEK|DCHK)$/ ) {
my $payinfo = $self->payinfo;
$payinfo =~ s/[^\d\@\.]//g;
diff --git a/FS/FS/cust_pkg.pm b/FS/FS/cust_pkg.pm
index 19ef1f326..0cb1b50a2 100644
--- a/FS/FS/cust_pkg.pm
+++ b/FS/FS/cust_pkg.pm
@@ -2613,14 +2613,30 @@ sub part_pkg_currency_option {
=item cust_svc [ OPTION => VALUE ... ] (current usage)
+=item cust_svc_unsorted [ OPTION => VALUE ... ]
+
Returns the services for this package, as FS::cust_svc objects (see
L<FS::cust_svc>). Available options are svcpart and svcdb. If either is
spcififed, returns only the matching services.
+As an optimization, use the cust_svc_unsorted version if you are not displaying
+the results.
+
=cut
sub cust_svc {
my $self = shift;
+ cluck "cust_pkg->cust_svc called" if $DEBUG > 2;
+ $self->_sort_cust_svc( $self->cust_svc_unsorted_arrayref );
+}
+
+sub cust_svc_unsorted {
+ my $self = shift;
+ @{ $self->cust_svc_unsorted_arrayref };
+}
+
+sub cust_svc_unsorted_arrayref {
+ my $self = shift;
return () unless $self->num_cust_svc(@_);
@@ -2645,13 +2661,7 @@ sub cust_svc {
$search{extra_sql} = ' AND svcdb = '. dbh->quote( $opt{'svcdb'} );
}
- cluck "cust_pkg->cust_svc called" if $DEBUG > 2;
-
- #if ( $self->{'_svcnum'} ) {
- # values %{ $self->{'_svcnum'}->cache };
- #} else {
- $self->_sort_cust_svc( [ qsearch(\%search) ] );
- #}
+ [ qsearch(\%search) ];
}
diff --git a/FS/FS/cust_svc.pm b/FS/FS/cust_svc.pm
index d6d7d4cf1..958209049 100644
--- a/FS/FS/cust_svc.pm
+++ b/FS/FS/cust_svc.pm
@@ -113,6 +113,10 @@ my $rt_session;
sub delete {
my $self = shift;
+
+ my $cust_pkg = $self->cust_pkg;
+ my $custnum = $cust_pkg->custnum if $cust_pkg;
+
my $error = $self->SUPER::delete;
return $error if $error;
@@ -126,7 +130,15 @@ sub delete {
$links->Limit(FIELD => 'Target',
VALUE => 'freeside://freeside/cust_svc/'.$svcnum);
while ( my $l = $links->Next ) {
- my ($val, $msg) = $l->Delete;
+ my ($val, $msg);
+ if ( $custnum ) {
+ # re-link to point to the customer instead
+ ($val, $msg) =
+ $l->SetTarget('freeside://freeside/cust_main/'.$custnum);
+ } else {
+ # unlinked service
+ ($val, $msg) = $l->Delete;
+ }
# can't do anything useful on error
warn "error unlinking ticket $svcnum: $msg\n" if !$val;
}
diff --git a/FS/FS/invoice_conf.pm b/FS/FS/invoice_conf.pm
new file mode 100644
index 000000000..043cab03c
--- /dev/null
+++ b/FS/FS/invoice_conf.pm
@@ -0,0 +1,274 @@
+package FS::invoice_conf;
+
+use strict;
+use base qw( FS::Record FS::Conf );
+use FS::Record qw( qsearch qsearchs );
+
+=head1 NAME
+
+FS::invoice_conf - Object methods for invoice_conf records
+
+=head1 SYNOPSIS
+
+ use FS::invoice_conf;
+
+ $record = new FS::invoice_conf \%hash;
+ $record = new FS::invoice_conf { 'column' => 'value' };
+
+ $error = $record->insert;
+
+ $error = $new_record->replace($old_record);
+
+ $error = $record->delete;
+
+ $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::invoice_conf object represents a set of localized invoice
+configuration values. FS::invoice_conf inherits from FS::Record and FS::Conf,
+and supports the FS::Conf interface. The following fields are supported:
+
+=over 4
+
+=item confnum - primary key
+
+=item modenum - L<FS::invoice_mode> foreign key
+
+=item locale - locale string (see L<FS::Locales>)
+
+=item notice_name - the title to display on the invoice
+
+=item subject - subject line of the email
+
+=item htmlnotes - "notes" section (HTML)
+
+=item htmlfooter - footer (HTML)
+
+=item htmlsummary - summary header, for invoices in summary format (HTML)
+
+=item htmlreturnaddress - return address (HTML)
+
+=item latexnotes - "notes" section (LaTeX)
+
+=item latexfooter - footer (LaTeX)
+
+=item latexsummary - summary header, for invoices in summary format (LaTeX)
+
+=item latexreturnaddress - return address (LaTeX)
+
+=item latexcoupon - payment coupon section (LaTeX)
+
+=item latexsmallfooter - footer for pages after the first (LaTeX)
+
+=item latextopmargin - top margin
+
+=item latexheadsep - distance from bottom of header to top of body
+
+=item latexaddresssep - distance from top of body to customer address
+
+=item latextextheight - maximum height of invoice body text
+
+=item latexextracouponspace - additional footer space to allow for coupon
+
+=item latexcouponfootsep - distance from bottom of coupon content to top
+of page footer
+
+=item latexcouponamountenclosedsep - distance from coupon balance line to
+"Amount Enclosed" box
+
+=item latexcoupontoaddresssep - distance from "Amount Enclosed" box to
+coupon mailing address
+
+=item latexverticalreturnaddress - 'Y' to place the return address below
+the company logo rather than beside it
+
+=item latexcouponaddcompanytoaddress - 'Y' to add the company name to the
+address on the payment coupon
+
+=item logo_png - company logo, as a PNG, for HTML invoices
+
+=item logo_eps - company logo, as an EPS, for LaTeX invoices
+
+=item lpr - command to print the invoice (passed on stdin as a PDF)
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new invoice configuration. To add it to the database, see
+L<"insert">.
+
+=cut
+
+# the new method can be inherited from FS::Record, if a table method is defined
+
+sub table { 'invoice_conf'; }
+
+=item insert
+
+Adds this record to the database. If there is an error, returns the error,
+otherwise returns false.
+
+=cut
+
+# slightly special: you can insert/replace the invoice mode this way
+
+sub insert {
+ my $self = shift;
+ if (!$self->modenum) {
+ my $invoice_mode = FS::invoice_mode->new({
+ 'modename' => $self->modename,
+ 'agentnum' => $self->agentnum,
+ });
+ my $error = $invoice_mode->insert;
+ return $error if $error;
+ $self->set('modenum' => $invoice_mode->modenum);
+ } else {
+ my $invoice_mode = FS::invoice_mode->by_key($self->modenum);
+ my $changed = 0;
+ foreach (qw(agentnum modename)) {
+ $changed ||= ($invoice_mode->get($_) eq $self->get($_));
+ $invoice_mode->set($_, $self->get($_));
+ }
+ my $error = $invoice_mode->replace if $changed;
+ return $error if $error;
+ }
+ $self->SUPER::insert(@_);
+}
+
+=item delete
+
+Delete this record from the database.
+
+=cut
+
+sub delete {
+ my $self = shift;
+ my $error = $self->FS::Record::delete; # not Conf::delete
+ return $error if $error;
+ my $invoice_mode = FS::invoice_mode->by_key($self->modenum);
+ if ( $invoice_mode and
+ FS::invoice_conf->count('modenum = '.$invoice_mode->modenum) == 0 ) {
+ $error = $invoice_mode->delete;
+ return $error if $error;
+ }
+ '';
+}
+
+=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
+
+sub replace {
+ my $self = shift;
+ my $error = $self->SUPER::replace(@_);
+ return $error if $error;
+
+ my $invoice_mode = FS::invoice_mode->by_key($self->modenum);
+ my $changed = 0;
+ foreach (qw(agentnum modename)) {
+ $changed ||= ($invoice_mode->get($_) eq $self->get($_));
+ $invoice_mode->set($_, $self->get($_));
+ }
+ $error = $invoice_mode->replace if $changed;
+ return $error if $error;
+}
+
+=item check
+
+Checks all fields to make sure this is a valid example. 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('confnum')
+ || $self->ut_number('modenum')
+ || $self->ut_textn('locale')
+ || $self->ut_anything('notice_name')
+ || $self->ut_anything('subject')
+ || $self->ut_anything('htmlnotes')
+ || $self->ut_anything('htmlfooter')
+ || $self->ut_anything('htmlsummary')
+ || $self->ut_anything('htmlreturnaddress')
+ || $self->ut_anything('latexnotes')
+ || $self->ut_anything('latexfooter')
+ || $self->ut_anything('latexsummary')
+ || $self->ut_anything('latexcoupon')
+ || $self->ut_anything('latexsmallfooter')
+ || $self->ut_anything('latexreturnaddress')
+ || $self->ut_textn('latextopmargin')
+ || $self->ut_textn('latexheadsep')
+ || $self->ut_textn('latexaddresssep')
+ || $self->ut_textn('latextextheight')
+ || $self->ut_textn('latexextracouponspace')
+ || $self->ut_textn('latexcouponfootsep')
+ || $self->ut_textn('latexcouponamountenclosedsep')
+ || $self->ut_textn('latexcoupontoaddresssep')
+ || $self->ut_flag('latexverticalreturnaddress')
+ || $self->ut_flag('latexcouponaddcompanytoaddress')
+ || $self->ut_anything('logo_png')
+ || $self->ut_anything('logo_eps')
+ ;
+ return $error if $error;
+
+ $self->SUPER::check;
+}
+
+# hook _config to substitute our own values; let FS::Conf do the rest of
+# the interface
+
+sub _config {
+ my $self = shift;
+ # if we fall back, we still want FS::Conf to respect our locale
+ $self->{locale} = $self->get('locale');
+ my ($key, $agentnum, $nodefault) = @_;
+ # some fields, but not all, start with invoice_
+ my $colname = $key;
+ if ( $key =~ /^invoice_(.*)$/ ) {
+ $colname = $1;
+ }
+ if ( length($self->get($colname)) ) {
+ return FS::conf->new({ 'name' => $key,
+ 'value' => $self->get($colname) });
+ } else {
+ return $self->FS::Conf::_config(@_);
+ }
+}
+
+# disambiguation
+sub set {
+ my $self = shift;
+ $self->FS::Record::set(@_);
+}
+
+sub exists {
+ my $self = shift;
+ $self->FS::Conf::exists(@_);
+}
+
+=back
+
+=head1 SEE ALSO
+
+L<FS::Template_Mixin>, L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/invoice_mode.pm b/FS/FS/invoice_mode.pm
new file mode 100644
index 000000000..115dd4469
--- /dev/null
+++ b/FS/FS/invoice_mode.pm
@@ -0,0 +1,157 @@
+package FS::invoice_mode;
+
+use strict;
+use base qw( FS::Record );
+use FS::Record qw( qsearch qsearchs );
+use FS::invoice_conf;
+
+=head1 NAME
+
+FS::invoice_mode - Object methods for invoice_mode records
+
+=head1 SYNOPSIS
+
+ use FS::invoice_mode;
+
+ $record = new FS::invoice_mode \%hash;
+ $record = new FS::invoice_mode { 'column' => 'value' };
+
+ $error = $record->insert;
+
+ $error = $new_record->replace($old_record);
+
+ $error = $record->delete;
+
+ $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::invoice_mode object represents an invoice rendering style.
+FS::invoice_mode inherits from FS::Record. The following fields are
+currently supported:
+
+=over 4
+
+=item modenum - primary key
+
+=item agentnum - the agent who owns this invoice mode (can be null)
+
+=item modename - descriptive name for internal use
+
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new invoice mode. To add the object 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 { 'invoice_mode'; }
+
+=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 example. 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('modenum')
+ || $self->ut_foreign_keyn('agentnum', 'agent', 'agentnum')
+ || $self->ut_text('modename')
+ ;
+ return $error if $error;
+
+ $self->SUPER::check;
+}
+
+=item invoice_conf [ LOCALE ]
+
+Returns the L<FS::invoice_conf> for this invoice mode, with the specified
+locale. If there isn't one with that locale, returns the one with null
+locale. If that doesn't exist, returns nothing.
+
+=cut
+
+sub invoice_conf {
+ my $self = shift;
+ my $locale = shift;
+ my $invoice_conf;
+ if ( $locale ) {
+ $invoice_conf = qsearchs('invoice_conf', {
+ modenum => $self->modenum,
+ locale => $locale,
+ });
+ }
+ $invoice_conf ||= qsearchs('invoice_conf', {
+ modenum => $self->modenum,
+ locale => '',
+ });
+ $invoice_conf;
+}
+
+=item agent
+
+Returns the agent associated with this invoice mode, if any.
+
+=cut
+
+sub agent {
+ my $self = shift;
+ $self->agentnum ? FS::agent->by_key($self->agentnum) : '';
+}
+
+=back
+
+=head1 SEE ALSO
+
+L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/part_event/Action/cust_bill_email.pm b/FS/FS/part_event/Action/cust_bill_email.pm
index 1a3bca4b7..3331a4cb6 100644
--- a/FS/FS/part_event/Action/cust_bill_email.pm
+++ b/FS/FS/part_event/Action/cust_bill_email.pm
@@ -9,14 +9,22 @@ sub eventtable_hashref {
{ 'cust_bill' => 1 };
}
+sub option_fields {
+ (
+ 'modenum' => { label => 'Invoice mode',
+ type => 'select-invoice_mode',
+ },
+ );
+}
+
sub default_weight { 51; }
sub do_action {
my( $self, $cust_bill ) = @_;
- #my $cust_main = $self->cust_main($cust_bill);
my $cust_main = $cust_bill->cust_main;
+ $cust_bill->set('mode' => $self->option('modenum'));
$cust_bill->email unless $cust_main->invoice_noemail;
}
diff --git a/FS/FS/part_event/Action/cust_bill_print.pm b/FS/FS/part_event/Action/cust_bill_print.pm
index 6b3e6f460..ea6e0aa8e 100644
--- a/FS/FS/part_event/Action/cust_bill_print.pm
+++ b/FS/FS/part_event/Action/cust_bill_print.pm
@@ -9,6 +9,14 @@ sub eventtable_hashref {
{ 'cust_bill' => 1 };
}
+sub option_fields {
+ (
+ 'modenum' => { label => 'Invoice mode',
+ type => 'select-invoice_mode',
+ },
+ );
+}
+
sub default_weight { 51; }
sub do_action {
@@ -17,6 +25,7 @@ sub do_action {
#my $cust_main = $self->cust_main($cust_bill);
my $cust_main = $cust_bill->cust_main;
+ $cust_bill->set('mode' => $self->option('modenum'));
$cust_bill->print;
}
diff --git a/FS/FS/part_event/Action/cust_bill_print_pdf.pm b/FS/FS/part_event/Action/cust_bill_print_pdf.pm
index 6b37f389f..6c01d4294 100644
--- a/FS/FS/part_event/Action/cust_bill_print_pdf.pm
+++ b/FS/FS/part_event/Action/cust_bill_print_pdf.pm
@@ -9,6 +9,14 @@ sub eventtable_hashref {
{ 'cust_bill' => 1 };
}
+sub option_fields {
+ (
+ 'modenum' => { label => 'Invoice mode',
+ type => 'select-invoice_mode'
+ },
+ );
+}
+
sub default_weight { 51; }
sub do_action {
@@ -20,6 +28,7 @@ sub do_action {
my $opt = { $self->options };
$opt->{'notice_name'} ||= 'Invoice';
+ $cust_bill->set('mode' => $self->option('modenum'));
$cust_bill->batch_invoice($opt);
}
diff --git a/FS/FS/part_event/Action/cust_bill_send.pm b/FS/FS/part_event/Action/cust_bill_send.pm
index 587a7c664..c6928dc00 100644
--- a/FS/FS/part_event/Action/cust_bill_send.pm
+++ b/FS/FS/part_event/Action/cust_bill_send.pm
@@ -9,11 +9,20 @@ sub eventtable_hashref {
{ 'cust_bill' => 1 };
}
+sub option_fields {
+ (
+ 'modenum' => { label => 'Invoice mode',
+ type => 'select-invoice_mode',
+ },
+ );
+}
+
sub default_weight { 50; }
sub do_action {
my( $self, $cust_bill ) = @_;
+ $cust_bill->set('mode' => $self->option('modenum'));
$cust_bill->send;
}
diff --git a/FS/FS/part_event/Action/cust_bill_send_agent.pm b/FS/FS/part_event/Action/cust_bill_send_agent.pm
index 670a32c5b..bbb757b59 100644
--- a/FS/FS/part_event/Action/cust_bill_send_agent.pm
+++ b/FS/FS/part_event/Action/cust_bill_send_agent.pm
@@ -7,6 +7,9 @@ sub description {
'Send invoice (email/print/fax) with alternate template, for specific agents';
}
+# this event is just cust_bill_send_alternate + an implicit (and inefficient)
+# 'agent' condition
+
sub eventtable_hashref {
{ 'cust_bill' => 1 };
}
@@ -17,6 +20,9 @@ sub option_fields {
type => 'select-agent',
multiple => 1
},
+ 'modenum' => { label => 'Invoice mode',
+ type => 'select-invoice_mode',
+ },
'agent_templatename' => { label => 'Template',
type => 'select-invoice_template',
},
@@ -32,10 +38,15 @@ sub do_action {
#my $cust_main = $self->cust_main($cust_bill);
my $cust_main = $cust_bill->cust_main;
+ my %agentnums = map { $_=>1 } split(/\s*,\s*/, $self->option('agentnum'));
+ if (keys(%agentnums) and !exists($agentnums{$cust_main->agentnum})) {
+ return;
+ }
+
+ $cust_bill->set('mode' => $self->option('modenum'));
$cust_bill->send(
- $self->option('agent_templatename'),
- [ split(/\s*,\s*/, $self->option('agentnum') ) ],
- $self->option('agent_invoice_from'),
+ 'template' => $self->option('agent_templatename'),
+ 'invoice_from' => $self->option('agent_invoice_from'),
);
}
diff --git a/FS/FS/part_event/Action/cust_bill_send_alternate.pm b/FS/FS/part_event/Action/cust_bill_send_alternate.pm
index cfd9264d8..fb71a5a39 100644
--- a/FS/FS/part_event/Action/cust_bill_send_alternate.pm
+++ b/FS/FS/part_event/Action/cust_bill_send_alternate.pm
@@ -11,6 +11,8 @@ sub eventtable_hashref {
sub option_fields {
(
+ 'modenum' => { label => 'Invoice mode',
+ type => 'select-invoice_mode' },
'templatename' => { label => 'Template',
type => 'select-invoice_template',
},
@@ -25,7 +27,8 @@ sub do_action {
#my $cust_main = $self->cust_main($cust_bill);
my $cust_main = $cust_bill->cust_main;
- $cust_bill->send( $self->option('templatename') );
+ $cust_bill->set('mode' => $self->option('modenum'));
+ $cust_bill->send({'template' => $self->option('templatename')});
}
1;
diff --git a/FS/FS/part_event/Action/cust_bill_send_if_newest.pm b/FS/FS/part_event/Action/cust_bill_send_if_newest.pm
index 083da8b08..c744362ce 100644
--- a/FS/FS/part_event/Action/cust_bill_send_if_newest.pm
+++ b/FS/FS/part_event/Action/cust_bill_send_if_newest.pm
@@ -18,6 +18,9 @@ sub eventtable_hashref {
sub option_fields {
(
+ 'modenum' => { label => 'Invoice mode',
+ type => 'select-invoice_mode',
+ },
'if_newest_templatename' => { label => 'Template',
type => 'select-invoice_template',
},
@@ -29,10 +32,17 @@ sub default_weight { 50; }
sub do_action {
my( $self, $cust_bill ) = @_;
- #my $cust_main = $self->cust_main($cust_bill);
- my $cust_main = $cust_bill->cust_main;
-
- $cust_bill->send( $self->option('templatename') );
+ my $invnum = $cust_bill->invnum;
+ my $custnum = $cust_bill->custnum;
+ return '' if scalar(
+ grep { $_->owed > 0 }
+ qsearch('cust_bill', {
+ 'custnum' => $custnum,
+ 'invnum' => { op=>'>', value=>$invnum },
+ })
+ );
+ $cust_bill->set('mode' => $self->option('modenum'));
+ $cust_bill->send( 'template' => $self->option('templatename') );
}
1;
diff --git a/FS/FS/part_event/Action/cust_bill_send_reminder.pm b/FS/FS/part_event/Action/cust_bill_send_reminder.pm
index 073bb8fd3..354f969d4 100644
--- a/FS/FS/part_event/Action/cust_bill_send_reminder.pm
+++ b/FS/FS/part_event/Action/cust_bill_send_reminder.pm
@@ -11,9 +11,13 @@ sub eventtable_hashref {
sub option_fields {
(
+ 'modenum' => { label => 'Invoice mode',
+ type => 'select-invoice_mode',
+ },
+ # totally unnecessary, since the invoice mode can set notice_name and lpr,
+ # but for compatibility...
'notice_name' => 'Reminder name',
- #'notes' => { 'label' => 'Reminder notes' },
- #include standard notes? no/prepend/append
+ #'notes' => { 'label' => 'Reminder notes' }, # invoice mode does this
'lpr' => 'Optional alternate print command',
);
}
@@ -23,9 +27,7 @@ sub default_weight { 50; }
sub do_action {
my( $self, $cust_bill ) = @_;
- #my $cust_main = $self->cust_main($cust_bill);
- #my $cust_main = $cust_bill->cust_main;
-
+ $cust_bill->set('mode' => $self->option('modenum'));
$cust_bill->send({
'notice_name' => $self->option('notice_name'),
'lpr' => $self->option('lpr'),
diff --git a/FS/FS/part_event/Action/cust_statement_send.pm b/FS/FS/part_event/Action/cust_statement_send.pm
index 74cc48ca8..67a94aaa1 100644
--- a/FS/FS/part_event/Action/cust_statement_send.pm
+++ b/FS/FS/part_event/Action/cust_statement_send.pm
@@ -19,7 +19,8 @@ sub default_weight {
sub do_action {
my( $self, $cust_statement ) = @_;
- $cust_statement->send( 'statement' ); #XXX configure
+ $cust_statement->send( 'template' => 'statement' ); #XXX configure
+ #XXX use an invoice mode?
}
diff --git a/FS/FS/part_event/Action/fee.pm b/FS/FS/part_event/Action/fee.pm
index cd9e200c8..c2b4673fa 100644
--- a/FS/FS/part_event/Action/fee.pm
+++ b/FS/FS/part_event/Action/fee.pm
@@ -32,7 +32,48 @@ sub _calc_fee {
if ( $balance >= 0 ) {
return 0;
} elsif ( (-1 * $balance) < $self->option('charge') ) {
- return -1 * $balance;
+ my $total = -1 * $balance;
+ # if it's tax exempt, then we're done
+ # XXX we also bail out if you're using external tax tables, because
+ # they're definitely NOT linear and we haven't yet had a reason to
+ # make that case work.
+ return $total if $self->option('setuptax') eq 'Y'
+ or FS::Conf->new->exists('enable_taxproducts');
+
+ # estimate tax rate
+ # false laziness with xmlhttp-calculate_taxes, cust_main::Billing, etc.
+ # XXX not accurate with monthly exemptions
+ my $cust_main = $cust_object->cust_main;
+ my $taxlisthash = {};
+ my $charge = FS::cust_bill_pkg->new({
+ setup => $total,
+ recur => 0,
+ details => []
+ });
+ my $part_pkg = FS::part_pkg->new({
+ taxclass => $self->option('taxclass')
+ });
+ my $error = $cust_main->_handle_taxes(
+ FS::part_pkg->new({ taxclass => ($self->option('taxclass') || '') }),
+ $taxlisthash,
+ $charge,
+ FS::cust_pkg->new({custnum => $cust_main->custnum}),
+ );
+ if ( $error ) {
+ warn "error estimating taxes for breakage charge: custnum ".$cust_main->custnum."\n";
+ return $total;
+ }
+ # $taxlisthash: tax identifier => [ cust_main_county, cust_bill_pkg... ]
+ my $total_rate = 0;
+ my @taxes = map { $_->[0] } values %$taxlisthash;
+ foreach (@taxes) {
+ $total_rate += $_->tax;
+ }
+ return $total if $total_rate == 0; # no taxes apply
+
+ my $total_cents = $total * 100;
+ my $charge_cents = sprintf('%.0f', $total_cents * 100/(100 + $total_rate));
+ return ($charge_cents / 100);
}
}
diff --git a/FS/FS/part_event/Condition/pkg_age_Common.pm b/FS/FS/part_event/Condition/pkg_age_Common.pm
index 726b01d70..33e49b8a6 100644
--- a/FS/FS/part_event/Condition/pkg_age_Common.pm
+++ b/FS/FS/part_event/Condition/pkg_age_Common.pm
@@ -49,7 +49,7 @@ sub condition {
}
sub pkg_age_age {
- my( $self, $cust_pkg, %opt );
+ my( $self, $cust_pkg, %opt ) = @_;
$self->option_age_from('age', $opt{'time'} );
}
diff --git a/FS/FS/part_export/domain_shellcommands.pm b/FS/FS/part_export/domain_shellcommands.pm
index 582e29217..8e85d71e1 100644
--- a/FS/FS/part_export/domain_shellcommands.pm
+++ b/FS/FS/part_export/domain_shellcommands.pm
@@ -49,8 +49,7 @@ The following variables are available for interpolation (prefixed with <code>new
<LI><code>$uid</code> - of catchall account
<LI><code>$gid</code> - of catchall account
<LI><code>$dir</code> - home directory of catchall account
- <LI>All other fields in
- <a href="../docs/schema.html#svc_domain">svc_domain</a> are also available.
+ <LI>All other fields in <b>svc_domain</b> are also available.
</UL>
END
);
diff --git a/FS/FS/part_export/shellcommands_withdomain.pm b/FS/FS/part_export/shellcommands_withdomain.pm
index 1b59589bf..29715b75b 100644
--- a/FS/FS/part_export/shellcommands_withdomain.pm
+++ b/FS/FS/part_export/shellcommands_withdomain.pm
@@ -141,7 +141,32 @@ The following variables are available for interpolation (prefixed with
<LI><code>$shell</code>
<LI><code>$quota</code>
<LI><code>@radius_groups</code>
- <LI>All other fields in <a href="../docs/schema.html#svc_acct">svc_acct</a> are also available.
+ <LI><code>$reasonnum (when suspending)</code>
+ <LI><code>$reasontext (when suspending)</code>
+ <LI><code>$reasontypenum (when suspending)</code>
+ <LI><code>$reasontypetext (when suspending)</code>
+ <LI><code>$pkgnum</code>
+ <LI><code>$custnum</code>
+ <LI>All other fields in <b>svc_acct</b> are also available.
+ <LI>The following fields from <b>cust_main</b> are also available (except during replace): company, address1, address2, city, state, zip, county, daytime, night, fax, otaker, agent_custid, locale. When used on the command line (rather than STDIN), they will be quoted for the shell already (do not add additional quotes).
+</UL>
+For the package changed command only, the following fields are also available:
+<UL>
+ <LI>$old_pkgnum and $new_pkgnum
+ <LI>$old_pkgpart and $new_pkgpart
+ <LI>$old_agent_pkgid and $new_agent_pkgid
+ <LI>$old_order_date and $new_order_date
+ <LI>$old_start_date and $new_start_date
+ <LI>$old_setup and $new_setup
+ <LI>$old_bill and $new_bill
+ <LI>$old_last_bill and $new_last_bill
+ <LI>$old_susp and $new_susp
+ <LI>$old_adjourn and $new_adjourn
+ <LI>$old_resume and $new_resume
+ <LI>$old_cancel and $new_cancel
+ <LI>$old_unancel and $new_unancel
+ <LI>$old_expire and $new_expire
+ <LI>$old_contract_end and $new_contract_end
</UL>
END
);
diff --git a/FS/FS/svc_cable.pm b/FS/FS/svc_cable.pm
index 1980c0ee9..596f69995 100644
--- a/FS/FS/svc_cable.pm
+++ b/FS/FS/svc_cable.pm
@@ -4,6 +4,7 @@ use base qw( FS::svc_Common ); #qw( FS::device_Common FS::svc_Common );
use strict;
use Tie::IxHash;
use FS::Record qw( qsearchs ); # qw( qsearch qsearchs );
+use FS::cable_provider;
use FS::cable_model;
=head1 NAME
@@ -72,24 +73,35 @@ sub search_sql {
sub table_info {
tie my %fields, 'Tie::IxHash',
- 'svcnum' => 'Service',
- 'modelnum' => { label => 'Model',
- type => 'select-cable_model',
- disable_inventory => 1,
- disable_select => 1,
- value_callback => sub {
- my $svc = shift;
- $svc->cable_model->model_name;
- },
- },
- 'serialnum' => 'Serial number',
- 'mac_addr' => { label => 'MAC address',
- type => 'input-mac_addr',
- value_callback => sub {
- my $svc = shift;
- join(':', $svc->mac_addr =~ /../g);
- },
- },
+ 'svcnum' => 'Service',
+ 'providernum' => { label => 'Provider',
+ type => 'select-cable_provider',
+ disable_inventory => 1,
+ disable_select => 1,
+ value_callback => sub {
+ my $svc = shift;
+ my $p = $svc->cable_provider;
+ $p ? $p->provider : '';
+ },
+ },
+ #XXX "Circuit ID/Order number"
+ 'modelnum' => { label => 'Model',
+ type => 'select-cable_model',
+ disable_inventory => 1,
+ disable_select => 1,
+ value_callback => sub {
+ my $svc = shift;
+ $svc->cable_model->model_name;
+ },
+ },
+ 'serialnum' => 'Serial number',
+ 'mac_addr' => { label => 'MAC address',
+ type => 'input-mac_addr',
+ value_callback => sub {
+ my $svc = shift;
+ join(':', $svc->mac_addr =~ /../g);
+ },
+ },
;
{
@@ -130,6 +142,7 @@ sub check {
my $error =
$self->ut_numbern('svcnum')
+ || $self->ut_foreign_key('providernum', 'cable_provider', 'providernum')
|| $self->ut_foreign_key('modelnum', 'cable_model', 'modelnum')
|| $self->ut_alpha('serialnum')
|| $self->ut_mac_addr('mac_addr')
@@ -139,6 +152,17 @@ sub check {
$self->SUPER::check;
}
+=item cable_provider
+
+Returns the cable_provider object for this record.
+
+=cut
+
+sub cable_provider {
+ my $self = shift;
+ qsearchs('cable_provider', { 'providernum'=>$self->providernum } );
+}
+
=item cable_model
Returns the cable_model object for this record.
diff --git a/FS/MANIFEST b/FS/MANIFEST
index a3b11f717..5dbe754c1 100644
--- a/FS/MANIFEST
+++ b/FS/MANIFEST
@@ -720,3 +720,9 @@ FS/svc_alarm.pm
t/svc_alarm.t
FS/cable_model.pm
t/cable_model.t
+FS/invoice_mode.pm
+t/invoice_mode.t
+FS/invoice_conf.pm
+t/invoice_conf.t
+FS/cable_provider.pm
+t/cable_provider.t
diff --git a/FS/t/cable_provider.t b/FS/t/cable_provider.t
new file mode 100644
index 000000000..c794379a9
--- /dev/null
+++ b/FS/t/cable_provider.t
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::cable_provider;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/invoice_conf.t b/FS/t/invoice_conf.t
new file mode 100644
index 000000000..b707fa3f0
--- /dev/null
+++ b/FS/t/invoice_conf.t
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::invoice_conf;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/invoice_mode.t b/FS/t/invoice_mode.t
new file mode 100644
index 000000000..5f945f0d4
--- /dev/null
+++ b/FS/t/invoice_mode.t
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::invoice_mode;
+$loaded=1;
+print "ok 1\n";
diff --git a/bin/generate-table-module b/bin/generate-table-module
index b536360c5..37a581200 100755
--- a/bin/generate-table-module
+++ b/bin/generate-table-module
@@ -95,10 +95,10 @@ close TEST;
# add them to MANIFEST
###
-#system('cvs edit FS/MANIFEST');
-
open(MANIFEST,">>FS/MANIFEST") or die $!;
print MANIFEST "FS/$table.pm\n",
"t/$table.t\n";
close MANIFEST;
+system("git add FS/FS/$table.pm FS/t/$table.t");
+
diff --git a/bin/standardize-locations b/bin/standardize-locations
new file mode 100755
index 000000000..6e5fd3c16
--- /dev/null
+++ b/bin/standardize-locations
@@ -0,0 +1,25 @@
+#!/usr/bin/perl -w
+
+use strict;
+use FS::UID 'adminsuidsetup';
+use FS::Conf;
+use FS::queue;
+
+my $user = shift or die "usage:\n standardize-locations user";
+adminsuidsetup($user);
+my $conf = FS::Conf->new;
+my $method = $conf->config('address_standardize_method')
+ or die "No address standardization method configured.\n";
+if ($method eq 'usps') {
+ # we're not supposed to do this
+ # (allow it anyway with a warning?)
+ die "USPS standardization does not allow batch processing.\n";
+}
+my $job = FS::queue->new({
+ job => 'FS::cust_location::process_standardize'
+});
+my $error = $job->insert('_JOB');
+die $error if $error;
+print "Address standardization job scheduled.\n";
+
+1;
diff --git a/bin/test-event b/bin/test-event
index d3a9f110d..73c9d31ec 100644..100755
--- a/bin/test-event
+++ b/bin/test-event
@@ -34,7 +34,10 @@ print "\n";
my @conditions = $part_event->part_event_condition;
foreach my $condition ( @conditions ) {
- my $sat = $condition->condition( $object, 'cust_event' => $cust_event );
+ my $sat = $condition->condition( $object,
+ 'cust_event' => $cust_event,
+ 'time' => time,
+ );
my $sql = $condition->condition_sql();
diff --git a/httemplate/browse/cable_provider.html b/httemplate/browse/cable_provider.html
new file mode 100644
index 000000000..0d344984b
--- /dev/null
+++ b/httemplate/browse/cable_provider.html
@@ -0,0 +1,32 @@
+<& elements/browse.html,
+ 'title' => 'Cable providers',
+ 'html_init' => $html_init,
+ 'name' => 'providers',
+ 'disableable' => 1,
+ 'disabled_statuspos' => 1,
+ 'query' => { 'table' => 'cable_provider',
+ 'hashref' => {},
+ 'order_by' => 'ORDER BY provider',
+ },
+ 'count_query' => $count_query,
+ 'header' => $header,
+ 'fields' => $fields,
+ 'links' => $links,
+&>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
+
+my $html_init =
+ qq!<A HREF="${p}edit/cable_provider.html"><I>Add a provider</I></A><BR><BR>!;
+
+my $count_query = 'SELECT COUNT(*) FROM cable_provider';
+
+my $link = [ $p.'edit/cable_provider.html?', 'providernum' ];
+
+my $header = [ 'Provider' ];
+my $fields = [ 'provider' ];
+my $links = [ $link ];
+
+</%init>
diff --git a/httemplate/browse/invoice_conf.html b/httemplate/browse/invoice_conf.html
new file mode 100644
index 000000000..c8fd1bffb
--- /dev/null
+++ b/httemplate/browse/invoice_conf.html
@@ -0,0 +1,70 @@
+<& elements/browse.html,
+ 'title' => 'Invoice modes',
+ 'name_singular' => 'configuration',
+ 'menubar' => \@menubar,
+ 'query' => {
+ 'select' => $select,
+ 'table' => 'invoice_conf',
+ 'addl_from' => ' JOIN invoice_mode USING (modenum)',
+ 'extra_sql' => ' WHERE '.$curuser->agentnums_sql(
+ 'table' => 'invoice_mode',
+ 'null_right' => ['Edit global templates'],
+ ),
+ 'order_by' => q( ORDER BY modename asc, COALESCE(locale,'') asc),
+ },
+ 'count_query' => 'SELECT COUNT(*) FROM invoice_conf JOIN invoice_mode USING (modenum)',
+ 'header' => [ 'Name', 'Agent', 'Locale', 'Overrides', ],
+ 'fields' => [ $modename,
+ $agent,
+ $locale_label,
+ $overrides,
+ ],
+ 'align' => 'llcl',
+ 'links' => [ '', '', $link ],
+ 'disable_maxselect' => 1,
+&>
+<%init>
+
+my $curuser = $FS::CurrentUser::CurrentUser;
+
+die "access denied"
+ unless $curuser->access_right([ 'View templates', 'View global templates',
+ 'Edit templates', 'Edit global templates', ]);
+
+my @overrides = grep {$_ ne 'modenum' and $_ ne 'confnum'} FS::invoice_conf->fields;
+my $select = join(',', 'modename', 'agentnum', 'confnum', 'invoice_conf.*');
+
+my @menubar = ();
+if ( $curuser->access_right(['Edit templates', 'Edit global templates']) ) {
+ push @menubar, 'Add a new invoice mode' => $p.'edit/invoice_conf.html';
+}
+
+my $locale_style = 'font-size:0.8em; padding:3px; background-color:';
+
+my $last_modenum = 0;
+my $modename = sub {
+ return '' if $_[0]->modenum == $last_modenum;
+ $_[0]->modename;
+};
+
+my $agent = sub {
+ return '' if $_[0]->modenum == $last_modenum;
+ $last_modenum = $_[0]->modenum;
+ $_[0]->agentnum ? FS::agent->by_key($_[0]->agentnum)->agent : '(global)';
+};
+
+my $locale_label = sub {
+ my $l = $_[0]->locale;
+ $l ? +{ FS::Locales->locale_info($l) }->{'label'} : '(default)';
+};
+
+my $overrides = sub {
+ my $invoice_conf = shift;
+ [ map { [ { data => $_ } ] }
+ grep { length $invoice_conf->get($_) }
+ @overrides
+ ],
+};
+
+my $link = [ $p.'edit/invoice_conf.html?', 'confnum' ];
+</%init>
diff --git a/httemplate/docs/about.html b/httemplate/docs/about.html
index c2ba4e4a0..80d9488b6 100644
--- a/httemplate/docs/about.html
+++ b/httemplate/docs/about.html
@@ -56,7 +56,7 @@ GNU <b>Affero</b> General Public License.<BR>
% unless ( $agentnum ) {
<CENTER>
- <FONT SIZE="-3">"I can't figure out ... if it's an end or the beginning" - R. Hunter</FONT>
+ <FONT SIZE="-3">"" - R. Hunter</FONT>
</CENTER>
% }
diff --git a/httemplate/edit/cable_provider.html b/httemplate/edit/cable_provider.html
new file mode 100644
index 000000000..9a911ccfa
--- /dev/null
+++ b/httemplate/edit/cable_provider.html
@@ -0,0 +1,20 @@
+<& elements/edit.html,
+ 'name_singular' => 'Provider',
+ 'table' => 'cable_provider',
+ 'fields' => [
+ 'provider',
+ { field=>'disabled', type=>'checkbox', value=>'Y', },
+ ],
+ 'labels' => {
+ 'providernum' => 'Provider',
+ 'provider' => 'Provider',
+ 'disabled' => 'Disabled',
+ },
+ 'viewall_dir' => 'browse',
+&>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
+
+</%init>
diff --git a/httemplate/edit/cust_main/bottomfixup.js b/httemplate/edit/cust_main/bottomfixup.js
index 9e18fa0df..ecfcb3cbc 100644
--- a/httemplate/edit/cust_main/bottomfixup.js
+++ b/httemplate/edit/cust_main/bottomfixup.js
@@ -10,17 +10,20 @@ my @fixups = ('copy_payby_fields', 'standardize_locations');
push @fixups, 'confirm_censustract'
if $conf->exists('cust_main-require_censustract');
-# currently doesn't work; disable to avoid problems
-#push @fixups, 'check_unique'
-# if $conf->exists('cust_main-check_unique') and !$opt{'custnum'};
+my $uniqueness = $conf->config('cust_main-check_unique');
+push @fixups, 'check_unique'
+ if $uniqueness and !$opt{'custnum'};
push @fixups, 'do_submit'; # always last
</%init>
-
var fixups = <% encode_json(\@fixups) %>;
var fixup_position;
var running = false;
+<&| /elements/onload.js &>
+submit_abort();
+</&>
+
%# state machine to deal with all the asynchronous stuff we're doing
%# call this after each fixup on success:
function submit_continue() {
@@ -132,10 +135,14 @@ function set_censustract(tract, year) {
}
function check_unique() {
- var search_hash = new Object;
-% foreach ($conf->config('cust_main-check_unique')) {
- search_hash['<% $_ %>'] = document.CustomerForm.elements['<% $_ %>'].value;
+ var search_hash = {};
+% if ($uniqueness eq 'address') {
+ search_hash['address'] = [
+ document.CustomerForm.elements['bill_address1'].value,
+ document.CustomerForm.elements['ship_address1'].value
+ ];
% }
+%# no other options yet
%# supported in IE8+, Firefox 3.5+, WebKit, Opera 10.5+
duplicates_form(JSON.stringify(search_hash), confirm_unique);
diff --git a/httemplate/edit/cust_main/top_misc.html b/httemplate/edit/cust_main/top_misc.html
index ebd9b927c..e25506f52 100644
--- a/httemplate/edit/cust_main/top_misc.html
+++ b/httemplate/edit/cust_main/top_misc.html
@@ -154,9 +154,10 @@
% } else {
- <& /elements/tr-select-part_referral.html,
- 'curr_value' => $refnum
- &>
+ <& /elements/tr-select-part_referral.html,
+ 'curr_value' => $refnum,
+ 'label' => "<B>${r}".emt('Advertising source')."</B>"
+ &>
% }
diff --git a/httemplate/edit/elements/edit.html b/httemplate/edit/elements/edit.html
index 060281115..6c965326b 100644
--- a/httemplate/edit/elements/edit.html
+++ b/httemplate/edit/elements/edit.html
@@ -338,6 +338,7 @@ Example:
% qw( width height config ), #htmlarea
% qw( alt_format ), #select-cust_location
% qw( classnum ), # select-inventory_item
+% qw( aligned ), # columnstart
% ;
%
% #select-table
diff --git a/httemplate/edit/invoice_conf.html b/httemplate/edit/invoice_conf.html
new file mode 100644
index 000000000..b7b3a4ebc
--- /dev/null
+++ b/httemplate/edit/invoice_conf.html
@@ -0,0 +1,296 @@
+<& elements/edit.html,
+ 'body_etc' => $body_etc,
+ 'name_singular' => 'invoice configuration',
+ 'table' => 'invoice_conf',
+ 'viewall_dir' => 'browse',
+ 'fields' => \@fields,
+ 'labels' => \%labels,
+ 'new_callback' => \&new_callback,
+ 'edit_callback' => \&edit_callback,
+ 'error_callback' => \&error_callback,
+ 'html_init' => \&html_init,
+ 'html_table_bottom' => \&html_table_bottom,
+ 'html_bottom' => '</DIV>', # close tablebreak-tabs
+&>
+<%init>
+
+my $curuser = $FS::CurrentUser::CurrentUser;
+
+# ???
+die "access denied"
+ unless $curuser->access_right([ 'Edit templates', 'Edit global templates' ]);
+
+my $body_etc = '';
+$body_etc = q!onload="document.getElementById('locale').onchange()"!
+ if $cgi->param('locale') eq 'new';
+
+my $modenum = $cgi->param('modenum');
+my $mode = $modenum ? qsearchs('invoice_mode', { modenum => $modenum }) : '';
+
+my %textarea = (type => 'textarea', rows => 10, cols => 40);
+my @fields = (
+ { field => 'modenum', type => 'hidden' },
+ { field => 'agentnum',
+ type => 'select-agent',
+ },
+ { field => 'modename', size=>60, },
+ { type => 'tablebreak-tabs',
+ include_opt_callback => \&menubar_opt_callback,
+ },
+ { field => 'locale', type => 'hidden' },
+ { field => 'notice_name', size=>60, },
+ { field => 'subject', size=>60, },
+ { field => 'lpr', size=>60, },
+
+ { type => 'columnstart', aligned => 1 },
+ { type => 'title', value => '<BR>' },
+ map ( { +{ type => 'justtitle', value => $_ } }
+ 'Notes',
+ 'Footer',
+ 'Summary header',
+ 'Return address',
+ 'Coupon',
+ 'Small footer',
+ 'Top margin',
+ 'Header separation',
+ 'Address separation',
+ 'Text height',
+ 'Coupon height',
+ 'Footer separation',
+ ),
+
+ { type => 'columnnext' },
+ { type => 'title', value => 'LaTeX' },
+ { field => 'latexnotes', %textarea },
+ { field => 'latexfooter', %textarea },
+ { field => 'latexsummary', %textarea },
+ { field => 'latexreturnaddress', %textarea },
+ { field => 'latexcoupon', %textarea },
+ { field => 'latexsmallfooter', %textarea },
+ { field => 'latextopmargin', size => 16 },
+ { field => 'latexheadsep', size => 16 },
+ { field => 'latexaddresssep', size => 16 },
+ { field => 'latextextheight', size => 16 },
+ { field => 'latexextracouponspace', size => 16 },
+ { field => 'latexcouponfootsep', size => 16 },
+ # are these still used?
+ #{ field => 'latexcouponamountenclosedsep', size => 16 },
+ #{ field => 'latexverticalreturnaddress', type => 'checkbox' },
+ #{ field => 'latexcouponaddcompanytoaddress',type => 'checkbox' },
+ # logo -- implement if someone really needs it...
+
+ { type => 'columnnext' },
+ { type => 'title', value => 'HTML' },
+ { field => 'htmlnotes', %textarea }, #htmlarea?
+ { field => 'htmlfooter', %textarea },
+ { field => 'htmlsummary', %textarea },
+ { field => 'htmlreturnaddress', %textarea },
+ # logo
+
+ { type => 'columnend' },
+);
+
+my %labels = (
+ 'confnum' => 'Configuration',
+ 'locale' => 'Locale',
+ 'agentnum' => 'Agent',
+ 'modename' => 'Mode name',
+ 'notice_name' => 'Notice name',
+ 'subject' => 'Email Subject: header',
+ 'lpr' => 'Alternate lpr command',
+
+ map { $_ => '' } (qw(
+ latexnotes
+ latexfooter
+ latexsummary
+ latexreturnaddress
+ latexcoupon
+ latexsmallfooter
+ latextopmargin
+ latexheadsep
+ latexaddresssep
+ latextextheight
+ latexextracouponspace
+ latexcouponfootsep
+ htmlnotes
+ htmlfooter
+ htmlsummary
+ htmlreturnaddress
+ logo_png
+ logo_eps
+ ) ),
+
+);
+
+sub get_invoice_mode { # because we can't quite use agent_virt here
+ my $modenum = shift;
+ qsearchs({
+ 'table' => 'invoice_mode',
+ 'hashref' => { 'modenum' => $modenum },
+ 'extra_sql' => ' AND '.
+ $FS::CurrentUser::CurrentUser->agentnums_sql(
+ 'null_right' => 'Edit global templates',
+ 'viewall_right' => 'Edit global templates' ),
+ });
+};
+
+sub error_callback {
+ my ($cgi, $object) = @_;
+ foreach (qw(modename agentnum)) {
+ $object->set($_, $cgi->param($_));
+ }
+ if ($object->confnum) {
+ return edit_callback(@_);
+ } else {
+ return new_callback(@_);
+ }
+}
+
+sub new_callback {
+ my ($cgi, $object, $fields_arrayref, $opt_hashref) = @_;
+ my $modenum;
+ if ( $cgi->param('locale') =~ /^(\w+)$/ ) {
+ $object->set('locale' => $1);
+ }
+
+ if ( $cgi->param('modenum') =~ /^(\d+)$/ ) {
+ $modenum = $1; # we're adding a locale to an existing mode
+ $object->set('modenum' => $modenum);
+ my $invoice_mode = get_invoice_mode($modenum)
+ or die "invoice mode $modenum not found";
+
+ $object->set('modename', $invoice_mode->modename);
+ $object->set('agentnum', $invoice_mode->agentnum);
+
+ # also, need to select a locale
+ # make a list of available locales
+ my %existing_locales = map { $_->locale }
+ qsearch('invoice_conf', { modenum => $modenum });
+
+ my @locales = grep { !exists($existing_locales{$_}) }
+ FS::Conf->new->config('available-locales');
+ my %labels;
+ foreach (@locales) {
+ my %info = FS::Locales->locale_info($_);
+ $labels{$_} = $info{'label'};
+ }
+ unshift @locales, 'new';
+ $labels{'new'} = 'Select language';
+
+ # insert a field def
+ my $i = 0;
+ $i++ until ( $fields_arrayref->[$i]->{'field'} eq 'locale' );
+ my $locale_field = $fields_arrayref->[$i];
+
+ my $onchange_locale = "document.getElementById('submit').disabled =
+ (this.options[this.selectedIndex].value == 'new');";
+
+ %$locale_field = (
+ field => 'locale',
+ type => 'select',
+ options => \@locales,
+ labels => \%labels,
+ curr_value => 'new',
+ onchange => $onchange_locale,
+ );
+
+ } # otherwise it's a completely new mode, so the locale is default
+
+}
+
+sub edit_callback {
+ # massive false laziness with msg_template UI
+ my ($cgi, $object, $fields_arrayref, $opt_hashref) = @_;
+
+ # a little different here in that we treat the content object
+ # as "primary" (this is edit/invoice_conf.html, etc.)
+ # so all we need from the invoice_mode is its name
+ # (and agent identity)
+ my $modenum = $object->modenum;
+ my $invoice_mode = get_invoice_mode($modenum)
+ or die "invoice mode $modenum not found";
+ $object->set('modename', $invoice_mode->modename);
+ $object->set('agentnum', $invoice_mode->agentnum);
+
+}
+
+sub menubar_opt_callback {
+ my $object = shift;
+ my $modenum = $object->modenum or return;
+ my (@tabs, @options, %labels);
+ my $display_new = 0;
+ my $selected = '';
+ foreach my $l ('', FS::Conf->new->config('available-locales')) {
+ my $invoice_conf =
+ qsearchs('invoice_conf', { modenum => $modenum, locale => $l });
+ if ( $invoice_conf ) {
+ my %info = FS::Locales->locale_info($l) if $l;
+ my $label = $info{'label'} || mt('Default');
+ push @tabs, $label, $invoice_conf->confnum;
+ $selected = $label if $object->locale eq $l;
+ }
+ else {
+ $display_new = 1; # there is at least one unused locale left
+ }
+ }
+ push @tabs, mt('New'), "modenum=$modenum;locale=new" if $display_new;
+ $selected = mt('New') if $object->locale eq 'new';
+ $selected ||= mt('Default');
+ (
+ 'url_base' => $cgi->url() . '?',
+ 'selected' => $selected,
+ 'tabs' => \@tabs
+ );
+}
+
+sub html_init {
+q!
+<STYLE>
+.fstabcontainer th { vertical-align: middle; text-align: center }
+</STYLE>
+!
+}
+
+sub html_table_bottom {
+ my $object = shift;
+ my $locale = '';
+ my $modenum = '';
+
+ if ($object->locale =~ /^(\w+)$/) {
+ $locale = $1;
+ }
+ if ($object->modenum =~ /^(\d+)$/) {
+ $modenum = $1;
+ }
+ my $html;
+ my $show_delete = 1;
+ # don't allow the default locale to be removed unless it's the last one
+ # in the mode
+ $show_delete = 0 if (
+ $locale eq 'new' or
+ $modenum eq '' or
+ ($locale eq '' and
+ FS::invoice_conf->count("modenum = $modenum and locale is not null") > 0
+ )
+ );
+
+ if ( $show_delete ) {
+ # set up a delete link
+ my $confnum = $object->confnum;
+ my $url = $p."misc/delete-invoice_conf.html?$confnum";
+ my $link = qq!<A HREF="javascript:areyousure('$url','Really delete this configuration?')">! .
+ 'Delete this configuration' .
+ '</A>';
+ $html = qq!<TR><TD></TD>
+ <TD STYLE="font-style: italic; font-size: small">$link</TD></TR>
+ <SCRIPT TYPE="text/javascript">
+ function areyousure(url, message) {
+ if (confirm(message)) window.location.href = url;
+ }
+ </SCRIPT>
+ !;
+ }
+ $html;
+}
+
+</%init>
diff --git a/httemplate/edit/process/cable_provider.html b/httemplate/edit/process/cable_provider.html
new file mode 100644
index 000000000..ecffaf692
--- /dev/null
+++ b/httemplate/edit/process/cable_provider.html
@@ -0,0 +1,10 @@
+<& elements/process.html,
+ 'table' => 'cable_provider',
+ 'viewall_dir' => 'browse',
+&>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
+
+</%init>
diff --git a/httemplate/edit/process/invoice_conf.html b/httemplate/edit/process/invoice_conf.html
new file mode 100644
index 000000000..1d45e126f
--- /dev/null
+++ b/httemplate/edit/process/invoice_conf.html
@@ -0,0 +1,21 @@
+<& elements/process.html,
+ 'table' => 'invoice_conf',
+ 'viewall_dir' => 'browse',
+ 'fields' => [ FS::invoice_conf->fields, 'modename', 'agentnum' ],
+ 'precheck_callback' => \&precheck_callback,
+&>
+<%init>
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right(['Edit templates','Edit global templates']);
+
+sub precheck_callback {
+ my $cgi = shift;
+ $cgi->param('locale') =~ /^(\w*)$/;
+ my $locale = $1;
+ return mt('Language required') if $locale eq 'new'; # the user didn't choose
+ die "unknown locale $locale" if ( $locale and
+ !FS::Locales->locale_info($locale) );
+}
+# invoice_conf itself knows to create/update invoice_mode if necessary,
+# so nothing special here
+</%init>
diff --git a/httemplate/elements/columnstart.html b/httemplate/elements/columnstart.html
index be37d817d..1ffbcb9e8 100644
--- a/httemplate/elements/columnstart.html
+++ b/httemplate/elements/columnstart.html
@@ -1,6 +1,81 @@
+<%doc>
+<table>
+ <& /elements/columnstart.html &>
+ <tr> ... </tr>
+ <tr> ... </tr>
+ <& /elements/columnnext.html &>
+ ...
+ <& /elements/columnend.html &>
+</table>
+
+Pass 'aligned' => 1 to have corresponding rows in the columns line up.
+</%doc>
+% my $id = sprintf('table%08d', rand(100000000));
<TR>
<TD CLASS="background" COLSPAN=99>
- <TABLE BORDER=0 CELLSPACING=0 CELLPADDING=0>
+ <TABLE BORDER=0 CELLSPACING=0 CELLPADDING=0 id="<%$id%>">
<TR>
<TD VALIGN="top">
<TABLE BGCOLOR="#cccccc" BORDER=0 CELLSPACING=0>
+% if ( $aligned ) {
+%# Instead of changing all the tr-* elements to sometimes output table
+%# cells without wrapping them in a row, we're just going to completely
+%# rebuild the table on the client side.
+<script type="text/javascript">
+<&| onload.js &>
+ var table = document.getElementById('<%$id%>'); // has one row, always
+ var rows = []; // row contents, each containing
+ var n_rows = []; // rows in each subtable
+ var n_cols = []; // cols in each subtable
+ var total_rows = 0; // max(n_rows)
+ for(var i=0; i < table.rows[0].cells.length; i++) {
+ // these are cells created by columnstart/columnnext
+ // each contains a table, and nothing else
+ var subtable = table.rows[0].cells[i].children[0];
+ n_rows[i] = subtable.rows.length;
+ if ( total_rows < n_rows[i] ) {
+ total_rows = n_rows[i];
+ }
+ n_cols[i] = 0;
+ var subrows = []; // the rows of this table
+ for(var j=0; j < n_rows[i]; j++) {
+ // these are the actual tr-* rows within the table, and
+ // can contain multiple cells
+ subrows[j] = [];
+ var tr = subtable.rows[j];
+ if ( n_cols[i] < tr.cells.length ) {
+ n_cols[i] = tr.cells.length;
+ }
+ for(var k=0; k < tr.cells.length; k++) {
+ subrows[j][k] = tr.cells[k];
+ }
+ } // for(j)
+ rows[i] = subrows;
+ } // for(i)
+ var new_table = document.createElement('TABLE');
+ for (var j = 0; j < total_rows; j++) {
+ var tr = document.createElement('TR');
+ for (var i = 0; i < rows.length; i++) { // subtables
+ var k = 0; // subrow position
+ if ( j < n_rows[i] ) { // then subtable i has this row
+ for (k = 0; k < rows[i][j].length; k++) { // cells
+ tr.appendChild(rows[i][j][k]);
+ }
+ } // else k is just 0
+ if ( k < n_cols[i] ) { // then we need a spacer
+ var spacer = document.createElement('TD');
+ spacer.setAttribute('colspan', n_cols[i] - k);
+ tr.appendChild(spacer);
+ }
+ } // for(i); subtables
+ // tr is complete
+ new_table.appendChild(tr);
+ } // for(j); rows
+ table.parentNode.insertBefore( new_table, table );
+ table.parentNode.removeChild(table);
+</&>
+</script>
+% } # if $aligned
+<%args>
+$aligned => 0
+</%args>
diff --git a/httemplate/elements/menu.html b/httemplate/elements/menu.html
index 8cbbd1742..8cb967518 100644
--- a/httemplate/elements/menu.html
+++ b/httemplate/elements/menu.html
@@ -197,6 +197,10 @@ foreach my $svcdb ( FS::part_svc->svc_tables() ) {
[ $fsurl. 'search/report_svc_phone_usage.html',
'Total usage (minutes, and amount billed) for the specified time period, per phone number.',
];
+ $report_svc{"${name} by state"} =
+ [ $fsurl. 'search/phone_state.html',
+ 'Current or historical phone services broken down by state.',
+ ];
}
@@ -517,6 +521,7 @@ tie my %config_radius, 'Tie::IxHash',
;
tie my %config_cable, 'Tie::IxHash',
+ 'Cable providers' => [ $fsurl.'browse/cable_provider.html', '' ],
'Cable modem models' => [ $fsurl.'browse/cable_model.html', '' ],
;
@@ -600,6 +605,7 @@ $config_billing{'Billing events'} = [ $fsurl.'browse/part_event.html', 'Billing
|| $curuser->access_right('Edit global billing events');
if ( $curuser->access_right('Configuration') ) {
#$config_billing{'Invoice events'} = [ $fsurl.'browse/part_bill_event.cgi', 'Deprecated, old-style actions for overdue invoices' ];
+ $config_billing{'Invoice configurations'} = [ $fsurl.'browse/invoice_conf.html', 'Adjust invoice settings for special-purpose notices' ];
$config_billing{'Invoice templates'} = [ $fsurl.'browse/invoice_template.html', 'Edit templates for HTML, plaintext and typeset invoices' ];
$config_billing{'separator'} = ''; #its a separator!
$config_billing{'Prepaid cards'} = [ $fsurl.'search/prepay_credit.html', 'View outstanding cards, generate new cards' ];
diff --git a/httemplate/elements/select-cable_provider.html b/httemplate/elements/select-cable_provider.html
new file mode 100644
index 000000000..9530b78c0
--- /dev/null
+++ b/httemplate/elements/select-cable_provider.html
@@ -0,0 +1,7 @@
+<% include( '/elements/select-table.html',
+ 'table' => 'cable_provider',
+ 'name_col' => 'provider',
+ 'empty_label' => 'Select provider',
+ @_,
+ )
+%>
diff --git a/httemplate/elements/tr-select-cable_provider.html b/httemplate/elements/tr-select-cable_provider.html
new file mode 100644
index 000000000..abb8564dc
--- /dev/null
+++ b/httemplate/elements/tr-select-cable_provider.html
@@ -0,0 +1,12 @@
+% #if ( scalar(@domains) < 2 ) {
+% #} else {
+ <TR>
+ <TD ALIGN="right"><% $opt{'label'} || 'Provider' %></TD>
+ <TD>
+ <% include( '/elements/select-cable_provider.html', %opt) %>
+ </TD>
+ </TR>
+% #}
+<%init>
+ my %opt = @_;
+</%init>
diff --git a/httemplate/elements/tr-select-invoice_mode.html b/httemplate/elements/tr-select-invoice_mode.html
new file mode 100644
index 000000000..3dccdccc2
--- /dev/null
+++ b/httemplate/elements/tr-select-invoice_mode.html
@@ -0,0 +1,10 @@
+<& tr-select-table.html,
+ 'label' => 'Invoice mode',
+ 'table' => 'invoice_mode',
+ 'field' => 'modenum',
+ 'name_col' => 'modename',
+ 'agent_virt' => 1,
+ 'agent_null' => 1,
+ 'empty_label' => '(none)',
+ @_
+&>
diff --git a/httemplate/misc/delete-invoice_conf.html b/httemplate/misc/delete-invoice_conf.html
new file mode 100644
index 000000000..6cc6ddc95
--- /dev/null
+++ b/httemplate/misc/delete-invoice_conf.html
@@ -0,0 +1,19 @@
+<%init>
+my $curuser = $FS::CurrentUser::CurrentUser;
+die "access denied"
+ unless $curuser->access_right(['Edit templates', 'Edit global templates']);
+
+my ($query) = $cgi->keywords;
+$query =~ /^(\d+)$/ or die "bad confnum";
+my $invoice_conf = FS::invoice_conf->by_key($1)
+ or die "couldn't find invoice_conf #$1";
+if ( !$curuser->access_right('Edit global templates') ) {
+ my $agentnum = FS::invoice_mode->by_key($invoice_conf->modenum)->agentnum;
+ die "access denied"
+ unless $curuser->agentnums_href->{$agentnum};
+}
+
+my $error = $invoice_conf->delete; # may also delete the invoice_mode
+my $url = $p.'browse/invoice_conf.html';
+</%init>
+<% $cgi->redirect($url) %>
diff --git a/httemplate/misc/email-invoice.cgi b/httemplate/misc/email-invoice.cgi
index 269722f67..b24e0420f 100755
--- a/httemplate/misc/email-invoice.cgi
+++ b/httemplate/misc/email-invoice.cgi
@@ -12,7 +12,7 @@ my $invnum = $3;
my $cust_bill = qsearchs('cust_bill',{'invnum'=>$invnum});
die "Can't find invoice!\n" unless $cust_bill;
-$cust_bill->email($template);
+$cust_bill->email({ 'template' => $template });
my $custnum = $cust_bill->getfield('custnum');
diff --git a/httemplate/misc/fax-invoice.cgi b/httemplate/misc/fax-invoice.cgi
index 2591fceb8..f72fc7eaf 100755
--- a/httemplate/misc/fax-invoice.cgi
+++ b/httemplate/misc/fax-invoice.cgi
@@ -12,7 +12,7 @@ my $invnum = $3;
my $cust_bill = qsearchs('cust_bill',{'invnum'=>$invnum});
die "Can't find invoice!\n" unless $cust_bill;
-$cust_bill->fax_invoice($template);
+$cust_bill->fax_invoice({ 'template' => $template });
my $custnum = $cust_bill->getfield('custnum');
diff --git a/httemplate/misc/print-invoice.cgi b/httemplate/misc/print-invoice.cgi
index aeef68795..5ce6e76df 100755
--- a/httemplate/misc/print-invoice.cgi
+++ b/httemplate/misc/print-invoice.cgi
@@ -12,7 +12,7 @@ my $invnum = $3;
my $cust_bill = qsearchs('cust_bill',{'invnum'=>$invnum});
die "Can't find invoice!\n" unless $cust_bill;
-$cust_bill->print($template);
+$cust_bill->print({ 'template' => $template});
my $custnum = $cust_bill->getfield('custnum');
diff --git a/httemplate/misc/send-invoice.cgi b/httemplate/misc/send-invoice.cgi
index 32dfe276d..08dd0e01c 100644
--- a/httemplate/misc/send-invoice.cgi
+++ b/httemplate/misc/send-invoice.cgi
@@ -13,6 +13,10 @@ my $invnum = $cgi->param('invnum');
my $template = $cgi->param('template');
my $notice_name = $cgi->param('notice_name') if $cgi->param('notice_name');
my $method = $cgi->param('method');
+my $mode;
+if ( $cgi->param('mode') =~ /^(\d+)$/ ) {
+ $mode = $1;
+}
$method .= '_invoice' if $method eq 'fax'; #!
@@ -21,6 +25,7 @@ die "unknown method $method" unless $method{$method};
my $cust_bill = qsearchs('cust_bill',{'invnum'=>$invnum});
die "Can't find invoice!\n" unless $cust_bill;
+$cust_bill->set('mode' => $mode) if $mode;
$cust_bill->$method({ 'template' => $template,
'notice_name' => $notice_name,
});
diff --git a/httemplate/misc/xmlhttp-cust_main-duplicates.html b/httemplate/misc/xmlhttp-cust_main-duplicates.html
index 7ee00af66..7cd463371 100644
--- a/httemplate/misc/xmlhttp-cust_main-duplicates.html
+++ b/httemplate/misc/xmlhttp-cust_main-duplicates.html
@@ -50,7 +50,9 @@ my $conf = new FS::Conf;
my $sub = $cgi->param('sub');
my $hashref = decode_json($cgi->param('arg'));
-my @cust_main = qsearch('cust_main', $hashref);
+my $search = FS::cust_main->search($hashref);
+#warn Dumper($search);
+my @cust_main = qsearch( $search );
my $set_to_customer = <<EOF;
var custnum_array = document.getElementsByName('dup_custnum');
diff --git a/httemplate/search/phone_state.html b/httemplate/search/phone_state.html
new file mode 100644
index 000000000..67965b702
--- /dev/null
+++ b/httemplate/search/phone_state.html
@@ -0,0 +1,167 @@
+<& elements/search.html,
+ 'title' => $title,
+ 'name' => 'states',
+ 'query' => $query,
+ 'count_query' => $count_query,
+ 'count_addl' => $count_addl,
+ 'header' => [ 'State', # if we add more group fields, change this
+ 'Count',
+ 'Phone numbers'
+ ],
+ 'fields' => [ 'state',
+ 'num_svcnums',
+ $detail_sub
+ ],
+ 'html_init' => include('.head', $time),
+&>
+<%def .head>
+% my $time = shift;
+<FORM STYLE="display:inline" ACTION=<% $cgi->url %> METHOD="GET">
+Active phone services as of <& /elements/input-date-field.html, {
+ 'name' => 'date',
+ 'value' => $time,
+ 'format' => FS::Conf->new->config('date_format') || '%m/%d/%Y'
+} &>
+<INPUT TYPE="hidden" NAME="order_by" VALUE="<% $cgi->param('order_by') %>">
+<INPUT TYPE="submit" VALUE="Refresh">
+</FORM>
+<BR>
+<BR>
+</%def>
+<%init>
+# svc_phone-specific for now; may change later
+my $curuser = $FS::CurrentUser::CurrentUser;
+die "access denied" unless $curuser->access_right('Services: Phone numbers');
+my $title = 'Phone services by state';
+
+my $time = time;
+if ( $cgi->param('date') ) {
+ $time = parse_datetime($cgi->param('date'));
+ $title .= time2str(' (%b %o, %Y)', $time);
+}
+
+my @tables = qw(svc_phone cust_svc cust_pkg cust_location cust_main);
+my @pkeys = qw(svcnum svcnum pkgnum locationnum custnum);
+my @h_tables = map "h_$_", @tables;
+
+my $addl_from = '';
+my @where;
+
+for(my $i = 0; $i < scalar(@tables); $i++) {
+ my $last_table = $h_tables[$i-1];
+ my $pkey = $pkeys[$i];
+ my $table = $tables[$i];
+ my $h_table = $h_tables[$i];
+ # alias the preceding table, and join to a subquery that finds the most
+ # recent change to $table.$pkey before $time
+ my $alias = $h_table;
+ my $inside = '';
+ if ( $i > 0 ) {
+ $alias = "t$i";
+ $inside = " AS $alias";
+ }
+ $inside .= "
+ JOIN
+ (SELECT $pkey AS num, MAX(history_date) AS history_date
+ FROM $h_table
+ WHERE history_date <= $time AND
+ history_action IN ('insert', 'replace_new')
+ GROUP BY $pkey
+ ) AS mostrecent_$table
+ ON ($alias.$pkey = mostrecent_$table.num AND
+ $alias.history_date = mostrecent_$table.history_date AND
+ $alias.history_action IN ('insert', 'replace_new')
+ )
+ LEFT JOIN
+ (SELECT $pkey AS num, MAX(history_date) AS history_date, 1 AS deleted
+ FROM $h_table
+ WHERE history_date <= $time AND
+ history_action = 'delete'
+ GROUP BY $pkey
+ ) AS deleted_$table
+ ON (mostrecent_$table.num = deleted_$table.num AND
+ mostrecent_$table.history_date < deleted_$table.history_date
+ )
+";
+ # join to the preceding table if there is one, and filter out
+ # deleted records
+ if ( $i > 0 ) {
+ # special case to make pre-3.x data work; remove this later
+ if ( $table eq 'cust_main' ) {
+ $last_table = 'h_cust_pkg';
+ }
+ $addl_from .= "
+ LEFT JOIN ( $h_table $inside ) AS $h_table
+ ON ($h_table.$pkey = $last_table.$pkey)";
+ push @where, "$h_table.deleted IS NULL";
+ } else {
+ $addl_from .= $inside;
+ push @where, "deleted_$table.deleted IS NULL";
+ }
+}
+
+# so that we know which services are still active
+$addl_from .= "
+ LEFT JOIN svc_phone ON (h_svc_phone.svcnum = svc_phone.svcnum AND
+ h_svc_phone.phonenum = svc_phone.phonenum)";
+
+#warn "\n\nJOIN EXPRESSION:\n$addl_from\n\n";
+
+push @where, $curuser->agentnums_sql(
+ 'table' => 'h_cust_main',
+ 'null_right' => 'View/link unlinked services'
+);
+my $where = " WHERE ".join(' AND ', map "($_)", @where);
+
+# for pre-3.x data
+my $group_field = 'COALESCE(h_cust_location.state, h_cust_main.ship_state, h_cust_main.state)';
+
+my @select = (
+ "$group_field AS state",
+ 'count(DISTINCT h_svc_phone.svcnum) AS num_svcnums',
+ # don't DISTINCT these (it reorders them)
+ "array_to_string(array_agg(h_svc_phone.phonenum), ',') AS all_phonenums",
+ "array_to_string(array_agg(h_svc_phone.svcnum), ',') AS all_svcnums",
+ "array_to_string(array_agg(svc_phone.svcnum), ',') AS active_svcnums",
+);
+
+my $query = {
+ 'select' => join(',', @select),
+ 'table' => 'h_svc_phone',
+ 'addl_from' => $addl_from,
+ 'extra_sql' => " $where GROUP BY $group_field",
+};
+
+# DISTINCT on these because of cross-producting effects when a cust_pkg
+# record (usually) was replaced more than once within one second.
+my $count_query =
+ "SELECT COUNT(DISTINCT $group_field), COUNT(DISTINCT h_svc_phone.svcnum) ".
+ "FROM h_svc_phone $addl_from $where";
+my $count_addl = [ '%d phone services' ];
+
+my $detail_sub = sub {
+ my $rec = shift;
+ warn Dumper $rec;
+ my @svcnums = split(',', $rec->all_svcnums);
+ my @phonenums = split(',', $rec->all_phonenums);
+ # identifies services that still exist with the same svcnum+phonenum
+ my %active = map { $_ => 1 } split(',', $rec->active_svcnums);
+ # make a single column of phonenums
+ my @return;
+ my %seen;
+ while (my $svcnum = shift @svcnums) {
+ my $phonenum = shift @phonenums;
+ next if $seen{$svcnum};
+ $seen{$svcnum} = 1;
+ my $link = $active{$svcnum} ?
+ $p.'view/svc_phone.cgi?'.$svcnum :
+ '';
+ push @return, [ { data => $phonenum,
+ link => $link,
+ data_style => ($active{$svcnum} ? '' : 'i')
+ } ];
+ }
+ \@return;
+};
+
+</%init>
diff --git a/httemplate/view/cust_bill.cgi b/httemplate/view/cust_bill.cgi
index 95ce60b1d..4822ab718 100755
--- a/httemplate/view/cust_bill.cgi
+++ b/httemplate/view/cust_bill.cgi
@@ -104,12 +104,35 @@
% my $br = 0;
% if ( $cust_bill->num_cust_event ) { $br++;
<A HREF="<%$p%>search/cust_event.html?invnum=<% $cust_bill->invnum %>">( <% mt('View invoice events') |h %> )</A>
-% }
+% }
% if ( $cust_bill->num_cust_bill_event ) { $br++;
<A HREF="<%$p%>search/cust_bill_event.cgi?invnum=<% $cust_bill->invnum %>">( <% mt('View deprecated, old-style invoice events') |h %> )</A>
% }
+% my @modes = grep {! $_->disabled}
+% $cust_bill->cust_main->agent->invoice_modes;
+% if ( @modes ) {
+( <% mt('View as:') %>
+<FORM STYLE="display:inline" ACTION="<% $cgi->url %>" METHOD="GET">
+<INPUT NAME="invnum" VALUE="<% $invnum %>" TYPE="hidden">
+<& /elements/select-table.html,
+ table => 'invoice_mode',
+ field => 'mode',
+ curr_value => scalar($cgi->param('mode')),
+ records => \@modes,
+ name_col => 'modename',
+ onchange => 'change_invoice_mode',
+ empty_label => '(default)',
+&> )
+<SCRIPT TYPE="text/javascript">
+function change_invoice_mode(obj) {
+ obj.form.submit();
+}
+</SCRIPT>
+% $br++;
+% }
+
<% $br ? '<BR><BR>' : '' %>
% if ( $conf->exists('invoice_html') ) {
@@ -126,7 +149,9 @@ my $curuser = $FS::CurrentUser::CurrentUser;
die "access denied"
unless $curuser->access_right('View invoices');
-my( $invnum, $template, $notice_name );
+my $conf = FS::Conf->new;
+
+my( $invnum, $mode, $template, $notice_name );
my($query) = $cgi->keywords;
if ( $query =~ /^((.+)-)?(\d+)$/ ) {
$template = $2;
@@ -136,10 +161,9 @@ if ( $query =~ /^((.+)-)?(\d+)$/ ) {
$invnum = $cgi->param('invnum');
$template = $cgi->param('template');
$notice_name = $cgi->param('notice_name');
+ $mode = $cgi->param('mode');
}
-my $conf = new FS::Conf;
-
my %opt = (
'unsquelch_cdr' => $conf->exists('voip-cdr_email'),
'template' => $template,
@@ -163,10 +187,13 @@ my $cust_bill = qsearchs({
});
die "Invoice #$invnum not found!" unless $cust_bill;
+$cust_bill->set('mode' => $mode);
+
my $custnum = $cust_bill->custnum;
my $display_custnum = $cust_bill->cust_main->display_custnum;
my $link = "invnum=$invnum";
+$link .= ';mode=' . $mode if $mode;
$link .= ';template='. uri_escape($template) if $template;
$link .= ';notice_name='. $notice_name if $notice_name;
diff --git a/httemplate/view/cust_main.cgi b/httemplate/view/cust_main.cgi
index 430c50c5f..391988190 100755
--- a/httemplate/view/cust_main.cgi
+++ b/httemplate/view/cust_main.cgi
@@ -147,7 +147,6 @@ function areyousure(href, message) {
% if ( $br ) {
<BR><BR>
% }
-</%doc>
%my $signupurl = $conf->config('signupurl');
%if ( $signupurl ) {
diff --git a/httemplate/view/cust_statement.html b/httemplate/view/cust_statement.html
index 3e1345ed5..5d37b3167 100755
--- a/httemplate/view/cust_statement.html
+++ b/httemplate/view/cust_statement.html
@@ -35,10 +35,10 @@
% if ( $conf->exists('invoice_html') ) {
- <% join('', $cust_statement->print_html('', $templatename) ) %>
+ <% join('', $cust_statement->print_html('template' => $templatename) ) %>
% } else {
- <PRE><% join('', $cust_statement->print_text('', $templatename) ) %></PRE>
+ <PRE><% join('', $cust_statement->print_text('template' => $templatename) ) %></PRE>
% }
<% include('/elements/footer.html') %>
diff --git a/httemplate/view/elements/cust_bill-typeset b/httemplate/view/elements/cust_bill-typeset
index 00f503fbb..778e538d1 100644
--- a/httemplate/view/elements/cust_bill-typeset
+++ b/httemplate/view/elements/cust_bill-typeset
@@ -6,7 +6,7 @@ die "access denied"
my $type = shift;
-my( $invnum, $template, $notice_name );
+my( $invnum, $mode, $template, $notice_name );
my($query) = $cgi->keywords;
if ( $query =~ /^((.+)-)?(\d+)(.pdf)?$/ ) { #probably not necessary anymore?
$template = $2;
@@ -16,7 +16,8 @@ if ( $query =~ /^((.+)-)?(\d+)(.pdf)?$/ ) { #probably not necessary anymore?
$invnum = $cgi->param('invnum');
$invnum =~ s/\.pdf//i; #probably not necessary anymore
$template = $cgi->param('template');
- $notice_name = ( $cgi->param('notice_name') || 'Invoice' );
+ $notice_name = $cgi->param('notice_name');
+ $mode = $cgi->param('mode');
}
my $conf = new FS::Conf;
@@ -36,6 +37,8 @@ my $cust_bill = qsearchs({
});
die "Invoice #$invnum not found!" unless $cust_bill;
+$cust_bill->set(mode => $mode);
+
my $method = "print_$type";
my $content = $cust_bill->$method(\%opt);
diff --git a/ng_selfservice/.freeside.class.php.swp b/ng_selfservice/.freeside.class.php.swp
deleted file mode 100644
index 5c3952439..000000000
--- a/ng_selfservice/.freeside.class.php.swp
+++ /dev/null
Binary files differ
diff --git a/ng_selfservice/.index.php.swp b/ng_selfservice/.index.php.swp
deleted file mode 100644
index 50c9cfbc5..000000000
--- a/ng_selfservice/.index.php.swp
+++ /dev/null
Binary files differ
diff --git a/ng_selfservice/.logout.php.swp b/ng_selfservice/.logout.php.swp
deleted file mode 100644
index ec27faaed..000000000
--- a/ng_selfservice/.logout.php.swp
+++ /dev/null
Binary files differ
diff --git a/ng_selfservice/.main.php.swp b/ng_selfservice/.main.php.swp
deleted file mode 100644
index cc5562690..000000000
--- a/ng_selfservice/.main.php.swp
+++ /dev/null
Binary files differ
diff --git a/ng_selfservice/.password.php.swp b/ng_selfservice/.password.php.swp
deleted file mode 100644
index e1e968f1f..000000000
--- a/ng_selfservice/.password.php.swp
+++ /dev/null
Binary files differ
diff --git a/ng_selfservice/.payment.php.swp b/ng_selfservice/.payment.php.swp
deleted file mode 100644
index 2b705a31f..000000000
--- a/ng_selfservice/.payment.php.swp
+++ /dev/null
Binary files differ
diff --git a/ng_selfservice/.payment_ach.php.swp b/ng_selfservice/.payment_ach.php.swp
deleted file mode 100644
index 1a87a2d34..000000000
--- a/ng_selfservice/.payment_ach.php.swp
+++ /dev/null
Binary files differ
diff --git a/ng_selfservice/.payment_cc.php.swp b/ng_selfservice/.payment_cc.php.swp
deleted file mode 100644
index 369d104d1..000000000
--- a/ng_selfservice/.payment_cc.php.swp
+++ /dev/null
Binary files differ
diff --git a/ng_selfservice/.payment_paypal.php.swp b/ng_selfservice/.payment_paypal.php.swp
deleted file mode 100644
index 3abff2f4d..000000000
--- a/ng_selfservice/.payment_paypal.php.swp
+++ /dev/null
Binary files differ
diff --git a/ng_selfservice/.payment_webpay.php.swp b/ng_selfservice/.payment_webpay.php.swp
deleted file mode 100644
index 6ef3df9d9..000000000
--- a/ng_selfservice/.payment_webpay.php.swp
+++ /dev/null
Binary files differ
diff --git a/ng_selfservice/.personal.php.swp b/ng_selfservice/.personal.php.swp
deleted file mode 100644
index f5e8c23c1..000000000
--- a/ng_selfservice/.personal.php.swp
+++ /dev/null
Binary files differ
diff --git a/ng_selfservice/.process_login.php.swp b/ng_selfservice/.process_login.php.swp
deleted file mode 100644
index c530f11d6..000000000
--- a/ng_selfservice/.process_login.php.swp
+++ /dev/null
Binary files differ
diff --git a/ng_selfservice/.process_ticket_create.php.swp b/ng_selfservice/.process_ticket_create.php.swp
deleted file mode 100644
index c286792cd..000000000
--- a/ng_selfservice/.process_ticket_create.php.swp
+++ /dev/null
Binary files differ
diff --git a/ng_selfservice/.services.php.swp b/ng_selfservice/.services.php.swp
deleted file mode 100644
index e063e406b..000000000
--- a/ng_selfservice/.services.php.swp
+++ /dev/null
Binary files differ
diff --git a/ng_selfservice/.services_new.php.swp b/ng_selfservice/.services_new.php.swp
deleted file mode 100644
index 8d0c657af..000000000
--- a/ng_selfservice/.services_new.php.swp
+++ /dev/null
Binary files differ
diff --git a/ng_selfservice/.ticket.php.swp b/ng_selfservice/.ticket.php.swp
deleted file mode 100644
index e9b25032b..000000000
--- a/ng_selfservice/.ticket.php.swp
+++ /dev/null
Binary files differ
diff --git a/ng_selfservice/.ticket_create.php.swp b/ng_selfservice/.ticket_create.php.swp
deleted file mode 100644
index 65b00fe06..000000000
--- a/ng_selfservice/.ticket_create.php.swp
+++ /dev/null
Binary files differ
diff --git a/ng_selfservice/.tickets.php.swp b/ng_selfservice/.tickets.php.swp
deleted file mode 100644
index 7b4d67b48..000000000
--- a/ng_selfservice/.tickets.php.swp
+++ /dev/null
Binary files differ
diff --git a/ng_selfservice/.tickets_resolved.php.swp b/ng_selfservice/.tickets_resolved.php.swp
deleted file mode 100644
index 1b3c634f0..000000000
--- a/ng_selfservice/.tickets_resolved.php.swp
+++ /dev/null
Binary files differ
diff --git a/ng_selfservice/.usage.php.swp b/ng_selfservice/.usage.php.swp
deleted file mode 100644
index 61fd4fa4d..000000000
--- a/ng_selfservice/.usage.php.swp
+++ /dev/null
Binary files differ
diff --git a/ng_selfservice/.usage_cdr.php.swp b/ng_selfservice/.usage_cdr.php.swp
deleted file mode 100644
index 83c270a83..000000000
--- a/ng_selfservice/.usage_cdr.php.swp
+++ /dev/null
Binary files differ
diff --git a/ng_selfservice/.usage_data.php.swp b/ng_selfservice/.usage_data.php.swp
deleted file mode 100644
index e5a9272b0..000000000
--- a/ng_selfservice/.usage_data.php.swp
+++ /dev/null
Binary files differ
diff --git a/ng_selfservice/elements/.card.php.swp b/ng_selfservice/elements/.card.php.swp
deleted file mode 100644
index 15d30cefd..000000000
--- a/ng_selfservice/elements/.card.php.swp
+++ /dev/null
Binary files differ
diff --git a/ng_selfservice/elements/.check.php.swp b/ng_selfservice/elements/.check.php.swp
deleted file mode 100644
index fe08303ff..000000000
--- a/ng_selfservice/elements/.check.php.swp
+++ /dev/null
Binary files differ
diff --git a/ng_selfservice/elements/.error.php.swp b/ng_selfservice/elements/.error.php.swp
deleted file mode 100644
index 1a6eb2833..000000000
--- a/ng_selfservice/elements/.error.php.swp
+++ /dev/null
Binary files differ
diff --git a/ng_selfservice/elements/.header.php.swp b/ng_selfservice/elements/.header.php.swp
deleted file mode 100644
index 237177069..000000000
--- a/ng_selfservice/elements/.header.php.swp
+++ /dev/null
Binary files differ
diff --git a/ng_selfservice/elements/.menu.php.swp b/ng_selfservice/elements/.menu.php.swp
deleted file mode 100644
index 0c29ff942..000000000
--- a/ng_selfservice/elements/.menu.php.swp
+++ /dev/null
Binary files differ
diff --git a/ng_selfservice/elements/.menu_footer.php.swp b/ng_selfservice/elements/.menu_footer.php.swp
deleted file mode 100644
index 4bd2b30a8..000000000
--- a/ng_selfservice/elements/.menu_footer.php.swp
+++ /dev/null
Binary files differ
diff --git a/ng_selfservice/elements/.session.php.swp b/ng_selfservice/elements/.session.php.swp
deleted file mode 100644
index ddd013701..000000000
--- a/ng_selfservice/elements/.session.php.swp
+++ /dev/null
Binary files differ
diff --git a/ng_selfservice/elements/.ticketlist.php.swp b/ng_selfservice/elements/.ticketlist.php.swp
deleted file mode 100644
index db3d0fe4a..000000000
--- a/ng_selfservice/elements/.ticketlist.php.swp
+++ /dev/null
Binary files differ
diff --git a/ng_selfservice/js/.menu.js.swp b/ng_selfservice/js/.menu.js.swp
deleted file mode 100644
index 8df94a9c4..000000000
--- a/ng_selfservice/js/.menu.js.swp
+++ /dev/null
Binary files differ
diff --git a/rt/lib/RT/Interface/Web.pm b/rt/lib/RT/Interface/Web.pm
index 4a6bfda88..2dc16e3f3 100644
--- a/rt/lib/RT/Interface/Web.pm
+++ b/rt/lib/RT/Interface/Web.pm
@@ -2070,7 +2070,6 @@ sub _ProcessUpdateMessageRecipients {
if (grep $_ eq 'Requestor' || $_ eq 'Requestors', @{ $args{ARGSRef}->{'SkipNotification'} || [] }) {
push @txn_squelch, map $_->address, Email::Address->parse( $message_args->{Requestor} );
push @txn_squelch, $args{TicketObj}->Requestors->MemberEmailAddresses;
-
}
push @txn_squelch, @{$args{ARGSRef}{SquelchMailTo}} if $args{ARGSRef}{SquelchMailTo};
@@ -2092,6 +2091,39 @@ sub _ProcessUpdateMessageRecipients {
}
}
+sub ProcessAttachments {
+ my %args = (
+ ARGSRef => {},
+ @_
+ );
+
+ my $ARGSRef = $args{ARGSRef} || {};
+ # deal with deleting uploaded attachments
+ foreach my $key ( keys %$ARGSRef ) {
+ if ( $key =~ m/^DeleteAttach-(.+)$/ ) {
+ delete $session{'Attachments'}{$1};
+ }
+ $session{'Attachments'} = { %{ $session{'Attachments'} || {} } };
+ }
+
+ # store the uploaded attachment in session
+ if ( defined $ARGSRef->{'Attach'} && length $ARGSRef->{'Attach'} )
+ { # attachment?
+ my $attachment = MakeMIMEEntity( AttachmentFieldName => 'Attach' );
+
+ my $file_path = Encode::decode_utf8("$ARGSRef->{'Attach'}");
+ $session{'Attachments'} =
+ { %{ $session{'Attachments'} || {} }, $file_path => $attachment, };
+ }
+
+ # delete temporary storage entry to make WebUI clean
+ unless ( keys %{ $session{'Attachments'} } and $ARGSRef->{'UpdateAttach'} )
+ {
+ delete $session{'Attachments'};
+ }
+}
+
+
=head2 MakeMIMEEntity PARAMHASH
Takes a paramhash Subject, Body and AttachmentFieldName.
@@ -2174,37 +2206,6 @@ sub MakeMIMEEntity {
}
-sub ProcessAttachments {
- my %args = (
- ARGSRef => {},
- @_
- );
-
- my $ARGSRef = $args{ARGSRef} || {};
- # deal with deleting uploaded attachments
- foreach my $key ( keys %$ARGSRef ) {
- if ( $key =~ m/^DeleteAttach-(.+)$/ ) {
- delete $session{'Attachments'}{$1};
- }
- $session{'Attachments'} = { %{ $session{'Attachments'} || {} } };
- }
-
- # store the uploaded attachment in session
- if ( defined $ARGSRef->{'Attach'} && length $ARGSRef->{'Attach'} )
- { # attachment?
- my $attachment = MakeMIMEEntity( AttachmentFieldName => 'Attach' );
-
- my $file_path = Encode::decode_utf8("$ARGSRef->{'Attach'}");
- $session{'Attachments'} =
- { %{ $session{'Attachments'} || {} }, $file_path => $attachment, };
- }
-
- # delete temporary storage entry to make WebUI clean
- unless ( keys %{ $session{'Attachments'} } and $ARGSRef->{'UpdateAttach'} )
- {
- delete $session{'Attachments'};
- }
-}
=head2 ParseDateToISO
diff --git a/rt/lib/RT/URI/freeside/Internal.pm b/rt/lib/RT/URI/freeside/Internal.pm
index b0962860d..d1479b5f9 100644
--- a/rt/lib/RT/URI/freeside/Internal.pm
+++ b/rt/lib/RT/URI/freeside/Internal.pm
@@ -152,6 +152,9 @@ sub AsStringLong {
if ( $table eq 'cust_main' ) {
my $rec = $self->_FreesideGetRecord();
+ if (!$rec) {
+ return '<I>Customer #'.$self->{'fspkey'}.' (not found)</I>';
+ }
return '<A HREF="' . $self->HREF . '">' .
small_custview( $rec->{'_object'},
scalar(FS::Conf->new->config('countrydefault')),
@@ -192,21 +195,37 @@ sub CustomerResolver {
}
elsif ( $self->{fstable} eq 'cust_svc' ) {
my $rec = $self->_FreesideGetRecord();
- return if !$rec;
- my $cust_pkg = $rec->{'_object'}->cust_pkg;
- if ( $cust_pkg ) {
- my $URI = RT::URI->new($self->CurrentUser);
- $URI->FromURI('freeside://freeside/cust_main/'.$cust_pkg->custnum);
- return $URI->Resolver;
+ if ($rec) {
+ my $cust_pkg = $rec->{'_object'}->cust_pkg;
+ if ( $cust_pkg ) {
+ my $URI = RT::URI->new($self->CurrentUser);
+ $URI->FromURI('freeside://freeside/cust_main/'.$cust_pkg->custnum);
+ return $URI->Resolver;
+ }
}
+ return;
}
return;
}
sub CustomerInfo {
my $self = shift;
- $self = $self->CustomerResolver or return;
- my $rec = $self->_FreesideGetRecord() or return;
+ $self = $self->CustomerResolver;
+ my $rec = $self->_FreesideGetRecord() if $self;
+ if (!$rec) {
+ # AsStringLong will report an error;
+ # here, just avoid breaking things
+ my $error = {
+ AgentName => '',
+ CustomerClass => '',
+ CustomerTags => [],
+ Referral => '',
+ InvoiceEmail => '',
+ BillingType => '',
+ };
+ return $error;
+ }
+
my $cust_main = delete $rec->{_object};
my $agent = $cust_main->agent;
my $class = $cust_main->cust_class;
diff --git a/rt/share/html/Ticket/Elements/Customers b/rt/share/html/Ticket/Elements/Customers
index d90ef1c44..fed678380 100644
--- a/rt/share/html/Ticket/Elements/Customers
+++ b/rt/share/html/Ticket/Elements/Customers
@@ -43,10 +43,12 @@ while (my $link = $customers->Next) {
} elsif ( $uri =~ /cust_svc\/(\d+)/ ) {
my $svc = $link->TargetURI->Resolver;
my $cust = $svc->CustomerResolver;
- my $custnum = $cust->{fspkey};
- $cust_main{$custnum} ||= $cust;
- $cust_svc{$custnum} ||= [];
- push @{$cust_svc{$custnum}}, $svc;
+ if ( $cust ) {
+ my $custnum = $cust->{fspkey};
+ $cust_main{$custnum} ||= $cust if $cust;
+ $cust_svc{$custnum} ||= [];
+ push @{$cust_svc{$custnum}}, $svc if $svc;
+ }
}
}
@custnums = sort { $a <=> $b } keys %cust_main;
diff --git a/rt/share/html/Ticket/Update.html b/rt/share/html/Ticket/Update.html
index 26a37e80a..8a3d8e30d 100755
--- a/rt/share/html/Ticket/Update.html
+++ b/rt/share/html/Ticket/Update.html
@@ -290,6 +290,7 @@ if ( $ARGS{'SubmitTicket'} ) {
my %squelched = ProcessTransactionSquelching( \%ARGS );
$ARGS{'SquelchMailTo'} = [keys %squelched] if keys %squelched;
+warn @{ $ARGS{'SquelchMailTo'} } if $ARGS{'SquelchMailTo'};
my $CFs = $TicketObj->TransactionCustomFields;
my $ValidCFs = $m->comp(
@@ -311,6 +312,7 @@ if ( $ARGS{'SubmitTicket'} ) {
);
$checks_failure = 1 unless $status;
}
+warn @{ $ARGS{'SquelchMailTo'} } if $ARGS{'SquelchMailTo'};
# check email addresses for RT's
{