19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:31:21 +01:00
parent 7dc55599c6
commit 7f43bbbfcc
650 changed files with 45260 additions and 33436 deletions

View file

@ -5,12 +5,10 @@ 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 -> Configuration -> 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>
@ -22,39 +20,15 @@ pip install odoo-bringout-oca-ocb-l10n_in_edi
## Dependencies
This addon depends on:
- account_edi
- l10n_in
- iap
## Manifest Information
- **Name**: Indian - E-invoicing
- **Version**: 1.03.00
- **Category**: Accounting/Localizations/EDI
- **License**: LGPL-3
- **Installable**: True
## Source
Based on [OCA/OCB](https://github.com/OCA/OCB) branch 16.0, addon `l10n_in_edi`.
- Repository: https://github.com/OCA/OCB
- Branch: 19.0
- Path: addons/l10n_in_edi
## License
This package maintains the original LGPL-3 license from the upstream Odoo project.
## Documentation
- Overview: doc/OVERVIEW.md
- Architecture: doc/ARCHITECTURE.md
- Models: doc/MODELS.md
- Controllers: doc/CONTROLLERS.md
- Wizards: doc/WIZARDS.md
- Reports: doc/REPORTS.md
- Security: doc/SECURITY.md
- Install: doc/INSTALL.md
- Usage: doc/USAGE.md
- Configuration: doc/CONFIGURATION.md
- Dependencies: doc/DEPENDENCIES.md
- Troubleshooting: doc/TROUBLESHOOTING.md
- FAQ: doc/FAQ.md
This package preserves the original LGPL-3 license.

View file

@ -1,4 +1,2 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import models
from . import wizard

View file

@ -1,38 +1,34 @@
# -*- 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": [
'name': "Indian - E-invoicing",
'version': "1.03.00",
'countries': ['in'],
'category': "Accounting/Localizations/EDI",
'depends': [
"account_edi",
"l10n_in",
"iap",
],
"description": """
'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 -> Configuration -> 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",
'data': [
'security/ir.model.access.csv',
'views/account_move_views.xml',
'views/edi_pdf_report.xml',
'views/res_config_settings_views.xml',
'wizard/l10n_in_edi_cancel_views.xml',
],
"demo": [
'demo': [
"demo/demo_company.xml",
],
"installable": True,
# only applicable for taxpayers turnover higher than Rs.50 crore so auto_install is False
"license": "LGPL-3",
'installable': True,
'author': "Odoo S.A.",
'license': "LGPL-3",
}

View file

@ -1,7 +0,0 @@
<?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>

View file

@ -1,7 +1,6 @@
-- disable l10n_in_edi integration
UPDATE res_company
SET l10n_in_edi_production_env = false,
l10n_in_edi_username = NULL,
SET l10n_in_edi_username = NULL,
l10n_in_edi_password = NULL,
l10n_in_edi_token = NULL,
l10n_in_edi_token_validity = NULL;

View file

@ -1,7 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- This is testing credentials -->
<record id="l10n_in.demo_company_in" model="res.company">
<record id="base.demo_company_in" model="res.company">
<field name="l10n_in_edi_feature">True</field>
<field name="l10n_in_edi_username">MGSTTEST</field>
<field name="l10n_in_edi_password">mgst@123</field>
</record>

View file

@ -0,0 +1,633 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * l10n_in_edi
#
# Weblate <noreply-mt-weblate@weblate.org>, 2025.
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 19.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-02-20 19:05+0000\n"
"PO-Revision-Date: 2025-11-17 03:12+0000\n"
"Last-Translator: Weblate <noreply-mt-weblate@weblate.org>\n"
"Language-Team: Hindi <https://translate.odoo.com/projects/odoo-19-l10n/"
"l10n_in_edi/hi/>\n"
"Language: hi\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n==0 || n==1);\n"
"X-Generator: Weblate 5.12.2\n"
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/account_move.py:0
msgid ""
"\n"
"\n"
"You must contact your system administrator to update the GSP."
msgstr ""
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/res_partner.py:0
msgid "- City required min 3 and max 100 characters"
msgstr ""
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/account_move.py:0
msgid "- Email: invalid or longer than 100 characters."
msgstr ""
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/account_move.py:0
msgid "- Phone number: must be 1012 digits."
msgstr ""
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/res_partner.py:0
msgid "- State TIN Number must be exactly 2 digits."
msgstr ""
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/res_partner.py:0
msgid "- State required min 3 and max 50 characters"
msgstr ""
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/res_partner.py:0
msgid "- Street required min 3 and max 100 characters"
msgstr ""
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/account_move.py:0
msgid "- Street2: must be 3100 characters."
msgstr ""
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/res_partner.py:0
msgid "- ZIP code required 6 digits ranging from 100000 to 999999"
msgstr ""
#. module: l10n_in_edi
#: model_terms:ir.ui.view,arch_db:l10n_in_edi.l10n_in_einvoice_report_invoice_document_inherit
msgid "<strong>Acknowledgement</strong>"
msgstr ""
#. module: l10n_in_edi
#: model_terms:ir.ui.view,arch_db:l10n_in_edi.l10n_in_einvoice_report_invoice_document_inherit
msgid "<strong>IRN:</strong>"
msgstr ""
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/res_config_settings.py:0
msgid "API credentials validated successfully"
msgstr ""
#. module: l10n_in_edi
#: model:ir.model,name:l10n_in_edi.model_account_move_send
msgid "Account Move Send"
msgstr "अकाउंट मूव सेंड"
#. module: l10n_in_edi
#: model_terms:ir.ui.view,arch_db:l10n_in_edi.invoice_form_inherit_l10n_in_edi
msgid ""
"Are you sure you want to cancel this invoice without waiting for the EDI "
"document to be canceled?"
msgstr "क्या आप इनवॉइस को ईडीआई दस्तावेज़ के रद्द हुए बिना ही रद्द करना चाहते हैं?"
#. module: l10n_in_edi
#: model:ir.model,name:l10n_in_edi.model_ir_attachment
msgid "Attachment"
msgstr "अटैचमेंट"
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/account_move.py:0
#: model:ir.model,name:l10n_in_edi.model_l10n_in_edi_cancel
#: model_terms:ir.ui.view,arch_db:l10n_in_edi.view_ewaybill_cancel_form
msgid "Cancel E-Invoice"
msgstr ""
#. module: l10n_in_edi
#: model:ir.model.fields,field_description:l10n_in_edi.field_l10n_in_edi_cancel__cancel_reason
msgid "Cancel Reason"
msgstr ""
#. module: l10n_in_edi
#: model:ir.model.fields,field_description:l10n_in_edi.field_l10n_in_edi_cancel__cancel_remarks
msgid "Cancel Remarks"
msgstr ""
#. module: l10n_in_edi
#: model:ir.model.fields.selection,name:l10n_in_edi.selection__account_move__l10n_in_edi_status__cancelled
msgid "Cancelled"
msgstr "रद्द किया गया"
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/res_company.py:0
msgid "Check Company Data"
msgstr ""
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/account_move_line.py:0
msgid "Check Invoice Line(s)"
msgstr ""
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/account_move.py:0
msgid "Check Invoices"
msgstr ""
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/res_partner.py:0
msgid "Check Partner Data"
msgstr ""
#. module: l10n_in_edi
#: model:ir.model,name:l10n_in_edi.model_res_company
msgid "Companies"
msgstr "कंपनियां"
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/res_company.py:0
msgid ""
"Companies should have a complete address, verify their Street, City, State, "
"Country and Zip code."
msgstr ""
#. module: l10n_in_edi
#: model:ir.model,name:l10n_in_edi.model_res_config_settings
msgid "Config Settings"
msgstr "कॉन्फ़िगरेशन सेटिंग"
#. module: l10n_in_edi
#: model:ir.model,name:l10n_in_edi.model_res_partner
msgid "Contact"
msgstr "संपर्क"
#. module: l10n_in_edi
#: model:ir.model,website_form_label:l10n_in_edi.model_res_partner
msgid "Create a Customer"
msgstr "ग्राहक बनाएं"
#. module: l10n_in_edi
#: model:ir.model.fields,field_description:l10n_in_edi.field_l10n_in_edi_cancel__create_uid
msgid "Created by"
msgstr "इन्होंने बनाया"
#. module: l10n_in_edi
#: model:ir.model.fields,field_description:l10n_in_edi.field_l10n_in_edi_cancel__create_date
msgid "Created on"
msgstr "इस तारीख को बनाया गया"
#. module: l10n_in_edi
#: model:ir.model.fields.selection,name:l10n_in_edi.selection__account_move__l10n_in_edi_cancel_reason__2
#: model:ir.model.fields.selection,name:l10n_in_edi.selection__l10n_in_edi_cancel__cancel_reason__2
msgid "Data Entry Mistake"
msgstr ""
#. module: l10n_in_edi
#: model_terms:ir.ui.view,arch_db:l10n_in_edi.view_ewaybill_cancel_form
msgid "Discard"
msgstr "खारिज करें"
#. module: l10n_in_edi
#: model:ir.model.fields,field_description:l10n_in_edi.field_account_move__display_name
#: model:ir.model.fields,field_description:l10n_in_edi.field_account_move_line__display_name
#: model:ir.model.fields,field_description:l10n_in_edi.field_account_move_send__display_name
#: model:ir.model.fields,field_description:l10n_in_edi.field_ir_attachment__display_name
#: model:ir.model.fields,field_description:l10n_in_edi.field_l10n_in_edi_cancel__display_name
#: model:ir.model.fields,field_description:l10n_in_edi.field_res_company__display_name
#: model:ir.model.fields,field_description:l10n_in_edi.field_res_config_settings__display_name
#: model:ir.model.fields,field_description:l10n_in_edi.field_res_partner__display_name
msgid "Display Name"
msgstr "डिस्प्ले का नाम"
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/account_move.py:0
msgid "Documentation"
msgstr ""
#. module: l10n_in_edi
#: model_terms:ir.ui.view,arch_db:l10n_in_edi.invoice_form_inherit_l10n_in_edi
msgid "Download EDI JSON"
msgstr ""
#. module: l10n_in_edi
#: model:ir.model.fields.selection,name:l10n_in_edi.selection__account_move__l10n_in_edi_cancel_reason__1
#: model:ir.model.fields.selection,name:l10n_in_edi.selection__l10n_in_edi_cancel__cancel_reason__1
msgid "Duplicate"
msgstr "नकली"
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/account_move.py:0
msgid ""
"Duplicate IRN found for this invoice, but the buyer details or invoice "
"values do not match."
msgstr ""
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/account_move.py:0
msgid ""
"E-Invoice has been cancelled successfully. Cancellation Reason: %(reason)s "
"and Cancellation Remark: %(remark)s"
msgstr ""
#. module: l10n_in_edi
#: model:ir.model.fields,field_description:l10n_in_edi.field_account_bank_statement_line__l10n_in_edi_attachment_id
#: model:ir.model.fields,field_description:l10n_in_edi.field_account_move__l10n_in_edi_attachment_id
msgid "E-Invoice(IN) Attachment"
msgstr ""
#. module: l10n_in_edi
#: model:ir.model.fields,field_description:l10n_in_edi.field_account_bank_statement_line__l10n_in_edi_cancel_reason
#: model:ir.model.fields,field_description:l10n_in_edi.field_account_move__l10n_in_edi_cancel_reason
msgid "E-Invoice(IN) Cancel Reason"
msgstr ""
#. module: l10n_in_edi
#: model:ir.model.fields,field_description:l10n_in_edi.field_account_bank_statement_line__l10n_in_edi_cancel_remarks
#: model:ir.model.fields,field_description:l10n_in_edi.field_account_move__l10n_in_edi_cancel_remarks
msgid "E-Invoice(IN) Cancel Remarks"
msgstr ""
#. module: l10n_in_edi
#: model:ir.model.fields,field_description:l10n_in_edi.field_account_bank_statement_line__l10n_in_edi_content
#: model:ir.model.fields,field_description:l10n_in_edi.field_account_move__l10n_in_edi_content
msgid "E-Invoice(IN) Content"
msgstr ""
#. module: l10n_in_edi
#: model:ir.model.fields,field_description:l10n_in_edi.field_account_bank_statement_line__l10n_in_edi_attachment_file
#: model:ir.model.fields,field_description:l10n_in_edi.field_account_move__l10n_in_edi_attachment_file
msgid "E-Invoice(IN) File"
msgstr ""
#. module: l10n_in_edi
#: model:ir.model.fields,field_description:l10n_in_edi.field_res_company__l10n_in_edi_password
msgid "E-invoice (IN) Password"
msgstr ""
#. module: l10n_in_edi
#: model:ir.model.fields,field_description:l10n_in_edi.field_res_company__l10n_in_edi_token
msgid "E-invoice (IN) Token"
msgstr ""
#. module: l10n_in_edi
#: model:ir.model.fields,field_description:l10n_in_edi.field_res_company__l10n_in_edi_username
msgid "E-invoice (IN) Username"
msgstr ""
#. module: l10n_in_edi
#: model:ir.model.fields,field_description:l10n_in_edi.field_res_company__l10n_in_edi_token_validity
msgid "E-invoice (IN) Valid Until"
msgstr ""
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/account_move.py:0
msgid "E-invoice submitted successfully."
msgstr ""
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/account_move_send.py:0
msgid "E-invoicing"
msgstr ""
#. module: l10n_in_edi
#: model_terms:ir.ui.view,arch_db:l10n_in_edi.invoice_form_inherit_l10n_in_edi
msgid "EDI Cancel Reason"
msgstr ""
#. module: l10n_in_edi
#: model_terms:ir.ui.view,arch_db:l10n_in_edi.invoice_form_inherit_l10n_in_edi
msgid "EDI Cancel Remarks"
msgstr ""
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/account_move.py:0
msgid "Ensure GST Number set on company setting and API are Verified."
msgstr ""
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/account_move_send.py:0
msgid "Error when sending the invoice to government:"
msgstr ""
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/account_move.py:0
msgid "Following:"
msgstr ""
#. module: l10n_in_edi
#: model_terms:ir.ui.view,arch_db:l10n_in_edi.invoice_form_inherit_l10n_in_edi
msgid "Force Cancel"
msgstr "जबरदस्ती रद्द करें"
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/account_move.py:0
msgid "Force cancelled %(invoice)s by %(username)s"
msgstr ""
#. module: l10n_in_edi
#: model:ir.model.fields,field_description:l10n_in_edi.field_account_move__id
#: model:ir.model.fields,field_description:l10n_in_edi.field_account_move_line__id
#: model:ir.model.fields,field_description:l10n_in_edi.field_account_move_send__id
#: model:ir.model.fields,field_description:l10n_in_edi.field_ir_attachment__id
#: model:ir.model.fields,field_description:l10n_in_edi.field_l10n_in_edi_cancel__id
#: model:ir.model.fields,field_description:l10n_in_edi.field_res_company__id
#: model:ir.model.fields,field_description:l10n_in_edi.field_res_config_settings__id
#: model:ir.model.fields,field_description:l10n_in_edi.field_res_partner__id
msgid "ID"
msgstr "आईडी"
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/res_config_settings.py:0
msgid ""
"Incorrect username or password, or the GST number on company does not match."
msgstr ""
#. module: l10n_in_edi
#: model:ir.model.fields,field_description:l10n_in_edi.field_account_bank_statement_line__l10n_in_edi_status
#: model:ir.model.fields,field_description:l10n_in_edi.field_account_move__l10n_in_edi_status
msgid "India E-Invoice Status"
msgstr ""
#. module: l10n_in_edi
#: model_terms:ir.ui.view,arch_db:l10n_in_edi.l10n_in_edi_inherit_account_move_search_view
msgid "Indian E-Invoices In Error"
msgstr ""
#. module: l10n_in_edi
#: model_terms:ir.ui.view,arch_db:l10n_in_edi.l10n_in_edi_inherit_account_move_search_view
msgid "Indian E-Invoices To Send"
msgstr ""
#. module: l10n_in_edi
#: model:ir.model.fields,field_description:l10n_in_edi.field_res_company__l10n_in_edi_feature
#: model:ir.model.fields,field_description:l10n_in_edi.field_res_config_settings__l10n_in_edi_feature
msgid "Indian E-Invoicing"
msgstr ""
#. module: l10n_in_edi
#: model_terms:ir.ui.view,arch_db:l10n_in_edi.l10n_in_edi_inherit_account_move_search_view
msgid "Indian E-invoice status"
msgstr ""
#. module: l10n_in_edi
#: model:ir.model.fields,field_description:l10n_in_edi.field_res_config_settings__l10n_in_edi_password
msgid "Indian EDI password"
msgstr ""
#. module: l10n_in_edi
#: model:ir.model.fields,field_description:l10n_in_edi.field_res_config_settings__l10n_in_edi_username
msgid "Indian EDI username"
msgstr ""
#. module: l10n_in_edi
#: model:ir.model.fields,field_description:l10n_in_edi.field_l10n_in_edi_cancel__move_id
msgid "Invoice"
msgstr "बीजक"
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/account_move.py:0
msgid "Invoice number should not be more than 16 characters"
msgstr ""
#. module: l10n_in_edi
#: model:ir.model,name:l10n_in_edi.model_account_move
msgid "Journal Entry"
msgstr "जर्नल एंट्री"
#. module: l10n_in_edi
#: model:ir.model,name:l10n_in_edi.model_account_move_line
msgid "Journal Item"
msgstr "जर्नल आइटम"
#. module: l10n_in_edi
#: model:ir.model.fields,field_description:l10n_in_edi.field_account_bank_statement_line__l10n_in_edi_error
#: model:ir.model.fields,field_description:l10n_in_edi.field_account_move__l10n_in_edi_error
msgid "L10N In Edi Error"
msgstr ""
#. module: l10n_in_edi
#: model:ir.model.fields,field_description:l10n_in_edi.field_l10n_in_edi_cancel__write_uid
msgid "Last Updated by"
msgstr "इन्होंने आखिरी बार अपडेट किया"
#. module: l10n_in_edi
#: model:ir.model.fields,field_description:l10n_in_edi.field_l10n_in_edi_cancel__write_date
msgid "Last Updated on"
msgstr "आखिरी बार अपडेट हुआ"
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/account_move_line.py:0
msgid ""
"Missing or invalid HSN/SAC code: Ensure that invoice lines contain 4, 6 or 8 "
"digits"
msgstr ""
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/account_move_line.py:0
msgid "Negative discount is not allowed"
msgstr ""
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/account_move.py:0
msgid ""
"Negative lines will be decreased from positive invoice lines having the same "
"taxes and HSN code"
msgstr ""
#. module: l10n_in_edi
#: model:ir.model.fields.selection,name:l10n_in_edi.selection__account_move__l10n_in_edi_cancel_reason__3
#: model:ir.model.fields.selection,name:l10n_in_edi.selection__l10n_in_edi_cancel__cancel_reason__3
msgid "Order Cancelled"
msgstr ""
#. module: l10n_in_edi
#: model:ir.model.fields.selection,name:l10n_in_edi.selection__account_move__l10n_in_edi_cancel_reason__4
#: model:ir.model.fields.selection,name:l10n_in_edi.selection__l10n_in_edi_cancel__cancel_reason__4
msgid "Others"
msgstr "अन्य"
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/res_partner.py:0
msgid ""
"Partners should have a complete address, verify their Street, City, State, "
"Country and Zip code."
msgstr ""
#. module: l10n_in_edi
#: model_terms:ir.ui.view,arch_db:l10n_in_edi.res_config_settings_view_form_inherit_l10n_in_edi
msgid "Password"
msgstr "पासवर्ड"
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/account_move.py:0
msgid ""
"Retrying to send cancellation request for E-Invoice to government portal."
msgstr ""
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/account_move.py:0
msgid "Retrying to send your E-Invoice to government portal."
msgstr ""
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/account_move_send.py:0
msgid ""
"Send the e-invoice json to the Indian Invoice Registration Portal (IRP)."
msgstr ""
#. module: l10n_in_edi
#: model:ir.model.fields.selection,name:l10n_in_edi.selection__account_move__l10n_in_edi_status__sent
msgid "Sent"
msgstr "भेजा गया"
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/account_move_line.py:0
msgid ""
"Set an appropriate GST tax on invoice lines (if it's zero rated or nil rated "
"then apply it too)"
msgstr ""
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/account_move.py:0
msgid ""
"Somehow this invoice had been cancelled to government before.%(br)sNormally, "
"this should not happen too often%(br)sJust verify by logging into government "
"website %(link)s"
msgstr ""
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/account_move.py:0
msgid ""
"Somehow this invoice has been submited to government before.%(br)sNormally, "
"this should not happen too often%(br)sJust verify value of invoice by upload "
"json to government website %(link)s."
msgstr ""
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/account_move.py:0
msgid "This electronic document is being processed already."
msgstr ""
#. module: l10n_in_edi
#: model:ir.model.fields.selection,name:l10n_in_edi.selection__account_move__l10n_in_edi_status__to_send
msgid "To Send"
msgstr "भेजने के लिए"
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/res_company.py:0
msgid ""
"Unable to connect to the online E-invoice service. The web service may be "
"temporary down. Please try again in a moment."
msgstr ""
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/account_move.py:0
msgid ""
"Unable to connect to the online E-invoice service.The web service may be "
"temporary down. Please try again in a moment."
msgstr ""
#. module: l10n_in_edi
#: model_terms:ir.ui.view,arch_db:l10n_in_edi.res_config_settings_view_form_inherit_l10n_in_edi
msgid "Username"
msgstr ""
#. module: l10n_in_edi
#: model_terms:ir.ui.view,arch_db:l10n_in_edi.res_config_settings_view_form_inherit_l10n_in_edi
msgid "Verify Username and Password"
msgstr ""
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/res_company.py:0
#: code:addons/l10n_in_edi/models/res_partner.py:0
msgid "View %s"
msgstr ""
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/res_company.py:0
msgid "View Companies"
msgstr ""
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/account_move_line.py:0
msgid "View Invoice Line(s)"
msgstr ""
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/account_move.py:0
msgid "View Invoices"
msgstr ""
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/res_partner.py:0
msgid "View Partners"
msgstr "पार्टनर देखें"
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/ir_attachment.py:0
msgid "You can't unlink an attachment that you received from the government"
msgstr ""
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/account_move.py:0
msgid "here"
msgstr "क्लिक करें"
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/account_move.py:0
msgid ""
"⚠️ Important Notice GSP Deprecation \n"
"The currently selected GSP (Tera Soft) will be deprecated soon.\n"
"To ensure uninterrupted e-Invoice and E-way operations, please switch to BVM "
"GSP as per the"
msgstr ""

View file

@ -4,10 +4,10 @@
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.0+e\n"
"Project-Id-Version: Odoo Server 19.0+e\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-08-20 11:49+0000\n"
"PO-Revision-Date: 2025-08-20 11:49+0000\n"
"POT-Creation-Date: 2026-02-20 19:05+0000\n"
"PO-Revision-Date: 2026-02-20 19:05+0000\n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
@ -17,70 +17,64 @@ msgstr ""
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/account_edi_format.py:0
#, python-format
#: code:addons/l10n_in_edi/models/account_move.py:0
msgid ""
"\n"
"\n"
"You must contact your system administrator to update the GSP."
msgstr ""
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/res_partner.py:0
msgid "- City required min 3 and max 100 characters"
msgstr ""
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/account_edi_format.py:0
#, python-format
msgid "- Email address should be valid and not more then 100 characters"
#: code:addons/l10n_in_edi/models/account_move.py:0
msgid "- Email: invalid or longer than 100 characters."
msgstr ""
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/account_edi_format.py:0
#, python-format
msgid "- Mobile number should be minimum 10 or maximum 12 digits"
#: code:addons/l10n_in_edi/models/account_move.py:0
msgid "- Phone number: must be 1012 digits."
msgstr ""
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/account_edi_format.py:0
#, python-format
#: code:addons/l10n_in_edi/models/res_partner.py:0
msgid "- State TIN Number must be exactly 2 digits."
msgstr ""
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/account_edi_format.py:0
#, python-format
#: code:addons/l10n_in_edi/models/res_partner.py:0
msgid "- State required min 3 and max 50 characters"
msgstr ""
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/account_edi_format.py:0
#, python-format
#: code:addons/l10n_in_edi/models/res_partner.py:0
msgid "- Street required min 3 and max 100 characters"
msgstr ""
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/account_edi_format.py:0
#, python-format
msgid "- Street2 should be min 3 and max 100 characters"
#: code:addons/l10n_in_edi/models/account_move.py:0
msgid "- Street2: must be 3100 characters."
msgstr ""
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/account_edi_format.py:0
#, python-format
msgid "- Zip code required 6 digits"
msgstr ""
#. module: l10n_in_edi
#: model_terms:ir.ui.view,arch_db:l10n_in_edi.res_config_settings_view_form_inherit_l10n_in_edi
msgid ""
"<span class=\"o_form_label\">Setup E-invoice</span>\n"
" <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\"/>"
#: code:addons/l10n_in_edi/models/res_partner.py:0
msgid "- ZIP code required 6 digits ranging from 100000 to 999999"
msgstr ""
#. module: l10n_in_edi
#: model_terms:ir.ui.view,arch_db:l10n_in_edi.l10n_in_einvoice_report_invoice_document_inherit
msgid "<strong>Acknowledgement:</strong>"
msgid "<strong>Acknowledgement</strong>"
msgstr ""
#. module: l10n_in_edi
@ -91,39 +85,71 @@ msgstr ""
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/res_config_settings.py:0
#, python-format
msgid "API credentials validated successfully"
msgstr ""
#. module: l10n_in_edi
#: model:ir.model,name:l10n_in_edi.model_account_move_send
msgid "Account Move Send"
msgstr ""
#. module: l10n_in_edi
#: model_terms:ir.ui.view,arch_db:l10n_in_edi.invoice_form_inherit_l10n_in_edi
msgid ""
"Are you sure you want to cancel this invoice without waiting for the EDI "
"document to be canceled?"
msgstr ""
#. module: l10n_in_edi
#: model:ir.model,name:l10n_in_edi.model_ir_attachment
msgid "Attachment"
msgstr ""
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/account_edi_format.py:0
#, python-format
msgid "Buy Credits"
#: code:addons/l10n_in_edi/models/account_move.py:0
#: model:ir.model,name:l10n_in_edi.model_l10n_in_edi_cancel
#: model_terms:ir.ui.view,arch_db:l10n_in_edi.view_ewaybill_cancel_form
msgid "Cancel E-Invoice"
msgstr ""
#. module: l10n_in_edi
#: model_terms:ir.ui.view,arch_db:l10n_in_edi.res_config_settings_view_form_inherit_l10n_in_edi
msgid "Buy credits"
#: model:ir.model.fields,field_description:l10n_in_edi.field_l10n_in_edi_cancel__cancel_reason
msgid "Cancel Reason"
msgstr ""
#. module: l10n_in_edi
#: model:ir.model.fields,field_description:l10n_in_edi.field_account_bank_statement_line__l10n_in_edi_cancel_reason
#: model:ir.model.fields,field_description:l10n_in_edi.field_account_move__l10n_in_edi_cancel_reason
#: model:ir.model.fields,field_description:l10n_in_edi.field_account_payment__l10n_in_edi_cancel_reason
msgid "Cancel reason"
#: model:ir.model.fields,field_description:l10n_in_edi.field_l10n_in_edi_cancel__cancel_remarks
msgid "Cancel Remarks"
msgstr ""
#. module: l10n_in_edi
#: model:ir.model.fields,field_description:l10n_in_edi.field_account_bank_statement_line__l10n_in_edi_cancel_remarks
#: model:ir.model.fields,field_description:l10n_in_edi.field_account_move__l10n_in_edi_cancel_remarks
#: model:ir.model.fields,field_description:l10n_in_edi.field_account_payment__l10n_in_edi_cancel_remarks
msgid "Cancel remarks"
#: model:ir.model.fields.selection,name:l10n_in_edi.selection__account_move__l10n_in_edi_status__cancelled
msgid "Cancelled"
msgstr ""
#. module: l10n_in_edi
#: model_terms:ir.ui.view,arch_db:l10n_in_edi.res_config_settings_view_form_inherit_l10n_in_edi
msgid "Check the"
#. odoo-python
#: code:addons/l10n_in_edi/models/res_company.py:0
msgid "Check Company Data"
msgstr ""
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/account_move_line.py:0
msgid "Check Invoice Line(s)"
msgstr ""
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/account_move.py:0
msgid "Check Invoices"
msgstr ""
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/res_partner.py:0
msgid "Check Partner Data"
msgstr ""
#. module: l10n_in_edi
@ -131,31 +157,123 @@ msgstr ""
msgid "Companies"
msgstr ""
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/res_company.py:0
msgid ""
"Companies should have a complete address, verify their Street, City, State, "
"Country and Zip code."
msgstr ""
#. module: l10n_in_edi
#: model:ir.model,name:l10n_in_edi.model_res_config_settings
msgid "Config Settings"
msgstr ""
#. module: l10n_in_edi
#: model_terms:ir.ui.view,arch_db:l10n_in_edi.res_config_settings_view_form_inherit_l10n_in_edi
msgid ""
"Costs 1 credit per transaction. Free 200 credits will be available for the "
"first time."
#: model:ir.model,name:l10n_in_edi.model_res_partner
msgid "Contact"
msgstr ""
#. module: l10n_in_edi
#: model:ir.model,website_form_label:l10n_in_edi.model_res_partner
msgid "Create a Customer"
msgstr ""
#. module: l10n_in_edi
#: model:ir.model.fields,field_description:l10n_in_edi.field_l10n_in_edi_cancel__create_uid
msgid "Created by"
msgstr ""
#. module: l10n_in_edi
#: model:ir.model.fields,field_description:l10n_in_edi.field_l10n_in_edi_cancel__create_date
msgid "Created on"
msgstr ""
#. module: l10n_in_edi
#: model:ir.model.fields.selection,name:l10n_in_edi.selection__account_move__l10n_in_edi_cancel_reason__2
#: model:ir.model.fields.selection,name:l10n_in_edi.selection__l10n_in_edi_cancel__cancel_reason__2
msgid "Data Entry Mistake"
msgstr ""
#. module: l10n_in_edi
#: model_terms:ir.ui.view,arch_db:l10n_in_edi.view_ewaybill_cancel_form
msgid "Discard"
msgstr ""
#. module: l10n_in_edi
#: model:ir.model.fields,field_description:l10n_in_edi.field_account_move__display_name
#: model:ir.model.fields,field_description:l10n_in_edi.field_account_move_line__display_name
#: model:ir.model.fields,field_description:l10n_in_edi.field_account_move_send__display_name
#: model:ir.model.fields,field_description:l10n_in_edi.field_ir_attachment__display_name
#: model:ir.model.fields,field_description:l10n_in_edi.field_l10n_in_edi_cancel__display_name
#: model:ir.model.fields,field_description:l10n_in_edi.field_res_company__display_name
#: model:ir.model.fields,field_description:l10n_in_edi.field_res_config_settings__display_name
#: model:ir.model.fields,field_description:l10n_in_edi.field_res_partner__display_name
msgid "Display Name"
msgstr ""
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/account_move.py:0
msgid "Documentation"
msgstr ""
#. module: l10n_in_edi
#: model_terms:ir.ui.view,arch_db:l10n_in_edi.invoice_form_inherit_l10n_in_edi
msgid "Download EDI JSON"
msgstr ""
#. module: l10n_in_edi
#: model:ir.model.fields.selection,name:l10n_in_edi.selection__account_move__l10n_in_edi_cancel_reason__1
#: model:ir.model.fields.selection,name:l10n_in_edi.selection__l10n_in_edi_cancel__cancel_reason__1
msgid "Duplicate"
msgstr ""
#. module: l10n_in_edi
#: model:ir.model.fields,field_description:l10n_in_edi.field_res_company__l10n_in_edi_production_env
msgid "E-invoice (IN) Is production OSE environment"
#. odoo-python
#: code:addons/l10n_in_edi/models/account_move.py:0
msgid ""
"Duplicate IRN found for this invoice, but the buyer details or invoice "
"values do not match."
msgstr ""
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/account_move.py:0
msgid ""
"E-Invoice has been cancelled successfully. Cancellation Reason: %(reason)s "
"and Cancellation Remark: %(remark)s"
msgstr ""
#. module: l10n_in_edi
#: model:ir.model.fields,field_description:l10n_in_edi.field_account_bank_statement_line__l10n_in_edi_attachment_id
#: model:ir.model.fields,field_description:l10n_in_edi.field_account_move__l10n_in_edi_attachment_id
msgid "E-Invoice(IN) Attachment"
msgstr ""
#. module: l10n_in_edi
#: model:ir.model.fields,field_description:l10n_in_edi.field_account_bank_statement_line__l10n_in_edi_cancel_reason
#: model:ir.model.fields,field_description:l10n_in_edi.field_account_move__l10n_in_edi_cancel_reason
msgid "E-Invoice(IN) Cancel Reason"
msgstr ""
#. module: l10n_in_edi
#: model:ir.model.fields,field_description:l10n_in_edi.field_account_bank_statement_line__l10n_in_edi_cancel_remarks
#: model:ir.model.fields,field_description:l10n_in_edi.field_account_move__l10n_in_edi_cancel_remarks
msgid "E-Invoice(IN) Cancel Remarks"
msgstr ""
#. module: l10n_in_edi
#: model:ir.model.fields,field_description:l10n_in_edi.field_account_bank_statement_line__l10n_in_edi_content
#: model:ir.model.fields,field_description:l10n_in_edi.field_account_move__l10n_in_edi_content
msgid "E-Invoice(IN) Content"
msgstr ""
#. module: l10n_in_edi
#: model:ir.model.fields,field_description:l10n_in_edi.field_account_bank_statement_line__l10n_in_edi_attachment_file
#: model:ir.model.fields,field_description:l10n_in_edi.field_account_move__l10n_in_edi_attachment_file
msgid "E-Invoice(IN) File"
msgstr ""
#. module: l10n_in_edi
@ -179,55 +297,100 @@ msgid "E-invoice (IN) Valid Until"
msgstr ""
#. module: l10n_in_edi
#: model:ir.model.fields,field_description:l10n_in_edi.field_account_bank_statement_line__l10n_in_edi_show_cancel
#: model:ir.model.fields,field_description:l10n_in_edi.field_account_move__l10n_in_edi_show_cancel
#: model:ir.model.fields,field_description:l10n_in_edi.field_account_payment__l10n_in_edi_show_cancel
msgid "E-invoice(IN) is sent?"
msgstr ""
#. module: l10n_in_edi
#: model:ir.model,name:l10n_in_edi.model_account_edi_format
msgid "EDI format"
msgstr ""
#. module: l10n_in_edi
#: model:ir.model.fields,help:l10n_in_edi.field_res_company__l10n_in_edi_production_env
#: model:ir.model.fields,help:l10n_in_edi.field_res_config_settings__l10n_in_edi_production_env
msgid "Enable the use of production credentials"
#. odoo-python
#: code:addons/l10n_in_edi/models/account_move.py:0
msgid "E-invoice submitted successfully."
msgstr ""
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/account_edi_format.py:0
#, python-format
#: code:addons/l10n_in_edi/models/account_move_send.py:0
msgid "E-invoicing"
msgstr ""
#. module: l10n_in_edi
#: model_terms:ir.ui.view,arch_db:l10n_in_edi.invoice_form_inherit_l10n_in_edi
msgid "EDI Cancel Reason"
msgstr ""
#. module: l10n_in_edi
#: model_terms:ir.ui.view,arch_db:l10n_in_edi.invoice_form_inherit_l10n_in_edi
msgid "EDI Cancel Remarks"
msgstr ""
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/account_move.py:0
msgid "Ensure GST Number set on company setting and API are Verified."
msgstr ""
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/res_config_settings.py:0
#, python-format
msgid "Go to Company"
#: code:addons/l10n_in_edi/models/account_move_send.py:0
msgid "Error when sending the invoice to government:"
msgstr ""
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/account_edi_format.py:0
#, python-format
msgid "HSN code is not set in product %s"
#: code:addons/l10n_in_edi/models/account_move.py:0
msgid "Following:"
msgstr ""
#. module: l10n_in_edi
#: model_terms:ir.ui.view,arch_db:l10n_in_edi.invoice_form_inherit_l10n_in_edi
msgid "Force Cancel"
msgstr ""
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/account_move.py:0
msgid "Force cancelled %(invoice)s by %(username)s"
msgstr ""
#. module: l10n_in_edi
#: model:ir.model.fields,field_description:l10n_in_edi.field_account_move__id
#: model:ir.model.fields,field_description:l10n_in_edi.field_account_move_line__id
#: model:ir.model.fields,field_description:l10n_in_edi.field_account_move_send__id
#: model:ir.model.fields,field_description:l10n_in_edi.field_ir_attachment__id
#: model:ir.model.fields,field_description:l10n_in_edi.field_l10n_in_edi_cancel__id
#: model:ir.model.fields,field_description:l10n_in_edi.field_res_company__id
#: model:ir.model.fields,field_description:l10n_in_edi.field_res_config_settings__id
#: model:ir.model.fields,field_description:l10n_in_edi.field_res_partner__id
msgid "ID"
msgstr ""
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/res_config_settings.py:0
#, python-format
msgid ""
"Incorrect username or password, or the GST number on company does not match."
msgstr ""
#. module: l10n_in_edi
#: model:ir.model.fields,field_description:l10n_in_edi.field_res_config_settings__l10n_in_edi_production_env
msgid "Indian EDI Testing Environment"
#: model:ir.model.fields,field_description:l10n_in_edi.field_account_bank_statement_line__l10n_in_edi_status
#: model:ir.model.fields,field_description:l10n_in_edi.field_account_move__l10n_in_edi_status
msgid "India E-Invoice Status"
msgstr ""
#. module: l10n_in_edi
#: model_terms:ir.ui.view,arch_db:l10n_in_edi.l10n_in_edi_inherit_account_move_search_view
msgid "Indian E-Invoices In Error"
msgstr ""
#. module: l10n_in_edi
#: model_terms:ir.ui.view,arch_db:l10n_in_edi.l10n_in_edi_inherit_account_move_search_view
msgid "Indian E-Invoices To Send"
msgstr ""
#. module: l10n_in_edi
#: model:ir.model.fields,field_description:l10n_in_edi.field_res_company__l10n_in_edi_feature
#: model:ir.model.fields,field_description:l10n_in_edi.field_res_config_settings__l10n_in_edi_feature
msgid "Indian E-Invoicing"
msgstr ""
#. module: l10n_in_edi
#: model_terms:ir.ui.view,arch_db:l10n_in_edi.l10n_in_edi_inherit_account_move_search_view
msgid "Indian E-invoice status"
msgstr ""
#. module: l10n_in_edi
@ -241,30 +404,13 @@ msgid "Indian EDI username"
msgstr ""
#. module: l10n_in_edi
#: model_terms:ir.ui.view,arch_db:l10n_in_edi.res_config_settings_view_form_inherit_l10n_in_edi
msgid "Indian Electronic Invoicing"
#: model:ir.model.fields,field_description:l10n_in_edi.field_l10n_in_edi_cancel__move_id
msgid "Invoice"
msgstr ""
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/account_edi_format.py:0
#, python-format
msgid "Invalid HSN Code (%s) in product %s"
msgstr ""
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/account_edi_format.py:0
#, python-format
msgid ""
"Invoice lines having a negative amount are not allowed to generate the IRN. "
"Please create a credit note instead."
msgstr ""
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/account_edi_format.py:0
#, python-format
#: code:addons/l10n_in_edi/models/account_move.py:0
msgid "Invoice number should not be more than 16 characters"
msgstr ""
@ -273,23 +419,69 @@ msgstr ""
msgid "Journal Entry"
msgstr ""
#. module: l10n_in_edi
#: model:ir.model,name:l10n_in_edi.model_account_move_line
msgid "Journal Item"
msgstr ""
#. module: l10n_in_edi
#: model:ir.model.fields,field_description:l10n_in_edi.field_account_bank_statement_line__l10n_in_edi_error
#: model:ir.model.fields,field_description:l10n_in_edi.field_account_move__l10n_in_edi_error
msgid "L10N In Edi Error"
msgstr ""
#. module: l10n_in_edi
#: model:ir.model.fields,field_description:l10n_in_edi.field_l10n_in_edi_cancel__write_uid
msgid "Last Updated by"
msgstr ""
#. module: l10n_in_edi
#: model:ir.model.fields,field_description:l10n_in_edi.field_l10n_in_edi_cancel__write_date
msgid "Last Updated on"
msgstr ""
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/account_edi_format.py:0
#, python-format
msgid "Negative discount is not allowed, set in line %s"
#: code:addons/l10n_in_edi/models/account_move_line.py:0
msgid ""
"Missing or invalid HSN/SAC code: Ensure that invoice lines contain 4, 6 or 8"
" digits"
msgstr ""
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/account_move_line.py:0
msgid "Negative discount is not allowed"
msgstr ""
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/account_move.py:0
msgid ""
"Negative lines will be decreased from positive invoice lines having the same"
" taxes and HSN code"
msgstr ""
#. module: l10n_in_edi
#: model:ir.model.fields.selection,name:l10n_in_edi.selection__account_move__l10n_in_edi_cancel_reason__3
#: model:ir.model.fields.selection,name:l10n_in_edi.selection__l10n_in_edi_cancel__cancel_reason__3
msgid "Order Cancelled"
msgstr ""
#. module: l10n_in_edi
#: model:ir.model.fields.selection,name:l10n_in_edi.selection__account_move__l10n_in_edi_cancel_reason__4
#: model:ir.model.fields.selection,name:l10n_in_edi.selection__l10n_in_edi_cancel__cancel_reason__4
msgid "Others"
msgstr ""
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/res_partner.py:0
msgid ""
"Partners should have a complete address, verify their Street, City, State, "
"Country and Zip code."
msgstr ""
#. module: l10n_in_edi
#: model_terms:ir.ui.view,arch_db:l10n_in_edi.res_config_settings_view_form_inherit_l10n_in_edi
msgid "Password"
@ -297,73 +489,77 @@ msgstr ""
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/account_edi_format.py:0
#, python-format
msgid "Please buy more credits and retry: "
msgstr ""
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/res_config_settings.py:0
#, python-format
msgid "Please enter a GST number in company."
msgstr ""
#. module: l10n_in_edi
#: model_terms:ir.ui.view,arch_db:l10n_in_edi.res_config_settings_view_form_inherit_l10n_in_edi
msgid "Production Environment"
#: code:addons/l10n_in_edi/models/account_move.py:0
msgid ""
"Retrying to send cancellation request for E-Invoice to government portal."
msgstr ""
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/account_move.py:0
#, python-format
msgid "Retrying EDI processing for the following documents:%s"
msgid "Retrying to send your E-Invoice to government portal."
msgstr ""
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/account_edi_format.py:0
#, python-format
#: code:addons/l10n_in_edi/models/account_move_send.py:0
msgid ""
"Set an appropriate GST tax on line \"%s\" (if it's zero rated or nil rated "
"then select it also)"
"Send the e-invoice json to the Indian Invoice Registration Portal (IRP)."
msgstr ""
#. module: l10n_in_edi
#: model:ir.model.fields.selection,name:l10n_in_edi.selection__account_move__l10n_in_edi_status__sent
msgid "Sent"
msgstr ""
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/account_edi_format.py:0
#, python-format
#: code:addons/l10n_in_edi/models/account_move_line.py:0
msgid ""
"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>."
msgstr ""
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/account_edi_format.py:0
#, python-format
msgid ""
"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>."
"Set an appropriate GST tax on invoice lines (if it's zero rated or nil rated"
" then apply it too)"
msgstr ""
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/account_move.py:0
#, python-format
msgid ""
"To cancel E-invoice set cancel reason and remarks at Other info tab in invoices: \n"
"%s"
"Somehow this invoice had been cancelled to government before.%(br)sNormally,"
" this should not happen too often%(br)sJust verify by logging into "
"government website %(link)s"
msgstr ""
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/account_edi_format.py:0
#, python-format
#: code:addons/l10n_in_edi/models/account_move.py:0
msgid ""
"Somehow this invoice has been submited to government before.%(br)sNormally, "
"this should not happen too often%(br)sJust verify value of invoice by upload"
" json to government website %(link)s."
msgstr ""
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/account_move.py:0
msgid "This electronic document is being processed already."
msgstr ""
#. module: l10n_in_edi
#: model:ir.model.fields.selection,name:l10n_in_edi.selection__account_move__l10n_in_edi_status__to_send
msgid "To Send"
msgstr ""
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/res_company.py:0
msgid ""
"Unable to connect to the online E-invoice service. The web service may be "
"temporary down. Please try again in a moment."
msgstr ""
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/account_move.py:0
msgid ""
"Unable to connect to the online E-invoice service.The web service may be "
"temporary down. Please try again in a moment."
@ -381,31 +577,52 @@ msgstr ""
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/account_edi_format.py:0
#, python-format
msgid "You have insufficient credits to send this document!"
#: code:addons/l10n_in_edi/models/res_company.py:0
#: code:addons/l10n_in_edi/models/res_partner.py:0
msgid "View %s"
msgstr ""
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/res_config_settings.py:0
#, python-format
msgid "You must enable production environment to buy credits"
msgstr ""
#. module: l10n_in_edi
#: model_terms:ir.ui.view,arch_db:l10n_in_edi.res_config_settings_view_form_inherit_l10n_in_edi
msgid "documentation"
#: code:addons/l10n_in_edi/models/res_company.py:0
msgid "View Companies"
msgstr ""
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/account_edi_format.py:0
#, python-format
msgid "product is required to get HSN code"
#: code:addons/l10n_in_edi/models/account_move_line.py:0
msgid "View Invoice Line(s)"
msgstr ""
#. module: l10n_in_edi
#: model_terms:ir.ui.view,arch_db:l10n_in_edi.res_config_settings_view_form_inherit_l10n_in_edi
msgid "to get credentials"
#. odoo-python
#: code:addons/l10n_in_edi/models/account_move.py:0
msgid "View Invoices"
msgstr ""
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/res_partner.py:0
msgid "View Partners"
msgstr ""
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/ir_attachment.py:0
msgid "You can't unlink an attachment that you received from the government"
msgstr ""
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/account_move.py:0
msgid "here"
msgstr ""
#. module: l10n_in_edi
#. odoo-python
#: code:addons/l10n_in_edi/models/account_move.py:0
msgid ""
"⚠️ Important Notice GSP Deprecation \n"
"The currently selected GSP (Tera Soft) will be deprecated soon.\n"
"To ensure uninterrupted e-Invoice and E-way operations, please switch to BVM GSP as per the"
msgstr ""

View file

@ -1,7 +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 account_move_line
from . import account_move_send
from . import ir_attachment
from . import res_company
from . import res_config_settings
from . import res_partner

View file

@ -1,736 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import re
import json
import pytz
import markupsafe
from collections import defaultdict
from odoo import models, fields, api, _
from odoo.tools import html_escape, float_is_zero, float_compare
from odoo.exceptions import AccessError, ValidationError
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 _get_l10n_in_base_tags(self):
return (
self.env.ref('l10n_in.tax_tag_base_sgst').ids
+ self.env.ref('l10n_in.tax_tag_base_cgst').ids
+ self.env.ref('l10n_in.tax_tag_base_igst').ids
+ self.env.ref('l10n_in.tax_tag_base_cess').ids
+ self.env.ref('l10n_in.tax_tag_zero_rated').ids
+ self.env.ref("l10n_in.tax_tag_exempt").ids
+ self.env.ref("l10n_in.tax_tag_nil_rated").ids
+ self.env.ref("l10n_in.tax_tag_non_gst_supplies").ids
)
def _get_l10n_in_gst_tags(self):
return (
self.env.ref('l10n_in.tax_tag_base_sgst')
+ self.env.ref('l10n_in.tax_tag_base_cgst')
+ self.env.ref('l10n_in.tax_tag_base_igst')
+ self.env.ref('l10n_in.tax_tag_base_cess')
+ self.env.ref('l10n_in.tax_tag_zero_rated')
).ids
def _get_l10n_in_non_taxable_tags(self):
return (
self.env.ref("l10n_in.tax_tag_exempt")
+ self.env.ref("l10n_in.tax_tag_nil_rated")
+ self.env.ref("l10n_in.tax_tag_non_gst_supplies")
).ids
def _get_move_applicability(self, move):
# EXTENDS account_edi
self.ensure_one()
if self.code != 'in_einvoice_1_03':
return super()._get_move_applicability(move)
is_under_gst = any(move_line_tag.id in self._get_l10n_in_gst_tags() for move_line_tag in move.line_ids.tax_tag_ids)
if move.is_sale_document(include_receipts=True) and move.country_code == 'IN' and is_under_gst and move.l10n_in_gst_treatment in (
"regular",
"composition",
"overseas",
"special_economic_zone",
"deemed_export",
):
return {
'post': self._l10n_in_edi_post_invoice,
'cancel': self._l10n_in_edi_cancel_invoice,
'edi_content': self._l10n_in_edi_xml_invoice_content,
}
def _needs_web_services(self):
self.ensure_one()
return self.code == "in_einvoice_1_03" or super()._needs_web_services()
def _l10n_in_edi_xml_invoice_content(self, invoice):
return json.dumps(self._l10n_in_edi_generate_invoice_json(invoice)).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"))
all_base_tags = self._get_l10n_in_gst_tags() + self._get_l10n_in_non_taxable_tags()
for line in move.invoice_line_ids.filtered(lambda line: line.display_type not in ('line_note', 'line_section', 'rounding')):
if line.price_subtotal < 0:
# Line having a negative amount is not allowed.
if not move._l10n_in_edi_is_managing_invoice_negative_lines_allowed():
raise ValidationError(_("Invoice lines having a negative amount are not allowed to generate the IRN. "
"Please create a credit note instead."))
if line.display_type == 'product' and line.discount < 0:
error_message.append(_("Negative discount is not allowed, set in line %s", line.name))
if not line.tax_tag_ids or not any(move_line_tag.id in all_base_tags for move_line_tag in line.tax_tag_ids):
error_message.append(_(
"""Set an appropriate GST tax on line "%s" (if it's zero rated or nil rated then select it also)""", line.product_id.name))
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):
url = self.env["iap.account"].get_credits_url(service_name="l10n_in_edi")
return markupsafe.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 _l10n_in_edi_post_invoice(self, invoice):
generate_json = self._l10n_in_edi_generate_invoice_json(invoice)
response = self._l10n_in_edi_generate(invoice.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(invoice.company_id)
if not authenticate_response.get("error"):
error = []
response = self._l10n_in_edi_generate(invoice.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(invoice.company_id, {
"doc_type": invoice.move_type == "out_refund" and "CRN" or "INV",
"doc_num": invoice.name,
"doc_date": invoice.invoice_date and invoice.invoice_date.strftime("%d/%m/%Y") or False,
})
if not response.get("error"):
error = []
odoobot = self.env.ref("base.partner_root")
invoice.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:
return {invoice: {
"success": False,
"error": self._l10n_in_edi_get_iap_buy_credits_message(invoice.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])
return {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_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",
})
return {invoice: {"success": True, "attachment": attachment}}
def _l10n_in_edi_cancel_invoice(self, invoice):
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 = {}
error = []
odoobot = self.env.ref("base.partner_root")
invoice.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:
return {invoice: {
"success": False,
"error": self._l10n_in_edi_get_iap_buy_credits_message(invoice.company_id),
"blocking_level": "error",
}}
if error:
error_message = "<br/>".join(["[%s] %s" % (e.get("code"), html_escape(e.get("message"))) for e in error])
return {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 = False
if json_dump:
attachment = self.env["ir.attachment"].create({
"name": json_name,
"raw": json_dump.encode(),
"res_model": "account.move",
"res_id": invoice.id,
"mimetype": "application/json",
})
return {invoice: {"success": True, "attachment": attachment}}
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(_("- Street required min 3 and max 100 characters"))
if partner.street2 and not re.match("^.{3,100}$", partner.street2):
message.append(_("- Street2 should be min 3 and max 100 characters"))
if not re.match("^.{3,100}$", partner.city or ""):
message.append(_("- City required min 3 and max 100 characters"))
if partner.country_id.code == "IN" and not re.match("^.{3,50}$", partner.state_id.name or ""):
message.append(_("- State required min 3 and max 50 characters"))
if (
partner.country_id.code == "IN"
and not re.match(r"^(?!0+$)([0-9]{2})$", partner.state_id.l10n_in_tin or "")
):
message.append(_("- State TIN Number must be exactly 2 digits."))
if partner.country_id.code == "IN" and not re.match("^[0-9]{6,}$", partner.zip or ""):
message.append(_("- 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(_("- 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(_("- Email address should be valid and not more then 100 characters"))
if message:
message.insert(0, "%s" %(partner.display_name))
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
"""
zip_digits = self._l10n_in_edi_extract_digits(partner.zip)
partner_details = {
"Addr1": partner.street or "",
"Loc": partner.city or "",
"Pin": zip_digits and int(zip_digits) or "",
"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 "URP",
})
else:
partner_details.update({"Nm": partner.name or partner.commercial_partner_id.name})
# For no country I would suppose it is India, so not sure this is super right
if is_overseas and (not partner.country_id or partner.country_id.code != 'IN'):
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
"""
value = round(amount, precision_digits)
# avoid -0.0
return value if value else 0.0
def _get_l10n_in_edi_line_details(self, index, line, line_tax_details):
"""
Create the dictionary with line details
return {
account.move.line('1'): {....},
account.move.line('2'): {....},
....
}
"""
sign = line.move_id.is_inbound() and -1 or 1
tax_details_by_code = self._get_l10n_in_tax_details_by_line_code(line_tax_details.get("tax_details", {}))
quantity = line.quantity
full_discount_or_zero_quantity = line.discount == 100.00 or float_is_zero(quantity, 3)
if full_discount_or_zero_quantity:
unit_price_in_inr = line.currency_id._convert(
line.price_unit,
line.company_currency_id,
line.company_id,
line.date or fields.Date.context_today(self)
)
else:
unit_price_in_inr = ((sign * line.balance) / (1 - (line.discount / 100))) / quantity
if unit_price_in_inr < 0 and quantity < 0:
# If unit price and quantity both is negative then
# We set unit price and quantity as positive because
# government does not accept negative in qty or unit price
unit_price_in_inr = unit_price_in_inr * -1
quantity = quantity * -1
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(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(unit_price_in_inr, 3),
# total amount is before discount
"TotAmt": self._l10n_in_round_value(unit_price_in_inr * quantity),
"Discount": self._l10n_in_round_value((unit_price_in_inr * quantity) * (line.discount / 100)),
"AssAmt": self._l10n_in_round_value((sign * line.balance)),
"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)),
"CgstAmt": self._l10n_in_round_value(tax_details_by_code.get("cgst_amount", 0.00)),
"SgstAmt": self._l10n_in_round_value(tax_details_by_code.get("sgst_amount", 0.00)),
"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)),
"CesNonAdvlAmt": self._l10n_in_round_value(
tax_details_by_code.get("cess_non_advol_amount", 0.00)),
"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)),
"StateCesNonAdvlAmt": self._l10n_in_round_value(
tax_details_by_code.get("state_cess_non_advol_amount", 0.00)),
"OthChrg": self._l10n_in_round_value(tax_details_by_code.get("other_amount", 0.00)),
"TotItemVal": self._l10n_in_round_value(((sign * line.balance) + line_tax_details.get("tax_amount", 0.00))),
}
def _l10n_in_edi_generate_invoice_json_managing_negative_lines(self, invoice, json_payload):
"""Set negative lines against positive lines as discount with same HSN code and tax rate
With negative lines
product name | hsn code | unit price | qty | discount | total
=============================================================
product A | 123456 | 1000 | 1 | 100 | 900
product B | 123456 | 1500 | 2 | 0 | 3000
Discount | 123456 | -300 | 1 | 0 | -300
Converted to without negative lines
product name | hsn code | unit price | qty | discount | total
=============================================================
product A | 123456 | 1000 | 1 | 100 | 900
product B | 123456 | 1500 | 2 | 300 | 2700
totally discounted lines are kept as 0, though
"""
def discount_group_key(line_vals):
return "%s-%s"%(line_vals['HsnCd'], line_vals['GstRt'])
def put_discount_on(discount_line_vals, other_line_vals):
discount = discount_line_vals['AssAmt'] * -1
discount_to_allow = other_line_vals['AssAmt']
if float_compare(discount_to_allow, discount, precision_rounding=invoice.currency_id.rounding) < 0:
# Update discount line, needed when discount is more then max line, in short remaining_discount is not zero
discount_line_vals.update({
'AssAmt': self._l10n_in_round_value(discount_line_vals['AssAmt'] + other_line_vals['AssAmt']),
'IgstAmt': self._l10n_in_round_value(discount_line_vals['IgstAmt'] + other_line_vals['IgstAmt']),
'CgstAmt': self._l10n_in_round_value(discount_line_vals['CgstAmt'] + other_line_vals['CgstAmt']),
'SgstAmt': self._l10n_in_round_value(discount_line_vals['SgstAmt'] + other_line_vals['SgstAmt']),
'CesAmt': self._l10n_in_round_value(discount_line_vals['CesAmt'] + other_line_vals['CesAmt']),
'CesNonAdvlAmt': self._l10n_in_round_value(discount_line_vals['CesNonAdvlAmt'] + other_line_vals['CesNonAdvlAmt']),
'StateCesAmt': self._l10n_in_round_value(discount_line_vals['StateCesAmt'] + other_line_vals['StateCesAmt']),
'StateCesNonAdvlAmt': self._l10n_in_round_value(discount_line_vals['StateCesNonAdvlAmt'] + other_line_vals['StateCesNonAdvlAmt']),
'OthChrg': self._l10n_in_round_value(discount_line_vals['OthChrg'] + other_line_vals['OthChrg']),
'TotItemVal': self._l10n_in_round_value(discount_line_vals['TotItemVal'] + other_line_vals['TotItemVal']),
})
other_line_vals.update({
'Discount': self._l10n_in_round_value(other_line_vals['Discount'] + discount_to_allow),
'AssAmt': 0.00,
'IgstAmt': 0.00,
'CgstAmt': 0.00,
'SgstAmt': 0.00,
'CesAmt': 0.00,
'CesNonAdvlAmt': 0.00,
'StateCesAmt': 0.00,
'StateCesNonAdvlAmt': 0.00,
'OthChrg': 0.00,
'TotItemVal': 0.00,
})
return False
other_line_vals.update({
'Discount': self._l10n_in_round_value(other_line_vals['Discount'] + discount),
'AssAmt': self._l10n_in_round_value(other_line_vals['AssAmt'] + discount_line_vals['AssAmt']),
'IgstAmt': self._l10n_in_round_value(other_line_vals['IgstAmt'] + discount_line_vals['IgstAmt']),
'CgstAmt': self._l10n_in_round_value(other_line_vals['CgstAmt'] + discount_line_vals['CgstAmt']),
'SgstAmt': self._l10n_in_round_value(other_line_vals['SgstAmt'] + discount_line_vals['SgstAmt']),
'CesAmt': self._l10n_in_round_value(other_line_vals['CesAmt'] + discount_line_vals['CesAmt']),
'CesNonAdvlAmt': self._l10n_in_round_value(other_line_vals['CesNonAdvlAmt'] + discount_line_vals['CesNonAdvlAmt']),
'StateCesAmt': self._l10n_in_round_value(other_line_vals['StateCesAmt'] + discount_line_vals['StateCesAmt']),
'StateCesNonAdvlAmt': self._l10n_in_round_value(other_line_vals['StateCesNonAdvlAmt'] + discount_line_vals['StateCesNonAdvlAmt']),
'OthChrg': self._l10n_in_round_value(other_line_vals['OthChrg'] + discount_line_vals['OthChrg']),
'TotItemVal': self._l10n_in_round_value(other_line_vals['TotItemVal'] + discount_line_vals['TotItemVal']),
})
return True
discount_lines = []
for discount_line in json_payload['ItemList'].copy(): #to be sure to not skip in the loop:
if discount_line['AssAmt'] < 0:
discount_lines.append(discount_line)
json_payload['ItemList'].remove(discount_line)
if not discount_lines:
return json_payload
lines_grouped_and_sorted = defaultdict(list)
for line in sorted(json_payload['ItemList'], key=lambda i: i['AssAmt'], reverse=True):
lines_grouped_and_sorted[discount_group_key(line)].append(line)
for discount_line in discount_lines:
apply_discount_on_lines = lines_grouped_and_sorted.get(discount_group_key(discount_line), [])
for apply_discount_on in apply_discount_on_lines:
if put_discount_on(discount_line, apply_discount_on):
break
return json_payload
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", {}))
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: line.display_type not in ('line_note', 'line_section', 'rounding'))
tax_details_per_record = tax_details.get("tax_details_per_record")
sign = invoice.is_inbound() and -1 or 1
rounding_amount = sum(line.balance for line in invoice.line_ids if line.display_type == 'rounding') * sign
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_amount") 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, tax_details_per_record.get(line, {}))
for index, line in enumerate(lines, start=1)
],
"ValDtls": {
"AssVal": self._l10n_in_round_value(tax_details.get("base_amount")),
"CgstVal": self._l10n_in_round_value(tax_details_by_code.get("cgst_amount", 0.00)),
"SgstVal": self._l10n_in_round_value(tax_details_by_code.get("sgst_amount", 0.00)),
"IgstVal": self._l10n_in_round_value(tax_details_by_code.get("igst_amount", 0.00)),
"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)),
),
"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)),
),
"RndOffAmt": self._l10n_in_round_value(
rounding_amount),
"TotInvVal": self._l10n_in_round_value(
(tax_details.get("base_amount") + tax_details.get("tax_amount") + rounding_amount)),
},
}
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")))
})
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"), is_overseas=is_overseas)
})
if is_overseas:
json_payload.update({
"ExpDtls": {
"RefClm": tax_details_by_code.get("igst_amount") and "Y" or "N",
"ForCur": invoice.currency_id.name,
"CntCode": saler_buyer.get("buyer_details").country_id.code or "",
}
})
if invoice.l10n_in_shipping_bill_number:
json_payload["ExpDtls"].update({
"ShipBNo": invoice.l10n_in_shipping_bill_number,
})
if invoice.l10n_in_shipping_bill_date:
json_payload["ExpDtls"].update({
"ShipBDt": invoice.l10n_in_shipping_bill_date.strftime("%d/%m/%Y"),
})
if invoice.l10n_in_shipping_port_code_id:
json_payload["ExpDtls"].update({
"Port": invoice.l10n_in_shipping_port_code_id.code
})
if not invoice._l10n_in_edi_is_managing_invoice_negative_lines_allowed():
return json_payload
return self._l10n_in_edi_generate_invoice_json_managing_negative_lines(invoice, json_payload)
@api.model
def _l10n_in_prepare_edi_tax_details(self, move, in_foreign=False, filter_invl_to_apply=None):
def l10n_in_grouping_key_generator(base_line, tax_values):
invl = base_line['record']
tax = tax_values['tax_repartition_line'].tax_id
tags = tax_values['tax_repartition_line'].tag_ids
line_code = "other"
if not invl.currency_id.is_zero(tax_values['tax_amount_currency']):
if any(tag in tags for tag in self.env.ref("l10n_in.tax_tag_cess")):
if tax.amount_type != "percent":
line_code = "cess_non_advol"
else:
line_code = "cess"
elif any(tag in tags for tag in self.env.ref("l10n_in.tax_tag_state_cess")):
if tax.amount_type != "percent":
line_code = "state_cess_non_advol"
else:
line_code = "state_cess"
else:
for gst in ["cgst", "sgst", "igst"]:
if any(tag in tags for tag in self.env.ref("l10n_in.tax_tag_%s"%(gst))):
line_code = 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):
if base_line['record'].display_type == 'rounding':
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,
filter_invl_to_apply=filter_invl_to_apply,
)
@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': '0',
'message': _(
"Ensure GST Number set on company setting and API are Verified."
)}
]}
@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:
tz = pytz.timezone("Asia/Kolkata")
local_time = tz.localize(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)

View file

@ -1,79 +1,843 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import base64
import json
import logging
import re
from collections import defaultdict
from markupsafe import Markup
from odoo import api, fields, models, _
from odoo.exceptions import UserError
from odoo import Command, _, api, fields, models
from odoo.exceptions import AccessError, LockError, UserError
from odoo.tools import float_is_zero, float_compare
from odoo.addons.l10n_in.models.account_invoice import EDI_CANCEL_REASON
_logger = logging.getLogger(__name__)
try:
import jwt
except ImportError:
_logger.warning("The 'jwt' library is not installed. Decoding for duplicate IRN e-invoices will be skipped.")
jwt = None
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?")
# E-Invoice Fields
l10n_in_edi_status = fields.Selection(
string="India E-Invoice Status",
selection=[
('to_send', "To Send"),
('sent', "Sent"),
('cancelled', "Cancelled"),
],
copy=False,
tracking=True,
readonly=True,
)
l10n_in_edi_attachment_id = fields.Many2one(
comodel_name='ir.attachment',
string="E-Invoice(IN) Attachment",
compute=lambda self: self._compute_linked_attachment_id(
'l10n_in_edi_attachment_id',
'l10n_in_edi_attachment_file'
),
depends=['l10n_in_edi_attachment_file']
)
l10n_in_edi_attachment_file = fields.Binary(
string="E-Invoice(IN) File",
attachment=True,
copy=False
)
l10n_in_edi_cancel_reason = fields.Selection(
selection=list(EDI_CANCEL_REASON.items()),
string="E-Invoice(IN) Cancel Reason",
copy=False
)
l10n_in_edi_cancel_remarks = fields.Char(
string="E-Invoice(IN) Cancel Remarks",
copy=False
)
l10n_in_edi_content = fields.Binary(
compute="_compute_l10n_in_edi_content",
string="E-Invoice(IN) Content"
)
l10n_in_edi_error = fields.Html(readonly=True, copy=False)
@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 action_retry_edi_documents_error(self):
# E-Invoice compute
def _compute_l10n_in_edi_content(self):
for move in self:
if move.country_code == 'IN':
move.message_post(body=_(
"Retrying EDI processing for the following documents: %(breakline)s %(edi_codes)s",
breakline=Markup("<br/>"),
edi_codes=Markup("<br/>").join(
move.edi_document_ids
.filtered(lambda doc: doc.blocking_level == "error")
.mapped("edi_format_name")
)
))
return super().action_retry_edi_documents_error()
move.l10n_in_edi_content = (
move.country_code == 'IN'
and move.company_id.l10n_in_edi_feature
and move.is_sale_document(include_receipts=True)
and move.journal_id.type == 'sale'
and base64.b64encode(
json.dumps(move._l10n_in_edi_generate_invoice_json()).encode()
)
)
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"]
def _compute_l10n_in_warning(self):
super()._compute_l10n_in_warning()
gsp_provider = self.env["ir.config_parameter"].sudo().get_param("l10n_in.gsp_provider", "tera")
if gsp_provider != "tera":
return
indian_invoice = self.filtered(lambda m: m.country_code == 'IN' and m.move_type != 'entry' and
m.l10n_in_edi_status in ('to_send', 'sent') and not m.l10n_in_edi_error
)
edi_error_message = _(
"⚠️ Important Notice GSP Deprecation \n"
"The currently selected GSP (Tera Soft) will be deprecated soon.\n"
"To ensure uninterrupted e-Invoice and E-way operations, please switch to BVM GSP as per the"
)
if not self.env.is_admin():
edi_error_message += _(
"\n\nYou must contact your system administrator to update the GSP."
)
for move in indian_invoice:
l10n_in_warning = move.l10n_in_warning or {}
l10n_in_warning['in_edi_gsp_deprecation'] = {
'message': edi_error_message,
'action_text': _("Documentation"),
'action': {
'name': _("Documentation"),
'type': 'ir.actions.act_url',
'url': 'https://www.odoo.com/documentation/19.0/applications/finance/fiscal_localizations/india.html#gsp-configuration',
}
}
move.l10n_in_warning = l10n_in_warning
# Action Methods
def action_export_l10n_in_edi_content_json(self):
self.ensure_one()
return {
'type': 'ir.actions.act_url',
'url': f'/web/content/account.move/{self.id}/l10n_in_edi_content'
}
def button_request_cancel(self):
if self._l10n_in_edi_need_cancel_request():
if self.l10n_in_edi_cancel_remarks and self.l10n_in_edi_cancel_reason:
return self._l10n_in_edi_cancel_invoice()
return self.env['l10n_in_edi.cancel'].with_context(
default_move_id=self.id
)._get_records_action(name=_("Cancel E-Invoice"), target='new')
elif self.l10n_in_edi_status == 'sent':
self.message_post(
body=_(
"Force cancelled %(invoice)s by %(username)s",
invoice=self.name, username=self.env.user.name
)
)
self.button_cancel()
self.write({
'l10n_in_edi_status': 'cancelled',
'l10n_in_edi_error': False,
})
return True
return super().button_request_cancel()
def action_l10n_in_edi_force_cancel(self):
self.with_context(l10n_in_edi_force_cancel=True).button_request_cancel()
def button_draft(self):
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()
if move.l10n_in_edi_status == 'to_send':
# Avoid resetting sent and cancelled invoices
move.l10n_in_edi_status = False
if move.l10n_in_edi_error:
move.l10n_in_edi_error = False
return super().button_draft()
# Business Methods
def _post(self, soft=True):
# EXTENDS 'account'
res = super()._post(soft=soft)
self.filtered(lambda m: m._l10n_in_check_einvoice_eligible()).l10n_in_edi_status = 'to_send'
return res
def _l10n_in_edi_need_cancel_request(self):
self.ensure_one()
return (
self.country_code == 'IN'
and not self.env.context.get('l10n_in_edi_force_cancel')
and self.is_sale_document()
and self.l10n_in_edi_status == 'sent'
)
def _need_cancel_request(self):
# EXTENDS 'account'
return super()._need_cancel_request() or self._l10n_in_edi_need_cancel_request()
# Indian E-invoice Business Methods
def _l10n_in_check_einvoice_eligible(self):
self.ensure_one()
return (
self.company_id.l10n_in_edi_feature
and self.journal_id.type == 'sale'
and any(
line.display_type == 'product'
and line.l10n_in_gstr_section in [
'sale_b2b_rcm', 'sale_b2b_regular', 'sale_exp_wp', 'sale_exp_wop',
'sale_sez_wp', 'sale_sez_wop', 'sale_deemed_export', 'sale_cdnr_rcm',
'sale_cdnr_regular', 'sale_cdnr_deemed_export', 'sale_cdnr_sez_wp',
'sale_cdnr_sez_wop', 'sale_cdnur_exp_wp', 'sale_cdnur_exp_wop',
]
for line in self.line_ids
)
)
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.sudo().attachment_id.raw.decode("utf-8"))
else:
return {}
if self.l10n_in_edi_attachment_id:
return json.loads(self.l10n_in_edi_attachment_id.sudo().raw.decode("utf-8"))
return {}
def _l10n_in_lock_invoice(self):
try:
self.lock_for_update()
except LockError:
raise UserError(_('This electronic document is being processed already.')) from None
def _l10n_in_edi_optional_field_validation(self, partner):
"""
Validates optional partner fields (e.g., email, phone, street2) for e-invoicing,
which are not mandatory in the government API JSON schema. Returns error messages
for posting in the chatter.
"""
message = []
if partner.email and (
not re.match(r"^[a-zA-Z0-9+_.-]+@[a-zA-Z0-9.-]+$", partner.email) or
not re.match(r"^.{6,100}$", partner.email)
):
message.append(_("- Email: invalid or longer than 100 characters."))
if partner.phone and not re.match(
r"^[0-9]{10,12}$",
partner.env['account.move']._l10n_in_extract_digits(partner.phone)
):
message.append(_("- Phone number: must be 1012 digits."))
if partner.street2 and not re.match(r"^.{3,100}$", partner.street2):
message.append(_("- Street2: must be 3100 characters."))
return message
def _l10n_in_edi_send_invoice(self):
self.ensure_one()
if self.l10n_in_edi_error:
# make sure to clear the error before sending again
self.l10n_in_edi_error = False
self.message_post(body=_(
"Retrying to send your E-Invoice to government portal."
))
partners = set(self._get_l10n_in_seller_buyer_party().values())
for partner in partners:
if partner_validation := partner._l10n_in_edi_strict_error_validation():
self.l10n_in_edi_error = Markup("<br>").join(partner_validation)
return {'messages': partner_validation}
self._l10n_in_lock_invoice()
generate_json = self._l10n_in_edi_generate_invoice_json()
response = self._l10n_in_edi_connect_to_server(
url_end_point='generate',
json_payload=generate_json
)
if error := response.get('error', {}):
odoobot_id = self.env.ref('base.partner_root').id
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_connect_to_server(
url_end_point='getirnbydocdetails',
params={
"doc_type": (
(self.move_type == "out_refund" and "CRN")
or (self.debit_origin_id and "DBN")
or "INV"
),
"doc_num": self.name,
"doc_date": self.invoice_date and self.invoice_date.strftime("%d/%m/%Y"),
}
)
mismatch_error = []
decoded_response = {}
if jwt:
try:
if data := response.get('data'):
signinvoice = data['SignedInvoice']
decoded_response = jwt.decode(signinvoice, options={'verify_signature': False})
decoded_response = json.loads(decoded_response['data'])
except (json.JSONDecodeError, jwt.exceptions.DecodeError) as e:
_logger.warning("Failed to decode SignedInvoice JWT payload: %s", str(e))
if decoded_response:
received_gstin = decoded_response['BuyerDtls']['Gstin']
expected_gstin = generate_json['BuyerDtls']['GSTIN']
received_total_invoice_value = decoded_response['ValDtls']['TotInvVal']
expected_total_invoice_value = generate_json['ValDtls']['TotInvVal']
# Check for mismatch between decoded and expected e-invoice details:
# - Buyer GSTIN must match
# - Total Invoice Value must be within the allowed government tolerance range:
# For example, if the expected invoice value is 100,
# the valid range is from 99 (value - 1) to 101 (value + 1),
# i.e., 99.00 < received value < 101.00
if (received_gstin != expected_gstin or
not expected_total_invoice_value - 1 < received_total_invoice_value < expected_total_invoice_value + 1
):
mismatch_error = [{
'code': '2150',
'message': _("Duplicate IRN found for this invoice, but the buyer details or invoice values do not match.")
}]
# Handle the result based on mismatch or response error
if mismatch_error:
error = mismatch_error
elif not response.get("error"):
error = []
link = Markup(
"<a href='https://einvoice1.gst.gov.in/Others/VSignedInvoice'>%s</a>"
) % (_("here"))
self.message_post(
author_id=odoobot_id,
body=_(
"Somehow this invoice has been submited to government before."
"%(br)sNormally, this should not happen too often"
"%(br)sJust verify value of invoice by upload json to government website %(link)s.",
br=Markup("<br/>"),
link=link
)
)
if (no_credit := 'no-credit' in error_codes) or error:
msg = Markup("<br/>").join(
["[%s] %s" % (e.get("code"), e.get("message")) for e in error]
)
is_warning = any(warning_code in error_codes for warning_code in ('404', 'timeout'))
self.l10n_in_edi_error = (
self._l10n_in_edi_get_iap_buy_credits_message()
if no_credit else msg
)
# avoid return `l10n_in_edi_error` because as a html field
# values are sanitized with `<p>` tag
return {
'messages': [msg],
'is_warning': is_warning
}
data = response.get("data", {})
json_dump = json.dumps(data)
json_name = "%s_einvoice.json" % (self.name.replace("/", "_"))
attachment = self.env["ir.attachment"].create({
'name': json_name,
'raw': json_dump.encode(),
'res_model': self._name,
'res_field': 'l10n_in_edi_attachment_file',
'res_id': self.id,
'mimetype': 'application/json',
'company_id': self.company_id.id,
})
request_json_dump = json.dumps(generate_json, indent=4)
request_json_name = "%s_request.json" % (self.name.replace("/", "_"))
request_attachment = self.env["ir.attachment"].create({
'name': request_json_name,
'raw': request_json_dump.encode(),
'res_model': self._name,
'res_id': self.id,
'mimetype': 'application/json',
'company_id': self.company_id.id,
})
self.l10n_in_edi_status = 'sent'
message = []
for partner in partners:
if partner_validation := self._l10n_in_edi_optional_field_validation(partner):
message.append(
Markup("<strong><em>%s</em></strong><br>%s") % (partner.name, Markup("<br>").join(partner_validation))
)
message.append(self.env._("E-invoice submitted successfully."))
if message:
self.message_post(
attachment_ids=[request_attachment.id, attachment.id],
body=Markup("<strong>%s</strong><br>%s") % (_("Following:"), Markup("<br>").join(message))
)
def _l10n_in_edi_cancel_invoice(self):
if self.l10n_in_edi_error:
# make sure to clear the error before cancelling again
self.l10n_in_edi_error = False
self.message_post(body=_(
"Retrying to send cancellation request for E-Invoice to government portal."
))
self._l10n_in_lock_invoice()
l10n_in_edi_response_json = self._get_l10n_in_edi_response_json()
cancel_json = {
"Irn": l10n_in_edi_response_json.get("Irn"),
"CnlRsn": self.l10n_in_edi_cancel_reason,
"CnlRem": self.l10n_in_edi_cancel_remarks,
}
response = self._l10n_in_edi_connect_to_server(url_end_point='cancel', json_payload=cancel_json)
# Creating a lambda function so it fetches the odoobot id only when needed
_get_odoobot_id = (
lambda self: self.env.ref('base.partner_root').id
)
if error := response.get('error'):
error_codes = [e.get('code') for e in error]
if '9999' in error_codes:
response = {}
error = []
link = Markup(
"<a href='https://einvoice1.gst.gov.in/Others/VSignedInvoice'>%s</a>"
) % (_("here"))
self.message_post(
author_id=_get_odoobot_id(self),
body=_(
"Somehow this invoice had been cancelled to government before."
"%(br)sNormally, this should not happen too often"
"%(br)sJust verify by logging into government website %(link)s",
br=Markup("<br/>"),
link=link
)
)
if "no-credit" in error_codes:
self.l10n_in_edi_error = self._l10n_in_edi_get_iap_buy_credits_message()
return
if error:
self.l10n_in_edi_error = (
Markup("<br/>").join(
["[%s] %s" % (e.get("code"), e.get("message")) for e in error]
)
)
if "error" not in response:
json_dump = json.dumps(response.get('data', {}))
json_name = "%s_cancel_einvoice.json" % (self.name.replace("/", "_"))
if json_dump:
attachment = self.env['ir.attachment'].create({
'name': json_name,
'raw': json_dump.encode(),
'res_model': self._name,
'res_field': 'l10n_in_edi_attachment_file',
'res_id': self.id,
'mimetype': 'application/json',
})
request_json_dump = json.dumps(cancel_json, indent=4)
request_json_name = "%s_cancel_request.json" % (self.name.replace("/", "_"))
request_attachment = self.env['ir.attachment'].create({
'name': request_json_name,
'raw': request_json_dump.encode(),
'res_model': self._name,
'res_id': self.id,
'mimetype': 'application/json',
})
self.message_post(author_id=_get_odoobot_id(self), body=_(
"E-Invoice has been cancelled successfully. "
"Cancellation Reason: %(reason)s and Cancellation Remark: %(remark)s",
reason=EDI_CANCEL_REASON[self.l10n_in_edi_cancel_reason],
remark=self.l10n_in_edi_cancel_remarks
), attachment_ids=[request_attachment.id, attachment.id] if attachment else [request_attachment.id])
self.l10n_in_edi_status = 'cancelled'
self.button_cancel()
if self._can_commit():
self.env.cr.commit()
return True
@api.model
def _l10n_in_edi_is_managing_invoice_negative_lines_allowed(self):
""" Negative lines are not allowed by the Indian government making some features unavailable like sale_coupon
or global discounts. This method allows odoo to distribute the negative discount lines to each others lines
with same HSN code making such features available even for Indian people.
:return: True if odoo needs to distribute the negative discount lines, False otherwise.
def _get_l10n_in_edi_partner_details(
self,
partner,
set_vat=True,
set_phone_and_email=True,
is_overseas=False,
pos_state_id=False
):
"""
param_name = 'l10n_in_edi.manage_invoice_negative_lines'
return bool(self.env['ir.config_parameter'].sudo().get_param(param_name))
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
"""
zip_digits = self._l10n_in_extract_digits(partner.zip)
partner_details = {
'Addr1': partner.street or '',
'Loc': partner.city or '',
'Pin': zip_digits and int(zip_digits) or '',
'Stcd': partner.state_id.l10n_in_tin or '',
}
if partner.street2 and re.match(r"^.{3,100}$", partner.street2):
partner_details['Addr2'] = partner.street2
if set_phone_and_email:
if (
partner.email
and re.match(r"^[a-zA-Z0-9+_.-]+@[a-zA-Z0-9.-]+$", partner.email)
and re.match(r"^.{6,100}$", partner.email)
):
partner_details['Em'] = partner.email
if (
partner.phone
and re.match(r"^[0-9]{10,12}$", self._l10n_in_extract_digits(partner.phone))
):
partner_details['Ph'] = self._l10n_in_extract_digits(partner.phone)
if pos_state_id:
partner_details['POS'] = pos_state_id.l10n_in_tin or ''
if set_vat:
partner_details.update({
'LglNm': partner.commercial_partner_id.name,
'GSTIN': partner.vat or 'URP',
})
else:
partner_details['Nm'] = partner.name
# For no country I would suppose it is India, so not sure this is super right
if is_overseas and (not partner.country_id or partner.country_id.code != 'IN'):
partner_details.update({
"GSTIN": "URP",
"Pin": 999999,
"Stcd": "96",
"POS": "96",
})
return partner_details
def _get_l10n_in_edi_line_details(self, index, line, line_tax_details):
"""
Create the dictionary with line details
"""
sign = self.is_inbound() and -1 or 1
tax_details_by_code = self._get_l10n_in_tax_details_by_line_code(line_tax_details['tax_details'])
quantity = line.quantity
if line.discount == 100.00 or float_is_zero(quantity, 3):
# Full discount or zero quantity
unit_price_in_inr = line.currency_id._convert(
line.price_unit,
line.company_currency_id,
line.company_id,
line.date or fields.Date.context_today(self)
)
else:
unit_price_in_inr = ((sign * line.balance) / (1 - (line.discount / 100))) / quantity
if unit_price_in_inr < 0 and quantity < 0:
# If unit price and quantity both is negative then
# We set unit price and quantity as positive because
# government does not accept negative in qty or unit price
unit_price_in_inr = -unit_price_in_inr
quantity = -quantity
in_round = self._l10n_in_round_value
line_details = {
'SlNo': str(index),
'IsServc': self._l10n_in_is_service_hsn(line.l10n_in_hsn_code) and 'Y' or 'N',
'HsnCd': self._l10n_in_extract_digits(line.l10n_in_hsn_code),
'Qty': in_round(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': in_round(unit_price_in_inr, 3),
# total amount is before discount
'TotAmt': in_round(unit_price_in_inr * quantity),
'Discount': in_round((unit_price_in_inr * quantity) * (line.discount / 100)),
'AssAmt': in_round(sign * line.balance),
'GstRt': in_round(
(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': in_round(tax_details_by_code.get('igst_amount', 0.00)),
'CgstAmt': in_round(tax_details_by_code.get('cgst_amount', 0.00)),
'SgstAmt': in_round(tax_details_by_code.get('sgst_amount', 0.00)),
'CesRt': in_round(tax_details_by_code.get('cess_rate', 0.00), 3),
'CesAmt': in_round(tax_details_by_code.get('cess_amount', 0.00)),
'CesNonAdvlAmt': in_round(
tax_details_by_code.get('cess_non_advol_amount', 0.00)
),
'StateCesRt': in_round(tax_details_by_code.get('state_cess_rate_amount', 0.00), 3),
'StateCesAmt': in_round(tax_details_by_code.get('state_cess_amount', 0.00)),
'StateCesNonAdvlAmt': in_round(
tax_details_by_code.get('state_cess_non_advol_amount', 0.00)
),
'OthChrg': in_round(tax_details_by_code.get('other_amount', 0.00)),
'TotItemVal': in_round((sign * line.balance) + line_tax_details.get('tax_amount', 0.00)),
}
if line.name:
line_details['PrdDesc'] = line.name.replace("\n", "")[:300]
return line_details
def _l10n_in_edi_generate_invoice_json_managing_negative_lines(self, json_payload):
"""Set negative lines against positive lines as discount with same HSN code and tax rate
With negative lines
product name | hsn code | unit price | qty | discount | total
=============================================================
product A | 123456 | 1000 | 1 | 100 | 900
product B | 123456 | 1500 | 2 | 0 | 3000
Discount | 123456 | -300 | 1 | 0 | -300
Converted to without negative lines
product name | hsn code | unit price | qty | discount | total
=============================================================
product A | 123456 | 1000 | 1 | 100 | 900
product B | 123456 | 1500 | 2 | 300 | 2700
totally discounted lines are kept as 0, though
"""
def discount_group_key(line_vals):
return "%s-%s" % (line_vals['HsnCd'], line_vals['GstRt'])
def put_discount_on(discount_line_vals, other_line_vals):
discount = -discount_line_vals['AssAmt']
discount_to_allow = other_line_vals['AssAmt']
in_round = self._l10n_in_round_value
amount_keys = (
'AssAmt', 'IgstAmt', 'CgstAmt', 'SgstAmt', 'CesAmt',
'CesNonAdvlAmt', 'StateCesAmt', 'StateCesNonAdvlAmt',
'OthChrg', 'TotItemVal'
)
if float_compare(discount_to_allow, discount, precision_rounding=self.currency_id.rounding) < 0:
# Update discount line, needed when discount is more then max line, in short remaining_discount is not zero
discount_line_vals.update({
key: in_round(discount_line_vals[key] + other_line_vals[key])
for key in amount_keys
})
other_line_vals['Discount'] = in_round(other_line_vals['Discount'] + discount_to_allow)
other_line_vals.update(dict.fromkeys(amount_keys, 0.00))
return False
other_line_vals['Discount'] = in_round(other_line_vals['Discount'] + discount)
other_line_vals.update({
key: in_round(other_line_vals[key] + discount_line_vals[key])
for key in amount_keys
})
return True
discount_lines = []
for discount_line in json_payload['ItemList'].copy(): #to be sure to not skip in the loop:
if discount_line['AssAmt'] < 0:
discount_lines.append(discount_line)
json_payload['ItemList'].remove(discount_line)
if not discount_lines:
return json_payload
self.message_post(
author_id=self.env.ref('base.partner_root').id,
body=_("Negative lines will be decreased from positive invoice lines having the same taxes and HSN code")
)
lines_grouped_and_sorted = defaultdict(list)
for line in sorted(json_payload['ItemList'], key=lambda i: i['AssAmt'], reverse=True):
lines_grouped_and_sorted[discount_group_key(line)].append(line)
for discount_line in discount_lines:
for apply_discount_on in lines_grouped_and_sorted[discount_group_key(discount_line)]:
if put_discount_on(discount_line, apply_discount_on):
break
return json_payload
def _l10n_in_edi_generate_invoice_json(self):
self.ensure_one()
tax_details = self._l10n_in_prepare_tax_details()
seller_buyer = self._get_l10n_in_seller_buyer_party()
tax_details_by_code = self._get_l10n_in_tax_details_by_line_code(tax_details['tax_details'])
is_intra_state = self.l10n_in_state_id == self.company_id.state_id
is_overseas = self.l10n_in_gst_treatment == "overseas"
line_ids = []
global_discount_line_ids = []
grouping_lines = self.invoice_line_ids.grouped(
lambda l: l.display_type == 'product' and (l._l10n_in_is_global_discount() and 'global_discount' or 'lines')
)
default_line = self.env['account.move.line'].browse()
lines = grouping_lines.get('lines', default_line)
global_discount_line = grouping_lines.get('global_discount', default_line)
tax_details_per_record = tax_details['tax_details_per_record']
sign = self.is_inbound() and -1 or 1
rounding_amount = sum(line.balance for line in self.line_ids if line.display_type == 'rounding') * sign
global_discount_amount = sum(line.balance for line in global_discount_line) * -sign
in_round = self._l10n_in_round_value
json_payload = {
"Version": "1.1",
"TranDtls": {
"TaxSch": "GST",
"SupTyp": self._l10n_in_get_supply_type(tax_details_by_code.get('igst_amount')),
"RegRev": tax_details_by_code.get('is_reverse_charge') and "Y" or "N",
"IgstOnIntra": (
# for Export SEZ LUT tax as per e-invoice api doc validation point 32
# Export and SEZ must be treated as Inter state supply
self.l10n_in_gst_treatment not in ('special_economic_zone', 'overseas')
and is_intra_state
and tax_details_by_code.get("igst_amount")
and "Y" or "N"
),
},
"DocDtls": {
"Typ": (self.move_type == "out_refund" and "CRN") or (self.debit_origin_id and "DBN") or "INV",
"No": self.name,
"Dt": self.invoice_date and self.invoice_date.strftime("%d/%m/%Y")
},
"SellerDtls": self._get_l10n_in_edi_partner_details(seller_buyer['seller_details']),
"BuyerDtls": self._get_l10n_in_edi_partner_details(
seller_buyer['buyer_details'],
pos_state_id=self.l10n_in_state_id,
is_overseas=is_overseas
),
"ItemList": [
self._get_l10n_in_edi_line_details(
index,
line,
tax_details_per_record.get(line, {})
)
for index, line in enumerate(lines, start=1)
],
"ValDtls": {
"AssVal": in_round(tax_details['base_amount']),
"CgstVal": in_round(tax_details_by_code.get("cgst_amount", 0.00)),
"SgstVal": in_round(tax_details_by_code.get("sgst_amount", 0.00)),
"IgstVal": in_round(tax_details_by_code.get("igst_amount", 0.00)),
"CesVal": in_round((
tax_details_by_code.get("cess_amount", 0.00)
+ tax_details_by_code.get("cess_non_advol_amount", 0.00)),
),
"StCesVal": in_round((
tax_details_by_code.get("state_cess_amount", 0.00)
+ tax_details_by_code.get("state_cess_non_advol_amount", 0.00)), # clean this up =p
),
"Discount": in_round(global_discount_amount),
"RndOffAmt": in_round(rounding_amount),
"TotInvVal": in_round(
tax_details["base_amount"]
+ tax_details["tax_amount"]
+ rounding_amount
- global_discount_amount
),
},
}
if self.company_currency_id != self.currency_id:
json_payload["ValDtls"].update({
"TotInvValFc": in_round(
(tax_details.get("base_amount_currency") + tax_details.get("tax_amount_currency")))
})
if seller_buyer['seller_details'] != seller_buyer['dispatch_details']:
json_payload['DispDtls'] = self._get_l10n_in_edi_partner_details(
seller_buyer['dispatch_details'],
set_vat=False,
set_phone_and_email=False
)
if seller_buyer['buyer_details'] != seller_buyer['ship_to_details']:
json_payload['ShipDtls'] = self._get_l10n_in_edi_partner_details(
seller_buyer['ship_to_details'],
is_overseas=is_overseas
)
if is_overseas:
json_payload['ExpDtls'] = {
'RefClm': tax_details_by_code.get('igst_amount') and 'Y' or 'N',
'ForCur': self.currency_id.name,
'CntCode': seller_buyer['buyer_details'].country_id.code or '',
}
if shipping_bill_no := self.l10n_in_shipping_bill_number:
json_payload['ExpDtls']['ShipBNo'] = shipping_bill_no
if shipping_bill_date := self.l10n_in_shipping_bill_date:
json_payload['ExpDtls']['ShipBDt'] = shipping_bill_date.strftime("%d/%m/%Y")
if shipping_port_code_id := self.l10n_in_shipping_port_code_id:
json_payload['ExpDtls']['Port'] = shipping_port_code_id.code
json_valdtls = json_payload['ValDtls']
base_and_tax_amount = tax_details.get("base_amount") + tax_details.get("tax_amount")
# For Export If with payment of Tax then we need to include Tax in Total Invoice Value
if json_payload['TranDtls']['SupTyp'] == 'EXPWP' and json_valdtls['AssVal'] == base_and_tax_amount:
json_payload["ValDtls"]["TotInvVal"] = self._l10n_in_round_value(sum([
json_valdtls['TotInvVal'],
json_valdtls['IgstVal'],
json_valdtls['CgstVal'],
json_valdtls['SgstVal'],
json_valdtls['CesVal'],
json_valdtls['StCesVal'],
]))
for line in json_payload["ItemList"]:
line["TotItemVal"] = self._l10n_in_round_value(sum([
line["TotItemVal"],
line["IgstAmt"],
line["CgstAmt"],
line["SgstAmt"],
line["CesAmt"],
line["CesNonAdvlAmt"],
line["StateCesAmt"],
line["StateCesNonAdvlAmt"],
]))
return self._l10n_in_edi_generate_invoice_json_managing_negative_lines(json_payload)
def _l10n_in_get_supply_type(self, is_igst_amount):
if self.l10n_in_gst_treatment in ("overseas", "special_economic_zone") and is_igst_amount:
return {
'overseas': 'EXPWP',
'special_economic_zone': 'SEZWP',
}[self.l10n_in_gst_treatment]
return {
'deemed_export': 'DEXP',
'overseas': 'EXPWOP',
'special_economic_zone': 'SEZWOP',
}.get(self.l10n_in_gst_treatment, 'B2B')
# ================= Get Error =================
def _l10n_in_check_einvoice_validation(self):
alerts = {
**self.company_id._l10n_in_check_einvoice_validation(),
**(self.partner_id | self.partner_shipping_id)._l10n_in_check_einvoice_validation(),
**self.invoice_line_ids._l10n_in_check_einvoice_validation(),
}
if invalid_records := self.filtered(lambda m: not re.match("^.{1,16}$", m.name)):
alerts['l10n_in_edi_invalid_invoice_number'] = {
'message': _("Invoice number should not be more than 16 characters"),
'action_text': _("View Invoices"),
'action': invalid_records._get_records_action(name=_("Check Invoices")),
}
return alerts
# ================================ API methods ===========================
def _l10n_in_edi_connect_to_server(self, url_end_point, json_payload=False, params=False):
"""
url_end_point possible values (generate, getirnbydocdetails, generate_ewaybill_by_irn, get_ewaybill_by_irn, cancel)
is used to get the EDI response from the server
"""
company = self.company_id
token = company._l10n_in_edi_get_token()
if not token:
return {
'error': [{
'code': '0',
'message': _(
"Ensure GST Number set on company setting and API are Verified."
)
}]
}
default_params = {
'auth_token': token,
'username': company.sudo().l10n_in_edi_username,
'gstin': company.vat,
}
if params:
# To be used when generate_ewaybill_by_irn, get_ewaybill_by_irn
params.update(default_params)
else:
params = {
**default_params,
'json_payload': json_payload
}
try:
response = self.env['iap.account']._l10n_in_connect_to_server(
company.sudo().l10n_in_edi_production_env,
params,
f"/iap/l10n_in_edi/1/{url_end_point}",
'l10n_in_edi.endpoint'
)
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."
)
}]
}
if (error := response.get('error')) and '1005' in [e.get("code") for e in error]:
# Invalid token error 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 = company._l10n_in_edi_authenticate()
if not authenticate_response.get("error"):
response = self._l10n_in_edi_connect_to_server(
url_end_point=url_end_point,
json_payload=json_payload,
params=params
)
else:
return authenticate_response
return response

View file

@ -0,0 +1,60 @@
from odoo import models
class AccountMoveLine(models.Model):
_inherit = 'account.move.line'
# E-Invoice Methods
def _l10n_in_is_global_discount(self):
self.ensure_one()
return not self.tax_ids and self.price_subtotal < 0
def _l10n_in_check_einvoice_validation(self):
_ = self.env._
error_messages = {
'invalid_hsn': _(
"Missing or invalid HSN/SAC code: Ensure that invoice lines contain "
"4, 6 or 8 digits"
),
'restrict_negative_discount_line': _("Negative discount is not allowed"),
'tax_validation': _(
"Set an appropriate GST tax on invoice lines "
"(if it's zero rated or nil rated then apply it too)"
),
}
error_lines = {}
for line in self:
error_codes = []
if line.display_type != 'product' or line._l10n_in_is_global_discount():
continue
if line._l10n_in_check_invalid_hsn_code():
error_codes.append('invalid_hsn')
if line.discount < 0:
error_codes.append('restrict_negative_discount_line')
if not any(tax.l10n_in_tax_type in ['gst', 'nil_rated', 'exempt', 'non_gst'] for tax in line.tax_ids.flatten_taxes_hierarchy()):
error_codes.append('tax_validation')
for code in error_codes:
error_lines[code] = error_lines.get(code, self.env['account.move.line']) | line
return {
f"l10n_in_edi_{error_code}": {
'level': 'danger' if error_code == 'invalid_hsn' else 'warning',
'message': error_messages[error_code],
'action_text': _("View Invoice Line(s)"),
# The context are set in view_move_line_tree_hsn_l10n_in
# Please make sure to change, if any change in error codes
'action': lines.with_context(**{
error_code: True,
'send_and_print': True
})._get_records_action(
name=_("Check Invoice Line(s)"),
domain=[('id', 'in', lines.ids)],
views=[(
self.env.ref('l10n_in.view_move_line_tree_hsn_l10n_in').id,
'list'
)],
),
}
for error_code, lines in error_lines.items()
}

View file

@ -0,0 +1,65 @@
from odoo import api, fields, models
class AccountMoveSend(models.AbstractModel):
_inherit = 'account.move.send'
@api.model
def _is_in_edi_applicable(self, move):
return (
move._l10n_in_check_einvoice_eligible()
and move.state == 'posted'
and move.l10n_in_edi_status != 'sent'
)
def _get_all_extra_edis(self) -> dict:
# EXTENDS 'account'
res = super()._get_all_extra_edis()
res.update({
'in_edi_send': {
'label': self.env._("E-invoicing"),
'is_applicable': self._is_in_edi_applicable,
'help': self.env._(
"Send the e-invoice json to the Indian Invoice Registration Portal (IRP)."
)
}
})
return res
# -------------------------------------------------------------------------
# ALERTS
# -------------------------------------------------------------------------
def _get_alerts(self, moves, moves_data):
# EXTENDS 'account'
alerts = super()._get_alerts(moves, moves_data)
if in_moves := moves.filtered(lambda m: 'in_edi_send' in moves_data[m]['extra_edis']):
if in_alerts := in_moves._l10n_in_check_einvoice_validation():
alerts.update(in_alerts)
return alerts
# -------------------------------------------------------------------------
# SENDING METHODS
# -------------------------------------------------------------------------
def _get_invoice_extra_attachments(self, invoice):
# EXTENDS 'account'
return super()._get_invoice_extra_attachments(invoice) + invoice.l10n_in_edi_attachment_id
def _call_web_service_before_invoice_pdf_render(self, invoices_data):
# EXTENDS 'account'
super()._call_web_service_before_invoice_pdf_render(invoices_data)
for invoice, invoice_data in invoices_data.items():
if 'in_edi_send' in invoice_data['extra_edis']:
if error := invoice._l10n_in_edi_send_invoice():
invoice_data['error'] = {
'error_title': self.env._(
"Error when sending the invoice to government:"
),
'errors': error['messages'],
'retry': error.get('is_warning'),
}
elif invoice.invoice_pdf_report_id:
invoice.write({'invoice_pdf_report_file': False})
if self._can_commit():
self.env.cr.commit()

View file

@ -0,0 +1,21 @@
from odoo import api, models
from odoo.exceptions import UserError
class IrAttachment(models.Model):
_inherit = 'ir.attachment'
@api.ondelete(at_uninstall=False)
def _unlink_except_l10n_in_government_document(self):
"""
Prevents the deletion of attachments related to government-issued documents.
"""
if any(
attachment.res_model == 'account.move'
and attachment.mimetype == 'application/json'
and attachment.res_field == 'l10n_in_edi_attachment_file'
for attachment in self
):
raise UserError(self.env._(
"You can't unlink an attachment that you received from the government"
))

View file

@ -1,24 +1,100 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import pytz
from odoo import fields, models
from odoo import api, fields, models, _
from odoo.exceptions import ValidationError
from odoo.exceptions import AccessError, ValidationError
from stdnum.in_ import pan, gstin
class ResCompany(models.Model):
_inherit = "res.company"
_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="base.group_system")
l10n_in_edi_token_validity = fields.Datetime("E-invoice (IN) Valid Until", groups="base.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",
# E-Invoice fields
l10n_in_edi_feature = fields.Boolean(string="Indian E-Invoicing")
l10n_in_edi_username = fields.Char(
string="E-invoice (IN) Username",
groups="base.group_system"
)
l10n_in_edi_password = fields.Char(
string="E-invoice (IN) Password",
groups="base.group_system"
)
l10n_in_edi_token = fields.Char(
string="E-invoice (IN) Token",
groups="base.group_system"
)
l10n_in_edi_token_validity = fields.Datetime(
string="E-invoice (IN) Valid Until",
groups="base.group_system"
)
# E-Invoice Business Methods
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 self.l10n_in_edi_token and self.l10n_in_edi_token_validity > fields.Datetime.now()
def _l10n_in_edi_get_token(self):
self_sudo = self.sudo()
if self_sudo.l10n_in_edi_username and self_sudo._l10n_in_edi_token_is_valid():
return self_sudo.l10n_in_edi_token
elif self_sudo.l10n_in_edi_username and self_sudo.l10n_in_edi_password:
self_sudo._l10n_in_edi_authenticate()
return self_sudo.l10n_in_edi_token
return False
def _l10n_in_edi_authenticate(self):
self_sudo = self.sudo()
params = {
"username": self_sudo.l10n_in_edi_username,
"password": self_sudo.l10n_in_edi_password,
"gstin": self_sudo.vat,
}
try:
response = self.env['iap.account']._l10n_in_connect_to_server(
self_sudo.l10n_in_edi_production_env,
params,
"/iap/l10n_in_edi/1/authenticate",
"l10n_in_edi.endpoint"
)
except AccessError as e:
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."
)
}]
}
# validity data-time in Indian standard time(UTC+05:30) convert IST to UTC
if data := response.get('data'):
tz = pytz.timezone("Asia/Kolkata")
local_time = tz.localize(fields.Datetime.to_datetime(data["TokenExpiry"]))
utc_time = local_time.astimezone(pytz.utc)
self_sudo.write({
'l10n_in_edi_token_validity': fields.Datetime.to_string(utc_time),
'l10n_in_edi_token': data['AuthToken'],
})
return response
def _l10n_in_check_einvoice_validation(self):
checks = {
'company_address_missing': {
'fields': ('street', 'zip', 'city', 'state_id', 'country_id',),
'message': _("Companies should have a complete address, verify their Street, City, State, Country and Zip code."),
},
}
return {
f"l10n_in_edi_{key}": {
'message': check['message'],
'action_text': (
_("View Companies") if len(invalid_records) > 1
else _("View %s", invalid_records.name)
),
'action': invalid_records._get_records_action(name=_("Check Company Data")),
}
for key, check in checks.items()
if (invalid_records := self.filtered(lambda record: any(not record[field] for field in check['fields'])))
}

View file

@ -1,54 +1,52 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models, fields, _
from odoo.exceptions import UserError, RedirectWarning
from odoo import fields, models
from odoo.exceptions import UserError
class ResConfigSettings(models.TransientModel):
_inherit = "res.config.settings"
_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",
# E-Invoice
l10n_in_edi_feature = fields.Boolean(
related='company_id.l10n_in_edi_feature',
readonly=False
)
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
)
def l10n_in_check_gst_number(self):
if not self.company_id.vat:
action = {
"view_mode": "form",
"res_model": "res.company",
"type": "ir.actions.act_window",
"res_id" : self.company_id.id,
"views": [[self.env.ref("base.view_company_form").id, "form"]],
}
raise RedirectWarning(_("Please enter a GST number in company."), action, _('Go to Company'))
# E-Invoice Methods
def l10n_in_edi_test(self):
self.l10n_in_check_gst_number()
response = self.env['account.edi.format']._l10n_in_edi_authenticate(self.company_id)
self._l10n_in_check_gst_number()
response = self.company_id._l10n_in_edi_authenticate()
_ = self.env._
if response.get('error'):
raise UserError("\n".join(["[%s] %s" % (e.get('code'), (e.get('message'))) for e in response['error']]))
elif not self.company_id.sudo()._l10n_in_edi_token_is_valid():
raise UserError(_("Incorrect username or password, or the GST number on company does not match."))
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'type': 'info',
'sticky': False,
'message': _("API credentials validated successfully"),
}
}
def l10n_in_edi_buy_iap(self):
if not self.l10n_in_edi_production_env:
raise UserError(_("You must enable production environment to buy credits"))
return {
'type': 'ir.actions.act_url',
'url': self.env["iap.account"].get_credits_url(service_name="l10n_in_edi", base_url=''),
'target': '_new'
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'type': 'info',
'sticky': False,
'message': _("API credentials validated successfully"),
}
}
def _l10n_in_gsp_provider_changed(self):
"""
This change should effect all Indian companies so we search for them and
Invalidate existing tokens if GSP provider changed
"""
super()._l10n_in_gsp_provider_changed()
self.env['res.company'].sudo().search([('account_fiscal_country_id.code', '=', 'IN')]).write({
'l10n_in_edi_token': False,
'l10n_in_edi_token_validity': False,
})

View file

@ -0,0 +1,56 @@
import re
from odoo import _, models
class ResPartner(models.Model):
_inherit = 'res.partner'
# E-Invoice Validation
def _l10n_in_edi_strict_error_validation(self):
"""
This method is used to check the strict validation of the partner data
as per government API json schema (https://einv-apisandbox.nic.in/version1.03/generate-irn.html#requestSampleJSON)
In case of any error, it will return the error message
Note - We stimulate as error message from API, so that user can understand the error
Also restrict unwanted request to government servers and avoid getting black listed
"""
message = []
if not re.match("^.{3,100}$", self.street or ""):
message.append(_("- Street required min 3 and max 100 characters"))
if not re.match("^.{3,100}$", self.city or ""):
message.append(_("- City required min 3 and max 100 characters"))
if self.country_id.code == "IN" and not re.match("^.{3,50}$", self.state_id.name or ""):
message.append(_("- State required min 3 and max 50 characters"))
if self.country_id.code == "IN" and not re.match("^([1-9][0-9]{5})$", self.zip or ""):
message.append(_("- ZIP code required 6 digits ranging from 100000 to 999999"))
if (
self.country_id.code == "IN"
and not re.match(r"^(?!0+$)([0-9]{2})$", self.state_id.l10n_in_tin or "")
):
message.append(_("- State TIN Number must be exactly 2 digits."))
if message:
message.insert(0, self.display_name)
return message
def _l10n_in_check_einvoice_validation(self):
checks = {
'partner_address_missing': {
'fields': ('street', 'zip', 'city', 'state_id', 'country_id',),
'message': _(
"Partners should have a complete address, verify their Street, City, State, Country and Zip code."
),
},
}
return {
f"l10n_in_edi_{key}": {
'message': check['message'],
'action_text': (
_("View Partners") if len(invalid_records) > 1
else _("View %s", invalid_records.name)
),
'action': invalid_records._get_records_action(name=_("Check Partner Data")),
}
for key, check in checks.items()
if (invalid_records := self.filtered(lambda record: any(not record[field] for field in check['fields'])))
}

View file

@ -0,0 +1,2 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_l10n_in_edi_cancel,access_l10n_in_edi_cancel,model_l10n_in_edi_cancel,account.group_account_invoice,1,1,1,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_l10n_in_edi_cancel access_l10n_in_edi_cancel model_l10n_in_edi_cancel account.group_account_invoice 1 1 1 0

View file

@ -1,4 +1 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import test_edi_json

View file

@ -5,10 +5,86 @@
<field name="model">account.move</field>
<field name="inherit_id" ref="account.view_move_form"/>
<field name="arch" type="xml">
<!-- E-Invoice -->
<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)]}"/>
<field name="l10n_in_edi_cancel_reason"
string="EDI Cancel Reason"
invisible="not l10n_in_edi_cancel_reason"
readonly="1"/>
<field name="l10n_in_edi_cancel_remarks"
string="EDI Cancel Remarks"
invisible="not l10n_in_edi_cancel_remarks"
readonly="1"/>
</xpath>
<xpath expr="//header" position="after">
<div class="alert alert-danger" role="alert"
invisible="not l10n_in_edi_error">
<div class="o_row">
<field name="l10n_in_edi_error"/>
<button name="action_l10n_in_edi_force_cancel"
type="object"
class="oe_link oe_inline"
confirm="Are you sure you want to cancel this invoice without waiting for the EDI document to be canceled?"
invisible="l10n_in_edi_status != 'sent'"
string="Force Cancel"/>
</div>
</div>
</xpath>
<xpath expr="//button[@name='button_draft']" position="after">
<button name="action_export_l10n_in_edi_content_json"
type="object"
class="btn-secondary"
invisible="not l10n_in_edi_error"
groups="base.group_no_one"
string="Download EDI JSON"/>
</xpath>
<xpath expr="//div[@name='journal_div']" position="after">
<field name="l10n_in_edi_status" invisible="not l10n_in_edi_status or state == 'draft'"/>
</xpath>
</field>
</record>
<record id="view_out_invoice_tree_inherit_l10n_in_edi" model="ir.ui.view">
<field name="name">out.invoice.list.inherit.l10n_in_edi</field>
<field name="model">account.move</field>
<field name="inherit_id" ref="account.view_out_invoice_tree"/>
<field name="arch" type="xml">
<field name="status_in_payment" position="before">
<field name="l10n_in_edi_status"
optional="hide"
widget="badge"
decoration-success="l10n_in_edi_status == 'sent'"
decoration-info="l10n_in_edi_status == 'to_send'"
decoration-danger="l10n_in_edi_status == 'cancelled'"/>
</field>
</field>
</record>
<record id="view_out_credit_note_tree_inherit_l10n_in_edi" model="ir.ui.view">
<field name="name">out.credit.note.list.inherit.l10n_in_edi</field>
<field name="model">account.move</field>
<field name="inherit_id" ref="account.view_out_credit_note_tree"/>
<field name="arch" type="xml">
<field name="status_in_payment" position="before">
<field name="l10n_in_edi_status"
optional="hide"
widget="badge"
decoration-success="l10n_in_edi_status == 'sent'"
decoration-info="l10n_in_edi_status == 'to_send'"
decoration-danger="l10n_in_edi_status == 'cancelled'"/>
</field>
</field>
</record>
<record id="l10n_in_edi_inherit_account_move_search_view" model="ir.ui.view">
<field name="name">l10n.in.edi.inherit.account.move.search</field>
<field name="model">account.move</field>
<field name="inherit_id" ref="account.view_account_invoice_filter" />
<field name="arch" type="xml">
<xpath expr="//filter[@name='cancel']" position="after">
<separator/>
<filter name="l10n_in_edi_to_send" string="Indian E-Invoices To Send" domain="[('l10n_in_edi_status', '=', 'to_send')]"/>
<filter name="l10n_in_edi_error_move" string="Indian E-Invoices In Error" domain="[('l10n_in_edi_error', '!=', False)]"/>
</xpath>
<xpath expr="//filter[@name='status']" position="after">
<filter name="groupby_l10n_in_edi_status" string="Indian E-invoice status" context="{'group_by' : 'l10n_in_edi_status'}"/>
</xpath>
</field>
</record>

View file

@ -3,26 +3,21 @@
<template id="l10n_in_einvoice_report_invoice_document_inherit" inherit_id="account.report_invoice_document">
<xpath expr="//div[@id='informations']" position="inside">
<t t-set="l10n_in_einvoice_json" t-value="o._get_l10n_in_edi_response_json()"/>
<div class="col-auto col-3 mw-100 mb-2" t-if="l10n_in_einvoice_json" name="ack_no">
<strong>Acknowledgement:</strong>
<p class="m-0" t-out="l10n_in_einvoice_json['AckNo']"/>
<p t-out="l10n_in_einvoice_json['AckDt']"/>
<div class="col" t-if="l10n_in_einvoice_json" name="ack_no">
<strong>Acknowledgement</strong>
<div t-out="l10n_in_einvoice_json['AckNo']"/>
<div t-out="l10n_in_einvoice_json['AckDt']"/>
</div>
</xpath>
<xpath expr="//div[@id='total']/div[1]" position="attributes">
<attribute name="t-attf-class">{{('col-6' if report_type != 'html' else 'col-sm-6 col-md-5 ms-auto') if l10n_in_einvoice_json else ''}}
{{(('col-6' if report_type != 'html' else 'col-sm-7 col-md-6') + ' ms-auto') if not l10n_in_einvoice_json else ''}}</attribute>
</xpath>
<xpath expr="//div[@id='total']/div[1]" position="before">
<xpath expr="//div[@id='right-elements']" position="after">
<t t-set="l10n_in_einvoice_json" t-value="o._get_l10n_in_edi_response_json()"/>
<div t-attf-class="#{'col-6' if report_type != 'html' else 'col-sm-7 col-md-6 ms-auto'} row" t-if="l10n_in_einvoice_json">
<div class="col-7 me-2">
<div t-attf-class="#{'col-5' if report_type != 'html' else 'ms-auto'} row avoid-page-break-inside" t-if="l10n_in_einvoice_json">
<div class="col-7 me-2" t-attf-style="#{'' if report_type != 'html' else 'padding: 0 !important;'}">
<strong>IRN:</strong>
<span t-esc="l10n_in_einvoice_json['Irn']"/>
</div>
<div class="col-3 mt-1">
<img t-att-src="'/report/barcode/?barcode_type=%s&amp;value=%s&amp;width=%s&amp;height=%s' %('QR', l10n_in_einvoice_json['SignedQRCode'], 500, 500)" style="max-height: 155px"/>
<img t-att-src="'/report/barcode/?barcode_type=%s&amp;value=%s&amp;width=%s&amp;height=%s&amp;quiet=%s' %('QR', l10n_in_einvoice_json['SignedQRCode'], 500, 500, 0)" style="max-height: 155px"/>
</div>
</div>
</xpath>

View file

@ -5,40 +5,24 @@
<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">
<span class="o_form_label">Setup E-invoice</span>
<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">
Check the <a href="https://www.odoo.com/documentation/16.0/applications/finance/fiscal_localizations/india.html">documentation</a> to get credentials
</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"/>
</div>
<div class="row">
<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"/>
</div>
<div class="row">
<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>
</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 class='mt8'>
<button name="l10n_in_edi_buy_iap" title="Costs 1 credit per transaction. Free 200 credits will be available for the first time." icon="fa-arrow-right" type="object" string="Buy credits" class="btn-link"/>
</div>
</div>
<xpath expr="//field[@name='module_l10n_in_edi']" position="replace">
<field name="l10n_in_edi_feature"/>
</xpath>
<xpath expr="//setting[@name='electronic_invoices_in']/div[hasclass('content-group')]" position="replace">
<div class="content-group" invisible="not l10n_in_edi_feature">
<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"/>
</div>
<div class="row">
<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"/>
</div>
</div>
</div>
<div class='mt8' invisible="not l10n_in_edi_feature">
<button name="l10n_in_edi_test" icon="oi-arrow-right" type="object" string="Verify Username and Password" class="btn-link"/>
</div>
</xpath>
</field>
</record>
</odoo>

View file

@ -0,0 +1 @@
from . import l10n_in_edi_cancel

View file

@ -0,0 +1,25 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
from odoo.addons.l10n_in.models.account_invoice import EDI_CANCEL_REASON
class L10n_In_EdiCancel(models.TransientModel):
_name = 'l10n_in_edi.cancel'
_description = "Cancel E-Invoice"
move_id = fields.Many2one('account.move', string="Invoice", required=True)
cancel_reason = fields.Selection(
selection=list(EDI_CANCEL_REASON.items()),
string="Cancel Reason",
required=True
)
cancel_remarks = fields.Char("Cancel Remarks", required=True)
def cancel_l10n_in_edi_move(self):
self.move_id.write({
'l10n_in_edi_cancel_reason': self.cancel_reason,
'l10n_in_edi_cancel_remarks': self.cancel_remarks,
})
self.move_id._l10n_in_edi_cancel_invoice()

View file

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="view_ewaybill_cancel_form" model="ir.ui.view">
<field name="name">l10n_in_edi.cancel.form</field>
<field name="model">l10n_in_edi.cancel</field>
<field name="arch" type="xml">
<form>
<group>
<group>
<field name="cancel_reason"/>
<field name="cancel_remarks"/>
</group>
</group>
<footer>
<button string="Cancel E-Invoice"
name="cancel_l10n_in_edi_move"
type="object"
class="btn-primary"
data-hotkey="c"/>
<button string="Discard"
class="btn-secondary"
special="cancel"
data-hotkey="x"/>
</footer>
</form>
</field>
</record>
</data>
</odoo>

View file

@ -1,14 +1,15 @@
[project]
name = "odoo-bringout-oca-ocb-l10n_in_edi"
version = "16.0.0"
description = "Indian - E-invoicing - Odoo addon"
description = "Indian - E-invoicing -
Odoo addon
"
authors = [
{ name = "Ernad Husremovic", email = "hernad@bring.out.ba" }
]
dependencies = [
"odoo-bringout-oca-ocb-account_edi>=16.0.0",
"odoo-bringout-oca-ocb-l10n_in>=16.0.0",
"odoo-bringout-oca-ocb-iap>=16.0.0",
"odoo-bringout-oca-ocb-account_edi>=19.0.0",
"odoo-bringout-oca-ocb-l10n_in>=19.0.0",
"requests>=2.25.1"
]
readme = "README.md"
@ -18,7 +19,7 @@ classifiers = [
"Intended Audience :: Developers",
"License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Office/Business",
]