2019-07-03 05:04:31 +00:00
frappe . provide ( "erpnext.accounts" ) ;
frappe . pages [ 'bank-reconciliation' ] . on _page _load = function ( wrapper ) {
new erpnext . accounts . bankReconciliation ( wrapper ) ;
}
erpnext . accounts . bankReconciliation = class BankReconciliation {
constructor ( wrapper ) {
this . page = frappe . ui . make _app _page ( {
parent : wrapper ,
title : _ _ ( "Bank Reconciliation" ) ,
single _column : true
} ) ;
this . parent = wrapper ;
this . page = this . parent . page ;
this . check _plaid _status ( ) ;
this . make ( ) ;
}
make ( ) {
const me = this ;
me . $main _section = $ ( ` <div class="reconciliation page-main-content"></div> ` ) . appendTo ( me . page . main ) ;
const empty _state = _ _ ( "Upload a bank statement, link or reconcile a bank account" )
me . $main _section . append ( ` <div class="flex justify-center align-center text-muted"
style = "height: 50vh; display: flex;" > < h5 class = "text-muted" > $ { empty _state } < / h 5 > < / d i v > ` )
me . page . add _field ( {
fieldtype : 'Link' ,
label : _ _ ( 'Company' ) ,
fieldname : 'company' ,
options : "Company" ,
onchange : function ( ) {
if ( this . value ) {
me . company = this . value ;
} else {
me . company = null ;
me . bank _account = null ;
}
}
} )
me . page . add _field ( {
fieldtype : 'Link' ,
label : _ _ ( 'Bank Account' ) ,
fieldname : 'bank_account' ,
options : "Bank Account" ,
get _query : function ( ) {
if ( ! me . company ) {
frappe . throw ( _ _ ( "Please select company first" ) ) ;
return
}
return {
filters : {
"company" : me . company
}
}
} ,
onchange : function ( ) {
if ( this . value ) {
me . bank _account = this . value ;
me . add _actions ( ) ;
} else {
me . bank _account = null ;
me . page . hide _actions _menu ( ) ;
}
}
} )
}
check _plaid _status ( ) {
const me = this ;
frappe . db . get _value ( "Plaid Settings" , "Plaid Settings" , "enabled" , ( r ) => {
2020-09-21 07:44:07 +00:00
if ( r && r . enabled === "1" ) {
2019-07-03 05:04:31 +00:00
me . plaid _status = "active"
} else {
me . plaid _status = "inactive"
}
} )
}
add _actions ( ) {
const me = this ;
me . page . show _menu ( )
me . page . add _menu _item ( _ _ ( "Upload a statement" ) , function ( ) {
me . clear _page _content ( ) ;
new erpnext . accounts . bankTransactionUpload ( me ) ;
} , true )
if ( me . plaid _status === "active" ) {
me . page . add _menu _item ( _ _ ( "Synchronize this account" ) , function ( ) {
me . clear _page _content ( ) ;
new erpnext . accounts . bankTransactionSync ( me ) ;
} , true )
}
me . page . add _menu _item ( _ _ ( "Reconcile this account" ) , function ( ) {
me . clear _page _content ( ) ;
me . make _reconciliation _tool ( ) ;
} , true )
}
clear _page _content ( ) {
const me = this ;
$ ( me . page . body ) . find ( '.frappe-list' ) . remove ( ) ;
me . $main _section . empty ( ) ;
}
make _reconciliation _tool ( ) {
const me = this ;
frappe . model . with _doctype ( "Bank Transaction" , ( ) => {
erpnext . accounts . ReconciliationList = new erpnext . accounts . ReconciliationTool ( {
parent : me . parent ,
doctype : "Bank Transaction"
} ) ;
} )
}
}
erpnext . accounts . bankTransactionUpload = class bankTransactionUpload {
constructor ( parent ) {
this . parent = parent ;
this . data = [ ] ;
const assets = [
"/assets/frappe/css/frappe-datatable.css" ,
"/assets/frappe/js/lib/clusterize.min.js" ,
"/assets/frappe/js/lib/Sortable.min.js" ,
"/assets/frappe/js/lib/frappe-datatable.js"
] ;
frappe . require ( assets , ( ) => {
this . make ( ) ;
} ) ;
}
make ( ) {
2020-09-21 07:44:07 +00:00
const me = this ;
2019-11-06 13:34:54 +00:00
new frappe . ui . FileUploader ( {
method : 'erpnext.accounts.doctype.bank_transaction.bank_transaction_upload.upload_bank_statement' ,
allow _multiple : 0 ,
on _success : function ( attachment , r ) {
2019-07-03 05:04:31 +00:00
if ( ! r . exc && r . message ) {
me . data = r . message ;
me . setup _transactions _dom ( ) ;
me . create _datatable ( ) ;
me . add _primary _action ( ) ;
}
}
} )
}
setup _transactions _dom ( ) {
const me = this ;
me . parent . $main _section . append ( ` <div class="transactions-table"></div> ` )
}
create _datatable ( ) {
try {
this . datatable = new DataTable ( '.transactions-table' , {
columns : this . data . columns ,
data : this . data . data
} )
}
catch ( err ) {
let msg = _ _ ( ` Your file could not be processed by ERPNext.
< br > It should be a standard CSV or XLSX file .
< br > The headers should be in the first row . ` )
frappe . throw ( msg )
}
}
add _primary _action ( ) {
const me = this ;
me . parent . page . set _primary _action ( _ _ ( "Submit" ) , function ( ) {
me . add _bank _entries ( )
} , null , _ _ ( "Creating bank entries..." ) )
}
add _bank _entries ( ) {
const me = this ;
frappe . xcall ( 'erpnext.accounts.doctype.bank_transaction.bank_transaction_upload.create_bank_entries' ,
{ columns : this . datatable . datamanager . columns , data : this . datatable . datamanager . data , bank _account : me . parent . bank _account }
) . then ( ( result ) => {
let result _title = result . errors == 0 ? _ _ ( "{0} bank transaction(s) created" , [ result . success ] ) : _ _ ( "{0} bank transaction(s) created and {1} errors" , [ result . success , result . errors ] )
let result _msg = `
< div class = "flex justify-center align-center text-muted" style = "height: 50vh; display: flex;" >
< h5 class = "text-muted" > $ { result _title } < / h 5 >
< / d i v > `
me . parent . page . clear _primary _action ( ) ;
me . parent . $main _section . empty ( ) ;
me . parent . $main _section . append ( result _msg ) ;
if ( result . errors == 0 ) {
frappe . show _alert ( { message : _ _ ( "All bank transactions have been created" ) , indicator : 'green' } ) ;
} else {
frappe . show _alert ( { message : _ _ ( "Please check the error log for details about the import errors" ) , indicator : 'red' } ) ;
}
} )
}
}
erpnext . accounts . bankTransactionSync = class bankTransactionSync {
constructor ( parent ) {
this . parent = parent ;
this . data = [ ] ;
this . init _config ( )
}
init _config ( ) {
const me = this ;
2020-09-21 07:44:07 +00:00
frappe . xcall ( 'erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.get_plaid_configuration' )
. then ( result => {
me . plaid _env = result . plaid _env ;
me . client _name = result . client _name ;
me . link _token = result . link _token ;
me . sync _transactions ( ) ;
} )
2019-07-03 05:04:31 +00:00
}
sync _transactions ( ) {
const me = this ;
2020-09-21 07:44:07 +00:00
frappe . db . get _value ( "Bank Account" , me . parent . bank _account , "bank" , ( r ) => {
2019-07-03 05:04:31 +00:00
frappe . xcall ( 'erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.sync_transactions' , {
2020-09-21 07:44:07 +00:00
bank : r . bank ,
2019-07-03 05:04:31 +00:00
bank _account : me . parent . bank _account ,
freeze : true
} )
. then ( ( result ) => {
2020-09-21 07:44:07 +00:00
let result _title = ( result && result . length > 0 )
? _ _ ( "{0} bank transaction(s) created" , [ result . length ] )
: _ _ ( "This bank account is already synchronized" ) ;
2019-07-03 05:04:31 +00:00
let result _msg = `
2020-09-21 07:44:07 +00:00
< div class = "flex justify-center align-center text-muted" style = "height: 50vh; display: flex;" >
< h5 class = "text-muted" > $ { result _title } < / h 5 >
< / d i v > `
2019-07-03 05:04:31 +00:00
this . parent . $main _section . append ( result _msg )
2020-09-21 07:44:07 +00:00
frappe . show _alert ( { message : _ _ ( "Bank account '{0}' has been synchronized" , [ me . parent . bank _account ] ) , indicator : 'green' } ) ;
2019-07-03 05:04:31 +00:00
} )
} )
}
}
erpnext . accounts . ReconciliationTool = class ReconciliationTool extends frappe . views . BaseList {
constructor ( opts ) {
super ( opts ) ;
this . show ( ) ;
}
setup _defaults ( ) {
super . setup _defaults ( ) ;
this . page _title = _ _ ( "Bank Reconciliation" ) ;
this . doctype = 'Bank Transaction' ;
this . fields = [ 'date' , 'description' , 'debit' , 'credit' , 'currency' ]
}
setup _view ( ) {
this . render _header ( ) ;
}
setup _side _bar ( ) {
//
}
make _standard _filters ( ) {
//
}
freeze ( ) {
this . $result . find ( '.list-count' ) . html ( ` <span> ${ _ _ ( 'Refreshing' ) } ...</span> ` ) ;
}
get _args ( ) {
const args = super . get _args ( ) ;
return Object . assign ( { } , args , {
... args . filters . push ( [ "Bank Transaction" , "docstatus" , "=" , 1 ] ,
[ "Bank Transaction" , "unallocated_amount" , ">" , 0 ] )
} ) ;
}
update _data ( r ) {
let data = r . message || [ ] ;
if ( this . start === 0 ) {
this . data = data ;
} else {
this . data = this . data . concat ( data ) ;
}
}
render ( ) {
const me = this ;
this . $result . find ( '.list-row-container' ) . remove ( ) ;
$ ( '[data-fieldname="name"]' ) . remove ( ) ;
me . data . map ( ( value ) => {
const row = $ ( '<div class="list-row-container">' ) . data ( "data" , value ) . appendTo ( me . $result ) . get ( 0 ) ;
new erpnext . accounts . ReconciliationRow ( row , value ) ;
} )
}
render _header ( ) {
const me = this ;
if ( $ ( this . wrapper ) . find ( '.transaction-header' ) . length === 0 ) {
me . $result . append ( frappe . render _template ( "bank_transaction_header" ) ) ;
}
}
}
erpnext . accounts . ReconciliationRow = class ReconciliationRow {
constructor ( row , data ) {
this . data = data ;
this . row = row ;
this . make ( ) ;
this . bind _events ( ) ;
}
make ( ) {
$ ( this . row ) . append ( frappe . render _template ( "bank_transaction_row" , this . data ) )
}
bind _events ( ) {
const me = this ;
$ ( me . row ) . on ( 'click' , '.clickable-section' , function ( ) {
me . bank _entry = $ ( this ) . attr ( "data-name" ) ;
me . show _dialog ( $ ( this ) . attr ( "data-name" ) ) ;
} )
$ ( me . row ) . on ( 'click' , '.new-reconciliation' , function ( ) {
me . bank _entry = $ ( this ) . attr ( "data-name" ) ;
me . show _dialog ( $ ( this ) . attr ( "data-name" ) ) ;
} )
$ ( me . row ) . on ( 'click' , '.new-payment' , function ( ) {
me . bank _entry = $ ( this ) . attr ( "data-name" ) ;
me . new _payment ( ) ;
} )
$ ( me . row ) . on ( 'click' , '.new-invoice' , function ( ) {
me . bank _entry = $ ( this ) . attr ( "data-name" ) ;
me . new _invoice ( ) ;
} )
$ ( me . row ) . on ( 'click' , '.new-expense' , function ( ) {
me . bank _entry = $ ( this ) . attr ( "data-name" ) ;
me . new _expense ( ) ;
} )
}
new _payment ( ) {
const me = this ;
const paid _amount = me . data . credit > 0 ? me . data . credit : me . data . debit ;
const payment _type = me . data . credit > 0 ? "Receive" : "Pay" ;
const party _type = me . data . credit > 0 ? "Customer" : "Supplier" ;
frappe . new _doc ( "Payment Entry" , { "payment_type" : payment _type , "paid_amount" : paid _amount ,
"party_type" : party _type , "paid_from" : me . data . bank _account } )
}
new _invoice ( ) {
const me = this ;
const invoice _type = me . data . credit > 0 ? "Sales Invoice" : "Purchase Invoice" ;
frappe . new _doc ( invoice _type )
}
new _expense ( ) {
frappe . new _doc ( "Expense Claim" )
}
show _dialog ( data ) {
const me = this ;
frappe . db . get _value ( "Bank Account" , me . data . bank _account , "account" , ( r ) => {
me . gl _account = r . account ;
} )
frappe . xcall ( 'erpnext.accounts.page.bank_reconciliation.bank_reconciliation.get_linked_payments' ,
2020-09-21 07:44:07 +00:00
{ bank _transaction : data , freeze : true , freeze _message : _ _ ( "Finding linked payments" ) }
2019-07-03 05:04:31 +00:00
) . then ( ( result ) => {
me . make _dialog ( result )
} )
}
make _dialog ( data ) {
const me = this ;
me . selected _payment = null ;
const fields = [
{
fieldtype : 'Section Break' ,
fieldname : 'section_break_1' ,
label : _ _ ( 'Automatic Reconciliation' )
} ,
{
fieldtype : 'HTML' ,
fieldname : 'payment_proposals'
} ,
{
fieldtype : 'Section Break' ,
fieldname : 'section_break_2' ,
label : _ _ ( 'Search for a payment' )
} ,
{
fieldtype : 'Link' ,
fieldname : 'payment_doctype' ,
options : 'DocType' ,
label : 'Payment DocType' ,
get _query : ( ) => {
return {
filters : {
"name" : [ "in" , [ "Payment Entry" , "Journal Entry" , "Sales Invoice" , "Purchase Invoice" , "Expense Claim" ] ]
}
}
} ,
} ,
{
fieldtype : 'Column Break' ,
fieldname : 'column_break_1' ,
} ,
{
fieldtype : 'Dynamic Link' ,
fieldname : 'payment_entry' ,
options : 'payment_doctype' ,
label : 'Payment Document' ,
get _query : ( ) => {
let dt = this . dialog . fields _dict . payment _doctype . value ;
if ( dt === "Payment Entry" ) {
return {
query : "erpnext.accounts.page.bank_reconciliation.bank_reconciliation.payment_entry_query" ,
filters : {
"bank_account" : this . data . bank _account ,
"company" : this . data . company
}
}
} else if ( dt === "Journal Entry" ) {
return {
query : "erpnext.accounts.page.bank_reconciliation.bank_reconciliation.journal_entry_query" ,
filters : {
"bank_account" : this . data . bank _account ,
"company" : this . data . company
}
}
} else if ( dt === "Sales Invoice" ) {
return {
query : "erpnext.accounts.page.bank_reconciliation.bank_reconciliation.sales_invoices_query"
}
} else if ( dt === "Purchase Invoice" ) {
return {
filters : [
[ "Purchase Invoice" , "ifnull(clearance_date, '')" , "=" , "" ] ,
[ "Purchase Invoice" , "docstatus" , "=" , 1 ] ,
[ "Purchase Invoice" , "company" , "=" , this . data . company ]
]
}
} else if ( dt === "Expense Claim" ) {
return {
filters : [
[ "Expense Claim" , "ifnull(clearance_date, '')" , "=" , "" ] ,
[ "Expense Claim" , "docstatus" , "=" , 1 ] ,
[ "Expense Claim" , "company" , "=" , this . data . company ]
]
}
}
} ,
onchange : function ( ) {
if ( me . selected _payment !== this . value ) {
me . selected _payment = this . value ;
me . display _payment _details ( this ) ;
}
}
} ,
{
fieldtype : 'Section Break' ,
fieldname : 'section_break_3'
} ,
{
fieldtype : 'HTML' ,
fieldname : 'payment_details'
} ,
] ;
me . dialog = new frappe . ui . Dialog ( {
title : _ _ ( "Choose a corresponding payment" ) ,
fields : fields ,
size : "large"
} ) ;
const proposals _wrapper = me . dialog . fields _dict . payment _proposals . $wrapper ;
if ( data && data . length > 0 ) {
proposals _wrapper . append ( frappe . render _template ( "linked_payment_header" ) ) ;
data . map ( value => {
proposals _wrapper . append ( frappe . render _template ( "linked_payment_row" , value ) )
} )
} else {
const empty _data _msg = _ _ ( "ERPNext could not find any matching payment entry" )
proposals _wrapper . append ( ` <div class="text-center"><h5 class="text-muted"> ${ empty _data _msg } </h5></div> ` )
}
$ ( me . dialog . body ) . on ( 'click' , '.reconciliation-btn' , ( e ) => {
const payment _entry = $ ( e . target ) . attr ( 'data-name' ) ;
const payment _doctype = $ ( e . target ) . attr ( 'data-doctype' ) ;
frappe . xcall ( 'erpnext.accounts.page.bank_reconciliation.bank_reconciliation.reconcile' ,
{ bank _transaction : me . bank _entry , payment _doctype : payment _doctype , payment _name : payment _entry } )
. then ( ( result ) => {
setTimeout ( function ( ) {
erpnext . accounts . ReconciliationList . refresh ( ) ;
} , 2000 ) ;
me . dialog . hide ( ) ;
} )
} )
me . dialog . show ( ) ;
}
display _payment _details ( event ) {
const me = this ;
if ( event . value ) {
let dt = me . dialog . fields _dict . payment _doctype . value ;
me . dialog . fields _dict [ 'payment_details' ] . $wrapper . empty ( ) ;
frappe . db . get _doc ( dt , event . value )
. then ( doc => {
let displayed _docs = [ ]
2019-11-11 11:59:53 +00:00
let payment = [ ]
2019-07-03 05:04:31 +00:00
if ( dt === "Payment Entry" ) {
payment . currency = doc . payment _type == "Receive" ? doc . paid _to _account _currency : doc . paid _from _account _currency ;
payment . doctype = dt
2019-11-11 11:59:53 +00:00
payment . posting _date = doc . posting _date ;
payment . party = doc . party ;
payment . reference _no = doc . reference _no ;
payment . reference _date = doc . reference _date ;
payment . paid _amount = doc . paid _amount ;
payment . name = doc . name ;
2019-07-03 05:04:31 +00:00
displayed _docs . push ( payment ) ;
} else if ( dt === "Journal Entry" ) {
doc . accounts . forEach ( payment => {
if ( payment . account === me . gl _account ) {
payment . doctype = dt ;
payment . posting _date = doc . posting _date ;
payment . party = doc . pay _to _recd _from ;
payment . reference _no = doc . cheque _no ;
payment . reference _date = doc . cheque _date ;
payment . currency = payment . account _currency ;
payment . paid _amount = payment . credit > 0 ? payment . credit : payment . debit ;
payment . name = doc . name ;
displayed _docs . push ( payment ) ;
}
} )
} else if ( dt === "Sales Invoice" ) {
doc . payments . forEach ( payment => {
if ( payment . clearance _date === null || payment . clearance _date === "" ) {
payment . doctype = dt ;
payment . posting _date = doc . posting _date ;
payment . party = doc . customer ;
payment . reference _no = doc . remarks ;
payment . currency = doc . currency ;
payment . paid _amount = payment . amount ;
payment . name = doc . name ;
displayed _docs . push ( payment ) ;
}
} )
}
const details _wrapper = me . dialog . fields _dict . payment _details . $wrapper ;
details _wrapper . append ( frappe . render _template ( "linked_payment_header" ) ) ;
2019-11-11 11:59:53 +00:00
displayed _docs . forEach ( payment => {
details _wrapper . append ( frappe . render _template ( "linked_payment_row" , payment ) ) ;
2019-07-03 05:04:31 +00:00
} )
} )
}
}
2019-11-06 13:34:54 +00:00
}