├── .codeclimate.yml ├── .gitignore ├── .travis.yml ├── LICENSE.md ├── README.md ├── build.sh ├── composer.json ├── createmariadb.sh ├── createmysqldb.sh ├── docs └── Test-Contract.md ├── doctrine-db.php ├── doctrine-migrations.xml ├── easy-coding-standard.yml ├── examples ├── currency-example.php └── journal-entries-example.php ├── phpunit.travis.xml ├── scripts ├── oqgraph.sql ├── test-user.sql ├── travis.install.mariadb.sh └── travis.install.mysql.sh ├── sqltest.sh ├── src ├── php │ └── SAccounts │ │ ├── Account.php │ │ ├── AccountType.php │ │ ├── Accountant.php │ │ ├── AccountsException.php │ │ ├── Chart.php │ │ ├── ChartDefinition.php │ │ ├── DbException.php │ │ ├── Doctrine │ │ └── Version20180428092520.php │ │ ├── Nominal.php │ │ ├── Transaction │ │ ├── Entries.php │ │ ├── Entry.php │ │ ├── SimpleTransaction.php │ │ └── SplitTransaction.php │ │ ├── Visitor │ │ ├── ChartArray.php │ │ ├── ChartPrinter.php │ │ ├── NodeFinder.php │ │ └── NodeSaver.php │ │ └── Zend │ │ └── ErrorHandler.php ├── sql │ ├── mariadb │ │ ├── build-procs.sql │ │ ├── build-tables.sql │ │ ├── build-triggers.sql │ │ ├── drop-procs.sql │ │ └── drop-tables.sql │ └── mysql │ │ ├── build-procs.sql │ │ ├── build-tables.sql │ │ ├── build-triggers.sql │ │ ├── drop-procs.sql │ │ └── drop-tables.sql ├── xml │ └── personal.xml └── xsd │ └── chart-definition.xsd └── test ├── php ├── .gitignore ├── SAccounts │ ├── AccountTest.php │ ├── AccountTypeTest.php │ ├── AccountantTest.php │ ├── ChartDefinitionTest.php │ ├── ChartTest.php │ ├── Transaction │ │ ├── EntriesTest.php │ │ ├── EntryTest.php │ │ ├── SimpleTransactionTest.php │ │ └── SplitTransactionTest.php │ └── Visitor │ │ ├── ChartArrayTest.php │ │ └── ChartPrinterTest.php └── phpunit.xml └── sql ├── add_account_test.sql ├── add_transaction_test.sql └── delete_account_test.sql /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | # Save as .codeclimate.yml (note leading .) in project root directory 2 | languages: 3 | Ruby: false 4 | JavaScript: false 5 | PHP: true 6 | Python: false 7 | exclude_paths: 8 | - "test/*" 9 | - "docs/*" 10 | - "examples/*" 11 | - "scripts/*" 12 | checks: 13 | argument-count: 14 | enabled: false 15 | file-lines: 16 | enabled: false 17 | method-complexity: 18 | enabled: false 19 | method-count: 20 | enabled: false 21 | method-lines: 22 | enabled: false 23 | return-statements: 24 | enabled: false 25 | similar-code: 26 | enabled: false 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | composer.lock -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # see http://about.travis-ci.org/docs/user/languages/php/ for more hints 2 | language: php 3 | 4 | before_install: 5 | - sudo apt-get update > /dev/null 6 | - pecl install ds 7 | - composer self-update 8 | 9 | before_script: 10 | - mkdir -p build/logs 11 | - if [[ "$DB" = "mariadb" ]]; then ./scripts/travis.install.mariadb.sh; fi; 12 | - if [[ "$DB" = "mysql" ]]; then ./scripts/travis.install.mysql.sh; fi; 13 | - composer install --no-interaction 14 | 15 | script: 16 | - vendor/phpunit/phpunit/phpunit --configuration ./phpunit.travis.xml test/php 17 | 18 | jobs: 19 | include: 20 | - stage: Test 21 | php: 7.2 22 | env: DB=mariadb 23 | addons: 24 | mariadb: '10.4' 25 | 26 | - stage: Test 27 | php: 7.3 28 | env: DB=mariadb 29 | addons: 30 | mariadb: '10.4' 31 | 32 | - stage: Test 33 | php: 7.4 34 | env: DB=mariadb 35 | addons: 36 | mariadb: '10.4' 37 | 38 | - stage: Test 39 | php: 7.2 40 | env: DB=mysql 41 | 42 | - stage: Test 43 | php: 7.3 44 | env: DB=mysql 45 | 46 | - stage: Test 47 | php: 7.4 48 | env: DB=mysql 49 | 50 | 51 | # configure notifications (email, IRC, campfire etc) 52 | notifications: 53 | email: "info@zf4.biz" 54 | 55 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2018 Ashley Kitson, UK 2 | 3 | Redistribution and use in source and binary forms, with or without modification, 4 | are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this list 7 | of conditions and the following disclaimer. 8 | 9 | 2. Redistributions in binary form must reproduce the above copyright notice, this 10 | list of conditions and the following disclaimer in the documentation and/or other 11 | materials provided with the distribution. 12 | 13 | 3. Neither the name of the copyright holder nor the names of its contributors may be 14 | used to endorse or promote products derived from this software without specific 15 | prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 18 | EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 19 | OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 20 | SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 21 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 22 | TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 23 | BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 24 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 25 | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH 26 | DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # chippyash/simple-accounts-3 2 | 3 | ## Quality Assurance 4 | 5 | 6 |  7 |  8 |  9 |  10 |  11 | [](https://travis-ci.org/chippyash/simple-accounts-3) 12 | [](https://codeclimate.com/github/chippyash/simple-accounts-3/test_coverage) 13 | [](https://codeclimate.com/github/chippyash/simple-accounts-3/maintainability) 14 | 15 | - Please note that developer support for PHP<7.2 was withdrawn at V2 of this library 16 | - 17 | ## What 18 | 19 | Provides a simple database backed general ledger accounting library that allows for a 20 | quick implementation of double entry book keeping into an application. 21 | 22 | This library replaces [chippyash/simple-accounts](https://github.com/chippyash/simple-accounts) 23 | 24 | This library does not provide sales ledgers, purchase ledgers, or other operational 25 | ledgers. Your application probably has these in one form or another. This provides 26 | the 'central' accounting functionality. 27 | 28 | ## Why 29 | 30 | Whilst full blown accounting systems are available, requiring a massive integration 31 | effort, some applications simply need to be able keep some form of internal account. 32 | This library is the direct descendant of something I wrote for a client many years 33 | ago to keep account of game points earned on a web site. Using the double entry 34 | accounting paradigm allowed the site owner to keep track of who had gathered points, 35 | and in which ways, whilst at the same time seeing what this meant to their business 36 | as game points translated into real world value for the customer by way of discounts 37 | and prizes. 38 | 39 | ## Requirements 40 | 41 | Support in this application is given for 2 flavours of SQL, MariaDb and MySql. 42 | 43 | ### MariaDb 44 | 45 | The MariaDb utilises a high performance plugin that greatly simplifies dealing with 46 | hierarchical data. 47 | 48 | You will need MariaDB >=10 with the [OQGraph plugin](https://mariadb.com/kb/en/library/oqgraph-storage-engine/) 49 | installed. Take a look at the `.travis.yml` build script for how we do this on the 50 | Travis build servers. One 'gotcha' that we know about is the setting of the user 51 | creating the triggers. Depending on your MariaDb setup, you may need to give the database 52 | creation script user 'SUPER' privileges. There seems no rhyme nor reason as to why this 53 | is, but be aware. For MariaDb 10 on the travis servers, it needs setting. See 54 | `scripts/test-user.sql` 55 | 56 | If for some reason you cannot use the OQGraph plugin, please follow the instructions 57 | for MySql. It will work in MariaDb as well. 58 | 59 | ### MySql 60 | 61 | Recognising that not everyone is able to utilise the MariaDb advantage, I have provided 62 | an alternate database implementation for plain MySql that utilises Nested Sets. See 63 | [Mike Hillyer's blog](http://mikehillyer.com/articles/managing-hierarchical-data-in-mysql/) 64 | for the basis of its implementation. 65 | 66 | Please note that this is not as performant as the MariaDb solution. Take a look at the 67 | stored procedure definitions for each DB to see the reason why. If you can, use MariaDb. 68 | If you can get the MySql code more performant, submit a pull request! 69 | 70 | Postgres users should be able to adapt the MySql code to implement the database. I rarely 71 | use it, so no direct support is given in this library. If you want to contribute the 72 | Postgres SQL statements, please feel free to provide a pull request. It would be 73 | welcome. 74 | 75 | No Windows variant support is provided at present. If you want it, please feel free 76 | to make a pull request. The library is developed under Linux. 77 | 78 | ## How 79 | 80 | ### Getting it up and running 81 | 82 | Create a database, let's say 'test'. 83 | 84 | Create a database user, 'test' with password 'test'. (You can run `scripts\test-user.sql` 85 | to do this.) 86 | 87 | Give that user all rights to the test database. (see note above re SUPER privs) 88 | 89 | Now run the create script: 90 | 91 | For MariaDb: 92 | 93 | `./createmariaddb.sh test test test` 94 | 95 | For MySql: 96 | 97 | `./createmysqldb.sh test test test` 98 | 99 | to create the database components. NB - PHP Doctrine Migration users should read the 100 | PHP code basic section to utilise the supplied migrations. 101 | 102 | You can run SQL tests by executing `./sqltest.sh test test test localhost`. 103 | 104 | - copy the `test/php/phpunit.xml` file to `test/php/local-phpunit.xml` and 105 | edit it to ensure that you have the correct database connection parameters 106 | 107 | You can run PHP tests by executing `./build.sh`. This also generates the test contract 108 | in the ./docs directory if you have the [TestDox-Converter](https://github.com/chippyash/Testdox-Converter) 109 | installed. If you don't then it may fail. Inspect the script contents to run the raw 110 | PHPUnit command. 111 | 112 | You can run the `examples/currency-example.php` program to see 113 | how you can convert between floating and integer types. 114 |
115 | cd examples 116 | chmod u+x currency-example.php 117 | ./currency-example.php 118 | 119 |120 | The library and supporting database only handle integers, so if you need float support, 121 | use [Chippyash\Currency](https://github.com/chippyash/currency) or provide your own handlers. 122 | 123 | ### Coding Basics- Terminology 124 | 125 | To keep things concise and avoid confusion, particularly if you are an Accountant and 126 | used to different terminology, here is a definition of terms used in this readme. 127 | 128 | - SA: Simple Accounts, this library 129 | - COA: Chart of Account. This is an entire chart. It has ledgers. A COA is hierarchical 130 | in nature, with a `root` ledger, normally called 'COA'. It generally has 2 main child 131 | ledgers, the balance sheet (BS) and the profit and loss account (P&L). Under these, various 132 | other ledgers will exist. You can find numerous references to COA construction on 133 | t'internet. 134 | - Ledger or Account: Used interchangeably. A line in the COA that holds the balance of 135 | all Journal transactions that have been made on the Ledger. In SA, the guiding principle 136 | is that if you update a child ledger, then it will update its parents all the way to 137 | the root ledger, thus keeping the the entire COA in balance. 138 | - Nominal or Nominal code: In General Ledger (GL), ledgers are often refered to by their 139 | `nominal code` or `nominal`. This is accountant or book keeper short code for a ledger. 140 | Each Ledger in this system has a Nominal Code. From a database point of view, it provides 141 | part of the ledger unique key along with the chart id. By convention, it is a Digit String 142 | and is usually from 4 digits upwards. Nominals are used to group related Ledgers 143 | together. By default this library supports up to a 10 digit Nominal Code. The example 144 | programs only use 4 digits which is more than sufficient for everyday usage. See the 145 | chart of account xml files for examples. 146 | - Journal or Journal Entry or Transaction: A record of a change in the balance on a 147 | Ledger. It comprises two parts, the details of the reason for the entry, and the details 148 | of the changes to each ledger that it effects. A Journal must be balanced. That is 149 | its debit and credit amounts must be equal. The system will bork if they are not. This 150 | is a principle defence for double entry book keeping. 151 | 152 | ### Coding Basics (SQL) 153 | As mentioned elsewhere, the fundamentals of this library lay in the SQL code, which 154 | runs on MariaDb with the OQGraph plugin installed or MySql. 155 | 156 | The SQL API is provided via stored procedures. If you want to provide variants, please 157 | respect the API. 158 | 159 | For MariaDb see: 160 | - the procedure definitions in the [src/sql/mariadb/build-procs](https://github.com/chippyash/simple-accounts-3/blob/master/src/sql/mariadb/build-procs.sql) 161 | - the trigger that maintains the account balances in the [src/sql/mariadb/build-triggers](https://github.com/chippyash/simple-accounts-3/blob/master/src/sql/mariadb/build-triggers.sql) 162 | script. 163 | 164 | Otherwise the SQL is pretty straight forward. Study the OQGraph 165 | docs for an understanding of how it's being used. Magic underneath, but simple to use - my kind of code ;-) 166 | 167 | For MySql see: 168 | - the procedure definitions in the [src/sql/mysql/build-procs](https://github.com/chippyash/simple-accounts-3/blob/master/src/sql/mysql/build-procs.sql) 169 | - the trigger that maintains the account balances in the [src/sql/mysql/build-triggers](https://github.com/chippyash/simple-accounts-3/blob/master/src/sql/mysql/build-triggers.sql) 170 | script. 171 | 172 | One slightly baffling procedure is `sa_fu_add_txn`. In particular the parameters, 173 | - arNominals TEXT, 174 | - arAmounts TEXT, 175 | - arTxnType TEXT 176 | 177 | These require a matching set of comma delimited values, which is the only way of getting an array into SQL.: 178 | - arNominals: the list of nominals to effect 179 | - arAmounts: the list of amounts to use 180 | - arTxnType: lst of dr or cr for each nominal 181 | 182 | The php code deals with this by imploding values into a string before calling the SP: 183 | 184 | See Accountant::writeTransaction() method. 185 | See test/sql/add_transaction_test.sql circa L17 for how the SQL proc is called natively. 186 | 187 | If you are a better SQL Head than me (not hard!), then I'd appreciate any suggestions 188 | for operational efficiency. 189 | 190 | ### Coding Basics (PHP) 191 | 192 | Whilst what follows will give you an introduction to what you can do with the library, 193 | you should always look to the tests to gain further insight. 194 | 195 | #### Changes from previous library versions 196 | ##### Organisations 197 | Unlike the previous version of this library, we don't support the concepts of 198 | organisations. Organisations are outside of the remit of this 199 | library as your implementation of them will differ according to your needs. Instead 200 | you should plan on creating some form of one to many join between your organisations 201 | and any chart of accounts (COA) that they use. The `sa_coa` table can hold an 202 | infinite number of COAs, so it shouldn't be too much of a problem. 203 | 204 | #### Doctrine Migrations 205 | 206 | If you are using Doctrine Migrations and Mariadb, you can take advantage of the supplied migration 207 | files in `src\php\SAccounts\Doctrine`. 208 | 209 | For development of this library you can migrate up the required DB structure into the 210 | test database by navigating to the root of this library and running 211 | 212 | `vendor/bin/doctrine-migrations migrations:migrate --configuration doctrine-migrations.xml --db-configuration doctrine-db.php` 213 | 214 | To migrate down use: 215 | 216 | `vendor/bin/doctrine-migrations migrations:migrate prev --configuration doctrine-migrations.xml --db-configuration doctrine-db.php` 217 | 218 | For production use, either copy the migration files into your own migrations directory, 219 | (files are in src/php/SAccounts/Doctrine), or possible more conveniently by creating 220 | your own migration classes in your existing structure and the extending them from the 221 | supplied migrations. That will keep them in your sequence. 222 | 223 | Be aware that new features may result in additional migrations so if you update this 224 | library to a new feature version, check for new ones. 225 | 226 | ##### Control accounts 227 | Like Organisations, we don't support the concept of control accounts in this library. 228 | They are again an implementation detail between your application and this library, 229 | more usually a configuration issue. So add config linking where you need such functionality. 230 | Another problem was the use of the term. Too many accountants objected to it being 231 | used in its previous incarnation, that it was safer to leave it out. 232 | 233 | #### The Accountant 234 | The Accountant is responsible for the majority of operations that you can carry out 235 | in Simple Accounts and needs to be created before anything else can happen. The 236 | Accountant requires a Zend Db Adapter as a construction parameter. 237 | 238 |
239 | use SAccounts\Accountant; 240 | use Zend\Db\Adapter\Adapter; 241 | 242 | $accountant = new Accountant( 243 | new Adapter( 244 | [ 245 | 'driver' => 'Pdo_mysql', 246 | 'database' => 'test', 247 | 'username' => 'test', 248 | 'password' => 'test' 249 | ] 250 | ) 251 | ); 252 |253 | 254 | #### Creating a new chart of accounts 255 | 256 | You create a new chart of accounts (COA) by supplying a ChartDefinition. The 257 | ChartDefinition is supplied with an XML definition file. An example of a definition 258 | can be found in `src\xml\personal.xml` along with the XSD that is used to validate 259 | any definitions in `src\xsd\chart-definition.xsd`. 260 | 261 |
262 | use SAccounts\ChartDefinition;
263 |
264 | $definition = new ChartDefinition('src/xml/personal.xml');
265 |
266 | $chartId = $accountant->createChart('Personal', $definition);
267 |
268 |
269 | This will create the entries in the `sa_coa` table and return you the id of the
270 | new chart. You will probably want to store this in your own tables so you can
271 | retrieve it later.
272 |
273 | The Accountant is now tied to that COA. To use another COA you will
274 | need to create another Accountant. To create the Accountant and tell it to use an
275 | existing COA, simply give the chart id as the second parameter
276 | when constructing the Accountant.
277 |
278 | Please note that you never have to explicitly save the COA. It is done transactionally
279 | by the Accountant when you carry out operations with it.
280 |
281 | #### Accountant operations
282 |
283 | Most operations on the COA are carried out via the Accountant.
284 |
285 | Operations on the COA invariably require you to give the Nominal code for the Account
286 | which is to say, the Account identifier. Whilst in the database primary integer
287 | ids are used, externally we operate using the Nominal code.
288 |
289 | ##### Adding an Account ledger to the COA
290 |
291 | use SAccounts\Nominal;
292 | use SAccounts\Account;
293 |
294 | $nominal = new Nominal('7700');
295 | $prntNominal = new Nominal(('7000'));
296 |
297 | $accountant->addAccount(
298 | $nominal, //nominal code
299 | AccountType::EXPENSE(), //account type
300 | new StringType('foo'), //account name
301 | $prntNominal //parent account nominal code (or null)
302 | );
303 |
304 |
305 | The parent Nominal must exist already with one exception. In a brand new COA you can
306 | add the root Account and leave out the parent Nominal parameter. For a root Account
307 | the AccountType must be AccountType::REAL(). Trying to add a second
308 | root Account will throw an exception.
309 |
310 | The AccountType is important and must be appropriate for the Account you are adding.
311 | It controls how the balance on the Account is derived. It also allows you to display
312 | appropriate labels for the debit and credit values on an account. Take a look at the
313 | `src\xml\personal.xml` file for an example of how AccountTypes are used.
314 |
315 | Under typical circumstances, the root account is a REAL account, and has two children,
316 | the Balance Sheet (type DR) and the Profit & Loss (type CR). All other accounts are
317 | children of these two. There is however nothing stopping you configuring your chart
318 | as you wish.
319 |
320 | All DR (debit) type accounts derive their balance as dr amount - cr amount. All CR (credit)
321 | type accounts derive their baslance as cr amount - dr amount. REAL accounts derive their
322 | balance as abs(cr amount - dr amount), which is the same as abs(dr amount - cr amount)
323 | and should usually equal zero.
324 |
325 | ##### Deleting an Account ledger from the COA
326 |
327 | You can delete an Account ledger only if its balance is zero. Attempting to delete
328 | a non zero ledger will throw an exception. NB. Deleting a ledger will delete all of
329 | its child ledgers as well.
330 |
331 | By zero, it is meant that both the debit and credit account values == zero. You cannot
332 | therefore delete account ledgers for which any transactions have been made for it or
333 | any child accounts. This is a simple security measure to ensure that data is not lost.
334 |
335 |
336 | $accountant->delAccount(new Nominal('7000'));
337 |
338 |
339 | #### Fetching the COA
340 |
341 | Having created the COA or instantiated the Accountant with the chart id, you can fetch
342 | the COA simply with:
343 |
344 | 345 | use SAccounts\Chart; 346 | 347 | /* @var Chart $chart */ 348 | $chart = $accountant->fetchChart(); 349 |350 | 351 | #### Operations on the COA 352 | 353 | ##### Basic COA operations 354 | 355 |
356 | //get an Account from the Chart
357 | /* @var Account $acount */
358 | $account = $chart->getAccount(new Nominal('2000'));
359 |
360 | //get the parent account of an Account
361 | $account = $chart->getAccount($chart->getParentId(new Nominal('2000')))
362 | //or
363 | $subAccount = $chart->getAccount(new Nominal('2000'));
364 | $prntAccount = $chart->getAccount($chart->getParentId($account->getNominal()));
365 |
366 | //testing if an account exists in the COA
367 | //returns true or false
368 | $exists = $chart->hasAccount(new Nominal('3000'));
369 |
370 | //get the name of the COA
371 | /* @var StringType $name */
372 | $name = $chart->getName();
373 |
374 | //get a ledger's values
375 | $account = $chart->getAccount(new Nominal('2000'));
376 | /* @var IntType $dr */
377 | $dr = $account->dr();
378 | /* @var IntType $cr */
379 | $cr = $account->cr();
380 |
381 | //NB getting the balance of the root COA account should return zero. If not
382 | //then your accounts are out of balance and need investigating. Perhaps something
383 | //outside of SA made an update, or a database glitch occurred.
384 | /* @var IntType $balance */
385 | $balance = $account->getBalance();
386 |
387 | /* @var Nominal $nom */
388 | $nom = $account->getNominal();
389 | /* @var StringType $name */
390 | $name = $account->getName();
391 | /* @var AccountType $type */
392 | $type = $account->getType();
393 |
394 |
395 |
396 | ##### The COA as a Tree
397 |
398 | Under the covers, the chart is kept as a [nicmart/Tree](https://github.com/nicmart/Tree)
399 | which I do recommend to you if you need to carry out Tree operations. Gaining access
400 | to it is useful for a variety of tasks, such as displaying a trial balance. For this
401 | we use tree Visitors. You can see a full working example of this in the
402 | `examples\currency-example.php` script. The line of interest is:
403 |
404 | 405 | $accountant->fetchChart()->getTree()->accept(new ChartPrinter(Crcy::create($crcyCd))); 406 |407 | 408 | Two end user Visitors are supplied: 409 | - `SAccounts\Visitor\ChartPrinter` which prints the COA to the console 410 | - `SAccounts\Visitor\ChartArray` which returns the COA as an array of values and 411 | account balances 412 | 413 | Other Visitors are used internally in the Chart, but they should all give you a firm 414 | grasp on how to create your own if you need to. 415 | 416 | #### Journal Entries 417 | 418 | ##### Creating Entries 419 | 420 | You create Journal entries in your accounts by adding Transactions to the system. A 421 | Transaction is made up of two parts, the Journal description and a list of transaction 422 | entries, one for each account that is effected by the transaction. Those transactions 423 | have a debit or credit amount. The sum of all debits must equal the sum of all 424 | credits so that the transaction balances in order for the Transaction to be accepted by 425 | the system. 426 | 427 | The basic Transaction type is the SplitTransaction: 428 | 429 |
430 | /** 431 | * Constructor 432 | * 433 | * @param string $note Defaults to '' if not set 434 | * @param string $src user defined source of transaction 435 | * @param int $ref user defined reference for transaction 436 | * @param \DateTime $date Defaults to today if not set 437 | */ 438 | public function __construct( 439 | string $note = null, 440 | string $src = null, 441 | int $ref = null, 442 | \DateTime $date = null 443 | ) 444 |445 | 446 | After construction, you add transaction entries by passing an Entry object to the addEntry() 447 | method. 448 | 449 |
450 | use SAccounts\Transaction\SplitTransaction;
451 | use SAccounts\Transaction\Entry;
452 |
453 | $amount = new IntType(100);
454 | $txn = (new SplitTransaction())
455 | ->addEntry(new Entry(new Nominal('0000'), $amount, AccountType::DR()))
456 | ->addEntry(new Entry(new Nominal('1000'), $amount, AccountType::CR()))
457 |
458 |
459 | Here, we added two entries for same amount to two accounts, but with the transaction
460 | type being debit for one and credit for the other. You can check that the
461 | transaction is balanced with the `checkBalance()` method which will return true
462 | if the transaction is balanced or false otherwise.
463 |
464 | Whilst the SplitTransaction is useful for adding transactions that comprise of many
465 | entries (e.g. a sale comprises of sale account, vat account and a bank account entries),
466 | for a simple two account entry(like a transfer between bank accounts) you can use the
467 | SimpleTransaction, which is a child of SplitTransaction.
468 |
469 | 470 | /** 471 | * Constructor 472 | * 473 | * @param Nominal $drAc Account to debit 474 | * @param Nominal $crAc Account to credit 475 | * @param int $amount Transaction amount 476 | * @param string $note Defaults to '' if not set 477 | * @param int $ref Defaults to 0 if not set 478 | * @param \DateTime $date Defaults to today if not set 479 | */ 480 | public function __construct( 481 | Nominal $drAc, 482 | Nominal $crAc, 483 | int $amount, 484 | ?string $note = null, 485 | ?string $src = null, 486 | ?int $ref = null, 487 | ?\DateTime $date = null 488 | ) 489 |490 | 491 | Thus: 492 | 493 |
494 | use SAccounts\Transaction\SimpleTransaction;
495 |
496 | $txn = new SimpleTransaction(new Nominal('0000'), new Nominal('1000'), 1226);
497 |
498 |
499 | Having created your transaction by whatever means, you can then add it to the accounts
500 | with:
501 |
502 | 503 | $txnId = $accountant->writeTransaction($txn); 504 |505 | 506 | Writing a transaction automatically updates the COA ledger balances. 507 | 508 | ##### Retrieving Entries 509 | 510 | You retrieve a single transaction from the accounts with 511 | 512 |
513 | /* @var SplitTransaction $txn */ 514 | $txn = $accountant->fetchTransaction(102); 515 |516 | 517 | To retrieve all entries for an account ledger: 518 |
519 | /** @var Ds\Set $transactions */
520 | $transactions = $accountant->fetchAccountJournals(new Nominal('2000'));
521 |
522 |
523 | The Set contains SplitTransactions with Entries reflecting only the side of the
524 | double entry that the transaction acted up for that journal entry. You can see an
525 | example of how this works in `examples/journal-entries-example.php`.
526 |
527 | ## Notes
528 |
529 | The library is built to rely on pure SQL in the database. Whilst I'm providing
530 | a PHP API layer to it, you can use the underlaying SQL from any language. If you
531 | are a Python, Java or other developer, please feel free to add your own
532 | language API under the `src` directory
533 |
534 | My references here apply to the fact that I develop primarily in PHP. If they don't
535 | apply to your dev language of choice, ignore them.
536 |
537 | Finally, if in doubt, read the source code. It's well documented.
538 |
539 | ## Further documentation
540 |
541 | [Test Contract](https://github.com/chippyash/simple-accounts-3/blob/master/docs/Test-Contract.md) in the docs directory.
542 |
543 | This library makes use of the Standard [PHP DS extension](https://www.php.net/manual/en/book.ds.php)
544 |
545 | It also employs a great deal of functional programming derived from the [Monad](https://github.com/chippyash/monad)
546 | and [Assembly](https://github.com/chippyash/Assembly-Builder) libraries.
547 |
548 | If you are unfamiliar with them, please take a moment to study them.
549 |
550 | As the library only deals with integers as values (to ensure numerical accuracy) you
551 | may want to have a look at the [Currency](https://github.com/chippyash/currency) library
552 | that is used in the example scripts.
553 |
554 | Check out [ZF4 Packages](http://zf4.biz/packages?utm_source=github&utm_medium=web&utm_campaign=blinks&utm_content=simpleaccounts3) for more packages
555 |
556 | ## Changing the library
557 |
558 | 1. fork it
559 | 2. write the test
560 | 3. amend it
561 | 4. do a pull request
562 |
563 | Found a bug you can't figure out?
564 |
565 | 1. fork it
566 | 2. write the test
567 | 3. do a pull request
568 |
569 | NB. Make sure you rebase to HEAD before your pull request
570 |
571 | Or - raise an issue ticket.
572 |
573 | ## Where?
574 |
575 | The library is hosted at [Github](https://github.com/chippyash/simple-accounts-3). It is
576 | available at [Packagist.org](https://packagist.org/packages/chippyash/simple-accounts-3)
577 |
578 | ### Installation (PHP)
579 |
580 | Install [Composer](https://getcomposer.org/)
581 |
582 | #### For production
583 |
584 | 585 | "chippyash/simple-accounts-3": "~2.0" 586 |587 | 588 | `composer install --no-dev` 589 | 590 | #### For development 591 | 592 | Clone this repo, and then run Composer in local repo root to pull in dependencies 593 | 594 |
595 | git clone git@github.com:chippyash/simple-accounts-3.git simple-accounts 596 | cd simple-accounts 597 | composer update 598 |599 | 600 | - copy the `test/php/phpunit.xml` file to `test/php/local-phpunit.xml` and 601 | edit it to ensure that you have the correct database connection parameters 602 | 603 | - To run the tests: `composer test:run` 604 | - To lint the code: `composer lint:run` 605 | - To fix lint issues: `composer lint:fix` 606 | 607 | ## License 608 | 609 | This software library is released under the [BSD 3 Clause license](https://opensource.org/licenses/BSD-3-Clause) 610 | 611 | This software library is Copyright (c) 2017-2020, Ashley Kitson, UK 612 | 613 | ## History 614 | 615 | V1.0.0 First production release 616 | 617 | V1.0.1 Documentation for first release 618 | 619 | V1.1.0 Add PHP Doctrine Migrations 620 | 621 | V1.1.1 Change namespace for migrations 622 | 623 | V1.1.2 Support PHP 7.0 & 7.1 624 | 625 | V1.1.3 Support PHP 7.2 626 | 627 | V1.2.0 Add ability to retrieve all transactions for an account 628 | 629 | V1.2.1 docs licenses update 630 | 631 | V1.3.0 Support for MySql 632 | 633 | V1.3.1 File path amend for mariadb migration 634 | 635 | V1.3.2 Remove ambiguity for treatment of real account balances 636 | 637 | V1.4.0 Change of license from GPL V3 to BSD 3 Clause 638 | 639 | V1.4.1 Fix issue 1 640 | 641 | V2.0.0 BC Break. PHP<7.2 support withdrawn. Remove dependency on chippyash/strongtype. 642 | 643 | V2.0.1 Remove dependency on chippyash/identity -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cd ~/Projects/chippyash/source/simple-accounts-3/ 3 | vendor/phpunit/phpunit/phpunit -c test/php/local-phpunit.xml --testdox-html contract.html test/php 4 | tdconv -t "Simple Accounts V3" contract.html docs/Test-Contract.md 5 | rm contract.html 6 | 7 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chippyash/simple-accounts-3", 3 | "description": "Database backed Simple Double Entry Accounting V3", 4 | "license": "BSD-3-Clause", 5 | "homepage" : "http://zf4.biz/packages?utm_source=packagist&utm_medium=web&utm_campaign=blinks&utm_content=accounts-2", 6 | "keywords": ["accounting","double entry","database","zend", "mysql", "mariadb", "oqgraph"], 7 | "authors": [ 8 | { 9 | "name": "Ashley Kitson", 10 | "email": "info@zf4.biz" 11 | } 12 | ], 13 | 14 | "minimum-stability": "stable", 15 | 16 | "require": { 17 | "php": ">=7.2", 18 | "ext-pdo": "*", 19 | "ext-ds": "*", 20 | "zendframework/zend-db": ">=2.9.2,<3", 21 | "chippyash/monad": ">=2,<3", 22 | "chippyash/assembly-builder": ">=2,<3", 23 | "myclabs/php-enum": ">=1.3.2,<2", 24 | "nicmart/tree": ">=0.2.5,<1" 25 | }, 26 | 27 | "require-dev": { 28 | "phpunit/phpunit": "~8.5", 29 | "doctrine/migrations": "~1.5.0|~1.6", 30 | "mikey179/vfsstream": ">=1.6.5,<2", 31 | "chippyash/currency": ">=5.0.2", 32 | "php-ds/php-ds": "~1.3.0", 33 | "symplify/easy-coding-standard": "^6.1", 34 | "ext-dom": "*" 35 | }, 36 | 37 | "suggest": { 38 | "chippyash/currency" : "To turn account values into real currency values", 39 | "doctrine/migrations": "To assist in database implementation for PHP" 40 | }, 41 | 42 | "autoload": { 43 | "psr-4": { 44 | "SAccounts\\": "src/php/SAccounts" 45 | } 46 | }, 47 | "scripts": { 48 | "lint:run": "vendor/bin/ecs check src", 49 | "lint:fix": "vendor/bin/ecs check --fix src", 50 | "test:run": "vendor/bin/phpunit -c test/php/local-phpunit.xml test/" 51 | }, 52 | "scripts-descriptions": { 53 | "lint:run": "Run code linter and look for problems (suggestion: set up your IDE to do this)", 54 | "lint:fix": "Run linter and fix (automatically) any issues if possible", 55 | "test:run": "Run PHP unit tests and display coverage (suggestion: set up your IDE to do this)" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /createmariadb.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Create MariaDb database 3 | # Requires OQGraph support 4 | # 5 | # usage createmariadb.sh dbname dbuid dbpwd 6 | # 7 | # dbname must exist 8 | # dbuid must have privileges to run DDL on database; 9 | 10 | DBNAME=$1 11 | DBUID=$2 12 | DBPWD=$3 13 | DBHOST=$4 14 | 15 | echo "building tables" 16 | cat ./src/sql/mariadb/build-tables.sql | mysql -h $DBHOST -u $DBUID -p$DBPWD $DBNAME 17 | echo "building procs and functions" 18 | cat ./src/sql/mariadb/build-procs.sql | mysql -h $DBHOST -u $DBUID -p$DBPWD $DBNAME 19 | echo "building triggers" 20 | cat ./src/sql/mariadb/build-triggers.sql | mysql -h $DBHOST -u $DBUID -p$DBPWD $DBNAME 21 | -------------------------------------------------------------------------------- /createmysqldb.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Create MySql database 3 | # 4 | # usage createmysqldb.sh dbname dbuid dbpwd 5 | # 6 | # dbname must exist 7 | # dbuid must have privileges to run DDL on database; 8 | 9 | DBNAME=$1 10 | DBUID=$2 11 | DBPWD=$3 12 | DBHOST=$4 13 | 14 | echo "building tables" 15 | cat ./src/sql/mysql/build-tables.sql | mysql -h $DBHOST -u $DBUID -p$DBPWD $DBNAME 16 | echo "building procs and functions" 17 | cat ./src/sql/mysql/build-procs.sql | mysql -h $DBHOST -u $DBUID -p$DBPWD $DBNAME 18 | echo "building triggers" 19 | cat ./src/sql/mysql/build-triggers.sql | mysql -h $DBHOST -u $DBUID -p$DBPWD $DBNAME 20 | -------------------------------------------------------------------------------- /docs/Test-Contract.md: -------------------------------------------------------------------------------- 1 | # Simple Accounts V3 2 | 3 | ## SAccounts\Account 4 | 5 | * ✓ You can create any valid account type 6 | * ✓ You can get a balance for account types that support it 7 | * ✓ Getting balance of a dummy account type will throw an exception 8 | * ✓ You can get the account nominal code 9 | * ✓ You can get the account type 10 | * ✓ You can get the account name 11 | * ✓ You can get the debit and credit amounts 12 | 13 | ## SAccounts\AccountType 14 | 15 | * ✓ Can get values as constants 16 | * ✓ Can get values as classes using static methods 17 | * ✓ Can get a debit column title for a valid account type 18 | * ✓ Get a debit column title with invalid account type will throw exception 19 | * ✓ Get a credit column title with invalid account type will throw exception 20 | * ✓ Can get a credit column title for a valid account type 21 | * ✓ Will get correct balance for all valid account types 22 | * ✓ Get a balance with invalid account type will throw exception 23 | 24 | ## SAccounts\Accountant 25 | 26 | * ✓ An accountant can create a new chart of accounts 27 | * ✓ An accountant can fetch a chart 28 | * ✓ Fetching a chart when chart id is not set will throw an exception 29 | * ✓ You can write a transaction to a journal and update a chart 30 | * ✓ Writing a transaction when chart id is not set will throw an exception 31 | * ✓ You can fetch a journal transaction by its id 32 | * ✓ You can add an account to a chart 33 | * ✓ Adding an account to a non existent parent will throw an exception 34 | * ✓ Trying to add a second root account will throw an exception 35 | * ✓ You can delete a zero balance account 36 | * ✓ Deleting a non zero balance account will throw an exception 37 | * ✓ You can fetch journal entries for an account 38 | * ✓ Fetching journal entries returns a set of split transactions 39 | * ✓ Fetching journal entries for an aggregate account will return an empty set 40 | 41 | ## SAccounts\ChartDefinition 42 | 43 | * ✓ Can construct with valid file name 44 | * ✓ Construction with invalid file name will throw exception 45 | * ✓ Construction with valid file name will return class 46 | * ✓ Getting the definition will throw exception if definition file is invalid xml 47 | * ✓ Getting definition will throw exception if definition fails validation 48 | * ✓ Getting the definition will return a dom document with valid definition file 49 | 50 | ## SAccounts\Chart 51 | 52 | * ✓ Construction creates chart 53 | * ✓ You can give a chart an optional tree in construction 54 | * ✓ You can get an account if it exists 55 | * ✓ Trying to get a non existent account will throw an exception 56 | * ✓ You can test if a chart has an account 57 | * ✓ Trying to get a parent id of a non existent account will throw an exception 58 | * ✓ Getting the parent id of an account that has a parent will return the parent nominal 59 | * ✓ You can provide an optional internal id when constructing a chart 60 | * ✓ You can set the chart root node 61 | 62 | ## SAccounts\Transaction\Entries 63 | 64 | * ✓ You can create an empty entries collection 65 | * ✓ You can create an entries collections with entry values 66 | * ✓ You cannot create an entries collection with non entry values 67 | * ✓ You can add another entry to entries and get new entries collection 68 | * ✓ Check balance will return true if entries are balanced 69 | * ✓ Check balance will return false if entries are not balanced 70 | 71 | ## SAccounts\Transaction\Entry 72 | 73 | * ✓ An entry requires an id an amount and a type 74 | * ✓ An entry must have cr or dr type 75 | * ✓ Constructing an entry with invalid type will throw exception 76 | * ✓ You can get the id of an entry 77 | * ✓ You can get the amount of an entry 78 | * ✓ You can get the type of an entry 79 | 80 | ## Chippyash\Test\SAccounts\Transaction\SimpleTransaction 81 | 82 | * ✓ Basic construction sets an empty note on the transaction 83 | * ✓ Basic construction sets date for today on the transaction 84 | * ✓ You can set an optional note on construction 85 | * ✓ You can set an optional source on construction 86 | * ✓ You can set an optional reference on construction 87 | * ✓ You can set an optional date on construction 88 | * ✓ Constructing a transaction does not set its id 89 | * ✓ You can set and get an id 90 | * ✓ You can get the debit account code 91 | * ✓ You can get the credit account code 92 | * ✓ You can get the transaction amount 93 | * ✓ You can get the transaction note 94 | * ✓ You can get the transaction datetime 95 | 96 | ## SAccounts\Transaction\SplitTransaction 97 | 98 | * ✓ Basic construction sets an empty note on the transaction 99 | * ✓ Basic construction sets date for today on the transaction 100 | * ✓ You can set an optional note on construction 101 | * ✓ A null note will be retrieved as an empty string 102 | * ✓ You can set an optional source on construction 103 | * ✓ A null source will be retrieved as an empty string 104 | * ✓ You can set an optional reference on construction 105 | * ✓ A null reference will be retrieved as a zero integer 106 | * ✓ You can set an optional date on construction 107 | * ✓ Constructing a split transaction does not set its id 108 | * ✓ You can set and get an id 109 | * ✓ Getting the debit account for a split transaction will return an array of nominals 110 | * ✓ Getting the credit account for a split transaction will return an array of nominals 111 | * ✓ Checking if a split transaction is balanced will return true if balanced 112 | * ✓ Checking if a split transaction is balanced will return false if not balanced 113 | * ✓ You can get the total transaction amount if the transaction is balanced 114 | * ✓ If the transaction is not balanced getting the total transaction amount will throw an exception 115 | * ✓ You can get the transaction note 116 | * ✓ You can get the transaction datetime 117 | * ✓ A split transaction is simple if it has one dr and one cr entry 118 | * ✓ You can get an entry by its nominal id 119 | * ✓ Getting an unknown entry will throw an exception 120 | 121 | ## SAccounts\Visitor\ChartArray 122 | 123 | * ✓ Constructing with no currency will return integer values 124 | * ✓ Constructing with a currency will return floats dependent on the currency precision 125 | 126 | ## SAccounts\Visitor\ChartPrinter 127 | 128 | * ✓ The output is sent to the console 129 | * ✓ Output is formatted using the currency symbol 130 | 131 | 132 | Generated by [chippyash/testdox-converter](https://github.com/chippyash/Testdox-Converter) -------------------------------------------------------------------------------- /doctrine-db.php: -------------------------------------------------------------------------------- 1 | 'test', 12 | 'user' => 'test', 13 | 'password' => 'test', 14 | 'host' => 'localhost', 15 | 'driver' => 'pdo_mysql', 16 | ); -------------------------------------------------------------------------------- /doctrine-migrations.xml: -------------------------------------------------------------------------------- 1 | 2 |
\d+) - (?P[\w, ]+)\)/',
28 | $errMsg,
29 | $matches
30 | );
31 | }
32 | if ($previous instanceof \PDOException) {
33 | preg_match('/.*\[45000\].*: (?P\d+) (?P[\w, ]+)/', $errMsg, $matches);
34 | }
35 | parent::__construct($matches['err'], (int) $matches['code'], $previous);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/php/SAccounts/Doctrine/Version20180428092520.php:
--------------------------------------------------------------------------------
1 | connection->getDatabasePlatform()
35 | ->registerDoctrineTypeMapping('enum', 'string');
36 | $this->sqlSrcDir = dirname(dirname(dirname(dirname(__FILE__)))) . '/sql/mariadb';
37 | }
38 |
39 | /**
40 | * @param Schema $schema
41 | */
42 | public function up(Schema $schema): void
43 | {
44 | $this->createTables();
45 | $this->createProcs();
46 | $this->createTriggers();
47 | }
48 |
49 | /**
50 | * @param Schema $schema
51 | */
52 | public function down(Schema $schema): void
53 | {
54 | $this->dropProcs();
55 | $this->dropTables();
56 | }
57 |
58 | protected function createTables(): void
59 | {
60 | $sql = file_get_contents($this->sqlSrcDir . '/build-tables.sql');
61 |
62 | $matches = [];
63 | preg_match_all('/DROP TABLE .*;/isU', $sql, $matches);
64 |
65 | foreach ($matches as $statement) {
66 | $this->addSql($statement);
67 | }
68 |
69 | $matches = [];
70 | preg_match_all('/CREATE TABLE .*;/isU', $sql, $matches);
71 |
72 | foreach ($matches as $statement) {
73 | $this->addSql($statement);
74 | }
75 |
76 | $matches = [];
77 | preg_match_all('/INSERT.*\);/isU', $sql, $matches);
78 |
79 | foreach ($matches as $statement) {
80 | $this->addSql($statement);
81 | }
82 | }
83 |
84 | protected function createProcs(): void
85 | {
86 | $sql = file_get_contents($this->sqlSrcDir . '/build-procs.sql');
87 |
88 | $matches = [];
89 | preg_match_all('/DROP FUNCTION .*;/isU', $sql, $matches);
90 |
91 | foreach ($matches as $statement) {
92 | $this->addSql($statement);
93 | }
94 |
95 | $matches = [];
96 | preg_match_all('/DROP PROCEDURE .*;/isU', $sql, $matches);
97 |
98 | foreach ($matches as $statement) {
99 | $this->addSql($statement);
100 | }
101 |
102 | $matches = [];
103 | preg_match_all('!CREATE DEFINER .*//!isU', $sql, $matches);
104 |
105 | foreach ($matches as $statement) {
106 | $this->addSql($statement);
107 | }
108 | }
109 |
110 | protected function createTriggers(): void
111 | {
112 | $sql = file_get_contents($this->sqlSrcDir . '/build-triggers.sql');
113 |
114 | $matches = [];
115 | preg_match_all('/DROP TRIGGER .*;/isU', $sql, $matches);
116 |
117 | foreach ($matches as $statement) {
118 | $this->addSql($statement);
119 | }
120 |
121 | $matches = [];
122 | preg_match_all('!CREATE DEFINER .*//!isU', $sql, $matches);
123 |
124 | foreach ($matches as $statement) {
125 | $this->addSql($statement);
126 | }
127 | }
128 |
129 | protected function dropProcs(): void
130 | {
131 | $sql = file_get_contents($this->sqlSrcDir . '/drop-procs.sql');
132 |
133 | $matches = [];
134 | preg_match_all('/DROP FUNCTION .*;/isU', $sql, $matches);
135 |
136 | foreach ($matches as $statement) {
137 | $this->addSql($statement);
138 | }
139 |
140 | $matches = [];
141 | preg_match_all('/DROP PROCEDURE .*;/isU', $sql, $matches);
142 |
143 | foreach ($matches as $statement) {
144 | $this->addSql($statement);
145 | }
146 | }
147 |
148 | protected function dropTables(): void
149 | {
150 | $sql = file_get_contents($this->sqlSrcDir . '/drop-tables.sql');
151 |
152 | $matches = [];
153 | preg_match_all('/DROP TABLE .*;/isU', $sql, $matches);
154 |
155 | foreach ($matches as $statement) {
156 | $this->addSql($statement);
157 | }
158 | }
159 | }
160 |
--------------------------------------------------------------------------------
/src/php/SAccounts/Nominal.php:
--------------------------------------------------------------------------------
1 | set($value);
43 | }
44 |
45 | /**
46 | * Set the object value.
47 | * Forces type
48 | *
49 | * @param mixed $value
50 | *
51 | * @return Nominal
52 | *
53 | * @see typeOf
54 | */
55 | public function set($value): Nominal
56 | {
57 | $this->value = $this->typeOf($value);
58 |
59 | return $this;
60 | }
61 |
62 | /**
63 | * Get the value of the object typed properly
64 | *
65 | * @return string
66 | */
67 | public function get()
68 | {
69 | return $this->value;
70 | }
71 |
72 | /**
73 | * Magic invoke method
74 | * Proxy to get()
75 | *
76 | * @see get
77 | *
78 | * @return string
79 | */
80 | public function __invoke(): string
81 | {
82 | return $this->get();
83 | }
84 |
85 | /**
86 | * Magic method - convert to string
87 | * Proxy to get()
88 | *
89 | * @return string
90 | */
91 | public function __toString(): string
92 | {
93 | return $this->get();
94 | }
95 |
96 | /**
97 | * This will filter out any non numeric characters. You may potentially
98 | * get an empty string
99 | *
100 | * @param mixed $value
101 | * @return string
102 | */
103 | protected function typeOf($value): string
104 | {
105 | return (string) $this->filter($value);
106 | }
107 |
108 | /**
109 | * Lifted entirely from the Zend framework so that we don't have to include
110 | * the Zend\Filter package and all its dependencies.
111 | *
112 | * @param string $value
113 | * @return string
114 | * zendframework/zend-filter/Zend/Filter/Digits.php
115 | * Zend Framework (http://framework.zend.com/)
116 | *
117 | * @link http://github.com/zendframework/zf2 for the canonical source repository
118 | * @copyright Copyright (c) 2005-2014 Zend Technologies USA Inc. (http://www.zend.com)
119 | * @license http://framework.zend.com/license/new-bsd New BSD License
120 | * Defined by Zend\Filter\FilterInterface
121 | *
122 | * Returns the string $value, removing all but digit characters
123 | *
124 | * If the value provided is non-scalar, the value will remain unfiltered
125 | *
126 | */
127 | protected function filter($value): string
128 | {
129 | if (!is_scalar($value)) {
130 | return $value;
131 | }
132 | $value = (string) $value;
133 |
134 | if (!$this->hasPcreUnicodeSupport()) {
135 | // POSIX named classes are not supported, use alternative 0-9 match
136 | return preg_replace('/[^0-9]/', '', $value);
137 | }
138 |
139 | if (extension_loaded('mbstring')) {
140 | // Filter for the value with mbstring
141 | return preg_replace('/[^[:digit:]]/', '', $value);
142 | }
143 |
144 | // Filter for the value without mbstring
145 | return preg_replace('/[\p{^N}]/', '', $value);
146 | }
147 |
148 | /**
149 | * Lifted entirely from Zend Framework (http://framework.zend.com/) so we don't have
150 | * to include Zend/Stdlib
151 | *
152 | * @link http://github.com/zendframework/zf2 for the canonical source repository
153 | * @copyright Copyright (c) 2005-2014 Zend Technologies USA Inc. (http://www.zend.com)
154 | * @license http://framework.zend.com/license/new-bsd New BSD License
155 | * Is PCRE compiled with Unicode support?
156 | *
157 | * @return bool
158 | */
159 | protected function hasPcreUnicodeSupport(): bool
160 | {
161 | if (static::$hasPcreUnicodeSupport === null) {
162 | ErrorHandler::start();
163 | static::$hasPcreUnicodeSupport =
164 | defined('PREG_BAD_UTF8_OFFSET_ERROR') && preg_match('/\pL/u', 'a') == 1;
165 | ErrorHandler::stop();
166 | }
167 | return static::$hasPcreUnicodeSupport;
168 | }
169 | }
170 |
--------------------------------------------------------------------------------
/src/php/SAccounts/Transaction/Entries.php:
--------------------------------------------------------------------------------
1 | vUnion(static::create([$entry]));
45 | }
46 |
47 | /**
48 | * Check balance of entries, returns true if they balance else false
49 | *
50 | * @return bool
51 | */
52 | public function checkBalance(): bool
53 | {
54 | $balance = $this->reduce(
55 | function ($carry, Entry $entry) {
56 | $amount = $entry->getAmount();
57 | return AccountType::DR()->equals($entry->getType()) ? $carry - $amount : $carry + $amount;
58 | },
59 | 0
60 | );
61 |
62 | return $balance == 0;
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/php/SAccounts/Transaction/Entry.php:
--------------------------------------------------------------------------------
1 | entryId = $entryId;
55 | $this->amount = $amount;
56 | $this->type = $this->checkType($type)
57 | ->pass()
58 | ->value();
59 | }
60 |
61 | /**
62 | * @return Nominal
63 | */
64 | public function getId(): Nominal
65 | {
66 | return $this->entryId;
67 | }
68 |
69 | /**
70 | * @return int
71 | */
72 | public function getAmount(): int
73 | {
74 | return $this->amount;
75 | }
76 |
77 | /**
78 | * @return AccountType
79 | */
80 | public function getType(): AccountType
81 | {
82 | return $this->type;
83 | }
84 |
85 | /**
86 | * @param AccountType $type
87 | * @return FTry
88 | */
89 | protected function checkType(AccountType $type): FTry
90 | {
91 | return Match::on($type->getValue())
92 | ->test(AccountType::CR, function () {
93 | return FTry::with(AccountType::CR());
94 | })
95 | ->test(AccountType::DR, function () {
96 | return FTry::with(AccountType::DR());
97 | })
98 | ->any(function () {
99 | return FTry::with(new AccountsException(self::ERR_NOTYPE));
100 | })
101 | ->value();
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/src/php/SAccounts/Transaction/SimpleTransaction.php:
--------------------------------------------------------------------------------
1 | addEntry(new Entry($drAc, $amount, AccountType::DR()));
46 | $this->addEntry(new Entry($crAc, $amount, AccountType::CR()));
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/php/SAccounts/Transaction/SplitTransaction.php:
--------------------------------------------------------------------------------
1 | Monad_Option_Some(function ($opt): void {
76 | $this->date = $opt->value();
77 | })
78 | ->Monad_Option_None(function (): void {
79 | $this->date = new \DateTime();
80 | });
81 |
82 | Match::on(Option::create($note))
83 | ->Monad_Option_Some(function ($opt): void {
84 | $this->note = $opt->value();
85 | })
86 | ->Monad_Option_None(function (): void {
87 | $this->note = null;
88 | });
89 |
90 | Match::on(Option::create($src))
91 | ->Monad_Option_Some(function ($opt): void {
92 | $this->src = $opt->value();
93 | })
94 | ->Monad_Option_None(function (): void {
95 | $this->src = null;
96 | });
97 |
98 | Match::on(Option::create($ref))
99 | ->Monad_Option_Some(function ($opt): void {
100 | $this->ref = $opt->value();
101 | })
102 | ->Monad_Option_None(function (): void {
103 | $this->ref = null;
104 | });
105 |
106 | $this->entries = new Entries();
107 | }
108 |
109 | /**
110 | * @param int $txnId
111 | *
112 | * @return SplitTransaction
113 | */
114 | public function setId(int $txnId): SplitTransaction
115 | {
116 | $this->txnId = $txnId;
117 | return $this;
118 | }
119 |
120 | /**
121 | * @return int|null
122 | */
123 | public function getId(): ?int
124 | {
125 | return $this->txnId ?? null;
126 | }
127 |
128 | /**
129 | * @return \DateTime
130 | */
131 | public function getDate(): \DateTime
132 | {
133 | return $this->date;
134 | }
135 |
136 | /**
137 | * @return string
138 | */
139 | public function getNote(): string
140 | {
141 | return $this->note ?? '';
142 | }
143 |
144 | /**
145 | * @return string
146 | */
147 | public function getSrc(): string
148 | {
149 | return $this->src ?? '';
150 | }
151 |
152 | /**
153 | * @return int
154 | */
155 | public function getRef(): int
156 | {
157 | return $this->ref ?? 0;
158 | }
159 |
160 | /**
161 | * Add a transaction entry
162 | *
163 | * @param Entry $entry
164 | *
165 | * @return SplitTransaction
166 | */
167 | public function addEntry(Entry $entry): SplitTransaction
168 | {
169 | $this->entries = $this->entries->addEntry($entry);
170 |
171 | return $this;
172 | }
173 |
174 | /**
175 | * Do the entries balance?
176 | *
177 | * @return bool
178 | */
179 | public function checkBalance(): bool
180 | {
181 | return $this->entries->checkBalance();
182 | }
183 |
184 | /**
185 | * @return Entries
186 | */
187 | public function getEntries(): Entries
188 | {
189 | return $this->entries;
190 | }
191 |
192 | /**
193 | * @param Nominal $id
194 | *
195 | * @return Entry
196 | *
197 | * @throws AccountsException
198 | */
199 | public function getEntry(Nominal $id): Entry
200 | {
201 | $entries = array_values($this->entries->filter(function (Entry $entry) use ($id) {
202 | return $entry->getId()->get() === $id();
203 | })->toArray());
204 |
205 | if (count($entries) == 0) {
206 | throw new AccountsException(self::ERR1);
207 | }
208 |
209 | return $entries[0];
210 | }
211 |
212 |
213 | /**
214 | * Get amount if the account is balanced
215 | *
216 | * @return int
217 | *
218 | * @throw AccountsException
219 | */
220 | public function getAmount(): int
221 | {
222 | return Match::create(Option::create($this->entries->checkBalance(), false))
223 | ->Monad_Option_Some(
224 | function () {
225 | $tot = 0;
226 | foreach ($this->entries as $entry) {
227 | $tot += $entry->getAmount();
228 | }
229 | return $tot / 2;
230 | }
231 | )
232 | ->Monad_Option_None(function (): void {
233 | throw new AccountsException('No amount for unbalanced transaction');
234 | })
235 | ->value();
236 | }
237 |
238 | /**
239 | * Return debit account ids
240 | * return zero, one or more Nominals in an array
241 | *
242 | * @return array [Nominal]
243 | */
244 | public function getDrAc(): array
245 | {
246 | $acs = [];
247 | foreach ($this->getEntries() as $entry) {
248 | if (AccountType::DR()->equals($entry->getType())) {
249 | $acs[] = $entry->getId();
250 | }
251 | }
252 |
253 | return $acs;
254 | }
255 |
256 | /**
257 | * Return credit account ids
258 | * return zero, one or more Nominals in an array
259 | *
260 | * @return array [Nominal]
261 | */
262 | public function getCrAc(): array
263 | {
264 | $acs = [];
265 | foreach ($this->getEntries() as $entry) {
266 | if (AccountType::CR()->equals($entry->getType())) {
267 | $acs[] = $entry->getId();
268 | }
269 | }
270 |
271 | return $acs;
272 | }
273 |
274 | /**
275 | * Is this a simple transaction, i.e. 1 dr and 1 cr entry
276 | *
277 | * @return bool
278 | */
279 | public function isSimple()
280 | {
281 | return count($this->getDrAc()) == 1
282 | && count($this->getCrAc()) == 1;
283 | }
284 | }
285 |
--------------------------------------------------------------------------------
/src/php/SAccounts/Visitor/ChartArray.php:
--------------------------------------------------------------------------------
1 | crcy = !is_null($crcy) ? $crcy : new Currency(0, '', '', 0);
42 | $this->asInt = is_null($crcy);
43 | }
44 |
45 | /**
46 | * @param NodeInterface $node
47 | *
48 | * @return array [[nominal, acName, dr, cr, balance],...]
49 | */
50 | public function visit(NodeInterface $node): array
51 | {
52 | return FFor::create([
53 | 'ret' => [],
54 | 'node' => $node,
55 | 'ac' => $node->getValue()
56 | ])
57 | ->dr(function ($ac) {
58 | return $this->asInt
59 | ? $ac->dr()
60 | : $this->crcy->setValue($ac->dr())->getAsFloat();
61 | })
62 | ->cr(function ($ac) {
63 | return $this->asInt
64 | ? $ac->cr()
65 | : $this->crcy->setValue($ac->cr())->getAsFloat();
66 | })
67 | ->balance(function ($ac) {
68 | return $this->asInt
69 | ? $ac->getBalance()
70 | : $this->crcy->setValue($ac->getBalance())->getAsFloat();
71 | })
72 | ->loop(function ($dr, $cr, $balance, $ac, $node, $ret) {
73 | $ret[] = [
74 | $ac->getNominal()->get(),
75 | $ac->getName(),
76 | $dr,
77 | $cr,
78 | $balance
79 | ];
80 |
81 | foreach ($node->getChildren() as $child) {
82 | $ret = array_merge($ret, $child->accept($this));
83 | }
84 | return $ret;
85 | })
86 | ->fyield('loop');
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/src/php/SAccounts/Visitor/ChartPrinter.php:
--------------------------------------------------------------------------------
1 | crcy = $crcy;
36 | $this->headerPrinted = false;
37 | }
38 |
39 | /**
40 | * @param NodeInterface $node
41 | *
42 | * @return mixed
43 | */
44 | public function visit(NodeInterface $node)
45 | {
46 | if (!$this->headerPrinted) {
47 | echo "Nominal Name DR CR Balance\n";
48 | $this->headerPrinted = true;
49 | }
50 |
51 | /** @var Account $ac */
52 | $ac = $node->getValue();
53 | $nominal = str_pad($ac->getNominal()->get(), 8);
54 | $name = str_pad($ac->getName(), 20);
55 | $dr = str_pad($this->crcy->setValue($ac->dr())->display(), 15, ' ', STR_PAD_LEFT);
56 | $cr = str_pad($this->crcy->setValue($ac->cr())->display(), 15, ' ', STR_PAD_LEFT);
57 | $balStr = $this->crcy->setValue($ac->getBalance())->display();
58 | $balance = str_pad($balStr, 15, ' ', STR_PAD_LEFT);
59 | echo "{$nominal}{$name}{$dr}{$cr}{$balance}\n";
60 |
61 | foreach ($node->getChildren() as $child) {
62 | $child->accept($this);
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/php/SAccounts/Visitor/NodeFinder.php:
--------------------------------------------------------------------------------
1 | valueToFind = $valueToFind->get();
35 | }
36 |
37 | /**
38 | * @param NodeInterface $node
39 | *
40 | * @return NodeInterface|null
41 | */
42 | public function visit(NodeInterface $node): ?NodeInterface
43 | {
44 | $currAc = $node->getValue();
45 |
46 | if ($currAc instanceof Account && $currAc->getNominal()->get() == $this->valueToFind) {
47 | return $node;
48 | }
49 |
50 | foreach ($node->getChildren() as $child) {
51 | /** @noinspection PhpVoidFunctionResultUsedInspection */
52 | $found = $child->accept($this);
53 | if (!is_null($found)) {
54 | return $found;
55 | }
56 | }
57 |
58 | return null;
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/php/SAccounts/Visitor/NodeSaver.php:
--------------------------------------------------------------------------------
1 | chartId = $chartId;
36 | $this->db = $db;
37 | $this->prnt = null;
38 | }
39 |
40 | /**
41 | * @param NodeInterface $node
42 | *
43 | * @return Account|null
44 | */
45 | public function visit(NodeInterface $node): ?Account
46 | {
47 | /** @var Account $currAc */
48 | $currAc = $node->getValue();
49 |
50 | $nominal = $currAc->getNominal()->get();
51 | $type = $currAc->getType()->getKey();
52 | $name = $currAc->getName();
53 | $prntNominal = ($node->isRoot() ? '' : $node->getParent()->getValue()->getNominal()->get());
54 |
55 | $this->db->query(
56 | "call sa_sp_add_ledger({$this->chartId}, '{$nominal}', '{$type}', '{$name}', '{$prntNominal}')",
57 | Adapter::QUERY_MODE_EXECUTE
58 | );
59 |
60 | foreach ($node->getChildren() as $child) {
61 | /** @noinspection PhpVoidFunctionResultUsedInspection */
62 | $child->accept($this);
63 | }
64 |
65 | return $currAc;
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/php/SAccounts/Zend/ErrorHandler.php:
--------------------------------------------------------------------------------
1 | 0) THEN
39 | SIGNAL SQLSTATE '45000' SET MYSQL_ERRNO = 1859, MESSAGE_TEXT = _utf8'Chart already has root account';
40 | END IF;
41 | END IF;
42 |
43 | SET prntId = 0;
44 | IF (prntNominal != '') THEN
45 | SELECT id from sa_coa_ledger l
46 | WHERE l.nominal = prntNominal
47 | AND l.chartId = chartInternalId
48 | INTO prntId;
49 | IF (prntId = 0) THEN
50 | SIGNAL SQLSTATE '45000' SET MYSQL_ERRNO = 1107, MESSAGE_TEXT = _utf8'Invalid parent account nominal';
51 | END IF;
52 | END IF;
53 |
54 | INSERT INTO sa_coa_ledger (`prntId`, `chartId`, `nominal`, `type`, `name`)
55 | VALUES (prntId, chartInternalId, nominal, type, name);
56 | END;
57 | //
58 | DROP PROCEDURE IF EXISTS sa_sp_del_ledger//
59 | CREATE DEFINER = CURRENT_USER PROCEDURE
60 | sa_sp_del_ledger(
61 | chartId INT(10) UNSIGNED,
62 | nominal VARCHAR(10)
63 | )
64 | MODIFIES SQL DATA DETERMINISTIC
65 | BEGIN
66 | DECLARE accId INT(10) UNSIGNED;
67 | DECLARE accDr INT(10) UNSIGNED;
68 | DECLARE accCr INT(10) UNSIGNED;
69 | SELECT id, acDr, acCr FROM sa_coa_ledger l
70 | WHERE l.nominal = nominal
71 | AND l.chartId = chartId
72 | INTO accId, accDr, accCr;
73 | IF (accDr > 0 OR accCr > 0) THEN
74 | SIGNAL SQLSTATE '45000' SET MYSQL_ERRNO = 2000, MESSAGE_TEXT = _utf8'Account balance is non zero';
75 | END IF;
76 |
77 | DELETE FROM sa_coa_ledger
78 | WHERE prntId = accId;
79 |
80 | DELETE FROM sa_coa_ledger
81 | WHERE id = accId;
82 | END;
83 | //
84 | DROP FUNCTION IF EXISTS sa_fu_add_txn;
85 | CREATE DEFINER = CURRENT_USER FUNCTION
86 | sa_fu_add_txn(
87 | chartId INT(10) UNSIGNED,
88 | note TEXT,
89 | date DATETIME,
90 | src VARCHAR(6),
91 | ref INT(10) UNSIGNED,
92 | arNominals TEXT,
93 | arAmounts TEXT,
94 | arTxnType TEXT
95 | )
96 | RETURNS INT(10) UNSIGNED
97 | MODIFIES SQL DATA DETERMINISTIC
98 | BEGIN
99 | DECLARE jrnId INT(10) UNSIGNED;
100 | DECLARE numInArray INT;
101 |
102 | SET date = IFNULL(date, CURRENT_TIMESTAMP);
103 |
104 | INSERT INTO sa_journal (`chartId`, `note`, `date`, `src`, `ref`)
105 | VALUES (chartId, note, date, src, ref);
106 | SELECT last_insert_id() INTO jrnId;
107 |
108 | SET numInArray = char_length(arNominals) - char_length(replace(arNominals, ',', '')) + 1;
109 |
110 | SET @x = numInArray;
111 | REPEAT
112 | SET @txnType = substring_index(substring_index(arTxnType, ',', @x),',',-1);
113 | SET @nominal = substring_index(substring_index(arNominals, ',', @x),',',-1);
114 | SET @drAmount = 0;
115 | SET @crAmount = 0;
116 | IF @txnType = 'dr' THEN
117 | SET @drAmount = substring_index(substring_index(arAmounts, ',', @x),',',-1);
118 | ELSE
119 | SET @crAmount = substring_index(substring_index(arAmounts, ',', @x),',',-1);
120 | END IF;
121 |
122 | INSERT INTO sa_journal_entry(`jrnId`, `nominal`, `acDr`, `acCr`)
123 | VALUE (jrnId, @nominal, @drAmount, @crAmount);
124 | SET @x = @x - 1;
125 | UNTIL @x = 0 END REPEAT;
126 |
127 | RETURN jrnId;
128 | END;
129 | //
130 |
131 | DROP PROCEDURE IF EXISTS sa_sp_get_tree//
132 | CREATE DEFINER = CURRENT_USER PROCEDURE
133 | sa_sp_get_tree(
134 | chartId INT(10) UNSIGNED
135 | )
136 | READS SQL DATA
137 | BEGIN
138 | SELECT origid, destid, l3.nominal, l3.name, l3.type, l3.acDr, l3.acCr from sa_coa_graph
139 | LEFT JOIN sa_coa_ledger as l1 ON origid = l1.id
140 | LEFT JOIN sa_coa_ledger as l3 ON destid = l3.id
141 | WHERE l1.chartId = chartId
142 | UNION
143 | SELECT 0 as origid, min(l2.id) as destid, l2.nominal, l2.name, l2.type, l2.acDr, l2.acCr
144 | FROM sa_coa_ledger as l2
145 | WHERE l2.chartId = chartId
146 | ORDER BY origid, destid;
147 | END;
148 | //
149 | DELIMITER ;
150 |
151 |
--------------------------------------------------------------------------------
/src/sql/mariadb/build-tables.sql:
--------------------------------------------------------------------------------
1 | # Build script for simple accounts database - MariaDb/OQGraph Variant
2 | # Copyright, 2018, Ashley Kitson, UK
3 | # License: BSD-3-Clause, see License.md
4 |
5 | DROP TABLE IF EXISTS sa_coa_ledger;
6 | DROP TABLE IF EXISTS sa_coa_graph;
7 | DROP TABLE IF EXISTS sa_journal_entry;
8 | DROP TABLE IF EXISTS sa_journal;
9 | DROP TABLE IF EXISTS sa_coa;
10 | DROP TABLE IF EXISTS sa_ac_type;
11 |
12 | CREATE TABLE `sa_ac_type` (
13 | `type` varchar(10) NOT NULL COMMENT 'External value of account type',
14 | `value` smallint(6) NOT NULL COMMENT 'Internal value of account type',
15 | PRIMARY KEY (`type`)
16 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='Account type enumeration';
17 |
18 | # Whilst the account type name is really important
19 | # the value is chosen deliberately to provide a BITWISE
20 | # capability
21 | # The main code uses a subset of this functionality
22 | # It is left to you to use it elsewhere
23 | # The values for each account type are chosen for their bit values
24 | # Don't fuck with them
25 | # 00000001011
26 | INSERT INTO sa_ac_type (type, value) VALUES ('ASSET', 11);
27 | # 00000011011
28 | INSERT INTO sa_ac_type (type, value) VALUES ('BANK', 27);
29 | # 00000000101
30 | INSERT INTO sa_ac_type (type, value) VALUES ('CR', 5);
31 | # 00000101100
32 | INSERT INTO sa_ac_type (type, value) VALUES ('CUSTOMER', 44);
33 | # 00000000011
34 | INSERT INTO sa_ac_type (type, value) VALUES ('DR', 3);
35 | # 00000000000
36 | INSERT INTO sa_ac_type (type, value) VALUES ('DUMMY', 0);
37 | # 01010000101
38 | INSERT INTO sa_ac_type (type, value) VALUES ('EQUITY', 645);
39 | # 00001001101
40 | INSERT INTO sa_ac_type (type, value) VALUES ('EXPENSE', 77);
41 | # 00110000101
42 | INSERT INTO sa_ac_type (type, value) VALUES ('INCOME', 389);
43 | # 00010000101
44 | INSERT INTO sa_ac_type (type, value) VALUES ('LIABILITY', 133);
45 | # 00000000001
46 | INSERT INTO sa_ac_type (type, value) VALUES ('REAL', 1);
47 | # 10010000101
48 | INSERT INTO sa_ac_type (type, value) VALUES ('SUPPLIER', 1157);
49 |
50 | CREATE TABLE `sa_coa` (
51 | `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'id of chart',
52 | `name` varchar(20) NOT NULL COMMENT 'name of chart',
53 | PRIMARY KEY (`id`),
54 | UNIQUE KEY `sa_coa_name_uindex` (`name`)
55 | ) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8 COMMENT='A Chart of Account';
56 |
57 | CREATE TABLE `sa_coa_ledger` (
58 | `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'internal ledger id',
59 | `prntId` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'internal id of parent account',
60 | `chartId` int(10) unsigned DEFAULT NULL COMMENT 'id of chart that this account belongs to',
61 | `nominal` char(10) NOT NULL COMMENT 'nominal id for this account',
62 | `type` varchar(10) DEFAULT NULL COMMENT 'type of account',
63 | `name` varchar(30) NOT NULL COMMENT 'name of account',
64 | `acDr` bigint(20) NOT NULL DEFAULT '0' COMMENT 'debit amount',
65 | `acCr` bigint(20) NOT NULL DEFAULT '0'COMMENT 'credit amount',
66 | PRIMARY KEY (`id`),
67 | UNIQUE KEY `sa_coa_ledger_chartId_nominal_index` (`chartId`,`nominal`),
68 | KEY `sa_coa_ledger_sa_ac_type_type_fk` (`type`),
69 | KEY `sa_coa_ledger_sa_coa_fk` (`chartId`),
70 | CONSTRAINT `sa_coa_ledger_sa_ac_type_type_fk` FOREIGN KEY (`type`) REFERENCES `sa_ac_type` (`type`) ON DELETE CASCADE,
71 | CONSTRAINT `sa_coa_ledger_sa_coa_fk` FOREIGN KEY (`chartId`) REFERENCES `sa_coa` (`id`) ON DELETE CASCADE
72 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='Chart of Account structure';
73 |
74 | CREATE TABLE `sa_coa_graph` (
75 | `latch` varchar(32) DEFAULT NULL,
76 | `origid` bigint(20) unsigned DEFAULT NULL,
77 | `destid` bigint(20) unsigned DEFAULT NULL,
78 | `weight` double DEFAULT NULL,
79 | `seq` bigint(20) unsigned DEFAULT NULL,
80 | `linkid` bigint(20) unsigned DEFAULT NULL,
81 | KEY `latch` (`latch`,`origid`,`destid`) USING HASH,
82 | KEY `latch_2` (`latch`,`destid`,`origid`) USING HASH
83 | ) ENGINE=OQGRAPH
84 | DEFAULT CHARSET=utf8 `data_table`='sa_coa_ledger' `origid`='prntId' `destid`='id'
85 | COMMENT 'oqgraph linking table';
86 |
87 | CREATE TABLE `sa_journal` (
88 | `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'internal id of the journal',
89 | `chartId` int(10) unsigned NOT NULL COMMENT 'the chart to which this journal belongs',
90 | `note` text COMMENT 'a note for the journal entry',
91 | `date` datetime DEFAULT CURRENT_TIMESTAMP COMMENT 'timestamp for this journal',
92 | `src` VARCHAR(6) COMMENT 'user defined source of journal',
93 | `ref` INT(10) UNSIGNED COMMENT 'user defined reference to this journal',
94 | PRIMARY KEY (`id`),
95 | KEY `sa_journal_sa_coa_id_fk` (`chartId`),
96 | KEY `sa_journal_external_reference` (`src`, `ref`),
97 | CONSTRAINT `sa_journal_sa_coa_id_fk` FOREIGN KEY (`chartId`) REFERENCES `sa_coa` (`id`) ON DELETE CASCADE
98 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='Txn Journal Header';
99 |
100 | CREATE TABLE `sa_journal_entry` (
101 | `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'internal id for entry',
102 | `jrnId` int(10) unsigned DEFAULT NULL COMMENT 'id if journal that this entry belongs to',
103 | `nominal` varchar(10) NOT NULL COMMENT 'nominal code for entry',
104 | `acDr` bigint(20) DEFAULT '0' COMMENT 'debit amount for entry',
105 | `acCr` bigint(20) DEFAULT '0' COMMENT 'credit amount for entry',
106 | PRIMARY KEY (`id`),
107 | KEY `sa_journal_entry_sa_org_id_fk` (`jrnId`),
108 | CONSTRAINT `sa_journal_entry_sa_jrn_id_fk` FOREIGN KEY (`jrnId`) REFERENCES `sa_journal` (`id`) ON DELETE CASCADE
109 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='Txn Journal Entry';
110 |
--------------------------------------------------------------------------------
/src/sql/mariadb/build-triggers.sql:
--------------------------------------------------------------------------------
1 | # Build script for simple accounts database - MariaDb/OQGraph Variant
2 | # Copyright, 2018, Ashley Kitson, UK
3 | # License: BSD-3-Clause, see License.md
4 |
5 | DELIMITER //
6 | DROP TRIGGER IF EXISTS sp_tr_jrn_entry_updt//
7 | CREATE DEFINER = CURRENT_USER TRIGGER sp_tr_jrn_entry_updt
8 | AFTER INSERT ON sa_journal_entry FOR EACH ROW
9 | BEGIN
10 |
11 | SELECT l.id FROM sa_coa_ledger l
12 | LEFT JOIN sa_journal j ON j.chartId = l.chartId
13 | WHERE l.nominal = NEW.nominal
14 | AND j.id = NEW.jrnId
15 | INTO @acId;
16 |
17 | UPDATE sa_coa_ledger l
18 | SET l.acDr = l.acDr + NEW.acDr,
19 | l.acCr = l.acCr + NEW.acCr
20 | WHERE id IN (
21 | SELECT linkid
22 | FROM sa_coa_graph
23 | WHERE latch = '1'
24 | AND linkid > 0
25 | AND destid = @acId
26 | );
27 | END;
28 | //
29 | DELIMITER ;
30 |
--------------------------------------------------------------------------------
/src/sql/mariadb/drop-procs.sql:
--------------------------------------------------------------------------------
1 | DROP PROCEDURE IF EXISTS sa_sp_add_ledger;
2 | DROP PROCEDURE IF EXISTS sa_sp_del_ledger;
3 | DROP FUNCTION IF EXISTS sa_fu_add_chart;
4 | DROP FUNCTION IF EXISTS sa_fu_add_txn;
5 | DROP PROCEDURE IF EXISTS sa_sp_get_tree;
--------------------------------------------------------------------------------
/src/sql/mariadb/drop-tables.sql:
--------------------------------------------------------------------------------
1 | DROP TABLE IF EXISTS sa_coa_ledger;
2 | DROP TABLE IF EXISTS sa_coa_graph;
3 | DROP TABLE IF EXISTS sa_journal_entry;
4 | DROP TABLE IF EXISTS sa_journal;
5 | DROP TABLE IF EXISTS sa_ac_type;
6 | DROP TABLE IF EXISTS sa_coa;
7 |
--------------------------------------------------------------------------------
/src/sql/mysql/build-procs.sql:
--------------------------------------------------------------------------------
1 | # Build script for simple accounts database - MySql Variant
2 | # Copyright, 2018, Ashley Kitson, UK
3 | # License: BSD-3-Clause, see License.md
4 | DELIMITER //
5 |
6 | DROP FUNCTION IF EXISTS sa_fu_add_chart//
7 | CREATE DEFINER = CURRENT_USER FUNCTION
8 | sa_fu_add_chart(
9 | name VARCHAR(20)
10 | )
11 | RETURNS INT(10) UNSIGNED
12 | MODIFIES SQL DATA DETERMINISTIC
13 | BEGIN
14 | INSERT INTO sa_coa (`name`) VALUES (name);
15 | RETURN last_insert_id();
16 | END;
17 | //
18 |
19 | DROP PROCEDURE IF EXISTS sa_sp_add_ledger//
20 | CREATE DEFINER = CURRENT_USER PROCEDURE
21 | sa_sp_add_ledger(
22 | chartInternalId INT(10) UNSIGNED,
23 | nominal VARCHAR(10),
24 | type VARCHAR(10),
25 | name VARCHAR(30),
26 | prntNominal VARCHAR(10)
27 | )
28 | MODIFIES SQL DATA DETERMINISTIC
29 | BEGIN
30 | DECLARE vPrntId INT(10) UNSIGNED;
31 | DECLARE cntPrnts INT;
32 | DECLARE rightChildId INT;
33 | DECLARE myLeft INT;
34 | DECLARE myRight INT;
35 |
36 | # check to see if we already have a root account
37 | IF (prntNominal = '')
38 | THEN
39 | SELECT count(id)
40 | FROM sa_coa_ledger l
41 | WHERE l.prntId = 0
42 | AND l.chartId = chartInternalId
43 | INTO cntPrnts;
44 |
45 | IF (cntPrnts > 0)
46 | THEN
47 | SIGNAL SQLSTATE '45000'
48 | SET MYSQL_ERRNO = 1859, MESSAGE_TEXT = _utf8'Chart already has root account';
49 | END IF;
50 | END IF;
51 |
52 | SET vPrntId := 0;
53 | # Find the parent ledger id if the nominal id is not empty
54 | # as id cannot be zero, return zero if not found
55 | IF (prntNominal != '')
56 | THEN
57 | SELECT IFNULL((SELECT id
58 | from sa_coa_ledger l
59 | WHERE l.nominal = prntNominal
60 | AND l.chartId = chartInternalId), 0)
61 | INTO vPrntId;
62 |
63 | IF (vPrntId = 0)
64 | THEN
65 | SIGNAL SQLSTATE '45000'
66 | SET MYSQL_ERRNO = 1107, MESSAGE_TEXT = _utf8'Invalid parent account nominal';
67 | END IF;
68 | END IF;
69 |
70 | IF (vPrntId = 0)
71 | THEN
72 | # We are inserting the root node - easy case
73 | INSERT INTO sa_coa_ledger (`prntId`, `lft`, `rgt`, `chartId`, `nominal`, `type`, `name`)
74 | VALUES (0, 1, 2, chartInternalId, nominal, type, name);
75 | ELSE
76 | # Does the parent have any children?
77 | SELECT IFNULL((SELECT max(id)
78 | FROM sa_coa_ledger
79 | WHERE prntId = vPrntId), 0)
80 | INTO rightChildId;
81 |
82 | IF (rightChildId = 0)
83 | THEN
84 | # no children
85 | SELECT lft
86 | FROM sa_coa_ledger
87 | WHERE id = vPrntId
88 | INTO myLeft;
89 |
90 | UPDATE sa_coa_ledger
91 | SET rgt = rgt + 2
92 | WHERE rgt > myLeft;
93 |
94 | UPDATE sa_coa_ledger
95 | SET lft = lft + 2
96 | WHERE lft > myLeft;
97 |
98 | INSERT INTO sa_coa_ledger (`prntId`, `lft`, `rgt`, `chartId`, `nominal`, `type`, `name`)
99 | VALUES
100 | (vPrntId, myLeft + 1, myLeft + 2, chartInternalId, nominal, type, name);
101 | ELSE
102 | # has children, add to right of last child
103 | SELECT rgt
104 | FROM sa_coa_ledger
105 | WHERE id = rightChildId
106 | INTO myRight;
107 |
108 | UPDATE sa_coa_ledger
109 | SET rgt = rgt + 2
110 | WHERE rgt > myRight;
111 |
112 | UPDATE sa_coa_ledger
113 | SET lft = lft + 2
114 | WHERE lft > myRight;
115 |
116 | INSERT INTO sa_coa_ledger (`prntId`, `lft`, `rgt`, `chartId`, `nominal`, `type`, `name`)
117 | VALUES
118 | (vPrntId, myRight + 1, myRight + 2, chartInternalId, nominal, type, name);
119 | END IF;
120 | END IF;
121 | END;
122 | //
123 |
124 | DROP PROCEDURE IF EXISTS sa_sp_del_ledger//
125 | CREATE DEFINER = CURRENT_USER PROCEDURE
126 | sa_sp_del_ledger(
127 | chartId INT(10) UNSIGNED,
128 | nominal VARCHAR(10)
129 | )
130 | MODIFIES SQL DATA DETERMINISTIC
131 | BEGIN
132 | DECLARE accId INT(10) UNSIGNED;
133 | DECLARE accDr INT(10) UNSIGNED;
134 | DECLARE accCr INT(10) UNSIGNED;
135 | SELECT
136 | id,
137 | acDr,
138 | acCr
139 | FROM sa_coa_ledger l
140 | WHERE l.nominal = nominal
141 | AND l.chartId = chartId
142 | INTO accId, accDr, accCr;
143 |
144 | IF (accDr > 0 OR accCr > 0)
145 | THEN
146 | SIGNAL SQLSTATE '45000'
147 | SET MYSQL_ERRNO = 2000, MESSAGE_TEXT = _utf8'Account balance is non zero';
148 | END IF;
149 |
150 | DELETE FROM sa_coa_ledger
151 | WHERE prntId = accId;
152 |
153 | DELETE FROM sa_coa_ledger
154 | WHERE id = accId;
155 | END;
156 | //
157 |
158 | DROP FUNCTION IF EXISTS sa_fu_add_txn//
159 | CREATE DEFINER = CURRENT_USER FUNCTION
160 | sa_fu_add_txn(
161 | chartId INT(10) UNSIGNED,
162 | note TEXT,
163 | date DATETIME,
164 | src VARCHAR(6),
165 | ref INT(10) UNSIGNED,
166 | arNominals TEXT,
167 | arAmounts TEXT,
168 | arTxnType TEXT
169 | )
170 | RETURNS INT(10) UNSIGNED
171 | MODIFIES SQL DATA DETERMINISTIC
172 | BEGIN
173 | DECLARE jrnId INT(10) UNSIGNED;
174 | DECLARE numInArray INT;
175 |
176 | SET date = IFNULL(date, CURRENT_TIMESTAMP);
177 |
178 | INSERT INTO sa_journal (`chartId`, `note`, `date`, `src`, `ref`)
179 | VALUES (chartId, note, date, src, ref);
180 |
181 | SELECT last_insert_id()
182 | INTO jrnId;
183 |
184 | SET numInArray =
185 | char_length(arNominals) - char_length(replace(arNominals, ',', '')) + 1;
186 |
187 | SET @x = numInArray;
188 | REPEAT
189 | SET @txnType = substring_index(substring_index(arTxnType, ',', @x), ',', -1);
190 | SET @nominal = substring_index(substring_index(arNominals, ',', @x), ',', -1);
191 | SET @drAmount = 0;
192 | SET @crAmount = 0;
193 | IF @txnType = 'dr'
194 | THEN
195 | SET @drAmount = substring_index(substring_index(arAmounts, ',', @x), ',',
196 | -1);
197 | ELSE
198 | SET @crAmount = substring_index(substring_index(arAmounts, ',', @x), ',',
199 | -1);
200 | END IF;
201 |
202 | INSERT INTO sa_journal_entry (`jrnId`, `nominal`, `acDr`, `acCr`)
203 | VALUE (jrnId, @nominal, @drAmount, @crAmount);
204 | SET @x = @x - 1;
205 | UNTIL @x = 0 END REPEAT;
206 |
207 | RETURN jrnId;
208 | END;
209 | //
210 |
211 | DROP PROCEDURE IF EXISTS sa_sp_get_tree//
212 | CREATE DEFINER = CURRENT_USER PROCEDURE
213 | sa_sp_get_tree(
214 | chartId INT(10) UNSIGNED
215 | )
216 | READS SQL DATA
217 | BEGIN
218 | SELECT
219 | prntId as origid,
220 | id as destid,
221 | nominal,
222 | name,
223 | type,
224 | acDr,
225 | acCr
226 | FROM sa_coa_ledger
227 | WHERE `chartId` = chartId
228 | ORDER BY origid, destid;
229 | END;
230 | //
231 | DELIMITER ;
232 |
233 |
--------------------------------------------------------------------------------
/src/sql/mysql/build-tables.sql:
--------------------------------------------------------------------------------
1 | # Build script for simple accounts database - MySql variant
2 | # Copyright, 2018, Ashley Kitson, UK
3 | # License: BSD-3-Clause, see License.md
4 |
5 | DROP TABLE IF EXISTS sa_coa_ledger;
6 | DROP TABLE IF EXISTS sa_journal_entry;
7 | DROP TABLE IF EXISTS sa_journal;
8 | DROP TABLE IF EXISTS sa_coa;
9 | DROP TABLE IF EXISTS sa_ac_type;
10 |
11 | CREATE TABLE `sa_ac_type` (
12 | `type` varchar(10) NOT NULL COMMENT 'External value of account type',
13 | `value` smallint(6) NOT NULL COMMENT 'Internal value of account type',
14 | PRIMARY KEY (`type`)
15 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='Account type enumeration';
16 |
17 | # Whilst the account type name is really important
18 | # the value is chosen deliberately to provide a BITWISE
19 | # capability
20 | # The main code uses a subset of this functionality
21 | # It is left to you to use it elsewhere
22 | # The values for each account type are chosen for their bit values
23 | # Don't fuck with them
24 | # 00000001011
25 | INSERT INTO sa_ac_type (type, value) VALUES ('ASSET', 11);
26 | # 00000011011
27 | INSERT INTO sa_ac_type (type, value) VALUES ('BANK', 27);
28 | # 00000000101
29 | INSERT INTO sa_ac_type (type, value) VALUES ('CR', 5);
30 | # 00000101100
31 | INSERT INTO sa_ac_type (type, value) VALUES ('CUSTOMER', 44);
32 | # 00000000011
33 | INSERT INTO sa_ac_type (type, value) VALUES ('DR', 3);
34 | # 00000000000
35 | INSERT INTO sa_ac_type (type, value) VALUES ('DUMMY', 0);
36 | # 01010000101
37 | INSERT INTO sa_ac_type (type, value) VALUES ('EQUITY', 645);
38 | # 00001001101
39 | INSERT INTO sa_ac_type (type, value) VALUES ('EXPENSE', 77);
40 | # 00110000101
41 | INSERT INTO sa_ac_type (type, value) VALUES ('INCOME', 389);
42 | # 00010000101
43 | INSERT INTO sa_ac_type (type, value) VALUES ('LIABILITY', 133);
44 | # 00000000001
45 | INSERT INTO sa_ac_type (type, value) VALUES ('REAL', 1);
46 | # 10010000101
47 | INSERT INTO sa_ac_type (type, value) VALUES ('SUPPLIER', 1157);
48 |
49 | CREATE TABLE `sa_coa` (
50 | `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'id of chart',
51 | `name` varchar(20) NOT NULL COMMENT 'name of chart',
52 | PRIMARY KEY (`id`),
53 | UNIQUE KEY `sa_coa_name_uindex` (`name`)
54 | ) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8 COMMENT='A Chart of Account';
55 |
56 | CREATE TABLE `sa_coa_ledger` (
57 | `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'internal ledger id',
58 | `prntId` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'parent node internal id',
59 | `lft` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'left node internal id',
60 | `rgt` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'node node internal id',
61 | `chartId` int(10) unsigned DEFAULT NULL COMMENT 'id of chart that this account belongs to',
62 | `nominal` char(10) NOT NULL COMMENT 'nominal id for this account',
63 | `type` varchar(10) DEFAULT NULL COMMENT 'type of account',
64 | `name` varchar(30) NOT NULL COMMENT 'name of account',
65 | `acDr` bigint(20) NOT NULL DEFAULT '0' COMMENT 'debit amount',
66 | `acCr` bigint(20) NOT NULL DEFAULT '0'COMMENT 'credit amount',
67 | PRIMARY KEY (`id`),
68 | UNIQUE KEY `sa_coa_ledger_chartId_nominal_index` (`chartId`,`nominal`),
69 | KEY `sa_coa_ledger_sa_ac_type_type_fk` (`type`),
70 | KEY `sa_coa_ledger_sa_coa_fk` (`chartId`),
71 | INDEX `sa_coa_ledger_lft_idx` (`lft`),
72 | INDEX `sa_coa_ledger_rgt_idx` (`rgt`),
73 | CONSTRAINT `sa_coa_ledger_sa_ac_type_type_fk` FOREIGN KEY (`type`) REFERENCES `sa_ac_type` (`type`) ON DELETE CASCADE,
74 | CONSTRAINT `sa_coa_ledger_sa_coa_fk` FOREIGN KEY (`chartId`) REFERENCES `sa_coa` (`id`) ON DELETE CASCADE
75 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='Chart of Account structure';
76 |
77 | CREATE TABLE `sa_journal` (
78 | `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'internal id of the journal',
79 | `chartId` int(10) unsigned NOT NULL COMMENT 'the chart to which this journal belongs',
80 | `note` text COMMENT 'a note for the journal entry',
81 | `date` datetime DEFAULT CURRENT_TIMESTAMP COMMENT 'timestamp for this journal',
82 | `src` VARCHAR(6) COMMENT 'user defined source of journal',
83 | `ref` INT(10) UNSIGNED COMMENT 'user defined reference to this journal',
84 | PRIMARY KEY (`id`),
85 | KEY `sa_journal_sa_coa_id_fk` (`chartId`),
86 | KEY `sa_journal_external_reference` (`src`, `ref`),
87 | CONSTRAINT `sa_journal_sa_coa_id_fk` FOREIGN KEY (`chartId`) REFERENCES `sa_coa` (`id`) ON DELETE CASCADE
88 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='Txn Journal Header';
89 |
90 | CREATE TABLE `sa_journal_entry` (
91 | `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'internal id for entry',
92 | `jrnId` int(10) unsigned DEFAULT NULL COMMENT 'id if journal that this entry belongs to',
93 | `nominal` varchar(10) NOT NULL COMMENT 'nominal code for entry',
94 | `acDr` bigint(20) DEFAULT '0' COMMENT 'debit amount for entry',
95 | `acCr` bigint(20) DEFAULT '0' COMMENT 'credit amount for entry',
96 | PRIMARY KEY (`id`),
97 | KEY `sa_journal_entry_sa_org_id_fk` (`jrnId`),
98 | CONSTRAINT `sa_journal_entry_sa_jrn_id_fk` FOREIGN KEY (`jrnId`) REFERENCES `sa_journal` (`id`) ON DELETE CASCADE
99 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='Txn Journal Entry';
100 |
--------------------------------------------------------------------------------
/src/sql/mysql/build-triggers.sql:
--------------------------------------------------------------------------------
1 | # Build script for simple accounts database - Mysql Variant
2 | # Copyright, 2018, Ashley Kitson, UK
3 | # License: BSD-3-Clause, see License.md
4 |
5 | DELIMITER //
6 | DROP TRIGGER IF EXISTS sp_tr_jrn_entry_updt//
7 | CREATE DEFINER = CURRENT_USER TRIGGER sp_tr_jrn_entry_updt
8 | AFTER INSERT ON sa_journal_entry FOR EACH ROW
9 | BEGIN
10 |
11 | # get the internal ledger id
12 | SELECT l.id FROM sa_coa_ledger l
13 | LEFT JOIN sa_journal j ON j.chartId = l.chartId
14 | WHERE l.nominal = NEW.nominal
15 | AND j.id = NEW.jrnId
16 | INTO @acId;
17 |
18 | # create a concatenated string of parent ids as
19 | # creating temporary tables to hold parent ledger ids
20 | # borks, and you can't select from a table whilst updating
21 |
22 | SELECT GROUP_CONCAT(DISTINCT parent.id SEPARATOR ',')
23 | FROM sa_coa_ledger AS node,
24 | sa_coa_ledger AS parent
25 | WHERE node.lft BETWEEN parent.lft AND parent.rgt
26 | AND node.id = @acId
27 | GROUP BY node.id
28 | INTO @parents;
29 |
30 | SET @numInArray =
31 | char_length(@parents) - char_length(replace(@parents, ',', '')) + 1;
32 |
33 | # update the parent ledger accounts
34 | WHILE (@numInArray > 0)
35 | DO
36 | SET @prntId = substring_index(substring_index(@parents, ',', @numInArray), ',', -1);
37 | UPDATE sa_coa_ledger l
38 | SET l.acDr = l.acDr + NEW.acDr,
39 | l.acCr = l.acCr + NEW.acCr
40 | WHERE id = @prntId;
41 | SET @numInArray = @numInArray - 1;
42 | END WHILE;
43 | END;
44 | //
45 | DELIMITER ;
46 |
--------------------------------------------------------------------------------
/src/sql/mysql/drop-procs.sql:
--------------------------------------------------------------------------------
1 | DROP PROCEDURE IF EXISTS sa_sp_add_ledger;
2 | DROP PROCEDURE IF EXISTS sa_sp_del_ledger;
3 | DROP FUNCTION IF EXISTS sa_fu_add_chart;
4 | DROP FUNCTION IF EXISTS sa_fu_add_txn;
5 | DROP PROCEDURE IF EXISTS sa_sp_get_tree;
--------------------------------------------------------------------------------
/src/sql/mysql/drop-tables.sql:
--------------------------------------------------------------------------------
1 | DROP TABLE IF EXISTS sa_coa_ledger;
2 | DROP TABLE IF EXISTS sa_journal_entry;
3 | DROP TABLE IF EXISTS sa_journal;
4 | DROP TABLE IF EXISTS sa_ac_type;
5 | DROP TABLE IF EXISTS sa_coa;
6 |
--------------------------------------------------------------------------------
/src/xml/personal.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
--------------------------------------------------------------------------------
/src/xsd/chart-definition.xsd:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/test/php/.gitignore:
--------------------------------------------------------------------------------
1 | local*
2 | .phpunit.result.cache
--------------------------------------------------------------------------------
/test/php/SAccounts/AccountTest.php:
--------------------------------------------------------------------------------
1 | sut = new Account(
31 | new Nominal('9999'),
32 | $acType,
33 | 'foo',
34 | 0,
35 | 0
36 | );
37 | $this->assertInstanceOf(Account::class, $this->sut);
38 | }
39 |
40 | public function validAccountTypes()
41 | {
42 | return [
43 | [AccountType::DUMMY()],
44 | [AccountType::DR()],
45 | [AccountType::CR()],
46 | [AccountType::ASSET()],
47 | [AccountType::LIABILITY()],
48 | [AccountType::BANK()],
49 | [AccountType::CUSTOMER()],
50 | [AccountType::EQUITY()],
51 | [AccountType::EXPENSE()],
52 | [AccountType::INCOME()],
53 | [AccountType::REAL()],
54 | [AccountType::SUPPLIER()],
55 | ];
56 | }
57 |
58 | /**
59 | * @dataProvider accountTypesThatHaveBalance
60 | */
61 | public function testYouCanGetABalanceForAccountTypesThatSupportIt(AccountType $acType, $dr, $cr)
62 | {
63 | $this->sut = new Account(
64 | new Nominal('9999'),
65 | $acType,
66 | 'foo',
67 | $dr,
68 | $cr
69 | );
70 | $this->assertIsInt($this->sut->getBalance());
71 | $this->assertEquals(12, $this->sut->getBalance(), "wrong balance for: " . $acType->getKey());
72 | }
73 |
74 | public function accountTypesThatHaveBalance()
75 | {
76 | return [
77 | [AccountType::DR(), 12, 0],
78 | [AccountType::CR(), 0, 12],
79 | [AccountType::ASSET(), 12, 0],
80 | [AccountType::LIABILITY(), 0, 12],
81 | [AccountType::BANK(), 12, 0],
82 | [AccountType::CUSTOMER(), 12, 0],
83 | [AccountType::EQUITY(), 0, 12],
84 | [AccountType::EXPENSE(), 12, 0],
85 | [AccountType::INCOME(), 0, 12],
86 | [AccountType::SUPPLIER(), 0, 12],
87 | ];
88 | }
89 |
90 | public function testGettingBalanceOfADummyAccountTypeWillThrowAnException()
91 | {
92 | $this->sut = new Account(
93 | new Nominal('9999'),
94 | AccountType::DUMMY(),
95 | 'foo',
96 | 0,
97 | 0
98 | );
99 | $this->expectException(AccountsException::class);
100 | $this->sut->getBalance();
101 | }
102 |
103 | public function testYouCanGetTheAccountNominalCode()
104 | {
105 | $this->sut = new Account(
106 | new Nominal('9999'),
107 | AccountType::DUMMY(),
108 | 'foo',
109 | 0,
110 | 0
111 | );
112 | $this->assertEquals(new Nominal('9999'), $this->sut->getNominal());
113 | }
114 |
115 | public function testYouCanGetTheAccountType()
116 | {
117 | $this->sut = new Account(
118 | new Nominal('9999'),
119 | AccountType::DUMMY(),
120 | 'foo',
121 | 0,
122 | 0
123 | );
124 | $this->assertTrue(AccountType::DUMMY()->equals($this->sut->getType()));
125 | }
126 |
127 | public function testYouCanGetTheAccountName()
128 | {
129 | $this->sut = new Account(
130 | new Nominal('9999'),
131 | AccountType::DUMMY(),
132 | 'foo',
133 | 0,
134 | 0
135 | );
136 | $this->assertEquals('foo', $this->sut->getName());
137 | }
138 |
139 | public function testYouCanGetTheDebitAndCreditAmounts()
140 | {
141 | $this->sut = new Account(
142 | new Nominal('9999'),
143 | AccountType::DUMMY(),
144 | 'foo',
145 | 12,
146 | 20
147 | );
148 |
149 | $this->assertEquals(12, $this->sut->dr());
150 | $this->assertEquals(20, $this->sut->cr());
151 | }
152 | }
--------------------------------------------------------------------------------
/test/php/SAccounts/AccountTypeTest.php:
--------------------------------------------------------------------------------
1 | assertIsNumeric(AccountType::REAL);
20 | $this->assertIsNumeric(AccountType::DUMMY);
21 | $this->assertIsNumeric(AccountType::CR);
22 | $this->assertIsNumeric(AccountType::DR);
23 | $this->assertIsNumeric(AccountType::ASSET);
24 | $this->assertIsNumeric(AccountType::BANK);
25 | $this->assertIsNumeric(AccountType::CUSTOMER);
26 | $this->assertIsNumeric(AccountType::EXPENSE);
27 | $this->assertIsNumeric(AccountType::INCOME);
28 | $this->assertIsNumeric(AccountType::LIABILITY);
29 | $this->assertIsNumeric(AccountType::EQUITY);
30 | $this->assertIsNumeric(AccountType::SUPPLIER);
31 | }
32 |
33 | public function testCanGetValuesAsClassesUsingStaticMethods()
34 | {
35 | $this->assertInstanceOf('SAccounts\AccountType', AccountType::REAL());
36 | $this->assertInstanceOf('SAccounts\AccountType', AccountType::CR());
37 | $this->assertInstanceOf('SAccounts\AccountType', AccountType::DR());
38 | $this->assertInstanceOf('SAccounts\AccountType', AccountType::ASSET());
39 | $this->assertInstanceOf('SAccounts\AccountType', AccountType::BANK());
40 | $this->assertInstanceOf('SAccounts\AccountType', AccountType::CUSTOMER());
41 | $this->assertInstanceOf('SAccounts\AccountType', AccountType::EXPENSE());
42 | $this->assertInstanceOf('SAccounts\AccountType', AccountType::INCOME());
43 | $this->assertInstanceOf('SAccounts\AccountType', AccountType::LIABILITY());
44 | $this->assertInstanceOf('SAccounts\AccountType', AccountType::EQUITY());
45 | $this->assertInstanceOf('SAccounts\AccountType', AccountType::SUPPLIER());
46 | }
47 |
48 | /**
49 | * @dataProvider validTitledata
50 | */
51 | public function testCanGetADebitColumnTitleForAValidAccountType($acType, $titles)
52 | {
53 | $ac = new AccountType($acType);
54 | $this->assertEquals($titles['dr'], $ac->drTitle());
55 | }
56 |
57 | public function testGetADebitColumnTitleWithInvalidAccountTypeWillThrowException()
58 | {
59 | $ac = new AccountType(0);
60 | $this->expectException(AccountsException::class);
61 | $ac->drTitle();
62 | }
63 |
64 | public function testGetACreditColumnTitleWithInvalidAccountTypeWillThrowException()
65 | {
66 | $ac = new AccountType(0);
67 | $this->expectException(AccountsException::class);
68 | $ac->crTitle();
69 | }
70 |
71 | /**
72 | * @dataProvider validTitledata
73 | */
74 | public function testCanGetACreditColumnTitleForAValidAccountType($acType, $titles)
75 | {
76 | $ac = new AccountType($acType);
77 | $this->assertEquals($titles['cr'], $ac->crTitle());
78 | }
79 |
80 | public function validTitleData()
81 | {
82 | return [
83 | [AccountType::DR, ['dr'=>'Debit','cr'=>'Credit']],
84 | [AccountType::CR, ['dr'=>'Debit','cr'=>'Credit']],
85 | [AccountType::ASSET, ['dr'=>'Increase','cr'=>'Decrease']],
86 | [AccountType::BANK, ['dr'=>'Increase','cr'=>'Decrease']],
87 | [AccountType::CUSTOMER, ['dr'=>'Increase','cr'=>'Decrease']],
88 | [AccountType::EXPENSE, ['dr'=>'Expense','cr'=>'Refund']],
89 | [AccountType::INCOME, ['dr'=>'Charge','cr'=>'Income']],
90 | [AccountType::LIABILITY, ['dr'=>'Decrease','cr'=>'Increase']],
91 | [AccountType::EQUITY, ['dr'=>'Decrease','cr'=>'Increase']],
92 | [AccountType::SUPPLIER, ['dr'=>'Decrease','cr'=>'Increase']],
93 | ];
94 | }
95 |
96 | /**
97 | * @dataProvider balanceData
98 | */
99 | public function testWillGetCorrectBalanceForAllValidAccountTypes($acType, $dr, $cr, $result)
100 | {
101 | $ac = new AccountType($acType);
102 | $test = $ac->balance($dr, $cr);
103 | $this->assertEquals($result, $test);
104 | }
105 |
106 | public function balanceData()
107 | {
108 | return [
109 | [AccountType::DR, 2, 1, 1],
110 | [AccountType::CR, 1, 2, 1],
111 | [AccountType::ASSET, 2, 1, 1],
112 | [AccountType::BANK, 2, 1, 1],
113 | [AccountType::CUSTOMER, 2, 1, 1],
114 | [AccountType::EXPENSE, 2, 1, 1],
115 | [AccountType::INCOME, 1, 2, 1],
116 | [AccountType::LIABILITY, 1, 2, 1],
117 | [AccountType::EQUITY, 1, 2, 1],
118 | [AccountType::SUPPLIER, 1, 2, 1],
119 | ];
120 | }
121 |
122 | public function testGetABalanceWithInvalidAccountTypeWillThrowException()
123 | {
124 | $ac = new AccountType(AccountType::DUMMY);
125 | $this->expectException(AccountsException::class);
126 | $ac->balance(0, 0);
127 | }
128 |
129 |
130 | }
131 |
--------------------------------------------------------------------------------
/test/php/SAccounts/AccountantTest.php:
--------------------------------------------------------------------------------
1 | 'Pdo_mysql',
40 | 'database' => DBNAME,
41 | 'username' => DBUID,
42 | 'password' => DBPWD,
43 | 'host' => DBHOST
44 | ];
45 | $this->adapter = new Adapter($config);
46 |
47 | $this->sut = new Accountant($this->adapter);
48 |
49 | $this->adapter->query('delete from sa_coa', Adapter::QUERY_MODE_EXECUTE);
50 | }
51 |
52 | public function testAnAccountantCanCreateANewChartOfAccounts()
53 | {
54 | $this->assertIsInt($this->createChart());
55 | }
56 |
57 | public function testAnAccountantCanFetchAChart()
58 | {
59 | $chartId = $this->createChart();
60 | $chart = $this->sut->fetchChart();
61 | $this->assertInstanceOf(
62 | 'SAccounts\Chart',
63 | $chart
64 | );
65 | $this->assertEquals($chartId, $chart->id());
66 | }
67 |
68 | public function testFetchingAChartWhenChartIdIsNotSetWillThrowAnException()
69 | {
70 | $this->expectException(AccountsException::class);
71 | $this->expectExceptionMessage('Chart id not set');
72 | $this->sut->fetchChart();
73 | }
74 |
75 | public function testYouCanWriteATransactionToAJournalAndUpdateAChart()
76 | {
77 | $chartId = $this->createChart();
78 | $txn = new SplitTransaction('test', 'PUR', 10);
79 | $txn->addEntry(new Entry(new Nominal('7100'),1226, AccountType::DR()))
80 | ->addEntry(new Entry(new Nominal('2110'),1226, AccountType::CR()));
81 |
82 | $txnId = $this->sut->writeTransaction($txn);
83 | $journal = $this->adapter->query("select * from sa_journal where id = ?")
84 | ->execute([$txnId])
85 | ->current();
86 | $this->assertEquals($chartId, $journal['chartId']);
87 | $this->assertEquals('test', $journal['note']);
88 | $this->assertEquals(10, $journal['ref']);
89 |
90 | $entries = $this->adapter->query('select * from sa_journal_entry where jrnId = ?')
91 | ->execute([$txnId]);
92 | $this->assertEquals(2, $entries->count());
93 | $entries = $entries->getResource()->fetchAll(\PDO::FETCH_ASSOC);
94 |
95 | $drEntry = array_filter(
96 | $entries,
97 | function($entry) {
98 | return $entry['nominal'] == '7100';
99 | }
100 | );
101 |
102 | $drEntry = array_pop($drEntry);
103 | $this->assertEquals($txnId, $drEntry['jrnId']);
104 | $this->assertEquals(1226, $drEntry['acDr']);
105 | $this->assertEquals(0, $drEntry['acCr']);
106 |
107 | $crEntry = array_filter(
108 | $entries,
109 | function($entry) {
110 | return $entry['nominal'] == '2110';
111 | }
112 | );
113 | $crEntry = array_pop($crEntry);
114 | $this->assertEquals($txnId, $crEntry['jrnId']);
115 | $this->assertEquals(1226, $crEntry['acCr']);
116 | $this->assertEquals(0, $crEntry['acDr']);
117 |
118 | $chart = $this->sut->fetchChart();
119 | $this->assertEquals(
120 | 1226,
121 | $chart->getAccount(new Nominal('7100'))->getBalance()
122 | );
123 | $this->assertEquals(
124 | 1226,
125 | $chart->getAccount(new Nominal('7000'))->getBalance()
126 | );
127 | $this->assertEquals(
128 | 1226,
129 | $chart->getAccount(new Nominal('5000'))->dr()
130 | );
131 | $this->assertEquals(
132 | -1226,
133 | $chart->getAccount(new Nominal('2110'))->getBalance()
134 | );
135 | $this->assertEquals(
136 | -1226,
137 | $chart->getAccount(new Nominal('2100'))->getBalance()
138 | );
139 | $this->assertEquals(
140 | -1226,
141 | $chart->getAccount(new Nominal('2000'))->getBalance()
142 | );
143 | $this->assertEquals(
144 | 1226,
145 | $chart->getAccount(new Nominal('1000'))->cr()
146 | );
147 | $this->assertEquals(
148 | 1226,
149 | $chart->getAccount(new Nominal('0000'))->dr()
150 | );
151 | $this->assertEquals(
152 | 1226,
153 | $chart->getAccount(new Nominal('0000'))->cr()
154 | );
155 | }
156 |
157 | public function testWritingATransactionWhenChartIdIsNotSetWillThrowAnException()
158 | {
159 | $txn = new SplitTransaction('test', 'PUR', 10);
160 | $txn->addEntry(new Entry(new Nominal('7100'),1226, AccountType::DR()))
161 | ->addEntry(new Entry(new Nominal('2110'),1226, AccountType::CR()));
162 |
163 | $this->expectException(AccountsException::class);
164 | $this->expectExceptionMessage('Chart id not set');
165 | $this->sut->writeTransaction($txn);
166 | }
167 |
168 | public function testYouCanFetchAJournalTransactionByItsId()
169 | {
170 | $chartId = $this->createChart();
171 | $txn = new SplitTransaction('test', 'PUR', 10);
172 | $txn->addEntry(new Entry(new Nominal('7100'),1226, AccountType::DR()))
173 | ->addEntry(new Entry(new Nominal('2110'),1226, AccountType::CR()));
174 |
175 | $txnId = $this->sut->writeTransaction($txn);
176 |
177 | $storedTxn = $this->sut->fetchTransaction($txnId);
178 | $this->assertInstanceOf(SplitTransaction::class, $storedTxn);
179 |
180 | $this->assertEquals(10, $storedTxn->getRef());
181 | $this->assertEquals('test', $storedTxn->getNote());
182 | $this->assertEquals($txnId, $storedTxn->getId());
183 |
184 | /** @var Entry $drAc */
185 | $drAc = $storedTxn->getEntry($storedTxn->getDrAc()[0]);
186 | $this->assertEquals('7100', $drAc->getId()->get());
187 | $this->assertEquals(1226, $drAc->getAmount());
188 | $this->assertTrue($drAc->getType()->equals(AccountType::DR()));
189 | /** @var Entry $crAc */
190 | $crAc = $storedTxn->getEntry($storedTxn->getCrAc()[0]);
191 | $this->assertEquals('2110', $crAc->getId()->get());
192 | $this->assertEquals(1226, $crAc->getAmount());
193 | $this->assertTrue($crAc->getType()->equals(AccountType::CR()));
194 | }
195 |
196 | public function testYouCanAddAnAccountToAChart()
197 | {
198 | $this->createChart();
199 | $nominal = new Nominal('7700');
200 | $prntNominal = new Nominal(('7000'));
201 | $chartBefore = $this->sut->fetchChart();
202 |
203 | $this->sut->addAccount($nominal, AccountType::EXPENSE(), 'foo', $prntNominal);
204 | $chartAfter = $this->sut->fetchChart();
205 |
206 | $this->assertFalse($chartBefore->hasAccount($nominal));
207 | $this->assertTrue($chartAfter->hasAccount($nominal));
208 | }
209 |
210 | public function testAddingAnAccountToANonExistentParentWillThrowAnException()
211 | {
212 | $this->createChart();
213 | $nominal = new Nominal('7700');
214 | $prntNominal = new Nominal(('9999'));
215 |
216 | $this->expectException(DbException::class);
217 | $this->expectExceptionMessage('Invalid parent account nominal');
218 | $this->sut->addAccount($nominal, AccountType::EXPENSE(), 'foo', $prntNominal);
219 |
220 | }
221 |
222 | public function testTryingToAddASecondRootAccountWillThrowAnException()
223 | {
224 | $this->createChart();
225 | $nominal = new Nominal(('9999'));
226 |
227 | $this->expectException(DbException::class);
228 | $this->expectExceptionMessage('Chart already has root account');
229 | $this->sut->addAccount($nominal, AccountType::EXPENSE(), 'foo');
230 | }
231 |
232 | public function testYouCanDeleteAZeroBalanceAccount()
233 | {
234 | $this->createChart();
235 | $nominal = new Nominal('3000');
236 | $chartBefore = $this->sut->fetchChart();
237 | $this->sut->delAccount($nominal);
238 | $chartAfter = $this->sut->fetchChart();
239 |
240 | $this->assertTrue($chartBefore->hasAccount($nominal));
241 | $this->assertFalse($chartAfter->hasAccount($nominal));
242 | }
243 |
244 | public function testDeletingANonZeroBalanceAccountWillThrowAnException()
245 | {
246 | $this->createChart();
247 | $nominal = new Nominal('3000');
248 | $txn = new SimpleTransaction(new Nominal('2110'), new Nominal('3100'),1226);
249 | $this->sut->writeTransaction($txn);
250 |
251 | $this->expectException(DbException::class);
252 | $this->expectExceptionMessage('Account balance is non zero');
253 | $this->sut->delAccount($nominal);
254 | }
255 |
256 | public function testYouCanFetchJournalEntriesForAnAccount()
257 | {
258 | $this->createChart();
259 | $this->sut->writeTransaction(
260 | new SimpleTransaction(
261 | new Nominal('2110'), new Nominal('3100'),1226
262 | )
263 | );
264 | $this->sut->writeTransaction(
265 | new SimpleTransaction(
266 | new Nominal('3100'), new Nominal('2110'),1000
267 | )
268 | );
269 | $entries = $this->sut->fetchAccountJournals(new Nominal('3100'));
270 |
271 | $this->assertEquals(2, $entries->count());
272 | }
273 |
274 | public function testFetchingJournalEntriesReturnsASetOfSplitTransactions()
275 | {
276 | $this->createChart();
277 | $this->sut->writeTransaction(
278 | new SimpleTransaction(
279 | new Nominal('2110'), new Nominal('3100'),1226
280 | )
281 | );
282 | $entries = $this->sut->fetchAccountJournals(new Nominal('3100'));
283 |
284 | $this->assertInstanceOf(Set::class, $entries);
285 | $this->assertInstanceOf(SplitTransaction::class, $entries[0]);
286 | }
287 |
288 | public function testFetchingJournalEntriesForAnAggregateAccountWillReturnAnEmptySet()
289 | {
290 | $this->createChart();
291 | $this->sut->writeTransaction(
292 | new SimpleTransaction(
293 | new Nominal('2110'), new Nominal('3100'),1226
294 | )
295 | );
296 | $entries = $this->sut->fetchAccountJournals(new Nominal('3000'));
297 |
298 | $this->assertEquals(0, $entries->count());
299 | }
300 |
301 | protected function createChart()
302 | {
303 | $def = $this->getMockBuilder(ChartDefinition::class)
304 | ->disableOriginalConstructor()
305 | ->getMock();
306 | $xml = <<
308 |
311 |
312 |
313 |
314 |
315 |
316 |
317 |
318 |
319 |
320 |
321 |
322 |
323 |
324 |
325 |
326 |
327 |
328 |
329 |
330 |
331 |
332 |
333 |
334 |
335 |
336 |
337 |
338 |
339 |
340 |
341 |
342 |
343 |
344 | EOT;
345 | $dom = new \DOMDocument();
346 | $dom->loadXML($xml);
347 | $def->expects($this->once())
348 | ->method('getDefinition')
349 | ->willReturn($dom);
350 |
351 | return $this->sut->createChart('Personal', $def);
352 | }
353 | }
354 |
--------------------------------------------------------------------------------
/test/php/SAccounts/ChartDefinitionTest.php:
--------------------------------------------------------------------------------
1 |
26 |
27 | EOT;
28 |
29 | protected function setUp(): void
30 | {
31 | $root = vfsStream::setup();
32 | $file = vfsStream::newFile('test.xml')
33 | ->withContent($this->xmlFile)
34 | ->at($root);
35 | $this->filePath = $file->url();
36 | }
37 |
38 | public function testCanConstructWithValidFileName()
39 | {
40 | $this->assertInstanceOf('SAccounts\ChartDefinition', new ChartDefinition($this->filePath));
41 | }
42 |
43 | public function testConstructionWithInvalidFileNameWillThrowException()
44 | {
45 | $this->expectException(AccountsException::class);
46 | new ChartDefinition('foo');
47 | }
48 |
49 | public function testConstructionWithValidFileNameWillReturnClass()
50 | {
51 | $sut = new ChartDefinition($this->filePath);
52 | $this->assertInstanceOf(ChartDefinition::class, $sut);
53 | }
54 |
55 | public function testGettingTheDefinitionWillThrowExceptionIfDefinitionFileIsInvalidXml()
56 | {
57 | $root = vfsStream::setup();
58 | $file = vfsStream::newFile('test2.xml')
59 | ->withContent('')
60 | ->at($root);
61 | $sut = new ChartDefinition($file->url());
62 |
63 | $this->expectException(AccountsException::class);
64 | $sut->getDefinition();
65 | }
66 |
67 | public function testGettingDefinitionWillThrowExceptionIfDefinitionFailsValidation()
68 | {
69 | $this->expectException(AccountsException::class);
70 | $sut = new ChartDefinition($this->filePath);
71 |
72 | $this->assertInstanceOf('DOMDocument', $sut->getDefinition());
73 | }
74 |
75 | public function testGettingTheDefinitionWillReturnADomDocumentWithValidDefinitionFile()
76 | {
77 | $xml = <<
79 |
80 |
81 |
82 |
83 |
84 | EOT;
85 |
86 | $root = vfsStream::setup();
87 | $file = vfsStream::newFile('test3.xml')
88 | ->withContent($xml)
89 | ->at($root);
90 | $sut = new ChartDefinition($file->url());
91 | $this->assertInstanceOf('DOMDocument', $sut->getDefinition());
92 | }
93 |
94 | }
95 |
--------------------------------------------------------------------------------
/test/php/SAccounts/ChartTest.php:
--------------------------------------------------------------------------------
1 | sut = new Chart('Foo Chart', $tree);
64 | }
65 |
66 | public function testConstructionCreatesChart()
67 | {
68 | $this->assertInstanceOf('SAccounts\Chart', $this->sut);
69 | }
70 |
71 | public function testYouCanGiveAChartAnOptionalTreeInConstruction()
72 | {
73 | $tree = new Node();
74 | $sut = new Chart('Foo Chart', $tree);
75 | $this->assertInstanceOf(Chart::class, $sut);
76 | }
77 |
78 |
79 | public function testYouCanGetAnAccountIfItExists()
80 | {
81 | $ac = $this->sut->getAccount(new Nominal('2000'));
82 | $this->assertEquals('2000', $ac->getNominal()->get());
83 | }
84 |
85 | public function testTryingToGetANonExistentAccountWillThrowAnException()
86 | {
87 | $this->expectException(AccountsException::class);
88 | $this->sut->getAccount(new Nominal('9999'));
89 | }
90 |
91 | public function testYouCanTestIfAChartHasAnAccount()
92 | {
93 | $this->assertTrue($this->sut->hasAccount(new Nominal('1000')));
94 | $this->assertFalse($this->sut->hasAccount(new Nominal('9999')));
95 | }
96 |
97 | public function testTryingToGetAParentIdOfANonExistentAccountWillThrowAnException()
98 | {
99 | $this->expectException(AccountsException::class);
100 | $this->sut->getParentId(new Nominal('9999'));
101 | }
102 |
103 | public function testGettingTheParentIdOfAnAccountThatHasAParentWillReturnTheParentNominal()
104 | {
105 | $this->assertEquals('0000', $this->sut->getParentId(new Nominal('2000'))->get());
106 | }
107 |
108 | public function testYouCanProvideAnOptionalInternalIdWhenConstructingAChart()
109 | {
110 | $sut = new Chart(
111 | 'Foo',
112 | null,
113 | 12
114 | );
115 |
116 | $this->assertEquals(12, $sut->id());
117 | }
118 |
119 | public function testYouCanSetTheChartRootNode()
120 | {
121 | $ac1 = new Account(
122 | new Nominal('9998'),
123 | AccountType::ASSET(),
124 | 'Asset',
125 | 0,
126 | 0
127 | );
128 | $root = new Node($ac1);
129 | $this->sut->setRootNode($root);
130 | $tree = $this->sut->getTree();
131 |
132 | $this->assertEquals($root, $tree);
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/test/php/SAccounts/Transaction/EntriesTest.php:
--------------------------------------------------------------------------------
1 | assertInstanceOf(Entries::class, new Entries());
23 | }
24 |
25 | public function testYouCanCreateAnEntriesCollectionsWithEntryValues()
26 | {
27 | $this->assertInstanceOf(
28 | 'SAccounts\Transaction\Entries',
29 | new Entries(
30 | array(
31 | $this->getEntry('7789', 1234, 'dr'),
32 | $this->getEntry('3456', 617, 'cr'),
33 | $this->getEntry('2001', 617, 'cr'),
34 | )
35 | )
36 | );
37 | }
38 |
39 | public function testYouCannotCreateAnEntriesCollectionWithNonEntryValues()
40 | {
41 | $this->expectException(\RuntimeException::class);
42 | $this->expectExceptionMessage('Value 0 is not a SAccounts\Transaction\Entry');
43 | new Entries(array(new \stdClass()));
44 | }
45 |
46 | public function testYouCanAddAnotherEntryToEntriesAndGetNewEntriesCollection()
47 | {
48 | $sut1 = new Entries(
49 | array(
50 | $this->getEntry('7789', 1234, 'dr'),
51 | $this->getEntry('3456', 617, 'cr'),
52 | $this->getEntry('2001', 617, 'cr'),
53 | )
54 | );
55 |
56 | $sut2 = $sut1->addEntry($this->getEntry('3333',1226,'cr'));
57 |
58 | $this->assertInstanceOf(Entries::class, $sut2);
59 | $this->assertEquals(3, count($sut1));
60 | $this->assertEquals(4, count($sut2));
61 | $this->assertTrue($sut1 != $sut2);
62 | }
63 |
64 | public function testCheckBalanceWillReturnTrueIfEntriesAreBalanced()
65 | {
66 | $sut1 = new Entries(
67 | array(
68 | $this->getEntry('7789', 1234, 'dr'),
69 | $this->getEntry('3456', 617, 'cr'),
70 | $this->getEntry('2001', 617, 'cr'),
71 | )
72 | );
73 |
74 | $this->assertTrue($sut1->checkBalance());
75 | }
76 |
77 | public function testCheckBalanceWillReturnFalseIfEntriesAreNotBalanced()
78 | {
79 | $sut1 = new Entries(
80 | array(
81 | $this->getEntry('7789', 1234, 'dr'),
82 | $this->getEntry('3456', 617, 'cr'),
83 | )
84 | );
85 |
86 | $this->assertFalse($sut1->checkBalance());
87 | }
88 |
89 | protected function getEntry($id, $amount, $type)
90 | {
91 | return new Entry(
92 | new Nominal($id),
93 | $amount,
94 | ($type == 'dr' ? AccountType::DR() : AccountType::CR())
95 | );
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/test/php/SAccounts/Transaction/EntryTest.php:
--------------------------------------------------------------------------------
1 | assertInstanceOf(Entry::class, $sut);
24 | }
25 |
26 | public function testAnEntryMustHaveCrOrDrType()
27 | {
28 | $sut = new Entry(new Nominal('9999'), 0, AccountType::CR());
29 | $this->assertInstanceOf(Entry::class, $sut);
30 | $sut = new Entry(new Nominal('9999'), 0, AccountType::DR());
31 | $this->assertInstanceOf(Entry::class, $sut);
32 | }
33 |
34 | /**
35 | * @dataProvider invalidAccountTypes
36 | * @param AccountType $type
37 | */
38 | public function testConstructingAnEntryWithInvalidTypeWillThrowException($type)
39 | {
40 | $this->expectException(AccountsException::class);
41 | $sut = new Entry(new Nominal('9999'), 0, $type);
42 | }
43 |
44 | public function invalidAccountTypes()
45 | {
46 | return array(
47 | array(AccountType::ASSET()),
48 | array(AccountType::BANK()),
49 | array(AccountType::CUSTOMER()),
50 | array(AccountType::EQUITY()),
51 | array(AccountType::EXPENSE()),
52 | array(AccountType::INCOME()),
53 | array(AccountType::LIABILITY()),
54 | array(AccountType::REAL()),
55 | array(AccountType::SUPPLIER()),
56 | );
57 | }
58 |
59 | public function testYouCanGetTheIdOfAnEntry()
60 | {
61 | $this->assertEquals(
62 | '9999',
63 | (new Entry(new Nominal('9999'), 0, AccountType::CR()))
64 | ->getId()
65 | ->get()
66 | );
67 | }
68 |
69 | public function testYouCanGetTheAmountOfAnEntry()
70 | {
71 | $this->assertEquals(
72 | 100,
73 | (new Entry(new Nominal('9999'), 100, AccountType::CR()))
74 | ->getAmount()
75 | );
76 | }
77 |
78 | public function testYouCanGetTheTypeOfAnEntry()
79 | {
80 | $this->assertEquals(
81 | AccountType::CR(),
82 | (new Entry(new Nominal('9999'), 1, AccountType::CR()))
83 | ->getType()
84 | );
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/test/php/SAccounts/Transaction/SimpleTransactionTest.php:
--------------------------------------------------------------------------------
1 | sut = new SimpleTransaction(new Nominal('0000'), new Nominal('1000'), 1226);
28 | }
29 |
30 | public function testBasicConstructionSetsAnEmptyNoteOnTheTransaction()
31 | {
32 | $this->assertEquals('', $this->sut->getNote());
33 | }
34 |
35 | public function testBasicConstructionSetsDateForTodayOnTheTransaction()
36 | {
37 | $dt = new \DateTime();
38 | $date = $dt->format('yyyy-mm-dd');
39 | $txnDate = $this->sut->getDate()->format('yyyy-mm-dd');
40 | $this->assertEquals($date, $txnDate);
41 | }
42 |
43 | public function testYouCanSetAnOptionalNoteOnConstruction()
44 | {
45 | $note = 'foo bar';
46 | $sut = new SimpleTransaction(new Nominal('0000'), new Nominal('1000'), 1226, $note);
47 | $this->assertEquals($note, $sut->getNote());
48 | }
49 |
50 | public function testYouCanSetAnOptionalSourceOnConstruction()
51 | {
52 | $sut = new SimpleTransaction(
53 | new Nominal('0000'),
54 | new Nominal('1000'),
55 | 1226,
56 | null,
57 | 'PUR'
58 | );
59 | $this->assertEquals('PUR', $sut->getSrc());
60 | }
61 |
62 | public function testYouCanSetAnOptionalReferenceOnConstruction()
63 | {
64 | $sut = new SimpleTransaction(
65 | new Nominal('0000'),
66 | new Nominal('1000'),
67 | 1226,
68 | null,
69 | null,
70 | 22
71 | );
72 | $this->assertEquals(22, $sut->getRef());
73 | }
74 |
75 | public function testYouCanSetAnOptionalDateOnConstruction()
76 | {
77 | $note = 'foo bar';
78 | $dt = new \DateTime();
79 | $sut = new SimpleTransaction(
80 | new Nominal('0000'),
81 | new Nominal('1000'),
82 | 1226,
83 | $note,
84 | null,
85 | null,
86 | $dt);
87 | $this->assertEquals($dt, $sut->getDate());
88 | }
89 |
90 | public function testConstructingATransactionDoesNotSetItsId()
91 | {
92 | $this->assertNull($this->sut->getId());
93 | }
94 |
95 | public function testYouCanSetAndGetAnId()
96 | {
97 | $id = 1;
98 | $this->assertEquals($id, $this->sut->setId($id)->getId());
99 | }
100 |
101 | public function testYouCanGetTheDebitAccountCode()
102 | {
103 | $this->assertEquals('0000', $this->sut->getDrAc()[0]->get());
104 | }
105 |
106 | public function testYouCanGetTheCreditAccountCode()
107 | {
108 | $this->assertEquals('1000', $this->sut->getCrAc()[0]->get());
109 | }
110 |
111 | public function testYouCanGetTheTransactionAmount()
112 | {
113 | $this->assertEquals(1226, $this->sut->getAmount());
114 | }
115 |
116 | public function testYouCanGetTheTransactionNote()
117 | {
118 | $this->assertIsString($this->sut->getNote());
119 | }
120 |
121 | public function testYouCanGetTheTransactionDatetime()
122 | {
123 | $this->assertInstanceOf(\DateTime::class, $this->sut->getDate());
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/test/php/SAccounts/Transaction/SplitTransactionTest.php:
--------------------------------------------------------------------------------
1 | sut = (new SplitTransaction())
29 | ->addEntry(new Entry(new Nominal('0000'), $amount, AccountType::DR()))
30 | ->addEntry(new Entry(new Nominal('1000'), $amount, AccountType::CR()));
31 | }
32 |
33 | public function testBasicConstructionSetsAnEmptyNoteOnTheTransaction()
34 | {
35 | $this->assertEquals('', $this->sut->getNote());
36 | }
37 |
38 | public function testBasicConstructionSetsDateForTodayOnTheTransaction()
39 | {
40 | $dt = new \DateTime();
41 | $date = $dt->format('yyyy-mm-dd');
42 | $txnDate = $this->sut->getDate()->format('yyyy-mm-dd');
43 | $this->assertEquals($date, $txnDate);
44 | }
45 |
46 | public function testYouCanSetAnOptionalNoteOnConstruction()
47 | {
48 | $note = 'foo bar';
49 | $amount = 1226;
50 | $sut = (new SplitTransaction($note))
51 | ->addEntry(new Entry(new Nominal('0000'), $amount, AccountType::DR()))
52 | ->addEntry(new Entry(new Nominal('1000'), $amount, AccountType::CR()));
53 | $this->assertEquals($note, $sut->getNote());
54 | }
55 |
56 | public function testANullNoteWillBeRetrievedAsAnEmptyString()
57 | {
58 | $amount = 1226;
59 | $sut = (new SplitTransaction())
60 | ->addEntry(new Entry(new Nominal('0000'), $amount, AccountType::DR()))
61 | ->addEntry(new Entry(new Nominal('1000'), $amount, AccountType::CR()));
62 | $this->assertEquals('', $sut->getNote());
63 | }
64 |
65 | public function testYouCanSetAnOptionalSourceOnConstruction()
66 | {
67 | $amount = 1226;
68 | $sut = (new SplitTransaction(null, 'PUR'))
69 | ->addEntry(new Entry(new Nominal('0000'), $amount, AccountType::DR()))
70 | ->addEntry(new Entry(new Nominal('1000'), $amount, AccountType::CR()));
71 | $this->assertEquals('PUR', $sut->getSrc());
72 | }
73 |
74 | public function testANullSourceWillBeRetrievedAsAnEmptyString()
75 | {
76 | $amount = 1226;
77 | $sut = (new SplitTransaction())
78 | ->addEntry(new Entry(new Nominal('0000'), $amount, AccountType::DR()))
79 | ->addEntry(new Entry(new Nominal('1000'), $amount, AccountType::CR()));
80 | $this->assertEquals('', $sut->getSrc());
81 | }
82 |
83 | public function testYouCanSetAnOptionalReferenceOnConstruction()
84 | {
85 | $amount = 1226;
86 | $sut = (new SplitTransaction(null, null, 22))
87 | ->addEntry(new Entry(new Nominal('0000'), $amount, AccountType::DR()))
88 | ->addEntry(new Entry(new Nominal('1000'), $amount, AccountType::CR()));
89 | $this->assertEquals(22, $sut->getRef());
90 | }
91 |
92 | public function testANullReferenceWillBeRetrievedAsAZeroInteger()
93 | {
94 | $amount = 1226;
95 | $sut = (new SplitTransaction())
96 | ->addEntry(new Entry(new Nominal('0000'), $amount, AccountType::DR()))
97 | ->addEntry(new Entry(new Nominal('1000'), $amount, AccountType::CR()));
98 | $this->assertEquals(0, $sut->getRef());
99 | }
100 |
101 | public function testYouCanSetAnOptionalDateOnConstruction()
102 | {
103 | $note = 'foo bar';
104 | $dt = new \DateTime();
105 | $amount = 1226;
106 | $sut = (new SplitTransaction($note, null, null, $dt))
107 | ->addEntry(new Entry(new Nominal('0000'), $amount, AccountType::DR()))
108 | ->addEntry(new Entry(new Nominal('1000'), $amount, AccountType::CR()));
109 | $this->assertEquals($dt, $sut->getDate());
110 | }
111 |
112 | public function testConstructingASplitTransactionDoesNotSetItsId()
113 | {
114 | $this->assertNull($this->sut->getId());
115 | }
116 |
117 | public function testYouCanSetAndGetAnId()
118 | {
119 | $id = 1;
120 | $this->assertEquals($id, $this->sut->setId($id)->getId());
121 | }
122 |
123 | public function testGettingTheDebitAccountForASplitTransactionWillReturnAnArrayOfNominals()
124 | {
125 | $codes = $this->sut->getDrAc();
126 | $this->assertIsArray($codes);
127 | $this->assertInstanceOf(Nominal::class, $codes[0]);
128 | }
129 |
130 | public function testGettingTheCreditAccountForASplitTransactionWillReturnAnArrayOfNominals()
131 | {
132 | $codes = $this->sut->getCrAc();
133 | $this->assertIsArray($codes);
134 | $this->assertInstanceOf(Nominal::class, $codes[0]);
135 | }
136 |
137 | public function testCheckingIfASplitTransactionIsBalancedWillReturnTrueIfBalanced()
138 | {
139 | $this->assertTrue($this->sut->checkBalance());
140 | }
141 |
142 | public function testCheckingIfASplitTransactionIsBalancedWillReturnFalseIfNotBalanced()
143 | {
144 | $amount = 10;
145 | $this->sut->addEntry(new Entry(new Nominal('2000'), $amount, AccountType::CR()));
146 | $this->assertFalse($this->sut->checkBalance());
147 | }
148 |
149 | public function testYouCanGetTheTotalTransactionAmountIfTheTransactionIsBalanced()
150 | {
151 | $this->assertEquals(1226, $this->sut->getAmount());
152 | }
153 |
154 | public function testIfTheTransactionIsNotBalancedGettingTheTotalTransactionAmountWillThrowAnException()
155 | {
156 | $this->expectException(AccountsException::class);
157 | $this->sut->addEntry(new Entry(new Nominal('2000'), 10, AccountType::CR()))
158 | ->getAmount();
159 | }
160 |
161 | public function testYouCanGetTheTransactionNote()
162 | {
163 | $this->assertIsString($this->sut->getNote());
164 | }
165 |
166 | public function testYouCanGetTheTransactionDatetime()
167 | {
168 | $this->assertInstanceOf(\DateTime::class, $this->sut->getDate());
169 | }
170 |
171 | public function testASplitTransactionIsSimpleIfItHasOneDrAndOneCrEntry()
172 | {
173 | $this->assertTrue($this->sut->isSimple());
174 | $this->sut->addEntry(new Entry(new Nominal('2000'), 10, AccountType::CR()));
175 | $this->assertFalse($this->sut->isSimple());
176 | }
177 |
178 | public function testYouCanGetAnEntryByItsNominalId()
179 | {
180 | $this->sut->addEntry(new Entry(new Nominal('2000'), 10, AccountType::CR()));
181 | $test = $this->sut->getEntry(new Nominal('2000'));
182 | $this->assertInstanceOf(Entry::class, $test);
183 | }
184 |
185 | public function testGettingAnUnknownEntryWillThrowAnException()
186 | {
187 | $this->expectException(AccountsException::class);
188 | $this->expectExceptionMessage('Entry not found');
189 | $this->sut->getEntry(new Nominal('2000'));
190 | }
191 | }
192 |
--------------------------------------------------------------------------------
/test/php/SAccounts/Visitor/ChartArrayTest.php:
--------------------------------------------------------------------------------
1 | tree = new Node(
29 | new Account(
30 | new Nominal('0000'),
31 | AccountType::REAL(),
32 | 'COA',
33 | 1001,
34 | 1001
35 | ),
36 | [
37 | new Node(
38 | new Account(
39 | new Nominal('1000'),
40 | AccountType::ASSET(),
41 | 'Assets',
42 | 1001,
43 | 0
44 | )
45 | ),
46 | new Node(
47 | new Account(
48 | new Nominal('2000'),
49 | AccountType::LIABILITY(),
50 | 'Liabilities',
51 | 0,
52 | 1001
53 | )
54 | )
55 | ]
56 | );
57 | }
58 |
59 | public function testConstructingWithNoCurrencyWillReturnIntegerValues()
60 | {
61 | $sut = new ChartArray();
62 | $result = $this->tree->accept($sut);
63 | $expected = [
64 | ['0000', 'COA', 1001, 1001, 0],
65 | ['1000', 'Assets', 1001, 0, 1001],
66 | ['2000', 'Liabilities', 0, 1001, 1001]
67 | ];
68 | $this->assertEquals($expected, $result);
69 | }
70 |
71 | public function testConstructingWithACurrencyWillReturnFloatsDependentOnTheCurrencyPrecision()
72 | {
73 | $sut = new ChartArray(new Currency(0,'','',2));
74 | $result = $this->tree->accept($sut);
75 | $expected = [
76 | ['0000', 'COA', 10.01, 10.01, 0],
77 | ['1000', 'Assets', 10.01, 0, 10.01],
78 | ['2000', 'Liabilities', 0, 10.01, 10.01]
79 | ];
80 | $this->assertEquals($expected, $result);
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/test/php/SAccounts/Visitor/ChartPrinterTest.php:
--------------------------------------------------------------------------------
1 | tree = new Node(
34 | new Account(
35 | new Nominal('0000'),
36 | AccountType::REAL(),
37 | 'COA',
38 | 1001,
39 | 1001
40 | ),
41 | [
42 | new Node(
43 | new Account(
44 | new Nominal('1000'),
45 | AccountType::ASSET(),
46 | 'Assets',
47 | 1001,
48 | 0
49 | )
50 | ),
51 | new Node(
52 | new Account(
53 | new Nominal('2000'),
54 | AccountType::LIABILITY(),
55 | 'Liabilities',
56 | 0,
57 | 1001
58 | )
59 | )
60 | ]
61 | );
62 |
63 | $this->sut = new ChartPrinter(new Currency(0, 'GBP', '£'));
64 | }
65 |
66 | public function testTheOutputIsSentToTheConsole()
67 | {
68 | $this->expectOutputRegex('/.*Nominal.*/');
69 | $this->tree->accept($this->sut);
70 | }
71 |
72 | public function testOutputIsFormattedUsingTheCurrencySymbol()
73 | {
74 | $this->expectOutputRegex('/.*£.*/');
75 | $this->tree->accept($this->sut);
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/test/php/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
26 |
27 |
31 |
32 |
33 | ../../src/php/SAccounts
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/test/sql/add_account_test.sql:
--------------------------------------------------------------------------------
1 | # Test script for simple accounts database
2 | # Copyright, 2018, Ashley Kitson, UK
3 | # License: GPL V3+, see License.md
4 |
5 | #before test
6 | DELETE FROM sa_coa;
7 | SET @testName = 'add account';
8 |
9 | #test
10 | SELECT sa_fu_add_chart('Test') INTO @chartId;
11 |
12 | CALL sa_sp_add_ledger(@chartId,'0000', 'REAL', 'COA', '');
13 | CALL sa_sp_add_ledger(@chartId,'1000', 'ASSET', 'Assets', '0000');
14 | CALL sa_sp_add_ledger(@chartId,'2000', 'LIABILITY', 'Liabilities', '0000');
15 |
16 | #results
17 | SELECT count(*) FROM sa_coa INTO @numCharts;
18 | SELECT count(*) FROM sa_coa_ledger INTO @numLedgers;
19 | SELECT id from sa_coa_ledger where nominal = '0000' into @baseId;
20 | SELECT prntId from sa_coa_ledger where nominal = '1000' into @nom1000prnt;
21 | SELECT prntId from sa_coa_ledger where nominal = '2000' into @nom2000prnt;
22 |
23 |
24 | #output
25 | SELECT
26 | @testName as test,
27 | 'number of charts = 1' as spec,
28 | if(@numCharts = 1, 'Passed', 'Failed') as result
29 | UNION SELECT
30 | @testName as test,
31 | 'number of ledgers = 3' as spec,
32 | if(@numLedgers = 3, 'Passed', 'Failed') as result
33 | UNION SELECT
34 | @testName as test,
35 | 'parent id for ac 1000 is correct' as spec,
36 | if(@baseId = @nom1000prnt, 'Passed', 'Failed') as result
37 | UNION SELECT
38 | @testName as test,
39 | 'parent id for ac 2000 is correct' as spec,
40 | if(@baseId = @nom2000prnt, 'Passed', 'Failed') as result
41 | ;
42 |
--------------------------------------------------------------------------------
/test/sql/add_transaction_test.sql:
--------------------------------------------------------------------------------
1 | # Test script for simple accounts database
2 | # Copyright, 2018, Ashley Kitson, UK
3 | # License: GPL V3+, see License.md
4 |
5 | #before test
6 | DELETE FROM sa_coa;
7 | SET @testName = 'add transaction';
8 |
9 | #test
10 | SELECT sa_fu_add_chart('Test')
11 | INTO @chartId;
12 |
13 | CALL sa_sp_add_ledger(@chartId, '0000', 'REAL', 'COA', '');
14 | CALL sa_sp_add_ledger(@chartId, '1000', 'ASSET', 'Assets', '0000');
15 | CALL sa_sp_add_ledger(@chartId, '2000', 'LIABILITY', 'Liabilities', '0000');
16 | #test 1
17 | SELECT sa_fu_add_txn(@chartId, 'foo', NULL, 'PUR', 23 , '1000,2000', '12,12', 'dr,cr')
18 | INTO @jrnId;
19 | #test 2
20 | SELECT (note = 'foo')
21 | FROM sa_journal
22 | INTO @testnote;
23 | #test 3
24 | SELECT !isnull(date)
25 | FROM sa_journal
26 | INTO @testdate;
27 | #test 5
28 | SELECT count(*)
29 | FROM sa_journal_entry
30 | WHERE jrnId = @jrnId
31 | INTO @entryCount;
32 | #test 6
33 | SELECT
34 | acDr,
35 | acCr
36 | FROM sa_coa_ledger
37 | WHERE nominal = '1000'
38 | AND chartId = @chartId
39 | INTO @ac1000dr, @ac1000cr;
40 | SELECT
41 | acDr,
42 | acCr
43 | FROM sa_coa_ledger
44 | WHERE nominal = '2000'
45 | AND chartId = @chartId
46 | INTO @ac2000dr, @ac2000cr;
47 | #test 7
48 | SELECT
49 | acDr,
50 | acCr
51 | FROM sa_coa_ledger
52 | WHERE nominal = '0000'
53 | AND chartId = @chartId
54 | INTO @ac0000dr, @ac0000cr;
55 | #test 8
56 | SELECT (src = 'PUR')
57 | FROM sa_journal
58 | WHERE id = @jrnId
59 | INTO @testsrc;
60 | #test 9
61 | SELECT (ref = 23)
62 | FROM sa_journal
63 | WHERE id = @jrnId
64 | INTO @testref;
65 |
66 | #output
67 | SELECT
68 | @testName AS test,
69 | 'adding journal creates journal header' AS spec,
70 | if(@jrnId > 0, 'Passed', 'Failed') AS result
71 | UNION SELECT
72 | @testName AS test,
73 | 'note is added if supplied' AS spec,
74 | if(@testnote, 'Passed', 'Failed') AS result
75 | UNION SELECT
76 | @testName AS test,
77 | 'src is added if supplied' AS spec,
78 | # @testsrc as result
79 | if(@testsrc, 'Passed', 'Failed') AS result
80 | UNION SELECT
81 | @testName AS test,
82 | 'ref is added if supplied' AS spec,
83 | if(@testref, 'Passed', 'Failed') AS result
84 | UNION SELECT
85 | @testName AS test,
86 | 'date is defaulted if not supplied' AS spec,
87 | if(@testdate, 'Passed', 'Failed') AS result
88 | UNION SELECT
89 | @testName AS test,
90 | 'transaction entries are written' AS spec,
91 | if(@entryCount = 2, 'Passed', 'Failed') AS result
92 | UNION SELECT
93 | @testName AS test,
94 | 'transaction entries update the ledgers' AS spec,
95 | if(@ac1000dr = 12 AND @ac1000cr = 0 AND @ac2000dr = 0 AND @ac2000cr = 12,
96 | 'Passed', 'Failed') AS result
97 | UNION SELECT
98 | @testName AS test,
99 | 'transaction entries update ledger parent accounts' AS spec,
100 | if(@ac0000dr = @ac0000cr AND @ac0000cr > 0,
101 | 'Passed', 'Failed') AS result;
102 |
--------------------------------------------------------------------------------
/test/sql/delete_account_test.sql:
--------------------------------------------------------------------------------
1 | # Test script for simple accounts database
2 | # Copyright, 2018, Ashley Kitson, UK
3 | # License: GPL V3+, see License.md
4 |
5 | #before test
6 | DELETE FROM sa_coa;
7 | SET @testName = 'delete account';
8 |
9 | #test
10 | SELECT sa_fu_add_chart('Test') INTO @chartId;
11 |
12 | CALL sa_sp_add_ledger(@chartId,'0000', 'REAL', 'COA', '');
13 | CALL sa_sp_add_ledger(@chartId,'1000', 'ASSET', 'Assets', '0000');
14 | CALL sa_sp_add_ledger(@chartId,'2000', 'LIABILITY', 'Liabilities', '0000');
15 | #test 1
16 | CALL sa_sp_del_ledger(@chartId,'2000');
17 | SELECT count(*) FROM sa_coa_ledger INTO @numLedgers1;
18 | #test 2
19 | CALL sa_sp_del_ledger(@chartId,'0000');
20 | SELECT count(*) FROM sa_coa_ledger INTO @numLedgers2;
21 |
22 | #output
23 | SELECT
24 | @testName as test,
25 | 'deleting leaf account' as spec,
26 | if(@numLedgers1 = 2, 'Passed', 'Failed') as result
27 | UNION SELECT
28 | @testName as test,
29 | 'deleting parent account' as spec,
30 | if(@numLedgers2 = 0, 'Passed', 'Failed') as result
31 | ;
32 |
--------------------------------------------------------------------------------