diff --git a/l10n_th_bank_payment_export/__manifest__.py b/l10n_th_bank_payment_export/__manifest__.py index 46ad65278..805099987 100644 --- a/l10n_th_bank_payment_export/__manifest__.py +++ b/l10n_th_bank_payment_export/__manifest__.py @@ -17,6 +17,7 @@ "data/report_action.xml", "data/server_action.xml", "templates/report_template.xml", + "views/bank_export_format_view.xml", "views/bank_payment_template_view.xml", "views/account_payment_view.xml", "views/bank_payment_export_view.xml", diff --git a/l10n_th_bank_payment_export/data/report_action.xml b/l10n_th_bank_payment_export/data/report_action.xml index 878d7a913..17a7b83cf 100644 --- a/l10n_th_bank_payment_export/data/report_action.xml +++ b/l10n_th_bank_payment_export/data/report_action.xml @@ -1,8 +1,8 @@ - - Export Text Payment Demo + + Export Text Payment bank.payment.export ir.actions.report rec.lenght: + raise UserError( + _("Value %(value)s is longer than lenght %(lenght)s") + % {"value": rec.value, "lenght": rec.lenght} + ) diff --git a/l10n_th_bank_payment_export/models/bank_payment_export.py b/l10n_th_bank_payment_export/models/bank_payment_export.py index 48c4a6aca..efd29c245 100644 --- a/l10n_th_bank_payment_export/models/bank_payment_export.py +++ b/l10n_th_bank_payment_export/models/bank_payment_export.py @@ -1,8 +1,11 @@ # Copyright 2021 Ecosoft Co., Ltd. (http://ecosoft.co.th) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from datetime import datetime + from odoo import _, api, fields, models from odoo.exceptions import UserError +from odoo.tools.safe_eval import safe_eval class BankPaymentExport(models.Model): @@ -32,6 +35,14 @@ class BankPaymentExport(models.Model): tracking=True, check_company=True, ) + bank_export_format_id = fields.Many2one( + comodel_name="bank.export.format", + string="Bank Export Format", + readonly=True, + states={"draft": [("readonly", False)]}, + domain="[('bank', '=', bank)]", + tracking=True, + ) effective_date = fields.Date( copy=False, readonly=True, @@ -154,7 +165,7 @@ def _get_report_base_filename(self): return "{}".format(self.name) def _get_view_report_text(self): - return "l10n_th_bank_payment_export.action_payment_demo_txt" + return "l10n_th_bank_payment_export.action_payment_txt" def _get_view_report_xlsx(self): return "l10n_th_bank_payment_export.action_export_payment_xlsx" @@ -168,18 +179,159 @@ def print_report(self, report_type): view_report = self._get_view_report_xlsx() return self.env.ref(view_report).sudo().report_action(self, config=False) + def _set_global_dict(self): + """Set global dict for eval""" + today = fields.Date.context_today(self) + today_datetime = fields.Datetime.context_timestamp( + self.env.user, datetime.now() + ) + globals_dict = { + "rec": self, + "line": self.export_line_ids, + "today": today, + "today_datetime": today_datetime, + } + return globals_dict + + def _update_global_dict(self, globals_dict, **kwargs): + """Update global dict with kwargs""" + globals_dict.update(kwargs) + return globals_dict + def _generate_bank_payment_text(self): self.ensure_one() - return + globals_dict = self._set_global_dict() + text_parts = [] + processed_match = set() + + # Get format from bank + if not self.bank_export_format_id: + raise UserError(_("Bank format not found.")) + + exp_format_lines = self.bank_export_format_id.export_format_ids + + for idx, exp_format in enumerate(exp_format_lines): + if exp_format.display_type: + continue + + # Skip if value has already been processed, and need_loop is True + if exp_format.need_loop and exp_format.match_group in processed_match: + continue + + # Add idx to globals_dict + globals_dict = self._update_global_dict(globals_dict, idx=idx) + + # Skip this line if condition is not met + if not exp_format.need_loop and exp_format.condition_line: + condition = safe_eval( + exp_format.condition_line, globals_dict=globals_dict + ) + if not condition: + continue + + # Add value to the set of processed values + if exp_format.match_group: + processed_match.add(exp_format.match_group) + + if exp_format.need_loop: + self._process_loop( + exp_format, exp_format_lines, globals_dict, text_parts + ) + continue + + # Get value from instruction + text_line = exp_format._get_value(globals_dict) + text_parts.append(text_line) + + if exp_format.end_line: + # TODO: Change this to configurable + text_parts.append("\r\n") + + text = "".join(text_parts) + return text + + def _process_loop(self, exp_format, exp_format_lines, globals_dict, text_parts): + # Get all lines that match the current group + for idx_line, line in enumerate(self.export_line_ids): + # Change the value of the line in the globals_dict + globals_dict_line = self._update_global_dict( + globals_dict, line=line, idx_line=idx_line + ) + + # search only lines that match the current group and condition + # filter in loop because we need to check condition_line + exp_format_line_group = exp_format_lines.filtered( + lambda l: l.match_group == exp_format.match_group + and ( + not l.condition_line + or safe_eval(l.condition_line, globals_dict=globals_dict_line) + ) + ) + + processed_subloop = set() + + for exp_format_line in exp_format_line_group: + # Sub-loop logic + if exp_format_line.sub_loop: + self._process_sub_loop( + exp_format_line, + exp_format_line_group, + globals_dict_line, + text_parts, + processed_subloop, + ) + continue + + # Get value from instruction + text_line = exp_format_line._get_value(globals_dict_line) + text_parts.append(text_line) + + if exp_format_line.end_line: + # TODO: Change this to configurable + text_parts.append("\r\n") + return text_parts + + def _process_sub_loop( + self, + exp_format_line, + exp_format_line_group, + globals_dict_line, + text_parts, + processed_subloop, + ): + if exp_format_line.sub_value_loop not in processed_subloop: + processed_subloop.add(exp_format_line.sub_value_loop) + + exp_format_sub_line_group = exp_format_line_group.filtered( + lambda l: l.sub_value_loop == exp_format_line.sub_value_loop + ) + sub_lines = safe_eval( + exp_format_line.sub_value_loop, globals_dict=globals_dict_line + ) + + for idx_sub_line, sub_line in enumerate(sub_lines): + for exp_format_sub_line in exp_format_sub_line_group: + # Update globals_dict for sub-loop + globals_dict_sub_line = self._update_global_dict( + globals_dict_line, sub_line=sub_line, idx_sub_line=idx_sub_line + ) + + # Get value from sub-instruction + sub_text_line = exp_format_sub_line._get_value( + globals_dict_sub_line + ) + text_parts.append(sub_text_line) + + if exp_format_sub_line.end_line: + # TODO: Change this to configurable + text_parts.append("\r\n") + return text_parts def _export_bank_payment_text_file(self): self.ensure_one() if self.bank: return self._generate_bank_payment_text() - return ( - "Demo Text File. You can inherit function " - "_generate_bank_payment_text() for customize your format." - ) + return "Demo Text File. You must config `Bank Export Format` First." def _check_constraint_line(self): # Add condition with line on this function @@ -239,10 +391,6 @@ def _get_context_create_bank_payment_export(self, payments): ) return ctx - def _get_amount_no_decimal(self, amount, digits=False): - """Implementation is available""" - return amount - @api.constrains("effective_date") def check_effective_date(self): today = fields.Date.context_today(self) @@ -303,3 +451,21 @@ def action_create_bank_payment_export(self): "view_id": view.id, "context": ctx, } + + # ====================== Function Common Text File ====================== + + def _get_receiver_address(self, object_address): + receiver_address = " ".join( + [ + object_address.street or "", + object_address.street2 or "", + object_address.city or "", + object_address.zip or "", + ] + ) + return receiver_address + + def _get_address(self, object_address, max_length): + receiver_address = self._get_receiver_address(object_address) + address = receiver_address[:max_length] + return address diff --git a/l10n_th_bank_payment_export/models/bank_payment_export_line.py b/l10n_th_bank_payment_export/models/bank_payment_export_line.py index 03a9066ca..c8d2b2d19 100644 --- a/l10n_th_bank_payment_export/models/bank_payment_export_line.py +++ b/l10n_th_bank_payment_export/models/bank_payment_export_line.py @@ -138,21 +138,10 @@ def unlink(self): # ====================== Function Common Text File ====================== - def _get_payment_net_amount(self): - # TODO: Not support multi-currency - # payment_net_amount = self.currency_id._convert( - # self.payment_amount, - # self.payment_id.company_id.currency_id, - # self.payment_id.company_id, - # self.payment_date, - # round=False, - # ) - payment_net_amount = self.payment_amount - return payment_net_amount - - def _get_amount_no_decimal(self, amount, digits=False): - """Implementation is available""" - return amount + def sanitize_account_number(self, acc_number): + if not acc_number: + return "" + return sanitize_account_number(acc_number) def _get_acc_number_digit(self, partner_bank_id): acc_number = partner_bank_id.acc_number @@ -194,30 +183,3 @@ def _get_acc_number_digit(self, partner_bank_id): or "**Digit account number is not correct**" ) return sanitize_acc_number - - def _get_receiver_information(self): - self.ensure_one() - partner_bank_id = self.payment_partner_bank_id - receiver_name = ( - partner_bank_id.acc_holder_name or partner_bank_id.partner_id.display_name - ) - receiver_bank_code = partner_bank_id.bank_id.bank_code - receiver_branch_code = partner_bank_id.bank_id.bank_branch_code - receiver_acc_number = self._get_acc_number_digit(partner_bank_id) - return ( - receiver_name, - receiver_bank_code, - receiver_branch_code, - receiver_acc_number, - ) - - def _get_sender_information(self): - self.ensure_one() - # Sender - sender_journal_id = self.payment_id.journal_id - sender_bank_code = sender_journal_id.bank_id.bank_code - sender_branch_code = sender_journal_id.bank_id.bank_branch_code - sender_acc_number = sanitize_account_number( - sender_journal_id.bank_account_id.acc_number - ) - return sender_bank_code, sender_branch_code, sender_acc_number diff --git a/l10n_th_bank_payment_export/security/ir.model.access.csv b/l10n_th_bank_payment_export/security/ir.model.access.csv index 7c3ebadf8..5d29ecc71 100644 --- a/l10n_th_bank_payment_export/security/ir.model.access.csv +++ b/l10n_th_bank_payment_export/security/ir.model.access.csv @@ -6,3 +6,5 @@ access_bank_payment_config_user,access_bank_payment_config_user,model_bank_payme access_bank_payment_export,access_bank_payment_export,model_bank_payment_export,account.group_account_invoice,1,1,1,1 access_bank_payment_export_line,access_bank_payment_export_line,model_bank_payment_export_line,account.group_account_invoice,1,1,1,1 access_ir_model_fields_invoices,access_ir_model_fields_invoices,base.model_ir_model_fields,account.group_account_invoice,1,0,0,0 +access_bank_export_format,access_bank_export_format,model_bank_export_format,,1,1,1,1 +access_bank_export_format_line,access_bank_export_format_line,model_bank_export_format_line,,1,1,1,1 diff --git a/l10n_th_bank_payment_export/templates/report_bank_payment_export_xlsx.py b/l10n_th_bank_payment_export/templates/report_bank_payment_export_xlsx.py index d487bbf74..b2ca3084b 100644 --- a/l10n_th_bank_payment_export/templates/report_bank_payment_export_xlsx.py +++ b/l10n_th_bank_payment_export/templates/report_bank_payment_export_xlsx.py @@ -90,20 +90,6 @@ def _get_export_payment_vals(self, obj): }, "width": 10, }, - "11_email": { - "header": {"value": "Email"}, - "data": { - "value": self._render("email"), - }, - "width": 20, - }, - "12_phone": { - "header": {"value": "Phone"}, - "data": { - "value": self._render("phone"), - }, - "width": 15, - }, } def _get_ws_params(self, wb, data, obj): @@ -124,23 +110,19 @@ def _get_render_space(self, idx, pe_line, obj): recipient_bank = pe_line.payment_partner_bank_id acc_number = recipient_bank.acc_number or False received_bank = recipient_bank.bank_id - partner_id = pe_line.payment_partner_id - payment_net_amount = pe_line._get_payment_net_amount() return { "sequence": idx + 1, "reference": pe_line.payment_id.name, "payment_date": pe_line.payment_date.strftime("%d/%m/%Y"), - "partner": partner_id.display_name or "", + "partner": pe_line.payment_partner_id.display_name or "", "acc_number": acc_number and acc_number.zfill(11) or "", "bank_name": received_bank.name or "", "acc_holder_name": recipient_bank.acc_holder_name or recipient_bank.partner_id.display_name or "", - "amount": payment_net_amount, + "amount": pe_line.payment_amount, "bank_code": received_bank.bank_code or "", "bank_branch_code": received_bank.bank_branch_code or "", - "email": partner_id.email or "", - "phone": partner_id.phone or "", } def _get_header_data_list(self, obj): @@ -177,12 +159,16 @@ def _write_ws_lines(self, row_pos, ws, ws_params, obj, payment_export_line): ) return row_pos - def _write_ws_footer(self, row_pos, ws, obj, payment_export_line): + def _set_range_footer(self, row_pos, ws, obj): + """Hooks this function to set range footer""" ws.merge_range(row_pos, 0, row_pos, 6, "") - ws.merge_range(row_pos, 8, row_pos, 11, "") + ws.merge_range(row_pos, 8, row_pos, 9, "") ws.write_row( row_pos, 0, ["Total Balance"], FORMATS["format_theader_blue_right"] ) + + def _write_ws_footer(self, row_pos, ws, obj, payment_export_line): + self._set_range_footer(row_pos, ws, obj) # TODO: Not support Multi Currency yet. total_balance_payment = sum( pe_line.currency_id._convert( diff --git a/l10n_th_bank_payment_export/tests/bank_payment_export_tester.py b/l10n_th_bank_payment_export/tests/bank_payment_export_tester.py index eb193bf9f..3efcab04c 100644 --- a/l10n_th_bank_payment_export/tests/bank_payment_export_tester.py +++ b/l10n_th_bank_payment_export/tests/bank_payment_export_tester.py @@ -20,3 +20,12 @@ class BankPaymentTemplateTester(models.Model): selection_add=[("TEST", "Test Bank")], ondelete={"TEST": "cascade"}, ) + + +class BankExportFormatTester(models.Model): + _inherit = "bank.export.format" + + bank = fields.Selection( + selection_add=[("TEST", "Test Bank")], + ondelete={"TEST": "cascade"}, + ) diff --git a/l10n_th_bank_payment_export/tests/common.py b/l10n_th_bank_payment_export/tests/common.py index 20163bada..8293795e8 100644 --- a/l10n_th_bank_payment_export/tests/common.py +++ b/l10n_th_bank_payment_export/tests/common.py @@ -14,17 +14,21 @@ def setUpClass(cls): cls.loader = FakeModelLoader(cls.env, cls.__module__) cls.loader.backup_registry() from .bank_payment_export_tester import ( + BankExportFormatTester, BankPaymentExportTester, BankPaymentTemplateTester, ) - cls.loader.update_registry((BankPaymentExportTester, BankPaymentTemplateTester)) + cls.loader.update_registry( + (BankPaymentExportTester, BankPaymentTemplateTester, BankExportFormatTester) + ) cls.move_model = cls.env["account.move"] cls.journal_model = cls.env["account.journal"] cls.bank_payment_template_model = cls.env["bank.payment.template"] cls.bank_payment_export_model = cls.env["bank.payment.export"] cls.field_model = cls.env["ir.model.fields"] + cls.bank_export_format_model = cls.env["bank.export.format"] cls.register_payments_model = cls.env["account.payment.register"] cls.main_company_id = cls.env.ref("base.main_company").id cls.main_currency_id = cls.env.ref("base.USD").id diff --git a/l10n_th_bank_payment_export/tests/test_bank_payment_export.py b/l10n_th_bank_payment_export/tests/test_bank_payment_export.py index 34efaa545..711cdfd29 100644 --- a/l10n_th_bank_payment_export/tests/test_bank_payment_export.py +++ b/l10n_th_bank_payment_export/tests/test_bank_payment_export.py @@ -16,19 +16,49 @@ def setUpClass(cls): field_effective_date = cls.field_model.search( [("name", "=", "effective_date"), ("model", "=", "bank.payment.export")] ) - cls.template_test_bank = cls.bank_payment_template_model.create( + field_bank_export_format_id = cls.field_model.search( + [("name", "=", "bank_export_format_id")] + ) + + data_dict = [ + { + "field_id": field_effective_date.id, + "value": "9999-01-01", + } + ] + + cls.template_test_bank = cls.create_bank_payment_template( + cls, + "TEST", + data_dict, + ) + cls.bank_export_format = cls.bank_export_format_model.create( { - "name": "Template Bank Test", + "name": "Test Bank", "bank": "TEST", - "template_config_line": [ + "export_format_ids": [ ( 0, 0, { - "field_id": field_effective_date.id, - "value": "9999-01-01", + "sequence": 10, + "lenght": 10, + "name": "Test Line1", + "value_type": "fixed", + "value": "9999999999", }, - ) + ), + ( + 0, + 0, + { + "sequence": 11, + "lenght": 4, + "name": "Test Line2", + "value_type": "fixed", + "value": "TEST", + }, + ), ], } ) @@ -140,11 +170,6 @@ def test_03_common_function(self): bank_payment.action_get_all_payments() self.assertEqual(len(bank_payment.export_line_ids.ids), 2) self.assertFalse(bank_payment.is_required_effective_date) - # Test function for generate text file - amount_test = bank_payment._get_amount_no_decimal(100.0) - self.assertEqual(amount_test, 100.0) - text_file = bank_payment._generate_bank_payment_text() - self.assertFalse(text_file) # Test bank difference bank payment with self.assertRaises(UserError): bank_payment.export_line_ids[ @@ -164,59 +189,18 @@ def test_03_common_function(self): line.payment_partner_bank_id.bank_id.bic = "BAABTHBK" line.payment_partner_bank_id.acc_number = "123456789012" self.assertEqual(len(line.payment_partner_bank_id.acc_number), 12) - ( - receiver_name, - receiver_bank_code, - receiver_branch_code, - receiver_acc_number, - ) = line._get_receiver_information() - self.assertEqual(len(receiver_acc_number), 11) - self.assertEqual(receiver_acc_number, "23456789012") # TFPCTHB1 14 digits -> 11 digits (5 - 14 and add 0 at first digit) line.payment_partner_bank_id.bank_id.bic = "TFPCTHB1" line.payment_partner_bank_id.acc_number = "12345678901234" self.assertEqual(len(line.payment_partner_bank_id.acc_number), 14) - ( - receiver_name, - receiver_bank_code, - receiver_branch_code, - receiver_acc_number, - ) = line._get_receiver_information() - self.assertEqual(len(receiver_acc_number), 11) - self.assertEqual(receiver_acc_number, "05678901234") # TIBTTHBK 12 digits -> 11 digits (3 - 12 and add 0 at first digit) line.payment_partner_bank_id.bank_id.bic = "TIBTTHBK" line.payment_partner_bank_id.acc_number = "123456789012" self.assertEqual(len(line.payment_partner_bank_id.acc_number), 12) - ( - receiver_name, - receiver_bank_code, - receiver_branch_code, - receiver_acc_number, - ) = line._get_receiver_information() - self.assertEqual(len(receiver_acc_number), 11) - self.assertEqual(receiver_acc_number, "03456789012") # GSBATHBK 12 digits -> 11 digits (2 - 12) line.payment_partner_bank_id.bank_id.bic = "GSBATHBK" line.payment_partner_bank_id.acc_number = "123456789012" self.assertEqual(len(line.payment_partner_bank_id.acc_number), 12) - ( - receiver_name, - receiver_bank_code, - receiver_branch_code, - receiver_acc_number, - ) = line._get_receiver_information() - self.assertEqual(len(receiver_acc_number), 11) - self.assertEqual(receiver_acc_number, "23456789012") - ( - sender_bank_code, - sender_branch_code, - sender_acc_number, - ) = line._get_sender_information() - # Default from recipient bank - if line.payment_partner_id == self.partner_2: - self.assertTrue(receiver_name) - self.assertTrue(receiver_acc_number) # Test register payment with not group payment invoice = self.create_invoice( @@ -248,6 +232,7 @@ def test_04_create_bank_payment_export_direct(self): self.assertFalse(bank_payment.bank) with Form(bank_payment) as pe: pe.template_id = self.template_test_bank + pe.bank_export_format_id = self.bank_export_format bank_payment = pe.save() self.assertEqual(bank_payment.bank, "TEST") # Test unlink document state draft @@ -294,8 +279,7 @@ def test_04_create_bank_payment_export_direct(self): text_word = bank_payment._export_bank_payment_text_file() self.assertEqual( text_word, - "Demo Text File. You can inherit function " - "_generate_bank_payment_text() for customize your format.", + "Demo Text File. You must config `Bank Export Format` First.", ) # Reject some payment (bank reject) export_line[0].action_reject() diff --git a/l10n_th_bank_payment_export/views/bank_export_format_view.xml b/l10n_th_bank_payment_export/views/bank_export_format_view.xml new file mode 100644 index 000000000..9e2cbd205 --- /dev/null +++ b/l10n_th_bank_payment_export/views/bank_export_format_view.xml @@ -0,0 +1,230 @@ + + + + bank.export.format.tree + bank.export.format + + + + + + + + + bank.export.format.form + bank.export.format + +
+ +
+

+ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

+ There are Condition Line, Sub Value Loop and Value can be any valid python expressions. +

+

+ Data that supports Python expressions, including: + + {rec, line, sub_line, today, today_datetime, idx, idx_line, idx_sub_line} + + . +

+
    +
  • + rec + : record on which the action is triggered (bank.payment.export) +
  • +
  • + line + : +
      +
    • If Loop is not selected, it will retrieve data from rec.export_line_ids (bank.payment.export.line).
    • +
    • If Loop is selected, it will retrieve data from that specific line (bank.payment.export.line).
    • +
    +
  • +
  • + sub_line + : object of the sub loop. it will have data if Sub Loop is selected. +
  • +
  • + today + : Current date (Format: Date). +
  • +
  • + today_datetime + : Current date and time (Format: Datetime). +
  • +
  • + idx + : The index of that line. +
  • +
  • + idx_line + : The index of that Loop (used when Loop is selected). +
  • +
  • + idx_sub_line + : The index of that Sub Loop (used when Sub Loop is selected). +
  • +
+

+ Looping is used to display data from rec.export_line_ids (bank.payment.export.line). +

+
    +
  • + Group + : Used to group for looping within the same group. +
  • +
  • + Loop + : For looping to display according to the lines of bank.payment.export.line. +
  • +
  • + Sub Loop + : For cases where you want to nest loops, such as retrieving all invoices of that line. +
  • +
  • + Sub Value Loop + : The field needed for the sub loop (must be filled in when selecting Sub Loop). +
  • +
+

+ Skipping Display + You can skip the display of a line if it doesn't meet the conditions set in the Condition Line. +

+

+ Formatting +

    +
  • + End Line + : If the line is the last line, it will use /r/t after put value. +
  • +
  • + Lenght + : The length of the displayed value. if Value Type is Fixed, Value will be throw error if it exceeds the specified length. +
  • +
  • + Alignment + : Aligning values to the left or right. +
  • +
  • + Blank Space + : Defining remaining space when the displayed value does not reach the specified length. +
  • +
  • + Value Type + : Fixed is used when the value is fixed, and Python is used when the value is dynamic. +
  • +
+

+
+
+
+
+
+
+
+
+ + Bank Export Format + ir.actions.act_window + bank.export.format + tree,form + +

+ Click to create a Format Bank Export. +

+
+
+ + + + +
diff --git a/l10n_th_bank_payment_export/views/bank_payment_export_view.xml b/l10n_th_bank_payment_export/views/bank_payment_export_view.xml index ebf08f20a..0040de1fa 100644 --- a/l10n_th_bank_payment_export/views/bank_payment_export_view.xml +++ b/l10n_th_bank_payment_export/views/bank_payment_export_view.xml @@ -91,6 +91,10 @@ + - - - - Payment KTB (TXT) - export.bank.payment - ir.actions.report - l10n_th_bank_payment_export.bank_payment_export_text_file - l10n_th_bank_payment_export.bank_payment_export_text_file - qweb-text - object._get_report_base_filename() - - diff --git a/l10n_th_bank_payment_export_ktb/models/__init__.py b/l10n_th_bank_payment_export_ktb/models/__init__.py index ef188c8ba..33942796c 100644 --- a/l10n_th_bank_payment_export_ktb/models/__init__.py +++ b/l10n_th_bank_payment_export_ktb/models/__init__.py @@ -1,6 +1,7 @@ # Copyright 2021 Ecosoft Co., Ltd. (http://ecosoft.co.th) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from . import res_partner +from . import bank_export_format from . import bank_payment_template from . import bank_payment_export -from . import bank_payment_export_line diff --git a/l10n_th_bank_payment_export_ktb/models/bank_export_format.py b/l10n_th_bank_payment_export_ktb/models/bank_export_format.py new file mode 100644 index 000000000..300fd422a --- /dev/null +++ b/l10n_th_bank_payment_export_ktb/models/bank_export_format.py @@ -0,0 +1,13 @@ +# Copyright 2024 Ecosoft Co., Ltd. (http://ecosoft.co.th) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class BankExportFormat(models.Model): + _inherit = "bank.export.format" + + bank = fields.Selection( + selection_add=[("KRTHTHBK", "KTB")], + ondelete={"KRTHTHBK": "cascade"}, + ) diff --git a/l10n_th_bank_payment_export_ktb/models/bank_payment_export.py b/l10n_th_bank_payment_export_ktb/models/bank_payment_export.py index 69201a12b..c09ccb7e4 100644 --- a/l10n_th_bank_payment_export_ktb/models/bank_payment_export.py +++ b/l10n_th_bank_payment_export_ktb/models/bank_payment_export.py @@ -38,61 +38,35 @@ class BankPaymentExport(models.Model): ) ktb_service_type_standard = fields.Selection( selection=[ - ("01", "เงินเดือน ค่าจ้าง บำเหน็จ บำนาญ"), - ("02", "เงินปันผล"), - ("03", "ดอกเบี้ย"), - ("04", "ค่าสินค้า บริการ"), - ("05", "ขายหลักทรัพย์"), - ("06", "คืนภาษี"), - ("07", "เงินกู้"), - ("59", "อื่น ๆ"), + ("01", "01 - เงินเดือน ค่าจ้าง บำเหน็จ บำนาญ"), + ("02", "02 - เงินปันผล"), + ("03", "03 - ดอกเบี้ย"), + ("04", "04 - ค่าสินค้า บริการ"), + ("05", "05 - ขายหลักทรัพย์"), + ("06", "06 - คืนภาษี"), + ("07", "07 - เงินกู้"), + ("59", "59 - อื่น ๆ"), ], - ondelete={ - "01": "cascade", - "02": "cascade", - "03": "cascade", - "04": "cascade", - "05": "cascade", - "06": "cascade", - "07": "cascade", - "59": "cascade", - }, readonly=True, states={"draft": [("readonly", False)]}, ) ktb_service_type_direct = fields.Selection( selection=[ - ("02", "รายการเข้าบัญชีเงินเดือน (Salary)"), - ("04", "รายการชำระดอกเบี้ย (Bond Interest)"), - ("09", "รายการชำระเบี้ยประกัน (Insurance Premium)"), - ("10", "รายการชำระค่าโทรศัพท์ (Telephone Payment)"), - ("11", "รายการชำระค่าไฟฟ้า (Electricity Payment)"), - ("12", "รายการชำระค่าน้ำประปา (Water Payment)"), - ("14", "รายการชำระค่าสินค้าและบริการ (Purchase & Service)"), - ("15", "รายการชำระเงินของธนาคารอาคารสงเคราะห์ (GSB)"), - ("21", "รายการชำระราคาหลักทรัพย์ (Securities)"), - ("25", "รายการชำระ Clearing Bank"), - ("27", "รายการชำระค่าประกันสังคม (SSO)"), - ("28", "รายการชำระของกองสลากฯ (Lottery)"), - ("37", "รายการชำระด้วยบัตรอิเลคทรอนิคส์ (Electronic Card)"), - ("46", "รายการจ่ายเงินบำนาญ (Pension Fund)"), + ("02", "02 - รายการเข้าบัญชีเงินเดือน (Salary)"), + ("04", "04 - รายการชำระดอกเบี้ย (Bond Interest)"), + ("09", "09 - รายการชำระเบี้ยประกัน (Insurance Premium)"), + ("10", "10 - รายการชำระค่าโทรศัพท์ (Telephone Payment)"), + ("11", "11 - รายการชำระค่าไฟฟ้า (Electricity Payment)"), + ("12", "12 - รายการชำระค่าน้ำประปา (Water Payment)"), + ("14", "14 - รายการชำระค่าสินค้าและบริการ (Purchase & Service)"), + ("15", "15 - รายการชำระเงินของธนาคารอาคารสงเคราะห์ (GSB)"), + ("21", "21 - รายการชำระราคาหลักทรัพย์ (Securities)"), + ("25", "25 - รายการชำระ Clearing Bank"), + ("27", "27 - รายการชำระค่าประกันสังคม (SSO)"), + ("28", "28 - รายการชำระของกองสลากฯ (Lottery)"), + ("37", "37 - รายการชำระด้วยบัตรอิเลคทรอนิคส์ (Electronic Card)"), + ("46", "46 - รายการจ่ายเงินบำนาญ (Pension Fund)"), ], - ondelete={ - "02": "cascade", - "04": "cascade", - "09": "cascade", - "10": "cascade", - "11": "cascade", - "12": "cascade", - "14": "cascade", - "15": "cascade", - "21": "cascade", - "25": "cascade", - "27": "cascade", - "28": "cascade", - "37": "cascade", - "46": "cascade", - }, readonly=True, states={"draft": [("readonly", False)]}, ) @@ -116,150 +90,6 @@ def _compute_ktb_editable(self): for export in self: export.ktb_is_editable = True if export.bank == "KRTHTHBK" else False - def _get_ktb_sender_name(self): - return self.ktb_sender_name - - def _get_ktb_receiver_info(self, pe_line): - return "".ljust(8) - - def _get_ktb_receiver_id(self, pe_line): - return "0".zfill(10) - - def _get_ktb_other_info1(self, pe_line): - return "".ljust(40) - - def _get_ktb_other_info2(self, pe_line): - return "".ljust(20) - - def _get_ktb_ddr_ref1(self, pe_line): - return "".ljust(18) - - def _get_ktb_ddr_ref2(self, pe_line): - return pe_line.payment_id.name.ljust(18) - - def _get_ktb_email(self, pe_line): - return ( - pe_line.payment_partner_id.email - and pe_line.payment_partner_id.email[:40].ljust(40) - or "".ljust(40) - ) - - def _get_ktb_sms(self, pe_line): - return ( - pe_line.payment_partner_id.phone - and pe_line.payment_partner_id.phone[:20].ljust(20) - or "".ljust(20) - ) - - def _get_ktb_receiver_sub_branch_code(self, pe_line): - return "0".zfill(4) - - def _get_text_header_ktb(self, payment_lines): - ktb_company_id = self.ktb_company_id or "**Company ID on KTB is not config**" - total_batch = len(payment_lines.ids) - total_amount = sum(payment_lines.mapped("payment_amount")) - total_batch_amount = payment_lines._get_amount_no_decimal(total_amount, 2) - text = ( - "101{idx}006{total_batch_transaction}{total_batch_amount}" - "{effective_date}C{receiver_no}{ktb_company_id}{space}\r\n".format( - idx="1".zfill(6), # 1 Batch = 1 File, How we can add more than 1 batch? - total_batch_transaction=str(total_batch).zfill(7), - total_batch_amount=str(total_batch_amount).zfill(19), - effective_date=self.effective_date.strftime("%d%m%Y"), - receiver_no="0".zfill(8), - ktb_company_id=ktb_company_id.ljust(16), - space="".ljust(427), # user id and filter (20 + 407 char) - ) - ) - return text - - def _get_text_body_ktb(self, idx, pe_line, payment_net_amount_bank): - # Sender - sender_name = self._get_ktb_sender_name() - ( - sender_bank_code, - sender_branch_code, - sender_acc_number, - ) = pe_line._get_sender_information() - # Receiver - ( - receiver_name, - receiver_bank_code, - receiver_branch_code, - receiver_acc_number, - ) = pe_line._get_receiver_information() - ktb_service_type = ( - self.ktb_bank_type == "standard" - and self.ktb_service_type_standard - or self.ktb_service_type_direct - ) - text = ( - "102{idx}{receiver_bank_code}{receiver_branch_code}{receiver_acc_number}" - "{sender_bank_code}{sender_branch_code}{sender_acc_number}" - "{effective_date}{ktb_service_type}00{payment_net_amount_bank}" - "{receiver_info}{receiver_id}{receiver_name}{sender_name}{other_info1}" - "{ddr_ref1}{space}{ddr_ref2}{space}{other_info2}" - "{ref_running_number}09{email}{sms}{receiver_sub_branch_code}" - "{filter}\r\n".format( - idx="1".zfill(6), # 1 Batch = 1 File, How we can add more than 1 batch? - receiver_bank_code=receiver_bank_code, # 10-12 - receiver_branch_code=receiver_branch_code, # 13-16 - receiver_acc_number=receiver_acc_number, # 17-27 - sender_bank_code=sender_bank_code, # 28-30 - sender_branch_code=sender_branch_code, # 31-34 - sender_acc_number=sender_acc_number, # 35-45 - effective_date=self.effective_date.strftime("%d%m%Y"), # 46-53 - ktb_service_type=ktb_service_type, # 54-55 - payment_net_amount_bank=payment_net_amount_bank # 58-74 - and str(payment_net_amount_bank)[:17].zfill(17) - or "0".zfill(17), - receiver_info=self._get_ktb_receiver_info(pe_line), # 75-82 - receiver_id=self._get_ktb_receiver_id(pe_line), # 83-92 - receiver_name=receiver_name, # 93-192 - sender_name=sender_name[:100].ljust(100), # 193-292 - other_info1=self._get_ktb_other_info1(pe_line), # 293-332 - ddr_ref1=self._get_ktb_ddr_ref1(pe_line), # 333-350 - ddr_ref2=self._get_ktb_ddr_ref2(pe_line), # 353-370 - space="".ljust(2), # 351-352 and 371-372 - other_info2=self._get_ktb_other_info2(pe_line), # 373-392 - ref_running_number=str(idx + 1).zfill(6), # 393-398 - email=self._get_ktb_email(pe_line), # 401-440 - sms=self._get_ktb_sms(pe_line), # 441-460 - receiver_sub_branch_code=self._get_ktb_receiver_sub_branch_code( - pe_line - ), # 461-464 - filter="".ljust(34), - ) - ) - return text - - def _format_ktb_text(self): - total_amount = 0 - payment_lines = self.export_line_ids - # Header - text = self._get_text_header_ktb(payment_lines) - # Details - for idx, pe_line in enumerate(payment_lines): - # This amount related decimal from invoice, Odoo invoice do not rounding. - payment_net_amount = pe_line._get_payment_net_amount() - payment_net_amount_bank = pe_line._get_amount_no_decimal( - payment_net_amount, 2 - ) - text += self._get_text_body_ktb(idx, pe_line, payment_net_amount_bank) - total_amount += payment_net_amount_bank - return text - - def _generate_bank_payment_text(self): - self.ensure_one() - if self.bank == "KRTHTHBK": # KTB - return self._format_ktb_text() - return super()._generate_bank_payment_text() - - def _get_view_report_text(self): - if self.bank == "KRTHTHBK": - return "l10n_th_bank_payment_export_ktb.action_payment_ktb_txt" - return super()._get_view_report_text() - def _check_constraint_confirm(self): res = super()._check_constraint_confirm() for rec in self.filtered(lambda l: l.bank == "KRTHTHBK"): diff --git a/l10n_th_bank_payment_export_ktb/models/bank_payment_export_line.py b/l10n_th_bank_payment_export_ktb/models/bank_payment_export_line.py deleted file mode 100644 index 4cdb12bd4..000000000 --- a/l10n_th_bank_payment_export_ktb/models/bank_payment_export_line.py +++ /dev/null @@ -1,56 +0,0 @@ -# Copyright 2021 Ecosoft Co., Ltd. (http://ecosoft.co.th) -# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). - -from odoo import models -from odoo.tools import float_round - - -class BankPaymentExportLine(models.Model): - _inherit = "bank.payment.export.line" - - def _get_receiver_information(self): - ( - receiver_name, - receiver_bank_code, - receiver_branch_code, - receiver_acc_number, - ) = super()._get_receiver_information() - if self.payment_export_id.bank == "KRTHTHBK": - receiver_name = ( - receiver_name and receiver_name[:100].ljust(100) or "".ljust(100) - ) - receiver_bank_code = ( - receiver_bank_code and receiver_bank_code[:3].zfill(3) or "---" - ) - receiver_branch_code = ( - receiver_branch_code and receiver_branch_code[:4].zfill(4) or "----" - ) - return ( - receiver_name, - receiver_bank_code, - receiver_branch_code, - receiver_acc_number, - ) - - def _get_sender_information(self): - ( - sender_bank_code, - sender_branch_code, - sender_acc_number, - ) = super()._get_sender_information() - if self.payment_export_id.bank == "KRTHTHBK": - sender_bank_code = ( - sender_bank_code and sender_bank_code[:3].zfill(3) or "---" - ) - sender_branch_code = ( - sender_branch_code and sender_branch_code[:4].zfill(4) or "----" - ) - sender_acc_number = ( - sender_acc_number and sender_acc_number[:11].zfill(11) or "-----------" - ) - return sender_bank_code, sender_branch_code, sender_acc_number - - def _get_amount_no_decimal(self, amount, digits=False): - if self.payment_export_id.bank == "KRTHTHBK": - return int(round(float_round(amount * 100, precision_rounding=1), digits)) - return super()._get_amount_no_decimal(amount, digits) diff --git a/l10n_th_bank_payment_export_ktb/models/res_partner.py b/l10n_th_bank_payment_export_ktb/models/res_partner.py new file mode 100644 index 000000000..639dd69df --- /dev/null +++ b/l10n_th_bank_payment_export_ktb/models/res_partner.py @@ -0,0 +1,11 @@ +# Copyright 2023 Ecosoft Co., Ltd (http://ecosoft.co.th/) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) + +from odoo import fields, models + + +class ResPartner(models.Model): + _inherit = "res.partner" + + ktb_email = fields.Char(string="Receiver's Email", size=40) + ktb_sms = fields.Char(string="Receiver's SMS", size=20) diff --git a/l10n_th_bank_payment_export_ktb/static/description/index.html b/l10n_th_bank_payment_export_ktb/static/description/index.html index fe41bda70..107265bdf 100644 --- a/l10n_th_bank_payment_export_ktb/static/description/index.html +++ b/l10n_th_bank_payment_export_ktb/static/description/index.html @@ -8,10 +8,11 @@ /* :Author: David Goodger (goodger@python.org) -:Id: $Id: html4css1.css 8954 2022-01-20 10:10:25Z milde $ +:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $ :Copyright: This stylesheet has been placed in the public domain. Default cascading style sheet for the HTML output of Docutils. +Despite the name, some widely supported CSS2 features are used. See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to customize this style sheet. @@ -274,7 +275,7 @@ margin-left: 2em ; margin-right: 2em } -pre.code .ln { color: grey; } /* line numbers */ +pre.code .ln { color: gray; } /* line numbers */ pre.code, code { background-color: #eeeeee } pre.code .comment, code .comment { color: #5C6576 } pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold } @@ -300,7 +301,7 @@ span.pre { white-space: pre } -span.problematic { +span.problematic, pre.problematic { color: red } span.section-subtitle { @@ -468,7 +469,9 @@

Contributors

Maintainers

This module is maintained by the OCA.

-Odoo Community Association + +Odoo Community Association +

OCA, or the Odoo Community Association, is a nonprofit organization whose mission is to support the collaborative development of Odoo features and promote its widespread use.

diff --git a/l10n_th_bank_payment_export_ktb/templates/report_bank_payment_export_xlsx.py b/l10n_th_bank_payment_export_ktb/templates/report_bank_payment_export_xlsx.py index e0653c23c..f1a0af208 100644 --- a/l10n_th_bank_payment_export_ktb/templates/report_bank_payment_export_xlsx.py +++ b/l10n_th_bank_payment_export_ktb/templates/report_bank_payment_export_xlsx.py @@ -3,6 +3,8 @@ from odoo import models +from odoo.addons.report_xlsx_helper.report.report_xlsx_format import FORMATS + class BankPaymentExportXslx(models.AbstractModel): _inherit = "report.bank.payment.export.xlsx" @@ -33,3 +35,41 @@ def _get_header_data_list(self, obj): ] ) return header_data_list + + def _get_render_space_ktb(self, idx, pe_line): + return { + "email": pe_line.payment_partner_id.ktb_email or "", + "sms": pe_line.payment_partner_id.ktb_sms or "", + } + + def _get_render_space(self, idx, pe_line, obj): + render_space = super()._get_render_space(idx, pe_line, obj) + if obj.bank == "KRTHTHBK": + render_space.update(self._get_render_space_ktb(idx, pe_line)) + return render_space + + def _get_export_payment_vals(self, obj): + """Addition of email and sms for KTB""" + vals = super()._get_export_payment_vals(obj) + if obj.bank == "KRTHTHBK": + vals["11_email"] = { + "header": {"value": "Email"}, + "data": {"value": self._render("email")}, + "width": 20, + } + vals["12_sms"] = { + "header": {"value": "SMS"}, + "data": {"value": self._render("sms")}, + "width": 20, + } + return vals + + def _set_range_footer(self, row_pos, ws, obj): + if obj.bank == "KRTHTHBK": + ws.merge_range(row_pos, 0, row_pos, 6, "") + ws.merge_range(row_pos, 8, row_pos, 11, "") + ws.write_row( + row_pos, 0, ["Total Balance"], FORMATS["format_theader_blue_right"] + ) + return + return super()._set_range_footer(row_pos, ws, obj) diff --git a/l10n_th_bank_payment_export_ktb/tests/test_bank_payment_export_ktb.py b/l10n_th_bank_payment_export_ktb/tests/test_bank_payment_export_ktb.py index 3ddc80bbb..abd774bbe 100644 --- a/l10n_th_bank_payment_export_ktb/tests/test_bank_payment_export_ktb.py +++ b/l10n_th_bank_payment_export_ktb/tests/test_bank_payment_export_ktb.py @@ -17,6 +17,14 @@ def setUpClass(cls): # setup config ktb_company_id = cls.field_model.search([("name", "=", "ktb_company_id")]) ktb_sender_name = cls.field_model.search([("name", "=", "ktb_sender_name")]) + field_bank_export_format_id = cls.field_model.search( + [("name", "=", "bank_export_format_id")] + ) + + bank_export_format = cls.bank_export_format_model.search( + [("bank", "=", "KRTHTHBK")], limit=1 + ) + data_dict = [ { "field_id": ktb_company_id.id, @@ -26,6 +34,10 @@ def setUpClass(cls): "field_id": ktb_sender_name.id, "value": "SENDER_NAME01", }, + { + "field_id": field_bank_export_format_id.id, + "value": bank_export_format.id, + }, ] cls.template1 = cls.create_bank_payment_template( cls, diff --git a/l10n_th_bank_payment_export_ktb/views/res_partner_views.xml b/l10n_th_bank_payment_export_ktb/views/res_partner_views.xml new file mode 100644 index 000000000..27430afa5 --- /dev/null +++ b/l10n_th_bank_payment_export_ktb/views/res_partner_views.xml @@ -0,0 +1,21 @@ + + + + res.partner.form + res.partner + + + + + + + + + + + + + + + +