/************************* * * * * * * * * * * * * *************************** Copyright (c) 1999-2005 Ryan Bobko ryan@ostrich-emulators.com This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. ************************** * * * * * * * * * * * * **************************/ #include "transeditor.h" #include "qhacc.h" #include "qhacctable.h" #include "qhaccutils.h" #include "metadialog.h" #include "splitdialog.h" #include "guiconstants.h" #include "qhacclineedits.h" #include "transeditor.moc" #include #include #include #include #include #include #include using namespace std; //#include TransactionEditor::TransactionEditor( QHacc * eng, const Account& acct, const Journal& l, bool de, QWidget * p, const char * n ) : QFrame( p, n ){ std::ostream * str=0; if( Utils::debug( Utils::CURIOSITY, str ) ) *str<<"creating new transaction editor"<getBP( "GLOBALTYPEAHEAD" ); nocaselkp=engine->getBP( "AUTOCOMPNOCASE" ); autocomp=engine->getIP( "AUTOCOMPLETION" ); // transaction types num=new QHaccChoiceEdit( true, this ); num->setEmptyText( tr( "" ) ); num->ignoreCase( nocaselkp ); // date field date=new QHaccDateEdit( this ); // payee field payee=new QHaccChoiceEdit( true, this ); payee->setEmptyText( tr( "" ) ); payee->ignoreCase( nocaselkp ); makePayees(); // amount field--use qhaccchoiceedit to capture up/down arrows credit=new QHaccMoneyEdit( engine->converter(), this ); credit->setAlignment( AlignRight ); credit->setEmptyText( tr( GUIC::DEP ) ); debit=new QHaccMoneyEdit( engine->converter(), this ); debit->setAlignment( AlignRight ); debit->setEmptyText( tr( GUIC::WD ) ); // reconcile checkbox reco=new QCheckBox( this ); // memo field memo=new QHaccLineEdit( this ); memo->setEmptyText( tr( "" ) ); // double entry transaction if ( doubleEntry ){ deAccount=new QHaccChoiceEdit( false, this ); deAccount->setEmptyText( tr( GUIC::DEATEXT ) ); deAccount->ignoreCase( nocaselkp ); deAccount->setFrame( false ); connect( deAccount, SIGNAL( returnPressed() ), SLOT( conditionallyAccept() ) ); okay=new QPushButton( tr( "Enter" ), this ); connect( okay, SIGNAL( clicked() ), SLOT( conditionallyAccept() ) ); split=new QPushButton( tr( "Split" ), this ); connect( split, SIGNAL( clicked() ), SLOT( openSplitEditor() ) ); meta=new QPushButton( tr( "More" ), this ); connect( meta, SIGNAL( clicked() ), SLOT( openMetaEditor() ) ); popDEA(); connect( engine, SIGNAL( addedA( const Account& ) ), SLOT( popDEA() ) ); connect( engine, SIGNAL( removedA( const Account& ) ), SLOT( popDEA() ) ); // updatedA is handled by updateAccount, below } else setTabOrder( payee, memo ); QLineEdit * edits[]={ num, date, payee, memo, credit, debit }; for ( int i=0; i<6; i++ ){ connect( edits[i], SIGNAL( returnPressed() ), SLOT( conditionallyAccept() ) ); edits[i]->setFrame( false ); } connect( engine, SIGNAL( changedP( const QString&, bool ) ), SLOT( changeP( const QString&, bool ) ) ); connect( engine, SIGNAL( changedP( const QString&, int ) ), SLOT( changeP( const QString&, int ) ) ); connect( engine, SIGNAL( updatedA( const Account &, const Account& ) ), this, SLOT( updateAccount( const Account &, const Account& ) ) ); } TransactionEditor::~TransactionEditor(){ std::ostream * str=0; if( Utils::debug( Utils::CURIOSITY, str ) ) *str<<"destroying old transaction editor"<clear(); // we need to fill in the account names for the double entry lookup vector tgs; for( int i=0; igetBP( "USEANUMSFORNAMES" ); auto_ptr accts=engine->getAs( TableGet( tgs ) ); uint rr=accts->rows(); for( uint i=0; iinsertItem( AccountAreaFrame::displayAName( engine, accts->at( i ), useanums ) ); } } } void TransactionEditor::reLayout( QHeader * header, const QRect& r ){ int h=fontMetrics().height()+2; // don't squish the text fields QRect myr( r ); if( doubleEntry && myr.height()<2*h ){ // we're in double-entry mode, but we've been asked to open // an editor with less than one row of space, so double it myr.setHeight( myr.height()*2 ); } // resizing a little bigger gives a nice shadow effect myr.setBottom( myr.bottom()+( doubleEntry ? 3 : 1 ) ); setGeometry( myr ); // lay out the fields so they take up all the space available if( doubleEntry ){ int y=1; //h=h/2; QWidget * wdgs[]={ num, date, payee, credit, debit, reco }; int idx=0; for( int i=0; i<7; i++ ){ int x=header->sectionPos( i )+1; int w=header->sectionSize( i )-1; if( idx==2 ) w+=header->sectionSize( ++i ); wdgs[idx]->setGeometry( x, y, w, h ); idx++; } // go to line 2 y+=( h+1 ); int x=header->sectionPos( 0 )+1; int w=header->sectionSize( 0 )+header->sectionSize( 1 )-2; split->setGeometry( x, y, w, h ); x=header->sectionPos( 2 )+1; w=header->sectionSize( 2 )-1; deAccount->setGeometry( x, y, w, h ); x=header->sectionPos( 3 )+1; w=header->sectionSize( 3 )-1; memo->setGeometry( x, y, w, h ); x=header->sectionPos( 4 )+1; w=header->sectionSize( 4 )+header->sectionSize( 5 )-2; okay->setGeometry( x, y, w, h ); x=header->sectionPos( 6 )+1; w=header->sectionSize( 6 )-1; meta->setGeometry( x, y, w, h ); } else{ QWidget * wdgs[]={ num, date, payee, memo, credit, debit, reco }; int y=1; for( int i=0; i<7; i++ ){ int x=header->sectionPos( i )+1; int w=header->sectionSize( i )-1; wdgs[i]->setGeometry( x, y, w, h ); } } } void TransactionEditor::makePayees(){ uint rr=0; auto_ptr temp; if( globalTA ){ // get all unique payees vector v; auto_ptr t2=engine->getWhere( TRANSACTIONS, TableGet( QC::TPAYEE, TableGet::UQ ), v, rr ); temp=t2; //cout<<"global "<rows()< v( 1, TableSelect( QC::XSACCTID, account[QC::AID] ) ); auto_ptrt2=engine->getWhere( XTRANS, TableGet( QC::XTPAYEE, TableGet::UQ ), v, rr ); temp=t2; //cout<<"local "<rows()<insertItem( temp->at( i ).gets( 0 ) ); //cout<<"payees to insert:"<rows(); i++ ) cout<at( i ).gets( 0 )<setText( t.gets( QC::TNUM ) ); date->setDate( t.getd( QC::TDATE ) ); payee->setText( t.gets( QC::TPAYEE ) ); QStringList sts=QStringList::split( " ", account.gets( QC::ATRANSNUMS ), false ); num->clear(); num->insertStringList( sts ); num->insertItem( t[QC::TNUM].gets() ); Split s; if( edit ){ QHaccTable splits=engine->getTSplits( t.getu( QC::TID ) ); s=splits.getWhere( TableSelect( QC::SACCTID, account[QC::AID] ) ); // named trans might not have a valid split for this account if( s.isNull() ){ s=TableRow( QC::SCOLS ); metatax=account.getb( QC::ATAXED ); } else metatax=s[QC::STAXABLE].getb(); metavoid=t[QC::TVOID].getb(); named=( t[QC::TTYPE]==QC::MEMORIZED ); if( !named ) reco->setChecked( s[QC::SRECO]!=QC::NREC ); } else{ s=TableRow( QC::SCOLS ); named=false; metatax=account.getb( QC::ATAXED ); metavoid=false; } origT=model=engine->makeXTrans( t, s ); setModel( model ); if( connected ){ if( autocomp==QC::NEVER || ( autocomp==QC::NEW && edit ) ){ disconnect( payee, SIGNAL( textChanged( const QString& ) ), this, SLOT( fillInForPayee( const QString& ) ) ); disconnect( credit, SIGNAL( textChanged( const QString& ) ), this, SLOT( moveInCredit( const QString& ) ) ); disconnect( debit, SIGNAL( textChanged( const QString& ) ), this, SLOT( moveInDebit( const QString& ) ) ); connected=false; } } else{ if( autocomp==QC::ALL || ( autocomp==QC::NEW && !edit ) ){ connect( payee, SIGNAL( textChanged( const QString& ) ), this, SLOT( fillInForPayee( const QString& ) ) ); connect( credit, SIGNAL( textChanged( const QString& ) ), this, SLOT( moveInCredit( const QString& ) ) ); connect( debit, SIGNAL( textChanged( const QString& ) ), this, SLOT( moveInDebit( const QString& ) ) ); connected=true; } } if( named ) credit->setFocus(); else num->setFocus(); } void TransactionEditor::setModel( const Transaction& t, bool convit ){ // set the sum, memo, and de fields appropriately for the given transaction //cout<<"setting model to "<setText( t.gets( QC::XTMEMO ) ); QString summer=t.gets( QC::XSSUM ); //cout<<"summer is "<convert( summer )<<" or "<unconvert( summer )<blockSignals( true ); debit->blockSignals( true ); Split marks; Transaction junk; engine->splitXTrans( t, junk, marks ); QString shares, price; bool ismkt=Utils::isMarket( marks, &shares, &price ); const MonCon& conv=engine->converter(); if( convit ) summer=conv.convert( summer ); else model.set( QC::XSSUM, conv.convert( summer, Engine, Preference ) ); //cout<<"settled on "<setText( "" ); if( ismkt ) debit->setText( shares.mid( 1 )+"@"+price ); else debit->setText( summer.mid( 1 ) ); } else{ if( summer==QC::REMAINDERVAL ) credit->setText( "" ); else if( ismkt ) credit->setText( shares+"@"+price ); else credit->setText( summer ); debit->setText( "" ); } credit->blockSignals( false ); debit->blockSignals( false ); if( doubleEntry ){ tsplits->clear(); QHaccTable sss( engine->getTSplits( t.getu( QC::XTID ) ) ); tsplits->load( &sss ); deAccount->setText( getPNameFor( t.getu( QC::XTID ), t.getu( QC::XSACCTID ) ) ); } } QString TransactionEditor::getPNameFor( uint tid, uint notAID ){ // if split trans, find the pair's parent's name and full name QString pair=""; if( tid==0 ) return pair; QHaccTable splits=engine->getTSplits( tid ); splits.deleteWhere( TableSelect( QC::SACCTID, notAID ) ); uint rows=splits.rows(); if( rows==0 ) return pair; else if( rows>1 ) return tr( GUIC::SPLIT ); else return engine->getFNameOfA( splits[0][QC::SACCTID].getu() ); } void TransactionEditor::fillInForPayee( const QString& smodel ){ // we are given a payee string...find all the transactions that // have this payee, and use them for the auto-completion. // if we're looking globally, there are a few extra hoops // and if our current string begins with a string that failed // to return anything last time, skip the whole process if( closing ) return; // if we're closing this editor, don't do anything //cout<<"fillin is "< crit; crit.push_back( atp ); atrans.reset( new QHaccTable( QC::XCOLS, QC::XCOLTYPES, "atrans" ) ); if( globalTA ){ // if we're looking globally, make sure we remove splits that point // to this account (they'll be circular, and will fail the engine's // transaction sanity checks), but we want to include splits that // originate in this account, too. // get every transaction with this payee uint rr=0; auto_ptr rslt=engine->getWhere( XTRANS, crit, rr ); QHaccTableIndex sacctid( rslt.get(), QC::XSACCTID, CTUINT ); //cout<at( sacctid[i] ).toString()< badtids; for( uint i=spos; iat( sacctid[i] )[QC::XTID].getu() ); for( uint i=0; iat( sacctid[i] ); if( ( i>=spos && iadd( t ); } } else{ // get unique payees from only this account crit.push_back( sga ); uint rr=0; auto_ptrrslt=engine->getWhere( XTRANS, crit, rr ); *atrans+=*rslt; } apidx.reset( new QHaccTableIndex( atrans.get(), QC::XTDATE, CTDATE, QC::XSID, CTUINT ) ); /* cout<<"atrans ("<rows()<<") is"<rows(); i++ ) cout<<" "<at( apidx->at( i ) ).toString()<clear(); debit->clear(); const MonCon& conv=engine->converter(); uint rr=atrans->rows(); for( uint i=0; iat( apidx->at( i ) ); const QString& ssum=conv.convert( t[QC::XSSUM].gets() ); float fsum=t[QC::XSSUM].getf(); if( fsum<0 ) debit->insertItem( ssum.mid( 1 ) ); else credit->insertItem( ssum ); } if( rr>0 ) setModel( atrans->at( apidx->at( rr-1 ) ) ); else setModel( origT ); } void TransactionEditor::moveInDebit( const QString& newsum ){ // see if we can find our payment in our transaction table //cout<<"debit sum is "<converter(); //cout<convert( mysum )<<" or "<unconvert( mysum )<getWhere( TableSelect( QC::XSSUM, conv.convert( mysum, Preference, Engine ) ) ); if( !tr.isNull() ){ tr.set( QC::XSSUM, mysum ); setModel( tr, false ); } } void TransactionEditor::moveInCredit( const QString& newsum ){ // see if we can find our payment in our transaction table //cout<<"credit sum is "<convert( newsum )<<" or "<unconvert( newsum )<converter(); TableRow tr=atrans->getWhere( TableSelect( QC::XSSUM, conv.convert( newsum, Preference, Engine ) ) ); if( !tr.isNull() ){ tr.set( QC::XSSUM, newsum ); setModel( tr, false ); } } void TransactionEditor::conditionallyAccept(){ // make sure we have all the data we need // (valid accounts and the final set of adjustments) // and if we do, call accept bool good=true; bool ismkt=false; QString mkttxt; // we'll need these for market transactions // don't convert the values at this point, just the currency separator //if(!credit->text().isEmpty()) cout<<"c="<text()<text().isEmpty()) cout<<"d="<text()<converter(); int cr=conv.converti( credit->text(), PrefSep, EngSep ); int db=conv.converti( debit->text(), PrefSep, EngSep ); //cout<<"cr="<text()+")"; if( Utils::isMarket( mkttxt, shares, price ) ){ ismkt=true; // we have a market transactions, but it is still localized by the user // we need to extract the values and multiply them together before we // can worry about converting the results QString str=QString::number( shares.toFloat()*price.toFloat(), 'f', QC::DECIMALS ); cr+=( conv.converti( str, PrefSep, EngSep ) ); } else{ mkttxt="market(-"+debit->text()+")"; if( Utils::isMarket( mkttxt, shares, price ) ){ ismkt=true; // we need to flip the shares sign, because // we added a - for the isMarket check QString str=QString::number( -( shares.toFloat() )*price.toFloat(), 'f', QC::DECIMALS ); db+=( conv.converti( str, PrefSep, EngSep ) ); } } } // if we still don't have a good value, something is wrong if( cr+db==0 && !metavoid ) return; if( doubleEntry ){ // check that the deAccount field is filled in const QString det=deAccount->text(); //if( det.isEmpty() ) return; if( det!=tr( GUIC::SPLIT ) ){ Account acct; good=AccountAreaFrame::verifyOrMakeA( engine, det, acct ); if( good ){ Split s( QC::SCOLS ); // we'll do the MonCon conversion in accept() s.set( QC::SSUM, TableCol( conv.convert( db-cr, Engine, Engine ) ) ); s.set( QC::SACCTID, acct[QC::AID] ); s.set( QC::STAXABLE, acct[QC::ATAXED] ); //cout<<"tsplits"<rows(); i++ ) //cout<<" "<at( i ).toString()<clear(); tsplits->add( s ); } } } if( good ){ // we're good to go, so make sure we have the split for THIS account // delete the "any" account and the current account from our // splits before adding a new row for our current account tsplits->deleteWhere( TableSelect( QC::SACCTID, TableCol( 0 ) ) ); tsplits->deleteWhere( TableSelect( QC::SACCTID, account[QC::AID] ) ); Split s( QC::SCOLS ); s.set( QC::SSUM, TableCol( conv.convert( cr-db, Engine, Engine ) ) ); s.set( QC::SACCTID, account[QC::AID] ); s.set( QC::STAXABLE, metatax ); if( ismkt ) s.set( QC::SMETA, mkttxt ); // if the reco box is checked, set the reconcile flag accordingly if( reco->isChecked() ) s.set( QC::SRECO, TableCol( recstate ) ); else s.set( QC::SRECO, TableCol( QC::NREC ) ); // else unset it tsplits->add( s ); // make sure if the user is in the date field, we commit any changes date->setDate( date->getValidDateFromString( date->text() ) ); const int CS[]={ QC::XTNUM, QC::XTPAYEE, QC::XTMEMO }; const QHaccLineEdit * WS[]={ num, payee, memo }; for( uint i=0; i<3; i++ ) model.set( CS[i], TableCol( WS[i]->text() ) ); model.set( QC::XTDATE, TableCol( date->getDate() ) ); accept(); } else{ std::ostream * str=0; if( Utils::error( Utils::ERROPER, str ) ) *str<<"transaction not acceptable"<rows(); i++ ) cout<<" "<at( i ).toString()<splitXTrans( model ); t.set( QC::TLID, journal[QC::LID] ); t.set( QC::TTYPE, TableCol( QC::REGULAR ) ); t.set( QC::TVOID, metavoid ); const MonCon& conv=engine->converter(); // go through the splits and unconvert all the sums QHaccTable t2( QC::SCOLS, QC::SCOLTYPES ); for( uint i=0; irows(); i++ ){ Split s=tsplits->at( i ); const QString& sum=s[QC::SSUM].gets(); if( !( sum==QC::REMAINDERVAL || sum.find( "%" )>-1 || sum=="p" || sum=="i" ) ){ // we've already converted the currency separator, but we // still need to convert the values (if they are values) s.set( QC::SSUM, conv.convert( sum, PrefVal, EngVal ) ); } t2+=s; } if( edit && !named ) engine->updateT( t, t2 ); else engine->addT( t, t2 ); payee->insertItem( payee->text() ); // include in type-aheads } void TransactionEditor::closeEvent( QCloseEvent * c ){ closing=true; emit closed(); QFrame::closeEvent( c ); } void TransactionEditor::openSplitEditor(){ // delete this account's split tsplits->deleteWhere( TableSelect( QC::SACCTID, model[QC::XSACCTID] ) ); tsplits->deleteWhere( TableSelect( QC::SACCTID, TableCol( 0 ) ) ); const MonCon& conv=engine->converter(); /* Split me=engine->getBlankRow( SPLITS ); int cr=engine->convMoney( credit->text() ); int db=engine->convMoney( debit->text() ); me.set( QC::SACCTID, account.get( QC::AID ) ); me.set( QC::SSUM, TableCol( engine->convMoney( cr-db ) ) ); tsplits->add( me ); */ SplitEditor se( engine, *tsplits, this ); if( se.exec() ){ // we'll get a valid table of splits QHaccTable splits=se.getSplits(); uint rows=splits.rows(); tsplits->clear(); tsplits->load( &splits ); int sum=0; for( uint i=0; igetA( splits[i][QC::SACCTID].getu() ); if( Utils::isLoan( aa, 0, &sstr ) ) sum+=conv.converti( sstr, Engine, Engine ); } else if( sstr!="i" ) sum+=conv.converti( sstr, Engine, Engine ); } // if the splits' sum is positive, // this transaction's sum must be negative, and vice versa if( sum>=0 ){ debit->setText( conv.convert( sum ) ); credit->setText( "" ); } else{ sum=0-sum; debit->setText( "" ); credit->setText( conv.convert( sum ) ); } QString det; if( rows>1 ) det=tr( GUIC::SPLIT ); else{ // not a split transaction Split s=tsplits->getWhere( TableSelect( QC::SACCTID, account[QC::AID], TableSelect::NE ) ); if( !s.isNull() ){ det=engine->getFNameOfA( engine->getA( s[QC::SACCTID].getu() ) ); } } deAccount->setText( det ); } else setModel( model ); } void TransactionEditor::openMetaEditor(){ MetaDialog md( metatax, metavoid, this ); if( md.exec() ){ metavoid=md.isVoid(); metatax=md.isTaxable(); } } void TransactionEditor::keyPressEvent( QKeyEvent * qke ){ if( qke->key()==Key_Escape ){ qke->accept(); close(); } } void TransactionEditor::setRecButton( bool b ){ reco->setChecked( b ); } void TransactionEditor::setRecMode( uint rec ){ // if the transaction is accepted with the reconcile button pushed, // this is the level of reconciliation to give it recstate=rec; } void TransactionEditor::changeP( const QString& pref, int v ){ if( pref=="AUTOCOMPLETION" ) autocomp=v; } void TransactionEditor::changeP( const QString& pref, bool b ){ if( pref=="AUTOCOMPNOCASE" ){ nocaselkp=b; payee->ignoreCase( b ); if( deAccount ) deAccount->ignoreCase( b ); } else if( pref=="GLOBALTYPEAHEAD" ) globalTA=b; } void TransactionEditor::updateAccount( const Account&, const Account& newy ){ if( newy[QC::AID]==account[QC::AID] ){ account=newy; const QString str( num->text() ); QStringList sts=QStringList::split( " ", account.gets( QC::ATRANSNUMS ), false ); num->clear(); num->insertStringList( sts ); num->insertItem( model[QC::TNUM].gets() ); num->setText( str ); } else popDEA(); }