# -*- coding: utf-8 -*- # This file is part of the cashbook-planner from m-ds for Tryton. # The COPYRIGHT file at the top level of this repository contains the # full copyright notices and license terms. from decimal import Decimal from unittest.mock import MagicMock from trytond.tests.test_tryton import with_transaction from trytond.pool import Pool from trytond.transaction import Transaction from trytond.exceptions import UserError from datetime import date class PlannerTestCase(object): """ test planner """ def prep_create_job(self, name='Job 1'): pool = Pool() Book = pool.get('cashbook.book') Planner = pool.get('cashbook.planner') Party = pool.get('party.party') types = self.prep_type() company = self.prep_company() job = None with Transaction().set_context({ 'company': company.id, 'nextrun_querydate': date(2022, 5, 1)}): book, = Book.create([{ 'name': 'Book 1', 'btype': types.id, 'company': company.id, 'currency': company.currency.id, 'number_sequ': self.prep_sequence().id, 'start_date': date(2022, 5, 1), }]) self.assertEqual( book.rec_name, 'Book 1 | 0.00 %s | Open' % company.currency.symbol) category = self.prep_category() party, = Party.create([{ 'name': 'Party', 'addresses': [('create', [{}])]}]) job, = Planner.create([{ 'cashbook': book.id, 'name': name, 'start_date': date(2022, 5, 1), 'bookingtype': 'out', 'category': category.id, 'party': party.id, 'subject': 'Booking text'}]) # check applied defaults self.assertEqual(job.rec_name, 'Job 1') self.assertEqual(job.start_date, date(2022, 5, 1)) self.assertEqual(job.end_date, None) self.assertEqual(job.frequ, 'month') self.assertEqual(job.weekday, '99') self.assertEqual(job.monthday, 1) self.assertEqual(job.interval, 1) self.assertEqual(job.setpos, None) self.assertEqual( job.nextdates, '05/01/2022 | 06/01/2022 | 07/01/2022 | 08/01/2022 |' + ' 09/01/2022') return job def prep_planner_asset_book(self): """ prepare asset-cashbook """ pool = Pool() Asset = pool.get('investment.asset') Book = pool.get('cashbook.book') BType = pool.get('cashbook.type') company = self.prep_company() with Transaction().set_context({'company': company.id}): type_depot = self.prep_type('Depot', 'D') BType.write(*[[type_depot], {'feature': 'asset'}]) asset = self.prep_asset_item( company=company, product=self.prep_asset_product(name='Product 1')) self.assertEqual(asset.symbol, 'usd/u') self.assertEqual(company.currency.symbol, 'usd') Asset.write(*[ [asset], { 'rates': [('create', [{ 'date': date(2022, 5, 1), 'rate': Decimal('10.0'), }, { 'date': date(2022, 5, 2), 'rate': Decimal('12.5'), }])], }]) self.assertEqual( asset.rec_name, 'Product 1 | 12.5000 usd/u | 05/02/2022') book, = Book.create([{ 'name': 'Depot', 'btype': type_depot.id, 'company': company.id, 'currency': company.currency.id, 'number_sequ': self.prep_sequence().id, 'start_date': date(2022, 5, 1), 'asset': asset.id, 'quantity_uom': asset.uom.id, }]) self.prep_valstore_run_worker() self.assertEqual( book.rec_name, 'Depot | 0.00 usd | Open | 0.0000 u') return book @with_transaction() def test_planner_create_job(self): """ create job, check rule + constraints """ Planner = Pool().get('cashbook.planner') job = self.prep_create_job() self.assertEqual( job._compute_dates_by_rrule( query_date=date(2022, 5, 1), count=5), [ date(2022, 5, 1), date(2022, 6, 1), date(2022, 7, 1), date(2022, 8, 1), date(2022, 9, 1)]) self.assertRaisesRegex( UserError, r'The value "2022-05-01" for field "End Date" in "Job 1" of ' + r'"Scheduled Booking" is not valid according to its domain\.', Planner.write, *[[job], {'end_date': date(2022, 5, 1)}]) Planner.write(*[[job], { 'end_date': date(2022, 9, 15), 'monthday': 3}]) self.assertEqual( job._compute_dates_by_rrule(query_date=date(2022, 5, 1)), [ date(2022, 5, 3), date(2022, 6, 3), date(2022, 7, 3), date(2022, 8, 3), date(2022, 9, 3)]) Planner.write(*[[job], { 'end_date': date(2022, 9, 15), 'monthday': 3, 'interval': 2}]) self.assertEqual( job._compute_dates_by_rrule(query_date=date(2022, 5, 1)), [ date(2022, 5, 3), date(2022, 7, 3), date(2022, 9, 3)]) # 3rd of each 2nd month Planner.write(*[[job], { 'end_date': None, 'monthday': 1, 'interval': 1}]) self.assertEqual( job._compute_dates_by_rrule( query_date=date(2022, 5, 1), params={ 'end_date': date(2022, 9, 15), 'monthday': 3, 'interval': 2}), [date(2022, 5, 3), date(2022, 7, 3), date(2022, 9, 3)]) # 1st wednesday of each 2nd month self.assertEqual( job._compute_dates_by_rrule( query_date=date(2022, 5, 1), params={ 'end_date': date(2022, 9, 15), 'weekday': '2', 'interval': 2, 'setpos': 1, 'monthday': None}), [date(2022, 5, 4), date(2022, 7, 6), date(2022, 9, 7)]) # 2nd wednesday of each 2nd month self.assertEqual( job._compute_dates_by_rrule( query_date=date(2022, 5, 1), params={ 'end_date': date(2022, 9, 15), 'weekday': '2', 'interval': 2, 'setpos': 2, 'monthday': None}), [date(2022, 5, 11), date(2022, 7, 13), date(2022, 9, 14)]) # 2nd wednesday of each month, 6x occurences self.assertEqual( job._compute_dates_by_rrule( query_date=date(2022, 5, 1), count=6, params={ 'weekday': '2', 'end_date': None, 'interval': 1, 'setpos': 2, 'monthday': None}), [date(2022, 5, 11), date(2022, 6, 8), date(2022, 7, 13), date(2022, 8, 10), date(2022, 9, 14), date(2022, 10, 12)]) Planner.write(*[[job], { 'frequ': 'year', 'start_date': date(2022, 5, 1), 'setpos': None, 'monthday': None, 'interval': 1, 'weekday': '99'}]) # invalid end_date self.assertRaisesRegex( UserError, 'The value "2022-04-30" for field "End Date" in "Job 1" of ' + '"Scheduled Booking" is not valid according to its domain.', Planner.write, *[[job], { 'frequ': 'year', 'start_date': date(2022, 5, 1), 'end_date': date(2022, 4, 30)}]) # monthday and weekday used together self.assertRaisesRegex( UserError, 'The value "2" for field "Day of month" in "Job 1" of ' + '"Scheduled Booking" is not valid according to its domain.', Planner.write, *[[job], { 'frequ': 'month', 'start_date': date(2022, 5, 1), 'monthday': 2, 'weekday': '1', 'end_date': None}]) # monthday out of range 1 self.assertRaisesRegex( UserError, 'The value "0" for field "Day of month" in "Job 1" of ' + '"Scheduled Booking" is not valid according to its domain.', Planner.write, *[[job], { 'frequ': 'year', 'start_date': date(2022, 5, 1), 'monthday': 0, 'weekday': '99', 'end_date': None}]) # monthday out of range 2 self.assertRaisesRegex( UserError, 'The value "32" for field "Day of month" in "Job 1" of ' + '"Scheduled Booking" is not valid according to its domain.', Planner.write, *[[job], { 'frequ': 'year', 'start_date': date(2022, 5, 1), 'monthday': 32, 'weekday': '99', 'end_date': None}]) # invalid usage of setpos self.assertRaisesRegex( UserError, 'The value "1" for field "Occurrence" in "Job 1" of ' + '"Scheduled Booking" is not valid according to its domain.', Planner.write, *[[job], { 'frequ': 'year', 'start_date': date(2022, 5, 1), 'setpos': 1, 'monthday': None, 'weekday': '99', 'end_date': None}]) # setpos out of range 1 self.assertRaisesRegex( UserError, 'The value "0" for field "Occurrence" in "Job 1" of ' + '"Scheduled Booking" is not valid according to its domain.', Planner.write, *[[job], { 'frequ': 'month', 'start_date': date(2022, 5, 1), 'setpos': 0, 'monthday': None, 'weekday': '2', 'end_date': None}]) # setpos out of range 2 self.assertRaisesRegex( UserError, 'The value "5" for field "Occurrence" in "Job 1" of ' + '"Scheduled Booking" is not valid according to its domain.', Planner.write, *[[job], { 'frequ': 'month', 'start_date': date(2022, 5, 1), 'setpos': 5, 'monthday': None, 'weekday': '2', 'end_date': None}]) @with_transaction() def test_planner_create_update_nextrun(self): """ create job, check nextrun-record """ Planner = Pool().get('cashbook.planner') job = self.prep_create_job() self.assertEqual( job._compute_dates_by_rrule( count=1, query_date=date(2022, 5, 1)), [ date(2022, 5, 1)]) Planner.update_next_occurence([job], query_date=date(2022, 5, 25)) self.assertEqual(len(job.nextrun), 1) self.assertEqual(job.nextrun[0].date, date(2022, 6, 1)) Planner.update_next_occurence([job], query_date=date(2022, 5, 30)) self.assertEqual(len(job.nextrun), 1) self.assertEqual(job.nextrun[0].date, date(2022, 6, 1)) Planner.update_next_occurence([job], query_date=date(2022, 6, 1)) self.assertEqual(len(job.nextrun), 1) self.assertEqual(job.nextrun[0].date, date(2022, 6, 1)) # cron will use 'today+1' as query_date Planner.update_next_occurence([job], query_date=date(2022, 6, 2)) self.assertEqual(len(job.nextrun), 1) self.assertEqual(job.nextrun[0].date, date(2022, 7, 1)) with Transaction().set_context({ 'nextrun_querydate': date(2022, 6, 2)}): # set end-date to check delete of future runs Planner.write(*[[job], {'end_date': date(2022, 6, 20)}]) # write to planner-record updates nextrun-records too self.assertEqual(len(job.nextrun), 0) @with_transaction() def test_planner_run_cronjobs(self): """ create job, check cron """ pool = Pool() Planner = pool.get('cashbook.planner') IrDate = pool.get('ir.date') job = self.prep_create_job() self.assertEqual( job._compute_dates_by_rrule( count=1, query_date=date(2022, 5, 1)), [ date(2022, 5, 1)]) # job was not yet run after configure IrDate.today = MagicMock(return_value=date(2022, 5, 24)) Planner.run_booking = MagicMock() job, = Planner.search([]) self.assertEqual(job.nextrun[0].date, date(2022, 5, 1)) Planner.cronjob() self.assertEqual(job.nextrun[0].date, date(2022, 6, 1)) Planner.run_booking.assert_called_with([job]) # next call before due date - nothing should happen IrDate.today = MagicMock(return_value=date(2022, 5, 30)) Planner.run_booking = MagicMock() # restart mock self.assertEqual(job.nextrun[0].date, date(2022, 6, 1)) Planner.cronjob() self.assertEqual(job.nextrun[0].date, date(2022, 6, 1)) Planner.run_booking.assert_not_called() # next call at due date - calls booking and set due date IrDate.today = MagicMock(return_value=date(2022, 6, 1)) Planner.run_booking = MagicMock() # restart mock self.assertEqual(job.nextrun[0].date, date(2022, 6, 1)) Planner.cronjob() self.assertEqual(job.nextrun[0].date, date(2022, 7, 1)) Planner.run_booking.assert_called_with([job]) IrDate.today = MagicMock(return_value=date.today()) @with_transaction() def test_planner_cronjobs_booking_with_category(self): """ create job, configure booking with category, run job """ pool = Pool() Planner = pool.get('cashbook.planner') IrDate = pool.get('ir.date') Category = pool.get('cashbook.category') Cashbook = pool.get('cashbook.book') job = self.prep_create_job() self.assertEqual( job._compute_dates_by_rrule( count=1, query_date=date(2022, 5, 1)), [ date(2022, 5, 1)]) IrDate.today = MagicMock(return_value=date(2022, 5, 24)) category, = Category.search([('name', '=', 'Cat1')]) Planner.write(*[ [job], { 'name': 'Booking to category', 'amount': Decimal('10.0'), 'bookingtype': 'out', 'category': category.id, 'subject': 'booking ${month}/${year}, ${date}', 'wfcheck': True}]) self.assertEqual(job.rec_name, 'Booking to category') self.assertEqual(job.cashbook.rec_name, 'Book 1 | 0.00 usd | Open') self.assertEqual(len(job.cashbook.lines), 0) job, = Planner.search([]) self.assertEqual(job.nextrun[0].date, date(2022, 6, 1)) IrDate.today = MagicMock(return_value=date(2022, 6, 1)) Planner.cronjob() self.assertEqual(job.nextrun[0].date, date(2022, 7, 1)) # check cashbook self.assertEqual(len(job.cashbook.lines), 1) self.assertEqual( job.cashbook.lines[0].rec_name, "06/01/2022|Exp|-10.00 usd|booking " + "${month}/${year}, ${date} [Cat1]") self.assertEqual(job.cashbook.lines[0].state, 'check') with Transaction().set_context({'date': date(2022, 6, 1)}): cashbook, = Cashbook.browse([job.cashbook]) self.assertEqual(cashbook.rec_name, 'Book 1 | -10.00 usd | Open') IrDate.today = MagicMock(return_value=date.today()) @with_transaction() def test_planner_cronjobs_booking_transfer_nonasset(self): """ create job, configure transfer-booking to non-asset-cashbook, run job """ pool = Pool() Planner = pool.get('cashbook.planner') IrDate = pool.get('ir.date') Category = pool.get('cashbook.category') Cashbook = pool.get('cashbook.book') company = self.prep_company() with Transaction().set_context({'company': company.id}): job = self.prep_create_job() target_book, = Cashbook.create([{ 'name': 'Book 2', 'btype': job.cashbook.btype.id, 'company': company.id, 'currency': company.currency.id, 'number_sequ': self.prep_sequence().id, 'start_date': date(2022, 5, 1)}]) self.assertEqual(target_book.rec_name, 'Book 2 | 0.00 usd | Open') self.assertEqual(len(target_book.lines), 0) IrDate.today = MagicMock(return_value=date(2022, 5, 24)) category, = Category.search([('name', '=', 'Cat1')]) Planner.write(*[ [job], { 'name': 'Transfer to Book-2', 'amount': Decimal('10.0'), 'bookingtype': 'mvout', 'category': category.id, 'subject': 'booking ${month}/${year}, ${date}', 'booktransf': target_book.id, 'wfcheck': True}]) self.assertEqual(job.rec_name, 'Transfer to Book-2') self.assertEqual(job.cashbook.rec_name, 'Book 1 | 0.00 usd | Open') self.assertEqual(job.booktransf.rec_name, 'Book 2 | 0.00 usd | Open') self.assertEqual(len(job.cashbook.lines), 0) job, = Planner.search([]) self.assertEqual(job.nextrun[0].date, date(2022, 6, 1)) IrDate.today = MagicMock(return_value=date(2022, 6, 1)) Planner.cronjob() self.assertEqual(job.nextrun[0].date, date(2022, 7, 1)) # check both cashbooks with Transaction().set_context({'date': date(2022, 6, 1)}): cashbook, = Cashbook.browse([job.cashbook]) self.assertEqual(cashbook.rec_name, 'Book 1 | -10.00 usd | Open') self.assertEqual(len(cashbook.lines), 1) self.assertEqual( cashbook.lines[0].rec_name, "06/01/2022|to|-10.00 usd|booking ${month}/${year}, " + "${date} [Book 2 | 10.00 usd | Open]") self.assertEqual(cashbook.lines[0].state, 'check') target_book, = Cashbook.browse([job.booktransf]) self.assertEqual(target_book.rec_name, 'Book 2 | 10.00 usd | Open') self.assertEqual(len(target_book.lines), 1) self.assertEqual( target_book.lines[0].rec_name, "06/01/2022|from|10.00 usd|booking ${month}/${year}, " + "${date} [Book 1 | -10.00 usd | Open]") self.assertEqual(target_book.lines[0].state, 'check') self.assertEqual(target_book.lines[0].state, 'check') IrDate.today = MagicMock(return_value=date.today()) @with_transaction() def test_planner_cronjobs_booking_transfer_nonasset_usd_eur(self): """ create job, configure transfer-booking to non-asset-cashbook, from usd to eur, run job """ pool = Pool() Planner = pool.get('cashbook.planner') IrDate = pool.get('ir.date') Category = pool.get('cashbook.category') Cashbook = pool.get('cashbook.book') company = self.prep_company() with Transaction().set_context({'company': company.id}): (usd, euro) = self.prep_2nd_currency(company) self.assertEqual(len(usd.rates), 1) self.assertEqual(usd.rates[0].rate, Decimal('1.05')) self.assertEqual(usd.rates[0].date, date(2022, 5, 2)) self.assertEqual(euro.rates[0].rate, Decimal('1.0')) self.assertEqual(euro.rates[0].date, date(2022, 5, 2)) self.assertEqual(company.currency.rec_name, 'Euro') job = self.prep_create_job() target_book, = Cashbook.create([{ 'name': 'Book 2', 'btype': job.cashbook.btype.id, 'company': company.id, 'currency': usd.id, 'number_sequ': self.prep_sequence().id, 'start_date': date(2022, 5, 1)}]) self.assertEqual(target_book.rec_name, 'Book 2 | 0.00 usd | Open') self.assertEqual(len(target_book.lines), 0) IrDate.today = MagicMock(return_value=date(2022, 5, 24)) category, = Category.search([('name', '=', 'Cat1')]) Planner.write(*[ [job], { 'name': 'Transfer to Book-2', 'amount': Decimal('10.0'), 'bookingtype': 'mvout', 'category': category.id, 'subject': 'booking 10 € --> 10.5 usd', 'booktransf': target_book.id, 'wfcheck': True}]) self.assertEqual(job.rec_name, 'Transfer to Book-2') self.assertEqual(job.cashbook.rec_name, 'Book 1 | 0.00 € | Open') self.assertEqual(job.booktransf.rec_name, 'Book 2 | 0.00 usd | Open') self.assertEqual(len(job.cashbook.lines), 0) job, = Planner.search([]) self.assertEqual(job.nextrun[0].date, date(2022, 6, 1)) IrDate.today = MagicMock(return_value=date(2022, 6, 1)) Planner.cronjob() self.assertEqual(job.nextrun[0].date, date(2022, 7, 1)) # check both cashbooks with Transaction().set_context({'date': date(2022, 6, 1)}): cashbook, = Cashbook.browse([job.cashbook]) self.assertEqual(cashbook.rec_name, 'Book 1 | -10.00 € | Open') self.assertEqual(len(cashbook.lines), 1) self.assertEqual( cashbook.lines[0].rec_name, "06/01/2022|to|-10.00 €|booking 10 € --> 10.5 usd" + " [Book 2 | 10.50 usd | Open]") self.assertEqual(cashbook.lines[0].state, 'check') target_book, = Cashbook.browse([job.booktransf]) self.assertEqual(target_book.rec_name, 'Book 2 | 10.50 usd | Open') self.assertEqual(len(target_book.lines), 1) self.assertEqual( target_book.lines[0].rec_name, "06/01/2022|from|10.50 usd|booking 10 € --> 10.5 usd" + " [Book 1 | -10.00 € | Open]") self.assertEqual(target_book.lines[0].state, 'check') self.assertEqual(target_book.lines[0].state, 'check') IrDate.today = MagicMock(return_value=date.today()) @with_transaction() def test_planner_cronjobs_booking_transfer_asset(self): """ create job, configure transfer-booking to asset-cashbook, same currencies between cashbooks, same units between asset and cashbook """ pool = Pool() Planner = pool.get('cashbook.planner') IrDate = pool.get('ir.date') Category = pool.get('cashbook.category') Cashbook = pool.get('cashbook.book') company = self.prep_company() with Transaction().set_context({'company': company.id}): job = self.prep_create_job() # rate of asset = 12.5 usd asset_book = self.prep_planner_asset_book() self.assertEqual( asset_book.rec_name, 'Depot | 0.00 usd | Open | 0.0000 u') self.assertEqual(len(asset_book.lines), 0) IrDate.today = MagicMock(return_value=date(2022, 5, 24)) category, = Category.search([('name', '=', 'Cat1')]) Planner.write(*[ [job], { 'name': 'buy asset', 'amount': Decimal('10.0'), 'bookingtype': 'mvout', 'category': category.id, 'subject': 'invest 10.00 usd to buy 0.80 units', 'booktransf': asset_book.id, 'wfcheck': True}]) self.assertEqual(job.rec_name, 'buy asset') self.assertEqual(job.cashbook.rec_name, 'Book 1 | 0.00 usd | Open') self.assertEqual( job.booktransf.rec_name, 'Depot | 0.00 usd | Open | 0.0000 u') self.assertEqual(len(job.cashbook.lines), 0) # check both cashbooks with Transaction().set_context({'date': date(2022, 6, 1)}): job, = Planner.search([]) self.assertEqual(job.nextrun[0].date, date(2022, 6, 1)) IrDate.today = MagicMock(return_value=date(2022, 6, 1)) Planner.cronjob() self.assertEqual(job.nextrun[0].date, date(2022, 7, 1)) cashbook, = Cashbook.browse([job.cashbook]) self.assertEqual(cashbook.rec_name, 'Book 1 | -10.00 usd | Open') self.assertEqual(len(cashbook.lines), 1) self.assertEqual( cashbook.lines[0].rec_name, "06/01/2022|to|-10.00 usd|invest 10.00 usd to buy 0.80 " + "units [Depot | 10.00 usd | Open | 0.8000 u]") self.assertEqual(cashbook.lines[0].state, 'check') asset_book, = Cashbook.browse([job.booktransf]) self.assertEqual( asset_book.rec_name, 'Depot | 10.00 usd | Open | 0.8000 u') self.assertEqual(len(asset_book.lines), 1) self.assertEqual( asset_book.lines[0].rec_name, "06/01/2022|from|10.00 usd|invest 10.00 usd to " + "buy 0.80 units [Book 1 | -10.00 usd | Open]|0.8000 u") self.assertEqual(asset_book.lines[0].state, 'check') self.assertEqual(asset_book.lines[0].state, 'check') IrDate.today = MagicMock(return_value=date.today()) # end PlannerTestCase