mirror of
https://github.com/bringout/oca-ocb-l10n_asia-pacific.git
synced 2026-04-26 18:01:58 +02:00
318 lines
16 KiB
Python
318 lines
16 KiB
Python
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
|