mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-13 10:11:20 +00:00
Merge branch 'version-12-hotfix' into v12-pre-release
This commit is contained in:
32
.flake8
Normal file
32
.flake8
Normal file
@@ -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
|
||||||
@@ -294,4 +294,8 @@ def rename_temporarily_named_docs(doctype):
|
|||||||
oldname = doc.name
|
oldname = doc.name
|
||||||
set_name_from_naming_options(frappe.get_meta(doctype).autoname, doc)
|
set_name_from_naming_options(frappe.get_meta(doctype).autoname, doc)
|
||||||
newname = doc.name
|
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
|
||||||
|
)
|
||||||
|
|||||||
@@ -467,7 +467,7 @@ def apply_pricing_rule_on_transaction(doc):
|
|||||||
|
|
||||||
if not d.get(pr_field): continue
|
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}")
|
frappe.msgprint(_("User has not applied rule on the invoice {0}")
|
||||||
.format(doc.name))
|
.format(doc.name))
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -509,7 +509,7 @@ frappe.ui.form.on("Purchase Invoice", {
|
|||||||
},
|
},
|
||||||
|
|
||||||
onload: function(frm) {
|
onload: function(frm) {
|
||||||
if(frm.doc.__onload) {
|
if(frm.doc.__onload && frm.is_new()) {
|
||||||
if(frm.doc.supplier) {
|
if(frm.doc.supplier) {
|
||||||
frm.doc.apply_tds = frm.doc.__onload.supplier_tds ? 1 : 0;
|
frm.doc.apply_tds = frm.doc.__onload.supplier_tds ? 1 : 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"grand_total": 0,
|
"grand_total": 0,
|
||||||
"naming_series": "_T-BILL",
|
"naming_series": "T-PINV-",
|
||||||
"taxes": [
|
"taxes": [
|
||||||
{
|
{
|
||||||
"account_head": "_Test Account Shipping Charges - _TC",
|
"account_head": "_Test Account Shipping Charges - _TC",
|
||||||
@@ -168,7 +168,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"grand_total": 0,
|
"grand_total": 0,
|
||||||
"naming_series": "_T-Purchase Invoice-",
|
"naming_series": "T-PINV-",
|
||||||
"taxes": [
|
"taxes": [
|
||||||
{
|
{
|
||||||
"account_head": "_Test Account Shipping Charges - _TC",
|
"account_head": "_Test Account Shipping Charges - _TC",
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
"base_grand_total": 561.8,
|
"base_grand_total": 561.8,
|
||||||
"grand_total": 561.8,
|
"grand_total": 561.8,
|
||||||
"is_pos": 0,
|
"is_pos": 0,
|
||||||
"naming_series": "_T-Sales Invoice-",
|
"naming_series": "T-SINV-",
|
||||||
"base_net_total": 500.0,
|
"base_net_total": 500.0,
|
||||||
"taxes": [
|
"taxes": [
|
||||||
{
|
{
|
||||||
@@ -103,7 +103,7 @@
|
|||||||
"base_grand_total": 630.0,
|
"base_grand_total": 630.0,
|
||||||
"grand_total": 630.0,
|
"grand_total": 630.0,
|
||||||
"is_pos": 0,
|
"is_pos": 0,
|
||||||
"naming_series": "_T-Sales Invoice-",
|
"naming_series": "T-SINV-",
|
||||||
"base_net_total": 500.0,
|
"base_net_total": 500.0,
|
||||||
"taxes": [
|
"taxes": [
|
||||||
{
|
{
|
||||||
@@ -174,7 +174,7 @@
|
|||||||
],
|
],
|
||||||
"grand_total": 0,
|
"grand_total": 0,
|
||||||
"is_pos": 0,
|
"is_pos": 0,
|
||||||
"naming_series": "_T-Sales Invoice-",
|
"naming_series": "T-SINV-",
|
||||||
"taxes": [
|
"taxes": [
|
||||||
{
|
{
|
||||||
"account_head": "_Test Account Shipping Charges - _TC",
|
"account_head": "_Test Account Shipping Charges - _TC",
|
||||||
@@ -300,7 +300,7 @@
|
|||||||
],
|
],
|
||||||
"grand_total": 0,
|
"grand_total": 0,
|
||||||
"is_pos": 0,
|
"is_pos": 0,
|
||||||
"naming_series": "_T-Sales Invoice-",
|
"naming_series": "T-SINV-",
|
||||||
"taxes": [
|
"taxes": [
|
||||||
{
|
{
|
||||||
"account_head": "_Test Account Excise Duty - _TC",
|
"account_head": "_Test Account Excise Duty - _TC",
|
||||||
|
|||||||
@@ -1860,7 +1860,17 @@ class TestSalesInvoice(unittest.TestCase):
|
|||||||
|
|
||||||
def test_einvoice_submission_without_irn(self):
|
def test_einvoice_submission_without_irn(self):
|
||||||
# init
|
# 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
|
country = frappe.flags.country
|
||||||
frappe.flags.country = 'India'
|
frappe.flags.country = 'India'
|
||||||
|
|
||||||
@@ -1871,7 +1881,8 @@ class TestSalesInvoice(unittest.TestCase):
|
|||||||
si.submit()
|
si.submit()
|
||||||
|
|
||||||
# reset
|
# 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
|
frappe.flags.country = country
|
||||||
|
|
||||||
def test_einvoice_json(self):
|
def test_einvoice_json(self):
|
||||||
@@ -2063,6 +2074,7 @@ def create_sales_invoice(**args):
|
|||||||
si.return_against = args.return_against
|
si.return_against = args.return_against
|
||||||
si.currency=args.currency or "INR"
|
si.currency=args.currency or "INR"
|
||||||
si.conversion_rate = args.conversion_rate or 1
|
si.conversion_rate = args.conversion_rate or 1
|
||||||
|
si.naming_series = args.naming_series or "T-SINV-"
|
||||||
|
|
||||||
si.append("items", {
|
si.append("items", {
|
||||||
"item_code": args.item or args.item_code or "_Test Item",
|
"item_code": args.item or args.item_code or "_Test Item",
|
||||||
|
|||||||
@@ -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)
|
debit_note_amount = get_debit_note_amount(suppliers, year_start_date, year_end_date)
|
||||||
supplier_credit_amount -= debit_note_amount
|
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)):
|
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,
|
if ldc and is_valid_certificate(ldc.valid_from, ldc.valid_upto, posting_date, tds_deducted, net_total,
|
||||||
|
|||||||
@@ -83,47 +83,6 @@ class TestTaxWithholdingCategory(unittest.TestCase):
|
|||||||
for d in invoices:
|
for d in invoices:
|
||||||
d.cancel()
|
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):
|
def create_purchase_invoice(**args):
|
||||||
# return sales invoice doc object
|
# return sales invoice doc object
|
||||||
item = frappe.get_doc('Item', {'item_name': 'TDS Item'})
|
item = frappe.get_doc('Item', {'item_name': 'TDS Item'})
|
||||||
|
|||||||
@@ -366,7 +366,6 @@ def make_purchase_receipt(source_name, target_doc=None):
|
|||||||
"Purchase Order": {
|
"Purchase Order": {
|
||||||
"doctype": "Purchase Receipt",
|
"doctype": "Purchase Receipt",
|
||||||
"field_map": {
|
"field_map": {
|
||||||
"per_billed": "per_billed",
|
|
||||||
"supplier_warehouse":"supplier_warehouse"
|
"supplier_warehouse":"supplier_warehouse"
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import frappe
|
|||||||
from frappe import _, msgprint
|
from frappe import _, msgprint
|
||||||
from frappe.utils import flt,cint, cstr, getdate
|
from frappe.utils import flt,cint, cstr, getdate
|
||||||
from six import iteritems
|
from six import iteritems
|
||||||
|
from collections import OrderedDict
|
||||||
from erpnext.accounts.party import get_party_details
|
from erpnext.accounts.party import get_party_details
|
||||||
from erpnext.stock.get_item_details import get_conversion_factor
|
from erpnext.stock.get_item_details import get_conversion_factor
|
||||||
from erpnext.buying.utils import validate_for_items, update_last_purchase_rate
|
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,
|
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)
|
qty, transferred_batch_qty_map, backflushed_batch_qty_map, item.purchase_order)
|
||||||
|
|
||||||
for batch_data in batches_qty:
|
for batch_data in batches_qty:
|
||||||
qty = batch_data['qty']
|
qty = batch_data['qty']
|
||||||
raw_material.batch_no = batch_data['batch']
|
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:
|
else:
|
||||||
self.append_raw_material_to_be_backflushed(item, raw_material, qty)
|
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:
|
for batch_data in transferred_batches:
|
||||||
key = ((batch_data.item_code, fg_item)
|
key = ((batch_data.item_code, fg_item)
|
||||||
if batch_data.subcontracted_item else (batch_data.item_code, purchase_order))
|
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
|
transferred_batch_qty_map[key][batch_data.batch_no] = batch_data.qty
|
||||||
|
|
||||||
return transferred_batch_qty_map
|
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:
|
if available_qty >= required_qty:
|
||||||
available_batches.append({'batch': batch, 'qty': required_qty})
|
available_batches.append({'batch': batch, 'qty': required_qty})
|
||||||
break
|
break
|
||||||
else:
|
elif available_qty != 0:
|
||||||
available_batches.append({'batch': batch, 'qty': available_qty})
|
available_batches.append({'batch': batch, 'qty': available_qty})
|
||||||
required_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
|
return available_batches
|
||||||
|
|||||||
@@ -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 status not in ("Stopped", "Closed") %(fcond)s
|
||||||
and (
|
and (
|
||||||
(`tabDelivery Note`.is_return = 0 and `tabDelivery Note`.per_billed < 100)
|
(`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 (
|
or (
|
||||||
`tabDelivery Note`.is_return = 1
|
`tabDelivery Note`.is_return = 1
|
||||||
and return_against in (select name from `tabDelivery Note` where per_billed < 100)
|
and return_against in (select name from `tabDelivery Note` where per_billed < 100)
|
||||||
|
|||||||
@@ -244,7 +244,6 @@ def make_quotation(source_name, target_doc=None):
|
|||||||
"doctype": "Quotation",
|
"doctype": "Quotation",
|
||||||
"field_map": {
|
"field_map": {
|
||||||
"opportunity_from": "quotation_to",
|
"opportunity_from": "quotation_to",
|
||||||
"opportunity_type": "order_type",
|
|
||||||
"name": "enq_no",
|
"name": "enq_no",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -17,8 +17,7 @@ class ShopifySettings(unittest.TestCase):
|
|||||||
frappe.set_user("Administrator")
|
frappe.set_user("Administrator")
|
||||||
|
|
||||||
# use the fixture data
|
# use the fixture data
|
||||||
import_doc(path=frappe.get_app_path("erpnext", "erpnext_integrations/doctype/shopify_settings/test_data/custom_field.json"),
|
import_doc(path=frappe.get_app_path("erpnext", "erpnext_integrations/doctype/shopify_settings/test_data/custom_field.json"))
|
||||||
ignore_links=True, overwrite=True)
|
|
||||||
|
|
||||||
frappe.reload_doctype("Customer")
|
frappe.reload_doctype("Customer")
|
||||||
frappe.reload_doctype("Sales Order")
|
frappe.reload_doctype("Sales Order")
|
||||||
|
|||||||
@@ -246,7 +246,7 @@ doc_events = {
|
|||||||
"on_submit": ["erpnext.regional.create_transaction_log", "erpnext.regional.italy.utils.sales_invoice_on_submit"],
|
"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_cancel": "erpnext.regional.italy.utils.sales_invoice_on_cancel",
|
||||||
"on_trash": "erpnext.regional.check_deletion_permission",
|
"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": {
|
"Purchase Invoice": {
|
||||||
"validate": "erpnext.regional.india.utils.update_grand_total_for_rcm"
|
"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'): {
|
('Sales Invoice', 'Sales Order', 'Delivery Note', 'Purchase Invoice', 'Purchase Order', 'Purchase Receipt'): {
|
||||||
'validate': ['erpnext.regional.india.utils.set_place_of_supply']
|
'validate': ['erpnext.regional.india.utils.set_place_of_supply']
|
||||||
},
|
},
|
||||||
('Sales Invoice', 'Purchase Invoice'): {
|
|
||||||
'validate': ['erpnext.regional.india.utils.validate_document_name']
|
|
||||||
},
|
|
||||||
"Contact": {
|
"Contact": {
|
||||||
"on_trash": "erpnext.support.doctype.issue.issue.update_issue",
|
"on_trash": "erpnext.support.doctype.issue.issue.update_issue",
|
||||||
"after_insert": "erpnext.communication.doctype.call_log.call_log.set_caller_information",
|
"after_insert": "erpnext.communication.doctype.call_log.call_log.set_caller_information",
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
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 frappe.model.document import Document
|
||||||
from erpnext.hr.utils import validate_dates, validate_overlap, get_leave_period, \
|
from erpnext.hr.utils import validate_dates, validate_overlap, get_leave_period, \
|
||||||
get_holidays_for_employee, create_additional_leave_ledger_entry
|
get_holidays_for_employee, create_additional_leave_ledger_entry
|
||||||
@@ -40,7 +40,12 @@ class CompensatoryLeaveRequest(Document):
|
|||||||
def validate_holidays(self):
|
def validate_holidays(self):
|
||||||
holidays = get_holidays_for_employee(self.employee, self.work_from_date, self.work_end_date)
|
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:
|
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):
|
def on_submit(self):
|
||||||
company = frappe.db.get_value("Employee", self.employee, "company")
|
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)
|
leave_allocation = self.create_leave_allocation(leave_period, date_difference)
|
||||||
self.leave_allocation=leave_allocation.name
|
self.leave_allocation=leave_allocation.name
|
||||||
else:
|
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):
|
def on_cancel(self):
|
||||||
if self.leave_allocation:
|
if self.leave_allocation:
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ class JobCard(Document):
|
|||||||
if d.completed_qty:
|
if d.completed_qty:
|
||||||
self.total_completed_qty += 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):
|
def get_overlap_for(self, args):
|
||||||
existing = frappe.db.sql("""select jc.name as name from
|
existing = frappe.db.sql("""select jc.name as name from
|
||||||
`tabJob Card Time Log` jctl, `tabJob Card` jc where jctl.parent = jc.name and
|
`tabJob Card Time Log` jctl, `tabJob Card` jc where jctl.parent = jc.name and
|
||||||
|
|||||||
@@ -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) {
|
frm.fields_dict['po_items'].grid.get_field('item_code').get_query = function(doc) {
|
||||||
return {
|
return {
|
||||||
query: "erpnext.controllers.queries.item_query",
|
query: "erpnext.controllers.queries.item_query",
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ class ProductionPlan(Document):
|
|||||||
from `tabMaterial Request` mr, `tabMaterial Request Item` mr_item
|
from `tabMaterial Request` mr, `tabMaterial Request Item` mr_item
|
||||||
where mr_item.parent = mr.name
|
where mr_item.parent = mr.name
|
||||||
and mr.material_request_type = "Manufacture"
|
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 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 (exists (select name from `tabBOM` bom where bom.item=mr_item.item_code
|
||||||
and bom.is_active = 1))
|
and bom.is_active = 1))
|
||||||
|
|||||||
@@ -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.add_transporter_address_field #2020-10-27
|
||||||
erpnext.patches.v12_0.setup_einvoice_fields #2020-12-02
|
erpnext.patches.v12_0.setup_einvoice_fields #2020-12-02
|
||||||
erpnext.patches.v12_0.add_state_code_for_ladakh
|
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
|
||||||
|
|||||||
@@ -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])
|
||||||
18
erpnext/patches/v12_0/create_taxable_value_field.py
Normal file
18
erpnext/patches/v12_0/create_taxable_value_field.py
Normal file
@@ -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)
|
||||||
24
erpnext/patches/v12_0/purchase_receipt_status.py
Normal file
24
erpnext/patches/v12_0/purchase_receipt_status.py
Normal file
@@ -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)
|
||||||
@@ -30,6 +30,7 @@ class Task(NestedSet):
|
|||||||
|
|
||||||
def validate(self):
|
def validate(self):
|
||||||
self.validate_dates()
|
self.validate_dates()
|
||||||
|
self.validate_parent_expected_end_date()
|
||||||
self.validate_parent_project_dates()
|
self.validate_parent_project_dates()
|
||||||
self.validate_progress()
|
self.validate_progress()
|
||||||
self.validate_status()
|
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.throw(_("{0} can not be greater than {1}").format(frappe.bold("Actual Start Date"), \
|
||||||
frappe.bold("Actual End 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):
|
def validate_parent_project_dates(self):
|
||||||
if not self.project or frappe.flags.in_test:
|
if not self.project or frappe.flags.in_test:
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -635,34 +635,34 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
|
|||||||
this.frm.trigger("item_code", cdt, cdn);
|
this.frm.trigger("item_code", cdt, cdn);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
var valid_serial_nos = [];
|
|
||||||
|
|
||||||
// Replacing all occurences of comma with carriage return
|
// Replacing all occurences of comma with carriage return
|
||||||
var serial_nos = item.serial_no.trim().replace(/,/g, '\n');
|
item.serial_no = item.serial_no.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.conversion_factor = item.conversion_factor || 1;
|
item.conversion_factor = item.conversion_factor || 1;
|
||||||
|
|
||||||
refresh_field("serial_no", item.name, item.parentfield);
|
refresh_field("serial_no", item.name, item.parentfield);
|
||||||
if(!doc.is_return && cint(user_defaults.set_qty_in_transactions_based_on_serial_no_input)) {
|
if (!doc.is_return && cint(frappe.user_defaults.set_qty_in_transactions_based_on_serial_no_input)) {
|
||||||
frappe.model.set_value(item.doctype, item.name,
|
setTimeout(() => {
|
||||||
"qty", valid_serial_nos.length / item.conversion_factor);
|
me.update_qty(cdt, cdn);
|
||||||
frappe.model.set_value(item.doctype, item.name, "stock_qty", valid_serial_nos.length);
|
}, 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() {
|
validate: function() {
|
||||||
this.calculate_taxes_and_totals(false);
|
this.calculate_taxes_and_totals(false);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
|
"company",
|
||||||
"gstin",
|
"gstin",
|
||||||
"username",
|
"username",
|
||||||
"password"
|
"password"
|
||||||
@@ -30,12 +31,20 @@
|
|||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Password",
|
"label": "Password",
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "company",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Company",
|
||||||
|
"options": "Company",
|
||||||
|
"reqd": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-12-22 15:10:53.466205",
|
"modified": "2021-03-22 12:16:56.365616",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Regional",
|
"module": "Regional",
|
||||||
"name": "E Invoice User",
|
"name": "E Invoice User",
|
||||||
|
|||||||
@@ -350,13 +350,12 @@ class GSTR3BReport(Document):
|
|||||||
return inter_state_supply_details
|
return inter_state_supply_details
|
||||||
|
|
||||||
def get_inward_nil_exempt(self, state):
|
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,
|
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
|
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
|
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
|
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 = {
|
inward_nil_exempt_details = {
|
||||||
"gst": {
|
"gst": {
|
||||||
|
|||||||
@@ -919,7 +919,8 @@
|
|||||||
"minLength": 1,
|
"minLength": 1,
|
||||||
"maxLength": 15,
|
"maxLength": 15,
|
||||||
"pattern": "^([0-9A-Z/-]){1,15}$",
|
"pattern": "^([0-9A-Z/-]){1,15}$",
|
||||||
"description": "Tranport Document Number"
|
"description": "Tranport Document Number",
|
||||||
|
"validationMsg": "Transport Receipt No is invalid"
|
||||||
},
|
},
|
||||||
"TransDocDt": {
|
"TransDocDt": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
erpnext.setup_einvoice_actions = (doctype) => {
|
erpnext.setup_einvoice_actions = (doctype) => {
|
||||||
frappe.ui.form.on(doctype, {
|
frappe.ui.form.on(doctype, {
|
||||||
refresh(frm) {
|
async refresh(frm) {
|
||||||
const einvoicing_enabled = frappe.db.get_value("E Invoice Settings", "E Invoice Settings", "enable");
|
const res = await frappe.call({
|
||||||
const supply_type = frm.doc.gst_category;
|
method: 'erpnext.regional.india.e_invoice.utils.validate_eligibility',
|
||||||
const valid_supply_type = ['Registered Regular', 'SEZ', 'Overseas', 'Deemed Export'].includes(supply_type);
|
args: { doc: frm.doc }
|
||||||
const company_transaction = frm.doc.billing_address_gstin == frm.doc.company_gstin;
|
});
|
||||||
|
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;
|
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) {
|
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 action = () => {
|
||||||
const d = new frappe.ui.Dialog({
|
let message = __('Cancellation of e-way bill is currently not supported. ');
|
||||||
title: __('Cancel E-Way Bill'),
|
message += '<br><br>';
|
||||||
fields: fields,
|
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() {
|
primary_action: function() {
|
||||||
const data = d.get_values();
|
|
||||||
frappe.call({
|
frappe.call({
|
||||||
method: 'erpnext.regional.india.e_invoice.utils.cancel_eway_bill',
|
method: 'erpnext.regional.india.e_invoice.utils.cancel_eway_bill',
|
||||||
args: {
|
args: { doctype, docname: name },
|
||||||
doctype,
|
|
||||||
docname: name,
|
|
||||||
eway_bill: ewaybill,
|
|
||||||
reason: data.reason.split('-')[0],
|
|
||||||
remark: data.remark
|
|
||||||
},
|
|
||||||
freeze: true,
|
freeze: true,
|
||||||
callback: () => frm.reload_doc() || d.hide(),
|
callback: () => frm.reload_doc()
|
||||||
error: () => d.hide()
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
primary_action_label: __('Submit')
|
primary_action_label: __('Yes')
|
||||||
});
|
});
|
||||||
d.show();
|
|
||||||
};
|
};
|
||||||
add_custom_button(__("Cancel E-Way Bill"), action);
|
add_custom_button(__("Cancel E-Way Bill"), action);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import six
|
||||||
import jwt
|
import jwt
|
||||||
import sys
|
import sys
|
||||||
import json
|
import json
|
||||||
@@ -16,16 +17,38 @@ from frappe import _, bold
|
|||||||
from pyqrcode import create as qrcreate
|
from pyqrcode import create as qrcreate
|
||||||
from frappe.integrations.utils import make_post_request, make_get_request
|
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 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):
|
@frappe.whitelist()
|
||||||
einvoicing_enabled = cint(frappe.db.get_value('E Invoice Settings', 'E Invoice Settings', 'enable'))
|
def validate_eligibility(doc):
|
||||||
invalid_doctype = doc.doctype != 'Sales Invoice'
|
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']
|
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')
|
company_transaction = doc.get('billing_address_gstin') == doc.get('company_gstin')
|
||||||
no_taxes_applied = not doc.get('taxes')
|
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
|
return
|
||||||
|
|
||||||
if doc.docstatus == 0 and doc._action == 'save':
|
if doc.docstatus == 0 and doc._action == 'save':
|
||||||
@@ -86,35 +109,39 @@ def get_doc_details(invoice):
|
|||||||
invoice_date=invoice_date
|
invoice_date=invoice_date
|
||||||
))
|
))
|
||||||
|
|
||||||
def get_party_details(address_name):
|
def validate_address_fields(address, is_shipping_address):
|
||||||
d = frappe.get_all('Address', filters={'name': address_name}, fields=['*'])[0]
|
if ((not address.gstin and not is_shipping_address)
|
||||||
|
or not address.city
|
||||||
if (not d.gstin
|
or not address.pincode
|
||||||
or not d.city
|
or not address.address_title
|
||||||
or not d.pincode
|
or not address.address_line1
|
||||||
or not d.address_title
|
or not address.gst_state_number):
|
||||||
or not d.address_line1
|
|
||||||
or not d.gst_state_number):
|
|
||||||
|
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
msg=_('Address lines, city, pincode, gstin is mandatory for address {}. Please set them and try again.').format(
|
msg=_('Address Lines, City, Pincode, GSTIN are mandatory for address {}. Please set them and try again.').format(address.name),
|
||||||
get_link_to_form('Address', address_name)
|
|
||||||
),
|
|
||||||
title=_('Missing Address Fields')
|
title=_('Missing Address Fields')
|
||||||
)
|
)
|
||||||
|
|
||||||
if d.gst_state_number == 97:
|
def get_party_details(address_name, is_shipping_address=False):
|
||||||
# according to einvoice standard
|
addr = frappe.get_doc('Address', address_name)
|
||||||
pincode = 999999
|
|
||||||
|
|
||||||
return frappe._dict(dict(
|
validate_address_fields(addr, is_shipping_address)
|
||||||
gstin=d.gstin, legal_name=d.address_title,
|
|
||||||
location=d.city, pincode=d.pincode,
|
if addr.gst_state_number == 97:
|
||||||
state_code=d.gst_state_number,
|
# according to einvoice standard
|
||||||
address_line1=d.address_line1,
|
addr.pincode = 999999
|
||||||
address_line2=d.address_line2
|
|
||||||
|
party_address_details = frappe._dict(dict(
|
||||||
|
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)
|
||||||
))
|
))
|
||||||
|
|
||||||
|
return party_address_details
|
||||||
|
|
||||||
def get_gstin_details(gstin):
|
def get_gstin_details(gstin):
|
||||||
if not hasattr(frappe.local, 'gstin_cache'):
|
if not hasattr(frappe.local, 'gstin_cache'):
|
||||||
frappe.local.gstin_cache = {}
|
frappe.local.gstin_cache = {}
|
||||||
@@ -163,10 +190,15 @@ def get_item_list(invoice):
|
|||||||
item.description = json.dumps(d.item_name)[1:-1]
|
item.description = json.dumps(d.item_name)[1:-1]
|
||||||
|
|
||||||
item.qty = abs(item.qty)
|
item.qty = abs(item.qty)
|
||||||
item.discount_amount = 0
|
|
||||||
item.unit_rate = abs(item.base_net_amount / item.qty)
|
if invoice.apply_discount_on == 'Net Total' and invoice.discount_amount:
|
||||||
item.gross_amount = abs(item.base_net_amount)
|
item.discount_amount = abs(item.base_amount - item.base_net_amount)
|
||||||
item.taxable_value = abs(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 = 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
|
item.batch_expiry_date = format_date(item.batch_expiry_date, 'dd/mm/yyyy') if item.batch_expiry_date else None
|
||||||
@@ -199,11 +231,11 @@ def update_item_taxes(invoice, item):
|
|||||||
is_applicable = t.tax_amount and t.account_head in gst_accounts_list
|
is_applicable = t.tax_amount and t.account_head in gst_accounts_list
|
||||||
if is_applicable:
|
if is_applicable:
|
||||||
# this contains item wise tax rate & tax amount (incl. discount)
|
# 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_rate = item_tax_detail[0]
|
||||||
# item tax amount excluding discount amount
|
# 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:
|
if t.account_head in gst_accounts.cess_account:
|
||||||
item_tax_amount_after_discount = item_tax_detail[1]
|
item_tax_amount_after_discount = item_tax_detail[1]
|
||||||
@@ -224,10 +256,14 @@ def get_invoice_value_details(invoice):
|
|||||||
invoice_value_details = frappe._dict(dict())
|
invoice_value_details = frappe._dict(dict())
|
||||||
|
|
||||||
if invoice.apply_discount_on == 'Net Total' and invoice.discount_amount:
|
if invoice.apply_discount_on == 'Net Total' and invoice.discount_amount:
|
||||||
invoice_value_details.base_total = abs(invoice.base_total)
|
# Discount already applied on net total which means on items
|
||||||
invoice_value_details.invoice_discount_amt = abs(invoice.base_discount_amount)
|
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:
|
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
|
# since tax already considers discount amount
|
||||||
invoice_value_details.invoice_discount_amt = 0
|
invoice_value_details.invoice_discount_amt = 0
|
||||||
|
|
||||||
@@ -248,7 +284,11 @@ def update_invoice_taxes(invoice, invoice_value_details):
|
|||||||
invoice_value_details.total_igst_amt = 0
|
invoice_value_details.total_igst_amt = 0
|
||||||
invoice_value_details.total_cess_amt = 0
|
invoice_value_details.total_cess_amt = 0
|
||||||
invoice_value_details.total_other_charges = 0
|
invoice_value_details.total_other_charges = 0
|
||||||
|
considered_rows = []
|
||||||
|
|
||||||
for t in invoice.taxes:
|
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_list:
|
||||||
if t.account_head in gst_accounts.cess_account:
|
if t.account_head in gst_accounts.cess_account:
|
||||||
# using after discount amt since item also uses after discount amt for cess calc
|
# using after discount amt since item also uses after discount amt for cess calc
|
||||||
@@ -256,12 +296,26 @@ def update_invoice_taxes(invoice, invoice_value_details):
|
|||||||
|
|
||||||
for tax_type in ['igst', 'cgst', 'sgst']:
|
for tax_type in ['igst', 'cgst', 'sgst']:
|
||||||
if t.account_head in gst_accounts['{}_account'.format(tax_type)]:
|
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:
|
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
|
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):
|
def get_payment_details(invoice):
|
||||||
payee_name = invoice.company
|
payee_name = invoice.company
|
||||||
mode_of_payment = ', '.join([d.mode_of_payment for d in invoice.payments])
|
mode_of_payment = ', '.join([d.mode_of_payment for d in invoice.payments])
|
||||||
@@ -274,6 +328,10 @@ def get_payment_details(invoice):
|
|||||||
))
|
))
|
||||||
|
|
||||||
def get_return_doc_reference(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')
|
invoice_date = frappe.db.get_value('Sales Invoice', invoice.return_against, 'posting_date')
|
||||||
return frappe._dict(dict(
|
return frappe._dict(dict(
|
||||||
invoice_name=invoice.return_against, invoice_date=format_date(invoice_date, 'dd/mm/yyyy')
|
invoice_name=invoice.return_against, invoice_date=format_date(invoice_date, 'dd/mm/yyyy')
|
||||||
@@ -281,7 +339,11 @@ def get_return_doc_reference(invoice):
|
|||||||
|
|
||||||
def get_eway_bill_details(invoice):
|
def get_eway_bill_details(invoice):
|
||||||
if invoice.is_return:
|
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' }
|
mode_of_transport = { '': '', 'Road': '1', 'Air': '2', 'Rail': '3', 'Ship': '4' }
|
||||||
vehicle_type = { 'Regular': 'R', 'Over Dimensional Cargo (ODC)': 'O' }
|
vehicle_type = { 'Regular': 'R', 'Over Dimensional Cargo (ODC)': 'O' }
|
||||||
@@ -299,9 +361,15 @@ def get_eway_bill_details(invoice):
|
|||||||
|
|
||||||
def validate_mandatory_fields(invoice):
|
def validate_mandatory_fields(invoice):
|
||||||
if not invoice.company_address:
|
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:
|
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'):
|
if not frappe.db.get_value('Address', invoice.company_address, 'gstin'):
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_('GSTIN is mandatory to fetch company GSTIN details. Please enter GSTIN in selected company address.'),
|
_('GSTIN is mandatory to fetch company GSTIN details. Please enter GSTIN in selected company address.'),
|
||||||
@@ -313,6 +381,39 @@ def validate_mandatory_fields(invoice):
|
|||||||
title=_('Missing Fields')
|
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):
|
def make_einvoice(invoice):
|
||||||
validate_mandatory_fields(invoice)
|
validate_mandatory_fields(invoice)
|
||||||
|
|
||||||
@@ -328,26 +429,32 @@ def make_einvoice(invoice):
|
|||||||
buyer_details = get_overseas_address_details(invoice.customer_address)
|
buyer_details = get_overseas_address_details(invoice.customer_address)
|
||||||
else:
|
else:
|
||||||
buyer_details = get_party_details(invoice.customer_address)
|
buyer_details = get_party_details(invoice.customer_address)
|
||||||
place_of_supply = get_place_of_supply(invoice, invoice.doctype) or invoice.billing_address_gstin
|
place_of_supply = get_place_of_supply(invoice, invoice.doctype)
|
||||||
place_of_supply = place_of_supply[:2]
|
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))
|
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({})
|
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.shipping_address_name and invoice.customer_address != invoice.shipping_address_name:
|
||||||
if invoice.gst_category == 'Overseas':
|
if invoice.gst_category == 'Overseas':
|
||||||
shipping_details = get_overseas_address_details(invoice.shipping_address_name)
|
shipping_details = get_overseas_address_details(invoice.shipping_address_name)
|
||||||
else:
|
else:
|
||||||
shipping_details = get_party_details(invoice.shipping_address_name)
|
shipping_details = get_party_details(invoice.shipping_address_name, shipping_address=True)
|
||||||
|
|
||||||
if invoice.is_pos and invoice.base_paid_amount:
|
if invoice.is_pos and invoice.base_paid_amount:
|
||||||
payment_details = get_payment_details(invoice)
|
payment_details = get_payment_details(invoice)
|
||||||
|
|
||||||
if invoice.is_return and invoice.return_against:
|
if invoice.is_return and invoice.return_against:
|
||||||
prev_doc_details = get_return_doc_reference(invoice)
|
prev_doc_details = get_return_doc_reference(invoice)
|
||||||
|
|
||||||
if invoice.transporter:
|
if invoice.transporter and flt(invoice.distance) and not invoice.is_return:
|
||||||
eway_bill_details = get_eway_bill_details(invoice)
|
eway_bill_details = get_eway_bill_details(invoice)
|
||||||
|
|
||||||
# not yet implemented
|
# not yet implemented
|
||||||
dispatch_details = period_details = export_details = frappe._dict({})
|
dispatch_details = period_details = export_details = frappe._dict({})
|
||||||
|
|
||||||
@@ -358,78 +465,86 @@ def make_einvoice(invoice):
|
|||||||
period_details=period_details, prev_doc_details=prev_doc_details,
|
period_details=period_details, prev_doc_details=prev_doc_details,
|
||||||
export_details=export_details, eway_bill_details=eway_bill_details
|
export_details=export_details, eway_bill_details=eway_bill_details
|
||||||
)
|
)
|
||||||
einvoice = json.loads(einvoice)
|
|
||||||
|
|
||||||
validations = json.loads(read_json('einv_validation'))
|
try:
|
||||||
errors = validate_einvoice(validations, einvoice)
|
einvoice = safe_json_load(einvoice)
|
||||||
if errors:
|
einvoice = santize_einvoice_fields(einvoice)
|
||||||
message = "\n".join([
|
except Exception:
|
||||||
"E Invoice: ", json.dumps(einvoice, indent=4),
|
show_link_to_error_log(invoice, einvoice)
|
||||||
"-" * 50,
|
|
||||||
"Errors: ", json.dumps(errors, indent=4)
|
validate_totals(einvoice)
|
||||||
])
|
|
||||||
frappe.log_error(title="E Invoice Validation Failed", message=message)
|
|
||||||
throw_error_list(errors, _('E Invoice Validation Failed'))
|
|
||||||
|
|
||||||
return einvoice
|
return einvoice
|
||||||
|
|
||||||
def throw_error_list(errors, title):
|
def show_link_to_error_log(invoice, einvoice):
|
||||||
if len(errors) > 1:
|
err_log = log_error(einvoice)
|
||||||
li = ['<li>'+ d +'</li>' for d in errors]
|
link_to_error_log = get_link_to_form('Error Log', err_log.name, 'Error Log')
|
||||||
frappe.throw("<ul style='padding-left: 20px'>{}</ul>".format(''.join(li)), title=title)
|
frappe.throw(
|
||||||
else:
|
_('An error occurred while creating e-invoice for {}. Please check {} for more information.').format(
|
||||||
frappe.throw(errors[0], title=title)
|
invoice.name, link_to_error_log),
|
||||||
|
title=_('E Invoice Creation Failed')
|
||||||
|
)
|
||||||
|
|
||||||
def validate_einvoice(validations, einvoice, errors=[]):
|
def log_error(data=None):
|
||||||
for fieldname, field_validation in validations.items():
|
if isinstance(data, six.string_types):
|
||||||
value = einvoice.get(fieldname, None)
|
data = json.loads(data)
|
||||||
if not value or value == "None":
|
|
||||||
# remove keys with empty values
|
|
||||||
einvoice.pop(fieldname, None)
|
|
||||||
continue
|
|
||||||
|
|
||||||
value_type = field_validation.get("type").lower()
|
seperator = "--" * 50
|
||||||
if value_type in ['object', 'array']:
|
err_tb = traceback.format_exc()
|
||||||
child_validations = field_validation.get('properties')
|
err_msg = str(sys.exc_info()[1])
|
||||||
|
data = json.dumps(data, indent=4)
|
||||||
|
|
||||||
if isinstance(value, list):
|
message = "\n".join([
|
||||||
for d in value:
|
"Error", err_msg, seperator,
|
||||||
validate_einvoice(child_validations, d, errors)
|
"Data:", data, seperator,
|
||||||
if not d:
|
"Exception:", err_tb
|
||||||
# remove empty dicts
|
])
|
||||||
einvoice.pop(fieldname, None)
|
frappe.log_error(title=_('E Invoice Request Failed'), message=message)
|
||||||
|
|
||||||
|
def santize_einvoice_fields(einvoice):
|
||||||
|
int_fields = ["Pin","Distance","CrDay"]
|
||||||
|
float_fields = ["Qty","FreeQty","UnitPrice","TotAmt","Discount","PreTaxVal","AssAmt","GstRt","IgstAmt","CgstAmt","SgstAmt","CesRt","CesAmt","CesNonAdvlAmt","StateCesRt","StateCesAmt","StateCesNonAdvlAmt","OthChrg","TotItemVal","AssVal","CgstVal","SgstVal","IgstVal","CesVal","StCesVal","Discount","OthChrg","RndOffAmt","TotInvVal","TotInvValFc","PaidAmt","PaymtDue","ExpDuty",]
|
||||||
|
copy = einvoice.copy()
|
||||||
|
for key, value in copy.items():
|
||||||
|
if isinstance(value, list):
|
||||||
|
for idx, d in enumerate(value):
|
||||||
|
santized_dict = santize_einvoice_fields(d)
|
||||||
|
if santized_dict:
|
||||||
|
einvoice[key][idx] = santized_dict
|
||||||
|
else:
|
||||||
|
einvoice[key].pop(idx)
|
||||||
|
|
||||||
|
if not einvoice[key]:
|
||||||
|
einvoice.pop(key, None)
|
||||||
|
|
||||||
|
elif isinstance(value, dict):
|
||||||
|
santized_dict = santize_einvoice_fields(value)
|
||||||
|
if santized_dict:
|
||||||
|
einvoice[key] = santized_dict
|
||||||
else:
|
else:
|
||||||
validate_einvoice(child_validations, value, errors)
|
einvoice.pop(key, None)
|
||||||
if not value:
|
|
||||||
# remove empty dicts
|
|
||||||
einvoice.pop(fieldname, None)
|
|
||||||
continue
|
|
||||||
|
|
||||||
# convert to int or str
|
elif not value or value == "None":
|
||||||
if value_type == 'string':
|
einvoice.pop(key, None)
|
||||||
einvoice[fieldname] = str(value)
|
|
||||||
elif value_type == 'number':
|
|
||||||
is_integer = '.' not in str(field_validation.get('maximum'))
|
|
||||||
precision = 3 if '.999' in str(field_validation.get('maximum')) else 2
|
|
||||||
einvoice[fieldname] = flt(value, precision) if not is_integer else cint(value)
|
|
||||||
value = einvoice[fieldname]
|
|
||||||
|
|
||||||
max_length = field_validation.get('maxLength')
|
elif key in float_fields:
|
||||||
minimum = flt(field_validation.get('minimum'))
|
einvoice[key] = flt(value, 2)
|
||||||
maximum = flt(field_validation.get('maximum'))
|
|
||||||
pattern_str = field_validation.get('pattern')
|
|
||||||
pattern = re.compile(pattern_str or '')
|
|
||||||
|
|
||||||
label = field_validation.get('description') or fieldname
|
elif key in int_fields:
|
||||||
|
einvoice[key] = cint(value)
|
||||||
|
|
||||||
if value_type == 'string' and len(value) > max_length:
|
return einvoice
|
||||||
errors.append(_('{} should not exceed {} characters').format(label, max_length))
|
|
||||||
if value_type == 'number' and (value > maximum or value < minimum):
|
def safe_json_load(json_string):
|
||||||
errors.append(_('{} {} should be between {} and {}').format(label, value, minimum, maximum))
|
JSONDecodeError = ValueError if six.PY2 else json.JSONDecodeError
|
||||||
if pattern_str and not pattern.match(value):
|
try:
|
||||||
errors.append(field_validation.get('validationMsg'))
|
return json.loads(json_string)
|
||||||
|
except JSONDecodeError as e:
|
||||||
return errors
|
# print a snippet of 40 characters around the location where error occured
|
||||||
|
pos = e.pos
|
||||||
|
start, end = max(0, pos-20), min(len(json_string)-1, pos+20)
|
||||||
|
snippet = json_string[start:end]
|
||||||
|
frappe.throw(_("Error in input data. Please check for any special characters near following input: <br> {}").format(snippet))
|
||||||
|
|
||||||
class RequestFailed(Exception): pass
|
class RequestFailed(Exception): pass
|
||||||
|
|
||||||
@@ -455,13 +570,17 @@ class GSPConnector():
|
|||||||
def get_credentials(self):
|
def get_credentials(self):
|
||||||
if self.invoice:
|
if self.invoice:
|
||||||
gstin = self.get_seller_gstin()
|
gstin = self.get_seller_gstin()
|
||||||
credentials = next(d for d in self.e_invoice_settings.credentials if d.gstin == gstin)
|
credentials_for_gstin = [d for d in self.e_invoice_settings.credentials if d.gstin == gstin]
|
||||||
|
if credentials_for_gstin:
|
||||||
|
self.credentials = credentials_for_gstin[0]
|
||||||
|
else:
|
||||||
|
frappe.throw(_('Cannot find e-invoicing credentials for selected Company GSTIN. Please check E-Invoice Settings'))
|
||||||
else:
|
else:
|
||||||
credentials = self.e_invoice_settings.credentials[0] if self.e_invoice_settings.credentials else None
|
credentials = self.e_invoice_settings.credentials[0] if self.e_invoice_settings.credentials else None
|
||||||
return credentials
|
return credentials
|
||||||
|
|
||||||
def get_seller_gstin(self):
|
def get_seller_gstin(self):
|
||||||
gstin = self.invoice.company_gstin or frappe.db.get_value('Address', self.invoice.company_address, 'gstin')
|
gstin = frappe.db.get_value('Address', self.invoice.company_address, 'gstin')
|
||||||
if not gstin:
|
if not gstin:
|
||||||
frappe.throw(_('Cannot retrieve Company GSTIN. Please select company address with valid GSTIN.'))
|
frappe.throw(_('Cannot retrieve Company GSTIN. Please select company address with valid GSTIN.'))
|
||||||
return gstin
|
return gstin
|
||||||
@@ -509,7 +628,7 @@ class GSPConnector():
|
|||||||
self.e_invoice_settings.reload()
|
self.e_invoice_settings.reload()
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
self.log_error(res)
|
log_error(res)
|
||||||
self.raise_error(True)
|
self.raise_error(True)
|
||||||
|
|
||||||
def get_headers(self):
|
def get_headers(self):
|
||||||
@@ -531,14 +650,14 @@ class GSPConnector():
|
|||||||
if res.get('success'):
|
if res.get('success'):
|
||||||
return res.get('result')
|
return res.get('result')
|
||||||
else:
|
else:
|
||||||
self.log_error(res)
|
log_error(res)
|
||||||
raise RequestFailed
|
raise RequestFailed
|
||||||
|
|
||||||
except RequestFailed:
|
except RequestFailed:
|
||||||
self.raise_error()
|
self.raise_error()
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
self.log_error()
|
log_error()
|
||||||
self.raise_error(True)
|
self.raise_error(True)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -584,7 +703,7 @@ class GSPConnector():
|
|||||||
self.raise_error(errors=errors)
|
self.raise_error(errors=errors)
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
self.log_error(data)
|
log_error(data)
|
||||||
self.raise_error(True)
|
self.raise_error(True)
|
||||||
|
|
||||||
def get_irn_details(self, irn):
|
def get_irn_details(self, irn):
|
||||||
@@ -603,7 +722,7 @@ class GSPConnector():
|
|||||||
self.raise_error(errors=errors)
|
self.raise_error(errors=errors)
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
self.log_error()
|
log_error()
|
||||||
self.raise_error(True)
|
self.raise_error(True)
|
||||||
|
|
||||||
def cancel_irn(self, irn, reason, remark):
|
def cancel_irn(self, irn, reason, remark):
|
||||||
@@ -616,7 +735,7 @@ class GSPConnector():
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
res = self.make_request('post', self.cancel_irn_url, headers, data)
|
res = self.make_request('post', self.cancel_irn_url, headers, data)
|
||||||
if res.get('success'):
|
if res.get('success') or '9999' in res.get('message'):
|
||||||
self.invoice.irn_cancelled = 1
|
self.invoice.irn_cancelled = 1
|
||||||
self.invoice.flags.updater_reference = {
|
self.invoice.flags.updater_reference = {
|
||||||
'doctype': self.invoice.doctype,
|
'doctype': self.invoice.doctype,
|
||||||
@@ -633,7 +752,7 @@ class GSPConnector():
|
|||||||
self.raise_error(errors=errors)
|
self.raise_error(errors=errors)
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
self.log_error(data)
|
log_error(data)
|
||||||
self.raise_error(True)
|
self.raise_error(True)
|
||||||
|
|
||||||
def generate_eway_bill(self, **kwargs):
|
def generate_eway_bill(self, **kwargs):
|
||||||
@@ -674,7 +793,7 @@ class GSPConnector():
|
|||||||
self.raise_error(errors=errors)
|
self.raise_error(errors=errors)
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
self.log_error(data)
|
log_error(data)
|
||||||
self.raise_error(True)
|
self.raise_error(True)
|
||||||
|
|
||||||
def cancel_eway_bill(self, eway_bill, reason, remark):
|
def cancel_eway_bill(self, eway_bill, reason, remark):
|
||||||
@@ -707,7 +826,7 @@ class GSPConnector():
|
|||||||
self.raise_error(errors=errors)
|
self.raise_error(errors=errors)
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
self.log_error(data)
|
log_error(data)
|
||||||
self.raise_error(True)
|
self.raise_error(True)
|
||||||
|
|
||||||
def sanitize_error_message(self, message):
|
def sanitize_error_message(self, message):
|
||||||
@@ -732,22 +851,6 @@ class GSPConnector():
|
|||||||
errors[idx] = errors[idx][:-6]
|
errors[idx] = errors[idx][:-6]
|
||||||
|
|
||||||
return errors
|
return errors
|
||||||
|
|
||||||
def log_error(self, data={}):
|
|
||||||
if not isinstance(data, dict):
|
|
||||||
data = json.loads(data)
|
|
||||||
|
|
||||||
seperator = "--" * 50
|
|
||||||
err_tb = traceback.format_exc()
|
|
||||||
err_msg = str(sys.exc_info()[1])
|
|
||||||
data = json.dumps(data, indent=4)
|
|
||||||
|
|
||||||
message = "\n".join([
|
|
||||||
"Error", err_msg, seperator,
|
|
||||||
"Data:", data, seperator,
|
|
||||||
"Exception:", err_tb
|
|
||||||
])
|
|
||||||
frappe.log_error(title=_('E Invoice Request Failed'), message=message)
|
|
||||||
|
|
||||||
def raise_error(self, raise_exception=False, errors=[]):
|
def raise_error(self, raise_exception=False, errors=[]):
|
||||||
title = _('E Invoice Request Failed')
|
title = _('E Invoice Request Failed')
|
||||||
@@ -768,6 +871,8 @@ class GSPConnector():
|
|||||||
|
|
||||||
self.invoice.irn = res.get('Irn')
|
self.invoice.irn = res.get('Irn')
|
||||||
self.invoice.ewaybill = res.get('EwbNo')
|
self.invoice.ewaybill = res.get('EwbNo')
|
||||||
|
self.invoice.ack_no = res.get('AckNo')
|
||||||
|
self.invoice.ack_date = res.get('AckDt')
|
||||||
self.invoice.signed_einvoice = dec_signed_invoice
|
self.invoice.signed_einvoice = dec_signed_invoice
|
||||||
self.invoice.signed_qr_code = res.get('SignedQRCode')
|
self.invoice.signed_qr_code = res.get('SignedQRCode')
|
||||||
|
|
||||||
@@ -807,6 +912,11 @@ class GSPConnector():
|
|||||||
self.invoice.flags.ignore_validate = True
|
self.invoice.flags.ignore_validate = True
|
||||||
self.invoice.save()
|
self.invoice.save()
|
||||||
|
|
||||||
|
def sanitize_for_json(string):
|
||||||
|
"""Escape JSON specific characters from a string."""
|
||||||
|
# json.dumps adds double-quotes to the string. Indexing to remove them.
|
||||||
|
return json.dumps(string)[1:-1]
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_einvoice(doctype, docname):
|
def get_einvoice(doctype, docname):
|
||||||
invoice = frappe.get_doc(doctype, docname)
|
invoice = frappe.get_doc(doctype, docname)
|
||||||
@@ -829,5 +939,9 @@ def generate_eway_bill(doctype, docname, **kwargs):
|
|||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def cancel_eway_bill(doctype, docname, eway_bill, reason, remark):
|
def cancel_eway_bill(doctype, docname, eway_bill, reason, remark):
|
||||||
gsp_connector = GSPConnector(doctype, docname)
|
# TODO: uncomment when eway_bill api from Adequare is enabled
|
||||||
gsp_connector.cancel_eway_bill(eway_bill, reason, remark)
|
# gsp_connector = GSPConnector(doctype, docname)
|
||||||
|
# gsp_connector.cancel_eway_bill(eway_bill, reason, remark)
|
||||||
|
|
||||||
|
# update cancelled status only, to be able to cancel irn next
|
||||||
|
frappe.db.set_value(doctype, docname, 'eway_bill_cancelled', 1)
|
||||||
|
|||||||
@@ -5,20 +5,22 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
import frappe, os, json
|
import frappe, os, json
|
||||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
|
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
|
||||||
|
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
|
||||||
from frappe.permissions import add_permission, update_permission_property
|
from frappe.permissions import add_permission, update_permission_property
|
||||||
from erpnext.regional.india import states
|
from erpnext.regional.india import states
|
||||||
from erpnext.accounts.utils import get_fiscal_year, FiscalYearError
|
from erpnext.accounts.utils import get_fiscal_year, FiscalYearError
|
||||||
from frappe.utils import today
|
from frappe.utils import today
|
||||||
|
|
||||||
def setup(company=None, patch=True):
|
def setup(company=None, patch=True):
|
||||||
setup_company_independent_fixtures()
|
setup_company_independent_fixtures(patch=patch)
|
||||||
if not patch:
|
if not patch:
|
||||||
update_address_template()
|
update_address_template()
|
||||||
make_fixtures(company)
|
make_fixtures(company)
|
||||||
|
|
||||||
# TODO: for all countries
|
# TODO: for all countries
|
||||||
def setup_company_independent_fixtures():
|
def setup_company_independent_fixtures(patch=False):
|
||||||
make_custom_fields()
|
make_custom_fields()
|
||||||
|
make_property_setters(patch=patch)
|
||||||
add_permissions()
|
add_permissions()
|
||||||
add_custom_roles_for_reports()
|
add_custom_roles_for_reports()
|
||||||
frappe.enqueue('erpnext.regional.india.setup.add_hsn_sac_codes', now=frappe.flags.in_test)
|
frappe.enqueue('erpnext.regional.india.setup.add_hsn_sac_codes', now=frappe.flags.in_test)
|
||||||
@@ -98,6 +100,12 @@ def add_print_formats():
|
|||||||
frappe.db.sql(""" update `tabPrint Format` set disabled = 0 where
|
frappe.db.sql(""" update `tabPrint Format` set disabled = 0 where
|
||||||
name in('GST POS Invoice', 'GST Tax Invoice', 'GST E-Invoice') """)
|
name in('GST POS Invoice', 'GST Tax Invoice', 'GST E-Invoice') """)
|
||||||
|
|
||||||
|
def make_property_setters(patch=False):
|
||||||
|
# GST rules do not allow for an invoice no. bigger than 16 characters
|
||||||
|
if not patch:
|
||||||
|
make_property_setter('Sales Invoice', 'naming_series', 'options', 'SINV-.YY.-\nSRET-.YY.-', '')
|
||||||
|
make_property_setter('Purchase Invoice', 'naming_series', 'options', 'PINV-.YY.-\nPRET-.YY.-', '')
|
||||||
|
|
||||||
def make_custom_fields(update=True):
|
def make_custom_fields(update=True):
|
||||||
hsn_sac_field = dict(fieldname='gst_hsn_code', label='HSN/SAC',
|
hsn_sac_field = dict(fieldname='gst_hsn_code', label='HSN/SAC',
|
||||||
fieldtype='Data', fetch_from='item_code.gst_hsn_code', insert_after='description',
|
fieldtype='Data', fetch_from='item_code.gst_hsn_code', insert_after='description',
|
||||||
@@ -108,6 +116,9 @@ def make_custom_fields(update=True):
|
|||||||
is_non_gst = dict(fieldname='is_non_gst', label='Is Non GST',
|
is_non_gst = dict(fieldname='is_non_gst', label='Is Non GST',
|
||||||
fieldtype='Check', fetch_from='item_code.is_non_gst', insert_after='is_nil_exempt',
|
fieldtype='Check', fetch_from='item_code.is_non_gst', insert_after='is_nil_exempt',
|
||||||
print_hide=1)
|
print_hide=1)
|
||||||
|
taxable_value = dict(fieldname='taxable_value', label='Taxable Value',
|
||||||
|
fieldtype='Currency', insert_after='base_net_amount', hidden=1, options="Company:company:default_currency",
|
||||||
|
print_hide=1)
|
||||||
|
|
||||||
purchase_invoice_gst_category = [
|
purchase_invoice_gst_category = [
|
||||||
dict(fieldname='gst_section', label='GST Details', fieldtype='Section Break',
|
dict(fieldname='gst_section', label='GST Details', fieldtype='Section Break',
|
||||||
@@ -397,9 +408,9 @@ def make_custom_fields(update=True):
|
|||||||
si_einvoice_fields = [
|
si_einvoice_fields = [
|
||||||
dict(fieldname='irn', label='IRN', fieldtype='Data', read_only=1, insert_after='customer', no_copy=1, print_hide=1,
|
dict(fieldname='irn', label='IRN', fieldtype='Data', read_only=1, insert_after='customer', no_copy=1, print_hide=1,
|
||||||
depends_on='eval:in_list(["Registered Regular", "SEZ", "Overseas", "Deemed Export"], doc.gst_category) && doc.irn_cancelled === 0'),
|
depends_on='eval:in_list(["Registered Regular", "SEZ", "Overseas", "Deemed Export"], doc.gst_category) && doc.irn_cancelled === 0'),
|
||||||
|
|
||||||
dict(fieldname='ack_no', label='Ack. No.', fieldtype='Data', read_only=1, hidden=1, insert_after='irn', no_copy=1, print_hide=1),
|
dict(fieldname='ack_no', label='Ack. No.', fieldtype='Data', read_only=1, hidden=1, insert_after='irn', no_copy=1, print_hide=1),
|
||||||
|
|
||||||
dict(fieldname='ack_date', label='Ack. Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_no', no_copy=1, print_hide=1),
|
dict(fieldname='ack_date', label='Ack. Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_no', no_copy=1, print_hide=1),
|
||||||
|
|
||||||
dict(fieldname='irn_cancelled', label='IRN Cancelled', fieldtype='Check', no_copy=1, print_hide=1,
|
dict(fieldname='irn_cancelled', label='IRN Cancelled', fieldtype='Check', no_copy=1, print_hide=1,
|
||||||
@@ -443,7 +454,7 @@ def make_custom_fields(update=True):
|
|||||||
'Supplier Quotation Item': [hsn_sac_field, nil_rated_exempt, is_non_gst],
|
'Supplier Quotation Item': [hsn_sac_field, nil_rated_exempt, is_non_gst],
|
||||||
'Sales Order Item': [hsn_sac_field, nil_rated_exempt, is_non_gst],
|
'Sales Order Item': [hsn_sac_field, nil_rated_exempt, is_non_gst],
|
||||||
'Delivery Note Item': [hsn_sac_field, nil_rated_exempt, is_non_gst],
|
'Delivery Note Item': [hsn_sac_field, nil_rated_exempt, is_non_gst],
|
||||||
'Sales Invoice Item': [hsn_sac_field, nil_rated_exempt, is_non_gst],
|
'Sales Invoice Item': [hsn_sac_field, nil_rated_exempt, is_non_gst, taxable_value],
|
||||||
'Purchase Order Item': [hsn_sac_field, nil_rated_exempt, is_non_gst],
|
'Purchase Order Item': [hsn_sac_field, nil_rated_exempt, is_non_gst],
|
||||||
'Purchase Receipt Item': [hsn_sac_field, nil_rated_exempt, is_non_gst],
|
'Purchase Receipt Item': [hsn_sac_field, nil_rated_exempt, is_non_gst],
|
||||||
'Purchase Invoice Item': [hsn_sac_field, nil_rated_exempt, is_non_gst],
|
'Purchase Invoice Item': [hsn_sac_field, nil_rated_exempt, is_non_gst],
|
||||||
@@ -616,7 +627,7 @@ def set_tax_withholding_category(company):
|
|||||||
fy_exist = [k for k in doc.get('rates') if k.get('fiscal_year')==fiscal_year]
|
fy_exist = [k for k in doc.get('rates') if k.get('fiscal_year')==fiscal_year]
|
||||||
if not fy_exist:
|
if not fy_exist:
|
||||||
doc.append("rates", d.get('rates')[0])
|
doc.append("rates", d.get('rates')[0])
|
||||||
|
|
||||||
doc.flags.ignore_permissions = True
|
doc.flags.ignore_permissions = True
|
||||||
doc.flags.ignore_mandatory = True
|
doc.flags.ignore_mandatory = True
|
||||||
doc.save()
|
doc.save()
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from __future__ import unicode_literals
|
|||||||
import frappe, re, json
|
import frappe, re, json
|
||||||
from frappe import _
|
from frappe import _
|
||||||
import erpnext
|
import erpnext
|
||||||
from frappe.utils import cstr, flt, date_diff, nowdate, round_based_on_smallest_currency_fraction, money_in_words, getdate
|
from frappe.utils import cstr, flt, cint, date_diff, nowdate, round_based_on_smallest_currency_fraction, money_in_words, getdate
|
||||||
from erpnext.regional.india import states, state_numbers
|
from erpnext.regional.india import states, state_numbers
|
||||||
from erpnext.controllers.taxes_and_totals import get_itemised_tax, get_itemised_taxable_amount, calculate_outstanding_amount
|
from erpnext.controllers.taxes_and_totals import get_itemised_tax, get_itemised_taxable_amount, calculate_outstanding_amount
|
||||||
from erpnext.controllers.accounts_controller import get_taxes_and_charges
|
from erpnext.controllers.accounts_controller import get_taxes_and_charges
|
||||||
@@ -748,16 +748,18 @@ def update_grand_total_for_rcm(doc, method):
|
|||||||
update_totals(gst_tax, base_gst_tax, doc)
|
update_totals(gst_tax, base_gst_tax, doc)
|
||||||
|
|
||||||
def update_totals(gst_tax, base_gst_tax, doc):
|
def update_totals(gst_tax, base_gst_tax, doc):
|
||||||
doc.base_grand_total -= base_gst_tax
|
|
||||||
doc.grand_total -= gst_tax
|
doc.grand_total -= gst_tax
|
||||||
|
doc.base_grand_total = (doc.grand_total * doc.conversion_rate)
|
||||||
|
|
||||||
if doc.meta.get_field("rounded_total"):
|
if doc.meta.get_field("rounded_total"):
|
||||||
if not doc.is_rounded_total_disabled():
|
if not doc.is_rounded_total_disabled():
|
||||||
doc.rounded_total = round_based_on_smallest_currency_fraction(doc.grand_total,
|
doc.rounded_total = round_based_on_smallest_currency_fraction(doc.grand_total,
|
||||||
doc.currency, doc.precision("rounded_total"))
|
doc.currency, doc.precision("rounded_total"))
|
||||||
|
doc.base_rounded_total += doc.rounded_total * doc.conversion_rate
|
||||||
|
|
||||||
doc.rounding_adjustment += flt(doc.rounded_total - doc.grand_total,
|
doc.rounding_adjustment += flt(doc.rounded_total - doc.grand_total,
|
||||||
doc.precision("rounding_adjustment"))
|
doc.precision("rounding_adjustment"))
|
||||||
|
doc.base_rounding_adjustment = doc.rounding_adjustment * doc.conversion_rate
|
||||||
|
|
||||||
calculate_outstanding_amount(doc)
|
calculate_outstanding_amount(doc)
|
||||||
|
|
||||||
@@ -817,7 +819,56 @@ def get_gst_tax_amount(doc):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
if flt(tax.base_tax_amount_after_discount_amount) and tax.account_head in gst_account_list:
|
if flt(tax.base_tax_amount_after_discount_amount) and tax.account_head in gst_account_list:
|
||||||
base_gst_tax += tax.base_tax_amount_after_discount_amount
|
if tax.add_deduct_tax == "Add":
|
||||||
gst_tax += tax.tax_amount_after_discount_amount
|
base_gst_tax += tax.base_tax_amount_after_discount_amount
|
||||||
|
gst_tax += tax.tax_amount_after_discount_amount
|
||||||
|
else:
|
||||||
|
base_gst_tax -= tax.base_tax_amount_after_discount_amount
|
||||||
|
gst_tax -= tax.tax_amount_after_discount_amount
|
||||||
|
|
||||||
return gst_tax, base_gst_tax
|
return gst_tax, base_gst_tax
|
||||||
|
|
||||||
|
def update_taxable_values(doc, method):
|
||||||
|
country = frappe.get_cached_value('Company', doc.company, 'country')
|
||||||
|
|
||||||
|
if country != 'India':
|
||||||
|
return
|
||||||
|
|
||||||
|
gst_accounts = get_gst_accounts(doc.company)
|
||||||
|
|
||||||
|
# Only considering sgst account to avoid inflating taxable value
|
||||||
|
gst_account_list = gst_accounts.get('sgst_account', []) + gst_accounts.get('sgst_account', []) \
|
||||||
|
+ gst_accounts.get('igst_account', [])
|
||||||
|
|
||||||
|
additional_taxes = 0
|
||||||
|
total_charges = 0
|
||||||
|
item_count = 0
|
||||||
|
considered_rows = []
|
||||||
|
|
||||||
|
for tax in doc.get('taxes'):
|
||||||
|
prev_row_id = cint(tax.row_id) - 1
|
||||||
|
if tax.account_head in gst_account_list and prev_row_id not in considered_rows:
|
||||||
|
if tax.charge_type == 'On Previous Row Amount':
|
||||||
|
additional_taxes += doc.get('taxes')[prev_row_id].tax_amount_after_discount_amount
|
||||||
|
considered_rows.append(prev_row_id)
|
||||||
|
if tax.charge_type == 'On Previous Row Total':
|
||||||
|
additional_taxes += doc.get('taxes')[prev_row_id].base_total - doc.base_net_total
|
||||||
|
considered_rows.append(prev_row_id)
|
||||||
|
|
||||||
|
for item in doc.get('items'):
|
||||||
|
if doc.apply_discount_on == 'Grand Total' and doc.discount_amount:
|
||||||
|
proportionate_value = item.base_amount if doc.base_total else item.qty
|
||||||
|
total_value = doc.base_total if doc.base_total else doc.total_qty
|
||||||
|
else:
|
||||||
|
proportionate_value = item.base_net_amount if doc.base_net_total else item.qty
|
||||||
|
total_value = doc.base_net_total if doc.base_net_total else doc.total_qty
|
||||||
|
|
||||||
|
applicable_charges = flt(flt(proportionate_value * (flt(additional_taxes) / flt(total_value)),
|
||||||
|
item.precision('taxable_value')))
|
||||||
|
item.taxable_value = applicable_charges + proportionate_value
|
||||||
|
total_charges += applicable_charges
|
||||||
|
item_count += 1
|
||||||
|
|
||||||
|
if total_charges != additional_taxes:
|
||||||
|
diff = additional_taxes - total_charges
|
||||||
|
doc.get('items')[item_count - 1].taxable_value += diff
|
||||||
|
|||||||
@@ -11,15 +11,10 @@ erpnext.setup_e_invoice_button = (doctype) => {
|
|||||||
callback: function(r) {
|
callback: function(r) {
|
||||||
frm.reload_doc();
|
frm.reload_doc();
|
||||||
if(r.message) {
|
if(r.message) {
|
||||||
var w = window.open(
|
open_url_post(frappe.request.url, {
|
||||||
frappe.urllib.get_full_url(
|
cmd: 'frappe.core.doctype.file.file.download_file',
|
||||||
"/api/method/erpnext.regional.italy.utils.download_e_invoice_file?"
|
file_url: r.message
|
||||||
+ "file_name=" + r.message
|
});
|
||||||
)
|
|
||||||
)
|
|
||||||
if (!w) {
|
|
||||||
frappe.msgprint(__("Please enable pop-ups")); return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -219,4 +219,4 @@ def add_permissions():
|
|||||||
update_permission_property(doctype, 'Accounts Manager', 0, 'delete', 1)
|
update_permission_property(doctype, 'Accounts Manager', 0, 'delete', 1)
|
||||||
add_permission(doctype, 'Accounts Manager', 1)
|
add_permission(doctype, 'Accounts Manager', 1)
|
||||||
update_permission_property(doctype, 'Accounts Manager', 1, 'write', 1)
|
update_permission_property(doctype, 'Accounts Manager', 1, 'write', 1)
|
||||||
update_permission_property(doctype, 'Accounts Manager', 1, 'create', 1)
|
update_permission_property(doctype, 'Accounts Manager', 1, 'create', 1)
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import frappe, json, os
|
import io
|
||||||
|
import json
|
||||||
|
import frappe
|
||||||
from frappe.utils import flt, cstr
|
from frappe.utils import flt, cstr
|
||||||
from erpnext.controllers.taxes_and_totals import get_itemised_tax
|
from erpnext.controllers.taxes_and_totals import get_itemised_tax
|
||||||
from frappe import _
|
from frappe import _
|
||||||
@@ -28,20 +30,22 @@ def update_itemised_tax_data(doc):
|
|||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def export_invoices(filters=None):
|
def export_invoices(filters=None):
|
||||||
saved_xmls = []
|
frappe.has_permission('Sales Invoice', throw=True)
|
||||||
|
|
||||||
invoices = frappe.get_all("Sales Invoice", filters=get_conditions(filters), fields=["*"])
|
invoices = frappe.get_all(
|
||||||
|
"Sales Invoice",
|
||||||
|
filters=get_conditions(filters),
|
||||||
|
fields=["name", "company_tax_id"]
|
||||||
|
)
|
||||||
|
|
||||||
for invoice in invoices:
|
attachments = get_e_invoice_attachments(invoices)
|
||||||
attachments = get_e_invoice_attachments(invoice)
|
|
||||||
saved_xmls += [attachment.file_name for attachment in attachments]
|
|
||||||
|
|
||||||
zip_filename = "{0}-einvoices.zip".format(frappe.utils.get_datetime().strftime("%Y%m%d_%H%M%S"))
|
zip_filename = "{0}-einvoices.zip".format(
|
||||||
|
frappe.utils.get_datetime().strftime("%Y%m%d_%H%M%S"))
|
||||||
|
|
||||||
download_zip(saved_xmls, zip_filename)
|
download_zip(attachments, zip_filename)
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
|
||||||
def prepare_invoice(invoice, progressive_number):
|
def prepare_invoice(invoice, progressive_number):
|
||||||
#set company information
|
#set company information
|
||||||
company = frappe.get_doc("Company", invoice.company)
|
company = frappe.get_doc("Company", invoice.company)
|
||||||
@@ -98,7 +102,7 @@ def prepare_invoice(invoice, progressive_number):
|
|||||||
def get_conditions(filters):
|
def get_conditions(filters):
|
||||||
filters = json.loads(filters)
|
filters = json.loads(filters)
|
||||||
|
|
||||||
conditions = {"docstatus": 1}
|
conditions = {"docstatus": 1, "company_tax_id": ("!=", "")}
|
||||||
|
|
||||||
if filters.get("company"): conditions["company"] = filters["company"]
|
if filters.get("company"): conditions["company"] = filters["company"]
|
||||||
if filters.get("customer"): conditions["customer"] = filters["customer"]
|
if filters.get("customer"): conditions["customer"] = filters["customer"]
|
||||||
@@ -111,23 +115,22 @@ def get_conditions(filters):
|
|||||||
|
|
||||||
return conditions
|
return conditions
|
||||||
|
|
||||||
#TODO: Use function from frappe once PR #6853 is merged.
|
|
||||||
def download_zip(files, output_filename):
|
def download_zip(files, output_filename):
|
||||||
from zipfile import ZipFile
|
import zipfile
|
||||||
|
|
||||||
input_files = [frappe.get_site_path('private', 'files', filename) for filename in files]
|
zip_stream = io.BytesIO()
|
||||||
output_path = frappe.get_site_path('private', 'files', output_filename)
|
with zipfile.ZipFile(zip_stream, 'w', zipfile.ZIP_DEFLATED) as zip_file:
|
||||||
|
for file in files:
|
||||||
|
file_path = frappe.utils.get_files_path(
|
||||||
|
file.file_name, is_private=file.is_private)
|
||||||
|
|
||||||
with ZipFile(output_path, 'w') as output_zip:
|
zip_file.write(file_path, arcname=file.file_name)
|
||||||
for input_file in input_files:
|
|
||||||
output_zip.write(input_file, arcname=os.path.basename(input_file))
|
|
||||||
|
|
||||||
with open(output_path, 'rb') as fileobj:
|
|
||||||
filedata = fileobj.read()
|
|
||||||
|
|
||||||
frappe.local.response.filename = output_filename
|
frappe.local.response.filename = output_filename
|
||||||
frappe.local.response.filecontent = filedata
|
frappe.local.response.filecontent = zip_stream.getvalue()
|
||||||
frappe.local.response.type = "download"
|
frappe.local.response.type = "download"
|
||||||
|
zip_stream.close()
|
||||||
|
|
||||||
def get_invoice_summary(items, taxes):
|
def get_invoice_summary(items, taxes):
|
||||||
summary_data = frappe._dict()
|
summary_data = frappe._dict()
|
||||||
@@ -307,23 +310,12 @@ def prepare_and_attach_invoice(doc, replace=False):
|
|||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def generate_single_invoice(docname):
|
def generate_single_invoice(docname):
|
||||||
doc = frappe.get_doc("Sales Invoice", docname)
|
doc = frappe.get_doc("Sales Invoice", docname)
|
||||||
|
frappe.has_permission("Sales Invoice", doc=doc, throw=True)
|
||||||
|
|
||||||
e_invoice = prepare_and_attach_invoice(doc, True)
|
e_invoice = prepare_and_attach_invoice(doc, True)
|
||||||
|
return e_invoice.file_url
|
||||||
|
|
||||||
return e_invoice.file_name
|
# Delete e-invoice attachment on cancel.
|
||||||
|
|
||||||
@frappe.whitelist()
|
|
||||||
def download_e_invoice_file(file_name):
|
|
||||||
content = None
|
|
||||||
with open(frappe.get_site_path('private', 'files', file_name), "r") as f:
|
|
||||||
content = f.read()
|
|
||||||
|
|
||||||
frappe.local.response.filename = file_name
|
|
||||||
frappe.local.response.filecontent = content
|
|
||||||
frappe.local.response.type = "download"
|
|
||||||
|
|
||||||
#Delete e-invoice attachment on cancel.
|
|
||||||
def sales_invoice_on_cancel(doc, method):
|
def sales_invoice_on_cancel(doc, method):
|
||||||
if get_company_country(doc.company) not in ['Italy',
|
if get_company_country(doc.company) not in ['Italy',
|
||||||
'Italia', 'Italian Republic', 'Repubblica Italiana']:
|
'Italia', 'Italian Republic', 'Repubblica Italiana']:
|
||||||
@@ -335,16 +327,38 @@ def sales_invoice_on_cancel(doc, method):
|
|||||||
def get_company_country(company):
|
def get_company_country(company):
|
||||||
return frappe.get_cached_value('Company', company, 'country')
|
return frappe.get_cached_value('Company', company, 'country')
|
||||||
|
|
||||||
def get_e_invoice_attachments(invoice):
|
def get_e_invoice_attachments(invoices):
|
||||||
if not invoice.company_tax_id:
|
if not isinstance(invoices, list):
|
||||||
return []
|
if not invoices.company_tax_id:
|
||||||
|
return
|
||||||
|
|
||||||
|
invoices = [invoices]
|
||||||
|
|
||||||
|
tax_id_map = {
|
||||||
|
invoice.name: (
|
||||||
|
invoice.company_tax_id
|
||||||
|
if invoice.company_tax_id.startswith("IT")
|
||||||
|
else "IT" + invoice.company_tax_id
|
||||||
|
) for invoice in invoices
|
||||||
|
}
|
||||||
|
|
||||||
|
attachments = frappe.get_all(
|
||||||
|
"File",
|
||||||
|
fields=("name", "file_name", "attached_to_name", "is_private"),
|
||||||
|
filters= {
|
||||||
|
"attached_to_name": ('in', tax_id_map),
|
||||||
|
"attached_to_doctype": 'Sales Invoice'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
out = []
|
out = []
|
||||||
attachments = get_attachments(invoice.doctype, invoice.name)
|
|
||||||
company_tax_id = invoice.company_tax_id if invoice.company_tax_id.startswith("IT") else "IT" + invoice.company_tax_id
|
|
||||||
|
|
||||||
for attachment in attachments:
|
for attachment in attachments:
|
||||||
if attachment.file_name and attachment.file_name.startswith(company_tax_id) and attachment.file_name.endswith(".xml"):
|
if (
|
||||||
|
attachment.file_name
|
||||||
|
and attachment.file_name.endswith(".xml")
|
||||||
|
and attachment.file_name.startswith(
|
||||||
|
tax_id_map.get(attachment.attached_to_name))
|
||||||
|
):
|
||||||
out.append(attachment)
|
out.append(attachment)
|
||||||
|
|
||||||
return out
|
return out
|
||||||
|
|||||||
@@ -114,7 +114,9 @@ class Customer(TransactionBase):
|
|||||||
'''If Customer created from Lead, update lead status to "Converted"
|
'''If Customer created from Lead, update lead status to "Converted"
|
||||||
update Customer link in Quotation, Opportunity'''
|
update Customer link in Quotation, Opportunity'''
|
||||||
if self.lead_name:
|
if self.lead_name:
|
||||||
frappe.db.set_value('Lead', self.lead_name, 'status', 'Converted', update_modified=False)
|
lead = frappe.get_doc('Lead', self.lead_name)
|
||||||
|
lead.status = 'Converted'
|
||||||
|
lead.save()
|
||||||
|
|
||||||
def create_lead_address_contact(self):
|
def create_lead_address_contact(self):
|
||||||
if self.lead_name:
|
if self.lead_name:
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ def delete_company_transactions(company_name):
|
|||||||
if doctype not in ("Account", "Cost Center", "Warehouse", "Budget",
|
if doctype not in ("Account", "Cost Center", "Warehouse", "Budget",
|
||||||
"Party Account", "Employee", "Sales Taxes and Charges Template",
|
"Party Account", "Employee", "Sales Taxes and Charges Template",
|
||||||
"Purchase Taxes and Charges Template", "POS Profile", "BOM",
|
"Purchase Taxes and Charges Template", "POS Profile", "BOM",
|
||||||
"Company", "Bank Account", "Item Tax Template", "Mode Of Payment",
|
"Company", "Bank Account", "Item Tax Template", "Mode Of Payment", "Mode of Payment Account",
|
||||||
"Item Default", "Customer", "Supplier", "GST Account"):
|
"Item Default", "Customer", "Supplier", "GST Account"):
|
||||||
delete_for_doctype(doctype, company_name)
|
delete_for_doctype(doctype, company_name)
|
||||||
|
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ frappe.ui.form.on("Item", {
|
|||||||
}
|
}
|
||||||
if (frm.doc.variant_of) {
|
if (frm.doc.variant_of) {
|
||||||
frm.set_intro(__('This Item is a Variant of {0} (Template).',
|
frm.set_intro(__('This Item is a Variant of {0} (Template).',
|
||||||
[`<a href="#Form/Item/${frm.doc.variant_of}">${frm.doc.variant_of}</a>`]), true);
|
[`<a href="/app/item/${frm.doc.variant_of}" onclick="location.reload()">${frm.doc.variant_of}</a>`]), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (frappe.defaults.get_default("item_naming_by")!="Naming Series" || frm.doc.variant_of) {
|
if (frappe.defaults.get_default("item_naming_by")!="Naming Series" || frm.doc.variant_of) {
|
||||||
|
|||||||
@@ -122,6 +122,7 @@
|
|||||||
"weightage",
|
"weightage",
|
||||||
"slideshow",
|
"slideshow",
|
||||||
"website_image",
|
"website_image",
|
||||||
|
"website_image_alt",
|
||||||
"thumbnail",
|
"thumbnail",
|
||||||
"cb72",
|
"cb72",
|
||||||
"website_warehouse",
|
"website_warehouse",
|
||||||
@@ -1053,14 +1054,21 @@
|
|||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"label": "Default Manufacturer Part No",
|
"label": "Default Manufacturer Part No",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "website_image_alt",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Image Description"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"has_web_view": 1,
|
"has_web_view": 1,
|
||||||
"icon": "fa fa-tag",
|
"icon": "fa fa-tag",
|
||||||
"idx": 2,
|
"idx": 2,
|
||||||
"image_field": "image",
|
"image_field": "image",
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"links": [],
|
||||||
"max_attachments": 1,
|
"max_attachments": 1,
|
||||||
"modified": "2020-08-06 17:03:26.594319",
|
"modified": "2021-03-18 11:24:58.384992",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Item",
|
"name": "Item",
|
||||||
@@ -1122,4 +1130,4 @@
|
|||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"title_field": "item_name",
|
"title_field": "item_name",
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -344,7 +344,7 @@ class Item(WebsiteGenerator):
|
|||||||
if variant:
|
if variant:
|
||||||
context.variant = frappe.get_doc("Item", variant)
|
context.variant = frappe.get_doc("Item", variant)
|
||||||
|
|
||||||
for fieldname in ("website_image", "web_long_description", "description",
|
for fieldname in ("website_image", "website_image_alt", "web_long_description", "description",
|
||||||
"website_specifications"):
|
"website_specifications"):
|
||||||
if context.variant.get(fieldname):
|
if context.variant.get(fieldname):
|
||||||
value = context.variant.get(fieldname)
|
value = context.variant.get(fieldname)
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ class PurchaseReceipt(BuyingController):
|
|||||||
if flt(self.per_billed) < 100:
|
if flt(self.per_billed) < 100:
|
||||||
self.update_billing_status()
|
self.update_billing_status()
|
||||||
else:
|
else:
|
||||||
self.status = "Completed"
|
self.db_set("status", "Completed")
|
||||||
|
|
||||||
|
|
||||||
# Updating stock ledger should always be called after updating prevdoc status,
|
# Updating stock ledger should always be called after updating prevdoc status,
|
||||||
|
|||||||
@@ -676,6 +676,56 @@ class TestPurchaseReceipt(unittest.TestCase):
|
|||||||
|
|
||||||
update_backflush_based_on("BOM")
|
update_backflush_based_on("BOM")
|
||||||
|
|
||||||
|
def test_po_to_pi_and_po_to_pr_worflow_full(self):
|
||||||
|
"""Test following behaviour:
|
||||||
|
- Create PO
|
||||||
|
- Create PI from PO and submit
|
||||||
|
- Create PR from PO and submit
|
||||||
|
"""
|
||||||
|
from erpnext.buying.doctype.purchase_order import test_purchase_order
|
||||||
|
from erpnext.buying.doctype.purchase_order import purchase_order
|
||||||
|
|
||||||
|
po = test_purchase_order.create_purchase_order()
|
||||||
|
|
||||||
|
pi = purchase_order.make_purchase_invoice(po.name)
|
||||||
|
pi.submit()
|
||||||
|
|
||||||
|
pr = purchase_order.make_purchase_receipt(po.name)
|
||||||
|
pr.submit()
|
||||||
|
|
||||||
|
pr.load_from_db()
|
||||||
|
|
||||||
|
self.assertEqual(pr.status, "Completed")
|
||||||
|
self.assertEqual(pr.per_billed, 100)
|
||||||
|
|
||||||
|
def test_po_to_pi_and_po_to_pr_worflow_partial(self):
|
||||||
|
"""Test following behaviour:
|
||||||
|
- Create PO
|
||||||
|
- Create partial PI from PO and submit
|
||||||
|
- Create PR from PO and submit
|
||||||
|
"""
|
||||||
|
from erpnext.buying.doctype.purchase_order import test_purchase_order
|
||||||
|
from erpnext.buying.doctype.purchase_order import purchase_order
|
||||||
|
|
||||||
|
po = test_purchase_order.create_purchase_order()
|
||||||
|
|
||||||
|
pi = purchase_order.make_purchase_invoice(po.name)
|
||||||
|
pi.items[0].qty /= 2 # roughly 50%, ^ this function only creates PI with 1 item.
|
||||||
|
pi.submit()
|
||||||
|
|
||||||
|
pr = purchase_order.make_purchase_receipt(po.name)
|
||||||
|
pr.save()
|
||||||
|
# per_billed is only updated after submission.
|
||||||
|
self.assertEqual(flt(pr.per_billed), 0)
|
||||||
|
|
||||||
|
pr.submit()
|
||||||
|
|
||||||
|
pi.load_from_db()
|
||||||
|
pr.load_from_db()
|
||||||
|
|
||||||
|
self.assertEqual(pr.status, "To Bill")
|
||||||
|
self.assertAlmostEqual(pr.per_billed, 50.0, places=2)
|
||||||
|
|
||||||
def get_gl_entries(voucher_type, voucher_no):
|
def get_gl_entries(voucher_type, voucher_no):
|
||||||
return frappe.db.sql("""select account, debit, credit, cost_center
|
return frappe.db.sql("""select account, debit, credit, cost_center
|
||||||
from `tabGL Entry` where voucher_type=%s and voucher_no=%s
|
from `tabGL Entry` where voucher_type=%s and voucher_no=%s
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ class StockReconciliation(StockController):
|
|||||||
self.remove_items_with_no_change()
|
self.remove_items_with_no_change()
|
||||||
self.validate_data()
|
self.validate_data()
|
||||||
self.validate_expense_account()
|
self.validate_expense_account()
|
||||||
|
self.validate_customer_provided_item()
|
||||||
|
self.set_zero_value_for_customer_provided_items()
|
||||||
self.set_total_qty_and_amount()
|
self.set_total_qty_and_amount()
|
||||||
|
|
||||||
if self._action=="submit":
|
if self._action=="submit":
|
||||||
@@ -213,7 +215,7 @@ class StockReconciliation(StockController):
|
|||||||
if row.valuation_rate in ("", None):
|
if row.valuation_rate in ("", None):
|
||||||
row.valuation_rate = previous_sle.get("valuation_rate", 0)
|
row.valuation_rate = previous_sle.get("valuation_rate", 0)
|
||||||
|
|
||||||
if row.qty and not row.valuation_rate:
|
if row.qty and not row.valuation_rate and not row.allow_zero_valuation_rate:
|
||||||
frappe.throw(_("Valuation Rate required for Item {0} at row {1}").format(row.item_code, row.idx))
|
frappe.throw(_("Valuation Rate required for Item {0} at row {1}").format(row.item_code, row.idx))
|
||||||
|
|
||||||
if (not item.has_batch_no and (previous_sle and row.qty == previous_sle.get("qty_after_transaction")
|
if (not item.has_batch_no and (previous_sle and row.qty == previous_sle.get("qty_after_transaction")
|
||||||
@@ -447,6 +449,20 @@ class StockReconciliation(StockController):
|
|||||||
if frappe.db.get_value("Account", self.expense_account, "report_type") == "Profit and Loss":
|
if frappe.db.get_value("Account", self.expense_account, "report_type") == "Profit and Loss":
|
||||||
frappe.throw(_("Difference Account must be a Asset/Liability type account, since this Stock Reconciliation is an Opening Entry"), OpeningEntryAccountError)
|
frappe.throw(_("Difference Account must be a Asset/Liability type account, since this Stock Reconciliation is an Opening Entry"), OpeningEntryAccountError)
|
||||||
|
|
||||||
|
def set_zero_value_for_customer_provided_items(self):
|
||||||
|
changed_any_values = False
|
||||||
|
|
||||||
|
for d in self.get('items'):
|
||||||
|
is_customer_item = frappe.db.get_value('Item', d.item_code, 'is_customer_provided_item')
|
||||||
|
if is_customer_item and d.valuation_rate:
|
||||||
|
d.valuation_rate = 0.0
|
||||||
|
changed_any_values = True
|
||||||
|
|
||||||
|
if changed_any_values:
|
||||||
|
msgprint(_("Valuation rate for customer provided items has been set to zero."),
|
||||||
|
title=_("Note"), indicator="blue")
|
||||||
|
|
||||||
|
|
||||||
def set_total_qty_and_amount(self):
|
def set_total_qty_and_amount(self):
|
||||||
for d in self.get("items"):
|
for d in self.get("items"):
|
||||||
d.amount = flt(d.qty, d.precision("qty")) * flt(d.valuation_rate, d.precision("valuation_rate"))
|
d.amount = flt(d.qty, d.precision("qty")) * flt(d.valuation_rate, d.precision("valuation_rate"))
|
||||||
@@ -548,4 +564,4 @@ def get_difference_account(purpose, company):
|
|||||||
account = frappe.db.get_value('Account', {'is_group': 0,
|
account = frappe.db.get_value('Account', {'is_group': 0,
|
||||||
'company': company, 'account_type': 'Temporary'}, 'name')
|
'company': company, 'account_type': 'Temporary'}, 'name')
|
||||||
|
|
||||||
return account
|
return account
|
||||||
|
|||||||
@@ -420,6 +420,16 @@ class TestStockReconciliation(unittest.TestCase):
|
|||||||
|
|
||||||
for doc in [sr, ste2, ste1]:
|
for doc in [sr, ste2, ste1]:
|
||||||
doc.cancel()
|
doc.cancel()
|
||||||
|
def test_customer_provided_items(self):
|
||||||
|
item_code = 'Stock-Reco-customer-Item-100'
|
||||||
|
create_item(item_code, is_customer_provided_item = 1,
|
||||||
|
customer = '_Test Customer', is_purchase_item = 0)
|
||||||
|
|
||||||
|
sr = create_stock_reconciliation(item_code = item_code, qty = 10, rate = 420)
|
||||||
|
|
||||||
|
self.assertEqual(sr.get("items")[0].allow_zero_valuation_rate, 1)
|
||||||
|
self.assertEqual(sr.get("items")[0].valuation_rate, 0)
|
||||||
|
self.assertEqual(sr.get("items")[0].amount, 0)
|
||||||
|
|
||||||
def insert_existing_sle(warehouse):
|
def insert_existing_sle(warehouse):
|
||||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
"qty",
|
"qty",
|
||||||
"valuation_rate",
|
"valuation_rate",
|
||||||
"amount",
|
"amount",
|
||||||
|
"allow_zero_valuation_rate",
|
||||||
"serial_no_and_batch_section",
|
"serial_no_and_batch_section",
|
||||||
"serial_no",
|
"serial_no",
|
||||||
"column_break_11",
|
"column_break_11",
|
||||||
@@ -166,10 +167,19 @@
|
|||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Batch No",
|
"label": "Batch No",
|
||||||
"options": "Batch"
|
"options": "Batch"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "allow_zero_valuation_rate",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Allow Zero Valuation Rate",
|
||||||
|
"print_hide": 1,
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"modified": "2019-06-14 17:10:53.188305",
|
"links": [],
|
||||||
|
"modified": "2021-03-23 11:09:44.407157",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Stock Reconciliation Item",
|
"name": "Stock Reconciliation Item",
|
||||||
@@ -179,4 +189,4 @@
|
|||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ product_image(website_image or image or 'no-image.jpg') }}
|
{{ product_image(website_image or image or 'no-image.jpg', alt=website_image_alt or item_name) }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- Simple image preview -->
|
<!-- Simple image preview -->
|
||||||
|
|||||||
@@ -7,9 +7,9 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
{% macro product_image(website_image, css_class="") %}
|
{% macro product_image(website_image, css_class="", alt="") %}
|
||||||
<div class="border text-center rounded h-100 {{ css_class }}" style="overflow: hidden;">
|
<div class="border text-center rounded h-100 {{ css_class }}" style="overflow: hidden;">
|
||||||
<img itemprop="image" class="website-image h-100 w-100" src="{{ frappe.utils.quoted(website_image or 'no-image.jpg') | abs_url }}">
|
<img itemprop="image" class="website-image h-100 w-100" alt="{{ alt }}" src="{{ frappe.utils.quoted(website_image or 'no-image.jpg') | abs_url }}">
|
||||||
</div>
|
</div>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user