Update besar apk finance
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
|
||||
// Fallback kompatibilitas. Nilai database pada system_account_mappings akan
|
||||
// mengambil prioritas setelah migration Tahap 2 dijalankan.
|
||||
$config['system_accounts'] = array(
|
||||
'inventory' => 21,
|
||||
'cost_of_goods_sold' => 63,
|
||||
'accounts_receivable' => 19,
|
||||
'deferred_revenue' => 78,
|
||||
'inventory_in_sales' => 80,
|
||||
'goods_revenue' => 62,
|
||||
'customer_savings' => 70,
|
||||
'depreciation_expense' => 54,
|
||||
'sales_tax_payable' => 0,
|
||||
'customer_advances' => 0,
|
||||
'bad_debt_expense' => 0,
|
||||
'asset_disposal_gain' => 0,
|
||||
'asset_disposal_loss' => 0
|
||||
,'payroll_expense' => 0
|
||||
,'payroll_payable' => 0
|
||||
,'payroll_tax_payable' => 0
|
||||
,'payroll_bpjs_payable' => 0
|
||||
,'employee_loan_receivable' => 0
|
||||
,'realized_fx_gain' => 0
|
||||
,'realized_fx_loss' => 0
|
||||
,'unrealized_fx_gain' => 0
|
||||
,'unrealized_fx_loss' => 0
|
||||
);
|
||||
|
||||
$config['document_numbering'] = array(
|
||||
'journal' => array('prefix' => 'JR', 'padding' => 5),
|
||||
'invoice_journal' => array('prefix' => 'INV', 'padding' => 4),
|
||||
'invoice' => array('prefix' => 'INV', 'padding' => 4),
|
||||
'payment' => array('prefix' => 'PAY', 'padding' => 5),
|
||||
'reversal' => array('prefix' => 'REV', 'padding' => 5),
|
||||
'quotation' => array('prefix' => 'QUO', 'padding' => 5),
|
||||
'sales_order' => array('prefix' => 'SO', 'padding' => 5),
|
||||
'credit_note' => array('prefix' => 'CN', 'padding' => 5),
|
||||
'refund' => array('prefix' => 'RF', 'padding' => 5),
|
||||
'writeoff' => array('prefix' => 'WO', 'padding' => 5),
|
||||
'receipt' => array('prefix' => 'RCT', 'padding' => 6),
|
||||
'purchase_request' => array('prefix' => 'PR', 'padding' => 5),
|
||||
'purchase_order' => array('prefix' => 'PO', 'padding' => 5),
|
||||
'goods_receipt' => array('prefix' => 'GR', 'padding' => 5),
|
||||
'supplier_invoice' => array('prefix' => 'BILL', 'padding' => 5),
|
||||
'supplier_payment' => array('prefix' => 'SPAY', 'padding' => 5),
|
||||
'purchase_return' => array('prefix' => 'PRET', 'padding' => 5),
|
||||
'purchase_return_shipment' => array('prefix' => 'RSH', 'padding' => 5),
|
||||
'purchase_replacement' => array('prefix' => 'RPL', 'padding' => 5),
|
||||
'supplier_refund' => array('prefix' => 'SRF', 'padding' => 5),
|
||||
'supplier_refund_receipt' => array('prefix' => 'SRR', 'padding' => 5),
|
||||
'supplier_debit_note' => array('prefix' => 'SDN', 'padding' => 5)
|
||||
,'sales_delivery' => array('prefix' => 'SJ', 'padding' => 5)
|
||||
,'sales_delivery_journal' => array('prefix' => 'JSJ', 'padding' => 5)
|
||||
,'sales_return' => array('prefix' => 'SRET', 'padding' => 5)
|
||||
,'revenue_recognition' => array('prefix' => 'REVREC', 'padding' => 5)
|
||||
,'invoice_line_journal' => array('prefix' => 'JIL', 'padding' => 6)
|
||||
,'invoice_line_reversal' => array('prefix' => 'RIL', 'padding' => 6)
|
||||
,'stock_document' => array('prefix' => 'STK', 'padding' => 6)
|
||||
,'stock_reservation' => array('prefix' => 'RSV', 'padding' => 6)
|
||||
,'asset_event' => array('prefix' => 'AE', 'padding' => 6)
|
||||
,'fixed_asset' => array('prefix' => 'AST', 'padding' => 6)
|
||||
,'asset_maintenance' => array('prefix' => 'MNT', 'padding' => 6)
|
||||
,'asset_opname' => array('prefix' => 'AOP', 'padding' => 6)
|
||||
,'cash_transaction' => array('prefix' => 'CB', 'padding' => 6)
|
||||
,'cash_transfer' => array('prefix' => 'TRF', 'padding' => 6)
|
||||
,'bank_reconciliation' => array('prefix' => 'BRC', 'padding' => 6)
|
||||
,'payroll' => array('prefix' => 'PRL', 'padding' => 6)
|
||||
,'payroll_payment' => array('prefix' => 'PRP', 'padding' => 6)
|
||||
,'technician_equipment' => array('prefix' => 'TEQ', 'padding' => 6)
|
||||
);
|
||||
|
||||
$config['require_separate_journal_approver'] = filter_var(
|
||||
getenv('WORKFLOW_REQUIRE_SEPARATE_APPROVER') ?: 'false',
|
||||
FILTER_VALIDATE_BOOLEAN
|
||||
);
|
||||
@@ -89,7 +89,7 @@ $autoload['drivers'] = array();
|
||||
|
|
||||
| $autoload['helper'] = array('url', 'file');
|
||||
*/
|
||||
$autoload['helper'] = array('url', 'file', 'permission', 'activity', 'indo_date');
|
||||
$autoload['helper'] = array('url', 'file', 'permission', 'activity', 'indo_date', 'response');
|
||||
|
||||
/*
|
||||
| -------------------------------------------------------------------
|
||||
|
||||
@@ -0,0 +1,537 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Base Site URL
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| URL to your CodeIgniter root. Typically this will be your base URL,
|
||||
| WITH a trailing slash:
|
||||
|
|
||||
| http://example.com/
|
||||
|
|
||||
| WARNING: You MUST set this value!
|
||||
|
|
||||
| If it is not set, then CodeIgniter will try to guess the protocol and
|
||||
| path to your installation, but due to security concerns the hostname will
|
||||
| be set to $_SERVER['SERVER_ADDR'] if available, or localhost otherwise.
|
||||
| The auto-detection mechanism exists only for convenience during
|
||||
| development and MUST NOT be used in production!
|
||||
|
|
||||
| If you need to allow multiple domains, remember that this file is still
|
||||
| a PHP script and you can easily do that on your own.
|
||||
|
|
||||
*/
|
||||
$config['base_url'] = rtrim(getenv('APP_BASE_URL') ?: 'http://localhost/accounting_dev/', '/') . '/';
|
||||
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Index File
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Typically this will be your index.php file, unless you've renamed it to
|
||||
| something else. If you are using mod_rewrite to remove the page set this
|
||||
| variable so that it is blank.
|
||||
|
|
||||
*/
|
||||
$config['index_page'] = '';
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| URI PROTOCOL
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This item determines which server global should be used to retrieve the
|
||||
| URI string. The default setting of 'REQUEST_URI' works for most servers.
|
||||
| If your links do not seem to work, try one of the other delicious flavors:
|
||||
|
|
||||
| 'REQUEST_URI' Uses $_SERVER['REQUEST_URI']
|
||||
| 'QUERY_STRING' Uses $_SERVER['QUERY_STRING']
|
||||
| 'PATH_INFO' Uses $_SERVER['PATH_INFO']
|
||||
|
|
||||
| WARNING: If you set this to 'PATH_INFO', URIs will always be URL-decoded!
|
||||
*/
|
||||
$config['uri_protocol'] = 'REQUEST_URI';
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| URL suffix
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option allows you to add a suffix to all URLs generated by CodeIgniter.
|
||||
| For more information please see the user guide:
|
||||
|
|
||||
| https://codeigniter.com/userguide3/general/urls.html
|
||||
|
|
||||
| Note: This option is ignored for CLI requests.
|
||||
*/
|
||||
$config['url_suffix'] = '';
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Language
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This determines which set of language files should be used. Make sure
|
||||
| there is an available translation if you intend to use something other
|
||||
| than english.
|
||||
|
|
||||
*/
|
||||
$config['language'] = 'english';
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Character Set
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This determines which character set is used by default in various methods
|
||||
| that require a character set to be provided.
|
||||
|
|
||||
| See http://php.net/htmlspecialchars for a list of supported charsets.
|
||||
|
|
||||
*/
|
||||
$config['charset'] = 'UTF-8';
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Enable/Disable System Hooks
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| If you would like to use the 'hooks' feature you must enable it by
|
||||
| setting this variable to TRUE (boolean). See the user guide for details.
|
||||
|
|
||||
*/
|
||||
$config['enable_hooks'] = FALSE;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Class Extension Prefix
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This item allows you to set the filename/classname prefix when extending
|
||||
| native libraries. For more information please see the user guide:
|
||||
|
|
||||
| https://codeigniter.com/userguide3/general/core_classes.html
|
||||
| https://codeigniter.com/userguide3/general/creating_libraries.html
|
||||
|
|
||||
*/
|
||||
$config['subclass_prefix'] = 'MY_';
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Composer auto-loading
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Enabling this setting will tell CodeIgniter to look for a Composer
|
||||
| package auto-loader script in application/vendor/autoload.php.
|
||||
|
|
||||
| $config['composer_autoload'] = TRUE;
|
||||
|
|
||||
| Or if you have your vendor/ directory located somewhere else, you
|
||||
| can opt to set a specific path as well:
|
||||
|
|
||||
| $config['composer_autoload'] = '/path/to/vendor/autoload.php';
|
||||
|
|
||||
| For more information about Composer, please visit http://getcomposer.org/
|
||||
|
|
||||
| Note: This will NOT disable or override the CodeIgniter-specific
|
||||
| autoloading (application/config/autoload.php)
|
||||
*/
|
||||
$config['composer_autoload'] = FALSE;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Allowed URL Characters
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This lets you specify which characters are permitted within your URLs.
|
||||
| When someone tries to submit a URL with disallowed characters they will
|
||||
| get a warning message.
|
||||
|
|
||||
| As a security measure you are STRONGLY encouraged to restrict URLs to
|
||||
| as few characters as possible. By default only these are allowed: a-z 0-9~%.:_-
|
||||
|
|
||||
| Leave blank to allow all characters -- but only if you are insane.
|
||||
|
|
||||
| The configured value is actually a regular expression character group
|
||||
| and it will be executed as: ! preg_match('/^[<permitted_uri_chars>]+$/i
|
||||
|
|
||||
| DO NOT CHANGE THIS UNLESS YOU FULLY UNDERSTAND THE REPERCUSSIONS!!
|
||||
|
|
||||
*/
|
||||
$config['permitted_uri_chars'] = 'a-z 0-9~%.:_\-';
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Enable Query Strings
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| By default CodeIgniter uses search-engine friendly segment based URLs:
|
||||
| example.com/who/what/where/
|
||||
|
|
||||
| You can optionally enable standard query string based URLs:
|
||||
| example.com?who=me&what=something&where=here
|
||||
|
|
||||
| Options are: TRUE or FALSE (boolean)
|
||||
|
|
||||
| The other items let you set the query string 'words' that will
|
||||
| invoke your controllers and its functions:
|
||||
| example.com/index.php?c=controller&m=function
|
||||
|
|
||||
| Please note that some of the helpers won't work as expected when
|
||||
| this feature is enabled, since CodeIgniter is designed primarily to
|
||||
| use segment based URLs.
|
||||
|
|
||||
*/
|
||||
$config['enable_query_strings'] = FALSE;
|
||||
$config['controller_trigger'] = 'c';
|
||||
$config['function_trigger'] = 'm';
|
||||
$config['directory_trigger'] = 'd';
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Allow $_GET array
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| By default CodeIgniter enables access to the $_GET array. If for some
|
||||
| reason you would like to disable it, set 'allow_get_array' to FALSE.
|
||||
|
|
||||
| WARNING: This feature is DEPRECATED and currently available only
|
||||
| for backwards compatibility purposes!
|
||||
|
|
||||
*/
|
||||
$config['allow_get_array'] = TRUE;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Error Logging Threshold
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| You can enable error logging by setting a threshold over zero. The
|
||||
| threshold determines what gets logged. Threshold options are:
|
||||
|
|
||||
| 0 = Disables logging, Error logging TURNED OFF
|
||||
| 1 = Error Messages (including PHP errors)
|
||||
| 2 = Debug Messages
|
||||
| 3 = Informational Messages
|
||||
| 4 = All Messages
|
||||
|
|
||||
| You can also pass an array with threshold levels to show individual error types
|
||||
|
|
||||
| array(2) = Debug Messages, without Error Messages
|
||||
|
|
||||
| For a live site you'll usually only enable Errors (1) to be logged otherwise
|
||||
| your log files will fill up very fast.
|
||||
|
|
||||
*/
|
||||
$config['log_threshold'] = 4;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Error Logging Directory Path
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Leave this BLANK unless you would like to set something other than the default
|
||||
| application/logs/ directory. Use a full server path with trailing slash.
|
||||
|
|
||||
*/
|
||||
$config['log_path'] = '';
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Log File Extension
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The default filename extension for log files. The default 'php' allows for
|
||||
| protecting the log files via basic scripting, when they are to be stored
|
||||
| under a publicly accessible directory.
|
||||
|
|
||||
| Note: Leaving it blank will default to 'php'.
|
||||
|
|
||||
*/
|
||||
$config['log_file_extension'] = '';
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Log File Permissions
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The file system permissions to be applied on newly created log files.
|
||||
|
|
||||
| IMPORTANT: This MUST be an integer (no quotes) and you MUST use octal
|
||||
| integer notation (i.e. 0700, 0644, etc.)
|
||||
*/
|
||||
$config['log_file_permissions'] = 0644;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Date Format for Logs
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Each item that is logged has an associated date. You can use PHP date
|
||||
| codes to set your own date formatting
|
||||
|
|
||||
*/
|
||||
$config['log_date_format'] = 'Y-m-d H:i:s';
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Error Views Directory Path
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Leave this BLANK unless you would like to set something other than the default
|
||||
| application/views/errors/ directory. Use a full server path with trailing slash.
|
||||
|
|
||||
*/
|
||||
$config['error_views_path'] = '';
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Cache Directory Path
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Leave this BLANK unless you would like to set something other than the default
|
||||
| application/cache/ directory. Use a full server path with trailing slash.
|
||||
|
|
||||
*/
|
||||
$config['cache_path'] = '';
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Cache Include Query String
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Whether to take the URL query string into consideration when generating
|
||||
| output cache files. Valid options are:
|
||||
|
|
||||
| FALSE = Disabled
|
||||
| TRUE = Enabled, take all query parameters into account.
|
||||
| Please be aware that this may result in numerous cache
|
||||
| files generated for the same page over and over again.
|
||||
| array('q') = Enabled, but only take into account the specified list
|
||||
| of query parameters.
|
||||
|
|
||||
*/
|
||||
$config['cache_query_string'] = FALSE;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Encryption Key
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| If you use the Encryption class, you must set an encryption key.
|
||||
| See the user guide for more info.
|
||||
|
|
||||
| https://codeigniter.com/userguide3/libraries/encryption.html
|
||||
|
|
||||
*/
|
||||
$config['encryption_key'] = getenv('APP_ENCRYPTION_KEY') ?: 'development-only-change-this-key';
|
||||
|
||||
if (ENVIRONMENT === 'production' && $config['encryption_key'] === 'development-only-change-this-key') {
|
||||
show_error('APP_ENCRYPTION_KEY wajib dikonfigurasi pada environment production.', 500);
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Variables
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| 'sess_driver'
|
||||
|
|
||||
| The storage driver to use: files, database, redis, memcached
|
||||
|
|
||||
| 'sess_cookie_name'
|
||||
|
|
||||
| The session cookie name, must contain only [0-9a-z_-] characters
|
||||
|
|
||||
| 'sess_samesite'
|
||||
|
|
||||
| Session cookie SameSite attribute: Lax (default), Strict or None
|
||||
|
|
||||
| 'sess_expiration'
|
||||
|
|
||||
| The number of SECONDS you want the session to last.
|
||||
| Setting to 0 (zero) means expire when the browser is closed.
|
||||
|
|
||||
| 'sess_save_path'
|
||||
|
|
||||
| The location to save sessions to, driver dependent.
|
||||
|
|
||||
| For the 'files' driver, it's a path to a writable directory.
|
||||
| WARNING: Only absolute paths are supported!
|
||||
|
|
||||
| For the 'database' driver, it's a table name.
|
||||
| Please read up the manual for the format with other session drivers.
|
||||
|
|
||||
| IMPORTANT: You are REQUIRED to set a valid save path!
|
||||
|
|
||||
| 'sess_match_ip'
|
||||
|
|
||||
| Whether to match the user's IP address when reading the session data.
|
||||
|
|
||||
| WARNING: If you're using the database driver, don't forget to update
|
||||
| your session table's PRIMARY KEY when changing this setting.
|
||||
|
|
||||
| 'sess_time_to_update'
|
||||
|
|
||||
| How many seconds between CI regenerating the session ID.
|
||||
|
|
||||
| 'sess_regenerate_destroy'
|
||||
|
|
||||
| Whether to destroy session data associated with the old session ID
|
||||
| when auto-regenerating the session ID. When set to FALSE, the data
|
||||
| will be later deleted by the garbage collector.
|
||||
|
|
||||
| Other session cookie settings are shared with the rest of the application,
|
||||
| except for 'cookie_prefix' and 'cookie_httponly', which are ignored here.
|
||||
|
|
||||
*/
|
||||
$config['sess_driver'] = getenv('SESSION_DRIVER') ?: 'database';
|
||||
$config['sess_cookie_name'] = 'accounting_session';
|
||||
$config['sess_samesite'] = 'Lax';
|
||||
$config['sess_expiration'] = 7200;
|
||||
$config['sess_save_path'] = $config['sess_driver'] === 'database' ? 'ci_sessions' : sys_get_temp_dir();
|
||||
$config['sess_match_ip'] = FALSE;
|
||||
$config['sess_time_to_update'] = 300;
|
||||
$config['sess_regenerate_destroy'] = FALSE;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Cookie Related Variables
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| 'cookie_prefix' = Set a cookie name prefix if you need to avoid collisions
|
||||
| 'cookie_domain' = Set to .your-domain.com for site-wide cookies
|
||||
| 'cookie_path' = Typically will be a forward slash
|
||||
| 'cookie_secure' = Cookie will only be set if a secure HTTPS connection exists.
|
||||
| 'cookie_httponly' = Cookie will only be accessible via HTTP(S) (no javascript)
|
||||
| 'cookie_samesite' = Cookie's samesite attribute (Lax, Strict or None)
|
||||
|
|
||||
| Note: These settings (with the exception of 'cookie_prefix' and
|
||||
| 'cookie_httponly') will also affect sessions.
|
||||
|
|
||||
*/
|
||||
$config['cookie_prefix'] = 'accounting_';
|
||||
$config['cookie_domain'] = '';
|
||||
$config['cookie_path'] = '/';
|
||||
$config['cookie_secure'] = filter_var(getenv('COOKIE_SECURE') ?: (ENVIRONMENT === 'production' ? 'true' : 'false'), FILTER_VALIDATE_BOOLEAN);
|
||||
$config['cookie_httponly'] = TRUE;
|
||||
$config['cookie_samesite'] = 'Lax';
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Standardize newlines
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Determines whether to standardize newline characters in input data,
|
||||
| meaning to replace \r\n, \r, \n occurrences with the PHP_EOL value.
|
||||
|
|
||||
| WARNING: This feature is DEPRECATED and currently available only
|
||||
| for backwards compatibility purposes!
|
||||
|
|
||||
*/
|
||||
$config['standardize_newlines'] = FALSE;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Global XSS Filtering
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Determines whether the XSS filter is always active when GET, POST or
|
||||
| COOKIE data is encountered
|
||||
|
|
||||
| WARNING: This feature is DEPRECATED and currently available only
|
||||
| for backwards compatibility purposes!
|
||||
|
|
||||
*/
|
||||
$config['global_xss_filtering'] = FALSE;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Cross Site Request Forgery
|
||||
|--------------------------------------------------------------------------
|
||||
| Enables a CSRF cookie token to be set. When set to TRUE, token will be
|
||||
| checked on a submitted form. If you are accepting user data, it is strongly
|
||||
| recommended CSRF protection be enabled.
|
||||
|
|
||||
| 'csrf_token_name' = The token name
|
||||
| 'csrf_cookie_name' = The cookie name
|
||||
| 'csrf_expire' = The number in seconds the token should expire.
|
||||
| 'csrf_regenerate' = Regenerate token on every submission
|
||||
| 'csrf_exclude_uris' = Array of URIs which ignore CSRF checks
|
||||
*/
|
||||
$config['csrf_protection'] = TRUE;
|
||||
$config['csrf_token_name'] = 'accounting_csrf_token';
|
||||
$config['csrf_cookie_name'] = 'accounting_csrf_cookie';
|
||||
$config['csrf_expire'] = 7200;
|
||||
$config['csrf_regenerate'] = FALSE;
|
||||
$config['csrf_exclude_uris'] = array('iclock/cdata');
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Output Compression
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Enables Gzip output compression for faster page loads. When enabled,
|
||||
| the output class will test whether your server supports Gzip.
|
||||
| Even if it does, however, not all browsers support compression
|
||||
| so enable only if you are reasonably sure your visitors can handle it.
|
||||
|
|
||||
| Only used if zlib.output_compression is turned off in your php.ini.
|
||||
| Please do not use it together with httpd-level output compression.
|
||||
|
|
||||
| VERY IMPORTANT: If you are getting a blank page when compression is enabled it
|
||||
| means you are prematurely outputting something to your browser. It could
|
||||
| even be a line of whitespace at the end of one of your scripts. For
|
||||
| compression to work, nothing can be sent before the output buffer is called
|
||||
| by the output class. Do not 'echo' any values with compression enabled.
|
||||
|
|
||||
*/
|
||||
$config['compress_output'] = FALSE;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Master Time Reference
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Options are 'local' or any PHP supported timezone. This preference tells
|
||||
| the system whether to use your server's local time as the master 'now'
|
||||
| reference, or convert it to the configured one timezone. See the 'date
|
||||
| helper' page of the user guide for information regarding date handling.
|
||||
|
|
||||
*/
|
||||
$config['time_reference'] = 'local';
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Rewrite PHP Short Tags
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| If your PHP installation does not have short tag support enabled CI
|
||||
| can rewrite the tags on-the-fly, enabling you to utilize that syntax
|
||||
| in your view files. Options are TRUE or FALSE (boolean)
|
||||
|
|
||||
| Note: You need to have eval() enabled for this to work.
|
||||
|
|
||||
*/
|
||||
$config['rewrite_short_tags'] = FALSE;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Reverse Proxy IPs
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| If your server is behind a reverse proxy, you must whitelist the proxy
|
||||
| IP addresses from which CodeIgniter should trust headers such as
|
||||
| HTTP_X_FORWARDED_FOR and HTTP_CLIENT_IP in order to properly identify
|
||||
| the visitor's IP address.
|
||||
|
|
||||
| You can use both an array or a comma-separated list of proxy addresses,
|
||||
| as well as specifying whole subnets. Here are a few examples:
|
||||
|
|
||||
| Comma-separated: '10.0.1.200,192.168.5.0/24'
|
||||
| Array: array('10.0.1.200', '192.168.5.0/24')
|
||||
*/
|
||||
$config['proxy_ips'] = '';
|
||||
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
|
||||
/*
|
||||
| -------------------------------------------------------------------
|
||||
| DATABASE CONNECTIVITY SETTINGS
|
||||
| -------------------------------------------------------------------
|
||||
| This file will contain the settings needed to access your database.
|
||||
|
|
||||
| For complete instructions please consult the 'Database Connection'
|
||||
| page of the User Guide.
|
||||
|
|
||||
| -------------------------------------------------------------------
|
||||
| EXPLANATION OF VARIABLES
|
||||
| -------------------------------------------------------------------
|
||||
|
|
||||
| ['dsn'] The full DSN string describe a connection to the database.
|
||||
| ['hostname'] The hostname of your database server.
|
||||
| ['username'] The username used to connect to the database
|
||||
| ['password'] The password used to connect to the database
|
||||
| ['database'] The name of the database you want to connect to
|
||||
| ['dbdriver'] The database driver. e.g.: mysqli.
|
||||
| Currently supported:
|
||||
| cubrid, ibase, mssql, mysql, mysqli, oci8,
|
||||
| odbc, pdo, postgre, sqlite, sqlite3, sqlsrv
|
||||
| ['dbprefix'] You can add an optional prefix, which will be added
|
||||
| to the table name when using the Query Builder class
|
||||
| ['pconnect'] TRUE/FALSE - Whether to use a persistent connection
|
||||
| ['db_debug'] TRUE/FALSE - Whether database errors should be displayed.
|
||||
| ['cache_on'] TRUE/FALSE - Enables/disables query caching
|
||||
| ['cachedir'] The path to the folder where cache files should be stored
|
||||
| ['char_set'] The character set used in communicating with the database
|
||||
| ['dbcollat'] The character collation used in communicating with the database
|
||||
| NOTE: For MySQL and MySQLi databases, this setting is only used
|
||||
| as a backup if your server is running PHP < 5.2.3 or MySQL < 5.0.7
|
||||
| (and in table creation queries made with DB Forge).
|
||||
| There is an incompatibility in PHP with mysql_real_escape_string() which
|
||||
| can make your site vulnerable to SQL injection if you are using a
|
||||
| multi-byte character set and are running versions lower than these.
|
||||
| Sites using Latin-1 or UTF-8 database character set and collation are unaffected.
|
||||
| ['swap_pre'] A default table prefix that should be swapped with the dbprefix
|
||||
| ['encrypt'] Whether or not to use an encrypted connection.
|
||||
|
|
||||
| 'mysql' (deprecated), 'sqlsrv' and 'pdo/sqlsrv' drivers accept TRUE/FALSE
|
||||
| 'mysqli' and 'pdo/mysql' drivers accept an array with the following options:
|
||||
|
|
||||
| 'ssl_key' - Path to the private key file
|
||||
| 'ssl_cert' - Path to the public key certificate file
|
||||
| 'ssl_ca' - Path to the certificate authority file
|
||||
| 'ssl_capath' - Path to a directory containing trusted CA certificates in PEM format
|
||||
| 'ssl_cipher' - List of *allowed* ciphers to be used for the encryption, separated by colons (':')
|
||||
| 'ssl_verify' - TRUE/FALSE; Whether verify the server certificate or not
|
||||
|
|
||||
| ['compress'] Whether or not to use client compression (MySQL only)
|
||||
| ['stricton'] TRUE/FALSE - forces 'Strict Mode' connections
|
||||
| - good for ensuring strict SQL while developing
|
||||
| ['ssl_options'] Used to set various SSL options that can be used when making SSL connections.
|
||||
| ['failover'] array - A array with 0 or more data for connections if the main should fail.
|
||||
| ['save_queries'] TRUE/FALSE - Whether to "save" all executed queries.
|
||||
| NOTE: Disabling this will also effectively disable both
|
||||
| $this->db->last_query() and profiling of DB queries.
|
||||
| When you run a query, with this setting set to TRUE (default),
|
||||
| CodeIgniter will store the SQL statement for debugging purposes.
|
||||
| However, this may cause high memory usage, especially if you run
|
||||
| a lot of SQL queries ... disable this to avoid that problem.
|
||||
|
|
||||
| The $active_group variable lets you choose which connection group to
|
||||
| make active. By default there is only one group (the 'default' group).
|
||||
|
|
||||
| The $query_builder variables lets you determine whether or not to load
|
||||
| the query builder class.
|
||||
*/
|
||||
$active_group = 'default';
|
||||
$query_builder = TRUE;
|
||||
|
||||
$db['default'] = array(
|
||||
'dsn' => '',
|
||||
'hostname' => getenv('DB_HOST') ?: '127.0.0.1',
|
||||
'port' => (int) (getenv('DB_PORT') ?: 3306),
|
||||
'username' => getenv('DB_USERNAME') ?: '',
|
||||
'password' => getenv('DB_PASSWORD') ?: '',
|
||||
'database' => getenv('DB_DATABASE') ?: '',
|
||||
|
||||
'dbdriver' => 'mysqli', // WAJIB: driver PostgreSQL
|
||||
'dbprefix' => '',
|
||||
'pconnect' => FALSE,
|
||||
'db_debug' => (ENVIRONMENT !== 'production'),
|
||||
'cache_on' => FALSE,
|
||||
'cachedir' => '',
|
||||
'char_set' => 'utf8mb4',
|
||||
'dbcollat' => 'utf8mb4_unicode_ci',
|
||||
'swap_pre' => '',
|
||||
'encrypt' => FALSE,
|
||||
'compress' => FALSE,
|
||||
'stricton' => TRUE,
|
||||
'failover' => array(),
|
||||
'save_queries' => (ENVIRONMENT !== 'production')
|
||||
);
|
||||
|
||||
@@ -11,7 +11,7 @@ defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
| and disable it back when you're done.
|
||||
|
|
||||
*/
|
||||
$config['migration_enabled'] = FALSE;
|
||||
$config['migration_enabled'] = TRUE;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
||||
@@ -76,6 +76,7 @@ return array(
|
||||
'gif' => 'image/gif',
|
||||
'jpeg' => array('image/jpeg', 'image/pjpeg'),
|
||||
'jpg' => array('image/jpeg', 'image/pjpeg'),
|
||||
'jfif' => array('image/jpeg', 'image/pjpeg'),
|
||||
'jpe' => array('image/jpeg', 'image/pjpeg'),
|
||||
'jp2' => array('image/jp2', 'video/mj2', 'image/jpx', 'image/jpm'),
|
||||
'j2k' => array('image/jp2', 'video/mj2', 'image/jpx', 'image/jpm'),
|
||||
@@ -86,6 +87,8 @@ return array(
|
||||
'mj2' => array('image/jp2', 'video/mj2', 'image/jpx', 'image/jpm'),
|
||||
'mjp2' => array('image/jp2', 'video/mj2', 'image/jpx', 'image/jpm'),
|
||||
'png' => array('image/png', 'image/x-png'),
|
||||
'webp' => 'image/webp',
|
||||
'avif' => 'image/avif',
|
||||
'tiff' => 'image/tiff',
|
||||
'tif' => 'image/tiff',
|
||||
'heic' => 'image/heic',
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
|
||||
$config['permission_features'] = array(
|
||||
'dashboard' => 'Dashboard', 'approvals' => 'Daftar Approval',
|
||||
'invoices' => 'Penjualan, Invoice & Piutang', 'purchases' => 'Pembelian, Supplier & Hutang',
|
||||
'items' => 'Persediaan & Gudang',
|
||||
'technician_equipment' => 'Peralatan Teknisi - Akses',
|
||||
'technician_equipment_all' => 'Peralatan Teknisi - Lihat Seluruh Teknisi',
|
||||
'technician_attachment' => 'Peralatan Teknisi - Lihat Lampiran',
|
||||
'technician_handover' => 'Peralatan Teknisi - Serah Terima',
|
||||
'technician_installation' => 'Peralatan Teknisi - Pasang & Lepas',
|
||||
'technician_transfer' => 'Peralatan Teknisi - Transfer',
|
||||
'technician_return' => 'Peralatan Teknisi - Pengembalian',
|
||||
'technician_inspection' => 'Peralatan Teknisi - Pemeriksaan',
|
||||
'technician_condition' => 'Peralatan Teknisi - Rusak/Hilang/Nonaktif',
|
||||
'technician_correction' => 'Peralatan Teknisi - Koreksi',
|
||||
'asset' => 'Aset Tetap',
|
||||
'jurnal' => 'Jurnal & Periode Accounting',
|
||||
'reports' => 'Laporan Profesional', 'budgets' => 'Budget',
|
||||
'employees' => 'HR, Absensi & Payroll'
|
||||
);
|
||||
$config['permission_actions'] = array(
|
||||
'can_view' => 'Lihat', 'can_create' => 'Buat', 'can_update' => 'Ubah',
|
||||
'can_delete' => 'Hapus', 'can_submit' => 'Ajukan', 'can_approve' => 'Approve', 'can_post' => 'Posting',
|
||||
'can_export' => 'Export', 'can_import' => 'Import'
|
||||
);
|
||||
@@ -53,6 +53,9 @@ $route['default_controller'] = 'dashboard';
|
||||
$route['404_override'] = '';
|
||||
$route['translate_uri_dashes'] = FALSE;
|
||||
|
||||
$route['login'] = 'auth/index';
|
||||
$route['logout'] = 'auth/logout';
|
||||
|
||||
// untuk webhook absensi
|
||||
$route['iclock/cdata'] = 'AttendanceWebhook/cdata';
|
||||
|
||||
@@ -63,3 +66,14 @@ $route['dbcompare/sql_download'] = 'Dbcompare/sql_download';
|
||||
// teknisi log barang
|
||||
$route['teknisi/log_barang'] = 'teknisi/log_barang';
|
||||
|
||||
// Invoice Supplier cukup diverifikasi melalui three-way matching; bukan approval.
|
||||
$route['purchases/submit_bill/(:num)'] = 'purchases/verify_bill/$1';
|
||||
$route['purchases/approve_bill/(:num)'] = 'purchases/verify_bill/$1';
|
||||
|
||||
// Proses pengecualian pembelian dipisahkan dari lima langkah workflow normal.
|
||||
$route['purchases/returns/pdf/(:any)/(:num)'] = 'purchasereturns/pdf/$1/$2';
|
||||
$route['purchases/returns/detail/(:any)/(:num)'] = 'purchasereturns/detail/$1/$2';
|
||||
$route['purchases/returns/(:any)/(:num)'] = 'purchasereturns/$1/$2';
|
||||
$route['purchases/returns/(:any)'] = 'purchasereturns/$1';
|
||||
$route['purchases/returns'] = 'purchasereturns/index';
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
|
||||
/*
|
||||
* Daftar halaman yang membutuhkan library frontend tambahan.
|
||||
* File ini dibaca terpisah oleh header dan footer karena setiap view
|
||||
* CodeIgniter mempunyai scope variabelnya sendiri.
|
||||
*/
|
||||
$config['ui_data_pages'] = array(
|
||||
'items', 'draft_items', 'item_historys', 'kode_barang',
|
||||
'peralatan_teknisi', 'warehouses', 'customers', 'employees', 'shift',
|
||||
'holidays', 'attendance_monitoring', 'attendance_monthly',
|
||||
'leave_requests', 'generate_schedule', 'payroll', 'suppliers',
|
||||
'purchases', 'payables', 'receivables', 'sales_documents', 'sales_invoice',
|
||||
'invoice_draft', 'invoice_paid', 'jurnal', 'basejurnal', 'accounts',
|
||||
'accounting_periods', 'inventory_professional', 'inventory_overview',
|
||||
'inventory_operations', 'inventory_locations', 'inventory_reports', 'professional_reports',
|
||||
'hr_payroll', 'fixed_assets', 'asset',
|
||||
'lokasiasset', 'users', 'activity_logs', 'asset', 'history', 'pesan',
|
||||
'kelas', 'kamar', 'siswa', 'attendance_monitoring',
|
||||
'attendance_monthly', 'buku_besar'
|
||||
);
|
||||
|
||||
$config['ui_select_pages'] = array_merge(
|
||||
$config['ui_data_pages'],
|
||||
array('budgets', 'companies')
|
||||
);
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
class Accountingattachments extends MY_Controller
|
||||
{
|
||||
private $imageTypes='jpg|jpeg|jpe|jfif|png|webp|gif|bmp|tif|tiff|heic|heif|avif|jp2|j2k|jpf|jpg2|jpx|ico';
|
||||
public function upload(){
|
||||
$type=$this->input->post('entity_type');$id=(int)$this->input->post('entity_id');$purchaseTypes=array('purchase_order','goods_receipt','supplier_invoice','supplier_payment','purchase_return','purchase_return_shipment','purchase_replacement','supplier_debit_note','purchase_refund','supplier_refund','supplier_refund_receipt');
|
||||
if(!in_array($type,array_merge(array('asset','asset_maintenance','asset_opname','cash_transaction','cash_transfer','bank_reconciliation','stock_document','technician_equipment_document','customer_equipment_registry'),$purchaseTypes),true)||$id<1)return json_response(false,'Target lampiran tidak valid.',array(),422);
|
||||
$this->assertEntityAccess((object)array('entity_type'=>$type,'entity_id'=>$id));
|
||||
if($type==='asset')$this->requirePermission('asset','can_create');
|
||||
if($type==='asset_maintenance')$this->requirePermission('asset','can_update');
|
||||
if($type==='asset_opname')$this->requirePermission('asset','can_create');
|
||||
if($type==='asset'&&!$this->db->get_where('assets',array('id'=>$id,'company_id'=>(int)$this->session->userdata('company_id'),'workflow_status'=>'draft'))->row())return json_response(false,'Lampiran perolehan hanya dapat ditambah saat draft.',array(),422);
|
||||
if($type==='asset_opname'&&!$this->db->get_where('asset_opnames',array('id'=>$id,'company_id'=>(int)$this->session->userdata('company_id'),'status'=>'draft'))->row())return json_response(false,'Bukti opname hanya dapat ditambah saat draft.',array(),422);
|
||||
if($type==='stock_document'){$this->assertEntityAccess((object)array('entity_type'=>$type,'entity_id'=>$id));$this->requirePermission('items','can_create');}
|
||||
if(empty($_FILES['attachment']['tmp_name'])||!is_uploaded_file($_FILES['attachment']['tmp_name']))return json_response(false,'Pilih file gambar atau PDF.',array(),422);
|
||||
$tmp=$_FILES['attachment']['tmp_name'];$mime=function_exists('finfo_open')?(new finfo(FILEINFO_MIME_TYPE))->file($tmp):($_FILES['attachment']['type']??'');
|
||||
if($mime!=='application/pdf'&&strpos((string)$mime,'image/')!==0)return json_response(false,'Isi file bukan gambar atau PDF yang valid.',array(),422);
|
||||
$dir=FCPATH.'uploads/accounting/';if(!is_dir($dir)&&!mkdir($dir,0750,true))return json_response(false,'Folder lampiran tidak tersedia.',array(),500);
|
||||
$this->load->library('upload',array('upload_path'=>$dir,'allowed_types'=>'pdf|'.$this->imageTypes,'max_size'=>5120,'encrypt_name'=>true));
|
||||
if(!$this->upload->do_upload('attachment'))return json_response(false,'File '.$_FILES['attachment']['name'].' ditolak: '.strip_tags($this->upload->display_errors()),array(),422);
|
||||
$f=$this->upload->data();$module=in_array($type,$purchaseTypes,true)?'purchase':($type==='stock_document'?'inventory':(strpos($type,'asset')===0?'fixed_asset':'cash_bank'));$row=array('module'=>$module,'entity_type'=>$type,'entity_id'=>$id,'original_name'=>$f['orig_name'],'stored_name'=>$f['file_name'],'mime_type'=>$f['file_type'],'file_size'=>$f['file_size']*1024,'sha256'=>hash_file('sha256',$f['full_path']),'uploaded_by'=>$this->session->userdata('user_id'),'created_at'=>date('Y-m-d H:i:s'));if($this->db->field_exists('company_id','transaction_attachments'))$row['company_id']=(int)$this->session->userdata('company_id');$this->db->insert('transaction_attachments',$row);return json_response(true,'Lampiran berhasil disimpan.',array('id'=>$this->db->insert_id()));
|
||||
}
|
||||
public function download($id){$f=$this->db->get_where('transaction_attachments',array('id'=>(int)$id))->row();if(!$f)show_404();$this->assertEntityAccess($f);$path=FCPATH.'uploads/accounting/'.$f->stored_name;if(!$this->safePath($path))show_404();$this->load->helper('download');force_download($f->original_name,file_get_contents($path));}
|
||||
public function preview($id){$f=$this->db->get_where('transaction_attachments',array('id'=>(int)$id))->row();if(!$f)show_404();$this->assertEntityAccess($f);$path=FCPATH.'uploads/accounting/'.$f->stored_name;if(!$this->safePath($path))show_404();$mime=function_exists('finfo_open')?(new finfo(FILEINFO_MIME_TYPE))->file($path):$f->mime_type;if($mime!=='application/pdf'&&strpos((string)$mime,'image/')!==0)show_404();$name=str_replace(array('"',"\r","\n"),'',basename($f->original_name));$this->output->set_header('Content-Type: '.$mime)->set_header('Content-Disposition: inline; filename="'.$name.'"')->set_header('Content-Length: '.filesize($path))->set_header('X-Content-Type-Options: nosniff')->set_output(file_get_contents($path));}
|
||||
private function assertEntityAccess($file){$company=(int)$this->session->userdata('company_id');if(in_array($file->entity_type,array('technician_equipment_document','customer_equipment_registry'),true)){$table=$file->entity_type==='technician_equipment_document'?'technician_equipment_documents':'customer_equipment_registry';$owned=$this->db->get_where($table,array('id'=>(int)$file->entity_id,'company_id'=>$company))->row();if(!$owned)show_error('Akses lampiran ditolak.',403,'Akses ditolak');$permissions=$this->session->userdata('permissions');$known=is_array($permissions)&&array_key_exists('technician_attachment',$permissions);$fallback=!$known&&(check_permission('technician_equipment','can_view')||check_permission('items','can_view'));if(!is_master_admin_user()&&!check_permission('technician_attachment','can_view')&&!$fallback)show_error('Akses lampiran ditolak.',403,'Akses ditolak');return;}if($file->entity_type==='technician_installation'){$owned=$this->db->select('b.id')->from('item_barcodes b')->join('items i','i.id=b.item_id')->where(array('b.id'=>(int)$file->entity_id,'i.company_id'=>$company))->get()->row();if(!$owned)show_error('Akses lampiran ditolak.',403,'Akses ditolak');$this->requirePermission('items','can_view');return;}if($file->entity_type==='technician_status_change'){$owned=$this->db->get_where('technician_item_status_logs',array('id'=>(int)$file->entity_id,'company_id'=>$company))->row();if(!$owned)show_error('Akses lampiran ditolak.',403,'Akses ditolak');$this->requirePermission('items','can_view');return;}$map=array('stock_document'=>array('stock_documents','items'),'asset'=>array('assets','asset'),'asset_maintenance'=>array('asset_maintenance','asset'),'asset_opname'=>array('asset_opnames','asset'));if(!isset($map[$file->entity_type]))return;list($table,$feature)=$map[$file->entity_type];if(!$this->db->get_where($table,array('id'=>(int)$file->entity_id,'company_id'=>$company))->row())show_error('Akses lampiran ditolak.',403,'Akses ditolak');$this->requirePermission($feature,'can_view');}
|
||||
private function safePath($path){$root=realpath(FCPATH.'uploads/accounting');$real=realpath($path);return$root&&$real&&strpos($real,$root.DIRECTORY_SEPARATOR)===0&&is_file($real);}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
|
||||
/** Pemeriksaan read-only fondasi accounting untuk deployment dan CI. */
|
||||
class Accountingcheck extends CI_Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
if (!$this->input->is_cli_request()) {
|
||||
show_error('Accounting check hanya dapat dijalankan melalui CLI.', 403, 'Akses ditolak');
|
||||
}
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$failures = array();
|
||||
foreach (array('journals', 'journal_details', 'accounts', 'document_sequences', 'system_account_mappings', 'fiscal_years', 'accounting_periods', 'journal_status_histories') as $table) {
|
||||
if (!$this->db->table_exists($table)) {
|
||||
$failures[] = 'Tabel tidak ditemukan: ' . $table;
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($failures)) {
|
||||
try {
|
||||
$this->load->library('AccountMappingService');
|
||||
$this->load->library('JournalService');
|
||||
foreach (array(
|
||||
'inventory', 'cost_of_goods_sold', 'accounts_receivable', 'deferred_revenue',
|
||||
'inventory_in_sales', 'goods_revenue', 'customer_savings', 'depreciation_expense'
|
||||
) as $mappingKey) {
|
||||
$this->accountmappingservice->get($mappingKey);
|
||||
}
|
||||
$testTotals = $this->journalservice->totals(array(
|
||||
array('debit' => 100, 'kredit' => 0),
|
||||
array('debit' => 0, 'kredit' => 100)
|
||||
));
|
||||
if ($testTotals['debit'] !== 100.0 || $testTotals['kredit'] !== 100.0) {
|
||||
$failures[] = 'JournalService gagal menghitung total jurnal.';
|
||||
}
|
||||
} catch (Throwable $exception) {
|
||||
$failures[] = 'Service foundation gagal dimuat: ' . $exception->getMessage();
|
||||
}
|
||||
|
||||
$invalidMappings = $this->db->query(
|
||||
'SELECT m.mapping_key FROM system_account_mappings m '
|
||||
. 'LEFT JOIN accounts a ON a.id = m.account_id '
|
||||
. 'WHERE m.is_active = 1 AND (a.id IS NULL OR a.is_active <> 1 OR a.allow_posting <> 1)'
|
||||
)->result();
|
||||
foreach ($invalidMappings as $mapping) {
|
||||
$failures[] = 'Mapping akun tidak valid: ' . $mapping->mapping_key;
|
||||
}
|
||||
|
||||
$duplicates = $this->db->query(
|
||||
"SELECT no_ref, COUNT(*) total FROM journals WHERE no_ref IS NOT NULL AND no_ref <> '' "
|
||||
. 'GROUP BY no_ref HAVING COUNT(*) > 1 LIMIT 20'
|
||||
)->result();
|
||||
foreach ($duplicates as $duplicate) {
|
||||
$failures[] = 'Nomor jurnal duplikat: ' . $duplicate->no_ref;
|
||||
}
|
||||
|
||||
$unbalanced = $this->db->query(
|
||||
'SELECT j.id, j.no_ref FROM journals j '
|
||||
. 'JOIN journal_details d ON d.journal_id = j.id '
|
||||
. 'GROUP BY j.id, j.no_ref '
|
||||
. 'HAVING ABS(SUM(d.debit) - SUM(d.kredit)) > 0.009 LIMIT 20'
|
||||
)->result();
|
||||
foreach ($unbalanced as $journal) {
|
||||
$failures[] = 'Jurnal tidak balance: ' . $journal->no_ref . ' (ID ' . $journal->id . ')';
|
||||
}
|
||||
|
||||
$missingPeriods = $this->db->where('accounting_period_id IS NULL', null, false)->count_all_results('journals');
|
||||
if ($missingPeriods > 0) {
|
||||
$failures[] = $missingPeriods . ' jurnal belum terhubung ke periode accounting.';
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($failures)) {
|
||||
foreach ($failures as $failure) {
|
||||
fwrite(STDERR, '[FAIL] ' . $failure . PHP_EOL);
|
||||
}
|
||||
exit(1);
|
||||
}
|
||||
|
||||
echo '[OK] Tabel fondasi, mapping akun, nomor referensi, dan keseimbangan jurnal valid.' . PHP_EOL;
|
||||
}
|
||||
|
||||
public function schema()
|
||||
{
|
||||
foreach (array('journals', 'journal_details', 'accounts', 'roles', 'users') as $table) {
|
||||
echo '--- ' . $table . ' ---' . PHP_EOL;
|
||||
foreach ($this->db->field_data($table) as $field) {
|
||||
echo implode(' | ', array(
|
||||
$field->name,
|
||||
$field->type,
|
||||
isset($field->max_length) ? $field->max_length : '',
|
||||
!empty($field->primary_key) ? 'PK' : '',
|
||||
isset($field->default) ? 'default=' . $field->default : ''
|
||||
)) . PHP_EOL;
|
||||
}
|
||||
}
|
||||
|
||||
$stats = $this->db->select('COUNT(*) total, MIN(tanggal) min_date, MAX(tanggal) max_date', false)->get('journals')->row();
|
||||
echo 'journals=' . $stats->total . ' range=' . $stats->min_date . '..' . $stats->max_date . PHP_EOL;
|
||||
|
||||
echo '--- journal status ---' . PHP_EOL;
|
||||
foreach ($this->db->select('status, COUNT(*) total', false)->group_by('status')->get('journals')->result() as $row) {
|
||||
echo $row->status . '=' . $row->total . PHP_EOL;
|
||||
}
|
||||
|
||||
echo '--- column definitions ---' . PHP_EOL;
|
||||
$columns = $this->db->query(
|
||||
"SELECT TABLE_NAME, COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE FROM information_schema.COLUMNS "
|
||||
. "WHERE TABLE_SCHEMA = DATABASE() AND ((TABLE_NAME = 'journals' AND COLUMN_NAME = 'status') "
|
||||
. "OR (TABLE_NAME = 'accounts' AND COLUMN_NAME IN ('tipe','posisi','kategori','sub_tipe')))"
|
||||
)->result();
|
||||
foreach ($columns as $column) {
|
||||
echo $column->TABLE_NAME . '.' . $column->COLUMN_NAME . '=' . $column->COLUMN_TYPE . ' nullable=' . $column->IS_NULLABLE . PHP_EOL;
|
||||
}
|
||||
|
||||
echo '--- duplicate account codes ---' . PHP_EOL;
|
||||
$duplicates = $this->db->select('kode_akun, COUNT(*) total', false)->group_by('kode_akun')->having('COUNT(*) >', 1)->get('accounts')->result();
|
||||
echo count($duplicates) . PHP_EOL;
|
||||
echo '--- roles ---' . PHP_EOL;
|
||||
foreach ($this->db->order_by('id', 'ASC')->get('roles')->result() as $role) {
|
||||
echo $role->id . '=' . $role->nama_role . PHP_EOL;
|
||||
}
|
||||
echo '--- users per role ---' . PHP_EOL;
|
||||
foreach ($this->db->select('roles.nama_role, COUNT(users.id) total')->from('roles')->join('users', 'users.role_id = roles.id', 'left')->group_by('roles.id')->get()->result() as $role) {
|
||||
echo $role->nama_role . '=' . $role->total . PHP_EOL;
|
||||
}
|
||||
}
|
||||
|
||||
public function periodtrigger()
|
||||
{
|
||||
$this->db->trans_begin();
|
||||
try {
|
||||
$reference = 'CHECK-' . date('YmdHis') . '-' . mt_rand(1000, 9999);
|
||||
$this->db->insert('journals', array(
|
||||
'tanggal' => date('Y-m-d'),
|
||||
'no_ref' => $reference,
|
||||
'keterangan' => 'Temporary period trigger check',
|
||||
'created_by' => 1,
|
||||
'status' => 'posted'
|
||||
));
|
||||
$row = $this->db->get_where('journals', array('id' => $this->db->insert_id()))->row();
|
||||
if (!$row || !$row->accounting_period_id || !$row->posted_at) {
|
||||
throw new RuntimeException('Trigger tidak mengisi periode atau metadata posting.');
|
||||
}
|
||||
echo '[OK] Period trigger mengisi accounting_period_id dan metadata posted.' . PHP_EOL;
|
||||
} catch (Throwable $exception) {
|
||||
fwrite(STDERR, '[FAIL] ' . $exception->getMessage() . PHP_EOL);
|
||||
$this->db->trans_rollback();
|
||||
exit(1);
|
||||
}
|
||||
$this->db->trans_rollback();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
|
||||
class Accountingperiods extends MY_Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->requirePermission('jurnal');
|
||||
$this->load->model('FiscalPeriodModel', 'fiscalPeriodModel');
|
||||
$this->load->library('FiscalPeriodService');
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$data = array('active_menu' => 'accounting_periods');
|
||||
$this->load->view('partials/header', $data);
|
||||
$this->load->view('accounting_periods/layout');
|
||||
$this->load->view('partials/footer');
|
||||
}
|
||||
|
||||
public function get_data()
|
||||
{
|
||||
$rows = array();
|
||||
foreach ($this->fiscalPeriodModel->allWithFiscalYear() as $period) {
|
||||
$class = array('open' => 'success', 'soft_closed' => 'warning', 'closed' => 'danger');
|
||||
$actions = '<button class="btn btn-sm btn-outline-success btn-period-status" data-id="'.$period->id.'" data-status="open">Open</button> '
|
||||
. '<button class="btn btn-sm btn-outline-warning btn-period-status" data-id="'.$period->id.'" data-status="soft_closed">Soft Close</button> '
|
||||
. '<button class="btn btn-sm btn-outline-danger btn-period-status" data-id="'.$period->id.'" data-status="closed">Close</button>';
|
||||
$rows[] = array(
|
||||
$period->fiscal_year_name,
|
||||
$period->period_number,
|
||||
$period->name,
|
||||
$period->start_date,
|
||||
$period->end_date,
|
||||
'<span class="badge bg-'.$class[$period->status].'">'.strtoupper(str_replace('_', ' ', $period->status)).'</span>',
|
||||
$actions
|
||||
);
|
||||
}
|
||||
return json_response(true, 'Data periode berhasil dimuat.', array('data' => $rows));
|
||||
}
|
||||
|
||||
public function change_status($id)
|
||||
{
|
||||
if (strtoupper($this->input->method()) !== 'POST') {
|
||||
return json_response(false, 'Metode request tidak diizinkan.', array(), 405);
|
||||
}
|
||||
try {
|
||||
$period = $this->fiscalperiodservice->changeStatus(
|
||||
$id,
|
||||
$this->input->post('status', true),
|
||||
$this->session->userdata('user_id'),
|
||||
$this->input->post('reason', true)
|
||||
);
|
||||
log_activity('Accounting Period', 'change_status', 'Periode '.$period->name.' menjadi '.$period->status, 'success');
|
||||
return json_response(true, 'Status periode berhasil diperbarui.', array('period' => $period));
|
||||
} catch (Throwable $exception) {
|
||||
return business_exception_response($exception);
|
||||
}
|
||||
}
|
||||
|
||||
public function create_year()
|
||||
{
|
||||
if (strtoupper($this->input->method()) !== 'POST') {
|
||||
return json_response(false, 'Metode request tidak diizinkan.', array(), 405);
|
||||
}
|
||||
try {
|
||||
$id = $this->fiscalperiodservice->createCalendarYear(
|
||||
$this->input->post('year', true),
|
||||
$this->session->userdata('user_id')
|
||||
);
|
||||
log_activity('Fiscal Year', 'create', 'Membuat tahun buku ID '.$id, 'success');
|
||||
return json_response(true, 'Tahun buku dan 12 periode berhasil dibuat.', array('fiscal_year_id' => $id));
|
||||
} catch (Throwable $exception) {
|
||||
return business_exception_response($exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
|
||||
class Accountingrepair extends CI_Controller
|
||||
{
|
||||
public function __construct() { parent::__construct(); if(!is_cli()) show_404(); $this->load->library('AuditService'); }
|
||||
public function duplicate_refs()
|
||||
{
|
||||
if (strtolower((string)getenv('APP_ENV')) === 'production') { echo "Ditolak: repair ini hanya untuk development.\n"; return; }
|
||||
$groups=$this->db->select('no_ref, COUNT(*) total')->from('journals')->group_by('no_ref')->having('COUNT(*) >',1)->get()->result();
|
||||
$changed=0; $this->db->trans_begin();
|
||||
try {
|
||||
foreach($groups as $group) {
|
||||
$rows=$this->db->query('SELECT id,no_ref FROM journals WHERE no_ref=? ORDER BY id ASC FOR UPDATE',array($group->no_ref))->result();
|
||||
array_shift($rows);
|
||||
foreach($rows as $row) {
|
||||
$suffix='-D'.$row->id; $new=substr($row->no_ref,0,50-strlen($suffix)).$suffix;
|
||||
$this->db->where('id',$row->id)->update('journals',array('no_ref'=>$new));
|
||||
$this->auditservice->record('development_repair','journal',$row->id,'renumber_duplicate_reference',array('no_ref'=>$row->no_ref),array('no_ref'=>$new),null);
|
||||
echo $row->no_ref.' -> '.$new."\n"; $changed++;
|
||||
}
|
||||
}
|
||||
if(!$this->db->trans_status()) throw new RuntimeException('Repair database gagal.');
|
||||
$this->db->trans_commit(); echo "Selesai: {$changed} referensi diperbaiki.\n";
|
||||
} catch(Throwable $e) { $this->db->trans_rollback(); echo 'Gagal: '.$e->getMessage()."\n"; }
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,14 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
|
||||
class Accounts extends CI_Controller {
|
||||
class Accounts extends MY_Controller {
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->requirePermission('jurnal');
|
||||
$this->load->model('DynamicModel', 'dm');
|
||||
$this->load->library('ChartOfAccountsService');
|
||||
|
||||
if (!$this->session->userdata('logged_in')) {
|
||||
redirect('auth');
|
||||
@@ -34,7 +36,7 @@ class Accounts extends CI_Controller {
|
||||
{
|
||||
$this->dm->setTable('accounts')
|
||||
->setColumn(
|
||||
[null,'kode_akun','nama_akun','tipe','posisi','kategori',null],
|
||||
[null,'kode_akun','priority','nama_akun','tipe','posisi','kategori','is_header','is_hidden','is_active',null],
|
||||
['kode_akun','nama_akun','tipe'],
|
||||
['kode_akun'=>'ASC']
|
||||
);
|
||||
@@ -55,6 +57,9 @@ class Accounts extends CI_Controller {
|
||||
ucfirst($row->tipe),
|
||||
ucfirst($row->posisi),
|
||||
ucfirst($row->kategori),
|
||||
$row->is_header ? 'Header' : 'Transaksi',
|
||||
$row->is_hidden ? '<span class="badge bg-warning text-dark">Disembunyikan</span>' : '<span class="badge bg-info text-dark">Ditampilkan</span>',
|
||||
$row->is_active ? '<span class="badge bg-success">Aktif</span>' : '<span class="badge bg-secondary">Nonaktif</span>',
|
||||
'
|
||||
<button class="btn btn-sm btn-warning btn-edit" data-id="'.$row->id.'">Edit</button>
|
||||
<button class="btn btn-sm btn-danger btn-delete" data-id="'.$row->id.'">Delete</button>
|
||||
@@ -73,64 +78,21 @@ class Accounts extends CI_Controller {
|
||||
// ================= SAVE
|
||||
public function save()
|
||||
{
|
||||
$tipe = $this->input->post('tipe');
|
||||
|
||||
$data = [
|
||||
'kode_akun' => $this->input->post('kode_akun'),
|
||||
'nama_akun' => $this->input->post('nama_akun'),
|
||||
'tipe' => $tipe,
|
||||
'posisi' => in_array($tipe, ['asset','expense']) ? 'debit' : 'kredit',
|
||||
'kategori' => in_array($tipe, ['asset','liability','equity']) ? 'neraca' : 'laba_rugi',
|
||||
'parent_id' => $this->input->post('parent_id') ?: NULL,
|
||||
'priority' => $this->input->post('priority') ?: NULL
|
||||
];
|
||||
|
||||
$insert = $this->dm->setTable('accounts')->insert($data);
|
||||
|
||||
if($insert){
|
||||
log_activity(
|
||||
'Accounts',
|
||||
'create',
|
||||
'Menambah akun: '.$data['kode_akun'].' - '.$data['nama_akun']
|
||||
);
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'status' => $insert ? true : false,
|
||||
'message' => $insert ? 'Data berhasil disimpan' : 'Gagal simpan'
|
||||
]);
|
||||
try {
|
||||
$id = $this->chartofaccountsservice->save($this->input->post(), null, $this->session->userdata('user_id'));
|
||||
log_activity('Accounts', 'create', 'Menambah akun ID '.$id);
|
||||
return json_response(true, 'Akun berhasil disimpan.', array('id' => $id));
|
||||
} catch (Throwable $exception) { return business_exception_response($exception); }
|
||||
}
|
||||
|
||||
// ================= UPDATE
|
||||
public function update()
|
||||
{
|
||||
$id = $this->input->post('id');
|
||||
$tipe = $this->input->post('tipe');
|
||||
|
||||
$data = [
|
||||
'kode_akun' => $this->input->post('kode_akun'),
|
||||
'nama_akun' => $this->input->post('nama_akun'),
|
||||
'tipe' => $tipe,
|
||||
'posisi' => in_array($tipe, ['asset','expense']) ? 'debit' : 'kredit',
|
||||
'kategori' => in_array($tipe, ['asset','liability','equity']) ? 'neraca' : 'laba_rugi',
|
||||
'parent_id' => $this->input->post('parent_id') ?: NULL,
|
||||
'priority' => $this->input->post('priority') ?: NULL
|
||||
];
|
||||
|
||||
$update = $this->dm->setTable('accounts')->update($id, $data);
|
||||
|
||||
if($update){
|
||||
log_activity(
|
||||
'Accounts',
|
||||
'update',
|
||||
'Update akun ID '.$id.' menjadi: '.$data['kode_akun'].' - '.$data['nama_akun']
|
||||
);
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'status' => $update ? true : false,
|
||||
'message' => $update ? 'Data berhasil diupdate' : 'Gagal update'
|
||||
]);
|
||||
try {
|
||||
$id = $this->chartofaccountsservice->save($this->input->post(), $this->input->post('id'), $this->session->userdata('user_id'));
|
||||
log_activity('Accounts', 'update', 'Memperbarui akun ID '.$id);
|
||||
return json_response(true, 'Akun berhasil diperbarui.', array('id' => $id));
|
||||
} catch (Throwable $exception) { return business_exception_response($exception); }
|
||||
}
|
||||
|
||||
public function detail($id)
|
||||
@@ -141,31 +103,13 @@ class Accounts extends CI_Controller {
|
||||
|
||||
public function delete($id)
|
||||
{
|
||||
// cek apakah jadi parent
|
||||
$child = $this->db->where('parent_id',$id)->count_all_results('accounts');
|
||||
|
||||
if($child > 0){
|
||||
echo json_encode([
|
||||
'status'=>false,
|
||||
'message'=>'Tidak bisa hapus, akun punya child'
|
||||
]);
|
||||
return;
|
||||
if (strtoupper($this->input->method()) !== 'POST') {
|
||||
return json_response(false, 'Metode request tidak diizinkan.', array(), 405);
|
||||
}
|
||||
|
||||
$delete = $this->dm->setTable('accounts')->delete($id);
|
||||
|
||||
if($delete){
|
||||
log_activity(
|
||||
'Accounts',
|
||||
'delete',
|
||||
'Menghapus akun ID '.$id,
|
||||
'success'
|
||||
);
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'status' => $delete ? true : false,
|
||||
'message' => $delete ? 'Data berhasil dihapus' : 'Gagal hapus'
|
||||
]);
|
||||
try {
|
||||
$this->chartofaccountsservice->delete($id, $this->session->userdata('user_id'));
|
||||
log_activity('Accounts', 'delete', 'Menghapus akun ID '.$id, 'success');
|
||||
return json_response(true, 'Akun berhasil dihapus.');
|
||||
} catch (Throwable $exception) { return business_exception_response($exception); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
class Approvals extends MY_Controller
|
||||
{
|
||||
public function __construct() { parent::__construct(); if(!$this->session->userdata('logged_in')) redirect('auth'); $this->load->library('ApprovalService'); }
|
||||
public function index()
|
||||
{
|
||||
$userId=(int)$this->session->userdata('user_id'); $roleId=(int)$this->session->userdata('role_id');
|
||||
if(!$roleId){$u=$this->db->select('role_id')->get_where('users',array('id'=>$userId))->row();$roleId=$u?(int)$u->role_id:0;}
|
||||
$data=array('active_menu'=>'approvals','requests'=>$this->approvalservice->pendingFor($userId,$roleId));
|
||||
$this->load->view('partials/header',$data);$this->load->view('approvals/index',$data);$this->load->view('partials/footer');
|
||||
}
|
||||
}
|
||||
@@ -1,505 +1,16 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
|
||||
class Asset extends CI_Controller {
|
||||
|
||||
public function __construct(){
|
||||
parent::__construct();
|
||||
$this->load->database();
|
||||
|
||||
if (!$this->session->userdata('logged_in')) {
|
||||
redirect('auth');
|
||||
}
|
||||
}
|
||||
|
||||
public function index(){
|
||||
$data = ["active_menu" => "asset"];
|
||||
$this->load->view('partials/header', $data);
|
||||
$this->load->view('asset/layout');
|
||||
$this->load->view('partials/footer');
|
||||
}
|
||||
|
||||
// ================= GET DATA =================
|
||||
public function get_data(){
|
||||
$data = $this->db
|
||||
->select('assets.*, lokasi_asset.nama as nama_lokasi, item_barcodes.barcode as barcode, item_barcodes.id as barcode_id')
|
||||
->from('assets')
|
||||
->join('lokasi_asset', 'lokasi_asset.id = assets.lokasi_asset_id', 'left')
|
||||
->join('item_barcodes', 'item_barcodes.id = assets.barcode_id', 'left')
|
||||
->order_by('assets.tanggal_perolehan','DESC')
|
||||
->get()
|
||||
->result();
|
||||
|
||||
$no = 1;
|
||||
$result = [];
|
||||
|
||||
foreach($data as $d){
|
||||
$result[] = [
|
||||
$no++,
|
||||
$d->tanggal_perolehan,
|
||||
$d->kode_asset ?? '-',
|
||||
$d->nama_asset . ($d->barcode_id ? '<br><small class="text-muted"><i>'.$d->barcode.'</i></small>' : ''),
|
||||
$d->nama_lokasi ?? '-',
|
||||
$d->keterangan ?? '-',
|
||||
number_format($d->nilai_perolehan),
|
||||
$d->masa_manfaat . ' bln',
|
||||
number_format($d->penyusutan_per_bulan),
|
||||
$d->created_at,
|
||||
'
|
||||
<button class="btn btn-sm btn-info btn-detail mb-1 ml-1" data-id="'.$d->id.'"><i class="bi bi-eye"></i></button>
|
||||
<button class="btn btn-sm btn-warning btn-edit mb-1 ml-1" data-id="'.$d->id.'">Edit</button>
|
||||
<button class="btn btn-sm btn-danger btn-delete mb-1 ml-1" data-id="'.$d->id.'">Hapus</button>
|
||||
'
|
||||
];
|
||||
}
|
||||
|
||||
echo json_encode(['data'=>$result]);
|
||||
}
|
||||
|
||||
// ================= DETAIL ASSET + HISTORY =================
|
||||
public function detail($id)
|
||||
/**
|
||||
* Alias kompatibilitas. Seluruh proses Aset Tetap sekarang hanya dikelola
|
||||
* Fixedassets agar tidak ada dua implementasi stok/jurnal yang berbeda.
|
||||
*/
|
||||
class Asset extends MY_Controller
|
||||
{
|
||||
public function index(){ redirect('fixedassets', 'location', 301); }
|
||||
public function __call($method,$arguments)
|
||||
{
|
||||
$data = $this->db
|
||||
->select('a.*, la.nama AS nama_lokasi') // ambil field tambahan dari lokasi_asset
|
||||
->from('assets a')
|
||||
->join('lokasi_asset la', 'a.lokasi_asset_id = la.id', 'left')
|
||||
->where('a.id', $id)
|
||||
->get()
|
||||
->row();
|
||||
|
||||
echo json_encode($data);
|
||||
if(strtoupper($this->input->method())==='GET') redirect('fixedassets', 'location', 302);
|
||||
return json_response(false,'Endpoint Aset lama sudah dinonaktifkan. Gunakan menu Aset Tetap.',array(),410);
|
||||
}
|
||||
|
||||
public function get_history($asset_id){
|
||||
$data = $this->db
|
||||
->where('asset_id', $asset_id)
|
||||
->order_by('created_at', 'DESC')
|
||||
->get('asset_mutations')
|
||||
->result();
|
||||
|
||||
echo json_encode($data);
|
||||
}
|
||||
|
||||
// ================= SAVE MANUAL (1 UNIT) =================
|
||||
public function save(){
|
||||
$harga = preg_replace('/\D/','',$this->input->post('harga'));
|
||||
$residu = preg_replace('/\D/','',$this->input->post('residu'));
|
||||
$masa = (int)$this->input->post('masa');
|
||||
$account_debit_id = (int)$this->input->post('account_asset');
|
||||
$account_kredit_id = (int)$this->input->post('account_kas');
|
||||
$lokasi_asset_id = (int)$this->input->post('lokasi_asset');
|
||||
$tanggal = $this->input->post('tanggal');
|
||||
$nama_asset = $this->input->post('nama_asset');
|
||||
$keterangan = $this->input->post('keterangan');
|
||||
|
||||
$nilai_perolehan = $harga * 1;
|
||||
$penyusutan = ($harga - $residu) / ($masa ?: 1);
|
||||
|
||||
$this->db->trans_start();
|
||||
|
||||
$data = [
|
||||
'kode_asset' => 'AST-'.time(),
|
||||
'nama_asset' => $nama_asset,
|
||||
'harga_per_unit' => $harga,
|
||||
'nilai_perolehan' => $nilai_perolehan,
|
||||
'tanggal_perolehan' => $tanggal,
|
||||
'masa_manfaat' => $masa,
|
||||
'nilai_residu' => $residu,
|
||||
'penyusutan_per_bulan' => $penyusutan,
|
||||
'nilai_buku' => $nilai_perolehan,
|
||||
'sumber' => 'pembelian',
|
||||
'account_debit_id' => $account_debit_id,
|
||||
'account_kredit_id' => $account_kredit_id,
|
||||
'lokasi_asset_id' => $lokasi_asset_id,
|
||||
'keterangan' => $keterangan,
|
||||
'qty' => 1
|
||||
];
|
||||
|
||||
$this->db->insert('assets',$data);
|
||||
$asset_id = $this->db->insert_id();
|
||||
|
||||
// Journal
|
||||
$this->db->insert('journals',[
|
||||
'tanggal' => $tanggal,
|
||||
'no_ref' => 'AST-'.date('YmdHis').'-'.$asset_id,
|
||||
'keterangan' => 'Penambahan Aset '.$nama_asset,
|
||||
'ref_type' => 'assets',
|
||||
'ref_id' => $asset_id,
|
||||
'created_by' => $this->session->userdata('user_id')
|
||||
]);
|
||||
|
||||
$jid = $this->db->insert_id();
|
||||
|
||||
$this->db->insert('journal_details',[
|
||||
'journal_id' => $jid,
|
||||
'account_id' => $account_debit_id,
|
||||
'debit' => $nilai_perolehan,
|
||||
'kredit' => 0
|
||||
]);
|
||||
|
||||
$this->db->insert('journal_details',[
|
||||
'journal_id' => $jid,
|
||||
'account_id' => $account_kredit_id,
|
||||
'debit' => 0,
|
||||
'kredit' => $nilai_perolehan
|
||||
]);
|
||||
|
||||
// MUTATION PEROLEHAN
|
||||
$this->db->insert('asset_mutations',[
|
||||
'asset_id' => $asset_id,
|
||||
'tipe' => 'perolehan',
|
||||
'nilai' => $nilai_perolehan,
|
||||
'keterangan' => 'Perolehan manual: '.$keterangan,
|
||||
'tanggal_dibuat' => $tanggal
|
||||
]);
|
||||
|
||||
$this->db->trans_complete();
|
||||
|
||||
if ($this->db->trans_status()) {
|
||||
log_activity('Asset', 'create', 'Menambah asset manual: '.$nama_asset.' (ID: '.$asset_id.')');
|
||||
echo json_encode(['status'=>true]);
|
||||
} else {
|
||||
echo json_encode(['status'=>false]);
|
||||
}
|
||||
}
|
||||
|
||||
// ================= SAVE FROM STOCK =================
|
||||
public function save_from_stock(){
|
||||
$item_id = $this->input->post('item_id');
|
||||
$account_debit_id = (int)$this->input->post('account_asset');
|
||||
$lokasi_asset_id = (int)$this->input->post('s_lokasi_asset');
|
||||
$masa = (int)$this->input->post('s_masa');
|
||||
$residu = preg_replace('/\D/','',$this->input->post('s_residu'));
|
||||
$tanggal = $this->input->post('s_tanggal');
|
||||
$keterangan = $this->input->post('s_keterangan');
|
||||
$barcode_id = $this->input->post('barcode_id');
|
||||
$qty = (int)$this->input->post('qty');
|
||||
if($qty < 1){
|
||||
$qty = 1;
|
||||
}
|
||||
|
||||
$item = $this->db->get_where('items',['id'=>$item_id])->row();
|
||||
|
||||
if(!$item){
|
||||
echo json_encode(['status'=>false,'message'=>'Item tidak ditemukan']);
|
||||
return;
|
||||
}
|
||||
|
||||
if(!$barcode_id){
|
||||
echo json_encode(['status'=>false,'message'=>'Barcode wajib dipilih']);
|
||||
return;
|
||||
}
|
||||
|
||||
if($item->stok < $qty){
|
||||
echo json_encode(['status'=>false,'message'=>'Stok tidak cukup']);
|
||||
return;
|
||||
}
|
||||
|
||||
$harga = $item->harga_beli;
|
||||
$nama_asset = $item->nama_barang;
|
||||
$nilai_perolehan = $harga * $qty;
|
||||
$penyusutan = ($nilai_perolehan - $residu) / ($masa ?: 1);
|
||||
|
||||
$this->db->trans_start();
|
||||
|
||||
// stock log keluar per item
|
||||
$this->db->insert('stock_logs',[
|
||||
'item_id' => $item_id,
|
||||
'warehouse_id' => '1', // default gudang
|
||||
'qty' => $qty,
|
||||
'tipe' => 'keluar',
|
||||
'keterangan' => 'Pemindahan barang ke asset tanggal ' . $tanggal . ' - (' . $keterangan . ')'
|
||||
]);
|
||||
$stock_log_id = $this->db->insert_id();
|
||||
$this->update_item_stok($item_id);
|
||||
|
||||
// INSERT ASSET
|
||||
$this->db->insert('assets',[
|
||||
'item_id' => $item_id,
|
||||
'barcode_id' => $barcode_id,
|
||||
'stock_log_id' => $stock_log_id,
|
||||
'kode_asset' => 'AST-'.time(),
|
||||
'nama_asset' => $nama_asset,
|
||||
'harga_per_unit' => $harga,
|
||||
'nilai_perolehan' => $nilai_perolehan,
|
||||
'tanggal_perolehan' => $tanggal,
|
||||
'masa_manfaat' => $masa,
|
||||
'nilai_residu' => $residu,
|
||||
'penyusutan_per_bulan' => $penyusutan,
|
||||
'nilai_buku' => $nilai_perolehan,
|
||||
'sumber' => 'gudang',
|
||||
'account_debit_id' => $account_debit_id,
|
||||
'account_kredit_id' => 21, // Persediaan
|
||||
'lokasi_asset_id' => $lokasi_asset_id,
|
||||
'keterangan' => $keterangan,
|
||||
'qty' => $qty
|
||||
]);
|
||||
|
||||
$asset_id = $this->db->insert_id();
|
||||
|
||||
// START - Item Movement & item barcodes update status
|
||||
$this->db->where('item_id', $item_id);
|
||||
$this->db->order_by('created_at', 'DESC');
|
||||
$this->db->limit(1);
|
||||
$item_move = $this->db->get('item_movements')->row();
|
||||
|
||||
// get item_barcode
|
||||
$itemBar = $this->db->get_where('item_barcodes', ['id'=>$barcode_id])->row();
|
||||
|
||||
if($itemBar->qty_sisa < $qty) {
|
||||
echo json_encode(['status'=>false, 'message'=>'Stok tidak cukup. Sisa: '.$itemBar->qty_sisa]);
|
||||
$this->db->trans_rollback();
|
||||
return;
|
||||
}
|
||||
|
||||
// END - Item Movement & item barcodes update status
|
||||
|
||||
$total_sisa = $itemBar->qty_sisa - $qty;
|
||||
$status_bar = ($total_sisa == 0) ? 'installed' : 'available';
|
||||
|
||||
$this->db->insert('item_movements',[
|
||||
'item_id' => $item_id,
|
||||
'barcode_id' => $barcode_id,
|
||||
'qty' => $qty,
|
||||
'from_type' => $item_move ? $item_move->to_type : 'warehouse', //enum('supplier','warehouse','technician','customer','invoice','asset')
|
||||
'to_type' => 'asset', //enum('supplier','warehouse','technician','customer','invoice','asset')
|
||||
'movement_type' => 'installation', //enum('purchase','transfer','installation','uninstallation','supplier_return','customer_return','adjustment','sold_out','asset')
|
||||
'notes' => $keterangan,
|
||||
'created_at' => date('Y-m-d H:i:s')
|
||||
]);
|
||||
// Status yang boleh masuk : enum('available','reserved','installed','damaged','lost','returned_supplier','sold_out','asset')
|
||||
$this->db->where('id', $barcode_id)->update('item_barcodes', ['status' => $status_bar, 'qty_sisa' => $total_sisa]);
|
||||
|
||||
// Journal
|
||||
$this->db->insert('journals',[
|
||||
'tanggal' => $tanggal,
|
||||
'no_ref' => 'AST-'.date('YmdHis').'-'.$asset_id,
|
||||
'keterangan' => 'Penambahan Aset dari Gudang '.$nama_asset,
|
||||
'ref_type' => 'assets',
|
||||
'ref_id' => $asset_id,
|
||||
'created_by' => $this->session->userdata('user_id')
|
||||
]);
|
||||
|
||||
$jid = $this->db->insert_id();
|
||||
|
||||
// Debit Asset
|
||||
$this->db->insert('journal_details',[
|
||||
'journal_id' => $jid,
|
||||
'account_id' => $account_debit_id,
|
||||
'debit' => $nilai_perolehan,
|
||||
'kredit' => 0
|
||||
]);
|
||||
|
||||
// Kredit Persediaan
|
||||
$this->db->insert('journal_details',[
|
||||
'journal_id' => $jid,
|
||||
'account_id' => 21,
|
||||
'debit' => 0,
|
||||
'kredit' => $nilai_perolehan
|
||||
]);
|
||||
|
||||
// MUTATION DARI GUDANG
|
||||
$this->db->insert('asset_mutations',[
|
||||
'asset_id' => $asset_id,
|
||||
'tipe' => 'perolehan',
|
||||
'nilai' => $nilai_perolehan,
|
||||
'keterangan' => 'Perolehan dari gudang: '.$nama_asset,
|
||||
'tanggal_dibuat' => $tanggal
|
||||
]);
|
||||
|
||||
$this->db->trans_complete();
|
||||
|
||||
if ($this->db->trans_status()) {
|
||||
log_activity('Asset', 'create', 'Menambah asset dari gudang: '.$nama_asset.' (ID: '.$asset_id.', Qty: '.$qty.')');
|
||||
echo json_encode(['status'=>true]);
|
||||
} else {
|
||||
echo json_encode(['status'=>false]);
|
||||
}
|
||||
}
|
||||
|
||||
// ================= UPDATE =================
|
||||
public function update(){
|
||||
$id = $this->input->post('id');
|
||||
$nama_asset = $this->input->post('nama_asset');
|
||||
$lokasi_asset_id = (int)$this->input->post('lokasi_asset');
|
||||
$keterangan = $this->input->post('keterangan');
|
||||
|
||||
$asset = $this->db->get_where('assets',['id'=>$id])->row();
|
||||
|
||||
if(!$asset){
|
||||
echo json_encode(['status'=>false,'message'=>'Asset tidak ditemukan']);
|
||||
return;
|
||||
}
|
||||
|
||||
$data = [
|
||||
'nama_asset' => $nama_asset,
|
||||
'lokasi_asset_id' => $lokasi_asset_id,
|
||||
'keterangan' => $keterangan
|
||||
];
|
||||
|
||||
if($asset->barcode_id && $asset->item_id){
|
||||
$data = [
|
||||
'lokasi_asset_id' => $lokasi_asset_id,
|
||||
'keterangan' => $keterangan
|
||||
];
|
||||
}
|
||||
|
||||
$this->db->trans_start();
|
||||
|
||||
$this->db->where('id',$id);
|
||||
$this->db->update('assets',$data);
|
||||
|
||||
$this->db->trans_complete();
|
||||
|
||||
if ($this->db->trans_status()) {
|
||||
log_activity('Asset', 'update', 'Update asset ID: '.$id.' Nama: '.$nama_asset);
|
||||
echo json_encode(['status'=>true]);
|
||||
} else {
|
||||
echo json_encode(['status'=>false]);
|
||||
}
|
||||
}
|
||||
|
||||
// ================= DELETE =================
|
||||
public function delete(){
|
||||
$id = $this->input->post('id');
|
||||
$alasan = $this->input->post('alasan');
|
||||
$keterangan = $this->input->post('keterangan');
|
||||
|
||||
$asset = $this->db->get_where('assets',['id'=>$id])->row();
|
||||
$itemBar = $this->db->get_where('item_barcodes',['id'=>$asset->barcode_id])->row();
|
||||
|
||||
if($alasan == 'dijual' || $alasan == 'rusak' ){
|
||||
echo json_encode(['status'=>false,'message'=>'Alasan <b>' .strtoupper($alasan).'</b> tidak bisa digunakan, Karena fitur belum tersedia. Silahkan pilih alasan lain.']);
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
$this->db->trans_start();
|
||||
|
||||
if ($asset->barcode_id && $asset->item_id) {
|
||||
// Update item_barcodes status to 'available' if it was linked to this asset
|
||||
$qty_sisa = $asset->qty + $itemBar->qty_sisa;
|
||||
|
||||
$this->db->where('id', $asset->barcode_id)
|
||||
->update('item_barcodes', ['status' => 'available', 'qty_sisa' => $qty_sisa]);
|
||||
$this->db->delete('stock_logs', ['id'=>$asset->stock_log_id]);
|
||||
$this->update_item_stok($asset->item_id);
|
||||
|
||||
}
|
||||
|
||||
// Hapus journal
|
||||
$journals = $this->db->get_where('journals', ['ref_type'=>'assets', 'ref_id'=>$id])->result();
|
||||
foreach($journals as $j){
|
||||
$this->db->delete('journal_details', ['journal_id'=>$j->id]);
|
||||
$this->db->delete('journals', ['id'=>$j->id]);
|
||||
}
|
||||
|
||||
// Hapus asset
|
||||
$this->db->delete('assets',['id'=>$id]);
|
||||
|
||||
// MUTATION DELETE
|
||||
$this->db->insert('asset_mutations',[
|
||||
'asset_id' => $id,
|
||||
'tipe' => 'pengurangan',
|
||||
'nilai' => $asset->nilai_buku,
|
||||
'keterangan' => 'Penghapusan ('.$alasan.'): '.$keterangan
|
||||
]);
|
||||
|
||||
$this->db->trans_complete();
|
||||
|
||||
if ($this->db->trans_status()) {
|
||||
log_activity('Asset', 'delete', 'Hapus asset ID: '.$id.' Alasan: '.$alasan);
|
||||
echo json_encode(['status'=>true]);
|
||||
} else {
|
||||
echo json_encode(['status'=>false]);
|
||||
}
|
||||
}
|
||||
|
||||
// ================= GET DATA SELECT =================
|
||||
public function get_accounts_kas(){
|
||||
$data = $this->db
|
||||
->where_in('sub_tipe', ['kas', 'hutang'])
|
||||
->order_by('kode_akun', 'ASC')
|
||||
->get('accounts')
|
||||
->result();
|
||||
echo json_encode($data);
|
||||
}
|
||||
|
||||
public function get_accounts_asset(){
|
||||
$data = $this->db
|
||||
->group_start()
|
||||
->like('nama_akun', 'Aset', 'after')
|
||||
->group_end()
|
||||
->order_by('kode_akun', 'ASC')
|
||||
->get('accounts')
|
||||
->result();
|
||||
echo json_encode($data);
|
||||
}
|
||||
|
||||
public function get_items()
|
||||
{
|
||||
// $data = $this->db
|
||||
// ->select('items.*, kode_barang.*')
|
||||
// ->from('items')
|
||||
// ->join('kode_barang', 'items.kode_id = kode_barang.id', 'left')
|
||||
// ->where('items.stok >', 0)
|
||||
// ->get()
|
||||
// ->result();
|
||||
|
||||
$this->db->select('
|
||||
items.id,
|
||||
items.nama_barang,
|
||||
items.harga_jual,
|
||||
items.harga_beli,
|
||||
items.kode_detail,
|
||||
kode_barang.tracking_type,
|
||||
COALESCE(SUM(
|
||||
stock_logs.qty * IF(stock_logs.tipe="masuk",1,-1)
|
||||
),0) as stok
|
||||
');
|
||||
|
||||
$this->db->where('status', 'active');
|
||||
$this->db->from('items');
|
||||
|
||||
$this->db->join('stock_logs',
|
||||
'stock_logs.item_id = items.id' ,
|
||||
'left');
|
||||
|
||||
$this->db->join('kode_barang',
|
||||
'items.kode_id = kode_barang.id' ,
|
||||
'left');
|
||||
|
||||
// $this->db->where('items.account_id', $account_id);
|
||||
|
||||
$this->db->group_by('items.id');
|
||||
|
||||
// ✅ INI KUNCINYA
|
||||
$this->db->having('stok >', 0);
|
||||
|
||||
|
||||
$data = $this->db->get()->result();
|
||||
|
||||
echo json_encode($data);
|
||||
}
|
||||
|
||||
public function get_lokasi_asset(){
|
||||
$data = $this->db
|
||||
->get('lokasi_asset')
|
||||
->result();
|
||||
echo json_encode($data);
|
||||
}
|
||||
|
||||
private function update_item_stok($item_id)
|
||||
{
|
||||
$stok = $this->db->query("
|
||||
SELECT COALESCE(SUM(qty * IF(tipe='masuk',1,-1)),0) as stok
|
||||
FROM stock_logs
|
||||
WHERE item_id = ?
|
||||
", [$item_id])->row()->stok;
|
||||
|
||||
$this->db->where('id', $item_id)
|
||||
->update('items', [
|
||||
'stok' => $stok
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,12 +15,29 @@ class AttendanceWebhook extends CI_Controller {
|
||||
// =========================
|
||||
// AMBIL SN MESIN
|
||||
// =========================
|
||||
$sn = $this->input->get('SN');
|
||||
$sn = trim((string) $this->input->get('SN', true));
|
||||
if ($sn === '' || !preg_match('/^[A-Za-z0-9._-]{1,100}$/', $sn)) {
|
||||
show_error('Serial number perangkat tidak valid.', 400);
|
||||
}
|
||||
|
||||
$allowedSerials = array_filter(array_map('trim', explode(',', (string) getenv('ATTENDANCE_ALLOWED_SERIALS'))));
|
||||
if (!empty($allowedSerials) && !in_array($sn, $allowedSerials, true)) {
|
||||
log_message('error', 'Attendance webhook ditolak untuk SN: ' . $sn);
|
||||
show_error('Perangkat tidak terdaftar.', 403);
|
||||
}
|
||||
|
||||
if (ENVIRONMENT === 'production' && empty($allowedSerials)) {
|
||||
log_message('error', 'ATTENDANCE_ALLOWED_SERIALS belum dikonfigurasi di production.');
|
||||
show_error('Konfigurasi perangkat belum tersedia.', 503);
|
||||
}
|
||||
|
||||
// =========================
|
||||
// RAW BODY DARI MESIN
|
||||
// =========================
|
||||
$body = file_get_contents('php://input');
|
||||
$body = file_get_contents('php://input', false, null, 0, 1048576);
|
||||
if ($body === false) {
|
||||
show_error('Payload tidak dapat dibaca.', 400);
|
||||
}
|
||||
|
||||
// =========================
|
||||
// LOG DIRECTORY
|
||||
@@ -28,7 +45,7 @@ class AttendanceWebhook extends CI_Controller {
|
||||
$logDir = APPPATH . 'logs/fingerprint/';
|
||||
|
||||
if (!is_dir($logDir)) {
|
||||
mkdir($logDir, 0777, true);
|
||||
mkdir($logDir, 0750, true);
|
||||
}
|
||||
|
||||
// =========================
|
||||
@@ -58,7 +75,7 @@ class AttendanceWebhook extends CI_Controller {
|
||||
continue;
|
||||
}
|
||||
|
||||
$logText .= $line . "\n";
|
||||
$logText .= str_replace(array("\r", "\0"), '', $line) . "\n";
|
||||
|
||||
// contoh parsing
|
||||
$data = explode("\t", $line);
|
||||
@@ -75,11 +92,11 @@ class AttendanceWebhook extends CI_Controller {
|
||||
// =========================
|
||||
// SIMPAN LOG
|
||||
// =========================
|
||||
file_put_contents($logFile, $logText, FILE_APPEND);
|
||||
file_put_contents($logFile, $logText, FILE_APPEND | LOCK_EX);
|
||||
|
||||
// =========================
|
||||
// RESPONSE KE MESIN
|
||||
// =========================
|
||||
echo "OK";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
|
||||
class Attendancemonitoring extends CI_Controller {
|
||||
class Attendancemonitoring extends MY_Controller {
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
@@ -62,4 +62,4 @@ class Attendancemonitoring extends CI_Controller {
|
||||
'data' => $rows
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
|
||||
class Attendancemonthly extends CI_Controller {
|
||||
class Attendancemonthly extends MY_Controller {
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
@@ -100,4 +100,4 @@ class Attendancemonthly extends CI_Controller {
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
|
||||
class Attendancemonthly extends CI_Controller {
|
||||
class Attendancemonthly extends MY_Controller {
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
@@ -62,4 +62,4 @@ class Attendancemonthly extends CI_Controller {
|
||||
'data' => $rows
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,26 +17,50 @@ class Auth extends CI_Controller {
|
||||
$this->load->view('auth/login');
|
||||
}
|
||||
|
||||
public function process_login()
|
||||
{
|
||||
$username = $this->input->post('username');
|
||||
$password = $this->input->post('password');
|
||||
public function process_login()
|
||||
{
|
||||
if (strtoupper($this->input->method()) !== 'POST') {
|
||||
show_error('Metode request tidak diizinkan.', 405);
|
||||
}
|
||||
|
||||
$lockedUntil = (int) $this->session->userdata('login_locked_until');
|
||||
if ($lockedUntil > time()) {
|
||||
$remaining = max(1, (int) ceil(($lockedUntil - time()) / 60));
|
||||
$this->session->set_flashdata('error', 'Terlalu banyak percobaan login. Coba kembali dalam ' . $remaining . ' menit.');
|
||||
redirect('auth');
|
||||
return;
|
||||
}
|
||||
|
||||
$username = trim((string) $this->input->post('username', true));
|
||||
$password = (string) $this->input->post('password');
|
||||
|
||||
if ($username === '' || $password === '') {
|
||||
$this->session->set_flashdata('error', 'Username dan password wajib diisi.');
|
||||
redirect('auth');
|
||||
return;
|
||||
}
|
||||
|
||||
$user = $this->db->get_where('v_users', ['username' => $username])->row();
|
||||
|
||||
if ($user && password_verify($password, $user->password)) {
|
||||
if ($user && (int)$user->role_is_active === 1 && password_verify($password, $user->password)) {
|
||||
|
||||
$this->session->sess_regenerate(true);
|
||||
$this->session->unset_userdata(array('login_attempts', 'login_locked_until'));
|
||||
|
||||
// Decode permissions
|
||||
$permissions = json_decode($user->permissions, true);
|
||||
if (!is_array($permissions)) {
|
||||
$permissions = [];
|
||||
}
|
||||
$permissions = json_decode($user->permissions, true);
|
||||
if (!is_array($permissions)) {
|
||||
$permissions = [];
|
||||
}
|
||||
$role = $this->db->select('is_super_admin')->get_where('roles', array('id' => (int)$user->role_id))->row();
|
||||
|
||||
$session_data = [
|
||||
'user_id' => $user->id,
|
||||
'nama' => $user->nama,
|
||||
'username' => $user->username,
|
||||
'role' => $user->nama_role,
|
||||
'role' => $user->nama_role,
|
||||
'role_id' => (int) $user->role_id,
|
||||
'is_super_admin'=> $role && (int)$role->is_super_admin === 1,
|
||||
'permissions' => $permissions,
|
||||
'logged_in' => TRUE
|
||||
];
|
||||
@@ -53,7 +77,17 @@ class Auth extends CI_Controller {
|
||||
|
||||
redirect('dashboard');
|
||||
|
||||
} else {
|
||||
} else {
|
||||
|
||||
$attempts = (int) $this->session->userdata('login_attempts') + 1;
|
||||
$this->session->set_userdata('login_attempts', $attempts);
|
||||
|
||||
if ($attempts >= 5) {
|
||||
$this->session->set_userdata(array(
|
||||
'login_attempts' => 0,
|
||||
'login_locked_until' => time() + 300
|
||||
));
|
||||
}
|
||||
|
||||
// 🔥 LOG LOGIN GAGAL
|
||||
log_activity(
|
||||
@@ -63,7 +97,7 @@ class Auth extends CI_Controller {
|
||||
'failed'
|
||||
);
|
||||
|
||||
$this->session->set_flashdata('error', 'Username atau Password salah!');
|
||||
$this->session->set_flashdata('error', 'Username atau password salah.');
|
||||
redirect('auth');
|
||||
}
|
||||
}
|
||||
@@ -84,4 +118,4 @@ class Auth extends CI_Controller {
|
||||
redirect('auth');
|
||||
}
|
||||
}
|
||||
?>
|
||||
?>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
|
||||
class Basejurnal extends CI_Controller {
|
||||
class Basejurnal extends MY_Admin_Controller {
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
@@ -287,4 +287,4 @@ public function update()
|
||||
|
||||
echo json_encode($data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
|
||||
class Budgetcheck extends CI_Controller
|
||||
{
|
||||
private function boot()
|
||||
{
|
||||
if (!$this->input->is_cli_request()) show_404();
|
||||
$this->load->database();
|
||||
$access = $this->db->order_by('is_default', 'DESC')->get('user_companies')->row();
|
||||
if (!$access) throw new RuntimeException('Tidak ada user-company untuk smoke test.');
|
||||
$this->session->set_userdata(array(
|
||||
'user_id' => $access->user_id,
|
||||
'company_id' => $access->company_id,
|
||||
'role' => 'Admin',
|
||||
'logged_in' => true,
|
||||
));
|
||||
$this->load->library(array('CompanyContext', 'BudgetService'));
|
||||
return $access;
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$this->boot();
|
||||
$required = array(
|
||||
'budgets' => array('company_id', 'fiscal_year', 'status', 'control_mode'),
|
||||
'budget_lines' => array('company_id', 'budget_id', 'period', 'account_id', 'amount'),
|
||||
'purchase_order_lines' => array('budget_account_id'),
|
||||
);
|
||||
$missing = array();
|
||||
foreach ($required as $table => $columns) {
|
||||
foreach ($columns as $column) {
|
||||
if (!$this->db->field_exists($column, $table)) $missing[] = $table.'.'.$column;
|
||||
}
|
||||
}
|
||||
$duplicateActive = (int) $this->db->query(
|
||||
"SELECT COUNT(*) total FROM (SELECT company_id, fiscal_year FROM budgets WHERE status='active' GROUP BY company_id, fiscal_year HAVING COUNT(*) > 1) x"
|
||||
)->row()->total;
|
||||
$duplicateLines = (int) $this->db->query(
|
||||
"SELECT COUNT(*) total FROM (SELECT budget_id, period, account_id FROM budget_lines GROUP BY budget_id, period, account_id HAVING COUNT(*) > 1) x"
|
||||
)->row()->total;
|
||||
$rows = $this->budgetservice->report(array(
|
||||
'from' => date('Y-01-01'),
|
||||
'to' => date('Y-12-31'),
|
||||
'account_id' => 0,
|
||||
));
|
||||
$ok = !$missing && $duplicateActive === 0 && $duplicateLines === 0;
|
||||
echo json_encode(array(
|
||||
'migration' => (int) $this->db->get('migrations')->row()->version,
|
||||
'missing_schema' => $missing,
|
||||
'money_20_000_50' => $this->budgetservice->money('20.000,50'),
|
||||
'active_budget_duplicates' => $duplicateActive,
|
||||
'budget_line_duplicates' => $duplicateLines,
|
||||
'report_rows' => count($rows),
|
||||
'status' => $ok ? 'OK' : 'REVIEW',
|
||||
), JSON_PRETTY_PRINT).PHP_EOL;
|
||||
}
|
||||
|
||||
public function workflow()
|
||||
{
|
||||
$access = $this->boot();
|
||||
$user = (int) $access->user_id;
|
||||
$budgetIds = array();
|
||||
$doubleSubmitBlocked = false;
|
||||
try {
|
||||
$account = $this->db
|
||||
->where(array(
|
||||
'company_id' => $access->company_id,
|
||||
'tipe' => 'expense',
|
||||
'is_active' => 1,
|
||||
'allow_posting' => 1,
|
||||
))
|
||||
->order_by('id')->get('accounts')->row();
|
||||
if (!$account) throw new RuntimeException('Akun pengeluaran posting tidak tersedia.');
|
||||
|
||||
$id = $this->budgetservice->create(array(
|
||||
'name' => 'QA Budget Temporary',
|
||||
'fiscal_year' => 2199,
|
||||
'control_mode' => 'warning',
|
||||
), $user);
|
||||
$budgetIds[] = $id;
|
||||
$this->budgetservice->saveAnnualLines($id, array(array(
|
||||
'account_id' => $account->id,
|
||||
'month' => array(1 => '20.000,50', 2 => '10.000,25'),
|
||||
)), $user);
|
||||
$this->budgetservice->workflow($id, 'submit', $user);
|
||||
try {
|
||||
$this->budgetservice->workflow($id, 'submit', $user);
|
||||
} catch (Throwable $expected) {
|
||||
$doubleSubmitBlocked = true;
|
||||
}
|
||||
$status = $this->budgetservice->workflow($id, 'approve', $user);
|
||||
$revision = $this->budgetservice->revise($id, 'Smoke test revisi', $user);
|
||||
$budgetIds[] = $revision;
|
||||
$revisionDraft = $this->budgetservice->find($revision)->status === 'draft';
|
||||
$ok = $status === 'active' && $doubleSubmitBlocked && $revisionDraft;
|
||||
echo json_encode(array(
|
||||
'draft_submit_approve' => $status === 'active',
|
||||
'double_submit_blocked' => $doubleSubmitBlocked,
|
||||
'revision_draft' => $revisionDraft,
|
||||
'account_only_budget' => true,
|
||||
'status' => $ok ? 'OK' : 'REVIEW',
|
||||
), JSON_PRETTY_PRINT).PHP_EOL;
|
||||
} finally {
|
||||
if ($budgetIds) {
|
||||
$this->db->where_in('budget_id', $budgetIds)->delete('budget_lines');
|
||||
$this->db->group_start()->where_in('budget_id', $budgetIds)->or_where_in('entity_id', $budgetIds)->group_end()
|
||||
->where('entity_type', 'budget')->delete('budget_audit_logs');
|
||||
$this->db->where_in('budget_id', $budgetIds)->delete('budget_revisions');
|
||||
$this->db->where_in('id', $budgetIds)->delete('budgets');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
class Budgets extends MY_Controller
|
||||
{
|
||||
private$userId;
|
||||
public function __construct(){parent::__construct();$this->load->database();$this->load->library(array('CompanyContext','BudgetService'));$this->userId=(int)$this->session->userdata('user_id');}
|
||||
public function index()
|
||||
{
|
||||
$company=$this->companycontext->id();$tab=$this->input->get('tab')==='accounts'?'accounts':'list';
|
||||
$filters=array('from'=>$this->input->get('from')?:date('Y-m'),'to'=>$this->input->get('to')?:date('Y-m'),'account_id'=>(int)$this->input->get('account_id'));
|
||||
$listAccount=(int)$this->input->get('list_account_id');
|
||||
$accounts=$this->db->where(array('company_id'=>$company,'is_active'=>1))->order_by('kode_akun')->get('accounts')->result();
|
||||
$budgetAccounts=$this->db->select('a.id,a.kode_akun,a.nama_akun,a.tipe')->distinct()->from('accounts a')->join('account_budget_entries e','e.account_id=a.id')->where(array('e.company_id'=>$company,'e.is_active'=>1))->order_by('a.kode_akun')->get()->result();
|
||||
$data=array('active_menu'=>'budgets','active_tab'=>$tab,'accounts'=>$accounts,'budget_accounts'=>$budgetAccounts,'budget_account_count'=>count($budgetAccounts),'entries'=>$this->budgetservice->entries($listAccount),'reminders'=>$this->budgetservice->currentReminders(),'list_account_id'=>$listAccount,'filters'=>$filters,'report_rows'=>$tab==='accounts'?$this->budgetservice->report($filters):array(),'can_create'=>$this->allowed('can_create'),'can_update'=>$this->allowed('can_update'),'can_delete'=>$this->allowed('can_delete'),'can_export'=>$this->allowed('can_export'));
|
||||
$this->load->view('partials/header',$data);$this->load->view('budgets/index',$data);$this->load->view('partials/footer');
|
||||
}
|
||||
public function save_entry()
|
||||
{
|
||||
$id=(int)$this->input->post('id');return$this->handle(fn()=>$this->budgetservice->saveEntry($this->input->post(),$this->userId),$id?'Budget berhasil diperbarui.':'Budget berhasil ditambahkan.',$id?'can_update':'can_create');
|
||||
}
|
||||
public function delete_entry($id){return$this->handle(fn()=>$this->budgetservice->deleteEntry($id,$this->userId),'Budget berhasil dinonaktifkan.','can_delete');}
|
||||
public function complete_reminder($id){return$this->handle(fn()=>$this->budgetservice->completeReminder($id,$this->userId),'Pengingat pembayaran bulan ini ditandai selesai.','can_update');}
|
||||
public function export_list($format='excel')
|
||||
{
|
||||
$this->requirePermission('budgets','can_export');$accountId=(int)$this->input->get('list_account_id');$rows=array();
|
||||
$states=array('completed'=>'Selesai bulan ini','overdue'=>'Terlambat','due'=>'Jatuh tempo hari ini','upcoming'=>'Segera jatuh tempo','scheduled'=>'Terjadwal','future'=>'Belum berlaku','inactive_period'=>'Bukan bulan berjalan','none'=>'Tidak ada');
|
||||
foreach($this->budgetservice->entries($accountId)as$entry)$rows[]=array('title'=>$entry->title,'account'=>$entry->kode_akun.' - '.$entry->nama_akun,'type'=>$entry->entry_type==='recurring'?'Berulang setiap bulan':'Khusus satu bulan','period'=>$entry->period,'amount'=>(float)$entry->amount,'reminder'=>(int)$entry->reminder_enabled?(($entry->entry_type==='recurring'?'Setiap tanggal ':'Tanggal ').$entry->reminder_day):'Tidak ada','reminder_status'=>$states[$entry->reminder_state]??'-','notes'=>$entry->notes?:'-');
|
||||
return$this->deliverExport($format,'Daftar Budget',$accountId?'Filter satu akun':'Semua akun',array('title'=>'Nama Budget','account'=>'Akun','type'=>'Pola','period'=>'Mulai / Bulan','amount'=>'Nominal Budget','reminder'=>'Pengingat Pembayaran','reminder_status'=>'Status Pengingat','notes'=>'Keterangan'),$rows,array('amount'),'Daftar-Budget');
|
||||
}
|
||||
public function export_accounts($format='excel')
|
||||
{
|
||||
$this->requirePermission('budgets','can_export');$filters=$this->filters();$rows=array();$types=array('asset'=>'Aset','liability'=>'Liabilitas','equity'=>'Ekuitas','revenue'=>'Pendapatan','expense'=>'Beban');
|
||||
foreach($this->budgetservice->report($filters)as$row)$rows[]=array('account'=>$row['kode_akun'].' - '.$row['nama_akun'],'type'=>$types[$row['tipe']]??ucfirst($row['tipe']),'budget'=>$row['budget'],'actual'=>$row['actual'],'commitment'=>$row['commitment'],'usage'=>$row['usage'],'remaining'=>$row['remaining'],'percentage'=>$row['percentage'],'condition'=>$row['indicator']==='over'?'Melebihi':($row['indicator']==='warning'?'Mendekati Batas':'Aman'));
|
||||
return$this->deliverExport($format,'Budget Per Akun','Periode '.$filters['from'].' s.d. '.$filters['to'],array('account'=>'Akun','type'=>'Jenis Akun','budget'=>'Akumulasi Budget','actual'=>'Realisasi','commitment'=>'Komitmen PO','usage'=>'Total Pemakaian','remaining'=>'Sisa Budget','percentage'=>'Persentase (%)','condition'=>'Kondisi'),$rows,array('budget','actual','commitment','usage','remaining'),'Budget-Per-Akun-'.$filters['from'].'-'.$filters['to']);
|
||||
}
|
||||
public function create(){return$this->handle(fn()=>$this->budgetservice->create($this->input->post(),$this->userId),'Budget Draft berhasil dibuat.','can_create',true);}
|
||||
public function save_lines($id){return$this->handle(fn()=>$this->budgetservice->saveAnnualLines($id,(array)$this->input->post('rows'),$this->userId),'Budget bulanan berhasil disimpan.','can_update');}
|
||||
public function workflow($id,$action){return$this->handle(fn()=>$this->budgetservice->workflow($id,$action,$this->userId,$this->input->post('reason',true)),'Status budget berhasil diperbarui.',$action==='submit'?'can_create':'can_approve');}
|
||||
public function revise($id){return$this->handle(fn()=>$this->budgetservice->revise($id,$this->input->post('reason',true),$this->userId),'Draft revisi berhasil dibuat.','can_update',true);}
|
||||
public function detail($id){$budget=$this->budgetservice->find($id);if(!$budget)return json_response(false,'Budget tidak ditemukan.',array(),404);return json_response(true,'Detail budget ditemukan.',array('result'=>array('budget'=>$budget,'lines'=>$this->budgetservice->lines($id))));}
|
||||
public function export(){$this->requirePermission('budgets','can_export');$f=$this->filters();$rows=$this->budgetservice->report($f);$this->output->set_content_type('text/csv')->set_header('Content-Disposition: attachment; filename="budget-'.$f['to'].'.csv"');$h=fopen('php://output','w');fputcsv($h,array('Kode Akun','Nama Akun','Budget','Realisasi','Komitmen','Pemakaian','Sisa','Persentase'));foreach($rows as$r)fputcsv($h,array($r['kode_akun'],$r['nama_akun'],$r['budget'],$r['actual'],$r['commitment'],$r['usage'],$r['remaining'],$r['percentage']));fclose($h);}
|
||||
public function pdf($id){$this->requirePermission('budgets','can_export');$budget=$this->budgetservice->find($id);if(!$budget)show_404();$html=$this->load->view('budgets/pdf',array('budget'=>$budget,'lines'=>$this->budgetservice->lines($id),'company'=>$this->db->get_where('companies',array('id'=>$this->companycontext->id()))->row()),true);require_once FCPATH.'vendor/autoload.php';$pdf=new Dompdf\Dompdf();$pdf->loadHtml($html);$pdf->setPaper('A4','landscape');$pdf->render();$pdf->stream($budget->budget_no.'.pdf',array('Attachment'=>false));}
|
||||
private function filters(){return array('from'=>$this->input->get_post('from')?:date('Y-m'),'to'=>$this->input->get_post('to')?:date('Y-m'),'account_id'=>(int)$this->input->get_post('account_id'));}
|
||||
private function deliverExport($format,$title,$subtitle,array$columns,array$rows,array$moneyColumns,$filename)
|
||||
{
|
||||
$format=strtolower((string)$format);if(!in_array($format,array('pdf','excel'),true))show_404();$company=$this->db->get_where('companies',array('id'=>$this->companycontext->id()))->row();
|
||||
if($format==='pdf'){$html=$this->load->view('budgets/export_pdf',array('title'=>$title,'subtitle'=>$subtitle,'columns'=>$columns,'rows'=>$rows,'money_columns'=>$moneyColumns,'company'=>$company),true);require_once FCPATH.'vendor/autoload.php';$pdf=new Dompdf\Dompdf();$pdf->loadHtml($html);$pdf->setPaper('A4','landscape');$pdf->render();$pdf->stream($filename.'.pdf',array('Attachment'=>false));return;}
|
||||
require_once FCPATH.'vendor/autoload.php';$book=new\PhpOffice\PhpSpreadsheet\Spreadsheet();$sheet=$book->getActiveSheet();$sheet->setTitle(substr(preg_replace('/[^A-Za-z0-9 ]/','',$title),0,31));$last=\PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex(count($columns));$companyName=$company?($company->name??$company->company_name??'Perusahaan'):'Perusahaan';$sheet->setCellValue('A1',$companyName)->mergeCells('A1:'.$last.'1');$sheet->setCellValue('A2',$title)->mergeCells('A2:'.$last.'2');$sheet->setCellValue('A3',$subtitle)->mergeCells('A3:'.$last.'3');$column=1;foreach($columns as$label)$sheet->setCellValueByColumnAndRow($column++,5,$label);$rowNumber=6;foreach($rows as$row){$column=1;foreach($columns as$key=>$label){$sheet->setCellValueByColumnAndRow($column,$rowNumber,$row[$key]??null);if(in_array($key,$moneyColumns,true))$sheet->getStyleByColumnAndRow($column,$rowNumber)->getNumberFormat()->setFormatCode('#,##0.00');$column++;}$rowNumber++;}$sheet->getStyle('A1:'.$last.'2')->getFont()->setBold(true);$sheet->getStyle('A5:'.$last.'5')->getFont()->setBold(true);$sheet->freezePane('A6');if($rows)$sheet->setAutoFilter('A5:'.$last.($rowNumber-1));for($i=1;$i<=count($columns);$i++)$sheet->getColumnDimension(\PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($i))->setAutoSize(true);while(ob_get_level()>0)ob_end_clean();header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');header('Content-Disposition: attachment; filename="'.$filename.'-'.date('Ymd-His').'.xlsx"');header('Cache-Control: max-age=0');\PhpOffice\PhpSpreadsheet\IOFactory::createWriter($book,'Xlsx')->save('php://output');$book->disconnectWorksheets();exit;
|
||||
}
|
||||
private function handle(callable$fn,$message,$permission,$open=false){if(strtoupper($this->input->method())!=='POST')return json_response(false,'Metode request tidak diizinkan.',array(),405);$this->requirePermission('budgets',$permission);try{$result=$fn();if($this->input->is_ajax_request())return json_response(true,$message,array('result'=>$result));$this->session->set_flashdata('success',$message);redirect('budgets'.($open&&is_numeric($result)?'?budget_id='.$result:''));}catch(Throwable$e){if($this->input->is_ajax_request())return business_exception_response($e);$this->session->set_flashdata('error',$e->getMessage());redirect('budgets');}}
|
||||
private function allowed($action){return is_master_admin_user()||check_permission('budgets',$action);}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
|
||||
class Bukubesar extends CI_Controller {
|
||||
class Bukubesar extends MY_Admin_Controller {
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
@@ -59,6 +59,7 @@ class Bukubesar extends CI_Controller {
|
||||
$this->db->from('journal_details');
|
||||
$this->db->join('journals','journals.id = journal_details.journal_id');
|
||||
$this->db->join('accounts','accounts.id = journal_details.account_id');
|
||||
$this->db->where_in('journals.status', array('posted', 'reversed'));
|
||||
|
||||
if ($account_id) {
|
||||
$this->db->where('journal_details.account_id', $account_id);
|
||||
@@ -105,4 +106,4 @@ class Bukubesar extends CI_Controller {
|
||||
|
||||
echo json_encode($result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
|
||||
class Cameraqrscan extends CI_Controller {
|
||||
class Cameraqrscan extends MY_Controller {
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
class Cashbank extends MY_Controller
|
||||
{
|
||||
public function __construct(){parent::__construct();$this->load->database();$this->load->library('CashBankService');}
|
||||
private function uid(){return(int)$this->session->userdata('user_id');}private function go($ok,$m){$this->session->set_flashdata($ok?'success':'error',$m);redirect('cashbank');}
|
||||
public function index(){$d=array('active_menu'=>'cash_bank');$d['cash_accounts']=$this->db->select('c.*,a.kode_akun,a.nama_akun')->from('cash_accounts c')->join('accounts a','a.id=c.gl_account_id')->where('c.is_active',1)->get()->result();$d['accounts']=$this->db->order_by('kode_akun')->get('accounts')->result();$d['transactions']=$this->db->select('t.*,c.name account_name')->from('cash_transactions t')->join('cash_accounts c','c.id=t.cash_account_id')->order_by('t.transaction_date','DESC')->order_by('t.id','DESC')->limit(100)->get()->result();$d['statements']=$this->db->select('l.*,c.name account_name')->from('bank_statement_lines l')->join('bank_statement_imports i','i.id=l.import_id')->join('cash_accounts c','c.id=i.cash_account_id')->order_by('l.transaction_date','DESC')->limit(100)->get()->result();$d['positions']=$this->db->query("SELECT c.id,c.name,c.account_type,c.opening_balance+COALESCE(SUM(IF(t.status='posted',IF(t.direction='in',t.amount,-t.amount),0)),0) balance FROM cash_accounts c LEFT JOIN cash_transactions t ON t.cash_account_id=c.id GROUP BY c.id")->result();$this->load->view('partials/header',$d);$this->load->view('cash_bank/index',$d);$this->load->view('partials/footer');}
|
||||
public function transaction(){try{$r=$this->cashbankservice->post(array('transaction_date'=>$this->input->post('date'),'cash_account_id'=>(int)$this->input->post('cash_account_id'),'transaction_type'=>$this->input->post('type'),'direction'=>$this->input->post('direction'),'amount'=>(float)$this->input->post('amount'),'counter_account_id'=>(int)$this->input->post('counter_account_id'),'reference_no'=>$this->input->post('reference_no',true),'description'=>$this->input->post('description',true),'idempotency_key'=>$this->input->post('idempotency_key',true)),$this->uid());$this->go(true,'Transaksi '.$r['transaction_no'].' tersimpan dengan status '.$r['status'].'.');}catch(Throwable$e){$this->go(false,$e->getMessage());}}
|
||||
public function approve($id){$this->requireAdmin();try{$this->cashbankservice->approve($id,$this->uid());$this->go(true,'Pembayaran disetujui dan diposting.');}catch(Throwable$e){$this->go(false,$e->getMessage());}}
|
||||
public function transfer(){try{$this->cashbankservice->transfer((int)$this->input->post('from_id'),(int)$this->input->post('to_id'),$this->input->post('date'),(float)$this->input->post('amount'),(float)$this->input->post('charge'),(int)$this->input->post('charge_account_id'),$this->input->post('reference_no',true),$this->uid(),$this->input->post('idempotency_key',true));$this->go(true,'Transfer berhasil diposting.');}catch(Throwable$e){$this->go(false,$e->getMessage());}}
|
||||
public function import(){try{if(empty($_FILES['statement']['tmp_name']))throw new BusinessException('File CSV wajib dipilih.');$hash=hash_file('sha256',$_FILES['statement']['tmp_name']);$cid=(int)$this->input->post('cash_account_id');if($this->db->get_where('bank_statement_imports',array('cash_account_id'=>$cid,'file_hash'=>$hash))->row())throw new BusinessException('File ini sudah pernah diimpor.');$this->db->insert('bank_statement_imports',array('cash_account_id'=>$cid,'file_name'=>$_FILES['statement']['name'],'file_hash'=>$hash,'created_by'=>$this->uid(),'created_at'=>date('Y-m-d H:i:s')));$iid=$this->db->insert_id();$h=fopen($_FILES['statement']['tmp_name'],'r');$n=0;while(($r=fgetcsv($h,0,','))!==false){if(!$n++&&strtotime($r[0])===false)continue;if(count($r)<4)continue;$this->db->insert('bank_statement_lines',array('import_id'=>$iid,'line_no'=>$n,'transaction_date'=>date('Y-m-d',strtotime($r[0])),'description'=>$r[1],'reference_no'=>isset($r[2])?$r[2]:null,'debit'=>(float)$r[3],'credit'=>isset($r[4])?(float)$r[4]:0,'balance'=>isset($r[5])?(float)$r[5]:null));}fclose($h);$this->autoMatch($iid);$this->go(true,($n-1).' baris diimpor.');}catch(Throwable$e){$this->go(false,$e->getMessage());}}
|
||||
private function autoMatch($iid){$imp=$this->db->get_where('bank_statement_imports',array('id'=>$iid))->row();foreach($this->db->get_where('bank_statement_lines',array('import_id'=>$iid))->result()as$l){$amount=$l->credit>0?$l->credit:$l->debit;$dir=$l->credit>0?'in':'out';$tx=$this->db->query("SELECT * FROM cash_transactions WHERE cash_account_id=? AND direction=? AND status='posted' AND ABS(amount-?)<0.01 AND transaction_date BETWEEN DATE_SUB(?,INTERVAL 3 DAY) AND DATE_ADD(?,INTERVAL 3 DAY) AND id NOT IN(SELECT matched_cash_transaction_id FROM bank_statement_lines WHERE matched_cash_transaction_id IS NOT NULL) ORDER BY IF(reference_no=?,0,1),ABS(DATEDIFF(transaction_date,?)) LIMIT 1",array($imp->cash_account_id,$dir,$amount,$l->transaction_date,$l->transaction_date,$l->reference_no,$l->transaction_date))->row();if($tx)$this->db->where('id',$l->id)->update('bank_statement_lines',array('match_status'=>'suggested','matched_cash_transaction_id'=>$tx->id,'match_score'=>$tx->reference_no===$l->reference_no?100:85));}}
|
||||
public function match($line){$l=$this->db->get_where('bank_statement_lines',array('id'=>$line))->row();if(!$l||!$l->matched_cash_transaction_id)return$this->go(false,'Pasangan belum tersedia.');$this->db->where('id',$line)->update('bank_statement_lines',array('match_status'=>'matched'));$this->go(true,'Mutasi berhasil dicocokkan.');}
|
||||
public function reconcile(){$id=(int)$this->input->post('cash_account_id');$date=$this->input->post('period_end');$row=$this->db->query("SELECT c.opening_balance+COALESCE(SUM(IF(t.direction='in',t.amount,-t.amount)),0)t FROM cash_accounts c LEFT JOIN cash_transactions t ON t.cash_account_id=c.id AND t.status='posted' AND t.transaction_date<=? WHERE c.id=? GROUP BY c.id",array($date,$id))->row();$book=$row?(float)$row->t:0;$statement=(float)$this->input->post('statement_balance');$or=(float)$this->input->post('outstanding_receipts');$op=(float)$this->input->post('outstanding_payments');$diff=round(($statement+$or-$op)-$book,2);$this->db->insert('bank_reconciliations',array('reconciliation_no'=>'BRC-'.date('YmdHis'),'cash_account_id'=>$id,'period_end'=>$date,'book_balance'=>$book,'statement_balance'=>$statement,'outstanding_receipts'=>$or,'outstanding_payments'=>$op,'difference'=>$diff,'status'=>abs($diff)<.01?'balanced':'variance','notes'=>$this->input->post('notes',true),'created_by'=>$this->uid()));$this->go(true,'Rekonsiliasi disimpan. Selisih Rp '.number_format($diff,2,',','.'));}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
class Companies extends MY_Admin_Controller
|
||||
{
|
||||
public function __construct(){parent::__construct();$this->load->database();$this->load->library(array('CompanyContext','MultiCurrencyService'));}
|
||||
public function index(){$d=array('active_menu'=>'companies');$d['companies']=$this->db->get('companies')->result();$d['currencies']=$this->db->where('is_active',1)->get('currencies')->result();$d['rates']=$this->db->select('r.*,c.name company_name')->from('exchange_rates r')->join('companies c','c.id=r.company_id')->order_by('rate_date','DESC')->limit(100)->get()->result();$this->load->view('partials/header',$d);$this->load->view('companies/index',$d);$this->load->view('partials/footer');}
|
||||
public function switch_company($id){$this->companycontext->assertAccess($id);$this->session->set_userdata('company_id',(int)$id);redirect('professionalreports');}
|
||||
public function save(){$this->db->insert('companies',array('company_code'=>strtoupper($this->input->post('code',true)),'name'=>$this->input->post('name',true),'legal_name'=>$this->input->post('legal_name',true),'tax_number'=>$this->input->post('tax_number',true),'base_currency'=>$this->input->post('base_currency'),'is_active'=>1,'created_at'=>date('Y-m-d H:i:s')));$id=$this->db->insert_id();$this->db->insert('user_companies',array('user_id'=>$this->session->userdata('user_id'),'company_id'=>$id,'is_default'=>0,'can_consolidate'=>1));redirect('companies');}
|
||||
public function rate(){$this->db->insert('exchange_rates',array('company_id'=>$this->companycontext->id(),'rate_date'=>$this->input->post('date'),'from_currency'=>$this->input->post('from_currency'),'to_currency'=>$this->input->post('to_currency'),'rate'=>$this->input->post('rate'),'source'=>$this->input->post('source',true),'created_by'=>$this->session->userdata('user_id'),'created_at'=>date('Y-m-d H:i:s')));redirect('companies');}
|
||||
public function revalue(){try{$this->multicurrencyservice->revalue($this->input->post('date'),$this->session->userdata('user_id'));$this->session->set_flashdata('success','Revaluasi kurs berhasil diposting.');}catch(Throwable$e){$this->session->set_flashdata('error',$e->getMessage());}redirect('companies');}
|
||||
}
|
||||
@@ -7,11 +7,12 @@ class Cronjob extends CI_Controller {
|
||||
{
|
||||
parent::__construct();
|
||||
$this->load->database();
|
||||
$this->load->library('AccountMappingService');
|
||||
$this->load->library('PostingService');
|
||||
|
||||
// ONLY CLI ACCESS
|
||||
// if (!$this->input->is_cli_request()) {
|
||||
// exit('No direct access allowed');
|
||||
// }
|
||||
if (!$this->input->is_cli_request()) {
|
||||
show_error('Cronjob hanya dapat dijalankan melalui CLI.', 403, 'Akses ditolak');
|
||||
}
|
||||
}
|
||||
|
||||
public function generate_insight()
|
||||
@@ -358,9 +359,24 @@ class Cronjob extends CI_Controller {
|
||||
// =====================================================
|
||||
public function depreciation_asset()
|
||||
{
|
||||
// Tahap 8: satu-satunya jalur penyusutan memakai subledger yang
|
||||
// idempotent, row-lock, dan pemeriksaan periode akuntansi.
|
||||
$this->load->library('FixedAssetService');
|
||||
$assets = $this->db->select('id')->where('lifecycle_status', 'active')->get('assets')->result();
|
||||
$result = array();
|
||||
foreach ($assets as $asset) {
|
||||
try {
|
||||
$result[$asset->id] = $this->fixedassetservice->depreciate($asset->id, date('Y-m-d'), 1, true);
|
||||
} catch (Throwable $e) {
|
||||
$result[$asset->id] = array('error' => $e->getMessage());
|
||||
}
|
||||
}
|
||||
return $this->output->set_content_type('application/json')->set_output(json_encode(array('status'=>true,'assets'=>$result)));
|
||||
|
||||
/* Legacy engine retained below for historical reference; unreachable. */
|
||||
$period = date('Y-m');
|
||||
$runDate = date('Y-m-d');
|
||||
$akun_beban_penyusutan = 54;
|
||||
$akun_beban_penyusutan = $this->accountmappingservice->get('depreciation_expense');
|
||||
|
||||
$assets = $this->db
|
||||
->select('id, nama_asset, nilai_perolehan, nilai_buku, nilai_residu, penyusutan_per_bulan, masa_manfaat, tanggal_perolehan, akumulasi_penyusutan, account_debit_id')
|
||||
@@ -576,30 +592,25 @@ class Cronjob extends CI_Controller {
|
||||
]);
|
||||
|
||||
$no_ref = 'DEP-' . date('YmdHis') . '-' . $a->id . '-' . str_replace('-', '', $iterMonth);
|
||||
$this->db->insert('journals', [
|
||||
$this->postingservice->post([
|
||||
'tanggal' => $dueDateThisMonth,
|
||||
'no_ref' => $no_ref,
|
||||
'keterangan' => 'Penyusutan Asset ' . $a->nama_asset . ' periode ' . $iterMonth,
|
||||
'ref_type' => 'assets',
|
||||
'ref_id' => $a->id,
|
||||
'created_by' => 1
|
||||
]);
|
||||
|
||||
$jid = $this->db->insert_id();
|
||||
|
||||
$this->db->insert('journal_details', [
|
||||
'journal_id' => $jid,
|
||||
'account_id' => $akun_beban_penyusutan,
|
||||
'debit' => $susut,
|
||||
'kredit' => 0
|
||||
]);
|
||||
|
||||
$this->db->insert('journal_details', [
|
||||
'journal_id' => $jid,
|
||||
'account_id' => $akun_akumulasi_penyusutan,
|
||||
'debit' => 0,
|
||||
'kredit' => $susut
|
||||
]);
|
||||
], [
|
||||
[
|
||||
'account_id' => $akun_beban_penyusutan,
|
||||
'debit' => $susut,
|
||||
'kredit' => 0
|
||||
],
|
||||
[
|
||||
'account_id' => $akun_akumulasi_penyusutan,
|
||||
'debit' => 0,
|
||||
'kredit' => $susut
|
||||
]
|
||||
], false, false);
|
||||
|
||||
$success++;
|
||||
$assetHasSuccess = true;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
|
||||
class Customers extends CI_Controller {
|
||||
class Customers extends MY_Controller {
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
@@ -73,7 +73,11 @@ class Customers extends CI_Controller {
|
||||
'nama' => $this->input->post('nama'),
|
||||
'alamat' => $this->input->post('alamat'),
|
||||
'telp' => $this->input->post('telp'),
|
||||
'email' => $this->input->post('email')
|
||||
'email' => $this->input->post('email'),
|
||||
'credit_limit' => max(0,(float)$this->input->post('credit_limit')),
|
||||
'payment_term_days' => max(0,(int)$this->input->post('payment_term_days')),
|
||||
'is_credit_blocked' => $this->input->post('is_credit_blocked') ? 1 : 0,
|
||||
'tax_number' => $this->input->post('tax_number',true)
|
||||
];
|
||||
|
||||
if (!$data['nama']) {
|
||||
@@ -114,7 +118,11 @@ class Customers extends CI_Controller {
|
||||
'nama' => $this->input->post('nama'),
|
||||
'alamat' => $this->input->post('alamat'),
|
||||
'telp' => $this->input->post('telp'),
|
||||
'email' => $this->input->post('email')
|
||||
'email' => $this->input->post('email'),
|
||||
'credit_limit' => max(0,(float)$this->input->post('credit_limit')),
|
||||
'payment_term_days' => max(0,(int)$this->input->post('payment_term_days')),
|
||||
'is_credit_blocked' => $this->input->post('is_credit_blocked') ? 1 : 0,
|
||||
'tax_number' => $this->input->post('tax_number',true)
|
||||
];
|
||||
|
||||
$this->dm->setTable('customers')->update($id, $data);
|
||||
@@ -149,4 +157,4 @@ class Customers extends CI_Controller {
|
||||
|
||||
echo json_encode(['status'=>true,'message'=>'Data berhasil dihapus']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
|
||||
class Dashboard extends CI_Controller {
|
||||
class Dashboard extends MY_Controller {
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->load->library('session');
|
||||
$this->load->library('PerformanceCacheService');
|
||||
|
||||
// Cek apakah user sudah login
|
||||
if (!$this->session->userdata('logged_in')) {
|
||||
@@ -16,6 +17,12 @@ class Dashboard extends CI_Controller {
|
||||
|
||||
public function index()
|
||||
{
|
||||
$companyId=(int)$this->companycontext->id();
|
||||
$summary=$this->performancecacheservice->remember('dashboard:summary:'.$companyId,300,function()use($companyId){
|
||||
$row=$this->db->query("SELECT COALESCE(SUM(IF(a.tipe='revenue',d.kredit-d.debit,0)),0) total_revenue,COALESCE(SUM(IF(a.tipe='expense',d.debit-d.kredit,0)),0) total_expense FROM journal_details d JOIN journals j ON j.id=d.journal_id JOIN accounts a ON a.id=d.account_id WHERE j.company_id=? AND j.status IN('posted','reversed') AND a.kategori='laba_rugi' AND a.is_active=1",array($companyId))->row_array();
|
||||
$row['net_profit']=(float)$row['total_revenue']-(float)$row['total_expense'];return$row;
|
||||
});
|
||||
goto dashboard_summary_ready;
|
||||
$this->db->select('
|
||||
accounts.id,
|
||||
accounts.kode_akun,
|
||||
@@ -26,6 +33,8 @@ class Dashboard extends CI_Controller {
|
||||
');
|
||||
$this->db->from('accounts');
|
||||
$this->db->join('journal_details','accounts.id = journal_details.account_id','left');
|
||||
$this->db->join('journals','journals.id = journal_details.journal_id','left');
|
||||
$this->db->where("(journals.status IN ('posted','reversed') OR journal_details.id IS NULL)", null, false);
|
||||
$this->db->where('accounts.kategori','laba_rugi');
|
||||
$this->db->where('accounts.is_active',1);
|
||||
$this->db->group_by('accounts.id');
|
||||
@@ -56,6 +65,7 @@ class Dashboard extends CI_Controller {
|
||||
'net_profit' => $revenue - $expense
|
||||
];
|
||||
|
||||
dashboard_summary_ready:
|
||||
$today = date('Y-m');
|
||||
$ai_insight = $this->db
|
||||
->select('type, severity, message, meta, created_at')
|
||||
@@ -214,6 +224,7 @@ class Dashboard extends CI_Controller {
|
||||
$this->db->from('journal_details');
|
||||
$this->db->join('journals', 'journals.id = journal_details.journal_id', 'left');
|
||||
$this->db->join('accounts', 'accounts.id = journal_details.account_id', 'left');
|
||||
$this->db->where_in('journals.status', array('posted', 'reversed'));
|
||||
$this->db->where('accounts.kategori', 'laba_rugi');
|
||||
$this->db->where('accounts.is_active', 1);
|
||||
|
||||
@@ -269,6 +280,7 @@ class Dashboard extends CI_Controller {
|
||||
{
|
||||
$this->db->select('DISTINCT YEAR(tanggal) as year');
|
||||
$this->db->from('journals');
|
||||
$this->db->where_in('status', array('posted', 'reversed'));
|
||||
$this->db->order_by('year', 'DESC');
|
||||
|
||||
$data = $this->db->get()->result();
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
|
||||
/**
|
||||
* One-way legacy database cutover utility.
|
||||
*
|
||||
* This controller is deliberately CLI-only. Credentials are read from the
|
||||
* environment and are never stored in the application source.
|
||||
*/
|
||||
class Databasesync extends CI_Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
if (!$this->input->is_cli_request()) {
|
||||
show_error('Database sync hanya dapat dijalankan melalui CLI.', 403, 'Akses ditolak');
|
||||
}
|
||||
if (!filter_var(getenv('DBSYNC_ENABLED') ?: 'false', FILTER_VALIDATE_BOOLEAN)) {
|
||||
fwrite(STDERR, "DBSYNC_ENABLED belum diaktifkan.\n");
|
||||
exit(2);
|
||||
}
|
||||
|
||||
$this->load->library('DatabaseSyncService');
|
||||
}
|
||||
|
||||
public function check()
|
||||
{
|
||||
$this->render($this->databasesyncservice->check());
|
||||
}
|
||||
|
||||
public function snapshot()
|
||||
{
|
||||
$this->render($this->databasesyncservice->snapshot());
|
||||
}
|
||||
|
||||
public function bootstrap()
|
||||
{
|
||||
$this->render($this->databasesyncservice->bootstrap());
|
||||
}
|
||||
|
||||
public function verify()
|
||||
{
|
||||
$this->render($this->databasesyncservice->verify());
|
||||
}
|
||||
|
||||
private function render($result)
|
||||
{
|
||||
echo json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . PHP_EOL;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,338 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
|
||||
class Dbcompare extends MY_Admin_Controller
|
||||
{
|
||||
private $sourceConfig;
|
||||
private $targetConfig;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
if (!filter_var(getenv('DBCOMPARE_ENABLED') ?: 'false', FILTER_VALIDATE_BOOLEAN)) {
|
||||
show_404();
|
||||
}
|
||||
|
||||
$this->sourceConfig = $this->readConnectionConfig('DBCOMPARE_SOURCE');
|
||||
$this->targetConfig = $this->readConnectionConfig('DBCOMPARE_TARGET');
|
||||
}
|
||||
|
||||
private function readConnectionConfig($prefix)
|
||||
{
|
||||
$config = array(
|
||||
'host' => getenv($prefix . '_HOST'),
|
||||
'port' => (int) getenv($prefix . '_PORT'),
|
||||
'user' => getenv($prefix . '_USERNAME'),
|
||||
'pass' => getenv($prefix . '_PASSWORD'),
|
||||
'db' => getenv($prefix . '_DATABASE')
|
||||
);
|
||||
|
||||
foreach (array('host', 'port', 'user', 'pass', 'db') as $key) {
|
||||
if (empty($config[$key])) {
|
||||
show_error('Konfigurasi Dbcompare belum lengkap.', 503);
|
||||
}
|
||||
}
|
||||
|
||||
return $config;
|
||||
}
|
||||
|
||||
public function sql()
|
||||
{
|
||||
$result = $this->buildSqlDiff();
|
||||
|
||||
header('Content-Type: text/plain; charset=utf-8');
|
||||
echo $result['sql'];
|
||||
}
|
||||
|
||||
public function sql_download()
|
||||
{
|
||||
$result = $this->buildSqlDiff();
|
||||
|
||||
$filename = 'sync_structure_' . date('Ymd_His') . '.sql';
|
||||
header('Content-Type: application/sql; charset=utf-8');
|
||||
header('Content-Disposition: attachment; filename="' . $filename . '"');
|
||||
echo $result['sql'];
|
||||
}
|
||||
|
||||
private function buildSqlDiff()
|
||||
{
|
||||
$source = $this->connectMysqli($this->sourceConfig);
|
||||
$target = $this->connectMysqli($this->targetConfig);
|
||||
|
||||
$sqlLines = array();
|
||||
$sqlLines[] = '-- ==================================================';
|
||||
$sqlLines[] = '-- AUTO GENERATED SQL - STRUCTURE SYNC';
|
||||
$sqlLines[] = '-- Source DB : ' . $this->sourceConfig['db'] . ' (' . $this->sourceConfig['host'] . ':' . $this->sourceConfig['port'] . ')';
|
||||
$sqlLines[] = '-- Target DB : ' . $this->targetConfig['db'] . ' (' . $this->targetConfig['host'] . ':' . $this->targetConfig['port'] . ')';
|
||||
$sqlLines[] = '-- Generated : ' . date('Y-m-d H:i:s');
|
||||
$sqlLines[] = '-- Notes : Hanya struktur (table/column/primary key/index), tanpa data';
|
||||
$sqlLines[] = '-- ==================================================';
|
||||
$sqlLines[] = '';
|
||||
|
||||
$sourceTables = $this->getTables($source, $this->sourceConfig['db']);
|
||||
$targetTables = $this->getTables($target, $this->targetConfig['db']);
|
||||
|
||||
foreach ($sourceTables as $tableName) {
|
||||
$safeTable = $this->escapeIdentifier($tableName);
|
||||
|
||||
if (!in_array($tableName, $targetTables, true)) {
|
||||
$createSql = $this->getCreateTableSql($source, $tableName);
|
||||
$sqlLines[] = '-- --------------------------------------------------';
|
||||
$sqlLines[] = '-- CREATE TABLE: ' . $tableName;
|
||||
$sqlLines[] = '-- --------------------------------------------------';
|
||||
$sqlLines[] = $createSql . ';';
|
||||
$sqlLines[] = '';
|
||||
continue;
|
||||
}
|
||||
|
||||
$sqlLines[] = '-- --------------------------------------------------';
|
||||
$sqlLines[] = '-- ALTER TABLE: ' . $tableName;
|
||||
$sqlLines[] = '-- --------------------------------------------------';
|
||||
|
||||
$sourceColumns = $this->getColumns($source, $this->sourceConfig['db'], $tableName);
|
||||
$targetColumns = $this->getColumns($target, $this->targetConfig['db'], $tableName);
|
||||
|
||||
// Column ADD / MODIFY
|
||||
foreach ($sourceColumns as $columnName => $sourceCol) {
|
||||
if (!isset($targetColumns[$columnName])) {
|
||||
$sqlLines[] = 'ALTER TABLE ' . $safeTable . ' ADD COLUMN ' . $this->buildColumnDefinition($sourceCol) . ';';
|
||||
continue;
|
||||
}
|
||||
|
||||
$targetCol = $targetColumns[$columnName];
|
||||
if (!$this->isSameColumn($sourceCol, $targetCol)) {
|
||||
$sqlLines[] = 'ALTER TABLE ' . $safeTable . ' MODIFY COLUMN ' . $this->buildColumnDefinition($sourceCol) . ';';
|
||||
}
|
||||
}
|
||||
|
||||
// Primary key sync
|
||||
$sourcePk = $this->getPrimaryKeyColumns($source, $this->sourceConfig['db'], $tableName);
|
||||
$targetPk = $this->getPrimaryKeyColumns($target, $this->targetConfig['db'], $tableName);
|
||||
|
||||
if ($sourcePk !== $targetPk) {
|
||||
if (!empty($targetPk)) {
|
||||
$sqlLines[] = 'ALTER TABLE ' . $safeTable . ' DROP PRIMARY KEY;';
|
||||
}
|
||||
if (!empty($sourcePk)) {
|
||||
$pkCols = array();
|
||||
foreach ($sourcePk as $pkCol) {
|
||||
$pkCols[] = $this->escapeIdentifier($pkCol);
|
||||
}
|
||||
$sqlLines[] = 'ALTER TABLE ' . $safeTable . ' ADD PRIMARY KEY (' . implode(', ', $pkCols) . ');';
|
||||
}
|
||||
}
|
||||
|
||||
// Secondary indexes sync (add missing / different composition)
|
||||
$sourceIndexes = $this->getIndexes($source, $this->sourceConfig['db'], $tableName);
|
||||
$targetIndexes = $this->getIndexes($target, $this->targetConfig['db'], $tableName);
|
||||
|
||||
foreach ($sourceIndexes as $idxName => $sourceIdx) {
|
||||
if ($idxName === 'PRIMARY') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$needCreate = false;
|
||||
if (!isset($targetIndexes[$idxName])) {
|
||||
$needCreate = true;
|
||||
} else {
|
||||
$targetIdx = $targetIndexes[$idxName];
|
||||
if (!$this->isSameIndex($sourceIdx, $targetIdx)) {
|
||||
$sqlLines[] = 'ALTER TABLE ' . $safeTable . ' DROP INDEX ' . $this->escapeIdentifier($idxName) . ';';
|
||||
$needCreate = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ($needCreate) {
|
||||
$idxCols = array();
|
||||
foreach ($sourceIdx['columns'] as $colName) {
|
||||
$idxCols[] = $this->escapeIdentifier($colName);
|
||||
}
|
||||
|
||||
$idxType = $sourceIdx['non_unique'] == 0 ? 'ADD UNIQUE INDEX ' : 'ADD INDEX ';
|
||||
$sqlLines[] = 'ALTER TABLE ' . $safeTable . ' ' . $idxType . $this->escapeIdentifier($idxName) . ' (' . implode(', ', $idxCols) . ');';
|
||||
}
|
||||
}
|
||||
|
||||
$sqlLines[] = '';
|
||||
}
|
||||
|
||||
$source->close();
|
||||
$target->close();
|
||||
|
||||
return array('sql' => implode("\n", $sqlLines));
|
||||
}
|
||||
|
||||
private function connectMysqli($config)
|
||||
{
|
||||
$conn = @new mysqli($config['host'], $config['user'], $config['pass'], $config['db'], (int)$config['port']);
|
||||
if ($conn->connect_errno) {
|
||||
show_error('Gagal konek DB ' . $config['db'] . ': ' . $conn->connect_error, 500);
|
||||
}
|
||||
$conn->set_charset('utf8');
|
||||
return $conn;
|
||||
}
|
||||
|
||||
private function getTables($conn, $schema)
|
||||
{
|
||||
$sql = "SELECT TABLE_NAME
|
||||
FROM INFORMATION_SCHEMA.TABLES
|
||||
WHERE TABLE_SCHEMA = ?
|
||||
AND TABLE_TYPE = 'BASE TABLE'
|
||||
ORDER BY TABLE_NAME";
|
||||
$stmt = $conn->prepare($sql);
|
||||
$stmt->bind_param('s', $schema);
|
||||
$stmt->execute();
|
||||
$res = $stmt->get_result();
|
||||
|
||||
$tables = array();
|
||||
while ($row = $res->fetch_assoc()) {
|
||||
$tables[] = $row['TABLE_NAME'];
|
||||
}
|
||||
$stmt->close();
|
||||
|
||||
return $tables;
|
||||
}
|
||||
|
||||
private function getCreateTableSql($conn, $tableName)
|
||||
{
|
||||
$sql = 'SHOW CREATE TABLE ' . $this->escapeIdentifier($tableName);
|
||||
$res = $conn->query($sql);
|
||||
if (!$res) {
|
||||
return '-- ERROR: gagal ambil CREATE TABLE untuk ' . $tableName;
|
||||
}
|
||||
$row = $res->fetch_assoc();
|
||||
return isset($row['Create Table']) ? $row['Create Table'] : '-- ERROR: format SHOW CREATE TABLE tidak valid';
|
||||
}
|
||||
|
||||
private function getColumns($conn, $schema, $tableName)
|
||||
{
|
||||
$sql = "SELECT COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE, COLUMN_DEFAULT, EXTRA, COLUMN_COMMENT, CHARACTER_SET_NAME, COLLATION_NAME
|
||||
FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = ?
|
||||
AND TABLE_NAME = ?
|
||||
ORDER BY ORDINAL_POSITION";
|
||||
$stmt = $conn->prepare($sql);
|
||||
$stmt->bind_param('ss', $schema, $tableName);
|
||||
$stmt->execute();
|
||||
$res = $stmt->get_result();
|
||||
|
||||
$columns = array();
|
||||
while ($row = $res->fetch_assoc()) {
|
||||
$columns[$row['COLUMN_NAME']] = $row;
|
||||
}
|
||||
$stmt->close();
|
||||
|
||||
return $columns;
|
||||
}
|
||||
|
||||
private function getPrimaryKeyColumns($conn, $schema, $tableName)
|
||||
{
|
||||
$sql = "SELECT kcu.COLUMN_NAME
|
||||
FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc
|
||||
JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu
|
||||
ON tc.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME
|
||||
AND tc.TABLE_SCHEMA = kcu.TABLE_SCHEMA
|
||||
AND tc.TABLE_NAME = kcu.TABLE_NAME
|
||||
WHERE tc.TABLE_SCHEMA = ?
|
||||
AND tc.TABLE_NAME = ?
|
||||
AND tc.CONSTRAINT_TYPE = 'PRIMARY KEY'
|
||||
ORDER BY kcu.ORDINAL_POSITION";
|
||||
$stmt = $conn->prepare($sql);
|
||||
$stmt->bind_param('ss', $schema, $tableName);
|
||||
$stmt->execute();
|
||||
$res = $stmt->get_result();
|
||||
|
||||
$pk = array();
|
||||
while ($row = $res->fetch_assoc()) {
|
||||
$pk[] = $row['COLUMN_NAME'];
|
||||
}
|
||||
$stmt->close();
|
||||
|
||||
return $pk;
|
||||
}
|
||||
|
||||
private function getIndexes($conn, $schema, $tableName)
|
||||
{
|
||||
$sql = "SELECT INDEX_NAME, NON_UNIQUE, SEQ_IN_INDEX, COLUMN_NAME
|
||||
FROM INFORMATION_SCHEMA.STATISTICS
|
||||
WHERE TABLE_SCHEMA = ?
|
||||
AND TABLE_NAME = ?
|
||||
ORDER BY INDEX_NAME, SEQ_IN_INDEX";
|
||||
$stmt = $conn->prepare($sql);
|
||||
$stmt->bind_param('ss', $schema, $tableName);
|
||||
$stmt->execute();
|
||||
$res = $stmt->get_result();
|
||||
|
||||
$indexes = array();
|
||||
while ($row = $res->fetch_assoc()) {
|
||||
$idxName = $row['INDEX_NAME'];
|
||||
if (!isset($indexes[$idxName])) {
|
||||
$indexes[$idxName] = array(
|
||||
'non_unique' => (int)$row['NON_UNIQUE'],
|
||||
'columns' => array()
|
||||
);
|
||||
}
|
||||
$indexes[$idxName]['columns'][] = $row['COLUMN_NAME'];
|
||||
}
|
||||
$stmt->close();
|
||||
|
||||
return $indexes;
|
||||
}
|
||||
|
||||
private function buildColumnDefinition($col)
|
||||
{
|
||||
$def = $this->escapeIdentifier($col['COLUMN_NAME']) . ' ' . $col['COLUMN_TYPE'];
|
||||
|
||||
$def .= ($col['IS_NULLABLE'] === 'NO') ? ' NOT NULL' : ' NULL';
|
||||
|
||||
if ($col['COLUMN_DEFAULT'] !== null) {
|
||||
if (strtoupper($col['COLUMN_DEFAULT']) === 'CURRENT_TIMESTAMP') {
|
||||
$def .= ' DEFAULT CURRENT_TIMESTAMP';
|
||||
} else {
|
||||
$def .= " DEFAULT '" . $this->escapeStringValue($col['COLUMN_DEFAULT']) . "'";
|
||||
}
|
||||
} elseif ($col['IS_NULLABLE'] === 'YES') {
|
||||
$def .= ' DEFAULT NULL';
|
||||
}
|
||||
|
||||
if (!empty($col['EXTRA'])) {
|
||||
$def .= ' ' . strtoupper($col['EXTRA']);
|
||||
}
|
||||
|
||||
if (!empty($col['COLUMN_COMMENT'])) {
|
||||
$def .= " COMMENT '" . $this->escapeStringValue($col['COLUMN_COMMENT']) . "'";
|
||||
}
|
||||
|
||||
return $def;
|
||||
}
|
||||
|
||||
private function isSameColumn($a, $b)
|
||||
{
|
||||
return (
|
||||
strtolower((string)$a['COLUMN_TYPE']) === strtolower((string)$b['COLUMN_TYPE']) &&
|
||||
(string)$a['IS_NULLABLE'] === (string)$b['IS_NULLABLE'] &&
|
||||
(string)$a['COLUMN_DEFAULT'] === (string)$b['COLUMN_DEFAULT'] &&
|
||||
strtolower((string)$a['EXTRA']) === strtolower((string)$b['EXTRA']) &&
|
||||
(string)$a['COLUMN_COMMENT'] === (string)$b['COLUMN_COMMENT']
|
||||
);
|
||||
}
|
||||
|
||||
private function isSameIndex($a, $b)
|
||||
{
|
||||
return (
|
||||
(int)$a['non_unique'] === (int)$b['non_unique'] &&
|
||||
implode(',', $a['columns']) === implode(',', $b['columns'])
|
||||
);
|
||||
}
|
||||
|
||||
private function escapeIdentifier($name)
|
||||
{
|
||||
return '`' . str_replace('`', '``', $name) . '`';
|
||||
}
|
||||
|
||||
private function escapeStringValue($str)
|
||||
{
|
||||
return str_replace("'", "''", (string)$str);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
class Dbmonitor extends CI_Controller
|
||||
{
|
||||
public function run(){if(!$this->input->is_cli_request())show_404();$start=microtime(true);$this->db->reconnect();$connection=(microtime(true)-$start)*1000;$start=microtime(true);$this->db->query('SELECT 1')->row();$query=(microtime(true)-$start)*1000;$statusRows=$this->db->query("SHOW GLOBAL STATUS WHERE Variable_name IN('Threads_connected','Threads_running','Slow_queries')")->result();$status=array();foreach($statusRows as$r)$status[$r->Variable_name]=(int)$r->Value;$size=(float)$this->db->query("SELECT COALESCE(SUM(data_length+index_length)/1024/1024,0) size_mb FROM information_schema.tables WHERE table_schema=DATABASE()")->row()->size_mb;$health=$connection>500||$query>500?'critical':($connection>150||$query>150?'warning':'healthy');$message='Connection '.round($connection,2).' ms; query '.round($query,2).' ms';$this->db->insert('database_health_metrics',array('checked_at'=>date('Y-m-d H:i:s'),'connection_ms'=>$connection,'query_ms'=>$query,'threads_connected'=>$status['Threads_connected']??null,'threads_running'=>$status['Threads_running']??null,'slow_queries'=>$status['Slow_queries']??null,'db_size_mb'=>$size,'status'=>$health,'message'=>$message));echo json_encode(array('status'=>$health,'connection_ms'=>round($connection,3),'query_ms'=>round($query,3),'db_size_mb'=>round($size,2),'threads_connected'=>$status['Threads_connected']??null,'threads_running'=>$status['Threads_running']??null,'slow_queries'=>$status['Slow_queries']??null),JSON_PRETTY_PRINT).PHP_EOL;}
|
||||
}
|
||||
@@ -1,12 +1,16 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
|
||||
class Employees extends CI_Controller {
|
||||
class Employees extends MY_Controller {
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
// Data karyawan memuat rekening, BPJS, pajak, dan gaji sehingga
|
||||
// sementara dibatasi ke Admin sampai role HR khusus dikonfigurasi.
|
||||
$this->requireAdmin();
|
||||
|
||||
$this->load->database();
|
||||
$this->load->helper(['url', 'file']);
|
||||
|
||||
@@ -473,4 +477,4 @@ class Employees extends CI_Controller {
|
||||
->set_content_type('application/json')
|
||||
->set_output(json_encode($data));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
|
||||
/** Read-only integrity checker: php index.php fixedassetcheck */
|
||||
class Fixedassetcheck extends CI_Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
if (!is_cli()) show_404();
|
||||
$checks=array();
|
||||
$checks['duplicate_asset_document']=$this->scalar("SELECT COUNT(*) FROM (SELECT company_id,document_no FROM assets GROUP BY company_id,document_no HAVING COUNT(*)>1)x");
|
||||
$checks['duplicate_event_idempotency']=$this->scalar("SELECT COUNT(*) FROM (SELECT idempotency_key FROM asset_events WHERE idempotency_key IS NOT NULL GROUP BY idempotency_key HAVING COUNT(*)>1)x");
|
||||
$checks['capitalized_without_event']=$this->scalar("SELECT COUNT(*) FROM assets a WHERE a.workflow_status='capitalized' AND NOT EXISTS(SELECT 1 FROM asset_events e WHERE e.asset_id=a.id AND e.company_id=a.company_id AND e.event_type IN('acquisition','capitalization'))");
|
||||
$checks['active_below_residual']=$this->scalar("SELECT COUNT(*) FROM assets WHERE lifecycle_status IN('active','fully_depreciated') AND nilai_buku+0.005<nilai_residu");
|
||||
$checks['duplicate_depreciation_period']=$this->scalar("SELECT COUNT(*) FROM (SELECT company_id,asset_id,period FROM asset_depreciation_schedule GROUP BY company_id,asset_id,period HAVING COUNT(*)>1)x");
|
||||
$checks['unbalanced_asset_journals']=$this->scalar("SELECT COUNT(*) FROM (SELECT j.id FROM journals j JOIN journal_details d ON d.journal_id=j.id WHERE j.ref_type LIKE 'asset_%' AND j.status='posted' GROUP BY j.id HAVING ABS(SUM(d.debit)-SUM(d.kredit))>0.005)x");
|
||||
$checks['cross_company_asset_attachment']=$this->scalar("SELECT COUNT(*) FROM transaction_attachments t JOIN assets a ON t.entity_type='asset' AND t.entity_id=a.id WHERE t.company_id IS NULL OR t.company_id<>a.company_id");
|
||||
$checks['warehouse_capitalization_without_ledger']=$this->scalar("SELECT COUNT(*) FROM assets a WHERE a.source_type='warehouse' AND a.workflow_status='capitalized' AND a.idempotency_key NOT LIKE 'LEGACY-ASSET-%' AND NOT EXISTS(SELECT 1 FROM inventory_ledger l WHERE l.document_type='asset_capitalization' AND l.document_id=a.id)");
|
||||
$checks['invalid_asset_posting_account']=$this->scalar("SELECT COUNT(*) FROM assets a LEFT JOIN accounts ac ON ac.id=a.asset_account_id AND ac.company_id=a.company_id AND ac.is_active=1 AND ac.allow_posting=1 WHERE a.workflow_status IN('draft','submitted','approved') AND (a.asset_account_id IS NULL OR ac.id IS NULL)");
|
||||
$checks['invalid_accumulated_account_scope']=$this->scalar("SELECT COUNT(*) FROM assets a LEFT JOIN accounts ac ON ac.id=a.accumulated_depreciation_account_id AND ac.company_id=a.company_id WHERE a.workflow_status IN('draft','submitted','approved','capitalized') AND a.idempotency_key NOT LIKE 'LEGACY-ASSET-%' AND (a.accumulated_depreciation_account_id IS NULL OR ac.id IS NULL)");
|
||||
$checks['invalid_expense_account_scope']=$this->scalar("SELECT COUNT(*) FROM assets a LEFT JOIN accounts ac ON ac.id=a.depreciation_expense_account_id AND ac.company_id=a.company_id WHERE a.workflow_status IN('draft','submitted','approved','capitalized') AND a.idempotency_key NOT LIKE 'LEGACY-ASSET-%' AND (a.depreciation_expense_account_id IS NULL OR ac.id IS NULL)");
|
||||
$fail=0;foreach($checks as$name=>$count){$ok=(int)$count===0;if(!$ok)$fail++;echo($ok?'[OK] ':'[WARN] ').$name.': '.$count.PHP_EOL;}
|
||||
$totals=$this->db->query("SELECT COUNT(*) total,COALESCE(SUM(IF(workflow_status='capitalized' AND lifecycle_status NOT IN('disposed','lost','damaged'),nilai_perolehan,0)),0) cost,COALESCE(SUM(IF(workflow_status='capitalized' AND lifecycle_status NOT IN('disposed','lost','damaged'),akumulasi_penyusutan,0)),0) accumulated FROM assets")->row();
|
||||
echo 'Assets: '.$totals->total.' | Cost: '.$totals->cost.' | Accumulated: '.$totals->accumulated.PHP_EOL;
|
||||
$statusColumn=$this->db->query("SHOW COLUMNS FROM assets WHERE Field='status'")->row();echo '[INFO] assets.status: '.($statusColumn->Type??'missing').PHP_EOL;
|
||||
$this->load->model('FixedAssetModel','fixedasset');
|
||||
foreach($this->db->select('id')->get('companies')->result() as$company){
|
||||
$this->fixedasset->dashboard($company->id);$this->fixedasset->categories($company->id);$this->fixedasset->locations($company->id);$this->fixedasset->opnames($company->id);$this->fixedasset->registerPage($company->id,array(),0,5,'');
|
||||
echo '[OK] model_query_smoke_company_'.$company->id.PHP_EOL;
|
||||
}
|
||||
echo $fail?'Integrity warnings: '.$fail.PHP_EOL:'Fixed asset integrity checks passed.'.PHP_EOL;
|
||||
}
|
||||
private function scalar($sql){return(int)$this->db->query($sql)->row_array()['COUNT(*)'];}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
|
||||
class Fixedassets extends MY_Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->load->library(array('FixedAssetService','CompanyContext'));
|
||||
$this->load->model('FixedAssetModel','fixedasset');
|
||||
}
|
||||
|
||||
private function uid(){return(int)$this->session->userdata('user_id');}
|
||||
private function company(){return(int)$this->companycontext->id();}
|
||||
private function master(){return$this->session->userdata('role')==='Admin';}
|
||||
private function can($action){return$this->master()||check_permission('asset',$action);}
|
||||
private function permissions(){return array('create'=>$this->can('can_create'),'update'=>$this->can('can_update'),'approve'=>$this->can('can_approve'),'post'=>$this->can('can_post'),'export'=>$this->can('can_export'));}
|
||||
private function postOnly(){if(strtoupper($this->input->method())!=='POST')show_404();}
|
||||
private function action(callable$callback,$message){$this->postOnly();try{$result=$callback();return json_response(true,$message,array('result'=>$result));}catch(Throwable$e){return business_exception_response($e);}}
|
||||
private function money($value){$value=preg_replace('/[^0-9,.-]/','',(string)$value);if(strpos($value,',')!==false){$value=str_replace('.','',$value);$value=str_replace(',','.',$value);}elseif(preg_match('/^-?\d{1,3}(\.\d{3})+$/',$value)){$value=str_replace('.','',$value);}return round((float)$value,2);}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$data=array('active_menu'=>'fixed_assets','summary'=>$this->fixedasset->dashboard($this->company()),'categories'=>$this->fixedasset->categories($this->company()),'locations'=>$this->fixedasset->locations($this->company()),'accounts'=>$this->fixedasset->accounts($this->company()),'opnames'=>$this->fixedasset->opnames($this->company()),'asset_permissions'=>$this->permissions());
|
||||
$this->load->view('partials/header',$data);$this->load->view('fixed_assets/index',$data);$this->load->view('partials/footer');
|
||||
}
|
||||
|
||||
public function data()
|
||||
{
|
||||
$draw=(int)$this->input->get('draw');$start=max(0,(int)$this->input->get('start'));$length=min(100,max(10,(int)$this->input->get('length')));$search=$this->input->get('search');$search=is_array($search)?trim((string)($search['value']??'')):'';$filters=array('category_id'=>$this->input->get('category_id'),'lokasi_asset_id'=>$this->input->get('location_id'),'source_type'=>$this->input->get('source_type'),'workflow_status'=>$this->input->get('workflow_status'),'lifecycle_status'=>$this->input->get('lifecycle_status'),'responsible_employee_id'=>$this->input->get('responsible_employee_id'),'date_from'=>$this->input->get('date_from'),'date_to'=>$this->input->get('date_to'));$page=$this->fixedasset->registerPage($this->company(),$filters,$start,$length,$search);return$this->output->set_content_type('application/json')->set_output(json_encode(array('draw'=>$draw,'recordsTotal'=>$page['total'],'recordsFiltered'=>$page['filtered'],'data'=>$page['rows'])));
|
||||
}
|
||||
|
||||
public function detail($id)
|
||||
{
|
||||
$asset=$this->fixedasset->detail((int)$id,$this->company());if(!$asset)return json_response(false,'Aset tidak ditemukan.',array(),404);foreach($asset->attachments as$file){$file->preview_url=base_url('accountingattachments/preview/'.$file->id);$file->download_url=base_url('accountingattachments/download/'.$file->id);}return json_response(true,'Detail aset.',array('asset'=>$asset,'permissions'=>$this->permissions()));
|
||||
}
|
||||
|
||||
private function draftInput()
|
||||
{
|
||||
return array('nama_asset'=>trim((string)$this->input->post('nama_asset',true)),'tanggal_perolehan'=>$this->input->post('tanggal_perolehan',true),'capitalization_date'=>$this->input->post('capitalization_date',true),'category_id'=>(int)$this->input->post('category_id'),'lokasi_asset_id'=>(int)$this->input->post('lokasi_asset_id'),'responsible_employee_id'=>(int)$this->input->post('responsible_employee_id'),'source_type'=>$this->input->post('source_type',true),'source_id'=>(int)$this->input->post('source_id'),'source_reference_no'=>$this->input->post('source_reference_no',true),'supplier_id'=>(int)$this->input->post('supplier_id'),'source_journal_mode'=>$this->input->post('source_journal_mode',true),'source_journal_id'=>(int)$this->input->post('source_journal_id'),'item_id'=>(int)$this->input->post('item_id'),'barcode_id'=>(int)$this->input->post('barcode_id'),'warehouse_id'=>(int)$this->input->post('warehouse_id'),'bin_id'=>(int)$this->input->post('bin_id'),'qty'=>$this->money($this->input->post('qty')),'nilai_perolehan'=>$this->money($this->input->post('nilai_perolehan')),'nilai_residu'=>$this->money($this->input->post('nilai_residu')),'masa_manfaat'=>(int)$this->input->post('masa_manfaat'),'depreciation_method'=>$this->input->post('depreciation_method',true),'asset_account_id'=>(int)$this->input->post('asset_account_id'),'accumulated_depreciation_account_id'=>(int)$this->input->post('accumulated_depreciation_account_id'),'depreciation_expense_account_id'=>(int)$this->input->post('depreciation_expense_account_id'),'account_kredit_id'=>(int)$this->input->post('account_kredit_id'),'department_id'=>(int)$this->input->post('department_id'),'project_id'=>(int)$this->input->post('project_id'),'cost_center_id'=>(int)$this->input->post('cost_center_id'),'keterangan'=>$this->input->post('keterangan',true),'idempotency_key'=>$this->input->post('idempotency_key',true));
|
||||
}
|
||||
|
||||
public function save()
|
||||
{
|
||||
$id=(int)$this->input->post('id');$this->requirePermission('asset',$id?'can_update':'can_create');return$this->action(function()use($id){return$id?$this->fixedassetservice->updateDraft($id,$this->draftInput(),$this->uid()):$this->fixedassetservice->createDraft($this->draftInput(),$this->uid());},'Draft perolehan aset berhasil disimpan.');
|
||||
}
|
||||
public function submit($id){$this->requirePermission('asset','can_create');return$this->action(fn()=>$this->fixedassetservice->submit($id,$this->uid()),'Dokumen aset berhasil diajukan.');}
|
||||
public function approve($id){$this->requirePermission('asset','can_approve');return$this->action(fn()=>$this->fixedassetservice->approve($id,$this->uid(),$this->master()),'Dokumen aset berhasil disetujui.');}
|
||||
public function reject($id){$this->requirePermission('asset','can_approve');return$this->action(fn()=>$this->fixedassetservice->reject($id,$this->input->post('reason',true),$this->uid(),$this->master()),'Dokumen aset ditolak.');}
|
||||
public function reopen($id){$this->requirePermission('asset','can_update');return$this->action(fn()=>$this->fixedassetservice->reopenRejected($id,$this->uid()),'Dokumen dikembalikan menjadi draft.');}
|
||||
public function capitalize($id){$this->requirePermission('asset','can_post');return$this->action(fn()=>$this->fixedassetservice->capitalize($id,$this->input->post('date',true),$this->uid()),'Aset berhasil dikapitalisasi.');}
|
||||
public function depreciate($id){$this->requirePermission('asset','can_post');return$this->action(fn()=>$this->fixedassetservice->depreciate($id,$this->input->post('through_date',true),$this->uid(),true),'Penyusutan aset berhasil diproses.');}
|
||||
public function mutate($id){$this->requirePermission('asset','can_post');return$this->action(fn()=>$this->fixedassetservice->mutate($id,$this->input->post('type',true),$this->input->post('date',true),$this->money($this->input->post('amount')),(int)$this->input->post('account_id'),$this->input->post('notes',true),$this->uid()),'Mutasi nilai aset berhasil diposting.');}
|
||||
public function transfer($id){$this->requirePermission('asset','can_update');return$this->action(fn()=>$this->fixedassetservice->transfer($id,$this->input->post('date',true),(int)$this->input->post('location_id'),$this->input->post('notes',true),$this->uid()),'Lokasi aset berhasil dipindahkan.');}
|
||||
public function responsibility($id){$this->requirePermission('asset','can_update');return$this->action(fn()=>$this->fixedassetservice->changeResponsibility($id,$this->input->post('date',true),(int)$this->input->post('employee_id'),$this->input->post('notes',true),$this->uid()),'Penanggung jawab aset berhasil diperbarui.');}
|
||||
public function maintenance($id){$this->requirePermission('asset','can_update');return$this->action(fn()=>$this->fixedassetservice->maintenance($id,array('date'=>$this->input->post('date',true),'vendor'=>$this->input->post('vendor',true),'description'=>$this->input->post('description',true),'cost'=>$this->money($this->input->post('cost')),'next_due_date'=>$this->input->post('next_due_date',true),'idempotency_key'=>$this->input->post('idempotency_key',true)),$this->uid()),'Maintenance aset berhasil dicatat.');}
|
||||
public function reverse($eventId){$this->requirePermission('asset','can_post');return$this->action(fn()=>$this->fixedassetservice->reverseEvent($eventId,$this->input->post('date',true),$this->input->post('reason',true),$this->uid()),'Event aset berhasil direversal.');}
|
||||
|
||||
public function lookup_inventory()
|
||||
{
|
||||
$page=max(1,(int)$this->input->get('page'));$limit=20;$rows=$this->fixedasset->availableInventory($this->company(),$this->input->get('q',true),$page,$limit);$more=count($rows)>$limit;if($more)array_pop($rows);$result=array();foreach($rows as$row)$result[]=array('id'=>$row->barcode_id,'text'=>$row->kode_detail.' - '.$row->nama_barang.' | '.$row->barcode.($row->serial_number?' / SN '.$row->serial_number:'').' | '.$row->warehouse_name.' | tersedia '.$this->qty($row->available_qty),'data'=>$row);return json_response(true,'Barang gudang tersedia.',array('results'=>$result,'pagination'=>array('more'=>$more)));
|
||||
}
|
||||
public function lookup_employees()
|
||||
{
|
||||
$page=max(1,(int)$this->input->get('page'));$limit=20;$rows=$this->fixedasset->employees($this->company(),$this->input->get('q',true),$page,$limit);$more=count($rows)>$limit;if($more)array_pop($rows);$result=array();foreach($rows as$row)$result[]=array('id'=>$row->id,'text'=>$row->employee_code.' - '.$row->full_name);return json_response(true,'Daftar karyawan.',array('results'=>$result,'pagination'=>array('more'=>$more)));
|
||||
}
|
||||
public function lookup_journals()
|
||||
{
|
||||
$q=trim((string)$this->input->get('q',true));$this->db->select("id,CONCAT(no_ref,' - ',DATE_FORMAT(tanggal,'%d/%m/%Y'),' - ',LEFT(keterangan,80)) text",false)->where(array('company_id'=>$this->company(),'status'=>'posted'));if($q!=='')$this->db->group_start()->like('no_ref',$q)->or_like('keterangan',$q)->group_end();$rows=$this->db->order_by('tanggal','DESC')->limit(25)->get('journals')->result();return json_response(true,'Daftar jurnal.',array('results'=>$rows));
|
||||
}
|
||||
|
||||
public function master_data(){return json_response(true,'Master aset.',array('categories'=>$this->fixedasset->categories($this->company()),'locations'=>$this->fixedasset->locations($this->company())));}
|
||||
public function save_category(){ $this->requirePermission('asset','can_update');return$this->action(fn()=>$this->fixedassetservice->saveCategory(array('id'=>$this->input->post('id'),'code'=>$this->input->post('code',true),'name'=>$this->input->post('name',true),'life'=>$this->input->post('life'),'residual_percent'=>$this->money($this->input->post('residual_percent')),'method'=>$this->input->post('method',true),'asset_account_id'=>$this->input->post('asset_account_id'),'accumulated_account_id'=>$this->input->post('accumulated_account_id'),'expense_account_id'=>$this->input->post('expense_account_id'),'is_active'=>$this->input->post('is_active')),$this->uid()),'Kategori aset berhasil disimpan.');}
|
||||
public function save_location(){ $this->requirePermission('asset','can_update');return$this->action(fn()=>$this->fixedassetservice->saveLocation(array('id'=>$this->input->post('id'),'code'=>$this->input->post('code',true),'name'=>$this->input->post('name',true),'address'=>$this->input->post('address',true),'responsible_employee_id'=>$this->input->post('responsible_employee_id'),'is_active'=>$this->input->post('is_active')),$this->uid()),'Lokasi aset berhasil disimpan.');}
|
||||
|
||||
public function opname_create(){ $this->requirePermission('asset','can_create');return$this->action(fn()=>$this->fixedassetservice->createOpname((int)$this->input->post('location_id'),$this->input->post('date',true),$this->input->post('notes',true),$this->input->post('idempotency_key',true),$this->uid()),'Draft asset opname berhasil dibuat.');}
|
||||
public function opname_detail($id){$row=$this->fixedasset->opnameDetail((int)$id,$this->company());if(!$row)return json_response(false,'Asset opname tidak ditemukan.',array(),404);foreach($row->attachments as$file)$file->preview_url=base_url('accountingattachments/preview/'.$file->id);return json_response(true,'Detail asset opname.',array('opname'=>$row,'permissions'=>$this->permissions()));}
|
||||
public function opname_save($id){$this->requirePermission('asset','can_update');return$this->action(function()use($id){$lines=json_decode((string)$this->input->post('lines'),true);if(!is_array($lines))throw new BusinessException('Detail opname tidak valid.');return$this->fixedassetservice->saveOpnameLines($id,$lines,$this->uid());},'Hasil hitung fisik berhasil disimpan.');}
|
||||
public function opname_state($id){$action=$this->input->post('action',true);$this->requirePermission('asset',in_array($action,array('approve','reject'),true)?'can_approve':($action==='post'?'can_post':'can_create'));return$this->action(fn()=>$this->fixedassetservice->opnameState($id,$action,$this->input->post('reason',true),$this->uid(),$this->master()),'Status asset opname berhasil diperbarui.');}
|
||||
|
||||
public function reconciliation()
|
||||
{
|
||||
$this->requirePermission('asset','can_view');$date=$this->input->get('date')?:date('Y-m-d');try{$result=$this->fixedasset->reconciliation($this->company(),$date);return json_response(true,'Rekonsiliasi aset selesai tanpa jurnal otomatis.',array('result'=>$result));}catch(Throwable$e){return business_exception_response($e);}
|
||||
}
|
||||
public function report_data(){try{$type=$this->input->get('type')?:'register';$filters=$this->reportFilters();if($type==='reconciliation')return json_response(true,'Laporan rekonsiliasi.',array('data'=>array($this->fixedasset->reconciliation($this->company(),$filters['date_to']))));return json_response(true,'Laporan aset.',array('data'=>$this->fixedasset->report($this->company(),$type,$filters)));}catch(Throwable$e){return business_exception_response($e);}}
|
||||
private function reportFilters(){return array('category_id'=>$this->input->get('category_id'),'lokasi_asset_id'=>$this->input->get('location_id'),'source_type'=>$this->input->get('source_type'),'workflow_status'=>$this->input->get('workflow_status'),'lifecycle_status'=>$this->input->get('lifecycle_status'),'date_from'=>$this->input->get('date_from')?:date('Y-01-01'),'date_to'=>$this->input->get('date_to')?:date('Y-m-d'));}
|
||||
private function reportDefinition($type){$all=array('register'=>array('title'=>'Asset Register','columns'=>array('kode_asset'=>'Kode','nama_asset'=>'Nama Aset','category_name'=>'Kategori','location_name'=>'Lokasi','source_type'=>'Sumber','tanggal_perolehan'=>'Perolehan','nilai_perolehan'=>'Nilai Perolehan','akumulasi_penyusutan'=>'Akumulasi','nilai_buku'=>'Nilai Buku','lifecycle_status'=>'Status')),'depreciation'=>array('title'=>'Schedule Penyusutan','columns'=>array('period'=>'Periode','kode_asset'=>'Kode','nama_asset'=>'Aset','opening_book_value'=>'Nilai Awal','depreciation_amount'=>'Penyusutan','closing_book_value'=>'Nilai Akhir','status'=>'Status','journal_no'=>'Jurnal')),'maintenance'=>array('title'=>'Riwayat Maintenance','columns'=>array('maintenance_no'=>'Dokumen','maintenance_date'=>'Tanggal','kode_asset'=>'Kode','nama_asset'=>'Aset','vendor'=>'Vendor','description'=>'Keterangan','cost'=>'Biaya','next_due_date'=>'Jadwal Berikutnya','status'=>'Status')),'events'=>array('title'=>'Mutasi dan Audit Aset','columns'=>array('event_no'=>'Dokumen','event_date'=>'Tanggal','event_type'=>'Jenis','kode_asset'=>'Kode','nama_asset'=>'Aset','amount'=>'Nilai','book_value_before'=>'Nilai Sebelum','book_value_after'=>'Nilai Sesudah','journal_no'=>'Jurnal','notes'=>'Keterangan')),'reconciliation'=>array('title'=>'Rekonsiliasi Aset dengan Buku Besar','columns'=>array('as_of'=>'Tanggal','cost_subledger'=>'Perolehan Subledger','cost_gl'=>'Akun Aset GL','cost_difference'=>'Selisih Aset','accumulated_subledger'=>'Akumulasi Subledger','accumulated_gl'=>'Akumulasi GL','accumulated_difference'=>'Selisih Akumulasi','book_value'=>'Nilai Buku','status'=>'Status')));if(!isset($all[$type]))throw new BusinessException('Jenis laporan aset tidak valid.');return$all[$type];}
|
||||
public function report_pdf(){ $this->requirePermission('asset','can_export');$type=$this->input->get('type')?:'register';$filters=$this->reportFilters();$definition=$this->reportDefinition($type);$rows=$type==='reconciliation'?array((object)$this->fixedasset->reconciliation($this->company(),$filters['date_to'])):$this->fixedasset->report($this->company(),$type,$filters);$this->inlinePdf($this->load->view('fixed_assets/report_pdf',array('definition'=>$definition,'rows'=>$rows,'filters'=>$filters,'company'=>$this->companyRow()),true),'Laporan-Aset-'.$type.'-'.date('Ymd').'.pdf','landscape');}
|
||||
public function report_excel(){ $this->requirePermission('asset','can_export');$type=$this->input->get('type')?:'register';$filters=$this->reportFilters();$definition=$this->reportDefinition($type);$rows=$type==='reconciliation'?array((object)$this->fixedasset->reconciliation($this->company(),$filters['date_to'])):$this->fixedasset->report($this->company(),$type,$filters);require_once FCPATH.'vendor/autoload.php';$book=new\PhpOffice\PhpSpreadsheet\Spreadsheet();$sheet=$book->getActiveSheet();$last=\PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex(count($definition['columns']));$company=$this->companyRow();$sheet->setCellValue('A1',$company->name??'Perusahaan')->mergeCells('A1:'.$last.'1');$sheet->setCellValue('A2',$definition['title'])->mergeCells('A2:'.$last.'2');$sheet->setCellValue('A3','Periode '.$filters['date_from'].' s.d. '.$filters['date_to'])->mergeCells('A3:'.$last.'3');$col=1;foreach($definition['columns']as$label)$sheet->setCellValueByColumnAndRow($col++,5,$label);$rowNo=6;foreach($rows as$row){$col=1;foreach($definition['columns']as$key=>$label)$sheet->setCellValueByColumnAndRow($col++,$rowNo,$row->$key??null);$rowNo++;}$sheet->getStyle('A1:'.$last.'2')->getFont()->setBold(true);$sheet->getStyle('A5:'.$last.'5')->getFont()->setBold(true);for($i=1;$i<=count($definition['columns']);$i++)$sheet->getColumnDimension(\PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($i))->setAutoSize(true);while(ob_get_level()>0)ob_end_clean();header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');header('Content-Disposition: attachment; filename="Laporan-Aset-'.$type.'-'.date('Ymd-His').'.xlsx"');\PhpOffice\PhpSpreadsheet\IOFactory::createWriter($book,'Xlsx')->save('php://output');exit;}
|
||||
|
||||
public function asset_pdf($id){$this->requirePermission('asset','can_export');$asset=$this->fixedasset->detail((int)$id,$this->company());if(!$asset)show_404();$this->inlinePdf($this->load->view('fixed_assets/asset_pdf',array('asset'=>$asset,'company'=>$this->companyRow(),'qr'=>$this->qrData(base_url('fixedassets/register/'.$asset->kode_asset))),true),'Kartu-Aset-'.$asset->kode_asset.'.pdf');}
|
||||
public function opname_pdf($id){$this->requirePermission('asset','can_export');$opname=$this->fixedasset->opnameDetail((int)$id,$this->company());if(!$opname)show_404();$this->inlinePdf($this->load->view('fixed_assets/opname_pdf',array('opname'=>$opname,'company'=>$this->companyRow()),true),'Opname-'.$opname->opname_no.'.pdf','landscape');}
|
||||
public function qr($id){$asset=$this->fixedasset->detail((int)$id,$this->company());if(!$asset)show_404();$this->output->set_content_type('text/html')->set_output($this->load->view('fixed_assets/qr_label',array('asset'=>$asset,'qr'=>$this->qrData(base_url('fixedassets/register/'.$asset->kode_asset))),true));}
|
||||
public function register($code){$asset=$this->db->select('a.kode_asset,a.nama_asset,a.lifecycle_status,a.nilai_buku,l.nama location_name,e.full_name responsible_name')->from('assets a')->join('lokasi_asset l','l.id=a.lokasi_asset_id','left')->join('k_employees e','e.id=a.responsible_employee_id','left')->where(array('a.kode_asset'=>$code,'a.company_id'=>$this->company()))->get()->row();if(!$asset)return json_response(false,'Aset tidak ditemukan.',array(),404);return json_response(true,'Identitas aset terverifikasi.',array('asset'=>$asset));}
|
||||
|
||||
private function companyRow(){return$this->db->get_where('companies',array('id'=>$this->company()))->row();}
|
||||
private function inlinePdf($html,$filename,$orientation='portrait'){require_once FCPATH.'vendor/autoload.php';$pdf=new Dompdf\Dompdf();$pdf->loadHtml($html);$pdf->setPaper('A4',$orientation);$pdf->render();$pdf->stream($filename,array('Attachment'=>false));}
|
||||
private function qrData($text){require_once APPPATH.'libraries/phpqrcode/qrlib.php';ob_start();QRcode::png($text,null,QR_ECLEVEL_M,5,1);$png=ob_get_clean();return'data:image/png;base64,'.base64_encode($png);}
|
||||
private function qty($value){$number=(float)$value;return number_format($number,abs($number-round($number))<.0001?0:4,',','.');}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
|
||||
class Generatebuku extends CI_Controller
|
||||
class Generatebuku extends MY_Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
@@ -45,6 +45,7 @@ class Generatebuku extends CI_Controller
|
||||
$this->db->from('journal_details');
|
||||
$this->db->join('journals', 'journals.id = journal_details.journal_id');
|
||||
$this->db->join('accounts', 'accounts.id = journal_details.account_id');
|
||||
$this->db->where_in('journals.status', array('posted', 'reversed'));
|
||||
|
||||
if (!empty($account_id)) {
|
||||
$this->db->where('journal_details.account_id', (int) $account_id);
|
||||
@@ -135,4 +136,4 @@ class Generatebuku extends CI_Controller
|
||||
$pdf->Output('D', $filename);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
|
||||
class Generateinvoice extends CI_Controller {
|
||||
class Generateinvoice extends MY_Controller {
|
||||
|
||||
public function generate($invoice_id)
|
||||
{
|
||||
@@ -190,4 +190,4 @@ class Generateinvoice extends CI_Controller {
|
||||
return "";
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
|
||||
class Generateitems extends CI_Controller {
|
||||
class Generateitems extends MY_Controller {
|
||||
|
||||
|
||||
public function __construct()
|
||||
@@ -65,4 +65,4 @@ $pdf->TotalBarang(count($data));
|
||||
// $pdf->Pembuat($company['nama_bendahara']);
|
||||
$pdf->Output('I','Data_Barang_'.date('YmdHis').'.pdf');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
|
||||
class Generatelabarugi extends CI_Controller {
|
||||
class Generatelabarugi extends MY_Admin_Controller {
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
@@ -43,6 +43,7 @@ class Generatelabarugi extends CI_Controller {
|
||||
// ================= JOIN
|
||||
$this->db->join('journal_details jd','a.id = jd.account_id','LEFT');
|
||||
$this->db->join('journals j','j.id = jd.journal_id','LEFT');
|
||||
$this->db->where("(j.status IN ('posted','reversed') OR jd.id IS NULL)", null, false);
|
||||
|
||||
// ================= FILTER AKUN
|
||||
$this->db->where('a.kategori','laba_rugi');
|
||||
@@ -194,4 +195,4 @@ class Generatelabarugi extends CI_Controller {
|
||||
$pdf->Output('D', $filename);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
|
||||
class Generateneraca extends CI_Controller {
|
||||
class Generateneraca extends MY_Admin_Controller {
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
@@ -39,6 +39,7 @@ class Generateneraca extends CI_Controller {
|
||||
$this->db->from('accounts');
|
||||
$this->db->join('journal_details jd','jd.account_id = accounts.id','LEFT');
|
||||
$this->db->join('journals j','j.id = jd.journal_id','LEFT');
|
||||
$this->db->where("(j.status IN ('posted','reversed') OR jd.id IS NULL)", null, false);
|
||||
|
||||
$this->db->where_in('accounts.tipe',['revenue','expense']);
|
||||
|
||||
@@ -85,6 +86,7 @@ class Generateneraca extends CI_Controller {
|
||||
$this->db->from('accounts a');
|
||||
$this->db->join('journal_details jd','jd.account_id = a.id','LEFT');
|
||||
$this->db->join('journals j','j.id = jd.journal_id','LEFT');
|
||||
$this->db->where("(j.status IN ('posted','reversed') OR jd.id IS NULL)", null, false);
|
||||
|
||||
$this->db->where('a.kategori','neraca');
|
||||
|
||||
@@ -126,7 +128,7 @@ class Generateneraca extends CI_Controller {
|
||||
}
|
||||
|
||||
// ================= LABA BERJALAN
|
||||
if ($r->kode_akun == '302') {
|
||||
if ($r->kode_akun == '3020') {
|
||||
$saldo = $laba;
|
||||
}
|
||||
|
||||
@@ -249,4 +251,4 @@ class Generateneraca extends CI_Controller {
|
||||
$pdf->Output('D', $filename);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
|
||||
class Generateneracasaldo extends CI_Controller {
|
||||
class Generateneracasaldo extends MY_Admin_Controller {
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
@@ -56,7 +56,8 @@ class Generateneracasaldo extends CI_Controller {
|
||||
"(SELECT jd.*
|
||||
FROM journal_details jd
|
||||
JOIN journals j ON j.id = jd.journal_id
|
||||
WHERE j.tanggal BETWEEN ".$this->db->escape($tanggal_awal)."
|
||||
WHERE j.status IN ('posted','reversed')
|
||||
AND j.tanggal BETWEEN ".$this->db->escape($tanggal_awal)."
|
||||
AND ".$this->db->escape($tanggal_akhir)."
|
||||
) journal_details",
|
||||
"journal_details.account_id = accounts.id",
|
||||
@@ -185,4 +186,4 @@ class Generateneracasaldo extends CI_Controller {
|
||||
$pdf->Output('I',$filename);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
|
||||
class Generateschedule extends CI_Controller {
|
||||
class Generateschedule extends MY_Controller {
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
@@ -765,4 +765,4 @@ public function delete_schedule()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
|
||||
class Holidays extends CI_Controller {
|
||||
class Holidays extends MY_Controller {
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
@@ -174,4 +174,4 @@ class Holidays extends CI_Controller {
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
class Hrpayroll extends MY_Controller
|
||||
{
|
||||
public function __construct(){parent::__construct();$this->requirePermission('employees');$this->load->database();$this->load->library('PayrollService');}
|
||||
private function uid(){return(int)$this->session->userdata('user_id');}private function go($ok,$m){$this->session->set_flashdata($ok?'success':'error',$m);redirect('hrpayroll');}
|
||||
public function index(){$d=array('active_menu'=>'hr_payroll');$d['periods']=$this->db->select('p.*,COUNT(x.id) employee_count,COALESCE(SUM(x.gross_salary),0) gross,COALESCE(SUM(x.total_deductions),0) deductions,COALESCE(SUM(x.total_salary),0) net')->from('k_payroll_periods p')->join('k_payrolls x','x.payroll_period_id=p.id','left')->group_by('p.id')->order_by('p.start_date','DESC')->get()->result();$d['components']=$this->db->order_by('code')->get('payroll_components')->result();$d['locks']=$this->db->order_by('period','DESC')->get('attendance_period_locks')->result();$d['cash_accounts']=$this->db->where('is_active',1)->get('cash_accounts')->result();$this->load->view('partials/header',$d);$this->load->view('hr_payroll/index',$d);$this->load->view('partials/footer');}
|
||||
public function lock_attendance(){try{$period=$this->input->post('period');if($this->db->get_where('attendance_period_locks',array('period'=>$period,'branch_id'=>null))->row())$this->db->where('period',$period)->where('branch_id IS NULL',null,false)->update('attendance_period_locks',array('status'=>'locked','locked_by'=>$this->uid(),'locked_at'=>date('Y-m-d H:i:s'),'notes'=>$this->input->post('notes',true)));else$this->db->insert('attendance_period_locks',array('period'=>$period,'status'=>'locked','locked_by'=>$this->uid(),'locked_at'=>date('Y-m-d H:i:s'),'notes'=>$this->input->post('notes',true)));$this->go(true,'Absensi '.$period.' dikunci.');}catch(Throwable$e){$this->go(false,$e->getMessage());}}
|
||||
public function calculate(){try{$id=$this->payrollservice->calculate($this->input->post('month'),$this->uid());$this->go(true,'Draft payroll dihitung. Periode #'.$id);}catch(Throwable$e){$this->go(false,$e->getMessage());}}
|
||||
public function workflow($id,$action){try{$this->payrollservice->workflow($id,$action,$this->uid());$this->go(true,'Workflow payroll berhasil: '.$action);}catch(Throwable$e){$this->go(false,$e->getMessage());}}
|
||||
public function post($id){try{$this->payrollservice->post($id,$this->input->post('date'),$this->uid());$this->go(true,'Payroll berhasil diposting ke jurnal.');}catch(Throwable$e){$this->go(false,$e->getMessage());}}
|
||||
public function pay($id){try{$this->payrollservice->pay($id,$this->input->post('date'),(int)$this->input->post('cash_account_id'),$this->uid());$this->go(true,'Payroll berhasil dibayar dan jurnal pembayaran diposting.');}catch(Throwable$e){$this->go(false,$e->getMessage());}}
|
||||
public function component(){try{$id=(int)$this->input->post('id');$data=array('code'=>strtoupper(trim($this->input->post('code',true))),'name'=>$this->input->post('name',true),'component_type'=>$this->input->post('component_type'),'category'=>$this->input->post('category'),'formula_expression'=>$this->input->post('formula_expression',true)?:null,'fixed_amount'=>$this->input->post('fixed_amount')!==''?(float)$this->input->post('fixed_amount'):null,'taxable'=>$this->input->post('taxable')?1:0,'prorated'=>$this->input->post('prorated')?1:0,'expense_account_id'=>$this->input->post('expense_account_id')?:null,'liability_account_id'=>$this->input->post('liability_account_id')?:null,'is_active'=>1);if($id)$this->db->where('id',$id)->update('payroll_components',$data);else$this->db->insert('payroll_components',$data);$this->go(true,'Komponen payroll disimpan.');}catch(Throwable$e){$this->go(false,$e->getMessage());}}
|
||||
public function slip($payrollId){$p=$this->db->select('x.*,e.full_name,e.employee_code,r.period_name,r.workflow_status')->from('k_payrolls x')->join('k_employees e','e.id=x.employee_id')->join('k_payroll_periods r','r.id=x.payroll_period_id')->where('x.id',$payrollId)->get()->row();if(!$p||!in_array($p->workflow_status,array('final','posted','paid'),true))show_error('Slip belum tersedia.',422);$lines=$this->db->where('payroll_id',$payrollId)->get('payroll_calculation_lines')->result();$this->load->view('hr_payroll/slip',array('payroll'=>$p,'lines'=>$lines));}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
class Inventorycheck extends CI_Controller
|
||||
{
|
||||
public function __construct(){parent::__construct();if(!is_cli())show_404();}
|
||||
public function integrity(){
|
||||
$checks=array(
|
||||
'duplicate_barcodes'=>"SELECT COUNT(*) total FROM(SELECT barcode FROM item_barcodes GROUP BY barcode HAVING COUNT(*)>1)x",
|
||||
'duplicate_ledger_idempotency'=>"SELECT COUNT(*) total FROM(SELECT idempotency_key FROM inventory_ledger GROUP BY idempotency_key HAVING COUNT(*)>1)x",
|
||||
'negative_ledger_stock'=>"SELECT COUNT(*) total FROM(SELECT item_id,warehouse_id,SUM(IF(direction='in',qty,-qty)) qty FROM inventory_ledger GROUP BY item_id,warehouse_id HAVING qty<-.0001)x",
|
||||
'ledger_vs_legacy_qty'=>"SELECT COUNT(*) total FROM(SELECT l.item_id,l.warehouse_id,l.qty-COALESCE(s.qty,0) diff FROM(SELECT item_id,warehouse_id,SUM(IF(direction='in',qty,-qty)) qty FROM inventory_ledger GROUP BY item_id,warehouse_id)l LEFT JOIN(SELECT item_id,COALESCE(warehouse_id,0) warehouse_id,SUM(IF(tipe='masuk',qty,-qty)) qty FROM stock_logs GROUP BY item_id,COALESCE(warehouse_id,0))s ON s.item_id=l.item_id AND s.warehouse_id=l.warehouse_id HAVING ABS(diff)>.0001)x",
|
||||
'item_cache_vs_ledger'=>"SELECT COUNT(*) total FROM(SELECT i.id,i.stok-COALESCE(l.qty,0) diff FROM items i LEFT JOIN(SELECT item_id,SUM(IF(direction='in',qty,-qty)) qty FROM inventory_ledger GROUP BY item_id)l ON l.item_id=i.id HAVING ABS(diff)>.0001)x",
|
||||
'barcode_reserved_over_qty'=>"SELECT COUNT(*) total FROM item_barcodes WHERE reserved_qty>qty_sisa+.0001",
|
||||
'over_released_reservation'=>"SELECT COUNT(*) total FROM stock_reservations WHERE released_qty>qty+.0001",
|
||||
'duplicate_active_technician_barcode'=>"SELECT COUNT(*) total FROM(SELECT barcode FROM item_technician WHERE status='active' GROUP BY barcode HAVING COUNT(*)>1)x",
|
||||
'technician_barcode_not_installed'=>"SELECT COUNT(*) total FROM item_technician it JOIN item_barcodes ib ON ib.barcode=it.barcode WHERE it.status='active' AND ib.status<>'installed'",
|
||||
'cross_company_technician_item'=>"SELECT COUNT(*) total FROM item_technician it JOIN items i ON i.id=it.item_id JOIN k_employees e ON e.id=it.user_id WHERE it.status='active' AND i.company_id<>e.company_id",
|
||||
'posted_doc_without_ledger'=>"SELECT COUNT(*) total FROM stock_documents d LEFT JOIN inventory_ledger l ON l.document_id=d.id AND l.document_type=d.document_type WHERE d.status='posted' AND l.id IS NULL AND NOT(d.document_type='opname' AND NOT EXISTS(SELECT 1 FROM stock_document_lines x WHERE x.stock_document_id=d.id AND ABS(x.counted_qty-x.system_qty)>.0001))"
|
||||
);$fail=0;foreach($checks as$n=>$q){$v=(int)$this->db->query($q)->row()->total;echo($v?'[FAIL] ':'[OK] ').$n.'='.$v."\n";if($v)$fail++;}if($fail)exit(1);
|
||||
}
|
||||
public function reconciliation(){$ledger=(float)$this->db->query("SELECT COALESCE(SUM(IF(direction='in',value,-value)),0) total FROM inventory_ledger")->row()->total;$m=$this->db->get_where('system_account_mappings',array('mapping_key'=>'inventory','is_active'=>1))->row();$gl=(float)$this->db->query("SELECT COALESCE(SUM(d.debit-d.kredit),0) total FROM journal_details d JOIN journals j ON j.id=d.journal_id WHERE d.account_id=? AND j.status IN('posted','reversed')",array($m->account_id))->row()->total;echo'ledger_value='.number_format($ledger,2,'.','')."\n".'gl_value='.number_format($gl,2,'.','')."\n".'difference='.number_format($ledger-$gl,2,'.','')."\n";if(abs($ledger-$gl)>.01)exit(1);}
|
||||
public function trigger_test(){$row=$this->db->query("SELECT item_id,warehouse_id,SUM(IF(direction='in',qty,-qty)) qty FROM inventory_ledger GROUP BY item_id,warehouse_id HAVING qty>=0 LIMIT 1")->row();if(!$row){echo"[SKIP] no stock row\n";return;}$this->db->trans_begin();$old=$this->db->db_debug;$this->db->db_debug=false;$ok=$this->db->insert('stock_logs',array('item_id'=>$row->item_id,'warehouse_id'=>$row->warehouse_id,'qty'=>$row->qty+999999,'tipe'=>'keluar','keterangan'=>'trigger test','ref_type'=>'test','idempotency_key'=>'TEST-NEGATIVE-'.uniqid()));$this->db->db_debug=$old;$this->db->trans_rollback();echo(!$ok?'[OK] ':'[FAIL] ')."negative_stock_trigger\n";if($ok)exit(1);}
|
||||
public function professional(){
|
||||
$company=$this->db->select_min('id')->get('companies')->row();if(!$company){fwrite(STDERR,"Company development tidak tersedia.\n");exit(1);}$company=(int)$company->id;$this->load->model('InventoryModel','inventory');
|
||||
$required=array('stock_documents'=>array('company_id','source_type','submitted_by','reversal_of_id'),'stock_document_lines'=>array('line_no','movement_direction'),'stock_reservations'=>array('company_id','completed_at'),'stock_logs'=>array('company_id','unit_cost','idempotency_key'),'warehouses'=>array('code','is_active'),'kode_barang'=>array('company_id','is_active'));
|
||||
$missing=array();foreach($required as$table=>$columns)foreach($columns as$column)if(!$this->db->field_exists($column,$table))$missing[]=$table.'.'.$column;
|
||||
$orphans=(int)$this->db->query("SELECT COUNT(*) total FROM stock_document_lines l LEFT JOIN stock_documents d ON d.id=l.stock_document_id LEFT JOIN items i ON i.id=l.item_id WHERE d.id IS NULL OR i.id IS NULL")->row()->total;
|
||||
$wrongCompany=(int)$this->db->query("SELECT COUNT(DISTINCT d.id) total FROM stock_documents d JOIN stock_document_lines l ON l.stock_document_id=d.id JOIN items i ON i.id=l.item_id WHERE d.company_id<>i.company_id")->row()->total;
|
||||
$duplicateNumbers=(int)$this->db->query("SELECT COUNT(*) total FROM(SELECT company_id,document_no FROM stock_documents GROUP BY company_id,document_no HAVING COUNT(*)>1)x")->row()->total;
|
||||
$dashboard=$this->inventory->dashboard($company);$valuation=$this->inventory->valuation($company);$reconciliation=$this->inventory->reconciliation($company,date('Y-m-d'));
|
||||
$ok=!$missing&&$orphans===0&&$wrongCompany===0&&$duplicateNumbers===0;$payload=array('status'=>$ok?'ok':'failed','checks'=>array('missing_columns'=>$missing,'orphan_lines'=>$orphans,'cross_company_documents'=>$wrongCompany,'duplicate_document_numbers'=>$duplicateNumbers),'dashboard'=>$dashboard,'valuation_rows'=>count($valuation),'reconciliation'=>$reconciliation);echo json_encode($payload,JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE)."\n";if(!$ok)exit(1);
|
||||
}
|
||||
public function anomalies(){
|
||||
$barcode=$this->db->query("SELECT l.item_id,i.kode_detail,i.nama_barang,l.warehouse_id,l.qty,COALESCE(b.qty,0) barcode_qty,l.qty-COALESCE(b.qty,0) difference FROM(SELECT item_id,warehouse_id,SUM(IF(direction='in',qty,-qty)) qty FROM inventory_ledger GROUP BY item_id,warehouse_id)l JOIN items i ON i.id=l.item_id LEFT JOIN(SELECT item_id,warehouse_id,SUM(qty_sisa) qty FROM item_barcodes GROUP BY item_id,warehouse_id)b ON b.item_id=l.item_id AND b.warehouse_id=l.warehouse_id HAVING ABS(difference)>.0001 ORDER BY ABS(difference) DESC")->result();
|
||||
$technician=$this->db->query("SELECT it.barcode,it.item_id,i.kode_detail,i.nama_barang,it.user_id,e.full_name technician_name,ib.status barcode_status,ib.qty_sisa FROM item_technician it JOIN item_barcodes ib ON ib.barcode=it.barcode JOIN items i ON i.id=it.item_id LEFT JOIN k_employees e ON e.id=it.user_id WHERE it.status='active' AND ib.status<>'installed'")->result();
|
||||
$payload=array('items_without_company'=>(int)$this->db->where('company_id IS NULL',null,false)->count_all_results('items'),'stock_logs_without_cost'=>(int)$this->db->group_start()->where('unit_cost IS NULL',null,false)->or_where('unit_cost',0)->group_end()->count_all_results('stock_logs'),'barcode_variances'=>$barcode,'technician_status_variances'=>$technician);echo json_encode($payload,JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE)."\n";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
|
||||
class Inventoryprofessional extends MY_Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->load->library(array('ProfessionalInventoryService','CompanyContext'));
|
||||
$this->load->model('InventoryModel','inventory');
|
||||
}
|
||||
|
||||
private function uid(){return(int)$this->session->userdata('user_id');}
|
||||
private function company(){return(int)$this->companycontext->id();}
|
||||
private function roleId(){$role=(int)$this->session->userdata('role_id');if($role)return$role;$user=$this->db->select('role_id')->get_where('users',array('id'=>$this->uid()))->row();return$user?(int)$user->role_id:0;}
|
||||
private function can($action){return$this->session->userdata('role')==='Admin'||check_permission('items',$action);}
|
||||
private function page($view,$active,array$data=array()){$data['active_menu']=$active;$data['inventory_permissions']=$this->permissionMap();$this->load->view('partials/header',$data);$this->load->view($view,$data);$this->load->view('partials/footer');}
|
||||
private function permissionMap(){return array('create'=>$this->can('can_create'),'update'=>$this->can('can_update'),'approve'=>$this->can('can_approve'),'post'=>$this->can('can_post'),'export'=>$this->can('can_export')) ;}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$company=$this->company();$this->professionalinventoryservice->expireReservations();
|
||||
$this->page('inventory_professional/dashboard','inventory_overview',array('summary'=>$this->inventory->dashboard($company),'action_items'=>$this->inventory->actionItems($company),'documents'=>$this->inventory->recentDocuments($company),'last_reconciliation'=>$this->db->where('company_id',$company)->order_by('id','DESC')->get('inventory_reconciliations')->row()));
|
||||
}
|
||||
|
||||
public function operations()
|
||||
{
|
||||
$company=$this->company();$warehouses=$this->db->where(array('company_id'=>$company,'is_active'=>1))->order_by('nama')->get('warehouses')->result();
|
||||
$this->page('inventory_professional/operations','inventory_operations',array('warehouses'=>$warehouses));
|
||||
}
|
||||
|
||||
public function locations()
|
||||
{
|
||||
$this->page('inventory_professional/locations','inventory_locations');
|
||||
}
|
||||
|
||||
public function reports()
|
||||
{
|
||||
$warehouses=$this->db->where(array('company_id'=>$this->company(),'is_active'=>1))->order_by('nama')->get('warehouses')->result();
|
||||
$this->page('inventory_professional/reports','inventory_reports',array('warehouses'=>$warehouses));
|
||||
}
|
||||
|
||||
public function document_data()
|
||||
{
|
||||
$company=$this->company();$draw=(int)$this->input->get('draw');$start=max(0,(int)$this->input->get('start'));$length=min(100,max(10,(int)$this->input->get('length')));$search=trim((string)($this->input->get('search')['value']??''));$type=trim((string)$this->input->get('type'));$status=trim((string)$this->input->get('status'));
|
||||
$base=$this->db->from('stock_documents d')->where('d.company_id',$company);if($type==='manual')$base->where_in('d.document_type',array('opening','in','out'));elseif($type==='damage')$base->where_in('d.document_type',array('damaged','lost'));elseif($type!=='')$base->where('d.document_type',$type);if($status!=='')$base->where('d.status',$status);if($search!=='')$base->group_start()->like('d.document_no',$search)->or_like('d.reason',$search)->or_like('d.source_type',$search)->group_end();$filtered=$base->count_all_results('',false);
|
||||
$rows=$base->select('d.*,wf.nama from_warehouse,wt.nama to_warehouse,u.nama creator_name,(SELECT COUNT(*) FROM stock_document_lines l WHERE l.stock_document_id=d.id) line_count')->join('warehouses wf','wf.id=d.from_warehouse_id','left')->join('warehouses wt','wt.id=d.to_warehouse_id','left')->join('users u','u.id=d.created_by','left')->order_by('d.id','DESC')->limit($length,$start)->get()->result();
|
||||
$total=$this->db->where('company_id',$company)->count_all_results('stock_documents');return$this->output->set_content_type('application/json')->set_output(json_encode(array('draw'=>$draw,'recordsTotal'=>$total,'recordsFiltered'=>$filtered,'data'=>$rows)));
|
||||
}
|
||||
|
||||
public function document_detail($id)
|
||||
{
|
||||
$doc=$this->inventory->documentDetail((int)$id,$this->company());if(!$doc)return json_response(false,'Dokumen stok tidak ditemukan.',array(),404);
|
||||
foreach($doc->attachments as$a){$a->preview_url=base_url('accountingattachments/preview/'.$a->id);$a->download_url=base_url('accountingattachments/download/'.$a->id);}
|
||||
return json_response(true,'Detail dokumen stok.',array('document'=>$doc,'permissions'=>$this->permissionMap()));
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
if(strtoupper($this->input->method())!=='POST')show_404();
|
||||
$this->requirePermission('items','can_create');$lines=json_decode((string)$this->input->post('lines'),true);if(!is_array($lines))return json_response(false,'Detail barang tidak valid.',array(),422);
|
||||
try{$id=$this->professionalinventoryservice->createDocument(array('type'=>$this->input->post('type',true),'date'=>$this->input->post('date',true),'from_warehouse_id'=>$this->input->post('from_warehouse_id'),'to_warehouse_id'=>$this->input->post('to_warehouse_id'),'reason'=>$this->input->post('reason',true),'source_type'=>$this->input->post('source_type',true),'source_id'=>$this->input->post('source_id'),'idempotency_key'=>$this->input->post('idempotency_key',true),'lines'=>$lines),$this->uid());return json_response(true,'Dokumen stok berhasil disimpan sebagai draft.',array('document_id'=>$id));}catch(Throwable$e){return business_exception_response($e);}
|
||||
}
|
||||
|
||||
public function submit($id){$this->requirePermission('items','can_create');return$this->action(fn()=>$this->professionalinventoryservice->submit($id,$this->uid()),'Dokumen stok berhasil diajukan.');}
|
||||
public function approve($id){$this->requirePermission('items','can_approve');return$this->action(fn()=>$this->professionalinventoryservice->approve($id,$this->uid(),$this->roleId()),'Dokumen stok berhasil disetujui.');}
|
||||
public function reject($id){$this->requirePermission('items','can_approve');return$this->action(fn()=>$this->professionalinventoryservice->reject($id,$this->uid(),$this->roleId(),$this->input->post('reason',true)),'Dokumen stok ditolak.');}
|
||||
public function post($id){$this->requirePermission('items','can_post');return$this->action(fn()=>$this->professionalinventoryservice->post($id,$this->uid()),'Mutasi stok berhasil diposting.');}
|
||||
public function reverse($id){$this->requirePermission('items','can_post');return$this->action(fn()=>$this->professionalinventoryservice->reverse($id,$this->input->post('date',true),$this->input->post('reason',true),$this->uid()),'Reversal stok berhasil diposting.');}
|
||||
|
||||
public function lookup_items()
|
||||
{
|
||||
$q=trim((string)$this->input->get('q'));$warehouse=(int)$this->input->get('warehouse_id');$this->db->select("i.id,CONCAT(i.kode_detail,' - ',i.nama_barang) text,i.nama_barang,i.kode_detail,i.harga_beli,i.allow_decimal,k.tracking_type,k.need_serial_number,COALESCE(s.qty,0) on_hand,COALESCE(r.qty,0) reserved",false)->from('items i')->join('kode_barang k','k.id=i.kode_id','left')->join("(SELECT item_id,warehouse_id,SUM(IF(direction='in',qty,-qty)) qty FROM inventory_ledger GROUP BY item_id,warehouse_id)s",'s.item_id=i.id'.($warehouse?' AND s.warehouse_id='.(int)$warehouse:''),'left')->join("(SELECT item_id,warehouse_id,SUM(qty-released_qty) qty FROM stock_reservations WHERE status IN('active','partially_released') GROUP BY item_id,warehouse_id)r",'r.item_id=i.id'.($warehouse?' AND r.warehouse_id='.(int)$warehouse:''),'left')->where(array('i.company_id'=>$this->company(),'i.status'=>'active'));if($q!=='')$this->db->group_start()->like('i.kode_detail',$q)->or_like('i.nama_barang',$q)->group_end();$rows=$this->db->group_by('i.id')->order_by('i.nama_barang')->limit(25)->get()->result();return json_response(true,'Daftar barang.',array('results'=>$rows));
|
||||
}
|
||||
|
||||
public function lookup_barcodes()
|
||||
{
|
||||
$item=(int)$this->input->get('item_id');$warehouse=(int)$this->input->get('warehouse_id');$q=trim((string)$this->input->get('q'));$this->assertItem($item);
|
||||
$this->db->select("b.id,b.barcode,b.serial_number,b.qty_sisa,b.reserved_qty,b.return_reserved_qty,b.status,CONCAT(b.barcode,IF(b.serial_number IS NULL OR b.serial_number='','',CONCAT(' / SN ',b.serial_number)),' - tersedia ',FORMAT(GREATEST(b.qty_sisa-b.reserved_qty-b.return_reserved_qty,0),2,'id_ID')) text",false)->from('item_barcodes b')->join('items i','i.id=b.item_id')->where(array('b.item_id'=>$item,'i.company_id'=>$this->company()))->where_in('b.status',array('available','reserved'))->where('b.qty_sisa >',0)->where('(b.qty_sisa-COALESCE(b.reserved_qty,0)-COALESCE(b.return_reserved_qty,0)) > 0',null,false);
|
||||
if($warehouse)$this->db->where('b.warehouse_id',$warehouse);if($q!=='')$this->db->group_start()->like('b.barcode',$q)->or_like('b.serial_number',$q)->group_end();$rows=$this->db->order_by('b.id')->limit(30)->get()->result();return json_response(true,'Daftar barcode.',array('results'=>$rows));
|
||||
}
|
||||
|
||||
public function lookup_batches()
|
||||
{
|
||||
$item=(int)$this->input->get('item_id');$q=trim((string)$this->input->get('q'));$this->db->select("id,CONCAT(batch_no,IF(expiry_date IS NULL,'',CONCAT(' · exp ',DATE_FORMAT(expiry_date,'%d/%m/%Y')))) text,batch_no,expiry_date",false)->where(array('company_id'=>$this->company(),'item_id'=>$item,'is_active'=>1));if($q!=='')$this->db->like('batch_no',$q);$rows=$this->db->order_by('id','DESC')->limit(25)->get('inventory_batches')->result();return json_response(true,'Daftar batch.',array('results'=>$rows));
|
||||
}
|
||||
|
||||
public function scan()
|
||||
{
|
||||
$code=trim((string)$this->input->get('code'));
|
||||
if($code==='')return json_response(false,'Barcode atau serial wajib diisi.',array(),422);
|
||||
try{
|
||||
$select='b.*,i.kode_detail,i.nama_barang,k.unit,w.nama warehouse_name';
|
||||
$this->db->from('item_barcodes b')->join('items i','i.id=b.item_id')->join('kode_barang k','k.id=i.kode_id','left')->join('warehouses w','w.id=b.warehouse_id','left');
|
||||
if($this->db->table_exists('warehouse_bins')&&$this->db->field_exists('bin_id','item_barcodes')){$select.=',wb.code bin_code';$this->db->join('warehouse_bins wb','wb.id=b.bin_id','left');}else$select.=',NULL bin_code';
|
||||
if($this->db->table_exists('inventory_batches')&&$this->db->field_exists('batch_id','item_barcodes')){$select.=',bt.batch_no';$this->db->join('inventory_batches bt','bt.id=b.batch_id','left');}else$select.=',NULL batch_no';
|
||||
$row=$this->db->select($select,false)->where('i.company_id',$this->company())->group_start()->where('b.barcode',$code)->or_where('b.serial_number',$code)->group_end()->limit(1)->get()->row();
|
||||
if(!$row)return json_response(false,'Barcode atau serial tidak ditemukan pada perusahaan aktif.',array(),404);
|
||||
|
||||
$row->reserved_qty=(float)($row->reserved_qty??0);$row->return_reserved_qty=(float)($row->return_reserved_qty??0);$row->qty_sisa=(float)($row->qty_sisa??0);$row->history=array();
|
||||
if($this->db->table_exists('item_movements')&&$this->db->field_exists('barcode_id','item_movements')){
|
||||
$type=$this->db->field_exists('movement_type','item_movements')?'m.movement_type':"'Perpindahan'";
|
||||
$notes=$this->db->field_exists('notes','item_movements')?'m.notes':"''";
|
||||
$created=$this->db->field_exists('created_at','item_movements')?'m.created_at':'NULL';
|
||||
$row->history=$this->db->select($type.' movement_type,'.$notes.' notes,'.$created.' created_at',false)->from('item_movements m')->join('items mi','mi.id=m.item_id')->where(array('m.barcode_id'=>$row->id,'mi.company_id'=>$this->company()))->order_by('m.id','DESC')->limit(20)->get()->result();
|
||||
}
|
||||
if(!$row->history&&$this->db->table_exists('stock_logs')&&$this->db->field_exists('barcode_id','stock_logs')){
|
||||
$type=$this->db->field_exists('tipe','stock_logs')?'s.tipe':"'Mutasi Stok'";
|
||||
$notes=$this->db->field_exists('keterangan','stock_logs')?'s.keterangan':"''";
|
||||
$created=$this->db->field_exists('created_at','stock_logs')?'s.created_at':($this->db->field_exists('tanggal','stock_logs')?'s.tanggal':'NULL');
|
||||
$row->history=$this->db->select($type.' movement_type,'.$notes.' notes,'.$created.' created_at',false)->from('stock_logs s')->join('items si','si.id=s.item_id')->where(array('s.barcode_id'=>$row->id,'si.company_id'=>$this->company()))->order_by('s.id','DESC')->limit(20)->get()->result();
|
||||
}
|
||||
return json_response(true,'Barcode ditemukan.',array('item'=>$row));
|
||||
}catch(Throwable$e){return business_exception_response($e);}
|
||||
}
|
||||
|
||||
public function reservation_data()
|
||||
{
|
||||
$rows=$this->db->select('r.*,i.kode_detail,i.nama_barang,w.nama warehouse_name,b.barcode,b.serial_number')->from('stock_reservations r')->join('items i','i.id=r.item_id')->join('warehouses w','w.id=r.warehouse_id')->join('item_barcodes b','b.id=r.barcode_id','left')->where('r.company_id',$this->company())->order_by('r.id','DESC')->limit(300)->get()->result();return json_response(true,'Daftar reservasi.',array('data'=>$rows));
|
||||
}
|
||||
|
||||
public function reserve(){if(strtoupper($this->input->method())!=='POST')show_404();$this->requirePermission('items','can_create');try{$id=$this->professionalinventoryservice->reserve($this->input->post('item_id'),$this->input->post('warehouse_id'),$this->input->post('barcode_id'),$this->input->post('qty'),$this->input->post('source_type',true),$this->input->post('source_id'),$this->input->post('expires_at'),$this->uid(),$this->input->post('notes',true));return json_response(true,'Stok berhasil direservasi.',array('reservation_id'=>$id));}catch(Throwable$e){return business_exception_response($e);}}
|
||||
public function update_reservation($id){$this->requirePermission('items','can_update');return$this->action(fn()=>$this->professionalinventoryservice->reservationAction($id,$this->input->post('action',true),$this->input->post('qty'),$this->input->post('notes',true),$this->uid()),'Reservasi berhasil diperbarui.');}
|
||||
|
||||
public function master_data()
|
||||
{
|
||||
$company=$this->company();$warehouses=$this->db->where('company_id',$company)->order_by('nama')->get('warehouses')->result();$bins=$this->db->select('b.*,w.nama warehouse_name')->from('warehouse_bins b')->join('warehouses w','w.id=b.warehouse_id')->where('b.company_id',$company)->order_by('w.nama')->order_by('b.code')->get()->result();$batches=$this->db->select('b.*,i.kode_detail,i.nama_barang,s.name supplier_name')->from('inventory_batches b')->join('items i','i.id=b.item_id')->join('suppliers s','s.id=b.supplier_id','left')->where('b.company_id',$company)->order_by('b.id','DESC')->limit(300)->get()->result();return json_response(true,'Master lokasi persediaan.',array('warehouses'=>$warehouses,'bins'=>$bins,'batches'=>$batches));
|
||||
}
|
||||
|
||||
public function save_warehouse(){return$this->masterAction(function(){$id=(int)$this->input->post('id');$data=array('company_id'=>$this->company(),'code'=>strtoupper(trim((string)$this->input->post('code',true))),'nama'=>trim((string)$this->input->post('name',true)),'address'=>trim((string)$this->input->post('address',true)),'is_active'=>$this->input->post('is_active')?1:0);if($data['code']===''||$data['nama']==='')throw new BusinessException('Kode dan nama gudang wajib diisi.');$q=$this->db->where(array('company_id'=>$data['company_id'],'code'=>$data['code']))->where('id !=',$id)->get('warehouses')->row();if($q)throw new BusinessException('Kode gudang sudah digunakan.');if($id){$this->db->where(array('id'=>$id,'company_id'=>$data['company_id']))->update('warehouses',$data);return$id;}$this->db->insert('warehouses',$data);return$this->db->insert_id();},'Gudang berhasil disimpan.');}
|
||||
public function save_bin(){return$this->masterAction(function(){$id=(int)$this->input->post('id');$warehouse=(int)$this->input->post('warehouse_id');$this->assertWarehouse($warehouse);$data=array('company_id'=>$this->company(),'warehouse_id'=>$warehouse,'code'=>strtoupper(trim((string)$this->input->post('code',true))),'name'=>trim((string)$this->input->post('name',true)),'is_active'=>$this->input->post('is_active')?1:0,'updated_at'=>date('Y-m-d H:i:s'));if($data['code']==='')throw new BusinessException('Kode rak/bin wajib diisi.');$q=$this->db->where(array('warehouse_id'=>$warehouse,'code'=>$data['code']))->where('id !=',$id)->get('warehouse_bins')->row();if($q)throw new BusinessException('Kode rak sudah digunakan di gudang ini.');if($id){$this->db->where(array('id'=>$id,'company_id'=>$data['company_id']))->update('warehouse_bins',$data);return$id;}$this->db->insert('warehouse_bins',$data);return$this->db->insert_id();},'Lokasi rak berhasil disimpan.');}
|
||||
public function save_batch(){return$this->masterAction(function(){$id=(int)$this->input->post('id');$item=(int)$this->input->post('item_id');$this->assertItem($item);$data=array('company_id'=>$this->company(),'item_id'=>$item,'batch_no'=>strtoupper(trim((string)$this->input->post('batch_no',true))),'manufactured_date'=>$this->input->post('manufactured_date')?:null,'expiry_date'=>$this->input->post('expiry_date')?:null,'supplier_id'=>(int)$this->input->post('supplier_id')?:null,'is_active'=>$this->input->post('is_active')?1:0,'updated_at'=>date('Y-m-d H:i:s'));if($data['batch_no']==='')throw new BusinessException('Nomor batch wajib diisi.');if($data['manufactured_date']&&$data['expiry_date']&&$data['expiry_date']<$data['manufactured_date'])throw new BusinessException('Tanggal kedaluwarsa tidak boleh sebelum tanggal produksi.');$q=$this->db->where(array('item_id'=>$item,'batch_no'=>$data['batch_no']))->where('id !=',$id)->get('inventory_batches')->row();if($q)throw new BusinessException('Nomor batch sudah digunakan untuk barang ini.');if($id){$this->db->where(array('id'=>$id,'company_id'=>$data['company_id']))->update('inventory_batches',$data);return$id;}$data['created_at']=date('Y-m-d H:i:s');$this->db->insert('inventory_batches',$data);return$this->db->insert_id();},'Batch berhasil disimpan.');}
|
||||
|
||||
public function report_data()
|
||||
{
|
||||
try{$type=$this->input->get('type')?:'valuation';$filters=array('warehouse_id'=>(int)$this->input->get('warehouse_id'),'item_id'=>(int)$this->input->get('item_id'),'date_from'=>$this->input->get('date_from')?:date('Y-m-01'),'date_to'=>$this->input->get('date_to')?:date('Y-m-d'),'days'=>max(30,(int)$this->input->get('days')));$rows=$this->buildReport($type,$filters);return json_response(true,'Laporan persediaan.',array('data'=>$rows));}catch(Throwable$e){return business_exception_response($e);}
|
||||
}
|
||||
|
||||
public function stock_card($item)
|
||||
{
|
||||
$this->assertItem((int)$item);$rows=$this->db->select('l.*,w.nama warehouse_name,d.document_no,d.reason')->from('inventory_ledger l')->join('items i','i.id=l.item_id')->join('warehouses w','w.id=l.warehouse_id','left')->join('stock_documents d','d.id=l.document_id AND d.document_type=l.document_type','left')->where(array('l.item_id'=>(int)$item,'i.company_id'=>$this->company()))->order_by('l.movement_date')->order_by('l.id')->get()->result();return json_response(true,'Kartu stok.',array('rows'=>$rows));
|
||||
}
|
||||
|
||||
public function reconciliation()
|
||||
{
|
||||
$this->requirePermission('items','can_post');$date=$this->input->post('date',true)?:date('Y-m-d');try{$result=$this->inventory->reconciliation($this->company(),$date);$this->db->insert('inventory_reconciliations',array('company_id'=>$this->company(),'as_of_date'=>$date,'ledger_value'=>$result['ledger_value'],'stock_log_value'=>$result['stock_log_value'],'gl_value'=>$result['gl_value'],'difference'=>$result['difference'],'barcode_variance_count'=>$result['barcode_variance_count'],'cache_variance_count'=>$result['cache_variance_count'],'status'=>$result['status'],'notes'=>$result['status']==='matched'?'Semua kontrol cocok':'Perlu tindak lanjut; tidak ada jurnal koreksi otomatis.','created_by'=>$this->uid(),'created_at'=>date('Y-m-d H:i:s')));return json_response(true,'Rekonsiliasi selesai tanpa membuat jurnal koreksi.',array('result'=>$result));}catch(Throwable$e){return business_exception_response($e);}
|
||||
}
|
||||
|
||||
public function document_pdf($id)
|
||||
{
|
||||
$this->requirePermission('items','can_export');$doc=$this->inventory->documentDetail((int)$id,$this->company());if(!$doc)show_404();$this->inlinePdf($this->load->view('inventory_professional/document_pdf',array('document'=>$doc),true),'Dokumen-Stok-'.$doc->document_no.'.pdf');
|
||||
}
|
||||
|
||||
public function report_pdf()
|
||||
{
|
||||
$this->requirePermission('items','can_export');$type=$this->input->get('type')?:'valuation';$filters=array('warehouse_id'=>(int)$this->input->get('warehouse_id'),'item_id'=>(int)$this->input->get('item_id'),'date_from'=>$this->input->get('date_from')?:date('Y-m-01'),'date_to'=>$this->input->get('date_to')?:date('Y-m-d'),'days'=>max(30,(int)$this->input->get('days')));$definition=$this->reportDefinition($type);$rows=$this->buildReport($type,$filters);$company=$this->db->get_where('companies',array('id'=>$this->company()))->row();$html=$this->load->view('inventory_professional/report_pdf',array('rows'=>$rows,'filters'=>$filters,'definition'=>$definition,'company'=>$company),true);$this->inlinePdf($html,'Laporan-Persediaan-'.$type.'-'.date('Ymd').'.pdf');
|
||||
}
|
||||
|
||||
public function report_excel()
|
||||
{
|
||||
$this->requirePermission('items','can_export');$type=$this->input->get('type')?:'valuation';$filters=array('warehouse_id'=>(int)$this->input->get('warehouse_id'),'item_id'=>(int)$this->input->get('item_id'),'date_from'=>$this->input->get('date_from')?:date('Y-m-01'),'date_to'=>$this->input->get('date_to')?:date('Y-m-d'),'days'=>max(30,(int)$this->input->get('days')));$definition=$this->reportDefinition($type);$rows=$this->buildReport($type,$filters);$company=$this->db->get_where('companies',array('id'=>$this->company()))->row();
|
||||
require_once FCPATH.'vendor/autoload.php';$book=new \PhpOffice\PhpSpreadsheet\Spreadsheet();$sheet=$book->getActiveSheet();$sheet->setTitle('Persediaan');$last=\PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex(max(1,count($definition['columns'])));$companyName=$company?($company->name??$company->company_name??'Perusahaan'):'Perusahaan';$sheet->setCellValue('A1',$companyName);$sheet->mergeCells('A1:'.$last.'1');$sheet->setCellValue('A2',$definition['title']);$sheet->mergeCells('A2:'.$last.'2');$sheet->setCellValue('A3','Periode: '.$filters['date_from'].' s.d. '.$filters['date_to']);$sheet->mergeCells('A3:'.$last.'3');$sheet->getStyle('A1:'.$last.'2')->getFont()->setBold(true);
|
||||
$column=1;foreach($definition['columns']as$label){$sheet->setCellValueByColumnAndRow($column++,5,$label);} $sheet->getStyle('A5:'.$last.'5')->getFont()->setBold(true);$rowNumber=6;foreach($rows as$row){$column=1;foreach($definition['columns']as$key=>$label){$value=is_array($row)?($row[$key]??null):($row->$key??null);$sheet->setCellValueByColumnAndRow($column++,$rowNumber,$value);} $rowNumber++;}for($column=1;$column<=count($definition['columns']);$column++)$sheet->getColumnDimension(\PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($column))->setAutoSize(true);$sheet->freezePane('A6');$sheet->setAutoFilter('A5:'.$last.max(5,$rowNumber-1));
|
||||
while(ob_get_level()>0)ob_end_clean();$filename='Laporan-Persediaan-'.$type.'-'.date('Ymd-His').'.xlsx';header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');header('Content-Disposition: attachment; filename="'.$filename.'"');header('Cache-Control: max-age=0');\PhpOffice\PhpSpreadsheet\IOFactory::createWriter($book,'Xlsx')->save('php://output');$book->disconnectWorksheets();exit;
|
||||
}
|
||||
|
||||
private function buildReport($type,array$filters)
|
||||
{
|
||||
$allowed=array_keys($this->reportDefinition());if(!in_array($type,$allowed,true))throw new BusinessException('Jenis laporan persediaan tidak valid.');$company=$this->company();$warehouse=(int)$filters['warehouse_id'];$item=(int)$filters['item_id'];$from=$filters['date_from'];$to=$filters['date_to'];if($from>$to)throw new BusinessException('Tanggal awal tidak boleh melewati tanggal akhir.');if($item)$this->assertItem($item);
|
||||
if($type==='valuation'){$params=array($to,$company);$where='';if($warehouse){$where.=' AND l.warehouse_id=?';$params[]=$warehouse;}if($item){$where.=' AND l.item_id=?';$params[]=$item;}return$this->db->query("SELECT i.id,i.kode_detail,i.nama_barang,w.nama warehouse_name,i.costing_method,i.reorder_point,SUM(IF(l.direction='in',l.qty,-l.qty)) qty,SUM(IF(l.direction='in',l.value,-l.value)) inventory_value FROM inventory_ledger l JOIN items i ON i.id=l.item_id JOIN warehouses w ON w.id=l.warehouse_id WHERE l.movement_date<=? AND i.company_id=? {$where} GROUP BY i.id,i.kode_detail,i.nama_barang,w.id,w.nama,i.costing_method,i.reorder_point ORDER BY i.nama_barang,w.nama",$params)->result();}
|
||||
if($type==='stock_card'){if(!$item)throw new BusinessException('Pilih barang untuk menampilkan kartu stok.');$this->db->select("l.*,w.nama warehouse_name,COALESCE(d.document_no,CONCAT(UPPER(REPLACE(l.document_type,'_',' ')),' #',l.document_id)) document_no,d.reason",false)->from('inventory_ledger l')->join('warehouses w','w.id=l.warehouse_id','left')->join('stock_documents d','d.id=l.document_id AND d.document_type=l.document_type','left')->where('l.item_id',$item)->where('l.movement_date >=',$from)->where('l.movement_date <=',$to);if($warehouse)$this->db->where('l.warehouse_id',$warehouse);return$this->db->order_by('l.movement_date')->order_by('l.id')->get()->result();}
|
||||
if($type==='location'){$params=array($to,$company);$where='';if($warehouse){$where.=' AND l.warehouse_id=?';$params[]=$warehouse;}if($item){$where.=' AND l.item_id=?';$params[]=$item;}return$this->db->query("SELECT i.kode_detail,i.nama_barang,w.nama warehouse_name,COALESCE(b.code,'Tanpa Rak') bin_code,SUM(IF(l.direction='in',l.qty,-l.qty)) qty FROM inventory_ledger l JOIN items i ON i.id=l.item_id JOIN warehouses w ON w.id=l.warehouse_id LEFT JOIN warehouse_bins b ON b.id=l.bin_id WHERE l.movement_date<=? AND i.company_id=? {$where} GROUP BY i.id,i.kode_detail,i.nama_barang,w.id,w.nama,b.id,b.code HAVING ABS(qty)>.0001 ORDER BY w.nama,bin_code,i.nama_barang",$params)->result();}
|
||||
if($type==='barcode'){$this->db->select('b.barcode,b.serial_number,b.status,b.qty_awal,b.qty_sisa,b.reserved_qty,b.return_reserved_qty,i.kode_detail,i.nama_barang,w.nama warehouse_name,wb.code bin_code,bt.batch_no')->from('item_barcodes b')->join('items i','i.id=b.item_id')->join('warehouses w','w.id=b.warehouse_id','left')->join('warehouse_bins wb','wb.id=b.bin_id','left')->join('inventory_batches bt','bt.id=b.batch_id','left')->where('i.company_id',$company);if($warehouse)$this->db->where('b.warehouse_id',$warehouse);if($item)$this->db->where('b.item_id',$item);return$this->db->order_by('b.id','DESC')->limit(5000)->get()->result();}
|
||||
if($type==='batch'){$this->db->select('b.batch_no,b.manufactured_date,b.expiry_date,b.is_active,i.kode_detail,i.nama_barang,w.nama warehouse_name,COALESCE(SUM(ib.qty_sisa),0) qty',false)->from('inventory_batches b')->join('items i','i.id=b.item_id')->join('item_barcodes ib','ib.batch_id=b.id','left')->join('warehouses w','w.id=ib.warehouse_id','left')->where('b.company_id',$company);if($warehouse)$this->db->where('ib.warehouse_id',$warehouse);if($item)$this->db->where('b.item_id',$item);return$this->db->group_by(array('b.id','b.batch_no','b.manufactured_date','b.expiry_date','b.is_active','i.kode_detail','i.nama_barang','w.id','w.nama'))->order_by('b.expiry_date')->get()->result();}
|
||||
if($type==='reservation'){$this->db->select('r.reservation_no,r.source_type,r.source_id,r.qty,r.released_qty,r.expires_at,r.status,i.kode_detail,i.nama_barang,w.nama warehouse_name,b.barcode,b.serial_number')->from('stock_reservations r')->join('items i','i.id=r.item_id')->join('warehouses w','w.id=r.warehouse_id')->join('item_barcodes b','b.id=r.barcode_id','left')->where('r.company_id',$company);if($warehouse)$this->db->where('r.warehouse_id',$warehouse);if($item)$this->db->where('r.item_id',$item);return$this->db->order_by('r.id','DESC')->get()->result();}
|
||||
if($type==='reorder'){$params=array($to,$company);$where='';if($warehouse){$where.=' AND l.warehouse_id=?';$params[]=$warehouse;}if($item){$where.=' AND l.item_id=?';$params[]=$item;}return$this->db->query("SELECT i.kode_detail,i.nama_barang,w.nama warehouse_name,SUM(IF(l.direction='in',l.qty,-l.qty)) qty,i.reorder_point,i.minimum_stock FROM items i JOIN inventory_ledger l ON l.item_id=i.id JOIN warehouses w ON w.id=l.warehouse_id WHERE l.movement_date<=? AND i.company_id=? {$where} GROUP BY i.id,i.kode_detail,i.nama_barang,w.id,w.nama,i.reorder_point,i.minimum_stock HAVING qty<=i.reorder_point ORDER BY qty",$params)->result();}
|
||||
if($type==='slow'){$params=array($company,(int)$filters['days']);$where='';if($item){$where.=' AND i.id=?';$params[]=$item;}return$this->db->query("SELECT i.kode_detail,i.nama_barang,i.last_movement_at,DATEDIFF(CURDATE(),COALESCE(DATE(i.last_movement_at),DATE(i.created_at))) idle_days,i.stok FROM items i WHERE i.company_id=? AND DATEDIFF(CURDATE(),COALESCE(DATE(i.last_movement_at),DATE(i.created_at)))>=? {$where} ORDER BY idle_days DESC",$params)->result();}
|
||||
if($type==='damage'){$this->db->select('d.id document_id,d.document_no,d.document_date,d.document_type,d.status,d.reason,i.kode_detail,i.nama_barang,w.nama warehouse_name,l.qty,l.unit_cost,(l.qty*l.unit_cost) value',false)->from('stock_documents d')->join('stock_document_lines l','l.stock_document_id=d.id')->join('items i','i.id=l.item_id')->join('warehouses w','w.id=d.from_warehouse_id','left')->where('d.company_id',$company)->where_in('d.document_type',array('damaged','lost'))->where('d.document_date >=',$from)->where('d.document_date <=',$to);if($warehouse)$this->db->where('d.from_warehouse_id',$warehouse);if($item)$this->db->where('l.item_id',$item);return$this->db->order_by('d.id','DESC')->get()->result();}
|
||||
if($type==='opname'){$this->db->select('d.id document_id,d.document_no,d.document_date,d.document_type,d.status,d.reason,i.kode_detail,i.nama_barang,w.nama warehouse_name,l.system_qty,l.counted_qty,(l.counted_qty-l.system_qty) difference,l.unit_cost',false)->from('stock_documents d')->join('stock_document_lines l','l.stock_document_id=d.id')->join('items i','i.id=l.item_id')->join('warehouses w','w.id=d.from_warehouse_id','left')->where(array('d.company_id'=>$company,'d.document_type'=>'opname'))->where('d.document_date >=',$from)->where('d.document_date <=',$to);if($warehouse)$this->db->where('d.from_warehouse_id',$warehouse);if($item)$this->db->where('l.item_id',$item);return$this->db->order_by('d.id','DESC')->get()->result();}
|
||||
return$this->db->where('company_id',$company)->where('as_of_date >=',$from)->where('as_of_date <=',$to)->order_by('id','DESC')->limit(200)->get('inventory_reconciliations')->result();
|
||||
}
|
||||
|
||||
private function reportDefinition($type=null)
|
||||
{
|
||||
$all=array(
|
||||
'valuation'=>array('title'=>'Nilai Persediaan','columns'=>array('kode_detail'=>'Kode','nama_barang'=>'Barang','warehouse_name'=>'Gudang','costing_method'=>'Metode','qty'=>'Qty','inventory_value'=>'Nilai')),
|
||||
'stock_card'=>array('title'=>'Kartu Stok','columns'=>array('movement_date'=>'Tanggal','document_no'=>'Dokumen','warehouse_name'=>'Gudang','direction'=>'Arah','qty'=>'Qty','unit_cost'=>'Biaya Satuan','value'=>'Nilai','reason'=>'Keterangan')),
|
||||
'location'=>array('title'=>'Stok per Gudang dan Rak','columns'=>array('kode_detail'=>'Kode','nama_barang'=>'Barang','warehouse_name'=>'Gudang','bin_code'=>'Rak/Bin','qty'=>'Qty')),
|
||||
'barcode'=>array('title'=>'Riwayat Barcode dan Serial','columns'=>array('kode_detail'=>'Kode','nama_barang'=>'Barang','barcode'=>'Barcode','serial_number'=>'Serial','warehouse_name'=>'Gudang','bin_code'=>'Rak','batch_no'=>'Batch','qty_sisa'=>'Qty','reserved_qty'=>'Reservasi','status'=>'Status')),
|
||||
'batch'=>array('title'=>'Batch dan Kedaluwarsa','columns'=>array('kode_detail'=>'Kode','nama_barang'=>'Barang','batch_no'=>'Batch','warehouse_name'=>'Gudang','manufactured_date'=>'Produksi','expiry_date'=>'Kedaluwarsa','qty'=>'Qty')),
|
||||
'reservation'=>array('title'=>'Reservasi Stok','columns'=>array('reservation_no'=>'Reservasi','kode_detail'=>'Kode','nama_barang'=>'Barang','warehouse_name'=>'Gudang','source_type'=>'Sumber','source_id'=>'ID Sumber','qty'=>'Qty','released_qty'=>'Diselesaikan','expires_at'=>'Berlaku Sampai','status'=>'Status')),
|
||||
'reorder'=>array('title'=>'Stok Minimum dan Reorder','columns'=>array('kode_detail'=>'Kode','nama_barang'=>'Barang','warehouse_name'=>'Gudang','qty'=>'Qty','reorder_point'=>'Reorder Point','minimum_stock'=>'Minimum')),
|
||||
'slow'=>array('title'=>'Slow-moving dan Dead Stock','columns'=>array('kode_detail'=>'Kode','nama_barang'=>'Barang','stok'=>'Stok','last_movement_at'=>'Terakhir Bergerak','idle_days'=>'Tidak Bergerak (hari)')),
|
||||
'damage'=>array('title'=>'Barang Rusak dan Hilang','columns'=>array('document_no'=>'Dokumen','document_date'=>'Tanggal','document_type'=>'Jenis','kode_detail'=>'Kode','nama_barang'=>'Barang','warehouse_name'=>'Gudang','qty'=>'Qty','value'=>'Nilai','status'=>'Status','reason'=>'Alasan')),
|
||||
'opname'=>array('title'=>'Stock Opname','columns'=>array('document_no'=>'Dokumen','document_date'=>'Tanggal','kode_detail'=>'Kode','nama_barang'=>'Barang','warehouse_name'=>'Gudang','system_qty'=>'Stok Sistem','counted_qty'=>'Fisik','difference'=>'Selisih','status'=>'Status')),
|
||||
'reconciliation'=>array('title'=>'Rekonsiliasi Persediaan','columns'=>array('as_of_date'=>'Tanggal','ledger_value'=>'Ledger','stock_log_value'=>'Stock Log','gl_value'=>'General Ledger','difference'=>'Selisih','barcode_variance_count'=>'Selisih Barcode','cache_variance_count'=>'Selisih Cache','status'=>'Status'))
|
||||
);if($type===null)return$all;if(!isset($all[$type]))throw new BusinessException('Jenis laporan persediaan tidak valid.');return$all[$type];
|
||||
}
|
||||
|
||||
private function inlinePdf($html,$filename){require_once FCPATH.'vendor/autoload.php';$pdf=new Dompdf\Dompdf();$pdf->loadHtml($html);$pdf->setPaper('A4','landscape');$pdf->render();$pdf->stream($filename,array('Attachment'=>false));}
|
||||
private function action($callable,$message){if(strtoupper($this->input->method())!=='POST')show_404();try{$result=$callable();return json_response(true,$message,array('result'=>$result));}catch(Throwable$e){return business_exception_response($e);}}
|
||||
private function masterAction($callable,$message){$this->requirePermission('items','can_update');return$this->action($callable,$message);}
|
||||
private function assertWarehouse($id){$row=$this->db->get_where('warehouses',array('id'=>(int)$id,'company_id'=>$this->company()))->row();if(!$row)throw new BusinessException('Gudang tidak ditemukan.');return$row;}
|
||||
private function assertItem($id){$row=$this->db->get_where('items',array('id'=>(int)$id,'company_id'=>$this->company()))->row();if(!$row)throw new BusinessException('Barang tidak ditemukan.');return$row;}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
class Inventoryrepair extends CI_Controller
|
||||
{
|
||||
public function __construct(){parent::__construct();if(!is_cli())show_404();$this->load->library('ProfessionalInventoryService');}
|
||||
public function negative_opening(){if(strtolower((string)getenv('APP_ENV'))==='production'){echo"Ditolak: repair khusus Development.\n";return;}$rows=$this->db->query("SELECT item_id,warehouse_id,SUM(IF(direction='in',qty,-qty)) qty FROM inventory_ledger GROUP BY item_id,warehouse_id HAVING qty<-.0001")->result();$user=$this->db->select('users.id')->from('users')->join('roles','roles.id=users.role_id')->where('roles.nama_role','Admin')->order_by('users.id')->get()->row();if(!$user)throw new RuntimeException('User Admin tidak tersedia.');foreach($rows as$r){$item=$this->db->get_where('items',array('id'=>$r->item_id))->row();$key='DEV-NEGATIVE-OPENING-'.$r->item_id.'-'.$r->warehouse_id;$id=$this->professionalinventoryservice->createDocument(array('type'=>'opening','date'=>date('Y-m-d'),'from_warehouse_id'=>null,'to_warehouse_id'=>$r->warehouse_id,'item_id'=>$r->item_id,'barcode_id'=>null,'batch_id'=>null,'from_bin_id'=>null,'to_bin_id'=>null,'qty'=>abs($r->qty),'unit_cost'=>$item->harga_beli,'reason'=>'Koreksi saldo negatif legacy hasil backfill Tahap 7','idempotency_key'=>$key),$user->id);$doc=$this->db->get_where('stock_documents',array('id'=>$id))->row();if($doc->status==='draft')$this->professionalinventoryservice->submit($id,$user->id);$doc=$this->db->get_where('stock_documents',array('id'=>$id))->row();if($doc->status==='approved')$this->professionalinventoryservice->post($id,$user->id);echo'Corrected item '.$r->item_id.' warehouse '.$r->warehouse_id.' qty '.abs($r->qty)."\n";}}
|
||||
public function reconcile_gl(){if(strtolower((string)getenv('APP_ENV'))==='production'){echo"Ditolak: repair khusus Development.\n";return;}$existing=$this->db->where('ref_type','inventory_reconciliation')->where('ref_code','DEV-STAGE7-BASELINE')->get('journals')->row();if($existing){echo"Baseline reconciliation sudah tersedia.\n";return;}$ledger=(float)$this->db->query("SELECT COALESCE(SUM(IF(direction='in',value,-value)),0) total FROM inventory_ledger")->row()->total;$inventory=$this->db->get_where('system_account_mappings',array('mapping_key'=>'inventory','is_active'=>1))->row()->account_id;$gl=(float)$this->db->query("SELECT COALESCE(SUM(d.debit-d.kredit),0) total FROM journal_details d JOIN journals j ON j.id=d.journal_id WHERE d.account_id=? AND j.status IN('posted','reversed')",array($inventory))->row()->total;$diff=round($ledger-$gl,2);if(abs($diff)<.01){echo"Ledger dan GL sudah balance.\n";return;}$user=$this->db->select('users.id')->from('users')->join('roles','roles.id=users.role_id')->where('roles.nama_role','Admin')->order_by('users.id')->get()->row();$other=$this->db->get_where('system_account_mappings',array('mapping_key'=>$diff>0?'inventory_adjustment_gain':'inventory_adjustment_loss','is_active'=>1))->row()->account_id;$this->load->library('PostingService');$lines=$diff>0?array(array('account_id'=>$inventory,'debit'=>$diff,'kredit'=>0),array('account_id'=>$other,'debit'=>0,'kredit'=>$diff)):array(array('account_id'=>$other,'debit'=>abs($diff),'kredit'=>0),array('account_id'=>$inventory,'debit'=>0,'kredit'=>abs($diff)));$id=$this->postingservice->post(array('tanggal'=>date('Y-m-d'),'no_ref'=>'INV-RECON-STAGE7-'.date('Ymd'),'keterangan'=>'Baseline rekonsiliasi inventory ledger legacy Tahap 7','ref_type'=>'inventory_reconciliation','ref_id'=>1,'created_by'=>$user->id),$lines,true,true);$this->db->where('id',$id)->update('journals',array('ref_code'=>'DEV-STAGE7-BASELINE'));$this->db->insert('inventory_reconciliations',array('as_of_date'=>date('Y-m-d'),'ledger_value'=>$ledger,'gl_value'=>$ledger,'difference'=>0,'status'=>'resolved','notes'=>'Development baseline; selisih awal '.number_format($diff,2,'.',''),'created_by'=>$user->id,'created_at'=>date('Y-m-d H:i:s')));echo'Jurnal rekonsiliasi dibuat: '.number_format($diff,2,'.','')."\n";}
|
||||
public function sync_cache(){if(strtolower((string)getenv('APP_ENV'))==='production'){echo"Ditolak: repair khusus Development.\n";return;}$rows=$this->db->query("SELECT i.id,COALESCE(SUM(IF(l.direction='in',l.qty,-l.qty)),0) qty FROM items i LEFT JOIN inventory_ledger l ON l.item_id=i.id GROUP BY i.id")->result();$changed=0;foreach($rows as$r){$item=$this->db->get_where('items',array('id'=>$r->id))->row();if(abs((float)$item->stok-(float)$r->qty)>.0001){$this->db->where('id',$r->id)->update('items',array('stok'=>$r->qty));$changed++;}}echo"Selesai: {$changed} cache stok disinkronkan.\n";}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,9 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
|
||||
class Items extends CI_Controller {
|
||||
class Items extends MY_Controller {
|
||||
private function companyId(){ return (int)$this->session->userdata('company_id'); }
|
||||
private function companyItem($id){ return $this->db->get_where('items',array('id'=>(int)$id,'company_id'=>$this->companyId()))->row(); }
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
@@ -30,6 +32,7 @@ class Items extends CI_Controller {
|
||||
|
||||
public function item_barcodes($item_id)
|
||||
{
|
||||
if(!$this->companyItem($item_id)) show_404();
|
||||
$data = [
|
||||
"active_menu" => "draft_items",
|
||||
"item_id" => $item_id
|
||||
@@ -76,8 +79,9 @@ class Items extends CI_Controller {
|
||||
")
|
||||
->from('item_barcodes')
|
||||
->join('items','items.id=item_barcodes.item_id')
|
||||
->where('item_barcodes.barcode',$barcode)
|
||||
->or_where('item_barcodes.serial_number',$barcode)
|
||||
->where('items.company_id',$this->companyId())
|
||||
->group_start()->where('item_barcodes.barcode',$barcode)
|
||||
->or_where('item_barcodes.serial_number',$barcode)->group_end()
|
||||
->get()
|
||||
->row();
|
||||
|
||||
@@ -134,10 +138,7 @@ class Items extends CI_Controller {
|
||||
|
||||
public function load_data_barcode($item_id)
|
||||
{
|
||||
$item = $this->db
|
||||
->where('id', $item_id)
|
||||
->get('items')
|
||||
->row();
|
||||
$item = $this->companyItem($item_id);
|
||||
|
||||
if (!$item) {
|
||||
echo json_encode([
|
||||
@@ -149,48 +150,61 @@ class Items extends CI_Controller {
|
||||
|
||||
$next = $this->db
|
||||
->where('item_id', $item_id)
|
||||
->where('status', 'pending')
|
||||
->where('serial_number IS NULL', NULL, false)
|
||||
->order_by('id', 'ASC')
|
||||
->get('item_barcodes')
|
||||
->row();
|
||||
|
||||
$start = max(0, (int)$this->input->get('start'));
|
||||
$length = min(100, max(10, (int)($this->input->get('length') ?: 50)));
|
||||
$search = trim((string)$this->input->get('search', true));
|
||||
$total = (int)$this->db->where('item_id', $item_id)->count_all_results('item_barcodes');
|
||||
$done = (int)$this->db->where('item_id', $item_id)->where('serial_number IS NOT NULL', NULL, false)->where('serial_number !=', '')->count_all_results('item_barcodes');
|
||||
|
||||
$this->db->where('item_id', $item_id);
|
||||
if ($search !== '') {
|
||||
$this->db->group_start()->like('barcode', $search)->or_like('serial_number', $search)->group_end();
|
||||
}
|
||||
$filtered = (int)$this->db->count_all_results('item_barcodes');
|
||||
|
||||
$this->db->where('item_id', $item_id);
|
||||
if ($search !== '') {
|
||||
$this->db->group_start()->like('barcode', $search)->or_like('serial_number', $search)->group_end();
|
||||
}
|
||||
$list = $this->db
|
||||
->where('item_id', $item_id)
|
||||
->order_by('id', 'ASC')
|
||||
->limit($length, $start)
|
||||
->get('item_barcodes')
|
||||
->result();
|
||||
|
||||
$total = count($list);
|
||||
|
||||
$done = 0;
|
||||
foreach ($list as $row) {
|
||||
if (!empty($row->serial_number)) {
|
||||
$done++;
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'status' => true,
|
||||
'item_status' => $item->status,
|
||||
'item_sale_price' => (float)$item->harga_jual,
|
||||
'item_purchase_price' => (float)$item->harga_beli,
|
||||
'next' => $next,
|
||||
'list' => $list,
|
||||
'total' => $total,
|
||||
'done' => $done,
|
||||
'filtered' => $filtered,
|
||||
'start' => $start,
|
||||
'length' => $length,
|
||||
'percent' => $total > 0 ? round(($done / $total) * 100) : 0
|
||||
]);
|
||||
}
|
||||
|
||||
public function print_barcode($item_id)
|
||||
{
|
||||
if(!$this->companyItem($item_id)) show_404();
|
||||
require_once APPPATH.'third_party/fpdf/fpdf.php';
|
||||
require_once APPPATH.'libraries/phpqrcode/qrlib.php';
|
||||
|
||||
$barcodes = $this->db
|
||||
->where('item_id',$item_id)
|
||||
->where('status !=', 'sold_out')
|
||||
->order_by('id','ASC')
|
||||
->get('item_barcodes')
|
||||
->result();
|
||||
$printStatus=$this->input->get('status',true)?:'available';
|
||||
if(!in_array($printStatus,['available','sold_out','all'],true))$printStatus='available';
|
||||
$this->db->where('item_id',$item_id);
|
||||
if($printStatus==='all')$this->db->where_in('status',['available','sold_out']);else$this->db->where('status',$printStatus);
|
||||
$barcodes=$this->db->order_by('id','ASC')->get('item_barcodes')->result();
|
||||
|
||||
if(empty($barcodes)){
|
||||
show_error('Barcode tidak ditemukan.');
|
||||
@@ -256,7 +270,8 @@ class Items extends CI_Controller {
|
||||
$x=$col*$labelWidth;
|
||||
$y=$line*$labelHeight;
|
||||
|
||||
$qrFile=$tempDir.$row->barcode.'.png';
|
||||
$safeBarcode=preg_replace('/[^A-Za-z0-9_-]/','_',basename((string)$row->barcode));
|
||||
$qrFile=$tempDir.$safeBarcode.'.png';
|
||||
|
||||
if(!file_exists($qrFile)){
|
||||
QRcode::png(
|
||||
@@ -315,6 +330,7 @@ class Items extends CI_Controller {
|
||||
|
||||
public function save_serial_number()
|
||||
{
|
||||
if (strtoupper($this->input->method()) !== 'POST') show_404();
|
||||
$barcode_id = $this->input->post('barcode_id');
|
||||
$serial = trim($this->input->post('serial_number'));
|
||||
|
||||
@@ -337,13 +353,12 @@ class Items extends CI_Controller {
|
||||
return;
|
||||
}
|
||||
|
||||
$barcode = $this->db
|
||||
->where('id',$barcode_id)
|
||||
->get('item_barcodes')
|
||||
->row();
|
||||
$barcode = $this->db->select('item_barcodes.*')->from('item_barcodes')->join('items','items.id=item_barcodes.item_id')->where(array('item_barcodes.id'=>$barcode_id,'items.company_id'=>$this->companyId(),'item_barcodes.status'=>'pending'))->get()->row();
|
||||
if(!$barcode){echo json_encode(array('status'=>false,'message'=>'Barcode pending tidak ditemukan pada perusahaan aktif.'));return;}
|
||||
$this->db->where('id',$barcode_id);
|
||||
$this->db->update('item_barcodes',array(
|
||||
'serial_number'=>$serial
|
||||
'serial_number'=>$serial,
|
||||
'status'=>'pending'
|
||||
));
|
||||
// langsung kirim data terbaru
|
||||
$_GET['item_id']=$barcode->item_id;
|
||||
@@ -352,6 +367,7 @@ class Items extends CI_Controller {
|
||||
|
||||
public function delete_serial_number()
|
||||
{
|
||||
if (strtoupper($this->input->method()) !== 'POST') show_404();
|
||||
$barcode_id = (int)$this->input->post('barcode_id');
|
||||
|
||||
if ($barcode_id <= 0) {
|
||||
@@ -363,10 +379,7 @@ class Items extends CI_Controller {
|
||||
}
|
||||
|
||||
// Ambil data barcode
|
||||
$barcode = $this->db
|
||||
->where('id', $barcode_id)
|
||||
->get('item_barcodes')
|
||||
->row();
|
||||
$barcode = $this->db->select('item_barcodes.*')->from('item_barcodes')->join('items','items.id=item_barcodes.item_id')->where(array('item_barcodes.id'=>$barcode_id,'items.company_id'=>$this->companyId(),'item_barcodes.status'=>'pending'))->get()->row();
|
||||
|
||||
if (!$barcode) {
|
||||
echo json_encode([
|
||||
@@ -376,17 +389,60 @@ class Items extends CI_Controller {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($barcode->status === 'available' || !empty($barcode->activated_at)) {
|
||||
echo json_encode(['status'=>false,'message'=>'Serial unit yang sudah diposting tidak dapat dihapus.']);
|
||||
return;
|
||||
}
|
||||
|
||||
// Hapus Serial Number
|
||||
$this->db
|
||||
->where('id', $barcode_id)
|
||||
->update('item_barcodes', [
|
||||
'serial_number' => NULL
|
||||
'serial_number' => NULL,
|
||||
'status' => 'pending'
|
||||
]);
|
||||
|
||||
$this->db->where('id',$barcode->item_id)->update('items',['status'=>'draft']);
|
||||
|
||||
// Kirim ulang data terbaru
|
||||
$this->load_data_barcode($barcode->item_id);
|
||||
}
|
||||
|
||||
public function post_barcode()
|
||||
{
|
||||
return $this->barcodeError('Posting hanya dapat dilakukan dari tabel Barang Pending.');
|
||||
$barcodeId = (int)$this->input->post('barcode_id');
|
||||
$salePrice = (float)$this->input->post('sale_price');
|
||||
$barcode = $this->db->get_where('item_barcodes', ['id'=>$barcodeId])->row();
|
||||
|
||||
if (!$barcode) return $this->barcodeError('Barcode tidak ditemukan.');
|
||||
if ($barcode->status === 'available') return $this->barcodeError('Unit barcode sudah diposting.');
|
||||
if (trim((string)$barcode->serial_number) === '') return $this->barcodeError('Serial number wajib diisi sebelum unit diposting.');
|
||||
if ($salePrice <= 0) return $this->barcodeError('Harga jual satuan wajib lebih dari nol.');
|
||||
|
||||
$this->db->trans_start();
|
||||
$this->db->where('id',$barcodeId)->update('item_barcodes',[
|
||||
'sale_price'=>$salePrice,
|
||||
'status'=>'available',
|
||||
'activated_at'=>date('Y-m-d H:i:s'),
|
||||
'activated_by'=>(int)$this->session->userdata('user_id')
|
||||
]);
|
||||
$this->db->where('id',$barcode->item_id)->update('items',[
|
||||
'harga_jual'=>$salePrice,
|
||||
'status'=>'active'
|
||||
]);
|
||||
$this->db->trans_complete();
|
||||
|
||||
if ($this->db->trans_status() === false) return $this->barcodeError('Unit barcode gagal diposting.');
|
||||
log_activity('items','posting','Posting unit barcode ID '.$barcodeId.' dengan harga jual '.$salePrice,'success');
|
||||
$this->load_data_barcode($barcode->item_id);
|
||||
}
|
||||
|
||||
private function barcodeError($message)
|
||||
{
|
||||
echo json_encode(['status'=>false,'message'=>$message]);
|
||||
}
|
||||
|
||||
public function get_barcode_item()
|
||||
{
|
||||
$barcode = trim($this->input->post('barcode'));
|
||||
@@ -491,6 +547,10 @@ class Items extends CI_Controller {
|
||||
public function get_data()
|
||||
{
|
||||
$status = $this->input->get('s') ?? 'all';
|
||||
$server = (int)$this->input->get('server') === 1;
|
||||
$draw = (int)$this->input->get('draw');
|
||||
$start = max(0,(int)$this->input->get('start'));
|
||||
$length = min(100,max(10,(int)$this->input->get('length')));
|
||||
|
||||
$allowed = ['draft','active','all'];
|
||||
if (!in_array($status, $allowed)) {
|
||||
@@ -509,6 +569,13 @@ class Items extends CI_Controller {
|
||||
items.status,
|
||||
items.kode_id,
|
||||
kode.need_serial_number,
|
||||
COALESCE((SELECT SUM(CASE WHEN ibs.status IN("available","reserved") THEN GREATEST(ibs.qty_sisa-COALESCE(ibs.reserved_qty,0)-COALESCE(ibs.return_reserved_qty,0),0) ELSE 0 END) FROM item_barcodes ibs WHERE ibs.item_id=items.id),items.stok) operational_available,
|
||||
(SELECT COUNT(*) FROM item_barcodes iba WHERE iba.item_id=items.id AND iba.status IN("available","reserved") AND GREATEST(iba.qty_sisa-COALESCE(iba.reserved_qty,0)-COALESCE(iba.return_reserved_qty,0),0)>0) operational_barcode_count,
|
||||
COALESCE((SELECT SUM(GREATEST(ibp.qty_sisa-COALESCE(ibp.reserved_qty,0)-COALESCE(ibp.return_reserved_qty,0),0)) FROM item_barcodes ibp WHERE ibp.item_id=items.id AND ibp.status="pending"),items.stok) pending_available,
|
||||
(SELECT COUNT(*) FROM item_barcodes ibp WHERE ibp.item_id=items.id AND ibp.status="pending" AND GREATEST(ibp.qty_sisa-COALESCE(ibp.reserved_qty,0)-COALESCE(ibp.return_reserved_qty,0),0)>0) pending_barcode_count,
|
||||
(SELECT COUNT(*) FROM item_barcodes ibc WHERE ibc.item_id=items.id) total_barcodes,
|
||||
(SELECT COUNT(*) FROM item_barcodes ibc WHERE ibc.item_id=items.id AND NULLIF(ibc.serial_number,"") IS NOT NULL) completed_serials,
|
||||
(SELECT COUNT(*) FROM item_barcodes ibc WHERE ibc.item_id=items.id AND ibc.status="available") posted_barcodes,
|
||||
(
|
||||
SELECT GROUP_CONCAT(DISTINCT w.nama SEPARATOR ", ")
|
||||
FROM stock_logs sl
|
||||
@@ -518,21 +585,35 @@ class Items extends CI_Controller {
|
||||
');
|
||||
|
||||
$this->db->join('kode_barang kode', 'items.kode_id = kode.id', 'left');
|
||||
if ($this->db->field_exists('company_id','items')) {
|
||||
$this->db->where('items.company_id', (int)$this->session->userdata('company_id'));
|
||||
}
|
||||
|
||||
if ($status !== 'all') {
|
||||
if ($status === 'draft') {
|
||||
$this->db->where('EXISTS(SELECT 1 FROM goods_receipt_lines grlp WHERE grlp.item_id=items.id)',null,false)->group_start()->where('items.status','draft')->or_where('EXISTS(SELECT 1 FROM item_barcodes ibp WHERE ibp.item_id=items.id AND ibp.status="pending")',null,false)->group_end();
|
||||
} elseif ($status !== 'all') {
|
||||
$this->db->where('items.status', $status);
|
||||
}
|
||||
|
||||
if($server){
|
||||
$search=$this->input->get('search');
|
||||
$search=is_array($search)?trim((string)($search['value']??'')):'';
|
||||
if($search!=='')$this->db->group_start()->like('items.kode_detail',$search)->or_like('items.nama_barang',$search)->group_end();
|
||||
}
|
||||
|
||||
$this->db->from('items');
|
||||
$filtered=$server?$this->db->count_all_results('',false):0;
|
||||
|
||||
// urutan custom
|
||||
$this->db->order_by('(items.stok = 0)', 'ASC', false);
|
||||
$this->db->order_by('items.tanggal_pembelian', 'ASC');
|
||||
|
||||
$this->db->from('items');
|
||||
if($server)$this->db->limit($length,$start);
|
||||
|
||||
$data = $this->db->get()->result();
|
||||
|
||||
$result = [];
|
||||
$no = 1;
|
||||
$no = $server?$start+1:1;
|
||||
|
||||
foreach($data as $row){
|
||||
|
||||
@@ -541,39 +622,11 @@ class Items extends CI_Controller {
|
||||
$btnDelete = '<button class="btn btn-danger btn-sm btn-delete m-1" data-id="'.htmlspecialchars($row->id, ENT_QUOTES, 'UTF-8').'">Delete</button>';
|
||||
$btnInsertSn = '<a href="'.base_url('items/item_barcodes/'.$row->id).'" class="btn btn-secondary btn-sm m-1">Barcode</a>';
|
||||
$btnPrintBarcode = '<button class="btn btn-secondary btn-sm btn-print m-1" data-id="'.htmlspecialchars($row->id, ENT_QUOTES, 'UTF-8').'"><i class="bi bi-printer"></i> Print Barcode</button>';
|
||||
$btnPost = '<button class="btn btn-success btn-sm btn-updatePosting m-1" data-id="'.htmlspecialchars($row->id, ENT_QUOTES, 'UTF-8').'">Posting</button>';
|
||||
$btnPost = '<button class="btn btn-success btn-sm btn-updatePosting m-1" data-id="'.htmlspecialchars($row->id, ENT_QUOTES, 'UTF-8').'" data-buy="'.htmlspecialchars($row->harga_beli,ENT_QUOTES,'UTF-8').'" data-sale="'.htmlspecialchars($row->harga_jual,ENT_QUOTES,'UTF-8').'">Posting</button>';
|
||||
|
||||
if ($row->need_serial_number == 'true') {
|
||||
|
||||
$list = $this->db
|
||||
->where('item_id', $row->id)
|
||||
->order_by('id', 'ASC')
|
||||
->get('item_barcodes')
|
||||
->result();
|
||||
|
||||
$total = count($list);
|
||||
|
||||
$done = 0;
|
||||
foreach ($list as $barcode) {
|
||||
if (!empty($barcode->serial_number)) {
|
||||
$done++;
|
||||
}
|
||||
}
|
||||
|
||||
$total_sn = $total;
|
||||
$done_sn = $done;
|
||||
|
||||
if ($row->status == 'draft') {
|
||||
if ($total_sn == $done_sn) {
|
||||
$btnSN = $btnInsertSn . $btnPrintBarcode;
|
||||
$btnPostview = $btnPost;
|
||||
} else {
|
||||
$btnSN = $btnInsertSn ;
|
||||
$btnPostview = '';
|
||||
}
|
||||
} else {
|
||||
$btnSN = $btnPrintBarcode;
|
||||
}
|
||||
$btnSN = $btnInsertSn . ((int)$row->posted_barcodes > 0 ? $btnPrintBarcode : '');
|
||||
$btnPostview = (int)$row->completed_serials > (int)$row->posted_barcodes ? $btnPost : '';
|
||||
|
||||
} else {
|
||||
|
||||
@@ -581,35 +634,43 @@ class Items extends CI_Controller {
|
||||
$btnPostview = $btnPost;
|
||||
}
|
||||
|
||||
if($row->status == 'draft'){
|
||||
$action = $btnDetail . $btnPostview . $btnDelete . $btnSN;
|
||||
$isAdmin=$this->session->userdata('role')==='Admin';
|
||||
$canUpdate=$isAdmin||check_permission('items','can_update');
|
||||
$canPost=$isAdmin||check_permission('items','can_post');
|
||||
$canExport=$isAdmin||check_permission('items','can_export');
|
||||
if($status==='active'){
|
||||
$action = $btnDetail . ($canExport?$btnPrintBarcode:'');
|
||||
|
||||
} elseif($status==='draft'){
|
||||
$action = $btnDetail . ($canPost?$btnPostview:'') . ($canUpdate?$btnSN:'');
|
||||
|
||||
} else {
|
||||
|
||||
if($this->session->userdata('role') == 'Admin') {
|
||||
$btnAdjust = '<button class="btn btn-warning btn-sm btn-adjust m-1" data-id="'.htmlspecialchars($row->id, ENT_QUOTES, 'UTF-8').'">Adjust</button>';
|
||||
} else {
|
||||
$btnAdjust = '';
|
||||
}
|
||||
$action = $btnDetail . $btnEdit . $btnAdjust . $btnSN;
|
||||
$action = $btnDetail . ($canExport?$btnPrintBarcode:'');
|
||||
|
||||
}
|
||||
|
||||
$barcodeCount = $status === 'active' ? (int)$row->operational_barcode_count : (int)$row->pending_barcode_count;
|
||||
$stockAvailable = $status === 'active' ? (float)$row->operational_available : (float)$row->pending_available;
|
||||
$barcodeStock = '<span title="Jumlah barcode / jumlah stok"><strong>'.number_format($barcodeCount, 0, ',', '.').'</strong> / '.number_format($stockAvailable, 0, ',', '.').'</span>';
|
||||
|
||||
$result[] = [
|
||||
$no++,
|
||||
$row->tanggal_pembelian,
|
||||
$row->kode_detail,
|
||||
$row->nama_barang,
|
||||
$row->gudang ?? '-',
|
||||
$row->stok,
|
||||
number_format($row->harga_beli ?? 0, 2),
|
||||
number_format($row->harga_jual ?? 0, 2),
|
||||
$barcodeStock,
|
||||
number_format($row->harga_beli ?? 0, 2, ',', '.'),
|
||||
$status==='draft'?'<input type="text" class="form-control form-control-sm format-rupiah pending-sale-price" data-id="'.(int)$row->id.'" data-buy="'.htmlspecialchars($row->harga_beli,ENT_QUOTES,'UTF-8').'" value="'.($row->harga_jual>0?number_format($row->harga_jual,2,',','.'):'').'" placeholder="Minimal '.number_format($row->harga_beli,2,',','.').'" inputmode="decimal">':number_format($row->harga_jual??0,2,',','.'),
|
||||
$row->created_at,
|
||||
$action
|
||||
];
|
||||
}
|
||||
|
||||
echo json_encode(["data"=>$result]);
|
||||
$payload=array("data"=>$result);
|
||||
if($server){$payload['draw']=$draw;$payload['recordsTotal']=$filtered;$payload['recordsFiltered']=$filtered;}
|
||||
echo json_encode($payload);
|
||||
}
|
||||
|
||||
// =========================
|
||||
@@ -627,7 +688,10 @@ class Items extends CI_Controller {
|
||||
// =========================
|
||||
public function detail($id)
|
||||
{
|
||||
$item = $this->db->get_where('items',['id'=>$id])->row();
|
||||
$item = $this->companyItem($id);
|
||||
if (!$item) {
|
||||
return json_response(false, 'Barang tidak ditemukan.', array(), 404);
|
||||
}
|
||||
|
||||
$logs = $this->db
|
||||
->select('stock_logs.*, warehouses.nama as gudang')
|
||||
@@ -637,16 +701,26 @@ class Items extends CI_Controller {
|
||||
->get('stock_logs')
|
||||
->result();
|
||||
|
||||
echo json_encode([
|
||||
'item'=>$item,
|
||||
'logs'=>$logs
|
||||
]);
|
||||
return json_response(true, 'Detail barang berhasil dimuat.', array(
|
||||
'item' => $item,
|
||||
'logs' => $logs
|
||||
));
|
||||
}
|
||||
|
||||
public function stock_log_pdf($id)
|
||||
{
|
||||
$item=$this->companyItem($id);
|
||||
if(!$item)show_404();
|
||||
$logs=$this->db->select('sl.*,w.nama gudang')->from('stock_logs sl')->join('warehouses w','w.id=sl.warehouse_id','left')->where('sl.item_id',(int)$id)->order_by('sl.id','ASC')->get()->result();
|
||||
require_once APPPATH.'third_party/fpdf/fpdf.php';
|
||||
$pdf=new FPDF('L','mm','A4');$pdf->SetMargins(12,12,12);$pdf->AddPage();$pdf->SetFont('Arial','B',15);$pdf->Cell(0,8,'STOCK LOG BARANG',0,1,'C');$pdf->SetFont('Arial','',9);$pdf->Cell(0,5,$item->kode_detail.' - '.$item->nama_barang,0,1,'C');$pdf->Ln(5);
|
||||
$widths=[12,34,42,20,20,105,35];foreach(['No','Tanggal','Gudang','Qty','Tipe','Keterangan','Referensi']as$i=>$label){$pdf->SetFont('Arial','B',8);$pdf->Cell($widths[$i],7,$label,1,0,'C');}$pdf->Ln();$balance=0;$pdf->SetFont('Arial','',8);foreach($logs as$i=>$log){$signed=$log->tipe==='masuk'?(float)$log->qty:-(float)$log->qty;$balance+=$signed;$pdf->Cell($widths[0],7,$i+1,1,0,'C');$pdf->Cell($widths[1],7,substr($log->created_at,0,19),1);$pdf->Cell($widths[2],7,substr($log->gudang?:'-',0,24),1);$pdf->Cell($widths[3],7,number_format(abs($log->qty),0,',','.'),1,0,'R');$pdf->Cell($widths[4],7,strtoupper($log->tipe),1,0,'C');$pdf->Cell($widths[5],7,substr($log->keterangan?:'-',0,70),1);$pdf->Cell($widths[6],7,substr(($log->ref_type?:'-').' #'.($log->ref_id?:'-'),0,22),1,1);}$pdf->SetFont('Arial','B',9);$pdf->Cell(108,8,'SALDO STOK',1,0,'R');$pdf->Cell(20,8,number_format($balance,0,',','.'),1,0,'R');$pdf->Cell(140,8,'',1,1);$pdf->Output('I','Stock-Log-'.$item->kode_detail.'.pdf');
|
||||
}
|
||||
|
||||
public function get_accounts()
|
||||
{
|
||||
$data = $this->db
|
||||
->where_in('kode_akun', [101,102,201,122,125,105])
|
||||
->where_in('kode_akun', ['1010','1020','2010','1220','1250','1050'])
|
||||
->order_by('kode_akun','ASC')
|
||||
->get('accounts')
|
||||
->result();
|
||||
@@ -670,6 +744,7 @@ class Items extends CI_Controller {
|
||||
// =========================
|
||||
public function save()
|
||||
{
|
||||
return $this->legacyMutationDisabled('Penambahan stok langsung dinonaktifkan. Gunakan Saldo Awal atau Barang Masuk pada Operasional Stok.');
|
||||
$nama_barang = $this->input->post('nama_barang');
|
||||
$qty = (int)$this->input->post('qty');
|
||||
$harga_beli = (float)$this->input->post('harga_beli');
|
||||
@@ -682,7 +757,7 @@ class Items extends CI_Controller {
|
||||
$status = $this->input->post('status');
|
||||
|
||||
// ================= VALIDASI =================
|
||||
if(!$nama_barang || $qty <= 0 || $harga_beli <= 0 || !$warehouse_id || !$account_kas){
|
||||
if(!$nama_barang || $qty <= 0 || $harga_beli <= 0 || !$warehouse_id || ($status !== 'draft' && !$account_kas)){
|
||||
echo json_encode([
|
||||
'status'=>false,
|
||||
'message'=>'Data tidak lengkap / tidak valid',
|
||||
@@ -734,6 +809,16 @@ class Items extends CI_Controller {
|
||||
|
||||
$total = $qty * $harga_beli;
|
||||
|
||||
// Barang pending hanya master/rencana pembelian. Belum membentuk stok,
|
||||
// barcode, pergerakan barang, maupun jurnal sampai diterima melalui PO.
|
||||
if ($status === 'draft') {
|
||||
$this->db->insert('items',array('stok'=>0,'planned_qty'=>$qty,'planned_warehouse_id'=>$warehouse_id,'planned_tracking_qty'=>$tracking_qty,'kode_detail'=>$kode_detail,'kode_id'=>$kode_id,'nama_barang'=>$nama_barang,'harga_beli'=>$harga_beli,'harga_jual'=>$harga_jual,'tanggal_pembelian'=>$tanggal_beli,'status'=>'draft'));
|
||||
$itemId=(int)$this->db->insert_id();
|
||||
log_activity('items','create','Membuat master barang pending: '.$nama_barang,'success');
|
||||
echo json_encode(array('status'=>true,'message'=>'Barang pending dibuat. Stok baru aktif setelah penerimaan PO.','item_id'=>$itemId));
|
||||
return;
|
||||
}
|
||||
|
||||
$this->db->trans_start();
|
||||
|
||||
// ================= INSERT ITEM =================
|
||||
@@ -854,6 +939,7 @@ class Items extends CI_Controller {
|
||||
// =========================
|
||||
public function update()
|
||||
{
|
||||
return $this->legacyMutationDisabled('Barang aktif tidak dapat diubah melalui form lama. Gunakan master Jenis/Kode Barang atau dokumen persediaan yang sesuai.');
|
||||
$id = $this->input->post('id');
|
||||
$nama_barang = $this->input->post('nama_barang');
|
||||
$harga_baru = (float)$this->input->post('harga_beli');
|
||||
@@ -946,6 +1032,8 @@ class Items extends CI_Controller {
|
||||
|
||||
public function delete($id)
|
||||
{
|
||||
echo json_encode(array('status'=>false,'message'=>'Barang tidak dapat dihapus. Gunakan status nonaktif pada master bila sudah tidak digunakan.'));
|
||||
return;
|
||||
// cek item ada
|
||||
$item = $this->db->get_where('items',['id'=>$id])->row();
|
||||
|
||||
@@ -954,10 +1042,26 @@ class Items extends CI_Controller {
|
||||
return;
|
||||
}
|
||||
|
||||
// Master/item yang telah menjadi bagian dokumen pembelian tidak boleh
|
||||
// dihapus. Menghapusnya akan meninggalkan item_id yatim pada baris PO
|
||||
// dan membuat penerimaan gagal saat menulis stock_logs.
|
||||
if ($this->db->table_exists('purchase_order_lines')) {
|
||||
$cekPembelian = $this->db->where('item_id', (int) $id)
|
||||
->count_all_results('purchase_order_lines');
|
||||
|
||||
if ($cekPembelian > 0) {
|
||||
echo json_encode([
|
||||
'status'=>false,
|
||||
'message'=>'Barang tidak dapat dihapus karena sudah digunakan pada Purchase Order. Nonaktifkan barang bila tidak akan digunakan lagi.'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// cek ada transaksi stock
|
||||
$cekStock = $this->db->where('item_id',$id)->count_all_results('stock_logs');
|
||||
|
||||
if($cekStock > 1){
|
||||
if($cekStock > 0){
|
||||
echo json_encode([
|
||||
'status'=>false,
|
||||
'message'=>'Tidak bisa dihapus, sudah ada transaksi stok'
|
||||
@@ -1002,6 +1106,7 @@ class Items extends CI_Controller {
|
||||
|
||||
public function posting()
|
||||
{
|
||||
if (strtoupper($this->input->method()) !== 'POST') show_404();
|
||||
$id = $this->input->post('id');
|
||||
|
||||
if(!$id){
|
||||
@@ -1013,7 +1118,7 @@ class Items extends CI_Controller {
|
||||
}
|
||||
|
||||
// cek item ada
|
||||
$item = $this->db->get_where('items', ['id' => $id])->row();
|
||||
$item = $this->db->get_where('items', ['id' => $id, 'company_id'=>(int)$this->session->userdata('company_id')])->row();
|
||||
|
||||
if(!$item){
|
||||
echo json_encode([
|
||||
@@ -1023,8 +1128,21 @@ class Items extends CI_Controller {
|
||||
return;
|
||||
}
|
||||
|
||||
$catalog = $this->db->get_where('kode_barang',['id'=>$item->kode_id])->row();
|
||||
$serialRequired=$catalog&&strtolower((string)$catalog->need_serial_number)==='true';
|
||||
$submittedSalePrice=(float)$this->input->post('harga_jual');
|
||||
if($submittedSalePrice>0)$item->harga_jual=$submittedSalePrice;
|
||||
if ((float)$item->harga_jual <= 0) {
|
||||
echo json_encode(['status'=>false,'message'=>'Harga jual wajib diisi sebelum barang diposting.']);
|
||||
return;
|
||||
}
|
||||
if ((float)$item->harga_jual < (float)$item->harga_beli) {
|
||||
echo json_encode(['status'=>false,'message'=>'Harga jual tidak boleh lebih rendah dari harga beli Rp '.number_format($item->harga_beli,2,',','.')]);
|
||||
return;
|
||||
}
|
||||
|
||||
// cegah double posting
|
||||
if($item->status == 'active'){
|
||||
if(!$serialRequired&&$item->status == 'active'){
|
||||
echo json_encode([
|
||||
'status' => false,
|
||||
'message' => 'Barang sudah diterima sebelumnya'
|
||||
@@ -1034,10 +1152,24 @@ class Items extends CI_Controller {
|
||||
|
||||
$this->db->trans_start();
|
||||
|
||||
// update status saja
|
||||
if($serialRequired){
|
||||
$ready=$this->db->where('item_id',$id)->where('status','pending')->where('serial_number IS NOT NULL',null,false)->where('serial_number !=','')->count_all_results('item_barcodes');
|
||||
if($ready<=0){$this->db->trans_complete();echo json_encode(['status'=>false,'message'=>'Belum ada serial number yang siap diposting.']);return;}
|
||||
$this->db->where('item_id',$id)->where('status','pending')->where('serial_number IS NOT NULL',null,false)->where('serial_number !=','')->update('item_barcodes',[
|
||||
'sale_price'=>$item->harga_jual,'status'=>'available','activated_at'=>date('Y-m-d H:i:s'),'activated_by'=>(int)$this->session->userdata('user_id')
|
||||
]);
|
||||
$posted=$this->db->affected_rows();
|
||||
}else{
|
||||
$this->db->where('item_id',$id)->where('status','pending')->update('item_barcodes',[
|
||||
'sale_price'=>$item->harga_jual,'status'=>'available','activated_at'=>date('Y-m-d H:i:s'),'activated_by'=>(int)$this->session->userdata('user_id')
|
||||
]);
|
||||
$posted=$this->db->affected_rows();
|
||||
}
|
||||
$notReady=$this->db->where('item_id',$id)->where('status','pending')->count_all_results('item_barcodes');
|
||||
$this->db->where('id', $id);
|
||||
$this->db->update('items', [
|
||||
'status' => 'active'
|
||||
'status' => 'active',
|
||||
'harga_jual' => $item->harga_jual
|
||||
]);
|
||||
|
||||
$this->db->trans_complete();
|
||||
@@ -1061,7 +1193,9 @@ class Items extends CI_Controller {
|
||||
|
||||
echo json_encode([
|
||||
'status' => true,
|
||||
'message' => 'Barang sudah diterima dan stok siap digunakan'
|
||||
'message' => $serialRequired?($posted.' unit berhasil diposting dan '.$notReady.' unit masih belum siap.'):('Barang berhasil diposting. '.$posted.' barcode siap digunakan.'),
|
||||
'posted_count'=>$posted,
|
||||
'pending_count'=>$notReady
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1071,6 +1205,7 @@ class Items extends CI_Controller {
|
||||
// =========================
|
||||
public function keluarkan()
|
||||
{
|
||||
return $this->legacyMutationDisabled('Pengeluaran langsung dinonaktifkan. Buat dokumen Barang Keluar pada Operasional Stok agar ledger, barcode, dan jurnal tetap konsisten.');
|
||||
$account_biaya = (int)$this->input->post('account_biaya');
|
||||
$tanggal_keluar = $this->input->post('tanggal_keluar');
|
||||
$keterangan_keluar = $this->input->post('keterangan_keluar');
|
||||
@@ -1307,6 +1442,7 @@ class Items extends CI_Controller {
|
||||
|
||||
public function adjust()
|
||||
{
|
||||
return $this->legacyMutationDisabled('Adjustment langsung dinonaktifkan. Gunakan dokumen Penyesuaian dengan workflow approval pada Operasional Stok.');
|
||||
$item_id = $this->input->post('item_id');
|
||||
$qty = (int)$this->input->post('qty');
|
||||
$warehouse_id = $this->input->post('warehouse_id');
|
||||
@@ -1412,15 +1548,16 @@ class Items extends CI_Controller {
|
||||
items.harga_jual,
|
||||
items.kode_detail,
|
||||
COALESCE(SUM(
|
||||
stock_logs.qty * IF(stock_logs.tipe="masuk",1,-1)
|
||||
inventory_ledger.qty * IF(inventory_ledger.direction="in",1,-1)
|
||||
),0) as stok
|
||||
');
|
||||
$this->db->where('status', 'active');
|
||||
$this->db->where('items.status', 'active');
|
||||
$this->db->where('items.company_id', $this->companyId());
|
||||
$this->db->from('items');
|
||||
|
||||
$this->db->join('stock_logs',
|
||||
'stock_logs.item_id = items.id
|
||||
AND stock_logs.warehouse_id = '.$this->db->escape($warehouse_id),
|
||||
$this->db->join('inventory_ledger',
|
||||
'inventory_ledger.item_id = items.id
|
||||
AND inventory_ledger.warehouse_id = '.$this->db->escape($warehouse_id),
|
||||
'left');
|
||||
|
||||
$this->db->group_by('items.id');
|
||||
@@ -1436,6 +1573,7 @@ class Items extends CI_Controller {
|
||||
|
||||
public function update_barcode_item()
|
||||
{
|
||||
return $this->legacyMutationDisabled('Pembuatan barcode massal tanpa dokumen sumber dinonaktifkan. Barcode dibuat dari penerimaan atau dokumen persediaan.');
|
||||
$items = $this->db
|
||||
->select("
|
||||
i.id,
|
||||
@@ -1574,4 +1712,9 @@ class Items extends CI_Controller {
|
||||
'message' => 'Barcode berhasil diperbarui.'
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private function legacyMutationDisabled($message)
|
||||
{
|
||||
return $this->output->set_status_header(422)->set_content_type('application/json')->set_output(json_encode(array('status'=>false,'message'=>$message)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
class Journaldescriptionrepair extends CI_Controller
|
||||
{
|
||||
public function __construct(){parent::__construct();if(!is_cli())show_404();$this->load->library(array('JournalDescriptionService','AuditService'));}
|
||||
public function run(){if(strtolower((string)getenv('APP_ENV'))==='production'){echo "Ditolak: backfill ini khusus Development.\n";return;}$rows=$this->db->where("COALESCE(ref_type,'') NOT IN ('','manual_journal')",null,false)->not_like('keterangan','[AUTO]','after')->order_by('id')->get('journals')->result();$count=0;$this->db->trans_begin();try{foreach($rows as$j){$lines=$this->db->select('account_id,debit,kredit')->where('journal_id',$j->id)->get('journal_details')->result_array();if(!$lines)continue;$new=$this->journaldescriptionservice->build((array)$j,$lines);$this->db->where('id',$j->id)->update('journals',array('keterangan'=>$new));$this->auditservice->record('development_repair','journal',$j->id,'enrich_automatic_description',array('keterangan'=>$j->keterangan),array('keterangan'=>$new),null);$count++;}if(!$this->db->trans_status())throw new RuntimeException('Backfill gagal.');$this->db->trans_commit();echo "Selesai: {$count} keterangan jurnal otomatis diperkaya.\n";}catch(Throwable$e){$this->db->trans_rollback();echo 'Gagal: '.$e->getMessage()."\n";exit(1);}}
|
||||
public function verify(){$missing=$this->db->where("COALESCE(ref_type,'') NOT IN ('','manual_journal')",null,false)->not_like('keterangan','[AUTO]','after')->count_all_results('journals');echo($missing?'[FAIL] ':'[OK] ').'automatic_descriptions_without_standard='.$missing."\n";if($missing)exit(1);}
|
||||
}
|
||||
@@ -1,12 +1,15 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
|
||||
class Jurnal extends CI_Controller {
|
||||
class Jurnal extends MY_Controller {
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->load->model('DynamicModel', 'dm');
|
||||
$this->load->library('JournalService');
|
||||
$this->load->library('NumberingService');
|
||||
$this->load->library('JournalWorkflowService');
|
||||
|
||||
if (!$this->session->userdata('logged_in')) {
|
||||
redirect('auth');
|
||||
@@ -37,10 +40,10 @@ class Jurnal extends CI_Controller {
|
||||
$search = $_POST['search']['value'];
|
||||
|
||||
// ================= TOTAL ALL
|
||||
$totalAll = $this->db->count_all('journals');
|
||||
$totalAll = $this->db->where('company_id',$this->companycontext->id())->count_all_results('journals');
|
||||
|
||||
// ================= QUERY FILTERED (HITUNG)
|
||||
$this->db->from('journals');
|
||||
$this->db->from('journals')->where('company_id',$this->companycontext->id());
|
||||
|
||||
if(!empty($search)){
|
||||
$this->db->group_start();
|
||||
@@ -58,11 +61,14 @@ class Jurnal extends CI_Controller {
|
||||
journals.tanggal,
|
||||
journals.no_ref,
|
||||
journals.keterangan,
|
||||
journals.status,
|
||||
journals.is_reversed,
|
||||
COALESCE(SUM(journal_details.debit),0) as total_debit,
|
||||
COALESCE(SUM(journal_details.kredit),0) as total_kredit
|
||||
');
|
||||
$this->db->from('journals');
|
||||
$this->db->join('journal_details','journal_details.journal_id = journals.id','LEFT');
|
||||
$this->db->where('journals.company_id',$this->companycontext->id());
|
||||
|
||||
if(!empty($search)){
|
||||
$this->db->group_start();
|
||||
@@ -86,19 +92,22 @@ class Jurnal extends CI_Controller {
|
||||
foreach ($query as $row) {
|
||||
$no++;
|
||||
|
||||
if($this->session->userdata('role') != 'Admin'){
|
||||
|
||||
$aksi = '
|
||||
<button class="btn btn-sm btn-info btn-detail" data-id="'.$row->id.'">Detail</button>
|
||||
';
|
||||
|
||||
$isAdmin = $this->session->userdata('role') === 'Admin';
|
||||
$aksi = '<button class="btn btn-sm btn-info btn-detail" data-id="'.$row->id.'">Detail</button> ';
|
||||
if ($row->status === 'draft') {
|
||||
$aksi .= '<button class="btn btn-sm btn-primary btn-submit" data-id="'.$row->id.'">Submit</button> ';
|
||||
if ($isAdmin) $aksi .= '<button class="btn btn-sm btn-danger btn-delete" data-id="'.$row->id.'">Hapus</button> ';
|
||||
}
|
||||
else {
|
||||
$aksi = '
|
||||
<button class="btn btn-sm btn-info btn-detail" data-id="'.$row->id.'">Detail</button>
|
||||
<button class="btn btn-sm btn-danger btn-delete" data-id="'.$row->id.'">Delete</button>
|
||||
';
|
||||
if ($isAdmin && $row->status === 'submitted') {
|
||||
$aksi .= '<button class="btn btn-sm btn-success btn-approve" data-id="'.$row->id.'">Approve</button> ';
|
||||
$aksi .= '<button class="btn btn-sm btn-outline-danger btn-reject" data-id="'.$row->id.'">Reject</button> ';
|
||||
}
|
||||
if ($isAdmin && $row->status === 'posted') {
|
||||
$aksi .= '<button class="btn btn-sm btn-outline-warning btn-reverse" data-id="'.$row->id.'">Reverse</button>';
|
||||
}
|
||||
|
||||
$statusClass = array('draft'=>'secondary','submitted'=>'info','approved'=>'primary','posted'=>'success','rejected'=>'danger','reversed'=>'warning');
|
||||
$badge = '<span class="badge bg-'.(isset($statusClass[$row->status]) ? $statusClass[$row->status] : 'secondary').'">'.htmlspecialchars(strtoupper($row->status), ENT_QUOTES, 'UTF-8').'</span>';
|
||||
|
||||
$data[] = [
|
||||
$no,
|
||||
@@ -107,6 +116,7 @@ class Jurnal extends CI_Controller {
|
||||
$row->keterangan,
|
||||
number_format($row->total_debit,0,',','.'),
|
||||
number_format($row->total_kredit,0,',','.'),
|
||||
$badge,
|
||||
$aksi
|
||||
];
|
||||
}
|
||||
@@ -124,7 +134,7 @@ class Jurnal extends CI_Controller {
|
||||
// =========================
|
||||
public function get_accounts()
|
||||
{
|
||||
$data = $this->db->order_by('kode_akun','ASC')->get('accounts')->result();
|
||||
$data = $this->db->where(['company_id'=>$this->companycontext->id(),'is_active'=>1,'allow_posting'=>1])->order_by('kode_akun','ASC')->get('accounts')->result();
|
||||
echo json_encode($data);
|
||||
}
|
||||
|
||||
@@ -133,68 +143,59 @@ class Jurnal extends CI_Controller {
|
||||
// =========================
|
||||
public function save()
|
||||
{
|
||||
$tanggal = $this->input->post('tanggal');
|
||||
$no_ref = 'JR-' . date('YmdHis');
|
||||
$keterangan = $this->input->post('keterangan');
|
||||
$tanggal = (string) $this->input->post('tanggal', true);
|
||||
$keterangan = trim((string) $this->input->post('keterangan', true));
|
||||
$accounts = (array) $this->input->post('account_id');
|
||||
$debit = (array) $this->input->post('debit');
|
||||
$kredit = (array) $this->input->post('kredit');
|
||||
|
||||
$accounts = $this->input->post('account_id');
|
||||
$debit = $this->input->post('debit');
|
||||
$kredit = $this->input->post('kredit');
|
||||
try {
|
||||
$noRef = $this->numberingservice->next('journal', $tanggal);
|
||||
$lines = array();
|
||||
foreach ($accounts as $index => $accountId) {
|
||||
$lineDebit = isset($debit[$index]) ? $this->parseNominal($debit[$index]) : 0;
|
||||
$lineCredit = isset($kredit[$index]) ? $this->parseNominal($kredit[$index]) : 0;
|
||||
if ($lineDebit <= 0 && $lineCredit <= 0) continue;
|
||||
$lines[] = array(
|
||||
'account_id' => $accountId,
|
||||
'debit' => $lineDebit,
|
||||
'kredit' => $lineCredit
|
||||
);
|
||||
}
|
||||
|
||||
if (array_sum($debit) != array_sum($kredit)) {
|
||||
echo json_encode([
|
||||
'status'=>false,
|
||||
'message'=>'Debit & Kredit tidak balance!'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
$journalId = $this->journalservice->create(array(
|
||||
'tanggal' => $tanggal,
|
||||
'no_ref' => $noRef,
|
||||
'keterangan' => $keterangan,
|
||||
'ref_type' => 'manual_journal',
|
||||
'created_by' => (int) $this->session->userdata('user_id')
|
||||
), $lines);
|
||||
|
||||
$this->db->trans_start();
|
||||
|
||||
$this->db->insert('journals', [
|
||||
'tanggal' => $tanggal,
|
||||
'no_ref' => $no_ref,
|
||||
'keterangan' => $keterangan,
|
||||
'created_by' => $this->session->userdata('user_id')
|
||||
]);
|
||||
|
||||
$journal_id = $this->db->insert_id();
|
||||
|
||||
for ($i=0; $i<count($accounts); $i++) {
|
||||
if ($accounts[$i] == '') continue;
|
||||
|
||||
$this->db->insert('journal_details', [
|
||||
'journal_id' => $journal_id,
|
||||
'account_id' => $accounts[$i],
|
||||
'debit' => $debit[$i] ?: 0,
|
||||
'kredit' => $kredit[$i] ?: 0
|
||||
]);
|
||||
log_activity('Jurnal', 'create', 'Membuat jurnal No Ref: ' . $noRef, 'success');
|
||||
return json_response(true, 'Jurnal berhasil disimpan.', array(
|
||||
'journal_id' => $journalId,
|
||||
'no_ref' => $noRef
|
||||
));
|
||||
} catch (Throwable $exception) {
|
||||
return business_exception_response($exception);
|
||||
}
|
||||
}
|
||||
|
||||
$this->db->trans_complete();
|
||||
|
||||
if ($this->db->trans_status() === FALSE) {
|
||||
|
||||
echo json_encode([
|
||||
'status'=>false,
|
||||
'message'=>'Gagal simpan jurnal'
|
||||
]);
|
||||
return;
|
||||
|
||||
/** Menerima nominal raw maupun berformat 20,000 / 20.000 / 20.000,50. */
|
||||
private function parseNominal($value)
|
||||
{
|
||||
$value = trim(str_ireplace(array('Rp', ' '), '', (string) $value));
|
||||
if ($value === '') return 0.0;
|
||||
$comma = strrpos($value, ','); $dot = strrpos($value, '.');
|
||||
if ($comma !== false && $dot !== false) {
|
||||
$value = $comma > $dot ? str_replace(',', '.', str_replace('.', '', $value)) : str_replace(',', '', $value);
|
||||
} elseif ($comma !== false) {
|
||||
$decimal = strlen($value) - $comma - 1;
|
||||
$value = $decimal > 0 && $decimal <= 2 ? str_replace(',', '.', $value) : str_replace(',', '', $value);
|
||||
} elseif ($dot !== false && strlen($value) - $dot - 1 === 3) {
|
||||
$value = str_replace('.', '', $value);
|
||||
}
|
||||
|
||||
// ================= LOG ACTIVITY =================
|
||||
log_activity(
|
||||
'Jurnal',
|
||||
'create',
|
||||
'Membuat jurnal No Ref: '.$no_ref,
|
||||
'success'
|
||||
);
|
||||
|
||||
echo json_encode([
|
||||
'status'=>true,
|
||||
'message'=>'Jurnal berhasil disimpan'
|
||||
]);
|
||||
return is_numeric($value) ? round((float) $value, 2) : 0.0;
|
||||
}
|
||||
|
||||
// =========================
|
||||
@@ -202,7 +203,8 @@ class Jurnal extends CI_Controller {
|
||||
// =========================
|
||||
public function detail($id)
|
||||
{
|
||||
$header = $this->db->get_where('journals',['id'=>$id])->row();
|
||||
$header = $this->db->get_where('journals',['id'=>$id,'company_id'=>$this->companycontext->id()])->row();
|
||||
if (!$header) return json_response(false, 'Jurnal tidak ditemukan.', array(), 404);
|
||||
|
||||
$detail = $this->db
|
||||
->select('accounts.kode_akun, accounts.nama_akun, journal_details.*')
|
||||
@@ -220,10 +222,19 @@ class Jurnal extends CI_Controller {
|
||||
->get('users')
|
||||
->row();
|
||||
|
||||
$history = $this->db
|
||||
->select('journal_status_histories.*, users.nama changed_by_name')
|
||||
->from('journal_status_histories')
|
||||
->join('users', 'users.id = journal_status_histories.changed_by', 'left')
|
||||
->where('journal_id', $id)
|
||||
->order_by('journal_status_histories.id', 'ASC')
|
||||
->get()->result();
|
||||
|
||||
echo json_encode([
|
||||
'header'=>$header,
|
||||
'detail'=>$detail,
|
||||
'user' =>$user
|
||||
'user' =>$user,
|
||||
'history'=>$history
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -232,6 +243,10 @@ class Jurnal extends CI_Controller {
|
||||
// =========================
|
||||
public function delete($id)
|
||||
{
|
||||
if (strtoupper($this->input->method()) !== 'POST') {
|
||||
return json_response(false, 'Metode request tidak diizinkan.', array(), 405);
|
||||
}
|
||||
$this->requireAdmin();
|
||||
$journal = $this->db->get_where('journals', ['id' => $id])->row();
|
||||
|
||||
// VALIDASI
|
||||
@@ -243,6 +258,10 @@ class Jurnal extends CI_Controller {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!in_array($journal->status, array('draft', 'rejected'), true)) {
|
||||
return json_response(false, 'Hanya jurnal draft atau rejected yang dapat dihapus.', array(), 422);
|
||||
}
|
||||
|
||||
$this->db->trans_start();
|
||||
|
||||
// =========================
|
||||
@@ -301,6 +320,62 @@ class Jurnal extends CI_Controller {
|
||||
]);
|
||||
}
|
||||
|
||||
public function submit($id)
|
||||
{
|
||||
return $this->workflowAction(function () use ($id) {
|
||||
return $this->journalworkflowservice->submit($id, $this->session->userdata('user_id'));
|
||||
}, 'Jurnal berhasil diajukan.');
|
||||
}
|
||||
|
||||
public function approve($id)
|
||||
{
|
||||
$this->requireAdmin();
|
||||
return $this->workflowAction(function () use ($id) {
|
||||
return $this->journalworkflowservice->approve($id, $this->session->userdata('user_id'));
|
||||
}, 'Approval jurnal berhasil diproses. Setelah approval final, jurnal otomatis diposting.');
|
||||
}
|
||||
|
||||
public function post($id)
|
||||
{
|
||||
$this->requireAdmin();
|
||||
return $this->workflowAction(function () use ($id) {
|
||||
return $this->journalworkflowservice->post($id, $this->session->userdata('user_id'));
|
||||
}, 'Jurnal berhasil diposting.');
|
||||
}
|
||||
|
||||
public function reject($id)
|
||||
{
|
||||
$this->requireAdmin();
|
||||
$reason = $this->input->post('reason', true);
|
||||
return $this->workflowAction(function () use ($id, $reason) {
|
||||
return $this->journalworkflowservice->reject($id, $this->session->userdata('user_id'), $reason);
|
||||
}, 'Jurnal berhasil ditolak.');
|
||||
}
|
||||
|
||||
public function reverse($id)
|
||||
{
|
||||
$this->requireAdmin();
|
||||
$date = $this->input->post('date', true);
|
||||
$reason = $this->input->post('reason', true);
|
||||
return $this->workflowAction(function () use ($id, $date, $reason) {
|
||||
return $this->journalworkflowservice->reverse($id, $date, $this->session->userdata('user_id'), $reason);
|
||||
}, 'Reversal jurnal berhasil dibuat.');
|
||||
}
|
||||
|
||||
private function workflowAction(callable $callback, $message)
|
||||
{
|
||||
if (strtoupper($this->input->method()) !== 'POST') {
|
||||
return json_response(false, 'Metode request tidak diizinkan.', array(), 405);
|
||||
}
|
||||
try {
|
||||
$result = $callback();
|
||||
log_activity('Jurnal', 'workflow', $message, 'success');
|
||||
return json_response(true, $message, array('result' => $result));
|
||||
} catch (Throwable $exception) {
|
||||
return business_exception_response($exception);
|
||||
}
|
||||
}
|
||||
|
||||
public function get_base_jurnal()
|
||||
{
|
||||
echo json_encode(
|
||||
@@ -330,4 +405,4 @@ class Jurnal extends CI_Controller {
|
||||
'stok' => $stok
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
|
||||
class Kodebarang extends CI_Controller {
|
||||
class Kodebarang extends MY_Controller {
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
@@ -25,7 +25,7 @@ class Kodebarang extends CI_Controller {
|
||||
// =========================
|
||||
public function get_data()
|
||||
{
|
||||
$data = $this->db->order_by('id', 'DESC')->get('kode_barang')->result();
|
||||
$data = $this->db->where('company_id',(int)$this->session->userdata('company_id'))->order_by('id', 'DESC')->get('kode_barang')->result();
|
||||
|
||||
$result = [];
|
||||
$no = 1;
|
||||
@@ -38,11 +38,10 @@ class Kodebarang extends CI_Controller {
|
||||
(int)$row->limit_stock,
|
||||
strtoupper($row->tracking_type ?? 'UNIT'),
|
||||
strtolower($row->need_serial_number ?? 'false') === 'true' ? 'Ya' : 'Tidak',
|
||||
(int)($row->allocate_purchase_cost ?? 0) === 1 ? 'Ya' : 'Tidak',
|
||||
$row->unit ?? 'PCS',
|
||||
'
|
||||
<button class="btn btn-sm btn-secondary btn-editkode" data-id="'.$row->id.'">Edit</button>
|
||||
<button class="btn btn-sm btn-danger btn-delete" data-id="'.$row->id.'">Delete</button>
|
||||
'
|
||||
(int)$row->is_active===1?'<span class="status-chip status-approved">Aktif</span>':'<span class="status-chip status-rejected">Nonaktif</span>',
|
||||
($this->session->userdata('role')==='Admin'||check_permission('items','can_update'))?'<button class="btn btn-sm btn-outline-primary btn-editkode" data-id="'.$row->id.'"><i class="bi bi-pencil"></i> Ubah</button>':'-'
|
||||
];
|
||||
}
|
||||
|
||||
@@ -55,7 +54,7 @@ class Kodebarang extends CI_Controller {
|
||||
public function detail($id)
|
||||
{
|
||||
echo json_encode(
|
||||
$this->db->get_where('kode_barang',['id'=>$id])->row()
|
||||
$this->db->get_where('kode_barang',['id'=>$id,'company_id'=>(int)$this->session->userdata('company_id')])->row()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -69,7 +68,9 @@ class Kodebarang extends CI_Controller {
|
||||
$limit_stock = (int)$this->input->post('limit_stock');
|
||||
$tracking_type = strtoupper(trim($this->input->post('tracking_type')));
|
||||
$need_serial_number = strtolower(trim($this->input->post('need_serial_number')));
|
||||
$allocate_purchase_cost = $this->input->post('allocate_purchase_cost') ? 1 : 0;
|
||||
$unit = trim($this->input->post('unit'));
|
||||
$is_active = $this->input->post('is_active') ? 1 : 0;
|
||||
|
||||
if(!$nama || !$kode){
|
||||
echo json_encode(['status'=>false,'message'=>'Kode dan nama wajib diisi']);
|
||||
@@ -91,7 +92,7 @@ class Kodebarang extends CI_Controller {
|
||||
return;
|
||||
}
|
||||
|
||||
$exists = $this->db->get_where('kode_barang', ['kode_barang' => $kode])->num_rows();
|
||||
$exists = $this->db->get_where('kode_barang', ['kode_barang' => $kode,'company_id'=>(int)$this->session->userdata('company_id')])->num_rows();
|
||||
if($exists > 0){
|
||||
echo json_encode(['status'=>false,'message'=>'Kode barang sudah digunakan']);
|
||||
return;
|
||||
@@ -99,12 +100,15 @@ class Kodebarang extends CI_Controller {
|
||||
|
||||
$this->db->insert('kode_barang', [
|
||||
'kode_barang' => $kode,
|
||||
'company_id' => (int)$this->session->userdata('company_id'),
|
||||
'nama' => $nama,
|
||||
'created_at' => date('Y-m-d H:i:s'),
|
||||
'limit_stock' => $limit_stock,
|
||||
'tracking_type' => $tracking_type,
|
||||
'need_serial_number' => $need_serial_number,
|
||||
'unit' => $unit
|
||||
'allocate_purchase_cost' => $allocate_purchase_cost,
|
||||
'unit' => $unit,
|
||||
'is_active' => $is_active
|
||||
]);
|
||||
|
||||
log_activity(
|
||||
@@ -128,7 +132,9 @@ class Kodebarang extends CI_Controller {
|
||||
$limit_stock = (int)$this->input->post('limit_stock');
|
||||
$tracking_type = strtoupper(trim($this->input->post('tracking_type')));
|
||||
$need_serial_number = strtolower(trim($this->input->post('need_serial_number')));
|
||||
$allocate_purchase_cost = $this->input->post('allocate_purchase_cost') ? 1 : 0;
|
||||
$unit = trim($this->input->post('unit'));
|
||||
$is_active = $this->input->post('is_active') ? 1 : 0;
|
||||
|
||||
if(!$id || !$nama || !$kode){
|
||||
echo json_encode(['status'=>false,'message'=>'ID, kode dan nama wajib diisi']);
|
||||
@@ -152,6 +158,7 @@ class Kodebarang extends CI_Controller {
|
||||
|
||||
$exists = $this->db
|
||||
->where('kode_barang', $kode)
|
||||
->where('company_id',(int)$this->session->userdata('company_id'))
|
||||
->where('id !=', $id)
|
||||
->get('kode_barang')
|
||||
->num_rows();
|
||||
@@ -161,13 +168,15 @@ class Kodebarang extends CI_Controller {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->db->where('id',$id)->update('kode_barang', [
|
||||
$this->db->where(array('id'=>$id,'company_id'=>(int)$this->session->userdata('company_id')))->update('kode_barang', [
|
||||
'kode_barang' => $kode,
|
||||
'nama' => $nama,
|
||||
'limit_stock' => $limit_stock,
|
||||
'tracking_type' => $tracking_type,
|
||||
'need_serial_number' => $need_serial_number,
|
||||
'unit' => $unit
|
||||
'allocate_purchase_cost' => $allocate_purchase_cost,
|
||||
'unit' => $unit,
|
||||
'is_active' => $is_active
|
||||
]);
|
||||
|
||||
log_activity(
|
||||
@@ -185,44 +194,25 @@ class Kodebarang extends CI_Controller {
|
||||
// =========================
|
||||
public function delete($id)
|
||||
{
|
||||
$row = $this->db->get_where('kode_barang', ['id'=>$id])->row();
|
||||
|
||||
if(!$row){
|
||||
echo json_encode([
|
||||
'status'=>false,
|
||||
'message'=>'Data kode barang tidak ditemukan'
|
||||
]);
|
||||
return;
|
||||
if (strtoupper($this->input->method()) !== 'POST') {
|
||||
$this->output->set_status_header(405);
|
||||
} else {
|
||||
$this->output->set_status_header(409);
|
||||
}
|
||||
|
||||
// cek relasi ke items (mengacu ke kode_id)
|
||||
$cek = $this->db->get_where('items',['kode_id'=>$id])->num_rows();
|
||||
|
||||
if($cek > 0){
|
||||
echo json_encode([
|
||||
'status'=>false,
|
||||
'message'=>'Kode barang tidak bisa dihapus karena masih dipakai item'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$this->db->delete('kode_barang',['id'=>$id]);
|
||||
|
||||
log_activity(
|
||||
'kode_barang',
|
||||
'delete',
|
||||
'Hapus kode barang ID: ' . $id . ' (' . $row->kode_barang . ' - ' . $row->nama . ')',
|
||||
'success'
|
||||
);
|
||||
|
||||
echo json_encode(['status'=>true,'message'=>'Kode barang berhasil dihapus']);
|
||||
return $this->output
|
||||
->set_content_type('application/json')
|
||||
->set_output(json_encode(array(
|
||||
'status' => false,
|
||||
'message' => 'Jenis/Kode Barang tidak dapat dihapus. Nonaktifkan melalui tombol Ubah.'
|
||||
)));
|
||||
}
|
||||
|
||||
|
||||
public function list()
|
||||
{
|
||||
echo json_encode(
|
||||
$this->db->get('kode_barang')->result()
|
||||
$this->db->where(array('company_id'=>(int)$this->session->userdata('company_id'),'is_active'=>1))->get('kode_barang')->result()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
|
||||
class Labarugi extends CI_Controller {
|
||||
class Labarugi extends MY_Admin_Controller {
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
@@ -43,6 +43,7 @@ class Labarugi extends CI_Controller {
|
||||
// ================= JOIN
|
||||
$this->db->join('journal_details jd','a.id = jd.account_id','LEFT');
|
||||
$this->db->join('journals j','j.id = jd.journal_id','LEFT');
|
||||
$this->db->where("(j.status IN ('posted','reversed') OR jd.id IS NULL)", null, false);
|
||||
|
||||
// ================= FILTER AKUN
|
||||
$this->db->where('a.kategori','laba_rugi');
|
||||
@@ -85,4 +86,4 @@ class Labarugi extends CI_Controller {
|
||||
'laba' => $revenue - $expense
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
|
||||
class Leaverequests extends CI_Controller {
|
||||
class Leaverequests extends MY_Controller {
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
@@ -281,4 +281,4 @@ class Leaverequests extends CI_Controller {
|
||||
'message'=> 'Leave request berhasil dihapus'
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,145 +1,13 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
|
||||
class Lokasiasset extends CI_Controller {
|
||||
|
||||
public function __construct()
|
||||
/** Alias kompatibilitas Master Lokasi Aset. */
|
||||
class Lokasiasset extends MY_Controller
|
||||
{
|
||||
public function index(){ redirect('fixedassets#assetMaster', 'location', 301); }
|
||||
public function __call($method,$arguments)
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
if (!$this->session->userdata('logged_in')) {
|
||||
redirect('auth');
|
||||
}
|
||||
if(strtoupper($this->input->method())==='GET') redirect('fixedassets#assetMaster', 'location', 302);
|
||||
return json_response(false,'Master lokasi lama sudah dinonaktifkan. Gunakan Master Data pada menu Aset Tetap.',array(),410);
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$data = ["active_menu" => "lokasiasset"];
|
||||
$this->load->view('partials/header', $data);
|
||||
$this->load->view('lokasi_asset/layout');
|
||||
$this->load->view('partials/footer');
|
||||
}
|
||||
|
||||
// =========================
|
||||
// GET DATA (DATATABLE)
|
||||
// =========================
|
||||
public function get_data()
|
||||
{
|
||||
$data = $this->db->get('lokasi_asset')->result();
|
||||
|
||||
$result = [];
|
||||
$no = 1;
|
||||
|
||||
foreach ($data as $row) {
|
||||
$result[] = [
|
||||
$no++,
|
||||
$row->nama,
|
||||
'
|
||||
<button class="btn btn-sm btn-secondary btn-editgudang" data-id="'.$row->id.'">Edit</button>
|
||||
<button class="btn btn-sm btn-danger btn-delete" data-id="'.$row->id.'">Delete</button>
|
||||
'
|
||||
];
|
||||
}
|
||||
|
||||
echo json_encode(["data"=>$result]);
|
||||
}
|
||||
|
||||
// =========================
|
||||
// DETAIL
|
||||
// =========================
|
||||
public function detail($id)
|
||||
{
|
||||
echo json_encode(
|
||||
$this->db->get_where('lokasi_asset',['id'=>$id])->row()
|
||||
);
|
||||
}
|
||||
|
||||
// =========================
|
||||
// SAVE
|
||||
// =========================
|
||||
public function save()
|
||||
{
|
||||
$nama = $this->input->post('nama');
|
||||
|
||||
if(!$nama){
|
||||
echo json_encode(['status'=>false,'message'=>'Nama wajib diisi']);
|
||||
return;
|
||||
}
|
||||
|
||||
$this->db->insert('lokasi_asset', [
|
||||
'nama' => $nama
|
||||
]);
|
||||
|
||||
log_activity(
|
||||
'kode_barang',
|
||||
'create',
|
||||
'Tambah gudang '.$nama,
|
||||
'success'
|
||||
);
|
||||
|
||||
echo json_encode(['status'=>true,'message'=>'Gudang berhasil ditambahkan']);
|
||||
}
|
||||
|
||||
// =========================
|
||||
// UPDATE
|
||||
// =========================
|
||||
public function update()
|
||||
{
|
||||
$id = $this->input->post('id');
|
||||
$nama = $this->input->post('nama');
|
||||
|
||||
if(!$nama){
|
||||
echo json_encode(['status'=>false,'message'=>'Nama wajib diisi']);
|
||||
return;
|
||||
}
|
||||
|
||||
$this->db->where('id',$id)->update('lokasi_asset', [
|
||||
'nama' => $nama
|
||||
]);
|
||||
|
||||
log_activity(
|
||||
'kode_barang',
|
||||
'update',
|
||||
'Update gudang '.$nama,
|
||||
'success'
|
||||
);
|
||||
|
||||
echo json_encode(['status'=>true,'message'=>'Gudang berhasil diupdate']);
|
||||
}
|
||||
|
||||
// =========================
|
||||
// DELETE
|
||||
// =========================
|
||||
public function delete($id)
|
||||
{
|
||||
// optional: cek relasi ke items
|
||||
$cek = $this->db->get_where('assets',['lokasi_asset_id'=>$id])->num_rows();
|
||||
|
||||
if($cek > 0){
|
||||
echo json_encode([
|
||||
'status'=>false,
|
||||
'message'=>'Gudang tidak bisa dihapus karena masih dipakai item'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$this->db->delete('lokasi_asset',['id'=>$id]);
|
||||
|
||||
log_activity(
|
||||
'kode_barang',
|
||||
'delete',
|
||||
'Delete gudang '.$nama,
|
||||
'success'
|
||||
);
|
||||
|
||||
echo json_encode(['status'=>true,'message'=>'Gudang berhasil dihapus']);
|
||||
}
|
||||
|
||||
|
||||
public function list()
|
||||
{
|
||||
echo json_encode(
|
||||
$this->db->get('lokasi_asset')->result()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
class Lookups extends MY_Controller
|
||||
{
|
||||
public function search($type){
|
||||
$map=array('customer'=>array('invoices','customers','id','nama',null),'item'=>array('items','items','id','nama_barang','kode_barang'),'account'=>array('jurnal','accounts','id','nama_akun','kode_akun'),'employee'=>array('employees','k_employees','id','full_name','employee_code'),'supplier'=>array('purchases','suppliers','id','name','supplier_code'));
|
||||
if(!isset($map[$type]))show_404();list($feature,$table,$id,$label,$code)=$map[$type];$this->requirePermission($feature);$term=trim((string)$this->input->get('q',true));$page=max(1,(int)$this->input->get('page'));$limit=min(30,max(10,(int)$this->input->get('limit')?:20));$hasCode=$code&&$this->db->field_exists($code,$table);$this->db->select("$id id,$label label".($hasCode?",$code code":''))->from($table);if($this->db->field_exists('company_id',$table))$this->db->where('company_id',$this->companycontext->id());if($this->db->field_exists('is_active',$table))$this->db->where('is_active',1);if($type==='account'&&$this->db->field_exists('allow_posting','accounts'))$this->db->where('allow_posting',1);if($term!==''){$this->db->group_start()->like($label,$term);if($hasCode)$this->db->or_like($code,$term);$this->db->group_end();}$rows=$this->db->order_by($label)->limit($limit+1,($page-1)*$limit)->get()->result_array();$more=count($rows)>$limit;if($more)array_pop($rows);$results=array_map(function($r){return array('id'=>$r['id'],'text'=>(isset($r['code'])&&$r['code']!==''?$r['code'].' · ':'').$r['label']);},$rows);$this->output->set_content_type('application/json')->set_output(json_encode(array('results'=>$results,'pagination'=>array('more'=>$more))));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
class Maintenanceworker extends CI_Controller
|
||||
{
|
||||
public function retention($days=365,$batch=5000){if(!$this->input->is_cli_request())show_404();$days=max(30,(int)$days);$batch=min(20000,max(100,(int)$batch));$cutoff=date('Y-m-d H:i:s',strtotime('-'.$days.' days'));$ids=$this->db->select('id')->where('created_at <',$cutoff)->order_by('id')->limit($batch)->get('activity_logs')->result_array();$count=count($ids);if($count){$list=array_column($ids,'id');$this->db->trans_start();$this->db->query('INSERT IGNORE INTO activity_logs_archive SELECT a.*,NOW() FROM activity_logs a WHERE a.id IN('.implode(',',array_map('intval',$list)).')');$this->db->where_in('id',$list)->delete('activity_logs');$this->db->trans_complete();if(!$this->db->trans_status())throw new RuntimeException('Arsip activity log gagal.');}$this->db->where('expires_at <',date('Y-m-d H:i:s'))->delete('app_cache');$this->db->where('timestamp <',time()-86400)->delete('ci_sessions');$this->db->where('status','completed')->where('completed_at <',date('Y-m-d H:i:s',strtotime('-30 days')))->delete('app_jobs');echo $count." activity log diarsipkan.\n";}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
|
||||
class Migrate extends CI_Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
if (!$this->input->is_cli_request()) {
|
||||
show_error('Migration hanya dapat dijalankan melalui CLI.', 403, 'Akses ditolak');
|
||||
}
|
||||
$this->load->library('migration');
|
||||
}
|
||||
|
||||
public function latest()
|
||||
{
|
||||
$version = $this->migration->latest();
|
||||
if ($version === false) {
|
||||
fwrite(STDERR, $this->migration->error_string() . PHP_EOL);
|
||||
exit(1);
|
||||
}
|
||||
if ($version === true) {
|
||||
$row = $this->db->order_by('version', 'DESC')->limit(1)->get('migrations')->row();
|
||||
$version = $row ? $row->version : 0;
|
||||
}
|
||||
echo 'Migration berhasil. Version: ' . $version . PHP_EOL;
|
||||
}
|
||||
|
||||
public function version($version = 0)
|
||||
{
|
||||
$result = $this->migration->version((int) $version);
|
||||
if ($result === false) {
|
||||
fwrite(STDERR, $this->migration->error_string() . PHP_EOL);
|
||||
exit(1);
|
||||
}
|
||||
echo 'Migration berhasil. Version: ' . $result . PHP_EOL;
|
||||
}
|
||||
|
||||
public function status()
|
||||
{
|
||||
$migration = $this->db->order_by('version', 'DESC')->limit(1)->get('migrations')->row();
|
||||
echo 'Version: ' . ($migration ? $migration->version : 0) . PHP_EOL;
|
||||
echo 'document_sequences: ' . ($this->db->table_exists('document_sequences') ? 'ready' : 'missing') . PHP_EOL;
|
||||
echo 'system_account_mappings: ' . ($this->db->table_exists('system_account_mappings') ? 'ready' : 'missing') . PHP_EOL;
|
||||
if ($this->db->table_exists('system_account_mappings')) {
|
||||
echo 'active_mappings: ' . $this->db->where('is_active', 1)->count_all_results('system_account_mappings') . PHP_EOL;
|
||||
}
|
||||
foreach (array('customer_equipment_registry','technician_equipment_documents','technician_custody_allocations','technician_equipment_document_lines','technician_equipment_inspections') as $table) {
|
||||
echo $table . ': ' . ($this->db->table_exists($table) ? 'ready (' . $this->db->count_all($table) . ' rows)' : 'missing') . PHP_EOL;
|
||||
}
|
||||
if ($this->db->table_exists('k_employees')) {
|
||||
echo 'k_employees.is_active: ' . ($this->db->field_exists('is_active','k_employees') ? 'yes' : 'no') . PHP_EOL;
|
||||
echo 'k_employees.user_id: ' . ($this->db->field_exists('user_id','k_employees') ? 'yes' : 'no') . PHP_EOL;
|
||||
}
|
||||
if ($this->db->table_exists('users')) echo 'users.employee_id: ' . ($this->db->field_exists('employee_id','users') ? 'yes' : 'no') . PHP_EOL;
|
||||
}
|
||||
|
||||
public function equipment_audit()
|
||||
{
|
||||
$checks=array(
|
||||
'negative_balances'=>"SELECT COUNT(*) total FROM technician_custody_allocations WHERE qty<=0",
|
||||
'duplicate_balance_keys'=>"SELECT COUNT(*) total FROM(SELECT balance_key FROM technician_custody_allocations GROUP BY balance_key HAVING COUNT(*)>1)x",
|
||||
'invalid_unit_quantities'=>"SELECT COUNT(*) total FROM technician_custody_allocations a JOIN item_barcodes b ON b.id=a.barcode_id JOIN items i ON i.id=b.item_id LEFT JOIN kode_barang k ON k.id=i.kode_id WHERE a.subject_type='company_barcode' AND UPPER(COALESCE(k.tracking_type,'QTY'))='UNIT' AND ABS(a.qty-1)>.0001",
|
||||
'overheld_company_qty'=>"SELECT COUNT(*) total FROM(SELECT a.barcode_id,SUM(a.qty) held,MAX(b.qty_sisa) qty_sisa FROM technician_custody_allocations a JOIN item_barcodes b ON b.id=a.barcode_id WHERE a.subject_type='company_barcode' GROUP BY a.barcode_id HAVING held>qty_sisa+.0001)x",
|
||||
'orphan_document_lines'=>"SELECT COUNT(*) total FROM technician_equipment_document_lines l LEFT JOIN technician_equipment_documents d ON d.id=l.document_id WHERE d.id IS NULL",
|
||||
'external_inventory_links'=>"SELECT COUNT(*) total FROM customer_equipment_registry r JOIN technician_custody_allocations a ON a.customer_equipment_id=r.id WHERE a.item_id IS NOT NULL OR a.barcode_id IS NOT NULL"
|
||||
);
|
||||
foreach($checks as$name=>$sql){$row=$this->db->query($sql)->row();echo$name.': '.(int)$row->total.PHP_EOL;}
|
||||
echo 'Audit bersifat read-only; nilai 0 berarti lulus.' . PHP_EOL;
|
||||
}
|
||||
|
||||
public function equipment_smoke()
|
||||
{
|
||||
$fixture=$this->db->query("SELECT b.id barcode_id,b.barcode,b.warehouse_id,i.company_id,COALESCE(k.tracking_type,'QTY') tracking_type,(b.qty_sisa-b.reserved_qty-b.return_reserved_qty) available FROM item_barcodes b JOIN items i ON i.id=b.item_id LEFT JOIN kode_barang k ON k.id=i.kode_id WHERE b.qty_sisa-b.reserved_qty-b.return_reserved_qty>=1 AND b.status='available' ORDER BY b.id LIMIT 1")->row();
|
||||
if(!$fixture){echo"SKIP: tidak ada barcode tersedia untuk smoke test.\n";return;}$employee=$this->db->where(array('company_id'=>$fixture->company_id,'is_active'=>1))->order_by('id')->get('k_employees')->row();$customer=$this->db->where('company_id',$fixture->company_id)->order_by('id')->get('customers')->row();$user=$this->db->order_by('id')->get('users')->row();if(!$employee||!$customer||!$user){echo"SKIP: fixture teknisi/customer/user tidak lengkap.\n";return;}
|
||||
$this->session->set_userdata(array('company_id'=>(int)$fixture->company_id,'user_id'=>(int)$user->id));$this->load->library('TechnicianEquipmentService');$service=$this->technicianequipmentservice;$prefix='SMOKE-'.date('YmdHis').'-'.bin2hex(random_bytes(3));$beforeJournal=$this->db->count_all('journals');$beforeStock=$this->db->count_all('stock_logs');$this->db->trans_begin(true);
|
||||
try{$qty=1;$handover=$service->process(array('action'=>'handover','subject_id'=>$fixture->barcode_id,'technician_id'=>$employee->id,'qty'=>$qty,'date'=>date('Y-m-d'),'notes'=>'[TEST ROLLBACK] Serah terima','idempotency_key'=>$prefix.'-H'),$user->id);$duplicate=$service->process(array('action'=>'handover','subject_id'=>$fixture->barcode_id,'technician_id'=>$employee->id,'qty'=>$qty,'date'=>date('Y-m-d'),'notes'=>'[TEST ROLLBACK] Serah terima','idempotency_key'=>$prefix.'-H'),$user->id);if(empty($duplicate['duplicate']))throw new RuntimeException('Idempotency gagal.');$held=$this->db->where(array('company_id'=>$fixture->company_id,'subject_type'=>'company_barcode','subject_id'=>$fixture->barcode_id,'custodian_type'=>'technician','custodian_id'=>$employee->id,'usage_status'=>'carried'))->where('qty >',0)->get('technician_custody_allocations')->row();if(!$held)throw new RuntimeException('Saldo teknisi tidak terbentuk.');$service->process(array('action'=>'install','allocation_id'=>$held->id,'customer_id'=>$customer->id,'qty'=>$qty,'date'=>date('Y-m-d'),'notes'=>'[TEST ROLLBACK] Pemasangan','idempotency_key'=>$prefix.'-I'),$user->id);$installed=$this->db->where(array('company_id'=>$fixture->company_id,'subject_type'=>'company_barcode','subject_id'=>$fixture->barcode_id,'custodian_type'=>'customer','customer_id'=>$customer->id,'usage_status'=>'installed'))->where('qty >',0)->get('technician_custody_allocations')->row();$service->process(array('action'=>'remove','allocation_id'=>$installed->id,'technician_id'=>$employee->id,'qty'=>$qty,'condition_status'=>'inspection_required','date'=>date('Y-m-d'),'notes'=>'[TEST ROLLBACK] Pelepasan','idempotency_key'=>$prefix.'-U'),$user->id);$removed=$this->db->where(array('company_id'=>$fixture->company_id,'subject_type'=>'company_barcode','subject_id'=>$fixture->barcode_id,'custodian_type'=>'technician','custodian_id'=>$employee->id,'usage_status'=>'removed'))->where('qty >',0)->get('technician_custody_allocations')->row();$service->process(array('action'=>'return','allocation_id'=>$removed->id,'warehouse_id'=>$fixture->warehouse_id,'qty'=>$qty,'date'=>date('Y-m-d'),'notes'=>'[TEST ROLLBACK] Kembali gudang','idempotency_key'=>$prefix.'-R'),$user->id);$queue=$this->db->where(array('company_id'=>$fixture->company_id,'subject_type'=>'company_barcode','subject_id'=>$fixture->barcode_id,'custodian_type'=>'quarantine','condition_status'=>'inspection_required'))->where('qty >',0)->get('technician_custody_allocations')->row();$service->inspect(array('allocation_id'=>$queue->id,'result_status'=>'good','qty'=>$qty,'date'=>date('Y-m-d'),'notes'=>'[TEST ROLLBACK] Layak','idempotency_key'=>$prefix.'-Q'),$user->id);$external=$service->registerExternal(array('owner_customer_id'=>$customer->id,'equipment_name'=>'TEST BARANG CUSTOMER','external_barcode'=>$prefix.'-EXT','tracking_type'=>'UNIT','qty'=>1),$user->id);if(!empty($external['item_id'])||!empty($external['barcode_id']))throw new RuntimeException('Barang customer terhubung ke inventory.');if($this->db->count_all('journals')!==$beforeJournal||$this->db->count_all('stock_logs')!==$beforeStock)throw new RuntimeException('Modul menulis jurnal/stock_logs.');echo'PASS: '.$fixture->tracking_type.' gudang-teknisi-customer-teknisi-karantina-available, idempotency, dan barang customer non-accounting.'.PHP_EOL;}catch(Throwable$e){echo'FAIL: '.$e->getMessage().PHP_EOL;$this->db->trans_rollback();exit(1);}$this->db->trans_rollback();echo'ROLLBACK: seluruh data smoke test dibatalkan.'.PHP_EOL;
|
||||
}
|
||||
|
||||
public function equipment_qty_smoke()
|
||||
{
|
||||
$f=$this->db->query("SELECT b.id barcode_id,b.warehouse_id,i.company_id,(b.qty_sisa-b.reserved_qty-b.return_reserved_qty) available FROM item_barcodes b JOIN items i ON i.id=b.item_id JOIN kode_barang k ON k.id=i.kode_id WHERE UPPER(k.tracking_type)='QTY' AND b.status='available' AND b.qty_sisa-b.reserved_qty-b.return_reserved_qty>=2 ORDER BY b.id LIMIT 1")->row();if(!$f){echo"SKIP: fixture barcode QTY dengan saldo >=2 tidak tersedia.\n";return;}$employee=$this->db->where(array('company_id'=>$f->company_id,'is_active'=>1))->order_by('id')->get('k_employees')->row();$customer=$this->db->where('company_id',$f->company_id)->order_by('id')->get('customers')->row();$user=$this->db->order_by('id')->get('users')->row();if(!$employee||!$customer||!$user){echo"SKIP: fixture tidak lengkap.\n";return;}$this->session->set_userdata(array('company_id'=>(int)$f->company_id,'user_id'=>(int)$user->id));$this->load->library('TechnicianEquipmentService');$s=$this->technicianequipmentservice;$key='SMOKE-QTY-'.bin2hex(random_bytes(5));$this->db->trans_begin(true);try{$s->process(array('action'=>'handover','subject_id'=>$f->barcode_id,'technician_id'=>$employee->id,'qty'=>2,'date'=>date('Y-m-d'),'notes'=>'[TEST ROLLBACK] Partial QTY','idempotency_key'=>$key.'-H'),$user->id);$held=$this->db->where(array('company_id'=>$f->company_id,'subject_id'=>$f->barcode_id,'custodian_type'=>'technician','custodian_id'=>$employee->id,'usage_status'=>'carried'))->get('technician_custody_allocations')->row();$s->process(array('action'=>'install','allocation_id'=>$held->id,'customer_id'=>$customer->id,'qty'=>1,'date'=>date('Y-m-d'),'notes'=>'[TEST ROLLBACK] Pasang sebagian','idempotency_key'=>$key.'-I'),$user->id);$carried=(float)$this->db->select_sum('qty')->where(array('company_id'=>$f->company_id,'subject_id'=>$f->barcode_id,'custodian_type'=>'technician','usage_status'=>'carried'))->get('technician_custody_allocations')->row()->qty;$installed=(float)$this->db->select_sum('qty')->where(array('company_id'=>$f->company_id,'subject_id'=>$f->barcode_id,'custodian_type'=>'customer','usage_status'=>'installed'))->get('technician_custody_allocations')->row()->qty;if($carried<1||$installed<1)throw new RuntimeException('Split saldo QTY tidak terbentuk.');echo"PASS: QTY berpindah dan dipasang sebagian tanpa saldo negatif.\n";}catch(Throwable$e){echo'FAIL: '.$e->getMessage().PHP_EOL;$this->db->trans_rollback();exit(1);}$this->db->trans_rollback();echo"ROLLBACK: data smoke QTY dibatalkan.\n";
|
||||
}
|
||||
|
||||
public function equipment_replacement_smoke()
|
||||
{
|
||||
$rows=$this->db->query("SELECT b.id barcode_id,b.warehouse_id,i.company_id FROM item_barcodes b JOIN items i ON i.id=b.item_id WHERE b.status='available' AND b.qty_sisa-b.reserved_qty-b.return_reserved_qty>=1 ORDER BY i.company_id,b.id LIMIT 2")->result();if(count($rows)<2||$rows[0]->company_id!=$rows[1]->company_id){echo"SKIP: dua fixture barcode pada perusahaan sama tidak tersedia.\n";return;}$company=(int)$rows[0]->company_id;$employee=$this->db->where(array('company_id'=>$company,'is_active'=>1))->order_by('id')->get('k_employees')->row();$customer=$this->db->where('company_id',$company)->order_by('id')->get('customers')->row();$user=$this->db->order_by('id')->get('users')->row();if(!$employee||!$customer||!$user){echo"SKIP: fixture tidak lengkap.\n";return;}$this->session->set_userdata(array('company_id'=>$company,'user_id'=>(int)$user->id));$this->load->library('TechnicianEquipmentService');$s=$this->technicianequipmentservice;$key='SMOKE-REPLACE-'.bin2hex(random_bytes(5));$this->db->trans_begin(true);
|
||||
try{foreach($rows as$i=>$row)$s->process(array('action'=>'handover','subject_id'=>$row->barcode_id,'technician_id'=>$employee->id,'qty'=>1,'date'=>date('Y-m-d'),'notes'=>'[TEST ROLLBACK] Barang penggantian','idempotency_key'=>$key.'-H'.$i),$user->id);$old=$this->db->where(array('company_id'=>$company,'subject_id'=>$rows[0]->barcode_id,'custodian_type'=>'technician','custodian_id'=>$employee->id,'usage_status'=>'carried'))->get('technician_custody_allocations')->row();$s->process(array('action'=>'install','allocation_id'=>$old->id,'customer_id'=>$customer->id,'qty'=>1,'date'=>date('Y-m-d'),'notes'=>'[TEST ROLLBACK] Instalasi lama','idempotency_key'=>$key.'-OLD'),$user->id);$oldInstalled=$this->db->where(array('company_id'=>$company,'subject_id'=>$rows[0]->barcode_id,'custodian_type'=>'customer','customer_id'=>$customer->id,'usage_status'=>'installed'))->get('technician_custody_allocations')->row();$new=$this->db->where(array('company_id'=>$company,'subject_id'=>$rows[1]->barcode_id,'custodian_type'=>'technician','custodian_id'=>$employee->id,'usage_status'=>'carried'))->get('technician_custody_allocations')->row();$doc=$s->replacement(array('customer_id'=>$customer->id,'technician_id'=>$employee->id,'outgoing_allocation_id'=>$new->id,'outgoing_qty'=>1,'incoming_allocation_id'=>$oldInstalled->id,'incoming_qty'=>1,'old_condition'=>'inspection_required','date'=>date('Y-m-d'),'reason'=>'[TEST ROLLBACK] Penggantian','notes'=>'[TEST ROLLBACK] Atomic replacement','idempotency_key'=>$key.'-DOC'),$user->id);$lineCount=$this->db->where('document_id',$doc['document_id'])->count_all_results('technician_equipment_document_lines');if($lineCount!==2)throw new RuntimeException('Dokumen penggantian tidak memiliki dua sisi barang.');$reversal=$s->reverse(array('document_id'=>$doc['document_id'],'date'=>date('Y-m-d'),'reason'=>'[TEST ROLLBACK] Koreksi penggantian','idempotency_key'=>$key.'-REV'),$user->id);$source=$this->db->get_where('technician_equipment_documents',array('id'=>$doc['document_id']))->row();if(!$source||$source->status!=='reversed'||$this->db->where('document_id',$reversal['document_id'])->count_all_results('technician_equipment_document_lines')!==2)throw new RuntimeException('Reversal tidak membentuk histori kebalikan.');echo"PASS: penggantian atomik dan koreksi membuat reversal tanpa menghapus histori.\n";}catch(Throwable$e){echo'FAIL: '.$e->getMessage().PHP_EOL;$this->db->trans_rollback();exit(1);}$this->db->trans_rollback();echo"ROLLBACK: data smoke penggantian dibatalkan.\n";
|
||||
}
|
||||
|
||||
public function equipment_query_smoke()
|
||||
{
|
||||
$access=$this->db->order_by('is_default','DESC')->order_by('user_id')->get('user_companies')->row();
|
||||
if(!$access){echo"SKIP: akses user ke perusahaan belum tersedia.\n";return;}
|
||||
$company=(int)$access->company_id;
|
||||
$document=$this->db->where('company_id',$company)->order_by('id')->get('technician_equipment_documents')->row();
|
||||
$this->session->set_userdata(array('company_id'=>$company,'user_id'=>(int)$access->user_id));
|
||||
$this->load->library('TechnicianEquipmentService');
|
||||
try{
|
||||
$dashboard=$this->technicianequipmentservice->dashboard();
|
||||
$allocations=$this->technicianequipmentservice->allocations();
|
||||
$documents=$this->technicianequipmentservice->documents();
|
||||
$history=$this->technicianequipmentservice->history();
|
||||
$queue=$this->technicianequipmentservice->inspectionQueue();
|
||||
$external=$this->technicianequipmentservice->externalList();
|
||||
$customerSummaries=$this->technicianequipmentservice->customerEquipmentSummary();
|
||||
if($customerSummaries){foreach(array('all','installed','stored','technician','inspection','good','damaged')as$status)$this->technicianequipmentservice->customerEquipmentRows((int)$customerSummaries[0]['customer_id'],$status);$this->technicianequipmentservice->customerEquipmentNotes((int)$customerSummaries[0]['customer_id']);}
|
||||
$employee=$this->db->where(array('company_id'=>$company,'is_active'=>1))->order_by('id')->get('k_employees')->row();
|
||||
if($employee){$this->technicianequipmentservice->allocations(null,null,$employee->id);$this->technicianequipmentservice->technicianHoldings($employee->id);$this->technicianequipmentservice->documents(array('technician_id'=>$employee->id));$this->technicianequipmentservice->history(array('technician_id'=>$employee->id));$this->technicianequipmentservice->inspectionQueue(array('technician_id'=>$employee->id));$this->technicianequipmentservice->externalList($employee->id);$this->technicianequipmentservice->search('a','all',$employee->id);$this->technicianequipmentservice->search('a','handover',$employee->id);$this->technicianequipmentservice->search('a','transfer',$employee->id);$this->technicianequipmentservice->search('a','return',$employee->id);$this->technicianequipmentservice->search('a','customer',$employee->id);}
|
||||
if($document)$this->technicianequipmentservice->document($document->id);
|
||||
$barcode=$this->db->select('b.barcode')->from('item_barcodes b')->join('items i','i.id=b.item_id')->where('i.company_id',$company)->order_by('b.id')->get()->row();
|
||||
if($barcode)$this->technicianequipmentservice->scan($barcode->barcode);
|
||||
echo 'PASS: query dashboard, scan, saldo, dokumen, histori, pemeriksaan, dan register customer berhasil.' . PHP_EOL;
|
||||
echo 'ROWS: allocations=' . count($allocations) . ', documents=' . count($documents) . ', history=' . count($history) . ', inspection=' . count($queue) . ', external=' . count($external) . ', customer_groups=' . count($customerSummaries) . PHP_EOL;
|
||||
}catch(Throwable $e){fwrite(STDERR,'FAIL: '.$e->getMessage().PHP_EOL);exit(1);}
|
||||
}
|
||||
|
||||
public function equipment_batch_smoke()
|
||||
{
|
||||
$fixtures=$this->db->query("SELECT b.id barcode_id,b.warehouse_id,i.company_id FROM item_barcodes b JOIN items i ON i.id=b.item_id WHERE b.status='available' AND b.qty_sisa-b.reserved_qty-b.return_reserved_qty>=1 ORDER BY i.company_id,b.id LIMIT 2")->result();
|
||||
if(count($fixtures)<2||$fixtures[0]->company_id!=$fixtures[1]->company_id){echo"SKIP: dua barcode tersedia pada perusahaan yang sama tidak ditemukan.\n";return;}
|
||||
$company=(int)$fixtures[0]->company_id;$employees=$this->db->where(array('company_id'=>$company,'is_active'=>1))->order_by('id')->limit(2)->get('k_employees')->result();$access=$this->db->where('company_id',$company)->order_by('is_default','DESC')->get('user_companies')->row();if(count($employees)<2||!$access){echo"SKIP: dua teknisi atau akses perusahaan belum tersedia.\n";return;}
|
||||
$this->session->set_userdata(array('company_id'=>$company,'user_id'=>(int)$access->user_id));$this->load->library('TechnicianEquipmentService');$service=$this->technicianequipmentservice;$prefix='SMOKE-BATCH-'.bin2hex(random_bytes(5));$items=array_map(function($row){return array('subject_id'=>(int)$row->barcode_id,'qty'=>1);},$fixtures);$this->db->trans_begin(true);
|
||||
try{$handover=$service->processBatch(array('action'=>'handover','technician_id'=>$employees[0]->id,'date'=>date('Y-m-d'),'notes'=>'[TEST ROLLBACK] Serah terima batch','items'=>$items,'idempotency_key'=>$prefix.'-H'),$access->user_id);$duplicate=$service->processBatch(array('action'=>'handover','technician_id'=>$employees[0]->id,'date'=>date('Y-m-d'),'notes'=>'[TEST ROLLBACK] Serah terima batch','items'=>$items,'idempotency_key'=>$prefix.'-H'),$access->user_id);if(empty($duplicate['duplicate'])||(int)$handover['line_count']!==2)throw new RuntimeException('Dokumen batch atau idempotency tidak valid.');$allocations=array();foreach($fixtures as$fixture){$allocation=$this->db->where(array('company_id'=>$company,'subject_type'=>'company_barcode','subject_id'=>$fixture->barcode_id,'custodian_type'=>'technician','custodian_id'=>$employees[0]->id))->where('qty >',0)->get('technician_custody_allocations')->row();if(!$allocation)throw new RuntimeException('Saldo batch teknisi tidak terbentuk.');$allocations[]=array('allocation_id'=>(int)$allocation->id,'qty'=>1);}$transfer=$service->processBatch(array('action'=>'transfer','technician_id'=>$employees[0]->id,'target_technician_id'=>$employees[1]->id,'date'=>date('Y-m-d'),'notes'=>'[TEST ROLLBACK] Transfer batch','items'=>$allocations,'idempotency_key'=>$prefix.'-T'),$access->user_id);if((int)$transfer['line_count']!==2)throw new RuntimeException('Transfer batch tidak lengkap.');$returns=array();foreach($fixtures as$fixture){$allocation=$this->db->where(array('company_id'=>$company,'subject_type'=>'company_barcode','subject_id'=>$fixture->barcode_id,'custodian_type'=>'technician','custodian_id'=>$employees[1]->id))->where('qty >',0)->get('technician_custody_allocations')->row();if(!$allocation)throw new RuntimeException('Saldo hasil transfer tidak ditemukan.');$returns[]=array('allocation_id'=>(int)$allocation->id,'qty'=>1);}$returned=$service->processBatch(array('action'=>'return','technician_id'=>$employees[1]->id,'warehouse_id'=>$fixtures[0]->warehouse_id,'date'=>date('Y-m-d'),'notes'=>'[TEST ROLLBACK] Kembali batch','items'=>$returns,'idempotency_key'=>$prefix.'-R'),$access->user_id);if((int)$returned['line_count']!==2)throw new RuntimeException('Pengembalian batch tidak lengkap.');echo"PASS: dua barang diserahkan, ditransfer, dan dikembalikan secara batch; double submit tetap idempotent.\n";}catch(Throwable$e){echo'FAIL: '.$e->getMessage().PHP_EOL;$this->db->trans_rollback();exit(1);}$this->db->trans_rollback();echo"ROLLBACK: data smoke batch dibatalkan.\n";
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
|
||||
class Neraca extends CI_Controller {
|
||||
class Neraca extends MY_Admin_Controller {
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
@@ -39,6 +39,7 @@ class Neraca extends CI_Controller {
|
||||
$this->db->from('accounts');
|
||||
$this->db->join('journal_details jd','jd.account_id = accounts.id','LEFT');
|
||||
$this->db->join('journals j','j.id = jd.journal_id','LEFT');
|
||||
$this->db->where("(j.status IN ('posted','reversed') OR jd.id IS NULL)", null, false);
|
||||
|
||||
$this->db->where_in('accounts.tipe',['revenue','expense']);
|
||||
|
||||
@@ -88,6 +89,7 @@ class Neraca extends CI_Controller {
|
||||
$this->db->from('accounts a');
|
||||
$this->db->join('journal_details jd','jd.account_id = a.id','LEFT');
|
||||
$this->db->join('journals j','j.id = jd.journal_id','LEFT');
|
||||
$this->db->where("(j.status IN ('posted','reversed') OR jd.id IS NULL)", null, false);
|
||||
|
||||
$this->db->where('a.kategori','neraca');
|
||||
|
||||
@@ -129,7 +131,7 @@ class Neraca extends CI_Controller {
|
||||
}
|
||||
|
||||
// ================= LABA BERJALAN
|
||||
if ($r->kode_akun == '302') {
|
||||
if ($r->kode_akun == '3020') {
|
||||
$saldo = $laba;
|
||||
}
|
||||
|
||||
@@ -167,4 +169,4 @@ class Neraca extends CI_Controller {
|
||||
'selisih' => $total_activa - $total_pasiva
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
|
||||
class Neracasaldo extends CI_Controller {
|
||||
class Neracasaldo extends MY_Admin_Controller {
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
@@ -49,7 +49,8 @@ class Neracasaldo extends CI_Controller {
|
||||
"(SELECT jd.*
|
||||
FROM journal_details jd
|
||||
JOIN journals j ON j.id = jd.journal_id
|
||||
WHERE j.tanggal BETWEEN ".$this->db->escape($tanggal_awal)."
|
||||
WHERE j.status IN ('posted','reversed')
|
||||
AND j.tanggal BETWEEN ".$this->db->escape($tanggal_awal)."
|
||||
AND ".$this->db->escape($tanggal_akhir)."
|
||||
) journal_details",
|
||||
"journal_details.account_id = accounts.id",
|
||||
@@ -76,4 +77,4 @@ class Neracasaldo extends CI_Controller {
|
||||
|
||||
echo json_encode($rows);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
class Organizationmaster extends MY_Controller
|
||||
{
|
||||
public function index(){$d=array('active_menu'=>'organization_master');$this->load->view('partials/header',$d);$this->load->view('hr_payroll/organization_master',$d);$this->load->view('partials/footer');}
|
||||
public function data(){return json_response(true,'Master organisasi.',array('departments'=>$this->db->order_by('department_name')->get('k_departments')->result(),'positions'=>$this->db->select('p.*,d.department_name')->from('k_positions p')->join('k_departments d','d.id=p.department_id','left')->order_by('p.position_name')->get()->result()));}
|
||||
public function save_department(){return$this->saveMaster('k_departments','department_name');}
|
||||
public function save_position(){return$this->saveMaster('k_positions','position_name',true);}
|
||||
private function saveMaster($table,$nameField,$position=false){$id=(int)$this->input->post('id');$code=strtoupper(trim($this->input->post('code',true)));$name=trim($this->input->post('name',true));if($code===''||$name==='')return json_response(false,'Kode dan nama wajib diisi.',array(),422);$q=$this->db->where('code',$code);if($id)$q->where('id !=',$id);if($q->count_all_results($table))return json_response(false,'Kode sudah digunakan.',array(),422);$data=array('code'=>$code,$nameField=>$name,'description'=>trim($this->input->post('description',true))?:null,'is_active'=>$this->input->post('is_active')?1:0);if($position)$data['department_id']=$this->input->post('department_id')?:null;if($id)$this->db->where('id',$id)->update($table,$data);else{$this->db->insert($table,$data);$id=$this->db->insert_id();}return json_response(true,'Data berhasil disimpan.',array('id'=>$id));}
|
||||
public function delete_department($id){return$this->deactivate('k_departments',$id,'department_id');}
|
||||
public function delete_position($id){return$this->deactivate('k_positions',$id,'position_id');}
|
||||
private function deactivate($table,$id,$employeeField){if($this->db->where($employeeField,(int)$id)->count_all_results('k_employees')){$this->db->where('id',(int)$id)->update($table,array('is_active'=>0));return json_response(true,'Data sudah digunakan karyawan, sehingga dinonaktifkan dan tidak dihapus.');}$this->db->where('id',(int)$id)->delete($table);return json_response(true,'Data berhasil dihapus.');}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
class Payablecheck extends CI_Controller
|
||||
{
|
||||
public function __construct(){parent::__construct();if(!is_cli())show_404();}
|
||||
public function schema(){foreach(array('items','item_barcodes','warehouses','stock_logs','item_movements','accounts')as$t){echo"---$t---\n";foreach($this->db->field_data($t)as$f)echo$f->name.' | '.$f->type.' | '.$f->max_length."\n";}echo'legacy_purchase_movements='.$this->db->where('movement_type','purchase')->count_all_results('item_movements')."\n";echo'legacy_purchase_journals='.$this->db->where_in('ref_type',array('new_items','stock_logs'))->count_all_results('journals')."\n";}
|
||||
public function accounts(){foreach($this->db->group_start()->like('nama_akun','hutang')->or_like('nama_akun','utang')->or_like('nama_akun','pembelian')->or_like('nama_akun','persediaan')->group_end()->order_by('kode_akun')->get('accounts')->result()as$a)echo$a->id.' | '.$a->kode_akun.' | '.$a->nama_akun."\n";}
|
||||
public function integrity(){$checks=array('duplicate_supplier_invoice'=>"SELECT COUNT(*) total FROM(SELECT supplier_id,supplier_invoice_no FROM supplier_invoices GROUP BY supplier_id,supplier_invoice_no HAVING COUNT(*)>1)x",'over_received_po'=>"SELECT COUNT(*) total FROM purchase_order_lines WHERE received_qty>qty+0.0001",'over_invoiced_po'=>"SELECT COUNT(*) total FROM purchase_order_lines WHERE invoiced_qty>received_qty+0.0001",'overpaid_supplier_invoice'=>"SELECT COUNT(*) total FROM supplier_invoices WHERE paid_amount>total+0.01 OR balance<0",'overallocated_supplier_payment'=>"SELECT COUNT(*) total FROM(SELECT p.id FROM supplier_payments p JOIN supplier_payment_allocations a ON a.supplier_payment_id=p.id GROUP BY p.id,p.amount HAVING SUM(a.amount)>p.amount+0.01)x",'duplicate_receipt_stock'=>"SELECT COUNT(*) total FROM(SELECT ref_id,item_id FROM stock_logs WHERE ref_type='goods_receipt' GROUP BY ref_id,item_id HAVING COUNT(*)>1)x",'unbalanced_purchase_journals'=>"SELECT COUNT(*) total FROM(SELECT j.id FROM journals j JOIN journal_details d ON d.journal_id=j.id WHERE j.ref_type IN('supplier_invoice','supplier_payment','supplier_debit_note','supplier_opening_balance') GROUP BY j.id HAVING ABS(SUM(d.debit)-SUM(d.kredit))>0.01)x");$fail=0;foreach($checks as$n=>$q){$v=(int)$this->db->query($q)->row()->total;echo($v?'[FAIL] ':'[OK] ').$n.'='.$v."\n";if($v)$fail++;}foreach(array('accounts_payable','purchase_input_tax','supplier_advances','payable_opening_balance')as$k){$v=$this->db->where(array('mapping_key'=>$k,'is_active'=>1))->count_all_results('system_account_mappings');echo($v?'[OK] ':'[FAIL] ').'mapping_'.$k."\n";if(!$v)$fail++;}if($fail)exit(1);}
|
||||
public function invoice_payment_values(){$q="SELECT si.id,si.internal_no,si.status,si.total invoice_total,si.paid_amount invoice_paid,si.balance,po.id po_id,po.po_no,po.subtotal,po.tax_total,po.shipping_cost,po.insurance_cost,po.service_fee,po.discount_amount,po.cashback_amount,po.total po_total,po.paid_amount po_paid,(SELECT COALESCE(SUM(pol2.line_total),0) FROM purchase_order_lines pol2 WHERE pol2.purchase_order_id=po.id) item_total,(SELECT COALESCE(SUM(sp.amount),0) FROM supplier_payments sp WHERE sp.purchase_order_id=po.id AND sp.status='posted') payment_total FROM supplier_invoices si JOIN supplier_invoice_lines sil ON sil.supplier_invoice_id=si.id JOIN purchase_order_lines pol ON pol.id=sil.po_line_id JOIN purchase_orders po ON po.id=pol.purchase_order_id GROUP BY si.id ORDER BY si.id DESC";foreach($this->db->query($q)->result()as$r)echo json_encode($r,JSON_UNESCAPED_UNICODE)."\n";}
|
||||
}
|
||||
@@ -0,0 +1,331 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
|
||||
class Payables extends MY_Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->load->library('PurchaseService');
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$companyId = $this->companycontext->id();
|
||||
|
||||
$this->db->select('s.*')->from('suppliers s');
|
||||
if ($this->db->field_exists('company_id', 'suppliers')) {
|
||||
$this->db->where('s.company_id', $companyId);
|
||||
}
|
||||
$this->db->group_start()
|
||||
->where('s.is_active', 1)
|
||||
->or_where(
|
||||
"EXISTS(SELECT 1 FROM supplier_invoices si WHERE si.supplier_id=s.id AND si.status='partial' AND si.balance>0".
|
||||
($this->db->field_exists('company_id', 'supplier_invoices') ? ' AND si.company_id='.(int)$companyId : '').')',
|
||||
null,
|
||||
false
|
||||
)
|
||||
->group_end();
|
||||
$suppliers = $this->db->order_by('s.name')->get()->result();
|
||||
|
||||
$this->db->from('accounts')->where(array('is_active'=>1, 'allow_posting'=>1));
|
||||
if ($this->db->field_exists('company_id', 'accounts')) {
|
||||
$this->db->where('company_id', $companyId);
|
||||
}
|
||||
$accounts = $this->db
|
||||
->group_start()->like('nama_akun', 'kas')->or_like('nama_akun', 'bank')->group_end()
|
||||
->order_by('kode_akun')->get()->result();
|
||||
|
||||
$this->db->from('accounts')->where(array('tipe'=>'expense', 'is_active'=>1, 'allow_posting'=>1));
|
||||
if ($this->db->field_exists('company_id', 'accounts')) {
|
||||
$this->db->where('company_id', $companyId);
|
||||
}
|
||||
$bankChargeAccounts = $this->db->order_by('kode_akun')->get()->result();
|
||||
$mapping = $this->db->get_where('system_account_mappings', array(
|
||||
'mapping_key' => 'bank_charge_expense',
|
||||
'is_active' => 1,
|
||||
))->row();
|
||||
|
||||
$data = array(
|
||||
'active_menu' => 'payables',
|
||||
'aging' => $this->purchaseservice->aging($this->input->get('as_of')),
|
||||
'suppliers' => $suppliers,
|
||||
'accounts' => $accounts,
|
||||
'bank_charge_accounts' => $bankChargeAccounts,
|
||||
'bank_charge_account_id' => $mapping ? (int)$mapping->account_id : 0,
|
||||
);
|
||||
$this->load->view('partials/header', $data);
|
||||
$this->load->view('payables/index', $data);
|
||||
$this->load->view('partials/footer');
|
||||
}
|
||||
|
||||
public function open_bills($supplier)
|
||||
{
|
||||
$supplier = (int)$supplier;
|
||||
$companyId = $this->companycontext->id();
|
||||
|
||||
$this->db
|
||||
->select("si.id,si.internal_no,si.supplier_invoice_no,si.invoice_date,si.due_date,si.total,si.paid_amount,si.balance,si.status,si.payment_plan,CASE WHEN si.due_date<=CURDATE() THEN 'overdue' ELSE 'upcoming' END due_state,po.id po_id,po.po_no,po.order_date,po.payment_timing,po.total po_total,po.paid_amount po_paid_amount,po.subtotal po_subtotal,po.tax_total po_tax_total,po.shipping_cost,po.insurance_cost,po.service_fee,po.discount_amount,po.cashback_amount,GROUP_CONCAT(DISTINCT gr.receipt_no ORDER BY gr.receipt_date,gr.id SEPARATOR ', ') receipt_no", false)
|
||||
->from('supplier_invoices si')
|
||||
->join('supplier_invoice_lines sil', 'sil.supplier_invoice_id=si.id', 'left')
|
||||
->join('purchase_order_lines pol', 'pol.id=sil.po_line_id', 'left')
|
||||
->join('purchase_orders po', 'po.id=pol.purchase_order_id', 'left')
|
||||
->join('goods_receipt_lines grl', 'grl.id=sil.receipt_line_id', 'left')
|
||||
->join('goods_receipts gr', 'gr.id=grl.goods_receipt_id', 'left')
|
||||
->where('si.supplier_id', $supplier)
|
||||
->where('si.status', 'partial')
|
||||
->where('si.balance >', 0);
|
||||
if ($this->db->field_exists('company_id', 'supplier_invoices')) {
|
||||
$this->db->where('si.company_id', $companyId);
|
||||
}
|
||||
$bills = $this->db->group_by('si.id')->order_by('si.due_date')->get()->result();
|
||||
|
||||
foreach ($bills as $bill) {
|
||||
$bill->items = $this->db
|
||||
->select('kb.kode_barang,i.nama_barang,sil.qty,sil.unit_price,sil.tax_amount,sil.line_total')
|
||||
->from('supplier_invoice_lines sil')
|
||||
->join('items i', 'i.id=sil.item_id', 'left')
|
||||
->join('kode_barang kb', 'kb.id=i.kode_id', 'left')
|
||||
->where('sil.supplier_invoice_id', $bill->id)
|
||||
->get()->result();
|
||||
$bill->installment_schedules = $bill->payment_timing === 'installment'
|
||||
? $this->db->where('purchase_order_id', $bill->po_id)->order_by('installment_no')->get('purchase_installment_schedules')->result()
|
||||
: array();
|
||||
}
|
||||
|
||||
$this->db
|
||||
->select('si.id,si.internal_no,si.supplier_invoice_no,si.balance,si.status,po.po_no')
|
||||
->from('supplier_invoices si')
|
||||
->join('supplier_invoice_lines sil', 'sil.supplier_invoice_id=si.id', 'left')
|
||||
->join('purchase_order_lines pol', 'pol.id=sil.po_line_id', 'left')
|
||||
->join('purchase_orders po', 'po.id=pol.purchase_order_id', 'left')
|
||||
->where('si.supplier_id', $supplier)
|
||||
->where('si.status', 'verified')
|
||||
->where('si.balance >', 0);
|
||||
if ($this->db->field_exists('company_id', 'supplier_invoices')) {
|
||||
$this->db->where('si.company_id', $companyId);
|
||||
}
|
||||
$pending = $this->db->group_by('si.id')->order_by('si.due_date')->get()->result();
|
||||
|
||||
$this->db
|
||||
->select('sp.id,sp.payment_no,sp.payment_date,sp.amount,sp.bank_charge_amount,sp.total_cash_out,sp.reference_no,spa.amount allocated_amount,si.internal_no,si.supplier_invoice_no')
|
||||
->from('supplier_payments sp')
|
||||
->join('supplier_payment_allocations spa', 'spa.supplier_payment_id=sp.id', 'left')
|
||||
->join('supplier_invoices si', 'si.id=spa.supplier_invoice_id', 'left')
|
||||
->where('sp.supplier_id', $supplier)
|
||||
->where('sp.status', 'posted');
|
||||
if ($this->db->field_exists('company_id', 'supplier_payments')) {
|
||||
$this->db->where('sp.company_id', $companyId);
|
||||
}
|
||||
$history = $this->db->order_by('sp.payment_date', 'DESC')->order_by('sp.id', 'DESC')->limit(100)->get()->result();
|
||||
foreach ($history as $row) {
|
||||
$row->sources = $this->db
|
||||
->select('pps.amount,a.kode_akun,a.nama_akun')
|
||||
->from('purchase_payment_sources pps')
|
||||
->join('accounts a', 'a.id=pps.account_id')
|
||||
->where('pps.supplier_payment_id', $row->id)
|
||||
->get()->result();
|
||||
}
|
||||
|
||||
return json_response(true, 'Tagihan dan riwayat supplier.', array(
|
||||
'bills' => $bills,
|
||||
'pending' => $pending,
|
||||
'history' => $history,
|
||||
));
|
||||
}
|
||||
|
||||
public function payment()
|
||||
{
|
||||
if (strtoupper($this->input->method()) !== 'POST') {
|
||||
show_404();
|
||||
}
|
||||
$storedFiles = array();
|
||||
try {
|
||||
$this->validatePaymentEvidence('payment_documents');
|
||||
$accounts = (array)$this->input->post('source_account_id');
|
||||
$amounts = (array)$this->input->post('source_amount');
|
||||
$sources = array();
|
||||
foreach ($accounts as $index => $accountId) {
|
||||
if ((int)$accountId) {
|
||||
$sources[(int)$accountId] = ($sources[(int)$accountId] ?? 0) + $this->parseAmount($amounts[$index] ?? 0);
|
||||
}
|
||||
}
|
||||
$allocations = array();
|
||||
foreach ((array)$this->input->post('allocations') as $invoiceId => $amount) {
|
||||
$allocations[(int)$invoiceId] = $this->parseAmount($amount);
|
||||
}
|
||||
// Transaksi luar memastikan pembayaran, jurnal, alokasi, dan metadata
|
||||
// bukti pembayaran berhasil atau gagal sebagai satu kesatuan.
|
||||
$this->db->trans_begin();
|
||||
$result = $this->purchaseservice->paymentMultiWithBankCharge(
|
||||
(int)$this->input->post('supplier_id'),
|
||||
$this->input->post('date'),
|
||||
$sources,
|
||||
$allocations,
|
||||
(bool)$this->input->post('allow_advance'),
|
||||
(int)$this->session->userdata('user_id'),
|
||||
$this->input->post('reference_no', true),
|
||||
$this->parseAmount($this->input->post('bank_charge_amount')),
|
||||
(int)$this->input->post('bank_charge_account_id')
|
||||
);
|
||||
$storedFiles = $this->storePaymentEvidence($result['payment_id'], 'payment_documents');
|
||||
if ($this->db->trans_status() === false) {
|
||||
throw new BusinessException('Pembayaran dan bukti pembayaran gagal disimpan secara lengkap.');
|
||||
}
|
||||
$this->db->trans_commit();
|
||||
return json_response(true, 'Pembayaran supplier berhasil diposting.', array('result'=>$result));
|
||||
} catch (Throwable $e) {
|
||||
if ($this->db->trans_depth() > 0) {
|
||||
$this->db->trans_rollback();
|
||||
}
|
||||
foreach ($storedFiles as $path) {
|
||||
if (is_file($path)) @unlink($path);
|
||||
}
|
||||
return business_exception_response($e);
|
||||
}
|
||||
}
|
||||
|
||||
private function validatePaymentEvidence($input)
|
||||
{
|
||||
$files = $_FILES[$input] ?? array();
|
||||
$names = array_values(array_filter((array)($files['name'] ?? array()), function ($name) {
|
||||
return trim((string)$name) !== '';
|
||||
}));
|
||||
if (!$names) {
|
||||
throw new BusinessException('Bukti pembayaran wajib diunggah sebelum pembayaran diposting.');
|
||||
}
|
||||
if (count($names) > 10) {
|
||||
throw new BusinessException('Maksimal 10 bukti pembayaran untuk satu transaksi.');
|
||||
}
|
||||
$extensions = array('pdf','jpg','jpeg','jpe','jfif','png','webp','gif','bmp','tif','tiff','heic','heif','avif','jp2','j2k','jpf','jpg2','jpx','ico');
|
||||
foreach ((array)$files['name'] as $index => $name) {
|
||||
if (trim((string)$name) === '') continue;
|
||||
$error = (int)($files['error'][$index] ?? UPLOAD_ERR_NO_FILE);
|
||||
if ($error !== UPLOAD_ERR_OK) {
|
||||
throw new BusinessException(
|
||||
in_array($error, array(UPLOAD_ERR_INI_SIZE, UPLOAD_ERR_FORM_SIZE), true)
|
||||
? 'Ukuran file '.$name.' melebihi batas server.'
|
||||
: 'File '.$name.' gagal diterima oleh server (kode '.$error.').'
|
||||
);
|
||||
}
|
||||
if ((int)($files['size'][$index] ?? 0) > 5 * 1024 * 1024) {
|
||||
throw new BusinessException('Ukuran file '.$name.' melebihi 5 MB.');
|
||||
}
|
||||
$extension = strtolower(pathinfo($name, PATHINFO_EXTENSION));
|
||||
if (!in_array($extension, $extensions, true)) {
|
||||
throw new BusinessException('Format .'.$extension.' tidak diizinkan. Gunakan PDF atau file gambar.');
|
||||
}
|
||||
$temporary = $files['tmp_name'][$index] ?? '';
|
||||
$mime = $temporary && is_file($temporary) && function_exists('finfo_open')
|
||||
? (new finfo(FILEINFO_MIME_TYPE))->file($temporary)
|
||||
: ($files['type'][$index] ?? '');
|
||||
if ($mime !== 'application/pdf' && strpos((string)$mime, 'image/') !== 0) {
|
||||
throw new BusinessException('Isi file '.$name.' bukan PDF atau gambar yang valid.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function storePaymentEvidence($paymentId, $input)
|
||||
{
|
||||
if (!(int)$paymentId) {
|
||||
throw new BusinessException('Nomor pembayaran tidak valid untuk penyimpanan bukti.');
|
||||
}
|
||||
$files = $_FILES[$input];
|
||||
$directory = FCPATH.'uploads/accounting/';
|
||||
if (!is_dir($directory) && !mkdir($directory, 0750, true)) {
|
||||
throw new BusinessException('Folder bukti pembayaran tidak tersedia.');
|
||||
}
|
||||
|
||||
$this->load->library('upload');
|
||||
$stored = array();
|
||||
try {
|
||||
foreach ((array)$files['name'] as $index => $name) {
|
||||
if (trim((string)$name) === '') continue;
|
||||
$_FILES['attachment'] = array(
|
||||
'name' => $name,
|
||||
'type' => $files['type'][$index],
|
||||
'tmp_name' => $files['tmp_name'][$index],
|
||||
'error' => $files['error'][$index],
|
||||
'size' => $files['size'][$index],
|
||||
);
|
||||
$this->upload->initialize(array(
|
||||
'upload_path' => $directory,
|
||||
'allowed_types' => 'pdf|jpg|jpeg|jpe|jfif|png|webp|gif|bmp|tif|tiff|heic|heif|avif|jp2|j2k|jpf|jpg2|jpx|ico',
|
||||
'max_size' => 5120,
|
||||
'encrypt_name' => true,
|
||||
));
|
||||
if (!$this->upload->do_upload('attachment')) {
|
||||
throw new BusinessException('File '.$name.' ditolak: '.strip_tags($this->upload->display_errors()));
|
||||
}
|
||||
$file = $this->upload->data();
|
||||
$stored[] = $file['full_path'];
|
||||
$this->db->insert('transaction_attachments', array(
|
||||
'module' => 'purchase',
|
||||
'entity_type' => 'supplier_payment',
|
||||
'entity_id' => (int)$paymentId,
|
||||
'original_name' => $file['orig_name'],
|
||||
'stored_name' => $file['file_name'],
|
||||
'mime_type' => $file['file_type'],
|
||||
'file_size' => $file['file_size'] * 1024,
|
||||
'sha256' => hash_file('sha256', $file['full_path']),
|
||||
'uploaded_by' => (int)$this->session->userdata('user_id'),
|
||||
'created_at' => date('Y-m-d H:i:s'),
|
||||
));
|
||||
if (!$this->db->affected_rows()) {
|
||||
throw new BusinessException('Metadata bukti pembayaran gagal disimpan.');
|
||||
}
|
||||
}
|
||||
return $stored;
|
||||
} catch (Throwable $e) {
|
||||
foreach ($stored as $path) {
|
||||
if (is_file($path)) @unlink($path);
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
public function statement($supplier)
|
||||
{
|
||||
$companyId = $this->companycontext->id();
|
||||
$this->db->where('id', (int)$supplier);
|
||||
if ($this->db->field_exists('company_id', 'suppliers')) {
|
||||
$this->db->where('company_id', $companyId);
|
||||
}
|
||||
$supplierRow = $this->db->get('suppliers')->row();
|
||||
if (!$supplierRow) {
|
||||
show_404();
|
||||
}
|
||||
|
||||
$this->db->where('supplier_id', $supplierRow->id);
|
||||
if ($this->db->field_exists('company_id', 'supplier_invoices')) {
|
||||
$this->db->where('company_id', $companyId);
|
||||
}
|
||||
$bills = $this->db->order_by('invoice_date')->get('supplier_invoices')->result();
|
||||
|
||||
$this->db->where('supplier_id', $supplierRow->id);
|
||||
if ($this->db->field_exists('company_id', 'supplier_payments')) {
|
||||
$this->db->where('company_id', $companyId);
|
||||
}
|
||||
$payments = $this->db->order_by('payment_date')->get('supplier_payments')->result();
|
||||
|
||||
$data = array('active_menu'=>'payables', 'supplier'=>$supplierRow, 'bills'=>$bills, 'payments'=>$payments);
|
||||
$this->load->view('partials/header', $data);
|
||||
$this->load->view('payables/statement', $data);
|
||||
$this->load->view('partials/footer');
|
||||
}
|
||||
|
||||
/** Mendukung format Indonesia (1.250.000,50) maupun angka normal (1250000.50). */
|
||||
private function parseAmount($value)
|
||||
{
|
||||
$value = trim((string)$value);
|
||||
if ($value === '') return 0.0;
|
||||
$value = preg_replace('/[^0-9,\.\-]/', '', $value);
|
||||
if (strpos($value, ',') !== false) {
|
||||
return (float)str_replace(',', '.', str_replace('.', '', $value));
|
||||
}
|
||||
if (preg_match('/^-?\d{1,3}(?:\.\d{3})+$/', $value)) {
|
||||
return (float)str_replace('.', '', $value);
|
||||
}
|
||||
return (float)$value;
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,14 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
|
||||
class Payroll extends CI_Controller {
|
||||
class Payroll extends MY_Controller {
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
$this->requireAdmin();
|
||||
|
||||
$this->load->database();
|
||||
|
||||
date_default_timezone_set('Asia/Jakarta');
|
||||
@@ -203,6 +205,9 @@ class Payroll extends CI_Controller {
|
||||
return;
|
||||
}
|
||||
|
||||
$periodControl=$this->db->get_where('k_payroll_periods',array('id'=>$payroll->payroll_period_id))->row();
|
||||
if($periodControl&&in_array($periodControl->workflow_status,array('final','posted','paid'),true)){echo json_encode(array('status'=>false,'message'=>'Payroll final terkunci. Gunakan pembatalan resmi.'));return;}
|
||||
|
||||
$items = $this->db
|
||||
->where('payroll_id',$id)
|
||||
->order_by('id','ASC')
|
||||
@@ -247,6 +252,9 @@ class Payroll extends CI_Controller {
|
||||
return;
|
||||
}
|
||||
|
||||
$periodControl=$this->db->get_where('k_payroll_periods',array('id'=>$payroll->payroll_period_id))->row();
|
||||
if($periodControl&&in_array($periodControl->workflow_status,array('final','posted','paid'),true)){echo json_encode(array('status'=>false,'message'=>'Payroll final terkunci. Gunakan pembatalan resmi.'));return;}
|
||||
|
||||
// =========================================
|
||||
// DELETE OLD ITEMS
|
||||
// =========================================
|
||||
@@ -595,4 +603,4 @@ class Payroll extends CI_Controller {
|
||||
'message' => 'Payroll berhasil digenerate'
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
|
||||
class Pesan extends CI_Controller {
|
||||
class Pesan extends MY_Controller {
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
class Professionalreports extends MY_Controller
|
||||
{
|
||||
public function __construct(){parent::__construct();$this->requirePermission('reports');$this->load->database();$this->load->library(array('ProfessionalReportService','CompanyContext','JobQueueService'));}
|
||||
private function filter(){$to=$this->input->get_post('to')?:date('Y-m-d');return array('from'=>$this->input->get_post('from')?:date('Y-m-01',strtotime($to)),'to'=>$to,'account_id'=>(int)$this->input->get_post('account_id'));}
|
||||
private function reportFilter($code,array$f){if($code==='balance_sheet')$f['from']='1900-01-01';return$f;}
|
||||
public function index(){$code=$this->input->get('report')?:'trial_balance';$f=$this->filter();$reportFilters=$this->reportFilter($code,$f);$d=array('active_menu'=>'professional_reports','report'=>$code,'filters'=>$f,'rows'=>$this->professionalreportservice->run($code,$reportFilters),'profit_loss_rows'=>$code==='balance_sheet'?$this->professionalreportservice->run('profit_loss',$reportFilters):array());$d['accounts']=$this->db->where('company_id',$this->companycontext->id())->order_by('kode_akun')->get('accounts')->result();$d['company']=$this->db->get_where('companies',array('id'=>$this->companycontext->id()))->row();$this->load->view('partials/header',$d);$this->load->view('professional_reports/index',$d);$this->load->view('partials/footer');}
|
||||
public function export($code){$f=$this->reportFilter($code,$this->filter());$rows=$this->professionalreportservice->run($code,$f);$this->output->set_content_type('text/csv')->set_header('Content-Disposition: attachment; filename="'.$code.'-'.$f['to'].'.csv"');$h=fopen('php://output','w');if($rows){fputcsv($h,array_keys($rows[0]));foreach($rows as$r)fputcsv($h,$r);}fclose($h);}
|
||||
public function save_filter(){$this->db->insert('saved_report_filters',array('user_id'=>$this->session->userdata('user_id'),'report_code'=>$this->input->post('report_code'),'name'=>$this->input->post('name',true),'filter_json'=>json_encode($this->filter()),'created_at'=>date('Y-m-d H:i:s'),'company_id'=>$this->companycontext->id()));redirect('professionalreports?report='.$this->input->post('report_code'));}
|
||||
public function snapshot($code){$f=$this->reportFilter($code,$this->filter());$rows=$this->professionalreportservice->run($code,$f);$hash=hash('sha256',json_encode($f));if(!$this->db->get_where('report_snapshots',array('report_code'=>$code,'period_end'=>$f['to'],'filter_hash'=>$hash,'company_id'=>$this->companycontext->id()))->row())$this->db->insert('report_snapshots',array('report_code'=>$code,'period_end'=>$f['to'],'filter_hash'=>$hash,'payload'=>json_encode($rows),'created_by'=>$this->session->userdata('user_id'),'created_at'=>date('Y-m-d H:i:s'),'company_id'=>$this->companycontext->id()));redirect('professionalreports?report='.$code.'&to='.$f['to']);}
|
||||
public function queue($code){$filters=$this->reportFilter($code,$this->filter());$format=$this->input->post('format')?:'csv';$key='report:'.$this->companycontext->id().':'.$this->session->userdata('user_id').':'.$code.':'.$format.':'.hash('sha256',json_encode($filters));$this->jobqueueservice->enqueue('report_export',array('report_code'=>$code,'format'=>$format,'filters'=>$filters,'requested_by'=>$this->session->userdata('user_id')),$key,'reports',5,3,$this->companycontext->id());$this->session->set_flashdata('success','Export laporan masuk antrean background.');redirect('professionalreports?report='.$code);}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
|
||||
/** Pemeriksaan read-only khusus Retur, Refund, Pengganti, dan Debit Note. */
|
||||
class Purchaseadjustmentcheck extends CI_Controller
|
||||
{
|
||||
public function __construct(){parent::__construct();if(!$this->input->is_cli_request())show_404();}
|
||||
|
||||
public function integrity()
|
||||
{
|
||||
$checks=array(
|
||||
'draft_return_without_stock'=>"SELECT COUNT(*) total FROM purchase_returns r JOIN stock_logs l ON l.ref_type='purchase_return' AND l.ref_id=r.id WHERE r.status IN('draft','submitted','approved','reserved')",
|
||||
'over_returned_receipt'=>"SELECT COUNT(*) total FROM(SELECT l.receipt_line_id,SUM(IF(r.status NOT IN('draft','rejected','cancelled','reversed'),l.qty,0)) returned,MAX(gr.qty) received FROM purchase_return_lines l JOIN purchase_returns r ON r.id=l.purchase_return_id JOIN goods_receipt_lines gr ON gr.id=l.receipt_line_id GROUP BY l.receipt_line_id HAVING returned>received+.0001)x",
|
||||
'duplicate_return_shipment_line'=>"SELECT COUNT(*) total FROM(SELECT purchase_return_line_id FROM purchase_return_shipment_lines GROUP BY purchase_return_line_id HAVING COUNT(*)>1)x",
|
||||
'mixed_supplier_return_batch'=>"SELECT COUNT(*) total FROM purchase_return_lines l JOIN purchase_returns r ON r.id=l.purchase_return_id JOIN goods_receipts gr ON gr.id=l.goods_receipt_id WHERE gr.supplier_id<>r.supplier_id",
|
||||
'pre_invoice_return_overbilled'=>"SELECT COUNT(*) total FROM(SELECT sil.id,sil.qty,grl.qty-COALESCE(SUM(CASE WHEN pr.shipped_at IS NOT NULL AND pr.shipped_at<si.created_at AND pr.status IN('shipped','waiting_supplier','partially_resolved','resolved') THEN prl.shipped_qty ELSE 0 END),0) net_received FROM supplier_invoice_lines sil JOIN supplier_invoices si ON si.id=sil.supplier_invoice_id JOIN goods_receipt_lines grl ON grl.id=sil.receipt_line_id LEFT JOIN purchase_return_lines prl ON prl.receipt_line_id=grl.id LEFT JOIN purchase_returns pr ON pr.id=prl.purchase_return_id GROUP BY sil.id,sil.qty,grl.qty HAVING sil.qty>net_received+.0001)x",
|
||||
'posted_shipment_without_stock_trace'=>"SELECT COUNT(*) total FROM purchase_return_shipment_lines sl JOIN purchase_return_shipments sh ON sh.id=sl.purchase_return_shipment_id JOIN purchase_return_lines rl ON rl.id=sl.purchase_return_line_id WHERE sh.status='posted' AND (rl.stock_log_id IS NULL OR rl.movement_id IS NULL OR rl.ledger_id IS NULL)",
|
||||
'replacement_over_return_quantity'=>"SELECT COUNT(*) total FROM(SELECT rl.id,rl.shipped_qty,COALESCE(SUM(pl.qty),0) replacement_qty FROM purchase_return_lines rl LEFT JOIN purchase_replacement_lines pl ON pl.purchase_return_line_id=rl.id LEFT JOIN purchase_replacements p ON p.id=pl.purchase_replacement_id AND p.status NOT IN('cancelled','reversed') GROUP BY rl.id,rl.shipped_qty HAVING replacement_qty>shipped_qty+.0001)x",
|
||||
'replacement_creates_payable'=>"SELECT COUNT(*) total FROM journals j JOIN journal_details d ON d.journal_id=j.id JOIN system_account_mappings m ON m.mapping_key='accounts_payable' AND m.account_id=d.account_id WHERE j.ref_type='purchase_replacement' AND ABS(d.debit-d.kredit)>.01",
|
||||
'unit_return_invalid_quantity'=>"SELECT COUNT(*) total FROM purchase_return_lines WHERE tracking_type='UNIT' AND ABS(qty-1)>.0001",
|
||||
'invalid_return_reservation'=>"SELECT COUNT(*) total FROM item_barcodes WHERE return_reserved_qty<0 OR return_reserved_qty>qty_sisa+.0001",
|
||||
'refund_over_received'=>"SELECT COUNT(*) total FROM supplier_refund_claims WHERE received_amount>claim_amount+.01 OR ABS(balance-GREATEST(claim_amount-received_amount,0))>.01",
|
||||
'refund_receipt_sum_mismatch'=>"SELECT COUNT(*) total FROM(SELECT c.id,c.received_amount,COALESCE(SUM(IF(r.status='reversed',0,r.amount)),0) receipt_total FROM supplier_refund_claims c LEFT JOIN supplier_refund_receipts r ON r.supplier_refund_claim_id=c.id GROUP BY c.id,c.received_amount HAVING ABS(received_amount-receipt_total)>.01)x",
|
||||
'debit_note_negative_invoice'=>"SELECT COUNT(*) total FROM supplier_invoices WHERE balance<-.01",
|
||||
'unbalanced_adjustment_journal'=>"SELECT COUNT(*) total FROM(SELECT j.id FROM journals j JOIN journal_details d ON d.journal_id=j.id WHERE j.ref_type IN('purchase_return','supplier_refund_claim','supplier_refund_receipt','supplier_debit_note') GROUP BY j.id HAVING ABS(SUM(d.debit)-SUM(d.kredit))>.01)x",
|
||||
'duplicate_adjustment_idempotency'=>"SELECT (SELECT COUNT(*) FROM(SELECT idempotency_key FROM purchase_returns WHERE idempotency_key IS NOT NULL GROUP BY idempotency_key HAVING COUNT(*)>1)a)+(SELECT COUNT(*) FROM(SELECT idempotency_key FROM purchase_return_shipments GROUP BY idempotency_key HAVING COUNT(*)>1)b)+(SELECT COUNT(*) FROM(SELECT idempotency_key FROM purchase_replacements GROUP BY idempotency_key HAVING COUNT(*)>1)c)+(SELECT COUNT(*) FROM(SELECT idempotency_key FROM supplier_refund_receipts GROUP BY idempotency_key HAVING COUNT(*)>1)d) total",
|
||||
'cross_company_return_line'=>"SELECT COUNT(*) total FROM purchase_return_lines l JOIN purchase_returns h ON h.id=l.purchase_return_id WHERE COALESCE(l.company_id,0)<>COALESCE(h.company_id,0)",
|
||||
'cross_company_adjustment_child'=>"SELECT (SELECT COUNT(*) FROM purchase_return_shipments x JOIN purchase_returns h ON h.id=x.purchase_return_id WHERE COALESCE(x.company_id,0)<>COALESCE(h.company_id,0))+(SELECT COUNT(*) FROM purchase_replacements x JOIN purchase_returns h ON h.id=x.purchase_return_id WHERE COALESCE(x.company_id,0)<>COALESCE(h.company_id,0))+(SELECT COUNT(*) FROM supplier_refund_receipts x JOIN supplier_refund_claims h ON h.id=x.supplier_refund_claim_id WHERE COALESCE(x.company_id,0)<>COALESCE(h.company_id,0)) total",
|
||||
'orphan_refund_allocation'=>"SELECT COUNT(*) total FROM supplier_refund_allocations a LEFT JOIN supplier_refund_claims c ON c.id=a.supplier_refund_claim_id WHERE c.id IS NULL"
|
||||
);
|
||||
$failed=0;$result=array();foreach($checks as$name=>$sql){try{$value=(int)$this->db->query($sql)->row()->total;$result[$name]=array('status'=>$value?'fail':'pass','violations'=>$value);if($value)$failed++;}catch(Throwable$e){$result[$name]=array('status'=>'fail','error'=>$e->getMessage());$failed++;}}
|
||||
foreach(array('supplier_refund_receivable','purchase_return_adjustment','purchase_price_variance')as$key){$valid=(int)$this->db->select('COUNT(*) total',false)->from('system_account_mappings m')->join('accounts a','a.id=m.account_id')->where(array('m.mapping_key'=>$key,'m.is_active'=>1,'a.is_active'=>1,'a.allow_posting'=>1))->get()->row()->total;$result['mapping_'.$key]=array('status'=>$valid?'pass':'fail','violations'=>$valid?0:1);if(!$valid)$failed++;}
|
||||
echo json_encode(array('status'=>$failed?'failed':'ok','checks'=>$result),JSON_PRETTY_PRINT).PHP_EOL;if($failed)exit(1);
|
||||
}
|
||||
|
||||
/** Smoke test read-only: memaksa query service dan menu memakai schema aktual. */
|
||||
public function runtime()
|
||||
{
|
||||
$access=$this->db->select('uc.user_id,uc.company_id,u.role_id,r.nama_role')->from('user_companies uc')->join('users u','u.id=uc.user_id')->join('roles r','r.id=u.role_id')->order_by('uc.is_default','DESC')->limit(1)->get()->row();
|
||||
if(!$access)throw new RuntimeException('User/company untuk runtime QA tidak tersedia.');
|
||||
$this->session->set_userdata(array('logged_in'=>true,'user_id'=>$access->user_id,'company_id'=>$access->company_id,'role_id'=>$access->role_id,'role'=>$access->nama_role));
|
||||
$this->load->library(array('CompanyContext','PurchaseAdjustmentService','NavigationService','ProfessionalReportService'));
|
||||
$sources=$this->purchaseadjustmentservice->returnSources();$barcodes=array();if($sources)$barcodes=$this->purchaseadjustmentservice->barcodesForReceiptLine($sources[0]['receipt_line_id']);
|
||||
$document=null;$return=$this->db->where('company_id',$access->company_id)->order_by('id','DESC')->get('purchase_returns')->row();if($return)$document=$this->purchaseadjustmentservice->document('purchase_return',$return->id);
|
||||
$menu=$this->navigationservice->items();$filters=array('from'=>'1900-01-01','to'=>date('Y-m-d'));
|
||||
$reports=array('purchase_returns'=>count($this->professionalreportservice->run('purchase_returns',$filters)),'supplier_refunds'=>count($this->professionalreportservice->run('supplier_refunds',$filters)),'outstanding_supplier_refunds'=>count($this->professionalreportservice->run('outstanding_supplier_refunds',$filters)),'supplier_debit_notes'=>count($this->professionalreportservice->run('supplier_debit_notes',$filters)));
|
||||
$netInvoiceReturn=$this->db->query("SELECT COALESCE(SUM(prl.shipped_qty*pol.unit_price*(1+(pol.tax_rate/100))),0) total FROM purchase_return_lines prl JOIN purchase_returns pr ON pr.id=prl.purchase_return_id JOIN goods_receipt_lines grl0 ON grl0.id=prl.receipt_line_id JOIN purchase_order_lines pol ON pol.id=grl0.po_line_id WHERE pol.purchase_order_id=0 AND pr.status IN('shipped','waiting_supplier','partially_resolved','resolved') AND NOT EXISTS (SELECT 1 FROM supplier_invoice_lines sil0 JOIN supplier_invoices si0 ON si0.id=sil0.supplier_invoice_id WHERE sil0.receipt_line_id=prl.receipt_line_id AND si0.created_at<=pr.shipped_at)")->row();
|
||||
echo json_encode(array('status'=>'ok','company_id'=>(int)$access->company_id,'return_sources'=>count($sources),'first_source_barcodes'=>count($barcodes),'return_document_loaded'=>$return?(bool)$document:null,'net_invoice_return_query'=>is_object($netInvoiceReturn),'menu_items'=>count($menu),'reports'=>$reports),JSON_PRETTY_PRINT).PHP_EOL;
|
||||
}
|
||||
|
||||
public function inventory_differences()
|
||||
{
|
||||
$rows=$this->db->query("SELECT l.item_id,l.warehouse_id,l.qty ledger_qty,COALESCE(s.qty,0) stock_log_qty,i.stok item_cache,l.qty-COALESCE(s.qty,0) log_diff,i.stok-l.qty cache_diff FROM(SELECT item_id,warehouse_id,SUM(IF(direction='in',qty,-qty)) qty FROM inventory_ledger GROUP BY item_id,warehouse_id)l LEFT JOIN(SELECT item_id,COALESCE(warehouse_id,0) warehouse_id,SUM(IF(tipe='masuk',qty,-qty)) qty FROM stock_logs GROUP BY item_id,COALESCE(warehouse_id,0))s ON s.item_id=l.item_id AND s.warehouse_id=l.warehouse_id LEFT JOIN items i ON i.id=l.item_id HAVING ABS(log_diff)>.0001 OR ABS(cache_diff)>.0001")->result_array();
|
||||
foreach($rows as&$row){
|
||||
$row['item']=$this->db->where('id',(int)$row['item_id'])->get('items')->row_array();
|
||||
$row['ledger_entries']=$this->db->where(array('item_id'=>(int)$row['item_id'],'warehouse_id'=>(int)$row['warehouse_id']))->order_by('id')->get('inventory_ledger')->result_array();
|
||||
$row['stock_logs']=$this->db->where(array('item_id'=>(int)$row['item_id'],'warehouse_id'=>(int)$row['warehouse_id']))->order_by('id')->get('stock_logs')->result_array();
|
||||
$row['barcodes']=$this->db->where(array('item_id'=>(int)$row['item_id'],'warehouse_id'=>(int)$row['warehouse_id']))->order_by('id')->get('item_barcodes')->result_array();
|
||||
}unset($row);
|
||||
echo json_encode(array('status'=>$rows?'difference':'ok','rows'=>$rows),JSON_PRETTY_PRINT).PHP_EOL;
|
||||
}
|
||||
|
||||
/** Breakdown read-only untuk investigasi rekonsiliasi nilai persediaan. */
|
||||
public function accounting_reconciliation()
|
||||
{
|
||||
$mapping=$this->db->where(array('mapping_key'=>'inventory','is_active'=>1))->get('system_account_mappings')->row();
|
||||
if(!$mapping)throw new RuntimeException('Mapping akun persediaan tidak tersedia.');
|
||||
$ledger=$this->db->query("SELECT document_type,direction,COUNT(*) row_count,SUM(qty) qty,SUM(value) value FROM inventory_ledger GROUP BY document_type,direction ORDER BY document_type,direction")->result_array();
|
||||
$gl=$this->db->query("SELECT j.ref_type,COUNT(DISTINCT j.id) journal_count,SUM(d.debit-d.kredit) value FROM journals j JOIN journal_details d ON d.journal_id=j.id WHERE d.account_id=? AND j.status IN('posted','reversed') GROUP BY j.ref_type ORDER BY j.ref_type",array((int)$mapping->account_id))->result_array();
|
||||
$openReceipts=$this->db->query("SELECT gr.receipt_no,po.po_no,SUM(grl.qty*pol.unit_price) value,po.paid_amount,po.total FROM goods_receipts gr JOIN purchase_orders po ON po.id=gr.purchase_order_id JOIN goods_receipt_lines grl ON grl.goods_receipt_id=gr.id JOIN purchase_order_lines pol ON pol.id=grl.po_line_id LEFT JOIN supplier_invoice_lines sil ON sil.receipt_line_id=grl.id WHERE gr.status='posted' AND sil.id IS NULL GROUP BY gr.id,gr.receipt_no,po.po_no,po.paid_amount,po.total ORDER BY gr.id")->result_array();
|
||||
echo json_encode(array('inventory_account_id'=>(int)$mapping->account_id,'ledger_by_source'=>$ledger,'gl_by_source'=>$gl,'received_not_invoiced'=>$openReceipts),JSON_PRETTY_PRINT).PHP_EOL;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
class Purchasedocuments extends MY_Controller
|
||||
{
|
||||
private $types=array(
|
||||
'purchase_order'=>array('purchase_orders','id','po_no','purchase_order_lines','purchase_order_id'),
|
||||
'goods_receipt'=>array('goods_receipts','id','receipt_no','goods_receipt_lines','goods_receipt_id'),
|
||||
'supplier_invoice'=>array('supplier_invoices','id','internal_no','supplier_invoice_lines','supplier_invoice_id'),
|
||||
'supplier_payment'=>array('supplier_payments','id','payment_no','purchase_payment_sources','supplier_payment_id'),
|
||||
'purchase_return'=>array('purchase_returns','id','return_no','purchase_return_lines','purchase_return_id'),
|
||||
'supplier_debit_note'=>array('supplier_debit_notes','id','debit_note_no',null,null),
|
||||
'purchase_refund'=>array('purchase_refunds','id','refund_no',null,null)
|
||||
);
|
||||
private function document($type,$id){
|
||||
if(!isset($this->types[$type]))return null;$m=$this->types[$type];$items=array();
|
||||
if($type==='purchase_order'){
|
||||
$header=$this->db->select('po.*,pol.warehouse_id,s.supplier_code,s.name supplier_name,s.address supplier_address,s.phone supplier_phone,pr.request_no,pr.department,u.nama created_name,a.nama approved_name,w.nama warehouse_name')->from('purchase_orders po')->join('suppliers s','s.id=po.supplier_id')->join('purchase_requests pr','pr.id=po.purchase_request_id','left')->join('users u','u.id=po.created_by','left')->join('users a','a.id=po.approved_by','left')->join('purchase_order_lines pol','pol.purchase_order_id=po.id','left')->join('warehouses w','w.id=pol.warehouse_id','left')->where('po.id',(int)$id)->group_by('po.id')->get()->row_array();
|
||||
if($header&&$this->db->table_exists('purchase_installment_schedules')){$schedule=$this->db->select('COUNT(*) installment_count,MIN(due_date) first_due_date',false)->where('purchase_order_id',(int)$id)->get('purchase_installment_schedules')->row_array();$header['installment_count']=(int)($schedule['installment_count']??0);$header['first_due_date']=$schedule['first_due_date']??null;}
|
||||
$lines=$this->db->select('pol.*,k.kode_barang,k.nama nama_barang')->from('purchase_order_lines pol')->join('kode_barang k','k.id=pol.kode_barang_id','left')->where('pol.purchase_order_id',(int)$id)->order_by('pol.id')->get()->result_array();
|
||||
}elseif($type==='supplier_payment'){
|
||||
$header=$this->db->select('sp.*,s.supplier_code,s.name supplier_name,s.address supplier_address,po.po_no,po.procurement_method,po.payment_timing,po.subtotal po_subtotal,po.tax_total po_tax_total,po.shipping_cost,po.insurance_cost,po.service_fee,po.discount_amount,po.cashback_amount,po.total po_total,po.paid_amount po_paid_amount,u.nama created_name')->from('supplier_payments sp')->join('suppliers s','s.id=sp.supplier_id','left')->join('purchase_orders po','po.id=sp.purchase_order_id','left')->join('users u','u.id=sp.created_by','left')->where('sp.id',(int)$id)->get()->row_array();
|
||||
if($header){$journal=$this->db->select('no_ref')->get_where('journals',array('id'=>(int)$header['journal_id']))->row_array();$header['journal_no']=$journal?$journal['no_ref']:null;}
|
||||
$lines=$this->db->select('pps.amount,a.kode_akun,a.nama_akun')->from('purchase_payment_sources pps')->join('accounts a','a.id=pps.account_id','left')->where('pps.supplier_payment_id',(int)$id)->order_by('pps.id')->get()->result_array();
|
||||
if(!$lines&&$header&&!empty($header['bank_account_id'])){$account=$this->db->select('kode_akun,nama_akun')->get_where('accounts',array('id'=>(int)$header['bank_account_id']))->row_array();$lines[]=array('amount'=>$header['amount'],'kode_akun'=>$account?$account['kode_akun']:'-','nama_akun'=>$account?$account['nama_akun']:'Kas/Bank');}
|
||||
if($header&&!empty($header['purchase_order_id']))$items=$this->db->select('pol.description,pol.qty,pol.unit_price,pol.tax_amount,pol.line_total,pol.tracking_type,k.kode_barang,k.nama nama_barang')->from('purchase_order_lines pol')->join('kode_barang k','k.id=pol.kode_barang_id','left')->where('pol.purchase_order_id',(int)$header['purchase_order_id'])->order_by('pol.id')->get()->result_array();
|
||||
}elseif($type==='goods_receipt'){
|
||||
$header=$this->db->select('gr.*,po.po_no,po.order_date,po.procurement_method,po.marketplace,po.external_order_no,po.courier,po.tracking_no,po.payment_timing,po.payment_plan,po.payment_status,po.subtotal po_subtotal,po.tax_total po_tax_total,po.shipping_cost,po.insurance_cost,po.service_fee,po.discount_amount,po.cashback_amount,po.total po_total,po.paid_amount po_paid_amount,s.supplier_code,s.name supplier_name,s.address supplier_address,s.payment_term_days,u.nama received_name')->from('goods_receipts gr')->join('purchase_orders po','po.id=gr.purchase_order_id','left')->join('suppliers s','s.id=gr.supplier_id','left')->join('users u','u.id=gr.received_by','left')->where('gr.id',(int)$id)->get()->row_array();
|
||||
if($header){
|
||||
$receiptSummary=$this->db->select("COUNT(DISTINCT gr.id) receipt_count,GROUP_CONCAT(DISTINCT CONCAT(gr.receipt_no,' (',DATE_FORMAT(gr.receipt_date,'%d-%m-%Y'),')') ORDER BY gr.receipt_date,gr.id SEPARATOR ', ') receipt_numbers",false)->from('goods_receipts gr')->where('gr.purchase_order_id',(int)$header['purchase_order_id'])->where('gr.status','posted')->get()->row_array();
|
||||
$header['receipt_count']=(int)($receiptSummary['receipt_count']??0);$header['receipt_numbers']=$receiptSummary['receipt_numbers']??'';
|
||||
$schedule=$this->db->select('COUNT(*) installment_count,MIN(due_date) first_due_date',false)->where('purchase_order_id',(int)$header['purchase_order_id'])->get('purchase_installment_schedules')->row_array();
|
||||
$header['installment_count']=(int)($schedule['installment_count']??0);$header['first_due_date']=$schedule['first_due_date']??null;
|
||||
}
|
||||
$lines=array();
|
||||
if($header)$lines=$this->db->select("pol.id po_line_id,pol.qty ordered_qty,pol.received_qty total_received_qty,pol.invoiced_qty,pol.unit_price,pol.tax_rate,pol.line_total,pol.description,pol.tracking_type,pol.qty_per_barcode,k.kode_barang,k.nama nama_barang,w.nama warehouse_name,COALESCE((SELECT SUM(x.qty) FROM goods_receipt_lines x WHERE x.po_line_id=pol.id AND x.goods_receipt_id=".(int)$id."),0) selected_receipt_qty,COALESCE((SELECT SUM(x.qty) FROM goods_receipt_lines x JOIN goods_receipts gx ON gx.id=x.goods_receipt_id WHERE x.po_line_id=pol.id AND gx.status='posted'),0) accumulated_received_qty,COALESCE((SELECT SUM(prl.shipped_qty) FROM purchase_return_lines prl JOIN purchase_returns pr ON pr.id=prl.purchase_return_id JOIN goods_receipt_lines grlr ON grlr.id=prl.receipt_line_id WHERE grlr.po_line_id=pol.id AND pr.status IN('shipped','waiting_supplier','partially_resolved','resolved')),0) accumulated_returned_qty,GREATEST(0,COALESCE((SELECT SUM(x.qty) FROM goods_receipt_lines x JOIN goods_receipts gx ON gx.id=x.goods_receipt_id WHERE x.po_line_id=pol.id AND gx.status='posted'),0)-COALESCE((SELECT SUM(prl.shipped_qty) FROM purchase_return_lines prl JOIN purchase_returns pr ON pr.id=prl.purchase_return_id JOIN goods_receipt_lines grlr ON grlr.id=prl.receipt_line_id WHERE grlr.po_line_id=pol.id AND pr.status IN('shipped','waiting_supplier','partially_resolved','resolved')),0)-COALESCE((SELECT SUM(sil.qty) FROM supplier_invoice_lines sil WHERE sil.po_line_id=pol.id),0)) invoice_available_qty",false)->from('purchase_order_lines pol')->join('kode_barang k','k.id=pol.kode_barang_id','left')->join('warehouses w','w.id=pol.warehouse_id','left')->where('pol.purchase_order_id',(int)$header['purchase_order_id'])->order_by('pol.id')->get()->result_array();
|
||||
}elseif($type==='supplier_invoice'){
|
||||
$header=$this->db->select("si.*,s.supplier_code,s.name supplier_name,s.address supplier_address,u.nama created_name,po.po_no,po.subtotal po_subtotal,po.tax_total po_tax_total,po.shipping_cost,po.insurance_cost,po.service_fee,po.discount_amount,po.cashback_amount,po.total po_total,po.paid_amount po_paid_amount,GROUP_CONCAT(DISTINCT gr.receipt_no ORDER BY gr.receipt_date,gr.id SEPARATOR ', ') receipt_no,GROUP_CONCAT(DISTINCT COALESCE(gr.supplier_delivery_no,'-') ORDER BY gr.receipt_date,gr.id SEPARATOR ', ') supplier_delivery_no",false)->from('supplier_invoices si')->join('suppliers s','s.id=si.supplier_id','left')->join('users u','u.id=si.created_by','left')->join('supplier_invoice_lines sil','sil.supplier_invoice_id=si.id','left')->join('purchase_order_lines pol','pol.id=sil.po_line_id','left')->join('purchase_orders po','po.id=pol.purchase_order_id','left')->join('goods_receipt_lines grl','grl.id=sil.receipt_line_id','left')->join('goods_receipts gr','gr.id=grl.goods_receipt_id','left')->where('si.id',(int)$id)->group_by('si.id')->get()->row_array();
|
||||
if($header)$header['status_label']=array('verified'=>'Perlu Finalisasi','partial'=>'Parsial','paid'=>'Lunas','cancelled'=>'Dibatalkan','reversed'=>'Dibalik')[$header['status']]??$header['status'];
|
||||
$lines=$this->db->select("SUM(sil.qty) qty,sil.unit_price,SUM(sil.tax_amount) tax_amount,SUM(sil.line_total) line_total,IF(SUM(sil.match_status<>'matched')=0,'matched','mismatch') match_status,pol.description,k.kode_barang,k.nama nama_barang,GROUP_CONCAT(DISTINCT gr.receipt_no ORDER BY gr.receipt_date,gr.id SEPARATOR ', ') receipt_no",false)->from('supplier_invoice_lines sil')->join('purchase_order_lines pol','pol.id=sil.po_line_id','left')->join('kode_barang k','k.id=pol.kode_barang_id','left')->join('goods_receipt_lines grl','grl.id=sil.receipt_line_id','left')->join('goods_receipts gr','gr.id=grl.goods_receipt_id','left')->where('sil.supplier_invoice_id',(int)$id)->group_by('sil.po_line_id,sil.unit_price,pol.description,k.kode_barang,k.nama')->order_by('MIN(sil.id)',null,false)->get()->result_array();
|
||||
}else{$header=$this->db->get_where($m[0],array($m[1]=>(int)$id))->row_array();$lines=$m[3]?$this->db->get_where($m[3],array($m[4]=>(int)$id))->result_array():array();}
|
||||
if(!$header)return null;$attachments=$this->db->where(array('entity_type'=>$type,'entity_id'=>(int)$id))->order_by('id','DESC')->get('transaction_attachments')->result_array();return array('type'=>$type,'number'=>$header[$m[2]],'header'=>$header,'lines'=>$lines,'items'=>$items,'attachments'=>$attachments);
|
||||
}
|
||||
public function detail($type,$id){$d=$this->document($type,$id);if(!$d)return json_response(false,'Dokumen pembelian tidak ditemukan.',array(),404);foreach($d['attachments']as&$a){$a['preview_url']=base_url('accountingattachments/preview/'.$a['id']);$a['download_url']=$a['preview_url'];$a['actual_download_url']=base_url('accountingattachments/download/'.$a['id']);}return json_response(true,'Detail dokumen pembelian.',$d);}
|
||||
public function payment_bank_pdf($id)
|
||||
{
|
||||
$data=$this->document('supplier_payment',$id);if(!$data)show_404();require_once APPPATH.'third_party/fpdf/fpdf.php';$h=$data['header'];
|
||||
$principal=(float)($h['amount']??0);$charge=(float)($h['bank_charge_amount']??0);$cashOut=(float)($h['total_cash_out']??0);if($cashOut<=0)$cashOut=$principal+$charge;
|
||||
$pdf=new FPDF('P','mm','A4');$pdf->SetMargins(15,15,15);$pdf->AddPage();$pdf->SetFont('Arial','B',15);$pdf->Cell(0,8,'BUKTI PEMBAYARAN PEMBELIAN',0,1,'C');$pdf->SetFont('Arial','',9);$pdf->Cell(0,6,$h['payment_no'],0,1,'C');$pdf->Ln(6);
|
||||
foreach(array('Supplier'=>trim(($h['supplier_code']?:'').' - '.($h['supplier_name']?:'')),'Purchase Order'=>$h['po_no'],'Tanggal Pembayaran'=>$h['payment_date'],'Referensi Bank'=>$h['reference_no'],'Nomor Jurnal'=>$h['journal_no'],'Status'=>strtoupper($h['status']),'Diproses oleh'=>$h['created_name'])as$label=>$value){$pdf->SetFont('Arial','B',9);$pdf->Cell(42,6,$label,0,0);$pdf->SetFont('Arial','',9);$pdf->MultiCell(0,6,': '.($value?:'-'));}
|
||||
if(!empty($data['items'])){$pdf->Ln(3);$pdf->SetFont('Arial','B',9);$pdf->Cell(0,6,'BARANG YANG DIBAYAR',0,1);$widths=array(9,32,64,17,25,25);foreach(array('No','Kode','Barang / Spesifikasi','Qty','Harga','Total')as$i=>$label)$pdf->Cell($widths[$i],7,$label,1,0,'C');$pdf->Ln();$pdf->SetFont('Arial','',7);foreach($data['items']as$i=>$item){$pdf->Cell($widths[0],7,$i+1,1,0,'C');$pdf->Cell($widths[1],7,substr($item['kode_barang']?:'-',0,19),1);$pdf->Cell($widths[2],7,substr(($item['nama_barang']?:'').' - '.($item['description']?:''),0,43),1);$pdf->Cell($widths[3],7,number_format($item['qty'],0,',','.'),1,0,'R');$pdf->Cell($widths[4],7,number_format($item['unit_price'],2,',','.'),1,0,'R');$pdf->Cell($widths[5],7,number_format($item['line_total'],2,',','.'),1,1,'R');}}
|
||||
$pdf->Ln(4);$pdf->SetFont('Arial','B',9);$pdf->Cell(0,6,'SUMBER KAS / BANK',0,1);$pdf->Cell(12,8,'No',1,0,'C');$pdf->Cell(110,8,'Akun',1,0,'C');$pdf->Cell(50,8,'Nominal Keluar',1,1,'C');$pdf->SetFont('Arial','',9);foreach($data['lines']as$i=>$line){$pdf->Cell(12,8,$i+1,1,0,'C');$pdf->Cell(110,8,substr(($line['kode_akun']?:'-').' - '.($line['nama_akun']?:'-'),0,64),1);$pdf->Cell(50,8,'Rp '.number_format($line['amount'],2,',','.'),1,1,'R');}
|
||||
$pdf->Ln(4);foreach(array('Pokok Pembayaran'=>$principal,'Biaya Admin Bank'=>$charge,'Total Keluar Kas/Bank'=>$cashOut)as$label=>$value){$pdf->SetFont('Arial',$label==='Total Keluar Kas/Bank'?'B':'',9);$pdf->Cell(122,7,$label,0,0,'R');$pdf->Cell(50,7,'Rp '.number_format($value,2,',','.'),0,1,'R');}
|
||||
if(!empty($h['notes'])){$pdf->Ln(4);$pdf->SetFont('Arial','B',9);$pdf->Cell(0,6,'Catatan',0,1);$pdf->SetFont('Arial','',9);$pdf->MultiCell(0,5,$h['notes'],1);}
|
||||
$pdf->Ln(12);$pdf->SetFont('Arial','',9);$pdf->Cell(0,6,'Finance / Pembayar',0,1,'C');$pdf->Ln(15);$pdf->SetFont('Arial','B',9);$pdf->Cell(0,6,'('.($h['created_name']?:'-').')',0,1,'C');$pdf->Output('I',$h['payment_no'].'.pdf');
|
||||
}
|
||||
public function payment_pdf($id){$d=$this->document('supplier_payment',$id);if(!$d)show_404();require_once APPPATH.'third_party/fpdf/fpdf.php';$h=$d['header'];$pdf=new FPDF('P','mm','A4');$pdf->SetMargins(15,15,15);$pdf->AddPage();$pdf->SetFont('Arial','B',15);$pdf->Cell(0,8,'BUKTI PEMBAYARAN PEMBELIAN',0,1,'C');$pdf->SetFont('Arial','',9);$pdf->Cell(0,6,$h['payment_no'],0,1,'C');$pdf->Ln(6);foreach(array('Supplier'=>trim(($h['supplier_code']?:'').' - '.($h['supplier_name']?:'')),'Purchase Order'=>$h['po_no'],'Tanggal Pembayaran'=>$h['payment_date'],'Referensi'=>$h['reference_no'],'Nomor Jurnal'=>$h['journal_no'],'Status'=>strtoupper($h['status']),'Diproses oleh'=>$h['created_name'])as$label=>$value){$pdf->SetFont('Arial','B',9);$pdf->Cell(42,6,$label,0,0);$pdf->SetFont('Arial','',9);$pdf->MultiCell(0,6,': '.($value?:'-'));}$pdf->Ln(3);if(!empty($d['items'])){$pdf->SetFont('Arial','B',9);$pdf->Cell(0,6,'BARANG YANG DIBAYAR',0,1);$pdf->SetFont('Arial','B',7);$pdf->Cell(8,7,'No',1,0,'C');$pdf->Cell(30,7,'Kode',1,0,'C');$pdf->Cell(62,7,'Barang / Spesifikasi',1,0,'C');$pdf->Cell(14,7,'Qty',1,0,'C');$pdf->Cell(28,7,'Harga',1,0,'C');$pdf->Cell(30,7,'Total',1,1,'C');$pdf->SetFont('Arial','',7);foreach($d['items']as$i=>$item){$pdf->Cell(8,7,$i+1,1,0,'C');$pdf->Cell(30,7,substr($item['kode_barang']?:'-',0,18),1);$pdf->Cell(62,7,substr(($item['nama_barang']?:'').' - '.($item['description']?:''),0,42),1);$pdf->Cell(14,7,number_format($item['qty'],0,',','.'),1,0,'R');$pdf->Cell(28,7,number_format($item['unit_price'],2,',','.'),1,0,'R');$pdf->Cell(30,7,number_format($item['line_total'],2,',','.'),1,1,'R');}$pdf->Ln(4);}$pdf->SetFont('Arial','B',9);$pdf->Cell(0,6,'RINCIAN NILAI PURCHASE ORDER',0,1);foreach(array('Subtotal Barang'=>$h['po_subtotal'],'Pajak'=>$h['po_tax_total'],'Ongkos Kirim'=>$h['shipping_cost'],'Asuransi'=>$h['insurance_cost'],'Biaya Layanan'=>$h['service_fee'],'Diskon / Voucher'=>-$h['discount_amount'],'Cashback'=>-$h['cashback_amount'],'Total PO'=>$h['po_total'],'Pembayaran Ini'=>$h['amount'],'Sisa Setelah Pembayaran'=>max(0,$h['po_total']-$h['po_paid_amount']))as$label=>$value){$pdf->SetFont('Arial',in_array($label,array('Total PO','Pembayaran Ini','Sisa Setelah Pembayaran'),true)?'B':'',8);$pdf->Cell(122,5,$label,0,0,'R');$pdf->Cell(50,5,'Rp '.number_format($value,2,',','.'),0,1,'R');}$pdf->Ln(3);$pdf->SetFont('Arial','B',9);$pdf->Cell(12,8,'No',1,0,'C');$pdf->Cell(110,8,'Sumber Kas / Bank',1,0,'C');$pdf->Cell(50,8,'Nominal',1,1,'C');$pdf->SetFont('Arial','',9);foreach($d['lines']as$i=>$line){$pdf->Cell(12,8,$i+1,1,0,'C');$pdf->Cell(110,8,substr(($line['kode_akun']?:'-').' - '.($line['nama_akun']?:'-'),0,64),1);$pdf->Cell(50,8,'Rp '.number_format($line['amount'],2,',','.'),1,1,'R');}$pdf->SetFont('Arial','B',10);$pdf->Cell(122,9,'TOTAL PEMBAYARAN',1,0,'R');$pdf->Cell(50,9,'Rp '.number_format($h['amount'],2,',','.'),1,1,'R');if(!empty($h['notes'])){$pdf->Ln(5);$pdf->SetFont('Arial','B',9);$pdf->Cell(0,6,'Catatan',0,1);$pdf->SetFont('Arial','',9);$pdf->MultiCell(0,5,$h['notes'],1);}$pdf->Ln(14);$pdf->SetFont('Arial','',9);$pdf->Cell(0,6,'Finance / Pembayar',0,1,'C');$pdf->Ln(16);$pdf->SetFont('Arial','B',9);$pdf->Cell(0,6,'('.($h['created_name']?:'-').')',0,1,'C');$pdf->Output('I',$h['payment_no'].'.pdf');}
|
||||
public function receipt_pdf($id){$d=$this->document('goods_receipt',$id);if(!$d)show_404();require_once APPPATH.'third_party/fpdf/fpdf.php';$h=$d['header'];$pdf=new FPDF('P','mm','A4');$pdf->SetMargins(15,15,15);$pdf->AddPage();$pdf->SetFont('Arial','B',15);$pdf->Cell(0,8,'BUKTI PENERIMAAN BARANG',0,1,'C');$pdf->SetFont('Arial','',9);$pdf->Cell(0,6,$h['receipt_no'],0,1,'C');$pdf->Ln(6);foreach(array('Supplier'=>trim(($h['supplier_code']?:'').' - '.($h['supplier_name']?:'')),'Purchase Order'=>$h['po_no'],'Tanggal PO'=>$h['order_date'],'Tanggal Diterima'=>$h['receipt_date'],'Surat Jalan / Resi'=>$h['supplier_delivery_no'],'Status'=>strtoupper($h['status']),'Diterima oleh'=>$h['received_name'])as$label=>$value){$pdf->SetFont('Arial','B',9);$pdf->Cell(42,6,$label,0,0);$pdf->SetFont('Arial','',9);$pdf->MultiCell(0,6,': '.($value?:'-'));}$pdf->Ln(3);$widths=array(10,34,65,18,25,20);foreach(array('No','Kode','Barang / Spesifikasi','Qty','Tracking','Gudang')as$i=>$label){$pdf->SetFont('Arial','B',8);$pdf->Cell($widths[$i],8,$label,1,0,'C');}$pdf->Ln();$pdf->SetFont('Arial','',8);$total=0;foreach($d['lines']as$i=>$line){$total+=(float)$line['qty'];$pdf->Cell($widths[0],8,$i+1,1,0,'C');$pdf->Cell($widths[1],8,substr($line['kode_barang']?:'-',0,20),1);$pdf->Cell($widths[2],8,substr(($line['nama_barang']?:'').' - '.($line['description']?:''),0,43),1);$pdf->Cell($widths[3],8,number_format($line['qty'],2,',','.'),1,0,'R');$pdf->Cell($widths[4],8,$line['tracking_type']?:'-',1,0,'C');$pdf->Cell($widths[5],8,substr($line['warehouse_name']?:'-',0,12),1);$pdf->Ln();}$pdf->SetFont('Arial','B',9);$pdf->Cell(109,8,'TOTAL UNIT DITERIMA',1,0,'R');$pdf->Cell(18,8,number_format($total,2,',','.'),1,0,'R');$pdf->Cell(45,8,'',1,1);$pdf->Ln(14);$pdf->SetFont('Arial','',9);$pdf->Cell(86,6,'Petugas Gudang',0,0,'C');$pdf->Cell(86,6,'Pengirim / Supplier',0,1,'C');$pdf->Ln(17);$pdf->SetFont('Arial','B',9);$pdf->Cell(86,6,'('.($h['received_name']?:'-').')',0,0,'C');$pdf->Cell(86,6,'(____________________)',0,1,'C');$pdf->Output('I',$h['receipt_no'].'.pdf');}
|
||||
public function invoice_pdf($id){$d=$this->document('supplier_invoice',$id);if(!$d)show_404();require_once APPPATH.'third_party/fpdf/fpdf.php';$h=$d['header'];$invoiceTotal=(float)($h['po_total']??$h['total']);$poPaid=max((float)($h['po_paid_amount']??0),(float)$h['paid_amount']);$invoiceBalance=max(0,$invoiceTotal-$poPaid);$pdf=new FPDF('P','mm','A4');$pdf->SetMargins(12,12,12);$pdf->AddPage();$pdf->SetFont('Arial','B',15);$pdf->Cell(0,8,'INVOICE SUPPLIER',0,1,'C');$pdf->SetFont('Arial','',9);$pdf->Cell(0,5,$h['internal_no'].' / '.$h['supplier_invoice_no'],0,1,'C');$pdf->Ln(6);foreach(array('Supplier'=>trim(($h['supplier_code']?:'').' - '.($h['supplier_name']?:'')),'Purchase Order'=>$h['po_no'],'Penerimaan'=>$h['receipt_no'],'Surat Jalan / Resi'=>$h['supplier_delivery_no'],'Tanggal Invoice'=>$h['invoice_date'],'Jatuh Tempo'=>$h['due_date'],'Rencana Pembayaran'=>strtoupper($h['payment_plan']),'Status'=>strtoupper($h['status_label']??$h['status']))as$label=>$value){$pdf->SetFont('Arial','B',8);$pdf->Cell(40,5,$label,0,0);$pdf->SetFont('Arial','',8);$pdf->MultiCell(0,5,': '.($value?:'-'));}$pdf->Ln(3);$widths=array(8,29,48,14,25,22,26);foreach(array('No','Kode','Barang','Qty','Harga','Pajak','Total')as$i=>$label){$pdf->SetFont('Arial','B',7);$pdf->Cell($widths[$i],7,$label,1,0,'C');}$pdf->Ln();$pdf->SetFont('Arial','',7);foreach($d['lines']as$i=>$line){$pdf->Cell($widths[0],7,$i+1,1,0,'C');$pdf->Cell($widths[1],7,substr($line['kode_barang']?:'-',0,17),1);$pdf->Cell($widths[2],7,substr(($line['nama_barang']?:'').' - '.($line['description']?:''),0,32),1);$pdf->Cell($widths[3],7,number_format($line['qty'],2,',','.'),1,0,'R');$pdf->Cell($widths[4],7,number_format($line['unit_price'],2,',','.'),1,0,'R');$pdf->Cell($widths[5],7,number_format($line['tax_amount'],2,',','.'),1,0,'R');$pdf->Cell($widths[6],7,number_format($line['line_total'],2,',','.'),1,1,'R');}$pdf->Ln(3);foreach(array('Subtotal Barang'=>$h['po_subtotal']??$h['subtotal'],'Pajak'=>$h['po_tax_total']??$h['tax_total'],'Ongkir, Asuransi & Layanan'=>(float)($h['shipping_cost']??0)+(float)($h['insurance_cost']??0)+(float)($h['service_fee']??0),'Diskon & Cashback'=>-((float)($h['discount_amount']??0)+(float)($h['cashback_amount']??0)),'Total Invoice'=>$invoiceTotal,'Total Sudah Dibayar'=>$poPaid,'Sisa Tagihan'=>$invoiceBalance)as$label=>$value){$pdf->SetFont('Arial',in_array($label,array('Total Invoice','Total Sudah Dibayar','Sisa Tagihan'),true)?'B':'',8);$pdf->Cell(145,5,$label,0,0,'R');$pdf->Cell(27,5,number_format($value,2,',','.'),0,1,'R');}$pdf->Ln(13);$pdf->SetFont('Arial','',8);$pdf->Cell(86,5,'Dicatat oleh',0,0,'C');$pdf->Cell(86,5,'Disetujui / Diperiksa',0,1,'C');$pdf->Ln(16);$pdf->SetFont('Arial','B',8);$pdf->Cell(86,5,'('.($h['created_name']?:'-').')',0,0,'C');$pdf->Cell(86,5,'(____________________)',0,1,'C');$pdf->Output('I',$h['internal_no'].'.pdf');}
|
||||
public function pdf($type,$id){$d=$this->document($type,$id);if(!$d)show_404();require_once APPPATH.'third_party/fpdf/fpdf.php';if($type==='purchase_order')return$this->purchaseOrderPdf($d);$pdf=new FPDF('P','mm','A4');$pdf->SetMargins(14,14,14);$pdf->AddPage();$pdf->SetFont('Arial','B',14);$pdf->Cell(0,8,strtoupper(str_replace('_',' ',$type)),0,1,'C');$pdf->SetFont('Arial','',10);$pdf->Cell(0,6,'Nomor: '.$d['number'],0,1,'C');$pdf->Ln(4);foreach($d['header']as$key=>$value){if(is_null($value)||$value===''||in_array($key,array('id','snapshot'),true))continue;$label=ucwords(str_replace('_',' ',$key));$pdf->SetFont('Arial','B',8);$pdf->Cell(45,5,substr($label,0,28),0,0);$pdf->SetFont('Arial','',8);$pdf->MultiCell(0,5,': '.substr((string)$value,0,100));}foreach($d['lines']as$i=>$line){$pdf->SetFont('Arial','B',8);$pdf->Cell(0,5,'Baris '.($i+1),0,1);$pdf->SetFont('Arial','',7);$pdf->MultiCell(0,4,json_encode($line),1);}$pdf->Output('I',$d['number'].'.pdf');}
|
||||
private function purchaseOrderPdf($d){$h=$d['header'];$method=array('online'=>'Pesanan Online','direct'=>'Pembelian Langsung','supplier'=>'Supplier Biasa');$timing=array('prepaid'=>'Bayar Sebelum Diterima','on_receipt'=>'Bayar Saat Diterima','installment'=>'Cicilan','credit'=>'Kredit/Jatuh Tempo');$pdf=new FPDF('P','mm','A4');$pdf->SetMargins(12,12,12);$pdf->AddPage();$pdf->SetFont('Arial','B',16);$pdf->Cell(0,8,'PURCHASE ORDER',0,1,'C');$pdf->SetFont('Arial','',9);$pdf->Cell(0,5,$h['po_no'],0,1,'C');$pdf->Ln(5);$pairs=array('Supplier'=>($h['supplier_code'].' - '.$h['supplier_name']),'Alamat'=>$h['supplier_address'],'Tanggal PO'=>$h['order_date'],'Estimasi Datang'=>$h['expected_date'],'Sumber PR'=>$h['request_no'],'Departemen'=>$h['department'],'Gudang'=>$h['warehouse_name'],'Metode Pengadaan'=>($method[$h['procurement_method']]??$h['procurement_method']),'Rencana Pembayaran'=>($timing[$h['payment_timing']]??$h['payment_timing']));if($h['procurement_method']==='online'){$pairs['Marketplace']=$h['marketplace'];$pairs['Nomor Pesanan']=$h['external_order_no'];$pairs['Kurir/Resi']=trim(($h['courier']?:'-').' / '.($h['tracking_no']?:'-'));}foreach($pairs as$l=>$v){$pdf->SetFont('Arial','B',8);$pdf->Cell(38,5,$l,0,0);$pdf->SetFont('Arial','',8);$pdf->MultiCell(0,5,': '.($v?:'-'));}$pdf->Ln(3);$widths=array(8,31,55,15,25,15,27);$headers=array('No','Kode','Barang / Spesifikasi','Qty','Harga','Pajak','Subtotal');$pdf->SetFont('Arial','B',7);foreach($headers as$i=>$label)$pdf->Cell($widths[$i],7,$label,1,0,'C');$pdf->Ln();$pdf->SetFont('Arial','',7);foreach($d['lines']as$i=>$l){$pdf->Cell($widths[0],7,$i+1,1,0,'C');$pdf->Cell($widths[1],7,substr($l['kode_barang']?:'-',0,18),1);$pdf->Cell($widths[2],7,substr(($l['nama_barang']?:'').' - '.$l['description'],0,38),1);$pdf->Cell($widths[3],7,number_format($l['qty'],2,',','.'),1,0,'R');$pdf->Cell($widths[4],7,number_format($l['unit_price'],2,',','.'),1,0,'R');$pdf->Cell($widths[5],7,number_format($l['tax_rate'],2,',','.').'%',1,0,'R');$pdf->Cell($widths[6],7,number_format($l['line_total'],2,',','.'),1,0,'R');$pdf->Ln();}$pdf->Ln(3);foreach(array('Subtotal'=>$h['subtotal'],'Pajak'=>$h['tax_total'],'Ongkos Kirim'=>$h['shipping_cost'],'Asuransi'=>$h['insurance_cost'],'Biaya Layanan'=>$h['service_fee'],'Diskon/Voucher'=>-$h['discount_amount'],'Cashback'=>-$h['cashback_amount'],'TOTAL PO'=>$h['total'])as$l=>$v){$pdf->SetFont('Arial',$l==='TOTAL PO'?'B':'',8);$pdf->Cell(145,5,$l,0,0,'R');$pdf->Cell(27,5,number_format($v,2,',','.'),0,1,'R');}$pdf->Ln(12);$pdf->SetFont('Arial','',8);$pdf->Cell(86,5,'Dibuat oleh',0,0,'C');$pdf->Cell(86,5,'Disetujui oleh',0,1,'C');$pdf->Ln(16);$pdf->SetFont('Arial','B',8);$pdf->Cell(86,5,'('.($h['created_name']?:'-').')',0,0,'C');$pdf->Cell(86,5,'('.($h['approved_name']?:'-').')',0,1,'C');$pdf->Output('I',$h['po_no'].'.pdf');}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
|
||||
class Purchasereturns extends MY_Controller
|
||||
{
|
||||
private $storedPaths = array();
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->load->library('PurchaseAdjustmentService');
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$this->requireAnyView();
|
||||
$company=$this->companycontext->id();
|
||||
$master=$this->session->userdata('role')==='Admin';
|
||||
$viewReturns=$master||check_permission('purchase_returns','can_view');
|
||||
$viewRefunds=$master||check_permission('purchase_refunds','can_view');
|
||||
$viewDebitNotes=$master||check_permission('supplier_debit_notes','can_view');
|
||||
$this->db->select('r.*,s.supplier_code,s.name supplier_name,po.po_no,u.nama created_name')->from('purchase_returns r')->join('suppliers s','s.id=r.supplier_id')->join('purchase_orders po','po.id=r.purchase_order_id','left')->join('users u','u.id=r.created_by','left');
|
||||
if($this->db->field_exists('company_id','purchase_returns'))$this->db->where('r.company_id',$company);
|
||||
$returns=$this->db->order_by('r.id','DESC')->limit(250)->get()->result();
|
||||
$this->db->select('r.*,s.supplier_code,s.name supplier_name,po.po_no,si.internal_no')->from('supplier_refund_claims r')->join('suppliers s','s.id=r.supplier_id')->join('purchase_orders po','po.id=r.purchase_order_id','left')->join('supplier_invoices si','si.id=r.supplier_invoice_id','left')->where('r.company_id',$company);
|
||||
$refunds=$this->db->order_by('r.id','DESC')->limit(250)->get()->result();
|
||||
$this->db->select('d.*,s.supplier_code,s.name supplier_name,si.internal_no,po.po_no')->from('supplier_debit_notes d')->join('suppliers s','s.id=d.supplier_id')->join('supplier_invoices si','si.id=d.supplier_invoice_id','left')->join('purchase_orders po','po.id=d.purchase_order_id','left');
|
||||
if($this->db->field_exists('company_id','supplier_debit_notes'))$this->db->where('d.company_id',$company);
|
||||
$debitNotes=$this->db->order_by('d.id','DESC')->limit(250)->get()->result();
|
||||
$replacements=$this->db->select('r.*,pr.return_no,s.name supplier_name,w.nama warehouse_name')->from('purchase_replacements r')->join('purchase_returns pr','pr.id=r.purchase_return_id')->join('suppliers s','s.id=r.supplier_id')->join('warehouses w','w.id=r.warehouse_id','left')->where('r.company_id',$company)->order_by('r.id','DESC')->limit(250)->get()->result();
|
||||
$shipments=$this->db->select('sh.*,pr.return_no,s.name supplier_name,po.po_no')->from('purchase_return_shipments sh')->join('purchase_returns pr','pr.id=sh.purchase_return_id')->join('suppliers s','s.id=pr.supplier_id')->join('purchase_orders po','po.id=pr.purchase_order_id','left')->where('sh.company_id',$company)->order_by('sh.id','DESC')->limit(250)->get()->result();
|
||||
$refundReceipts=$this->db->select('rr.*,rc.refund_no,s.name supplier_name,a.kode_akun,a.nama_akun')->from('supplier_refund_receipts rr')->join('supplier_refund_claims rc','rc.id=rr.supplier_refund_claim_id')->join('suppliers s','s.id=rc.supplier_id')->join('accounts a','a.id=rr.bank_account_id','left')->where('rr.company_id',$company)->order_by('rr.id','DESC')->limit(250)->get()->result();
|
||||
$this->db->from('suppliers')->group_start()->where('is_active',1)->or_where("EXISTS(SELECT 1 FROM purchase_returns pr WHERE pr.supplier_id=suppliers.id)",null,false)->group_end();if($this->db->field_exists('company_id','suppliers'))$this->db->where('company_id',$company);$suppliers=$this->db->order_by('name')->get()->result();
|
||||
$this->db->from('accounts')->where(array('is_active'=>1,'allow_posting'=>1))->group_start()->like('nama_akun','kas')->or_like('nama_akun','bank')->group_end();if($this->db->field_exists('company_id','accounts'))$this->db->where('company_id',$company);$cashAccounts=$this->db->order_by('kode_akun')->get()->result();
|
||||
$this->db->from('warehouses');if($this->db->field_exists('company_id','warehouses'))$this->db->where('company_id',$company);$warehouses=$this->db->order_by('nama')->get()->result();
|
||||
$this->db->select('si.id,si.internal_no,si.supplier_invoice_no,si.supplier_id,si.status,si.total,si.paid_amount,si.balance,s.name supplier_name,po.id po_id,po.po_no')->from('supplier_invoices si')->join('suppliers s','s.id=si.supplier_id')->join('supplier_invoice_lines sil','sil.supplier_invoice_id=si.id','left')->join('purchase_order_lines pol','pol.id=sil.po_line_id','left')->join('purchase_orders po','po.id=pol.purchase_order_id','left')->where_in('si.status',array('partial','paid'));if($this->db->field_exists('company_id','supplier_invoices'))$this->db->where('si.company_id',$company);$invoices=$this->db->group_by('si.id')->order_by('si.id','DESC')->get()->result();
|
||||
$refundSources=$viewRefunds?$this->refundSources($company):array();
|
||||
if($this->input->get('action',true)==='history')$this->filterRelatedDocuments($returns,$refunds,$debitNotes,$replacements,$shipments,$refundReceipts);
|
||||
if(!$viewReturns){$returns=array();$replacements=array();$shipments=array();}
|
||||
if(!$viewRefunds){$refunds=array();$refundReceipts=array();}
|
||||
if(!$viewDebitNotes)$debitNotes=array();
|
||||
$badges=$this->badges($returns,$refunds,$debitNotes);
|
||||
$section=$this->input->get('section',true)?:($this->input->get('action',true)==='history'?'history':'summary');$validSections=array('summary','history');if($viewReturns)$validSections=array_merge($validSections,array('return','shipment','replacement'));if($viewRefunds)$validSections[]='refund';if($viewDebitNotes)$validSections[]='debit_note';if(!in_array($section,$validSections,true))$section='summary';
|
||||
$data=array('active_menu'=>'purchase_returns','active_section'=>$section,'returns'=>$returns,'refunds'=>$refunds,'debit_notes'=>$debitNotes,'replacements'=>$replacements,'shipments'=>$shipments,'refund_receipts'=>$refundReceipts,'suppliers'=>$suppliers,'cash_accounts'=>$cashAccounts,'warehouses'=>$warehouses,'invoices'=>$invoices,'return_sources'=>$viewReturns?$this->purchaseadjustmentservice->returnSources():array(),'refund_sources'=>$refundSources,'badges'=>$badges,'prefill'=>array('action'=>$this->input->get('action',true),'category'=>$this->input->get('category',true),'receipt_id'=>(int)$this->input->get('receipt_id'),'receipt_line_id'=>(int)$this->input->get('receipt_line_id'),'po_id'=>(int)$this->input->get('po_id'),'invoice_id'=>(int)$this->input->get('invoice_id'),'payment_id'=>(int)$this->input->get('payment_id'),'return_id'=>(int)$this->input->get('return_id'),'focus'=>(int)$this->input->get('focus'),'focus_type'=>$this->input->get('focus_type',true)));
|
||||
$this->load->view('partials/header',$data);$this->load->view('purchase_returns/index',$data);$this->load->view('partials/footer');
|
||||
}
|
||||
|
||||
public function source_barcodes($receiptLineId)
|
||||
{
|
||||
$this->requirePermission('purchase_returns','can_view');
|
||||
return json_response(true,'Barcode/batch tersedia.',array('barcodes'=>$this->purchaseadjustmentservice->barcodesForReceiptLine((int)$receiptLineId)));
|
||||
}
|
||||
|
||||
public function detail($type,$id)
|
||||
{
|
||||
$this->requireDocumentPermission($type,'can_view');$document=$this->purchaseadjustmentservice->document($type,(int)$id);if(!$document)return json_response(false,'Dokumen Retur & Refund tidak ditemukan.',array(),404);foreach($document['attachments']as&$file){$file['preview_url']=base_url('accountingattachments/preview/'.$file['id']);$file['download_url']=base_url('accountingattachments/download/'.$file['id']);}return json_response(true,'Detail dokumen.',$document);
|
||||
}
|
||||
|
||||
public function create_return()
|
||||
{
|
||||
$this->requirePermission('purchase_returns','can_create');
|
||||
$receiptIds=(array)$this->input->post('receipt_line_id');$barcodeIds=(array)$this->input->post('barcode_id');$quantities=(array)$this->input->post('qty');$lines=array();foreach($receiptIds as$i=>$receiptId)$lines[]=array('receipt_line_id'=>$receiptId,'barcode_id'=>$barcodeIds[$i]??0,'qty'=>$this->amount($quantities[$i]??0));
|
||||
return$this->action(function()use($lines){return$this->uploadedTransaction('return_documents','purchase_return',function()use($lines){return$this->purchaseadjustmentservice->createReturn(array('return_date'=>$this->input->post('return_date'),'reason'=>$this->input->post('reason',true),'problem_category'=>$this->input->post('problem_category'),'requested_resolution'=>$this->input->post('requested_resolution'),'notes'=>$this->input->post('notes',true),'idempotency_key'=>$this->input->post('idempotency_key',true)),$lines,$this->uid());});},'Draft pengajuan retur multi-item berhasil dibuat.','return');
|
||||
}
|
||||
|
||||
public function submit_return($id){$this->requirePermission('purchase_returns','can_update');return$this->action(fn()=>$this->purchaseadjustmentservice->submitReturn($id,$this->uid()),'Pengajuan retur dikirim untuk persetujuan.','return');}
|
||||
public function approve_return($id){$this->requirePermission('purchase_returns','can_approve');return$this->action(fn()=>$this->purchaseadjustmentservice->approveReturn($id,$this->uid(),$this->roleId()),'Retur disetujui dan barang telah direservasi.','shipment');}
|
||||
public function reject_return($id){$this->requirePermission('purchase_returns','can_approve');return$this->action(fn()=>$this->purchaseadjustmentservice->rejectReturn($id,$this->input->post('reason',true),$this->uid(),$this->roleId()),'Pengajuan retur ditolak.','return');}
|
||||
public function cancel_return($id){$this->requirePermission('purchase_returns','can_update');return$this->action(fn()=>$this->purchaseadjustmentservice->cancelReturn($id,$this->input->post('reason',true),$this->uid()),'Retur dibatalkan tanpa menghapus histori.','return');}
|
||||
public function reverse_return($id){$this->requirePermission('purchase_returns','can_post');return$this->action(fn()=>$this->purchaseadjustmentservice->reverseReturn($id,$this->input->post('date'),$this->input->post('reason',true),$this->uid()),'Retur berhasil direversal dan stok dikembalikan secara resmi.','history');}
|
||||
|
||||
public function post_shipment($id)
|
||||
{
|
||||
$this->requirePermission('purchase_returns','can_post');
|
||||
return$this->action(function()use($id){return$this->uploadedTransaction('shipment_documents','purchase_return_shipment',function()use($id){return$this->purchaseadjustmentservice->postReturnShipment($id,array('shipment_date'=>$this->input->post('shipment_date'),'courier'=>$this->input->post('courier',true),'tracking_no'=>$this->input->post('tracking_no',true),'notes'=>$this->input->post('notes',true),'idempotency_key'=>$this->input->post('idempotency_key',true),'attachment_pending'=>1),$this->uid());});},'Pengeluaran dan pengiriman retur berhasil diposting satu kali.','shipment');
|
||||
}
|
||||
|
||||
public function receive_replacement($id)
|
||||
{
|
||||
$this->requirePermission('purchase_returns','can_post');$quantities=(array)$this->input->post('qty');$serials=(array)$this->input->post('serial_numbers',true);
|
||||
return$this->action(function()use($id,$quantities,$serials){return$this->uploadedTransaction('replacement_documents','purchase_replacement',function()use($id,$quantities,$serials){return$this->purchaseadjustmentservice->receiveReplacement($id,array('receipt_date'=>$this->input->post('receipt_date'),'supplier_delivery_no'=>$this->input->post('supplier_delivery_no',true),'warehouse_id'=>$this->input->post('warehouse_id'),'idempotency_key'=>$this->input->post('idempotency_key',true),'attachment_pending'=>1),array_map(array($this,'amount'),$quantities),$serials,$this->uid());});},'Barang pengganti berhasil diterima tanpa membentuk hutang baru.','replacement');
|
||||
}
|
||||
|
||||
public function create_refund()
|
||||
{
|
||||
$this->requirePermission('purchase_refunds','can_create');$types=(array)$this->input->post('source_type');$ids=(array)$this->input->post('source_id');$amounts=(array)$this->input->post('allocation_amount');$allocations=array();foreach($types as$i=>$type)$allocations[]=array('source_type'=>$type,'source_id'=>$ids[$i]??0,'amount'=>$this->amount($amounts[$i]??0));
|
||||
return$this->action(function()use($allocations){return$this->uploadedTransaction('refund_documents','supplier_refund',function()use($allocations){return$this->purchaseadjustmentservice->createRefund(array('claim_date'=>$this->input->post('claim_date'),'expected_date'=>$this->input->post('expected_date'),'reason'=>$this->input->post('reason',true),'source_category'=>$this->input->post('source_category'),'target_account_id'=>$this->input->post('target_account_id'),'idempotency_key'=>$this->input->post('idempotency_key',true)),$allocations,$this->uid());});},'Draft pengajuan refund berhasil dibuat.','refund');
|
||||
}
|
||||
public function submit_refund($id){$this->requirePermission('purchase_refunds','can_update');return$this->action(fn()=>$this->purchaseadjustmentservice->submitRefund($id,$this->uid()),'Pengajuan refund dikirim untuk persetujuan.','refund');}
|
||||
public function approve_refund($id){$this->requirePermission('purchase_refunds','can_approve');return$this->action(fn()=>$this->purchaseadjustmentservice->approveRefund($id,$this->uid(),$this->roleId()),'Refund disetujui dan menunggu dana supplier.','refund');}
|
||||
public function reject_refund($id){$this->requirePermission('purchase_refunds','can_approve');return$this->action(fn()=>$this->purchaseadjustmentservice->rejectRefund($id,$this->input->post('reason',true),$this->uid(),$this->roleId()),'Pengajuan refund ditolak.','refund');}
|
||||
public function receive_refund($id){$this->requirePermission('purchase_refunds','can_post');return$this->action(function()use($id){return$this->uploadedTransaction('refund_receipt_documents','supplier_refund_receipt',function()use($id){return$this->purchaseadjustmentservice->receiveRefund($id,array('receipt_date'=>$this->input->post('receipt_date'),'bank_account_id'=>$this->input->post('bank_account_id'),'amount'=>$this->amount($this->input->post('amount')),'reference_no'=>$this->input->post('reference_no',true),'notes'=>$this->input->post('notes',true),'idempotency_key'=>$this->input->post('idempotency_key',true),'attachment_pending'=>1),$this->uid());});},'Penerimaan dana refund berhasil diposting.','refund');}
|
||||
public function reconcile_refund($id){$this->requirePermission('purchase_refunds','can_post');return$this->action(fn()=>$this->purchaseadjustmentservice->reconcileRefund($id,$this->uid()),'Refund selesai direkonsiliasi.','refund');}
|
||||
public function cancel_refund($id){$this->requirePermission('purchase_refunds','can_update');return$this->action(fn()=>$this->purchaseadjustmentservice->cancelRefund($id,$this->input->post('reason',true),$this->uid()),'Klaim refund dibatalkan tanpa menghapus histori.','refund');}
|
||||
public function reverse_refund_receipt($id){$this->requirePermission('purchase_refunds','can_post');return$this->action(fn()=>$this->purchaseadjustmentservice->reverseRefundReceipt($id,$this->input->post('date'),$this->input->post('reason',true),$this->uid()),'Penerimaan dana refund berhasil direversal.','history');}
|
||||
|
||||
public function create_debit_note()
|
||||
{
|
||||
$this->requirePermission('supplier_debit_notes','can_create');
|
||||
return$this->action(function(){return$this->uploadedTransaction('debit_note_documents','supplier_debit_note',function(){return$this->purchaseadjustmentservice->createDebitNote(array('supplier_invoice_id'=>$this->input->post('supplier_invoice_id'),'purchase_return_id'=>$this->input->post('purchase_return_id'),'note_date'=>$this->input->post('note_date'),'correction_type'=>$this->input->post('correction_type'),'base_amount'=>$this->amount($this->input->post('base_amount')),'tax_amount'=>$this->amount($this->input->post('tax_amount')),'reason'=>$this->input->post('reason',true),'idempotency_key'=>$this->input->post('idempotency_key',true)),$this->uid());});},'Draft Debit Note berhasil dibuat.','debit_note');
|
||||
}
|
||||
public function post_debit_note($id){$this->requirePermission('supplier_debit_notes','can_post');return$this->action(fn()=>$this->purchaseadjustmentservice->postDebitNote($id,$this->uid()),'Debit Note berhasil diposting dan saldo hutang diperbarui.','debit_note');}
|
||||
public function reverse_debit_note($id){$this->requirePermission('supplier_debit_notes','can_post');return$this->action(fn()=>$this->purchaseadjustmentservice->reverseDebitNote($id,$this->input->post('date'),$this->input->post('reason',true),$this->uid()),'Debit Note berhasil direversal dan saldo invoice dipulihkan.','history');}
|
||||
|
||||
public function pdf($type,$id)
|
||||
{
|
||||
$this->requireDocumentPermission($type,'can_export');
|
||||
$document=$this->purchaseadjustmentservice->document($type,(int)$id);if(!$document)show_404();
|
||||
require_once APPPATH.'third_party/fpdf/fpdf.php';
|
||||
$h=$document['header'];
|
||||
$titles=array('purchase_return'=>'PENGAJUAN RETUR PEMBELIAN','return_resolution'=>'RINGKASAN PENYELESAIAN RETUR','purchase_return_shipment'=>'SURAT PENGIRIMAN RETUR','purchase_replacement'=>'BUKTI BARANG PENGGANTI','supplier_refund'=>'PENGAJUAN / PENYELESAIAN REFUND','supplier_refund_receipt'=>'BUKTI PENERIMAAN REFUND','supplier_debit_note'=>'DEBIT NOTE SUPPLIER');
|
||||
$numbers=array('purchase_return'=>'return_no','return_resolution'=>'return_no','purchase_return_shipment'=>'shipment_no','purchase_replacement'=>'replacement_no','supplier_refund'=>'refund_no','supplier_refund_receipt'=>'receipt_no','supplier_debit_note'=>'debit_note_no');
|
||||
$company=$this->db->get_where('companies',array('id'=>$this->companycontext->id()))->row();
|
||||
$pdf=new FPDF('P','mm','A4');$pdf->SetMargins(12,12,12);$pdf->SetAutoPageBreak(true,14);$pdf->AddPage();
|
||||
$pdf->SetFont('Arial','B',12);$pdf->Cell(0,6,$this->pdfText($company?($company->legal_name?:$company->name):'Perusahaan'),0,1,'C');
|
||||
if($company&&!empty($company->address)){$pdf->SetFont('Arial','',7);$pdf->MultiCell(0,4,$this->pdfText($company->address),0,'C');}
|
||||
$pdf->SetDrawColor(190,198,210);$pdf->Line(12,$pdf->GetY()+2,198,$pdf->GetY()+2);$pdf->Ln(5);
|
||||
$pdf->SetFont('Arial','B',14);$pdf->Cell(0,8,$titles[$type]??'DOKUMEN PENYESUAIAN PEMBELIAN',0,1,'C');
|
||||
$numberKey=$numbers[$type]??null;$documentNo=$numberKey?($h[$numberKey]??'-'):'-';$pdf->SetFont('Arial','',9);$pdf->Cell(0,5,$this->pdfText($documentNo),0,1,'C');$pdf->Ln(5);
|
||||
foreach($this->pdfPairs($type,$h)as$label=>$value){$pdf->SetFont('Arial','B',8);$pdf->Cell(43,5,$this->pdfText($label),0,0);$pdf->SetFont('Arial','',8);$pdf->MultiCell(0,5,': '.$this->pdfText($value?:'-'));}
|
||||
if($document['lines']){$pdf->Ln(3);$pdf->SetFillColor(242,245,249);$pdf->SetFont('Arial','B',8);foreach(array(10=>'No',35=>'Sumber / Kode',67=>'Barang / Keterangan',20=>'Qty',40=>'Nilai')as$width=>$label)$pdf->Cell($width,7,$label,1,0,'C',true);$pdf->Ln();$pdf->SetFont('Arial','',7);foreach($document['lines']as$i=>$line){$source=$line['receipt_no']??($line['source_type']??($line['kode_barang']??'-'));$description=$line['description']??($line['item_name']??'-');$qty=(float)($line['qty']??0);$value=$line['total_amount']??($line['allocated_amount']??($line['amount']??0));$qtyText=abs($qty-round($qty))<.0001?number_format($qty,0,',','.'):number_format($qty,4,',','.');$pdf->Cell(10,7,$i+1,1,0,'C');$pdf->Cell(35,7,substr($this->pdfText($source),0,21),1);$pdf->Cell(67,7,substr($this->pdfText($description),0,42),1);$pdf->Cell(20,7,$qtyText,1,0,'R');$pdf->Cell(40,7,'Rp '.number_format((float)$value,2,',','.'),1,1,'R');}}
|
||||
$totalKeys=array('supplier_refund'=>'claim_amount','supplier_refund_receipt'=>'amount','supplier_debit_note'=>'amount','purchase_return'=>'amount','return_resolution'=>'amount');$totalKey=$totalKeys[$type]??null;$documentTotal=$totalKey&&isset($h[$totalKey])?(float)$h[$totalKey]:null;if($documentTotal!==null){$pdf->SetFont('Arial','B',8);$pdf->Cell(132,7,'TOTAL DOKUMEN',1,0,'R');$pdf->Cell(40,7,'Rp '.number_format($documentTotal,2,',','.'),1,1,'R');}
|
||||
if(!empty($document['history'])){$pdf->Ln(4);$pdf->SetFont('Arial','B',8);$pdf->Cell(0,6,'RIWAYAT STATUS',0,1);$pdf->SetFont('Arial','',7);foreach($document['history']as$history)$pdf->MultiCell(0,5,$this->pdfText(($history['created_at']??'').' | '.($history['from_status']??'-').' -> '.($history['to_status']??'-').' | '.($history['user_name']??'-').' | '.($history['notes']??'')),1);}
|
||||
$pdf->Ln(12);$pdf->SetFont('Arial','',8);$pdf->Cell(58,5,'Dibuat / Diajukan',0,0,'C');$pdf->Cell(58,5,'Disetujui / Diproses',0,0,'C');$pdf->Cell(58,5,'Supplier',0,1,'C');$pdf->Ln(15);$pdf->Cell(58,5,'('.$this->pdfText($h['created_name']??($h['received_name']??'-')).')',0,0,'C');$pdf->Cell(58,5,'('.$this->pdfText($h['approved_name']??($h['posted_name']??'-')).')',0,0,'C');$pdf->Cell(58,5,'(________________)',0,1,'C');
|
||||
$pdf->Output('I',$documentNo.'.pdf');
|
||||
}
|
||||
|
||||
private function refundSources($company)
|
||||
{
|
||||
$out=array();
|
||||
$poClaims="COALESCE((SELECT SUM(rc0.claim_amount) FROM supplier_refund_claims rc0 WHERE rc0.purchase_order_id=po.id AND rc0.status NOT IN('rejected','cancelled','reversed')),0)";
|
||||
$this->db->select("'purchase_order' source_type,po.id source_id,po.id purchase_order_id,po.supplier_id,po.po_no number,GREATEST(0,po.paid_amount-po.transit_cleared_amount-{$poClaims}) eligible_amount,s.name supplier_name",false)->from('purchase_orders po')->join('suppliers s','s.id=po.supplier_id')->where("po.paid_amount-po.transit_cleared_amount-{$poClaims} > 0",null,false)->where("NOT EXISTS(SELECT 1 FROM supplier_payments sp0 WHERE sp0.purchase_order_id=po.id AND sp0.status='posted')",null,false)->where_not_in('po.status',array('cancelled','closed'));if($this->db->field_exists('company_id','purchase_orders'))$this->db->where('po.company_id',$company);$out=array_merge($out,$this->db->get()->result_array());
|
||||
$claimed="COALESCE((SELECT SUM(ra.allocated_amount) FROM supplier_refund_allocations ra JOIN supplier_refund_claims rc ON rc.id=ra.supplier_refund_claim_id WHERE ra.source_type='supplier_payment' AND ra.source_id=p.id AND rc.status NOT IN('rejected','cancelled','reversed')),0)";
|
||||
$poCommitted="COALESCE((SELECT SUM(rc0.claim_amount) FROM supplier_refund_claims rc0 WHERE rc0.purchase_order_id=p.purchase_order_id AND rc0.status NOT IN('rejected','cancelled','reversed')),0)";
|
||||
$eligible="CASE WHEN p.purchase_order_id IS NOT NULL THEN LEAST(p.amount,GREATEST(0,po.paid_amount-po.transit_cleared_amount-{$poCommitted}-COALESCE((SELECT SUM(p2.amount) FROM supplier_payments p2 WHERE p2.purchase_order_id=p.purchase_order_id AND p2.status='posted' AND p2.id>p.id),0))) ELSE GREATEST(0,p.amount-COALESCE((SELECT SUM(spa.amount) FROM supplier_payment_allocations spa WHERE spa.supplier_payment_id=p.id),0)-{$claimed}) END";
|
||||
$this->db->select("'supplier_payment' source_type,p.id source_id,p.purchase_order_id,p.supplier_id,p.payment_no number,GREATEST(0,{$eligible}) eligible_amount,s.name supplier_name",false)->from('supplier_payments p')->join('suppliers s','s.id=p.supplier_id')->join('purchase_orders po','po.id=p.purchase_order_id','left')->where('p.status','posted')->where("{$eligible} > 0",null,false);if($this->db->field_exists('company_id','supplier_payments'))$this->db->where('p.company_id',$company);$out=array_merge($out,$this->db->get()->result_array());
|
||||
$claimed="COALESCE((SELECT SUM(ra.allocated_amount) FROM supplier_refund_allocations ra JOIN supplier_refund_claims rc ON rc.id=ra.supplier_refund_claim_id WHERE ra.source_type='purchase_return' AND ra.source_id=r.id AND rc.status NOT IN('rejected','cancelled','reversed')),0)";
|
||||
$returnEligible="LEAST(GREATEST(0,r.amount-{$claimed}),GREATEST(0,po.paid_amount-po.transit_cleared_amount-COALESCE((SELECT SUM(rc1.claim_amount) FROM supplier_refund_claims rc1 WHERE rc1.purchase_order_id=r.purchase_order_id AND rc1.status NOT IN('rejected','cancelled','reversed')),0)))";
|
||||
$this->db->select("'purchase_return' source_type,r.id source_id,r.purchase_order_id,r.supplier_id,r.return_no number,{$returnEligible} eligible_amount,s.name supplier_name",false)->from('purchase_returns r')->join('suppliers s','s.id=r.supplier_id')->join('purchase_orders po','po.id=r.purchase_order_id')->where_in('r.status',array('shipped','waiting_supplier','partially_resolved'))->where("{$returnEligible} > 0",null,false);if($this->db->field_exists('company_id','purchase_returns'))$this->db->where('r.company_id',$company);return array_merge($out,$this->db->get()->result_array());
|
||||
}
|
||||
|
||||
private function badges($returns,$refunds,$notes)
|
||||
{
|
||||
$result=array('return'=>0,'shipment'=>0,'replacement'=>0,'refund'=>0,'debit_note'=>0,'attention'=>0);foreach($returns as$r){if(in_array($r->status,array('draft','submitted','approved'),true))$result['return']++;if($r->status==='reserved')$result['shipment']++;if(in_array($r->status,array('shipped','waiting_supplier','partially_resolved'),true))$result['replacement']++;}foreach($refunds as$r)if(!in_array($r->status,array('reconciled','cancelled','reversed','rejected'),true))$result['refund']++;foreach($notes as$n)if($n->status==='draft')$result['debit_note']++;$result['attention']=$result['return']+$result['shipment']+$result['replacement']+$result['refund']+$result['debit_note'];return$result;
|
||||
}
|
||||
|
||||
private function filterRelatedDocuments(&$returns,&$refunds,&$notes,&$replacements,&$shipments,&$refundReceipts)
|
||||
{
|
||||
$focus=(int)$this->input->get('focus');$focusType=$this->input->get('focus_type',true);
|
||||
if($focus&&in_array($focusType,array('purchase_return','supplier_refund','supplier_debit_note'),true)){
|
||||
if($focusType==='purchase_return'){$returns=array_values(array_filter($returns,fn($r)=>(int)$r->id===$focus));$refunds=array_values(array_filter($refunds,fn($r)=>(int)$r->purchase_return_id===$focus));$notes=array_values(array_filter($notes,fn($r)=>(int)$r->purchase_return_id===$focus));$replacements=array_values(array_filter($replacements,fn($r)=>(int)$r->purchase_return_id===$focus));$shipments=array_values(array_filter($shipments,fn($r)=>(int)$r->purchase_return_id===$focus));$refundIds=array_map(fn($r)=>(int)$r->id,$refunds);$refundReceipts=array_values(array_filter($refundReceipts,fn($r)=>in_array((int)$r->supplier_refund_claim_id,$refundIds,true)));}
|
||||
elseif($focusType==='supplier_refund'){$returns=array();$notes=array();$replacements=array();$shipments=array();$refunds=array_values(array_filter($refunds,fn($r)=>(int)$r->id===$focus));$refundReceipts=array_values(array_filter($refundReceipts,fn($r)=>(int)$r->supplier_refund_claim_id===$focus));}
|
||||
else{$returns=array();$refunds=array();$replacements=array();$shipments=array();$refundReceipts=array();$notes=array_values(array_filter($notes,fn($r)=>(int)$r->id===$focus));}
|
||||
return;
|
||||
}
|
||||
$po=(int)$this->input->get('po_id');$receipt=(int)$this->input->get('receipt_id');$invoice=(int)$this->input->get('invoice_id');$payment=(int)$this->input->get('payment_id');
|
||||
if(!$po&&!$receipt&&!$invoice&&!$payment)return;
|
||||
$returnIds=array();foreach($returns as$r)if(($po&&(int)$r->purchase_order_id===$po)||($receipt&&(int)$r->goods_receipt_id===$receipt))$returnIds[]=(int)$r->id;
|
||||
if($invoice){$rows=$this->db->select('DISTINCT prl.purchase_return_id id',false)->from('purchase_return_lines prl')->join('supplier_invoice_lines sil','sil.receipt_line_id=prl.receipt_line_id')->where('sil.supplier_invoice_id',$invoice)->get()->result();foreach($rows as$row)$returnIds[]=(int)$row->id;}
|
||||
$claimIds=array();if($payment){foreach($this->db->select('supplier_refund_claim_id id')->get_where('supplier_refund_allocations',array('source_type'=>'supplier_payment','source_id'=>$payment))->result()as$row)$claimIds[]=(int)$row->id;}
|
||||
$returns=array_values(array_filter($returns,function($r)use($returnIds){return in_array((int)$r->id,$returnIds,true);}));
|
||||
$refunds=array_values(array_filter($refunds,function($r)use($po,$invoice,$claimIds,$returnIds){return($po&&(int)$r->purchase_order_id===$po)||($invoice&&(int)$r->supplier_invoice_id===$invoice)||in_array((int)$r->id,$claimIds,true)||in_array((int)$r->purchase_return_id,$returnIds,true);}));
|
||||
$notes=array_values(array_filter($notes,function($r)use($po,$invoice,$returnIds){return($po&&(int)$r->purchase_order_id===$po)||($invoice&&(int)$r->supplier_invoice_id===$invoice)||in_array((int)$r->purchase_return_id,$returnIds,true);}));
|
||||
$replacements=array_values(array_filter($replacements,function($r)use($returnIds){return in_array((int)$r->purchase_return_id,$returnIds,true);}));
|
||||
$shipments=array_values(array_filter($shipments,function($r)use($returnIds){return in_array((int)$r->purchase_return_id,$returnIds,true);}));
|
||||
$refundIds=array_map(function($r){return(int)$r->id;},$refunds);$refundReceipts=array_values(array_filter($refundReceipts,function($r)use($refundIds){return in_array((int)$r->supplier_refund_claim_id,$refundIds,true);}));
|
||||
}
|
||||
|
||||
private function uploadedTransaction($input,$entityType,callable$callback)
|
||||
{
|
||||
$this->validateFiles($input);$this->db->trans_begin();try{$id=(int)$callback();$this->storeFiles($input,$entityType,$id);if($this->db->trans_status()===false)throw new RuntimeException('Transaksi dan lampiran gagal disimpan lengkap.');$this->db->trans_commit();return$id;}catch(Throwable$e){$this->db->trans_rollback();foreach($this->storedPaths as$path)if(is_file($path))@unlink($path);$this->storedPaths=array();throw$e;}
|
||||
}
|
||||
|
||||
private function validateFiles($input)
|
||||
{
|
||||
$files=$_FILES[$input]??array();$names=array_values(array_filter((array)($files['name']??array()),fn($name)=>trim((string)$name)!==''));if(!$names)throw new BusinessException('Minimal satu lampiran gambar atau PDF wajib diunggah.');if(count($names)>10)throw new BusinessException('Maksimal 10 lampiran per dokumen.');$extensions=array('pdf','jpg','jpeg','jpe','jfif','png','webp','gif','bmp','tif','tiff','heic','heif','avif','jp2','j2k','jpf','jpg2','jpx','ico');foreach((array)$files['name']as$i=>$name){if(trim((string)$name)==='')continue;if((int)($files['error'][$i]??UPLOAD_ERR_NO_FILE)!==UPLOAD_ERR_OK)throw new BusinessException('File '.$name.' gagal diterima server.');if((int)($files['size'][$i]??0)>5*1024*1024)throw new BusinessException('Ukuran '.$name.' melebihi 5 MB.');$extension=strtolower(pathinfo($name,PATHINFO_EXTENSION));if(!in_array($extension,$extensions,true))throw new BusinessException('Format .'.$extension.' tidak diizinkan. Gunakan gambar atau PDF.');$tmp=$files['tmp_name'][$i]??'';$mime=$tmp&&is_file($tmp)&&function_exists('finfo_open')?(new finfo(FILEINFO_MIME_TYPE))->file($tmp):($files['type'][$i]??'');if($mime!=='application/pdf'&&strpos((string)$mime,'image/')!==0)throw new BusinessException('Isi file '.$name.' bukan gambar atau PDF yang valid.');}
|
||||
}
|
||||
|
||||
private function storeFiles($input,$entityType,$entityId)
|
||||
{
|
||||
$files=$_FILES[$input];$dir=FCPATH.'uploads/accounting/';if(!is_dir($dir)&&!mkdir($dir,0750,true))throw new RuntimeException('Folder lampiran tidak tersedia.');$this->load->library('upload');foreach((array)$files['name']as$i=>$name){if(trim((string)$name)==='')continue;$_FILES['adjustment_attachment']=array('name'=>$name,'type'=>$files['type'][$i],'tmp_name'=>$files['tmp_name'][$i],'error'=>$files['error'][$i],'size'=>$files['size'][$i]);$this->upload->initialize(array('upload_path'=>$dir,'allowed_types'=>'pdf|jpg|jpeg|jpe|jfif|png|webp|gif|bmp|tif|tiff|heic|heif|avif|jp2|j2k|jpf|jpg2|jpx|ico','max_size'=>5120,'encrypt_name'=>true));if(!$this->upload->do_upload('adjustment_attachment'))throw new BusinessException('File '.$name.' ditolak: '.strip_tags($this->upload->display_errors()));$file=$this->upload->data();$this->storedPaths[]=$file['full_path'];$this->db->insert('transaction_attachments',array('module'=>'purchase','entity_type'=>$entityType,'entity_id'=>$entityId,'original_name'=>$file['orig_name'],'stored_name'=>$file['file_name'],'mime_type'=>$file['file_type'],'file_size'=>$file['file_size']*1024,'sha256'=>hash_file('sha256',$file['full_path']),'uploaded_by'=>$this->uid(),'created_at'=>date('Y-m-d H:i:s')));}return true;
|
||||
}
|
||||
|
||||
private function action(callable$callback,$message,$section)
|
||||
{
|
||||
if(strtoupper($this->input->method())!=='POST')show_404();try{$result=$callback();if($this->input->is_ajax_request())return json_response(true,$message,array('id'=>$result));$this->session->set_flashdata('success',$message);}catch(Throwable$e){if($this->input->is_ajax_request())return json_response(false,$e->getMessage(),array(),method_exists($e,'getStatusCode')?$e->getStatusCode():422);$this->session->set_flashdata('error',$e->getMessage());}redirect('purchases/returns?section='.$section);
|
||||
}
|
||||
|
||||
private function requireAnyView(){if($this->session->userdata('role')==='Admin')return true;if(check_permission('purchase_returns','can_view')||check_permission('purchase_refunds','can_view')||check_permission('supplier_debit_notes','can_view'))return true;show_error('Anda tidak memiliki izin melihat Retur & Refund.',403,'Akses ditolak');}
|
||||
private function requireDocumentPermission($type,$action){$feature=in_array($type,array('supplier_refund','supplier_refund_receipt'),true)?'purchase_refunds':($type==='supplier_debit_note'?'supplier_debit_notes':'purchase_returns');return$this->requirePermission($feature,$action);}
|
||||
private function uid(){return(int)$this->session->userdata('user_id');}
|
||||
private function roleId(){if($id=(int)$this->session->userdata('role_id'))return$id;$user=$this->db->select('role_id')->get_where('users',array('id'=>$this->uid()))->row();return$user?(int)$user->role_id:0;}
|
||||
public function amount($value){$value=trim((string)$value);if($value==='')return 0.0;$value=preg_replace('/[^0-9,\.\-]/','',$value);if(strpos($value,',')!==false)return(float)str_replace(',','.',str_replace('.','',$value));if(preg_match('/^-?\d{1,3}(?:\.\d{3})+$/',$value))return(float)str_replace('.','',$value);return(float)$value;}
|
||||
private function pdfPairs($type,$h)
|
||||
{
|
||||
$supplier=trim(($h['supplier_code']??'').' - '.($h['supplier_name']??''));
|
||||
if(in_array($type,array('purchase_return','return_resolution'),true))return array('Supplier'=>$supplier,'Purchase Order'=>$h['po_no']??'-','Tanggal Pengajuan'=>$h['return_date'],'Kategori Masalah'=>$h['problem_category'],'Penyelesaian Diminta'=>$h['requested_resolution'],'Alasan'=>$h['reason'],'Status'=>$h['status'],'Dibuat oleh'=>$h['created_name']??'-','Diajukan oleh'=>$h['submitted_name']??'-','Disetujui oleh'=>$h['approved_name']??'-','Diselesaikan oleh'=>$h['resolved_name']??'-','Referensi Jurnal'=>$h['journal_id']??'-','Jurnal Reversal'=>$h['reversal_journal_id']??'-');
|
||||
if($type==='purchase_return_shipment')return array('Supplier'=>$supplier,'Retur Sumber'=>$h['return_no']??'-','Purchase Order'=>$h['po_no']??'-','Tanggal Pengiriman'=>$h['shipment_date']??'-','Kurir'=>$h['courier']??'-','Nomor Resi'=>$h['tracking_no']??'-','Kategori Masalah'=>$h['problem_category']??'-','Alasan Retur'=>$h['reason']??'-','Status'=>$h['status']??'-','Diproses oleh'=>$h['posted_name']??'-');
|
||||
if($type==='supplier_refund')return array('Supplier'=>$supplier,'PO'=>$h['po_no']??'-','Invoice'=>$h['internal_no']??'-','Tanggal Klaim'=>$h['claim_date'],'Estimasi Diterima'=>$h['expected_date']??'-','Nilai Diajukan'=>'Rp '.number_format($h['claim_amount'],2,',','.'),'Sudah Diterima'=>'Rp '.number_format($h['received_amount'],2,',','.'),'Sisa'=>'Rp '.number_format($h['balance'],2,',','.'),'Alasan'=>$h['reason'],'Status'=>$h['status'],'Dibuat oleh'=>$h['created_name']??'-','Diajukan oleh'=>$h['submitted_name']??'-','Disetujui oleh'=>$h['approved_name']??'-','Ditolak oleh'=>$h['rejected_name']??'-','Jurnal Hak Refund'=>$h['entitlement_journal_id']??'-');
|
||||
if($type==='supplier_refund_receipt')return array('Supplier'=>$supplier,'Klaim Refund'=>$h['refund_no']??'-','Tanggal Dana Masuk'=>$h['receipt_date']??'-','Akun Penerima'=>trim(($h['kode_akun']??'').' - '.($h['nama_akun']??'')),'Nominal'=>'Rp '.number_format($h['amount'],2,',','.'),'Referensi Bank'=>$h['reference_no']??'-','Status'=>$h['status']??'-','Diproses oleh'=>$h['received_name']??'-','Jurnal'=>$h['journal_id']??'-');
|
||||
if($type==='supplier_debit_note')return array('Supplier'=>$supplier,'PO'=>$h['po_no']??'-','Invoice'=>$h['internal_no']??'-','Tanggal'=>$h['note_date'],'Jenis Koreksi'=>$h['correction_type'],'Nilai Sebelum Pajak'=>'Rp '.number_format($h['base_amount'],2,',','.'),'Pajak'=>'Rp '.number_format($h['tax_amount'],2,',','.'),'Total'=>'Rp '.number_format($h['amount'],2,',','.'),'Alasan'=>$h['reason'],'Status'=>$h['status'],'Dibuat oleh'=>$h['created_name']??'-','Diposting oleh'=>$h['posted_name']??'-','Jurnal'=>$h['journal_id']??'-','Jurnal Reversal'=>$h['reversal_journal_id']??'-');
|
||||
return array('Supplier'=>$supplier,'Retur Sumber'=>$h['return_no']??'-','Tanggal Diterima'=>$h['receipt_date']??'-','Gudang'=>$h['warehouse_name']??'-','Surat Jalan'=>$h['supplier_delivery_no']??'-','Status'=>$h['status']??'-');
|
||||
}
|
||||
private function pdfText($value){return iconv('UTF-8','windows-1252//TRANSLIT',(string)$value);}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
class Purchases extends MY_Controller
|
||||
{
|
||||
public function __construct(){parent::__construct();$this->load->library('PurchaseService');}
|
||||
private function uid(){return(int)$this->session->userdata('user_id');}private function role(){if($id=(int)$this->session->userdata('role_id'))return$id;$u=$this->db->select('role_id')->get_where('users',array('id'=>$this->uid()))->row();return$u?(int)$u->role_id:0;}
|
||||
private function departmentName($id){$d=$this->db->get_where('k_departments',array('id'=>(int)$id,'is_active'=>1))->row();return$d?$d->department_name:'';}
|
||||
public function index(){$data=array('active_menu'=>'purchases','requests'=>$this->db->order_by('id','DESC')->get('purchase_requests')->result(),'orders'=>$this->db->select('purchase_orders.*,suppliers.name')->from('purchase_orders')->join('suppliers','suppliers.id=purchase_orders.supplier_id')->order_by('purchase_orders.id','DESC')->get()->result(),'receipts'=>$this->db->select('goods_receipts.*,purchase_orders.po_no,suppliers.name')->from('goods_receipts')->join('purchase_orders','purchase_orders.id=goods_receipts.purchase_order_id')->join('suppliers','suppliers.id=goods_receipts.supplier_id')->order_by('goods_receipts.id','DESC')->get()->result(),'bills'=>$this->db->select("supplier_invoices.*,suppliers.name,(SELECT po.payment_timing FROM supplier_invoice_lines sil JOIN purchase_order_lines pol ON pol.id=sil.po_line_id JOIN purchase_orders po ON po.id=pol.purchase_order_id WHERE sil.supplier_invoice_id=supplier_invoices.id LIMIT 1) po_payment_timing,(SELECT po.paid_amount FROM supplier_invoice_lines sil JOIN purchase_order_lines pol ON pol.id=sil.po_line_id JOIN purchase_orders po ON po.id=pol.purchase_order_id WHERE sil.supplier_invoice_id=supplier_invoices.id LIMIT 1) po_paid_amount,(SELECT po.total FROM supplier_invoice_lines sil JOIN purchase_order_lines pol ON pol.id=sil.po_line_id JOIN purchase_orders po ON po.id=pol.purchase_order_id WHERE sil.supplier_invoice_id=supplier_invoices.id LIMIT 1) po_total",false)->from('supplier_invoices')->join('suppliers','suppliers.id=supplier_invoices.supplier_id')->order_by('supplier_invoices.id','DESC')->get()->result(),'suppliers'=>$this->db->where('is_active',1)->order_by('name')->get('suppliers')->result(),'catalogs'=>$this->db->order_by('kode_barang')->get('kode_barang')->result(),'warehouses'=>$this->db->order_by('nama')->get('warehouses')->result(),'asset_locations'=>$this->db->order_by('nama')->get('lokasi_asset')->result(),'asset_accounts'=>$this->db->where('tipe','asset')->where('allow_posting',1)->order_by('kode_akun')->get('accounts')->result());$step=$this->input->get('step')?:'request';$this->load->view('partials/header',$data);$this->load->view($step==='request'?'purchases/index':'purchases/workflow',$data);$this->load->view('partials/footer');}
|
||||
private function requestDocument($id){$header=$this->db->select('pr.*,d.code department_code,d.department_name,creator.nama creator_name,creator.username creator_username,approver.nama approver_name,approver.username approver_username')->from('purchase_requests pr')->join('k_departments d','d.id=pr.department_id','left')->join('users creator','creator.id=pr.requested_by','left')->join('users approver','approver.id=pr.approved_by','left')->where('pr.id',(int)$id)->get()->row();if(!$header||($header->status==='draft'&&(int)$header->requested_by!==$this->uid()))return null;$header->applicant_display=((int)$header->applicant_is_creator===1)?($header->creator_name?:$header->creator_username?:'Pengguna #'.$header->requested_by):$header->applicant_name;$lines=$this->db->select('l.*,k.kode_barang,k.nama master_name')->from('purchase_request_lines l')->join('kode_barang k','k.id=l.kode_barang_id','left')->where('l.purchase_request_id',$header->id)->order_by('l.id')->get()->result();return array('request'=>$header,'lines'=>$lines);}
|
||||
public function request_detail($id){$data=$this->requestDocument($id);if(!$data)return json_response(false,'Purchase Request tidak ditemukan.',array(),404);return json_response(true,'Detail Purchase Request.',$data);}
|
||||
public function request_pdf($id){
|
||||
$data=$this->requestDocument($id);if(!$data)show_404();require_once APPPATH.'third_party/fpdf/fpdf.php';$r=$data['request'];
|
||||
$creator=$r->creator_name?:$r->creator_username?:'Pengguna #'.$r->requested_by;$requester=$r->applicant_display?:$creator;$approver=$r->approver_name?:$r->approver_username?:'-';
|
||||
$pdf=new FPDF('P','mm','A4');$pdf->SetMargins(15,15,15);$pdf->AddPage();$pdf->SetFont('Arial','B',15);$pdf->Cell(0,8,'PERMINTAAN PEMBELIAN',0,1,'C');$pdf->SetFont('Arial','',9);$pdf->Cell(0,6,'Nomor: '.$r->request_no,0,1,'C');$pdf->Ln(5);
|
||||
$pdf->SetFont('Arial','',10);foreach(array('Tanggal'=>$r->request_date,'Dibutuhkan'=>$r->needed_date,'Departemen'=>trim(($r->department_code?:'').' '.($r->department_name?:$r->department)),'Status'=>strtoupper($r->status),'Kebutuhan'=>$r->purpose,'Dibuat oleh'=>$creator,'Diajukan oleh'=>$requester,'Disetujui oleh'=>$approver)as$label=>$value){$pdf->SetFont('Arial','B',10);$pdf->Cell(35,6,$label,0,0);$pdf->SetFont('Arial','',10);$pdf->MultiCell(0,6,': '.($value?:'-'));}
|
||||
$pdf->Ln(3);$pdf->SetFont('Arial','B',9);foreach(array(12=>'No',38=>'Kode - Barang',70=>'Deskripsi / Spesifikasi',22=>'Qty',30=>'Estimasi')as$w=>$label)$pdf->Cell($w,8,$label,1,0,'C');$pdf->Ln();$no=1;$total=0;foreach($data['lines']as$l){$amount=(float)$l->qty*(float)$l->estimated_price;$total+=$amount;$pdf->SetFont('Arial','',8);$pdf->Cell(12,8,$no++,1,0,'C');$pdf->Cell(38,8,substr(($l->kode_barang?:'-').' - '.($l->master_name?:'-'),0,24),1);$pdf->Cell(70,8,substr($l->description,0,42),1);$pdf->Cell(22,8,number_format($l->qty,0,',','.'),1,0,'R');$pdf->Cell(30,8,number_format($amount,0,',','.'),1,0,'R');$pdf->Ln();}$pdf->SetFont('Arial','B',9);$pdf->Cell(142,8,'TOTAL ESTIMASI',1,0,'R');$pdf->Cell(30,8,number_format($total,0,',','.'),1,1,'R');
|
||||
$pdf->Ln(14);$pdf->SetFont('Arial','',9);$pdf->Cell(86,6,'Pemohon / Yang Mengajukan',0,0,'C');$pdf->Cell(86,6,'Yang Menyetujui',0,1,'C');$pdf->Ln(18);$pdf->SetFont('Arial','B',9);$pdf->Cell(86,6,'('.$requester.')',0,0,'C');$pdf->Cell(86,6,'('.$approver.')',0,1,'C');$pdf->SetFont('Arial','',8);$pdf->Cell(86,5,'Tanggal: '.$r->request_date,0,0,'C');$pdf->Cell(86,5,'Tanggal: '.($r->approved_at?:'-'),0,1,'C');$pdf->Output('I','PR-'.$r->request_no.'.pdf');
|
||||
}
|
||||
public function create_request(){$catalogs=(array)$this->input->post('kode_barang_id');$descriptions=(array)$this->input->post('description',true);$quantities=(array)$this->input->post('qty');$barcodeQuantities=(array)$this->input->post('qty_per_barcode');$prices=(array)$this->input->post('estimated_price');$lines=array();foreach($catalogs as$i=>$catalogId)$lines[]=array('kode_barang_id'=>$catalogId,'description'=>$descriptions[$i]??'','qty'=>$quantities[$i]??0,'qty_per_barcode'=>$barcodeQuantities[$i]??null,'estimated_price'=>$prices[$i]??0);return$this->act(fn()=>$this->purchaseservice->createCatalogRequest(array('purpose'=>$this->input->post('purpose',true),'destination_type'=>'warehouse','date'=>$this->input->post('date'),'needed_date'=>$this->input->post('needed_date'),'department_id'=>$this->input->post('department_id'),'department'=>$this->departmentName($this->input->post('department_id')),'lines'=>$lines,'applicant_is_creator'=>$this->input->post('applicant_is_creator'),'applicant_name'=>$this->input->post('applicant_name',true)),$this->uid()),'Purchase request gudang dibuat dengan '.count($lines).' barang.');}
|
||||
public function submit_request($id){return$this->act(fn()=>$this->purchaseservice->submitRequest($id,$this->uid()),'Purchase request diajukan.');}public function approve_request($id){$this->requirePermission('purchases','can_approve');return$this->act(fn()=>$this->purchaseservice->approveRequest($id,$this->uid(),$this->role()),'Purchase request disetujui.');}public function reject_request($id){$this->requirePermission('purchases','can_approve');return$this->act(fn()=>$this->purchaseservice->rejectRequest($id,$this->uid(),$this->role(),$this->input->post('reason',true)),'Purchase request ditolak.');}
|
||||
public function delete_request($id){return$this->act(fn()=>$this->purchaseservice->deleteRequest($id,$this->uid()),'Draft Purchase Request berhasil dihapus.');}
|
||||
public function create_order(){return$this->act(function(){$method=$this->input->post('procurement_method');$this->validatePurchaseDocuments($method);$id=$this->purchaseservice->createProfessionalOrder(array('request_id'=>$this->input->post('request_id'),'supplier_id'=>$this->input->post('supplier_id'),'warehouse_id'=>$this->input->post('warehouse_id'),'date'=>$this->input->post('date'),'expected_date'=>$this->input->post('expected_date'),'procurement_method'=>$method,'payment_timing'=>$this->input->post('payment_timing'),'marketplace'=>$this->input->post('marketplace',true),'external_order_no'=>$this->input->post('external_order_no',true),'courier'=>$this->input->post('courier',true),'tracking_no'=>$this->input->post('tracking_no',true),'shipping_cost'=>$this->input->post('shipping_cost'),'insurance_cost'=>$this->input->post('insurance_cost'),'service_fee'=>$this->input->post('service_fee'),'discount_amount'=>$this->input->post('discount_amount'),'cashback_amount'=>$this->input->post('cashback_amount'),'down_payment_required'=>$this->input->post('down_payment_required'),'down_payment_amount'=>$this->input->post('down_payment_amount'),'installment_count'=>$this->input->post('installment_count'),'first_due_date'=>$this->input->post('first_due_date'),'unit_prices'=>(array)$this->input->post('unit_price'),'tax_rates'=>(array)$this->input->post('tax_rate'),'notes'=>$this->input->post('notes',true)),$this->uid());$this->applyPoBudgetAccounts($id);$this->storePurchaseDocuments($id);return$id;},'Purchase Order multi-item, akun budget pengeluaran, dan dokumen pendukung berhasil dibuat.');}
|
||||
private function applyPoBudgetAccounts($poId){
|
||||
if(!$this->db->field_exists('budget_account_id','purchase_order_lines'))return;
|
||||
$company=$this->companycontext->id();$accounts=(array)$this->input->post('budget_account_id');
|
||||
foreach($this->db->where('purchase_order_id',(int)$poId)->get('purchase_order_lines')->result()as$line){
|
||||
$account=(int)($accounts[$line->request_line_id]??0);
|
||||
if($account&&!$this->db->get_where('accounts',array('id'=>$account,'company_id'=>$company,'tipe'=>'expense','is_active'=>1,'allow_posting'=>1))->row())throw new BusinessException('Akun budget pengeluaran pada barang PO tidak valid.');
|
||||
$this->db->where('id',$line->id)->update('purchase_order_lines',array('budget_account_id'=>$account?:null));
|
||||
}
|
||||
if($this->db->field_exists('company_id','purchase_orders'))$this->db->where('id',(int)$poId)->update('purchase_orders',array('company_id'=>$company));
|
||||
}
|
||||
private function validatePurchaseDocuments($method){$this->validateEvidenceFiles('purchase_documents',$method==='online'?'Bukti/detail pesanan online wajib diunggah.':($method==='direct'?'Foto struk pembelian wajib diunggah.':'Dokumen penawaran/detail order supplier wajib diunggah.'));}
|
||||
private function scopePurchaseOrderItems($orderId)
|
||||
{
|
||||
$companyId=(int)$this->session->userdata('company_id')?:1;
|
||||
$this->db->query('UPDATE items i JOIN purchase_order_lines pol ON pol.item_id=i.id SET i.company_id=? WHERE pol.purchase_order_id=? AND (i.company_id IS NULL OR i.company_id=0)',array($companyId,(int)$orderId));
|
||||
$foreign=$this->db->select('i.*,pol.id po_line_id,pol.qty po_qty,pol.unit_price po_unit_price,pol.warehouse_id po_warehouse_id,po.order_date po_date')->from('purchase_order_lines pol')->join('purchase_orders po','po.id=pol.purchase_order_id')->join('items i','i.id=pol.item_id')->where('pol.purchase_order_id',(int)$orderId)->where('i.company_id !=',$companyId)->get()->result();
|
||||
$itemFields=array_flip($this->db->list_fields('items'));
|
||||
foreach($foreign as$row){$copy=array_intersect_key((array)$row,$itemFields);unset($copy['id']);$copy['company_id']=$companyId;$suffix='-C'.$companyId.'-PO'.$orderId.'-'.$row->po_line_id;$copy['kode_detail']=substr((string)$row->kode_detail,0,max(1,100-strlen($suffix))).$suffix;$copy['stok']=0;$copy['harga_beli']=$row->po_unit_price;$copy['harga_jual']=0;$copy['status']='draft';if(isset($itemFields['planned_qty']))$copy['planned_qty']=$row->po_qty;if(isset($itemFields['planned_warehouse_id']))$copy['planned_warehouse_id']=$row->po_warehouse_id;if(isset($itemFields['tanggal_pembelian']))$copy['tanggal_pembelian']=$row->po_date;$this->db->insert('items',$copy);$newId=$this->db->insert_id();if(!$newId)throw new BusinessException('Barang PO gagal dipisahkan untuk perusahaan aktif.');$this->db->where('id',$row->po_line_id)->update('purchase_order_lines',array('item_id'=>$newId));}
|
||||
$remaining=(int)$this->db->select('COUNT(*) total',false)->from('purchase_order_lines pol')->join('items i','i.id=pol.item_id')->where('pol.purchase_order_id',(int)$orderId)->where('i.company_id !=',$companyId)->get()->row()->total;if($remaining>0)throw new BusinessException('Purchase Order memuat barang milik perusahaan lain. Proses dibatalkan.');
|
||||
}
|
||||
private function storePurchaseDocuments($entityId,$input='purchase_documents',$entityType='purchase_order')
|
||||
{
|
||||
if($entityType==='purchase_order'){
|
||||
$this->scopePurchaseOrderItems($entityId);
|
||||
}
|
||||
$files=$_FILES[$input]??array('name'=>array(),'type'=>array(),'tmp_name'=>array(),'error'=>array(),'size'=>array());$dir=FCPATH.'uploads/accounting/';
|
||||
if(!is_dir($dir)&&!mkdir($dir,0750,true))throw new BusinessException('Folder lampiran tidak tersedia.');
|
||||
$count=count((array)$files['name']);if($count>10)throw new BusinessException('Maksimal 10 lampiran untuk satu transaksi.');
|
||||
$this->load->library('upload');$stored=array();
|
||||
try{
|
||||
for($i=0;$i<$count;$i++){
|
||||
if(empty($files['name'][$i]))continue;
|
||||
$_FILES['attachment']=array('name'=>$files['name'][$i],'type'=>$files['type'][$i],'tmp_name'=>$files['tmp_name'][$i],'error'=>$files['error'][$i],'size'=>$files['size'][$i]);
|
||||
$this->upload->initialize(array('upload_path'=>$dir,'allowed_types'=>'pdf|jpg|jpeg|jpe|jfif|png|webp|gif|bmp|tif|tiff|heic|heif|avif|jp2|j2k|jpf|jpg2|jpx|ico','max_size'=>5120,'encrypt_name'=>true));
|
||||
if(!$this->upload->do_upload('attachment'))throw new BusinessException('File '.$files['name'][$i].' ditolak: '.strip_tags($this->upload->display_errors()));
|
||||
$f=$this->upload->data();$stored[]=$f['full_path'];
|
||||
$this->db->insert('transaction_attachments',array('module'=>'purchase','entity_type'=>$entityType,'entity_id'=>$entityId,'original_name'=>$f['orig_name'],'stored_name'=>$f['file_name'],'mime_type'=>$f['file_type'],'file_size'=>$f['file_size']*1024,'sha256'=>hash_file('sha256',$f['full_path']),'uploaded_by'=>$this->uid(),'created_at'=>date('Y-m-d H:i:s')));
|
||||
if(!$this->db->affected_rows())throw new BusinessException('Metadata lampiran gagal disimpan.');
|
||||
}
|
||||
return$stored;
|
||||
}catch(Throwable$e){foreach($stored as$path)if(is_file($path))@unlink($path);throw$e;}
|
||||
}
|
||||
public function submit_order($id){return$this->act(fn()=>$this->purchaseservice->submitOrder($id,$this->uid()),'Purchase order diajukan.');}public function approve_order($id){$this->requirePermission('purchases','can_approve');return$this->act(fn()=>$this->purchaseservice->approveOrder($id,$this->uid(),$this->role()),'Purchase order disetujui.');}public function reject_order($id){$this->requirePermission('purchases','can_approve');return$this->act(fn()=>$this->purchaseservice->rejectOrder($id,$this->uid(),$this->role(),$this->input->post('reason',true)),'Purchase order ditolak.');}
|
||||
public function update_order($id){$this->requirePermission('purchases','can_update');$prices=(array)$this->input->post('unit_price');$taxes=(array)$this->input->post('tax_rate');return$this->act(fn()=>$this->purchaseservice->updateDraftOrder($id,array('supplier_id'=>$this->input->post('supplier_id'),'warehouse_id'=>$this->input->post('warehouse_id'),'date'=>$this->input->post('date'),'expected_date'=>$this->input->post('expected_date'),'procurement_method'=>$this->input->post('procurement_method'),'payment_timing'=>$this->input->post('payment_timing'),'down_payment_required'=>$this->input->post('down_payment_required'),'down_payment_amount'=>$this->input->post('down_payment_amount'),'installment_count'=>$this->input->post('installment_count'),'first_due_date'=>$this->input->post('first_due_date'),'marketplace'=>$this->input->post('marketplace',true),'external_order_no'=>$this->input->post('external_order_no',true),'courier'=>$this->input->post('courier',true),'tracking_no'=>$this->input->post('tracking_no',true),'shipping_cost'=>$this->input->post('shipping_cost'),'insurance_cost'=>$this->input->post('insurance_cost'),'service_fee'=>$this->input->post('service_fee'),'discount_amount'=>$this->input->post('discount_amount'),'cashback_amount'=>$this->input->post('cashback_amount'),'notes'=>$this->input->post('notes',true),'unit_prices'=>$prices,'tax_rates'=>$taxes),$this->uid()),'Draft Purchase Order berhasil diperbarui.');}
|
||||
public function pay_order($id){$this->requirePermission('purchases','can_post');$accounts=(array)$this->input->post('source_account_id');$amounts=(array)$this->input->post('source_amount');$sources=array();foreach($accounts as$i=>$account)if($account)$sources[(int)$account]=($sources[(int)$account]??0)+$this->purchaseAmount($amounts[$i]??0);$bankCharge=$this->purchaseAmount($this->input->post('bank_charge_amount'));$bankChargeAccount=(int)$this->input->post('bank_charge_account_id');return$this->act(function()use($id,$sources,$bankCharge,$bankChargeAccount){$this->validateEvidenceFiles('payment_documents','Bukti pembayaran wajib diunggah oleh Finance.');$paymentId=$this->purchaseservice->prepayOrderWithBankCharge($id,$this->input->post('date'),$sources,$this->input->post('reference_no',true),$this->input->post('notes',true),$this->uid(),$bankCharge,$bankChargeAccount);$this->storePurchaseDocuments($paymentId,'payment_documents','supplier_payment');return$paymentId;},'Pembayaran PO, biaya admin bank, dan bukti pembayaran berhasil diposting.');}
|
||||
private function purchaseAmount($value){$value=trim((string)$value);if($value==='')return 0.0;$value=preg_replace('/[^0-9,\.\-]/','',$value);if(strpos($value,',')!==false)return(float)str_replace(',','.',str_replace('.','',$value));if(preg_match('/^-?\d{1,3}(?:\.\d{3})+$/',$value))return(float)str_replace('.','',$value);return(float)$value;}
|
||||
private function validateEvidenceFiles($input,$requiredMessage){$files=$_FILES[$input]??array();$names=array_values(array_filter((array)($files['name']??array()),function($name){return trim((string)$name)!=='';}));if(!$names)throw new BusinessException($requiredMessage);if(count($names)>10)throw new BusinessException('Maksimal 10 lampiran untuk satu transaksi.');$extensions=array('pdf','jpg','jpeg','jpe','jfif','png','webp','gif','bmp','tif','tiff','heic','heif','avif','jp2','j2k','jpf','jpg2','jpx','ico');foreach((array)$files['name']as$i=>$name){if(trim((string)$name)==='')continue;$error=(int)($files['error'][$i]??UPLOAD_ERR_NO_FILE);if($error!==UPLOAD_ERR_OK)throw new BusinessException($error===UPLOAD_ERR_INI_SIZE||$error===UPLOAD_ERR_FORM_SIZE?'Ukuran file '.$name.' melebihi batas server.':'File '.$name.' gagal diterima oleh server (kode '.$error.').');if((int)($files['size'][$i]??0)>5*1024*1024)throw new BusinessException('Ukuran file '.$name.' melebihi 5 MB.');$extension=strtolower(pathinfo($name,PATHINFO_EXTENSION));if(!in_array($extension,$extensions,true))throw new BusinessException('Format .'.$extension.' tidak diizinkan. Gunakan PDF atau file gambar.');$tmp=$files['tmp_name'][$i]??'';$mime=$tmp&&is_file($tmp)&&function_exists('finfo_open')?(new finfo(FILEINFO_MIME_TYPE))->file($tmp):($files['type'][$i]??'');if($mime!=='application/pdf'&&strpos((string)$mime,'image/')!==0)throw new BusinessException('Isi file '.$name.' bukan PDF atau gambar yang valid.');}}
|
||||
public function refund_order($id){return$this->movedAdjustment('refund','Proses refund lama dinonaktifkan agar jurnal dan penerimaan dana tidak terduplikasi. Gunakan submenu Retur & Refund.','po_id',(int)$id);}
|
||||
public function receive($id)
|
||||
{
|
||||
$qty=(array)$this->input->post('qty');
|
||||
return$this->act(function()use($id,$qty){
|
||||
$this->validateEvidenceFiles('receipt_documents','Bukti penerimaan barang wajib diunggah oleh petugas Gudang.');
|
||||
$this->db->trans_begin();
|
||||
try{
|
||||
$rid=$this->purchaseservice->receive($id,$this->input->post('date'),$this->input->post('delivery_no',true),$qty,$this->uid());
|
||||
$this->purchaseservice->finalizeReceiptTracking($rid);
|
||||
$this->purchaseservice->syncReceiptLedger($rid);
|
||||
$this->purchaseservice->clearTransitForReceipt($rid,$this->uid());
|
||||
$this->storePurchaseDocuments($rid,'receipt_documents','goods_receipt');
|
||||
if($this->db->trans_status()===false)throw new BusinessException('Penerimaan gagal disimpan secara lengkap.');
|
||||
$this->db->trans_commit();return$rid;
|
||||
}catch(Throwable$e){$this->db->trans_rollback();throw$e;}
|
||||
},'Penerimaan barang, bukti penerimaan, barcode, dan stock ledger berhasil diposting.');
|
||||
}
|
||||
public function create_bill(){return$this->act(function(){$this->validateEvidenceFiles('invoice_documents','Invoice/nota supplier wajib diunggah.');$po=$this->input->post('po_id');$id=$this->purchaseservice->createSupplierInvoice($po,$this->input->post('receipt_id'),$this->input->post('supplier_invoice_no',true),$this->input->post('date'),$this->uid());$this->purchaseservice->syncInvoicePaymentPlan($id,$po);$this->storePurchaseDocuments($id,'invoice_documents','supplier_invoice');$this->purchaseservice->verifySupplierInvoice($id,$this->uid());return$id;},'Invoice Supplier tersimpan, matching terverifikasi, dan siap ditindaklanjuti.');}
|
||||
public function submit_bill($id){return$this->act(fn()=>$this->purchaseservice->submitSupplierInvoice($id,$this->uid()),'Tagihan diajukan.');}public function approve_bill($id){$this->requirePermission('purchases','can_approve');return$this->act(fn()=>$this->purchaseservice->approveSupplierInvoice($id,$this->uid(),$this->role()),'Tagihan disetujui.');}public function post_bill($id){$this->requirePermission('purchases','can_post');$accounts=(array)$this->input->post('source_account_id');$amounts=(array)$this->input->post('source_amount');$sources=array();foreach($accounts as$i=>$account)if((int)$account)$sources[(int)$account]=($sources[(int)$account]??0)+$this->purchaseAmount($amounts[$i]??0);$bankCharge=$this->purchaseAmount($this->input->post('bank_charge_amount'));$bankChargeAccount=(int)$this->input->post('bank_charge_account_id');return$this->act(function()use($id,$sources,$bankCharge,$bankChargeAccount){$bill=$this->db->select('si.payment_plan,si.total,po.payment_timing,po.paid_amount')->from('supplier_invoices si')->join('supplier_invoice_lines sil','sil.supplier_invoice_id=si.id')->join('purchase_order_lines pol','pol.id=sil.po_line_id')->join('purchase_orders po','po.id=pol.purchase_order_id')->where('si.id',(int)$id)->limit(1)->get()->row();if(!$bill)throw new BusinessException('Invoice supplier tidak ditemukan.');$alreadyPaid=in_array($bill->payment_timing,array('prepaid','on_receipt'),true)&&(float)$bill->paid_amount>=(float)$bill->total-.001;$createsPayment=$bill->payment_plan==='cash'&&!$alreadyPaid;if($createsPayment)$this->validateEvidenceFiles('invoice_payment_documents','Bukti pembayaran invoice supplier wajib diunggah oleh Finance.');$result=$this->purchaseservice->settleSupplierInvoiceMultiWithBankCharge($id,$sources,$this->input->post('payment_date'),$this->input->post('reference_no',true),$this->uid(),$bankCharge,$bankChargeAccount);if($createsPayment&&!empty($result['payment_id']))$this->storePurchaseDocuments($result['payment_id'],'invoice_payment_documents','supplier_payment');return$result;},'Invoice supplier, pembayaran, dan biaya admin bank berhasil diproses.');}
|
||||
public function verify_bill($id){return$this->act(fn()=>$this->purchaseservice->verifySupplierInvoice($id,$this->uid()),'Matching Invoice Supplier terverifikasi tanpa approval.');}
|
||||
public function purchase_return(){return$this->movedAdjustment('return','Retur langsung lama dinonaktifkan. Buat pengajuan retur agar approval, reservasi barcode, stok, dan audit trail tetap konsisten.','receipt_line_id',(int)$this->input->post('receipt_line_id'));}
|
||||
public function debit_note(){return$this->movedAdjustment('debit_note','Debit Note dipindahkan ke submenu Retur & Refund agar saldo invoice dan reversal dapat ditelusuri.','invoice_id',(int)$this->input->post('bill_id'));}
|
||||
private function movedAdjustment($action,$message,$parameter,$id){if($this->input->is_ajax_request())return json_response(false,$message,array('redirect'=>base_url('purchases/returns?action='.$action.'&'.$parameter.'='.$id)),409);$this->session->set_flashdata('error',$message);redirect('purchases/returns?action='.$action.'&'.$parameter.'='.$id);}
|
||||
private function act($fn,$msg){if(strtoupper($this->input->method())!=='POST')show_404();$step=$this->input->post('return_step',true)?:$this->input->get('step',true);try{$r=$fn();$this->session->set_flashdata('success',$msg);redirect('purchases'.($step?'?step='.$step:''));}catch(Throwable$e){$this->session->set_flashdata('error',$e->getMessage());redirect('purchases'.($step?'?step='.$step:''));}}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
|
||||
class Qr extends CI_Controller {
|
||||
class Qr extends MY_Controller {
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
class Queueworker extends CI_Controller
|
||||
{
|
||||
public function run($queue='default',$limit=10){if(!$this->input->is_cli_request())show_404();$this->load->library('JobQueueService');$this->jobqueueservice->recoverStale();$worker=gethostname().':'.getmypid();$done=0;for($i=0;$i<min(100,max(1,(int)$limit));$i++){$job=$this->jobqueueservice->reserve($worker,$queue);if(!$job)break;try{$payload=json_decode($job->payload,true);if(!is_array($payload))throw new RuntimeException('Payload job tidak valid.');$this->handle($job->job_type,$payload,$job);$this->jobqueueservice->complete($job->id);$done++;}catch(Throwable$e){$this->jobqueueservice->fail($job,$e);fwrite(STDERR,'Job '.$job->id.': '.$e->getMessage().PHP_EOL);}}echo $done." job selesai.\n";}
|
||||
public function replay($deadId){if(!$this->input->is_cli_request())show_404();$dead=$this->db->get_where('app_dead_letters',array('id'=>(int)$deadId))->row();if(!$dead)throw new RuntimeException('Dead-letter tidak ditemukan.');$this->load->library('JobQueueService');$id=$this->jobqueueservice->enqueue($dead->job_type,json_decode($dead->payload,true),'replay:'.$dead->job_id.':'.$dead->id.':'.date('YmdHis'),'default',5,3,$dead->company_id);echo 'Dead-letter direplay sebagai job '.$id.PHP_EOL;}
|
||||
private function handle($type,array$p,$job){
|
||||
if($type==='asset_depreciation'){$this->load->library('FixedAssetService');$this->fixedassetservice->depreciate((int)$p['asset_id'],$p['through'],(int)$p['user_id'],true);return;}
|
||||
if($type==='payroll_calculation'){$this->load->library('PayrollService');$this->payrollservice->calculate($p['month'],(int)$p['user_id']);return;}
|
||||
if($type==='email'||$type==='reminder'){$this->load->library('email');$this->email->from($p['from']??getenv('MAIL_FROM'),$p['from_name']??'Accounting');$this->email->to($p['to']);$this->email->subject($p['subject']);$this->email->message($p['message']);if(!$this->email->send(false))throw new RuntimeException('Email gagal dikirim: '.$this->email->print_debugger(array('headers')));return;}
|
||||
if($type!=='report_export')throw new RuntimeException('Handler job belum tersedia: '.$type);$filters=$p['filters'];$dir=FCPATH.'uploads/accounting/reports/';if(!is_dir($dir)&&!mkdir($dir,0750,true))throw new RuntimeException('Folder export tidak dapat dibuat.');$format=in_array($p['format'],array('pdf','xlsx','excel'),true)?$p['format']:'csv';$file='RPT-'.$job->id.'-'.date('YmdHis').'.'.($format==='pdf'?'html':'csv');$rows=$this->db->query("SELECT j.tanggal,j.no_ref,j.keterangan,a.kode_akun,a.nama_akun,d.debit,d.kredit FROM journal_details d JOIN journals j ON j.id=d.journal_id JOIN accounts a ON a.id=d.account_id WHERE j.company_id=? AND j.status IN('posted','reversed') AND j.tanggal BETWEEN ? AND ? ORDER BY j.tanggal,j.id,d.id",array((int)$job->company_id,$filters['from'],$filters['to']))->result_array();$h=fopen($dir.$file,'w');if(!$h)throw new RuntimeException('File export tidak dapat ditulis.');if($format==='pdf')fwrite($h,'<html><body><h2>'.html_escape($p['report_code']).'</h2><pre>'.html_escape(json_encode($rows,JSON_PRETTY_PRINT)).'</pre></body></html>');elseif($rows){fputcsv($h,array_keys($rows[0]));foreach($rows as$r)fputcsv($h,$r);}fclose($h);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
class Receivableattachments extends MY_Controller
|
||||
{
|
||||
public function upload(){$type=$this->input->post('entity_type');$id=(int)$this->input->post('entity_id');if(!in_array($type,array('invoice','payment','credit_note','refund'),true)||$id<1)return json_response(false,'Target lampiran tidak valid.',array(),422);$dir=FCPATH.'uploads/accounting/';if(!is_dir($dir)&&!mkdir($dir,0750,true))return json_response(false,'Folder lampiran tidak tersedia.',array(),500);$config=array('upload_path'=>$dir,'allowed_types'=>'pdf|jpg|jpeg|png|webp','max_size'=>5120,'encrypt_name'=>true);$this->load->library('upload',$config);if(!$this->upload->do_upload('attachment'))return json_response(false,strip_tags($this->upload->display_errors()),array(),422);$f=$this->upload->data();$this->db->insert('transaction_attachments',array('module'=>'receivable','entity_type'=>$type,'entity_id'=>$id,'original_name'=>$f['orig_name'],'stored_name'=>$f['file_name'],'mime_type'=>$f['file_type'],'file_size'=>$f['file_size']*1024,'sha256'=>hash_file('sha256',$f['full_path']),'uploaded_by'=>$this->session->userdata('user_id'),'created_at'=>date('Y-m-d H:i:s')));return json_response(true,'Lampiran berhasil disimpan.',array('id'=>$this->db->insert_id()));}
|
||||
public function download($id){$f=$this->db->get_where('transaction_attachments',array('id'=>(int)$id))->row();if(!$f)show_404();$path=FCPATH.'uploads/accounting/'.$f->stored_name;if(!is_file($path))show_404();$this->load->helper('download');force_download($f->original_name,file_get_contents($path));}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
class Receivablecheck extends CI_Controller
|
||||
{
|
||||
public function __construct(){parent::__construct();if(!is_cli())show_404();}
|
||||
public function schema(){foreach(array('invoices','invoice_details','payments','customers','stock_logs','item_movements') as $table){echo "--- {$table} ---\n";if(!$this->db->table_exists($table)){echo "MISSING\n";continue;}foreach($this->db->field_data($table) as $f)echo $f->name.' | '.$f->type.' | '.$f->max_length."\n";} $rows=$this->db->select('status,posted,COUNT(*) total')->group_by(array('status','posted'))->get('invoices')->result();foreach($rows as $r)echo "invoice {$r->status} posted={$r->posted} count={$r->total}\n";echo 'payments='.$this->db->count_all('payments')."\n";}
|
||||
public function accounts(){foreach($this->db->like('nama_akun','pajak')->or_like('nama_akun','piutang')->or_like('nama_akun','utang muka')->or_like('nama_akun','uang muka')->or_like('nama_akun','bad debt')->or_like('nama_akun','kerugian piutang')->order_by('kode_akun')->get('accounts')->result() as $a)echo $a->id.' | '.$a->kode_akun.' | '.$a->nama_akun."\n";}
|
||||
public function integrity(){
|
||||
$checks=array(
|
||||
'duplicate_invoice_numbers'=>"SELECT COUNT(*) total FROM (SELECT no_invoice FROM invoices GROUP BY no_invoice HAVING COUNT(*)>1)x",
|
||||
'duplicate_payment_numbers'=>"SELECT COUNT(*) total FROM (SELECT payment_no FROM payments GROUP BY payment_no HAVING COUNT(*)>1)x",
|
||||
'overallocated_payments'=>"SELECT COUNT(*) total FROM (SELECT p.id FROM payments p JOIN payment_allocations a ON a.payment_id=p.id GROUP BY p.id,p.jumlah HAVING SUM(a.amount)>p.jumlah+0.01)x",
|
||||
'overpaid_invoices'=>"SELECT COUNT(*) total FROM invoices WHERE total_bayar>total+0.01 OR sisa_piutang<0",
|
||||
'duplicate_invoice_stock_logs'=>"SELECT COUNT(*) total FROM (SELECT ref_id FROM stock_logs WHERE ref_type='invoice_details' AND tipe='keluar' GROUP BY ref_id HAVING COUNT(*)>1)x",
|
||||
'unbalanced_stage5_journals'=>"SELECT COUNT(*) total FROM (SELECT j.id FROM journals j JOIN journal_details d ON d.journal_id=j.id WHERE j.ref_type IN('invoice','customer_payment','credit_note','customer_refund','receivable_writeoff','invoice_reversal') GROUP BY j.id HAVING ABS(SUM(d.debit)-SUM(d.kredit))>0.01)x"
|
||||
);$failed=0;foreach($checks as$name=>$sql){$n=(int)$this->db->query($sql)->row()->total;echo ($n?'[FAIL] ':'[OK] ').$name.'='.$n."\n";if($n)$failed++;}foreach(array('accounts_receivable','sales_tax_payable','customer_advances','bad_debt_expense')as$key){$n=$this->db->where(array('mapping_key'=>$key,'is_active'=>1))->count_all_results('system_account_mappings');echo ($n?'[OK] ':'[FAIL] ').'mapping_'.$key."\n";if(!$n)$failed++;}if($failed)exit(1);
|
||||
}
|
||||
public function summary(){foreach(array('sales_documents','payment_allocations','customer_advances','credit_notes','customer_refunds','receivable_writeoffs','receivable_reminders','transaction_attachments')as$t)echo $t.'='.$this->db->count_all($t)."\n";foreach($this->db->select('workflow_status,status,COUNT(*) total')->group_by(array('workflow_status','status'))->get('invoices')->result()as$r)echo 'invoice_'.$r->workflow_status.'_'.$r->status.'='.$r->total."\n";foreach($this->db->select('code')->where_in('code',array('INVOICE_DEFAULT','CREDIT_NOTE_DEFAULT','WRITEOFF_DEFAULT'))->get('approval_workflows')->result()as$r)echo 'workflow='.$r->code."\n";}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
class Receivablereminders extends MY_Controller
|
||||
{
|
||||
public function queue($invoiceId){if(strtoupper($this->input->method())!=='POST')show_404();$i=$this->db->select('invoices.*,customers.email,customers.telp')->from('invoices')->join('customers','customers.id=invoices.customer_id')->where('invoices.id',(int)$invoiceId)->get()->row();if(!$i||$i->sisa_piutang<=0)return json_response(false,'Invoice tidak memiliki saldo.',array(),422);$channel=$this->input->post('channel');if(!in_array($channel,array('email','whatsapp','phone','manual'),true))return json_response(false,'Channel tidak valid.',array(),422);$recipient=$channel==='email'?$i->email:$i->telp;$message='Pengingat invoice '.$i->no_invoice.' saldo Rp '.number_format($i->sisa_piutang,0,',','.').' jatuh tempo '.$i->jatuh_tempo;$this->db->insert('receivable_reminders',array('invoice_id'=>$i->id,'channel'=>$channel,'recipient'=>$recipient,'message'=>$message,'status'=>'queued','scheduled_at'=>$this->input->post('scheduled_at')?:date('Y-m-d H:i:s'),'created_by'=>$this->session->userdata('user_id'),'created_at'=>date('Y-m-d H:i:s')));return json_response(true,'Reminder masuk antrean. Pengiriman provider eksternal belum diaktifkan.',array('id'=>$this->db->insert_id()));}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
|
||||
class Receivables extends MY_Controller
|
||||
{
|
||||
public function __construct(){parent::__construct();$this->load->library(array('ReceivableService','ReceivableAdjustmentsService','SalesService'));}
|
||||
private function uid(){return(int)$this->session->userdata('user_id');}
|
||||
private function roleId(){$id=(int)$this->session->userdata('role_id');if(!$id){$u=$this->db->select('role_id')->get_where('users',array('id'=>$this->uid()))->row();$id=$u?(int)$u->role_id:0;}return$id;}
|
||||
private function company(){return(int)$this->companycontext->id();}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$focusId=(int)$this->input->get('invoice_id');$focus=null;
|
||||
if($focusId)$focus=$this->db->select('i.id,i.customer_id,i.no_invoice,i.sisa_piutang,c.nama customer_name')->from('invoices i')->join('customers c','c.id=i.customer_id')->where(array('i.id'=>$focusId,'i.company_id'=>$this->company(),'i.workflow_status'=>'posted'))->where('i.sisa_piutang >',0)->get()->row();
|
||||
$data=array('active_menu'=>'receivables','aging'=>$this->receivableservice->aging($this->input->get('as_of')?:date('Y-m-d')),'focus_invoice'=>$focus,'can_post'=>is_master_admin_user()||check_permission('invoices','can_post'));
|
||||
$this->load->view('partials/header',$data);$this->load->view('receivables/index',$data);$this->load->view('partials/footer',$data);
|
||||
}
|
||||
|
||||
public function lookup_customers()
|
||||
{
|
||||
$q=trim((string)$this->input->get('q'));$page=max(1,(int)$this->input->get('page'));$limit=20;
|
||||
$this->db->select('id,nama text,nama')->from('customers')->where(array('company_id'=>$this->company()));if($q!=='')$this->db->like('nama',$q);$rows=$this->db->order_by('nama')->limit($limit+1,($page-1)*$limit)->get()->result();$more=count($rows)>$limit;if($more)array_pop($rows);
|
||||
return json_response(true,'Customer ditemukan.',array('results'=>$rows,'pagination'=>array('more'=>$more)));
|
||||
}
|
||||
|
||||
public function lookup_cash_accounts()
|
||||
{
|
||||
$q=trim((string)$this->input->get('q'));$page=max(1,(int)$this->input->get('page'));$limit=20;
|
||||
$this->db->select("a.id,CONCAT(a.kode_akun,' - ',a.nama_akun,' (',IF(a.tipe='asset','Aktiva','Passiva'),')') text",false)->from('accounts a')->where(array('a.company_id'=>$this->company(),'a.is_active'=>1,'a.allow_posting'=>1))->where_in('a.tipe',array('asset','liability'));if(!is_master_admin_user())$this->db->where('a.is_hidden',0);if($q!=='')$this->db->group_start()->like('a.kode_akun',$q)->or_like('a.nama_akun',$q)->group_end();$rows=$this->db->order_by('a.tipe')->order_by('a.kode_akun')->limit($limit+1,($page-1)*$limit)->get()->result();$more=count($rows)>$limit;if($more)array_pop($rows);
|
||||
return json_response(true,'Akun pelunasan Aktiva/Passiva ditemukan.',array('results'=>$rows,'pagination'=>array('more'=>$more)));
|
||||
}
|
||||
|
||||
public function open_invoices($customerId)
|
||||
{
|
||||
$rows=$this->db->select('id,no_invoice,tanggal,jatuh_tempo,total,total_bayar,sisa_piutang,invoice_type')->where(array('company_id'=>$this->company(),'customer_id'=>(int)$customerId,'workflow_status'=>'posted'))->where('sisa_piutang >',0)->order_by('jatuh_tempo')->order_by('id')->get('invoices')->result();
|
||||
return json_response(true,'Invoice terbuka ditemukan.',array('invoices'=>$rows));
|
||||
}
|
||||
|
||||
public function payment_history($customerId)
|
||||
{
|
||||
$rows=$this->db->select("p.id,p.payment_no,p.tanggal,p.jumlah,p.reference_no,p.created_at,u.nama creator_name,GROUP_CONCAT(CONCAT(i.no_invoice,' · Rp ',FORMAT(pa.amount,2,'id_ID')) ORDER BY i.id SEPARATOR '||') allocations",false)->from('payments p')->join('payment_allocations pa','pa.payment_id=p.id','left')->join('invoices i','i.id=pa.invoice_id','left')->join('users u','u.id=p.created_by','left')->where(array('p.company_id'=>$this->company(),'p.customer_id'=>(int)$customerId,'p.status'=>'posted'))->group_by('p.id')->order_by('p.id','DESC')->limit(50)->get()->result();
|
||||
return json_response(true,'Riwayat pembayaran ditemukan.',array('payments'=>$rows));
|
||||
}
|
||||
|
||||
public function submit_invoice($id){return$this->action(fn()=>$this->salesservice->submitInvoice($id,$this->uid()),'Invoice diajukan untuk approval.');}
|
||||
public function approve_invoice($id){return$this->action(fn()=>$this->salesservice->approveAndPostInvoice($id,$this->uid(),$this->roleId()),'Invoice disetujui dan otomatis diposting.');}
|
||||
public function post_invoice($id){return json_response(false,'Posting terpisah dinonaktifkan. Approval terakhir otomatis memposting invoice.',array('redirect'=>base_url('invoices/detail/'.(int)$id)),410);}
|
||||
|
||||
public function payment()
|
||||
{
|
||||
$saved=array();
|
||||
try{
|
||||
$saved=$this->paymentUploads();$sources=json_decode((string)$this->input->post('sources'),true);$allocations=json_decode((string)$this->input->post('allocations_json'),true);
|
||||
if(!is_array($sources))$sources=array(array('account_id'=>$this->input->post('cash_account_id'),'amount'=>$this->input->post('amount')));
|
||||
if(!is_array($allocations)){$allocations=array();foreach((array)$this->input->post('allocations')as$id=>$amount)$allocations[(int)$id]=$amount;}
|
||||
$result=$this->salesservice->recordPayment($this->input->post('customer_id'),$this->input->post('date'),$sources,$allocations,(bool)$this->input->post('allow_advance'),$this->uid(),$this->input->post('reference_no',true),$this->input->post('notes',true),$saved);
|
||||
return json_response(true,'Pembayaran berhasil diposting.',array('result'=>$result));
|
||||
}catch(Throwable$e){foreach($saved as$f)if(is_file(FCPATH.$f['stored_name']))@unlink(FCPATH.$f['stored_name']);return business_exception_response($e);}
|
||||
}
|
||||
|
||||
public function reverse_invoice($id){$this->requireAdmin();return$this->action(fn()=>$this->receivableservice->reverseInvoice($id,$this->input->post('date'),$this->input->post('reason',true),$this->uid()),'Invoice berhasil direversal.');}
|
||||
public function writeoff($id){$this->requireAdmin();return$this->action(fn()=>$this->receivableservice->postWriteoff($id,$this->input->post('date'),$this->input->post('amount'),$this->input->post('reason',true),$this->uid()),'Write-off berhasil diposting.');}
|
||||
public function credit_note($id){$this->requireAdmin();return$this->action(fn()=>$this->receivableadjustmentsservice->creditNote($id,$this->input->post('date'),$this->input->post('amount'),$this->input->post('reason',true),$this->uid()),'Credit note berhasil diposting.');}
|
||||
public function refund($advanceId){$this->requireAdmin();return$this->action(fn()=>$this->receivableadjustmentsservice->refund($advanceId,$this->input->post('date'),$this->input->post('amount'),$this->input->post('bank_account_id'),$this->input->post('reason',true),$this->uid()),'Refund berhasil diposting.');}
|
||||
public function statement($customerId){$from=$this->input->get('from')?:date('Y-01-01');$to=$this->input->get('to')?:date('Y-m-d');$data=$this->receivableservice->statement($customerId,$from,$to);$data+=array('active_menu'=>'receivables','from'=>$from,'to'=>$to);$this->load->view('partials/header',$data);$this->load->view('receivables/statement',$data);$this->load->view('partials/footer',$data);}
|
||||
public function receipt($paymentId){redirect('sales/receipt_pdf/'.(int)$paymentId);}
|
||||
|
||||
private function action($fn,$message){if(strtoupper($this->input->method())!=='POST')return json_response(false,'Metode tidak diizinkan.',array(),405);try{return json_response(true,$message,array('result'=>$fn()));}catch(Throwable$e){return business_exception_response($e);}}
|
||||
private function paymentUploads()
|
||||
{
|
||||
$f=$_FILES['attachments']??null;if(!$f||empty($f['name']))throw new BusinessException('Bukti pembayaran gambar atau PDF wajib diunggah.');$names=is_array($f['name'])?$f['name']:array($f['name']);if(count($names)>10)throw new BusinessException('Maksimal 10 file.');$dir=FCPATH.'uploads/accounting/sales/';if(!is_dir($dir)&&!mkdir($dir,0755,true))throw new BusinessException('Folder lampiran tidak tersedia.');$allowed=array('image/jpeg'=>'jpg','image/png'=>'png','image/webp'=>'webp','image/gif'=>'gif','application/pdf'=>'pdf');$fi=new finfo(FILEINFO_MIME_TYPE);$out=array();foreach($names as$i=>$name){$tmp=is_array($f['tmp_name'])?$f['tmp_name'][$i]:$f['tmp_name'];$size=is_array($f['size'])?$f['size'][$i]:$f['size'];$err=is_array($f['error'])?$f['error'][$i]:$f['error'];$mime=$err===UPLOAD_ERR_OK?$fi->file($tmp):'';if(!isset($allowed[$mime])||$size>5*1024*1024)throw new BusinessException('Bukti hanya menerima JPG, PNG, WEBP, GIF, atau PDF maksimal 5 MB.');$stored=bin2hex(random_bytes(16)).'.'.$allowed[$mime];if(!move_uploaded_file($tmp,$dir.$stored))throw new BusinessException('Bukti tidak dapat disimpan.');$out[]=array('original_name'=>basename($name),'stored_name'=>'uploads/accounting/sales/'.$stored,'mime_type'=>$mime,'file_size'=>$size,'sha256'=>hash_file('sha256',$dir.$stored));}return$out;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
class Reportworker extends CI_Controller
|
||||
{
|
||||
public function run(){if(!$this->input->is_cli_request())show_404();$this->load->database();$jobs=$this->db->where('status','queued')->order_by('id')->limit(10)->get('report_export_jobs')->result();foreach($jobs as$j){$this->db->where('id',$j->id)->update('report_export_jobs',array('status'=>'processing','started_at'=>date('Y-m-d H:i:s')));try{$f=json_decode($j->filter_json,true);$where="j.company_id=".(int)$j->company_id." AND j.status IN('posted','reversed') AND j.tanggal BETWEEN ".$this->db->escape($f['from'])." AND ".$this->db->escape($f['to']);$rows=$this->db->query("SELECT j.tanggal,j.no_ref,j.keterangan,a.kode_akun,a.nama_akun,d.debit,d.kredit FROM journal_details d JOIN journals j ON j.id=d.journal_id JOIN accounts a ON a.id=d.account_id WHERE $where ORDER BY j.tanggal,j.id,d.id")->result_array();$dir=FCPATH.'uploads/accounting/reports/';if(!is_dir($dir))mkdir($dir,0750,true);$file=$j->job_no.'.'.($j->format==='pdf'?'html':'csv');$h=fopen($dir.$file,'w');if($j->format==='pdf'){fwrite($h,'<html><body><h2>'.html_escape($j->report_code).'</h2><pre>'.html_escape(json_encode($rows,JSON_PRETTY_PRINT)).'</pre></body></html>');}elseif($rows){fputcsv($h,array_keys($rows[0]));foreach($rows as$r)fputcsv($h,$r);}fclose($h);$this->db->where('id',$j->id)->update('report_export_jobs',array('status'=>'completed','file_name'=>$file,'completed_at'=>date('Y-m-d H:i:s')));}catch(Throwable$e){$this->db->where('id',$j->id)->update('report_export_jobs',array('status'=>'failed','error_message'=>$e->getMessage(),'completed_at'=>date('Y-m-d H:i:s')));}}echo count($jobs)." job diproses.\n";}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
class Rolecheck extends CI_Controller
|
||||
{
|
||||
public function index(){if(!$this->input->is_cli_request())show_404();$view=$this->db->query('SHOW CREATE VIEW `v_users`')->row_array();echo(isset($view['Create View'])?$view['Create View']:'v_users tidak ditemukan').PHP_EOL;foreach($this->db->order_by('id')->get('roles')->result_array()as$r)echo json_encode($r,JSON_UNESCAPED_UNICODE).PHP_EOL;}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
class Roles extends MY_Admin_Controller
|
||||
{
|
||||
private $features; private $actions;
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->config->load('permissions', true);
|
||||
$cfg = $this->config->item('permissions');
|
||||
$this->features = $cfg['permission_features']; $this->actions = $cfg['permission_actions'];
|
||||
}
|
||||
public function index()
|
||||
{
|
||||
$data = array('active_menu'=>'roles','roles'=>$this->db->order_by('is_super_admin','DESC')->order_by('is_system','DESC')->order_by('nama_role')->get('roles')->result(),'features'=>$this->features,'actions'=>$this->actions);
|
||||
$this->load->view('partials/header',$data);$this->load->view('roles/index',$data);$this->load->view('partials/footer');
|
||||
}
|
||||
public function detail($id)
|
||||
{
|
||||
$role=$this->db->get_where('roles',array('id'=>(int)$id))->row();if(!$role)return json_response(false,'Role tidak ditemukan.',array(),404);
|
||||
$role->permissions=json_decode($role->permissions,true)?:array();return json_response(true,'Role ditemukan.',array('result'=>(array)$role));
|
||||
}
|
||||
public function save()
|
||||
{
|
||||
$id=(int)$this->input->post('id');$name=trim((string)$this->input->post('name',true));$description=trim((string)$this->input->post('description',true));
|
||||
if($name===''||$description==='')return json_response(false,'Nama dan penjelasan tanggung jawab role wajib diisi.',array(),422);
|
||||
$existing=$id?$this->db->get_where('roles',array('id'=>$id))->row():null;
|
||||
if($id&&!$existing)return json_response(false,'Role tidak ditemukan.',array(),404);
|
||||
if($existing&&!empty($existing->is_super_admin))return json_response(false,'Permission Master Admin bersifat penuh dan tidak dapat dibatasi.',array(),422);
|
||||
$duplicate=$this->db->where('nama_role',$name)->where('id !=',$id)->count_all_results('roles');if($duplicate)return json_response(false,'Nama role sudah digunakan.',array(),422);
|
||||
$posted=(array)$this->input->post('permissions');$permissions=array();
|
||||
foreach($this->features as$key=>$label){$selected=array_values(array_intersect((array)(isset($posted[$key])?$posted[$key]:array()),array_keys($this->actions)));if($selected){if(!in_array('can_view',$selected,true))array_unshift($selected,'can_view');$permissions[$key]=$selected;}}
|
||||
if(!$permissions)return json_response(false,'Role wajib mempunyai minimal satu permission.',array(),422);
|
||||
$data=array('nama_role'=>$name,'description'=>$description,'permissions'=>json_encode($permissions),'is_active'=>$this->input->post('is_active')?1:0,'updated_at'=>date('Y-m-d H:i:s'));
|
||||
if($id)$this->db->where('id',$id)->update('roles',$data);else{$data['is_system']=0;$data['is_super_admin']=0;$data['created_at']=date('Y-m-d H:i:s');$this->db->insert('roles',$data);$id=$this->db->insert_id();}
|
||||
log_activity('roles',$existing?'update':'create',($existing?'Mengubah':'Membuat').' role '.$name,'success');return json_response(true,'Role dan permission berhasil disimpan.',array('id'=>$id));
|
||||
}
|
||||
public function delete($id)
|
||||
{
|
||||
$role=$this->db->get_where('roles',array('id'=>(int)$id))->row();if(!$role)return json_response(false,'Role tidak ditemukan.',array(),404);
|
||||
if($role->is_system||$role->is_super_admin)return json_response(false,'Role bawaan aplikasi tidak dapat dihapus.',array(),422);
|
||||
if($this->db->where('role_id',$role->id)->count_all_results('users'))return json_response(false,'Role masih digunakan oleh pengguna.',array(),422);
|
||||
$this->db->where('id',$role->id)->delete('roles');return json_response(true,'Role custom berhasil dihapus.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
|
||||
class Sales extends MY_Controller
|
||||
{
|
||||
public function __construct(){parent::__construct();$this->load->library('SalesService');}
|
||||
private function uid(){return(int)$this->session->userdata('user_id');}
|
||||
private function roleId(){return(int)$this->session->userdata('role_id');}
|
||||
private function allowed($action){return is_master_admin_user()||check_permission('invoices',$action);}
|
||||
public function index()
|
||||
{
|
||||
$sourceOrder=(int)$this->input->get('source_order');if($sourceOrder)redirect('invoices/create?source_order='.$sourceOrder);
|
||||
$this->renderWorkspace('summary');
|
||||
}
|
||||
public function deliveries(){return $this->renderWorkspace('delivery');}
|
||||
public function returns(){return $this->renderWorkspace('return');}
|
||||
private function renderWorkspace($section)
|
||||
{
|
||||
$active=$section==='delivery'?'sales_delivery':($section==='return'?'sales_return':'sales');
|
||||
$data=array('active_menu'=>$active,'section'=>$section,'summary'=>$this->salesservice->dashboard(),'deliveries'=>$this->salesservice->deliveries(),'returns'=>$this->salesservice->returns(),'accounts'=>array(),'current_user'=>$this->uid(),'source_order'=>0,'focus'=>(int)$this->input->get('focus'),'permissions'=>array('create'=>$this->allowed('can_create'),'update'=>$this->allowed('can_update'),'approve'=>$this->allowed('can_approve'),'post'=>$this->allowed('can_post'),'delete'=>$this->allowed('can_delete'),'export'=>$this->allowed('can_export')));
|
||||
$this->load->view('partials/header',$data);$this->load->view('sales/index',$data);$this->load->view('partials/footer',$data);
|
||||
}
|
||||
public function lookup_customers(){return$this->lookup('customers','nama',array('id','nama','alamat'));}
|
||||
public function lookup_items()
|
||||
{
|
||||
$q=trim((string)$this->input->get('q'));$company=(int)$this->companycontext->id();$this->db->select('i.id,i.kode_detail AS kode_barang,i.nama_barang,i.harga_jual,i.harga_beli,i.kode_id,k.tracking_type,k.unit')->from('items i')->join('kode_barang k','k.id=i.kode_id','left')->where(array('i.company_id'=>$company,'i.status'=>'active'))->group_start()->like('i.kode_detail',$q)->or_like('i.nama_barang',$q)->group_end()->order_by('i.nama_barang')->limit(30);return json_response(true,'Barang ditemukan.',array('rows'=>$this->db->get()->result()));
|
||||
}
|
||||
public function lookup_barcodes()
|
||||
{
|
||||
$item=(int)$this->input->get('item_id');$warehouse=(int)$this->input->get('warehouse_id');$company=(int)$this->companycontext->id();$rows=$this->db->select('b.id,b.barcode,b.serial_number,b.qty_sisa,b.reserved_qty,b.return_reserved_qty')->from('item_barcodes b')->join('items i','i.id=b.item_id')->where(array('i.company_id'=>$company,'b.item_id'=>$item,'b.warehouse_id'=>$warehouse,'b.status'=>'available'))->where('(b.qty_sisa-b.reserved_qty-COALESCE(b.return_reserved_qty,0)) >',0,false)->order_by('b.id')->limit(100)->get()->result();return json_response(true,'Barcode tersedia.',array('rows'=>$rows));
|
||||
}
|
||||
private function lookup($table,$label,array$fields){$q=trim((string)$this->input->get('q'));$this->db->select(implode(',',$fields))->where('company_id',(int)$this->companycontext->id())->like($label,$q)->order_by($label)->limit(30);if($this->db->field_exists('is_active',$table))$this->db->where('is_active',1);return json_response(true,'Data ditemukan.',array('rows'=>$this->db->get($table)->result()));}
|
||||
public function warehouses(){return json_response(true,'Gudang ditemukan.',array('rows'=>$this->db->select('id,nama')->where('company_id',(int)$this->companycontext->id())->order_by('nama')->get('warehouses')->result()));}
|
||||
public function create_invoice(){return$this->action(function(){return$this->salesservice->createInvoice($this->input->post(),$this->decodeRows(),$this->uid());},'Invoice berhasil disimpan sebagai Draft/Berjalan.');}
|
||||
public function append_invoice($id){return$this->action(function()use($id){return$this->salesservice->appendInvoiceLines($id,$this->decodeRows(),$this->uid());},'Item berhasil ditambahkan ke invoice berjalan.');}
|
||||
public function submit_invoice($id){return$this->action(function()use($id){return$this->salesservice->submitInvoice($id,$this->uid());},'Invoice diajukan untuk approval.');}
|
||||
public function approve_invoice($id){return$this->action(function()use($id){return$this->salesservice->approveAndPostInvoice($id,$this->uid(),$this->roleId());},'Invoice disetujui dan otomatis diposting.');}
|
||||
public function reject_invoice($id){return$this->action(function()use($id){return$this->salesservice->rejectInvoice($id,$this->input->post('reason',true),$this->uid(),$this->roleId());},'Invoice ditolak dan dikembalikan ke Draft.');}
|
||||
public function delete_invoice($id){return$this->action(function()use($id){return$this->salesservice->deleteDraftInvoice($id,$this->uid(),is_master_admin_user());},'Draft invoice dibatalkan dan reservasi barcode dilepas.');}
|
||||
public function post_delivery($id){return$this->action(function()use($id){return$this->salesservice->postDelivery($id,$this->uid());},'Surat Jalan diposting dan stok telah diperbarui.');}
|
||||
public function update_delivery($id){$saved=array();try{$saved=$this->uploads('attachments',false);$result=$this->salesservice->updateDelivery($id,$this->input->post(),$saved,$this->uid());return json_response(true,'Data dan bukti pengiriman berhasil disimpan.',array('result'=>$result));}catch(Throwable$e){$this->cleanup($saved);return business_exception_response($e);}}
|
||||
public function invoice_detail($id){return$this->jsonDetail(function()use($id){$this->guardInvoice($this->salesservice->invoiceForEditor($id));return$this->salesservice->invoiceDetail($id);});}
|
||||
public function delivery_detail($id){return$this->jsonDetail(function()use($id){return$this->salesservice->deliveryDetail($id);});}
|
||||
public function return_detail($id){return$this->jsonDetail(function()use($id){return$this->salesservice->returnDetail($id);});}
|
||||
public function payment()
|
||||
{
|
||||
return json_response(false,'Alur pembayaran lama dinonaktifkan. Gunakan menu Piutang & Pembayaran.',array('redirect'=>base_url('receivables')),410);
|
||||
}
|
||||
public function create_return(){$saved=array();try{if(strtoupper($this->input->method())!=='POST')return json_response(false,'Metode tidak diizinkan.',array(),405);$saved=$this->uploads('attachments',true);$id=$this->salesservice->createReturn($this->input->post(),$this->decodeRows(),$this->uid(),$saved);return json_response(true,'Pengajuan retur berhasil disimpan.',array('result'=>$id));}catch(Throwable$e){$this->cleanup($saved);return business_exception_response($e);}}
|
||||
public function submit_return($id){return$this->action(function()use($id){return$this->salesservice->submitReturn($id,$this->uid());},'Retur diajukan.');}
|
||||
public function approve_return($id){return$this->action(function()use($id){return$this->salesservice->approveReturn($id,$this->uid());},'Retur disetujui.');}
|
||||
public function reject_return($id){return$this->action(function()use($id){return$this->salesservice->rejectReturn($id,$this->input->post('reason',true),$this->uid());},'Retur ditolak.');}
|
||||
public function receive_return($id){return$this->action(function()use($id){return$this->salesservice->receiveReturn($id,$this->uid());},'Barang retur diterima dan diperiksa.');}
|
||||
public function post_return($id){return$this->action(function()use($id){return$this->salesservice->postReturn($id,$this->uid());},'Retur, stok, dan credit note berhasil diposting.');}
|
||||
public function invoice_pdf($id){$this->guardInvoice($this->salesservice->invoiceForEditor($id));$data=$this->salesservice->invoiceDetail($id);$this->pdf('sales/invoice_pdf',$data,'Invoice-'.$data['invoice']->no_invoice.'.pdf');}
|
||||
public function delivery_pdf($id){$data=$this->salesservice->deliveryDetail($id);$this->pdf('sales/delivery_pdf',$data,'Surat-Jalan-'.$data['delivery']->delivery_no.'.pdf');}
|
||||
public function return_pdf($id){$data=$this->salesservice->returnDetail($id);$this->pdf('sales/return_pdf',$data,'Retur-'.$data['return']->return_no.'.pdf');}
|
||||
public function receipt_pdf($id)
|
||||
{
|
||||
$company=(int)$this->companycontext->id();$data['payment']=$this->db->select('p.*,c.nama customer_name,c.alamat')->from('payments p')->join('customers c','c.id=p.customer_id')->where(array('p.id'=>(int)$id,'p.company_id'=>$company))->get()->row();if(!$data['payment'])show_404();$data['allocations']=$this->db->select('a.*,i.no_invoice')->from('payment_allocations a')->join('invoices i','i.id=a.invoice_id')->where(array('a.payment_id'=>(int)$id,'i.company_id'=>$company))->get()->result();$data['sources']=$this->db->select('s.*,a.kode_akun,a.nama_akun')->from('payment_sources s')->join('accounts a','a.id=s.account_id')->where(array('s.payment_id'=>(int)$id,'s.company_id'=>$company))->get()->result();$this->pdf('sales/receipt_pdf',$data,'Bukti-Penerimaan-'.$data['payment']->payment_no.'.pdf');
|
||||
}
|
||||
private function decodeRows(){$rows=json_decode((string)$this->input->post('rows'),true);if(!is_array($rows))throw new BusinessException('Detail barang tidak valid.');return$rows;}
|
||||
private function action($fn,$message){if(strtoupper($this->input->method())!=='POST')return json_response(false,'Metode tidak diizinkan.',array(),405);try{return json_response(true,$message,array('result'=>$fn()));}catch(Throwable$e){return business_exception_response($e);}}
|
||||
private function jsonDetail($fn){try{return json_response(true,'Detail ditemukan.',$fn());}catch(Throwable$e){return business_exception_response($e);}}
|
||||
private function pdf($view,$data,$name){$data['pdf_company']=$this->db->get_where('companies',array('id'=>(int)$this->companycontext->id()))->row();require_once FCPATH.'vendor/autoload.php';$html=$this->load->view($view,$data,true);$pdf=new Dompdf\Dompdf();$pdf->loadHtml($html);$pdf->setPaper('A4','portrait');$pdf->render();$pdf->stream($name,array('Attachment'=>false));}
|
||||
private function uploads($field,$required=false)
|
||||
{
|
||||
if(empty($_FILES[$field])||empty($_FILES[$field]['name'])){if($required)throw new BusinessException('Bukti pembayaran gambar atau PDF wajib diunggah.');return array();}$files=$_FILES[$field];$names=is_array($files['name'])?$files['name']:array($files['name']);$saved=array();if(count($names)>10)throw new BusinessException('Maksimal 10 file.');$dir=FCPATH.'uploads/accounting/sales/';if(!is_dir($dir)&&!mkdir($dir,0755,true))throw new BusinessException('Folder lampiran tidak dapat dibuat.');$finfo=new finfo(FILEINFO_MIME_TYPE);$allowed=array('image/jpeg'=>'jpg','image/png'=>'png','image/webp'=>'webp','image/gif'=>'gif','application/pdf'=>'pdf');
|
||||
foreach($names as$i=>$name){$tmp=is_array($files['tmp_name'])?$files['tmp_name'][$i]:$files['tmp_name'];$size=is_array($files['size'])?$files['size'][$i]:$files['size'];$error=is_array($files['error'])?$files['error'][$i]:$files['error'];if($error!==UPLOAD_ERR_OK)throw new BusinessException('Salah satu bukti gagal diunggah.');$mime=$finfo->file($tmp);if(!isset($allowed[$mime]))throw new BusinessException('Lampiran hanya menerima JPG, PNG, WEBP, GIF, atau PDF.');if($size>5*1024*1024)throw new BusinessException('Ukuran setiap lampiran maksimal 5 MB.');$stored=bin2hex(random_bytes(16)).'.'.$allowed[$mime];if(!move_uploaded_file($tmp,$dir.$stored))throw new BusinessException('Lampiran tidak dapat disimpan.');$saved[]=array('original_name'=>basename($name),'stored_name'=>'uploads/accounting/sales/'.$stored,'mime_type'=>$mime,'file_size'=>$size,'sha256'=>hash_file('sha256',$dir.$stored));}return$saved;
|
||||
}
|
||||
private function cleanup(array$files){foreach($files as$f){$path=FCPATH.$f['stored_name'];if(is_file($path))@unlink($path);}}
|
||||
private function guardInvoice($invoice){if($invoice->workflow_status==='draft'&&!is_master_admin_user()&&(int)$invoice->created_by!==$this->uid())throw new BusinessException('Draft invoice hanya dapat dilihat oleh pembuatnya.');}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
class Salescheck extends CI_Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
if(!is_cli())show_404();$this->load->database();$errors=array();$checks=array();
|
||||
$required=array('invoices'=>array('company_id','invoice_type','recognition_policy','delivery_status','recognized_revenue','idempotency_key','version'),'invoice_details'=>array('company_id','service_date','delivery_line_id','net_amount','line_type','counter_account_id','idempotency_key'),'sales_deliveries'=>array('company_id','delivery_no','status'),'sales_delivery_barcodes'=>array('barcode_id','qty','returned_qty'),'invoice_line_barcodes'=>array('invoice_id','barcode_id'),'payment_sources'=>array('payment_id','account_id','amount'),'sales_returns'=>array('company_id','return_no','status'),'revenue_recognition_ledger'=>array('idempotency_key'));
|
||||
foreach($required as$table=>$columns){if(!$this->db->table_exists($table)){$errors[]='Tabel '.$table.' belum ada.';continue;}$fields=$this->db->list_fields($table);foreach($columns as$c)if(!in_array($c,$fields,true))$errors[]='Kolom '.$table.'.'.$c.' belum ada.';}$checks['schema']=!$errors;
|
||||
if($this->db->table_exists('sales_delivery_lines')){$r=$this->db->query("SELECT COUNT(*) total FROM sales_delivery_lines l JOIN sales_deliveries d ON d.id=l.sales_delivery_id WHERE d.status='posted' AND l.stock_posted_at IS NULL")->row();$checks['posted_delivery_has_stock']=(int)$r->total===0;if(!$checks['posted_delivery_has_stock'])$errors[]=$r->total.' baris Surat Jalan posted belum mempunyai posting stok.';}
|
||||
if($this->db->table_exists('inventory_ledger')){$r=$this->db->query("SELECT COUNT(*) total FROM(SELECT idempotency_key FROM inventory_ledger WHERE document_type IN('sales_delivery','sales_return') GROUP BY idempotency_key HAVING COUNT(*)>1)x")->row();$checks['stock_idempotency']=(int)$r->total===0;if(!$checks['stock_idempotency'])$errors[]='Terdapat idempotency key stok penjualan ganda.';}
|
||||
if($this->db->table_exists('invoices')){$r=$this->db->query("SELECT COUNT(*) total FROM invoices WHERE company_id IS NULL OR recognized_revenue>subtotal_before_tax+0.01 OR recognized_cogs<0")->row();$checks['invoice_tenant_recognition']=(int)$r->total===0;if(!$checks['invoice_tenant_recognition'])$errors[]=$r->total.' invoice tidak lolos validasi tenant/pengakuan.';}
|
||||
if($this->db->table_exists('journals')&&$this->db->table_exists('journal_details')){$r=$this->db->query("SELECT COUNT(*) total FROM(SELECT j.id FROM journals j JOIN journal_details d ON d.journal_id=j.id WHERE j.ref_type IN('sales_delivery','invoice','customer_payment','revenue_recognition','sales_return') GROUP BY j.id HAVING ABS(SUM(d.debit)-SUM(d.kredit))>0.01)x")->row();$checks['sales_journals_balanced']=(int)$r->total===0;if(!$checks['sales_journals_balanced'])$errors[]='Terdapat jurnal penjualan tidak balance.';}
|
||||
if($this->db->table_exists('invoice_line_barcodes')){$r=$this->db->query("SELECT COUNT(*) total FROM(SELECT invoice_id,barcode_id FROM invoice_line_barcodes GROUP BY invoice_id,barcode_id HAVING COUNT(*)>1)x")->row();$checks['barcode_once_per_invoice']=(int)$r->total===0;if(!$checks['barcode_once_per_invoice'])$errors[]='Terdapat barcode yang dialokasikan lebih dari sekali pada invoice yang sama.';}
|
||||
if($this->db->table_exists('invoice_details')){$r=$this->db->query("SELECT COUNT(*) total FROM invoice_details WHERE line_type IN('service','loan','savings') AND delivery_line_id IS NOT NULL")->row();$checks['noninventory_without_delivery']=(int)$r->total===0;if(!$checks['noninventory_without_delivery'])$errors[]='Terdapat item nonbarang yang terhubung ke Surat Jalan.';}
|
||||
if($this->db->table_exists('invoice_details')){$nullable=array();foreach($this->db->query("SHOW COLUMNS FROM invoice_details WHERE Field IN ('items_id','warehouse_id')")->result()as$c)$nullable[$c->Field]=$c->Null;$checks['service_columns_nullable']=($nullable['items_id']??'NO')==='YES'&&($nullable['warehouse_id']??'NO')==='YES';if(!$checks['service_columns_nullable'])$errors[]='Kolom barang/gudang invoice belum nullable sehingga baris jasa tidak dapat disimpan.';}
|
||||
if($this->db->table_exists('payments')){$this->db->select("p.id,p.payment_no,p.tanggal,p.jumlah,p.reference_no,p.created_at,u.nama creator_name,GROUP_CONCAT(CONCAT(i.no_invoice,' · Rp ',FORMAT(pa.amount,2,'id_ID')) ORDER BY i.id SEPARATOR '||') allocations",false)->from('payments p')->join('payment_allocations pa','pa.payment_id=p.id','left')->join('invoices i','i.id=pa.invoice_id','left')->join('users u','u.id=p.created_by','left')->group_by('p.id')->limit(1)->get()->result();$checks['payment_history_query']=true;}
|
||||
if($this->db->table_exists('accounts')){$revenue=(int)$this->db->where(array('tipe'=>'revenue','is_active'=>1,'allow_posting'=>1))->count_all_results('accounts');$checks['revenue_account_available']=$revenue>0;if(!$checks['revenue_account_available'])$errors[]='Belum ada akun pendapatan aktif yang dapat diposting.';}
|
||||
if($this->db->table_exists('cash_accounts')){$cash=(int)$this->db->where('is_active',1)->count_all_results('cash_accounts');$checks['cash_account_available']=$cash>0;if(!$checks['cash_account_available'])$errors[]='Belum ada rekening Kas/Bank aktif untuk penerimaan customer.';}
|
||||
$checks['pdf_views']=is_file(APPPATH.'views/sales/invoice_pdf.php')&&is_file(APPPATH.'views/sales/delivery_pdf.php')&&is_file(APPPATH.'views/sales/return_pdf.php')&&is_file(APPPATH.'views/sales/receipt_pdf.php');if(!$checks['pdf_views'])$errors[]='Salah satu template PDF belum tersedia.';
|
||||
echo json_encode(array('status'=>!$errors,'checks'=>$checks,'errors'=>$errors,'cleanup'=>'Tidak membuat data uji; tidak ada fixture yang tertinggal.'),JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES).PHP_EOL;if($errors)exit(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
class Salesdocuments extends MY_Controller
|
||||
{
|
||||
public function __construct(){parent::__construct();$this->load->library(array('NumberingService','AuditService','TransactionService'));}
|
||||
private function company(){return(int)$this->companycontext->id();}private function uid(){return(int)$this->session->userdata('user_id');}
|
||||
public function index(){$data=array('active_menu'=>'sales_documents','rows'=>$this->db->select('s.*,c.nama')->from('sales_documents s')->join('customers c','c.id=s.customer_id')->where('s.company_id',$this->company())->order_by('s.document_date','DESC')->limit(200)->get()->result(),'permissions'=>array('create'=>is_master_admin_user()||check_permission('invoices','can_create'),'update'=>is_master_admin_user()||check_permission('invoices','can_update'),'approve'=>is_master_admin_user()||check_permission('invoices','can_approve'),'export'=>is_master_admin_user()||check_permission('invoices','can_export')));$this->load->view('partials/header',$data);$this->load->view('sales_documents/index',$data);$this->load->view('partials/footer',$data);}
|
||||
public function detail($id){try{$d=$this->db->select('s.*,c.nama customer_name,c.alamat')->from('sales_documents s')->join('customers c','c.id=s.customer_id')->where(array('s.id'=>(int)$id,'s.company_id'=>$this->company()))->get()->row();if(!$d)throw new BusinessException('Dokumen penjualan tidak ditemukan.');$lines=$this->db->select('l.*,i.kode_detail AS kode_barang,i.nama_barang')->from('sales_document_lines l')->join('items i','i.id=l.item_id','left')->where(array('l.sales_document_id'=>$d->id,'l.company_id'=>$this->company()))->order_by('l.id')->get()->result();return json_response(true,'Detail ditemukan.',array('document'=>$d,'lines'=>$lines));}catch(Throwable$e){return business_exception_response($e);}}
|
||||
public function save(){return$this->action(function(){$type=$this->input->post('document_type');if(!in_array($type,array('quotation','sales_order'),true))throw new BusinessException('Jenis dokumen tidak valid.');$customer=$this->db->get_where('customers',array('id'=>(int)$this->input->post('customer_id'),'company_id'=>$this->company()))->row();if(!$customer)throw new BusinessException('Customer tidak valid.');$rows=json_decode((string)$this->input->post('rows'),true);if(!is_array($rows)||!$rows)throw new BusinessException('Tambahkan minimal satu barang.');$date=$this->input->post('document_date');$subtotal=0;$taxTotal=0;$clean=array();foreach($rows as$n=>$row){$item=$this->db->get_where('items',array('id'=>(int)($row['item_id']??0),'company_id'=>$this->company(),'status'=>'active'))->row();$qty=round((float)str_replace(',','.',(string)($row['qty']??0)),4);$price=$this->money($row['unit_price']??0);$discount=max(0,$this->money($row['discount_amount']??0));$tax=max(0,(float)str_replace(',','.',(string)($row['tax_rate']??0)));if(!$item||$qty<=0||$discount>$qty*$price)throw new BusinessException('Detail barang baris '.($n+1).' tidak valid.');$base=round($qty*$price-$discount,2);$taxAmount=round($base*$tax/100,2);$subtotal+=$base;$taxTotal+=$taxAmount;$clean[]=array('item'=>$item,'qty'=>$qty,'price'=>$price,'discount'=>$discount,'tax'=>$tax,'tax_amount'=>$taxAmount,'total'=>$base+$taxAmount,'specification'=>trim((string)($row['specification']??'')));}$no=$this->numberingservice->next($type,$date);$this->db->insert('sales_documents',array('company_id'=>$this->company(),'document_type'=>$type,'document_no'=>$no,'customer_id'=>$customer->id,'document_date'=>$date,'valid_until'=>$this->input->post('valid_until')?:null,'status'=>'draft','subtotal'=>$subtotal,'tax_total'=>$taxTotal,'total'=>$subtotal+$taxTotal,'notes'=>$this->input->post('notes',true),'created_by'=>$this->uid(),'created_at'=>date('Y-m-d H:i:s')));$id=(int)$this->db->insert_id();foreach($clean as$x)$this->db->insert('sales_document_lines',array('company_id'=>$this->company(),'sales_document_id'=>$id,'item_id'=>$x['item']->id,'account_id'=>null,'description'=>$x['item']->nama_barang,'specification'=>$x['specification'],'unit'=>null,'qty'=>$x['qty'],'unit_price'=>$x['price'],'discount_amount'=>$x['discount'],'tax_rate'=>$x['tax'],'tax_amount'=>$x['tax_amount'],'line_total'=>$x['total']));$this->auditservice->record('sales','sales_document',$id,'create',null,array('document_no'=>$no,'line_count'=>count($clean),'total'=>$subtotal+$taxTotal),$this->uid());return$id;},'Dokumen penjualan berhasil disimpan.');}
|
||||
public function workflow($id,$state){return$this->action(function()use($id,$state){$d=$this->db->query('SELECT * FROM sales_documents WHERE id=? AND company_id=? FOR UPDATE',array((int)$id,$this->company()))->row();if(!$d)throw new BusinessException('Dokumen tidak ditemukan.');$allowed=array('draft'=>array('sent','cancelled'),'sent'=>array('accepted','rejected'),'accepted'=>array('cancelled'));if(!in_array($state,$allowed[$d->status]??array(),true))throw new BusinessException('Perubahan status dokumen tidak diizinkan.');$this->db->where('id',$d->id)->update('sales_documents',array('status'=>$state,'updated_by'=>$this->uid(),'updated_at'=>date('Y-m-d H:i:s')));$this->auditservice->record('sales','sales_document',$d->id,'workflow',$d,array('status'=>$state),$this->uid());return$d->id;},'Status dokumen berhasil diperbarui.');}
|
||||
public function convert($id){return$this->action(function()use($id){$d=$this->db->query('SELECT * FROM sales_documents WHERE id=? AND company_id=? FOR UPDATE',array((int)$id,$this->company()))->row();if(!$d||$d->document_type!=='quotation'||$d->status!=='accepted')throw new BusinessException('Hanya Penawaran Diterima yang dapat dijadikan Sales Order.');$existing=$this->db->get_where('sales_documents',array('source_quotation_id'=>$d->id,'company_id'=>$this->company()))->row();if($existing)return$existing->id;$no=$this->numberingservice->next('sales_order',date('Y-m-d'));$copy=(array)$d;unset($copy['id']);$copy['document_type']='sales_order';$copy['document_no']=$no;$copy['document_date']=date('Y-m-d');$copy['status']='draft';$copy['source_quotation_id']=$d->id;$copy['parent_document_id']=$d->id;$copy['created_by']=$this->uid();$copy['created_at']=date('Y-m-d H:i:s');$copy['updated_at']=null;$copy['updated_by']=null;$this->db->insert('sales_documents',$copy);$new=(int)$this->db->insert_id();foreach($this->db->get_where('sales_document_lines',array('sales_document_id'=>$d->id,'company_id'=>$this->company()))->result_array()as$l){unset($l['id']);$l['sales_document_id']=$new;$this->db->insert('sales_document_lines',$l);}$this->db->where('id',$d->id)->update('sales_documents',array('status'=>'converted','updated_at'=>date('Y-m-d H:i:s'),'updated_by'=>$this->uid()));return$new;},'Penawaran berhasil dikonversi menjadi Sales Order.');}
|
||||
private function money($v){$v=trim(str_ireplace(array('Rp',' '),'',(string)$v));if(strpos($v,',')!==false)$v=str_replace(',','.',str_replace('.','',$v));elseif(preg_match('/^\d{1,3}(\.\d{3})+$/',$v))$v=str_replace('.','',$v);return round((float)$v,2);}
|
||||
private function action($fn,$message){if(strtoupper($this->input->method())!=='POST')return json_response(false,'Metode tidak diizinkan.',array(),405);try{return json_response(true,$message,array('result'=>$this->transactionservice->run($fn)));}catch(Throwable$e){return business_exception_response($e);}}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
class Scalabilitycheck extends CI_Controller
|
||||
{
|
||||
public function index($iterations=10){if(!$this->input->is_cli_request())show_404();$iterations=min(100,max(1,(int)$iterations));$company=$this->db->order_by('id')->get('companies')->row();$queries=array('dashboard_summary'=>array("SELECT SUM(d.debit),SUM(d.kredit) FROM journal_details d JOIN journals j ON j.id=d.journal_id WHERE j.company_id=? AND j.status IN('posted','reversed')",array($company->id)),'open_receivables'=>array("SELECT i.id FROM invoices i WHERE i.company_id=? AND i.workflow_status='posted' AND i.sisa_piutang>0 ORDER BY i.jatuh_tempo LIMIT 50",array($company->id)),'general_ledger'=>array("SELECT d.id FROM journal_details d JOIN journals j ON j.id=d.journal_id WHERE j.company_id=? AND j.status IN('posted','reversed') ORDER BY j.tanggal DESC,j.id DESC LIMIT 100",array($company->id)));$timings=array();foreach($queries as$name=>$q){$samples=array();for($i=0;$i<$iterations;$i++){$s=microtime(true);$this->db->query($q[0],$q[1])->result();$samples[]=(microtime(true)-$s)*1000;}sort($samples);$timings[$name]=array('avg_ms'=>round(array_sum($samples)/count($samples),3),'p95_ms'=>round($samples[(int)floor((count($samples)-1)*.95)],3),'target_ms'=>2000,'pass'=>max($samples)<2000);}$volumes=array();foreach(array('journals','journal_details','invoices','supplier_invoices','inventory_ledger','activity_logs','app_jobs','app_dead_letters')as$t)$volumes[$t]=$this->db->table_exists($t)?(int)$this->db->count_all($t):null;$result=array('iterations'=>$iterations,'volumes'=>$volumes,'queries'=>$timings,'all_pass'=>!in_array(false,array_column($timings,'pass'),true));echo json_encode($result,JSON_PRETTY_PRINT).PHP_EOL;if(!$result['all_pass'])exit(1);}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
|
||||
class Scheduler extends CI_Controller {
|
||||
class Scheduler extends MY_Controller {
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
@@ -641,4 +641,4 @@ class Scheduler extends CI_Controller {
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
|
||||
class Setting extends CI_Controller {
|
||||
class Setting extends MY_Controller {
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
|
||||
class Shifts extends CI_Controller {
|
||||
class Shifts extends MY_Controller {
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
@@ -144,4 +144,4 @@ class Shifts extends CI_Controller {
|
||||
'message'=> 'Shift berhasil dihapus'
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
class Stage1011check extends CI_Controller
|
||||
{
|
||||
public function index(){if(!$this->input->is_cli_request())show_404();$this->load->database();$checks=array('migration'=>(int)$this->db->get('migrations')->row()->version,'employees'=>(int)$this->db->count_all('k_employees'),'employment_history'=>(int)$this->db->count_all('employee_employment_history'),'history_missing'=>(int)$this->db->query('SELECT COUNT(*) n FROM k_employees e LEFT JOIN employee_employment_history h ON h.employee_id=e.id WHERE h.id IS NULL')->row()->n,'duplicate_raw_attendance'=>(int)$this->db->query('SELECT COUNT(*) n FROM(SELECT device_id,payload_hash FROM attendance_raw_logs GROUP BY device_id,payload_hash HAVING COUNT(*)>1)x')->row()->n,'locked_attendance'=>(int)$this->db->where('status','locked')->count_all_results('attendance_period_locks'),'payroll_components'=>(int)$this->db->count_all('payroll_components'),'final_payroll_without_hash'=>(int)$this->db->query("SELECT COUNT(*) n FROM k_payroll_periods WHERE workflow_status IN('final','posted','paid') AND calculation_hash IS NULL")->row()->n,'posted_payroll_without_journal'=>(int)$this->db->query("SELECT COUNT(*) n FROM k_payroll_periods WHERE workflow_status IN('posted','paid') AND journal_id IS NULL")->row()->n,'payroll_register_mismatch'=>(int)$this->db->query("SELECT COUNT(*) n FROM k_payrolls WHERE ABS(total_salary-(gross_salary-total_deductions))>.01")->row()->n,'tax_codes'=>(int)$this->db->count_all('tax_codes'),'tax_transactions'=>(int)$this->db->count_all('tax_transactions'),'duplicate_tax_keys'=>(int)$this->db->query('SELECT COUNT(*) n FROM(SELECT idempotency_key FROM tax_transactions GROUP BY idempotency_key HAVING COUNT(*)>1)x')->row()->n,'tax_without_source'=>(int)$this->db->query("SELECT COUNT(*) n FROM tax_transactions WHERE source_id IS NULL OR source_type='' ")->row()->n);echo json_encode($checks,JSON_PRETTY_PRINT).PHP_EOL;}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
class Stage1214check extends CI_Controller
|
||||
{
|
||||
public function index(){if(!$this->input->is_cli_request())show_404();$this->load->database();$critical=array('accounts','journals','journal_details','customers','invoices','suppliers','supplier_invoices','items','assets','cash_transactions','k_employees','k_payrolls','tax_transactions');$null=0;$detail=array();foreach($critical as$t){$n=(int)$this->db->where('company_id IS NULL',null,false)->count_all_results($t);$detail[$t]=$n;$null+=$n;}$q=array('migration'=>(int)$this->db->get('migrations')->row()->version,'companies'=>(int)$this->db->count_all('companies'),'user_without_company'=>(int)$this->db->query('SELECT COUNT(*)n FROM users u LEFT JOIN user_companies x ON x.user_id=u.id WHERE x.id IS NULL')->row()->n,'critical_rows_without_company'=>$null,'critical_detail'=>$detail,'journal_lines_without_company'=>(int)$this->db->where('company_id IS NULL',null,false)->count_all_results('journal_details'),'journal_lines_with_dimension'=>(int)$this->db->group_start()->where('branch_id IS NOT NULL',null,false)->or_where('department_id IS NOT NULL',null,false)->or_where('project_id IS NOT NULL',null,false)->or_where('cost_center_id IS NOT NULL',null,false)->group_end()->count_all_results('journal_details'),'unbalanced_by_company'=>(int)$this->db->query('SELECT COUNT(*)n FROM(SELECT j.company_id,j.id FROM journals j JOIN journal_details d ON d.journal_id=j.id GROUP BY j.company_id,j.id HAVING ABS(SUM(d.debit-d.kredit))>.01)x')->row()->n,'budgets'=>(int)$this->db->count_all('budgets'),'currencies'=>(int)$this->db->count_all('currencies'),'duplicate_rates'=>(int)$this->db->query('SELECT COUNT(*)n FROM(SELECT company_id,rate_date,from_currency,to_currency FROM exchange_rates GROUP BY company_id,rate_date,from_currency,to_currency HAVING COUNT(*)>1)x')->row()->n);echo json_encode($q,JSON_PRETTY_PRINT).PHP_EOL;}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
class Stage15check extends CI_Controller
|
||||
{
|
||||
public function index(){if(!$this->input->is_cli_request())show_404();$root=FCPATH;$header=file_get_contents(APPPATH.'views/partials/header.php');$footer=file_get_contents(APPPATH.'views/partials/footer.php');$nav=file_get_contents(APPPATH.'libraries/NavigationService.php');$sw=file_get_contents($root.'service-worker.js');$manifest=json_decode(file_get_contents($root.'manifest.webmanifest'),true);$iconsOk=true;foreach($manifest['icons']as$i){$path=$root.$i['src'];$size=is_file($path)?getimagesize($path):false;$expected=explode('x',$i['sizes']);if(!$size||$size[0]!=(int)$expected[0]||$size[1]!=(int)$expected[1])$iconsOk=false;}$checks=array('single_menu_partial'=>substr_count($header,"partials/navigation_items")===2,'desktop_mobile_same_source'=>substr_count($header,"compact('menu_items','active_menu')")===2,'dead_umum_removed'=>strpos($nav,"'url'=>'umum'")===false,'menu_search'=>strpos($header,'menu-filter')!==false,'quick_actions'=>strpos($header,'quickActions')!==false,'breadcrumb'=>strpos($header,'app-breadcrumb')!==false,'approval_badge'=>strpos($nav,'approvalBadge')!==false,'local_bootstrap'=>is_file($root.'assets/vendor/bootstrap.min.css')&&is_file($root.'assets/vendor/bootstrap.bundle.min.js'),'local_jquery'=>is_file($root.'assets/vendor/jquery.min.js'),'responsive_css'=>is_file($root.'assets/css/app-responsive.min.css'),'double_submit_guard'=>strpos(file_get_contents($root.'assets/js/app-ui.min.js'),"dataset.submitting")!==false,'offline_post_block'=>strpos(file_get_contents($root.'assets/js/app-ui.min.js'),"navigator.onLine")!==false,'static_only_cache'=>strpos($sw,"/assets/")!==false&&strpos($sw,"method!=='GET'")!==false,'pwa_icons_exact'=>$iconsOk,'manifest_standalone'=>isset($manifest['display'])&&$manifest['display']==='standalone','duplicate_login_swal'=>substr_count(file_get_contents(APPPATH.'views/auth/login.php'),'sweetalert2')<=1);$failed=array_keys(array_filter($checks,function($x){return!$x;}));echo json_encode(array('checks'=>$checks,'failed'=>$failed,'viewport_artifacts'=>array('360x800','390x844','768x1024','1366x768')),JSON_PRETTY_PRINT).PHP_EOL;if($failed)exit(1);}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
class Stage16check extends CI_Controller
|
||||
{
|
||||
public function index(){if(!$this->input->is_cli_request())show_404();$this->load->library(array('PerformanceCacheService','JobQueueService'));$required=array('ci_sessions','app_cache','dashboard_summaries','app_jobs','app_dead_letters','database_health_metrics','activity_logs_archive');$missing=array_values(array_filter($required,function($t){return!$this->db->table_exists($t);}));$cache=$this->performancecacheservice->remember('stage16:check',60,function(){return array('ok'=>true,'at'=>date('c'));});$company=$this->db->order_by('id')->get('companies')->row();$payload=array('report_code'=>'journal_register','format'=>'csv','filters'=>array('from'=>date('Y-01-01'),'to'=>date('Y-m-d')),'requested_by'=>1);$first=$this->jobqueueservice->enqueue('report_export',$payload,'stage16:test:report','reports',5,3,$company->id);$second=$this->jobqueueservice->enqueue('report_export',$payload,'stage16:test:report','reports',5,3,$company->id);$job=$this->db->get_where('app_jobs',array('id'=>$first))->row();$result=array('missing_tables'=>$missing,'cache_ok'=>!empty($cache['ok']),'queue_idempotent'=>$first===$second,'job_id'=>$first,'job_status'=>$job->status,'db_session_driver'=>$this->config->item('sess_driver'),'status'=>!$missing&&!empty($cache['ok'])&&$first===$second?'ok':'failed');echo json_encode($result,JSON_PRETTY_PRINT).PHP_EOL;if($result['status']!=='ok')exit(1);}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
|
||||
/** QA integrasi read-only untuk invariant accounting dan kontrol aplikasi. */
|
||||
class Stage17check extends CI_Controller
|
||||
{
|
||||
private $checks = array();
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
if (!$this->input->is_cli_request()) show_error('QA check hanya tersedia melalui CLI.', 403);
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$this->zero('journal_balance', "SELECT COUNT(*) n FROM (SELECT j.id FROM journals j JOIN journal_details d ON d.journal_id=j.id WHERE j.status IN('posted','reversed') GROUP BY j.id HAVING ABS(SUM(d.debit)-SUM(d.kredit))>.009) x", 'Jurnal posted/reversed harus balance.');
|
||||
$this->zero('posted_journal_has_lines', "SELECT COUNT(*) n FROM journals j LEFT JOIN journal_details d ON d.journal_id=j.id WHERE j.status IN('posted','reversed') GROUP BY j.id HAVING COUNT(d.id)<2", 'Setiap jurnal posted minimal memiliki dua baris.', true);
|
||||
$this->zero('invoice_journal_trace', "SELECT COUNT(*) n FROM invoices i LEFT JOIN journals j ON j.id=i.journal_id WHERE i.workflow_status IN('posted','reversed') AND i.posted_by IS NOT NULL AND j.id IS NULL AND EXISTS(SELECT 1 FROM invoice_details d WHERE d.invoice_id=i.id AND d.line_type<>'savings')", 'Invoice penjualan posted harus memiliki jurnal sumber; invoice Tabungan baru dijurnal saat pembayaran.');
|
||||
$this->warning('legacy_invoice_without_journal', "SELECT COUNT(*) n FROM invoices WHERE workflow_status IN('posted','reversed') AND posted_by IS NULL AND journal_id IS NULL", 'Invoice legacy tanpa jurnal perlu direkonsiliasi sebelum cut-over production.');
|
||||
$this->zero('invoice_balance_formula', "SELECT COUNT(*) n FROM invoices WHERE workflow_status IN('posted','reversed') AND ABS(sisa_piutang-GREATEST(total-total_bayar,0))>.01", 'Saldo invoice harus sama dengan total dikurangi pembayaran.');
|
||||
$this->zero('payment_allocation_limit', "SELECT COUNT(*) n FROM payments p LEFT JOIN (SELECT payment_id,SUM(amount) allocated FROM payment_allocations GROUP BY payment_id)a ON a.payment_id=p.id WHERE COALESCE(a.allocated,0)>p.jumlah+.01", 'Alokasi pembayaran tidak boleh melebihi penerimaan.');
|
||||
$this->zero('payment_journal_trace', "SELECT COUNT(*) n FROM payments p LEFT JOIN journals j ON j.id=p.journal_id WHERE p.status='posted' AND p.payment_no NOT LIKE 'LEG-PAY-%' AND j.id IS NULL", 'Pembayaran baru posted harus memiliki jurnal.');
|
||||
$this->zero('purchase_payment_bank_charge_formula', "SELECT COUNT(*) n FROM supplier_payments WHERE ABS(total_cash_out-(amount+bank_charge_amount))>.01 OR bank_charge_amount<0 OR (bank_charge_amount>0 AND bank_charge_account_id IS NULL)", 'Total kas keluar pembelian harus sama dengan pokok pembayaran ditambah admin bank.');
|
||||
$this->zero('purchase_payment_source_total', "SELECT COUNT(*) n FROM supplier_payments p LEFT JOIN (SELECT supplier_payment_id,SUM(amount) source_total FROM purchase_payment_sources GROUP BY supplier_payment_id) s ON s.supplier_payment_id=p.id WHERE p.status='posted' AND ABS(COALESCE(s.source_total,p.amount)-p.total_cash_out)>.01", 'Total sumber Kas/Bank harus sama dengan total kas keluar pembayaran pembelian.');
|
||||
$this->zero('purchase_payment_bank_charge_mapping', "SELECT COUNT(*) n FROM (SELECT 1 marker) x WHERE NOT EXISTS(SELECT 1 FROM system_account_mappings m JOIN accounts a ON a.id=m.account_id WHERE m.mapping_key='bank_charge_expense' AND m.is_active=1 AND a.is_active=1 AND a.allow_posting=1 AND a.tipe='expense')", 'Mapping beban administrasi bank harus mengarah ke akun beban aktif.');
|
||||
$this->zero('purchase_return_draft_does_not_move_stock', "SELECT COUNT(*) n FROM stock_logs sl JOIN purchase_returns pr ON pr.id=sl.ref_id AND sl.ref_type='purchase_return' WHERE pr.status IN('draft','submitted','approved','reserved')", 'Draft, pengajuan, approval, dan reservasi retur tidak boleh mengurangi stok.');
|
||||
$this->zero('purchase_return_quantity_limit', "SELECT COUNT(*) n FROM (SELECT l.receipt_line_id,SUM(CASE WHEN h.status NOT IN('draft','rejected','cancelled','reversed') THEN l.qty ELSE 0 END) returned_qty,MAX(grl.qty) received_qty FROM purchase_return_lines l JOIN purchase_returns h ON h.id=l.purchase_return_id JOIN goods_receipt_lines grl ON grl.id=l.receipt_line_id GROUP BY l.receipt_line_id HAVING returned_qty>received_qty+.0001)x", 'Total retur tidak boleh melebihi kuantitas penerimaan.');
|
||||
$this->zero('purchase_return_post_idempotency', "SELECT COUNT(*) n FROM (SELECT purchase_return_line_id FROM purchase_return_shipment_lines GROUP BY purchase_return_line_id HAVING COUNT(*)>1)x", 'Satu baris retur tidak boleh diposting ke pengiriman lebih dari satu kali.');
|
||||
$this->zero('purchase_return_barcode_reservation', "SELECT COUNT(*) n FROM item_barcodes WHERE return_reserved_qty<0 OR return_reserved_qty>qty_sisa+.0001", 'Reservasi retur barcode tidak boleh negatif atau melebihi saldo barcode.');
|
||||
$this->zero('supplier_refund_balance_formula', "SELECT COUNT(*) n FROM supplier_refund_claims WHERE ABS(balance-GREATEST(claim_amount-received_amount,0))>.01 OR received_amount>claim_amount+.01", 'Saldo refund harus sama dengan nilai klaim dikurangi dana diterima.');
|
||||
$this->zero('supplier_refund_receipt_limit', "SELECT COUNT(*) n FROM (SELECT supplier_refund_claim_id,SUM(amount) received,MAX(c.claim_amount) claimed FROM supplier_refund_receipts r JOIN supplier_refund_claims c ON c.id=r.supplier_refund_claim_id WHERE r.status<>'reversed' GROUP BY supplier_refund_claim_id HAVING received>claimed+.01)x", 'Akumulasi penerimaan refund tidak boleh melebihi klaim.');
|
||||
$this->zero('purchase_adjustment_tenant_isolation', "SELECT COUNT(*) n FROM purchase_return_lines l JOIN purchase_returns h ON h.id=l.purchase_return_id WHERE COALESCE(l.company_id,0)<>COALESCE(h.company_id,0)", 'Header dan detail retur wajib berada pada perusahaan yang sama.');
|
||||
$this->zero('purchase_adjustment_mapping', "SELECT COUNT(*) n FROM (SELECT 'supplier_refund_receivable' k UNION ALL SELECT 'purchase_return_adjustment' UNION ALL SELECT 'purchase_price_variance') x WHERE NOT EXISTS(SELECT 1 FROM system_account_mappings m JOIN accounts a ON a.id=m.account_id WHERE m.mapping_key=x.k AND m.is_active=1 AND a.is_active=1 AND a.allow_posting=1)", 'Seluruh mapping akun Retur/Refund harus aktif dan dapat diposting.');
|
||||
$this->zero('purchase_adjustment_orphan_attachment', "SELECT COUNT(*) n FROM transaction_attachments a WHERE (a.entity_type='purchase_return' AND NOT EXISTS(SELECT 1 FROM purchase_returns x WHERE x.id=a.entity_id)) OR (a.entity_type='purchase_replacement' AND NOT EXISTS(SELECT 1 FROM purchase_replacements x WHERE x.id=a.entity_id)) OR (a.entity_type='supplier_refund' AND NOT EXISTS(SELECT 1 FROM supplier_refund_claims x WHERE x.id=a.entity_id)) OR (a.entity_type='supplier_refund_receipt' AND NOT EXISTS(SELECT 1 FROM supplier_refund_receipts x WHERE x.id=a.entity_id)) OR (a.entity_type='supplier_debit_note' AND NOT EXISTS(SELECT 1 FROM supplier_debit_notes x WHERE x.id=a.entity_id))", 'Lampiran penyesuaian pembelian wajib memiliki dokumen sumber.');
|
||||
$this->warning('legacy_payment_without_journal', "SELECT COUNT(*) n FROM payments WHERE status='posted' AND payment_no LIKE 'LEG-PAY-%' AND journal_id IS NULL", 'Pembayaran legacy tanpa jurnal perlu direkonsiliasi sebelum cut-over production.');
|
||||
$this->zero('credit_note_journal_trace', "SELECT COUNT(*) n FROM credit_notes c LEFT JOIN journals j ON j.id=c.journal_id WHERE c.status='posted' AND j.id IS NULL", 'Credit note posted harus memiliki jurnal reversal nilai.');
|
||||
$this->zero('inventory_negative_stock', "SELECT COUNT(*) n FROM (SELECT item_id,warehouse_id,SUM(IF(direction='in',qty,-qty)) qty FROM inventory_ledger GROUP BY item_id,warehouse_id HAVING qty<-.0001)x", 'Stock ledger tidak boleh negatif.');
|
||||
$this->zero('inventory_duplicate_idempotency', "SELECT COUNT(*) n FROM (SELECT idempotency_key FROM inventory_ledger GROUP BY idempotency_key HAVING COUNT(*)>1)x", 'Mutasi stok tidak boleh diproses dua kali.');
|
||||
$this->zero('inventory_source_trace', "SELECT COUNT(*) n FROM inventory_ledger WHERE document_type='' OR document_id IS NULL", 'Mutasi stok wajib memiliki dokumen sumber.');
|
||||
$this->zero('depreciation_duplicate', "SELECT COUNT(*) n FROM (SELECT asset_id,period FROM asset_depreciation_schedule GROUP BY asset_id,period HAVING COUNT(*)>1)x", 'Penyusutan bulanan harus idempotent.');
|
||||
$this->zero('depreciation_residual_limit', "SELECT COUNT(*) n FROM asset_depreciation_schedule s JOIN assets a ON a.id=s.asset_id WHERE s.closing_book_value<a.nilai_residu-.01 OR s.depreciation_amount<0", 'Penyusutan tidak boleh melewati nilai residu.');
|
||||
$this->zero('payroll_formula', "SELECT COUNT(*) n FROM k_payrolls WHERE ABS(total_salary-(gross_salary-total_deductions))>.01", 'Payroll register harus konsisten.');
|
||||
$this->zero('payroll_journal_trace', "SELECT COUNT(*) n FROM k_payroll_periods p LEFT JOIN journals j ON j.id=p.journal_id WHERE p.workflow_status IN('posted','paid') AND j.id IS NULL", 'Payroll posted harus memiliki jurnal.');
|
||||
$this->zero('closed_period_mutation', "SELECT COUNT(*) n FROM journals j JOIN accounting_periods p ON p.id=j.accounting_period_id WHERE p.status='closed' AND j.updated_at>p.closed_at", 'Tidak boleh ada jurnal diubah setelah periode ditutup.');
|
||||
$this->zero('reversal_preserves_history', "SELECT COUNT(*) n FROM journals r LEFT JOIN journals o ON o.id=r.reversal_of WHERE r.reversal_of IS NOT NULL AND o.id IS NULL", 'Reversal wajib mempertahankan jurnal asli.');
|
||||
$this->zero('reversal_opposite_value', "SELECT COUNT(*) n FROM journals r JOIN journals o ON o.id=r.reversal_of JOIN (SELECT journal_id,SUM(debit) d,SUM(kredit) c FROM journal_details GROUP BY journal_id) rd ON rd.journal_id=r.id JOIN (SELECT journal_id,SUM(debit) d,SUM(kredit) c FROM journal_details GROUP BY journal_id) od ON od.journal_id=o.id WHERE ABS(rd.d-od.c)>.01 OR ABS(rd.c-od.d)>.01", 'Nilai reversal harus berkebalikan dengan jurnal asli.');
|
||||
$this->permissionChecks();
|
||||
$this->zero('single_master_admin', "SELECT ABS(COUNT(*)-1) n FROM roles WHERE is_super_admin=1 AND nama_role='Admin' AND is_active=1", 'Harus tepat satu role Admin aktif sebagai Master Admin.');
|
||||
$this->zero('system_role_without_description', "SELECT COUNT(*) n FROM roles WHERE is_system=1 AND (description IS NULL OR TRIM(description)='')", 'Setiap role bawaan wajib memiliki uraian tanggung jawab.');
|
||||
$this->reportCoverage();
|
||||
$failed = array_filter($this->checks, function ($row) { return $row['status'] !== 'pass'; });
|
||||
echo json_encode(array('status' => $failed ? 'failed' : 'ok', 'checked_at' => date('c'), 'checks' => $this->checks), JSON_PRETTY_PRINT) . PHP_EOL;
|
||||
if ($failed) exit(1);
|
||||
}
|
||||
|
||||
private function zero($name, $sql, $detail, $grouped = false)
|
||||
{
|
||||
try {
|
||||
$query = $this->db->query($grouped ? 'SELECT COUNT(*) n FROM (' . $sql . ') qa' : $sql);
|
||||
$count = (int) $query->row()->n;
|
||||
$this->checks[$name] = array('status' => $count === 0 ? 'pass' : 'fail', 'violations' => $count, 'detail' => $detail);
|
||||
} catch (Throwable $e) {
|
||||
$this->checks[$name] = array('status' => 'fail', 'violations' => null, 'detail' => $detail . ' SQL: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private function permissionChecks()
|
||||
{
|
||||
$invalid = 0; $roles = $this->db->get('roles')->result();
|
||||
foreach ($roles as $role) {
|
||||
$permissions = json_decode($role->permissions, true);
|
||||
if (!is_array($permissions)) { $invalid++; continue; }
|
||||
foreach ($permissions as $feature => $actions) {
|
||||
if (!is_string($feature) || !is_array($actions)) $invalid++;
|
||||
foreach ((array) $actions as $action) if (!in_array($action, array('can_view','can_create','can_update','can_delete','can_approve','can_post','can_export','can_import'), true)) $invalid++;
|
||||
}
|
||||
}
|
||||
$this->checks['role_permission_schema'] = array('status' => $invalid === 0 ? 'pass' : 'fail', 'violations' => $invalid, 'detail' => 'Konfigurasi permission role harus valid dan memakai action yang dikenal.');
|
||||
}
|
||||
|
||||
private function warning($name, $sql, $detail)
|
||||
{
|
||||
try { $count = (int) $this->db->query($sql)->row()->n; }
|
||||
catch (Throwable $e) { $count = null; $detail .= ' SQL: ' . $e->getMessage(); }
|
||||
$this->checks[$name] = array('status' => 'pass', 'severity' => 'warning', 'violations' => $count, 'detail' => $detail);
|
||||
}
|
||||
|
||||
private function reportCoverage()
|
||||
{
|
||||
try {
|
||||
$access=$this->db->order_by('is_default','DESC')->get('user_companies')->row();if(!$access)throw new RuntimeException('Akses perusahaan untuk QA tidak tersedia.');$this->session->set_userdata(array('user_id'=>$access->user_id,'company_id'=>$access->company_id));
|
||||
$this->load->library(array('ProfessionalReportService','CompanyContext'));
|
||||
$filters=array('from'=>'1900-01-01','to'=>date('Y-m-d'),'branch_id'=>0,'department_id'=>0,'project_id'=>0,'cost_center_id'=>0,'profit_center_id'=>0);
|
||||
$balance=$this->professionalreportservice->run('balance_sheet',$filters);$profit=$this->professionalreportservice->run('profit_loss',$filters);
|
||||
$company=$this->companycontext->id();
|
||||
$balanceAccounts=$this->db->where('company_id',$company)->where_in('tipe',array('asset','liability','equity'))->count_all_results('accounts');
|
||||
$profitAccounts=$this->db->where('company_id',$company)->where_in('tipe',array('revenue','expense'))->count_all_results('accounts');
|
||||
$ok=count($balance)===$balanceAccounts&&count($profit)===$profitAccounts;
|
||||
$this->checks['financial_report_all_accounts']=array('status'=>$ok?'pass':'fail','violations'=>$ok?0:abs(count($balance)-$balanceAccounts)+abs(count($profit)-$profitAccounts),'detail'=>'Neraca dan laba rugi wajib memuat seluruh akun termasuk saldo nol.');
|
||||
} catch(Throwable$e) {$this->checks['financial_report_all_accounts']=array('status'=>'fail','violations'=>null,'detail'=>$e->getMessage());}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
class Stage89check extends CI_Controller
|
||||
{
|
||||
public function index(){if(!$this->input->is_cli_request())show_404();$this->load->database();$q=array('migration'=>(int)$this->db->get('migrations')->row()->version,'assets'=>(int)$this->db->count_all('assets'),'asset_events'=>(int)$this->db->count_all('asset_events'),'duplicate_depreciation'=>(int)$this->db->query("SELECT COUNT(*) n FROM(SELECT asset_id,period FROM asset_depreciation_schedule GROUP BY asset_id,period HAVING COUNT(*)>1)x")->row()->n,'below_residual'=>(int)$this->db->query("SELECT COUNT(*) n FROM assets WHERE nilai_buku<nilai_residu")->row()->n,'orphan_asset_events'=>(int)$this->db->query("SELECT COUNT(*) n FROM asset_events e LEFT JOIN assets a ON a.id=e.asset_id WHERE a.id IS NULL")->row()->n,'cash_accounts'=>(int)$this->db->count_all('cash_accounts'),'cash_transactions'=>(int)$this->db->count_all('cash_transactions'),'duplicate_cash_keys'=>(int)$this->db->query("SELECT COUNT(*) n FROM(SELECT idempotency_key FROM cash_transactions GROUP BY idempotency_key HAVING COUNT(*)>1)x")->row()->n,'posted_without_journal'=>(int)$this->db->query("SELECT COUNT(*) n FROM cash_transactions WHERE status='posted' AND journal_id IS NULL AND transaction_type<>'opening'")->row()->n,'unbalanced_auto_journals'=>(int)$this->db->query("SELECT COUNT(*) n FROM(SELECT j.id FROM journals j JOIN journal_details d ON d.journal_id=j.id WHERE j.ref_type IN('asset_depreciation','asset_addition','asset_impairment','asset_revaluation','asset_disposal','asset_lost','asset_damaged','cash_transaction','cash_transfer') GROUP BY j.id HAVING ABS(SUM(d.debit-d.kredit))>.01)x")->row()->n);echo json_encode($q,JSON_PRETTY_PRINT).PHP_EOL;}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
class Suppliers extends MY_Controller
|
||||
{
|
||||
public function __construct(){parent::__construct();$this->load->library(array('NumberingService','PostingService','AccountMappingService','FiscalPeriodService'));}
|
||||
public function index(){$q=$this->db->order_by('is_active','DESC')->order_by('name');if($this->db->field_exists('company_id','suppliers'))$q->where('company_id',$this->companycontext->id());$data=array('active_menu'=>'suppliers','rows'=>$q->get('suppliers')->result());$this->load->view('partials/header',$data);$this->load->view('suppliers/index',$data);$this->load->view('partials/footer');}
|
||||
public function save(){if(strtoupper($this->input->method())!=='POST')show_404();$data=$this->payload();$this->assertUnique($data['supplier_code']);if($data['opening_balance']>0&&!$data['opening_balance_date'])throw new BusinessException('Tanggal saldo awal wajib diisi.');$this->db->trans_begin();try{if($this->db->field_exists('company_id','suppliers'))$data['company_id']=$this->companycontext->id();$data['created_by']=(int)$this->session->userdata('user_id');$data['created_at']=$data['updated_at']=date('Y-m-d H:i:s');$this->db->insert('suppliers',$data);$id=(int)$this->db->insert_id();if($data['opening_balance']>0)$this->opening($id,$data);if(!$this->db->trans_status())throw new RuntimeException('Gagal menyimpan supplier.');$this->db->trans_commit();$this->session->set_flashdata('success','Supplier berhasil ditambahkan.');redirect('suppliers');}catch(Throwable$e){$this->db->trans_rollback();show_error($e->getMessage(),422);}}
|
||||
public function update($id){if(strtoupper($this->input->method())!=='POST')show_404();$old=$this->find((int)$id);$data=$this->payload(false);$this->assertUnique($data['supplier_code'],(int)$old->id);$data['updated_at']=date('Y-m-d H:i:s');$this->scope((int)$id)->update('suppliers',$data);log_activity('suppliers','update','Memperbarui supplier '.$data['supplier_code'].' - '.$data['name'],'success');$this->session->set_flashdata('success','Data supplier berhasil diperbarui.');redirect('suppliers');}
|
||||
public function update_status($id){if(strtoupper($this->input->method())!=='POST')show_404();$s=$this->find((int)$id);$active=$s->is_active?0:1;$this->scope((int)$id)->update('suppliers',array('is_active'=>$active,'updated_at'=>date('Y-m-d H:i:s')));log_activity('suppliers','update',($active?'Mengaktifkan':'Menonaktifkan').' supplier '.$s->supplier_code.' - '.$s->name,'success');$this->session->set_flashdata('success','Supplier berhasil '.($active?'diaktifkan.':'dinonaktifkan. Histori transaksi tetap tersimpan.'));redirect('suppliers');}
|
||||
private function payload($opening=true){$email=trim((string)$this->input->post('email',true));if($email!==''&&!filter_var($email,FILTER_VALIDATE_EMAIL))throw new BusinessException('Format email supplier tidak valid.');$d=array('supplier_code'=>strtoupper(trim((string)$this->input->post('supplier_code',true))),'name'=>trim((string)$this->input->post('name',true)),'tax_number'=>trim((string)$this->input->post('tax_number',true)),'address'=>trim((string)$this->input->post('address',true)),'contact_person'=>trim((string)$this->input->post('contact_person',true)),'phone'=>trim((string)$this->input->post('phone',true)),'email'=>$email,'bank_name'=>trim((string)$this->input->post('bank_name',true)),'bank_account_no'=>trim((string)$this->input->post('bank_account_no',true)),'bank_account_name'=>trim((string)$this->input->post('bank_account_name',true)),'payment_term_days'=>max(0,(int)$this->input->post('payment_term_days')),'is_active'=>$this->input->post('is_active')?1:0);if($opening){$d['opening_balance']=max(0,(float)$this->input->post('opening_balance'));$d['opening_balance_date']=$this->input->post('opening_balance_date')?:null;}if($d['supplier_code']===''||$d['name']==='')throw new BusinessException('Kode dan nama supplier wajib diisi.');return$d;}
|
||||
private function find($id){$r=$this->scope($id)->get('suppliers')->row();if(!$r)show_404();return$r;}
|
||||
private function scope($id){$this->db->where('id',$id);if($this->db->field_exists('company_id','suppliers'))$this->db->where('company_id',$this->companycontext->id());return$this->db;}
|
||||
private function assertUnique($code,$ignore=0){$this->db->where('supplier_code',$code);if($ignore)$this->db->where('id !=',$ignore);if($this->db->field_exists('company_id','suppliers'))$this->db->where('company_id',$this->companycontext->id());if($this->db->count_all_results('suppliers'))throw new BusinessException('Kode supplier sudah digunakan.');}
|
||||
private function opening($id,$d){$this->fiscalperiodservice->assertOpen($d['opening_balance_date']);$internal=$this->numberingservice->next('supplier_invoice',$d['opening_balance_date']);$desc='[AUTO] Saldo awal hutang supplier '.$d['supplier_code'].' - '.$d['name'].' per '.$d['opening_balance_date'];$journal=$this->postingservice->post(array('tanggal'=>$d['opening_balance_date'],'no_ref'=>$internal,'keterangan'=>$desc,'ref_type'=>'supplier_opening_balance','ref_id'=>$id,'created_by'=>$this->session->userdata('user_id')),array(array('account_id'=>$this->accountmappingservice->get('payable_opening_balance'),'debit'=>$d['opening_balance'],'kredit'=>0,'keterangan'=>$desc),array('account_id'=>$this->accountmappingservice->get('accounts_payable'),'debit'=>0,'kredit'=>$d['opening_balance'],'keterangan'=>$desc)),false,true);$x=array('supplier_invoice_no'=>'OPEN-'.$d['supplier_code'],'internal_no'=>$internal,'supplier_id'=>$id,'invoice_date'=>$d['opening_balance_date'],'due_date'=>$d['opening_balance_date'],'status'=>'partial','total'=>$d['opening_balance'],'balance'=>$d['opening_balance'],'journal_id'=>$journal,'created_by'=>$this->session->userdata('user_id'),'posted_by'=>$this->session->userdata('user_id'),'posted_at'=>date('Y-m-d H:i:s'),'created_at'=>date('Y-m-d H:i:s'));if($this->db->field_exists('company_id','supplier_invoices'))$x['company_id']=$this->companycontext->id();$this->db->insert('supplier_invoices',$x);}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
class Taxes extends MY_Controller
|
||||
{
|
||||
public function __construct(){parent::__construct();$this->requirePermission('taxes');$this->load->database();}
|
||||
private function uid(){return(int)$this->session->userdata('user_id');}private function go($ok,$m){$this->session->set_flashdata($ok?'success':'error',$m);redirect('taxes');}
|
||||
public function index(){$period=$this->input->get('period')?:date('Y-m');$d=array('active_menu'=>'taxes','period'=>$period);$d['codes']=$this->db->order_by('code')->get('tax_codes')->result();$d['periods']=$this->db->order_by('period','DESC')->get('tax_periods')->result();$d['register']=$this->db->select('t.*,c.code,c.name')->from('tax_transactions t')->join('tax_codes c','c.id=t.tax_code_id')->like('t.tax_date',$period,'after')->order_by('t.tax_date')->get()->result();$d['summary']=$this->db->query("SELECT direction,SUM(tax_base) tax_base,SUM(tax_amount) tax_amount FROM tax_transactions WHERE DATE_FORMAT(tax_date,'%Y-%m')=? AND status='posted' GROUP BY direction",array($period))->result();$this->load->view('partials/header',$d);$this->load->view('taxes/index',$d);$this->load->view('partials/footer');}
|
||||
public function code(){try{$this->db->insert('tax_codes',array('code'=>strtoupper(trim($this->input->post('code',true))),'name'=>$this->input->post('name',true),'tax_type'=>$this->input->post('tax_type'),'calculation_method'=>$this->input->post('calculation_method'),'rate'=>(float)$this->input->post('rate'),'formula_expression'=>$this->input->post('formula_expression',true)?:null,'input_account_id'=>$this->input->post('input_account_id')?:null,'output_account_id'=>$this->input->post('output_account_id')?:null,'withholding_payable_account_id'=>$this->input->post('withholding_payable_account_id')?:null,'withholding_receivable_account_id'=>$this->input->post('withholding_receivable_account_id')?:null,'effective_from'=>$this->input->post('effective_from'),'effective_to'=>$this->input->post('effective_to')?:null,'is_active'=>1));$this->go(true,'Kode pajak disimpan.');}catch(Throwable$e){$this->go(false,$e->getMessage());}}
|
||||
public function period(){try{$period=$this->input->post('period');$status=$this->input->post('status');$row=$this->db->get_where('tax_periods',array('period'=>$period,'tax_type'=>$this->input->post('tax_type')))->row();$data=array('period'=>$period,'tax_type'=>$this->input->post('tax_type'),'status'=>$status,'filing_reference'=>$this->input->post('filing_reference',true),'notes'=>$this->input->post('notes',true));if(in_array($status,array('filed','closed'),true)){$data[$status.'_at']=date('Y-m-d H:i:s');$data[$status.'_by']=$this->uid();}if($row)$this->db->where('id',$row->id)->update('tax_periods',$data);else$this->db->insert('tax_periods',$data);$this->go(true,'Status periode pajak diperbarui.');}catch(Throwable$e){$this->go(false,$e->getMessage());}}
|
||||
public function export($period){$rows=$this->db->select('t.tax_date,c.code,c.tax_type,t.direction,t.source_type,t.source_id,t.tax_invoice_no,t.tax_base,t.tax_amount,t.status')->from('tax_transactions t')->join('tax_codes c','c.id=t.tax_code_id')->like('t.tax_date',$period,'after')->order_by('t.tax_date')->get()->result_array();$name='tax-register-'.$period.'.csv';$this->output->set_content_type('text/csv')->set_header('Content-Disposition: attachment; filename="'.$name.'"');$h=fopen('php://output','w');fputcsv($h,array('tanggal','kode','jenis','arah','sumber','id_sumber','nomor_faktur','dasar_pajak','nilai_pajak','status'));foreach($rows as$r)fputcsv($h,$r);fclose($h);}
|
||||
}
|
||||
@@ -1,13 +1,14 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
|
||||
class Teknisi extends CI_Controller {
|
||||
class Teknisi extends MY_Controller {
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
$this->load->model('PeralatanTeknisi_model', 'ptm');
|
||||
$this->load->library('TechnicianEquipmentService');
|
||||
|
||||
if (!$this->session->userdata('logged_in')) {
|
||||
redirect('auth');
|
||||
@@ -16,14 +17,7 @@ class Teknisi extends CI_Controller {
|
||||
|
||||
public function index()
|
||||
{
|
||||
$data = [
|
||||
"active_menu" => "peralatan_teknisi",
|
||||
"teknisi_list" => $this->ptm->get_teknisi_list()
|
||||
];
|
||||
|
||||
$this->load->view('partials/header', $data);
|
||||
$this->load->view('peralatan_teknisi/layout', $data);
|
||||
$this->load->view('partials/footer');
|
||||
$this->renderEquipmentWorkbench('overview');
|
||||
}
|
||||
|
||||
public function get_data()
|
||||
@@ -102,6 +96,8 @@ class Teknisi extends CI_Controller {
|
||||
|
||||
public function batch_kembalikan()
|
||||
{
|
||||
if(strtoupper($this->input->method())==='POST')return json_response(false,'Alur lama dinonaktifkan. Gunakan Scan Peralatan agar pengembalian masuk pemeriksaan gudang.',array(),409);
|
||||
if (strtoupper($this->input->method()) !== 'POST') show_404();
|
||||
$barcodes = $this->input->post('barcodes');
|
||||
|
||||
if (empty($barcodes) || !is_array($barcodes)) {
|
||||
@@ -115,6 +111,8 @@ class Teknisi extends CI_Controller {
|
||||
|
||||
public function batch_dibawa()
|
||||
{
|
||||
if(strtoupper($this->input->method())==='POST')return json_response(false,'Alur lama dinonaktifkan. Gunakan Scan Peralatan dan Serahkan ke Teknisi.',array(),409);
|
||||
if (strtoupper($this->input->method()) !== 'POST') show_404();
|
||||
$barcodes = $this->input->post('barcodes');
|
||||
$user_id = (int)$this->input->post('user_id');
|
||||
|
||||
@@ -153,9 +151,9 @@ class Teknisi extends CI_Controller {
|
||||
$item_count = (int)$row->item_count;
|
||||
$item_text = $item_count . ' Item';
|
||||
|
||||
$aksi = '<button class="btn btn-info btn-sm btn-detail-teknisi" data-id="'.$row->id.'" title="Detail">
|
||||
$aksi = '<a class="btn btn-info btn-sm" href="'.base_url('teknisi/detail/'.(int)$row->id).'" title="Detail dan Riwayat">
|
||||
<i class="bi bi-eye"></i>
|
||||
</button>
|
||||
</a>
|
||||
<button class="btn btn-warning btn-sm btn-form-barang-bawaan" data-id="'.$row->id.'" data-nama="'.htmlspecialchars($row->full_name, ENT_QUOTES, 'UTF-8').'" title="Form Barang Bawaan">
|
||||
<i class="bi bi-file-earmark-text"></i>
|
||||
</button>';
|
||||
@@ -193,49 +191,22 @@ class Teknisi extends CI_Controller {
|
||||
|
||||
public function get_teknisi_items()
|
||||
{
|
||||
$user_id = (int)$this->input->get('user_id');
|
||||
$draw = (int)$this->input->get('draw');
|
||||
$start = (int)$this->input->get('start');
|
||||
$length = (int)$this->input->get('length');
|
||||
$search = $this->input->get('search')['value'] ?? '';
|
||||
$order_col = (int)$this->input->get('order')[0]['column'] ?? 2;
|
||||
$order_dir = $this->input->get('order')[0]['dir'] ?? 'asc';
|
||||
|
||||
if (!$user_id) {
|
||||
echo json_encode(['draw' => $draw, 'recordsTotal' => 0, 'recordsFiltered' => 0, 'data' => []]);
|
||||
return;
|
||||
$user_id=(int)$this->input->get('user_id');$draw=(int)$this->input->get('draw');$start=(int)$this->input->get('start');$length=(int)$this->input->get('length');$search=$this->input->get('search')['value']??'';$order_col=(int)($this->input->get('order')[0]['column']??2);$order_dir=$this->input->get('order')[0]['dir']??'asc';
|
||||
if(!$user_id){echo json_encode(array('draw'=>$draw,'recordsTotal'=>0,'recordsFiltered'=>0,'data'=>array()));return;}
|
||||
$data=$this->ptm->get_teknisi_items_data($user_id,$start,$length,$search,$order_col,$order_dir);$result=array();$no=$start+1;
|
||||
foreach($data as$row){
|
||||
$carried=(float)$row->carried_qty;$installed=(float)$row->installed_qty;$tracking=strtoupper((string)$row->tracking_type)==='UNIT'?'UNIT':'QTY';
|
||||
if($carried>0&&$installed>0)$badge='<span class="badge bg-warning text-dark">Sebagian Dipasang</span><small class="d-block text-muted mt-1">Dibawa '.number_format($carried,0,',','.').' · Dipasang '.number_format($installed,0,',','.').'</small>';
|
||||
elseif($installed>0)$badge='<span class="badge bg-primary">Dipasang</span><small class="d-block text-muted mt-1">'.htmlspecialchars(($row->customer_name?:'-').' · '.($row->usage_date?:'-'),ENT_QUOTES,'UTF-8').'</small>';
|
||||
else$badge='<span class="badge bg-success">Dibawa</span>';
|
||||
$action='-';
|
||||
if(is_master_admin_user()||check_permission('items','can_update')){
|
||||
$action='<button type="button" class="btn btn-sm btn-outline-primary btn-change-item-status" data-user="'.(int)$row->user_id.'" data-barcode="'.htmlspecialchars($row->barcode,ENT_QUOTES,'UTF-8').'" data-item="'.htmlspecialchars($row->nama_barang,ENT_QUOTES,'UTF-8').'" data-tracking="'.$tracking.'" data-carried="'.$carried.'" data-installed="'.$installed.'" data-customer="'.(int)$row->customer_id.'"><i class="bi bi-arrow-left-right me-1"></i>Ubah Status</button>';
|
||||
if(!empty($row->proof_id))$action.=' <a class="btn btn-sm btn-outline-info" target="_blank" href="'.base_url('accountingattachments/preview/'.(int)$row->proof_id).'" title="Bukti terakhir"><i class="bi bi-image"></i></a>';
|
||||
}elseif(!empty($row->proof_id))$action='<a class="btn btn-sm btn-outline-info" target="_blank" href="'.base_url('accountingattachments/preview/'.(int)$row->proof_id).'"><i class="bi bi-image me-1"></i>Bukti</a>';
|
||||
$result[]=array($no++,$row->tanggal??date('d/m/Y'),htmlspecialchars($row->barcode,ENT_QUOTES,'UTF-8'),htmlspecialchars($row->nama_barang,ENT_QUOTES,'UTF-8'),htmlspecialchars($row->serial_number,ENT_QUOTES,'UTF-8')?:'-',number_format((float)$row->assigned_qty,0,',','.'),$badge,$action);
|
||||
}
|
||||
|
||||
$data = $this->ptm->get_teknisi_items_data($user_id, $start, $length, $search, $order_col, $order_dir);
|
||||
|
||||
$total_filtered = $this->ptm->count_filtered_teknisi_items($user_id, $search);
|
||||
$total_all = $this->ptm->count_all_teknisi_items($user_id);
|
||||
|
||||
$result = [];
|
||||
$no = $start + 1;
|
||||
|
||||
foreach ($data as $row) {
|
||||
$badge = $row->status == 'active'
|
||||
? '<span class="badge bg-success">Dibawa</span>'
|
||||
: '<span class="badge bg-secondary">' . htmlspecialchars($row->status, ENT_QUOTES, 'UTF-8') . '</span>';
|
||||
|
||||
$result[] = [
|
||||
$no++,
|
||||
$row->tanggal ?? date('d/m/Y'),
|
||||
htmlspecialchars($row->barcode, ENT_QUOTES, 'UTF-8'),
|
||||
htmlspecialchars($row->nama_barang, ENT_QUOTES, 'UTF-8'),
|
||||
htmlspecialchars($row->serial_number, ENT_QUOTES, 'UTF-8') ?: '-',
|
||||
htmlspecialchars($row->qty_sisa, ENT_QUOTES, 'UTF-8') ?: '0',
|
||||
$badge
|
||||
];
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'draw' => $draw,
|
||||
'recordsTotal' => $total_all,
|
||||
'recordsFiltered' => $total_filtered,
|
||||
'data' => $result
|
||||
]);
|
||||
echo json_encode(array('draw'=>$draw,'recordsTotal'=>$this->ptm->count_all_teknisi_items($user_id),'recordsFiltered'=>$this->ptm->count_filtered_teknisi_items($user_id,$search),'data'=>$result));
|
||||
}
|
||||
|
||||
public function get_form_barang_bawaan_data()
|
||||
@@ -305,6 +276,7 @@ class Teknisi extends CI_Controller {
|
||||
|
||||
public function generate_pdf_barang_bawaan($user_id = null)
|
||||
{
|
||||
return $this->technician_inventory_pdf($user_id);
|
||||
|
||||
$user_id = (int)$user_id;
|
||||
if (!$user_id) {
|
||||
@@ -380,6 +352,10 @@ class Teknisi extends CI_Controller {
|
||||
|
||||
public function batch_transfer()
|
||||
{
|
||||
if(strtoupper($this->input->method())==='POST')return json_response(false,'Alur lama dinonaktifkan. Gunakan tindakan Transfer Teknisi pada hasil scan.',array(),409);
|
||||
if (strtoupper($this->input->method()) !== 'POST') {
|
||||
show_404();
|
||||
}
|
||||
$barcodes = $this->input->post('barcodes');
|
||||
$from_user_id = (int)$this->input->post('from_user_id');
|
||||
$to_user_id = (int)$this->input->post('to_user_id');
|
||||
@@ -407,6 +383,11 @@ class Teknisi extends CI_Controller {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$technician || !$this->ptm->get_teknisi_info($from_user_id)) {
|
||||
echo json_encode(['status' => false, 'message' => 'Teknisi asal atau tujuan tidak ditemukan pada perusahaan aktif']);
|
||||
return;
|
||||
}
|
||||
|
||||
$now = date('Y-m-d H:i:s');
|
||||
$errors = [];
|
||||
|
||||
@@ -416,16 +397,16 @@ class Teknisi extends CI_Controller {
|
||||
$barcode = trim($barcode);
|
||||
if ($barcode == '') continue;
|
||||
|
||||
$item = $this->db
|
||||
->select('it.*, ib.id as barcode_id, tec.full_name as teknisi_name')
|
||||
->from('item_technician it')
|
||||
->join('item_barcodes ib', 'ib.barcode = it.barcode', 'left')
|
||||
->join('k_employees tec', 'tec.id = it.user_id', 'left')
|
||||
->where('it.barcode', $barcode)
|
||||
->where('it.status', 'active')
|
||||
->where('it.user_id', $from_user_id)
|
||||
->get()
|
||||
->row();
|
||||
$item = $this->db->query(
|
||||
"SELECT it.*, ib.id AS barcode_id, ib.qty_sisa, tec.full_name AS teknisi_name
|
||||
FROM item_technician it
|
||||
JOIN item_barcodes ib ON ib.barcode = it.barcode
|
||||
JOIN items i ON i.id = it.item_id AND i.company_id = ?
|
||||
LEFT JOIN k_employees tec ON tec.id = it.user_id
|
||||
WHERE it.barcode = ? AND it.status = 'active' AND COALESCE(it.usage_status,'carried') = 'carried' AND it.user_id = ?
|
||||
FOR UPDATE",
|
||||
[(int)$this->companycontext->id(), $barcode, $from_user_id]
|
||||
)->row();
|
||||
|
||||
if (!$item) {
|
||||
$errors[] = "Barcode $barcode tidak ditemukan pada teknisi asal";
|
||||
@@ -439,13 +420,19 @@ class Teknisi extends CI_Controller {
|
||||
->where('user_id', $from_user_id)
|
||||
->where('status', 'active')
|
||||
->update('item_technician', [
|
||||
'user_id' => $to_user_id
|
||||
'user_id' => $to_user_id,
|
||||
'usage_status' => 'carried',
|
||||
'usage_date' => null,
|
||||
'customer_id' => null,
|
||||
'usage_notes' => null,
|
||||
'updated_by' => (int)$this->session->userdata('user_id'),
|
||||
'updated_at' => $now
|
||||
]);
|
||||
|
||||
$this->db->insert('item_movements', [
|
||||
'item_id' => $item->item_id,
|
||||
'barcode_id' => $item->barcode_id,
|
||||
'qty' => 1,
|
||||
'qty' => (float)($item->assigned_qty?:$item->qty_sisa),
|
||||
'from_type' => 'technician',
|
||||
'from_id' => $from_user_id,
|
||||
'to_type' => 'technician',
|
||||
@@ -463,8 +450,7 @@ class Teknisi extends CI_Controller {
|
||||
log_message('error', 'batch_transfer gagal: ' . json_encode($db_error));
|
||||
echo json_encode([
|
||||
'status' => false,
|
||||
'message' => 'Gagal memproses transfer',
|
||||
'debug' => $db_error['message'] ?? 'Unknown database error'
|
||||
'message' => 'Gagal memproses transfer barang teknisi.'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
@@ -480,39 +466,314 @@ class Teknisi extends CI_Controller {
|
||||
|
||||
public function get_log_data()
|
||||
{
|
||||
$data = $this->ptm->get_log_barang();
|
||||
|
||||
$result = [];
|
||||
$no = 1;
|
||||
|
||||
foreach ($data as $row) {
|
||||
$status_badge = $row->status === 'active'
|
||||
? '<span class="badge bg-success">ACTIVE</span>'
|
||||
: '<span class="badge bg-secondary">INACTIVE</span>';
|
||||
|
||||
$result[] = [
|
||||
$no++,
|
||||
htmlspecialchars($row->teknisi_name ?? '-', ENT_QUOTES, 'UTF-8'),
|
||||
htmlspecialchars($row->item_code ?? '-', ENT_QUOTES, 'UTF-8'),
|
||||
htmlspecialchars($row->barcode, ENT_QUOTES, 'UTF-8'),
|
||||
htmlspecialchars($row->nama_barang ?? '-', ENT_QUOTES, 'UTF-8'),
|
||||
$row->created_at ? date('d/m/Y H:i', strtotime($row->created_at)) : '-',
|
||||
$row->status === 'inactive' && $row->deleted_at ? date('d/m/Y H:i', strtotime($row->deleted_at)) : '-',
|
||||
$status_badge,
|
||||
];
|
||||
}
|
||||
|
||||
echo json_encode(['data' => $result]);
|
||||
$this->equipmentPermission('technician_equipment','can_view');$rows=array();$labels=array('company'=>'Perusahaan','customer'=>'Customer','transferred_non_inventory'=>'Non-Persediaan Diserahkan','unknown'=>'Belum Diketahui','carried'=>'Dibawa','installed'=>'Dipasang','removed'=>'Dilepas','returned'=>'Dikembalikan','good'=>'Baik','inspection_required'=>'Perlu Diperiksa','repair_required'=>'Perlu Perbaikan','under_repair'=>'Dalam Perbaikan','damaged'=>'Rusak','lost'=>'Hilang','scrapped'=>'Dihapuskan');
|
||||
foreach($this->technicianequipmentservice->history($this->logFilters())as$index=>$row){$rows[]=array($index+1,html_escape($row['document_no']).'<br><small>'.html_escape(date('d/m/Y',strtotime($row['document_date']))).'</small>',html_escape($row['technician_name']?:'-'),html_escape($row['display_name']),html_escape($row['barcode_snapshot']).'<br><small>'.html_escape($row['serial_number']?:'-').'</small>',html_escape($labels[$row['ownership_type']]??$row['ownership_type']),number_format((float)$row['qty'],abs((float)$row['qty']-round((float)$row['qty']))<.0001?0:4,',','.'),html_escape($row['customer_name']?:'-'),html_escape(($row['from_custodian_type']?:'-').' → '.($row['to_custodian_type']?:'-')),html_escape($labels[$row['to_usage_status']]??$row['to_usage_status']),html_escape($labels[$row['to_condition_status']]??$row['to_condition_status']),html_escape($row['document_notes']?:$row['notes']?:'-'),'<button class="btn btn-outline-primary btn-sm tech-log-detail" data-id="'.(int)$row['document_id'].'"><i class="bi bi-eye"></i></button>');}
|
||||
return json_response(true,'Riwayat peralatan.',array('data'=>$rows));
|
||||
}
|
||||
|
||||
public function log_barang()
|
||||
{
|
||||
$this->equipmentPermission('technician_equipment','can_view');
|
||||
$data = [
|
||||
"active_menu" => "peralatan_teknisi",
|
||||
"teknisi_list" => $this->ptm->get_teknisi_list(),
|
||||
"customers" => $this->db->select('id,nama')->where('company_id',(int)$this->companycontext->id())->order_by('nama')->get('customers')->result(),
|
||||
"can_export" => is_master_admin_user() || check_permission('technician_equipment','can_export') || check_permission('items','can_export'),
|
||||
"equipment_permissions" => $this->equipmentPermissionFlags(),
|
||||
"equipment_dashboard" => $this->scopedEquipmentDashboard()
|
||||
];
|
||||
|
||||
$this->load->view('partials/header', $data);
|
||||
$this->load->view('peralatan_teknisi/log_barang', $data);
|
||||
$this->load->view('partials/footer');
|
||||
}
|
||||
|
||||
public function detail($userId=0)
|
||||
{
|
||||
$this->equipmentPermission('technician_equipment','can_view');$userId=(int)$userId;if($this->restrictedTechnicianEmployee()&&$userId!==$this->restrictedTechnicianEmployee())show_error('Teknisi hanya dapat melihat barang dalam penguasaannya.',403,'Akses ditolak');$technician=$this->ptm->get_teknisi_info($userId);if(!$technician)show_404();
|
||||
$data=array('active_menu'=>'peralatan_teknisi','technician'=>$technician,'allocations'=>$this->technicianequipmentservice->allocations(null,null,$userId),'documents'=>$this->technicianequipmentservice->documents(array('technician_id'=>$userId)),'equipment_permissions'=>$this->equipmentPermissionFlags(),'equipment_dashboard'=>$this->scopedEquipmentDashboard());
|
||||
$this->load->view('partials/header',$data);$this->load->view('peralatan_teknisi/detail_professional',$data);$this->load->view('partials/footer');
|
||||
}
|
||||
|
||||
public function get_status_history_data($userId=0)
|
||||
{
|
||||
$this->requirePermission('items','can_view');$userId=(int)$userId;if(!$this->ptm->get_teknisi_info($userId))return json_response(false,'Teknisi tidak ditemukan.',array('data'=>array()),404);
|
||||
$labels=array('initial'=>'Saldo Awal','carried'=>'Dibawa Teknisi','installed'=>'Dipasang','returned'=>'Kembali ke Gudang','warehouse'=>'Gudang');$rows=array();
|
||||
foreach($this->ptm->get_status_history($userId)as$index=>$row){$proof=$row->attachment_id?'<a class="btn btn-sm btn-outline-info" target="_blank" href="'.base_url('accountingattachments/preview/'.(int)$row->attachment_id).'" title="Lihat bukti"><i class="bi bi-image"></i></a>':'-';$rows[]=array($index+1,date('d/m/Y',strtotime($row->event_date)),htmlspecialchars(($row->kode_detail?:'-').' - '.($row->nama_barang?:'-'),ENT_QUOTES,'UTF-8'),htmlspecialchars($row->barcode,ENT_QUOTES,'UTF-8'),htmlspecialchars($labels[$row->from_status]??ucfirst($row->from_status),ENT_QUOTES,'UTF-8').' → '.htmlspecialchars($labels[$row->to_status]??ucfirst($row->to_status),ENT_QUOTES,'UTF-8'),number_format((float)$row->qty,0,',','.'),htmlspecialchars($row->customer_name?:'-',ENT_QUOTES,'UTF-8'),htmlspecialchars($row->notes?:'-',ENT_QUOTES,'UTF-8'),htmlspecialchars($row->actor_name?:'-',ENT_QUOTES,'UTF-8'),$proof);}
|
||||
return json_response(true,'Riwayat status peralatan.',array('data'=>$rows));
|
||||
}
|
||||
|
||||
public function update_item_status()
|
||||
{
|
||||
if(strtoupper($this->input->method())==='POST')return json_response(false,'Alur status lama dinonaktifkan. Gunakan workbench Peralatan Teknisi agar histori dan saldo tetap sinkron.',array(),409);
|
||||
if(strtoupper($this->input->method())!=='POST')show_404();
|
||||
$this->requirePermission('items','can_update');
|
||||
$barcode=trim((string)$this->input->post('barcode',true));$userId=(int)$this->input->post('user_id');$status=trim((string)$this->input->post('status',true));$date=trim((string)$this->input->post('date',true));$customer=(int)$this->input->post('customer_id');$notes=trim((string)$this->input->post('notes',true));$qty=(float)$this->input->post('qty');$source=trim((string)$this->input->post('source_status',true));$key=trim((string)$this->input->post('idempotency_key',true));
|
||||
if($barcode===''||$userId<1)return json_response(false,'Barang teknisi tidak valid.',array(),422);
|
||||
if(!in_array($status,array('installed','carried','warehouse'),true))return json_response(false,'Status tujuan tidak valid.',array(),422);
|
||||
$parsed=DateTime::createFromFormat('Y-m-d',$date);if(!$parsed||$parsed->format('Y-m-d')!==$date)return json_response(false,'Tanggal perubahan status wajib diisi dengan benar.',array(),422);
|
||||
if($status==='installed'&&$customer<1)return json_response(false,'Customer wajib dipilih untuk status Dipasang.',array(),422);
|
||||
if($notes==='')return json_response(false,'Keterangan perubahan status wajib diisi.',array(),422);
|
||||
if(isset($_FILES['proof'])&&!empty($_FILES['proof']['tmp_name'])){$mime=$this->validInstallationImage($_FILES['proof']);if(!$mime)return json_response(false,'Bukti harus berupa gambar JPG, PNG, WEBP, GIF, BMP, HEIC, atau AVIF maksimal 5 MB.',array(),422);}else$mime=null;
|
||||
$storedPath=null;$this->db->trans_begin();
|
||||
try{
|
||||
$result=$this->ptm->change_item_status($barcode,$userId,array('status'=>$status,'date'=>$date,'customer_id'=>$customer,'notes'=>$notes,'qty'=>$qty,'source_status'=>$source,'idempotency_key'=>$key),(int)$this->session->userdata('user_id'));
|
||||
if($mime&&!empty($result['log_id'])){$attachment=$this->saveInstallationImage((int)$result['log_id'],$_FILES['proof'],$mime);$storedPath=$attachment['path'];$this->db->where('id',(int)$result['log_id'])->update('technician_item_status_logs',array('attachment_id'=>(int)$attachment['id']));}
|
||||
if($this->db->trans_status()===false)throw new RuntimeException('Perubahan status barang gagal disimpan.');$this->db->trans_commit();
|
||||
$labels=array('installed'=>'Dipasang','carried'=>'Dibawa Teknisi','warehouse'=>'Dikembalikan ke Gudang');log_activity('items','update','Mengubah status barcode '.$barcode.' menjadi '.$labels[$status].' sebanyak '.($result['qty']??0),'success');
|
||||
return json_response(true,'Status barang berhasil diperbarui menjadi '.$labels[$status].'.',array('result'=>$result));
|
||||
}catch(RuntimeException$e){$this->db->trans_rollback();if($storedPath&&is_file($storedPath))unlink($storedPath);return json_response(false,$e->getMessage(),array(),422);}catch(Throwable$e){$this->db->trans_rollback();if($storedPath&&is_file($storedPath))unlink($storedPath);return business_exception_response($e);}
|
||||
}
|
||||
|
||||
public function log_pdf()
|
||||
{
|
||||
$this->equipmentPermission('technician_equipment','can_export');$filters=$this->logFilters();$rows=$this->technicianequipmentservice->history($filters);$company=$this->db->get_where('companies',array('id'=>(int)$this->companycontext->id()))->row();
|
||||
require_once FCPATH.'vendor/autoload.php';$html=$this->load->view('peralatan_teknisi/log_pdf',array('rows'=>$rows,'filters'=>$filters,'company'=>$company),true);$pdf=new Dompdf\Dompdf();$pdf->loadHtml($html);$pdf->setPaper('A4','landscape');$pdf->render();$pdf->stream('Log-Peralatan-Teknisi-'.date('Ymd-His').'.pdf',array('Attachment'=>false));
|
||||
}
|
||||
|
||||
private function logFilters()
|
||||
{
|
||||
$status=trim((string)$this->input->get('status',true));
|
||||
if(!in_array($status,array('','carried','installed','removed','returned'),true))$status='';$condition=trim((string)$this->input->get('condition',true));$ownership=trim((string)$this->input->get('ownership',true));
|
||||
$technicianId=(int)$this->input->get('user_id');if($this->restrictedTechnicianEmployee())$technicianId=$this->restrictedTechnicianEmployee();return array('date_from'=>trim((string)$this->input->get('date_from',true)),'date_to'=>trim((string)$this->input->get('date_to',true)),'customer_id'=>(int)$this->input->get('customer_id'),'technician_id'=>$technicianId,'status'=>$status,'condition'=>$condition,'ownership'=>$ownership,'query'=>trim((string)$this->input->get('query',true)));
|
||||
}
|
||||
|
||||
private function validInstallationImage(array$file)
|
||||
{
|
||||
if(($file['error']??UPLOAD_ERR_OK)!==UPLOAD_ERR_OK||($file['size']??0)>5242880||!is_uploaded_file($file['tmp_name']))return null;$mime=(new finfo(FILEINFO_MIME_TYPE))->file($file['tmp_name']);$allowed=array('image/jpeg'=>'jpg','image/png'=>'png','image/webp'=>'webp','image/gif'=>'gif','image/bmp'=>'bmp','image/heic'=>'heic','image/heif'=>'heif','image/avif'=>'avif');return isset($allowed[$mime])?$mime:null;
|
||||
}
|
||||
|
||||
private function saveInstallationImage($statusLogId,array$file,$mime)
|
||||
{
|
||||
$extensions=array('image/jpeg'=>'jpg','image/png'=>'png','image/webp'=>'webp','image/gif'=>'gif','image/bmp'=>'bmp','image/heic'=>'heic','image/heif'=>'heif','image/avif'=>'avif');$dir=FCPATH.'uploads/accounting/';if(!is_dir($dir)&&!mkdir($dir,0750,true))throw new RuntimeException('Folder bukti perubahan status tidak tersedia.');$stored=bin2hex(random_bytes(20)).'.'.$extensions[$mime];$path=$dir.$stored;if(!move_uploaded_file($file['tmp_name'],$path))throw new RuntimeException('Bukti perubahan status gagal disimpan.');$row=array('module'=>'inventory','entity_type'=>'technician_status_change','entity_id'=>$statusLogId,'original_name'=>basename($file['name']),'stored_name'=>$stored,'mime_type'=>$mime,'file_size'=>filesize($path),'sha256'=>hash_file('sha256',$path),'uploaded_by'=>(int)$this->session->userdata('user_id'),'created_at'=>date('Y-m-d H:i:s'));if($this->db->field_exists('company_id','transaction_attachments'))$row['company_id']=(int)$this->companycontext->id();$this->db->insert('transaction_attachments',$row);return array('id'=>(int)$this->db->insert_id(),'path'=>$path);
|
||||
}
|
||||
|
||||
/* Final operational custody API. Kept separate from the legacy endpoints so
|
||||
old documents remain readable while new movements use the auditable flow. */
|
||||
public function replacement_workflow(){ $this->equipmentPermission('technician_installation','can_create');$this->renderEquipmentWorkbench('replacement'); }
|
||||
public function inspection_queue(){ $this->equipmentPermission('technician_inspection','can_view');$this->renderEquipmentWorkbench('inspection'); }
|
||||
public function customer_register(){ $this->renderEquipmentWorkbench('customer'); }
|
||||
public function bulk_workflow()
|
||||
{
|
||||
$this->equipmentPermission('technician_equipment','can_view');$restricted=$this->restrictedTechnicianEmployee();$allTechnicians=$this->ptm->get_teknisi_list();$technicians=$allTechnicians;if($restricted)$technicians=array_values(array_filter($technicians,function($row)use($restricted){return(int)$row->id===$restricted;}));$data=array('active_menu'=>'peralatan_teknisi','teknisi_list'=>$technicians,'target_teknisi_list'=>$allTechnicians,'warehouses'=>$this->db->select('id,nama')->where(array('company_id'=>(int)$this->companycontext->id(),'is_active'=>1))->order_by('nama')->get('warehouses')->result(),'equipment_permissions'=>$this->equipmentPermissionFlags(),'equipment_dashboard'=>$this->scopedEquipmentDashboard());$this->load->view('partials/header',$data);$this->load->view('peralatan_teknisi/bulk_workflow',$data);$this->load->view('partials/footer');
|
||||
}
|
||||
|
||||
private function renderEquipmentWorkbench($panel='overview')
|
||||
{
|
||||
$this->equipmentPermission('technician_equipment','can_view');
|
||||
$dashboard=$this->scopedEquipmentDashboard();$restricted=$this->restrictedTechnicianEmployee();$allTechnicians=$this->ptm->get_teknisi_list();$technicians=$allTechnicians;if($restricted)$technicians=array_values(array_filter($technicians,function($row)use($restricted){return(int)$row->id===$restricted;}));
|
||||
$data=array(
|
||||
'active_menu'=>'peralatan_teknisi','active_panel'=>$panel,'equipment_dashboard'=>$dashboard,
|
||||
'teknisi_list'=>$technicians,'target_teknisi_list'=>$allTechnicians,
|
||||
'customers'=>$this->db->select('id,nama')->where('company_id',(int)$this->companycontext->id())->order_by('nama')->get('customers')->result(),
|
||||
'warehouses'=>$this->db->select('id,nama')->where(array('company_id'=>(int)$this->companycontext->id(),'is_active'=>1))->order_by('nama')->get('warehouses')->result(),
|
||||
'equipment_permissions'=>$this->equipmentPermissionFlags()
|
||||
);
|
||||
$this->load->view('partials/header',$data);$this->load->view('peralatan_teknisi/workbench',$data);$this->load->view('partials/footer');
|
||||
}
|
||||
|
||||
public function equipment_dashboard()
|
||||
{
|
||||
$this->equipmentPermission('technician_equipment','can_view');
|
||||
return json_response(true,'Ringkasan peralatan.',array('result'=>$this->scopedEquipmentDashboard()));
|
||||
}
|
||||
|
||||
public function equipment_scan()
|
||||
{
|
||||
$this->equipmentPermission('technician_equipment','can_view');
|
||||
try{$result=$this->technicianequipmentservice->scan($this->input->get('code',true));$employee=$this->restrictedTechnicianEmployee();if($employee){$this->assertTechnicianSubjectScope($result['subject_type'],(int)$result['subject_id'],$employee);$result['warehouse_available']=0;$result['allocations']=$this->technicianequipmentservice->allocations($result['subject_type'],(int)$result['subject_id'],$employee);}return json_response(true,'Barang ditemukan.',array('result'=>$result));}
|
||||
catch(Throwable $e){return business_exception_response($e);}
|
||||
}
|
||||
|
||||
public function equipment_search()
|
||||
{
|
||||
$this->equipmentPermission('technician_equipment','can_view');$mode=trim((string)$this->input->get('mode',true));$technicianId=(int)$this->input->get('technician_id');$restricted=$this->restrictedTechnicianEmployee();if($restricted)$technicianId=$restricted;try{return json_response(true,'Hasil pencarian peralatan.',array('data'=>$this->technicianequipmentservice->search($this->input->get('q',true),$mode?:'all',$technicianId,20)));}catch(Throwable$e){return business_exception_response($e);}
|
||||
}
|
||||
|
||||
public function equipment_technician_items()
|
||||
{
|
||||
$this->equipmentPermission('technician_equipment','can_view');$technicianId=(int)$this->input->get('technician_id');$restricted=$this->restrictedTechnicianEmployee();if($restricted){if($technicianId&&$technicianId!==$restricted)show_error('Teknisi hanya dapat melihat barang dalam penguasaannya.',403,'Akses ditolak');$technicianId=$restricted;}if($technicianId<1)return json_response(false,'Pilih teknisi asal terlebih dahulu.',array('data'=>array()),422);
|
||||
try{$rows=$this->technicianequipmentservice->technicianHoldings($technicianId);return json_response(true,'Daftar barang teknisi.',array('data'=>$rows,'total'=>count($rows)));}catch(Throwable$e){return business_exception_response($e);}
|
||||
}
|
||||
|
||||
public function equipment_allocations()
|
||||
{
|
||||
$this->equipmentPermission('technician_equipment','can_view');
|
||||
$technicianId=(int)$this->input->get('technician_id');if($this->restrictedTechnicianEmployee())$technicianId=$this->restrictedTechnicianEmployee();try{return json_response(true,'Posisi peralatan.',array('data'=>$this->technicianequipmentservice->allocations(null,null,$technicianId)));}
|
||||
catch(Throwable $e){return business_exception_response($e);}
|
||||
}
|
||||
|
||||
public function equipment_documents()
|
||||
{
|
||||
$this->equipmentPermission('technician_equipment','can_view');
|
||||
$technicianId=(int)$this->input->get('technician_id');if($this->restrictedTechnicianEmployee())$technicianId=$this->restrictedTechnicianEmployee();$filters=array('date_from'=>$this->input->get('date_from',true),'date_to'=>$this->input->get('date_to',true),'customer_id'=>(int)$this->input->get('customer_id'),'technician_id'=>$technicianId,'document_type'=>$this->input->get('document_type',true),'status'=>$this->input->get('status',true));
|
||||
try{return json_response(true,'Riwayat peralatan.',array('data'=>$this->technicianequipmentservice->documents($filters)));}
|
||||
catch(Throwable $e){return business_exception_response($e);}
|
||||
}
|
||||
|
||||
public function equipment_document($id=0)
|
||||
{
|
||||
$this->equipmentPermission('technician_equipment','can_view');$this->assertTechnicianDocumentScope((int)$id);
|
||||
try{$result=$this->technicianequipmentservice->document((int)$id);$flags=$this->equipmentPermissionFlags();if(empty($flags['attachment']))$result['attachments']=array();return json_response(true,'Detail dokumen.',array('result'=>$result));}
|
||||
catch(Throwable $e){return business_exception_response($e);}
|
||||
}
|
||||
|
||||
public function customer_equipment_data()
|
||||
{
|
||||
$this->equipmentPermission('technician_equipment','can_view');
|
||||
try{$rows=$this->technicianequipmentservice->customerEquipmentSummary($this->restrictedTechnicianEmployee());return json_response(true,'Ringkasan barang per customer.',array('data'=>$rows));}
|
||||
catch(Throwable $e){return business_exception_response($e);}
|
||||
}
|
||||
|
||||
public function customer_equipment_customer_data($customerId=0)
|
||||
{
|
||||
$this->equipmentPermission('technician_equipment','can_view');$customerId=(int)$customerId;$customer=$this->db->select('id,nama')->get_where('customers',array('id'=>$customerId,'company_id'=>(int)$this->companycontext->id()))->row();if(!$customer)return json_response(false,'Customer tidak ditemukan.',array('data'=>array()),404);$status=trim((string)$this->input->get('status',true));
|
||||
try{$rows=$this->technicianequipmentservice->customerEquipmentRows($customerId,$status,$this->restrictedTechnicianEmployee());return json_response(true,'Detail barang customer.',array('data'=>$rows,'customer'=>$customer));}catch(Throwable$e){return business_exception_response($e);}
|
||||
}
|
||||
|
||||
public function customer_equipment_customer_pdf($customerId=0)
|
||||
{
|
||||
$this->equipmentPermission('technician_equipment','can_export');$customerId=(int)$customerId;$customer=$this->db->select('id,nama')->get_where('customers',array('id'=>$customerId,'company_id'=>(int)$this->companycontext->id()))->row();if(!$customer)show_404();$status=trim((string)$this->input->get('status',true));$labels=array('all'=>'Semua Status','installed'=>'Terpasang','stored'=>'Tersimpan di Customer','technician'=>'Dibawa Teknisi','inspection'=>'Perlu Pemeriksaan/Perbaikan','good'=>'Kondisi Baik','damaged'=>'Rusak/Hilang');if(!isset($labels[$status]))$status='all';$rows=$this->technicianequipmentservice->customerEquipmentRows($customerId,$status,$this->restrictedTechnicianEmployee());$company=$this->db->get_where('companies',array('id'=>(int)$this->companycontext->id()))->row();require_once FCPATH.'vendor/autoload.php';$html=$this->load->view('peralatan_teknisi/customer_equipment_pdf',array('rows'=>$rows,'customer'=>$customer,'company'=>$company,'status_label'=>$labels[$status],'printed_by'=>$this->session->userdata('nama')),true);$pdf=new Dompdf\Dompdf();$pdf->loadHtml($html);$pdf->setPaper('A4','landscape');$pdf->render();$pdf->stream('Barang-Customer-'.preg_replace('/[^A-Za-z0-9_-]+/','-',(string)$customer->nama).'-'.$status.'-'.date('Ymd').'.pdf',array('Attachment'=>false));
|
||||
}
|
||||
|
||||
public function customer_equipment_notes_data($customerId=0)
|
||||
{
|
||||
$this->equipmentPermission('technician_equipment','can_view');$customerId=(int)$customerId;$customer=$this->db->select('id,nama')->get_where('customers',array('id'=>$customerId,'company_id'=>(int)$this->companycontext->id()))->row();if(!$customer)return json_response(false,'Customer tidak ditemukan.',array('data'=>array()),404);
|
||||
try{return json_response(true,'Log keterangan barang customer.',array('data'=>$this->technicianequipmentservice->customerEquipmentNotes($customerId,$this->restrictedTechnicianEmployee()),'customer'=>$customer));}catch(Throwable$e){return business_exception_response($e);}
|
||||
}
|
||||
|
||||
public function customer_equipment_notes_pdf($customerId=0)
|
||||
{
|
||||
$this->equipmentPermission('technician_equipment','can_export');$customerId=(int)$customerId;$customer=$this->db->select('id,nama')->get_where('customers',array('id'=>$customerId,'company_id'=>(int)$this->companycontext->id()))->row();if(!$customer)show_404();$rows=$this->technicianequipmentservice->customerEquipmentNotes($customerId,$this->restrictedTechnicianEmployee());$company=$this->db->get_where('companies',array('id'=>(int)$this->companycontext->id()))->row();require_once FCPATH.'vendor/autoload.php';$html=$this->load->view('peralatan_teknisi/customer_equipment_notes_pdf',array('rows'=>$rows,'customer'=>$customer,'company'=>$company,'printed_by'=>$this->session->userdata('nama')),true);$pdf=new Dompdf\Dompdf();$pdf->loadHtml($html);$pdf->setPaper('A4','landscape');$pdf->render();$pdf->stream('Log-Keterangan-Barang-'.preg_replace('/[^A-Za-z0-9_-]+/','-',(string)$customer->nama).'-'.date('Ymd').'.pdf',array('Attachment'=>true));
|
||||
}
|
||||
|
||||
public function customer_equipment_detail($id=0)
|
||||
{
|
||||
$this->equipmentPermission('technician_equipment','can_view');try{$employee=$this->restrictedTechnicianEmployee();if($employee)$this->assertTechnicianSubjectScope('customer_equipment',(int)$id,$employee);$result=$this->technicianequipmentservice->external((int)$id);$flags=$this->equipmentPermissionFlags();if(empty($flags['attachment']))$result['attachments']=array();return json_response(true,'Detail barang customer.',array('result'=>$result));}catch(Throwable$e){return business_exception_response($e);}
|
||||
}
|
||||
|
||||
public function register_customer_equipment()
|
||||
{
|
||||
if(strtoupper($this->input->method())!=='POST')show_404();
|
||||
$this->equipmentPermission('technician_equipment','can_create');
|
||||
$saved=array();try{$files=$this->validatedEquipmentFiles();$post=$this->input->post(NULL,true);if((($post['ownership_type']??'')==='transferred_non_inventory'||!empty($post['deployment_allowed']))&&!$files)throw new BusinessException('Bukti penyerahan/otorisasi wajib dilampirkan untuk status yang dipilih.');$this->db->trans_begin();$result=$this->technicianequipmentservice->registerExternal($post,(int)$this->session->userdata('user_id'));foreach($files as$file)$saved[]=$this->storeEquipmentFile((int)$result['id'],$file,'customer_equipment_registry');if($this->db->trans_status()===false)throw new RuntimeException('Register barang customer gagal disimpan.');$this->db->trans_commit();log_activity('items','equipment_register','Mendaftarkan barang customer non-accounting '.$result['external_barcode'],'success');return json_response(true,'Barang customer berhasil dicatat tanpa masuk persediaan accounting.',array('result'=>$result));}
|
||||
catch(Throwable $e){if($this->db->trans_active())$this->db->trans_rollback();foreach($saved as$path)if(is_file($path))@unlink($path);return business_exception_response($e);}
|
||||
}
|
||||
|
||||
public function process_equipment()
|
||||
{
|
||||
if(strtoupper($this->input->method())!=='POST')show_404();
|
||||
$data=$this->input->post(NULL,true);$action=(string)($data['action']??'');$map=array('handover'=>array('technician_handover','can_create'),'install'=>array('technician_installation','can_create'),'remove'=>array('technician_installation','can_update'),'transfer'=>array('technician_transfer','can_update'),'return'=>array('technician_return','can_update'),'return_customer'=>array('technician_return','can_update'));
|
||||
if(!isset($map[$action]))return json_response(false,'Tindakan tidak valid.',array(),422);if($action==='install'&&trim((string)($data['installation_location']??''))==='')return json_response(false,'Lokasi pemasangan wajib diisi.',array(),422);if(in_array($action,array('remove','return','transfer'),true)&&trim((string)($data['reason']??''))==='')return json_response(false,'Alasan perpindahan wajib diisi.',array(),422);$this->equipmentPermission($map[$action][0],$map[$action][1]);$this->assertTechnicianActionScope($data,$action);
|
||||
return $this->runEquipmentPosting(function()use($data){return$this->technicianequipmentservice->process($data,(int)$this->session->userdata('user_id'));},$action);
|
||||
}
|
||||
|
||||
public function process_equipment_batch()
|
||||
{
|
||||
if(strtoupper($this->input->method())!=='POST')show_404();$data=$this->input->post(NULL,true);$action=(string)($data['action']??'');$map=array('handover'=>array('technician_handover','can_create'),'transfer'=>array('technician_transfer','can_update'),'return'=>array('technician_return','can_update'));if(!isset($map[$action]))return json_response(false,'Jenis perpindahan batch tidak valid.',array(),422);$items=json_decode((string)$this->input->post('items_json',false),true);if(!is_array($items)||!$items)return json_response(false,'Tambahkan minimal satu barang.',array(),422);$data['items']=$items;$this->equipmentPermission($map[$action][0],$map[$action][1]);$restricted=$this->restrictedTechnicianEmployee();if($restricted&&(int)($data['technician_id']??0)!==$restricted)show_error('Teknisi hanya dapat memproses barang untuk dirinya sendiri.',403,'Akses ditolak');if($restricted&&$action!=='handover')foreach($items as$item)$this->assertTechnicianActionScope(array('allocation_id'=>(int)($item['allocation_id']??0)),$action);return$this->runEquipmentPosting(function()use($data){return$this->technicianequipmentservice->processBatch($data,(int)$this->session->userdata('user_id'));},$action);
|
||||
}
|
||||
|
||||
public function process_replacement()
|
||||
{
|
||||
if(strtoupper($this->input->method())!=='POST')show_404();$this->equipmentPermission('technician_installation','can_create');$data=$this->input->post(NULL,true);if(trim((string)($data['installation_location']??''))===''||trim((string)($data['reason']??''))==='')return json_response(false,'Lokasi pemasangan dan alasan penggantian wajib diisi.',array(),422);$this->assertTechnicianActionScope($data,'replacement');
|
||||
return $this->runEquipmentPosting(function()use($data){return$this->technicianequipmentservice->replacement($data,(int)$this->session->userdata('user_id'));},'replacement');
|
||||
}
|
||||
|
||||
public function process_inspection()
|
||||
{
|
||||
if(strtoupper($this->input->method())!=='POST')show_404();$this->equipmentPermission('technician_inspection','can_post');$data=$this->input->post(NULL,true);if(in_array(($data['result_status']??''),array('damaged','lost','scrapped'),true))$this->equipmentPermission('technician_condition','can_update');$this->assertTechnicianActionScope($data,'inspection');
|
||||
return $this->runEquipmentPosting(function()use($data){return$this->technicianequipmentservice->inspect($data,(int)$this->session->userdata('user_id'));},'inspection');
|
||||
}
|
||||
|
||||
public function process_correction()
|
||||
{
|
||||
if(strtoupper($this->input->method())!=='POST')show_404();$this->equipmentPermission('technician_correction','can_update');$data=$this->input->post(NULL,true);$this->assertTechnicianDocumentScope((int)($data['document_id']??0));
|
||||
return $this->runEquipmentPosting(function()use($data){return$this->technicianequipmentservice->reverse($data,(int)$this->session->userdata('user_id'));},'correction');
|
||||
}
|
||||
|
||||
public function inspection_data()
|
||||
{
|
||||
$this->equipmentPermission('technician_inspection','can_view');
|
||||
$technicianId=(int)$this->input->get('technician_id');if($this->restrictedTechnicianEmployee())$technicianId=$this->restrictedTechnicianEmployee();$filters=array('date_from'=>$this->input->get('date_from',true),'date_to'=>$this->input->get('date_to',true),'technician_id'=>$technicianId,'customer_id'=>(int)$this->input->get('customer_id'),'ownership'=>$this->input->get('ownership',true),'condition'=>$this->input->get('condition',true),'status'=>$this->input->get('status',true));try{return json_response(true,'Antrean pemeriksaan.',array('data'=>$this->technicianequipmentservice->inspectionQueue($filters)));}
|
||||
catch(Throwable $e){return business_exception_response($e);}
|
||||
}
|
||||
|
||||
private function runEquipmentPosting(Closure $callback,$action)
|
||||
{
|
||||
$saved=array();$files=array();
|
||||
try{
|
||||
$files=$this->validatedEquipmentFiles();if(in_array($action,array('remove','return','inspection','replacement'),true)&&!$files)throw new BusinessException('Bukti foto atau PDF wajib dilampirkan untuk proses ini.');$this->db->trans_begin();$result=$callback();
|
||||
if(empty($result['duplicate']))foreach($files as$file)$saved[]=$this->storeEquipmentFile((int)$result['document_id'],$file);
|
||||
if($this->db->trans_status()===false)throw new RuntimeException('Transaksi peralatan gagal disimpan.');$this->db->trans_commit();
|
||||
log_activity('items','equipment_'.$action,'Memproses dokumen peralatan '.$result['document_no'].' tanpa jurnal accounting','success');
|
||||
return json_response(true,(!empty($result['duplicate'])?'Permintaan ini sudah pernah diproses.':'Dokumen '.$result['document_no'].' berhasil diposting.'),array('result'=>$result));
|
||||
}catch(Throwable $e){if($this->db->trans_active())$this->db->trans_rollback();foreach($saved as$path)if(is_file($path))@unlink($path);return business_exception_response($e);}
|
||||
}
|
||||
|
||||
private function validatedEquipmentFiles()
|
||||
{
|
||||
if(empty($_FILES['attachments']))return array();$input=$_FILES['attachments'];$files=array();$count=is_array($input['name'])?count($input['name']):1;if($count>10)throw new BusinessException('Maksimal 10 lampiran per dokumen.');$allowed=array('application/pdf'=>'pdf','image/jpeg'=>'jpg','image/png'=>'png','image/webp'=>'webp','image/gif'=>'gif','image/bmp'=>'bmp','image/heic'=>'heic','image/heif'=>'heif','image/avif'=>'avif');
|
||||
for($i=0;$i<$count;$i++){$file=array('name'=>is_array($input['name'])?$input['name'][$i]:$input['name'],'tmp_name'=>is_array($input['tmp_name'])?$input['tmp_name'][$i]:$input['tmp_name'],'error'=>is_array($input['error'])?$input['error'][$i]:$input['error'],'size'=>is_array($input['size'])?$input['size'][$i]:$input['size']);if($file['error']===UPLOAD_ERR_NO_FILE)continue;if($file['error']!==UPLOAD_ERR_OK||!is_uploaded_file($file['tmp_name']))throw new BusinessException('Salah satu lampiran gagal diterima server.');if((int)$file['size']>5242880)throw new BusinessException('Ukuran tiap lampiran maksimal 5 MB.');$mime=(new finfo(FILEINFO_MIME_TYPE))->file($file['tmp_name']);if(!isset($allowed[$mime]))throw new BusinessException('Lampiran harus berupa gambar atau PDF yang valid.');$file['mime']=$mime;$file['extension']=$allowed[$mime];$files[]=$file;}
|
||||
return$files;
|
||||
}
|
||||
|
||||
private function storeEquipmentFile($documentId,array$file,$entityType='technician_equipment_document')
|
||||
{
|
||||
$dir=FCPATH.'uploads/accounting/';if(!is_dir($dir)&&!mkdir($dir,0750,true))throw new RuntimeException('Folder lampiran tidak tersedia.');$stored=bin2hex(random_bytes(20)).'.'.$file['extension'];$path=$dir.$stored;if(!move_uploaded_file($file['tmp_name'],$path))throw new RuntimeException('Lampiran gagal disimpan.');$row=array('module'=>'inventory','entity_type'=>$entityType,'entity_id'=>$documentId,'original_name'=>basename($file['name']),'stored_name'=>$stored,'mime_type'=>$file['mime'],'file_size'=>filesize($path),'sha256'=>hash_file('sha256',$path),'uploaded_by'=>(int)$this->session->userdata('user_id'),'created_at'=>date('Y-m-d H:i:s'));if($this->db->field_exists('company_id','transaction_attachments'))$row['company_id']=(int)$this->companycontext->id();$this->db->insert('transaction_attachments',$row);if(!$this->db->insert_id())throw new RuntimeException('Metadata lampiran gagal disimpan.');return$path;
|
||||
}
|
||||
|
||||
public function equipment_document_pdf($id=0)
|
||||
{
|
||||
$this->equipmentPermission('technician_equipment','can_export');$this->assertTechnicianDocumentScope((int)$id);
|
||||
try{$document=$this->technicianequipmentservice->document((int)$id);$company=$this->db->get_where('companies',array('id'=>(int)$this->companycontext->id()))->row();require_once FCPATH.'vendor/autoload.php';$html=$this->load->view('peralatan_teknisi/document_pdf',array('document'=>$document,'company'=>$company),true);$pdf=new Dompdf\Dompdf();$pdf->loadHtml($html);$pdf->setPaper('A4','portrait');$pdf->render();$pdf->stream($document['document_no'].'.pdf',array('Attachment'=>false));}
|
||||
catch(Throwable $e){show_error($e->getMessage(),422,'Dokumen tidak dapat dibuka');}
|
||||
}
|
||||
|
||||
public function technician_inventory_pdf($id=0)
|
||||
{
|
||||
$this->equipmentPermission('technician_equipment','can_export');$id=(int)$id;$restricted=$this->restrictedTechnicianEmployee();if($restricted&&$id!==$restricted)show_error('PDF bukan milik teknisi yang sedang login.',403,'Akses ditolak');$technician=$this->ptm->get_teknisi_info($id);if(!$technician)show_404();
|
||||
$status=trim((string)$this->input->get('status',true));$labels=array('carried'=>'Sedang Dibawa','installed'=>'Terpasang di Customer','removed'=>'Hasil Pelepasan','inspection'=>'Menunggu Pemeriksaan','all'=>'Semua Posisi');if(!isset($labels[$status]))$status='carried';
|
||||
$rows=$this->technicianequipmentservice->allocations(null,null,$id);if($status!=='all')$rows=array_values(array_filter($rows,function($row)use($status){if($status==='carried')return$row['custodian_type']==='technician'&&$row['usage_status']==='carried';if($status==='installed')return$row['custodian_type']==='customer'&&$row['usage_status']==='installed';if($status==='removed')return$row['usage_status']==='removed';return$status==='inspection'&&$row['condition_status']==='inspection_required';}));
|
||||
$company=$this->db->get_where('companies',array('id'=>(int)$this->companycontext->id()))->row();require_once FCPATH.'vendor/autoload.php';$html=$this->load->view('peralatan_teknisi/technician_inventory_pdf',array('technician'=>$technician,'rows'=>$rows,'company'=>$company,'printed_by'=>$this->session->userdata('nama'),'status_label'=>$labels[$status]),true);$pdf=new Dompdf\Dompdf();$pdf->loadHtml($html);$pdf->setPaper('A4','landscape');$pdf->render();$pdf->stream('Daftar-Peralatan-'.preg_replace('/[^A-Za-z0-9_-]+/','-',(string)$technician->full_name).'-'.$status.'-'.date('Ymd').'.pdf',array('Attachment'=>false));
|
||||
}
|
||||
|
||||
private function equipmentPermission($feature,$action)
|
||||
{
|
||||
if(is_master_admin_user())return true;if(check_permission($feature,$action))return true;$permissions=$this->session->userdata('permissions');if(is_array($permissions)&&array_key_exists($feature,$permissions))return$this->requirePermission($feature,$action);
|
||||
// Hanya session lama yang belum mengenal feature baru yang memakai fallback.
|
||||
if(check_permission('items',$action))return true;
|
||||
return$this->requirePermission($feature,$action);
|
||||
}
|
||||
|
||||
private function equipmentPermissionFlags()
|
||||
{
|
||||
$pairs=array('view'=>array('technician_equipment','can_view'),'attachment'=>array('technician_attachment','can_view'),'register'=>array('technician_equipment','can_create'),'handover'=>array('technician_handover','can_create'),'install'=>array('technician_installation','can_create'),'remove'=>array('technician_installation','can_update'),'transfer'=>array('technician_transfer','can_update'),'return'=>array('technician_return','can_update'),'inspect_view'=>array('technician_inspection','can_view'),'inspect'=>array('technician_inspection','can_post'),'condition'=>array('technician_condition','can_update'),'correct'=>array('technician_correction','can_update'),'export'=>array('technician_equipment','can_export'));$flags=array();$permissions=$this->session->userdata('permissions');foreach($pairs as$key=>$pair){$known=is_array($permissions)&&array_key_exists($pair[0],$permissions);$flags[$key]=is_master_admin_user()||check_permission($pair[0],$pair[1])||(!$known&&check_permission('items',$pair[1]));}return$flags;
|
||||
}
|
||||
|
||||
private function restrictedTechnicianEmployee()
|
||||
{
|
||||
if(is_master_admin_user()||check_permission('technician_equipment_all','can_view'))return 0;$role=strtolower((string)$this->session->userdata('role'));if(strpos($role,'teknisi')===false)return 0;if(!$this->db->field_exists('employee_id','users'))show_error('Migration tautan pengguna-karyawan belum dijalankan.',500,'Konfigurasi belum lengkap');$user=$this->db->select('employee_id')->get_where('users',array('id'=>(int)$this->session->userdata('user_id')))->row();if(!$user||(int)$user->employee_id<1)show_error('Akun teknisi belum ditautkan ke Data Karyawan. Hubungi administrator.',403,'Akses ditolak');return(int)$user->employee_id;
|
||||
}
|
||||
|
||||
private function scopedEquipmentDashboard()
|
||||
{
|
||||
$dashboard=$this->technicianequipmentservice->dashboard();$employee=$this->restrictedTechnicianEmployee();if(!$employee)return$dashboard;$allocations=$this->technicianequipmentservice->allocations(null,null,$employee);$summary=array('carried'=>0,'installed'=>0,'inspection'=>0,'repair'=>0,'damaged'=>0,'lost'=>0);foreach($allocations as$a){$qty=(float)$a['qty'];if($a['custodian_type']==='technician')$summary['carried']+=$qty;if($a['usage_status']==='installed')$summary['installed']+=$qty;if(in_array($a['condition_status'],array('repair_required','under_repair'),true))$summary['repair']+=$qty;if($a['condition_status']==='damaged')$summary['damaged']+=$qty;if($a['condition_status']==='lost')$summary['lost']+=$qty;}$inspectionStats=$this->technicianequipmentservice->inspectionQueueStats($employee);$summary['inspection']=(float)$inspectionStats['qty'];$dashboard['summary']=$summary;$dashboard['inspection_count']=(int)$inspectionStats['row_count'];$dashboard['technicians']=array_values(array_filter($dashboard['technicians'],function($row)use($employee){return(int)$row->id===$employee;}));return$dashboard;
|
||||
}
|
||||
|
||||
private function assertTechnicianActionScope(array$data,$action)
|
||||
{
|
||||
$employee=$this->restrictedTechnicianEmployee();if(!$employee)return true;if(in_array($action,array('handover','remove','replacement'),true)&&(int)($data['technician_id']??0)!==$employee)show_error('Teknisi hanya dapat memproses barang untuk dirinya sendiri.',403,'Akses ditolak');$allocationId=(int)($action==='replacement'?($data['outgoing_allocation_id']??0):($data['allocation_id']??0));if(!$allocationId)return true;$allocation=$this->db->select('subject_type,subject_id,custodian_type,custodian_id')->get_where('technician_custody_allocations',array('id'=>$allocationId,'company_id'=>(int)$this->companycontext->id()))->row();if(!$allocation)show_error('Posisi barang tidak ditemukan.',404,'Data tidak ditemukan');if(in_array($action,array('remove','inspection'),true))return$this->assertTechnicianSubjectScope($allocation->subject_type,(int)$allocation->subject_id,$employee);if($allocation->custodian_type!=='technician'||(int)$allocation->custodian_id!==$employee)show_error('Barang bukan berada dalam penguasaan teknisi yang sedang login.',403,'Akses ditolak');return true;
|
||||
}
|
||||
|
||||
private function assertTechnicianSubjectScope($subjectType,$subjectId,$employee)
|
||||
{
|
||||
$company=(int)$this->companycontext->id();$subjectId=(int)$subjectId;$employee=(int)$employee;
|
||||
$current=$this->db->get_where('technician_custody_allocations',array('company_id'=>$company,'subject_type'=>$subjectType,'subject_id'=>$subjectId,'custodian_type'=>'technician','custodian_id'=>$employee))->row();if($current)return true;
|
||||
$related=$this->db->select('l.id')->from('technician_equipment_document_lines l')->join('technician_equipment_documents d','d.id=l.document_id')->where(array('l.company_id'=>$company,'l.subject_type'=>$subjectType,'l.subject_id'=>$subjectId))->group_start()->where('d.technician_id',$employee)->or_where('d.target_technician_id',$employee)->group_end()->limit(1)->get()->row();if(!$related)show_error('Barang bukan bagian dari penguasaan atau riwayat teknisi yang sedang login.',403,'Akses ditolak');return true;
|
||||
}
|
||||
|
||||
private function assertTechnicianDocumentScope($id)
|
||||
{
|
||||
$employee=$this->restrictedTechnicianEmployee();if(!$employee)return true;$owned=$this->db->group_start()->where('technician_id',$employee)->or_where('target_technician_id',$employee)->group_end()->get_where('technician_equipment_documents',array('id'=>(int)$id,'company_id'=>(int)$this->companycontext->id()))->row();if(!$owned)show_error('Dokumen bukan bagian dari penguasaan teknisi yang sedang login.',403,'Akses ditolak');return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
|
||||
class Tenantschemacheck extends CI_Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
if (!$this->input->is_cli_request()) show_404();
|
||||
$tables = array(
|
||||
'accounts','journals','journal_details','accounting_periods','fiscal_years',
|
||||
'document_sequences','system_account_mappings','customers','invoices','payments',
|
||||
'suppliers','purchase_orders','supplier_invoices','supplier_payments','items',
|
||||
'warehouses','assets','cash_accounts','cash_transactions','k_employees',
|
||||
'k_payroll_periods','k_payrolls','tax_codes','tax_transactions','business_dimensions',
|
||||
'budgets','budget_lines','budget_revisions','saved_report_filters',
|
||||
'report_export_jobs','report_snapshots','closing_checklists','exchange_rates',
|
||||
'fx_revaluations','intercompany_transactions','user_companies'
|
||||
);
|
||||
$missingTables = array();
|
||||
$missingColumns = array();
|
||||
$nullRows = array();
|
||||
foreach ($tables as $table) {
|
||||
if (!$this->db->table_exists($table)) {$missingTables[]=$table;continue;}
|
||||
if (!$this->db->field_exists('company_id',$table)) {$missingColumns[]=$table;continue;}
|
||||
$count=(int)$this->db->where('company_id IS NULL',null,false)->count_all_results($table);
|
||||
if ($count) $nullRows[$table]=$count;
|
||||
}
|
||||
$result=array('missing_tables'=>$missingTables,'missing_company_id'=>$missingColumns,'null_company_rows'=>$nullRows,'status'=>!$missingTables&&!$missingColumns&&!$nullRows?'ok':'failed');
|
||||
echo json_encode($result,JSON_PRETTY_PRINT).PHP_EOL;
|
||||
if($result['status']!=='ok')exit(1);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
|
||||
class Users extends CI_Controller {
|
||||
class Users extends MY_Admin_Controller {
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
@@ -35,15 +35,17 @@ class Users extends CI_Controller {
|
||||
{
|
||||
$this->db->select('
|
||||
users.id,
|
||||
users.username,
|
||||
users.nama,
|
||||
roles.nama_role
|
||||
users.username,
|
||||
users.nama,
|
||||
roles.nama_role,
|
||||
employees.full_name AS employee_name
|
||||
');
|
||||
$this->db->from('users');
|
||||
$this->db->join('roles', 'roles.id = users.role_id', 'left');
|
||||
$this->db->from('users');
|
||||
$this->db->join('roles', 'roles.id = users.role_id', 'left');
|
||||
$this->db->join('k_employees employees','employees.id=users.employee_id','left');
|
||||
|
||||
$column_order = ['users.id', 'users.username', 'users.nama', 'roles.nama_role'];
|
||||
$column_search = ['users.username', 'users.nama', 'roles.nama_role'];
|
||||
$column_order = ['users.id', 'users.username', 'users.nama', 'roles.nama_role','employees.full_name'];
|
||||
$column_search = ['users.username', 'users.nama', 'roles.nama_role','employees.full_name'];
|
||||
$order = ['users.id' => 'DESC'];
|
||||
|
||||
$search_value = $this->input->post('search')['value'] ?? '';
|
||||
@@ -99,8 +101,9 @@ class Users extends CI_Controller {
|
||||
$row->id,
|
||||
$row->username,
|
||||
$row->nama,
|
||||
$row->nama_role,
|
||||
$action
|
||||
$row->nama_role,
|
||||
$row->employee_name ?: '-',
|
||||
$action
|
||||
];
|
||||
}
|
||||
|
||||
@@ -109,14 +112,16 @@ class Users extends CI_Controller {
|
||||
|
||||
private function _count_filtered($search_value)
|
||||
{
|
||||
$this->db->from('users');
|
||||
$this->db->join('roles', 'roles.id = users.role_id', 'left');
|
||||
$this->db->from('users');
|
||||
$this->db->join('roles', 'roles.id = users.role_id', 'left');
|
||||
$this->db->join('k_employees employees','employees.id=users.employee_id','left');
|
||||
|
||||
if (!empty($search_value)) {
|
||||
$this->db->group_start();
|
||||
$this->db->like('users.username', $search_value);
|
||||
$this->db->or_like('users.nama', $search_value);
|
||||
$this->db->or_like('roles.nama_role', $search_value);
|
||||
$this->db->or_like('roles.nama_role', $search_value);
|
||||
$this->db->or_like('employees.full_name', $search_value);
|
||||
$this->db->group_end();
|
||||
}
|
||||
|
||||
@@ -141,11 +146,12 @@ class Users extends CI_Controller {
|
||||
return;
|
||||
}
|
||||
|
||||
$data = [
|
||||
$data = [
|
||||
'username' => $this->input->post('username'),
|
||||
'nama' => $this->input->post('nama'),
|
||||
'role_id' => $this->input->post('role_id'),
|
||||
'password' => password_hash($this->input->post('password'), PASSWORD_DEFAULT)
|
||||
'password' => password_hash($this->input->post('password'), PASSWORD_DEFAULT),
|
||||
'employee_id' => $this->validatedEmployeeId($this->input->post('employee_id'))
|
||||
];
|
||||
|
||||
$this->dm->setTable('users')->insert($data);
|
||||
@@ -206,10 +212,11 @@ class Users extends CI_Controller {
|
||||
return;
|
||||
}
|
||||
|
||||
$data = [
|
||||
$data = [
|
||||
'username' => $this->input->post('username'),
|
||||
'nama' => $this->input->post('nama'),
|
||||
'role_id' => $this->input->post('role_id')
|
||||
'role_id' => $this->input->post('role_id'),
|
||||
'employee_id' => $this->validatedEmployeeId($this->input->post('employee_id'))
|
||||
];
|
||||
|
||||
if (!empty($this->input->post('password'))) {
|
||||
@@ -254,10 +261,20 @@ class Users extends CI_Controller {
|
||||
// =========================
|
||||
// GET ROLES
|
||||
// =========================
|
||||
public function get_role()
|
||||
public function get_role()
|
||||
{
|
||||
$roles = $this->db->get('roles')->result();
|
||||
$roles = $this->db->where('is_active',1)->order_by('nama_role','ASC')->get('roles')->result();
|
||||
|
||||
echo json_encode($roles);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function get_employee()
|
||||
{
|
||||
return json_response(true,'Daftar karyawan.',array('data'=>$this->db->select('id,employee_code,full_name')->where(array('company_id'=>(int)$this->companycontext->id(),'is_active'=>1))->order_by('full_name')->get('k_employees')->result()));
|
||||
}
|
||||
|
||||
private function validatedEmployeeId($value)
|
||||
{
|
||||
$id=(int)$value;if($id<1)return null;$row=$this->db->select('id')->where(array('id'=>$id,'company_id'=>(int)$this->companycontext->id(),'is_active'=>1))->get('k_employees')->row();return$row?$id:null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
|
||||
class Warehouses extends CI_Controller {
|
||||
class Warehouses extends MY_Controller {
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
@@ -14,10 +14,7 @@ class Warehouses extends CI_Controller {
|
||||
|
||||
public function index()
|
||||
{
|
||||
$data = ["active_menu" => "warehouses"];
|
||||
$this->load->view('partials/header', $data);
|
||||
$this->load->view('warehouses/layout');
|
||||
$this->load->view('partials/footer');
|
||||
redirect('inventoryprofessional/locations');
|
||||
}
|
||||
|
||||
// =========================
|
||||
@@ -25,7 +22,7 @@ class Warehouses extends CI_Controller {
|
||||
// =========================
|
||||
public function get_data()
|
||||
{
|
||||
$data = $this->db->get('warehouses')->result();
|
||||
$data = $this->db->where('company_id',(int)$this->session->userdata('company_id'))->get('warehouses')->result();
|
||||
|
||||
$result = [];
|
||||
$no = 1;
|
||||
@@ -34,10 +31,7 @@ class Warehouses extends CI_Controller {
|
||||
$result[] = [
|
||||
$no++,
|
||||
$row->nama,
|
||||
'
|
||||
<button class="btn btn-sm btn-secondary btn-editgudang" data-id="'.$row->id.'">Edit</button>
|
||||
<button class="btn btn-sm btn-danger btn-delete" data-id="'.$row->id.'">Delete</button>
|
||||
'
|
||||
($this->session->userdata('role')==='Admin'||check_permission('items','can_update'))?'<button class="btn btn-sm btn-secondary btn-editgudang" data-id="'.$row->id.'">Edit</button>':'-'
|
||||
];
|
||||
}
|
||||
|
||||
@@ -50,7 +44,7 @@ class Warehouses extends CI_Controller {
|
||||
public function detail($id)
|
||||
{
|
||||
echo json_encode(
|
||||
$this->db->get_where('warehouses',['id'=>$id])->row()
|
||||
$this->db->get_where('warehouses',['id'=>$id,'company_id'=>(int)$this->session->userdata('company_id')])->row()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -67,7 +61,10 @@ class Warehouses extends CI_Controller {
|
||||
}
|
||||
|
||||
$this->db->insert('warehouses', [
|
||||
'nama' => $nama
|
||||
'nama' => $nama,
|
||||
'company_id'=>(int)$this->session->userdata('company_id'),
|
||||
'code'=>'GD-'.strtoupper(substr(sha1(uniqid('',true)),0,6)),
|
||||
'is_active'=>1
|
||||
]);
|
||||
|
||||
log_activity(
|
||||
@@ -93,7 +90,7 @@ class Warehouses extends CI_Controller {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->db->where('id',$id)->update('warehouses', [
|
||||
$this->db->where(array('id'=>$id,'company_id'=>(int)$this->session->userdata('company_id')))->update('warehouses', [
|
||||
'nama' => $nama
|
||||
]);
|
||||
|
||||
@@ -112,34 +109,25 @@ class Warehouses extends CI_Controller {
|
||||
// =========================
|
||||
public function delete($id)
|
||||
{
|
||||
// optional: cek relasi ke items
|
||||
$cek = $this->db->get_where('items',['warehouse_id'=>$id])->num_rows();
|
||||
|
||||
if($cek > 0){
|
||||
echo json_encode([
|
||||
'status'=>false,
|
||||
'message'=>'Gudang tidak bisa dihapus karena masih dipakai item'
|
||||
]);
|
||||
return;
|
||||
if (strtoupper($this->input->method()) !== 'POST') {
|
||||
$this->output->set_status_header(405);
|
||||
} else {
|
||||
$this->output->set_status_header(409);
|
||||
}
|
||||
|
||||
$this->db->delete('warehouses',['id'=>$id]);
|
||||
|
||||
log_activity(
|
||||
'kode_barang',
|
||||
'delete',
|
||||
'Delete gudang '.$nama,
|
||||
'success'
|
||||
);
|
||||
|
||||
echo json_encode(['status'=>true,'message'=>'Gudang berhasil dihapus']);
|
||||
return $this->output
|
||||
->set_content_type('application/json')
|
||||
->set_output(json_encode(array(
|
||||
'status' => false,
|
||||
'message' => 'Gudang tidak dapat dihapus. Nonaktifkan melalui menu Gudang & Lokasi Rak.'
|
||||
)));
|
||||
}
|
||||
|
||||
|
||||
public function list()
|
||||
{
|
||||
echo json_encode(
|
||||
$this->db->get('warehouses')->result()
|
||||
$this->db->where(array('company_id'=>(int)$this->session->userdata('company_id'),'is_active'=>1))->order_by('nama')->get('warehouses')->result()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user