diff --git a/.flake8 b/.flake8
new file mode 100644
index 00000000000..399b176e1d0
--- /dev/null
+++ b/.flake8
@@ -0,0 +1,32 @@
+[flake8]
+ignore =
+ E121,
+ E126,
+ E127,
+ E128,
+ E203,
+ E225,
+ E226,
+ E231,
+ E241,
+ E251,
+ E261,
+ E265,
+ E302,
+ E303,
+ E305,
+ E402,
+ E501,
+ E741,
+ W291,
+ W292,
+ W293,
+ W391,
+ W503,
+ W504,
+ F403,
+ B007,
+ B950,
+ W191,
+
+max-line-length = 200
\ No newline at end of file
diff --git a/erpnext/__init__.py b/erpnext/__init__.py
index a76090d609d..566dd158a67 100644
--- a/erpnext/__init__.py
+++ b/erpnext/__init__.py
@@ -5,7 +5,7 @@ import frappe
from erpnext.hooks import regional_overrides
from frappe.utils import getdate
-__version__ = '12.19.0'
+__version__ = '12.20.0'
def get_default_company(user=None):
'''Get default company for user'''
diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py
index 4702b8a434f..b4599ba0f46 100644
--- a/erpnext/accounts/doctype/gl_entry/gl_entry.py
+++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py
@@ -294,4 +294,8 @@ def rename_temporarily_named_docs(doctype):
oldname = doc.name
set_name_from_naming_options(frappe.get_meta(doctype).autoname, doc)
newname = doc.name
- frappe.db.sql("""UPDATE `tab{}` SET name = %s, to_rename = 0 where name = %s""".format(doctype), (newname, oldname))
+ frappe.db.sql(
+ "UPDATE `tab{}` SET name = %s, to_rename = 0 where name = %s".format(doctype),
+ (newname, oldname),
+ auto_commit=True
+ )
diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py
index fb2f9ff8991..c9a820e2e27 100644
--- a/erpnext/accounts/doctype/pricing_rule/utils.py
+++ b/erpnext/accounts/doctype/pricing_rule/utils.py
@@ -467,7 +467,7 @@ def apply_pricing_rule_on_transaction(doc):
if not d.get(pr_field): continue
- if d.validate_applied_rule and doc.get(field) < d.get(pr_field):
+ if d.validate_applied_rule and doc.get(field) is not None and doc.get(field) < d.get(pr_field):
frappe.msgprint(_("User has not applied rule on the invoice {0}")
.format(doc.name))
else:
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
index 3cf4d5994a5..8c0d4efb954 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
@@ -509,7 +509,7 @@ frappe.ui.form.on("Purchase Invoice", {
},
onload: function(frm) {
- if(frm.doc.__onload) {
+ if(frm.doc.__onload && frm.is_new()) {
if(frm.doc.supplier) {
frm.doc.apply_tds = frm.doc.__onload.supplier_tds ? 1 : 0;
}
diff --git a/erpnext/accounts/doctype/purchase_invoice/test_records.json b/erpnext/accounts/doctype/purchase_invoice/test_records.json
index 171927c1822..2a8c97b0ecf 100644
--- a/erpnext/accounts/doctype/purchase_invoice/test_records.json
+++ b/erpnext/accounts/doctype/purchase_invoice/test_records.json
@@ -43,7 +43,7 @@
}
],
"grand_total": 0,
- "naming_series": "_T-BILL",
+ "naming_series": "T-PINV-",
"taxes": [
{
"account_head": "_Test Account Shipping Charges - _TC",
@@ -168,7 +168,7 @@
}
],
"grand_total": 0,
- "naming_series": "_T-Purchase Invoice-",
+ "naming_series": "T-PINV-",
"taxes": [
{
"account_head": "_Test Account Shipping Charges - _TC",
diff --git a/erpnext/accounts/doctype/sales_invoice/test_records.json b/erpnext/accounts/doctype/sales_invoice/test_records.json
index 11ebe6a573a..5fad6e263e3 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_records.json
+++ b/erpnext/accounts/doctype/sales_invoice/test_records.json
@@ -30,7 +30,7 @@
"base_grand_total": 561.8,
"grand_total": 561.8,
"is_pos": 0,
- "naming_series": "_T-Sales Invoice-",
+ "naming_series": "T-SINV-",
"base_net_total": 500.0,
"taxes": [
{
@@ -103,7 +103,7 @@
"base_grand_total": 630.0,
"grand_total": 630.0,
"is_pos": 0,
- "naming_series": "_T-Sales Invoice-",
+ "naming_series": "T-SINV-",
"base_net_total": 500.0,
"taxes": [
{
@@ -174,7 +174,7 @@
],
"grand_total": 0,
"is_pos": 0,
- "naming_series": "_T-Sales Invoice-",
+ "naming_series": "T-SINV-",
"taxes": [
{
"account_head": "_Test Account Shipping Charges - _TC",
@@ -300,7 +300,7 @@
],
"grand_total": 0,
"is_pos": 0,
- "naming_series": "_T-Sales Invoice-",
+ "naming_series": "T-SINV-",
"taxes": [
{
"account_head": "_Test Account Excise Duty - _TC",
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index 4db739e09df..a5efe546dda 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -1860,7 +1860,17 @@ class TestSalesInvoice(unittest.TestCase):
def test_einvoice_submission_without_irn(self):
# init
- frappe.db.set_value('E Invoice Settings', 'E Invoice Settings', 'enable', 1)
+ einvoice_settings = frappe.get_doc('E Invoice Settings')
+ einvoice_settings.enable = 1
+ einvoice_settings.applicable_from = nowdate()
+ einvoice_settings.append('credentials', {
+ 'company': '_Test Company',
+ 'gstin': '27AAECE4835E1ZR',
+ 'username': 'test',
+ 'password': 'test'
+ })
+ einvoice_settings.save()
+
country = frappe.flags.country
frappe.flags.country = 'India'
@@ -1871,7 +1881,8 @@ class TestSalesInvoice(unittest.TestCase):
si.submit()
# reset
- frappe.db.set_value('E Invoice Settings', 'E Invoice Settings', 'enable', 0)
+ einvoice_settings = frappe.get_doc('E Invoice Settings')
+ einvoice_settings.enable = 0
frappe.flags.country = country
def test_einvoice_json(self):
@@ -2063,6 +2074,7 @@ def create_sales_invoice(**args):
si.return_against = args.return_against
si.currency=args.currency or "INR"
si.conversion_rate = args.conversion_rate or 1
+ si.naming_series = args.naming_series or "T-SINV-"
si.append("items", {
"item_code": args.item or args.item_code or "_Test Item",
diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
index f6a7218d601..05ecbc9677b 100644
--- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
+++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
@@ -163,7 +163,7 @@ def get_tds_amount(suppliers, net_total, company, tax_details, fiscal_year_detai
debit_note_amount = get_debit_note_amount(suppliers, year_start_date, year_end_date)
supplier_credit_amount -= debit_note_amount
- if ((tax_details.get('threshold', 0) and supplier_credit_amount >= tax_details.threshold)
+ if ((tax_details.get('threshold', 0) and net_total >= tax_details.threshold)
or (tax_details.get('cumulative_threshold', 0) and supplier_credit_amount >= tax_details.cumulative_threshold)):
if ldc and is_valid_certificate(ldc.valid_from, ldc.valid_upto, posting_date, tds_deducted, net_total,
diff --git a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py
index a0b0cbb9956..a0b02bccf10 100644
--- a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py
+++ b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py
@@ -83,47 +83,6 @@ class TestTaxWithholdingCategory(unittest.TestCase):
for d in invoices:
d.cancel()
- def test_single_threshold_tds_with_previous_vouchers(self):
- invoices = []
- frappe.db.set_value("Supplier", "Test TDS Supplier2", "tax_withholding_category", "Single Threshold TDS")
- pi = create_purchase_invoice(supplier="Test TDS Supplier2")
- pi.submit()
- invoices.append(pi)
-
- pi = create_purchase_invoice(supplier="Test TDS Supplier2")
- pi.submit()
- invoices.append(pi)
-
- self.assertEqual(pi.taxes_and_charges_deducted, 2000)
- self.assertEqual(pi.grand_total, 8000)
-
- # delete invoices to avoid clashing
- for d in invoices:
- d.cancel()
-
- def test_single_threshold_tds_with_previous_vouchers_and_no_tds(self):
- invoices = []
- frappe.db.set_value("Supplier", "Test TDS Supplier2", "tax_withholding_category", "Single Threshold TDS")
- pi = create_purchase_invoice(supplier="Test TDS Supplier2")
- pi.submit()
- invoices.append(pi)
-
- # TDS not applied
- pi = create_purchase_invoice(supplier="Test TDS Supplier2", do_not_apply_tds=True)
- pi.submit()
- invoices.append(pi)
-
- pi = create_purchase_invoice(supplier="Test TDS Supplier2")
- pi.submit()
- invoices.append(pi)
-
- self.assertEqual(pi.taxes_and_charges_deducted, 2000)
- self.assertEqual(pi.grand_total, 8000)
-
- # delete invoices to avoid clashing
- for d in invoices:
- d.cancel()
-
def create_purchase_invoice(**args):
# return sales invoice doc object
item = frappe.get_doc('Item', {'item_name': 'TDS Item'})
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py
index c7efb8a1a17..a2290170bd8 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.py
@@ -366,7 +366,6 @@ def make_purchase_receipt(source_name, target_doc=None):
"Purchase Order": {
"doctype": "Purchase Receipt",
"field_map": {
- "per_billed": "per_billed",
"supplier_warehouse":"supplier_warehouse"
},
"validation": {
diff --git a/erpnext/change_log/v12/v12_20_0.md b/erpnext/change_log/v12/v12_20_0.md
new file mode 100644
index 00000000000..b7c16c236ca
--- /dev/null
+++ b/erpnext/change_log/v12/v12_20_0.md
@@ -0,0 +1,34 @@
+## Version 12.20.0 Release Notes
+
+
+### Fixes & Enhancements
+
+- E-invoicing - Validations & tax calculation fixes ([#25315](https://github.com/frappe/erpnext/pull/25315))
+- Allow zero valuation in stock reconciliation ([#24986](https://github.com/frappe/erpnext/pull/24986))
+- Remove shipping address GSTIN validation for e-invoice ([#25133](https://github.com/frappe/erpnext/pull/25133))
+- Allow Item image alt ([#24935](https://github.com/frappe/erpnext/pull/24935))
+- Added parent task expected end date validation ([#24889](https://github.com/frappe/erpnext/pull/24889))
+- Incorrect Nil Exempt and Non GST amount in GSTR3B report ([#24919](https://github.com/frappe/erpnext/pull/24919))
+- Item attributes not editable until refresh ([#24887](https://github.com/frappe/erpnext/pull/24887))
+- Create property setters for shorter naming series ([#25134](https://github.com/frappe/erpnext/pull/25134))
+- Serial no refresh issue ([#25130](https://github.com/frappe/erpnext/pull/25130))
+- Sales Order not saving due type mismatch in promo scheme (#24748) ([#25056](https://github.com/frappe/erpnext/pull/25056))
+- TDS check getting checked after reload ([#24974](https://github.com/frappe/erpnext/pull/24974))
+- Round total quantity in job card ([#25246](https://github.com/frappe/erpnext/pull/25246))
+- santize_for_json is not defined ([#25157](https://github.com/frappe/erpnext/pull/25157))
+- Apply single transaction threshold on net_total instead of supplier credit amount ([#25208](https://github.com/frappe/erpnext/pull/25208))
+- Zero amount completed delivery notes being shown in Sales Invoice get items ([#25318](https://github.com/frappe/erpnext/pull/25318))
+- Error message compensatory leave request ([#25216](https://github.com/frappe/erpnext/pull/25216))
+- RCM tax calculation ([#25226](https://github.com/frappe/erpnext/pull/25226))
+- Don't delete mode of payment account details while deleting comp… ([#25218](https://github.com/frappe/erpnext/pull/25218))
+- Opportunity-quotation mapping order status ([#25002](https://github.com/frappe/erpnext/pull/25002))
+- Object referencing same memory address issue ([#25165](https://github.com/frappe/erpnext/pull/25165))
+- Validation msg for TransDocNo e-invoicing ([#25120](https://github.com/frappe/erpnext/pull/25120))
+- Do not fetch stopped MR in production plan ([#25110](https://github.com/frappe/erpnext/pull/25110))
+- Incorrect status creating PR from PO after creating PI ([#25203](https://github.com/frappe/erpnext/pull/25203))
+- Remove gst name validation for purchase Invoice ([#25236](https://github.com/frappe/erpnext/pull/25236))
+- Assignment Rule Unassign Condition doesn't work ([#24890](https://github.com/frappe/erpnext/pull/24890))
+- Place of supply for e-invoicing ([#25149](https://github.com/frappe/erpnext/pull/25149))
+- Serial no trim issue ([#24950](https://github.com/frappe/erpnext/pull/24950))
+- Commit individual SLE rename for large datasets (v12) ([#25085](https://github.com/frappe/erpnext/pull/25085))
+- Incorrect batch picked in subcontracted purchase receipt ([#25169](https://github.com/frappe/erpnext/pull/25169))
\ No newline at end of file
diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py
index 59c371200c8..ac50fe9f12e 100644
--- a/erpnext/controllers/buying_controller.py
+++ b/erpnext/controllers/buying_controller.py
@@ -6,6 +6,7 @@ import frappe
from frappe import _, msgprint
from frappe.utils import flt,cint, cstr, getdate
from six import iteritems
+from collections import OrderedDict
from erpnext.accounts.party import get_party_details
from erpnext.stock.get_item_details import get_conversion_factor
from erpnext.buying.utils import validate_for_items, update_last_purchase_rate
@@ -325,10 +326,12 @@ class BuyingController(StockController):
batches_qty = get_batches_with_qty(raw_material.rm_item_code, raw_material.main_item_code,
qty, transferred_batch_qty_map, backflushed_batch_qty_map, item.purchase_order)
+
for batch_data in batches_qty:
qty = batch_data['qty']
raw_material.batch_no = batch_data['batch']
- self.append_raw_material_to_be_backflushed(item, raw_material, qty)
+ if qty > 0:
+ self.append_raw_material_to_be_backflushed(item, raw_material, qty)
else:
self.append_raw_material_to_be_backflushed(item, raw_material, qty)
@@ -1009,7 +1012,7 @@ def get_transferred_batch_qty_map(purchase_order, fg_item):
for batch_data in transferred_batches:
key = ((batch_data.item_code, fg_item)
if batch_data.subcontracted_item else (batch_data.item_code, purchase_order))
- transferred_batch_qty_map.setdefault(key, {})
+ transferred_batch_qty_map.setdefault(key, OrderedDict())
transferred_batch_qty_map[key][batch_data.batch_no] = batch_data.qty
return transferred_batch_qty_map
@@ -1062,8 +1065,14 @@ def get_batches_with_qty(item_code, fg_item, required_qty, transferred_batch_qty
if available_qty >= required_qty:
available_batches.append({'batch': batch, 'qty': required_qty})
break
- else:
+ elif available_qty != 0:
available_batches.append({'batch': batch, 'qty': available_qty})
required_qty -= available_qty
+ for row in available_batches:
+ if backflushed_batches.get(row.get('batch'), 0) > 0:
+ backflushed_batches[row.get('batch')] += row.get('qty')
+ else:
+ backflushed_batches[row.get('batch')] = row.get('qty')
+
return available_batches
diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py
index afc63fc8f51..b664df82470 100644
--- a/erpnext/controllers/queries.py
+++ b/erpnext/controllers/queries.py
@@ -320,7 +320,7 @@ def get_delivery_notes_to_be_billed(doctype, txt, searchfield, start, page_len,
and status not in ("Stopped", "Closed") %(fcond)s
and (
(`tabDelivery Note`.is_return = 0 and `tabDelivery Note`.per_billed < 100)
- or `tabDelivery Note`.grand_total = 0
+ or (`tabDelivery Note`.grand_total = 0 and `tabDelivery Note`.per_billed < 100)
or (
`tabDelivery Note`.is_return = 1
and return_against in (select name from `tabDelivery Note` where per_billed < 100)
diff --git a/erpnext/crm/doctype/opportunity/opportunity.py b/erpnext/crm/doctype/opportunity/opportunity.py
index 8302978e1c1..6ed3a239e98 100644
--- a/erpnext/crm/doctype/opportunity/opportunity.py
+++ b/erpnext/crm/doctype/opportunity/opportunity.py
@@ -244,7 +244,6 @@ def make_quotation(source_name, target_doc=None):
"doctype": "Quotation",
"field_map": {
"opportunity_from": "quotation_to",
- "opportunity_type": "order_type",
"name": "enq_no",
}
},
diff --git a/erpnext/erpnext_integrations/doctype/shopify_settings/test_shopify_settings.py b/erpnext/erpnext_integrations/doctype/shopify_settings/test_shopify_settings.py
index 64ef3dc0859..58e64e83bf8 100644
--- a/erpnext/erpnext_integrations/doctype/shopify_settings/test_shopify_settings.py
+++ b/erpnext/erpnext_integrations/doctype/shopify_settings/test_shopify_settings.py
@@ -17,8 +17,7 @@ class ShopifySettings(unittest.TestCase):
frappe.set_user("Administrator")
# use the fixture data
- import_doc(path=frappe.get_app_path("erpnext", "erpnext_integrations/doctype/shopify_settings/test_data/custom_field.json"),
- ignore_links=True, overwrite=True)
+ import_doc(path=frappe.get_app_path("erpnext", "erpnext_integrations/doctype/shopify_settings/test_data/custom_field.json"))
frappe.reload_doctype("Customer")
frappe.reload_doctype("Sales Order")
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index 2d995ed0e67..56bffdd5637 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -246,7 +246,7 @@ doc_events = {
"on_submit": ["erpnext.regional.create_transaction_log", "erpnext.regional.italy.utils.sales_invoice_on_submit"],
"on_cancel": "erpnext.regional.italy.utils.sales_invoice_on_cancel",
"on_trash": "erpnext.regional.check_deletion_permission",
- "validate": "erpnext.regional.india.utils.set_transporter_address"
+ "validate": ["erpnext.regional.india.utils.set_transporter_address", "erpnext.regional.india.utils.update_taxable_values", "erpnext.regional.india.utils.validate_document_name"]
},
"Purchase Invoice": {
"validate": "erpnext.regional.india.utils.update_grand_total_for_rcm"
@@ -261,9 +261,6 @@ doc_events = {
('Sales Invoice', 'Sales Order', 'Delivery Note', 'Purchase Invoice', 'Purchase Order', 'Purchase Receipt'): {
'validate': ['erpnext.regional.india.utils.set_place_of_supply']
},
- ('Sales Invoice', 'Purchase Invoice'): {
- 'validate': ['erpnext.regional.india.utils.validate_document_name']
- },
"Contact": {
"on_trash": "erpnext.support.doctype.issue.issue.update_issue",
"after_insert": "erpnext.communication.doctype.call_log.call_log.set_caller_information",
diff --git a/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.py b/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.py
index 7a9727f18c4..f11f08462ad 100644
--- a/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.py
+++ b/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.py
@@ -5,7 +5,7 @@
from __future__ import unicode_literals
import frappe
from frappe import _
-from frappe.utils import date_diff, add_days, getdate, cint
+from frappe.utils import date_diff, add_days, getdate, cint, formatdate as format_date
from frappe.model.document import Document
from erpnext.hr.utils import validate_dates, validate_overlap, get_leave_period, \
get_holidays_for_employee, create_additional_leave_ledger_entry
@@ -40,7 +40,12 @@ class CompensatoryLeaveRequest(Document):
def validate_holidays(self):
holidays = get_holidays_for_employee(self.employee, self.work_from_date, self.work_end_date)
if len(holidays) < date_diff(self.work_end_date, self.work_from_date) + 1:
- frappe.throw(_("Compensatory leave request days not in valid holidays"))
+ if date_diff(self.work_end_date, self.work_from_date):
+ msg = _("The days between {0} to {1} are not valid holidays.").format(frappe.bold(format_date(self.work_from_date)), frappe.bold(format_date(self.work_end_date)))
+ else:
+ msg = _("{0} is not a holiday.").format(frappe.bold(format_date(self.work_from_date)))
+
+ frappe.throw(msg)
def on_submit(self):
company = frappe.db.get_value("Employee", self.employee, "company")
@@ -63,7 +68,7 @@ class CompensatoryLeaveRequest(Document):
leave_allocation = self.create_leave_allocation(leave_period, date_difference)
self.leave_allocation=leave_allocation.name
else:
- frappe.throw(_("There is no leave period in between {0} and {1}").format(self.work_from_date, self.work_end_date))
+ frappe.throw(_("There is no leave period in between {0} and {1}").format(format_date(self.work_from_date), format_date(self.work_end_date)))
def on_cancel(self):
if self.leave_allocation:
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py
index 7b924f7025a..dfb4a03c3f4 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.py
+++ b/erpnext/manufacturing/doctype/job_card/job_card.py
@@ -39,6 +39,8 @@ class JobCard(Document):
if d.completed_qty:
self.total_completed_qty += d.completed_qty
+ self.total_completed_qty = flt(self.total_completed_qty, self.precision("total_completed_qty"))
+
def get_overlap_for(self, args):
existing = frappe.db.sql("""select jc.name as name from
`tabJob Card Time Log` jctl, `tabJob Card` jc where jctl.parent = jc.name and
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js
index cb8d3a02068..be4babe417a 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.js
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js
@@ -24,6 +24,16 @@ frappe.ui.form.on('Production Plan', {
}
});
+ frm.set_query('material_request', 'material_requests', function() {
+ return {
+ filters: {
+ material_request_type: "Manufacture",
+ docstatus: 1,
+ status: ["!=", "Stopped"],
+ }
+ };
+ });
+
frm.fields_dict['po_items'].grid.get_field('item_code').get_query = function(doc) {
return {
query: "erpnext.controllers.queries.item_query",
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py
index 5a193d7e4bf..1b2cd542a4f 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py
@@ -68,7 +68,7 @@ class ProductionPlan(Document):
from `tabMaterial Request` mr, `tabMaterial Request Item` mr_item
where mr_item.parent = mr.name
and mr.material_request_type = "Manufacture"
- and mr.docstatus = 1 and mr.company = %(company)s
+ and mr.docstatus = 1 and mr.status != "Stopped" and mr.company = %(company)s
and mr_item.qty > ifnull(mr_item.ordered_qty,0) {0} {1}
and (exists (select name from `tabBOM` bom where bom.item=mr_item.item_code
and bom.is_active = 1))
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index b40549c5ba3..28349f93fff 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -681,3 +681,6 @@ erpnext.patches.v12_0.update_payment_entry_status
erpnext.patches.v12_0.add_transporter_address_field #2020-10-27
erpnext.patches.v12_0.setup_einvoice_fields #2020-12-02
erpnext.patches.v12_0.add_state_code_for_ladakh
+erpnext.patches.v12_0.create_taxable_value_field
+erpnext.patches.v12_0.purchase_receipt_status
+erpnext.patches.v12_0.add_company_link_to_einvoice_settings
diff --git a/erpnext/patches/v12_0/add_company_link_to_einvoice_settings.py b/erpnext/patches/v12_0/add_company_link_to_einvoice_settings.py
new file mode 100644
index 00000000000..b6bd5fa311c
--- /dev/null
+++ b/erpnext/patches/v12_0/add_company_link_to_einvoice_settings.py
@@ -0,0 +1,16 @@
+from __future__ import unicode_literals
+import frappe
+
+def execute():
+ company = frappe.get_all('Company', filters = {'country': 'India'})
+ if not company or not frappe.db.count('E Invoice User'):
+ return
+
+ frappe.reload_doc("regional", "doctype", "e_invoice_user")
+ for creds in frappe.db.get_all('E Invoice User', fields=['name', 'gstin']):
+ company_name = frappe.db.sql("""
+ select dl.link_name from `tabAddress` a, `tabDynamic Link` dl
+ where a.gstin = %s and dl.parent = a.name and dl.link_doctype = 'Company'
+ """, (creds.get('gstin')))
+ if company_name and len(company_name) > 0:
+ frappe.db.set_value('E Invoice User', creds.get('name'), 'company', company_name[0][0])
\ No newline at end of file
diff --git a/erpnext/patches/v12_0/create_taxable_value_field.py b/erpnext/patches/v12_0/create_taxable_value_field.py
new file mode 100644
index 00000000000..a0c9fcf4cbe
--- /dev/null
+++ b/erpnext/patches/v12_0/create_taxable_value_field.py
@@ -0,0 +1,18 @@
+from __future__ import unicode_literals
+import frappe
+from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
+
+def execute():
+ company = frappe.get_all('Company', filters = {'country': 'India'})
+ if not company:
+ return
+
+ custom_fields = {
+ 'Sales Invoice Item': [
+ dict(fieldname='taxable_value', label='Taxable Value',
+ fieldtype='Currency', insert_after='base_net_amount', hidden=1, options="Company:company:default_currency",
+ print_hide=1)
+ ]
+ }
+
+ create_custom_fields(custom_fields, update=True)
\ No newline at end of file
diff --git a/erpnext/patches/v12_0/purchase_receipt_status.py b/erpnext/patches/v12_0/purchase_receipt_status.py
new file mode 100644
index 00000000000..62d2a726bb0
--- /dev/null
+++ b/erpnext/patches/v12_0/purchase_receipt_status.py
@@ -0,0 +1,24 @@
+""" This patch fixes old purchase receipts (PR) where even after submitting
+ the PR, the `status` remains "Draft". `per_billed` field was copied over from previous
+ doc (PO), hence it is recalculated for setting new correct status of PR.
+"""
+
+import frappe
+
+def execute():
+ affected_purchase_receipts = frappe.db.sql(
+ """select name from `tabPurchase Receipt`
+ where status = 'Draft' and per_billed = 100 and docstatus = 1"""
+ )
+
+ if not affected_purchase_receipts:
+ return
+
+
+ for pr in affected_purchase_receipts:
+ pr_name = pr[0]
+
+ pr_doc = frappe.get_doc("Purchase Receipt", pr_name)
+
+ pr_doc.update_billing_status(update_modified=False)
+ pr_doc.set_status(update=True, update_modified=False)
diff --git a/erpnext/projects/doctype/task/task.py b/erpnext/projects/doctype/task/task.py
index 0d403b193f3..6238306ae25 100755
--- a/erpnext/projects/doctype/task/task.py
+++ b/erpnext/projects/doctype/task/task.py
@@ -30,6 +30,7 @@ class Task(NestedSet):
def validate(self):
self.validate_dates()
+ self.validate_parent_expected_end_date()
self.validate_parent_project_dates()
self.validate_progress()
self.validate_status()
@@ -44,6 +45,12 @@ class Task(NestedSet):
frappe.throw(_("{0} can not be greater than {1}").format(frappe.bold("Actual Start Date"), \
frappe.bold("Actual End Date")))
+ def validate_parent_expected_end_date(self):
+ if self.parent_task:
+ parent_exp_end_date = frappe.db.get_value("Task", self.parent_task, "exp_end_date")
+ if parent_exp_end_date and getdate(self.get("exp_end_date")) > getdate(parent_exp_end_date):
+ frappe.throw(_("Expected End Date should be less than or equal to parent task's Expected End Date {0}.").format(getdate(parent_exp_end_date)))
+
def validate_parent_project_dates(self):
if not self.project or frappe.flags.in_test:
return
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index 3cd8e27bc40..ee61e9787af 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -635,34 +635,34 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
this.frm.trigger("item_code", cdt, cdn);
}
else {
- var valid_serial_nos = [];
-
// Replacing all occurences of comma with carriage return
- var serial_nos = item.serial_no.trim().replace(/,/g, '\n');
-
- serial_nos = serial_nos.trim().split('\n');
-
- // Trim each string and push unique string to new list
- for (var x=0; x<=serial_nos.length - 1; x++) {
- if (serial_nos[x].trim() != "" && valid_serial_nos.indexOf(serial_nos[x].trim()) == -1) {
- valid_serial_nos.push(serial_nos[x].trim());
- }
- }
-
- // Add the new list to the serial no. field in grid with each in new line
- item.serial_no = valid_serial_nos.join('\n');
+ item.serial_no = item.serial_no.replace(/,/g, '\n');
item.conversion_factor = item.conversion_factor || 1;
-
refresh_field("serial_no", item.name, item.parentfield);
- if(!doc.is_return && cint(user_defaults.set_qty_in_transactions_based_on_serial_no_input)) {
- frappe.model.set_value(item.doctype, item.name,
- "qty", valid_serial_nos.length / item.conversion_factor);
- frappe.model.set_value(item.doctype, item.name, "stock_qty", valid_serial_nos.length);
+ if (!doc.is_return && cint(frappe.user_defaults.set_qty_in_transactions_based_on_serial_no_input)) {
+ setTimeout(() => {
+ me.update_qty(cdt, cdn);
+ }, 10000);
}
}
}
},
+ update_qty: function(cdt, cdn) {
+ var valid_serial_nos = [];
+ var serialnos = [];
+ var item = frappe.get_doc(cdt, cdn);
+ serialnos = item.serial_no.split("\n");
+ for (var i = 0; i < serialnos.length; i++) {
+ if (serialnos[i] != "") {
+ valid_serial_nos.push(serialnos[i]);
+ }
+ }
+ frappe.model.set_value(item.doctype, item.name,
+ "qty", valid_serial_nos.length / item.conversion_factor);
+ frappe.model.set_value(item.doctype, item.name, "stock_qty", valid_serial_nos.length);
+ },
+
validate: function() {
this.calculate_taxes_and_totals(false);
},
diff --git a/erpnext/regional/doctype/e_invoice_user/e_invoice_user.json b/erpnext/regional/doctype/e_invoice_user/e_invoice_user.json
index dd9d99773a3..a65b1ca7ca8 100644
--- a/erpnext/regional/doctype/e_invoice_user/e_invoice_user.json
+++ b/erpnext/regional/doctype/e_invoice_user/e_invoice_user.json
@@ -5,6 +5,7 @@
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
+ "company",
"gstin",
"username",
"password"
@@ -30,12 +31,20 @@
"in_list_view": 1,
"label": "Password",
"reqd": 1
+ },
+ {
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Company",
+ "options": "Company",
+ "reqd": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2020-12-22 15:10:53.466205",
+ "modified": "2021-03-22 12:16:56.365616",
"modified_by": "Administrator",
"module": "Regional",
"name": "E Invoice User",
diff --git a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py
index 9e841483554..f1c5210b237 100644
--- a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py
+++ b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py
@@ -350,13 +350,12 @@ class GSTR3BReport(Document):
return inter_state_supply_details
def get_inward_nil_exempt(self, state):
-
inward_nil_exempt = frappe.db.sql(""" select p.place_of_supply, sum(i.base_amount) as base_amount,
i.is_nil_exempt, i.is_non_gst from `tabPurchase Invoice` p , `tabPurchase Invoice Item` i
where p.docstatus = 1 and p.name = i.parent
- and i.is_nil_exempt = 1 or i.is_non_gst = 1 and
+ and (i.is_nil_exempt = 1 or i.is_non_gst = 1) and
month(p.posting_date) = %s and year(p.posting_date) = %s and p.company = %s and p.company_gstin = %s
- group by p.place_of_supply """, (self.month_no, self.year, self.company, self.gst_details.get("gstin")), as_dict=1)
+ group by p.place_of_supply, i.is_nil_exempt, i.is_non_gst""", (self.month_no, self.year, self.company, self.gst_details.get("gstin")), as_dict=1)
inward_nil_exempt_details = {
"gst": {
diff --git a/erpnext/regional/india/e_invoice/einv_validation.json b/erpnext/regional/india/e_invoice/einv_validation.json
index 86290cfe524..f4a3542a60e 100644
--- a/erpnext/regional/india/e_invoice/einv_validation.json
+++ b/erpnext/regional/india/e_invoice/einv_validation.json
@@ -919,7 +919,8 @@
"minLength": 1,
"maxLength": 15,
"pattern": "^([0-9A-Z/-]){1,15}$",
- "description": "Tranport Document Number"
+ "description": "Tranport Document Number",
+ "validationMsg": "Transport Receipt No is invalid"
},
"TransDocDt": {
"type": "string",
diff --git a/erpnext/regional/india/e_invoice/einvoice.js b/erpnext/regional/india/e_invoice/einvoice.js
index 9fa1334840d..cda732b0fb7 100644
--- a/erpnext/regional/india/e_invoice/einvoice.js
+++ b/erpnext/regional/india/e_invoice/einvoice.js
@@ -1,12 +1,13 @@
erpnext.setup_einvoice_actions = (doctype) => {
frappe.ui.form.on(doctype, {
- refresh(frm) {
- const einvoicing_enabled = frappe.db.get_value("E Invoice Settings", "E Invoice Settings", "enable");
- const supply_type = frm.doc.gst_category;
- const valid_supply_type = ['Registered Regular', 'SEZ', 'Overseas', 'Deemed Export'].includes(supply_type);
- const company_transaction = frm.doc.billing_address_gstin == frm.doc.company_gstin;
+ async refresh(frm) {
+ const res = await frappe.call({
+ method: 'erpnext.regional.india.e_invoice.utils.validate_eligibility',
+ args: { doc: frm.doc }
+ });
+ const invoice_eligible = res.message;
- if (!einvoicing_enabled || !valid_supply_type || company_transaction) return;
+ if (!invoice_eligible) return;
const { doctype, irn, irn_cancelled, ewaybill, eway_bill_cancelled, name, __unsaved } = frm.doc;
@@ -113,45 +114,25 @@ erpnext.setup_einvoice_actions = (doctype) => {
}
if (irn && ewaybill && !irn_cancelled && !eway_bill_cancelled) {
- const fields = [
- {
- "label": "Reason",
- "fieldname": "reason",
- "fieldtype": "Select",
- "reqd": 1,
- "default": "1-Duplicate",
- "options": ["1-Duplicate", "2-Data Entry Error", "3-Order Cancelled", "4-Other"]
- },
- {
- "label": "Remark",
- "fieldname": "remark",
- "fieldtype": "Data",
- "reqd": 1
- }
- ];
const action = () => {
- const d = new frappe.ui.Dialog({
- title: __('Cancel E-Way Bill'),
- fields: fields,
+ let message = __('Cancellation of e-way bill is currently not supported. ');
+ message += '
';
+ message += __('You must first use the portal to cancel the e-way bill and then update the cancelled status in the ERPNext system.');
+
+ frappe.msgprint({
+ title: __('Update E-Way Bill Cancelled Status?'),
+ message: message,
+ indicator: 'orange',
primary_action: function() {
- const data = d.get_values();
frappe.call({
method: 'erpnext.regional.india.e_invoice.utils.cancel_eway_bill',
- args: {
- doctype,
- docname: name,
- eway_bill: ewaybill,
- reason: data.reason.split('-')[0],
- remark: data.remark
- },
+ args: { doctype, docname: name },
freeze: true,
- callback: () => frm.reload_doc() || d.hide(),
- error: () => d.hide()
+ callback: () => frm.reload_doc()
});
},
- primary_action_label: __('Submit')
+ primary_action_label: __('Yes')
});
- d.show();
};
add_custom_button(__("Cancel E-Way Bill"), action);
}
diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py
index 3e96b7e48b7..7637d2d4583 100644
--- a/erpnext/regional/india/e_invoice/utils.py
+++ b/erpnext/regional/india/e_invoice/utils.py
@@ -5,6 +5,7 @@
from __future__ import unicode_literals
import os
import re
+import six
import jwt
import sys
import json
@@ -16,16 +17,38 @@ from frappe import _, bold
from pyqrcode import create as qrcreate
from frappe.integrations.utils import make_post_request, make_get_request
from erpnext.regional.india.utils import get_gst_accounts, get_place_of_supply
-from frappe.utils.data import cstr, cint, formatdate as format_date, flt, time_diff_in_seconds, now_datetime, add_to_date, get_link_to_form
+from frappe.utils.data import cstr, cint, formatdate as format_date, flt, time_diff_in_seconds, now_datetime, add_to_date, getdate, get_link_to_form
-def validate_einvoice_fields(doc):
- einvoicing_enabled = cint(frappe.db.get_value('E Invoice Settings', 'E Invoice Settings', 'enable'))
- invalid_doctype = doc.doctype != 'Sales Invoice'
+@frappe.whitelist()
+def validate_eligibility(doc):
+ if isinstance(doc, six.string_types):
+ doc = json.loads(doc)
+
+ invalid_doctype = doc.get('doctype') != 'Sales Invoice'
+ if invalid_doctype:
+ return False
+
+ einvoicing_enabled = cint(frappe.db.get_single_value('E Invoice Settings', 'enable'))
+ if not einvoicing_enabled:
+ return False
+
+ if getdate(doc.get('posting_date')) < getdate('2021-04-01'):
+ return False
+
+ invalid_company = not frappe.db.get_value('E Invoice User', { 'company': doc.get('company') })
invalid_supply_type = doc.get('gst_category') not in ['Registered Regular', 'SEZ', 'Overseas', 'Deemed Export']
company_transaction = doc.get('billing_address_gstin') == doc.get('company_gstin')
no_taxes_applied = not doc.get('taxes')
- if not einvoicing_enabled or invalid_doctype or invalid_supply_type or company_transaction or no_taxes_applied:
+ if invalid_company or invalid_supply_type or company_transaction or no_taxes_applied:
+ return False
+
+ return True
+
+def validate_einvoice_fields(doc):
+ invoice_eligible = validate_eligibility(doc)
+
+ if not invoice_eligible:
return
if doc.docstatus == 0 and doc._action == 'save':
@@ -86,39 +109,43 @@ def get_doc_details(invoice):
invoice_date=invoice_date
))
-def get_party_details(address_name, company_address=None, billing_address=None, shipping_address=None):
- d = frappe.get_all('Address', filters={'name': address_name}, fields=['*'])[0]
-
- if ((not d.gstin and not shipping_address)
- or not d.city
- or not d.pincode
- or not d.address_title
- or not d.address_line1
- or not d.gst_state_number):
+def validate_address_fields(address, is_shipping_address):
+ if ((not address.gstin and not is_shipping_address)
+ or not address.city
+ or not address.pincode
+ or not address.address_title
+ or not address.address_line1
+ or not address.gst_state_number):
frappe.throw(
- msg=_('Address lines, city, pincode, gstin is mandatory for address {}. Please set them and try again.').format(
- get_link_to_form('Address', address_name)
- ),
+ msg=_('Address Lines, City, Pincode, GSTIN are mandatory for address {}. Please set them and try again.').format(address.name),
title=_('Missing Address Fields')
)
- if d.gst_state_number == 97:
+def get_party_details(address_name, is_shipping_address=False):
+ addr = frappe.get_doc('Address', address_name)
+
+ validate_address_fields(addr, is_shipping_address)
+
+ if addr.gst_state_number == 97:
# according to einvoice standard
- pincode = 999999
+ addr.pincode = 999999
party_address_details = frappe._dict(dict(
- legal_name=d.address_title,
- location=d.city, pincode=d.pincode,
- state_code=d.gst_state_number,
- address_line1=d.address_line1,
- address_line2=d.address_line2
+ legal_name=sanitize_for_json(addr.address_title),
+ location=sanitize_for_json(addr.city),
+ pincode=addr.pincode, gstin=addr.gstin,
+ state_code=addr.gst_state_number,
+ address_line1=sanitize_for_json(addr.address_line1),
+ address_line2=sanitize_for_json(addr.address_line2)
))
if d.gstin:
party_address_details.gstin = d.gstin
return party_address_details
+ return party_address_details
+
def get_gstin_details(gstin):
if not hasattr(frappe.local, 'gstin_cache'):
frappe.local.gstin_cache = {}
@@ -167,10 +194,15 @@ def get_item_list(invoice):
item.description = json.dumps(d.item_name)[1:-1]
item.qty = abs(item.qty)
- item.discount_amount = 0
- item.unit_rate = abs(item.base_net_amount / item.qty)
- item.gross_amount = abs(item.base_net_amount)
- item.taxable_value = abs(item.base_net_amount)
+
+ if invoice.apply_discount_on == 'Net Total' and invoice.discount_amount:
+ item.discount_amount = abs(item.base_amount - item.base_net_amount)
+ else:
+ item.discount_amount = 0
+
+ item.unit_rate = abs((abs(item.taxable_value) - item.discount_amount)/ item.qty)
+ item.gross_amount = abs(item.taxable_value) + item.discount_amount
+ item.taxable_value = abs(item.taxable_value)
item.batch_expiry_date = frappe.db.get_value('Batch', d.batch_no, 'expiry_date') if d.batch_no else None
item.batch_expiry_date = format_date(item.batch_expiry_date, 'dd/mm/yyyy') if item.batch_expiry_date else None
@@ -203,11 +235,11 @@ def update_item_taxes(invoice, item):
is_applicable = t.tax_amount and t.account_head in gst_accounts_list
if is_applicable:
# this contains item wise tax rate & tax amount (incl. discount)
- item_tax_detail = json.loads(t.item_wise_tax_detail).get(item.item_code)
+ item_tax_detail = json.loads(t.item_wise_tax_detail).get(item.item_code or item.item_name)
item_tax_rate = item_tax_detail[0]
# item tax amount excluding discount amount
- item_tax_amount = (item_tax_rate / 100) * item.base_net_amount
+ item_tax_amount = (item_tax_rate / 100) * item.taxable_value
if t.account_head in gst_accounts.cess_account:
item_tax_amount_after_discount = item_tax_detail[1]
@@ -228,10 +260,14 @@ def get_invoice_value_details(invoice):
invoice_value_details = frappe._dict(dict())
if invoice.apply_discount_on == 'Net Total' and invoice.discount_amount:
- invoice_value_details.base_total = abs(invoice.base_total)
- invoice_value_details.invoice_discount_amt = abs(invoice.base_discount_amount)
+ # Discount already applied on net total which means on items
+ invoice_value_details.base_total = abs(sum([i.taxable_value for i in invoice.get('items')]))
+ invoice_value_details.invoice_discount_amt = 0
+ elif invoice.apply_discount_on == 'Grand Total' and invoice.discount_amount:
+ invoice_value_details.invoice_discount_amt = invoice.base_discount_amount
+ invoice_value_details.base_total = abs(sum([i.taxable_value for i in invoice.get('items')]))
else:
- invoice_value_details.base_total = abs(invoice.base_net_total)
+ invoice_value_details.base_total = abs(sum([i.taxable_value for i in invoice.get('items')]))
# since tax already considers discount amount
invoice_value_details.invoice_discount_amt = 0
@@ -252,7 +288,11 @@ def update_invoice_taxes(invoice, invoice_value_details):
invoice_value_details.total_igst_amt = 0
invoice_value_details.total_cess_amt = 0
invoice_value_details.total_other_charges = 0
+ considered_rows = []
+
for t in invoice.taxes:
+ tax_amount = t.base_tax_amount if (invoice.apply_discount_on == 'Grand Total' and invoice.discount_amount) \
+ else t.base_tax_amount_after_discount_amount
if t.account_head in gst_accounts_list:
if t.account_head in gst_accounts.cess_account:
# using after discount amt since item also uses after discount amt for cess calc
@@ -260,12 +300,26 @@ def update_invoice_taxes(invoice, invoice_value_details):
for tax_type in ['igst', 'cgst', 'sgst']:
if t.account_head in gst_accounts['{}_account'.format(tax_type)]:
- invoice_value_details['total_{}_amt'.format(tax_type)] += abs(t.base_tax_amount_after_discount_amount)
+
+ invoice_value_details['total_{}_amt'.format(tax_type)] += abs(tax_amount)
+ update_other_charges(t, invoice_value_details, gst_accounts_list, invoice, considered_rows)
else:
- invoice_value_details.total_other_charges += abs(t.base_tax_amount_after_discount_amount)
-
+ invoice_value_details.total_other_charges += abs(tax_amount)
+
return invoice_value_details
+def update_other_charges(tax_row, invoice_value_details, gst_accounts_list, invoice, considered_rows):
+ prev_row_id = cint(tax_row.row_id) - 1
+ if tax_row.account_head in gst_accounts_list and prev_row_id not in considered_rows:
+ if tax_row.charge_type == 'On Previous Row Amount':
+ amount = invoice.get('taxes')[prev_row_id].tax_amount_after_discount_amount
+ invoice_value_details.total_other_charges -= abs(amount)
+ considered_rows.append(prev_row_id)
+ if tax_row.charge_type == 'On Previous Row Total':
+ amount = invoice.get('taxes')[prev_row_id].base_total - invoice.base_net_total
+ invoice_value_details.total_other_charges -= abs(amount)
+ considered_rows.append(prev_row_id)
+
def get_payment_details(invoice):
payee_name = invoice.company
mode_of_payment = ', '.join([d.mode_of_payment for d in invoice.payments])
@@ -278,6 +332,10 @@ def get_payment_details(invoice):
))
def get_return_doc_reference(invoice):
+ if not invoice.return_against:
+ frappe.throw(_('For generating IRN, reference to the original invoice is mandatory for a credit note. Please set {} field to generate e-invoice.')
+ .format(frappe.bold('Return Against')), title=_('Missing Field'))
+
invoice_date = frappe.db.get_value('Sales Invoice', invoice.return_against, 'posting_date')
return frappe._dict(dict(
invoice_name=invoice.return_against, invoice_date=format_date(invoice_date, 'dd/mm/yyyy')
@@ -285,7 +343,11 @@ def get_return_doc_reference(invoice):
def get_eway_bill_details(invoice):
if invoice.is_return:
- frappe.throw(_('E-Way Bill cannot be generated for Credit Notes & Debit Notes'), title=_('E Invoice Validation Failed'))
+ frappe.throw(_('E-Way Bill cannot be generated for Credit Notes & Debit Notes. Please clear fields in the Transporter Section of the invoice.'),
+ title=_('Invalid Fields'))
+
+ if not invoice.distance:
+ frappe.throw(_('Distance is mandatory for generating e-way bill for an e-invoice.'), title=_('Missing Field'))
mode_of_transport = { '': '', 'Road': '1', 'Air': '2', 'Rail': '3', 'Ship': '4' }
vehicle_type = { 'Regular': 'R', 'Over Dimensional Cargo (ODC)': 'O' }
@@ -303,9 +365,15 @@ def get_eway_bill_details(invoice):
def validate_mandatory_fields(invoice):
if not invoice.company_address:
- frappe.throw(_('Company Address is mandatory to fetch company GSTIN details.'), title=_('Missing Fields'))
+ frappe.throw(
+ _('Company Address is mandatory to fetch company GSTIN details. Please set Company Address and try again.'),
+ title=_('Missing Fields')
+ )
if not invoice.customer_address:
- frappe.throw(_('Customer Address is mandatory to fetch customer GSTIN details.'), title=_('Missing Fields'))
+ frappe.throw(
+ _('Customer Address is mandatory to fetch customer GSTIN details. Please set Company Address and try again.'),
+ title=_('Missing Fields')
+ )
if not frappe.db.get_value('Address', invoice.company_address, 'gstin'):
frappe.throw(
_('GSTIN is mandatory to fetch company GSTIN details. Please enter GSTIN in selected company address.'),
@@ -317,6 +385,39 @@ def validate_mandatory_fields(invoice):
title=_('Missing Fields')
)
+def validate_totals(einvoice):
+ item_list = einvoice['ItemList']
+ value_details = einvoice['ValDtls']
+
+ total_item_ass_value = 0
+ total_item_cgst_value = 0
+ total_item_sgst_value = 0
+ total_item_igst_value = 0
+ total_item_value = 0
+ for item in item_list:
+ total_item_ass_value += flt(item['AssAmt'])
+ total_item_cgst_value += flt(item['CgstAmt'])
+ total_item_sgst_value += flt(item['SgstAmt'])
+ total_item_igst_value += flt(item['IgstAmt'])
+ total_item_value += flt(item['TotItemVal'])
+
+ if abs(flt(item['AssAmt']) * flt(item['GstRt']) / 100) - (flt(item['CgstAmt']) + flt(item['SgstAmt']) + flt(item['IgstAmt'])) > 1:
+ frappe.throw(_('Row #{}: GST rate is invalid. Please remove tax rows with zero tax amount from taxes table.').format(item.idx))
+
+ if abs(flt(value_details['AssVal']) - total_item_ass_value) > 1:
+ frappe.throw(_('Total Taxable Value of the items is not equal to the Invoice Net Total. Please check item taxes / discounts for any correction.'))
+
+ if abs(flt(value_details['TotInvVal']) + flt(value_details['Discount']) - flt(value_details['OthChrg']) - total_item_value) > 1:
+ frappe.throw(_('Total Value of the items is not equal to the Invoice Grand Total. Please check item taxes / discounts for any correction.'))
+
+ calculated_invoice_value = \
+ flt(value_details['AssVal']) + flt(value_details['CgstVal']) \
+ + flt(value_details['SgstVal']) + flt(value_details['IgstVal']) \
+ + flt(value_details['OthChrg']) - flt(value_details['Discount'])
+
+ if abs(flt(value_details['TotInvVal']) - calculated_invoice_value) > 1:
+ frappe.throw(_('Total Item Value + Taxes - Discount is not equal to the Invoice Grand Total. Please check taxes / discounts for any correction.'))
+
def make_einvoice(invoice):
validate_mandatory_fields(invoice)
@@ -331,27 +432,33 @@ def make_einvoice(invoice):
if invoice.gst_category == 'Overseas':
buyer_details = get_overseas_address_details(invoice.customer_address)
else:
- buyer_details = get_party_details(invoice.customer_address, billing_address=1)
+ buyer_details = get_party_details(invoice.customer_address)
place_of_supply = get_place_of_supply(invoice, invoice.doctype)
if place_of_supply:
place_of_supply = place_of_supply.split('-')[0]
else:
place_of_supply = invoice.billing_address_gstin[:2]
-
buyer_details.update(dict(place_of_supply=place_of_supply))
+
+ seller_details.update(dict(legal_name=invoice.company))
+ buyer_details.update(dict(legal_name=invoice.customer_name or invoice.customer))
shipping_details = payment_details = prev_doc_details = eway_bill_details = frappe._dict({})
if invoice.shipping_address_name and invoice.customer_address != invoice.shipping_address_name:
if invoice.gst_category == 'Overseas':
shipping_details = get_overseas_address_details(invoice.shipping_address_name)
else:
- shipping_details = get_party_details(invoice.shipping_address_name, shipping_address=1)
-
+ shipping_details = get_party_details(invoice.shipping_address_name, shipping_address=True)
+
+ if invoice.is_pos and invoice.base_paid_amount:
+ payment_details = get_payment_details(invoice)
+
if invoice.is_return and invoice.return_against:
prev_doc_details = get_return_doc_reference(invoice)
- if invoice.transporter and cint(invoice.distance):
+
+ if invoice.transporter and flt(invoice.distance) and not invoice.is_return:
eway_bill_details = get_eway_bill_details(invoice)
-
+
# not yet implemented
dispatch_details = period_details = export_details = frappe._dict({})
@@ -362,81 +469,86 @@ def make_einvoice(invoice):
period_details=period_details, prev_doc_details=prev_doc_details,
export_details=export_details, eway_bill_details=eway_bill_details
)
- einvoice = json.loads(einvoice)
- validations = json.loads(read_json('einv_validation'))
- errors = validate_einvoice(validations, einvoice)
- if errors:
- message = "\n".join([
- "E Invoice: ", json.dumps(einvoice, indent=4),
- "-" * 50,
- "Errors: ", json.dumps(errors, indent=4)
- ])
- frappe.log_error(title="E Invoice Validation Failed", message=message)
- throw_error_list(errors, _('E Invoice Validation Failed'))
+ try:
+ einvoice = safe_json_load(einvoice)
+ einvoice = santize_einvoice_fields(einvoice)
+ except Exception:
+ show_link_to_error_log(invoice, einvoice)
+
+ validate_totals(einvoice)
return einvoice
-def throw_error_list(errors, title):
- if len(errors) > 1:
- li = ['