From 367bfe1df5f0f181f134b9b03a671de8fb48e3a6 Mon Sep 17 00:00:00 2001 From: smjo-odoo Date: Tue, 18 Mar 2025 12:52:55 +0530 Subject: [PATCH] [ADD] account_custom_duty: manage custom duties on import & export - Added a setting named 'Enable Import-Export' to configure journals and accounts for managing custom duty on import-export. - Added a 'Bill of Entry' button that opens a wizard to add custom currency rate, tax details and custom duty incurred. - Added a smart button 'journal entries' to display taxes, total custom duty and additional charges , amount payable, debited and credited amount corresponding to the account configured. - Added a button 'reverse entry' to reverse an entry from the journal. --- account_custom_duty/__init__.py | 2 + account_custom_duty/__manifest__.py | 16 ++ account_custom_duty/models/__init__.py | 4 + account_custom_duty/models/account_move.py | 24 +++ account_custom_duty/models/res_company.py | 12 ++ .../models/res_config_settings.py | 42 +++++ .../security/ir.model.access.csv | 3 + .../views/account_move_views.xml | 28 ++++ .../views/res_config_settings_views.xml | 91 +++++++++++ account_custom_duty/wizard/__init__.py | 2 + .../wizard/bill_entry_details_wizard.py | 46 ++++++ .../wizard/bill_entry_wizard.py | 150 ++++++++++++++++++ .../wizard/bill_entry_wizard_view.xml | 87 ++++++++++ 13 files changed, 507 insertions(+) create mode 100644 account_custom_duty/__init__.py create mode 100644 account_custom_duty/__manifest__.py create mode 100644 account_custom_duty/models/__init__.py create mode 100644 account_custom_duty/models/account_move.py create mode 100644 account_custom_duty/models/res_company.py create mode 100644 account_custom_duty/models/res_config_settings.py create mode 100644 account_custom_duty/security/ir.model.access.csv create mode 100644 account_custom_duty/views/account_move_views.xml create mode 100644 account_custom_duty/views/res_config_settings_views.xml create mode 100644 account_custom_duty/wizard/__init__.py create mode 100644 account_custom_duty/wizard/bill_entry_details_wizard.py create mode 100644 account_custom_duty/wizard/bill_entry_wizard.py create mode 100644 account_custom_duty/wizard/bill_entry_wizard_view.xml diff --git a/account_custom_duty/__init__.py b/account_custom_duty/__init__.py new file mode 100644 index 00000000000..9b4296142f4 --- /dev/null +++ b/account_custom_duty/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import wizard diff --git a/account_custom_duty/__manifest__.py b/account_custom_duty/__manifest__.py new file mode 100644 index 00000000000..dc8acae8ba6 --- /dev/null +++ b/account_custom_duty/__manifest__.py @@ -0,0 +1,16 @@ +{ + "name": "Custom Duty on Import Export", + "summary": "Manage custom duty on import & export transactions.", + "description": "Provide functionality to calculate custom duties on import and export transactions in the accounting module.", + "category": "Accounting", + "version": "1.0", + "depends": ["accountant", "l10n_in"], + "data": [ + "security/ir.model.access.csv", + "views/res_config_settings_views.xml", + "wizard/bill_entry_wizard_view.xml", + "views/account_move_views.xml", + ], + "installable": True, + "license": "LGPL-3", +} diff --git a/account_custom_duty/models/__init__.py b/account_custom_duty/models/__init__.py new file mode 100644 index 00000000000..89b6d4eb19f --- /dev/null +++ b/account_custom_duty/models/__init__.py @@ -0,0 +1,4 @@ +from . import res_company +from . import res_config_settings +from . import account_move + diff --git a/account_custom_duty/models/account_move.py b/account_custom_duty/models/account_move.py new file mode 100644 index 00000000000..2c09af5dbb9 --- /dev/null +++ b/account_custom_duty/models/account_move.py @@ -0,0 +1,24 @@ +from odoo import _, fields, models +from odoo.exceptions import UserError + +class AccountMove(models.Model): + _inherit = 'account.move' + + #trace which invoice generated the custom duty journal entry. + custom_duty_journal_entry_id = fields.Many2one('account.move', string='Journal Entry of Custom Duty', readonly=True) + + def action_open_bill_of_entry_wizard(self): + self.ensure_one() + if self.state != 'posted' or self.l10n_in_gst_treatment not in ['overseas', 'special_economic_zone', 'deemed_export']: + raise UserError('Action can only be performed on posted moves with GST treatment like overseas, special economic zone, or deemed export .') + return { + 'name': 'Bill of Entry', + 'type': 'ir.actions.act_window', + 'view_mode': 'form', + 'res_model': 'bill.entry.wizard', + 'view_id': self.env.ref('account_custom_duty.bill_entry_wizard_view_form').id, + 'target': 'new', + 'context': { + 'move_id': self.id + }, + } diff --git a/account_custom_duty/models/res_company.py b/account_custom_duty/models/res_company.py new file mode 100644 index 00000000000..973836e994c --- /dev/null +++ b/account_custom_duty/models/res_company.py @@ -0,0 +1,12 @@ +from odoo import fields, models + +class ResCompany(models.Model): + _inherit = 'res.company' + + is_import_export = fields.Boolean(string="Enable Import-Export Settings", store=True) + account_import_journal_id = fields.Many2one('account.journal', string="Default Import Journal") + account_import_custom_duty_account_id = fields.Many2one('account.account', string="Default Import Custom Duty Account") + account_import_tax_account_id = fields.Many2one('account.account', string="Default Import Tax Account") + account_export_journal_id = fields.Many2one('account.journal', string="Default Export Journal") + account_export_custom_duty_account_id = fields.Many2one('account.account', string="Default Export Custom Duty Account") + account_export_tax_account_id = fields.Many2one('account.account', string="Default Export Tax Account") diff --git a/account_custom_duty/models/res_config_settings.py b/account_custom_duty/models/res_config_settings.py new file mode 100644 index 00000000000..51813fa17a8 --- /dev/null +++ b/account_custom_duty/models/res_config_settings.py @@ -0,0 +1,42 @@ +from odoo import fields, models + +class ResConfigSettings(models.TransientModel): + _inherit = 'res.config.settings' + + is_import_export = fields.Boolean(string="Enable Import-Export Settings", company_dependent=True, store=True) + account_import_journal_id = fields.Many2one( + 'account.journal', + string="Default Import Journal", + related='company_id.account_import_journal_id', + readonly=False + ) + account_import_custom_duty_account_id = fields.Many2one( + 'account.account', + string="Default Import Custom Duty Account", + related='company_id.account_import_custom_duty_account_id', + readonly=False + ) + account_import_tax_account_id = fields.Many2one( + 'account.account', + string="Default Import Tax Account", + related='company_id.account_import_tax_account_id', + readonly=False + ) + account_export_journal_id = fields.Many2one( + 'account.journal', + string="Default Export Journal", + related='company_id.account_export_journal_id', + readonly=False + ) + account_export_custom_duty_account_id = fields.Many2one( + 'account.account', + string="Default Export Custom Duty Account", + related='company_id.account_export_custom_duty_account_id', + readonly=False + ) + account_export_tax_account_id = fields.Many2one( + 'account.account', + string="Default Export Tax Account", + related='company_id.account_export_tax_account_id', + readonly=False + ) diff --git a/account_custom_duty/security/ir.model.access.csv b/account_custom_duty/security/ir.model.access.csv new file mode 100644 index 00000000000..1a9d2b1b675 --- /dev/null +++ b/account_custom_duty/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_bill_entry_wizard,access_bill_entry_wizard,model_bill_entry_wizard,account.group_account_user,1,1,1,1 +access_bill_entry_details_wizard,access_bill_entry_details_wizard,model_bill_entry_details_wizard,account.group_account_user,1,1,1,1 diff --git a/account_custom_duty/views/account_move_views.xml b/account_custom_duty/views/account_move_views.xml new file mode 100644 index 00000000000..60382908ed4 --- /dev/null +++ b/account_custom_duty/views/account_move_views.xml @@ -0,0 +1,28 @@ + + + + account.custom.duty.view.move.form + account.move + + + + + + + + diff --git a/account_custom_duty/views/res_config_settings_views.xml b/account_custom_duty/views/res_config_settings_views.xml new file mode 100644 index 00000000000..ae0e90fef31 --- /dev/null +++ b/account_custom_duty/views/res_config_settings_views.xml @@ -0,0 +1,91 @@ + + + res.config.settings.view.form.inherit.account.import.export.duty + res.config.settings + + + + + + + + + +
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/account_custom_duty/wizard/__init__.py b/account_custom_duty/wizard/__init__.py new file mode 100644 index 00000000000..4cfb9d81be2 --- /dev/null +++ b/account_custom_duty/wizard/__init__.py @@ -0,0 +1,2 @@ +from . import bill_entry_wizard +from . import bill_entry_details_wizard diff --git a/account_custom_duty/wizard/bill_entry_details_wizard.py b/account_custom_duty/wizard/bill_entry_details_wizard.py new file mode 100644 index 00000000000..d5a296cbaf7 --- /dev/null +++ b/account_custom_duty/wizard/bill_entry_details_wizard.py @@ -0,0 +1,46 @@ +from odoo import api, fields, models +from odoo.exceptions import ValidationError + + +class BillEntryDetailsWizard(models.TransientModel): + _name = 'bill.entry.details.wizard' + _description = 'Bill Entry Details Wizard' + + currency_id = fields.Many2one('res.currency', string='Currency', default=lambda self: self.env.company.currency_id) + wizard_id = fields.Many2one('bill.entry.wizard', string="Wizard Reference", ondelete='cascade') + move_line_id = fields.Many2one('account.move.line', string='Move Line', required=True) + + product_id = fields.Many2one('product.product', string='Product', related='move_line_id.product_id') + quantity = fields.Float(string='Quantity', related='move_line_id.quantity') + price_unit = fields.Float(string='Unit Price', related='move_line_id.price_unit') + + custom_currency_rate = fields.Monetary(string='Custom Currency Rate', currency_field='currency_id', related='wizard_id.custom_currency_rate') + + custom_duty = fields.Monetary(string='Custom Duty & Additional Charges', currency_field='currency_id', default=0.0) + tax_ids = fields.Many2many('account.tax', string='Taxes', domain=[('type_tax_use', '=', 'purchase')]) + + assessable_value = fields.Monetary(string='Assessable Value', currency_field='currency_id', compute='_compute_assessable_value', default=0.0) + taxable_amount = fields.Monetary(string='Taxable Amount', currency_field='currency_id', compute='_compute_taxable_amount', default=0.0) + tax_amount = fields.Monetary(string='Tax Amount', currency_field='currency_id', compute='_compute_tax_amount', default=0.0) + + @api.constrains('custom_duty') + def _check_positive_custom_duty(self): + for record in self: + if record.custom_duty < 0: + raise ValidationError("Custom Duty cannot be a negative amount.") + + @api.depends('quantity', 'price_unit', 'wizard_id.custom_currency_rate') + def _compute_assessable_value(self): + for record in self: + record.assessable_value = (record.quantity * record.price_unit * record.wizard_id.custom_currency_rate) + + @api.depends('assessable_value', 'custom_duty') + def _compute_taxable_amount(self): + for record in self: + record.taxable_amount = record.assessable_value + record.custom_duty + + @api.depends('taxable_amount', 'tax_ids') + def _compute_tax_amount(self): + for record in self: + tax_factor = sum(record.tax_ids.mapped('amount')) / 100 if record.tax_ids else 0.0 + record.tax_amount = record.taxable_amount * tax_factor diff --git a/account_custom_duty/wizard/bill_entry_wizard.py b/account_custom_duty/wizard/bill_entry_wizard.py new file mode 100644 index 00000000000..6520dbd938d --- /dev/null +++ b/account_custom_duty/wizard/bill_entry_wizard.py @@ -0,0 +1,150 @@ +from odoo import api, Command, fields, models +from odoo.exceptions import UserError, ValidationError + +class BillEntryWizard(models.TransientModel): + _name = 'bill.entry.wizard' + _description = 'Bill Entry Wizard' + + company_id = fields.Many2one('res.company', string='Company', default=lambda self: self.env.company) + currency_id = fields.Many2one('res.currency', string='Currency', default=lambda self: self.env.company.currency_id) + move_id = fields.Many2one('account.move', string='Bill Reference', required=True) + + journal_entry_number = fields.Char(string='Journal Entry Number', compute='_compute_journal_entry_number', store=True) + journal_entry_date = fields.Date(string='Journal Entry Date', default=fields.Date.context_today, readonly=True) + + custom_currency_rate = fields.Monetary(string='Custom Currency Rate', currency_field='currency_id', default=80) + bill_of_entry_number = fields.Char(string='Bill of Entry Number', related='move_id.l10n_in_shipping_bill_number') + bill_of_entry_date = fields.Date(string='Bill of Entry Date', related='move_id.l10n_in_shipping_bill_date') + line_ids = fields.One2many('bill.entry.details.wizard','wizard_id', string='Product Lines') + port_code_id = fields.Many2one('l10n_in.port.code', string='Port Code', related='move_id.l10n_in_shipping_port_code_id') + + total_custom_duty_and_additional_charges = fields.Monetary(string='Total Custom Duty and Additional Charges', currency_field='currency_id', compute='_compute_total_custom_duty_and_additional_charges') + total_tax_amount = fields.Monetary(string='Total Tax Amount', currency_field='currency_id', compute='_compute_total_tax_amount') + total_amount_payable = fields.Monetary(string='Total Amount Payable', currency_field='currency_id', compute='_compute_total_amount_payable') + + journal_id = fields.Many2one('account.journal', string='Journal', readonly=True, default=lambda self: self.env.company.account_import_journal_id) + account_import_custom_duty_account_id = fields.Many2one('account.account', string="Default Import Custom Duty Account", related='company_id.account_import_custom_duty_account_id') + account_import_tax_account_id = fields.Many2one('account.account', string="Default Import Tax Account", related='company_id.account_import_tax_account_id') + + custom_duty_journal_entry_id = fields.Many2one('account.move', string='Custom Duty Journal Entry', related='move_id.custom_duty_journal_entry_id') + custom_duty_journal_entry_lines = fields.One2many(related='move_id.custom_duty_journal_entry_id.line_ids', string="Journal Items") + + @api.constrains('custom_currency_rate') + def _check_positive_custom_currency_rate(self): + for record in self: + if record.custom_currency_rate <= 0: + raise ValidationError("Currency Rate must be greater than zero.") + + @api.depends('line_ids.custom_duty') + def _compute_total_custom_duty_and_additional_charges(self): + for record in self: + record.total_custom_duty_and_additional_charges = sum(record.line_ids.mapped('custom_duty')) + + @api.depends('line_ids.tax_amount') + def _compute_total_tax_amount(self): + for record in self: + record.total_tax_amount = sum(record.line_ids.mapped('tax_amount')) + + @api.depends('total_custom_duty_and_additional_charges', 'total_tax_amount') + def _compute_total_amount_payable(self): + for record in self: + record.total_amount_payable = (record.total_custom_duty_and_additional_charges + record.total_tax_amount) + + @api.depends('custom_duty_journal_entry_id') + def _compute_journal_entry_number(self): + for record in self: + if record.custom_duty_journal_entry_id: + record.journal_entry_number = record.custom_duty_journal_entry_id.name + else: + record.journal_entry_number = False + + @api.model + def default_get(self, fields_list): + move_id = self.env.context['move_id'] + res = super().default_get(fields_list) + + if not move_id: + return res + + move_lines = self.env['account.move.line'].search([('move_id', '=', move_id), ('product_id', '!=', False), ('quantity', '>', 0)]) + + if move_lines: + res['line_ids'] = [Command.create({'move_line_id': line.id}) for line in move_lines] + res['move_id'] = move_id + return res + + def action_confirm_post_journal_entry(self): + self.ensure_one() + for record in self: + if record.custom_duty_journal_entry_id: + raise UserError("A journal entry already exists for Bill of Entry %s. Please check the journal entry %s." % (self.move_id.name, self.custom_duty_journal_entry_id.name)) + if record.line_ids: + for line in record.line_ids: + if not line.custom_duty and not line.tax_amount: + raise UserError("Please enter Custom Duty and Tax Amount for each product line.") + if not record.line_ids: + raise UserError("Journal Entry Cannot be created without Bill Entry Detail Line.") + + journal_entry = self.env['account.move'].create({ + 'move_type': 'entry', + 'journal_id': record.journal_id.id, + 'date': record.journal_entry_date, + 'ref': record.move_id.name, + 'line_ids': [ + Command.create({ + 'name': 'Custom Duty and Additional Charges', + 'partner_id': record.move_id.partner_id.id, + 'debit': record.total_custom_duty_and_additional_charges, + 'credit': 0.0, + 'account_id': record.account_import_custom_duty_account_id.id, + 'company_currency_id': record.currency_id.id, + 'amount_currency': record.total_custom_duty_and_additional_charges, + }), + Command.create({ + 'name': 'Total Tax Amount', + 'partner_id': record.move_id.partner_id.id, + 'debit': record.total_tax_amount, + 'credit': 0.0, + 'account_id': record.account_import_tax_account_id.id, + 'company_currency_id': record.currency_id.id, + 'amount_currency': record.total_tax_amount, + }), + Command.create({ + 'name': 'Total Amount Payable', + 'partner_id': record.move_id.partner_id.id, + 'debit': 0.0, + 'credit': record.total_amount_payable, + 'account_id': record.move_id.line_ids[0].account_id.id, + 'company_currency_id': record.currency_id.id, + 'amount_currency': -record.total_amount_payable, + }), + ], + }) + journal_entry.action_post() + record.move_id.custom_duty_journal_entry_id = journal_entry.id + + def button_draft_journal_entry(self): + for record in self: + if record.custom_duty_journal_entry_id: + record.custom_duty_journal_entry_id.button_draft() + record.move_id.custom_duty_journal_entry_id = False + + def action_reverse_entry(self): + self.ensure_one() + + reverse_dialog = self.env['account.move.reversal'].create({ + 'move_ids' : [(6, 0, [self.custom_duty_journal_entry_id.id])], + 'date' : fields.Date.context_today(self), + 'company_id' : self.company_id.id, + 'journal_id' : self.journal_id.id, + }) + + return { + 'name': 'Reverse Entry', + 'type': 'ir.actions.act_window', + 'res_model': 'account.move.reversal', + 'view_mode': 'form', + 'view_id': self.env.ref('account.view_account_move_reversal').id, + 'target': 'new', + 'res_id' : reverse_dialog.id + } diff --git a/account_custom_duty/wizard/bill_entry_wizard_view.xml b/account_custom_duty/wizard/bill_entry_wizard_view.xml new file mode 100644 index 00000000000..cddecb2bda9 --- /dev/null +++ b/account_custom_duty/wizard/bill_entry_wizard_view.xml @@ -0,0 +1,87 @@ + + + Bill of Entry + bill.entry.wizard + form + + + + bill.entry.line.wizard.view.form + bill.entry.wizard + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+