mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2025-07-09 03:14:30 -04:00
Merge: Make automatic conversion of quote to invoice optional (#636)
This commit is contained in:
parent
bf778aa616
commit
6166464003
@ -662,6 +662,7 @@ class AccountController extends BaseController
|
|||||||
$account->invoice_terms = Input::get('invoice_terms');
|
$account->invoice_terms = Input::get('invoice_terms');
|
||||||
$account->invoice_footer = Input::get('invoice_footer');
|
$account->invoice_footer = Input::get('invoice_footer');
|
||||||
$account->quote_terms = Input::get('quote_terms');
|
$account->quote_terms = Input::get('quote_terms');
|
||||||
|
$account->auto_convert_quote = Input::get('auto_convert_quote');
|
||||||
|
|
||||||
if (Input::has('recurring_hour')) {
|
if (Input::has('recurring_hour')) {
|
||||||
$account->recurring_hour = Input::get('recurring_hour');
|
$account->recurring_hour = Input::get('recurring_hour');
|
||||||
|
@ -475,7 +475,7 @@ class InvoiceController extends BaseController
|
|||||||
public function convertQuote($publicId)
|
public function convertQuote($publicId)
|
||||||
{
|
{
|
||||||
$invoice = Invoice::with('invoice_items')->scope($publicId)->firstOrFail();
|
$invoice = Invoice::with('invoice_items')->scope($publicId)->firstOrFail();
|
||||||
$clone = $this->invoiceService->approveQuote($invoice);
|
$clone = $this->invoiceService->convertQuote($invoice);
|
||||||
|
|
||||||
Session::flash('message', trans('texts.converted_to_invoice'));
|
Session::flash('message', trans('texts.converted_to_invoice'));
|
||||||
return Redirect::to('invoices/'.$clone->public_id);
|
return Redirect::to('invoices/'.$clone->public_id);
|
||||||
|
@ -97,6 +97,9 @@ class PublicClientController extends BaseController
|
|||||||
if ($invoice->due_date) {
|
if ($invoice->due_date) {
|
||||||
$showApprove = time() < strtotime($invoice->due_date);
|
$showApprove = time() < strtotime($invoice->due_date);
|
||||||
}
|
}
|
||||||
|
if ($invoice->invoice_status_id >= INVOICE_STATUS_APPROVED) {
|
||||||
|
$showApprove = false;
|
||||||
|
}
|
||||||
|
|
||||||
// Checkout.com requires first getting a payment token
|
// Checkout.com requires first getting a payment token
|
||||||
$checkoutComToken = false;
|
$checkoutComToken = false;
|
||||||
|
@ -131,7 +131,7 @@ class QuoteController extends BaseController
|
|||||||
|
|
||||||
if ($action == 'convert') {
|
if ($action == 'convert') {
|
||||||
$invoice = Invoice::with('invoice_items')->scope($ids)->firstOrFail();
|
$invoice = Invoice::with('invoice_items')->scope($ids)->firstOrFail();
|
||||||
$clone = $this->invoiceService->approveQuote($invoice);
|
$clone = $this->invoiceService->convertQuote($invoice);
|
||||||
|
|
||||||
Session::flash('message', trans('texts.converted_to_invoice'));
|
Session::flash('message', trans('texts.converted_to_invoice'));
|
||||||
return Redirect::to('invoices/'.$clone->public_id);
|
return Redirect::to('invoices/'.$clone->public_id);
|
||||||
|
@ -368,8 +368,9 @@ if (!defined('CONTACT_EMAIL')) {
|
|||||||
define('INVOICE_STATUS_DRAFT', 1);
|
define('INVOICE_STATUS_DRAFT', 1);
|
||||||
define('INVOICE_STATUS_SENT', 2);
|
define('INVOICE_STATUS_SENT', 2);
|
||||||
define('INVOICE_STATUS_VIEWED', 3);
|
define('INVOICE_STATUS_VIEWED', 3);
|
||||||
define('INVOICE_STATUS_PARTIAL', 4);
|
define('INVOICE_STATUS_APPROVED', 4);
|
||||||
define('INVOICE_STATUS_PAID', 5);
|
define('INVOICE_STATUS_PARTIAL', 5);
|
||||||
|
define('INVOICE_STATUS_PAID', 6);
|
||||||
|
|
||||||
define('PAYMENT_TYPE_CREDIT', 1);
|
define('PAYMENT_TYPE_CREDIT', 1);
|
||||||
define('CUSTOM_DESIGN', 11);
|
define('CUSTOM_DESIGN', 11);
|
||||||
|
@ -252,6 +252,14 @@ class Invoice extends EntityModel implements BalanceAffecting
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function markApproved()
|
||||||
|
{
|
||||||
|
if ($this->is_quote) {
|
||||||
|
$this->invoice_status_id = INVOICE_STATUS_APPROVED;
|
||||||
|
$this->save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function updateBalances($balanceAdjustment, $partial = 0)
|
public function updateBalances($balanceAdjustment, $partial = 0)
|
||||||
{
|
{
|
||||||
if ($this->is_deleted) {
|
if ($this->is_deleted) {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
<?php namespace App\Services;
|
<?php namespace App\Services;
|
||||||
|
|
||||||
|
use Auth;
|
||||||
use Utils;
|
use Utils;
|
||||||
use URL;
|
use URL;
|
||||||
use App\Services\BaseService;
|
use App\Services\BaseService;
|
||||||
@ -62,20 +63,13 @@ class InvoiceService extends BaseService
|
|||||||
return $invoice;
|
return $invoice;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function approveQuote($quote, $invitation = null)
|
public function convertQuote($quote, $invitation = null)
|
||||||
{
|
{
|
||||||
if (!$quote->is_quote || $quote->quote_invoice_id) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$invoice = $this->invoiceRepo->cloneInvoice($quote, $quote->id);
|
$invoice = $this->invoiceRepo->cloneInvoice($quote, $quote->id);
|
||||||
|
|
||||||
if (!$invitation) {
|
if (!$invitation) {
|
||||||
return $invoice;
|
return $invoice;
|
||||||
}
|
}
|
||||||
|
|
||||||
event(new QuoteInvitationWasApproved($quote, $invoice, $invitation));
|
|
||||||
|
|
||||||
foreach ($invoice->invitations as $invoiceInvitation) {
|
foreach ($invoice->invitations as $invoiceInvitation) {
|
||||||
if ($invitation->contact_id == $invoiceInvitation->contact_id) {
|
if ($invitation->contact_id == $invoiceInvitation->contact_id) {
|
||||||
return $invoiceInvitation->invitation_key;
|
return $invoiceInvitation->invitation_key;
|
||||||
@ -83,6 +77,32 @@ class InvoiceService extends BaseService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function approveQuote($quote, $invitation = null)
|
||||||
|
{
|
||||||
|
$account = Auth::user()->account;
|
||||||
|
if (!$quote->is_quote || $quote->quote_invoice_id) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($account->auto_convert_quote) {
|
||||||
|
$invoice = $this->convertQuote($quote, $invitation);
|
||||||
|
|
||||||
|
event(new QuoteInvitationWasApproved($quote, $invoice, $invitation));
|
||||||
|
|
||||||
|
return $invoice;
|
||||||
|
} else {
|
||||||
|
$quote->markApproved();
|
||||||
|
|
||||||
|
event(new QuoteInvitationWasApproved($quote, null, $invitation));
|
||||||
|
|
||||||
|
foreach ($quote->invitations as $invoiceInvitation) {
|
||||||
|
if ($invitation->contact_id == $invoiceInvitation->contact_id) {
|
||||||
|
return $invoiceInvitation->invitation_key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function getDatatable($accountId, $clientPublicId = null, $entityType, $search)
|
public function getDatatable($accountId, $clientPublicId = null, $entityType, $search)
|
||||||
{
|
{
|
||||||
$query = $this->invoiceRepo->getInvoices($accountId, $clientPublicId, $entityType, $search)
|
$query = $this->invoiceRepo->getInvoices($accountId, $clientPublicId, $entityType, $search)
|
||||||
@ -227,6 +247,9 @@ class InvoiceService extends BaseService
|
|||||||
case INVOICE_STATUS_VIEWED:
|
case INVOICE_STATUS_VIEWED:
|
||||||
$class = 'warning';
|
$class = 'warning';
|
||||||
break;
|
break;
|
||||||
|
case INVOICE_STATUS_APPROVED:
|
||||||
|
$class = 'success';
|
||||||
|
break;
|
||||||
case INVOICE_STATUS_PARTIAL:
|
case INVOICE_STATUS_PARTIAL:
|
||||||
$class = 'primary';
|
$class = 'primary';
|
||||||
break;
|
break;
|
||||||
|
@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
|
||||||
|
class AddQuoteToInvoiceOption extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
* Make the conversion of a quote into an invoice automatically after a client approves optional.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::table('accounts', function (Blueprint $table) {
|
||||||
|
$table->boolean('auto_convert_quote')->default(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
// we need to create the last status to resolve a foreign key constraint
|
||||||
|
DB::table('invoice_statuses')->insert([
|
||||||
|
'id' => 6,
|
||||||
|
'name' => 'Paid'
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::table('invoices')
|
||||||
|
->whereIn('invoice_status_id', [4, 5])
|
||||||
|
->increment('invoice_status_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::table('accounts', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('auto_convert_quote');
|
||||||
|
});
|
||||||
|
|
||||||
|
DB::table('invoices')
|
||||||
|
->whereIn('invoice_status_id', [5, 6])
|
||||||
|
->decrement('invoice_status_id');
|
||||||
|
}
|
||||||
|
}
|
@ -50,12 +50,6 @@ class ConstantsSeeder extends Seeder
|
|||||||
Theme::create(array('name' => 'united'));
|
Theme::create(array('name' => 'united'));
|
||||||
Theme::create(array('name' => 'yeti'));
|
Theme::create(array('name' => 'yeti'));
|
||||||
|
|
||||||
InvoiceStatus::create(array('name' => 'Draft'));
|
|
||||||
InvoiceStatus::create(array('name' => 'Sent'));
|
|
||||||
InvoiceStatus::create(array('name' => 'Viewed'));
|
|
||||||
InvoiceStatus::create(array('name' => 'Partial'));
|
|
||||||
InvoiceStatus::create(array('name' => 'Paid'));
|
|
||||||
|
|
||||||
Frequency::create(array('name' => 'Weekly'));
|
Frequency::create(array('name' => 'Weekly'));
|
||||||
Frequency::create(array('name' => 'Two weeks'));
|
Frequency::create(array('name' => 'Two weeks'));
|
||||||
Frequency::create(array('name' => 'Four weeks'));
|
Frequency::create(array('name' => 'Four weeks'));
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
class DatabaseSeeder extends Seeder {
|
class DatabaseSeeder extends Seeder
|
||||||
|
{
|
||||||
/**
|
/**
|
||||||
* Run the database seeds.
|
* Run the database seeds.
|
||||||
*
|
*
|
||||||
@ -18,6 +18,7 @@ class DatabaseSeeder extends Seeder {
|
|||||||
$this->call('PaymentLibrariesSeeder');
|
$this->call('PaymentLibrariesSeeder');
|
||||||
$this->call('FontsSeeder');
|
$this->call('FontsSeeder');
|
||||||
$this->call('BanksSeeder');
|
$this->call('BanksSeeder');
|
||||||
|
$this->call('FontsSeeder');
|
||||||
|
$this->call('InvoiceStatusSeeder');
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
38
database/seeds/InvoiceStatusSeeder.php
Normal file
38
database/seeds/InvoiceStatusSeeder.php
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\InvoiceStatus;
|
||||||
|
|
||||||
|
class InvoiceStatusSeeder extends Seeder
|
||||||
|
{
|
||||||
|
public function run()
|
||||||
|
{
|
||||||
|
Eloquent::unguard();
|
||||||
|
|
||||||
|
$this->createInvoiceStatuses();
|
||||||
|
|
||||||
|
Eloquent::reguard();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createInvoiceStatuses()
|
||||||
|
{
|
||||||
|
$statuses = [
|
||||||
|
['id' => '1', 'name' => 'Draft'],
|
||||||
|
['id' => '2', 'name' => 'Sent'],
|
||||||
|
['id' => '3', 'name' => 'Viewed'],
|
||||||
|
['id' => '4', 'name' => 'Approved'],
|
||||||
|
['id' => '5', 'name' => 'Partial'],
|
||||||
|
['id' => '6', 'name' => 'Paid'],
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($statuses as $status) {
|
||||||
|
$record = InvoiceStatus::find($status['id']);
|
||||||
|
if ($record) {
|
||||||
|
$record->name = $status['name'];
|
||||||
|
$record->save();
|
||||||
|
} else {
|
||||||
|
InvoiceStatus::create($status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -30394,8 +30394,9 @@ var CONSTS = {};
|
|||||||
CONSTS.INVOICE_STATUS_DRAFT = 1;
|
CONSTS.INVOICE_STATUS_DRAFT = 1;
|
||||||
CONSTS.INVOICE_STATUS_SENT = 2;
|
CONSTS.INVOICE_STATUS_SENT = 2;
|
||||||
CONSTS.INVOICE_STATUS_VIEWED = 3;
|
CONSTS.INVOICE_STATUS_VIEWED = 3;
|
||||||
CONSTS.INVOICE_STATUS_PARTIAL = 4;
|
CONSTS.INVOICE_STATUS_APPROVED = 4;
|
||||||
CONSTS.INVOICE_STATUS_PAID = 5;
|
CONSTS.INVOICE_STATUS_PARTIAL = 5;
|
||||||
|
CONSTS.INVOICE_STATUS_PAID = 6;
|
||||||
|
|
||||||
$.fn.datepicker.defaults.autoclose = true;
|
$.fn.datepicker.defaults.autoclose = true;
|
||||||
$.fn.datepicker.defaults.todayHighlight = true;
|
$.fn.datepicker.defaults.todayHighlight = true;
|
||||||
@ -30870,6 +30871,7 @@ function actionListHandler() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
var NINJA = NINJA || {};
|
var NINJA = NINJA || {};
|
||||||
|
|
||||||
NINJA.TEMPLATES = {
|
NINJA.TEMPLATES = {
|
||||||
|
@ -516,8 +516,9 @@ var CONSTS = {};
|
|||||||
CONSTS.INVOICE_STATUS_DRAFT = 1;
|
CONSTS.INVOICE_STATUS_DRAFT = 1;
|
||||||
CONSTS.INVOICE_STATUS_SENT = 2;
|
CONSTS.INVOICE_STATUS_SENT = 2;
|
||||||
CONSTS.INVOICE_STATUS_VIEWED = 3;
|
CONSTS.INVOICE_STATUS_VIEWED = 3;
|
||||||
CONSTS.INVOICE_STATUS_PARTIAL = 4;
|
CONSTS.INVOICE_STATUS_APPROVED = 4;
|
||||||
CONSTS.INVOICE_STATUS_PAID = 5;
|
CONSTS.INVOICE_STATUS_PARTIAL = 5;
|
||||||
|
CONSTS.INVOICE_STATUS_PAID = 6;
|
||||||
|
|
||||||
$.fn.datepicker.defaults.autoclose = true;
|
$.fn.datepicker.defaults.autoclose = true;
|
||||||
$.fn.datepicker.defaults.todayHighlight = true;
|
$.fn.datepicker.defaults.todayHighlight = true;
|
||||||
|
@ -1054,5 +1054,9 @@ return array(
|
|||||||
'username' => 'Username',
|
'username' => 'Username',
|
||||||
'account_number' => 'Account Number',
|
'account_number' => 'Account Number',
|
||||||
'bank_account_error' => 'Failed to retreive account details, please check your credentials.',
|
'bank_account_error' => 'Failed to retreive account details, please check your credentials.',
|
||||||
|
'status_approved' => 'Approved',
|
||||||
|
'quote_settings' => 'Quote Settings',
|
||||||
|
'auto_convert_quote' => 'Auto convert quote',
|
||||||
|
'auto_convert_quote_help' => 'When a client approves a quote automatically convert it to an invoice.',
|
||||||
|
|
||||||
);
|
);
|
@ -390,6 +390,7 @@ return array(
|
|||||||
'notification_quote_viewed_subject' => 'Offerte :invoice is bekeken door :client',
|
'notification_quote_viewed_subject' => 'Offerte :invoice is bekeken door :client',
|
||||||
'notification_quote_sent' => 'Klant :client heeft offerte :invoice voor :amount per email ontvangen.',
|
'notification_quote_sent' => 'Klant :client heeft offerte :invoice voor :amount per email ontvangen.',
|
||||||
'notification_quote_viewed' => 'Klant :client heeft offerte :invoice voor :amount bekeken.',
|
'notification_quote_viewed' => 'Klant :client heeft offerte :invoice voor :amount bekeken.',
|
||||||
|
'auto_convert_quote' => 'Offerte automatisch omzetten in factuur als deze goed gekeurd wordt',
|
||||||
|
|
||||||
'session_expired' => 'Uw sessie is verlopen.',
|
'session_expired' => 'Uw sessie is verlopen.',
|
||||||
|
|
||||||
@ -757,6 +758,7 @@ return array(
|
|||||||
'status_draft' => 'Concept',
|
'status_draft' => 'Concept',
|
||||||
'status_sent' => 'Verstuurd',
|
'status_sent' => 'Verstuurd',
|
||||||
'status_viewed' => 'Bekeken',
|
'status_viewed' => 'Bekeken',
|
||||||
|
'status_approved' => 'Goedgekeurd',
|
||||||
'status_partial' => 'Gedeeltelijk',
|
'status_partial' => 'Gedeeltelijk',
|
||||||
'status_paid' => 'Betaald',
|
'status_paid' => 'Betaald',
|
||||||
'show_line_item_tax' => '<b>BTW-tarieven per regel</b> tonen',
|
'show_line_item_tax' => '<b>BTW-tarieven per regel</b> tonen',
|
||||||
|
@ -22,6 +22,7 @@
|
|||||||
|
|
||||||
{!! Former::open()->rules(['iframe_url' => 'url'])->addClass('warn-on-exit') !!}
|
{!! Former::open()->rules(['iframe_url' => 'url'])->addClass('warn-on-exit') !!}
|
||||||
{{ Former::populate($account) }}
|
{{ Former::populate($account) }}
|
||||||
|
{{ Former::populateField('auto_convert_quote', intval($account->auto_convert_quote)) }}
|
||||||
{{ Former::populateField('custom_invoice_taxes1', intval($account->custom_invoice_taxes1)) }}
|
{{ Former::populateField('custom_invoice_taxes1', intval($account->custom_invoice_taxes1)) }}
|
||||||
{{ Former::populateField('custom_invoice_taxes2', intval($account->custom_invoice_taxes2)) }}
|
{{ Former::populateField('custom_invoice_taxes2', intval($account->custom_invoice_taxes2)) }}
|
||||||
{{ Former::populateField('share_counter', intval($account->share_counter)) }}
|
{{ Former::populateField('share_counter', intval($account->share_counter)) }}
|
||||||
@ -98,6 +99,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<h3 class="panel-title">{!! trans('texts.custom_fields') !!}</h3>
|
<h3 class="panel-title">{!! trans('texts.custom_fields') !!}</h3>
|
||||||
@ -172,6 +174,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h3 class="panel-title">{!! trans('texts.quote_settings') !!}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body form-padding-right">
|
||||||
|
{!! Former::checkbox('auto_convert_quote')
|
||||||
|
->text(trans('texts.enable'))
|
||||||
|
->blockHelp(trans('texts.auto_convert_quote_help')) !!}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<h3 class="panel-title">{!! trans('texts.default_messages') !!}</h3>
|
<h3 class="panel-title">{!! trans('texts.default_messages') !!}</h3>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user