Compare commits
	
		
			8 Commits
		
	
	
		
			621020b0f0
			...
			260e3cbe82
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 260e3cbe82 | |||
| cd039520b6 | |||
| 05e652aa62 | |||
| 5c9bf0638c | |||
| bbc78433fd | |||
| 7bcc2b28b2 | |||
| c1d9ca284c | |||
| 165e28441a | 
| @@ -13,7 +13,7 @@ sys.path.insert(0, os.path.abspath('../../src/')) | |||||||
| project = 'Mia! Accounting' | project = 'Mia! Accounting' | ||||||
| copyright = '2023, imacat' | copyright = '2023, imacat' | ||||||
| author = 'imacat' | author = 'imacat' | ||||||
| release = '1.3.2' | release = '1.3.3' | ||||||
|  |  | ||||||
| # -- General configuration --------------------------------------------------- | # -- General configuration --------------------------------------------------- | ||||||
| # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration | ||||||
|   | |||||||
| @@ -17,7 +17,7 @@ | |||||||
|  |  | ||||||
| [project] | [project] | ||||||
| name = "mia-accounting" | name = "mia-accounting" | ||||||
| version = "1.3.2" | version = "1.3.3" | ||||||
| description = "A Flask accounting module." | description = "A Flask accounting module." | ||||||
| readme = "README.rst" | readme = "README.rst" | ||||||
| requires-python = ">=3.11" | requires-python = ">=3.11" | ||||||
|   | |||||||
| @@ -1,306 +0,0 @@ | |||||||
| #! env python3 |  | ||||||
| # The Mia! Accounting Project. |  | ||||||
| # Author: imacat@mail.imacat.idv.tw (imacat), 2023/4/9 |  | ||||||
|  |  | ||||||
| #  Copyright (c) 2023 imacat. |  | ||||||
| # |  | ||||||
| #  Licensed under the Apache License, Version 2.0 (the "License"); |  | ||||||
| #  you may not use this file except in compliance with the License. |  | ||||||
| #  You may obtain a copy of the License at |  | ||||||
| # |  | ||||||
| #      http://www.apache.org/licenses/LICENSE-2.0 |  | ||||||
| # |  | ||||||
| #  Unless required by applicable law or agreed to in writing, software |  | ||||||
| #  distributed under the License is distributed on an "AS IS" BASIS, |  | ||||||
| #  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |  | ||||||
| #  See the License for the specific language governing permissions and |  | ||||||
| #  limitations under the License. |  | ||||||
| """The sample data generation. |  | ||||||
|  |  | ||||||
| """ |  | ||||||
| from datetime import date, timedelta |  | ||||||
|  |  | ||||||
| import click |  | ||||||
|  |  | ||||||
| from testlib import Accounts, create_test_app, JournalEntryLineItemData, \ |  | ||||||
|     JournalEntryCurrencyData, JournalEntryData, \ |  | ||||||
|     BaseTestData |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @click.command() |  | ||||||
| @click.argument("file") |  | ||||||
| def main(file) -> None: |  | ||||||
|     """Creates the sample data and output to a file.""" |  | ||||||
|     data: SampleData = SampleData(create_test_app(), "editor") |  | ||||||
|     with open(file, "wt") as fp: |  | ||||||
|         fp.write(data.json()) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class SampleData(BaseTestData): |  | ||||||
|     """The sample data.""" |  | ||||||
|  |  | ||||||
|     def _init_data(self) -> None: |  | ||||||
|         self.__add_recurring() |  | ||||||
|         self.__add_offsets() |  | ||||||
|         self.__add_meals() |  | ||||||
|  |  | ||||||
|     def __add_recurring(self) -> None: |  | ||||||
|         """Adds the recurring data. |  | ||||||
|  |  | ||||||
|         :return: None. |  | ||||||
|         """ |  | ||||||
|         self.__add_usd_recurring() |  | ||||||
|         self.__add_twd_recurring() |  | ||||||
|  |  | ||||||
|     def __add_usd_recurring(self) -> None: |  | ||||||
|         """Adds the recurring data in USD. |  | ||||||
|  |  | ||||||
|         :return: None. |  | ||||||
|         """ |  | ||||||
|         today: date = date.today() |  | ||||||
|         days: int |  | ||||||
|         year: int |  | ||||||
|         month: int |  | ||||||
|  |  | ||||||
|         # Recurring in USD |  | ||||||
|         j_date: date = date(today.year - 5, today.month, today.day) |  | ||||||
|         j_date = j_date + timedelta(days=(4 - j_date.weekday())) |  | ||||||
|         days = (today - j_date).days |  | ||||||
|         while True: |  | ||||||
|             if days < 0: |  | ||||||
|                 break |  | ||||||
|             self.__add_journal_entry( |  | ||||||
|                 days, "USD", "2600", |  | ||||||
|                 Accounts.BANK, "Transfer", Accounts.SERVICE, "Payroll") |  | ||||||
|  |  | ||||||
|             days = days - 1 |  | ||||||
|             if days < 0: |  | ||||||
|                 break |  | ||||||
|             self.__add_journal_entry( |  | ||||||
|                 days, "USD", "1200", |  | ||||||
|                 Accounts.CASH, None, Accounts.BANK, "Withdraw") |  | ||||||
|             days = days - 13 |  | ||||||
|  |  | ||||||
|         year = today.year - 5 |  | ||||||
|         month = today.month |  | ||||||
|         while True: |  | ||||||
|             month = month + 1 |  | ||||||
|             if month > 12: |  | ||||||
|                 year = year + 1 |  | ||||||
|                 month = 1 |  | ||||||
|             days = (today - date(year, month, 1)).days |  | ||||||
|             if days < 0: |  | ||||||
|                 break |  | ||||||
|             self.__add_journal_entry( |  | ||||||
|                 days, "USD", "1800", |  | ||||||
|                 Accounts.RENT_EXPENSE, "Rent", Accounts.BANK, "Transfer") |  | ||||||
|  |  | ||||||
|     def __add_twd_recurring(self) -> None: |  | ||||||
|         """Adds the recurring data in TWD. |  | ||||||
|  |  | ||||||
|         :return: None. |  | ||||||
|         """ |  | ||||||
|         today: date = date.today() |  | ||||||
|  |  | ||||||
|         year: int = today.year - 5 |  | ||||||
|         month: int = today.month |  | ||||||
|         while True: |  | ||||||
|             days: int = (today - date(year, month, 5)).days |  | ||||||
|             if days < 0: |  | ||||||
|                 break |  | ||||||
|             self.__add_journal_entry( |  | ||||||
|                 days, "TWD", "50000", |  | ||||||
|                 Accounts.BANK, "薪資轉帳", Accounts.SERVICE, "薪水") |  | ||||||
|  |  | ||||||
|             days = days - 1 |  | ||||||
|             if days < 0: |  | ||||||
|                 break |  | ||||||
|             self.__add_journal_entry( |  | ||||||
|                 days, "TWD", "25000", |  | ||||||
|                 Accounts.CASH, None, Accounts.BANK, "提款") |  | ||||||
|  |  | ||||||
|             days = days - 4 |  | ||||||
|             if days < 0: |  | ||||||
|                 break |  | ||||||
|             self.__add_journal_entry( |  | ||||||
|                 days, "TWD", "18000", |  | ||||||
|                 Accounts.RENT_EXPENSE, "房租", Accounts.BANK, "轉帳") |  | ||||||
|  |  | ||||||
|             month = month + 1 |  | ||||||
|             if month > 12: |  | ||||||
|                 year = year + 1 |  | ||||||
|                 month = 1 |  | ||||||
|  |  | ||||||
|     def __add_offsets(self) -> None: |  | ||||||
|         """Adds the offset data. |  | ||||||
|  |  | ||||||
|         :return: None. |  | ||||||
|         """ |  | ||||||
|         days: int |  | ||||||
|         year: int |  | ||||||
|         month: int |  | ||||||
|         description: str |  | ||||||
|         line_item_or: JournalEntryLineItemData |  | ||||||
|         line_item_of: JournalEntryLineItemData |  | ||||||
|  |  | ||||||
|         # Full offset and unmatched in USD |  | ||||||
|         description = "Speaking—Institute" |  | ||||||
|         line_item_or = JournalEntryLineItemData( |  | ||||||
|             Accounts.RECEIVABLE, description, "120") |  | ||||||
|         self._add_journal_entry(JournalEntryData( |  | ||||||
|             40, [JournalEntryCurrencyData( |  | ||||||
|                 "USD", [line_item_or], [JournalEntryLineItemData( |  | ||||||
|                     Accounts.SERVICE, description, "120")])])) |  | ||||||
|         line_item_of = JournalEntryLineItemData( |  | ||||||
|             Accounts.RECEIVABLE, description, "120", |  | ||||||
|             original_line_item=line_item_or) |  | ||||||
|         self._add_journal_entry(JournalEntryData( |  | ||||||
|             5, [JournalEntryCurrencyData( |  | ||||||
|                 "USD", [JournalEntryLineItemData( |  | ||||||
|                     Accounts.BANK, description, "120")], |  | ||||||
|                 [line_item_of])])) |  | ||||||
|         self.__add_journal_entry( |  | ||||||
|             30, "USD", "120", |  | ||||||
|             Accounts.BANK, description, Accounts.SERVICE, description) |  | ||||||
|  |  | ||||||
|         # Partial offset in USD |  | ||||||
|         line_item_or = JournalEntryLineItemData( |  | ||||||
|             Accounts.PAYABLE, "Computer", "1600") |  | ||||||
|         self._add_journal_entry(JournalEntryData( |  | ||||||
|             60, [JournalEntryCurrencyData( |  | ||||||
|                 "USD", [JournalEntryLineItemData( |  | ||||||
|                     Accounts.MACHINERY, "Computer", "1600")], |  | ||||||
|                 [line_item_or])])) |  | ||||||
|         line_item_of = JournalEntryLineItemData( |  | ||||||
|             Accounts.PAYABLE, "Computer", "800", |  | ||||||
|             original_line_item=line_item_or) |  | ||||||
|         self._add_journal_entry(JournalEntryData( |  | ||||||
|             35, [JournalEntryCurrencyData( |  | ||||||
|                 "USD", [line_item_of], [JournalEntryLineItemData( |  | ||||||
|                     Accounts.BANK, "Computer", "800")])])) |  | ||||||
|         line_item_of = JournalEntryLineItemData( |  | ||||||
|             Accounts.PAYABLE, "Computer", "400", |  | ||||||
|             original_line_item=line_item_or) |  | ||||||
|         self._add_journal_entry(JournalEntryData( |  | ||||||
|             10, [JournalEntryCurrencyData( |  | ||||||
|                 "USD", [line_item_of], [JournalEntryLineItemData( |  | ||||||
|                     Accounts.CASH, "Computer", "400")])])) |  | ||||||
|  |  | ||||||
|         # Full offset and unmatched in TWD |  | ||||||
|         description = "演講費—母校" |  | ||||||
|         line_item_or = JournalEntryLineItemData( |  | ||||||
|             Accounts.RECEIVABLE, description, "3000") |  | ||||||
|         self._add_journal_entry(JournalEntryData( |  | ||||||
|             45, [JournalEntryCurrencyData( |  | ||||||
|                 "TWD", [line_item_or], [JournalEntryLineItemData( |  | ||||||
|                     Accounts.SERVICE, description, "3000")])])) |  | ||||||
|         line_item_of = JournalEntryLineItemData( |  | ||||||
|             Accounts.RECEIVABLE, description, "3000", |  | ||||||
|             original_line_item=line_item_or) |  | ||||||
|         self._add_journal_entry(JournalEntryData( |  | ||||||
|             6, [JournalEntryCurrencyData( |  | ||||||
|                 "TWD", [JournalEntryLineItemData( |  | ||||||
|                     Accounts.BANK, description, "3000")], |  | ||||||
|                 [line_item_of])])) |  | ||||||
|         self.__add_journal_entry( |  | ||||||
|             25, "TWD", "3000", |  | ||||||
|             Accounts.BANK, description, Accounts.SERVICE, description) |  | ||||||
|  |  | ||||||
|         # Partial offset in TWD |  | ||||||
|         line_item_or = JournalEntryLineItemData( |  | ||||||
|             Accounts.PAYABLE, "手機", "30000") |  | ||||||
|         self._add_journal_entry(JournalEntryData( |  | ||||||
|             55, [JournalEntryCurrencyData( |  | ||||||
|                 "TWD", [JournalEntryLineItemData( |  | ||||||
|                     Accounts.MACHINERY, "手機", "30000")], |  | ||||||
|                 [line_item_or])])) |  | ||||||
|         line_item_of = JournalEntryLineItemData( |  | ||||||
|             Accounts.PAYABLE, "手機", "16000", |  | ||||||
|             original_line_item=line_item_or) |  | ||||||
|         self._add_journal_entry(JournalEntryData( |  | ||||||
|             27, [JournalEntryCurrencyData( |  | ||||||
|                 "TWD", [line_item_of], [JournalEntryLineItemData( |  | ||||||
|                     Accounts.BANK, "手機", "16000")])])) |  | ||||||
|         line_item_of = JournalEntryLineItemData( |  | ||||||
|             Accounts.PAYABLE, "手機", "6000", |  | ||||||
|             original_line_item=line_item_or) |  | ||||||
|         self._add_journal_entry(JournalEntryData( |  | ||||||
|             8, [JournalEntryCurrencyData( |  | ||||||
|                 "TWD", [line_item_of], [JournalEntryLineItemData( |  | ||||||
|                     Accounts.CASH, "手機", "6000")])])) |  | ||||||
|  |  | ||||||
|     def __add_meals(self) -> None: |  | ||||||
|         """Adds the meal data. |  | ||||||
|  |  | ||||||
|         :return: None. |  | ||||||
|         """ |  | ||||||
|         days = 60 |  | ||||||
|         while days >= 0: |  | ||||||
|             # Meals in USD |  | ||||||
|             if days % 4 == 2: |  | ||||||
|                 self.__add_journal_entry( |  | ||||||
|                     days, "USD", "2.9", |  | ||||||
|                     Accounts.MEAL, "Lunch—Coffee", Accounts.CASH, None) |  | ||||||
|             else: |  | ||||||
|                 self.__add_journal_entry( |  | ||||||
|                     days, "USD", "3.9", |  | ||||||
|                     Accounts.MEAL, "Lunch—Coffee", Accounts.CASH, None) |  | ||||||
|  |  | ||||||
|             if days % 15 == 3: |  | ||||||
|                 self.__add_journal_entry( |  | ||||||
|                     days, "USD", "5.45", |  | ||||||
|                     Accounts.MEAL, "Dinner—Pizza", |  | ||||||
|                     Accounts.PAYABLE, "Dinner—Pizza") |  | ||||||
|             else: |  | ||||||
|                 self.__add_journal_entry( |  | ||||||
|                     days, "USD", "5.9", |  | ||||||
|                     Accounts.MEAL, "Dinner—Pasta", Accounts.CASH, None) |  | ||||||
|  |  | ||||||
|             # Meals in TWD |  | ||||||
|             if days % 5 == 3: |  | ||||||
|                 self.__add_journal_entry( |  | ||||||
|                     days, "TWD", "125", |  | ||||||
|                     Accounts.MEAL, "午餐—鄰家咖啡", Accounts.CASH, None) |  | ||||||
|             else: |  | ||||||
|                 self.__add_journal_entry( |  | ||||||
|                     days, "TWD", "80", |  | ||||||
|                     Accounts.MEAL, "午餐—便當", Accounts.CASH, None) |  | ||||||
|  |  | ||||||
|             if days % 15 == 3: |  | ||||||
|                 self.__add_journal_entry( |  | ||||||
|                     days, "TWD", "320", |  | ||||||
|                     Accounts.MEAL, "晚餐—牛排", Accounts.PAYABLE, "晚餐—牛排") |  | ||||||
|             else: |  | ||||||
|                 self.__add_journal_entry( |  | ||||||
|                     days, "TWD", "100", |  | ||||||
|                     Accounts.MEAL, "晚餐—自助餐", Accounts.CASH, None) |  | ||||||
|  |  | ||||||
|             days = days - 1 |  | ||||||
|  |  | ||||||
|     def __add_journal_entry( |  | ||||||
|             self, days: int, currency: str, amount: str, |  | ||||||
|             debit_account: str, debit_description: str | None, |  | ||||||
|             credit_account: str, credit_description: str | None) -> None: |  | ||||||
|         """Adds a simple journal entry. |  | ||||||
|  |  | ||||||
|         :param days: The number of days before today. |  | ||||||
|         :param currency: The currency code. |  | ||||||
|         :param amount: The amount. |  | ||||||
|         :param debit_account: The debit account code. |  | ||||||
|         :param debit_description: The debit description. |  | ||||||
|         :param credit_account: The credit account code. |  | ||||||
|         :param credit_description: The credit description. |  | ||||||
|         :return: None. |  | ||||||
|         """ |  | ||||||
|         self._add_journal_entry(JournalEntryData( |  | ||||||
|             days, |  | ||||||
|             [JournalEntryCurrencyData( |  | ||||||
|                 currency, |  | ||||||
|                 [JournalEntryLineItemData( |  | ||||||
|                     debit_account, debit_description, amount)], |  | ||||||
|                 [JournalEntryLineItemData( |  | ||||||
|                     credit_account, credit_description, amount)])])) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| if __name__ == "__main__": |  | ||||||
|     main() |  | ||||||
| @@ -26,9 +26,10 @@ import httpx | |||||||
| from flask import Flask | from flask import Flask | ||||||
|  |  | ||||||
| from test_site import db | from test_site import db | ||||||
| from testlib import Accounts, create_test_app, get_client, \ | from test_site.lib import JournalEntryLineItemData, JournalEntryCurrencyData, \ | ||||||
|     match_journal_entry_detail, JournalEntryLineItemData, \ |     JournalEntryData, BaseTestData | ||||||
|     JournalEntryCurrencyData, JournalEntryData, BaseTestData | from testlib import NEXT_URI, Accounts, create_test_app, get_client, \ | ||||||
|  |     match_journal_entry_detail | ||||||
|  |  | ||||||
| PREFIX: str = "/accounting/journal-entries" | PREFIX: str = "/accounting/journal-entries" | ||||||
| """The URL prefix for the journal entry management.""" | """The URL prefix for the journal entry management.""" | ||||||
| @@ -84,14 +85,14 @@ class OffsetTestCase(unittest.TestCase): | |||||||
|                      original_line_item=self.data.l_r_or3d)])]) |                      original_line_item=self.data.l_r_or3d)])]) | ||||||
|  |  | ||||||
|         # Non-existing original line item ID |         # Non-existing original line item ID | ||||||
|         form = journal_entry_data.new_form(self.csrf_token) |         form = journal_entry_data.new_form(self.csrf_token, NEXT_URI) | ||||||
|         form["currency-1-credit-1-original_line_item_id"] = "9999" |         form["currency-1-credit-1-original_line_item_id"] = "9999" | ||||||
|         response = self.client.post(store_uri, data=form) |         response = self.client.post(store_uri, data=form) | ||||||
|         self.assertEqual(response.status_code, 302) |         self.assertEqual(response.status_code, 302) | ||||||
|         self.assertEqual(response.headers["Location"], create_uri) |         self.assertEqual(response.headers["Location"], create_uri) | ||||||
|  |  | ||||||
|         # The same debit or credit |         # The same debit or credit | ||||||
|         form = journal_entry_data.new_form(self.csrf_token) |         form = journal_entry_data.new_form(self.csrf_token, NEXT_URI) | ||||||
|         form["currency-1-credit-1-original_line_item_id"] \ |         form["currency-1-credit-1-original_line_item_id"] \ | ||||||
|             = str(self.data.l_p_or1c.id) |             = str(self.data.l_p_or1c.id) | ||||||
|         form["currency-1-credit-1-account_code"] = self.data.l_p_or1c.account |         form["currency-1-credit-1-account_code"] = self.data.l_p_or1c.account | ||||||
| @@ -106,7 +107,8 @@ class OffsetTestCase(unittest.TestCase): | |||||||
|             account.is_need_offset = False |             account.is_need_offset = False | ||||||
|             db.session.commit() |             db.session.commit() | ||||||
|         response = self.client.post( |         response = self.client.post( | ||||||
|             store_uri, data=journal_entry_data.new_form(self.csrf_token)) |             store_uri, | ||||||
|  |             data=journal_entry_data.new_form(self.csrf_token, NEXT_URI)) | ||||||
|         self.assertEqual(response.status_code, 302) |         self.assertEqual(response.status_code, 302) | ||||||
|         self.assertEqual(response.headers["Location"], create_uri) |         self.assertEqual(response.headers["Location"], create_uri) | ||||||
|         with self.app.app_context(): |         with self.app.app_context(): | ||||||
| @@ -115,7 +117,7 @@ class OffsetTestCase(unittest.TestCase): | |||||||
|             db.session.commit() |             db.session.commit() | ||||||
|  |  | ||||||
|         # The original line item is also an offset |         # The original line item is also an offset | ||||||
|         form = journal_entry_data.new_form(self.csrf_token) |         form = journal_entry_data.new_form(self.csrf_token, NEXT_URI) | ||||||
|         form["currency-1-credit-1-original_line_item_id"] \ |         form["currency-1-credit-1-original_line_item_id"] \ | ||||||
|             = str(self.data.l_p_of1d.id) |             = str(self.data.l_p_of1d.id) | ||||||
|         form["currency-1-credit-1-account_code"] = self.data.l_p_of1d.account |         form["currency-1-credit-1-account_code"] = self.data.l_p_of1d.account | ||||||
| @@ -124,21 +126,21 @@ class OffsetTestCase(unittest.TestCase): | |||||||
|         self.assertEqual(response.headers["Location"], create_uri) |         self.assertEqual(response.headers["Location"], create_uri) | ||||||
|  |  | ||||||
|         # Not the same currency |         # Not the same currency | ||||||
|         form = journal_entry_data.new_form(self.csrf_token) |         form = journal_entry_data.new_form(self.csrf_token, NEXT_URI) | ||||||
|         form["currency-1-code"] = "EUR" |         form["currency-1-code"] = "EUR" | ||||||
|         response = self.client.post(store_uri, data=form) |         response = self.client.post(store_uri, data=form) | ||||||
|         self.assertEqual(response.status_code, 302) |         self.assertEqual(response.status_code, 302) | ||||||
|         self.assertEqual(response.headers["Location"], create_uri) |         self.assertEqual(response.headers["Location"], create_uri) | ||||||
|  |  | ||||||
|         # Not the same account |         # Not the same account | ||||||
|         form = journal_entry_data.new_form(self.csrf_token) |         form = journal_entry_data.new_form(self.csrf_token, NEXT_URI) | ||||||
|         form["currency-1-credit-1-account_code"] = Accounts.NOTES_RECEIVABLE |         form["currency-1-credit-1-account_code"] = Accounts.NOTES_RECEIVABLE | ||||||
|         response = self.client.post(store_uri, data=form) |         response = self.client.post(store_uri, data=form) | ||||||
|         self.assertEqual(response.status_code, 302) |         self.assertEqual(response.status_code, 302) | ||||||
|         self.assertEqual(response.headers["Location"], create_uri) |         self.assertEqual(response.headers["Location"], create_uri) | ||||||
|  |  | ||||||
|         # Not exceeding net balance - partially offset |         # Not exceeding net balance - partially offset | ||||||
|         form = journal_entry_data.new_form(self.csrf_token) |         form = journal_entry_data.new_form(self.csrf_token, NEXT_URI) | ||||||
|         form["currency-1-credit-1-amount"] \ |         form["currency-1-credit-1-amount"] \ | ||||||
|             = str(journal_entry_data.currencies[0].credit[0].amount |             = str(journal_entry_data.currencies[0].credit[0].amount | ||||||
|                   + Decimal("0.01")) |                   + Decimal("0.01")) | ||||||
| @@ -147,7 +149,7 @@ class OffsetTestCase(unittest.TestCase): | |||||||
|         self.assertEqual(response.headers["Location"], create_uri) |         self.assertEqual(response.headers["Location"], create_uri) | ||||||
|  |  | ||||||
|         # Not exceeding net balance - unmatched |         # Not exceeding net balance - unmatched | ||||||
|         form = journal_entry_data.new_form(self.csrf_token) |         form = journal_entry_data.new_form(self.csrf_token, NEXT_URI) | ||||||
|         form["currency-1-credit-3-amount"] \ |         form["currency-1-credit-3-amount"] \ | ||||||
|             = str(journal_entry_data.currencies[0].credit[2].amount |             = str(journal_entry_data.currencies[0].credit[2].amount | ||||||
|                   + Decimal("0.01")) |                   + Decimal("0.01")) | ||||||
| @@ -158,14 +160,14 @@ class OffsetTestCase(unittest.TestCase): | |||||||
|         # Not before the original line items |         # Not before the original line items | ||||||
|         old_days = journal_entry_data.days |         old_days = journal_entry_data.days | ||||||
|         journal_entry_data.days = old_days + 1 |         journal_entry_data.days = old_days + 1 | ||||||
|         form = journal_entry_data.new_form(self.csrf_token) |         form = journal_entry_data.new_form(self.csrf_token, NEXT_URI) | ||||||
|         response = self.client.post(store_uri, data=form) |         response = self.client.post(store_uri, data=form) | ||||||
|         self.assertEqual(response.status_code, 302) |         self.assertEqual(response.status_code, 302) | ||||||
|         self.assertEqual(response.headers["Location"], create_uri) |         self.assertEqual(response.headers["Location"], create_uri) | ||||||
|         journal_entry_data.days = old_days |         journal_entry_data.days = old_days | ||||||
|  |  | ||||||
|         # Success |         # Success | ||||||
|         form = journal_entry_data.new_form(self.csrf_token) |         form = journal_entry_data.new_form(self.csrf_token, NEXT_URI) | ||||||
|         response = self.client.post(store_uri, data=form) |         response = self.client.post(store_uri, data=form) | ||||||
|         self.assertEqual(response.status_code, 302) |         self.assertEqual(response.status_code, 302) | ||||||
|         journal_entry_id: int \ |         journal_entry_id: int \ | ||||||
| @@ -194,14 +196,14 @@ class OffsetTestCase(unittest.TestCase): | |||||||
|         journal_entry_data.currencies[0].credit[2].amount = Decimal("600") |         journal_entry_data.currencies[0].credit[2].amount = Decimal("600") | ||||||
|  |  | ||||||
|         # Non-existing original line item ID |         # Non-existing original line item ID | ||||||
|         form = journal_entry_data.update_form(self.csrf_token) |         form = journal_entry_data.update_form(self.csrf_token, NEXT_URI) | ||||||
|         form["currency-1-credit-1-original_line_item_id"] = "9999" |         form["currency-1-credit-1-original_line_item_id"] = "9999" | ||||||
|         response = self.client.post(update_uri, data=form) |         response = self.client.post(update_uri, data=form) | ||||||
|         self.assertEqual(response.status_code, 302) |         self.assertEqual(response.status_code, 302) | ||||||
|         self.assertEqual(response.headers["Location"], edit_uri) |         self.assertEqual(response.headers["Location"], edit_uri) | ||||||
|  |  | ||||||
|         # The same debit or credit |         # The same debit or credit | ||||||
|         form = journal_entry_data.update_form(self.csrf_token) |         form = journal_entry_data.update_form(self.csrf_token, NEXT_URI) | ||||||
|         form["currency-1-credit-1-original_line_item_id"] \ |         form["currency-1-credit-1-original_line_item_id"] \ | ||||||
|             = str(self.data.l_p_or1c.id) |             = str(self.data.l_p_or1c.id) | ||||||
|         form["currency-1-credit-1-account_code"] = self.data.l_p_or1c.account |         form["currency-1-credit-1-account_code"] = self.data.l_p_or1c.account | ||||||
| @@ -217,7 +219,8 @@ class OffsetTestCase(unittest.TestCase): | |||||||
|             account.is_need_offset = False |             account.is_need_offset = False | ||||||
|             db.session.commit() |             db.session.commit() | ||||||
|         response = self.client.post( |         response = self.client.post( | ||||||
|             update_uri, data=journal_entry_data.update_form(self.csrf_token)) |             update_uri, | ||||||
|  |             data=journal_entry_data.update_form(self.csrf_token, NEXT_URI)) | ||||||
|         self.assertEqual(response.status_code, 302) |         self.assertEqual(response.status_code, 302) | ||||||
|         self.assertEqual(response.headers["Location"], edit_uri) |         self.assertEqual(response.headers["Location"], edit_uri) | ||||||
|         with self.app.app_context(): |         with self.app.app_context(): | ||||||
| @@ -226,7 +229,7 @@ class OffsetTestCase(unittest.TestCase): | |||||||
|             db.session.commit() |             db.session.commit() | ||||||
|  |  | ||||||
|         # The original line item is also an offset |         # The original line item is also an offset | ||||||
|         form = journal_entry_data.update_form(self.csrf_token) |         form = journal_entry_data.update_form(self.csrf_token, NEXT_URI) | ||||||
|         form["currency-1-credit-1-original_line_item_id"] \ |         form["currency-1-credit-1-original_line_item_id"] \ | ||||||
|             = str(self.data.l_p_of1d.id) |             = str(self.data.l_p_of1d.id) | ||||||
|         form["currency-1-credit-1-account_code"] = self.data.l_p_of1d.account |         form["currency-1-credit-1-account_code"] = self.data.l_p_of1d.account | ||||||
| @@ -235,21 +238,21 @@ class OffsetTestCase(unittest.TestCase): | |||||||
|         self.assertEqual(response.headers["Location"], edit_uri) |         self.assertEqual(response.headers["Location"], edit_uri) | ||||||
|  |  | ||||||
|         # Not the same currency |         # Not the same currency | ||||||
|         form = journal_entry_data.update_form(self.csrf_token) |         form = journal_entry_data.update_form(self.csrf_token, NEXT_URI) | ||||||
|         form["currency-1-code"] = "EUR" |         form["currency-1-code"] = "EUR" | ||||||
|         response = self.client.post(update_uri, data=form) |         response = self.client.post(update_uri, data=form) | ||||||
|         self.assertEqual(response.status_code, 302) |         self.assertEqual(response.status_code, 302) | ||||||
|         self.assertEqual(response.headers["Location"], edit_uri) |         self.assertEqual(response.headers["Location"], edit_uri) | ||||||
|  |  | ||||||
|         # Not the same account |         # Not the same account | ||||||
|         form = journal_entry_data.update_form(self.csrf_token) |         form = journal_entry_data.update_form(self.csrf_token, NEXT_URI) | ||||||
|         form["currency-1-credit-1-account_code"] = Accounts.NOTES_RECEIVABLE |         form["currency-1-credit-1-account_code"] = Accounts.NOTES_RECEIVABLE | ||||||
|         response = self.client.post(update_uri, data=form) |         response = self.client.post(update_uri, data=form) | ||||||
|         self.assertEqual(response.status_code, 302) |         self.assertEqual(response.status_code, 302) | ||||||
|         self.assertEqual(response.headers["Location"], edit_uri) |         self.assertEqual(response.headers["Location"], edit_uri) | ||||||
|  |  | ||||||
|         # Not exceeding net balance - partially offset |         # Not exceeding net balance - partially offset | ||||||
|         form = journal_entry_data.update_form(self.csrf_token) |         form = journal_entry_data.update_form(self.csrf_token, NEXT_URI) | ||||||
|         form["currency-1-debit-1-amount"] \ |         form["currency-1-debit-1-amount"] \ | ||||||
|             = str(journal_entry_data.currencies[0].debit[0].amount |             = str(journal_entry_data.currencies[0].debit[0].amount | ||||||
|                   + Decimal("0.01")) |                   + Decimal("0.01")) | ||||||
| @@ -261,7 +264,7 @@ class OffsetTestCase(unittest.TestCase): | |||||||
|         self.assertEqual(response.headers["Location"], edit_uri) |         self.assertEqual(response.headers["Location"], edit_uri) | ||||||
|  |  | ||||||
|         # Not exceeding net balance - unmatched |         # Not exceeding net balance - unmatched | ||||||
|         form = journal_entry_data.update_form(self.csrf_token) |         form = journal_entry_data.update_form(self.csrf_token, NEXT_URI) | ||||||
|         form["currency-1-debit-3-amount"] \ |         form["currency-1-debit-3-amount"] \ | ||||||
|             = str(journal_entry_data.currencies[0].debit[2].amount |             = str(journal_entry_data.currencies[0].debit[2].amount | ||||||
|                   + Decimal("0.01")) |                   + Decimal("0.01")) | ||||||
| @@ -275,14 +278,14 @@ class OffsetTestCase(unittest.TestCase): | |||||||
|         # Not before the original line items |         # Not before the original line items | ||||||
|         old_days: int = journal_entry_data.days |         old_days: int = journal_entry_data.days | ||||||
|         journal_entry_data.days = old_days + 1 |         journal_entry_data.days = old_days + 1 | ||||||
|         form = journal_entry_data.update_form(self.csrf_token) |         form = journal_entry_data.update_form(self.csrf_token, NEXT_URI) | ||||||
|         response = self.client.post(update_uri, data=form) |         response = self.client.post(update_uri, data=form) | ||||||
|         self.assertEqual(response.status_code, 302) |         self.assertEqual(response.status_code, 302) | ||||||
|         self.assertEqual(response.headers["Location"], edit_uri) |         self.assertEqual(response.headers["Location"], edit_uri) | ||||||
|         journal_entry_data.days = old_days |         journal_entry_data.days = old_days | ||||||
|  |  | ||||||
|         # Success |         # Success | ||||||
|         form = journal_entry_data.update_form(self.csrf_token) |         form = journal_entry_data.update_form(self.csrf_token, NEXT_URI) | ||||||
|         response = self.client.post(update_uri, data=form) |         response = self.client.post(update_uri, data=form) | ||||||
|         self.assertEqual(response.status_code, 302) |         self.assertEqual(response.status_code, 302) | ||||||
|         self.assertEqual(response.headers["Location"], |         self.assertEqual(response.headers["Location"], | ||||||
| @@ -307,21 +310,21 @@ class OffsetTestCase(unittest.TestCase): | |||||||
|         journal_entry_data.currencies[0].credit[1].amount = Decimal("3.4") |         journal_entry_data.currencies[0].credit[1].amount = Decimal("3.4") | ||||||
|  |  | ||||||
|         # Not the same currency |         # Not the same currency | ||||||
|         form = journal_entry_data.update_form(self.csrf_token) |         form = journal_entry_data.update_form(self.csrf_token, NEXT_URI) | ||||||
|         form["currency-1-code"] = "EUR" |         form["currency-1-code"] = "EUR" | ||||||
|         response = self.client.post(update_uri, data=form) |         response = self.client.post(update_uri, data=form) | ||||||
|         self.assertEqual(response.status_code, 302) |         self.assertEqual(response.status_code, 302) | ||||||
|         self.assertEqual(response.headers["Location"], edit_uri) |         self.assertEqual(response.headers["Location"], edit_uri) | ||||||
|  |  | ||||||
|         # Not the same account |         # Not the same account | ||||||
|         form = journal_entry_data.update_form(self.csrf_token) |         form = journal_entry_data.update_form(self.csrf_token, NEXT_URI) | ||||||
|         form["currency-1-debit-1-account_code"] = Accounts.NOTES_RECEIVABLE |         form["currency-1-debit-1-account_code"] = Accounts.NOTES_RECEIVABLE | ||||||
|         response = self.client.post(update_uri, data=form) |         response = self.client.post(update_uri, data=form) | ||||||
|         self.assertEqual(response.status_code, 302) |         self.assertEqual(response.status_code, 302) | ||||||
|         self.assertEqual(response.headers["Location"], edit_uri) |         self.assertEqual(response.headers["Location"], edit_uri) | ||||||
|  |  | ||||||
|         # Not less than offset total - partially offset |         # Not less than offset total - partially offset | ||||||
|         form = journal_entry_data.update_form(self.csrf_token) |         form = journal_entry_data.update_form(self.csrf_token, NEXT_URI) | ||||||
|         form["currency-1-debit-1-amount"] \ |         form["currency-1-debit-1-amount"] \ | ||||||
|             = str(journal_entry_data.currencies[0].debit[0].amount |             = str(journal_entry_data.currencies[0].debit[0].amount | ||||||
|                   - Decimal("0.01")) |                   - Decimal("0.01")) | ||||||
| @@ -333,7 +336,7 @@ class OffsetTestCase(unittest.TestCase): | |||||||
|         self.assertEqual(response.headers["Location"], edit_uri) |         self.assertEqual(response.headers["Location"], edit_uri) | ||||||
|  |  | ||||||
|         # Not less than offset total - fully offset |         # Not less than offset total - fully offset | ||||||
|         form = journal_entry_data.update_form(self.csrf_token) |         form = journal_entry_data.update_form(self.csrf_token, NEXT_URI) | ||||||
|         form["currency-1-debit-2-amount"] \ |         form["currency-1-debit-2-amount"] \ | ||||||
|             = str(journal_entry_data.currencies[0].debit[1].amount |             = str(journal_entry_data.currencies[0].debit[1].amount | ||||||
|                   - Decimal("0.01")) |                   - Decimal("0.01")) | ||||||
| @@ -347,21 +350,21 @@ class OffsetTestCase(unittest.TestCase): | |||||||
|         # Not after the offset items |         # Not after the offset items | ||||||
|         old_days: int = journal_entry_data.days |         old_days: int = journal_entry_data.days | ||||||
|         journal_entry_data.days = old_days - 1 |         journal_entry_data.days = old_days - 1 | ||||||
|         form = journal_entry_data.update_form(self.csrf_token) |         form = journal_entry_data.update_form(self.csrf_token, NEXT_URI) | ||||||
|         response = self.client.post(update_uri, data=form) |         response = self.client.post(update_uri, data=form) | ||||||
|         self.assertEqual(response.status_code, 302) |         self.assertEqual(response.status_code, 302) | ||||||
|         self.assertEqual(response.headers["Location"], edit_uri) |         self.assertEqual(response.headers["Location"], edit_uri) | ||||||
|         journal_entry_data.days = old_days |         journal_entry_data.days = old_days | ||||||
|  |  | ||||||
|         # Not deleting matched original line items |         # Not deleting matched original line items | ||||||
|         form = journal_entry_data.update_form(self.csrf_token) |         form = journal_entry_data.update_form(self.csrf_token, NEXT_URI) | ||||||
|         del form["currency-1-debit-1-id"] |         del form["currency-1-debit-1-id"] | ||||||
|         response = self.client.post(update_uri, data=form) |         response = self.client.post(update_uri, data=form) | ||||||
|         self.assertEqual(response.status_code, 302) |         self.assertEqual(response.status_code, 302) | ||||||
|         self.assertEqual(response.headers["Location"], edit_uri) |         self.assertEqual(response.headers["Location"], edit_uri) | ||||||
|  |  | ||||||
|         # Success |         # Success | ||||||
|         form = journal_entry_data.update_form(self.csrf_token) |         form = journal_entry_data.update_form(self.csrf_token, NEXT_URI) | ||||||
|         response = self.client.post(update_uri, data=form) |         response = self.client.post(update_uri, data=form) | ||||||
|         self.assertEqual(response.status_code, 302) |         self.assertEqual(response.status_code, 302) | ||||||
|         self.assertEqual(response.headers["Location"], |         self.assertEqual(response.headers["Location"], | ||||||
| @@ -408,14 +411,14 @@ class OffsetTestCase(unittest.TestCase): | |||||||
|                 [])]) |                 [])]) | ||||||
|  |  | ||||||
|         # Non-existing original line item ID |         # Non-existing original line item ID | ||||||
|         form = journal_entry_data.new_form(self.csrf_token) |         form = journal_entry_data.new_form(self.csrf_token, NEXT_URI) | ||||||
|         form["currency-1-debit-1-original_line_item_id"] = "9999" |         form["currency-1-debit-1-original_line_item_id"] = "9999" | ||||||
|         response = self.client.post(store_uri, data=form) |         response = self.client.post(store_uri, data=form) | ||||||
|         self.assertEqual(response.status_code, 302) |         self.assertEqual(response.status_code, 302) | ||||||
|         self.assertEqual(response.headers["Location"], create_uri) |         self.assertEqual(response.headers["Location"], create_uri) | ||||||
|  |  | ||||||
|         # The same debit or credit |         # The same debit or credit | ||||||
|         form = journal_entry_data.new_form(self.csrf_token) |         form = journal_entry_data.new_form(self.csrf_token, NEXT_URI) | ||||||
|         form["currency-1-debit-1-original_line_item_id"] \ |         form["currency-1-debit-1-original_line_item_id"] \ | ||||||
|             = str(self.data.l_r_or1d.id) |             = str(self.data.l_r_or1d.id) | ||||||
|         form["currency-1-debit-1-account_code"] = self.data.l_r_or1d.account |         form["currency-1-debit-1-account_code"] = self.data.l_r_or1d.account | ||||||
| @@ -430,7 +433,8 @@ class OffsetTestCase(unittest.TestCase): | |||||||
|             account.is_need_offset = False |             account.is_need_offset = False | ||||||
|             db.session.commit() |             db.session.commit() | ||||||
|         response = self.client.post( |         response = self.client.post( | ||||||
|             store_uri, data=journal_entry_data.new_form(self.csrf_token)) |             store_uri, | ||||||
|  |             data=journal_entry_data.new_form(self.csrf_token, NEXT_URI)) | ||||||
|         self.assertEqual(response.status_code, 302) |         self.assertEqual(response.status_code, 302) | ||||||
|         self.assertEqual(response.headers["Location"], create_uri) |         self.assertEqual(response.headers["Location"], create_uri) | ||||||
|         with self.app.app_context(): |         with self.app.app_context(): | ||||||
| @@ -439,7 +443,7 @@ class OffsetTestCase(unittest.TestCase): | |||||||
|             db.session.commit() |             db.session.commit() | ||||||
|  |  | ||||||
|         # The original line item is also an offset |         # The original line item is also an offset | ||||||
|         form = journal_entry_data.new_form(self.csrf_token) |         form = journal_entry_data.new_form(self.csrf_token, NEXT_URI) | ||||||
|         form["currency-1-debit-1-original_line_item_id"] \ |         form["currency-1-debit-1-original_line_item_id"] \ | ||||||
|             = str(self.data.l_r_of1c.id) |             = str(self.data.l_r_of1c.id) | ||||||
|         form["currency-1-debit-1-account_code"] = self.data.l_r_of1c.account |         form["currency-1-debit-1-account_code"] = self.data.l_r_of1c.account | ||||||
| @@ -448,21 +452,21 @@ class OffsetTestCase(unittest.TestCase): | |||||||
|         self.assertEqual(response.headers["Location"], create_uri) |         self.assertEqual(response.headers["Location"], create_uri) | ||||||
|  |  | ||||||
|         # Not the same currency |         # Not the same currency | ||||||
|         form = journal_entry_data.new_form(self.csrf_token) |         form = journal_entry_data.new_form(self.csrf_token, NEXT_URI) | ||||||
|         form["currency-1-code"] = "EUR" |         form["currency-1-code"] = "EUR" | ||||||
|         response = self.client.post(store_uri, data=form) |         response = self.client.post(store_uri, data=form) | ||||||
|         self.assertEqual(response.status_code, 302) |         self.assertEqual(response.status_code, 302) | ||||||
|         self.assertEqual(response.headers["Location"], create_uri) |         self.assertEqual(response.headers["Location"], create_uri) | ||||||
|  |  | ||||||
|         # Not the same account |         # Not the same account | ||||||
|         form = journal_entry_data.new_form(self.csrf_token) |         form = journal_entry_data.new_form(self.csrf_token, NEXT_URI) | ||||||
|         form["currency-1-debit-1-account_code"] = Accounts.NOTES_PAYABLE |         form["currency-1-debit-1-account_code"] = Accounts.NOTES_PAYABLE | ||||||
|         response = self.client.post(store_uri, data=form) |         response = self.client.post(store_uri, data=form) | ||||||
|         self.assertEqual(response.status_code, 302) |         self.assertEqual(response.status_code, 302) | ||||||
|         self.assertEqual(response.headers["Location"], create_uri) |         self.assertEqual(response.headers["Location"], create_uri) | ||||||
|  |  | ||||||
|         # Not exceeding net balance - partially offset |         # Not exceeding net balance - partially offset | ||||||
|         form = journal_entry_data.new_form(self.csrf_token) |         form = journal_entry_data.new_form(self.csrf_token, NEXT_URI) | ||||||
|         form["currency-1-debit-1-amount"] \ |         form["currency-1-debit-1-amount"] \ | ||||||
|             = str(journal_entry_data.currencies[0].debit[0].amount |             = str(journal_entry_data.currencies[0].debit[0].amount | ||||||
|                   + Decimal("0.01")) |                   + Decimal("0.01")) | ||||||
| @@ -471,7 +475,7 @@ class OffsetTestCase(unittest.TestCase): | |||||||
|         self.assertEqual(response.headers["Location"], create_uri) |         self.assertEqual(response.headers["Location"], create_uri) | ||||||
|  |  | ||||||
|         # Not exceeding net balance - unmatched |         # Not exceeding net balance - unmatched | ||||||
|         form = journal_entry_data.new_form(self.csrf_token) |         form = journal_entry_data.new_form(self.csrf_token, NEXT_URI) | ||||||
|         form["currency-1-debit-3-amount"] \ |         form["currency-1-debit-3-amount"] \ | ||||||
|             = str(journal_entry_data.currencies[0].debit[2].amount |             = str(journal_entry_data.currencies[0].debit[2].amount | ||||||
|                   + Decimal("0.01")) |                   + Decimal("0.01")) | ||||||
| @@ -482,14 +486,14 @@ class OffsetTestCase(unittest.TestCase): | |||||||
|         # Not before the original line items |         # Not before the original line items | ||||||
|         old_days: int = journal_entry_data.days |         old_days: int = journal_entry_data.days | ||||||
|         journal_entry_data.days = old_days + 1 |         journal_entry_data.days = old_days + 1 | ||||||
|         form = journal_entry_data.new_form(self.csrf_token) |         form = journal_entry_data.new_form(self.csrf_token, NEXT_URI) | ||||||
|         response = self.client.post(store_uri, data=form) |         response = self.client.post(store_uri, data=form) | ||||||
|         self.assertEqual(response.status_code, 302) |         self.assertEqual(response.status_code, 302) | ||||||
|         self.assertEqual(response.headers["Location"], create_uri) |         self.assertEqual(response.headers["Location"], create_uri) | ||||||
|         journal_entry_data.days = old_days |         journal_entry_data.days = old_days | ||||||
|  |  | ||||||
|         # Success |         # Success | ||||||
|         form = journal_entry_data.new_form(self.csrf_token) |         form = journal_entry_data.new_form(self.csrf_token, NEXT_URI) | ||||||
|         response = self.client.post(store_uri, data=form) |         response = self.client.post(store_uri, data=form) | ||||||
|         self.assertEqual(response.status_code, 302) |         self.assertEqual(response.status_code, 302) | ||||||
|         journal_entry_id: int \ |         journal_entry_id: int \ | ||||||
| @@ -518,14 +522,14 @@ class OffsetTestCase(unittest.TestCase): | |||||||
|         journal_entry_data.currencies[0].credit[2].amount = Decimal("900") |         journal_entry_data.currencies[0].credit[2].amount = Decimal("900") | ||||||
|  |  | ||||||
|         # Non-existing original line item ID |         # Non-existing original line item ID | ||||||
|         form = journal_entry_data.update_form(self.csrf_token) |         form = journal_entry_data.update_form(self.csrf_token, NEXT_URI) | ||||||
|         form["currency-1-debit-1-original_line_item_id"] = "9999" |         form["currency-1-debit-1-original_line_item_id"] = "9999" | ||||||
|         response = self.client.post(update_uri, data=form) |         response = self.client.post(update_uri, data=form) | ||||||
|         self.assertEqual(response.status_code, 302) |         self.assertEqual(response.status_code, 302) | ||||||
|         self.assertEqual(response.headers["Location"], edit_uri) |         self.assertEqual(response.headers["Location"], edit_uri) | ||||||
|  |  | ||||||
|         # The same debit or credit |         # The same debit or credit | ||||||
|         form = journal_entry_data.update_form(self.csrf_token) |         form = journal_entry_data.update_form(self.csrf_token, NEXT_URI) | ||||||
|         form["currency-1-debit-1-original_line_item_id"] \ |         form["currency-1-debit-1-original_line_item_id"] \ | ||||||
|             = str(self.data.l_r_or1d.id) |             = str(self.data.l_r_or1d.id) | ||||||
|         form["currency-1-debit-1-account_code"] = self.data.l_r_or1d.account |         form["currency-1-debit-1-account_code"] = self.data.l_r_or1d.account | ||||||
| @@ -541,7 +545,8 @@ class OffsetTestCase(unittest.TestCase): | |||||||
|             account.is_need_offset = False |             account.is_need_offset = False | ||||||
|             db.session.commit() |             db.session.commit() | ||||||
|         response = self.client.post( |         response = self.client.post( | ||||||
|             update_uri, data=journal_entry_data.update_form(self.csrf_token)) |             update_uri, | ||||||
|  |             data=journal_entry_data.update_form(self.csrf_token, NEXT_URI)) | ||||||
|         self.assertEqual(response.status_code, 302) |         self.assertEqual(response.status_code, 302) | ||||||
|         self.assertEqual(response.headers["Location"], edit_uri) |         self.assertEqual(response.headers["Location"], edit_uri) | ||||||
|         with self.app.app_context(): |         with self.app.app_context(): | ||||||
| @@ -550,7 +555,7 @@ class OffsetTestCase(unittest.TestCase): | |||||||
|             db.session.commit() |             db.session.commit() | ||||||
|  |  | ||||||
|         # The original line item is also an offset |         # The original line item is also an offset | ||||||
|         form = journal_entry_data.update_form(self.csrf_token) |         form = journal_entry_data.update_form(self.csrf_token, NEXT_URI) | ||||||
|         form["currency-1-debit-1-original_line_item_id"] \ |         form["currency-1-debit-1-original_line_item_id"] \ | ||||||
|             = str(self.data.l_r_of1c.id) |             = str(self.data.l_r_of1c.id) | ||||||
|         form["currency-1-debit-1-account_code"] = self.data.l_r_of1c.account |         form["currency-1-debit-1-account_code"] = self.data.l_r_of1c.account | ||||||
| @@ -559,21 +564,21 @@ class OffsetTestCase(unittest.TestCase): | |||||||
|         self.assertEqual(response.headers["Location"], edit_uri) |         self.assertEqual(response.headers["Location"], edit_uri) | ||||||
|  |  | ||||||
|         # Not the same currency |         # Not the same currency | ||||||
|         form = journal_entry_data.update_form(self.csrf_token) |         form = journal_entry_data.update_form(self.csrf_token, NEXT_URI) | ||||||
|         form["currency-1-code"] = "EUR" |         form["currency-1-code"] = "EUR" | ||||||
|         response = self.client.post(update_uri, data=form) |         response = self.client.post(update_uri, data=form) | ||||||
|         self.assertEqual(response.status_code, 302) |         self.assertEqual(response.status_code, 302) | ||||||
|         self.assertEqual(response.headers["Location"], edit_uri) |         self.assertEqual(response.headers["Location"], edit_uri) | ||||||
|  |  | ||||||
|         # Not the same account |         # Not the same account | ||||||
|         form = journal_entry_data.update_form(self.csrf_token) |         form = journal_entry_data.update_form(self.csrf_token, NEXT_URI) | ||||||
|         form["currency-1-debit-1-account_code"] = Accounts.NOTES_PAYABLE |         form["currency-1-debit-1-account_code"] = Accounts.NOTES_PAYABLE | ||||||
|         response = self.client.post(update_uri, data=form) |         response = self.client.post(update_uri, data=form) | ||||||
|         self.assertEqual(response.status_code, 302) |         self.assertEqual(response.status_code, 302) | ||||||
|         self.assertEqual(response.headers["Location"], edit_uri) |         self.assertEqual(response.headers["Location"], edit_uri) | ||||||
|  |  | ||||||
|         # Not exceeding net balance - partially offset |         # Not exceeding net balance - partially offset | ||||||
|         form = journal_entry_data.update_form(self.csrf_token) |         form = journal_entry_data.update_form(self.csrf_token, NEXT_URI) | ||||||
|         form["currency-1-debit-1-amount"] \ |         form["currency-1-debit-1-amount"] \ | ||||||
|             = str(journal_entry_data.currencies[0].debit[0].amount |             = str(journal_entry_data.currencies[0].debit[0].amount | ||||||
|                   + Decimal("0.01")) |                   + Decimal("0.01")) | ||||||
| @@ -585,7 +590,7 @@ class OffsetTestCase(unittest.TestCase): | |||||||
|         self.assertEqual(response.headers["Location"], edit_uri) |         self.assertEqual(response.headers["Location"], edit_uri) | ||||||
|  |  | ||||||
|         # Not exceeding net balance - unmatched |         # Not exceeding net balance - unmatched | ||||||
|         form = journal_entry_data.update_form(self.csrf_token) |         form = journal_entry_data.update_form(self.csrf_token, NEXT_URI) | ||||||
|         form["currency-1-debit-3-amount"] \ |         form["currency-1-debit-3-amount"] \ | ||||||
|             = str(journal_entry_data.currencies[0].debit[2].amount |             = str(journal_entry_data.currencies[0].debit[2].amount | ||||||
|                   + Decimal("0.01")) |                   + Decimal("0.01")) | ||||||
| @@ -599,14 +604,14 @@ class OffsetTestCase(unittest.TestCase): | |||||||
|         # Not before the original line items |         # Not before the original line items | ||||||
|         old_days: int = journal_entry_data.days |         old_days: int = journal_entry_data.days | ||||||
|         journal_entry_data.days = old_days + 1 |         journal_entry_data.days = old_days + 1 | ||||||
|         form = journal_entry_data.update_form(self.csrf_token) |         form = journal_entry_data.update_form(self.csrf_token, NEXT_URI) | ||||||
|         response = self.client.post(update_uri, data=form) |         response = self.client.post(update_uri, data=form) | ||||||
|         self.assertEqual(response.status_code, 302) |         self.assertEqual(response.status_code, 302) | ||||||
|         self.assertEqual(response.headers["Location"], edit_uri) |         self.assertEqual(response.headers["Location"], edit_uri) | ||||||
|         journal_entry_data.days = old_days |         journal_entry_data.days = old_days | ||||||
|  |  | ||||||
|         # Success |         # Success | ||||||
|         form = journal_entry_data.update_form(self.csrf_token) |         form = journal_entry_data.update_form(self.csrf_token, NEXT_URI) | ||||||
|         response = self.client.post(update_uri, data=form) |         response = self.client.post(update_uri, data=form) | ||||||
|         self.assertEqual(response.status_code, 302) |         self.assertEqual(response.status_code, 302) | ||||||
|         journal_entry_id: int \ |         journal_entry_id: int \ | ||||||
| @@ -635,21 +640,21 @@ class OffsetTestCase(unittest.TestCase): | |||||||
|         journal_entry_data.currencies[0].credit[1].amount = Decimal("0.9") |         journal_entry_data.currencies[0].credit[1].amount = Decimal("0.9") | ||||||
|  |  | ||||||
|         # Not the same currency |         # Not the same currency | ||||||
|         form = journal_entry_data.update_form(self.csrf_token) |         form = journal_entry_data.update_form(self.csrf_token, NEXT_URI) | ||||||
|         form["currency-1-code"] = "EUR" |         form["currency-1-code"] = "EUR" | ||||||
|         response = self.client.post(update_uri, data=form) |         response = self.client.post(update_uri, data=form) | ||||||
|         self.assertEqual(response.status_code, 302) |         self.assertEqual(response.status_code, 302) | ||||||
|         self.assertEqual(response.headers["Location"], edit_uri) |         self.assertEqual(response.headers["Location"], edit_uri) | ||||||
|  |  | ||||||
|         # Not the same account |         # Not the same account | ||||||
|         form = journal_entry_data.update_form(self.csrf_token) |         form = journal_entry_data.update_form(self.csrf_token, NEXT_URI) | ||||||
|         form["currency-1-credit-1-account_code"] = Accounts.NOTES_PAYABLE |         form["currency-1-credit-1-account_code"] = Accounts.NOTES_PAYABLE | ||||||
|         response = self.client.post(update_uri, data=form) |         response = self.client.post(update_uri, data=form) | ||||||
|         self.assertEqual(response.status_code, 302) |         self.assertEqual(response.status_code, 302) | ||||||
|         self.assertEqual(response.headers["Location"], edit_uri) |         self.assertEqual(response.headers["Location"], edit_uri) | ||||||
|  |  | ||||||
|         # Not less than offset total - partially offset |         # Not less than offset total - partially offset | ||||||
|         form = journal_entry_data.update_form(self.csrf_token) |         form = journal_entry_data.update_form(self.csrf_token, NEXT_URI) | ||||||
|         form["currency-1-debit-1-amount"] \ |         form["currency-1-debit-1-amount"] \ | ||||||
|             = str(journal_entry_data.currencies[0].debit[0].amount |             = str(journal_entry_data.currencies[0].debit[0].amount | ||||||
|                   - Decimal("0.01")) |                   - Decimal("0.01")) | ||||||
| @@ -661,7 +666,7 @@ class OffsetTestCase(unittest.TestCase): | |||||||
|         self.assertEqual(response.headers["Location"], edit_uri) |         self.assertEqual(response.headers["Location"], edit_uri) | ||||||
|  |  | ||||||
|         # Not less than offset total - fully offset |         # Not less than offset total - fully offset | ||||||
|         form = journal_entry_data.update_form(self.csrf_token) |         form = journal_entry_data.update_form(self.csrf_token, NEXT_URI) | ||||||
|         form["currency-1-debit-2-amount"] \ |         form["currency-1-debit-2-amount"] \ | ||||||
|             = str(journal_entry_data.currencies[0].debit[1].amount |             = str(journal_entry_data.currencies[0].debit[1].amount | ||||||
|                   - Decimal("0.01")) |                   - Decimal("0.01")) | ||||||
| @@ -675,21 +680,21 @@ class OffsetTestCase(unittest.TestCase): | |||||||
|         # Not after the offset items |         # Not after the offset items | ||||||
|         old_days: int = journal_entry_data.days |         old_days: int = journal_entry_data.days | ||||||
|         journal_entry_data.days = old_days - 1 |         journal_entry_data.days = old_days - 1 | ||||||
|         form = journal_entry_data.update_form(self.csrf_token) |         form = journal_entry_data.update_form(self.csrf_token, NEXT_URI) | ||||||
|         response = self.client.post(update_uri, data=form) |         response = self.client.post(update_uri, data=form) | ||||||
|         self.assertEqual(response.status_code, 302) |         self.assertEqual(response.status_code, 302) | ||||||
|         self.assertEqual(response.headers["Location"], edit_uri) |         self.assertEqual(response.headers["Location"], edit_uri) | ||||||
|         journal_entry_data.days = old_days |         journal_entry_data.days = old_days | ||||||
|  |  | ||||||
|         # Not deleting matched original line items |         # Not deleting matched original line items | ||||||
|         form = journal_entry_data.update_form(self.csrf_token) |         form = journal_entry_data.update_form(self.csrf_token, NEXT_URI) | ||||||
|         del form["currency-1-credit-1-id"] |         del form["currency-1-credit-1-id"] | ||||||
|         response = self.client.post(update_uri, data=form) |         response = self.client.post(update_uri, data=form) | ||||||
|         self.assertEqual(response.status_code, 302) |         self.assertEqual(response.status_code, 302) | ||||||
|         self.assertEqual(response.headers["Location"], edit_uri) |         self.assertEqual(response.headers["Location"], edit_uri) | ||||||
|  |  | ||||||
|         # Success |         # Success | ||||||
|         form = journal_entry_data.update_form(self.csrf_token) |         form = journal_entry_data.update_form(self.csrf_token, NEXT_URI) | ||||||
|         response = self.client.post(update_uri, data=form) |         response = self.client.post(update_uri, data=form) | ||||||
|         self.assertEqual(response.status_code, 302) |         self.assertEqual(response.status_code, 302) | ||||||
|         self.assertEqual(response.headers["Location"], |         self.assertEqual(response.headers["Location"], | ||||||
|   | |||||||
| @@ -23,7 +23,8 @@ from datetime import date | |||||||
| import httpx | import httpx | ||||||
| from flask import Flask | from flask import Flask | ||||||
|  |  | ||||||
| from testlib import create_test_app, get_client, Accounts, BaseTestData | from test_site.lib import BaseTestData | ||||||
|  | from testlib import create_test_app, get_client, Accounts | ||||||
|  |  | ||||||
| PREFIX: str = "/accounting" | PREFIX: str = "/accounting" | ||||||
| """The URL prefix for the reports.""" | """The URL prefix for the reports.""" | ||||||
|   | |||||||
| @@ -17,8 +17,10 @@ | |||||||
| """The authentication for the Mia! Accounting demonstration website. | """The authentication for the Mia! Accounting demonstration website. | ||||||
|  |  | ||||||
| """ | """ | ||||||
|  | import typing as t | ||||||
|  |  | ||||||
| from flask import Blueprint, render_template, Flask, redirect, url_for, \ | from flask import Blueprint, render_template, Flask, redirect, url_for, \ | ||||||
|     session, request, g, Response |     session, request, g, Response, abort | ||||||
|  |  | ||||||
| from . import db | from . import db | ||||||
|  |  | ||||||
| @@ -93,6 +95,31 @@ def current_user() -> User | None: | |||||||
|     return g.user |     return g.user | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def admin_required(view: t.Callable) -> t.Callable: | ||||||
|  |     """The view decorator to require the user to be an administrator. | ||||||
|  |  | ||||||
|  |     :param view: The view. | ||||||
|  |     :return: The decorated view. | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     def decorated_view(*args, **kwargs): | ||||||
|  |         """The decorated view that tests against a permission rule. | ||||||
|  |  | ||||||
|  |         :param args: The arguments of the view. | ||||||
|  |         :param kwargs: The keyword arguments of the view. | ||||||
|  |         :return: The response of the view. | ||||||
|  |         :raise Forbidden: When the user is denied. | ||||||
|  |         """ | ||||||
|  |         from accounting.utils.next_uri import append_next | ||||||
|  |         if "user" not in session: | ||||||
|  |             return redirect(append_next(url_for("auth.login"))) | ||||||
|  |         if session["user"] != "admin": | ||||||
|  |             abort(403) | ||||||
|  |         return view(*args, **kwargs) | ||||||
|  |  | ||||||
|  |     return decorated_view | ||||||
|  |  | ||||||
|  |  | ||||||
| def init_app(app: Flask) -> None: | def init_app(app: Flask) -> None: | ||||||
|     """Initialize the localization. |     """Initialize the localization. | ||||||
|  |  | ||||||
|   | |||||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										335
									
								
								tests/test_site/lib.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										335
									
								
								tests/test_site/lib.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,335 @@ | |||||||
|  | # The Mia! Accounting Demonstration Website. | ||||||
|  | # Author: imacat@mail.imacat.idv.tw (imacat), 2023/4/13 | ||||||
|  |  | ||||||
|  | #  Copyright (c) 2023 imacat. | ||||||
|  | # | ||||||
|  | #  Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
|  | #  you may not use this file except in compliance with the License. | ||||||
|  | #  You may obtain a copy of the License at | ||||||
|  | # | ||||||
|  | #      http://www.apache.org/licenses/LICENSE-2.0 | ||||||
|  | # | ||||||
|  | #  Unless required by applicable law or agreed to in writing, software | ||||||
|  | #  distributed under the License is distributed on an "AS IS" BASIS, | ||||||
|  | #  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||||
|  | #  See the License for the specific language governing permissions and | ||||||
|  | #  limitations under the License. | ||||||
|  | """The common library for the Mia! Accounting demonstration website. | ||||||
|  |  | ||||||
|  | """ | ||||||
|  | from __future__ import annotations | ||||||
|  |  | ||||||
|  | import typing as t | ||||||
|  | from abc import ABC, abstractmethod | ||||||
|  | from datetime import date, timedelta | ||||||
|  | from decimal import Decimal | ||||||
|  | from secrets import randbelow | ||||||
|  |  | ||||||
|  | import sqlalchemy as sa | ||||||
|  | from flask import Flask | ||||||
|  |  | ||||||
|  | from . import db | ||||||
|  | from .auth import User | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Accounts: | ||||||
|  |     """The shortcuts to the common accounts.""" | ||||||
|  |     CASH: str = "1111-001" | ||||||
|  |     BANK: str = "1113-001" | ||||||
|  |     RECEIVABLE: str = "1141-001" | ||||||
|  |     MACHINERY: str = "1441-001" | ||||||
|  |     PAYABLE: str = "2141-001" | ||||||
|  |     SERVICE: str = "4611-001" | ||||||
|  |     RENT_EXPENSE: str = "6252-001" | ||||||
|  |     MEAL: str = "6272-001" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class JournalEntryLineItemData: | ||||||
|  |     """The journal entry line item data.""" | ||||||
|  |  | ||||||
|  |     def __init__(self, account: str, description: str | None, amount: str, | ||||||
|  |                  original_line_item: JournalEntryLineItemData | None = None): | ||||||
|  |         """Constructs the journal entry line item data. | ||||||
|  |  | ||||||
|  |         :param account: The account code. | ||||||
|  |         :param description: The description. | ||||||
|  |         :param amount: The amount. | ||||||
|  |         :param original_line_item: The original journal entry line item. | ||||||
|  |         """ | ||||||
|  |         self.journal_entry: JournalEntryData | None = None | ||||||
|  |         self.id: int = -1 | ||||||
|  |         self.no: int = -1 | ||||||
|  |         self.original_line_item: JournalEntryLineItemData | None \ | ||||||
|  |             = original_line_item | ||||||
|  |         self.account: str = account | ||||||
|  |         self.description: str | None = description | ||||||
|  |         self.amount: Decimal = Decimal(amount) | ||||||
|  |  | ||||||
|  |     def form(self, prefix: str, debit_credit: str, index: int, | ||||||
|  |              is_update: bool) -> dict[str, str]: | ||||||
|  |         """Returns the line item as form data. | ||||||
|  |  | ||||||
|  |         :param prefix: The prefix of the form fields. | ||||||
|  |         :param debit_credit: Either "debit" or "credit". | ||||||
|  |         :param index: The line item index. | ||||||
|  |         :param is_update: True for an update operation, or False otherwise | ||||||
|  |         :return: The form data. | ||||||
|  |         """ | ||||||
|  |         prefix = f"{prefix}-{debit_credit}-{index}" | ||||||
|  |         form: dict[str, str] = {f"{prefix}-account_code": self.account, | ||||||
|  |                                 f"{prefix}-description": self.description, | ||||||
|  |                                 f"{prefix}-amount": str(self.amount)} | ||||||
|  |         if is_update and self.id != -1: | ||||||
|  |             form[f"{prefix}-id"] = str(self.id) | ||||||
|  |         form[f"{prefix}-no"] = str(index) if self.no == -1 else str(self.no) | ||||||
|  |         if self.original_line_item is not None: | ||||||
|  |             assert self.original_line_item.id != -1 | ||||||
|  |             form[f"{prefix}-original_line_item_id"] \ | ||||||
|  |                 = str(self.original_line_item.id) | ||||||
|  |         return form | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class JournalEntryCurrencyData: | ||||||
|  |     """The journal entry currency data.""" | ||||||
|  |  | ||||||
|  |     def __init__(self, currency: str, debit: list[JournalEntryLineItemData], | ||||||
|  |                  credit: list[JournalEntryLineItemData]): | ||||||
|  |         """Constructs the journal entry currency data. | ||||||
|  |  | ||||||
|  |         :param currency: The currency code. | ||||||
|  |         :param debit: The debit line items. | ||||||
|  |         :param credit: The credit line items. | ||||||
|  |         """ | ||||||
|  |         self.code: str = currency | ||||||
|  |         self.debit: list[JournalEntryLineItemData] = debit | ||||||
|  |         self.credit: list[JournalEntryLineItemData] = credit | ||||||
|  |  | ||||||
|  |     def form(self, index: int, is_update: bool) -> dict[str, str]: | ||||||
|  |         """Returns the currency as form data. | ||||||
|  |  | ||||||
|  |         :param index: The currency index. | ||||||
|  |         :param is_update: True for an update operation, or False otherwise | ||||||
|  |         :return: The form data. | ||||||
|  |         """ | ||||||
|  |         prefix: str = f"currency-{index}" | ||||||
|  |         form: dict[str, str] = {f"{prefix}-code": self.code} | ||||||
|  |         for i in range(len(self.debit)): | ||||||
|  |             form.update(self.debit[i].form(prefix, "debit", i + 1, is_update)) | ||||||
|  |         for i in range(len(self.credit)): | ||||||
|  |             form.update(self.credit[i].form(prefix, "credit", i + 1, | ||||||
|  |                                             is_update)) | ||||||
|  |         return form | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class JournalEntryData: | ||||||
|  |     """The journal entry data.""" | ||||||
|  |  | ||||||
|  |     def __init__(self, days: int, currencies: list[JournalEntryCurrencyData]): | ||||||
|  |         """Constructs a journal entry. | ||||||
|  |  | ||||||
|  |         :param days: The number of days before today. | ||||||
|  |         :param currencies: The journal entry currency data. | ||||||
|  |         """ | ||||||
|  |         self.id: int = -1 | ||||||
|  |         self.days: int = days | ||||||
|  |         self.currencies: list[JournalEntryCurrencyData] = currencies | ||||||
|  |         self.note: str | None = None | ||||||
|  |         for currency in self.currencies: | ||||||
|  |             for line_item in currency.debit: | ||||||
|  |                 line_item.journal_entry = self | ||||||
|  |             for line_item in currency.credit: | ||||||
|  |                 line_item.journal_entry = self | ||||||
|  |  | ||||||
|  |     def new_form(self, csrf_token: str, next_uri: str) -> dict[str, str]: | ||||||
|  |         """Returns the journal entry as a creation form. | ||||||
|  |  | ||||||
|  |         :param csrf_token: The CSRF token. | ||||||
|  |         :param next_uri: The next URI. | ||||||
|  |         :return: The journal entry as a creation form. | ||||||
|  |         """ | ||||||
|  |         return self.__form(csrf_token, next_uri, is_update=False) | ||||||
|  |  | ||||||
|  |     def update_form(self, csrf_token: str, next_uri: str) -> dict[str, str]: | ||||||
|  |         """Returns the journal entry as an update form. | ||||||
|  |  | ||||||
|  |         :param csrf_token: The CSRF token. | ||||||
|  |         :param next_uri: The next URI. | ||||||
|  |         :return: The journal entry as an update form. | ||||||
|  |         """ | ||||||
|  |         return self.__form(csrf_token, next_uri, is_update=True) | ||||||
|  |  | ||||||
|  |     def __form(self, csrf_token: str, next_uri: str, is_update: bool = False) \ | ||||||
|  |             -> dict[str, str]: | ||||||
|  |         """Returns the journal entry as a form. | ||||||
|  |  | ||||||
|  |         :param csrf_token: The CSRF token. | ||||||
|  |         :param next_uri: The next URI. | ||||||
|  |         :param is_update: True for an update operation, or False otherwise | ||||||
|  |         :return: The journal entry as a form. | ||||||
|  |         """ | ||||||
|  |         journal_entry_date: date = date.today() - timedelta(days=self.days) | ||||||
|  |         form: dict[str, str] = {"csrf_token": csrf_token, | ||||||
|  |                                 "next": next_uri, | ||||||
|  |                                 "date": journal_entry_date.isoformat()} | ||||||
|  |         for i in range(len(self.currencies)): | ||||||
|  |             form.update(self.currencies[i].form(i + 1, is_update)) | ||||||
|  |         if self.note is not None: | ||||||
|  |             form["note"] = self.note | ||||||
|  |         return form | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class BaseTestData(ABC): | ||||||
|  |     """The base test data.""" | ||||||
|  |  | ||||||
|  |     def __init__(self, app: Flask, username: str): | ||||||
|  |         """Constructs the test data. | ||||||
|  |  | ||||||
|  |         :param app: The Flask application. | ||||||
|  |         :param username: The username. | ||||||
|  |         """ | ||||||
|  |         self._app: Flask = app | ||||||
|  |         with self._app.app_context(): | ||||||
|  |             current_user: User | None = User.query\ | ||||||
|  |                 .filter(User.username == username).first() | ||||||
|  |             assert current_user is not None | ||||||
|  |             self.__current_user_id: int = current_user.id | ||||||
|  |             self.__journal_entries: list[dict[str, t.Any]] = [] | ||||||
|  |             self.__line_items: list[dict[str, t.Any]] = [] | ||||||
|  |             self._init_data() | ||||||
|  |  | ||||||
|  |     @abstractmethod | ||||||
|  |     def _init_data(self) -> None: | ||||||
|  |         """Initializes the test data. | ||||||
|  |  | ||||||
|  |         :return: None | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |     def populate(self) -> None: | ||||||
|  |         """Populates the data into the database. | ||||||
|  |  | ||||||
|  |         :return: None | ||||||
|  |         """ | ||||||
|  |         from accounting.models import JournalEntry, JournalEntryLineItem | ||||||
|  |         with self._app.app_context(): | ||||||
|  |             db.session.execute(sa.insert(JournalEntry), self.__journal_entries) | ||||||
|  |             db.session.execute(sa.insert(JournalEntryLineItem), | ||||||
|  |                                self.__line_items) | ||||||
|  |             db.session.commit() | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def _couple(description: str, amount: str, debit: str, credit: str) \ | ||||||
|  |             -> tuple[JournalEntryLineItemData, JournalEntryLineItemData]: | ||||||
|  |         """Returns a couple of debit-credit line items. | ||||||
|  |  | ||||||
|  |         :param description: The description. | ||||||
|  |         :param amount: The amount. | ||||||
|  |         :param debit: The debit account code. | ||||||
|  |         :param credit: The credit account code. | ||||||
|  |         :return: The debit line item and credit line item. | ||||||
|  |         """ | ||||||
|  |         return JournalEntryLineItemData(debit, description, amount),\ | ||||||
|  |             JournalEntryLineItemData(credit, description, amount) | ||||||
|  |  | ||||||
|  |     def _add_journal_entry(self, journal_entry_data: JournalEntryData) -> None: | ||||||
|  |         """Adds a journal entry. | ||||||
|  |  | ||||||
|  |         :param journal_entry_data: The journal entry data. | ||||||
|  |         :return: None. | ||||||
|  |         """ | ||||||
|  |         from accounting.models import Account | ||||||
|  |         existing_j_id: set[int] = {x["id"] for x in self.__journal_entries} | ||||||
|  |         existing_l_id: set[int] = {x["id"] for x in self.__line_items} | ||||||
|  |         journal_entry_data.id = self.__new_id(existing_j_id) | ||||||
|  |         j_date: date = date.today() - timedelta(days=journal_entry_data.days) | ||||||
|  |         self.__journal_entries.append( | ||||||
|  |             {"id": journal_entry_data.id, | ||||||
|  |              "date": j_date, | ||||||
|  |              "no": self.__next_j_no(j_date), | ||||||
|  |              "note": journal_entry_data.note, | ||||||
|  |              "created_by_id": self.__current_user_id, | ||||||
|  |              "updated_by_id": self.__current_user_id}) | ||||||
|  |         debit_no: int = 0 | ||||||
|  |         credit_no: int = 0 | ||||||
|  |         for currency in journal_entry_data.currencies: | ||||||
|  |             for line_item in currency.debit: | ||||||
|  |                 account: Account | None \ | ||||||
|  |                     = Account.find_by_code(line_item.account) | ||||||
|  |                 assert account is not None | ||||||
|  |                 debit_no = debit_no + 1 | ||||||
|  |                 line_item.id = self.__new_id(existing_l_id) | ||||||
|  |                 data: dict[str, t.Any] \ | ||||||
|  |                     = {"id": line_item.id, | ||||||
|  |                        "journal_entry_id": journal_entry_data.id, | ||||||
|  |                        "is_debit": True, | ||||||
|  |                        "no": debit_no, | ||||||
|  |                        "account_id": account.id, | ||||||
|  |                        "currency_code": currency.code, | ||||||
|  |                        "description": line_item.description, | ||||||
|  |                        "amount": line_item.amount} | ||||||
|  |                 if line_item.original_line_item is not None: | ||||||
|  |                     data["original_line_item_id"] \ | ||||||
|  |                         = line_item.original_line_item.id | ||||||
|  |                 self.__line_items.append(data) | ||||||
|  |             for line_item in currency.credit: | ||||||
|  |                 account: Account | None \ | ||||||
|  |                     = Account.find_by_code(line_item.account) | ||||||
|  |                 assert account is not None | ||||||
|  |                 credit_no = credit_no + 1 | ||||||
|  |                 line_item.id = self.__new_id(existing_l_id) | ||||||
|  |                 data: dict[str, t.Any] \ | ||||||
|  |                     = {"id": line_item.id, | ||||||
|  |                        "journal_entry_id": journal_entry_data.id, | ||||||
|  |                        "is_debit": False, | ||||||
|  |                        "no": credit_no, | ||||||
|  |                        "account_id": account.id, | ||||||
|  |                        "currency_code": currency.code, | ||||||
|  |                        "description": line_item.description, | ||||||
|  |                        "amount": line_item.amount} | ||||||
|  |                 if line_item.original_line_item is not None: | ||||||
|  |                     data["original_line_item_id"] \ | ||||||
|  |                         = line_item.original_line_item.id | ||||||
|  |                 self.__line_items.append(data) | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def __new_id(existing_id: set[int]) -> int: | ||||||
|  |         """Generates and returns a new random unique ID. | ||||||
|  |  | ||||||
|  |         :param existing_id: The existing ID. | ||||||
|  |         :return: The newly-generated random unique ID. | ||||||
|  |         """ | ||||||
|  |         while True: | ||||||
|  |             obj_id: int = 100000000 + randbelow(900000000) | ||||||
|  |             if obj_id not in existing_id: | ||||||
|  |                 existing_id.add(obj_id) | ||||||
|  |                 return obj_id | ||||||
|  |  | ||||||
|  |     def __next_j_no(self, j_date: date) -> int: | ||||||
|  |         """Returns the next journal entry number in a day. | ||||||
|  |  | ||||||
|  |         :param j_date: The journal entry date. | ||||||
|  |         :return: The next journal entry number. | ||||||
|  |         """ | ||||||
|  |         existing: set[int] = {x["no"] for x in self.__journal_entries | ||||||
|  |                               if x["date"] == j_date} | ||||||
|  |         return 1 if len(existing) == 0 else max(existing) + 1 | ||||||
|  |  | ||||||
|  |     def _add_simple_journal_entry( | ||||||
|  |             self, days: int, currency: str, description: str, amount: str, | ||||||
|  |             debit: str, credit: str) \ | ||||||
|  |             -> tuple[JournalEntryLineItemData, JournalEntryLineItemData]: | ||||||
|  |         """Adds a simple journal entry. | ||||||
|  |  | ||||||
|  |         :param days: The number of days before today. | ||||||
|  |         :param currency: The currency code. | ||||||
|  |         :param description: The description. | ||||||
|  |         :param amount: The amount. | ||||||
|  |         :param debit: The debit account code. | ||||||
|  |         :param credit: The credit account code. | ||||||
|  |         :return: The debit line item and credit line item. | ||||||
|  |         """ | ||||||
|  |         debit_item, credit_item = self._couple( | ||||||
|  |             description, amount, debit, credit) | ||||||
|  |         self._add_journal_entry(JournalEntryData( | ||||||
|  |             days, [JournalEntryCurrencyData( | ||||||
|  |                 currency, [debit_item], [credit_item])])) | ||||||
|  |         return debit_item, credit_item | ||||||
| @@ -17,25 +17,22 @@ | |||||||
| """The data reset for the Mia! Accounting demonstration website. | """The data reset for the Mia! Accounting demonstration website. | ||||||
|  |  | ||||||
| """ | """ | ||||||
| import json |  | ||||||
| import typing as t |  | ||||||
| from datetime import date, timedelta | from datetime import date, timedelta | ||||||
| from decimal import Decimal |  | ||||||
| from pathlib import Path |  | ||||||
|  |  | ||||||
| import sqlalchemy as sa |  | ||||||
| from flask import Flask, Blueprint, url_for, flash, redirect, session, \ | from flask import Flask, Blueprint, url_for, flash, redirect, session, \ | ||||||
|     render_template |     render_template, current_app | ||||||
| from flask_babel import lazy_gettext | from flask_babel import lazy_gettext | ||||||
|  |  | ||||||
| from accounting.utils.cast import s |  | ||||||
| from . import db | from . import db | ||||||
| from .auth import User, current_user | from .auth import admin_required | ||||||
|  | from .lib import Accounts, JournalEntryLineItemData, JournalEntryData, \ | ||||||
|  |     JournalEntryCurrencyData, BaseTestData | ||||||
|  |  | ||||||
| bp: Blueprint = Blueprint("reset", __name__, url_prefix="/") | bp: Blueprint = Blueprint("reset", __name__, url_prefix="/") | ||||||
|  |  | ||||||
|  |  | ||||||
| @bp.get("reset", endpoint="reset-page") | @bp.get("reset", endpoint="reset-page") | ||||||
|  | @admin_required | ||||||
| def reset() -> str: | def reset() -> str: | ||||||
|     """Resets the sample data. |     """Resets the sample data. | ||||||
|  |  | ||||||
| @@ -45,79 +42,33 @@ def reset() -> str: | |||||||
|  |  | ||||||
|  |  | ||||||
| @bp.post("sample", endpoint="sample") | @bp.post("sample", endpoint="sample") | ||||||
|  | @admin_required | ||||||
| def reset_sample() -> redirect: | def reset_sample() -> redirect: | ||||||
|     """Resets the sample data. |     """Resets the sample data. | ||||||
|  |  | ||||||
|     :return: Redirection to the accounting application. |     :return: Redirection to the accounting application. | ||||||
|     """ |     """ | ||||||
|  |     from accounting.utils.cast import s | ||||||
|     __reset_database() |     __reset_database() | ||||||
|     __populate_sample_data() |     SampleData(current_app, "editor").populate() | ||||||
|     flash(s(lazy_gettext( |     flash(s(lazy_gettext( | ||||||
|         "The sample data are emptied and reset successfully.")), "success") |         "The sample data are emptied and reset successfully.")), "success") | ||||||
|     return redirect(url_for("accounting-report.default")) |     return redirect(url_for("accounting-report.default")) | ||||||
|  |  | ||||||
|  |  | ||||||
| @bp.post("reset", endpoint="clean-up") | @bp.post("reset", endpoint="clean-up") | ||||||
|  | @admin_required | ||||||
| def clean_up() -> redirect: | def clean_up() -> redirect: | ||||||
|     """Clean-up the database data. |     """Clean-up the database data. | ||||||
|  |  | ||||||
|     :return: Redirection to the accounting application. |     :return: Redirection to the accounting application. | ||||||
|     """ |     """ | ||||||
|  |     from accounting.utils.cast import s | ||||||
|     __reset_database() |     __reset_database() | ||||||
|     db.session.commit() |  | ||||||
|     flash(s(lazy_gettext("The database is emptied successfully.")), "success") |     flash(s(lazy_gettext("The database is emptied successfully.")), "success") | ||||||
|     return redirect(url_for("accounting-report.default")) |     return redirect(url_for("accounting-report.default")) | ||||||
|  |  | ||||||
|  |  | ||||||
| def __populate_sample_data() -> None: |  | ||||||
|     """Populates the sample data. |  | ||||||
|  |  | ||||||
|     :return: None. |  | ||||||
|     """ |  | ||||||
|     from accounting.models import Account, JournalEntry, JournalEntryLineItem |  | ||||||
|     file: Path = Path(__file__).parent / "data" / "sample.json" |  | ||||||
|     with open(file) as fp: |  | ||||||
|         json_data = json.load(fp) |  | ||||||
|     today: date = date.today() |  | ||||||
|     user: User | None = current_user() |  | ||||||
|     assert user is not None |  | ||||||
|  |  | ||||||
|     def filter_journal_entry(data: list[t.Any]) -> dict[str, t.Any]: |  | ||||||
|         """Filters the journal entry data from JSON. |  | ||||||
|  |  | ||||||
|         :param data: The journal entry data. |  | ||||||
|         :return: The journal entry data from JSON. |  | ||||||
|         """ |  | ||||||
|         return {"id": data[0], |  | ||||||
|                 "date": today - timedelta(days=data[1]), |  | ||||||
|                 "no": data[2], |  | ||||||
|                 "note": data[3], |  | ||||||
|                 "created_by_id": user.id, |  | ||||||
|                 "updated_by_id": user.id} |  | ||||||
|  |  | ||||||
|     def filter_line_item(data: list[t.Any]) -> dict[str, t.Any]: |  | ||||||
|         """Filters the journal entry line item data from JSON. |  | ||||||
|  |  | ||||||
|         :param data: The journal entry line item data. |  | ||||||
|         :return: The journal entry line item data from JSON. |  | ||||||
|         """ |  | ||||||
|         return {"id": data[0], |  | ||||||
|                 "journal_entry_id": data[1], |  | ||||||
|                 "original_line_item_id": data[2], |  | ||||||
|                 "is_debit": data[3], |  | ||||||
|                 "no": data[4], |  | ||||||
|                 "account_id": Account.find_by_code(data[5]).id, |  | ||||||
|                 "currency_code": data[6], |  | ||||||
|                 "description": data[7], |  | ||||||
|                 "amount": Decimal(data[8])} |  | ||||||
|  |  | ||||||
|     db.session.execute(sa.insert(JournalEntry), |  | ||||||
|                        [filter_journal_entry(x) for x in json_data[0]]) |  | ||||||
|     db.session.execute(sa.insert(JournalEntryLineItem), |  | ||||||
|                        [filter_line_item(x) for x in json_data[1]]) |  | ||||||
|     db.session.commit() |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def __reset_database() -> None: | def __reset_database() -> None: | ||||||
|     """Resets the database. |     """Resets the database. | ||||||
|  |  | ||||||
| @@ -141,6 +92,273 @@ def __reset_database() -> None: | |||||||
|     init_base_accounts_command() |     init_base_accounts_command() | ||||||
|     init_accounts_command(session["user"]) |     init_accounts_command(session["user"]) | ||||||
|     init_currencies_command(session["user"]) |     init_currencies_command(session["user"]) | ||||||
|  |     db.session.commit() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class SampleData(BaseTestData): | ||||||
|  |     """The sample data.""" | ||||||
|  |  | ||||||
|  |     def _init_data(self) -> None: | ||||||
|  |         self.__add_recurring() | ||||||
|  |         self.__add_offsets() | ||||||
|  |         self.__add_meals() | ||||||
|  |  | ||||||
|  |     def __add_recurring(self) -> None: | ||||||
|  |         """Adds the recurring data. | ||||||
|  |  | ||||||
|  |         :return: None. | ||||||
|  |         """ | ||||||
|  |         self.__add_usd_recurring() | ||||||
|  |         self.__add_twd_recurring() | ||||||
|  |  | ||||||
|  |     def __add_usd_recurring(self) -> None: | ||||||
|  |         """Adds the recurring data in USD. | ||||||
|  |  | ||||||
|  |         :return: None. | ||||||
|  |         """ | ||||||
|  |         today: date = date.today() | ||||||
|  |         days: int | ||||||
|  |         year: int | ||||||
|  |         month: int | ||||||
|  |  | ||||||
|  |         # Recurring in USD | ||||||
|  |         j_date: date = date(today.year - 5, today.month, today.day) | ||||||
|  |         j_date = j_date + timedelta(days=(4 - j_date.weekday())) | ||||||
|  |         days = (today - j_date).days | ||||||
|  |         while True: | ||||||
|  |             if days < 0: | ||||||
|  |                 break | ||||||
|  |             self.__add_journal_entry( | ||||||
|  |                 days, "USD", "2600", | ||||||
|  |                 Accounts.BANK, "Transfer", Accounts.SERVICE, "Payroll") | ||||||
|  |  | ||||||
|  |             days = days - 1 | ||||||
|  |             if days < 0: | ||||||
|  |                 break | ||||||
|  |             self.__add_journal_entry( | ||||||
|  |                 days, "USD", "1200", | ||||||
|  |                 Accounts.CASH, None, Accounts.BANK, "Withdraw") | ||||||
|  |             days = days - 13 | ||||||
|  |  | ||||||
|  |         year = today.year - 5 | ||||||
|  |         month = today.month | ||||||
|  |         while True: | ||||||
|  |             month = month + 1 | ||||||
|  |             if month > 12: | ||||||
|  |                 year = year + 1 | ||||||
|  |                 month = 1 | ||||||
|  |             days = (today - date(year, month, 1)).days | ||||||
|  |             if days < 0: | ||||||
|  |                 break | ||||||
|  |             self.__add_journal_entry( | ||||||
|  |                 days, "USD", "1800", | ||||||
|  |                 Accounts.RENT_EXPENSE, "Rent", Accounts.BANK, "Transfer") | ||||||
|  |  | ||||||
|  |     def __add_twd_recurring(self) -> None: | ||||||
|  |         """Adds the recurring data in TWD. | ||||||
|  |  | ||||||
|  |         :return: None. | ||||||
|  |         """ | ||||||
|  |         today: date = date.today() | ||||||
|  |  | ||||||
|  |         year: int = today.year - 5 | ||||||
|  |         month: int = today.month | ||||||
|  |         while True: | ||||||
|  |             days: int = (today - date(year, month, 5)).days | ||||||
|  |             if days < 0: | ||||||
|  |                 break | ||||||
|  |             self.__add_journal_entry( | ||||||
|  |                 days, "TWD", "50000", | ||||||
|  |                 Accounts.BANK, "薪資轉帳", Accounts.SERVICE, "薪水") | ||||||
|  |  | ||||||
|  |             days = days - 1 | ||||||
|  |             if days < 0: | ||||||
|  |                 break | ||||||
|  |             self.__add_journal_entry( | ||||||
|  |                 days, "TWD", "25000", | ||||||
|  |                 Accounts.CASH, None, Accounts.BANK, "提款") | ||||||
|  |  | ||||||
|  |             days = days - 4 | ||||||
|  |             if days < 0: | ||||||
|  |                 break | ||||||
|  |             self.__add_journal_entry( | ||||||
|  |                 days, "TWD", "18000", | ||||||
|  |                 Accounts.RENT_EXPENSE, "房租", Accounts.BANK, "轉帳") | ||||||
|  |  | ||||||
|  |             month = month + 1 | ||||||
|  |             if month > 12: | ||||||
|  |                 year = year + 1 | ||||||
|  |                 month = 1 | ||||||
|  |  | ||||||
|  |     def __add_offsets(self) -> None: | ||||||
|  |         """Adds the offset data. | ||||||
|  |  | ||||||
|  |         :return: None. | ||||||
|  |         """ | ||||||
|  |         days: int | ||||||
|  |         year: int | ||||||
|  |         month: int | ||||||
|  |         description: str | ||||||
|  |         line_item_or: JournalEntryLineItemData | ||||||
|  |         line_item_of: JournalEntryLineItemData | ||||||
|  |  | ||||||
|  |         # Full offset and unmatched in USD | ||||||
|  |         description = "Speaking—Institute" | ||||||
|  |         line_item_or = JournalEntryLineItemData( | ||||||
|  |             Accounts.RECEIVABLE, description, "120") | ||||||
|  |         self._add_journal_entry(JournalEntryData( | ||||||
|  |             40, [JournalEntryCurrencyData( | ||||||
|  |                 "USD", [line_item_or], [JournalEntryLineItemData( | ||||||
|  |                     Accounts.SERVICE, description, "120")])])) | ||||||
|  |         line_item_of = JournalEntryLineItemData( | ||||||
|  |             Accounts.RECEIVABLE, description, "120", | ||||||
|  |             original_line_item=line_item_or) | ||||||
|  |         self._add_journal_entry(JournalEntryData( | ||||||
|  |             5, [JournalEntryCurrencyData( | ||||||
|  |                 "USD", [JournalEntryLineItemData( | ||||||
|  |                     Accounts.BANK, description, "120")], | ||||||
|  |                 [line_item_of])])) | ||||||
|  |         self.__add_journal_entry( | ||||||
|  |             30, "USD", "120", | ||||||
|  |             Accounts.BANK, description, Accounts.SERVICE, description) | ||||||
|  |  | ||||||
|  |         # Partial offset in USD | ||||||
|  |         line_item_or = JournalEntryLineItemData( | ||||||
|  |             Accounts.PAYABLE, "Computer", "1600") | ||||||
|  |         self._add_journal_entry(JournalEntryData( | ||||||
|  |             60, [JournalEntryCurrencyData( | ||||||
|  |                 "USD", [JournalEntryLineItemData( | ||||||
|  |                     Accounts.MACHINERY, "Computer", "1600")], | ||||||
|  |                 [line_item_or])])) | ||||||
|  |         line_item_of = JournalEntryLineItemData( | ||||||
|  |             Accounts.PAYABLE, "Computer", "800", | ||||||
|  |             original_line_item=line_item_or) | ||||||
|  |         self._add_journal_entry(JournalEntryData( | ||||||
|  |             35, [JournalEntryCurrencyData( | ||||||
|  |                 "USD", [line_item_of], [JournalEntryLineItemData( | ||||||
|  |                     Accounts.BANK, "Computer", "800")])])) | ||||||
|  |         line_item_of = JournalEntryLineItemData( | ||||||
|  |             Accounts.PAYABLE, "Computer", "400", | ||||||
|  |             original_line_item=line_item_or) | ||||||
|  |         self._add_journal_entry(JournalEntryData( | ||||||
|  |             10, [JournalEntryCurrencyData( | ||||||
|  |                 "USD", [line_item_of], [JournalEntryLineItemData( | ||||||
|  |                     Accounts.CASH, "Computer", "400")])])) | ||||||
|  |  | ||||||
|  |         # Full offset and unmatched in TWD | ||||||
|  |         description = "演講費—母校" | ||||||
|  |         line_item_or = JournalEntryLineItemData( | ||||||
|  |             Accounts.RECEIVABLE, description, "3000") | ||||||
|  |         self._add_journal_entry(JournalEntryData( | ||||||
|  |             45, [JournalEntryCurrencyData( | ||||||
|  |                 "TWD", [line_item_or], [JournalEntryLineItemData( | ||||||
|  |                     Accounts.SERVICE, description, "3000")])])) | ||||||
|  |         line_item_of = JournalEntryLineItemData( | ||||||
|  |             Accounts.RECEIVABLE, description, "3000", | ||||||
|  |             original_line_item=line_item_or) | ||||||
|  |         self._add_journal_entry(JournalEntryData( | ||||||
|  |             6, [JournalEntryCurrencyData( | ||||||
|  |                 "TWD", [JournalEntryLineItemData( | ||||||
|  |                     Accounts.BANK, description, "3000")], | ||||||
|  |                 [line_item_of])])) | ||||||
|  |         self.__add_journal_entry( | ||||||
|  |             25, "TWD", "3000", | ||||||
|  |             Accounts.BANK, description, Accounts.SERVICE, description) | ||||||
|  |  | ||||||
|  |         # Partial offset in TWD | ||||||
|  |         line_item_or = JournalEntryLineItemData( | ||||||
|  |             Accounts.PAYABLE, "手機", "30000") | ||||||
|  |         self._add_journal_entry(JournalEntryData( | ||||||
|  |             55, [JournalEntryCurrencyData( | ||||||
|  |                 "TWD", [JournalEntryLineItemData( | ||||||
|  |                     Accounts.MACHINERY, "手機", "30000")], | ||||||
|  |                 [line_item_or])])) | ||||||
|  |         line_item_of = JournalEntryLineItemData( | ||||||
|  |             Accounts.PAYABLE, "手機", "16000", | ||||||
|  |             original_line_item=line_item_or) | ||||||
|  |         self._add_journal_entry(JournalEntryData( | ||||||
|  |             27, [JournalEntryCurrencyData( | ||||||
|  |                 "TWD", [line_item_of], [JournalEntryLineItemData( | ||||||
|  |                     Accounts.BANK, "手機", "16000")])])) | ||||||
|  |         line_item_of = JournalEntryLineItemData( | ||||||
|  |             Accounts.PAYABLE, "手機", "6000", | ||||||
|  |             original_line_item=line_item_or) | ||||||
|  |         self._add_journal_entry(JournalEntryData( | ||||||
|  |             8, [JournalEntryCurrencyData( | ||||||
|  |                 "TWD", [line_item_of], [JournalEntryLineItemData( | ||||||
|  |                     Accounts.CASH, "手機", "6000")])])) | ||||||
|  |  | ||||||
|  |     def __add_meals(self) -> None: | ||||||
|  |         """Adds the meal data. | ||||||
|  |  | ||||||
|  |         :return: None. | ||||||
|  |         """ | ||||||
|  |         days = 60 | ||||||
|  |         while days >= 0: | ||||||
|  |             # Meals in USD | ||||||
|  |             if days % 4 == 2: | ||||||
|  |                 self.__add_journal_entry( | ||||||
|  |                     days, "USD", "2.9", | ||||||
|  |                     Accounts.MEAL, "Lunch—Coffee", Accounts.CASH, None) | ||||||
|  |             else: | ||||||
|  |                 self.__add_journal_entry( | ||||||
|  |                     days, "USD", "3.9", | ||||||
|  |                     Accounts.MEAL, "Lunch—Coffee", Accounts.CASH, None) | ||||||
|  |  | ||||||
|  |             if days % 15 == 3: | ||||||
|  |                 self.__add_journal_entry( | ||||||
|  |                     days, "USD", "5.45", | ||||||
|  |                     Accounts.MEAL, "Dinner—Pizza", | ||||||
|  |                     Accounts.PAYABLE, "Dinner—Pizza") | ||||||
|  |             else: | ||||||
|  |                 self.__add_journal_entry( | ||||||
|  |                     days, "USD", "5.9", | ||||||
|  |                     Accounts.MEAL, "Dinner—Pasta", Accounts.CASH, None) | ||||||
|  |  | ||||||
|  |             # Meals in TWD | ||||||
|  |             if days % 5 == 3: | ||||||
|  |                 self.__add_journal_entry( | ||||||
|  |                     days, "TWD", "125", | ||||||
|  |                     Accounts.MEAL, "午餐—鄰家咖啡", Accounts.CASH, None) | ||||||
|  |             else: | ||||||
|  |                 self.__add_journal_entry( | ||||||
|  |                     days, "TWD", "80", | ||||||
|  |                     Accounts.MEAL, "午餐—便當", Accounts.CASH, None) | ||||||
|  |  | ||||||
|  |             if days % 15 == 3: | ||||||
|  |                 self.__add_journal_entry( | ||||||
|  |                     days, "TWD", "320", | ||||||
|  |                     Accounts.MEAL, "晚餐—牛排", Accounts.PAYABLE, "晚餐—牛排") | ||||||
|  |             else: | ||||||
|  |                 self.__add_journal_entry( | ||||||
|  |                     days, "TWD", "100", | ||||||
|  |                     Accounts.MEAL, "晚餐—自助餐", Accounts.CASH, None) | ||||||
|  |  | ||||||
|  |             days = days - 1 | ||||||
|  |  | ||||||
|  |     def __add_journal_entry( | ||||||
|  |             self, days: int, currency: str, amount: str, | ||||||
|  |             debit_account: str, debit_description: str | None, | ||||||
|  |             credit_account: str, credit_description: str | None) -> None: | ||||||
|  |         """Adds a simple journal entry. | ||||||
|  |  | ||||||
|  |         :param days: The number of days before today. | ||||||
|  |         :param currency: The currency code. | ||||||
|  |         :param amount: The amount. | ||||||
|  |         :param debit_account: The debit account code. | ||||||
|  |         :param debit_description: The debit description. | ||||||
|  |         :param credit_account: The credit account code. | ||||||
|  |         :param credit_description: The credit description. | ||||||
|  |         :return: None. | ||||||
|  |         """ | ||||||
|  |         self._add_journal_entry(JournalEntryData( | ||||||
|  |             days, | ||||||
|  |             [JournalEntryCurrencyData( | ||||||
|  |                 currency, | ||||||
|  |                 [JournalEntryLineItemData( | ||||||
|  |                     debit_account, debit_description, amount)], | ||||||
|  |                 [JournalEntryLineItemData( | ||||||
|  |                     credit_account, credit_description, amount)])])) | ||||||
|  |  | ||||||
|  |  | ||||||
| def init_app(app: Flask) -> None: | def init_app(app: Flask) -> None: | ||||||
| @@ -150,4 +368,3 @@ def init_app(app: Flask) -> None: | |||||||
|     :return: None. |     :return: None. | ||||||
|     """ |     """ | ||||||
|     app.register_blueprint(bp) |     app.register_blueprint(bp) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -23,8 +23,9 @@ import httpx | |||||||
| from flask import Flask | from flask import Flask | ||||||
|  |  | ||||||
| from test_site import db | from test_site import db | ||||||
| from testlib import create_test_app, get_client, Accounts, \ | from test_site.lib import JournalEntryCurrencyData, JournalEntryData, \ | ||||||
|     JournalEntryCurrencyData, JournalEntryData, BaseTestData |     BaseTestData | ||||||
|  | from testlib import create_test_app, get_client, Accounts | ||||||
|  |  | ||||||
| PREFIX: str = "/accounting/unmatched-offsets" | PREFIX: str = "/accounting/unmatched-offsets" | ||||||
| """The URL prefix for the unmatched offset management.""" | """The URL prefix for the unmatched offset management.""" | ||||||
|   | |||||||
							
								
								
									
										341
									
								
								tests/testlib.py
									
									
									
									
									
								
							
							
						
						
									
										341
									
								
								tests/testlib.py
									
									
									
									
									
								
							| @@ -19,21 +19,13 @@ | |||||||
| """ | """ | ||||||
| from __future__ import annotations | from __future__ import annotations | ||||||
|  |  | ||||||
| import json |  | ||||||
| import re | import re | ||||||
| import typing as t | import typing as t | ||||||
| from abc import ABC, abstractmethod |  | ||||||
| from datetime import date, timedelta |  | ||||||
| from secrets import randbelow |  | ||||||
|  |  | ||||||
| from decimal import Decimal |  | ||||||
| import sqlalchemy as sa |  | ||||||
|  |  | ||||||
| import httpx | import httpx | ||||||
| from flask import Flask, render_template_string | from flask import Flask, render_template_string | ||||||
|  |  | ||||||
| from test_site import create_app, db | from test_site import create_app | ||||||
| from test_site.auth import User |  | ||||||
|  |  | ||||||
| TEST_SERVER: str = "https://testserver" | TEST_SERVER: str = "https://testserver" | ||||||
| """The test server URI.""" | """The test server URI.""" | ||||||
| @@ -163,334 +155,3 @@ def match_journal_entry_detail(location: str) -> int: | |||||||
|         r"^/accounting/journal-entries/(\d+)\?next=%2F_next", location) |         r"^/accounting/journal-entries/(\d+)\?next=%2F_next", location) | ||||||
|     assert m is not None |     assert m is not None | ||||||
|     return int(m.group(1)) |     return int(m.group(1)) | ||||||
|  |  | ||||||
|  |  | ||||||
| class JournalEntryLineItemData: |  | ||||||
|     """The journal entry line item data.""" |  | ||||||
|  |  | ||||||
|     def __init__(self, account: str, description: str | None, amount: str, |  | ||||||
|                  original_line_item: JournalEntryLineItemData | None = None): |  | ||||||
|         """Constructs the journal entry line item data. |  | ||||||
|  |  | ||||||
|         :param account: The account code. |  | ||||||
|         :param description: The description. |  | ||||||
|         :param amount: The amount. |  | ||||||
|         :param original_line_item: The original journal entry line item. |  | ||||||
|         """ |  | ||||||
|         self.journal_entry: JournalEntryData | None = None |  | ||||||
|         self.id: int = -1 |  | ||||||
|         self.no: int = -1 |  | ||||||
|         self.original_line_item: JournalEntryLineItemData | None \ |  | ||||||
|             = original_line_item |  | ||||||
|         self.account: str = account |  | ||||||
|         self.description: str | None = description |  | ||||||
|         self.amount: Decimal = Decimal(amount) |  | ||||||
|  |  | ||||||
|     def form(self, prefix: str, debit_credit: str, index: int, |  | ||||||
|              is_update: bool) -> dict[str, str]: |  | ||||||
|         """Returns the line item as form data. |  | ||||||
|  |  | ||||||
|         :param prefix: The prefix of the form fields. |  | ||||||
|         :param debit_credit: Either "debit" or "credit". |  | ||||||
|         :param index: The line item index. |  | ||||||
|         :param is_update: True for an update operation, or False otherwise |  | ||||||
|         :return: The form data. |  | ||||||
|         """ |  | ||||||
|         prefix = f"{prefix}-{debit_credit}-{index}" |  | ||||||
|         form: dict[str, str] = {f"{prefix}-account_code": self.account, |  | ||||||
|                                 f"{prefix}-description": self.description, |  | ||||||
|                                 f"{prefix}-amount": str(self.amount)} |  | ||||||
|         if is_update and self.id != -1: |  | ||||||
|             form[f"{prefix}-id"] = str(self.id) |  | ||||||
|         form[f"{prefix}-no"] = str(index) if self.no == -1 else str(self.no) |  | ||||||
|         if self.original_line_item is not None: |  | ||||||
|             assert self.original_line_item.id != -1 |  | ||||||
|             form[f"{prefix}-original_line_item_id"] \ |  | ||||||
|                 = str(self.original_line_item.id) |  | ||||||
|         return form |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class JournalEntryCurrencyData: |  | ||||||
|     """The journal entry currency data.""" |  | ||||||
|  |  | ||||||
|     def __init__(self, currency: str, debit: list[JournalEntryLineItemData], |  | ||||||
|                  credit: list[JournalEntryLineItemData]): |  | ||||||
|         """Constructs the journal entry currency data. |  | ||||||
|  |  | ||||||
|         :param currency: The currency code. |  | ||||||
|         :param debit: The debit line items. |  | ||||||
|         :param credit: The credit line items. |  | ||||||
|         """ |  | ||||||
|         self.code: str = currency |  | ||||||
|         self.debit: list[JournalEntryLineItemData] = debit |  | ||||||
|         self.credit: list[JournalEntryLineItemData] = credit |  | ||||||
|  |  | ||||||
|     def form(self, index: int, is_update: bool) -> dict[str, str]: |  | ||||||
|         """Returns the currency as form data. |  | ||||||
|  |  | ||||||
|         :param index: The currency index. |  | ||||||
|         :param is_update: True for an update operation, or False otherwise |  | ||||||
|         :return: The form data. |  | ||||||
|         """ |  | ||||||
|         prefix: str = f"currency-{index}" |  | ||||||
|         form: dict[str, str] = {f"{prefix}-code": self.code} |  | ||||||
|         for i in range(len(self.debit)): |  | ||||||
|             form.update(self.debit[i].form(prefix, "debit", i + 1, is_update)) |  | ||||||
|         for i in range(len(self.credit)): |  | ||||||
|             form.update(self.credit[i].form(prefix, "credit", i + 1, |  | ||||||
|                                             is_update)) |  | ||||||
|         return form |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class JournalEntryData: |  | ||||||
|     """The journal entry data.""" |  | ||||||
|  |  | ||||||
|     def __init__(self, days: int, currencies: list[JournalEntryCurrencyData]): |  | ||||||
|         """Constructs a journal entry. |  | ||||||
|  |  | ||||||
|         :param days: The number of days before today. |  | ||||||
|         :param currencies: The journal entry currency data. |  | ||||||
|         """ |  | ||||||
|         self.id: int = -1 |  | ||||||
|         self.days: int = days |  | ||||||
|         self.currencies: list[JournalEntryCurrencyData] = currencies |  | ||||||
|         self.note: str | None = None |  | ||||||
|         for currency in self.currencies: |  | ||||||
|             for line_item in currency.debit: |  | ||||||
|                 line_item.journal_entry = self |  | ||||||
|             for line_item in currency.credit: |  | ||||||
|                 line_item.journal_entry = self |  | ||||||
|  |  | ||||||
|     def new_form(self, csrf_token: str) -> dict[str, str]: |  | ||||||
|         """Returns the journal entry as a creation form. |  | ||||||
|  |  | ||||||
|         :param csrf_token: The CSRF token. |  | ||||||
|         :return: The journal entry as a creation form. |  | ||||||
|         """ |  | ||||||
|         return self.__form(csrf_token, is_update=False) |  | ||||||
|  |  | ||||||
|     def update_form(self, csrf_token: str) -> dict[str, str]: |  | ||||||
|         """Returns the journal entry as an update form. |  | ||||||
|  |  | ||||||
|         :param csrf_token: The CSRF token. |  | ||||||
|         :return: The journal entry as an update form. |  | ||||||
|         """ |  | ||||||
|         return self.__form(csrf_token, is_update=True) |  | ||||||
|  |  | ||||||
|     def __form(self, csrf_token: str, is_update: bool = False) \ |  | ||||||
|             -> dict[str, str]: |  | ||||||
|         """Returns the journal entry as a form. |  | ||||||
|  |  | ||||||
|         :param csrf_token: The CSRF token. |  | ||||||
|         :param is_update: True for an update operation, or False otherwise |  | ||||||
|         :return: The journal entry as a form. |  | ||||||
|         """ |  | ||||||
|         journal_entry_date: date = date.today() - timedelta(days=self.days) |  | ||||||
|         form: dict[str, str] = {"csrf_token": csrf_token, |  | ||||||
|                                 "next": NEXT_URI, |  | ||||||
|                                 "date": journal_entry_date.isoformat()} |  | ||||||
|         for i in range(len(self.currencies)): |  | ||||||
|             form.update(self.currencies[i].form(i + 1, is_update)) |  | ||||||
|         if self.note is not None: |  | ||||||
|             form["note"] = self.note |  | ||||||
|         return form |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class BaseTestData(ABC): |  | ||||||
|     """The base test data.""" |  | ||||||
|  |  | ||||||
|     def __init__(self, app: Flask, username: str): |  | ||||||
|         """Constructs the test data. |  | ||||||
|  |  | ||||||
|         :param app: The Flask application. |  | ||||||
|         :param username: The username. |  | ||||||
|         """ |  | ||||||
|         self.__app: Flask = app |  | ||||||
|         with self.__app.app_context(): |  | ||||||
|             current_user: User | None = User.query\ |  | ||||||
|                 .filter(User.username == username).first() |  | ||||||
|             assert current_user is not None |  | ||||||
|             self.__current_user_id: int = current_user.id |  | ||||||
|             self.__journal_entries: list[dict[str, t.Any]] = [] |  | ||||||
|             self.__line_items: list[dict[str, t.Any]] = [] |  | ||||||
|             self._init_data() |  | ||||||
|  |  | ||||||
|     @abstractmethod |  | ||||||
|     def _init_data(self) -> None: |  | ||||||
|         """Initializes the test data. |  | ||||||
|  |  | ||||||
|         :return: None |  | ||||||
|         """ |  | ||||||
|  |  | ||||||
|     def populate(self) -> None: |  | ||||||
|         """Populates the data into the database. |  | ||||||
|  |  | ||||||
|         :return: None |  | ||||||
|         """ |  | ||||||
|         from accounting.models import JournalEntry, JournalEntryLineItem |  | ||||||
|         with self.__app.app_context(): |  | ||||||
|             db.session.execute(sa.insert(JournalEntry), self.__journal_entries) |  | ||||||
|             db.session.execute(sa.insert(JournalEntryLineItem), |  | ||||||
|                                self.__line_items) |  | ||||||
|             db.session.commit() |  | ||||||
|  |  | ||||||
|     def json(self) -> str: |  | ||||||
|         """Returns the data as JSON. |  | ||||||
|  |  | ||||||
|         :return: The JSON string. |  | ||||||
|         """ |  | ||||||
|         from accounting.models import Account |  | ||||||
|         today: date = date.today() |  | ||||||
|  |  | ||||||
|         def filter_journal_entry(data: dict[str, t.Any]) -> list[t.Any]: |  | ||||||
|             """Filters the journal entry data for JSON encoding. |  | ||||||
|  |  | ||||||
|             :param data: The journal entry data. |  | ||||||
|             :return: The journal entry data for JSON encoding. |  | ||||||
|             """ |  | ||||||
|             data = data.copy() |  | ||||||
|             data["date"] = (today - data["date"]).days |  | ||||||
|             del data["created_by_id"] |  | ||||||
|             del data["updated_by_id"] |  | ||||||
|             return [data[x] for x in ["id", "date", "no", "note"]] |  | ||||||
|  |  | ||||||
|         def filter_line_item(data: dict[str, t.Any]) -> list[t.Any]: |  | ||||||
|             """Filters the journal entry line item data for JSON encoding. |  | ||||||
|  |  | ||||||
|             :param data: The journal entry line item data. |  | ||||||
|             :return: The journal entry line item data for JSON encoding. |  | ||||||
|             """ |  | ||||||
|             data = data.copy() |  | ||||||
|             with self.__app.app_context(): |  | ||||||
|                 data["account_id"] \ |  | ||||||
|                     = db.session.get(Account, data["account_id"]).code |  | ||||||
|             data["amount"] = str(data["amount"]) |  | ||||||
|             if "original_line_item_id" not in data: |  | ||||||
|                 data["original_line_item_id"] = None |  | ||||||
|             return [data[x] for x in ["id", "journal_entry_id", |  | ||||||
|                                       "original_line_item_id", "is_debit", |  | ||||||
|                                       "no", "account_id", "currency_code", |  | ||||||
|                                       "description", "amount"]] |  | ||||||
|  |  | ||||||
|         return json.dumps( |  | ||||||
|             [[filter_journal_entry(x) for x in self.__journal_entries], |  | ||||||
|              [filter_line_item(x) for x in self.__line_items]], |  | ||||||
|             ensure_ascii=False, separators=(",", ":")) |  | ||||||
|  |  | ||||||
|     @staticmethod |  | ||||||
|     def _couple(description: str, amount: str, debit: str, credit: str) \ |  | ||||||
|             -> tuple[JournalEntryLineItemData, JournalEntryLineItemData]: |  | ||||||
|         """Returns a couple of debit-credit line items. |  | ||||||
|  |  | ||||||
|         :param description: The description. |  | ||||||
|         :param amount: The amount. |  | ||||||
|         :param debit: The debit account code. |  | ||||||
|         :param credit: The credit account code. |  | ||||||
|         :return: The debit line item and credit line item. |  | ||||||
|         """ |  | ||||||
|         return JournalEntryLineItemData(debit, description, amount),\ |  | ||||||
|             JournalEntryLineItemData(credit, description, amount) |  | ||||||
|  |  | ||||||
|     def _add_journal_entry(self, journal_entry_data: JournalEntryData) -> None: |  | ||||||
|         """Adds a journal entry. |  | ||||||
|  |  | ||||||
|         :param journal_entry_data: The journal entry data. |  | ||||||
|         :return: None. |  | ||||||
|         """ |  | ||||||
|         from accounting.models import Account |  | ||||||
|         existing_j_id: set[int] = {x["id"] for x in self.__journal_entries} |  | ||||||
|         existing_l_id: set[int] = {x["id"] for x in self.__line_items} |  | ||||||
|         journal_entry_data.id = self.__new_id(existing_j_id) |  | ||||||
|         j_date: date = date.today() - timedelta(days=journal_entry_data.days) |  | ||||||
|         self.__journal_entries.append( |  | ||||||
|             {"id": journal_entry_data.id, |  | ||||||
|              "date": j_date, |  | ||||||
|              "no": self.__next_j_no(j_date), |  | ||||||
|              "note": journal_entry_data.note, |  | ||||||
|              "created_by_id": self.__current_user_id, |  | ||||||
|              "updated_by_id": self.__current_user_id}) |  | ||||||
|         debit_no: int = 0 |  | ||||||
|         credit_no: int = 0 |  | ||||||
|         for currency in journal_entry_data.currencies: |  | ||||||
|             for line_item in currency.debit: |  | ||||||
|                 account: Account | None \ |  | ||||||
|                     = Account.find_by_code(line_item.account) |  | ||||||
|                 assert account is not None |  | ||||||
|                 debit_no = debit_no + 1 |  | ||||||
|                 line_item.id = self.__new_id(existing_l_id) |  | ||||||
|                 data: dict[str, t.Any] \ |  | ||||||
|                     = {"id": line_item.id, |  | ||||||
|                        "journal_entry_id": journal_entry_data.id, |  | ||||||
|                        "is_debit": True, |  | ||||||
|                        "no": debit_no, |  | ||||||
|                        "account_id": account.id, |  | ||||||
|                        "currency_code": currency.code, |  | ||||||
|                        "description": line_item.description, |  | ||||||
|                        "amount": line_item.amount} |  | ||||||
|                 if line_item.original_line_item is not None: |  | ||||||
|                     data["original_line_item_id"] \ |  | ||||||
|                         = line_item.original_line_item.id |  | ||||||
|                 self.__line_items.append(data) |  | ||||||
|             for line_item in currency.credit: |  | ||||||
|                 account: Account | None \ |  | ||||||
|                     = Account.find_by_code(line_item.account) |  | ||||||
|                 assert account is not None |  | ||||||
|                 credit_no = credit_no + 1 |  | ||||||
|                 line_item.id = self.__new_id(existing_l_id) |  | ||||||
|                 data: dict[str, t.Any] \ |  | ||||||
|                     = {"id": line_item.id, |  | ||||||
|                        "journal_entry_id": journal_entry_data.id, |  | ||||||
|                        "is_debit": False, |  | ||||||
|                        "no": credit_no, |  | ||||||
|                        "account_id": account.id, |  | ||||||
|                        "currency_code": currency.code, |  | ||||||
|                        "description": line_item.description, |  | ||||||
|                        "amount": line_item.amount} |  | ||||||
|                 if line_item.original_line_item is not None: |  | ||||||
|                     data["original_line_item_id"] \ |  | ||||||
|                         = line_item.original_line_item.id |  | ||||||
|                 self.__line_items.append(data) |  | ||||||
|  |  | ||||||
|     @staticmethod |  | ||||||
|     def __new_id(existing_id: set[int]) -> int: |  | ||||||
|         """Generates and returns a new random unique ID. |  | ||||||
|  |  | ||||||
|         :param existing_id: The existing ID. |  | ||||||
|         :return: The newly-generated random unique ID. |  | ||||||
|         """ |  | ||||||
|         while True: |  | ||||||
|             obj_id: int = 100000000 + randbelow(900000000) |  | ||||||
|             if obj_id not in existing_id: |  | ||||||
|                 existing_id.add(obj_id) |  | ||||||
|                 return obj_id |  | ||||||
|  |  | ||||||
|     def __next_j_no(self, j_date: date) -> int: |  | ||||||
|         """Returns the next journal entry number in a day. |  | ||||||
|  |  | ||||||
|         :param j_date: The journal entry date. |  | ||||||
|         :return: The next journal entry number. |  | ||||||
|         """ |  | ||||||
|         existing: set[int] = {x["no"] for x in self.__journal_entries |  | ||||||
|                               if x["date"] == j_date} |  | ||||||
|         return 1 if len(existing) == 0 else max(existing) + 1 |  | ||||||
|  |  | ||||||
|     def _add_simple_journal_entry( |  | ||||||
|             self, days: int, currency: str, description: str, amount: str, |  | ||||||
|             debit: str, credit: str) \ |  | ||||||
|             -> tuple[JournalEntryLineItemData, JournalEntryLineItemData]: |  | ||||||
|         """Adds a simple journal entry. |  | ||||||
|  |  | ||||||
|         :param days: The number of days before today. |  | ||||||
|         :param currency: The currency code. |  | ||||||
|         :param description: The description. |  | ||||||
|         :param amount: The amount. |  | ||||||
|         :param debit: The debit account code. |  | ||||||
|         :param credit: The credit account code. |  | ||||||
|         :return: The debit line item and credit line item. |  | ||||||
|         """ |  | ||||||
|         debit_item, credit_item = self._couple( |  | ||||||
|             description, amount, debit, credit) |  | ||||||
|         self._add_journal_entry(JournalEntryData( |  | ||||||
|             days, [JournalEntryCurrencyData( |  | ||||||
|                 currency, [debit_item], [credit_item])])) |  | ||||||
|         return debit_item, credit_item |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user