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
+
+
+
+
+