Update Purchase Order Scaffold

This commit is contained in:
David Bomba 2022-06-05 19:22:58 +10:00
commit 4a2ecdb6a7
72 changed files with 530191 additions and 526330 deletions

View File

@ -1 +1 @@
5.3.93
5.3.96

View File

@ -103,7 +103,7 @@ class Kernel extends ConsoleKernel
if (config('queue.default') == 'database' && Ninja::isSelfHost() && config('ninja.internal_queue_enabled') && !config('ninja.is_docker')) {
$schedule->command('queue:work database --stop-when-empty')->everyMinute()->withoutOverlapping();
$schedule->command('queue:work database --stop-when-empty --memory=256')->everyMinute()->withoutOverlapping();
$schedule->command('queue:restart')->everyFiveMinutes()->withoutOverlapping();

View File

@ -98,11 +98,11 @@ class CompanySettings extends BaseSettings
public $expense_number_pattern = ''; //@implemented
public $expense_number_counter = 1; //@implemented
public $recurring_expense_number_pattern = '';
public $recurring_expense_number_counter = 1;
public $recurring_expense_number_pattern = '';
public $recurring_expense_number_counter = 1;
public $recurring_quote_number_pattern = '';
public $recurring_quote_number_counter = 1;
public $recurring_quote_number_pattern = '';
public $recurring_quote_number_counter = 1;
public $vendor_number_pattern = ''; //@implemented
public $vendor_number_counter = 1; //@implemented
@ -279,6 +279,9 @@ class CompanySettings extends BaseSettings
public $email_from_name = '';
public $auto_archive_invoice_cancelled = false;
public $purchase_order_number_counter = 1; //TODO
public static $casts = [
'purchase_order_number_pattern' => 'purchase_order_number_pattern',
'purchase_order_number_counter' => 'int',
@ -479,6 +482,7 @@ class CompanySettings extends BaseSettings
'portal_custom_footer' => 'string',
'portal_custom_js' => 'string',
'client_portal_enable_uploads' => 'bool',
'purchase_order_number_counter' => 'integer',
];
public static $free_plan_casts = [

View File

@ -0,0 +1,38 @@
<?php
namespace App\Events\PurchaseOrder;
use App\Models\Company;
use App\Models\PurchaseOrder;
use Illuminate\Queue\SerializesModels;
/**
* Class PurchaseOrderWasMarkedSent.
*/
class PurchaseOrderWasMarkedSent
{
use SerializesModels;
/**
* @var \App\Models\PurchaseOrder
*/
public $purchase_order;
public $company;
public $event_vars;
/**
* Create a new event instance.
*
* @param PurchaseOrder $purchase_order
* @param Company $company
* @param array $event_vars
*/
public function __construct(PurchaseOrder $purchase_order, Company $company, array $event_vars)
{
$this->purchase_order = $purchase_order;
$this->company = $company;
$this->event_vars = $event_vars;
}
}

View File

@ -78,7 +78,7 @@ class QuoteItemExport extends BaseExport
'tax_name2' => 'item.tax_name2',
'tax_name3' => 'item.tax_name3',
'line_total' => 'item.line_total',
'gross_line_total' => 'item.gross_line_total',
// 'gross_line_total' => 'item.gross_line_total',
'custom_value1' => 'item.custom_value1',
'custom_value2' => 'item.custom_value2',
'custom_value3' => 'item.custom_value3',

View File

@ -0,0 +1,31 @@
<?php
namespace App\Factory;
use App\Models\PurchaseOrderInvitation;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
class PurchaseOrderInvitationFactory
{
public static function create(int $company_id, int $user_id) :PurchaseOrderInvitation
{
$ci = new PurchaseOrderInvitation();
$ci->company_id = $company_id;
$ci->user_id = $user_id;
$ci->vendor_contact_id = null;
$ci->purchase_order_id = null;
$ci->key = Str::random(config('ninja.key_length'));
$ci->transaction_reference = null;
$ci->message_id = null;
$ci->email_error = '';
$ci->signature_base64 = '';
$ci->signature_date = null;
$ci->sent_date = null;
$ci->viewed_date = null;
$ci->opened_date = null;
return $ci;
}
}

View File

@ -48,6 +48,7 @@ class RecurringInvoiceFactory
$invoice->frequency_id = RecurringInvoice::FREQUENCY_MONTHLY;
$invoice->last_sent_date = null;
$invoice->next_send_date = null;
$invoice->next_send_date_client = null;
$invoice->remaining_cycles = -1;
$invoice->paid_to_date = 0;
$invoice->auto_bill_enabled = false;

View File

@ -22,6 +22,7 @@ use App\Models\ClientContact;
use App\Models\CreditInvitation;
use App\Models\InvoiceInvitation;
use App\Models\Payment;
use App\Models\PurchaseOrderInvitation;
use App\Models\QuoteInvitation;
use App\Services\ClientPortal\InstantPayment;
use App\Utils\CurlUtils;
@ -41,7 +42,7 @@ class InvitationController extends Controller
use MakesDates;
public function router(string $entity, string $invitation_key)
{
{
Auth::logout();
return $this->genericRouter($entity, $invitation_key);
@ -166,7 +167,7 @@ class InvitationController extends Controller
{
set_time_limit(45);
if(Ninja::isHosted())
return $this->returnRawPdf($entity, $invitation_key);
@ -202,7 +203,7 @@ class InvitationController extends Controller
return response()->streamDownload(function () use($file) {
echo $file;
}, $file_name, $headers);
}
public function routerForIframe(string $entity, string $client_hash, string $invitation_key)
@ -228,14 +229,14 @@ class InvitationController extends Controller
$invitation = InvoiceInvitation::where('key', $invitation_key)
->with('contact.client')
->firstOrFail();
auth()->guard('contact')->loginUsingId($invitation->contact->id, true);
$invoice = $invitation->invoice;
if($invoice->partial > 0)
$amount = round($invoice->partial, (int)$invoice->client->currency()->precision);
else
else
$amount = round($invoice->balance, (int)$invoice->client->currency()->precision);
$gateways = $invitation->contact->client->service()->getPaymentMethods($amount);
@ -279,6 +280,10 @@ class InvitationController extends Controller
$invite = CreditInvitation::withTrashed()->where('key', $invitation_key)->first();
$invite->contact->send_email = false;
$invite->contact->save();
}elseif($entity == 'purchase_order'){
$invite = PurchaseOrderInvitation::withTrashed()->where('key', $invitation_key)->first();
$invite->contact->send_email = false;
$invite->contact->save();
}
else
return abort(404);

View File

@ -204,9 +204,9 @@ class RecurringInvoiceController extends BaseController
{
$recurring_invoice = $this->recurring_invoice_repo->save($request->all(), RecurringInvoiceFactory::create(auth()->user()->company()->id, auth()->user()->id));
$offset = $recurring_invoice->client->timezone_offset();
$recurring_invoice->next_send_date = Carbon::parse($recurring_invoice->next_send_date)->startOfDay()->addSeconds($offset);
$recurring_invoice->saveQuietly();
// $offset = $recurring_invoice->client->timezone_offset();
// $recurring_invoice->next_send_date = Carbon::parse($recurring_invoice->next_send_date)->startOfDay()->addSeconds($offset);
// $recurring_invoice->saveQuietly();
$recurring_invoice->service()
->triggeredActions($request)

View File

@ -16,6 +16,7 @@ use App\Models\Client;
use App\Utils\Ninja;
use App\Utils\Traits\AppSetup;
use App\Utils\Traits\ClientGroupSettingsSaver;
use Beganovich\Snappdf\Snappdf;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Storage;
@ -134,21 +135,30 @@ class SelfUpdateController extends BaseController
nlog("Extracting zip");
// $zipFile = new \PhpZip\ZipFile();
// try{
// $s = new Snappdf;
// $s->getChromiumPath();
// chmod($this->generatePlatformExecutable($s->getChromiumPath()), 0755);
// }
// catch(\Exception $e){
// nlog("I could not set the file permissions for chrome");
// }
// $zipFile->openFile($file);
$zipFile = new \PhpZip\ZipFile();
// $zipFile->extractTo(base_path());
$zipFile->openFile($file);
// $zipFile->close();
$zipFile->extractTo(base_path());
$zip = new \ZipArchive;
$zipFile->close();
// $zip = new \ZipArchive;
$res = $zip->open($file);
if ($res === TRUE) {
$zip->extractTo(base_path());
$zip->close();
}
// $res = $zip->open($file);
// if ($res === TRUE) {
// $zip->extractTo(base_path());
// $zip->close();
// }
nlog("Finished extracting files");

View File

@ -55,6 +55,10 @@ class StoreRecurringExpenseRequest extends Request
$input = $this->decodePrimaryKeys($input);
if (array_key_exists('next_send_date', $input) && is_string($input['next_send_date'])) {
$input['next_send_date_client'] = $input['next_send_date'];
}
if (array_key_exists('category_id', $input) && is_string($input['category_id'])) {
$input['category_id'] = $this->decodePrimaryKey($input['category_id']);
}

View File

@ -66,6 +66,10 @@ class UpdateRecurringExpenseRequest extends Request
$input = $this->decodePrimaryKeys($input);
if (array_key_exists('next_send_date', $input) && is_string($input['next_send_date'])) {
$input['next_send_date_client'] = $input['next_send_date'];
}
if (array_key_exists('category_id', $input) && is_string($input['category_id'])) {
$input['category_id'] = $this->decodePrimaryKey($input['category_id']);
}

View File

@ -67,6 +67,10 @@ class StoreRecurringInvoiceRequest extends Request
{
$input = $this->all();
if (array_key_exists('next_send_date', $input) && is_string($input['next_send_date'])) {
$input['next_send_date_client'] = $input['next_send_date'];
}
if (array_key_exists('design_id', $input) && is_string($input['design_id'])) {
$input['design_id'] = $this->decodePrimaryKey($input['design_id']);
}

View File

@ -61,6 +61,10 @@ class UpdateRecurringInvoiceRequest extends Request
{
$input = $this->all();
if (array_key_exists('next_send_date', $input) && is_string($input['next_send_date'])) {
$input['next_send_date_client'] = $input['next_send_date'];
}
if (array_key_exists('design_id', $input) && is_string($input['design_id'])) {
$input['design_id'] = $this->decodePrimaryKey($input['design_id']);
}

View File

@ -562,7 +562,7 @@ class BaseImport
}
}
protected function finalizeImport()
public function finalizeImport()
{
$data = [
'errors' => $this->error_array,

View File

@ -60,10 +60,7 @@ class Csv extends BaseImport implements ImportInterface
) {
$this->{$entity}();
}
//collate any errors
$this->finalizeImport();
}
public function client()

View File

@ -41,7 +41,7 @@ class Freshbooks extends BaseImport
//collate any errors
$this->finalizeImport();
// $this->finalizeImport();
}
public function client()

View File

@ -39,7 +39,7 @@ class Invoice2Go extends BaseImport
//collate any errors
$this->finalizeImport();
// $this->finalizeImport();
}

View File

@ -38,7 +38,7 @@ class Invoicely extends BaseImport
//collate any errors
$this->finalizeImport();
// $this->finalizeImport();
}
public function client()

View File

@ -54,7 +54,7 @@ class Wave extends BaseImport implements ImportInterface
//collate any errors
$this->finalizeImport();
// $this->finalizeImport();
}
public function client()

View File

@ -40,7 +40,7 @@ class Zoho extends BaseImport
//collate any errors
$this->finalizeImport();
// $this->finalizeImport();
}
public function client()

View File

@ -94,6 +94,8 @@ class RecurringExpensesCron
$expense->save();
$recurring_expense->next_send_date = $recurring_expense->nextSendDate();
$recurring_expense->next_send_date_client = $recurring_expense->next_send_date;
$recurring_expense->remaining_cycles = $recurring_expense->remainingCycles();
$recurring_expense->save();
}

View File

@ -79,6 +79,8 @@ class CSVIngest implements ShouldQueue {
}
$engine->finalizeImport();
$this->checkContacts();
}

View File

@ -0,0 +1,95 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Jobs\Inventory;
use App\Libraries\MultiDB;
use App\Models\Company;
use App\Models\Invoice;
use App\Models\Product;
use App\Utils\Traits\NumberFormatter;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
//todo - ensure we are MultiDB Aware in dispatched jobs
class AdjustProductInventory implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public Company $company;
public Invoice $invoice;
public array $old_invoice;
public function __construct(Company $company, Invoice $invoice, array $old_invoice = [])
{
$this->company = $company;
$this->invoice = $invoice;
$this->old_invoice = $old_invoice;
}
/**
* Execute the job.
*
*
* @return false
*/
public function handle()
{
MultiDB::setDb($this->company->db);
if(count($this->old_invoice) > 0)
return $this->existingInventoryAdjustment();
return $this->newInventoryAdjustment();
}
private function newInventoryAdjustment()
{
$line_items = $this->invoice->line_items;
foreach($line_items as $item)
{
$p = Product::where('product_key', $item->product_key)->where('company_id', $this->company->id)->where('in_stock_quantity', '>', 0)->first();
$p->in_stock_quantity -= $item->quantity;
$p->save();
//check threshols and notify user
if($p->stock_notification_threshold && $p->in_stock_quantity <= $p->stock_notification_threshold)
$this->notifyStockLevels($p, 'product');
elseif($this->company->stock_notification_threshold && $p->in_stock_quantity <= $this->company->stock_notification_threshold){
$this->notifyStocklevels($p, 'company');
}
}
}
private function existingInventoryAdjustment()
{
}
private function notifyStocklevels(Product $product, string $notification_level)
{
}
}

View File

@ -105,6 +105,7 @@ class SendRecurring implements ShouldQueue
nlog("updating recurring invoice dates");
/* Set next date here to prevent a recurring loop forming */
$this->recurring_invoice->next_send_date = $this->recurring_invoice->nextSendDate();
$this->recurring_invoice->next_send_date_client = $this->recurring_invoice->nextSendDateClient();
$this->recurring_invoice->remaining_cycles = $this->recurring_invoice->remainingCycles();
$this->recurring_invoice->last_sent_date = now();

View File

@ -666,7 +666,7 @@ class Client extends BaseModel implements HasLocalePreference
$offset -= $timezone->utc_offset;
$offset += ($entity_send_time * 3600);
return $offset;
}

View File

@ -103,6 +103,10 @@ class Company extends BaseModel
'markdown_email_enabled',
'stop_on_unpaid_recurring',
'use_quote_terms_on_conversion',
'enable_applying_payments',
'track_inventory',
'inventory_notification_threshold',
'stock_notification'
];
protected $hidden = [

View File

@ -36,6 +36,9 @@ class Product extends BaseModel
'tax_rate1',
'tax_rate2',
'tax_rate3',
'in_stock_quantity',
'stock_notification_threshold',
'stock_notification',
];
protected $touches = [];

View File

@ -12,10 +12,12 @@
namespace App\Models;
use App\Helpers\Invoice\InvoiceSum;
use App\Helpers\Invoice\InvoiceSumInclusive;
use App\Jobs\Entity\CreateEntityPdf;
use App\Services\PurchaseOrder\PurchaseOrderService;
use App\Utils\Ninja;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Storage;
class PurchaseOrder extends BaseModel
{
@ -94,9 +96,8 @@ class PurchaseOrder extends BaseModel
const STATUS_DRAFT = 1;
const STATUS_SENT = 2;
const STATUS_APPROVED = 3;
const STATUS_CONVERTED = 4;
const STATUS_EXPIRED = -1;
const STATUS_PARTIAL = 3;
const STATUS_APPLIED = 4;
public function assigned_user()
{
@ -132,10 +133,52 @@ class PurchaseOrder extends BaseModel
{
return $this->belongsTo(Client::class)->withTrashed();
}
public function markInvitationsSent()
{
$this->invitations->each(function ($invitation) {
if (! isset($invitation->sent_date)) {
$invitation->sent_date = Carbon::now();
$invitation->save();
}
});
}
public function pdf_file_path($invitation = null, string $type = 'path', bool $portal = false)
{
if (! $invitation) {
if($this->invitations()->exists())
$invitation = $this->invitations()->first();
else{
$this->service()->createInvitations();
$invitation = $this->invitations()->first();
}
}
if(!$invitation)
throw new \Exception('Hard fail, could not create an invitation - is there a valid contact?');
$file_path = $this->client->credit_filepath($invitation).$this->numberFormatter().'.pdf';
if(Ninja::isHosted() && $portal && Storage::disk(config('filesystems.default'))->exists($file_path)){
return Storage::disk(config('filesystems.default'))->{$type}($file_path);
}
elseif(Ninja::isHosted() && $portal){
$file_path = CreateEntityPdf::dispatchNow($invitation,config('filesystems.default'));
return Storage::disk(config('filesystems.default'))->{$type}($file_path);
}
if(Storage::disk('public')->exists($file_path))
return Storage::disk('public')->{$type}($file_path);
$file_path = CreateEntityPdf::dispatchNow($invitation);
return Storage::disk('public')->{$type}($file_path);
}
public function invitations()
{
return $this->hasMany(CreditInvitation::class);
return $this->hasMany(PurchaseOrderInvitation::class);
}
public function project()
@ -168,17 +211,4 @@ class PurchaseOrder extends BaseModel
return $this->morphMany(Document::class, 'documentable');
}
public function calc()
{
$credit_calc = null;
if ($this->uses_inclusive_taxes) {
$credit_calc = new InvoiceSumInclusive($this);
} else {
$credit_calc = new InvoiceSum($this);
}
return $credit_calc->build();
}
}

View File

@ -0,0 +1,81 @@
<?php
namespace App\Models;
use App\Utils\Traits\Inviteable;
use App\Utils\Traits\MakesDates;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\SoftDeletes;
class PurchaseOrderInvitation extends BaseModel
{
use MakesDates;
use SoftDeletes;
use Inviteable;
protected $fillable = [
'id',
'vendor_contact_id',
];
protected $with = [
'company',
'contact',
];
protected $touches = ['purchase_order'];
public function getEntityType()
{
return self::class;
}
public function entityType()
{
return PurchaseOrder::class;
}
/**
* @return mixed
*/
public function purchase_order()
{
return $this->belongsTo(PurchaseOrder::class)->withTrashed();
}
/**
* @return mixed
*/
public function contact()
{
return $this->belongsTo(VendorContact::class, 'vendor_contact_id', 'id')->withTrashed();
}
/**
* @return mixed
*/
public function user()
{
return $this->belongsTo(User::class)->withTrashed();
}
public function company()
{
return $this->belongsTo(Company::class);
}
public function getName()
{
return $this->key;
}
public function markViewed()
{
$this->viewed_date = Carbon::now();
$this->save();
}
}

View File

@ -63,6 +63,7 @@ class RecurringExpense extends BaseModel
'last_sent_date',
'next_send_date',
'remaining_cycles',
'next_send_date_client',
];
protected $casts = [
@ -153,6 +154,43 @@ class RecurringExpense extends BaseModel
}
}
public function nextSendDateClient() :?Carbon
{
if (!$this->next_send_date) {
return null;
}
switch ($this->frequency_id) {
case RecurringInvoice::FREQUENCY_DAILY:
return Carbon::parse($this->next_send_date)->startOfDay()->addDay();
case RecurringInvoice::FREQUENCY_WEEKLY:
return Carbon::parse($this->next_send_date)->startOfDay()->addWeek();
case RecurringInvoice::FREQUENCY_TWO_WEEKS:
return Carbon::parse($this->next_send_date)->startOfDay()->addWeeks(2);
case RecurringInvoice::FREQUENCY_FOUR_WEEKS:
return Carbon::parse($this->next_send_date)->startOfDay()->addWeeks(4);
case RecurringInvoice::FREQUENCY_MONTHLY:
return Carbon::parse($this->next_send_date)->startOfDay()->addMonthNoOverflow();
case RecurringInvoice::FREQUENCY_TWO_MONTHS:
return Carbon::parse($this->next_send_date)->startOfDay()->addMonthsNoOverflow(2);
case RecurringInvoice::FREQUENCY_THREE_MONTHS:
return Carbon::parse($this->next_send_date)->startOfDay()->addMonthsNoOverflow(3);
case RecurringInvoice::FREQUENCY_FOUR_MONTHS:
return Carbon::parse($this->next_send_date)->startOfDay()->addMonthsNoOverflow(4);
case RecurringInvoice::FREQUENCY_SIX_MONTHS:
return Carbon::parse($this->next_send_date)->startOfDay()->addMonthsNoOverflow(6);
case RecurringInvoice::FREQUENCY_ANNUALLY:
return Carbon::parse($this->next_send_date)->startOfDay()->addYear();
case RecurringInvoice::FREQUENCY_TWO_YEARS:
return Carbon::parse($this->next_send_date)->startOfDay()->addYears(2);
case RecurringInvoice::FREQUENCY_THREE_YEARS:
return Carbon::parse($this->next_send_date)->startOfDay()->addYears(3);
default:
return null;
}
}
public function remainingCycles() : int
{
if ($this->remaining_cycles == 0) {

View File

@ -108,6 +108,7 @@ class RecurringInvoice extends BaseModel
'assigned_user_id',
'exchange_rate',
'vendor_id',
'next_send_date_client',
];
protected $casts = [
@ -224,7 +225,7 @@ class RecurringInvoice extends BaseModel
public function nextSendDate() :?Carbon
{
if (!$this->next_send_date) {
if (!$this->next_send_date_client) {
return null;
}
@ -236,49 +237,93 @@ class RecurringInvoice extends BaseModel
/* Lets set the next send date to now so we increment from today, rather than in the past*/
if(Carbon::parse($this->next_send_date)->lt(now()->subDays(3)))
$this->next_send_date = now()->format('Y-m-d');
$this->next_send_date_client = now()->format('Y-m-d');
}
/*
As we are firing at UTC+0 if our offset is negative it is technically firing the day before so we always need
to add ON a day - a day = 86400 seconds
*/
if($offset < 0)
$offset += 86400;
// if($offset < 0)
// $offset += 86400;
switch ($this->frequency_id) {
case self::FREQUENCY_DAILY:
return Carbon::parse($this->next_send_date)->startOfDay()->addDay()->addSeconds($offset);
return Carbon::parse($this->next_send_date_client)->startOfDay()->addDay()->addSeconds($offset);
case self::FREQUENCY_WEEKLY:
return Carbon::parse($this->next_send_date)->startOfDay()->addWeek()->addSeconds($offset);
return Carbon::parse($this->next_send_date_client)->startOfDay()->addWeek()->addSeconds($offset);
case self::FREQUENCY_TWO_WEEKS:
return Carbon::parse($this->next_send_date)->startOfDay()->addWeeks(2)->addSeconds($offset);
return Carbon::parse($this->next_send_date_client)->startOfDay()->addWeeks(2)->addSeconds($offset);
case self::FREQUENCY_FOUR_WEEKS:
return Carbon::parse($this->next_send_date)->startOfDay()->addWeeks(4)->addSeconds($offset);
return Carbon::parse($this->next_send_date_client)->startOfDay()->addWeeks(4)->addSeconds($offset);
case self::FREQUENCY_MONTHLY:
return Carbon::parse($this->next_send_date)->startOfDay()->addMonthNoOverflow()->addSeconds($offset);
return Carbon::parse($this->next_send_date_client)->startOfDay()->addMonthNoOverflow()->addSeconds($offset);
case self::FREQUENCY_TWO_MONTHS:
return Carbon::parse($this->next_send_date)->startOfDay()->addMonthsNoOverflow(2)->addSeconds($offset);
return Carbon::parse($this->next_send_date_client)->startOfDay()->addMonthsNoOverflow(2)->addSeconds($offset);
case self::FREQUENCY_THREE_MONTHS:
return Carbon::parse($this->next_send_date)->startOfDay()->addMonthsNoOverflow(3)->addSeconds($offset);
return Carbon::parse($this->next_send_date_client)->startOfDay()->addMonthsNoOverflow(3)->addSeconds($offset);
case self::FREQUENCY_FOUR_MONTHS:
return Carbon::parse($this->next_send_date)->startOfDay()->addMonthsNoOverflow(4)->addSeconds($offset);
return Carbon::parse($this->next_send_date_client)->startOfDay()->addMonthsNoOverflow(4)->addSeconds($offset);
case self::FREQUENCY_SIX_MONTHS:
return Carbon::parse($this->next_send_date)->startOfDay()->addMonthsNoOverflow(6)->addSeconds($offset);
return Carbon::parse($this->next_send_date_client)->startOfDay()->addMonthsNoOverflow(6)->addSeconds($offset);
case self::FREQUENCY_ANNUALLY:
return Carbon::parse($this->next_send_date)->startOfDay()->addYear()->addSeconds($offset);
return Carbon::parse($this->next_send_date_client)->startOfDay()->addYear()->addSeconds($offset);
case self::FREQUENCY_TWO_YEARS:
return Carbon::parse($this->next_send_date)->startOfDay()->addYears(2)->addSeconds($offset);
return Carbon::parse($this->next_send_date_client)->startOfDay()->addYears(2)->addSeconds($offset);
case self::FREQUENCY_THREE_YEARS:
return Carbon::parse($this->next_send_date)->startOfDay()->addYears(3)->addSeconds($offset);
return Carbon::parse($this->next_send_date_client)->startOfDay()->addYears(3)->addSeconds($offset);
default:
return null;
}
}
public function nextSendDateClient() :?Carbon
{
if (!$this->next_send_date_client) {
return null;
}
/* If this setting is enabled, the recurring invoice may be set in the past */
if($this->company->stop_on_unpaid_recurring) {
/* Lets set the next send date to now so we increment from today, rather than in the past*/
if(Carbon::parse($this->next_send_date)->lt(now()->subDays(3)))
$this->next_send_date_client = now()->format('Y-m-d');
}
switch ($this->frequency_id) {
case self::FREQUENCY_DAILY:
return Carbon::parse($this->next_send_date_client)->startOfDay()->addDay();
case self::FREQUENCY_WEEKLY:
return Carbon::parse($this->next_send_date_client)->startOfDay()->addWeek();
case self::FREQUENCY_TWO_WEEKS:
return Carbon::parse($this->next_send_date_client)->startOfDay()->addWeeks(2);
case self::FREQUENCY_FOUR_WEEKS:
return Carbon::parse($this->next_send_date_client)->startOfDay()->addWeeks(4);
case self::FREQUENCY_MONTHLY:
return Carbon::parse($this->next_send_date_client)->startOfDay()->addMonthNoOverflow();
case self::FREQUENCY_TWO_MONTHS:
return Carbon::parse($this->next_send_date_client)->startOfDay()->addMonthsNoOverflow(2);
case self::FREQUENCY_THREE_MONTHS:
return Carbon::parse($this->next_send_date_client)->startOfDay()->addMonthsNoOverflow(3);
case self::FREQUENCY_FOUR_MONTHS:
return Carbon::parse($this->next_send_date_client)->startOfDay()->addMonthsNoOverflow(4);
case self::FREQUENCY_SIX_MONTHS:
return Carbon::parse($this->next_send_date_client)->startOfDay()->addMonthsNoOverflow(6);
case self::FREQUENCY_ANNUALLY:
return Carbon::parse($this->next_send_date_client)->startOfDay()->addYear();
case self::FREQUENCY_TWO_YEARS:
return Carbon::parse($this->next_send_date_client)->startOfDay()->addYears(2);
case self::FREQUENCY_THREE_YEARS:
return Carbon::parse($this->next_send_date_client)->startOfDay()->addYears(3);
default:
return null;
}
}
public function nextDateByFrequency($date)
{
$offset = $this->client->timezone_offset();
@ -463,11 +508,11 @@ class RecurringInvoice extends BaseModel
$data = [];
if (!Carbon::parse($this->next_send_date)) {
if (!Carbon::parse($this->next_send_date_client)) {
return $data;
}
$next_send_date = Carbon::parse($this->next_send_date)->copy();
$next_send_date = Carbon::parse($this->next_send_date_client)->copy();
for ($x=0; $x<$iterations; $x++) {
// we don't add the days... we calc the day of the month!!

View File

@ -136,4 +136,8 @@ class VendorContact extends Authenticatable implements HasLocalePreference
->withTrashed()
->where('id', $this->decodePrimaryKey($value))->firstOrFail();
}
public function purchase_order_invitations(): \Illuminate\Database\Eloquent\Relations\HasMany
{
return $this->hasMany(PurchaseOrderInvitation::class);
}
}

View File

@ -12,11 +12,14 @@
namespace App\PaymentDrivers\Authorize;
use App\Models\Invoice;
use App\PaymentDrivers\AuthorizePaymentDriver;
use App\Utils\Traits\MakesHash;
use net\authorize\api\contract\v1\CreateTransactionRequest;
use net\authorize\api\contract\v1\CustomerProfilePaymentType;
use net\authorize\api\contract\v1\OrderType;
use net\authorize\api\contract\v1\PaymentProfileType;
use net\authorize\api\contract\v1\ExtendedAmountType;
use net\authorize\api\contract\v1\TransactionRequestType;
use net\authorize\api\controller\CreateTransactionController;
@ -25,6 +28,8 @@ use net\authorize\api\controller\CreateTransactionController;
*/
class ChargePaymentProfile
{
use MakesHash;
public function __construct(AuthorizePaymentDriver $authorize)
{
$this->authorize = $authorize;
@ -44,19 +49,40 @@ class ChargePaymentProfile
$profileToCharge->setPaymentProfile($paymentProfile);
$invoice_numbers = '';
$taxAmount = 0;
$invoiceTotal = 0;
$invoiceTaxes = 0;
if($this->authorize->payment_hash->data)
$invoice_numbers = collect($this->authorize->payment_hash->data->invoices)->pluck('invoice_number')->implode(',');
if($this->authorize->payment_hash->data) {
$invoice_numbers = collect($this->authorize->payment_hash->data->invoices)->pluck('invoice_number')->implode(",");
$invObj = Invoice::whereIn('id', $this->transformKeys(array_column($this->authorize->payment_hash->invoices(), 'invoice_id')))->withTrashed()->get();
$invoiceTotal = round($invObj->pluck('amount')->sum(), 2);
$invoiceTaxes = round($invObj->pluck('total_taxes')->sum(), 2);
if ($invoiceTotal != $amount) {
$taxRatio = $amount/$invoiceTotal;
$taxAmount = round($invoiceTaxes*$taxRatio, 2);
} else {
$taxAmount = $invoiceTaxes;
}
}
$description = "Invoices: {$invoice_numbers} for {$amount} for client {$this->authorize->client->present()->name()}";
$order = new OrderType();
$order->setInvoiceNumber(substr($invoice_numbers,0,19));
$order->setDescription(substr($description,0,255));
$tax = new ExtendedAmountType();
$tax->setName('tax');
$tax->setAmount($taxAmount);
$transactionRequestType = new TransactionRequestType();
$transactionRequestType->setTransactionType('authCaptureTransaction');
$transactionRequestType->setAmount($amount);
$transactionRequestType->setTax($tax);
$transactionRequestType->setTaxExempt(empty($taxAmount));
$transactionRequestType->setOrder($order);
$transactionRequestType->setProfile($profileToCharge);
$transactionRequestType->setCurrencyCode($this->authorize->client->currency()->code);

View File

@ -245,7 +245,7 @@ class GoCardlessPaymentDriver extends BaseDriver
sleep(1);
foreach ($request->events as $event) {
if ($event['action'] === 'confirmed' || $event['action'] === 'paid_out') {
if ($event['action'] === 'confirmed' || $event['action'] === 'paid_out' || $event['action'] === 'paid') {
nlog("Searching for transaction reference");

View File

@ -60,6 +60,7 @@ use App\Events\Payment\PaymentWasRefunded;
use App\Events\Payment\PaymentWasRestored;
use App\Events\Payment\PaymentWasUpdated;
use App\Events\Payment\PaymentWasVoided;
use App\Events\PurchaseOrder\PurchaseOrderWasMarkedSent;
use App\Events\Quote\QuoteWasApproved;
use App\Events\Quote\QuoteWasArchived;
use App\Events\Quote\QuoteWasCreated;
@ -558,6 +559,8 @@ class EventServiceProvider extends ServiceProvider
VendorWasUpdated::class => [
VendorUpdatedActivity::class,
],
PurchaseOrderWasMarkedSent::class => [
],
];

View File

@ -11,10 +11,9 @@
namespace App\Repositories;
use App\Factory\PurchaseOrderFactory;
use App\Models\PurchaseOrder;
use App\Models\Vendor;
use App\Models\VendorContact;
use App\Models\PurchaseOrderInvitation;
use App\Utils\Traits\MakesHash;
class PurchaseOrderRepository extends BaseRepository
@ -27,144 +26,14 @@ class PurchaseOrderRepository extends BaseRepository
public function save(array $data, PurchaseOrder $purchase_order) : ?PurchaseOrder
{
if(array_key_exists('vendor_id', $data))
$purchase_order->vendor_id = $data['vendor_id'];
$vendor = Vendor::where('id', $purchase_order->vendor_id)->withTrashed()->firstOrFail();
$state = [];
$resource = class_basename($purchase_order); //ie Invoice
if (! $purchase_order->id) {
$company_defaults = $vendor->setCompanyDefaults($data, lcfirst($resource));
$purchase_order->uses_inclusive_taxes = $vendor->getSetting('inclusive_taxes');
$data = array_merge($company_defaults, $data);
}
$tmp_data = $data; //preserves the $data array
/* We need to unset some variable as we sometimes unguard the model */
if (isset($tmp_data['invitations']))
unset($tmp_data['invitations']);
if (isset($tmp_data['vendor_contacts']))
unset($tmp_data['vendor_contacts']);
$purchase_order->fill($tmp_data);
$purchase_order->custom_surcharge_tax1 = $vendor->company->custom_surcharge_taxes1;
$purchase_order->custom_surcharge_tax2 = $vendor->company->custom_surcharge_taxes2;
$purchase_order->custom_surcharge_tax3 = $vendor->company->custom_surcharge_taxes3;
$purchase_order->custom_surcharge_tax4 = $vendor->company->custom_surcharge_taxes4;
if(!$purchase_order->id)
$this->new_model = true;
$purchase_order->saveQuietly();
/* Save any documents */
if (array_key_exists('documents', $data))
$this->saveDocuments($data['documents'], $purchase_order);
if (array_key_exists('file', $data))
$this->saveDocuments($data['file'], $purchase_order);
/* If invitations are present we need to filter existing invitations with the new ones */
if (isset($data['invitations'])) {
$invitations = collect($data['invitations']);
/* Get array of Keys which have been removed from the invitations array and soft delete each invitation */
$purchase_order->invitations->pluck('key')->diff($invitations->pluck('key'))->each(function ($invitation) {
$invitation = PurchaseOrderInvitation::where('key', $invitation)->first();
if ($invitation)
$invitation->delete();
});
foreach ($data['invitations'] as $invitation) {
//if no invitations are present - create one.
if (! $this->getInvitation($invitation, $resource)) {
if (isset($invitation['id']))
unset($invitation['id']);
//make sure we are creating an invite for a contact who belongs to the client only!
$contact = VendorContact::find($invitation['vendor_contact_id']);
if ($contact && $purchase_order->client_id == $contact->client_id) {
$new_invitation = PurchaseOrderInvitation::withTrashed()
->where('vendor_contact_id', $contact->id)
->where('purchase_order_id', $purchase_order->id)
->first();
if ($new_invitation && $new_invitation->trashed()) {
$new_invitation->restore();
} else {
$new_invitation = PurchaseOrderFactory::create($purchase_order->company_id, $purchase_order->user_id);
$new_invitation->purchase_order_id = $purchase_order->id;
$new_invitation->vendor_contact_id = $contact->id;
$new_invitation->key = $this->createDbHash($purchase_order->company->db);
$new_invitation->save();
}
}
}
}
}
/* If no invitations have been created, this is our fail safe to maintain state*/
if ($purchase_order->invitations()->count() == 0)
$purchase_order->service()->createInvitations();
/* Apply entity number */
$purchase_order = $purchase_order->service()->applyNumber()->save();
/* Handle attempts where the deposit is greater than the amount/balance of the invoice */
if((int)$purchase_order->balance != 0 && $purchase_order->partial > $purchase_order->amount)
$purchase_order->partial = min($purchase_order->amount, $purchase_order->balance);
$purchase_order = $purchase_order->calc()->getPurchaseOrder();
if (! $purchase_order->design_id)
$purchase_order->design_id = $this->decodePrimaryKey($client->getSetting('credit_design_id'));
if(array_key_exists('invoice_id', $data) && $data['invoice_id'])
$purchase_order->invoice_id = $data['invoice_id'];
if($this->new_model)
event('eloquent.created: App\Models\PurchaseOrder', $purchase_order);
else
event('eloquent.updated: App\Models\PurchaseOrder', $purchase_order);
$purchase_order->fill($data);
$purchase_order->save();
return $purchase_order->fresh();
// $purchase_order->fill($data);
// $purchase_order->save();
// return $purchase_order;
return $purchase_order;
}
public function getInvitation($invitation, $resource)
public function getInvitationByKey($key) :?PurchaseOrderInvitation
{
// if (is_array($invitation) && ! array_key_exists('key', $invitation))
// return false;
// $invitation = PurchaseOrderInvitation::where('key', $invitation['key'])->first();
return $invitation;
return PurchaseOrderInvitation::where('key', $key)->first();
}
}

View File

@ -13,6 +13,7 @@ namespace App\Services\Invoice;
use App\Events\Invoice\InvoiceWasArchived;
use App\Jobs\Entity\CreateEntityPdf;
use App\Jobs\Inventory\AdjustProductInventory;
use App\Jobs\Invoice\InvoiceWorkflowSettings;
use App\Jobs\Util\UnlinkFile;
use App\Libraries\Currency\Conversion\CurrencyApi;
@ -564,6 +565,14 @@ class InvoiceService
return $this;
}
public function adjustInventory()
{
if($this->invoice->company->track_inventory)
AdjustProductInventory::dispatch($this->invoice->company, $this->invoice, null);
return $this;
}
/**
* Saves the invoice.
* @return Invoice object

View File

@ -763,7 +763,7 @@ class Design extends BaseDesign
} elseif (Str::startsWith($variable, '$custom_surcharge')) {
$_variable = ltrim($variable, '$'); // $custom_surcharge1 -> custom_surcharge1
$visible = $this->entity->{$_variable} > 0 || $this->entity->{$_variable} > '0';
$visible = $this->entity->{$_variable} != 0 || $this->entity->{$_variable} != '0';
$elements[1]['elements'][] = ['element' => 'div', 'elements' => [
['element' => 'span', 'content' => $variable . '_label', 'properties' => ['hidden' => !$visible, 'data-ref' => 'totals_table-' . substr($variable, 1) . '-label']],

View File

@ -1,18 +1,12 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Services\PurchaseOrder;
use App\Models\Client;
use App\Models\Credit;
use App\Models\PurchaseOrder;
use App\Models\Vendor;
use App\Services\AbstractService;
use App\Utils\Traits\GeneratesCounter;
use Illuminate\Database\QueryException;
@ -21,15 +15,15 @@ class ApplyNumber extends AbstractService
{
use GeneratesCounter;
private $vendor;
private Client $client;
private $purchase_order;
private PurchaseOrder $purchase_order;
private $completed = true;
private bool $completed = true;
public function __construct(Vendor $vendor, PurchaseOrder $purchase_order)
public function __construct(Client $client, PurchaseOrder $purchase_order)
{
$this->vendor = $vendor;
$this->client = $client;
$this->purchase_order = $purchase_order;
}
@ -40,47 +34,24 @@ class ApplyNumber extends AbstractService
return $this->purchase_order;
}
switch ($this->client->getSetting('counter_number_applied')) {
case 'when_saved':
$this->trySaving();
break;
case 'when_sent':
if ($this->purchase_order->status_id == PurchaseOrder::STATUS_SENT) {
$this->trySaving();
}
break;
default:
break;
}
$this->trySaving();
return $this->purchase_order;
}
private function trySaving()
{
$x=1;
do{
try{
$this->purchase_order->number = $this->getNextPurchaseOrderNumber($this->purchase_order);
$this->purchase_order->number = $this->getNextPurchaseOrderNumber($this->client, $this->purchase_order);
$this->purchase_order->saveQuietly();
$this->completed = false;
}
catch(QueryException $e){
$x++;
if($x>10)
$this->completed = false;
}
}
while($this->completed);

View File

@ -1,20 +1,10 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Services\PurchaseOrder;
use App\Factory\ClientContactFactory;
use App\Factory\PurchaseOrderInvitationFactory;
use App\Factory\VendorContactFactory;
use App\Models\Invoice;
use App\Models\PurchaseOrder;
use App\Models\PurchaseOrderInvitation;
use App\Services\AbstractService;
@ -24,18 +14,24 @@ use Illuminate\Support\Str;
class CreateInvitations extends AbstractService
{
use MakesHash;
private $purchase_order;
public PurchaseOrder $purchase_order;
public function __construct(PurchaseOrder $purchase_order)
{
$this->purchase_order = $purchase_order;
}
private function createBlankContact()
{
$new_contact = PurchaseOrderInvitationFactory::create($this->purchase_order->company_id, $this->purchase_order->user_id);
$new_contact->client_id = $this->purchase_order->client_id;
$new_contact->contact_key = Str::random(40);
$new_contact->is_primary = true;
$new_contact->save();
}
public function run()
{
$contacts = $this->purchase_order->vendor->contacts()->where('send_email', true)->get();
$contacts = $this->purchase_order->vendor->contacts;
if($contacts->count() == 0){
$this->createBlankContact();
@ -45,62 +41,51 @@ class CreateInvitations extends AbstractService
}
$contacts->each(function ($contact) {
$invitation = PurchaseOrderInvitation::where('company_id', $this->purchase_order->company_id)
->where('vendor_contact_id', $contact->id)
->where('purchase_order_id', $this->purchase_order->id)
->withTrashed()
->first();
$invitation = PurchaseOrderInvitation::whereCompanyId($this->purchase_order->company_id)
->whereClientContactId($contact->id)
->whereCreditId($this->purchase_order->id)
->withTrashed()
->first();
if (! $invitation && $contact->send_email) {
$ii = PurchaseOrderInvitationFactory::create($this->purchase_order->company_id, $this->purchase_order->user_id);
if (! $invitation) {
$ii = PurchaseOrderInvitation::create($this->purchase_order->company_id, $this->purchase_order->user_id);
$ii->key = $this->createDbHash($this->purchase_order->company->db);
$ii->purchase_order_id = $this->purchase_order->id;
$ii->vendor_contact_id = $contact->id;
$ii->save();
} elseif ($invitation && ! $contact->send_email) {
} elseif (! $contact->send_email) {
$invitation->delete();
}
});
if($this->purchase_order->invitations()->count() == 0) {
if($contacts->count() == 0){
$contact = $this->createBlankContact();
}
else{
$contact = $contacts->first();
$invitation = PurchaseOrderInvitation::where('company_id', $this->purchase_order->company_id)
->where('vendor_contact_id', $contact->id)
->where('purchase_order_id', $this->purchase_order->id)
->withTrashed()
->first();
$invitation = PurchaseOrder::where('company_id', $this->purchase_order->company_id)
->where('vendor_contact_id', $contact->id)
->where('purchase_order_id', $this->purchase_order->id)
->withTrashed()
->first();
if($invitation){
$invitation->restore();
return $this->purchase_order;
}
if($invitation){
$invitation->restore();
return $this->purchase_order;
}
}
$ii = PurchaseOrderInvitationFactory::create($this->purchase_order->company_id, $this->purchase_order->user_id);
$ii->key = $this->createDbHash($this->purchase_order->company->db);
$ii->purchase_order_id = $this->purchase_order->id;
$ii->vendor_contact_id = $contact->id;
$ii->save();
$ii = PurchaseOrderInvitation::create($this->purchase_order->company_id, $this->purchase_order->user_id);
$ii->key = $this->createDbHash($this->purchase_order->company->db);
$ii->purchase_order_id = $this->purchase_order->id;
$ii->vendor_contact_id = $contact->id;
$ii->save();
}
return $this->purchase_order;
}
private function createBlankContact()
{
$new_contact = VendorContactFactory::create($this->purchase_order->company_id, $this->purchase_order->user_id);
$new_contact->vendor_id = $this->purchase_order->vendor_id;
$new_contact->contact_key = Str::random(40);
$new_contact->is_primary = true;
$new_contact->send_email = true;
$new_contact->save();
return $new_contact;
}
}

View File

@ -0,0 +1,45 @@
<?php
namespace App\Services\PurchaseOrder;
use App\Events\PurchaseOrder\PurchaseOrderWasMarkedSent;
use App\Models\PurchaseOrder;
use App\Utils\Ninja;
class MarkSent
{
private $vendor;
private $purchase_order;
public function __construct($vendor, $purchase_order)
{
$this->vendor = $vendor;
$this->purchase_order = $purchase_order;
}
public function run()
{
/* Return immediately if status is not draft */
if ($this->purchase_order->status_id != PurchaseOrder::STATUS_DRAFT) {
return $this->purchase_order;
}
$this->purchase_order->markInvitationsSent();
$this->purchase_order
->service()
->setStatus(PurchaseOrder::STATUS_SENT)
->applyNumber()
// ->adjustBalance($this->purchase_order->amount)
// ->touchPdf()
->save();
event(new PurchaseOrderWasMarkedSent($this->purchase_order, $this->purchase_order->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null)));
return $this->purchase_order;
}
}

View File

@ -71,4 +71,19 @@ class PurchaseOrderService
return $this;
}
public function setStatus($status)
{
$this->purchase_order->status_id = $status;
return $this;
}
public function markSent()
{
$this->purchase_order = (new MarkSent($this->purchase_order->vendor, $this->purchase_order))->run();
return $this;
}
}

View File

@ -106,6 +106,12 @@ class RecurringService
$this->stop();
}
if(isset($this->recurring_entity->client))
{
$offset = $this->recurring_entity->client->timezone_offset();
$this->recurring_entity->next_send_date = Carbon::parse($this->recurring_entity->next_send_date_client)->startOfDay()->addSeconds($offset);
}
return $this;
}

View File

@ -170,6 +170,10 @@ class CompanyTransformer extends EntityTransformer
'markdown_email_enabled' => (bool) $company->markdown_email_enabled,
'stop_on_unpaid_recurring' => (bool) $company->stop_on_unpaid_recurring,
'use_quote_terms_on_conversion' => (bool) $company->use_quote_terms_on_conversion,
'stock_notification' => (bool) $company->stock_notification,
'inventory_notification_threshold' => (int) $company->inventory_notification_threshold,
'track_inventory' => (bool) $company->track_inventory,
'enable_applying_payments' => (bool) $company->enable_applying_payments,
];
}

View File

@ -90,6 +90,9 @@ class ProductTransformer extends EntityTransformer
'custom_value3' => $product->custom_value3 ?: '',
'custom_value4' => $product->custom_value4 ?: '',
'is_deleted' => (bool) $product->is_deleted,
'in_stock_quantity' => (int) $product->in_stock_quantity ?: 0,
'stock_notification' => (bool) $product->stock_notification,
'stock_notification_threshold' => (int) $product->stock_notification_threshold,
];
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace App\Transformers;
use App\Models\PurchaseOrderInvitation;
use App\Utils\Traits\MakesHash;
class PurchaseOrderInvitationTransformer extends EntityTransformer
{
use MakesHash;
public function transform(PurchaseOrderInvitation $invitation)
{
return [
'id' => $this->encodePrimaryKey($invitation->id),
'vendor_contact_id' => $this->encodePrimaryKey($invitation->vendor_contact_id),
'key' => $invitation->key,
'link' => $invitation->getLink() ?: '',
'sent_date' => $invitation->sent_date ?: '',
'viewed_date' => $invitation->viewed_date ?: '',
'opened_date' => $invitation->opened_date ?: '',
'updated_at' => (int)$invitation->updated_at,
'archived_at' => (int)$invitation->deleted_at,
'created_at' => (int)$invitation->created_at,
'email_status' => $invitation->email_status ?: '',
'email_error' => (string)$invitation->email_error,
];
}
}

View File

@ -13,12 +13,24 @@ namespace App\Transformers;
use App\Models\PurchaseOrder;
use App\Models\PurchaseOrderInvitation;
use App\Utils\Traits\MakesHash;
class PurchaseOrderTransformer extends EntityTransformer
{
use MakesHash;
protected $defaultIncludes = [
'invitations',
];
public function includeInvitations(PurchaseOrder $purchase_order)
{
$transformer = new PurchaseOrderInvitationTransformer($this->serializer);
return $this->includeCollection($purchase_order->invitations, $transformer, PurchaseOrderInvitation::class);
}
public function transform(PurchaseOrder $purchase_order)
{
return [
@ -26,19 +38,18 @@ class PurchaseOrderTransformer extends EntityTransformer
'user_id' => $this->encodePrimaryKey($purchase_order->user_id),
'project_id' => $this->encodePrimaryKey($purchase_order->project_id),
'assigned_user_id' => $this->encodePrimaryKey($purchase_order->assigned_user_id),
'vendor_id' => (string) $this->encodePrimaryKey($purchase_order->vendor_id),
'amount' => (float) $purchase_order->amount,
'balance' => (float) $purchase_order->balance,
'client_id' => (string) $this->encodePrimaryKey($purchase_order->client_id),
'vendor_id' => (string) $this->encodePrimaryKey($purchase_order->vendor_id),
'status_id' => (string) ($purchase_order->status_id ?: 1),
'design_id' => (string) $this->encodePrimaryKey($purchase_order->design_id),
'created_at' => (int) $purchase_order->created_at,
'updated_at' => (int) $purchase_order->updated_at,
'archived_at' => (int) $purchase_order->deleted_at,
'is_deleted' => (bool) $purchase_order->is_deleted,
'vendor_id' => (string)$this->encodePrimaryKey($purchase_order->vendor_id),
'amount' => (float)$purchase_order->amount,
'balance' => (float)$purchase_order->balance,
'client_id' => (string)$this->encodePrimaryKey($purchase_order->client_id),
'status_id' => (string)($purchase_order->status_id ?: 1),
'design_id' => (string)$this->encodePrimaryKey($purchase_order->design_id),
'created_at' => (int)$purchase_order->created_at,
'updated_at' => (int)$purchase_order->updated_at,
'archived_at' => (int)$purchase_order->deleted_at,
'is_deleted' => (bool)$purchase_order->is_deleted,
'number' => $purchase_order->number ?: '',
'discount' => (float) $purchase_order->discount,
'discount' => (float)$purchase_order->discount,
'po_number' => $purchase_order->po_number ?: '',
'date' => $purchase_order->date ?: '',
'last_sent_date' => $purchase_order->last_sent_date ?: '',
@ -51,36 +62,36 @@ class PurchaseOrderTransformer extends EntityTransformer
'terms' => $purchase_order->terms ?: '',
'public_notes' => $purchase_order->public_notes ?: '',
'private_notes' => $purchase_order->private_notes ?: '',
'uses_inclusive_taxes' => (bool) $purchase_order->uses_inclusive_taxes,
'uses_inclusive_taxes' => (bool)$purchase_order->uses_inclusive_taxes,
'tax_name1' => $purchase_order->tax_name1 ? $purchase_order->tax_name1 : '',
'tax_rate1' => (float) $purchase_order->tax_rate1,
'tax_rate1' => (float)$purchase_order->tax_rate1,
'tax_name2' => $purchase_order->tax_name2 ? $purchase_order->tax_name2 : '',
'tax_rate2' => (float) $purchase_order->tax_rate2,
'tax_rate2' => (float)$purchase_order->tax_rate2,
'tax_name3' => $purchase_order->tax_name3 ? $purchase_order->tax_name3 : '',
'tax_rate3' => (float) $purchase_order->tax_rate3,
'total_taxes' => (float) $purchase_order->total_taxes,
'is_amount_discount' => (bool) ($purchase_order->is_amount_discount ?: false),
'tax_rate3' => (float)$purchase_order->tax_rate3,
'total_taxes' => (float)$purchase_order->total_taxes,
'is_amount_discount' => (bool)($purchase_order->is_amount_discount ?: false),
'footer' => $purchase_order->footer ?: '',
'partial' => (float) ($purchase_order->partial ?: 0.0),
'partial' => (float)($purchase_order->partial ?: 0.0),
'partial_due_date' => $purchase_order->partial_due_date ?: '',
'custom_value1' => (string) $purchase_order->custom_value1 ?: '',
'custom_value2' => (string) $purchase_order->custom_value2 ?: '',
'custom_value3' => (string) $purchase_order->custom_value3 ?: '',
'custom_value4' => (string) $purchase_order->custom_value4 ?: '',
'has_tasks' => (bool) $purchase_order->has_tasks,
'has_expenses' => (bool) $purchase_order->has_expenses,
'custom_surcharge1' => (float) $purchase_order->custom_surcharge1,
'custom_surcharge2' => (float) $purchase_order->custom_surcharge2,
'custom_surcharge3' => (float) $purchase_order->custom_surcharge3,
'custom_surcharge4' => (float) $purchase_order->custom_surcharge4,
'custom_surcharge_tax1' => (bool) $purchase_order->custom_surcharge_tax1,
'custom_surcharge_tax2' => (bool) $purchase_order->custom_surcharge_tax2,
'custom_surcharge_tax3' => (bool) $purchase_order->custom_surcharge_tax3,
'custom_surcharge_tax4' => (bool) $purchase_order->custom_surcharge_tax4,
'line_items' => $purchase_order->line_items ?: (array) [],
'custom_value1' => (string)$purchase_order->custom_value1 ?: '',
'custom_value2' => (string)$purchase_order->custom_value2 ?: '',
'custom_value3' => (string)$purchase_order->custom_value3 ?: '',
'custom_value4' => (string)$purchase_order->custom_value4 ?: '',
'has_tasks' => (bool)$purchase_order->has_tasks,
'has_expenses' => (bool)$purchase_order->has_expenses,
'custom_surcharge1' => (float)$purchase_order->custom_surcharge1,
'custom_surcharge2' => (float)$purchase_order->custom_surcharge2,
'custom_surcharge3' => (float)$purchase_order->custom_surcharge3,
'custom_surcharge4' => (float)$purchase_order->custom_surcharge4,
'custom_surcharge_tax1' => (bool)$purchase_order->custom_surcharge_tax1,
'custom_surcharge_tax2' => (bool)$purchase_order->custom_surcharge_tax2,
'custom_surcharge_tax3' => (bool)$purchase_order->custom_surcharge_tax3,
'custom_surcharge_tax4' => (bool)$purchase_order->custom_surcharge_tax4,
'line_items' => $purchase_order->line_items ?: (array)[],
'entity_type' => 'credit',
'exchange_rate' => (float) $purchase_order->exchange_rate,
'paid_to_date' => (float) $purchase_order->paid_to_date,
'exchange_rate' => (float)$purchase_order->exchange_rate,
'paid_to_date' => (float)$purchase_order->paid_to_date,
'subscription_id' => $this->encodePrimaryKey($purchase_order->subscription_id),
];
}

View File

@ -100,7 +100,8 @@ class RecurringExpenseTransformer extends EntityTransformer
'frequency_id' => (string) $recurring_expense->frequency_id,
'remaining_cycles' => (int) $recurring_expense->remaining_cycles,
'last_sent_date' => $recurring_expense->last_sent_date ?: '',
'next_send_date' => $recurring_expense->next_send_date ?: '',
// 'next_send_date' => $recurring_expense->next_send_date ?: '',
'next_send_date' => $recurring_expense->next_send_date_client ?: '',
'recurring_dates' => (array) [],
];

View File

@ -95,7 +95,8 @@ class RecurringInvoiceTransformer extends EntityTransformer
'po_number' => $invoice->po_number ?: '',
'date' => $invoice->date ?: '',
'last_sent_date' => $invoice->last_sent_date ?: '',
'next_send_date' => $invoice->next_send_date ?: '',
// 'next_send_date' => $invoice->next_send_date ?: '',
'next_send_date' => $invoice->next_send_date_client ?: '',
'due_date' => $invoice->due_date ?: '',
'terms' => $invoice->terms ?: '',
'public_notes' => $invoice->public_notes ?: '',

View File

@ -45,8 +45,8 @@ trait GeneratesCounter
$is_client_counter = false;
$counter_string = $this->getEntityCounter($entity, $client);
$pattern = $this->getNumberPattern($entity, $client);
$counter_string = $this->getEntityCounter($entity, $client);
$pattern = $this->getNumberPattern($entity, $client);
if ((strpos($pattern, 'clientCounter') !== false) || (strpos($pattern, 'client_counter') !==false) ) {
@ -72,9 +72,9 @@ trait GeneratesCounter
$counter_entity = $client->company;
}
//If it is a quote - we need to
//If it is a quote - we need to
$pattern = $this->getNumberPattern($entity, $client);
if(strlen($pattern) > 1 && (stripos($pattern, 'counter') === false)){
$pattern = $pattern.'{$counter}';
}
@ -128,9 +128,9 @@ trait GeneratesCounter
break;
case Quote::class:
if ($this->hasSharedCounter($client, 'quote'))
if ($this->hasSharedCounter($client, 'quote'))
return 'invoice_number_counter';
return 'quote_number_counter';
break;
case RecurringInvoice::class:
@ -146,14 +146,17 @@ trait GeneratesCounter
return 'payment_number_counter';
break;
case Credit::class:
if ($this->hasSharedCounter($client, 'credit'))
if ($this->hasSharedCounter($client, 'credit'))
return 'invoice_number_counter';
return 'credit_number_counter';
break;
case Project::class:
return 'project_number_counter';
break;
case PurchaseOrder::class:
return 'purchase_order_number_counter';
break;
case PurchaseOrder::class:
return 'purchase_order_number_counter';
@ -193,6 +196,20 @@ trait GeneratesCounter
return $this->replaceUserVars($credit, $entity_number);
}
/**
* Gets the next purchase order number.
*
* @param PurchaseOrder $purchase_order The purchase order
*
* @return string The next purchase order number.
*/
public function getNextPurchaseOrderNumber(Client $client, ?PurchaseOrder $purchase_order) :string
{
$entity_number = $this->getNextEntityNumber(PurchaseOrder::class, $client);
return $this->replaceUserVars($purchase_order, $entity_number);
}
/**
@ -407,7 +424,7 @@ trait GeneratesCounter
*
* @return bool True if has shared counter, False otherwise.
*/
public function hasSharedCounter(Client $client, string $type = 'quote') : bool
public function hasSharedCounter(Client $client, string $type = 'quote') : bool
{
if($type == 'quote')
return (bool) $client->getSetting('shared_invoice_quote_counter');
@ -460,9 +477,9 @@ trait GeneratesCounter
public function checkNumberAvailable($class, $entity, $number) :bool
{
if ($entity = $class::whereCompanyId($entity->company_id)->whereNumber($number)->withTrashed()->exists())
if ($entity = $class::whereCompanyId($entity->company_id)->whereNumber($number)->withTrashed()->exists())
return false;
return true;
}
@ -526,7 +543,7 @@ trait GeneratesCounter
if($reset_counter_frequency == 0)
return;
$timezone = Timezone::find($client->getSetting('timezone_id'));
$reset_date = Carbon::parse($client->getSetting('reset_counter_date'), $timezone->name);
@ -580,6 +597,7 @@ trait GeneratesCounter
$settings->invoice_number_counter = 1;
$settings->quote_number_counter = 1;
$settings->credit_number_counter = 1;
$settings->purchase_order_number_counter = 1;
$client->company->settings = $settings;
$client->company->save();
@ -644,6 +662,7 @@ trait GeneratesCounter
$settings->task_number_counter = 1;
$settings->expense_number_counter = 1;
$settings->recurring_expense_number_counter =1;
$settings->purchase_order_number_counter = 1;
$company->settings = $settings;
$company->save();
@ -666,7 +685,7 @@ trait GeneratesCounter
$search = [];
$replace = [];
$search[] = '{$counter}';
$replace[] = $counter;
@ -681,7 +700,7 @@ trait GeneratesCounter
$search[] = '{$year}';
$replace[] = Carbon::now($entity->company->timezone()->name)->format('Y');
if (strstr($pattern, '{$user_id}') || strstr($pattern, '{$userId}')) {
$user_id = $entity->user_id ? $entity->user_id : 0;
$search[] = '{$user_id}';
@ -705,7 +724,7 @@ trait GeneratesCounter
$search[] = '{$vendor_id_number}';
$replace[] = $entity->id_number;
}
if ($entity instanceof Expense) {
if ($entity->vendor) {
$search[] = '{$vendor_id_number}';
@ -730,7 +749,7 @@ trait GeneratesCounter
$search[] = '{$expense_id_number}';
$replace[] = $entity->id_number;
}
if ($entity->client || ($entity instanceof Client)) {
$client = $entity->client ?: $entity;

View File

@ -14,8 +14,8 @@ return [
'require_https' => env('REQUIRE_HTTPS', true),
'app_url' => rtrim(env('APP_URL', ''), '/'),
'app_domain' => env('APP_DOMAIN', 'invoicing.co'),
'app_version' => '5.3.93',
'app_tag' => '5.3.93',
'app_version' => '5.3.96',
'app_tag' => '5.3.96',
'minimum_client_version' => '5.0.16',
'terms_version' => '1.0.1',
'api_secret' => env('API_SECRET', ''),

View File

@ -0,0 +1,31 @@
<?php
namespace Database\Factories;
use App\Models\PurchaseOrderInvitation;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;
class PurchaseOrderInvitationFactory extends Factory
{
/**
* The name of the factory's corresponding model.
*
* @var string
*/
protected $model = PurchaseOrderInvitation::class;
/**
* Define the model's default state.
*
* @return array
*/
public function definition()
{
return [
'key' => Str::random(40),
];
}
}

View File

@ -51,6 +51,7 @@ class RecurringInvoiceFactory extends Factory
'frequency_id' => RecurringInvoice::FREQUENCY_MONTHLY,
'last_sent_date' => now()->subMonth(),
'next_send_date' => now()->addMonthNoOverflow(),
'next_send_date_client' => now()->addMonthNoOverflow(),
'remaining_cycles' => $this->faker->numberBetween(1, 10),
'amount' => $this->faker->randomFloat(2, $min = 1, $max = 1000), // 48.8932

View File

@ -30,9 +30,6 @@ class CreateSchedulersTable extends Migration
$table->timestamp('start_from');
$table->timestamp('scheduled_run');
$table->foreignIdFor(\App\Models\Company::class);
$table->string('action_name')->index();
$table->string('action_class');
$table->json('parameters')->nullable();
$table->timestamps();
$table->softDeletes();
});

View File

@ -122,6 +122,5 @@ class CreatePurchaseOrdersTable extends Migration
*/
public function down()
{
Schema::dropIfExists('purchase_orders');
}
}

View File

@ -0,0 +1,40 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddJobRelatedFieldsToSchedulersTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('schedulers', function (Blueprint $table) {
$table->string('action_name')->index();
$table->string('action_class');
$table->json('parameters')->nullable();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
}
}

View File

@ -0,0 +1,44 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class InventoryManagementSchema extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('companies', function (Blueprint $table) {
$table->boolean('enable_applying_payments')->default(0);
$table->boolean('track_inventory')->default(0);
$table->integer('inventory_notification_threshold')->default(0);
$table->boolean('stock_notification')->default(1);
});
Schema::table('products', function (Blueprint $table){
$table->integer('in_stock_quantity')->default(0);
$table->boolean('stock_notification')->default(1);
$table->integer('stock_notification_threshold')->default(0);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
//
}
}

View File

@ -0,0 +1,52 @@
<?php
use App\Models\RecurringExpense;
use App\Models\RecurringInvoice;
use Carbon\Carbon;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class SetRecurringClientTimestamp extends Migration
{
/**
* Run the migrations.
*
*/
public function up()
{
Schema::table('recurring_invoices', function (Blueprint $table) {
$table->datetime('next_send_date_client')->nullable();
});
Schema::table('recurring_expenses', function (Blueprint $table) {
$table->datetime('next_send_date_client')->nullable();
});
RecurringInvoice::whereNotNull('next_send_date')->cursor()->each(function ($recurring_invoice){
// $offset = $recurring_invoice->client->timezone_offset();
// $re = Carbon::parse($recurring_invoice->next_send_date)->subSeconds($offset)->format('Y-m-d');
$re = Carbon::parse($recurring_invoice->next_send_date)->format('Y-m-d');
$recurring_invoice->next_send_date_client = $re;
$recurring_invoice->saveQuietly();
});
RecurringExpense::whereNotNull('next_send_date')->cursor()->each(function ($recurring_expense){
$recurring_expense->next_send_date_client = $recurring_expense->next_send_date;
$recurring_expense->saveQuietly();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
//
}
}

View File

@ -0,0 +1,53 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreatePurchaseOrderInvitationsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('purchase_order_invitations', function (Blueprint $table) {
$table->id();
$table->unsignedInteger('company_id')->index();
$table->unsignedInteger('user_id');
$table->unsignedInteger('vendor_contact_id')->unique();
$table->unsignedBigInteger('purchase_order_id')->index()->unique();
$table->string('key')->index();
$table->string('transaction_reference')->nullable();
$table->string('message_id')->nullable()->index();
$table->mediumText('email_error')->nullable();
$table->text('signature_base64')->nullable();
$table->datetime('signature_date')->nullable();
$table->datetime('sent_date')->nullable();
$table->datetime('viewed_date')->nullable();
$table->datetime('opened_date')->nullable();
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade')->onUpdate('cascade');
$table->foreign('vendor_contact_id')->references('id')->on('vendor_contacts')->onDelete('cascade')->onUpdate('cascade');
$table->foreign('purchase_order_id')->references('id')->on('purchase_orders')->onDelete('cascade')->onUpdate('cascade');
$table->foreign('company_id')->references('id')->on('companies')->onDelete('cascade')->onUpdate('cascade');
$table->timestamps(6);
$table->softDeletes('deleted_at', 6);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('purchase_order_invitations');
}
}

View File

@ -3,43 +3,43 @@ const MANIFEST = 'flutter-app-manifest';
const TEMP = 'flutter-temp-cache';
const CACHE_NAME = 'flutter-app-cache';
const RESOURCES = {
"main.dart.js": "208309d12730fc06c9bf9ef0bd0c2483",
"icons/Icon-192.png": "bb1cf5f6982006952211c7c8404ffbed",
"icons/Icon-512.png": "0f9aff01367f0a0c69773d25ca16ef35",
"/": "90082634bee3634faebfddf446a803a2",
"main.dart.js": "cf8b4f4a686adceb3f1085f27a463220",
"version.json": "3afb81924daf4f751571755436069115",
"favicon.png": "dca91c54388f52eded692718d5a98b8b",
"flutter.js": "0816e65a103ba8ba51b174eeeeb2cb67",
"favicon.ico": "51636d3a390451561744c42188ccd628",
"canvaskit/canvaskit.wasm": "4b83d89d9fecbea8ca46f2f760c5a9ba",
"canvaskit/profiling/canvaskit.wasm": "95e736ab31147d1b2c7b25f11d4c32cd",
"canvaskit/profiling/canvaskit.js": "ae2949af4efc61d28a4a80fffa1db900",
"canvaskit/canvaskit.js": "c2b4e5f3d7a3d82aed024e7249a78487",
"flutter.js": "0816e65a103ba8ba51b174eeeeb2cb67",
"/": "d60cdb0e60692160d5f962b98b88e24d",
"favicon.png": "dca91c54388f52eded692718d5a98b8b",
"version.json": "3afb81924daf4f751571755436069115",
"assets/FontManifest.json": "cf3c681641169319e61b61bd0277378f",
"assets/AssetManifest.json": "38d9aea341601f3a5c6fa7b5a1216ea5",
"assets/packages/material_design_icons_flutter/lib/fonts/materialdesignicons-webfont.ttf": "b62641afc9ab487008e996a5c5865e56",
"assets/assets/images/icon.png": "090f69e23311a4b6d851b3880ae52541",
"assets/assets/images/payment_types/ach.png": "7433f0aff779dc98a649b7a2daf777cf",
"assets/assets/images/payment_types/jcb.png": "07e0942d16c5592118b72e74f2f7198c",
"assets/fonts/MaterialIcons-Regular.otf": "95db9098c58fd6db106f1116bae85a0b",
"assets/assets/images/logo_light.png": "e5f46d5a78e226e7a9553d4ca6f69219",
"assets/assets/images/payment_types/discover.png": "6c0a386a00307f87db7bea366cca35f5",
"assets/assets/images/payment_types/carteblanche.png": "d936e11fa3884b8c9f1bd5c914be8629",
"assets/assets/images/payment_types/switch.png": "4fa11c45327f5fdc20205821b2cfd9cc",
"assets/assets/images/payment_types/dinerscard.png": "06d85186ba858c18ab7c9caa42c92024",
"assets/assets/images/payment_types/solo.png": "2030c3ccaccf5d5e87916a62f5b084d6",
"assets/assets/images/payment_types/mastercard.png": "6f6cdc29ee2e22e06b1ac029cb52ef71",
"assets/assets/images/payment_types/unionpay.png": "7002f52004e0ab8cc0b7450b0208ccb2",
"assets/assets/images/payment_types/visa.png": "3ddc4a4d25c946e8ad7e6998f30fd4e3",
"assets/assets/images/payment_types/amex.png": "c49a4247984b3732a4af50a3390aa978",
"assets/assets/images/payment_types/laser.png": "b4e6e93dd35517ac429301119ff05868",
"assets/assets/images/payment_types/paypal.png": "8e06c094c1871376dfea1da8088c29d1",
"assets/assets/images/payment_types/other.png": "d936e11fa3884b8c9f1bd5c914be8629",
"assets/assets/images/payment_types/laser.png": "b4e6e93dd35517ac429301119ff05868",
"assets/assets/images/payment_types/ach.png": "7433f0aff779dc98a649b7a2daf777cf",
"assets/assets/images/payment_types/dinerscard.png": "06d85186ba858c18ab7c9caa42c92024",
"assets/assets/images/payment_types/unionpay.png": "7002f52004e0ab8cc0b7450b0208ccb2",
"assets/assets/images/payment_types/maestro.png": "e533b92bfb50339fdbfa79e3dfe81f08",
"assets/assets/images/payment_types/paypal.png": "8e06c094c1871376dfea1da8088c29d1",
"assets/assets/images/payment_types/visa.png": "3ddc4a4d25c946e8ad7e6998f30fd4e3",
"assets/assets/images/payment_types/solo.png": "2030c3ccaccf5d5e87916a62f5b084d6",
"assets/assets/images/payment_types/amex.png": "c49a4247984b3732a4af50a3390aa978",
"assets/assets/images/payment_types/switch.png": "4fa11c45327f5fdc20205821b2cfd9cc",
"assets/assets/images/payment_types/jcb.png": "07e0942d16c5592118b72e74f2f7198c",
"assets/assets/images/payment_types/mastercard.png": "6f6cdc29ee2e22e06b1ac029cb52ef71",
"assets/assets/images/icon.png": "090f69e23311a4b6d851b3880ae52541",
"assets/assets/images/google_logo.png": "0f118259ce403274f407f5e982e681c3",
"assets/assets/images/logo_light.png": "e5f46d5a78e226e7a9553d4ca6f69219",
"assets/assets/images/logo_dark.png": "a233ed1d4d0f7414bf97a9a10f11fb0a",
"assets/NOTICES": "52d7174bb068ef86545951d5bc8c5744",
"assets/FontManifest.json": "cf3c681641169319e61b61bd0277378f",
"assets/fonts/MaterialIcons-Regular.otf": "95db9098c58fd6db106f1116bae85a0b",
"manifest.json": "ef43d90e57aa7682d7e2cfba2f484a40",
"icons/Icon-512.png": "0f9aff01367f0a0c69773d25ca16ef35",
"icons/Icon-192.png": "bb1cf5f6982006952211c7c8404ffbed",
"favicon.ico": "51636d3a390451561744c42188ccd628"
"manifest.json": "ef43d90e57aa7682d7e2cfba2f484a40"
};
// The application shell files that are downloaded before a service worker can

263138
public/main.dart.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

260422
public/main.foss.dart.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

255260
public/main.html.dart.js vendored

File diff suppressed because one or more lines are too long

260344
public/main.next.dart.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -117,13 +117,20 @@
</div>
</tr>
<tr>
<td>
<td align="center">
<div class="dark-bg"
style="background-color:#f9f9f9; border: 1px solid #c2c2c2; border-bottom: none; padding-bottom: 20px; border-top-left-radius: 3px; border-top-right-radius: 3px;">
<img class="logo-light"
style="margin-top: 20px; max-width: 155px; display: block; margin-left: auto; margin-right: auto; "
src="{{ $logo ?? '' }}"
width="155" />
<!--[if gte mso 9]>
<img src="{{ $logo ?? '' }}" alt="" width="155" border="0" align="middle" style="display:block;" />
<div style="mso-hide:all;">
<![endif]-->
<img class="logo-light" src="{{ $logo ?? '' }}" alt="" style="margin-top: 10px; max-width: 570px; display: block; margin-left: auto; margin-right: auto;"/>
<!--[if gte mso 9]>
</div>
<![endif]-->
</div>
</td>
</tr>

View File

@ -94,19 +94,24 @@
<div style="text-align: center;margin-top: 25px; margin-bottom: 10px;"></div>
</tr>
<tr>
<td>
<td align="center" cellpadding="20">
<div
style="border: 1px solid #c2c2c2; border-bottom: none; padding-bottom: 10px; border-top-left-radius: 3px; border-top-right-radius: 3px;">
<img
style="margin-top: 40px; height: 40px; display: block; margin-left: auto; margin-right: auto;"
alt=""
src="{{ $logo ?? '' }}"/>
<!--[if gte mso 9]>
<img src="{{ $logo ?? '' }}" alt="" width="400" border="0" align="middle" style="display:block;" />
<div style="mso-hide:all;">
<![endif]-->
<img src="{{ $logo ?? '' }}" alt="" style="margin-top: 40px; max-width: 155px; display: block; margin-left: auto; margin-right: auto;"/>
<!--[if gte mso 9]>
</div>
<![endif]-->
</div>
</td>
</tr>
<tr>
<td>
<td cellpadding="20">
<div style="border: 1px solid #c2c2c2; border-top: none; border-bottom: none; padding: 20px; text-align: center" id="content">
<div style="padding-top: 10px;"></div>
@ -120,10 +125,16 @@
</div>
</div>
</td>
</tr>
<tr>
<td height="20">
<div style="border: 1px solid #c2c2c2; border-top: none; border-bottom: none; padding: 20px; text-align: center" id="content"> </div>
</td>
</tr>
<tr>
<td>
<td cellpadding="20" bgcolor="#f9f9f9">
<div class="dark-bg dark-text-white"
style="text-align: center; padding-top: 10px; padding-bottom: 25px; background-color: #f9f9f9; border: 1px solid #c2c2c2; border-top: none; border-bottom-color: #f9f9f9;">
@isset($signature)
@ -145,7 +156,7 @@
</tr>
<tr>
<td>
<td bgcolor="#242424" cellpadding="20">
<div class="dark-bg-base"
style="padding-top: 10px;padding-bottom: 10px; background-color: #242424; border: 1px solid #c2c2c2; border-top-color: #242424; border-bottom-color: #242424;">
@if(isset($company))

View File

@ -37,6 +37,7 @@ class PurchaseOrderTest extends TestCase
Model::reguard();
$this->makeTestData();
}
public function testPurchaseOrderRest()
@ -44,18 +45,18 @@ class PurchaseOrderTest extends TestCase
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->get('/api/v1/purchase_orders/'.$this->encodePrimaryKey($this->purchase_order->id));
])->get('/api/v1/purchase_orders/' . $this->encodePrimaryKey($this->purchase_order->id));
$response->assertStatus(200);
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->get('/api/v1/purchase_orders/'.$this->encodePrimaryKey($this->purchase_order->id).'/edit');
])->get('/api/v1/purchase_orders/' . $this->encodePrimaryKey($this->purchase_order->id) . '/edit');
$response->assertStatus(200);
$credit_update = [
$purchase_order_update = [
'tax_name1' => 'dippy',
];
@ -64,14 +65,14 @@ class PurchaseOrderTest extends TestCase
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->put('/api/v1/purchase_orders/'.$this->encodePrimaryKey($this->purchase_order->id), $credit_update)
])->put('/api/v1/purchase_orders/' . $this->encodePrimaryKey($this->purchase_order->id), $purchase_order_update)
->assertStatus(200);
}
public function testPostNewPurchaseOrder()
{
$purchase_order = [
'status_id' => 1,
'number' => 'dfdfd',
'discount' => 0,
'is_amount_discount' => 1,
'number' => '34343xx43',
@ -91,20 +92,21 @@ class PurchaseOrderTest extends TestCase
])->post('/api/v1/purchase_orders/', $purchase_order)
->assertStatus(200);
}
public function testPurchaseOrderDelete()
{
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->delete('/api/v1/purchase_orders/'.$this->encodePrimaryKey($this->purchase_order->id));
])->delete('/api/v1/purchase_orders/' . $this->encodePrimaryKey($this->purchase_order->id));
$response->assertStatus(200);
}
public function testPurchaseOrderUpdate()
{
$data = [
'status_id' => 1,
'number' => 'dfdfd',
'discount' => 0,
'is_amount_discount' => 1,
'number' => '3434343',
@ -121,14 +123,14 @@ class PurchaseOrderTest extends TestCase
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->put('/api/v1/purchase_orders/'.$this->encodePrimaryKey($this->purchase_order->id), $data);
])->put('/api/v1/purchase_orders/' . $this->encodePrimaryKey($this->purchase_order->id), $data);
$response->assertStatus(200);
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->put('/api/v1/purchase_orders/'.$this->encodePrimaryKey($this->purchase_order->id), $data);
])->put('/api/v1/purchase_orders/' . $this->encodePrimaryKey($this->purchase_order->id), $data);
$response->assertStatus(200);

View File

@ -36,6 +36,8 @@ use App\Models\GroupSetting;
use App\Models\InvoiceInvitation;
use App\Models\Product;
use App\Models\Project;
use App\Models\PurchaseOrder;
use App\Models\PurchaseOrderInvitation;
use App\Models\Quote;
use App\Models\QuoteInvitation;
use App\Models\RecurringExpense;
@ -476,6 +478,26 @@ trait MockAccountData
$this->purchase_order->save();
PurchaseOrderInvitation::factory()->create([
'user_id' => $user_id,
'company_id' => $this->company->id,
'vendor_contact_id' => $vendor_contact->id,
'purchase_order_id' => $this->purchase_order->id,
]);
$purchase_order_invitations = PurchaseOrderInvitation::whereCompanyId($this->purchase_order->company_id)
->wherePurchaseOrderId($this->purchase_order->id);
$this->purchase_order->setRelation('invitations', $purchase_order_invitations);
$this->purchase_order->service()->markSent();
$this->purchase_order->setRelation('client', $this->client);
$this->purchase_order->setRelation('company', $this->company);
$this->purchase_order->save();
$this->credit = CreditFactory::create($this->company->id, $user_id);