83 lines
10 KiB
PHP
83 lines
10 KiB
PHP
<?php
|
|
namespace App\Modules\Accounting;
|
|
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\DB;
|
|
use App\Shared\Context\CompanyContext;
|
|
use Tempo\Finance\Payables\Payables;
|
|
use Tempo\Finance\GeneralLedger\ReportsWorkspace;
|
|
use Tempo\Purchasing\Embedded\Models\AuditLog;
|
|
use Tempo\Purchasing\Embedded\Services\RequisitionWorkflow;
|
|
|
|
final class ComposedLedgerController
|
|
{
|
|
public function focused(Request $r,string $family)
|
|
{
|
|
[$alias,$target,$kind]=match($family){'sales'=>['invoice_id','sales_invoice_id','sales.invoice'],'cost'=>['order_id','cost_order_id','workshop.direct_cost.validated'],'industrial'=>['lot_id','industrial_lot_id','industry.cost.validated']};
|
|
$d=$r->validate([$alias=>'sometimes|required|integer|min:1','page'=>'sometimes|integer|min:1|max:100000']);abort_if(array_diff(array_keys($r->query()),array_keys($d)),422);$query=['source_kind'=>$kind];if(isset($d[$alias]))$query[$target]=(int)$d[$alias];if(isset($d['page']))$query['page']=(int)$d['page'];$r->query->replace($query);return $this->page($r,app(ComposedLedger::class));
|
|
}
|
|
public function receipt(Request $r,string $key,?string $domain=null)
|
|
{
|
|
abort_if($r->query()!==[],422);abort_unless($domain===null||in_array($domain,['recognition','settlement'],true),422);
|
|
try{return app(ComposedLedger::class)->run('view',function($engine,$actor)use($key,$domain){if($domain===null)$data=$engine->receipt($key);else{$service=app(\Tempo\Purchasing\Embedded\Services\SupplierPayables::class);$service->readContext($actor);$marker=DB::table('gl_sgf_composition')->where('tenant_id',$actor->tenant_id)->first();$financialScope=['tenant_id'=>(int)$actor->tenant_id,'company_id'=>(int)$marker->financial_company_id,'unit_id'=>(int)$marker->financial_unit_id];$data=\Tempo\Finance\Payables\ReceiptReader::read(DB::connection(),$domain,$financialScope,(int)$actor->id,$key,fn()=>$service->readContext($actor));}abort_if($data===null,404,'Comprovativo não encontrado para esta pessoa e contexto.');return response()->json($data)->header('Cache-Control','no-store');});}catch(\InvalidArgumentException $e){abort(422,$e->getMessage());}catch(\DomainException $e){abort(403,$e->getMessage());}
|
|
}
|
|
public function flow(Request $r)
|
|
{
|
|
$d=$r->validate(['source'=>'required|integer|min:1']);abort_if(array_diff(array_keys($r->query()),['source']),422);
|
|
return app(ComposedLedger::class)->run('view',function($engine,$actor)use($d){$id=DB::table('pa_obligations as o')->join('pa_sources as s','s.id','=','o.source_id')->where(['o.tenant_id'=>$actor->tenant_id,'o.company_id'=>0,'o.unit_id'=>0,'s.local_source_id'=>(int)$d['source']])->value('o.id');abort_unless($id,404,'Origem sem obrigação reconhecida neste contexto.');return redirect('/general-ledger?obligation_id='.$id);});
|
|
}
|
|
private function rights():array
|
|
{
|
|
$ctx=app(CompanyContext::class);$scope=$ctx->identifiers()+['unit_id'=>$ctx->unitId(),'user_id'=>$ctx->userId()];$permissions=DB::table('permission_grants')->where($scope)->pluck('permission')->all();$out=[];
|
|
foreach(['view','prepare','validate','post','correct_prepare','correct_validate','correct_post','period_prepare','period_validate','period_close'] as $right)$out[$right]=in_array('general_ledger.'.$right,$permissions,true);return $out;
|
|
}
|
|
|
|
public function page(Request $r,ComposedLedger $ledger)
|
|
{
|
|
$d=$r->validate(['page'=>'sometimes|integer|min:1|max:100000','accounts_page'=>'sometimes|integer|min:1|max:100000','accounts_per_page'=>'sometimes|integer|in:25,100','account_q'=>'sometimes|nullable|string|max:80','source_page'=>'sometimes|integer|min:1|max:100000','source_per_page'=>'sometimes|integer|in:25,100','source_kind'=>'sometimes|in:'.implode(',',ComposedLedgerCandidates::KINDS),'period_key'=>'sometimes|string|max:80','account_id'=>'sometimes|nullable|integer|min:1','obligation_id'=>'sometimes|integer|min:1','sales_invoice_id'=>'sometimes|integer|min:1','cost_order_id'=>'sometimes|integer|min:1','industrial_lot_id'=>'sometimes|integer|min:1']);abort_if(array_diff(array_keys($r->query()),array_keys($d)),422);if(($d['account_id']??null)===null)unset($d['account_id']);$rights=$this->rights();
|
|
return $ledger->run('view',function($engine,$actor,$binding,$origin,$scope,$stock,$hostRead)use($r,$d,$rights){
|
|
if(isset($d['sales_invoice_id']))abort_unless($hostRead(fn()=>\App\Modules\Services\QualifiedInvoiceSource::query()->where($scope)->where('id',$d['sales_invoice_id'])->exists()),403,'Documento fora do contexto e módulo.');
|
|
$data=$engine->listing(array_diff_key($d,['source_page'=>true,'source_per_page'=>true,'source_kind'=>true,'accounts_page'=>true,'accounts_per_page'=>true,'account_q'=>true]));$data['cycle']=$engine->cycleListing((int)($d['page']??1));$data['permissions']=$rights;$data['current_actor']=$actor->id;$data['sgf_scope']=$origin['sgf_host_scope'];
|
|
$data=array_merge($data,app(ComposedLedgerCandidates::class)->read($d,$actor,$scope,$stock,$hostRead));
|
|
$accounts=DB::table('gl_accounts')->where($scope);if(($d['account_q']??'')!==''){$needle='%'.str_replace(['!','%','_'],['!!','!%','!_'],$d['account_q']).'%';$accounts->where(fn($q)=>$q->whereRaw("code ILIKE ? ESCAPE '!'",[$needle])->orWhereRaw("label ILIKE ? ESCAPE '!'",[$needle]));}
|
|
$chart=$accounts->orderBy('code')->paginate((int)($d['accounts_per_page']??25),['id','code','label','currency','is_movement'],'accounts_page',(int)($d['accounts_page']??1));$data['chart_accounts']=$chart->items();$data['chart_pagination']=['page'=>$chart->currentPage(),'last_page'=>$chart->lastPage(),'per_page'=>$chart->perPage(),'total'=>$chart->total()];
|
|
if($r->is('api/*'))return response()->json($data)->header('Cache-Control','no-store');
|
|
return response()->view('accounting.composed-ledger',['data'=>$data,'context'=>$r->session()->get('company_context.token'),'csrf'=>csrf_token()])->header('Cache-Control','no-store');
|
|
});
|
|
}
|
|
|
|
public function command(Request $r,string $action,ComposedLedger $ledger)
|
|
{
|
|
$allowed=['prepare','validate','post','effect-prepare','correct-prepare','correct-validate','correct-post','period-prepare','period-validate','period-close'];abort_unless(in_array($action,$allowed,true),404);
|
|
$right=$action==='effect-prepare'?'prepare':str_replace('-','_',$action);$key=strtolower((string)$r->header('Idempotency-Key'));abort_unless(preg_match('/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/D',$key),422);$data=$r->all();abort_if(strlen(json_encode($data))>65536,422);
|
|
try{return $ledger->run($right,function($engine,$actor,$binding,$origin)use($action,$data,$key){$hash=hash('sha256',Payables::canonical([$action,$data]));$old=AuditLog::where('tenant_id',$actor->tenant_id)->where('action','sgf.ledger.command')->where('after->operation_key',$key)->first();if($old)abort_unless(($old->after['sgf_context_key']??null)===$origin['sgf_context_key']&&($old->after['request_hash']??null)===$hash,409,'Chave utilizada noutro contexto/conteúdo.');$result=$engine->command($action,$data,$key);if(!$old)app(RequisitionWorkflow::class)->audit($actor,'sgf.ledger.command',null,null,$origin+['operation_key'=>$key,'request_hash'=>$hash,'command'=>$action,'reason'=>$data['reason']??'','result'=>$result]);return response()->json($result)->header('Cache-Control','no-store');});}catch(\InvalidArgumentException $e){abort(422,$e->getMessage());}catch(\DomainException $e){abort(409,$e->getMessage());}
|
|
}
|
|
|
|
public function report(Request $r,string $kind,ComposedLedger $ledger)
|
|
{
|
|
abort_unless(in_array($kind,['trial-balance','account-ledger'],true),404);$d=$r->validate(['from_on'=>'required|date_format:Y-m-d','to_on'=>'required|date_format:Y-m-d|after_or_equal:from_on','account_id'=>'sometimes|integer|min:1','page'=>'sometimes|integer|min:1|max:100000','per_page'=>'sometimes|integer|in:25,100']);abort_if(array_diff(array_keys($r->query()),array_keys($d)),422);
|
|
try{return $ledger->run('view',function($engine)use($r,$kind,$d){$data=$kind==='trial-balance'?$engine->trialBalance($d):$engine->accountLedger($d);return $r->is('api/*')?response()->json($data)->header('Cache-Control','no-store'):response(ReportsWorkspace::render($data,['host'=>'SGF · livro integrado','return_url'=>'/general-ledger']))->header('Cache-Control','no-store');});}
|
|
catch(\InvalidArgumentException $e){abort(422,$e->getMessage());}
|
|
catch(\DomainException $e){abort(403,$e->getMessage());}
|
|
}
|
|
public function stockCommand(Request $r,string $action,ComposedLedger $ledger)
|
|
{
|
|
abort_unless(in_array($action,['rate-prepare','rate-review'],true),404);$right=$action==='rate-prepare'?'prepare':'validate';$key=strtolower((string)$r->header('Idempotency-Key'));abort_unless(preg_match('/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/D',$key),422);
|
|
$data=$r->all();abort_if(strlen(json_encode($data))>65536,422);
|
|
try{return $ledger->run($right,function($e,$actor,$b,$origin,$s,$stock)use($action,$data,$key){
|
|
$hash=hash('sha256',Payables::canonical([$action,$data]));
|
|
$old=AuditLog::where('tenant_id',$actor->tenant_id)->where('action','sgf.ledger.stock-command')->where('after->operation_key',$key)->first();
|
|
if($old)abort_unless(($old->after['sgf_context_key']??null)===$origin['sgf_context_key']&&($old->after['request_hash']??null)===$hash,409,'Chave utilizada noutro contexto/conteúdo.');
|
|
// The public review label maps to the original engine's validation command.
|
|
$result=$stock->command($action==='rate-review'?'rate-validate':$action,$data,$key);
|
|
if(!$old)app(RequisitionWorkflow::class)->audit($actor,'sgf.ledger.stock-command',null,null,$origin+['operation_key'=>$key,'request_hash'=>$hash,'command'=>$action,'reason'=>$data['reason']??'','result'=>$result]);
|
|
return response()->json($result)->header('Cache-Control','no-store');
|
|
});}catch(\InvalidArgumentException $e){abort(422,$e->getMessage());}catch(\DomainException $e){abort(409,$e->getMessage());}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
|