mirror of
https://github.com/bringout/oca-ocb-l10n_asia-pacific.git
synced 2026-04-26 11:22:00 +02:00
19.0 vanilla
This commit is contained in:
parent
7dc55599c6
commit
7f43bbbfcc
650 changed files with 45260 additions and 33436 deletions
|
|
@ -1,13 +1,19 @@
|
|||
# -*- coding:utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import account
|
||||
from . import template_in
|
||||
from . import account_invoice
|
||||
from . import chart_template
|
||||
from . import account_journal
|
||||
from . import account_move_line
|
||||
from . import account_payment
|
||||
from . import account_tax
|
||||
from . import company
|
||||
from . import iap_account
|
||||
from . import product_template
|
||||
from . import port_code
|
||||
from . import res_config_settings
|
||||
from . import res_country_state
|
||||
from . import res_partner
|
||||
from . import uom_uom
|
||||
from . import mail_message
|
||||
from . import account_account
|
||||
from . import l10n_in_section_alert
|
||||
from . import l10n_in_pan_entity
|
||||
from . import l10n_in_report_handler
|
||||
|
|
|
|||
|
|
@ -1,77 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo import tools
|
||||
|
||||
|
||||
class AccountJournal(models.Model):
|
||||
_inherit = "account.journal"
|
||||
|
||||
# Use for filter import and export type.
|
||||
l10n_in_gstin_partner_id = fields.Many2one('res.partner', string="GSTIN Unit", ondelete="restrict", help="GSTIN related to this journal. If empty then consider as company GSTIN.")
|
||||
|
||||
def name_get(self):
|
||||
"""
|
||||
Add GSTIN number in name as suffix so user can easily find the right journal.
|
||||
Used super to ensure nothing is missed.
|
||||
"""
|
||||
result = super().name_get()
|
||||
result_dict = dict(result)
|
||||
indian_journals = self.filtered(lambda j: j.company_id.account_fiscal_country_id.code == 'IN' and
|
||||
j.l10n_in_gstin_partner_id and j.l10n_in_gstin_partner_id.vat)
|
||||
for journal in indian_journals:
|
||||
name = result_dict[journal.id]
|
||||
name += "- %s" % (journal.l10n_in_gstin_partner_id.vat)
|
||||
result_dict[journal.id] = name
|
||||
return list(result_dict.items())
|
||||
|
||||
|
||||
class AccountMoveLine(models.Model):
|
||||
_inherit = "account.move.line"
|
||||
|
||||
def init(self):
|
||||
tools.create_index(self._cr, 'account_move_line_move_product_index', self._table, ['move_id', 'product_id'])
|
||||
|
||||
@api.depends('move_id.line_ids', 'move_id.line_ids.tax_line_id', 'move_id.line_ids.debit', 'move_id.line_ids.credit')
|
||||
def _compute_tax_base_amount(self):
|
||||
aml = self.filtered(lambda l: l.company_id.account_fiscal_country_id.code == 'IN' and l.tax_line_id and l.product_id)
|
||||
for move_line in aml:
|
||||
base_lines = move_line.move_id.line_ids.filtered(lambda line: move_line.tax_line_id in line.tax_ids and move_line.product_id == line.product_id)
|
||||
move_line.tax_base_amount = abs(sum(base_lines.mapped('balance')))
|
||||
remaining_aml = self - aml
|
||||
if remaining_aml:
|
||||
return super(AccountMoveLine, remaining_aml)._compute_tax_base_amount()
|
||||
|
||||
|
||||
class AccountTax(models.Model):
|
||||
_inherit = 'account.tax'
|
||||
|
||||
l10n_in_reverse_charge = fields.Boolean("Reverse charge", help="Tick this if this tax is reverse charge. Only for Indian accounting")
|
||||
|
||||
@api.model
|
||||
def _get_generation_dict_from_base_line(self, line_vals, tax_vals, force_caba_exigibility=False):
|
||||
# EXTENDS account
|
||||
# Group taxes also by product.
|
||||
res = super()._get_generation_dict_from_base_line(line_vals, tax_vals, force_caba_exigibility=force_caba_exigibility)
|
||||
record = line_vals['record']
|
||||
if isinstance(record, models.Model)\
|
||||
and record._name == 'account.move.line'\
|
||||
and record.company_id.account_fiscal_country_id.code == 'IN':
|
||||
res['product_id'] = record.product_id.id
|
||||
res['product_uom_id'] = record.product_uom_id.id
|
||||
return res
|
||||
|
||||
@api.model
|
||||
def _get_generation_dict_from_tax_line(self, line_vals):
|
||||
# EXTENDS account
|
||||
# Group taxes also by product.
|
||||
res = super()._get_generation_dict_from_tax_line(line_vals)
|
||||
record = line_vals['record']
|
||||
if isinstance(record, models.Model)\
|
||||
and record._name == 'account.move.line'\
|
||||
and record.company_id.account_fiscal_country_id.code == 'IN':
|
||||
res['product_id'] = record.product_id.id
|
||||
res['product_uom_id'] = record.product_uom_id.id
|
||||
return res
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class AccountAccount(models.Model):
|
||||
_inherit = 'account.account'
|
||||
|
||||
l10n_in_tds_tcs_section_id = fields.Many2one('l10n_in.section.alert', string="TCS/TDS Section")
|
||||
l10n_in_tds_feature_enabled = fields.Boolean(compute='_compute_tds_tcs_features', store=True)
|
||||
l10n_in_tcs_feature_enabled = fields.Boolean(compute='_compute_tds_tcs_features', store=True)
|
||||
|
||||
@api.depends('company_ids.l10n_in_tds_feature', 'company_ids.l10n_in_tcs_feature')
|
||||
def _compute_tds_tcs_features(self):
|
||||
for record in self:
|
||||
record.l10n_in_tds_feature_enabled = any(company.l10n_in_tds_feature for company in record.company_ids)
|
||||
record.l10n_in_tcs_feature_enabled = any(company.l10n_in_tcs_feature for company in record.company_ids)
|
||||
|
|
@ -1,19 +1,34 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import base64
|
||||
import logging
|
||||
import json
|
||||
import re
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from contextlib import contextmanager
|
||||
from markupsafe import Markup
|
||||
|
||||
from odoo import Command, _, api, fields, models
|
||||
from odoo.exceptions import ValidationError, RedirectWarning, UserError
|
||||
from odoo.tools.float_utils import json_float_round
|
||||
from odoo.tools.image import image_data_uri
|
||||
from odoo.tools import float_compare, SQL
|
||||
from odoo.tools.date_utils import get_month
|
||||
from odoo.addons.l10n_in.models.iap_account import IAP_SERVICE_NAME
|
||||
|
||||
EDI_CANCEL_REASON = {
|
||||
# Same for both e-way bill and IRN
|
||||
'1': "Duplicate",
|
||||
'2': "Data Entry Mistake",
|
||||
'3': "Order Cancelled",
|
||||
'4': "Others",
|
||||
}
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AccountMove(models.Model):
|
||||
_inherit = "account.move"
|
||||
|
||||
amount_total_words = fields.Char("Total (In Words)", compute="_compute_amount_total_words")
|
||||
l10n_in_gst_treatment = fields.Selection([
|
||||
l10n_in_gst_treatment = fields.Selection(
|
||||
selection=[
|
||||
('regular', 'Registered Business - Regular'),
|
||||
('composition', 'Registered Business - Composition'),
|
||||
('unregistered', 'Unregistered Business'),
|
||||
|
|
@ -22,57 +37,517 @@ class AccountMove(models.Model):
|
|||
('special_economic_zone', 'Special Economic Zone'),
|
||||
('deemed_export', 'Deemed Export'),
|
||||
('uin_holders', 'UIN Holders'),
|
||||
], string="GST Treatment", compute="_compute_l10n_in_gst_treatment", store=True, readonly=False, copy=True)
|
||||
l10n_in_state_id = fields.Many2one('res.country.state', string="Place of supply", compute="_compute_l10n_in_state_id", store=True, readonly=False)
|
||||
],
|
||||
string="GST Treatment",
|
||||
compute="_compute_l10n_in_gst_treatment",
|
||||
store=True,
|
||||
readonly=False,
|
||||
copy=True,
|
||||
precompute=True
|
||||
)
|
||||
l10n_in_state_id = fields.Many2one(
|
||||
comodel_name='res.country.state',
|
||||
string="Place of supply",
|
||||
compute="_compute_l10n_in_state_id",
|
||||
store=True,
|
||||
copy=True,
|
||||
readonly=False,
|
||||
precompute=True
|
||||
)
|
||||
l10n_in_gstin = fields.Char(string="GSTIN")
|
||||
# For Export invoice this data is need in GSTR report
|
||||
l10n_in_shipping_bill_number = fields.Char('Shipping bill number', readonly=True, states={'draft': [('readonly', False)]})
|
||||
l10n_in_shipping_bill_date = fields.Date('Shipping bill date', readonly=True, states={'draft': [('readonly', False)]})
|
||||
l10n_in_shipping_port_code_id = fields.Many2one('l10n_in.port.code', 'Port code', readonly=True, states={'draft': [('readonly', False)]})
|
||||
l10n_in_reseller_partner_id = fields.Many2one('res.partner', 'Reseller', domain=[('vat', '!=', False)], help="Only Registered Reseller", readonly=True, states={'draft': [('readonly', False)]})
|
||||
l10n_in_shipping_bill_number = fields.Char('Shipping bill number')
|
||||
l10n_in_shipping_bill_date = fields.Date('Shipping bill date')
|
||||
l10n_in_shipping_port_code_id = fields.Many2one('l10n_in.port.code', 'Port code')
|
||||
l10n_in_reseller_partner_id = fields.Many2one(
|
||||
comodel_name='res.partner',
|
||||
string="Reseller",
|
||||
domain=[('vat', '!=', False)],
|
||||
help="Only Registered Reseller"
|
||||
)
|
||||
l10n_in_journal_type = fields.Selection(string="Journal Type", related='journal_id.type')
|
||||
l10n_in_warning = fields.Json(compute="_compute_l10n_in_warning")
|
||||
l10n_in_is_gst_registered_enabled = fields.Boolean(related='company_id.l10n_in_is_gst_registered')
|
||||
l10n_in_tds_deduction = fields.Selection(related='commercial_partner_id.l10n_in_pan_entity_id.tds_deduction', string="TDS Deduction")
|
||||
|
||||
@api.depends('amount_total')
|
||||
def _compute_amount_total_words(self):
|
||||
for invoice in self:
|
||||
invoice.amount_total_words = invoice.currency_id.amount_to_text(invoice.amount_total)
|
||||
# withholding related fields
|
||||
l10n_in_is_withholding = fields.Boolean(
|
||||
string="Is Indian TDS Entry",
|
||||
copy=False,
|
||||
help="Technical field to identify Indian withholding entry"
|
||||
)
|
||||
l10n_in_withholding_ref_move_id = fields.Many2one(
|
||||
comodel_name='account.move',
|
||||
string="Indian TDS Ref Move",
|
||||
readonly=True,
|
||||
index='btree_not_null',
|
||||
copy=False,
|
||||
help="Reference move for withholding entry",
|
||||
)
|
||||
l10n_in_withholding_ref_payment_id = fields.Many2one(
|
||||
comodel_name='account.payment',
|
||||
string="Indian TDS Ref Payment",
|
||||
index='btree_not_null',
|
||||
readonly=True,
|
||||
copy=False,
|
||||
help="Reference Payment for withholding entry",
|
||||
)
|
||||
l10n_in_withhold_move_ids = fields.One2many(
|
||||
'account.move', 'l10n_in_withholding_ref_move_id',
|
||||
string="Indian TDS Entries"
|
||||
)
|
||||
l10n_in_withholding_line_ids = fields.One2many(
|
||||
'account.move.line', 'move_id',
|
||||
string="Indian TDS Lines",
|
||||
compute='_compute_l10n_in_withholding_line_ids',
|
||||
)
|
||||
l10n_in_total_withholding_amount = fields.Monetary(
|
||||
string="Total Indian TDS Amount",
|
||||
compute='_compute_l10n_in_total_withholding_amount',
|
||||
help="Total withholding amount for the move",
|
||||
)
|
||||
l10n_in_display_higher_tcs_button = fields.Boolean(string="Display higher TCS button", compute="_compute_l10n_in_display_higher_tcs_button")
|
||||
l10n_in_tds_feature_enabled = fields.Boolean(related='company_id.l10n_in_tds_feature')
|
||||
l10n_in_tcs_feature_enabled = fields.Boolean(related='company_id.l10n_in_tcs_feature')
|
||||
|
||||
# gstin_status related field
|
||||
l10n_in_partner_gstin_status = fields.Boolean(
|
||||
string="GST Status",
|
||||
compute="_compute_l10n_in_partner_gstin_status_and_date",
|
||||
)
|
||||
l10n_in_show_gstin_status = fields.Boolean(compute="_compute_l10n_in_show_gstin_status")
|
||||
l10n_in_gstin_verified_date = fields.Date(compute="_compute_l10n_in_partner_gstin_status_and_date")
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# COMPUTE METHODS
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
@api.depends('partner_id')
|
||||
def _compute_l10n_in_gst_treatment(self):
|
||||
indian_invoice = self.filtered(lambda m: m.country_code == 'IN')
|
||||
for record in indian_invoice:
|
||||
gst_treatment = record.partner_id.l10n_in_gst_treatment
|
||||
if not gst_treatment:
|
||||
gst_treatment = 'unregistered'
|
||||
if record.partner_id.country_id.code == 'IN' and record.partner_id.vat:
|
||||
gst_treatment = 'regular'
|
||||
elif record.partner_id.country_id and record.partner_id.country_id.code != 'IN':
|
||||
gst_treatment = 'overseas'
|
||||
record.l10n_in_gst_treatment = gst_treatment
|
||||
(self - indian_invoice).l10n_in_gst_treatment = False
|
||||
for invoice in self.filtered(lambda m: m.country_code == 'IN' and m.state == 'draft'):
|
||||
partner = invoice.partner_id
|
||||
invoice.l10n_in_gst_treatment = (
|
||||
partner.l10n_in_gst_treatment
|
||||
or (
|
||||
'overseas' if partner.country_id and partner.country_id.code != 'IN'
|
||||
else partner.check_vat_in(partner.vat) and 'regular' or 'consumer'
|
||||
)
|
||||
)
|
||||
|
||||
@api.depends('partner_id', 'company_id')
|
||||
@api.depends('partner_id', 'partner_shipping_id', 'company_id')
|
||||
def _compute_l10n_in_state_id(self):
|
||||
foreign_state = self.env.ref('l10n_in.state_in_oc', raise_if_not_found=False)
|
||||
for move in self:
|
||||
if move.country_code == 'IN' and move.journal_id.type == 'sale':
|
||||
country_code = move.partner_id.country_id.code
|
||||
if country_code == 'IN':
|
||||
move.l10n_in_state_id = move.partner_id.state_id
|
||||
elif country_code:
|
||||
move.l10n_in_state_id = self.env.ref('l10n_in.state_in_oc', raise_if_not_found=False)
|
||||
else:
|
||||
move.l10n_in_state_id = move.company_id.state_id
|
||||
if move.country_code == 'IN' and move.is_sale_document(include_receipts=True):
|
||||
partner = (
|
||||
move.partner_id.commercial_partner_id == move.partner_shipping_id.commercial_partner_id
|
||||
and move.partner_shipping_id
|
||||
or move.partner_id
|
||||
)
|
||||
if partner.country_id and partner.country_id.code != 'IN':
|
||||
move.l10n_in_state_id = foreign_state
|
||||
continue
|
||||
partner_state = partner.state_id or move.partner_id.commercial_partner_id.state_id or move.company_id.state_id
|
||||
country_code = partner_state.country_id.code or move.country_code
|
||||
move.l10n_in_state_id = partner_state if country_code == 'IN' else foreign_state
|
||||
elif move.country_code == 'IN' and move.journal_id.type == 'purchase':
|
||||
move.l10n_in_state_id = move.company_id.state_id
|
||||
else:
|
||||
move.l10n_in_state_id = False
|
||||
|
||||
@api.depends('l10n_in_state_id', 'l10n_in_gst_treatment')
|
||||
def _compute_fiscal_position_id(self):
|
||||
|
||||
foreign_state = self.env['res.country.state'].search([('code', '!=', 'IN')], limit=1)
|
||||
|
||||
def _get_fiscal_state(move):
|
||||
"""
|
||||
Maps each move to its corresponding fiscal state based on its type,
|
||||
fiscal conditions, and the state of the associated partner or company.
|
||||
"""
|
||||
|
||||
if (
|
||||
move.country_code != 'IN'
|
||||
or not move.is_invoice(include_receipts=True)
|
||||
# Partner's FP takes precedence through super
|
||||
or move.partner_shipping_id.property_account_position_id
|
||||
or move.partner_id.property_account_position_id
|
||||
):
|
||||
return False
|
||||
elif move.l10n_in_gst_treatment == 'special_economic_zone':
|
||||
# Special Economic Zone
|
||||
return self.env.ref('l10n_in.state_in_oc')
|
||||
elif move.is_sale_document(include_receipts=True):
|
||||
# In Sales Documents: Compare place of supply with company state
|
||||
return move.l10n_in_state_id if move.l10n_in_state_id.l10n_in_tin != '96' else foreign_state
|
||||
elif move.is_purchase_document(include_receipts=True) and move.partner_id.country_id.code == 'IN':
|
||||
# In Purchases Documents: Compare place of supply with vendor state
|
||||
pos_state_id = move.l10n_in_state_id
|
||||
if pos_state_id.l10n_in_tin == '96':
|
||||
return pos_state_id
|
||||
elif pos_state_id == move.partner_id.state_id:
|
||||
# Intra-State: Group by state matching the company's state.
|
||||
return move.company_id.state_id
|
||||
elif pos_state_id != move.partner_id.state_id:
|
||||
# Inter-State: Group by state that doesn't match the company's state.
|
||||
return (
|
||||
pos_state_id == move.company_id.state_id
|
||||
and move.partner_id.state_id
|
||||
or pos_state_id
|
||||
)
|
||||
return False
|
||||
|
||||
FiscalPosition = self.env['account.fiscal.position']
|
||||
for state_id, moves in self.grouped(_get_fiscal_state).items():
|
||||
if state_id:
|
||||
virtual_partner = self.env['res.partner'].new({
|
||||
'state_id': state_id.id,
|
||||
'country_id': state_id.country_id.id,
|
||||
})
|
||||
# Group moves by company to avoid multi-company conflicts
|
||||
for company_id, company_moves in moves.grouped('company_id').items():
|
||||
company_moves.fiscal_position_id = FiscalPosition.with_company(
|
||||
company_id
|
||||
)._get_fiscal_position(virtual_partner)
|
||||
else:
|
||||
super(AccountMove, moves)._compute_fiscal_position_id()
|
||||
|
||||
@api.onchange('name')
|
||||
def _onchange_name_warning(self):
|
||||
if (
|
||||
self.country_code == 'IN'
|
||||
and self.company_id.l10n_in_is_gst_registered
|
||||
and self.journal_id.type == 'sale'
|
||||
and self.name
|
||||
and (len(self.name) > 16 or not re.match(r'^[a-zA-Z0-9-\/]+$', self.name))
|
||||
):
|
||||
return {'warning': {
|
||||
'title' : _("Invalid sequence as per GST rule 46(b)"),
|
||||
'message': _(
|
||||
"The invoice number should not exceed 16 characters\n"
|
||||
"and must only contain '-' (hyphen) and '/' (slash) as special characters"
|
||||
)
|
||||
}}
|
||||
return super()._onchange_name_warning()
|
||||
|
||||
@api.depends(
|
||||
'invoice_line_ids.l10n_in_hsn_code',
|
||||
'company_id.l10n_in_hsn_code_digit',
|
||||
'invoice_line_ids.tax_ids',
|
||||
'commercial_partner_id.l10n_in_pan_entity_id',
|
||||
'invoice_line_ids.price_total'
|
||||
)
|
||||
def _compute_l10n_in_warning(self):
|
||||
indian_invoice = self.filtered(lambda m: m.country_code == 'IN' and m.move_type != 'entry')
|
||||
line_filter_func = lambda line: line.display_type == 'product' and line.tax_ids and line._origin
|
||||
_xmlid_to_res_id = self.env['ir.model.data']._xmlid_to_res_id
|
||||
for move in indian_invoice:
|
||||
warnings = {}
|
||||
company = move.company_id
|
||||
action_name = _("Journal Item(s)")
|
||||
action_text = _("View Journal Item(s)")
|
||||
if company.l10n_in_tcs_feature or company.l10n_in_tds_feature:
|
||||
invalid_tax_lines = move._get_l10n_in_invalid_tax_lines()
|
||||
if company.l10n_in_tcs_feature and invalid_tax_lines:
|
||||
warnings['lower_tcs_tax'] = {
|
||||
'message': _("As the Partner's PAN missing/invalid apply TCS at the higher rate."),
|
||||
'actions': invalid_tax_lines.with_context(tax_validation=True)._get_records_action(
|
||||
name=action_name,
|
||||
views=[(_xmlid_to_res_id("l10n_in.view_move_line_tree_hsn_l10n_in"), "list")],
|
||||
domain=[('id', 'in', invalid_tax_lines.ids)],
|
||||
),
|
||||
'action_text': action_text,
|
||||
}
|
||||
|
||||
if applicable_sections := move._get_l10n_in_tds_tcs_applicable_sections():
|
||||
tds_tcs_applicable_lines = (
|
||||
move.move_type == 'out_invoice'
|
||||
and move._get_tcs_applicable_lines(move.invoice_line_ids)
|
||||
or move.invoice_line_ids
|
||||
)
|
||||
warnings['tds_tcs_threshold_alert'] = {
|
||||
'message': applicable_sections._get_warning_message(),
|
||||
'action': tds_tcs_applicable_lines.with_context(
|
||||
default_tax_type_use=True,
|
||||
move_type=move.move_type == 'in_invoice'
|
||||
)._get_records_action(
|
||||
name=action_name,
|
||||
domain=[('id', 'in', tds_tcs_applicable_lines.ids)],
|
||||
views=[(_xmlid_to_res_id("l10n_in.view_move_line_list_l10n_in_withholding"), "list")]
|
||||
),
|
||||
'action_text': action_text,
|
||||
}
|
||||
|
||||
if (
|
||||
company.l10n_in_is_gst_registered
|
||||
and company.l10n_in_hsn_code_digit
|
||||
and (filtered_lines := move.invoice_line_ids.filtered(line_filter_func))
|
||||
):
|
||||
lines = self.env['account.move.line']
|
||||
for line in filtered_lines:
|
||||
hsn_code = line.l10n_in_hsn_code
|
||||
if (
|
||||
not hsn_code
|
||||
or (
|
||||
not re.match(r'^\d{4}$|^\d{6}$|^\d{8}$', hsn_code)
|
||||
or len(hsn_code) < int(company.l10n_in_hsn_code_digit)
|
||||
)
|
||||
):
|
||||
lines |= line._origin
|
||||
|
||||
if lines:
|
||||
digit_suffixes = {
|
||||
'4': _("4 digits, 6 digits or 8 digits"),
|
||||
'6': _("6 digits or 8 digits"),
|
||||
'8': _("8 digits")
|
||||
}
|
||||
msg = _(
|
||||
"Ensure that the HSN/SAC Code consists either %s in invoice lines",
|
||||
digit_suffixes.get(company.l10n_in_hsn_code_digit, _("Invalid HSN/SAC Code digit"))
|
||||
)
|
||||
warnings['invalid_hsn_code_length'] = {
|
||||
'message': msg,
|
||||
'action': lines._get_records_action(
|
||||
name=action_name,
|
||||
views=[(_xmlid_to_res_id("l10n_in.view_move_line_tree_hsn_l10n_in"), "list")],
|
||||
domain=[('id', 'in', lines.ids)]
|
||||
),
|
||||
'action_text': action_text,
|
||||
}
|
||||
|
||||
move.l10n_in_warning = warnings
|
||||
(self - indian_invoice).l10n_in_warning = {}
|
||||
|
||||
@api.depends('partner_id', 'state', 'payment_state', 'l10n_in_gst_treatment')
|
||||
def _compute_l10n_in_show_gstin_status(self):
|
||||
indian_moves = self.filtered(
|
||||
lambda m: m.country_code == 'IN' and m.company_id.l10n_in_gstin_status_feature
|
||||
)
|
||||
(self - indian_moves).l10n_in_show_gstin_status = False
|
||||
for move in indian_moves:
|
||||
move.l10n_in_show_gstin_status = (
|
||||
move.partner_id
|
||||
and move.state == 'posted'
|
||||
and move.move_type != 'entry'
|
||||
and move.payment_state not in ['paid', 'reversed']
|
||||
and move.l10n_in_gst_treatment in [
|
||||
'regular',
|
||||
'composition',
|
||||
'special_economic_zone',
|
||||
'deemed_export',
|
||||
'uin_holders'
|
||||
]
|
||||
)
|
||||
|
||||
@api.depends('partner_id')
|
||||
def _compute_l10n_in_partner_gstin_status_and_date(self):
|
||||
for move in self:
|
||||
if (
|
||||
move.country_code == 'IN'
|
||||
and move.company_id.l10n_in_gstin_status_feature
|
||||
and move.payment_state not in ['paid', 'reversed']
|
||||
and move.state != 'cancel'
|
||||
):
|
||||
move.l10n_in_partner_gstin_status = move.partner_id.l10n_in_gstin_verified_status
|
||||
move.l10n_in_gstin_verified_date = move.partner_id.l10n_in_gstin_verified_date
|
||||
else:
|
||||
move.l10n_in_partner_gstin_status = False
|
||||
move.l10n_in_gstin_verified_date = False
|
||||
|
||||
@api.depends('line_ids', 'l10n_in_is_withholding')
|
||||
def _compute_l10n_in_withholding_line_ids(self):
|
||||
# Compute the withholding lines for the move
|
||||
for move in self:
|
||||
if move.l10n_in_is_withholding:
|
||||
move.l10n_in_withholding_line_ids = move.line_ids.filtered('tax_ids')
|
||||
else:
|
||||
move.l10n_in_withholding_line_ids = False
|
||||
|
||||
def _compute_l10n_in_total_withholding_amount(self):
|
||||
for move in self:
|
||||
if self.env.company.l10n_in_tds_feature:
|
||||
move.l10n_in_total_withholding_amount = sum(
|
||||
move.l10n_in_withhold_move_ids.filtered(
|
||||
lambda m: m.state == 'posted'
|
||||
).l10n_in_withholding_line_ids.mapped('l10n_in_withhold_tax_amount')
|
||||
)
|
||||
else:
|
||||
move.l10n_in_total_withholding_amount = 0.0
|
||||
|
||||
@api.depends('l10n_in_warning')
|
||||
def _compute_l10n_in_display_higher_tcs_button(self):
|
||||
for move in self:
|
||||
if move.company_id.l10n_in_tcs_feature:
|
||||
move.l10n_in_display_higher_tcs_button = (
|
||||
move.l10n_in_warning
|
||||
and move.l10n_in_warning.get('lower_tcs_tax')
|
||||
)
|
||||
else:
|
||||
move.l10n_in_display_higher_tcs_button = False
|
||||
|
||||
def action_l10n_in_withholding_entries(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
'name': "TDS Entries",
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'account.move',
|
||||
'view_mode': 'list,form',
|
||||
'domain': [('id', 'in', self.l10n_in_withhold_move_ids.ids)],
|
||||
}
|
||||
|
||||
def action_l10n_in_apply_higher_tax(self):
|
||||
self.ensure_one()
|
||||
invalid_lines = self._get_l10n_in_invalid_tax_lines()
|
||||
for line in invalid_lines:
|
||||
updated_tax_ids = []
|
||||
for tax in line.tax_ids:
|
||||
if tax.l10n_in_tax_type == 'tcs':
|
||||
max_tax = max(
|
||||
tax.l10n_in_section_id.l10n_in_section_tax_ids,
|
||||
key=lambda t: t.amount
|
||||
)
|
||||
updated_tax_ids.append(max_tax.id)
|
||||
else:
|
||||
updated_tax_ids.append(tax.id)
|
||||
if set(line.tax_ids.ids) != set(updated_tax_ids):
|
||||
line.write({'tax_ids': [Command.clear()] + [Command.set(updated_tax_ids)]})
|
||||
|
||||
def _get_l10n_in_invalid_tax_lines(self):
|
||||
self.ensure_one()
|
||||
if self.country_code == 'IN' and not self.commercial_partner_id.l10n_in_pan_entity_id:
|
||||
lines = self.env['account.move.line']
|
||||
for line in self.invoice_line_ids:
|
||||
for tax in line.tax_ids:
|
||||
if (
|
||||
tax.l10n_in_tax_type == 'tcs'
|
||||
and tax.amount != max(tax.l10n_in_section_id.l10n_in_section_tax_ids, key=lambda t: abs(t.amount)).amount
|
||||
):
|
||||
lines |= line._origin
|
||||
return lines
|
||||
|
||||
def _get_sections_aggregate_sum_by_pan(self, section_alert, commercial_partner_id):
|
||||
self.ensure_one()
|
||||
month_start_date, month_end_date = get_month(self.date)
|
||||
company_fiscalyear_dates = self.company_id.sudo().compute_fiscalyear_dates(self.date)
|
||||
fiscalyear_start_date, fiscalyear_end_date = company_fiscalyear_dates['date_from'], company_fiscalyear_dates['date_to']
|
||||
default_domain = [
|
||||
('account_id.l10n_in_tds_tcs_section_id', '=', section_alert.id),
|
||||
('move_id.move_type', '!=', 'entry'),
|
||||
('company_id.l10n_in_tds_feature', '!=', False),
|
||||
('company_id.l10n_in_tan', '=', self.company_id.l10n_in_tan),
|
||||
('parent_state', '=', 'posted')
|
||||
]
|
||||
if commercial_partner_id.l10n_in_pan_entity_id:
|
||||
default_domain += [('move_id.commercial_partner_id.l10n_in_pan_entity_id', '=', commercial_partner_id.l10n_in_pan_entity_id.id)]
|
||||
else:
|
||||
default_domain += [('move_id.commercial_partner_id', '=', commercial_partner_id.id)]
|
||||
frequency_domains = {
|
||||
'monthly': [('date', '>=', month_start_date), ('date', '<=', month_end_date)],
|
||||
'fiscal_yearly': [('date', '>=', fiscalyear_start_date), ('date', '<=', fiscalyear_end_date)],
|
||||
}
|
||||
aggregate_result = {}
|
||||
for frequency, frequency_domain in frequency_domains.items():
|
||||
query = self.env['account.move.line']._search(default_domain + frequency_domain, bypass_access=True, active_test=False)
|
||||
result = self.env.execute_query_dict(SQL(
|
||||
"""
|
||||
SELECT COALESCE(sum(account_move_line.balance), 0) as balance,
|
||||
COALESCE(sum(account_move_line.price_total * am.invoice_currency_rate), 0) as price_total
|
||||
FROM %s
|
||||
JOIN account_move AS am ON am.id = account_move_line.move_id
|
||||
WHERE %s
|
||||
""",
|
||||
query.from_clause,
|
||||
query.where_clause)
|
||||
)
|
||||
aggregate_result[frequency] = result[0]
|
||||
return aggregate_result
|
||||
|
||||
def _l10n_in_is_warning_applicable(self, section_id):
|
||||
self.ensure_one()
|
||||
match section_id.tax_source_type:
|
||||
case 'tcs':
|
||||
return self.company_id.l10n_in_tcs_feature and self.journal_id.type == 'sale'
|
||||
case 'tds':
|
||||
return (
|
||||
self.company_id.l10n_in_tds_feature
|
||||
and self.journal_id.type == 'purchase'
|
||||
and section_id not in self.l10n_in_withhold_move_ids.filtered(lambda m:
|
||||
m.state == 'posted'
|
||||
).mapped('line_ids.tax_ids.l10n_in_section_id')
|
||||
)
|
||||
case _:
|
||||
return False
|
||||
|
||||
def _get_l10n_in_tds_tcs_applicable_sections(self):
|
||||
def _group_by_section_alert(invoice_lines):
|
||||
group_by_lines = {}
|
||||
for line in invoice_lines:
|
||||
group_key = line.account_id.sudo().l10n_in_tds_tcs_section_id
|
||||
if group_key and not line.company_currency_id.is_zero(line.price_total):
|
||||
group_by_lines.setdefault(group_key, [])
|
||||
group_by_lines[group_key].append(line)
|
||||
return group_by_lines
|
||||
|
||||
def _is_section_applicable(section_alert, threshold_sums, invoice_currency_rate, lines):
|
||||
lines_total = sum(
|
||||
(line.price_total * invoice_currency_rate) if section_alert.consider_amount == 'total_amount' else line.balance
|
||||
for line in lines
|
||||
)
|
||||
if section_alert.is_aggregate_limit:
|
||||
aggregate_period_key = section_alert.consider_amount == 'total_amount' and 'price_total' or 'balance'
|
||||
aggregate_total = threshold_sums.get(section_alert.aggregate_period, {}).get(aggregate_period_key)
|
||||
if self.state == 'draft':
|
||||
aggregate_total += lines_total
|
||||
if aggregate_total > section_alert.aggregate_limit:
|
||||
return True
|
||||
return (
|
||||
section_alert.is_per_transaction_limit
|
||||
and lines_total > section_alert.per_transaction_limit
|
||||
)
|
||||
|
||||
if self.country_code == 'IN' and self.move_type in ['in_invoice', 'out_invoice']:
|
||||
warning = set()
|
||||
commercial_partner_id = self.commercial_partner_id
|
||||
if commercial_partner_id.l10n_in_pan_entity_id.tds_deduction == 'no':
|
||||
invoice_lines = self.invoice_line_ids.filtered(lambda l: l.account_id.l10n_in_tds_tcs_section_id.tax_source_type != 'tds')
|
||||
else:
|
||||
invoice_lines = self.invoice_line_ids
|
||||
existing_section = (
|
||||
self.l10n_in_withhold_move_ids.line_ids + self.line_ids
|
||||
).tax_ids.l10n_in_section_id
|
||||
for section_alert, lines in _group_by_section_alert(invoice_lines).items():
|
||||
if (
|
||||
(section_alert not in existing_section
|
||||
or self._get_tcs_applicable_lines(lines))
|
||||
and self._l10n_in_is_warning_applicable(section_alert)
|
||||
and _is_section_applicable(
|
||||
section_alert,
|
||||
self._get_sections_aggregate_sum_by_pan(
|
||||
section_alert,
|
||||
commercial_partner_id
|
||||
),
|
||||
self.invoice_currency_rate,
|
||||
lines
|
||||
)
|
||||
):
|
||||
warning.add(section_alert.id)
|
||||
return self.env['l10n_in.section.alert'].browse(warning)
|
||||
|
||||
def _get_tcs_applicable_lines(self, lines):
|
||||
tcs_applicable_lines = set()
|
||||
for line in lines:
|
||||
if line.l10n_in_tds_tcs_section_id not in line.tax_ids.l10n_in_section_id:
|
||||
tcs_applicable_lines.add(line.id)
|
||||
return self.env['account.move.line'].browse(tcs_applicable_lines)
|
||||
|
||||
def l10n_in_verify_partner_gstin_status(self):
|
||||
self.ensure_one()
|
||||
return self.with_company(self.company_id).partner_id.action_l10n_in_verify_gstin_status()
|
||||
|
||||
def _get_name_invoice_report(self):
|
||||
self.ensure_one()
|
||||
if self.country_code == 'IN':
|
||||
# TODO: remove the view mode check in master, only for stable releases
|
||||
in_invoice_view = self.env.ref('l10n_in.l10n_in_report_invoice_document_inherit', raise_if_not_found=False)
|
||||
if (in_invoice_view and in_invoice_view.sudo().mode == "primary"):
|
||||
return 'l10n_in.l10n_in_report_invoice_document_inherit'
|
||||
return 'l10n_in.l10n_in_report_invoice_document_inherit'
|
||||
return super()._get_name_invoice_report()
|
||||
|
||||
def _post(self, soft=True):
|
||||
|
|
@ -80,14 +555,12 @@ class AccountMove(models.Model):
|
|||
posted = super()._post(soft)
|
||||
gst_treatment_name_mapping = {k: v for k, v in
|
||||
self._fields['l10n_in_gst_treatment']._description_selection(self.env)}
|
||||
for move in posted.filtered(lambda m: m.country_code == 'IN' and m.is_sale_document()):
|
||||
"""Check state is set in company/sub-unit"""
|
||||
company_unit_partner = move.journal_id.l10n_in_gstin_partner_id or move.journal_id.company_id
|
||||
for move in posted.filtered(lambda m: m.country_code == 'IN' and m.company_id.l10n_in_is_gst_registered and m.is_sale_document()):
|
||||
if move.l10n_in_state_id and not move.l10n_in_state_id.l10n_in_tin:
|
||||
raise UserError(_("Please set a valid TIN Number on the Place of Supply %s", move.l10n_in_state_id.name))
|
||||
if not company_unit_partner.state_id:
|
||||
if not move.company_id.state_id:
|
||||
msg = _("Your company %s needs to have a correct address in order to validate this invoice.\n"
|
||||
"Set the address of your company (Don't forget the State field)") % (company_unit_partner.name)
|
||||
"Set the address of your company (Don't forget the State field)", move.company_id.name)
|
||||
action = {
|
||||
"view_mode": "form",
|
||||
"res_model": "res.company",
|
||||
|
|
@ -96,9 +569,16 @@ class AccountMove(models.Model):
|
|||
"views": [[self.env.ref("base.view_company_form").id, "form"]],
|
||||
}
|
||||
raise RedirectWarning(msg, action, _('Go to Company configuration'))
|
||||
|
||||
move.l10n_in_gstin = move.partner_id.vat
|
||||
if not move.l10n_in_gstin and move.l10n_in_gst_treatment in ['regular', 'composition', 'special_economic_zone', 'deemed_export']:
|
||||
if (
|
||||
not move.l10n_in_gstin
|
||||
and move.l10n_in_gst_treatment in [
|
||||
'regular',
|
||||
'composition',
|
||||
'special_economic_zone',
|
||||
'deemed_export'
|
||||
]
|
||||
):
|
||||
raise ValidationError(_(
|
||||
"Partner %(partner_name)s (%(partner_id)s) GSTIN is required under GST Treatment %(name)s",
|
||||
partner_name=move.partner_id.name,
|
||||
|
|
@ -113,30 +593,163 @@ class AccountMove(models.Model):
|
|||
self.ensure_one()
|
||||
return False
|
||||
|
||||
@api.ondelete(at_uninstall=False)
|
||||
def _unlink_l10n_in_except_once_post(self):
|
||||
# Prevent deleting entries once it's posted for Indian Company only
|
||||
if any(m.country_code == 'IN' and m.posted_before for m in self) and not self._context.get('force_delete'):
|
||||
raise UserError(_("To keep the audit trail, you can not delete journal entries once they have been posted.\nInstead, you can cancel the journal entry."))
|
||||
|
||||
def _can_be_unlinked(self):
|
||||
self.ensure_one()
|
||||
return (self.country_code != 'IN' or not self.posted_before) and super()._can_be_unlinked()
|
||||
|
||||
def unlink(self):
|
||||
# Add logger here becouse in api ondelete account.move.line is deleted and we can't get total amount
|
||||
logger_msg = False
|
||||
if any(m.country_code == 'IN' and m.posted_before for m in self):
|
||||
if self._context.get('force_delete'):
|
||||
moves_details = ", ".join("{entry_number} ({move_id}) amount {amount_total} {currency} and partner {partner_name}".format(
|
||||
entry_number=m.name,
|
||||
move_id=m.id,
|
||||
amount_total=m.amount_total,
|
||||
currency=m.currency_id.name,
|
||||
partner_name=m.partner_id.display_name)
|
||||
for m in self)
|
||||
logger_msg = 'Force deleted Journal Entries %s by %s (%s)' % (moves_details, self.env.user.name, self.env.user.id)
|
||||
res = super().unlink()
|
||||
if logger_msg:
|
||||
_logger.info(logger_msg)
|
||||
return res
|
||||
def _generate_qr_code(self, silent_errors=False):
|
||||
self.ensure_one()
|
||||
if self.company_id.country_code == 'IN' and self.company_id.l10n_in_upi_id:
|
||||
payment_url = 'upi://pay?pa=%s&pn=%s&am=%s&tr=%s&tn=%s' % (
|
||||
self.company_id.l10n_in_upi_id,
|
||||
self.company_id.name,
|
||||
self.amount_residual,
|
||||
self.payment_reference or self.name,
|
||||
("Payment for %s" % self.name))
|
||||
barcode = self.env['ir.actions.report'].barcode(barcode_type="QR", value=payment_url, width=120, height=120, quiet=False)
|
||||
return image_data_uri(base64.b64encode(barcode))
|
||||
return super()._generate_qr_code(silent_errors)
|
||||
|
||||
def _l10n_in_get_hsn_summary_table(self):
|
||||
self.ensure_one()
|
||||
base_lines, _tax_lines = self._get_rounded_base_and_tax_lines()
|
||||
display_uom = self.env.user.has_group('uom.group_uom')
|
||||
return self.env['account.tax']._l10n_in_get_hsn_summary_table(base_lines, display_uom)
|
||||
|
||||
def _l10n_in_get_bill_from_irn(self, irn):
|
||||
# TO OVERRIDE
|
||||
return False
|
||||
|
||||
# ------Utils------
|
||||
@api.model
|
||||
def _l10n_in_prepare_tax_details(self):
|
||||
def l10n_in_grouping_key_generator(base_line, tax_data):
|
||||
invl = base_line['record']
|
||||
tax = tax_data['tax']
|
||||
if self.l10n_in_gst_treatment in ('overseas', 'special_economic_zone') and all(
|
||||
self.env.ref("l10n_in.tax_tag_igst") in rl.tag_ids
|
||||
for rl in tax.invoice_repartition_line_ids if rl.repartition_type == 'tax'
|
||||
):
|
||||
tax_data['is_reverse_charge'] = False
|
||||
tag_ids = tax.invoice_repartition_line_ids.tag_ids.ids
|
||||
line_code = "other"
|
||||
xmlid_to_res_id = self.env['ir.model.data']._xmlid_to_res_id
|
||||
if not invl.currency_id.is_zero(tax_data['tax_amount_currency']):
|
||||
if xmlid_to_res_id("l10n_in.tax_tag_cess") in tag_ids:
|
||||
if tax.amount_type != "percent":
|
||||
line_code = "cess_non_advol"
|
||||
else:
|
||||
line_code = "cess"
|
||||
elif xmlid_to_res_id("l10n_in.tax_tag_state_cess") in tag_ids:
|
||||
if tax.amount_type != "percent":
|
||||
line_code = "state_cess_non_advol"
|
||||
else:
|
||||
line_code = "state_cess"
|
||||
else:
|
||||
for gst in ["cgst", "sgst", "igst"]:
|
||||
if xmlid_to_res_id("l10n_in.tax_tag_%s" % (gst)) in tag_ids:
|
||||
# need to separate rc tax value so it's not pass to other values
|
||||
line_code = f'{gst}_rc' if tax_data['is_reverse_charge'] else gst
|
||||
return {
|
||||
"tax": tax,
|
||||
"base_product_id": invl.product_id,
|
||||
"tax_product_id": invl.product_id,
|
||||
"base_product_uom_id": invl.product_uom_id,
|
||||
"tax_product_uom_id": invl.product_uom_id,
|
||||
"line_code": line_code,
|
||||
}
|
||||
|
||||
def l10n_in_filter_to_apply(base_line, tax_values):
|
||||
return base_line['record'].display_type != 'rounding'
|
||||
|
||||
return self._prepare_invoice_aggregated_taxes(
|
||||
filter_tax_values_to_apply=l10n_in_filter_to_apply,
|
||||
grouping_key_generator=l10n_in_grouping_key_generator,
|
||||
)
|
||||
|
||||
def _get_l10n_in_seller_buyer_party(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
"seller_details": self.company_id.partner_id,
|
||||
"dispatch_details": self._l10n_in_get_warehouse_address() or self.company_id.partner_id,
|
||||
"buyer_details": self.partner_id,
|
||||
"ship_to_details": self.partner_shipping_id or self.partner_id
|
||||
}
|
||||
|
||||
@api.model
|
||||
def _l10n_in_extract_digits(self, string):
|
||||
if not string:
|
||||
return ""
|
||||
matches = re.findall(r"\d+", string)
|
||||
return "".join(matches)
|
||||
|
||||
@api.model
|
||||
def _l10n_in_is_service_hsn(self, hsn_code):
|
||||
return self._l10n_in_extract_digits(hsn_code).startswith('99')
|
||||
|
||||
@api.model
|
||||
def _l10n_in_round_value(self, amount, precision_digits=2):
|
||||
"""
|
||||
This method is call for rounding.
|
||||
If anything is wrong with rounding then we quick fix in method
|
||||
"""
|
||||
return json_float_round(amount, precision_digits)
|
||||
|
||||
@api.model
|
||||
def _get_l10n_in_tax_details_by_line_code(self, tax_details):
|
||||
l10n_in_tax_details = {}
|
||||
for tax_detail in tax_details.values():
|
||||
if tax_detail["tax"].l10n_in_reverse_charge:
|
||||
l10n_in_tax_details.setdefault("is_reverse_charge", True)
|
||||
line_code = tax_detail["line_code"]
|
||||
l10n_in_tax_details.setdefault("%s_rate" % (line_code), tax_detail["tax"].amount)
|
||||
l10n_in_tax_details.setdefault("%s_amount" % (line_code), 0.00)
|
||||
l10n_in_tax_details.setdefault("%s_amount_currency" % (line_code), 0.00)
|
||||
l10n_in_tax_details["%s_amount" % (line_code)] += tax_detail["tax_amount"]
|
||||
l10n_in_tax_details["%s_amount_currency" % (line_code)] += tax_detail["tax_amount_currency"]
|
||||
return l10n_in_tax_details
|
||||
|
||||
@api.model
|
||||
def _l10n_in_edi_get_iap_buy_credits_message(self):
|
||||
url = self.env['iap.account'].get_credits_url(service_name=IAP_SERVICE_NAME)
|
||||
return Markup("""<p><b>%s</b></p><p>%s <a href="%s">%s</a></p>""") % (
|
||||
_("You have insufficient credits to send this document!"),
|
||||
_("Please buy more credits and retry: "),
|
||||
url,
|
||||
_("Buy Credits")
|
||||
)
|
||||
|
||||
def _get_sync_stack(self, container):
|
||||
stack, update_containers = super()._get_sync_stack(container)
|
||||
if all(move.country_code != 'IN' for move in self):
|
||||
return stack, update_containers
|
||||
_tax_container, invoice_container, misc_container = update_containers()
|
||||
moves = invoice_container['records'] + misc_container['records']
|
||||
stack.append((9, self._sync_l10n_in_gstr_section(moves)))
|
||||
return stack, update_containers
|
||||
|
||||
@contextmanager
|
||||
def _sync_l10n_in_gstr_section(self, moves):
|
||||
yield
|
||||
tax_tags_dict = self.env['account.move.line']._get_l10n_in_tax_tag_ids()
|
||||
# we set the section on the invoice lines
|
||||
moves.line_ids._set_l10n_in_gstr_section(tax_tags_dict)
|
||||
|
||||
def _get_l10n_in_invoice_label(self):
|
||||
self.ensure_one()
|
||||
exempt_types = {'exempt', 'nil_rated', 'non_gst'}
|
||||
if self.country_code != 'IN' or not self.is_sale_document(include_receipts=False):
|
||||
return
|
||||
gst_treatment = self.l10n_in_gst_treatment
|
||||
company = self.company_id
|
||||
tax_types = set(self.invoice_line_ids.tax_ids.mapped('l10n_in_tax_type'))
|
||||
if company.l10n_in_is_gst_registered and tax_types:
|
||||
if gst_treatment in ['overseas', 'special_economic_zone']:
|
||||
return 'Tax Invoice'
|
||||
elif tax_types.issubset(exempt_types):
|
||||
return 'Bill of Supply'
|
||||
elif tax_types.isdisjoint(exempt_types):
|
||||
return 'Tax Invoice'
|
||||
elif gst_treatment in ['unregistered', 'consumer']:
|
||||
return 'Invoice-cum-Bill of Supply'
|
||||
return 'Invoice'
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
from odoo import models
|
||||
|
||||
|
||||
class AccountJournal(models.Model):
|
||||
_inherit = "account.journal"
|
||||
|
||||
def _update_payment_method_lines(self, payment_type):
|
||||
bank_journals = self.filtered(lambda j: j.type == "bank" and j.company_id.chart_template == "in")
|
||||
if not bank_journals:
|
||||
return
|
||||
|
||||
if payment_type == 'inbound':
|
||||
account_xmlid = "account_journal_payment_debit_account_id"
|
||||
else:
|
||||
account_xmlid = "account_journal_payment_credit_account_id"
|
||||
|
||||
lines_to_update = bank_journals[f"{payment_type}_payment_method_line_ids"].filtered(
|
||||
lambda l: l.payment_method_id.code == 'manual'
|
||||
)
|
||||
for company, lines in lines_to_update.grouped('company_id').items():
|
||||
if account := self.env['account.chart.template'].with_company(company).ref(account_xmlid, raise_if_not_found=False):
|
||||
lines.payment_account_id = account
|
||||
|
||||
def _compute_inbound_payment_method_line_ids(self):
|
||||
super()._compute_inbound_payment_method_line_ids()
|
||||
self._update_payment_method_lines("inbound")
|
||||
|
||||
def _compute_outbound_payment_method_line_ids(self):
|
||||
super()._compute_outbound_payment_method_line_ids()
|
||||
self._update_payment_method_lines("outbound")
|
||||
|
|
@ -0,0 +1,318 @@
|
|||
import re
|
||||
from datetime import date
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
|
||||
|
||||
class AccountMoveLine(models.Model):
|
||||
_inherit = "account.move.line"
|
||||
|
||||
l10n_in_hsn_code = fields.Char(string="HSN/SAC Code", compute="_compute_l10n_in_hsn_code", store=True, readonly=False, copy=False)
|
||||
l10n_in_gstr_section = fields.Selection(
|
||||
selection=[
|
||||
("sale_b2b_rcm", "B2B RCM"),
|
||||
("sale_b2b_regular", "B2B Regular"),
|
||||
("sale_b2cl", "B2CL"),
|
||||
("sale_b2cs", "B2CS"),
|
||||
("sale_exp_wp", "EXP(WP)"),
|
||||
("sale_exp_wop", "EXP(WOP)"),
|
||||
("sale_sez_wp", "SEZ(WP)"),
|
||||
("sale_sez_wop", "SEZ(WOP)"),
|
||||
("sale_deemed_export", "Deemed Export"),
|
||||
("sale_cdnr_rcm", "CDNR RCM"),
|
||||
("sale_cdnr_regular", "CDNR Regular"),
|
||||
("sale_cdnr_deemed_export", "CDNR(Deemed Export)"),
|
||||
("sale_cdnr_sez_wp", "CDNR(SEZ-WP)"),
|
||||
("sale_cdnr_sez_wop", "CDNR(SEZ-WOP)"),
|
||||
("sale_cdnur_b2cl", "CDNUR(B2CL)"),
|
||||
("sale_cdnur_exp_wp", "CDNUR(EXP-WP)"),
|
||||
("sale_cdnur_exp_wop", "CDNUR(EXP-WOP)"),
|
||||
("sale_nil_rated", "Nil Rated"),
|
||||
("sale_exempt", "Exempt"),
|
||||
("sale_non_gst_supplies", "Non-GST Supplies"),
|
||||
("sale_eco_9_5", "ECO 9(5)"),
|
||||
("sale_out_of_scope", "Out of Scope"),
|
||||
("purchase_b2b_regular", "B2B Regular"),
|
||||
("purchase_b2c_regular", "B2C Regular"), # will be removed in master
|
||||
("purchase_b2b_rcm", "B2B RCM"),
|
||||
("purchase_b2c_rcm", "B2C RCM"),
|
||||
("purchase_imp_services", "IMP(service)"),
|
||||
("purchase_imp_goods", "IMP(goods)"),
|
||||
("purchase_cdnr_regular", "CDNR Regular"),
|
||||
("purchase_cdnur_regular", "CDNUR Regular"),
|
||||
("purchase_cdnr_rcm", "CDNR RCM"),
|
||||
("purchase_cdnur_rcm", "CDNUR RCM"),
|
||||
("purchase_nil_rated", "Nil Rated"),
|
||||
("purchase_exempt", "Exempt"),
|
||||
("purchase_non_gst_supplies", "Non-GST Supplies"),
|
||||
("purchase_out_of_scope", "Out of Scope"),
|
||||
],
|
||||
string="GSTR Section",
|
||||
index="btree_not_null",
|
||||
)
|
||||
|
||||
# withholding related fields
|
||||
l10n_in_withhold_tax_amount = fields.Monetary(string="TDS Tax Amount", compute='_compute_l10n_in_withhold_tax_amount')
|
||||
l10n_in_tds_tcs_section_id = fields.Many2one(related="account_id.l10n_in_tds_tcs_section_id")
|
||||
|
||||
@api.depends('tax_ids')
|
||||
def _compute_l10n_in_withhold_tax_amount(self):
|
||||
# Compute the withhold tax amount for the withholding lines
|
||||
withholding_lines = self.filtered('move_id.l10n_in_is_withholding')
|
||||
(self - withholding_lines).l10n_in_withhold_tax_amount = False
|
||||
for line in withholding_lines:
|
||||
line.l10n_in_withhold_tax_amount = line.currency_id.round(abs(line.price_total - line.price_subtotal))
|
||||
|
||||
@api.depends('product_id', 'product_id.l10n_in_hsn_code')
|
||||
def _compute_l10n_in_hsn_code(self):
|
||||
for line in self:
|
||||
if line.move_id.country_code == 'IN' and line.parent_state == 'draft':
|
||||
line.l10n_in_hsn_code = line.product_id.l10n_in_hsn_code
|
||||
|
||||
def _l10n_in_check_invalid_hsn_code(self):
|
||||
self.ensure_one()
|
||||
hsn_code = self.env['account.move']._l10n_in_extract_digits(self.l10n_in_hsn_code)
|
||||
if not hsn_code:
|
||||
return _("HSN code is not set in product line %(name)s", name=self.name)
|
||||
elif not re.match(r'^\d{4}$|^\d{6}$|^\d{8}$', hsn_code):
|
||||
return _(
|
||||
"Invalid HSN Code (%(hsn_code)s) in product line %(product_line)s",
|
||||
hsn_code=hsn_code,
|
||||
product_line=self.product_id.name or self.name
|
||||
)
|
||||
return False
|
||||
|
||||
def _get_l10n_in_tax_tag_ids(self):
|
||||
xmlid_to_res_id = self.env['ir.model.data']._xmlid_to_res_id
|
||||
tag_refs = {
|
||||
'sgst': ['l10n_in.tax_tag_base_sgst', 'l10n_in.tax_tag_sgst'],
|
||||
'cgst': ['l10n_in.tax_tag_base_cgst', 'l10n_in.tax_tag_cgst'],
|
||||
'igst': ['l10n_in.tax_tag_base_igst', 'l10n_in.tax_tag_igst'],
|
||||
'cess': ['l10n_in.tax_tag_base_cess', 'l10n_in.tax_tag_cess'],
|
||||
'eco_9_5': ['l10n_in.tax_tag_eco_9_5'],
|
||||
}
|
||||
return {
|
||||
categ: [xmlid_to_res_id(xml_id) for xml_id in ref]
|
||||
for categ, ref in tag_refs.items()
|
||||
}
|
||||
|
||||
def _get_l10n_in_gstr_section(self, tax_tags_dict):
|
||||
|
||||
def tags_have_categ(line_tax_tags, categories):
|
||||
return any(tag in line_tax_tags for category in categories for tag in tax_tags_dict.get(category, []))
|
||||
|
||||
def is_invoice(move):
|
||||
return move.is_inbound() and not move.debit_origin_id
|
||||
|
||||
def is_move_bill(move):
|
||||
return move.is_outbound() and not move.debit_origin_id
|
||||
|
||||
def get_transaction_type(move):
|
||||
return 'intra_state' if move.l10n_in_state_id == move.company_id.state_id else 'inter_state'
|
||||
|
||||
def is_reverse_charge_tax(line):
|
||||
return any(tax.l10n_in_reverse_charge for tax in line.tax_ids | line.tax_line_id)
|
||||
|
||||
def is_lut_tax(line):
|
||||
return any(tax.l10n_in_is_lut for tax in line.tax_ids | line.tax_line_id)
|
||||
|
||||
def get_sales_section(line):
|
||||
move = line.move_id
|
||||
gst_treatment = move.l10n_in_gst_treatment
|
||||
transaction_type = get_transaction_type(move)
|
||||
line_tags = line.tax_tag_ids.ids
|
||||
is_inv = is_invoice(move)
|
||||
amt_limit = 100000 if not line.invoice_date or line.invoice_date >= date(2024, 11, 1) else 250000
|
||||
|
||||
# ECO 9(5) Section: Check if the line has the ECO 9(5) tax tag
|
||||
if tags_have_categ(line_tags, ['eco_9_5']):
|
||||
return 'sale_eco_9_5'
|
||||
|
||||
# Nil rated, Exempt, Non-GST Sales
|
||||
if gst_treatment != 'overseas':
|
||||
if any(tax.l10n_in_tax_type == 'nil_rated' for tax in line.tax_ids):
|
||||
return 'sale_nil_rated'
|
||||
elif any(tax.l10n_in_tax_type == 'exempt' for tax in line.tax_ids):
|
||||
return 'sale_exempt'
|
||||
elif any(tax.l10n_in_tax_type == 'non_gst' for tax in line.tax_ids):
|
||||
return 'sale_non_gst_supplies'
|
||||
|
||||
# B2CS: Unregistered or Consumer sales with gst tags
|
||||
if gst_treatment in ('unregistered', 'consumer') and not is_reverse_charge_tax(line):
|
||||
if (transaction_type == 'intra_state' and tags_have_categ(line_tags, ['sgst', 'cgst', 'cess'])) or (
|
||||
transaction_type == "inter_state"
|
||||
and tags_have_categ(line_tags, ['igst', 'cess'])
|
||||
and not is_lut_tax(line)
|
||||
and (
|
||||
is_inv and move.amount_total <= amt_limit
|
||||
or move.debit_origin_id and move.debit_origin_id.amount_total <= amt_limit
|
||||
or move.reversed_entry_id and move.reversed_entry_id.amount_total <= amt_limit
|
||||
)
|
||||
):
|
||||
return 'sale_b2cs'
|
||||
|
||||
# If no relevant tags are found, or the tags do not match any category, mark as out of scope
|
||||
if not line_tags or not tags_have_categ(line_tags, ['sgst', 'cgst', 'igst', 'cess', 'eco_9_5']):
|
||||
return 'sale_out_of_scope'
|
||||
|
||||
# If it's a standard invoice (not a debit/credit note)
|
||||
if is_inv:
|
||||
# B2B with Reverse Charge and Regular
|
||||
if gst_treatment in ('regular', 'composition', 'uin_holders') and tags_have_categ(line_tags, ['sgst', 'cgst', 'igst', 'cess']) and not is_lut_tax(line):
|
||||
if is_reverse_charge_tax(line):
|
||||
return 'sale_b2b_rcm'
|
||||
return 'sale_b2b_regular'
|
||||
|
||||
if not is_reverse_charge_tax(line):
|
||||
# B2CL: Unregistered interstate sales above threshold
|
||||
if (
|
||||
gst_treatment in ('unregistered', 'consumer')
|
||||
and tags_have_categ(line_tags, ['igst', 'cess'])
|
||||
and not is_lut_tax(line)
|
||||
and transaction_type == 'inter_state'
|
||||
and move.amount_total > amt_limit
|
||||
):
|
||||
return 'sale_b2cl'
|
||||
# Export with payment and without payment (under LUT) of tax
|
||||
if gst_treatment == 'overseas' and tags_have_categ(line_tags, ['igst', 'cess']):
|
||||
if is_lut_tax(line):
|
||||
return 'sale_exp_wop'
|
||||
return 'sale_exp_wp'
|
||||
# SEZ with payment and without payment of tax
|
||||
if gst_treatment == 'special_economic_zone' and tags_have_categ(line_tags, ['igst', 'cess']):
|
||||
if is_lut_tax(line):
|
||||
return 'sale_sez_wop'
|
||||
return 'sale_sez_wp'
|
||||
# Deemed export
|
||||
if gst_treatment == 'deemed_export' and tags_have_categ(line_tags, ['sgst', 'cgst', 'igst', 'cess']) and not is_lut_tax(line):
|
||||
return 'sale_deemed_export'
|
||||
|
||||
# If it's not a standard invoice (i.e., it's a debit/credit note)
|
||||
if not is_inv:
|
||||
# CDN for B2B reverse charge and B2B regular
|
||||
if gst_treatment in ('regular', 'composition', 'uin_holders') and tags_have_categ(line_tags, ['sgst', 'cgst', 'igst', 'cess']) and not is_lut_tax(line):
|
||||
if is_reverse_charge_tax(line):
|
||||
return 'sale_cdnr_rcm'
|
||||
return 'sale_cdnr_regular'
|
||||
if not is_reverse_charge_tax(line):
|
||||
# CDN for SEZ exports with payment and without payment
|
||||
if gst_treatment == 'special_economic_zone' and tags_have_categ(line_tags, ['igst', 'cess']):
|
||||
if is_lut_tax(line):
|
||||
return 'sale_cdnr_sez_wop'
|
||||
return 'sale_cdnr_sez_wp'
|
||||
# CDN for deemed exports
|
||||
if gst_treatment == 'deemed_export' and tags_have_categ(line_tags, ['sgst', 'cgst', 'igst', 'cess']) and not is_lut_tax(line):
|
||||
return 'sale_cdnr_deemed_export'
|
||||
# CDN for B2CL (interstate > threshold)
|
||||
if (
|
||||
gst_treatment in ('unregistered', 'consumer')
|
||||
and tags_have_categ(line_tags, ['igst', 'cess'])
|
||||
and not is_lut_tax(line)
|
||||
and transaction_type == 'inter_state'
|
||||
and (
|
||||
move.debit_origin_id and move.debit_origin_id.amount_total > amt_limit
|
||||
or move.reversed_entry_id and move.reversed_entry_id.amount_total > amt_limit
|
||||
or not move.reversed_entry_id and not move.is_inbound()
|
||||
)
|
||||
):
|
||||
return 'sale_cdnur_b2cl'
|
||||
# CDN for exports with payment and without payment
|
||||
if gst_treatment == 'overseas' and tags_have_categ(line_tags, ['igst', 'cess']):
|
||||
if is_lut_tax(line):
|
||||
return 'sale_cdnur_exp_wop'
|
||||
return 'sale_cdnur_exp_wp'
|
||||
# If none of the above match, default to out of scope
|
||||
return 'sale_out_of_scope'
|
||||
|
||||
def get_purchase_section(line):
|
||||
move = line.move_id
|
||||
gst_treatment = move.l10n_in_gst_treatment
|
||||
line_tags = line.tax_tag_ids.ids
|
||||
is_bill = is_move_bill(move)
|
||||
|
||||
# Nil rated, Exempt, Non-GST purchases
|
||||
if gst_treatment != 'overseas':
|
||||
if any(tax.l10n_in_tax_type == 'nil_rated' for tax in line.tax_ids):
|
||||
return 'purchase_nil_rated'
|
||||
elif any(tax.l10n_in_tax_type == 'exempt' for tax in line.tax_ids):
|
||||
return 'purchase_exempt'
|
||||
elif any(tax.l10n_in_tax_type == 'non_gst' for tax in line.tax_ids):
|
||||
return 'purchase_non_gst_supplies'
|
||||
|
||||
# If no relevant tags are found, or the tags do not match any category, mark as out of scope
|
||||
if not line_tags or not tags_have_categ(line_tags, ['sgst', 'cgst', 'igst', 'cess']):
|
||||
return 'purchase_out_of_scope'
|
||||
|
||||
if is_bill:
|
||||
# B2B Regular and Reverse Charge purchases
|
||||
if (gst_treatment in ('regular', 'composition', 'uin_holders') and tags_have_categ(line_tags, ['sgst', 'cgst', 'igst', 'cess'])):
|
||||
if is_reverse_charge_tax(line):
|
||||
return 'purchase_b2b_rcm'
|
||||
return 'purchase_b2b_regular'
|
||||
|
||||
if not is_reverse_charge_tax(line) and (
|
||||
gst_treatment == 'deemed_export' and tags_have_categ(line_tags, ['sgst', 'cgst', 'igst', 'cess'])
|
||||
or gst_treatment == 'special_economic_zone' and tags_have_categ(line_tags, ['igst', 'cess'])
|
||||
):
|
||||
return 'purchase_b2b_regular'
|
||||
|
||||
# B2C Unregistered or Consumer sales with gst tags
|
||||
if gst_treatment in ('unregistered', 'consumer') and tags_have_categ(line_tags, ['sgst', 'cgst', 'igst', 'cess']) and is_reverse_charge_tax(line):
|
||||
return 'purchase_b2c_rcm'
|
||||
|
||||
# export service type products purchases
|
||||
if gst_treatment == 'overseas' and any(tax.tax_scope == 'service' for tax in line.tax_ids | line.tax_line_id) and tags_have_categ(line_tags, ['igst', 'cess']):
|
||||
return 'purchase_imp_services'
|
||||
|
||||
# export goods type products purchases
|
||||
if gst_treatment == 'overseas' and tags_have_categ(line_tags, ['igst', 'cess']) and not is_reverse_charge_tax(line):
|
||||
return 'purchase_imp_goods'
|
||||
|
||||
if not is_bill:
|
||||
# credit notes for b2b purchases
|
||||
if gst_treatment in ('regular', 'composition', 'uin_holders') and tags_have_categ(line_tags, ['sgst', 'cgst', 'igst', 'cess']):
|
||||
if is_reverse_charge_tax(line):
|
||||
return 'purchase_cdnr_rcm'
|
||||
return 'purchase_cdnr_regular'
|
||||
|
||||
# credit notes for b2c purchases
|
||||
if gst_treatment in ('unregistered', 'consumer') and tags_have_categ(line_tags, ['sgst', 'cgst', 'igst', 'cess']) and is_reverse_charge_tax(line):
|
||||
return 'purchase_cdnur_rcm'
|
||||
|
||||
if not is_reverse_charge_tax(line):
|
||||
if gst_treatment == 'deemed_export' and tags_have_categ(line_tags, ['sgst', 'cgst', 'igst', 'cess'])\
|
||||
or gst_treatment == 'special_economic_zone' and tags_have_categ(line_tags, ['igst', 'cess']):
|
||||
return 'purchase_cdnr_regular'
|
||||
|
||||
if gst_treatment == 'overseas' and tags_have_categ(line_tags, ['igst', 'cess']):
|
||||
return 'purchase_cdnur_regular'
|
||||
|
||||
# If none of the above match, default to out of scope
|
||||
return 'purchase_out_of_scope'
|
||||
|
||||
indian_sale_moves_lines = self.filtered(
|
||||
lambda l: l.move_id.country_code == 'IN'
|
||||
and l.move_id.is_sale_document(include_receipts=True)
|
||||
and l.display_type in ('product', 'tax')
|
||||
)
|
||||
indian_moves_purchase_lines = self.filtered(
|
||||
lambda l: l.move_id.country_code == 'IN'
|
||||
and l.move_id.is_purchase_document(include_receipts=True)
|
||||
and l.display_type in ('product', 'tax')
|
||||
)
|
||||
# No Indian sale or purchase lines to process
|
||||
if not indian_sale_moves_lines and not indian_moves_purchase_lines:
|
||||
return {}
|
||||
|
||||
move_lines_by_gstr_section = {
|
||||
**indian_sale_moves_lines.grouped(get_sales_section),
|
||||
**indian_moves_purchase_lines.grouped(get_purchase_section),
|
||||
}
|
||||
|
||||
return move_lines_by_gstr_section
|
||||
|
||||
def _set_l10n_in_gstr_section(self, tax_tags_dict):
|
||||
move_lines_by_gstr_section = self._get_l10n_in_gstr_section(tax_tags_dict)
|
||||
if move_lines_by_gstr_section:
|
||||
for gstr_section, move_lines in move_lines_by_gstr_section.items():
|
||||
move_lines.l10n_in_gstr_section = gstr_section
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
from odoo import fields, models
|
||||
|
||||
|
||||
class AccountPayment(models.Model):
|
||||
_inherit = "account.payment"
|
||||
|
||||
# withholding related fields
|
||||
l10n_in_withhold_move_ids = fields.One2many(
|
||||
'account.move', 'l10n_in_withholding_ref_payment_id',
|
||||
string="Indian Payment TDS Entries",
|
||||
)
|
||||
l10n_in_total_withholding_amount = fields.Monetary(compute='_compute_l10n_in_total_withholding_amount')
|
||||
l10n_in_tds_feature_enabled = fields.Boolean(related='company_id.l10n_in_tds_feature')
|
||||
|
||||
def _compute_l10n_in_total_withholding_amount(self):
|
||||
for payment in self:
|
||||
if payment.company_id.l10n_in_tds_feature:
|
||||
payment.l10n_in_total_withholding_amount = sum(payment.l10n_in_withhold_move_ids.filtered(
|
||||
lambda m: m.state == 'posted').l10n_in_withholding_line_ids.mapped('l10n_in_withhold_tax_amount'))
|
||||
else:
|
||||
payment.l10n_in_total_withholding_amount = 0.0
|
||||
|
||||
def action_l10n_in_withholding_entries(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
'name': "TDS Entries",
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'account.move',
|
||||
'view_mode': 'list,form',
|
||||
'domain': [('id', 'in', self.l10n_in_withhold_move_ids.ids)],
|
||||
}
|
||||
143
odoo-bringout-oca-ocb-l10n_in/l10n_in/models/account_tax.py
Normal file
143
odoo-bringout-oca-ocb-l10n_in/l10n_in/models/account_tax.py
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
from collections import defaultdict
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo.tools import frozendict
|
||||
|
||||
|
||||
class AccountTax(models.Model):
|
||||
_inherit = 'account.tax'
|
||||
|
||||
l10n_in_reverse_charge = fields.Boolean("Reverse charge", help="Tick this if this tax is reverse charge. Only for Indian accounting")
|
||||
l10n_in_gst_tax_type = fields.Selection(
|
||||
selection=[('igst', 'igst'), ('cgst', 'cgst'), ('sgst', 'sgst'), ('cess', 'cess')],
|
||||
compute='_compute_l10n_in_gst_tax_type',
|
||||
)
|
||||
l10n_in_is_lut = fields.Boolean(
|
||||
string="LUT",
|
||||
help="Tick this if this tax is used in LUT (Letter of Undertaking) transactions. Only for Indian accounting.",
|
||||
)
|
||||
l10n_in_tax_type = fields.Selection(
|
||||
selection=[
|
||||
('gst', 'GST'),
|
||||
('tcs', 'TCS'),
|
||||
('tds_sale', 'TDS Sale'),
|
||||
('tds_purchase', 'TDS Purchase'),
|
||||
('nil_rated', 'Nil Rated'),
|
||||
('exempt', 'Exempt'),
|
||||
('non_gst', 'Non-GST'),
|
||||
],
|
||||
string="Indian Tax Type",
|
||||
)
|
||||
|
||||
# withholding related fields
|
||||
l10n_in_section_id = fields.Many2one('l10n_in.section.alert', string="Section")
|
||||
l10n_in_tds_feature_enabled = fields.Boolean(related='company_id.l10n_in_tds_feature')
|
||||
l10n_in_tcs_feature_enabled = fields.Boolean(related='company_id.l10n_in_tcs_feature')
|
||||
|
||||
@api.depends('country_code', 'invoice_repartition_line_ids.tag_ids')
|
||||
def _compute_l10n_in_gst_tax_type(self):
|
||||
self.l10n_in_gst_tax_type = False
|
||||
in_taxes = self.filtered(lambda tax: tax.country_code == 'IN')
|
||||
if in_taxes:
|
||||
tags_mapping = {
|
||||
'igst': self.env.ref('l10n_in.tax_tag_igst'),
|
||||
'cgst': self.env.ref('l10n_in.tax_tag_cgst'),
|
||||
'sgst': self.env.ref('l10n_in.tax_tag_sgst'),
|
||||
'cess': self.env.ref('l10n_in.tax_tag_cess'),
|
||||
}
|
||||
for tax in in_taxes:
|
||||
tags = tax.invoice_repartition_line_ids.tag_ids
|
||||
for tag_code, tag in tags_mapping.items():
|
||||
if tag in tags:
|
||||
tax.l10n_in_gst_tax_type = tag_code
|
||||
break
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# HELPERS IN BOTH PYTHON/JAVASCRIPT (hsn_summary.js / account_tax.py)
|
||||
|
||||
# HSN SUMMARY
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def _prepare_base_line_for_taxes_computation(self, record, **kwargs):
|
||||
# EXTENDS 'account'
|
||||
results = super()._prepare_base_line_for_taxes_computation(record, **kwargs)
|
||||
results['l10n_in_hsn_code'] = self._get_base_line_field_value_from_record(record, 'l10n_in_hsn_code', kwargs, False)
|
||||
return results
|
||||
|
||||
@api.model
|
||||
def _l10n_in_get_hsn_summary_table(self, base_lines, display_uom):
|
||||
l10n_in_gst_tax_types = set()
|
||||
items_map = defaultdict(lambda: {
|
||||
'quantity': 0.0,
|
||||
'amount_untaxed': 0.0,
|
||||
'tax_amount_igst': 0.0,
|
||||
'tax_amount_cgst': 0.0,
|
||||
'tax_amount_sgst': 0.0,
|
||||
'tax_amount_cess': 0.0,
|
||||
})
|
||||
|
||||
def get_base_line_grouping_key(base_line):
|
||||
unique_taxes_data = set(
|
||||
tax_data['tax']
|
||||
for tax_data in base_line['tax_details']['taxes_data']
|
||||
if tax_data['tax']['l10n_in_gst_tax_type'] in ('igst', 'cgst', 'sgst')
|
||||
)
|
||||
rate = sum(tax.amount for tax in unique_taxes_data)
|
||||
|
||||
return {
|
||||
'l10n_in_hsn_code': base_line['l10n_in_hsn_code'],
|
||||
'uom_name': base_line['product_uom_id'].name,
|
||||
'rate': rate,
|
||||
}
|
||||
|
||||
# quantity / amount_untaxed.
|
||||
for base_line in base_lines:
|
||||
key = frozendict(get_base_line_grouping_key(base_line))
|
||||
if not key['l10n_in_hsn_code']:
|
||||
continue
|
||||
|
||||
item = items_map[key]
|
||||
item['quantity'] += base_line['quantity']
|
||||
item['amount_untaxed'] += (
|
||||
base_line['tax_details']['total_excluded_currency']
|
||||
+ base_line['tax_details']['delta_total_excluded_currency']
|
||||
)
|
||||
|
||||
# Tax amounts.
|
||||
def grouping_function(base_line, tax_data):
|
||||
return {
|
||||
**get_base_line_grouping_key(base_line),
|
||||
'l10n_in_gst_tax_type': tax_data['tax'].l10n_in_gst_tax_type,
|
||||
} if tax_data else None
|
||||
|
||||
base_lines_aggregated_values = self._aggregate_base_lines_tax_details(base_lines, grouping_function)
|
||||
values_per_grouping_key = self._aggregate_base_lines_aggregated_values(base_lines_aggregated_values)
|
||||
for grouping_key, values in values_per_grouping_key.items():
|
||||
if (
|
||||
not grouping_key
|
||||
or not grouping_key['l10n_in_hsn_code']
|
||||
or not grouping_key['l10n_in_gst_tax_type']
|
||||
):
|
||||
continue
|
||||
|
||||
key = frozendict({
|
||||
'l10n_in_hsn_code': grouping_key['l10n_in_hsn_code'],
|
||||
'rate': grouping_key['rate'],
|
||||
'uom_name': grouping_key['uom_name'],
|
||||
})
|
||||
item = items_map[key]
|
||||
l10n_in_gst_tax_type = grouping_key['l10n_in_gst_tax_type']
|
||||
item[f'tax_amount_{l10n_in_gst_tax_type}'] += values['tax_amount_currency']
|
||||
l10n_in_gst_tax_types.add(l10n_in_gst_tax_type)
|
||||
|
||||
return {
|
||||
'has_igst': 'igst' in l10n_in_gst_tax_types,
|
||||
'has_gst': bool({'cgst', 'sgst'} & l10n_in_gst_tax_types),
|
||||
'has_cess': 'cess' in l10n_in_gst_tax_types,
|
||||
'nb_columns': 5 + len(l10n_in_gst_tax_types),
|
||||
'display_uom': display_uom,
|
||||
'items': [
|
||||
key | values
|
||||
for key, values in items_map.items()
|
||||
],
|
||||
}
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
|
||||
|
||||
class AccountChartTemplate(models.Model):
|
||||
_inherit = 'account.chart.template'
|
||||
|
||||
def _prepare_all_journals(self, acc_template_ref, company, journals_dict=None):
|
||||
res = super(AccountChartTemplate, self)._prepare_all_journals(acc_template_ref, company, journals_dict=journals_dict)
|
||||
if self == self.env.ref('l10n_in.indian_chart_template_standard'):
|
||||
for journal in res:
|
||||
if journal.get('type') in ('sale','purchase'):
|
||||
journal['l10n_in_gstin_partner_id'] = company.partner_id.id
|
||||
return res
|
||||
|
||||
def _load(self, company):
|
||||
res = super(AccountChartTemplate, self)._load(company)
|
||||
if self == self.env.ref("l10n_in.indian_chart_template_standard"):
|
||||
company.write({
|
||||
'account_opening_date': fields.Date.context_today(self).replace(month=4, day=1),
|
||||
'fiscalyear_last_month': '3',
|
||||
})
|
||||
return res
|
||||
|
||||
|
||||
class AccountTaxTemplate(models.Model):
|
||||
_inherit = 'account.tax.template'
|
||||
|
||||
l10n_in_reverse_charge = fields.Boolean("Reverse charge", help="Tick this if this tax is reverse charge. Only for Indian accounting")
|
||||
|
||||
def _get_tax_vals(self, company, tax_template_to_tax):
|
||||
val = super(AccountTaxTemplate, self)._get_tax_vals(company, tax_template_to_tax)
|
||||
if self.tax_group_id:
|
||||
val['l10n_in_reverse_charge'] = self.l10n_in_reverse_charge
|
||||
return val
|
||||
195
odoo-bringout-oca-ocb-l10n_in/l10n_in/models/company.py
Normal file
195
odoo-bringout-oca-ocb-l10n_in/l10n_in/models/company.py
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
import pytz
|
||||
from stdnum.in_ import pan, gstin
|
||||
|
||||
from odoo import Command, _, api, fields, models
|
||||
from odoo.exceptions import RedirectWarning
|
||||
|
||||
|
||||
class ResCompany(models.Model):
|
||||
_inherit = 'res.company'
|
||||
|
||||
l10n_in_upi_id = fields.Char(string="UPI Id")
|
||||
l10n_in_hsn_code_digit = fields.Selection(
|
||||
selection=[
|
||||
("4", "4 Digits (turnover < 5 CR.)"),
|
||||
("6", "6 Digits (turnover > 5 CR.)"),
|
||||
("8", "8 Digits"),
|
||||
],
|
||||
string="HSN Code Digit",
|
||||
compute="_compute_l10n_in_hsn_code_digit",
|
||||
store=True,
|
||||
readonly=False,
|
||||
)
|
||||
l10n_in_edi_production_env = fields.Boolean(
|
||||
string="Indian Production Environment",
|
||||
help="Enable the use of production credentials",
|
||||
groups="base.group_system",
|
||||
default=True,
|
||||
)
|
||||
l10n_in_pan_entity_id = fields.Many2one(
|
||||
related="partner_id.l10n_in_pan_entity_id",
|
||||
string="PAN",
|
||||
store=True,
|
||||
readonly=False,
|
||||
help="PAN enables the department to link all transactions of the person with the department.\n"
|
||||
"These transactions include taxpayments, TDS/TCS credits, returns of income/wealth/gift/FBT,"
|
||||
"specified transactions, correspondence, and so on.\n"
|
||||
"Thus, PAN acts as an identifier for the person with the tax department.",
|
||||
)
|
||||
l10n_in_pan_type = fields.Selection(related="l10n_in_pan_entity_id.type", string="PAN Type")
|
||||
l10n_in_tan = fields.Char(related="partner_id.l10n_in_tan", string="TAN", readonly=False)
|
||||
l10n_in_gst_state_warning = fields.Char(related="partner_id.l10n_in_gst_state_warning")
|
||||
|
||||
# TDS/TCS settings
|
||||
l10n_in_tds_feature = fields.Boolean(
|
||||
string="TDS",
|
||||
compute="_compute_l10n_in_parent_based_features",
|
||||
inverse="_inverse_l10n_in_tds_feature",
|
||||
recursive=True,
|
||||
store=True,
|
||||
)
|
||||
l10n_in_tcs_feature = fields.Boolean(
|
||||
string="TCS",
|
||||
compute="_compute_l10n_in_parent_based_features",
|
||||
inverse="_inverse_l10n_in_tcs_feature",
|
||||
recursive=True,
|
||||
store=True,
|
||||
)
|
||||
l10n_in_withholding_account_id = fields.Many2one(
|
||||
comodel_name='account.account',
|
||||
string="TDS Account",
|
||||
check_company=True,
|
||||
)
|
||||
l10n_in_withholding_journal_id = fields.Many2one(
|
||||
comodel_name='account.journal',
|
||||
string="TDS Journal",
|
||||
check_company=True,
|
||||
)
|
||||
|
||||
# GST settings
|
||||
l10n_in_is_gst_registered = fields.Boolean(
|
||||
string="Registered Under GST",
|
||||
compute="_compute_l10n_in_parent_based_features",
|
||||
inverse="_inverse_l10n_in_is_gst_registered",
|
||||
recursive=True,
|
||||
store=True,
|
||||
)
|
||||
l10n_in_gstin_status_feature = fields.Boolean(string="Check GST Number Status")
|
||||
|
||||
def _inverse_l10n_in_tds_feature(self):
|
||||
for company in self:
|
||||
self._activate_l10n_in_taxes(['tds_group'], company, company.l10n_in_tds_feature)
|
||||
|
||||
def _inverse_l10n_in_tcs_feature(self):
|
||||
for company in self:
|
||||
self._activate_l10n_in_taxes(['tcs_group'], company, company.l10n_in_tcs_feature)
|
||||
|
||||
def _inverse_l10n_in_is_gst_registered(self):
|
||||
for company in self:
|
||||
gst_group_refs = [
|
||||
'sgst_group',
|
||||
'cgst_group',
|
||||
'igst_group',
|
||||
'cess_group',
|
||||
'gst_group',
|
||||
'exempt_group',
|
||||
'nil_rated_group',
|
||||
'non_gst_supplies_group',
|
||||
]
|
||||
if company.l10n_in_is_gst_registered:
|
||||
self._activate_l10n_in_taxes(gst_group_refs, company, True)
|
||||
# Set sale and purchase tax accounts when user registered under GST.
|
||||
company.account_sale_tax_id = self.env['account.chart.template'].with_company(company).ref('sgst_sale_5', raise_if_not_found=False)
|
||||
company.account_purchase_tax_id = self.env['account.chart.template'].with_company(company).ref('sgst_purchase_5', raise_if_not_found=False)
|
||||
else:
|
||||
self._activate_l10n_in_taxes(gst_group_refs, company, False)
|
||||
company.account_sale_tax_id = False
|
||||
company.account_purchase_tax_id = False
|
||||
|
||||
@api.depends('parent_id.l10n_in_tds_feature', 'parent_id.l10n_in_tcs_feature', 'parent_id.l10n_in_is_gst_registered')
|
||||
def _compute_l10n_in_parent_based_features(self):
|
||||
for company in self:
|
||||
if company.parent_id:
|
||||
company.l10n_in_tds_feature = company.parent_id.l10n_in_tds_feature
|
||||
company.l10n_in_tcs_feature = company.parent_id.l10n_in_tcs_feature
|
||||
company.l10n_in_is_gst_registered = company.parent_id.l10n_in_is_gst_registered
|
||||
|
||||
def _activate_l10n_in_taxes(self, group_refs, company, active=True):
|
||||
tax_group_ids = [
|
||||
tax_group.id
|
||||
for group_ref in group_refs
|
||||
if (tax_group := self.env['account.chart.template'].with_company(company).ref(group_ref, raise_if_not_found=False))
|
||||
]
|
||||
|
||||
if tax_group_ids:
|
||||
taxes = self.env['account.tax'].with_company(company).with_context(active_test=False).search([
|
||||
('tax_group_id', 'in', tax_group_ids),
|
||||
('active', '!=', active)
|
||||
])
|
||||
taxes.write({'active': active})
|
||||
|
||||
@api.depends('vat')
|
||||
def _compute_l10n_in_hsn_code_digit(self):
|
||||
for record in self:
|
||||
if record.country_code == "IN" and record.vat:
|
||||
record.l10n_in_hsn_code_digit = "4"
|
||||
else:
|
||||
record.l10n_in_hsn_code_digit = False
|
||||
|
||||
@api.onchange('vat')
|
||||
def onchange_vat(self):
|
||||
self.partner_id.onchange_vat()
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
res = super().create(vals_list)
|
||||
# Update Fiscal Positions for new branch
|
||||
res._update_l10n_in_fiscal_position()
|
||||
return res
|
||||
|
||||
def write(self, vals):
|
||||
res = super().write(vals)
|
||||
if vals.get('vat'):
|
||||
# Enable GST(l10n_in_is_gst_registered) when a valid GSTIN(vat) is applied.
|
||||
self._update_l10n_in_is_gst_registered()
|
||||
if (vals.get('state_id') or vals.get('country_id')) and not self.env.context.get('delay_account_group_sync'):
|
||||
# Update Fiscal Positions for companies setting up state for the first time
|
||||
self._update_l10n_in_fiscal_position()
|
||||
return res
|
||||
|
||||
def _update_l10n_in_fiscal_position(self):
|
||||
companies_need_update_fp = self.filtered(lambda c: c.parent_ids[0].chart_template == 'in')
|
||||
for company in companies_need_update_fp:
|
||||
ChartTemplate = self.env['account.chart.template'].with_company(company)
|
||||
fiscal_position_data = ChartTemplate._get_in_account_fiscal_position()
|
||||
for values in fiscal_position_data.values():
|
||||
values['tax_ids'] = [Command.set([
|
||||
xml_id
|
||||
for xml_id in values['tax_ids'][0][2]
|
||||
if ChartTemplate.ref(xml_id, raise_if_not_found=False)
|
||||
])]
|
||||
ChartTemplate._load_data({'account.fiscal.position': fiscal_position_data})
|
||||
|
||||
def _update_l10n_in_is_gst_registered(self):
|
||||
for company in self:
|
||||
if company.country_code == "IN" and company.vat:
|
||||
company.l10n_in_is_gst_registered = company.partner_id.check_vat_in(company.vat)
|
||||
|
||||
def action_update_state_as_per_gstin(self):
|
||||
self.ensure_one()
|
||||
self.partner_id.action_update_state_as_per_gstin()
|
||||
|
||||
def _check_tax_return_configuration(self):
|
||||
"""
|
||||
Check if the company is properly configured for tax returns.
|
||||
:raises RedirectWarning: if something is wrong configured.
|
||||
"""
|
||||
|
||||
if self.country_code != 'IN':
|
||||
return super()._check_tax_return_configuration()
|
||||
|
||||
is_l10n_in_reports_installed = 'l10n_in_reports' in self.env['ir.module.module']._installed()
|
||||
if not is_l10n_in_reports_installed:
|
||||
msg = _("First enable GST e-Filing feature from configuration for company %s.", (self.name))
|
||||
action = self.env.ref("account.action_account_config")
|
||||
raise RedirectWarning(msg, action.id, _('Go to configuration'))
|
||||
30
odoo-bringout-oca-ocb-l10n_in/l10n_in/models/iap_account.py
Normal file
30
odoo-bringout-oca-ocb-l10n_in/l10n_in/models/iap_account.py
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
from odoo import api, models
|
||||
from odoo.addons.iap import jsonrpc
|
||||
|
||||
DEFAULT_IAP_ENDPOINT = "https://l10n-in-edi.api.odoo.com"
|
||||
DEFAULT_IAP_TEST_ENDPOINT = "https://l10n-in-edi-demo.api.odoo.com"
|
||||
IAP_SERVICE_NAME = 'l10n_in_edi'
|
||||
TEST_GST_NUMBER = '24FANCY1234AAZA'
|
||||
|
||||
|
||||
class IapAccount(models.Model):
|
||||
_inherit = 'iap.account'
|
||||
|
||||
@api.model
|
||||
def _l10n_in_connect_to_server(self, is_production, params, url_path, config_parameter, timeout=25):
|
||||
IrConfigParam = self.env['ir.config_parameter'].sudo()
|
||||
user_token = self.get(IAP_SERVICE_NAME)
|
||||
params.update({
|
||||
"dbuuid": IrConfigParam.get_param("database.uuid"),
|
||||
"account_token": user_token.sudo().account_token,
|
||||
})
|
||||
gsp_provider = IrConfigParam.get_param("l10n_in.gsp_provider")
|
||||
if gsp_provider:
|
||||
params.update({"gsp_provider": gsp_provider})
|
||||
if params.get('gstin') == TEST_GST_NUMBER:
|
||||
default_endpoint = DEFAULT_IAP_TEST_ENDPOINT
|
||||
else:
|
||||
default_endpoint = DEFAULT_IAP_ENDPOINT if is_production else DEFAULT_IAP_TEST_ENDPOINT
|
||||
endpoint = IrConfigParam.get_param(config_parameter, default_endpoint)
|
||||
url = "%s%s" % (endpoint, url_path)
|
||||
return jsonrpc(url, params=params, timeout=timeout)
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
import base64
|
||||
|
||||
from stdnum.in_ import pan
|
||||
from odoo import api, fields, models, _
|
||||
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class L10nInPanEntity(models.Model):
|
||||
_name = 'l10n_in.pan.entity'
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||
_description = 'Indian PAN Entity'
|
||||
|
||||
name = fields.Char(string="PAN", tracking=1, required=True)
|
||||
type = fields.Selection([
|
||||
('a', 'Association of Persons'),
|
||||
('b', 'Body of Individuals'),
|
||||
('c', 'Company'),
|
||||
('f', 'Firms'),
|
||||
('g', 'Government'),
|
||||
('h', 'Hindu Undivided Family'),
|
||||
('j', 'Artificial Judicial Person'),
|
||||
('l', 'Local Authority'),
|
||||
('p', 'Individual'),
|
||||
('t', 'Association of Persons for a Trust'),
|
||||
('k', 'Krish (Trust Krish)'),
|
||||
], compute='_compute_type', readonly=True, store=True)
|
||||
partner_ids = fields.One2many(
|
||||
comodel_name='res.partner',
|
||||
inverse_name='l10n_in_pan_entity_id',
|
||||
string="Partners",
|
||||
domain="[('l10n_in_pan_entity_id', '=', False), '|', ('vat', '=', False), ('vat', 'like', name)]"
|
||||
)
|
||||
tds_deduction = fields.Selection([
|
||||
('normal', 'Normal'),
|
||||
('lower', 'Lower'),
|
||||
('higher', 'Higher'),
|
||||
('no', 'No'),
|
||||
], string="TDS Deduction", default='normal', tracking=2)
|
||||
tds_certificate = fields.Binary(string="TDS Certificate", copy=False)
|
||||
tds_certificate_filename = fields.Char(string="TDS Certificate Filename", copy=False)
|
||||
|
||||
# MSME/Udyam Registration details
|
||||
msme_type = fields.Selection([
|
||||
("micro", "Micro"),
|
||||
("small", "Small"),
|
||||
("medium", "Medium")
|
||||
], string="MSME/Udyam Registration Type", copy=False)
|
||||
msme_number = fields.Char(string="MSME/Udyam Registration Number", copy=False)
|
||||
|
||||
_name_uniq = models.Constraint(
|
||||
'unique (name)',
|
||||
'A PAN Entity with same PAN Number already exists.',
|
||||
)
|
||||
|
||||
@api.constrains('name')
|
||||
def _check_pan_name(self):
|
||||
if 'import_file' in self.env.context:
|
||||
return
|
||||
for record in self:
|
||||
if record.name and not pan.is_valid(record.name):
|
||||
raise ValidationError(_("The entered PAN %s seems invalid. Please enter a valid PAN.", record.name))
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
records = super().create(vals_list)
|
||||
for record in records:
|
||||
record.name = record.name.upper()
|
||||
return records
|
||||
|
||||
def write(self, vals):
|
||||
if vals.get('name'):
|
||||
vals['name'] = vals['name'].upper()
|
||||
res = super().write(vals)
|
||||
if vals.get('tds_certificate'):
|
||||
for rec in self:
|
||||
rec.message_post(
|
||||
body=_("TDS Certificate Added"),
|
||||
message_type='notification',
|
||||
subtype_xmlid='mail.mt_note',
|
||||
attachments=[(rec.tds_certificate_filename, base64.b64decode(vals['tds_certificate']))]
|
||||
)
|
||||
return res
|
||||
|
||||
@api.depends('name')
|
||||
def _compute_type(self):
|
||||
for record in self:
|
||||
if record.name:
|
||||
if pan.is_valid(record.name):
|
||||
record.type = record.name[3].lower()
|
||||
else:
|
||||
record.type = False
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
from odoo import models
|
||||
|
||||
|
||||
class AccountReport(models.Model):
|
||||
_inherit = 'account.report'
|
||||
|
||||
def _init_options_buttons(self, options, previous_options):
|
||||
super()._init_options_buttons(options, previous_options)
|
||||
company = self.env.company
|
||||
generic_report_id = self.env.ref('account.generic_tax_report').id
|
||||
|
||||
# Remove 'Returns' button from generic report for indian company
|
||||
if company.country_id.code == 'IN' and self.id == generic_report_id and not self.root_report_id:
|
||||
options['buttons'] = [
|
||||
button for button in options['buttons']
|
||||
if button.get('action') != 'action_open_returns'
|
||||
]
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
from odoo import _, api, fields, models
|
||||
|
||||
|
||||
class L10n_InSectionAlert(models.Model):
|
||||
_name = 'l10n_in.section.alert'
|
||||
_description = "indian section alert"
|
||||
|
||||
name = fields.Char("Section Name")
|
||||
tax_source_type = fields.Selection([
|
||||
('tds', 'TDS'),
|
||||
('tcs', 'TCS'),
|
||||
], string="Tax Source Type")
|
||||
consider_amount = fields.Selection([
|
||||
('untaxed_amount', 'Untaxed Amount'),
|
||||
('total_amount', 'Total Amount'),
|
||||
], string="Consider", default='untaxed_amount', required=True)
|
||||
is_per_transaction_limit = fields.Boolean("Per Transaction")
|
||||
per_transaction_limit = fields.Float("Per Transaction limit")
|
||||
is_aggregate_limit = fields.Boolean("Aggregate")
|
||||
aggregate_limit = fields.Float("Aggregate limit")
|
||||
aggregate_period = fields.Selection([
|
||||
('monthly', 'Monthly'),
|
||||
('fiscal_yearly', 'Financial Yearly'),
|
||||
], string="Aggregate Period", default='fiscal_yearly')
|
||||
l10n_in_section_tax_ids = fields.One2many("account.tax", "l10n_in_section_id", string="Taxes")
|
||||
tax_report_line_id = fields.Many2one(string="Tax Report Line", comodel_name='account.report.line')
|
||||
|
||||
_per_transaction_limit = models.Constraint(
|
||||
'CHECK(per_transaction_limit >= 0)',
|
||||
'Per transaction limit must be positive',
|
||||
)
|
||||
_aggregate_limit = models.Constraint(
|
||||
'CHECK(aggregate_limit >= 0)',
|
||||
'Aggregate limit must be positive',
|
||||
)
|
||||
|
||||
@api.depends('tax_source_type')
|
||||
def _compute_display_name(self):
|
||||
for record in self:
|
||||
record.display_name = f"{record.tax_source_type.upper()} {record.name or ''}" if record.tax_source_type else f"{record.name or ''}"
|
||||
|
||||
def _get_warning_message(self):
|
||||
warning = ", ".join(self.mapped('name'))
|
||||
section_type = next(iter(set(self.mapped('tax_source_type')))).upper()
|
||||
action = _('collect') if section_type == 'TCS' else _('deduct')
|
||||
return _("It's advisable to %(action)s %(section_type)s u/s %(warning)s on this transaction.",
|
||||
action=action,
|
||||
section_type=section_type,
|
||||
warning=warning
|
||||
)
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from markupsafe import Markup
|
||||
|
||||
from odoo import fields, api, models, _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class Message(models.Model):
|
||||
_inherit = 'mail.message'
|
||||
|
||||
l10n_in_audit_log_preview = fields.Html(string="Description", compute="_compute_l10n_in_audit_log_preview")
|
||||
l10n_in_audit_log_account_move_id = fields.Many2one('account.move', string="Journal Entry", compute="_compute_l10n_in_audit_log_document_name", search="_search_l10n_in_audit_log_document_name")
|
||||
|
||||
@api.depends('body', 'subject', 'tracking_value_ids', 'subtype_id')
|
||||
def _compute_l10n_in_audit_log_preview(self):
|
||||
for message in self:
|
||||
title = message.subject or message.preview
|
||||
tracking_value_ids = message.sudo().tracking_value_ids
|
||||
if not title and tracking_value_ids:
|
||||
title = _("Updated")
|
||||
elif not title and message.subtype_id and not message.subtype_id.internal:
|
||||
title = message.subtype_id.display_name
|
||||
audit_log_preview = Markup("<div>%s</div>") % (title)
|
||||
for value in tracking_value_ids:
|
||||
audit_log_preview += Markup(
|
||||
"<li>%(old_value)s <i class='o_TrackingValue_separator fa fa-long-arrow-right mx-1 text-600' title='%(title)s' role='img' aria-label='%(title)s'></i>%(new_value)s (%(field)s)</li>"
|
||||
) % {
|
||||
'old_value': value._get_old_display_value()[0] or _("None"),
|
||||
'new_value': value._get_new_display_value()[0] or _("None"),
|
||||
'title': _("Changed"),
|
||||
'field': value.field.field_description,
|
||||
}
|
||||
message.l10n_in_audit_log_preview = audit_log_preview
|
||||
|
||||
@api.depends('model', 'res_id')
|
||||
def _compute_l10n_in_audit_log_document_name(self):
|
||||
messages_of_account_move = self.filtered(lambda m: m.model == 'account.move' and m.res_id)
|
||||
(self - messages_of_account_move).l10n_in_audit_log_account_move_id = False
|
||||
moves = self.env['account.move'].search([('id', 'in', messages_of_account_move.mapped('res_id'))])
|
||||
moves_by_id = {m.id: m for m in moves}
|
||||
for message in messages_of_account_move:
|
||||
message.l10n_in_audit_log_account_move_id = moves_by_id.get(message.res_id, False)
|
||||
|
||||
def _search_l10n_in_audit_log_document_name(self, operator, value):
|
||||
is_set = False
|
||||
if operator == '!=' and isinstance(value, bool):
|
||||
is_set = True
|
||||
elif operator not in ['=', 'ilike'] or not isinstance(value, str):
|
||||
raise UserError(_('Operation not supported'))
|
||||
move_domain = [('company_id.account_fiscal_country_id.code', '=', 'IN')]
|
||||
if not is_set:
|
||||
move_domain += [('name', operator, value)]
|
||||
move_query = self.env['account.move']._search(move_domain)
|
||||
return [('model', '=', 'account.move'), ('res_id', 'in', move_query)]
|
||||
|
|
@ -1,10 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class L10nInPortCode(models.Model):
|
||||
class L10n_InPortCode(models.Model):
|
||||
"""Port code must be mentioned in export and import of goods under GST."""
|
||||
_name = 'l10n_in.port.code'
|
||||
_description = "Indian port code"
|
||||
|
|
@ -14,6 +11,7 @@ class L10nInPortCode(models.Model):
|
|||
name = fields.Char(string="Port", required=True)
|
||||
state_id = fields.Many2one('res.country.state', string="State")
|
||||
|
||||
_sql_constraints = [
|
||||
('code_uniq', 'unique (code)', 'The Port Code must be unique!')
|
||||
]
|
||||
_code_uniq = models.Constraint(
|
||||
'unique (code)',
|
||||
'The Port Code must be unique!',
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,11 +1,41 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models, fields
|
||||
import re
|
||||
from odoo import _, api, models, fields
|
||||
|
||||
|
||||
class ProductTemplate(models.Model):
|
||||
_inherit = 'product.template'
|
||||
|
||||
l10n_in_hsn_code = fields.Char(string="HSN/SAC Code", help="Harmonized System Nomenclature/Services Accounting Code")
|
||||
l10n_in_hsn_description = fields.Char(string="HSN/SAC Description", help="HSN/SAC description is required if HSN/SAC code is not provided.")
|
||||
l10n_in_hsn_warning = fields.Text(string="HSC/SAC warning", compute="_compute_l10n_in_hsn_warning")
|
||||
l10n_in_is_gst_registered_enabled = fields.Boolean(compute="_compute_l10n_in_is_gst_registered_enabled")
|
||||
|
||||
@api.depends('company_id.l10n_in_is_gst_registered')
|
||||
@api.depends_context('allowed_company_ids')
|
||||
def _compute_l10n_in_is_gst_registered_enabled(self):
|
||||
for record in self:
|
||||
allowed_companies = record.company_id or self.env.companies
|
||||
record.l10n_in_is_gst_registered_enabled = any(
|
||||
company.l10n_in_is_gst_registered
|
||||
for company in allowed_companies
|
||||
)
|
||||
|
||||
@api.depends('sale_ok', 'l10n_in_hsn_code')
|
||||
def _compute_l10n_in_hsn_warning(self):
|
||||
digit_suffixes = {
|
||||
'4': _("either 4, 6 or 8"),
|
||||
'6': _("either 6 or 8"),
|
||||
'8': _("8")
|
||||
}
|
||||
active_hsn_code_digit_len = max(
|
||||
int(company.l10n_in_hsn_code_digit)
|
||||
for company in self.env.companies
|
||||
)
|
||||
for record in self:
|
||||
check_hsn = record.sale_ok and record.l10n_in_hsn_code and active_hsn_code_digit_len
|
||||
if check_hsn and (not re.match(r'^\d{4}$|^\d{6}$|^\d{8}$', record.l10n_in_hsn_code) or len(record.l10n_in_hsn_code) < active_hsn_code_digit_len):
|
||||
record.l10n_in_hsn_warning = _(
|
||||
"HSN code field must consist solely of digits and be %s in length.",
|
||||
digit_suffixes.get(str(active_hsn_code_digit_len))
|
||||
)
|
||||
continue
|
||||
record.l10n_in_hsn_warning = False
|
||||
|
|
|
|||
|
|
@ -1,10 +1,190 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from odoo import _, fields, models
|
||||
from odoo.exceptions import UserError, ValidationError, RedirectWarning
|
||||
from odoo.tools.sql import column_exists, create_column
|
||||
from odoo.tools import SQL
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo.addons.l10n_in.models.iap_account import IAP_SERVICE_NAME
|
||||
|
||||
|
||||
class ResConfigSettings(models.TransientModel):
|
||||
_inherit = 'res.config.settings'
|
||||
|
||||
group_l10n_in_reseller = fields.Boolean(implied_group='l10n_in.group_l10n_in_reseller', string="Manage Reseller(E-Commerce)")
|
||||
group_l10n_in_reseller = fields.Boolean(
|
||||
implied_group='l10n_in.group_l10n_in_reseller',
|
||||
string="Manage Reseller(E-Commerce)"
|
||||
)
|
||||
l10n_in_edi_production_env = fields.Boolean(
|
||||
string="Indian Production Environment",
|
||||
related="company_id.l10n_in_edi_production_env",
|
||||
readonly=False
|
||||
)
|
||||
l10n_in_gsp = fields.Selection(selection=[
|
||||
('bvm', 'BVM IT Consulting'),
|
||||
('tera', 'Tera Software (Deprecated)'),
|
||||
], string="GSP",
|
||||
inverse="_set_l10n_in_gsp", # use an inverse method to invalidate existing tokens if the GSP is changed
|
||||
store=False,
|
||||
help="Select the GST Suvidha Provider (GSP) you want to use for GST services.",
|
||||
)
|
||||
l10n_in_hsn_code_digit = fields.Selection(
|
||||
related='company_id.l10n_in_hsn_code_digit',
|
||||
readonly=False
|
||||
)
|
||||
|
||||
# TDS/TCS settings
|
||||
l10n_in_tds_feature = fields.Boolean(
|
||||
related='company_id.l10n_in_tds_feature',
|
||||
readonly=False
|
||||
)
|
||||
l10n_in_tcs_feature = fields.Boolean(
|
||||
related='company_id.l10n_in_tcs_feature',
|
||||
readonly=False
|
||||
)
|
||||
l10n_in_withholding_account_id = fields.Many2one(
|
||||
related='company_id.l10n_in_withholding_account_id',
|
||||
readonly=False
|
||||
)
|
||||
l10n_in_withholding_journal_id = fields.Many2one(
|
||||
related='company_id.l10n_in_withholding_journal_id',
|
||||
readonly=False
|
||||
)
|
||||
l10n_in_tan = fields.Char(
|
||||
related='company_id.l10n_in_tan',
|
||||
readonly=False
|
||||
)
|
||||
|
||||
# GST settings
|
||||
l10n_in_is_gst_registered = fields.Boolean(
|
||||
related='company_id.l10n_in_is_gst_registered',
|
||||
readonly=False
|
||||
)
|
||||
l10n_in_gstin = fields.Char(
|
||||
string="GST Number",
|
||||
related='company_id.vat',
|
||||
readonly=False
|
||||
)
|
||||
l10n_in_gstin_status_feature = fields.Boolean(
|
||||
related='company_id.l10n_in_gstin_status_feature',
|
||||
readonly=False
|
||||
)
|
||||
l10n_in_gst_efiling_feature = fields.Boolean(string="GST E-Filing & Matching Feature")
|
||||
l10n_in_fetch_vendor_edi_feature = fields.Boolean(string="Fetch Vendor E-Invoiced Document")
|
||||
l10n_in_enet_vendor_batch_payment_feature = fields.Boolean(string="ENet Vendor Batch Payment")
|
||||
|
||||
module_l10n_in_reports = fields.Boolean("GST E-Filing & Matching")
|
||||
module_l10n_in_edi = fields.Boolean("Indian Electronic Invoicing")
|
||||
module_l10n_in_ewaybill = fields.Boolean("Indian Electronic Waybill")
|
||||
|
||||
def set_values(self):
|
||||
super().set_values()
|
||||
if self.country_code == 'IN':
|
||||
if (
|
||||
not self.module_l10n_in_reports
|
||||
and (
|
||||
self.l10n_in_fetch_vendor_edi_feature
|
||||
or self.l10n_in_gst_efiling_feature
|
||||
or self.l10n_in_enet_vendor_batch_payment_feature
|
||||
)
|
||||
):
|
||||
self.module_l10n_in_reports = True
|
||||
for l10n_in_feature in (
|
||||
"l10n_in_fetch_vendor_edi_feature",
|
||||
"l10n_in_gst_efiling_feature",
|
||||
"l10n_in_enet_vendor_batch_payment_feature",
|
||||
):
|
||||
if self[l10n_in_feature]:
|
||||
self._update_l10n_in_feature(l10n_in_feature)
|
||||
if self.module_l10n_in_edi:
|
||||
self._update_l10n_in_feature("l10n_in_edi_feature")
|
||||
if self.module_l10n_in_ewaybill:
|
||||
self._update_l10n_in_feature("l10n_in_ewaybill_feature")
|
||||
|
||||
def _update_l10n_in_feature(self, column):
|
||||
""" This way, after installing the module, the field will already be set for the active company. """
|
||||
if not column_exists(self.env.cr, "res_company", column):
|
||||
create_column(self.env.cr, "res_company", column, "boolean")
|
||||
self.env.cr.execute(SQL(
|
||||
f"""
|
||||
UPDATE res_company
|
||||
SET {column} = true
|
||||
WHERE id = {self.env.company.id}
|
||||
"""
|
||||
))
|
||||
|
||||
def l10n_in_edi_buy_iap(self):
|
||||
if (
|
||||
not self.l10n_in_edi_production_env
|
||||
or not (
|
||||
self.module_l10n_in_edi
|
||||
or self.module_l10n_in_ewaybill
|
||||
or self.l10n_in_gstin_status_feature
|
||||
or self.l10n_in_gst_efiling_feature
|
||||
)
|
||||
):
|
||||
raise ValidationError(_(
|
||||
"Please ensure that at least one Indian service and production environment is enabled,"
|
||||
" and save the configuration to proceed with purchasing credits."
|
||||
))
|
||||
return {
|
||||
'type': 'ir.actions.act_url',
|
||||
'url': self.env["iap.account"].get_credits_url(service_name=IAP_SERVICE_NAME),
|
||||
'target': '_new'
|
||||
}
|
||||
|
||||
def _l10n_in_check_gst_number(self):
|
||||
company = self.company_id
|
||||
if not company.partner_id.check_vat_in(company.vat):
|
||||
action = {
|
||||
'view_mode': 'form',
|
||||
'res_model': 'res.company',
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_id': company.id,
|
||||
'views': [[self.env.ref('base.view_company_form').id, 'form']],
|
||||
}
|
||||
raise RedirectWarning(_("Please set a valid GST number on company."), action, _("Go to Company"))
|
||||
|
||||
def reload_template(self):
|
||||
super().reload_template()
|
||||
if self.country_code == 'IN':
|
||||
branch_companies = self.company_id.child_ids
|
||||
if branch_companies:
|
||||
branch_companies._update_l10n_in_fiscal_position()
|
||||
|
||||
def _l10n_in_is_first_time_setup(self):
|
||||
"""
|
||||
Check if at least one company for India has been configured with the localization settings.
|
||||
If not, it means it's the first time setup.
|
||||
"""
|
||||
all_validity_fields = ['l10n_in_gstr_gst_token_validity', 'l10n_in_edi_token_validity', 'l10n_in_ewaybill_auth_validity']
|
||||
validity_fields = (field_name for field_name in self.company_id._fields if field_name in all_validity_fields)
|
||||
if validity_fields:
|
||||
validity_fields_domain = fields.Domain.OR([[(field_name, '!=', False)] for field_name in validity_fields])
|
||||
configured_company_count = self.env['res.company'].sudo().search_count([
|
||||
('account_fiscal_country_id.code', '=', 'IN'),
|
||||
*validity_fields_domain
|
||||
])
|
||||
return not configured_company_count
|
||||
return True
|
||||
|
||||
def get_values(self):
|
||||
res = super().get_values()
|
||||
res['l10n_in_gsp'] = self.env['ir.config_parameter'].sudo().get_param('l10n_in.gsp_provider')
|
||||
if not res['l10n_in_gsp']:
|
||||
if self._l10n_in_is_first_time_setup():
|
||||
# Default to BVM for new databases setting up India localization for the first time
|
||||
res['l10n_in_gsp'] = 'bvm'
|
||||
else:
|
||||
res['l10n_in_gsp'] = 'tera'
|
||||
return res
|
||||
|
||||
def _l10n_in_gsp_provider_changed(self):
|
||||
""" Hook to be overridden in other modules to handle GSP provider change. """
|
||||
self.ensure_one()
|
||||
self.env['ir.config_parameter'].sudo().set_param('l10n_in.gsp_provider', self.l10n_in_gsp)
|
||||
|
||||
def _set_l10n_in_gsp(self):
|
||||
gsp_before = self.env['ir.config_parameter'].sudo().get_param('l10n_in.gsp_provider')
|
||||
for config in self:
|
||||
if gsp_before != config.l10n_in_gsp:
|
||||
config._l10n_in_gsp_provider_changed()
|
||||
return
|
||||
|
|
|
|||
|
|
@ -1,10 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class CountryState(models.Model):
|
||||
class ResCountryState(models.Model):
|
||||
_inherit = 'res.country.state'
|
||||
|
||||
l10n_in_tin = fields.Char('TIN Number', size=2, help="TIN number-first two digits")
|
||||
l10n_in_tin = fields.Char("TIN Number", size=2, help="TIN number-first two digits")
|
||||
|
|
|
|||
|
|
@ -1,9 +1,17 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
import logging
|
||||
import re
|
||||
from stdnum.in_ import pan
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError, AccessError, ValidationError
|
||||
from odoo.addons.l10n_in.models.iap_account import IAP_SERVICE_NAME
|
||||
from odoo.tools.misc import clean_context
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
TEST_GST_NUMBER = "36AABCT1332L011"
|
||||
TEST_GST_NUMBER_BVM = "29AAGCB1286Q000"
|
||||
|
||||
|
||||
class ResPartner(models.Model):
|
||||
_inherit = 'res.partner'
|
||||
|
|
@ -19,33 +27,206 @@ class ResPartner(models.Model):
|
|||
('uin_holders', 'UIN Holders'),
|
||||
], string="GST Treatment")
|
||||
|
||||
@api.onchange('company_type')
|
||||
def onchange_company_type(self):
|
||||
res = super().onchange_company_type()
|
||||
if self.country_id and self.country_id.code == 'IN':
|
||||
self.l10n_in_gst_treatment = (self.company_type == 'company') and 'regular' or 'consumer'
|
||||
l10n_in_pan_entity_id = fields.Many2one(
|
||||
comodel_name='l10n_in.pan.entity',
|
||||
string="PAN",
|
||||
ondelete='restrict',
|
||||
help="PAN enables the department to link all transactions of the person with the department.\n"
|
||||
"These transactions include taxpayments, TDS/TCS credits, returns of income/wealth/gift/FBT,"
|
||||
" specified transactions, correspondence, and so on.\n"
|
||||
"Thus, PAN acts as an identifier for the person with the tax department."
|
||||
)
|
||||
l10n_in_tan = fields.Char("TAN")
|
||||
|
||||
display_pan_warning = fields.Boolean(string="Display pan warning", compute="_compute_display_pan_warning")
|
||||
l10n_in_gst_state_warning = fields.Char(compute="_compute_l10n_in_gst_state_warning")
|
||||
l10n_in_is_gst_registered_enabled = fields.Boolean(compute="_compute_l10n_in_gst_registered_and_status")
|
||||
|
||||
# gstin_status related field
|
||||
l10n_in_gstin_verified_status = fields.Boolean(string="GST Status", tracking=True)
|
||||
l10n_in_gstin_verified_date = fields.Date(string="GSTIN Verified Date", tracking=True)
|
||||
l10n_in_gstin_status_feature_enabled = fields.Boolean(compute="_compute_l10n_in_gst_registered_and_status")
|
||||
|
||||
@api.depends('vat', 'state_id', 'country_id', 'fiscal_country_codes')
|
||||
def _compute_l10n_in_gst_state_warning(self):
|
||||
for partner in self:
|
||||
if (
|
||||
"IN" in partner.fiscal_country_codes
|
||||
and partner.check_vat_in(partner.vat)
|
||||
):
|
||||
if partner.vat[:2] == "99":
|
||||
partner.l10n_in_gst_state_warning = _(
|
||||
"As per GSTN the country should be other than India, so it's recommended to"
|
||||
)
|
||||
else:
|
||||
state_id = self.env['res.country.state'].search([('l10n_in_tin', '=', partner.vat[:2])], limit=1)
|
||||
if state_id and state_id != partner.state_id:
|
||||
partner.l10n_in_gst_state_warning = _(
|
||||
"As per GSTN the state should be %s, so it's recommended to", state_id.name
|
||||
)
|
||||
else:
|
||||
partner.l10n_in_gst_state_warning = False
|
||||
else:
|
||||
partner.l10n_in_gst_state_warning = False
|
||||
|
||||
@api.depends('l10n_in_pan_entity_id')
|
||||
def _compute_display_pan_warning(self):
|
||||
for partner in self:
|
||||
partner.display_pan_warning = partner.vat and partner.l10n_in_pan_entity_id and partner.l10n_in_pan_entity_id.name != partner.vat[2:12]
|
||||
|
||||
@api.depends('company_id.l10n_in_is_gst_registered', 'company_id.l10n_in_gstin_status_feature')
|
||||
def _compute_l10n_in_gst_registered_and_status(self):
|
||||
for record in self:
|
||||
company = record.company_id or self.env.company
|
||||
record.l10n_in_is_gst_registered_enabled = company.l10n_in_is_gst_registered
|
||||
record.l10n_in_gstin_status_feature_enabled = company.l10n_in_gstin_status_feature
|
||||
|
||||
@api.onchange('vat')
|
||||
def _onchange_l10n_in_gst_status(self):
|
||||
"""
|
||||
Reset GST Status Whenever the `vat` of partner changes
|
||||
"""
|
||||
for partner in self:
|
||||
if partner.country_code == 'IN' and (partner.l10n_in_gstin_verified_status or partner.l10n_in_gstin_verified_date):
|
||||
partner.l10n_in_gstin_verified_status = False
|
||||
partner.l10n_in_gstin_verified_date = False
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
res = super().create(vals_list)
|
||||
if 'import_file' in self.env.context:
|
||||
return res
|
||||
for partner in res.filtered(lambda p: p.country_code == 'IN' and p.vat and p.check_vat_in(p.vat)):
|
||||
partner._set_l10n_in_pan_tan_from_vat()
|
||||
return res
|
||||
|
||||
@api.onchange('country_id')
|
||||
def _onchange_country_id(self):
|
||||
res = super()._onchange_country_id()
|
||||
if self.country_id and self.country_id.code != 'IN':
|
||||
self.l10n_in_gst_treatment = 'overseas'
|
||||
elif self.country_id and self.country_id.code == 'IN':
|
||||
self.l10n_in_gst_treatment = (self.company_type == 'company') and 'regular' or 'consumer'
|
||||
def write(self, vals):
|
||||
res = super().write(vals)
|
||||
if 'import_file' in self.env.context:
|
||||
return res
|
||||
if vals.get('vat') or vals.get('country_id'):
|
||||
for partner in self.filtered(lambda p: p.country_code == 'IN' and p.vat and p.check_vat_in(p.vat)):
|
||||
partner._set_l10n_in_pan_tan_from_vat()
|
||||
return res
|
||||
|
||||
def _set_l10n_in_pan_tan_from_vat(self):
|
||||
self.ensure_one()
|
||||
identifier = self.vat[2:12].upper()
|
||||
if pan.is_valid(identifier):
|
||||
self.l10n_in_pan_entity_id = self._l10n_in_search_create_pan_entity_from_vat(self.vat).id
|
||||
elif re.match(r'^[A-Z]{4}[0-9]{5}[A-Z]{1}$', identifier):
|
||||
self.l10n_in_tan = identifier
|
||||
|
||||
def _l10n_in_search_create_pan_entity_from_vat(self, vat):
|
||||
pan_number = vat[2:12].upper()
|
||||
pan_entity = self.env['l10n_in.pan.entity'].search([('name', '=', pan_number)], limit=1)
|
||||
if not pan_entity:
|
||||
context = clean_context(self.env.context)
|
||||
pan_entity = self.env['l10n_in.pan.entity'].with_context(context).create({'name': pan_number})
|
||||
return pan_entity
|
||||
|
||||
def action_l10n_in_verify_gstin_status(self):
|
||||
self.ensure_one()
|
||||
self.check_access('write')
|
||||
if self.env.company.sudo().account_fiscal_country_id.code != 'IN':
|
||||
raise UserError(_('You must be logged in an Indian company to use this feature'))
|
||||
if not self.vat:
|
||||
raise ValidationError(_("Please enter the GSTIN"))
|
||||
if not self.env.company.l10n_in_gstin_status_feature:
|
||||
raise ValidationError(_("This feature is not activated. Go to Settings to activate this feature."))
|
||||
is_production = self.env.company.sudo().l10n_in_edi_production_env
|
||||
params = {
|
||||
"gstin_to_search": self.vat,
|
||||
"gstin": self.env.company.vat,
|
||||
}
|
||||
try:
|
||||
response = self.env['iap.account']._l10n_in_connect_to_server(
|
||||
is_production,
|
||||
params,
|
||||
'/iap/l10n_in_reports/1/public/search',
|
||||
"l10n_in.endpoint"
|
||||
)
|
||||
except AccessError:
|
||||
raise UserError(_("Unable to connect with GST network"))
|
||||
if response.get('error') and any(e.get('code') == 'no-credit' for e in response['error']):
|
||||
return self.env["bus.bus"]._sendone(self.env.user.partner_id, "iap_notification",
|
||||
{
|
||||
"type": "no_credit",
|
||||
"title": _("Not enough credits to check GSTIN status"),
|
||||
"get_credits_url": self.env["iap.account"].get_credits_url(service_name=IAP_SERVICE_NAME),
|
||||
},
|
||||
)
|
||||
gst_status = response.get('data', {}).get('sts', "")
|
||||
if gst_status.casefold() == 'active':
|
||||
l10n_in_gstin_verified_status = True
|
||||
elif gst_status:
|
||||
l10n_in_gstin_verified_status = False
|
||||
date_from = response.get("data", {}).get("cxdt", '')
|
||||
if date_from and re.search(r'\d', date_from):
|
||||
message = _(
|
||||
"GSTIN %(vat)s is %(status)s and Effective from %(date_from)s.",
|
||||
vat=self.vat,
|
||||
status=gst_status,
|
||||
date_from=date_from,
|
||||
)
|
||||
else:
|
||||
message = _(
|
||||
"GSTIN %(vat)s is %(status)s, effective date is not available.",
|
||||
vat=self.vat,
|
||||
status=gst_status
|
||||
)
|
||||
if not is_production:
|
||||
message += _(" Warning: You are currently in a test environment. The result is a dummy.")
|
||||
self.message_post(body=message)
|
||||
else:
|
||||
_logger.info("GST status check error %s", response)
|
||||
if response.get('error') and any(e.get('code') == 'SWEB_9035' for e in response['error']):
|
||||
raise UserError(
|
||||
_("The provided GSTIN is invalid. Please check the GSTIN and try again.")
|
||||
)
|
||||
default_error_message = _(
|
||||
"Something went wrong while fetching the GST status."
|
||||
"Please Contact Support if the error persists with"
|
||||
"Response: %(response)s",
|
||||
response=response
|
||||
)
|
||||
error_messages = [
|
||||
f"[{error.get('code') or _('Unknown')}] {error.get('message') or default_error_message}"
|
||||
for error in response.get('error')
|
||||
]
|
||||
raise UserError(
|
||||
error_messages
|
||||
and '\n'.join(error_messages)
|
||||
or default_error_message
|
||||
)
|
||||
self.write({
|
||||
"l10n_in_gstin_verified_status": l10n_in_gstin_verified_status,
|
||||
"l10n_in_gstin_verified_date": fields.Date.today(),
|
||||
})
|
||||
return {
|
||||
"type": "ir.actions.client",
|
||||
"tag": "display_notification",
|
||||
"params": {
|
||||
"type": "info",
|
||||
"message": _("GSTIN Status Updated Successfully"),
|
||||
"next": {"type": "ir.actions.act_window_close"},
|
||||
},
|
||||
}
|
||||
|
||||
@api.onchange('vat')
|
||||
def onchange_vat(self):
|
||||
if self.vat and self.check_vat_in(self.vat):
|
||||
self.vat = self.vat.upper()
|
||||
state_id = self.env['res.country.state'].search([('l10n_in_tin', '=', self.vat[:2])], limit=1)
|
||||
if state_id:
|
||||
self.state_id = state_id
|
||||
pan_entity = self.env['l10n_in.pan.entity'].search([('name', '=', self.vat[2:12])], limit=1)
|
||||
if pan_entity:
|
||||
self.l10n_in_pan_entity_id = pan_entity.id
|
||||
|
||||
@api.model
|
||||
def _commercial_fields(self):
|
||||
res = super()._commercial_fields()
|
||||
return res + ['l10n_in_gst_treatment']
|
||||
return super()._commercial_fields() + ['l10n_in_gst_treatment', 'l10n_in_pan_entity_id', 'l10n_in_tan']
|
||||
|
||||
def check_vat_in(self, vat):
|
||||
"""
|
||||
|
|
@ -53,6 +234,28 @@ class ResPartner(models.Model):
|
|||
but this is not a valid number as per the regular expression
|
||||
so TEST_GST_NUMBER is considered always valid
|
||||
"""
|
||||
if vat == TEST_GST_NUMBER:
|
||||
if vat in (TEST_GST_NUMBER, TEST_GST_NUMBER_BVM):
|
||||
return True
|
||||
return super().check_vat_in(vat)
|
||||
|
||||
@api.model
|
||||
def _l10n_in_get_partner_vals_by_vat(self, vat):
|
||||
partner_data = self.enrich_by_gst(vat)
|
||||
for fname in list(partner_data.keys()):
|
||||
if fname not in self.env['res.partner']._fields:
|
||||
partner_data.pop(fname, None)
|
||||
partner_data.update({
|
||||
'country_id': partner_data.get('country_id', {}).get('id'),
|
||||
'state_id': partner_data.get('state_id', {}).get('id'),
|
||||
'company_type': 'company',
|
||||
'l10n_in_gst_treatment': partner_data.get('l10n_in_gst_treatment', 'regular'),
|
||||
})
|
||||
return partner_data
|
||||
|
||||
def action_update_state_as_per_gstin(self):
|
||||
self.ensure_one()
|
||||
if self.check_vat_in(self.vat):
|
||||
state_id = self.env['res.country.state'].search([('l10n_in_tin', '=', self.vat[:2])], limit=1)
|
||||
self.state_id = state_id
|
||||
if self.ref_company_ids:
|
||||
self.ref_company_ids._update_l10n_in_fiscal_position()
|
||||
|
|
|
|||
143
odoo-bringout-oca-ocb-l10n_in/l10n_in/models/template_in.py
Normal file
143
odoo-bringout-oca-ocb-l10n_in/l10n_in/models/template_in.py
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
from odoo import Command, models
|
||||
from odoo.addons.account.models.chart_template import template
|
||||
|
||||
|
||||
class AccountChartTemplate(models.AbstractModel):
|
||||
_inherit = 'account.chart.template'
|
||||
|
||||
@template('in')
|
||||
def _get_in_template_data(self):
|
||||
return {
|
||||
'property_account_receivable_id': 'p10040',
|
||||
'property_account_payable_id': 'p11211',
|
||||
'code_digits': '6',
|
||||
'display_invoice_amount_total_words': True,
|
||||
}
|
||||
|
||||
@template('in', 'res.company')
|
||||
def _get_in_res_company(self):
|
||||
return {
|
||||
self.env.company.id: {
|
||||
'account_fiscal_country_id': 'base.in',
|
||||
'bank_account_code_prefix': '1002',
|
||||
'cash_account_code_prefix': '1001',
|
||||
'transfer_account_code_prefix': '1008',
|
||||
'account_default_pos_receivable_account_id': 'p10041',
|
||||
'income_currency_exchange_account_id': 'p2013',
|
||||
'expense_currency_exchange_account_id': 'p2117',
|
||||
'account_journal_early_pay_discount_loss_account_id': 'p2132',
|
||||
'account_journal_early_pay_discount_gain_account_id': '2012',
|
||||
'fiscalyear_last_month': '3',
|
||||
'account_sale_tax_id': 'sgst_sale_5',
|
||||
'account_purchase_tax_id': 'sgst_purchase_5',
|
||||
'deferred_expense_account_id': 'p10084',
|
||||
'deferred_revenue_account_id': 'p10085',
|
||||
'expense_account_id': 'p2107',
|
||||
'income_account_id': 'p20011',
|
||||
'l10n_in_withholding_account_id': 'p100595',
|
||||
'tax_calculation_rounding_method': 'round_per_line',
|
||||
},
|
||||
}
|
||||
|
||||
@template('in', 'account.cash.rounding')
|
||||
def _get_in_account_cash_rounding(self):
|
||||
return {
|
||||
'l10n_in.cash_rounding_in_half_up': {
|
||||
'profit_account_id': 'p213202',
|
||||
'loss_account_id': 'p213201',
|
||||
}
|
||||
}
|
||||
|
||||
@template('in', 'account.fiscal.position')
|
||||
def _get_in_account_fiscal_position(self):
|
||||
_ = self.env._
|
||||
company = self.env.company
|
||||
state_ids = [Command.set(company.state_id.ids)] if company.state_id else False
|
||||
intra_state_name = company.state_id and _("Within %s", company.state_id.name) or _("Intra State")
|
||||
country_in_id = self.env.ref('base.in').id
|
||||
state_specific = {
|
||||
'fiscal_position_in_intra_state': {
|
||||
'name': intra_state_name,
|
||||
'sequence': 1,
|
||||
'auto_apply': True,
|
||||
'state_ids': state_ids,
|
||||
'tax_ids': self._get_l10n_in_fiscal_tax_vals('fiscal_position_in_intra_state'),
|
||||
'country_id': country_in_id,
|
||||
},
|
||||
'fiscal_position_in_inter_state': {
|
||||
'name': _("Inter State"),
|
||||
'sequence': 2,
|
||||
'auto_apply': True,
|
||||
'country_group_id': 'l10n_in.inter_state_group',
|
||||
'tax_ids': self._get_l10n_in_fiscal_tax_vals('fiscal_position_in_inter_state'),
|
||||
},
|
||||
}
|
||||
if company.parent_id:
|
||||
return {
|
||||
self.company_xmlid(k): v
|
||||
for k, v in state_specific.items()
|
||||
}
|
||||
return {
|
||||
**state_specific,
|
||||
'fiscal_position_in_sez': {
|
||||
'name': _("Special Economic Zone (SEZ)"),
|
||||
'sequence': 3,
|
||||
'auto_apply': True,
|
||||
'state_ids': [Command.set(self.env.ref('l10n_in.state_in_oc').ids)],
|
||||
'country_id': country_in_id,
|
||||
'note': _("SUPPLY MEANT FOR EXPORT/SUPPLY TO SEZ UNIT OR SEZ DEVELOPER FOR AUTHORISED OPERATIONS ON PAYMENT OF INTEGRATED TAX."),
|
||||
'tax_ids': self._get_l10n_in_fiscal_tax_vals('fiscal_position_in_inter_state'),
|
||||
},
|
||||
'fiscal_position_in_export_sez_in': {
|
||||
'name': _("Export"),
|
||||
'sequence': 4,
|
||||
'auto_apply': True,
|
||||
'note': _("SUPPLY MEANT FOR EXPORT/SUPPLY TO SEZ UNIT OR SEZ DEVELOPER FOR AUTHORISED OPERATIONS ON PAYMENT OF INTEGRATED TAX."),
|
||||
'tax_ids': self._get_l10n_in_fiscal_tax_vals('fiscal_position_in_export_sez_in'),
|
||||
},
|
||||
'fiscal_position_in_lut_sez_1': {
|
||||
'name': _("SEZ - LUT (WOP)"),
|
||||
'sequence': 5,
|
||||
'state_ids': [Command.set(self.env.ref('l10n_in.state_in_oc').ids)],
|
||||
'country_id': country_in_id,
|
||||
'note': _("SUPPLY MEANT FOR EXPORT/SUPPLY TO SEZ UNIT OR SEZ DEVELOPER FOR AUTHORISED OPERATIONS UNDER BOND OR LETTER OF UNDERTAKING WITHOUT PAYMENT OF INTEGRATED TAX."),
|
||||
'tax_ids': self._get_l10n_in_fiscal_tax_vals('fiscal_position_in_lut_sez_1'),
|
||||
},
|
||||
'fiscal_position_in_lut_sez': {
|
||||
'name': _("Export - LUT (WOP)"),
|
||||
'sequence': 6,
|
||||
'note': _('SUPPLY MEANT FOR EXPORT/SUPPLY TO SEZ UNIT OR SEZ DEVELOPER FOR AUTHORISED OPERATIONS UNDER BOND OR LETTER OF UNDERTAKING WITHOUT PAYMENT OF INTEGRATED TAX.'),
|
||||
'tax_ids': self._get_l10n_in_fiscal_tax_vals('fiscal_position_in_lut_sez'),
|
||||
},
|
||||
}
|
||||
|
||||
def _get_l10n_in_fiscal_tax_vals(self, fiscal_position_xml_ids):
|
||||
rates = [1, 2, 5, 12, 18, 28, 40]
|
||||
taxes_xml_ids = []
|
||||
|
||||
if fiscal_position_xml_ids == 'fiscal_position_in_intra_state':
|
||||
taxes_xml_ids = [f"sgst_{tax_type}_{rate}" for tax_type in ["sale", "purchase"] for rate in rates]
|
||||
elif fiscal_position_xml_ids == 'fiscal_position_in_inter_state':
|
||||
taxes_xml_ids = [f"igst_{tax_type}_{rate}" for tax_type in ["sale", "purchase"] for rate in rates]
|
||||
elif fiscal_position_xml_ids == 'fiscal_position_in_export_sez_in':
|
||||
taxes_xml_ids = [f"igst_sale_{rate}_sez_exp" for rate in rates] + [f"igst_purchase_{rate}" for rate in rates] + ['igst_sale_0_sez_exp']
|
||||
elif fiscal_position_xml_ids == 'fiscal_position_in_lut_sez':
|
||||
taxes_xml_ids = [f"igst_sale_{rate}_sez_exp_lut" for rate in rates] + ['igst_sale_0_sez_exp_lut']
|
||||
elif fiscal_position_xml_ids == 'fiscal_position_in_lut_sez_1':
|
||||
taxes_xml_ids = [f"igst_sale_{rate}_sez_lut" for rate in rates] + ['igst_sale_0_sez_lut']
|
||||
return [Command.set(taxes_xml_ids)]
|
||||
|
||||
def _post_load_data(self, template_code, company, template_data):
|
||||
super()._post_load_data(template_code, company, template_data)
|
||||
if template_code == 'in':
|
||||
company = company or self.env.company
|
||||
company._update_l10n_in_is_gst_registered()
|
||||
|
||||
# The COA (Chart of Accounts) data is loaded after the initial compute methods are called.
|
||||
# During initial journal setup, the payment methods and accounts may not exist yet,
|
||||
# causing the payment method lines to not be properly configured.
|
||||
# We call these helper methods again in _post_load_data to ensure all payment method lines
|
||||
# are correctly assigned once all COA data is fully available.
|
||||
bank_journals = company.bank_journal_ids
|
||||
bank_journals._update_payment_method_lines("inbound")
|
||||
bank_journals._update_payment_method_lines("outbound")
|
||||
|
|
@ -1,10 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class UoM(models.Model):
|
||||
class UomUom(models.Model):
|
||||
_inherit = "uom.uom"
|
||||
|
||||
# As per GST Rules you need to Specify UQC given by GST.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue