1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-09-19 16:01:34 +02:00

Support signing for invoice

This commit is contained in:
Hillel Coren 2016-11-04 15:34:15 +02:00
parent fbf618a226
commit 3ccb33ec21
20 changed files with 349 additions and 33 deletions

View File

@ -747,12 +747,7 @@ class AccountController extends BaseController
private function saveClientPortal()
{
$account = Auth::user()->account;
$account->enable_client_portal = !!Input::get('enable_client_portal');
$account->enable_client_portal_dashboard = !!Input::get('enable_client_portal_dashboard');
$account->enable_portal_password = !!Input::get('enable_portal_password');
$account->send_portal_password = !!Input::get('send_portal_password');
$account->enable_buy_now_buttons = !!Input::get('enable_buy_now_buttons');
$account->fill(Input::all());
// Only allowed for pro Invoice Ninja users or white labeled self-hosted users
if (Auth::user()->account->hasFeature(FEATURE_CLIENT_PORTAL_CSS)) {

View File

@ -198,6 +198,19 @@ class ClientPortalController extends BaseController
return $pdfString;
}
public function sign($invitationKey)
{
if (!$invitation = $this->invoiceRepo->findInvoiceByInvitation($invitationKey)) {
return RESULT_FAILURE;
}
$invitation->signature_base64 = Input::get('signature');
$invitation->signature_date = date_create();
$invitation->save();
return RESULT_SUCCESS;
}
public function dashboard($contactKey = false)
{
if ($contactKey) {

View File

@ -218,6 +218,8 @@ class InvoiceController extends BaseController
$contact->invitation_viewed = $invitation->viewed_date && $invitation->viewed_date != '0000-00-00 00:00:00' ? $invitation->viewed_date : false;
$contact->invitation_openend = $invitation->opened_date && $invitation->opened_date != '0000-00-00 00:00:00' ? $invitation->opened_date : false;
$contact->invitation_status = $contact->email_error ? false : $invitation->getStatus();
$contact->invitation_signature_svg = $invitation->signature_base64;
$contact->invitation_signature_date = $invitation->signature_date;
}
}
}

View File

@ -38,6 +38,7 @@ Route::post('/get_started', 'AccountController@getStarted');
Route::group(['middleware' => 'auth:client'], function() {
Route::get('view/{invitation_key}', 'ClientPortalController@view');
Route::get('download/{invitation_key}', 'ClientPortalController@download');
Route::put('sign/{invitation_key}', 'ClientPortalController@sign');
Route::get('view', 'HomeController@viewLogo');
Route::get('approve/{invitation_key}', 'QuoteController@approve');
Route::get('payment/{invitation_key}/{gateway_type?}/{source_id?}', 'OnlinePaymentController@showPayment');

View File

@ -101,7 +101,9 @@ class Utils
return false;
}
return \App\Models\Account::first()->hasFeature(FEATURE_WHITE_LABEL);
$account = \App\Models\Account::first();
return $account && $account->hasFeature(FEATURE_WHITE_LABEL);
}
public static function getResllerType()

View File

@ -70,6 +70,15 @@ class Account extends Eloquent
'include_item_taxes_inline',
'start_of_week',
'financial_year_start',
'enable_client_portal',
'enable_client_portal_dashboard',
'enable_portal_password',
'send_portal_password',
'enable_buy_now_buttons',
'show_accept_invoice_terms',
'show_accept_quote_terms',
'require_invoice_signature',
'require_quote_signature',
];
/**
@ -1861,6 +1870,29 @@ class Account extends Eloquent
return $this->enabled_modules & static::$modules[$entityType];
}
public function showAuthenticatePanel($invoice)
{
return $this->showAcceptTerms($invoice) || $this->showSignature($invoice);
}
public function showAcceptTerms($invoice)
{
if ( ! $this->isPro() || ! $invoice->terms) {
return false;
}
return $invoice->is_quote ? $this->show_accept_quote_terms : $this->show_accept_invoice_terms;
}
public function showSignature($invoice)
{
if ( ! $this->isPro()) {
return false;
}
return $invoice->is_quote ? $this->require_quote_signature : $this->require_invoice_signature;
}
}
Account::updated(function ($account)

View File

@ -31,7 +31,8 @@
"dropzone": "~4.3.0",
"nouislider": "~8.5.1",
"bootstrap-daterangepicker": "~2.1.24",
"sweetalert2": "^5.3.8"
"sweetalert2": "^5.3.8",
"jSignature": "brinley/jSignature#^2.1.0"
},
"resolutions": {
"jquery": "~1.11"

View File

@ -0,0 +1,73 @@
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddInvoiceSignature extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('invitations', function($table)
{
$table->text('signature_base64')->nullable();
$table->timestamp('signature_date')->nullable();
});
Schema::table('companies', function($table)
{
$table->string('utm_source')->nullable();
$table->string('utm_medium')->nullable();
$table->string('utm_campaign')->nullable();
$table->string('utm_term')->nullable();
$table->string('utm_content')->nullable();
});
Schema::table('payment_methods', function($table)
{
$table->dropForeign('payment_methods_account_gateway_token_id_foreign');
});
Schema::table('payment_methods', function($table)
{
$table->foreign('account_gateway_token_id')->references('id')->on('account_gateway_tokens')->onDelete('cascade');
});
Schema::table('payments', function($table)
{
$table->dropForeign('payments_payment_method_id_foreign');
});
Schema::table('payments', function($table)
{
$table->foreign('payment_method_id')->references('id')->on('payment_methods')->onDelete('cascade');;
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('invitations', function($table)
{
$table->dropColumn('signature_base64');
$table->dropColumn('signature_date');
});
Schema::table('companies', function($table)
{
$table->dropColumn('utm_source');
$table->dropColumn('utm_medium');
$table->dropColumn('utm_campaign');
$table->dropColumn('utm_term');
$table->dropColumn('utm_content');
});
}
}

View File

@ -80,6 +80,10 @@ elixir(function(mix) {
bowerDir + '/bootstrap-daterangepicker/daterangepicker.js'
], 'public/js/daterangepicker.min.js');
mix.scripts([
bowerDir + '/jSignature/libs/jSignature.min.js'
], 'public/js/jSignature.min.js');
mix.scripts([
bowerDir + '/jquery/dist/jquery.js',
bowerDir + '/jquery-ui/jquery-ui.js',

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2
public/js/jSignature.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -2175,6 +2175,19 @@ $LANG = array(
'created_by' => 'Created by :name',
'modules' => 'Modules',
'financial_year_start' => 'First Month of the Year',
'authentication' => 'Authentication',
'checkbox' => 'Checkbox',
'invoice_signature' => 'Signature',
'show_accept_invoice_terms' => 'Invoice Terms Checkbox',
'show_accept_invoice_terms_help' => 'Require client to confirm that they accept the invoice terms.',
'show_accept_quote_terms' => 'Quote Terms Checkbox',
'show_accept_quote_terms_help' => 'Require client to confirm that they accept the quote terms.',
'require_invoice_signature' => 'Invoice Signature',
'require_invoice_signature_help' => 'Require client to provide their signature.',
'require_quote_signature' => 'Quote Signature',
'require_quote_signature_help' => 'Require client to provide their signature.',
'i_agree' => 'I Agree To The Terms & Conditions',
'sign_here' => 'Please sign here:',
);

View File

@ -27,6 +27,10 @@
{!! Former::populateField('enable_portal_password', intval($enable_portal_password)) !!}
{!! Former::populateField('send_portal_password', intval($send_portal_password)) !!}
{!! Former::populateField('enable_buy_now_buttons', intval($account->enable_buy_now_buttons)) !!}
{!! Former::populateField('show_accept_invoice_terms', intval($account->show_accept_invoice_terms)) !!}
{!! Former::populateField('show_accept_quote_terms', intval($account->show_accept_quote_terms)) !!}
{!! Former::populateField('require_invoice_signature', intval($account->require_invoice_signature)) !!}
{!! Former::populateField('require_quote_signature', intval($account->require_quote_signature)) !!}
@if (!Utils::isNinja() && !Auth::user()->account->hasFeature(FEATURE_WHITE_LABEL))
<div class="alert alert-warning" style="font-size:larger;">
@ -61,20 +65,71 @@
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{!! trans('texts.security') !!}</h3>
<h3 class="panel-title">{!! trans('texts.authentication') !!}</h3>
</div>
<div class="panel-body">
<div class="col-md-10 col-md-offset-1">
{!! Former::checkbox('enable_portal_password')
->text(trans('texts.enable'))
->help(trans('texts.enable_portal_password_help'))
->label(trans('texts.enable_portal_password')) !!}
<div role="tabpanel">
<ul class="nav nav-tabs" role="tablist" style="border: none">
<li role="presentation" class="active"><a href="#password" aria-controls="password" role="tab" data-toggle="tab">{{ trans('texts.password') }}</a></li>
<li role="presentation"><a href="#checkbox" aria-controls="checkbox" role="tab" data-toggle="tab">{{ trans('texts.checkbox') }}</a></li>
<li role="presentation"><a href="#signature" aria-controls="signature" role="tab" data-toggle="tab">{{ trans('texts.invoice_signature') }}</a></li>
</ul>
</div>
<div class="col-md-10 col-md-offset-1">
{!! Former::checkbox('send_portal_password')
->text(trans('texts.enable'))
->help(trans('texts.send_portal_password_help'))
->label(trans('texts.send_portal_password')) !!}
<div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="password">
<div class="panel-body">
<div class="row">
<div class="col-md-10 col-md-offset-1">
{!! Former::checkbox('enable_portal_password')
->text(trans('texts.enable'))
->help(trans('texts.enable_portal_password_help'))
->label(trans('texts.enable_portal_password')) !!}
</div>
<div class="col-md-10 col-md-offset-1">
{!! Former::checkbox('send_portal_password')
->text(trans('texts.enable'))
->help(trans('texts.send_portal_password_help'))
->label(trans('texts.send_portal_password')) !!}
</div>
</div>
</div>
</div>
<div role="tabpanel" class="tab-pane" id="checkbox">
<div class="panel-body">
<div class="row">
<div class="col-md-10 col-md-offset-1">
{!! Former::checkbox('show_accept_invoice_terms')
->text(trans('texts.enable'))
->help(trans('texts.show_accept_invoice_terms_help'))
->label(trans('texts.show_accept_invoice_terms')) !!}
</div>
<div class="col-md-10 col-md-offset-1">
{!! Former::checkbox('show_accept_quote_terms')
->text(trans('texts.enable'))
->help(trans('texts.show_accept_quote_terms_help'))
->label(trans('texts.show_accept_quote_terms')) !!}
</div>
</div>
</div>
</div>
<div role="tabpanel" class="tab-pane" id="signature">
<div class="panel-body">
<div class="row">
<div class="col-md-10 col-md-offset-1">
{!! Former::checkbox('require_invoice_signature')
->text(trans('texts.enable'))
->help(trans('texts.require_invoice_signature_help'))
->label(trans('texts.require_invoice_signature')) !!}
</div>
<div class="col-md-10 col-md-offset-1">
{!! Former::checkbox('require_quote_signature')
->text(trans('texts.enable'))
->help(trans('texts.require_quote_signature_help'))
->label(trans('texts.require_quote_signature')) !!}
</div>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -120,11 +120,13 @@
<span data-bind="visible: !$root.invoice().is_recurring()">
<span data-bind="html: $data.view_as_recipient"></span>&nbsp;&nbsp;
@if (Utils::isConfirmed())
<span style="vertical-align:text-top;color:red" class="fa fa-exclamation-triangle"
data-bind="visible: $data.email_error, tooltip: {title: $data.email_error}"></span>
<span style="vertical-align:text-top" class="glyphicon glyphicon-info-sign"
data-bind="visible: $data.invitation_status, tooltip: {title: $data.invitation_status, html: true},
style: {color: $data.info_color}"></span>
<span style="vertical-align:text-top;color:red" class="fa fa-exclamation-triangle"
data-bind="visible: $data.email_error, tooltip: {title: $data.email_error}"></span>
<span style="vertical-align:text-top" class="fa fa-info-circle"
data-bind="visible: $data.invitation_status, tooltip: {title: $data.invitation_status, html: true},
style: {color: $data.info_color}"></span>
<span style="vertical-align:text-top" class="fa fa-user-circle"
data-bind="visible: $data.invitation_signature_svg, tooltip: {title: '', html: true}"></span>
@endif
</span>
@endif
@ -563,7 +565,7 @@
</div>
<p>&nbsp;</p>
@if (Auth::user()->account->live_preview))
@if (Auth::user()->account->live_preview)
@include('invoices.pdf', ['account' => Auth::user()->account])
@else
<script type="text/javascript">

View File

@ -596,6 +596,8 @@ function ContactModel(data) {
self.invitation_openend = ko.observable(false);
self.invitation_viewed = ko.observable(false);
self.email_error = ko.observable('');
self.invitation_signature_svg = ko.observable('');
self.invitation_signature_date = ko.observable('');
if (data) {
ko.mapping.fromJS(data, {}, this);

View File

@ -6,20 +6,31 @@
@include('money_script')
@foreach ($invoice->client->account->getFontFolders() as $font)
<script src="{{ asset('js/vfs_fonts/'.$font.'.js') }}" type="text/javascript"></script>
<script src="{{ asset('js/vfs_fonts/'.$font.'.js') }}" type="text/javascript"></script>
@endforeach
<script src="{{ asset('pdf.built.js') }}?no_cache={{ NINJA_VERSION }}" type="text/javascript"></script>
@if ($account->showSignature($invoice))
<script src="{{ asset('js/jSignature.min.js') }}"></script>
@endif
<style type="text/css">
body {
background-color: #f8f8f8;
}
.dropdown-menu li a{
overflow:hidden;
margin-top:5px;
margin-bottom:5px;
}
#signature {
border: 2px dotted black;
background-color:lightgrey;
}
</style>
@if (!empty($transactionToken) && $accountGateway->gateway_id == GATEWAY_BRAINTREE)
@ -99,7 +110,7 @@
@if (!empty($partialView))
@include($partialView)
@else
<div class="pull-right" style="text-align:right">
<div id="paymentButtons" class="pull-right" style="text-align:right">
@if ($invoice->isQuote())
{!! Button::normal(trans('texts.download_pdf'))->withAttributes(['onclick' => 'onDownloadClick()'])->large() !!}&nbsp;&nbsp;
@if ($showApprove)
@ -191,6 +202,33 @@
@else
refreshPDF();
@endif
@if ($account->showAuthenticatePanel($invoice))
$('#paymentButtons a').on('click', function(e) {
e.preventDefault();
window.pendingPaymentHref = $(this).attr('href');
@if ($account->showSignature($invoice))
if (window.pendingPaymentInit) {
$("#signature").jSignature('reset');
}
@endif
@if ($account->showAcceptTerms($invoice))
$('#termsCheckbox').attr('checked', false);
@endif
$('#authenticationModal').modal('show');
});
@if ($account->showSignature($invoice))
$('#authenticationModal').on('shown.bs.modal', function () {
if ( ! window.pendingPaymentInit) {
window.pendingPaymentInit = true;
$("#signature").jSignature().bind('change', function(e) {
setModalPayNowEnabled();
});;
}
});
@endif
@endif
});
function onDownloadClick() {
@ -203,6 +241,48 @@
$('#customGatewayModal').modal('show');
}
function onModalPayNowClick() {
@if ($account->showSignature($invoice))
var data = {
signature: $('#signature').jSignature('getData', 'svgbase64')[1]
};
$.ajax({
url: "{{ URL::to('sign/' . $invitation->invitation_key) }}",
type: 'PUT',
data: data,
success: function(response) {
redirectToPayment();
}
});
@else
redirectToPayment();
@endif
}
function redirectToPayment() {
$('#authenticationModal').modal('hide');
location.href = window.pendingPaymentHref;
}
function setModalPayNowEnabled() {
var disabled = false;
@if ($account->showAcceptTerms($invoice))
if ( ! $('#termsCheckbox').is(':checked')) {
disabled = true;
}
@endif
@if ($account->showSignature($invoice))
if ( ! $('#signature').jSignature('isModified')) {
disabled = true;
}
@endif
$('#modalPayNowButton').attr('disabled', disabled);
}
</script>
@include('invoices.pdf', ['account' => $invoice->client->account, 'viewPDF' => true])
@ -232,4 +312,42 @@
</div>
</div>
@endif
@if ($account->showAuthenticatePanel($invoice))
<div class="modal fade" id="authenticationModal" tabindex="-1" role="dialog" aria-labelledby="authenticationModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">&nbsp;</h4>
</div>
<div class="panel-body">
<div class="well">
{!! nl2br(e($invoice->terms)) !!}
</div>
@if ($account->showSignature($invoice))
<div>
{{ trans('texts.sign_here') }}
</div>
<div id="signature"></div><br/>
@endif
</div>
<div class="modal-footer">
@if ($account->showAcceptTerms($invoice))
<div class="pull-left">
<label for="termsCheckbox" style="font-weight:normal">
<input id="termsCheckbox" type="checkbox" onclick="setModalPayNowEnabled()"/>
&nbsp;{{ trans('texts.i_agree') }}
</label>
</div>
@endif
<button id="modalPayNowButton" type="button" class="btn btn-success" onclick="onModalPayNowClick()" disabled="">{{ trans('texts.pay_now') }}</button>
</div>
</div>
</div>
</div>
@endif
@stop