App-GnuCash-MembershipUtils

 view release on metacpan or  search on metacpan

bin/gc-members-generate-invoices  view on Meta::CPAN

#!/usr/bin/env perl
##----------------------------------------------------------------------------
## :mode=perl:indentSize=2:tabSize=2:noTabs=true:
##****************************************************************************
## NOTES:
##  * Before comitting this file to the repository, ensure Perl Critic can be
##    invoked at the HARSH [3] level with no errors
##****************************************************************************
use strict;
use warnings;
use feature qw( say state );

## Cannot use Find::Bin because script may be invoked as an
## argument to another script, so instead we use __FILE__
use File::Basename qw(dirname fileparse basename);
use File::Spec;
## Add script directory
use lib File::Spec->catdir(File::Spec->splitdir(dirname(__FILE__)));
## Add script directory/lib
use lib File::Spec->catdir(File::Spec->splitdir(dirname(__FILE__)), qq{lib});
## Add script directory/../lib
use lib File::Spec->catdir(
  File::Spec->splitdir(dirname(__FILE__)), 
  qq{..}, 
  qq{lib});
use App::GnuCash::MembershipUtils qw( :all );
use Getopt::Long qw( :config no_ignore_case bundling );
use Pod::Usage;
use Cwd qw(abs_path);
use Readonly;
use Data::Dump qw( pp );
use Text::CSV;
use DateTime;
use DateTime::Format::DateParse;

Readonly::Hash my %DATE_FORMATS => (
    us     => "%m/%d/%Y",   # MM/DD/YYYY
    uk     => "%d/%m/%Y",   # DD/MM/YYYY
    iso    => "%Y-%m-%d",   # YYYY-MM-DD
    europe => "%d.%m.%Y",   # DD.MM.YYYY
);

## Used for the version string
Readonly::Scalar my $VERSION       => qq{0.01};
Readonly::Scalar my $DEFAULT_TITLE => qq{GnuCash Members Generate Invoices};

##--------------------------------------------------------
## Return codes for booleans
##--------------------------------------------------------
Readonly::Scalar my $FALSE => 0;
Readonly::Scalar my $TRUE  => 1;

##--------------------------------------------------------
## A list of all command line options
## For GetOptions the following parameter indicaters are used
##    = Required parameter
##    : Optional parameter
##        s String parameter
##        i Integer parameter
##        f Real number (float)
##    + Parameter is inremented for each occurrence on
##      command line
##    ! boolean parameter that also allows "no-xxx"
## If a parameter is not indicated, then a value of 1
## indicates the parameter was found on the command line
##--------------------------------------------------------
#<<< begin perltidy exclusion zone
my @CommandLineOptions = (
  "config|c:s",
  "debug|d+",
  "due|D:s",

bin/gc-members-generate-invoices  view on Meta::CPAN

process_commandline();

my $dt_tz = DateTime::TimeZone->new( name => 'local' );
my $inv_open_dt;
if ($gOptions{open}) {
    $inv_open_dt = DateTime::Format::DateParse->parse_datetime($gOptions{open});
} else {
    # Default is first of the current month
    $inv_open_dt = DateTime->today( time_zone => $dt_tz, )->set_day(1);
}

my $inv_due_dt;
if ($gOptions{due}) {
    $inv_due_dt = DateTime::Format::DateParse->parse_datetime($gOptions{due});
} else {
    # Default is last of current month.
    # This is done by getting the 1st day of next month and subtrating 1 day
    $inv_due_dt = DateTime->today( time_zone => $dt_tz, )->set_day(1)->add(months => 1)->subtract(days => 1);
}

if ($inv_open_dt > $inv_due_dt) {
  printf STDERR "ERROR: The opening date cannot be after the due date!\n";
  exit(1);
}

my ($error, $config) = get_config($gOptions{config}, $gOptions{debug});

if ($error) {
  printf STDERR "Error getting config: %s\n", $error;
  exit(1);
}

$gOptions{format} //= $config->{GnuCash}{format};
unless ($DATE_FORMATS{lc($gOptions{format})}) {
    printf STDERR "Invalid date format option '%s'. Must be one of '%s'\n",
        $gOptions{format},
        join("', '", keys(%DATE_FORMATS));
    exit(1);
}

$gOptions{memo} //= $config->{GnuCash}{memo};

$gOptions{note} //= $inv_open_dt->strftime("%B membership dues");

unless ($gOptions{outfile}) {
    $gOptions{outfile} = DateTime->today( time_zone => $dt_tz, )->strftime("%Y-%m-%d_gnucash_customer_invoices.csv");
}

my $schema;
($error, $schema) = open_gnucash(get_gnucash_filename($gOptions{file}, $config));
if ($error) {
  printf STDERR "Error opening GnuCash file: %s\n", $error;
  exit(1);
}

my $warning;
($error, $warning) = validate_accounts_in_config({
  schema => $schema,
  config => $config,
});
say STDERR $warning if ($warning);
if ($error) {
  say STDERR $error;
  exit(1);
}

my @members = get_all_members({
  active_only => 1,
  schema      => $schema,
  config      => $config,
  debug       => $gOptions{debug},
});

my $last_inv_id = $schema->resultset('Invoice')->last_invoice_id;
my $next_inv_id = $last_inv_id + 1;
printf(
    join(
        "\n",
        "Output File:   '%s'",
        "Opening Date:  %s",
        "Due Date:      %s",
        "Item Memo:     '%s'",
        "Invoice Memo:  '%s'",
        "First Invoice: '%06d'",
        "",
    ),
    $gOptions{outfile},
    format_date($inv_open_dt),
    format_date($inv_due_dt),
    $gOptions{memo},
    $gOptions{note},
    $next_inv_id,
);

my $csv = Text::CSV->new ({
  # Cannot quote spaces, or GnuCash won't find your accounts
  quote_space => 0,
  sep_char    => ';',
  binary      => 1,
  auto_diag   => 1,
});

my %inv_template = (
  date_opened    => format_date($inv_open_dt),
  date           => format_date($inv_open_dt),
  date_posted    => format_date($inv_open_dt),
  due_date       => format_date($inv_due_dt),
  quantity       => 1,
  account_posted => $config->{GnuCash}{account},
  memo_posted    => $gOptions{note},
  accu_splits    => "Y",
);

## no critic (InputOutput::RequireBriefOpen)
if (open my $fh, ">:encoding(utf8)", $gOptions{outfile}) {
  for my $member (@members) {
      my %inv = (
        %inv_template,
          id       => sprintf("%06d", $next_inv_id++),
          owner_id => $member->{id},
          account  => $member->{membership_account},
          price    => $member->{membership_amount},
          desc     => join(" - ", grep { length } ($member->{membership_type}, $gOptions{memo})),
      );
      $csv->say ($fh, [ map { $inv{$_} } @CSV_COLUMNS ]);
  }
  close($fh);
} else {
  say STDERR "Error writing '$gOptions{outfile}': $!";
  exit(1);
}
## use critic
#      "First Invoice: '%06d'"
printf("Last Invoice:  '%06d'\n", --$next_inv_id);
exit(0);

__END__

__DATA__

##----------------------------------------------------------------------------
## By placing the POD in the DATA section, we can use
##   pod2usage(input => \*DATA)
## even if the script is compiled using PerlApp, perl2exe or Perl::PAR
##----------------------------------------------------------------------------

=head1 NAME

gc-members-generate-invoices - Generate a CSV file suitable for importing into GnuCash.
 
=head1 DESCRIPTION

Generate a CSV file suitable for importing into GnuCash.

=head1 SYNOPSIS

  gc-members-generate-invoices

  # Provide a config file and GnuCash file 
  gc-members-generate-invoices --config my-config.yml --file my-organization.gnucash

  # Provide a note when posting the invoices
  gc-members-generate-invoices --config my-config.yml --note "January dues"

=head1 OPTIONS

=over 4

=item B<--config> I<ConfigFilename>, B<-c> I<ConfigFilename>

Specify the filename for the config file.

=item B<--file> I<GnuCashFilename>, B<-f> I<GnuCashFilename>

Specify the filename for the GnuCash file.

=item B<--due> I<DueDate>, B<-D> I<DueDate>

Specify the due date for the posted invoices. Defaults to the last day
of the current month.

=item B<--opened> I<OpenedDate>, B<-O> I<OpenedDate>

Specify the opened and invoice date for the posted invoices. Defaults to the first day
of the current month.

=item B<--format> I<DateFormat>, B<-F> I<DateFormat>

Specify the date format used in GnuCash. B<NOTE:> This must match the format specified in



( run in 2.606 seconds using v1.01-cache-2.11-cpan-d80b1682f3f )