[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: Josse Colpaert <jco@odoo.com>
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import models
|
||||
@@ -0,0 +1,40 @@
|
||||
# -*- 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",
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?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>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?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>
|
||||
@@ -0,0 +1,7 @@
|
||||
# -*- 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
|
||||
@@ -0,0 +1,571 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import re
|
||||
import json
|
||||
import pytz
|
||||
import markupsafe
|
||||
|
||||
from odoo import models, fields, api, _
|
||||
from odoo.tools import html_escape
|
||||
from odoo.exceptions import AccessError
|
||||
from odoo.addons.iap import jsonrpc
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_IAP_ENDPOINT = "https://l10n-in-edi.api.odoo.com"
|
||||
DEFAULT_IAP_TEST_ENDPOINT = "https://l10n-in-edi-demo.api.odoo.com"
|
||||
|
||||
|
||||
class AccountEdiFormat(models.Model):
|
||||
_inherit = "account.edi.format"
|
||||
|
||||
def _is_enabled_by_default_on_journal(self, journal):
|
||||
self.ensure_one()
|
||||
if self.code == "in_einvoice_1_03":
|
||||
return journal.company_id.country_id.code == 'IN'
|
||||
return super()._is_enabled_by_default_on_journal(journal)
|
||||
|
||||
def _is_required_for_invoice(self, invoice):
|
||||
self.ensure_one()
|
||||
if self.code == "in_einvoice_1_03":
|
||||
return invoice.is_sale_document() and invoice.country_code == 'IN' and invoice.l10n_in_gst_treatment in (
|
||||
"regular",
|
||||
"composition",
|
||||
"overseas",
|
||||
"special_economic_zone",
|
||||
"deemed_export",
|
||||
)
|
||||
return super()._is_required_for_invoice(invoice)
|
||||
|
||||
def _needs_web_services(self):
|
||||
self.ensure_one()
|
||||
return self.code == "in_einvoice_1_03" or super()._needs_web_services()
|
||||
|
||||
def _get_invoice_edi_content(self, move):
|
||||
if self.code != "in_einvoice_1_03":
|
||||
return super()._get_invoice_edi_content(move)
|
||||
json_dump = json.dumps(self._l10n_in_edi_generate_invoice_json(move))
|
||||
return json_dump.encode()
|
||||
|
||||
def _l10n_in_edi_extract_digits(self, string):
|
||||
if not string:
|
||||
return string
|
||||
matches = re.findall(r"\d+", string)
|
||||
result = "".join(matches)
|
||||
return result
|
||||
|
||||
def _check_move_configuration(self, move):
|
||||
if self.code != "in_einvoice_1_03":
|
||||
return super()._check_move_configuration(move)
|
||||
error_message = []
|
||||
error_message += self._l10n_in_validate_partner(move.partner_id)
|
||||
error_message += self._l10n_in_validate_partner(move.company_id.partner_id, is_company=True)
|
||||
if not re.match("^.{1,16}$", move.name):
|
||||
error_message.append(_("Invoice number should not be more than 16 characters"))
|
||||
for line in move.invoice_line_ids.filtered(lambda line: not (line.display_type or line.is_rounding_line)):
|
||||
if line.product_id:
|
||||
hsn_code = self._l10n_in_edi_extract_digits(line.product_id.l10n_in_hsn_code)
|
||||
if not hsn_code:
|
||||
error_message.append(_("HSN code is not set in product %s", line.product_id.name))
|
||||
elif not re.match("^[0-9]+$", hsn_code):
|
||||
error_message.append(_(
|
||||
"Invalid HSN Code (%s) in product %s", hsn_code, line.product_id.name
|
||||
))
|
||||
else:
|
||||
error_message.append(_("product is required to get HSN code"))
|
||||
return error_message
|
||||
|
||||
def _l10n_in_edi_get_iap_buy_credits_message(self, company):
|
||||
base_url = "https://iap-sandbox.odoo.com/iap/1/credit" if not company.sudo().l10n_in_edi_production_env else ""
|
||||
url = self.env["iap.account"].get_credits_url(service_name="l10n_in_edi", base_url=base_url)
|
||||
return markupsafe.Markup("""<p><b>%s</b></p><p>%s</p>""" % (
|
||||
_("You have insufficient credits to send this document!"),
|
||||
_("Please proceed to buy more credits <a href='%s'>here.</a>", url),
|
||||
))
|
||||
|
||||
def _post_invoice_edi(self, invoices):
|
||||
if self.code != "in_einvoice_1_03":
|
||||
return super()._post_invoice_edi(invoices)
|
||||
response = {}
|
||||
res = {}
|
||||
generate_json = self._l10n_in_edi_generate_invoice_json(invoices)
|
||||
response = self._l10n_in_edi_generate(invoices.company_id, generate_json)
|
||||
if response.get("error"):
|
||||
error = response["error"]
|
||||
error_codes = [e.get("code") for e in error]
|
||||
if "1005" in error_codes:
|
||||
# Invalid token eror then create new token and send generate request again.
|
||||
# This happen when authenticate called from another odoo instance with same credentials (like. Demo/Test)
|
||||
authenticate_response = self._l10n_in_edi_authenticate(invoices.company_id)
|
||||
if not authenticate_response.get("error"):
|
||||
error = []
|
||||
response = self._l10n_in_edi_generate(invoices.company_id, generate_json)
|
||||
if response.get("error"):
|
||||
error = response["error"]
|
||||
error_codes = [e.get("code") for e in error]
|
||||
if "2150" in error_codes:
|
||||
# Get IRN by details in case of IRN is already generated
|
||||
# this happens when timeout from the Government portal but IRN is generated
|
||||
response = self._l10n_in_edi_get_irn_by_details(invoices.company_id, {
|
||||
"doc_type": invoices.move_type == "out_refund" and "CRN" or "INV",
|
||||
"doc_num": invoices.name,
|
||||
"doc_date": invoices.invoice_date and invoices.invoice_date.strftime("%d/%m/%Y") or False,
|
||||
})
|
||||
if not response.get("error"):
|
||||
error = []
|
||||
odoobot = self.env.ref("base.partner_root")
|
||||
invoices.message_post(author_id=odoobot.id, body=_(
|
||||
"Somehow this invoice had been submited to government before." \
|
||||
"<br/>Normally, this should not happen too often" \
|
||||
"<br/>Just verify value of invoice by uploade json to government website " \
|
||||
"<a href='https://einvoice1.gst.gov.in/Others/VSignedInvoice'>here<a>."
|
||||
))
|
||||
if "no-credit" in error_codes:
|
||||
res[invoices] = {
|
||||
"success": False,
|
||||
"error": self._l10n_in_edi_get_iap_buy_credits_message(invoices.company_id),
|
||||
"blocking_level": "error",
|
||||
}
|
||||
elif error:
|
||||
error_message = "<br/>".join(["[%s] %s" % (e.get("code"), html_escape(e.get("message"))) for e in error])
|
||||
res[invoices] = {
|
||||
"success": False,
|
||||
"error": error_message,
|
||||
"blocking_level": ("404" in error_codes) and "warning" or "error",
|
||||
}
|
||||
if not response.get("error"):
|
||||
json_dump = json.dumps(response.get("data"))
|
||||
json_name = "%s_einvoice.json" % (invoices.name.replace("/", "_"))
|
||||
attachment = self.env["ir.attachment"].create({
|
||||
"name": json_name,
|
||||
"raw": json_dump.encode(),
|
||||
"res_model": "account.move",
|
||||
"res_id": invoices.id,
|
||||
"mimetype": "application/json",
|
||||
})
|
||||
res[invoices] = {"success": True, "attachment": attachment}
|
||||
return res
|
||||
|
||||
def _cancel_invoice_edi(self, invoices):
|
||||
if self.code != "in_einvoice_1_03":
|
||||
return super()._cancel_invoice_edi(invoices)
|
||||
res = {}
|
||||
for invoice in invoices:
|
||||
l10n_in_edi_response_json = invoice._get_l10n_in_edi_response_json()
|
||||
cancel_json = {
|
||||
"Irn": l10n_in_edi_response_json.get("Irn"),
|
||||
"CnlRsn": invoice.l10n_in_edi_cancel_reason,
|
||||
"CnlRem": invoice.l10n_in_edi_cancel_remarks,
|
||||
}
|
||||
response = self._l10n_in_edi_cancel(invoice.company_id, cancel_json)
|
||||
if response.get("error"):
|
||||
error = response["error"]
|
||||
error_codes = [e.get("code") for e in error]
|
||||
if "1005" in error_codes:
|
||||
# Invalid token eror then create new token and send generate request again.
|
||||
# This happen when authenticate called from another odoo instance with same credentials (like. Demo/Test)
|
||||
authenticate_response = self._l10n_in_edi_authenticate(invoice.company_id)
|
||||
if not authenticate_response.get("error"):
|
||||
error = []
|
||||
response = self._l10n_in_edi_cancel(invoice.company_id, cancel_json)
|
||||
if response.get("error"):
|
||||
error = response["error"]
|
||||
error_codes = [e.get("code") for e in error]
|
||||
if "9999" in error_codes:
|
||||
response = {}
|
||||
odoobot = self.env.ref("base.partner_root")
|
||||
invoices.message_post(author_id=odoobot.id, body=_(
|
||||
"Somehow this invoice had been cancelled to government before." \
|
||||
"<br/>Normally, this should not happen too often" \
|
||||
"<br/>Just verify by logging into government website " \
|
||||
"<a href='https://einvoice1.gst.gov.in'>here<a>."
|
||||
))
|
||||
if "no-credit" in error_codes:
|
||||
res[invoice] = {
|
||||
"success": False,
|
||||
"error": self._l10n_in_edi_get_iap_buy_credits_message(invoice.company_id),
|
||||
"blocking_level": "error",
|
||||
}
|
||||
else:
|
||||
error_message = "<br/>".join(["[%s] %s" % (e.get("code"), html_escape(e.get("message"))) for e in error])
|
||||
res[invoice] = {
|
||||
"success": False,
|
||||
"error": error_message,
|
||||
"blocking_level": ("404" in error_codes) and "warning" or "error",
|
||||
}
|
||||
if not response.get("error"):
|
||||
json_dump = json.dumps(response.get("data", {}))
|
||||
json_name = "%s_cancel_einvoice.json" % (invoice.name.replace("/", "_"))
|
||||
attachment = self.env["ir.attachment"].create({
|
||||
"name": json_name,
|
||||
"raw": json_dump.encode(),
|
||||
"res_model": "account.move",
|
||||
"res_id": invoice.id,
|
||||
"mimetype": "application/json",
|
||||
})
|
||||
res[invoice] = {"success": True, "attachment": attachment}
|
||||
return res
|
||||
|
||||
def _l10n_in_validate_partner(self, partner, is_company=False):
|
||||
self.ensure_one()
|
||||
message = []
|
||||
if not re.match("^.{3,100}$", partner.street or ""):
|
||||
message.append(_("\n- Street required min 3 and max 100 characters"))
|
||||
if partner.street2 and not re.match("^.{3,100}$", partner.street2):
|
||||
message.append(_("\n- Street2 should be min 3 and max 100 characters"))
|
||||
if not re.match("^.{3,100}$", partner.city or ""):
|
||||
message.append(_("\n- City required min 3 and max 100 characters"))
|
||||
if not re.match("^.{3,50}$", partner.state_id.name or ""):
|
||||
message.append(_("\n- State required min 3 and max 50 characters"))
|
||||
if partner.country_id.code == "IN" and not re.match("^[0-9]{6,}$", partner.zip or ""):
|
||||
message.append(_("\n- Zip code required 6 digits"))
|
||||
if partner.phone and not re.match("^[0-9]{10,12}$",
|
||||
self._l10n_in_edi_extract_digits(partner.phone)
|
||||
):
|
||||
message.append(_("\n- Mobile number should be minimum 10 or maximum 12 digits"))
|
||||
if partner.email and (
|
||||
not re.match(r"^[a-zA-Z0-9+_.-]+@[a-zA-Z0-9.-]+$", partner.email)
|
||||
or not re.match("^.{6,100}$", partner.email)
|
||||
):
|
||||
message.append(_("\n- Email address should be valid and not more then 100 characters"))
|
||||
return message
|
||||
|
||||
def _get_l10n_in_edi_saler_buyer_party(self, move):
|
||||
return {
|
||||
"seller_details": move.company_id.partner_id,
|
||||
"dispatch_details": move._l10n_in_get_warehouse_address() or move.company_id.partner_id,
|
||||
"buyer_details": move.partner_id,
|
||||
"ship_to_details": move.partner_shipping_id or move.partner_id,
|
||||
}
|
||||
|
||||
@api.model
|
||||
def _get_l10n_in_edi_partner_details(self, partner, set_vat=True, set_phone_and_email=True,
|
||||
is_overseas=False, pos_state_id=False):
|
||||
"""
|
||||
Create the dictionary based partner details
|
||||
if set_vat is true then, vat(GSTIN) and legal name(LglNm) is added
|
||||
if set_phone_and_email is true then phone and email is add
|
||||
if set_pos is true then state code from partner or passed state_id is added as POS(place of supply)
|
||||
if is_overseas is true then pin is 999999 and GSTIN(vat) is URP and Stcd is .
|
||||
if pos_state_id is passed then we use set POS
|
||||
"""
|
||||
partner_details = {
|
||||
"Addr1": partner.street or "",
|
||||
"Loc": partner.city or "",
|
||||
"Pin": int(self._l10n_in_edi_extract_digits(partner.zip)),
|
||||
"Stcd": partner.state_id.l10n_in_tin or "",
|
||||
}
|
||||
if partner.street2:
|
||||
partner_details.update({"Addr2": partner.street2})
|
||||
if set_phone_and_email:
|
||||
if partner.email:
|
||||
partner_details.update({"Em": partner.email})
|
||||
if partner.phone:
|
||||
partner_details.update({"Ph": self._l10n_in_edi_extract_digits(partner.phone)})
|
||||
if pos_state_id:
|
||||
partner_details.update({"POS": pos_state_id.l10n_in_tin or ""})
|
||||
if set_vat:
|
||||
partner_details.update({
|
||||
"LglNm": partner.commercial_partner_id.name,
|
||||
"GSTIN": partner.vat or "",
|
||||
})
|
||||
else:
|
||||
partner_details.update({"Nm": partner.name})
|
||||
if is_overseas:
|
||||
partner_details.update({
|
||||
"GSTIN": "URP",
|
||||
"Pin": 999999,
|
||||
"Stcd": "96",
|
||||
"POS": "96",
|
||||
})
|
||||
return partner_details
|
||||
|
||||
@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 round(amount, precision_digits)
|
||||
|
||||
def _get_l10n_in_edi_line_details(self, index, line, line_tax_details, sign):
|
||||
"""
|
||||
Create the dictionary with line details
|
||||
return {
|
||||
account.move.line('1'): {....},
|
||||
account.move.line('2'): {....},
|
||||
....
|
||||
}
|
||||
"""
|
||||
tax_details_by_code = self._get_l10n_in_tax_details_by_line_code(line_tax_details.get("tax_details", {}))
|
||||
return {
|
||||
"SlNo": str(index),
|
||||
"PrdDesc": line.name.replace("\n", ""),
|
||||
"IsServc": line.product_id.type == "service" and "Y" or "N",
|
||||
"HsnCd": self._l10n_in_edi_extract_digits(line.product_id.l10n_in_hsn_code),
|
||||
"Qty": self._l10n_in_round_value(line.quantity or 0.0, 3),
|
||||
"Unit": line.product_uom_id.l10n_in_code and line.product_uom_id.l10n_in_code.split("-")[0] or "OTH",
|
||||
# Unit price in company currency and tax excluded so its different then price_unit
|
||||
"UnitPrice": self._l10n_in_round_value(
|
||||
((line.balance / (1 - (line.discount / 100))) / line.quantity) * sign, 3),
|
||||
# total amount is before discount
|
||||
"TotAmt": self._l10n_in_round_value((line.balance / (1 - (line.discount / 100))) * sign),
|
||||
"Discount": self._l10n_in_round_value(
|
||||
((line.balance / (1 - (line.discount / 100))) - line.balance) * sign),
|
||||
"AssAmt": self._l10n_in_round_value(line.balance * sign),
|
||||
"GstRt": self._l10n_in_round_value(tax_details_by_code.get("igst_rate", 0.00) or (
|
||||
tax_details_by_code.get("cgst_rate", 0.00) + tax_details_by_code.get("sgst_rate", 0.00)), 3),
|
||||
"IgstAmt": self._l10n_in_round_value(tax_details_by_code.get("igst_amount", 0.00) * sign),
|
||||
"CgstAmt": self._l10n_in_round_value(tax_details_by_code.get("cgst_amount", 0.00) * sign),
|
||||
"SgstAmt": self._l10n_in_round_value(tax_details_by_code.get("sgst_amount", 0.00) * sign),
|
||||
"CesRt": self._l10n_in_round_value(tax_details_by_code.get("cess_rate", 0.00), 3),
|
||||
"CesAmt": self._l10n_in_round_value(tax_details_by_code.get("cess_amount", 0.00) * sign),
|
||||
"CesNonAdvlAmt": self._l10n_in_round_value(
|
||||
tax_details_by_code.get("cess_non_advol_amount", 0.00) * sign),
|
||||
"StateCesRt": self._l10n_in_round_value(tax_details_by_code.get("state_cess_rate_amount", 0.00), 3),
|
||||
"StateCesAmt": self._l10n_in_round_value(tax_details_by_code.get("state_cess_amount", 0.00) * sign),
|
||||
"StateCesNonAdvlAmt": self._l10n_in_round_value(
|
||||
tax_details_by_code.get("state_cess_non_advol_amount", 0.00) * sign),
|
||||
"OthChrg": self._l10n_in_round_value(tax_details_by_code.get("other_amount", 0.00) * sign),
|
||||
"TotItemVal": self._l10n_in_round_value(
|
||||
(line.balance + line_tax_details.get("tax_amount", 0.00)) * sign),
|
||||
}
|
||||
|
||||
def _l10n_in_edi_generate_invoice_json(self, invoice):
|
||||
tax_details = self._l10n_in_prepare_edi_tax_details(invoice)
|
||||
saler_buyer = self._get_l10n_in_edi_saler_buyer_party(invoice)
|
||||
tax_details_by_code = self._get_l10n_in_tax_details_by_line_code(tax_details.get("tax_details", {}))
|
||||
sign = invoice.is_inbound() and -1 or 1
|
||||
is_intra_state = invoice.l10n_in_state_id == invoice.company_id.state_id
|
||||
is_overseas = invoice.l10n_in_gst_treatment == "overseas"
|
||||
lines = invoice.invoice_line_ids.filtered(lambda line: not (line.display_type or line.is_rounding_line))
|
||||
invoice_line_tax_details = tax_details.get("invoice_line_tax_details")
|
||||
json_payload = {
|
||||
"Version": "1.1",
|
||||
"TranDtls": {
|
||||
"TaxSch": "GST",
|
||||
"SupTyp": self._l10n_in_get_supply_type(invoice, tax_details_by_code),
|
||||
"RegRev": tax_details_by_code.get("is_reverse_charge") and "Y" or "N",
|
||||
"IgstOnIntra": is_intra_state and tax_details_by_code.get("igst") and "Y" or "N"},
|
||||
"DocDtls": {
|
||||
"Typ": invoice.move_type == "out_refund" and "CRN" or "INV",
|
||||
"No": invoice.name,
|
||||
"Dt": invoice.invoice_date.strftime("%d/%m/%Y")},
|
||||
"SellerDtls": self._get_l10n_in_edi_partner_details(saler_buyer.get("seller_details")),
|
||||
"BuyerDtls": self._get_l10n_in_edi_partner_details(
|
||||
saler_buyer.get("buyer_details"), pos_state_id=invoice.l10n_in_state_id, is_overseas=is_overseas),
|
||||
"ItemList": [
|
||||
self._get_l10n_in_edi_line_details(index, line, invoice_line_tax_details.get(line, {}), sign)
|
||||
for index, line in enumerate(lines, start=1)
|
||||
],
|
||||
"ValDtls": {
|
||||
"AssVal": self._l10n_in_round_value(tax_details.get("base_amount") * sign),
|
||||
"CgstVal": self._l10n_in_round_value(tax_details_by_code.get("cgst_amount", 0.00) * sign),
|
||||
"SgstVal": self._l10n_in_round_value(tax_details_by_code.get("sgst_amount", 0.00) * sign),
|
||||
"IgstVal": self._l10n_in_round_value(tax_details_by_code.get("igst_amount", 0.00) * sign),
|
||||
"CesVal": self._l10n_in_round_value((
|
||||
tax_details_by_code.get("cess_amount", 0.00)
|
||||
+ tax_details_by_code.get("cess_non_advol_amount", 0.00)) * sign,
|
||||
),
|
||||
"StCesVal": self._l10n_in_round_value((
|
||||
tax_details_by_code.get("state_cess_amount", 0.00)
|
||||
+ tax_details_by_code.get("state_cess_non_advol_amount", 0.00)) * sign,
|
||||
),
|
||||
"RndOffAmt": self._l10n_in_round_value(
|
||||
sum(line.balance for line in invoice.invoice_line_ids if line.is_rounding_line)),
|
||||
"TotInvVal": self._l10n_in_round_value(
|
||||
(tax_details.get("base_amount") + tax_details.get("tax_amount")) * sign),
|
||||
},
|
||||
}
|
||||
if invoice.company_currency_id != invoice.currency_id:
|
||||
json_payload["ValDtls"].update({
|
||||
"TotInvValFc": self._l10n_in_round_value(
|
||||
(tax_details.get("base_amount_currency") + tax_details.get("tax_amount_currency")) * sign)
|
||||
})
|
||||
if saler_buyer.get("seller_details") != saler_buyer.get("dispatch_details"):
|
||||
json_payload.update({
|
||||
"DispDtls": self._get_l10n_in_edi_partner_details(saler_buyer.get("dispatch_details"),
|
||||
set_vat=False, set_phone_and_email=False)
|
||||
})
|
||||
if saler_buyer.get("buyer_details") != saler_buyer.get("ship_to_details"):
|
||||
json_payload.update({
|
||||
"ShipDtls": self._get_l10n_in_edi_partner_details(saler_buyer.get("ship_to_details"))
|
||||
})
|
||||
if is_overseas:
|
||||
json_payload.update({
|
||||
"ExpDtls": {
|
||||
"ShipBNo": invoice.l10n_in_shipping_bill_number or "",
|
||||
"ShipBDt": invoice.l10n_in_shipping_bill_date
|
||||
and invoice.l10n_in_shipping_bill_date.strftime("%d/%m/%Y") or "",
|
||||
"Port": invoice.l10n_in_shipping_port_code_id.code or "",
|
||||
"RefClm": tax_details_by_code.get("igst") and "Y" or "N",
|
||||
"ForCur": invoice.currency_id.name,
|
||||
"CntCode": saler_buyer.get("buyer_details").country_id.code or "",
|
||||
}
|
||||
})
|
||||
return json_payload
|
||||
|
||||
@api.model
|
||||
def _l10n_in_prepare_edi_tax_details(self, move, in_foreign=False):
|
||||
def l10n_in_grouping_key_generator(tax_values):
|
||||
base_line = tax_values["base_line_id"]
|
||||
tax_line = tax_values["tax_line_id"]
|
||||
line_code = "other"
|
||||
if tax_line.tax_tag_ids in self.env.ref("l10n_in.tax_report_line_cess").sudo().tag_ids:
|
||||
if tax_line.tax_line_id.amount_type != "percent":
|
||||
line_code = "cess_non_advol"
|
||||
else:
|
||||
line_code = "cess"
|
||||
elif tax_line.tax_tag_ids in self.env.ref("l10n_in.tax_report_line_state_cess").sudo().tag_ids:
|
||||
if tax_line.tax_line_id.amount_type != "percent":
|
||||
line_code = "state_cess_non_advol"
|
||||
else:
|
||||
line_code = "state_cess"
|
||||
else:
|
||||
for gst in ["cgst", "sgst", "igst"]:
|
||||
if tax_line.tax_tag_ids in self.env.ref("l10n_in.tax_report_line_%s"%(gst)).sudo().tag_ids:
|
||||
line_code = gst
|
||||
return {
|
||||
"tax": tax_values["tax_id"],
|
||||
"base_product_id": base_line.product_id,
|
||||
"tax_product_id": tax_line.product_id,
|
||||
"base_product_uom_id": base_line.product_uom_id,
|
||||
"tax_product_uom_id": tax_line.product_uom_id,
|
||||
"line_code": line_code,
|
||||
}
|
||||
|
||||
def l10n_in_filter_to_apply(tax_values):
|
||||
if tax_values["base_line_id"].is_rounding_line:
|
||||
return False
|
||||
return True
|
||||
|
||||
return move._prepare_edi_tax_details(
|
||||
filter_to_apply=l10n_in_filter_to_apply,
|
||||
grouping_key_generator=l10n_in_grouping_key_generator,
|
||||
)
|
||||
|
||||
@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)
|
||||
l10n_in_tax_details.setdefault("%s_rate" % (tax_detail["line_code"]), tax_detail["tax"].amount)
|
||||
l10n_in_tax_details.setdefault("%s_amount" % (tax_detail["line_code"]), 0.00)
|
||||
l10n_in_tax_details.setdefault("%s_amount_currency" % (tax_detail["line_code"]), 0.00)
|
||||
l10n_in_tax_details["%s_amount" % (tax_detail["line_code"])] += tax_detail["tax_amount"]
|
||||
l10n_in_tax_details["%s_amount_currency" % (tax_detail["line_code"])] += tax_detail["tax_amount_currency"]
|
||||
return l10n_in_tax_details
|
||||
|
||||
def _l10n_in_get_supply_type(self, move, tax_details_by_code):
|
||||
supply_type = "B2B"
|
||||
if move.l10n_in_gst_treatment in ("overseas", "special_economic_zone") and tax_details_by_code.get("igst_amount"):
|
||||
supply_type = move.l10n_in_gst_treatment == "overseas" and "EXPWP" or "SEZWP"
|
||||
elif move.l10n_in_gst_treatment in ("overseas", "special_economic_zone"):
|
||||
supply_type = move.l10n_in_gst_treatment == "overseas" and "EXPWOP" or "SEZWOP"
|
||||
elif move.l10n_in_gst_treatment == "deemed_export":
|
||||
supply_type = "DEXP"
|
||||
return supply_type
|
||||
|
||||
#================================ API methods ===========================
|
||||
|
||||
@api.model
|
||||
def _l10n_in_edi_no_config_response(self):
|
||||
return {'error': [{
|
||||
'code': '000',
|
||||
'message': _(
|
||||
"A username and password still needs to be set or it's wrong for the E-invoice(IN). "
|
||||
"It needs to be added and verify in the Settings."
|
||||
)}
|
||||
]}
|
||||
|
||||
@api.model
|
||||
def _l10n_in_edi_get_token(self, company):
|
||||
sudo_company = company.sudo()
|
||||
if sudo_company.l10n_in_edi_username and sudo_company._l10n_in_edi_token_is_valid():
|
||||
return sudo_company.l10n_in_edi_token
|
||||
elif sudo_company.l10n_in_edi_username and sudo_company.l10n_in_edi_password:
|
||||
self._l10n_in_edi_authenticate(company)
|
||||
return sudo_company.l10n_in_edi_token
|
||||
return False
|
||||
|
||||
@api.model
|
||||
def _l10n_in_edi_connect_to_server(self, company, url_path, params):
|
||||
user_token = self.env["iap.account"].get("l10n_in_edi")
|
||||
params.update({
|
||||
"account_token": user_token.account_token,
|
||||
"dbuuid": self.env["ir.config_parameter"].sudo().get_param("database.uuid"),
|
||||
"username": company.sudo().l10n_in_edi_username,
|
||||
"gstin": company.vat,
|
||||
})
|
||||
if company.sudo().l10n_in_edi_production_env:
|
||||
default_endpoint = DEFAULT_IAP_ENDPOINT
|
||||
else:
|
||||
default_endpoint = DEFAULT_IAP_TEST_ENDPOINT
|
||||
endpoint = self.env["ir.config_parameter"].sudo().get_param("l10n_in_edi.endpoint", default_endpoint)
|
||||
url = "%s%s" % (endpoint, url_path)
|
||||
try:
|
||||
return jsonrpc(url, params=params, timeout=25)
|
||||
except AccessError as e:
|
||||
_logger.warning("Connection error: %s", e.args[0])
|
||||
return {
|
||||
"error": [{
|
||||
"code": "404",
|
||||
"message": _("Unable to connect to the online E-invoice service."
|
||||
"The web service may be temporary down. Please try again in a moment.")
|
||||
}]
|
||||
}
|
||||
|
||||
@api.model
|
||||
def _l10n_in_edi_authenticate(self, company):
|
||||
params = {"password": company.sudo().l10n_in_edi_password}
|
||||
response = self._l10n_in_edi_connect_to_server(company, url_path="/iap/l10n_in_edi/1/authenticate", params=params)
|
||||
# validity data-time in Indian standard time(UTC+05:30) so remove that gap and store in odoo
|
||||
if "data" in response:
|
||||
local_time = fields.Datetime.context_timestamp(
|
||||
self.with_context(tz="Asia/Kolkata"),
|
||||
fields.Datetime.to_datetime(response["data"]["TokenExpiry"]
|
||||
))
|
||||
utc_time = local_time.astimezone(pytz.utc)
|
||||
company.sudo().l10n_in_edi_token_validity = fields.Datetime.to_string(utc_time)
|
||||
company.sudo().l10n_in_edi_token = response["data"]["AuthToken"]
|
||||
return response
|
||||
|
||||
@api.model
|
||||
def _l10n_in_edi_generate(self, company, json_payload):
|
||||
token = self._l10n_in_edi_get_token(company)
|
||||
if not token:
|
||||
return self._l10n_in_edi_no_config_response()
|
||||
params = {
|
||||
"auth_token": token,
|
||||
"json_payload": json_payload,
|
||||
}
|
||||
return self._l10n_in_edi_connect_to_server(company, url_path="/iap/l10n_in_edi/1/generate", params=params)
|
||||
|
||||
@api.model
|
||||
def _l10n_in_edi_get_irn_by_details(self, company, json_payload):
|
||||
token = self._l10n_in_edi_get_token(company)
|
||||
if not token:
|
||||
return self._l10n_in_edi_no_config_response()
|
||||
params = {
|
||||
"auth_token": token,
|
||||
}
|
||||
params.update(json_payload)
|
||||
return self._l10n_in_edi_connect_to_server(
|
||||
company,
|
||||
url_path="/iap/l10n_in_edi/1/getirnbydocdetails",
|
||||
params=params,
|
||||
)
|
||||
|
||||
@api.model
|
||||
def _l10n_in_edi_cancel(self, company, json_payload):
|
||||
token = self._l10n_in_edi_get_token(company)
|
||||
if not token:
|
||||
return self._l10n_in_edi_no_config_response()
|
||||
params = {
|
||||
"auth_token": token,
|
||||
"json_payload": json_payload,
|
||||
}
|
||||
return self._l10n_in_edi_connect_to_server(company, url_path="/iap/l10n_in_edi/1/cancel", params=params)
|
||||
@@ -0,0 +1,53 @@
|
||||
# -*- 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 {}
|
||||
@@ -0,0 +1,30 @@
|
||||
# -*- 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")
|
||||
@@ -0,0 +1,19 @@
|
||||
# -*- 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)
|
||||
@@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import test_edi_json
|
||||
@@ -0,0 +1,96 @@
|
||||
# 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")
|
||||
@@ -0,0 +1,15 @@
|
||||
<?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>
|
||||
@@ -0,0 +1,31 @@
|
||||
<?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&value=%s&width=%s&height=%s' %
|
||||
('QR', l10n_in_einvoice_json['SignedQRCode'], 200, 200)"/>
|
||||
</p>
|
||||
</xpath>
|
||||
</template>
|
||||
</odoo>
|
||||
@@ -0,0 +1,40 @@
|
||||
<?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>
|
||||
Reference in New Issue
Block a user