Skip to content
Snippets Groups Projects
Commit aa43799e authored by Jigar Vaghela's avatar Jigar Vaghela
Browse files

[ADD] l10n_in_edi: Send invoice to Indian Government


New module to send invoice to Indian Government using IAP service

Task 2179664

closes odoo/odoo#79995

Related: odoo/enterprise#25899
Signed-off-by: default avatarJosse Colpaert <jco@odoo.com>
parent 534167bf
No related branches found
No related tags found
No related merge requests found
Showing
with 925 additions and 0 deletions
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import models
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
"name": """Indian - E-invoicing""",
"version": "1.03.00",
"icon": "/l10n_in/static/description/icon.png",
"category": "Accounting/Localizations/EDI",
"depends": [
"account_edi",
"l10n_in",
"iap",
],
"description": """
Indian - E-invoicing
====================
To submit invoicing through API to the government.
We use "Tera Software Limited" as GSP
Step 1: First you need to create an API username and password in the E-invoice portal.
Step 2: Switch to company related to that GST number
Step 3: Set that username and password in Odoo (Goto: Invoicing/Accounting -> Configration -> Settings -> Customer Invoices or find "E-invoice" in search bar)
Step 4: Repeat steps 1,2,3 for all GSTIN you have in odoo. If you have a multi-company with the same GST number then perform step 1 for the first company only.
For the creation of API username and password please ref this document: <https://service.odoo.co.in/einvoice_create_api_user>
""",
"data": [
"data/account_edi_data.xml",
"views/res_config_settings_views.xml",
"views/edi_pdf_report.xml",
"views/account_move_views.xml",
],
"demo": [
"demo/demo_company.xml",
],
"installable": True,
# only applicable for taxpayers turnover higher than Rs.50 crore so auto_install is False
"auto_install": False,
"application": False,
"license": "LGPL-3",
}
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="edi_in_einvoice_json_1_03" model="account.edi.format">
<field name="name">E-Invoice (IN)</field>
<field name="code">in_einvoice_1_03</field>
</record>
</odoo>
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- This is testing credentials -->
<record id="l10n_in.demo_company_in" model="res.company">
<field name="l10n_in_edi_username">MGSTTEST</field>
<field name="l10n_in_edi_password">mgst@123</field>
</record>
</odoo>
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import account_edi_format
from . import account_move
from . import res_company
from . import res_config_settings
This diff is collapsed.
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import json
from odoo import api, fields, models, _
from odoo.exceptions import UserError
class AccountMove(models.Model):
_inherit = "account.move"
l10n_in_edi_cancel_reason = fields.Selection(selection=[
("1", "Duplicate"),
("2", "Data Entry Mistake"),
("3", "Order Cancelled"),
("4", "Others"),
], string="Cancel reason", copy=False)
l10n_in_edi_cancel_remarks = fields.Char("Cancel remarks", copy=False)
l10n_in_edi_show_cancel = fields.Boolean(compute="_compute_l10n_in_edi_show_cancel", string="E-invoice(IN) is sent?")
@api.depends('edi_document_ids')
def _compute_l10n_in_edi_show_cancel(self):
for invoice in self:
invoice.l10n_in_edi_show_cancel = bool(invoice.edi_document_ids.filtered(
lambda i: i.edi_format_id.code == "in_einvoice_1_03"
and i.state in ("sent", "to_cancel", "cancelled")
))
def button_cancel_posted_moves(self):
"""Mark the edi.document related to this move to be canceled."""
reason_and_remarks_not_set = self.env["account.move"]
for move in self:
send_l10n_in_edi = move.edi_document_ids.filtered(lambda doc: doc.edi_format_id.code == "in_einvoice_1_03")
# check submitted E-invoice does not have reason and remarks
# because it's needed to cancel E-invoice
if send_l10n_in_edi and (not move.l10n_in_edi_cancel_reason or not move.l10n_in_edi_cancel_remarks):
reason_and_remarks_not_set += move
if reason_and_remarks_not_set:
raise UserError(_(
"To cancel E-invoice set cancel reason and remarks at Other info tab in invoices: \n%s",
("\n".join(reason_and_remarks_not_set.mapped("name"))),
))
return super().button_cancel_posted_moves()
def _get_l10n_in_edi_response_json(self):
self.ensure_one()
l10n_in_edi = self.edi_document_ids.filtered(lambda i: i.edi_format_id.code == "in_einvoice_1_03"
and i.state in ("sent", "to_cancel"))
if l10n_in_edi:
return json.loads(l10n_in_edi.attachment_id.raw.decode("utf-8"))
else:
return {}
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class ResCompany(models.Model):
_inherit = "res.company"
l10n_in_edi_username = fields.Char("E-invoice (IN) Username", groups="base.group_system")
l10n_in_edi_password = fields.Char("E-invoice (IN) Password", groups="base.group_system")
l10n_in_edi_token = fields.Char("E-invoice (IN) Token", groups="account.group_system")
l10n_in_edi_token_validity = fields.Datetime("E-invoice (IN) Valid Until", groups="account.group_system")
l10n_in_edi_production_env = fields.Boolean(
string="E-invoice (IN) Is production OSE environment",
help="Enable the use of production credentials",
groups="base.group_system",
)
def _l10n_in_edi_token_is_valid(self):
self.ensure_one()
if self.l10n_in_edi_token and self.l10n_in_edi_token_validity > fields.Datetime.now():
return True
return False
def _neutralize(self):
super()._neutralize()
self.flush()
self.invalidate_cache()
self.env.cr.execute("UPDATE res_company SET l10n_in_edi_production_env = true")
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models, fields
class ResConfigSettings(models.TransientModel):
_inherit = "res.config.settings"
l10n_in_edi_username = fields.Char("Indian EDI username", related="company_id.l10n_in_edi_username", readonly=False)
l10n_in_edi_password = fields.Char("Indian EDI password", related="company_id.l10n_in_edi_password", readonly=False)
l10n_in_edi_production_env = fields.Boolean(
string="Indian EDI Testing Environment",
related="company_id.l10n_in_edi_production_env",
readonly=False
)
def l10n_in_edi_test(self):
self.env["account.edi.format"]._l10n_in_edi_authenticate(self.company_id)
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import test_edi_json
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
from odoo.tests import tagged
@tagged("post_install_l10n", "post_install", "-at_install")
class TestEdiJson(AccountTestInvoicingCommon):
@classmethod
def setUpClass(cls, chart_template_ref="l10n_in.indian_chart_template_standard"):
super().setUpClass(chart_template_ref=chart_template_ref)
cls.maxDiff = None
cls.company_data["company"].write({
"street": "Block no. 401",
"street2": "Street 2",
"city": "City 1",
"zip": "500001",
"state_id": cls.env.ref("base.state_in_ts").id,
"country_id": cls.env.ref("base.in").id,
"vat": "36AABCT1332L011",
})
cls.partner_a.write({
"vat": "36BBBFF5679L8ZR",
"street": "Block no. 401",
"street2": "Street 2",
"city": "City 2",
"zip": "500001",
"state_id": cls.env.ref("base.state_in_ts").id,
"country_id": cls.env.ref("base.in").id,
"l10n_in_gst_treatment": "regular",
})
cls.product_a.write({"l10n_in_hsn_code": "01111"})
gst_with_cess = cls.env.ref("l10n_in.%s_sgst_sale_12" % (cls.company_data["company"].id)
) + cls.env.ref("l10n_in.%s_cess_5_plus_1591_sale" % (cls.company_data["company"].id))
product_with_cess = cls.env["product.product"].create({
"name": "product_with_cess",
"uom_id": cls.env.ref("uom.product_uom_unit").id,
"lst_price": 1000.0,
"standard_price": 800.0,
"property_account_income_id": cls.company_data["default_account_revenue"].id,
"property_account_expense_id": cls.company_data["default_account_expense"].id,
"taxes_id": [(6, 0, gst_with_cess.ids)],
"supplier_taxes_id": [(6, 0, cls.tax_purchase_a.ids)],
"l10n_in_hsn_code": "02222",
})
cls.invoice = cls.init_invoice("out_invoice", post=False, products=cls.product_a + product_with_cess)
cls.invoice.write({
"invoice_line_ids": [(1, l_id, {"discount": 10}) for l_id in cls.invoice.invoice_line_ids.ids]})
cls.invoice.action_post()
def test_edi_json(self):
json_value = self.env["account.edi.format"]._l10n_in_edi_generate_invoice_json(self.invoice)
expected = {
"Version": "1.1",
"TranDtls": {"TaxSch": "GST", "SupTyp": "B2B", "RegRev": "N", "IgstOnIntra": "N"},
"DocDtls": {"Typ": "INV", "No": "INV/2019/00001", "Dt": "01/01/2019"},
"SellerDtls": {
"LglNm": "company_1_data",
"Addr1": "Block no. 401",
"Addr2": "Street 2",
"Loc": "City 1",
"Pin": 500001,
"Stcd": "36",
"GSTIN": "36AABCT1332L011"},
"BuyerDtls": {
"LglNm": "partner_a",
"Addr1": "Block no. 401",
"Addr2": "Street 2",
"Loc": "City 2",
"Pin": 500001,
"Stcd": "36",
"POS": "36",
"GSTIN": "36BBBFF5679L8ZR"},
"ItemList": [
{
"SlNo": "1", "PrdDesc": "product_a", "IsServc": "N", "HsnCd": "01111", "Qty": 1.0,
"Unit": "UNT", "UnitPrice": 1000.0, "TotAmt": 1000.0, "Discount": 100.0, "AssAmt": 900.0,
"GstRt": 5.0, "IgstAmt": 0.0, "CgstAmt": 22.5, "SgstAmt": 22.5, "CesRt": 0.0, "CesAmt": 0.0,
"CesNonAdvlAmt": 0.0, "StateCesRt": 0.0, "StateCesAmt": 0.0, "StateCesNonAdvlAmt": 0.0,
"OthChrg": 0.0, "TotItemVal": 945.0
},
{
"SlNo": "2", "PrdDesc": "product_with_cess", "IsServc": "N", "HsnCd": "02222", "Qty": 1.0,
"Unit": "UNT", "UnitPrice": 1000.0, "TotAmt": 1000.0, "Discount": 100.0, "AssAmt": 900.0,
"GstRt": 12.0, "IgstAmt": 0.0, "CgstAmt": 54.0, "SgstAmt": 54.0, "CesRt": 5.0, "CesAmt": 45.0,
"CesNonAdvlAmt": 1.59, "StateCesRt": 0.0, "StateCesAmt": 0.0, "StateCesNonAdvlAmt": 0.0,
"OthChrg": 0.0, "TotItemVal": 1054.59
}
],
"ValDtls": {
"AssVal": 1800.0, "CgstVal": 76.5, "SgstVal": 76.5, "IgstVal": 0.0, "CesVal": 46.59,
"StCesVal": 0.0, "RndOffAmt": 0.0, "TotInvVal": 1999.59
}
}
self.assertDictEqual(json_value, expected, "Indian EDI send json value is not matched")
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="invoice_form_inherit_l10n_in_edi" model="ir.ui.view">
<field name="name">account.move.form.inherit.l10n.in.edi</field>
<field name="model">account.move</field>
<field name="inherit_id" ref="account.view_move_form"/>
<field name="arch" type="xml">
<xpath expr="//group[@name='sale_info_group']" position="inside">
<field name="l10n_in_edi_show_cancel" invisible="1"/>
<field name="l10n_in_edi_cancel_reason" attrs="{'invisible': ['|', '|', ('country_code', '!=', 'IN'), ('state', '!=', 'posted'), ('l10n_in_edi_show_cancel', '!=', True)]}"/>
<field name="l10n_in_edi_cancel_remarks" attrs="{'invisible': ['|', '|', ('country_code', '!=', 'IN'), ('state', '!=', 'posted'), ('l10n_in_edi_show_cancel', '!=', True)]}"/>
</xpath>
</field>
</record>
</odoo>
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="l10n_in_einvoice_report_invoice_document_inherit" inherit_id="account.report_invoice_document">
<xpath expr="//div[@id='informations']" position="before">
<t t-set="l10n_in_einvoice_json" t-value="o._get_l10n_in_edi_response_json()"/>
<div id="l10n_in_einvoice_informations_inr" class="row mt-4 mb-4">
<div class="col-auto col-3 mw-100 mb-2" t-if="l10n_in_einvoice_json" name="irn">
<strong>IRN:</strong>
<p class="m-0" t-esc="l10n_in_einvoice_json['Irn']"/>
</div>
</div>
<div id="l10n_in_einvoice_informations_other" class="row mt-4 mb-4">
<div class="col-auto col-3 mw-100 mb-2" t-if="l10n_in_einvoice_json" name="ack_no">
<strong>Ack. No:</strong>
<p class="m-0" t-esc="l10n_in_einvoice_json['AckNo']"/>
</div>
<div class="col-auto col-3 mw-100 mb-2" t-if="l10n_in_einvoice_json" name="ack_date">
<strong>Ack. Date:</strong>
<p class="m-0" t-esc="l10n_in_einvoice_json['AckDt']"/>
</div>
</div>
</xpath>
<xpath expr="//div[@id='qrcode']" position="after">
<p t-if="l10n_in_einvoice_json">
<strong class="text-center">Scan me with E-invoice app.</strong><br/><br/>
<img t-att-src="'/report/barcode/?barcode_type=%s&amp;value=%s&amp;width=%s&amp;height=%s' %
('QR', l10n_in_einvoice_json['SignedQRCode'], 200, 200)"/>
</p>
</xpath>
</template>
</odoo>
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="res_config_settings_view_form_inherit_l10n_in_edi" model="ir.ui.view">
<field name="name">res.config.settings.form.inherit.l10n_in_edi</field>
<field name="model">res.config.settings</field>
<field name="inherit_id" ref="account.res_config_settings_view_form"/>
<field name="arch" type="xml">
<div data-key="account" position="inside">
<h2 attrs="{'invisible': [('country_code', '!=', 'IN')]}">Indian Electronic Invoicing</h2>
<div class='row mt16 o_settings_container' name="l10n_in_edi_iap" attrs="{'invisible': [('country_code', '!=', 'IN')]}">
<div class="col-12 col-lg-6 o_setting_box" id="gsp_setting">
<div class="o_setting_right_pane">
<t class="o_form_label">Setup E-invoice</t>
<span class="fa fa-lg fa-building-o" title="Values set here are company-specific." aria-label="Values set here are company-specific." groups="base.group_multi_company" role="img"/>
<div class="text-muted">
Setup E-invoice Service for this company
</div>
<div class="content-group">
<div class="mt16 row">
<label for="l10n_in_edi_username" string="Username" class="col-3 col-lg-3 o_light_label"/>
<field name="l10n_in_edi_username" nolabel="1"/>
<label for="l10n_in_edi_password" string="Password" class="col-3 col-lg-3 o_light_label" />
<field name="l10n_in_edi_password" password="True" nolabel="1"/>
<label for="l10n_in_edi_production_env" string="Production Environment" class="col-3 col-lg-3 o_light_label"/>
<field name="l10n_in_edi_production_env" nolabel="1"/>
<div class="text-muted">
Only check if you are in production.
</div>
</div>
</div>
<div class='mt8'>
<button name="l10n_in_edi_test" icon="fa-arrow-right" type="object" string="Verify Username and Password" class="btn-link"/>
</div>
</div>
</div>
</div>
</div>
</field>
</record>
</odoo>
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment