diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 0d2b1edae7..badbdef1d5 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -14,6 +14,11 @@ https://invoiceninja.github.io/docs/self-host-troubleshooting/ --> - Version: - Environment: +## Interface +- Flutter: [] +- React: [] +- Both: [] + ## Checklist - Can you replicate the issue on our v5 demo site https://demo.invoiceninja.com or https://react.invoicing.co/demo? - Have you searched existing issues? diff --git a/app/Jobs/Util/Import.php b/app/Jobs/Util/Import.php index 8a12e02e11..9ee89e3457 100644 --- a/app/Jobs/Util/Import.php +++ b/app/Jobs/Util/Import.php @@ -1097,6 +1097,9 @@ class Import implements ShouldQueue $modified['user_id'] = $this->processUserId($resource); $modified['company_id'] = $this->company->id; $modified['line_items'] = $this->cleanItems($modified['line_items']); + + //31/08-2023 set correct paid to date here: + $modified['paid_to_date'] = $modified['amount'] - $modified['balance'] ?? 0; unset($modified['id']); diff --git a/app/Models/Backup.php b/app/Models/Backup.php index c0fd324fdf..78ba6b36fe 100644 --- a/app/Models/Backup.php +++ b/app/Models/Backup.php @@ -54,13 +54,13 @@ class Backup extends BaseModel return $this->belongsTo(Activity::class); } - public function storeRemotely(?string $html, Client $client) + public function storeRemotely(?string $html, Client | Vendor $client_or_vendor) { if (! $html || strlen($html) == 0) { return; } - $path = $client->backup_path().'/'; + $path = $client_or_vendor->backup_path().'/'; $filename = now()->format('Y_m_d').'_'.md5(time()).'.html'; $file_path = $path.$filename; diff --git a/app/Models/Vendor.php b/app/Models/Vendor.php index 84dcd3f286..25ebbc4d9e 100644 --- a/app/Models/Vendor.php +++ b/app/Models/Vendor.php @@ -274,4 +274,9 @@ class Vendor extends BaseModel { return $this->company->date_format(); } + + public function backup_path() :string + { + return $this->company->company_key.'/'.$this->vendor_hash.'/backups'; + } } diff --git a/app/Repositories/ActivityRepository.php b/app/Repositories/ActivityRepository.php index adafdfb2ca..3ea31c3c7b 100644 --- a/app/Repositories/ActivityRepository.php +++ b/app/Repositories/ActivityRepository.php @@ -11,21 +11,23 @@ namespace App\Repositories; -use App\Models\Activity; +use App\Models\User; +use App\Models\Quote; use App\Models\Backup; -use App\Models\CompanyToken; use App\Models\Credit; use App\Models\Design; use App\Models\Invoice; -use App\Models\Quote; +use App\Models\Activity; +use App\Utils\HtmlEngine; +use App\Models\CompanyToken; +use App\Models\PurchaseOrder; +use App\Utils\Traits\MakesHash; +use App\Utils\VendorHtmlEngine; use App\Models\RecurringInvoice; -use App\Models\User; +use App\Utils\Traits\MakesInvoiceHtml; use App\Services\PdfMaker\Design as PdfDesignModel; use App\Services\PdfMaker\Design as PdfMakerDesign; use App\Services\PdfMaker\PdfMaker as PdfMakerService; -use App\Utils\HtmlEngine; -use App\Utils\Traits\MakesHash; -use App\Utils\Traits\MakesInvoiceHtml; /** * Class for activity repository. @@ -85,13 +87,30 @@ class ActivityRepository extends BaseRepository ) { $backup = new Backup(); $entity->load('client'); - $contact = $entity->client->primary_contact()->first(); $backup->amount = $entity->amount; $backup->activity_id = $activity->id; $backup->json_backup = ''; $backup->save(); $backup->storeRemotely($this->generateHtml($entity), $entity->client); + + return; + } + + if(get_class($entity) == PurchaseOrder::class) + { + + $backup = new Backup(); + $entity->load('client'); + $backup->amount = $entity->amount; + $backup->activity_id = $activity->id; + $backup->json_backup = ''; + $backup->save(); + + $backup->storeRemotely($this->generateVendorHtml($entity), $entity->vendor); + + return; + } } @@ -109,6 +128,58 @@ class ActivityRepository extends BaseRepository return false; } + private function generateVendorHtml($entity) + { + $entity_design_id = $entity->design_id ? $entity->design_id : $this->decodePrimaryKey($entity->vendor->getSetting('purchase_order_design_id')); + + $design = Design::withTrashed()->find($entity_design_id); + + if (! $entity->invitations()->exists() || ! $design) { + nlog("No invitations for entity {$entity->id} - {$entity->number}"); + return ''; + } + + $entity->load('vendor.company', 'invitations'); + + $html = new VendorHtmlEngine($entity->invitations->first()->load('purchase_order', 'contact')); + + if ($design->is_custom) { + $options = [ + 'custom_partials' => json_decode(json_encode($design->design), true), + ]; + $template = new PdfMakerDesign(PdfDesignModel::CUSTOM, $options); + } else { + $template = new PdfMakerDesign(strtolower($design->name)); + } + + $state = [ + 'template' => $template->elements([ + 'vendor' => $entity->vendor, + 'entity' => $entity, + 'pdf_variables' => (array) $entity->company->settings->pdf_variables, + '$product' => $design->design->product, + ]), + 'variables' => $html->generateLabelsAndValues(), + 'options' => [ + 'all_pages_header' => $entity->vendor->getSetting('all_pages_header'), + 'all_pages_footer' => $entity->vendor->getSetting('all_pages_footer'), + ], + 'process_markdown' => $entity->vendor->company->markdown_enabled, + ]; + + $maker = new PdfMakerService($state); + + $html = $maker->design($template) + ->build() + ->getCompiledHTML(true); + + $maker = null; + $state = null; + + return $html; + + } + private function generateHtml($entity) { $entity_design_id = ''; @@ -128,8 +199,6 @@ class ActivityRepository extends BaseRepository $entity_design_id = 'credit_design_id'; } - // $entity->load('client.company'); - $entity_design_id = $entity->design_id ? $entity->design_id : $this->decodePrimaryKey($entity->client->getSetting($entity_design_id)); $design = Design::withTrashed()->find($entity_design_id); diff --git a/app/Transformers/PurchaseOrderHistoryTransformer.php b/app/Transformers/PurchaseOrderHistoryTransformer.php new file mode 100644 index 0000000000..5f2af4d659 --- /dev/null +++ b/app/Transformers/PurchaseOrderHistoryTransformer.php @@ -0,0 +1,61 @@ + '', + 'activity_id' => '', + 'json_backup' => (string) '', + 'html_backup' => (string) '', //deprecated + 'amount' => (float) 0, + 'created_at' => (int) 0, + 'updated_at' => (int) 0, + ]; + } + + return [ + 'id' => $this->encodePrimaryKey($backup->id), + 'activity_id' => $this->encodePrimaryKey($backup->activity_id), + 'json_backup' => (string) '', + 'html_backup' => (string) '', //deprecated + 'amount' => (float) $backup->amount, + 'created_at' => (int) $backup->created_at, + 'updated_at' => (int) $backup->updated_at, + ]; + } + + public function includeActivity(Backup $backup) + { + $transformer = new ActivityTransformer($this->serializer); + + return $this->includeItem($backup->activity, $transformer, Activity::class); + } +} diff --git a/app/Transformers/PurchaseOrderTransformer.php b/app/Transformers/PurchaseOrderTransformer.php index 527cca3466..532d16d24a 100644 --- a/app/Transformers/PurchaseOrderTransformer.php +++ b/app/Transformers/PurchaseOrderTransformer.php @@ -11,8 +11,10 @@ namespace App\Transformers; +use App\Models\Backup; use App\Models\Vendor; use App\Models\Expense; +use App\Models\Activity; use App\Models\Document; use App\Models\PurchaseOrder; use App\Utils\Traits\MakesHash; @@ -30,8 +32,16 @@ class PurchaseOrderTransformer extends EntityTransformer protected $availableIncludes = [ 'expense', 'vendor', + 'activities', ]; + public function includeActivities(PurchaseOrder $purchase_order) + { + $transformer = new ActivityTransformer($this->serializer); + + return $this->includeCollection($purchase_order->activities, $transformer, Activity::class); + } + public function includeInvitations(PurchaseOrder $purchase_order) { $transformer = new PurchaseOrderInvitationTransformer($this->serializer); @@ -39,6 +49,12 @@ class PurchaseOrderTransformer extends EntityTransformer return $this->includeCollection($purchase_order->invitations, $transformer, PurchaseOrderInvitation::class); } + public function includeHistory(PurchaseOrder $purchase_order) + { + $transformer = new PurchaseOrderHistoryTransformer($this->serializer); + + return $this->includeCollection($purchase_order->history, $transformer, Backup::class); + } public function includeDocuments(PurchaseOrder $purchase_order) { diff --git a/lang/en/texts.php b/lang/en/texts.php index 183cbf5c8c..06502e3354 100644 --- a/lang/en/texts.php +++ b/lang/en/texts.php @@ -660,7 +660,7 @@ $LANG = array( 'customize_help' => '

We use :pdfmake_link to define the invoice designs declaratively. The pdfmake :playground_link provides a great way to see the library in action.

If you need help figuring something out post a question to our :forum_link with the design you\'re using.

', 'playground' => 'playground', - 'support_forum' => 'support forum', + 'support_forum' => 'Support Forums', 'invoice_due_date' => 'Due Date', 'quote_due_date' => 'Valid Until', 'valid_until' => 'Valid Until', diff --git a/tests/Feature/PurchaseOrderTest.php b/tests/Feature/PurchaseOrderTest.php index b3fc1be635..15183ac0f2 100644 --- a/tests/Feature/PurchaseOrderTest.php +++ b/tests/Feature/PurchaseOrderTest.php @@ -11,14 +11,20 @@ namespace Tests\Feature; +use Tests\TestCase; +use App\Utils\Ninja; +use App\Models\Activity; +use Tests\MockAccountData; +use Illuminate\Support\Str; use App\Models\PurchaseOrder; use App\Utils\Traits\MakesHash; use Illuminate\Database\Eloquent\Model; -use Illuminate\Foundation\Testing\DatabaseTransactions; use Illuminate\Support\Facades\Session; -use Illuminate\Support\Str; -use Tests\MockAccountData; -use Tests\TestCase; +use App\Repositories\ActivityRepository; +use App\Events\PurchaseOrder\PurchaseOrderWasCreated; +use App\Events\PurchaseOrder\PurchaseOrderWasUpdated; +use App\Events\PurchaseOrder\PurchaseOrderWasAccepted; +use Illuminate\Foundation\Testing\DatabaseTransactions; class PurchaseOrderTest extends TestCase { @@ -26,6 +32,8 @@ class PurchaseOrderTest extends TestCase use DatabaseTransactions; use MockAccountData; + public $faker; + protected function setUp(): void { parent::setUp(); @@ -39,6 +47,37 @@ class PurchaseOrderTest extends TestCase $this->makeTestData(); } + public function testPurchaseOrderHistory() + { + event(new PurchaseOrderWasUpdated($this->purchase_order, $this->company, Ninja::eventVars($this->company, $this->user))); + event(new PurchaseOrderWasCreated($this->purchase_order, $this->company, Ninja::eventVars($this->company, $this->user))); + + $ar = new ActivityRepository(); + $fields = new \stdClass; + $fields->user_id = $this->purchase_order->user_id; + $fields->vendor_id = $this->purchase_order->vendor_id; + $fields->company_id = $this->purchase_order->company_id; + $fields->activity_type_id = Activity::UPDATE_PURCHASE_ORDER; + $fields->purchase_order_id = $this->purchase_order->id; + + $ar->save($fields, $this->purchase_order, Ninja::eventVars($this->company, $this->user)); + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->get("/api/v1/purchase_orders/{$this->purchase_order->hashed_id}?include=activities.history") + ->assertStatus(200); + + $arr = $response->json(); + + $activities = $arr['data']['activities']; + + foreach($activities as $activity) { + $this->assertTrue(count($activity['history']) >= 1); + } + + } + public function testPurchaseOrderBulkActions() { $i = $this->purchase_order->invitations->first(); @@ -194,7 +233,7 @@ class PurchaseOrderTest extends TestCase $x = $purchase_order->service()->markSent()->getPurchaseOrderPdf(); - nlog($x); + // nlog($x); } public function testPurchaseOrderRest() diff --git a/tests/MockAccountData.php b/tests/MockAccountData.php index 218520c45f..ee34248dc1 100644 --- a/tests/MockAccountData.php +++ b/tests/MockAccountData.php @@ -184,6 +184,11 @@ trait MockAccountData */ public $scheduler; + /** + * @var + */ + public $purchase_order; + public $contact; public $product; diff --git a/tests/Unit/RecurringDateTest.php b/tests/Unit/RecurringDateTest.php index 4321031b29..36c49e32b3 100644 --- a/tests/Unit/RecurringDateTest.php +++ b/tests/Unit/RecurringDateTest.php @@ -41,4 +41,19 @@ class RecurringDateTest extends TestCase $this->assertequals($trial_ends->format('Y-m-d'), '2021-12-03'); } + + public function testDateOverflowsForEndOfMonth() + { + $today = Carbon::parse('2022-01-31'); + + $next_month = $today->addMonthNoOverflow(); + + $this->assertEquals('2022-02-28', $next_month->format('Y-m-d')); + + // $next_month = $today->addMonthNoOverflow(); + + // $this->assertEquals('2022-03-31', $next_month->format('Y-m-d')); + + } + }