mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2024-09-20 00:11:35 +02:00
Merge pull request #594 from joshuadwire/more-payment-terms
Add due date settings for recurring invoices
This commit is contained in:
commit
6e98e9b60d
@ -227,6 +227,7 @@ class InvoiceController extends BaseController
|
||||
}
|
||||
|
||||
$invoice->invoice_date = Utils::fromSqlDate($invoice->invoice_date);
|
||||
$invoice->recurring_due_date = $invoice->due_date;// Keep in SQL form
|
||||
$invoice->due_date = Utils::fromSqlDate($invoice->due_date);
|
||||
$invoice->start_date = Utils::fromSqlDate($invoice->start_date);
|
||||
$invoice->end_date = Utils::fromSqlDate($invoice->end_date);
|
||||
@ -357,6 +358,54 @@ class InvoiceController extends BaseController
|
||||
}
|
||||
}
|
||||
|
||||
$recurringDueDateHelp = '';
|
||||
foreach (preg_split("/((\r?\n)|(\r\n?))/", trans('texts.recurring_due_date_help')) as $line) {
|
||||
$parts = explode("=>", $line);
|
||||
if (count($parts) > 1) {
|
||||
$line = $parts[0].' => '.Utils::processVariables($parts[0]);
|
||||
$recurringDueDateHelp .= '<li>'.strip_tags($line).'</li>';
|
||||
} else {
|
||||
$recurringDueDateHelp .= $line;
|
||||
}
|
||||
}
|
||||
|
||||
// Create due date options
|
||||
$recurringDueDates = array(
|
||||
trans('texts.use_client_terms') => array('value' => '', 'class' => 'monthly weekly'),
|
||||
);
|
||||
|
||||
$ends = array('th','st','nd','rd','th','th','th','th','th','th');
|
||||
for($i = 1; $i < 31; $i++){
|
||||
if ($i >= 11 && $i <= 13) $ordinal = $i. 'th';
|
||||
else $ordinal = $i . $ends[$i % 10];
|
||||
|
||||
$dayStr = str_pad($i, 2, '0', STR_PAD_LEFT);
|
||||
$str = trans('texts.day_of_month', array('ordinal'=>$ordinal));
|
||||
|
||||
$recurringDueDates[$str] = array('value' => "1998-01-$dayStr", 'data-num' => $i, 'class' => 'monthly');
|
||||
}
|
||||
$recurringDueDates[trans('texts.last_day_of_month')] = array('value' => "1998-01-31", 'data-num' => 31, 'class' => 'monthly');
|
||||
|
||||
|
||||
$daysOfWeek = array(
|
||||
trans('texts.sunday'),
|
||||
trans('texts.monday'),
|
||||
trans('texts.tuesday'),
|
||||
trans('texts.wednesday'),
|
||||
trans('texts.thursday'),
|
||||
trans('texts.friday'),
|
||||
trans('texts.saturday'),
|
||||
);
|
||||
foreach(array('1st','2nd','3rd','4th') as $i=>$ordinal){
|
||||
foreach($daysOfWeek as $j=>$dayOfWeek){
|
||||
$str = trans('texts.day_of_week_after', array('ordinal' => $ordinal, 'day' => $dayOfWeek));
|
||||
|
||||
$day = $i * 7 + $j + 1;
|
||||
$dayStr = str_pad($day, 2, '0', STR_PAD_LEFT);
|
||||
$recurringDueDates[$str] = array('value' => "1998-02-$dayStr", 'data-num' => $day, 'class' => 'weekly');
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'data' => Input::old('data'),
|
||||
'account' => Auth::user()->account->load('country'),
|
||||
@ -377,7 +426,9 @@ class InvoiceController extends BaseController
|
||||
6 => 'Six months',
|
||||
7 => 'Annually',
|
||||
),
|
||||
'recurringDueDates' => $recurringDueDates,
|
||||
'recurringHelp' => $recurringHelp,
|
||||
'recurringDueDateHelp' => $recurringDueDateHelp,
|
||||
'invoiceLabels' => Auth::user()->account->getInvoiceLabels(),
|
||||
'tasks' => Session::get('tasks') ? json_encode(Session::get('tasks')) : null,
|
||||
];
|
||||
|
@ -486,6 +486,93 @@ class Invoice extends EntityModel implements BalanceAffecting
|
||||
return $schedule[1]->getStart();
|
||||
}
|
||||
|
||||
public function getDueDate($invoice_date = null){
|
||||
if(!$this->is_recurring) {
|
||||
return $this->due_date ? $this->due_date : null;
|
||||
}
|
||||
else{
|
||||
$now = time();
|
||||
if($invoice_date) {
|
||||
// If $invoice_date is specified, all calculations are based on that date
|
||||
if(is_numeric($invoice_date)) {
|
||||
$now = $invoice_date;
|
||||
}
|
||||
else if(is_string($invoice_date)) {
|
||||
$now = strtotime($invoice_date);
|
||||
}
|
||||
elseif ($invoice_date instanceof \DateTime) {
|
||||
$now = $invoice_date->getTimestamp();
|
||||
}
|
||||
}
|
||||
|
||||
if($this->due_date){
|
||||
// This is a recurring invoice; we're using a custom format here.
|
||||
// The year is always 1998; January is 1st, 2nd, last day of the month.
|
||||
// February is 1st Sunday after, 1st Monday after, ..., through 4th Saturday after.
|
||||
$dueDateVal = strtotime($this->due_date);
|
||||
$monthVal = (int)date('n', $dueDateVal);
|
||||
$dayVal = (int)date('j', $dueDateVal);
|
||||
|
||||
if($monthVal == 1) {// January; day of month
|
||||
$currentDay = (int)date('j', $now);
|
||||
$lastDayOfMonth = (int)date('t', $now);
|
||||
|
||||
$dueYear = (int)date('Y', $now);// This year
|
||||
$dueMonth = (int)date('n', $now);// This month
|
||||
$dueDay = $dayVal;// The day specified for the invoice
|
||||
|
||||
if($dueDay > $lastDayOfMonth) {
|
||||
// No later than the end of the month
|
||||
$dueDay = $lastDayOfMonth;
|
||||
}
|
||||
|
||||
if($currentDay >= $dueDay) {
|
||||
// Wait until next month
|
||||
// We don't need to handle the December->January wraparaound, since PHP handles month 13 as January of next year
|
||||
$dueMonth++;
|
||||
|
||||
// Reset the due day
|
||||
$dueDay = $dayVal;
|
||||
$lastDayOfMonth = (int)date('t', mktime(0, 0, 0, $dueMonth, 1, $dueYear));// The number of days in next month
|
||||
|
||||
// Check against the last day again
|
||||
if($dueDay > $lastDayOfMonth){
|
||||
// No later than the end of the month
|
||||
$dueDay = $lastDayOfMonth;
|
||||
}
|
||||
}
|
||||
|
||||
$dueDate = mktime(0, 0, 0, $dueMonth, $dueDay, $dueYear);
|
||||
}
|
||||
else if($monthVal == 2) {// February; day of week
|
||||
$ordinals = array('first', 'second', 'third', 'fourth');
|
||||
$daysOfWeek = array('sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday');
|
||||
|
||||
$ordinalIndex = ceil($dayVal / 7) - 1;// 1-7 are "first"; 8-14 are "second", etc.
|
||||
$dayOfWeekIndex = ($dayVal - 1) % 7;// 1,8,15,22 are Sunday, 2,9,16,23 are Monday, etc.
|
||||
$dayStr = $ordinals[$ordinalIndex] . ' ' . $daysOfWeek[$dayOfWeekIndex];// "first sunday", "first monday", etc.
|
||||
|
||||
$dueDate = strtotime($dayStr, $now);
|
||||
}
|
||||
|
||||
if($dueDate) {
|
||||
return date('Y-m-d', $dueDate);// SQL format
|
||||
}
|
||||
}
|
||||
else if ($this->client->payment_terms != 0) {
|
||||
// No custom due date set for this invoice; use the client's payment terms
|
||||
$days = $this->client->payment_terms;
|
||||
if ($days == -1) {
|
||||
$days = 0;
|
||||
}
|
||||
return date('Y-m-d', strtotime('+'.$days.' day', $now));
|
||||
}
|
||||
}
|
||||
|
||||
// Couldn't calculate one
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getPrettySchedule($min = 1, $max = 10)
|
||||
{
|
||||
if (!$schedule = $this->getSchedule($max)) {
|
||||
@ -496,7 +583,14 @@ class Invoice extends EntityModel implements BalanceAffecting
|
||||
|
||||
for ($i=$min; $i<min($max, count($schedule)); $i++) {
|
||||
$date = $schedule[$i];
|
||||
$date = $this->account->formatDate($date->getStart());
|
||||
$dateStart = $date->getStart();
|
||||
$date = $this->account->formatDate($dateStart);
|
||||
$dueDate = $this->getDueDate($dateStart);
|
||||
|
||||
if($dueDate) {
|
||||
$date .= ' <small>(' . trans('texts.due') . ' ' . $this->account->formatDate($dueDate) . ')</small>';
|
||||
}
|
||||
|
||||
$dates[] = $date;
|
||||
}
|
||||
|
||||
|
@ -245,6 +245,9 @@ class InvoiceRepository extends BaseRepository
|
||||
$invoice->end_date = Utils::toSqlDate($data['end_date']);
|
||||
$invoice->due_date = null;
|
||||
$invoice->auto_bill = isset($data['auto_bill']) && $data['auto_bill'] ? true : false;
|
||||
if(isset($data['recurring_due_date'])){
|
||||
$invoice->due_date = $data['recurring_due_date'];
|
||||
}
|
||||
} else {
|
||||
if (isset($data['due_date']) || isset($data['due_date_sql'])) {
|
||||
$invoice->due_date = isset($data['due_date_sql']) ? $data['due_date_sql'] : Utils::toSqlDate($data['due_date']);
|
||||
@ -593,14 +596,7 @@ class InvoiceRepository extends BaseRepository
|
||||
$invoice->custom_text_value1 = $recurInvoice->custom_text_value1;
|
||||
$invoice->custom_text_value2 = $recurInvoice->custom_text_value2;
|
||||
$invoice->is_amount_discount = $recurInvoice->is_amount_discount;
|
||||
|
||||
if ($invoice->client->payment_terms != 0) {
|
||||
$days = $invoice->client->payment_terms;
|
||||
if ($days == -1) {
|
||||
$days = 0;
|
||||
}
|
||||
$invoice->due_date = date_create()->modify($days.' day')->format('Y-m-d');
|
||||
}
|
||||
$invoice->due_date = $recurInvoice->getDueDate();
|
||||
|
||||
$invoice->save();
|
||||
|
||||
|
@ -998,4 +998,32 @@ return array(
|
||||
'white_label_custom_css' => ':link for $'.WHITE_LABEL_PRICE.' to enable custom styling and help support our project.',
|
||||
'white_label_purchase_link' => 'Purchase a white label license',
|
||||
|
||||
// recurring due dates
|
||||
'recurring_due_dates' => 'Recurring Invoice Due Dates',
|
||||
'recurring_due_date_help' => '<p>Automatically sets a due date for the invoice.</p>
|
||||
<p>Invoices on a monthly or yearly cycle set to be due on or before the day they are created will be due the next month. Invoices set to be due on the 29th or 30th in months that don\'t have that day will be due the last day of the month.</p>
|
||||
<p>Invoices on a weekly cycle set to be due on the day of the week they are created will be due the next week.</p>
|
||||
<p>For example:</p>
|
||||
<ul>
|
||||
<li>Today is the 15th, due date is 1st of the month. The due date should likely be the 1st of the next month.</li>
|
||||
<li>Today is the 15th, due date is the last day of the month. The due date will be the last day of the this month.
|
||||
</li>
|
||||
<li>Today is the 15th, due date is the 15th day of the month. The due date will be the 15th day of <strong>next</strong> month.
|
||||
</li>
|
||||
<li>Today is the Friday, due date is the 1st Friday after. The due date will be next Friday, not today.
|
||||
</li>
|
||||
</ul>',
|
||||
'due' => 'Due',
|
||||
'next_due_on' => 'Due Next: :date',
|
||||
'use_client_terms' => 'Use client terms',
|
||||
'day_of_month' => ':ordinal day of month',
|
||||
'last_day_of_month' => 'Last day of month',
|
||||
'day_of_week_after' => ':ordinal :day after',
|
||||
'sunday' => 'Sunday',
|
||||
'monday' => 'Monday',
|
||||
'tuesday' => 'Tuesday',
|
||||
'wednesday' => 'Wednesday',
|
||||
'thursday' => 'Thursday',
|
||||
'friday' => 'Friday',
|
||||
'saturday' => 'Saturday',
|
||||
);
|
||||
|
@ -113,36 +113,17 @@
|
||||
@if ($entityType == ENTITY_INVOICE)
|
||||
<div data-bind="visible: is_recurring" style="display: none">
|
||||
{!! Former::select('frequency_id')->options($frequencies)->data_bind("value: frequency_id")
|
||||
->appendIcon('question-sign')->addGroupClass('frequency_id') !!}
|
||||
->appendIcon('question-sign')->addGroupClass('frequency_id')->onchange('onFrequencyChange()') !!}
|
||||
{!! Former::text('start_date')->data_bind("datePicker: start_date, valueUpdate: 'afterkeydown'")
|
||||
->data_date_format(Session::get(SESSION_DATE_PICKER_FORMAT, DEFAULT_DATE_PICKER_FORMAT))->appendIcon('calendar')->addGroupClass('start_date') !!}
|
||||
{!! Former::text('end_date')->data_bind("datePicker: end_date, valueUpdate: 'afterkeydown'")
|
||||
->data_date_format(Session::get(SESSION_DATE_PICKER_FORMAT, DEFAULT_DATE_PICKER_FORMAT))->appendIcon('calendar')->addGroupClass('end_date') !!}
|
||||
{!! Former::select('recurring_due_date')->label(trans('texts.due_date'))->options($recurringDueDates)->data_bind("value: recurring_due_date")->appendIcon('question-sign')->addGroupClass('recurring_due_date') !!}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($account->showCustomField('custom_invoice_text_label1', $invoice))
|
||||
{!! Former::text('custom_text_value1')->label($account->custom_invoice_text_label1)->data_bind("value: custom_text_value1, valueUpdate: 'afterkeydown'") !!}
|
||||
@endif
|
||||
|
||||
@if ($entityType == ENTITY_INVOICE)
|
||||
<div class="form-group" style="margin-bottom: 8px">
|
||||
<div class="col-lg-8 col-sm-8 col-sm-offset-4" style="padding-top: 10px">
|
||||
@if ($invoice->recurring_invoice)
|
||||
{!! trans('texts.created_by_invoice', ['invoice' => link_to('/invoices/'.$invoice->recurring_invoice->public_id, trans('texts.recurring_invoice'))]) !!}
|
||||
@elseif ($invoice->id)
|
||||
<span class="smaller">
|
||||
@if (isset($lastSent) && $lastSent)
|
||||
{!! trans('texts.last_sent_on', ['date' => link_to('/invoices/'.$lastSent->public_id, $invoice->last_sent_date, ['id' => 'lastSent'])]) !!} <br/>
|
||||
@endif
|
||||
@if ($invoice->is_recurring && $invoice->getNextSendDate())
|
||||
{!! trans('texts.next_send_on', ['date' => '<span data-bind="tooltip: {title: \''.$invoice->getPrettySchedule().'\', html: true}">'.$account->formatDate($invoice->getNextSendDate()).
|
||||
'<span class="glyphicon glyphicon-info-sign" style="padding-left:10px;color:#B1B5BA"></span></span>']) !!}
|
||||
@endif
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@ -169,6 +150,29 @@
|
||||
{!! Former::text('custom_text_value2')->label($account->custom_invoice_text_label2)->data_bind("value: custom_text_value2, valueUpdate: 'afterkeydown'") !!}
|
||||
@endif
|
||||
|
||||
@if ($entityType == ENTITY_INVOICE)
|
||||
<div class="form-group" style="margin-bottom: 8px">
|
||||
<div class="col-lg-8 col-sm-8 col-sm-offset-4" style="padding-top: 10px">
|
||||
@if ($invoice->recurring_invoice)
|
||||
{!! trans('texts.created_by_invoice', ['invoice' => link_to('/invoices/'.$invoice->recurring_invoice->public_id, trans('texts.recurring_invoice'))]) !!}
|
||||
@elseif ($invoice->id)
|
||||
<span class="smaller">
|
||||
@if (isset($lastSent) && $lastSent)
|
||||
{!! trans('texts.last_sent_on', ['date' => link_to('/invoices/'.$lastSent->public_id, $invoice->last_sent_date, ['id' => 'lastSent'])]) !!} <br/>
|
||||
@endif
|
||||
@if ($invoice->is_recurring && $invoice->getNextSendDate())
|
||||
{!! trans('texts.next_send_on', ['date' => '<span data-bind="tooltip: {title: \''.$invoice->getPrettySchedule().'\', html: true}">'.$account->formatDate($invoice->getNextSendDate()).
|
||||
'<span class="glyphicon glyphicon-info-sign" style="padding-left:10px;color:#B1B5BA"></span></span>']) !!}
|
||||
@if ($invoice->getDueDate())
|
||||
<br>
|
||||
{!! trans('texts.next_due_on', ['date' => '<span>'.$account->formatDate($invoice->getDueDate($invoice->getNextSendDate())).'</span>']) !!}
|
||||
@endif
|
||||
@endif
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -592,6 +596,26 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="recurringDueDateModal" tabindex="-1" role="dialog" aria-labelledby="recurringDueDateModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog" style="min-width:150px">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
<h4 class="modal-title" id="recurringDueDateModalLabel">{{ trans('texts.recurring_due_dates') }}</h4>
|
||||
</div>
|
||||
|
||||
<div style="background-color: #fff; padding-left: 16px; padding-right: 16px">
|
||||
{!! isset($recurringDueDateHelp) ? $recurringDueDateHelp : '' !!}
|
||||
</div>
|
||||
|
||||
<div class="modal-footer" style="margin-top: 0px">
|
||||
<button type="button" class="btn btn-primary" data-dismiss="modal">{{ trans('texts.close') }}</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!! Former::close() !!}
|
||||
|
||||
{!! Former::open("{$entityType}s/bulk")->addClass('bulkForm') !!}
|
||||
@ -760,6 +784,10 @@
|
||||
showLearnMore();
|
||||
});
|
||||
|
||||
$('.recurring_due_date .input-group-addon').click(function() {
|
||||
showRecurringDueDateLearnMore();
|
||||
});
|
||||
|
||||
var fields = ['invoice_date', 'due_date', 'start_date', 'end_date', 'last_sent_date'];
|
||||
for (var i=0; i<fields.length; i++) {
|
||||
var field = fields[i];
|
||||
@ -809,6 +837,24 @@
|
||||
applyComboboxListeners();
|
||||
});
|
||||
|
||||
function onFrequencyChange(){
|
||||
var currentName = $('#frequency_id').find('option:selected').text()
|
||||
var currentDueDateNumber = $('#recurring_due_date').find('option:selected').attr('data-num');
|
||||
var optionClass = currentName && currentName.toLowerCase().indexOf('week') > -1 ? 'weekly' : 'monthly';
|
||||
var replacementOption = $('#recurring_due_date option[data-num=' + currentDueDateNumber + '].' + optionClass);
|
||||
|
||||
$('#recurring_due_date option').hide();
|
||||
$('#recurring_due_date option.' + optionClass).show();
|
||||
|
||||
// Switch to an equivalent option
|
||||
if(replacementOption.length){
|
||||
replacementOption.attr('selected','selected');
|
||||
}
|
||||
else{
|
||||
$('#recurring_due_date').val('');
|
||||
}
|
||||
}
|
||||
|
||||
function applyComboboxListeners() {
|
||||
var selectorStr = '.invoice-table input, .invoice-table textarea';
|
||||
$(selectorStr).off('change').on('change', function(event) {
|
||||
@ -1136,6 +1182,10 @@
|
||||
$('#recurringModal').modal('show');
|
||||
}
|
||||
|
||||
function showRecurringDueDateLearnMore() {
|
||||
$('#recurringDueDateModal').modal('show');
|
||||
}
|
||||
|
||||
function setInvoiceNumber(client) {
|
||||
@if ($invoice->id || !$account->hasClientNumberPattern($invoice))
|
||||
return;
|
||||
|
Loading…
Reference in New Issue
Block a user