diff --git a/erpnext/accounts/doctype/bank/bank.js b/erpnext/accounts/doctype/bank/bank.js
index 49b2b186c4..059e1d3158 100644
--- a/erpnext/accounts/doctype/bank/bank.js
+++ b/erpnext/accounts/doctype/bank/bank.js
@@ -42,10 +42,9 @@ let add_fields_to_mapping_table = function (frm) {
});
});
- frappe.meta.get_docfield("Bank Transaction Mapping", "bank_transaction_field",
- frm.doc.name).options = options;
-
- frm.fields_dict.bank_transaction_mapping.grid.refresh();
+ frm.fields_dict.bank_transaction_mapping.grid.update_docfield_property(
+ 'bank_transaction_field', 'options', options
+ );
};
erpnext.integrations.refreshPlaidLink = class refreshPlaidLink {
diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.json b/erpnext/accounts/doctype/bank_transaction/bank_transaction.json
index 69ee4971cd..88aa7ef8b5 100644
--- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.json
+++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.json
@@ -175,22 +175,24 @@
},
{
"fieldname": "deposit",
- "oldfieldname": "debit",
"fieldtype": "Currency",
"in_list_view": 1,
- "label": "Deposit"
+ "label": "Deposit",
+ "oldfieldname": "debit",
+ "options": "currency"
},
{
"fieldname": "withdrawal",
- "oldfieldname": "credit",
"fieldtype": "Currency",
"in_list_view": 1,
- "label": "Withdrawal"
+ "label": "Withdrawal",
+ "oldfieldname": "credit",
+ "options": "currency"
}
],
"is_submittable": 1,
"links": [],
- "modified": "2020-12-30 19:40:54.221070",
+ "modified": "2021-04-14 17:31:58.963529",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bank Transaction",
diff --git a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py
index 03c3eb0ac0..f96f59169e 100644
--- a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py
+++ b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py
@@ -293,6 +293,11 @@ def validate_accounts(file_name):
accounts_dict = {}
for account in accounts:
accounts_dict.setdefault(account["account_name"], account)
+ if not hasattr(account, "parent_account"):
+ msg = _("Please make sure the file you are using has 'Parent Account' column present in the header.")
+ msg += "
"
+ msg += _("Alternatively, you can download the template and fill your data in.")
+ frappe.throw(msg, title=_("Parent Account Missing"))
if account["parent_account"] and accounts_dict.get(account["parent_account"]):
accounts_dict[account["parent_account"]]["is_group"] = 1
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js
index 37b03f3f0e..d76641dc9b 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.js
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js
@@ -327,18 +327,16 @@ erpnext.accounts.JournalEntry = frappe.ui.form.Controller.extend({
},
setup_balance_formatter: function() {
- var me = this;
- $.each(["balance", "party_balance"], function(i, field) {
- var df = frappe.meta.get_docfield("Journal Entry Account", field, me.frm.doc.name);
- df.formatter = function(value, df, options, doc) {
- var currency = frappe.meta.get_field_currency(df, doc);
- var dr_or_cr = value ? ('') : "";
- return "
';
-
- // main table
-
- out +='
'; + message += __('You must first use the portal to cancel the e-way bill and then update the cancelled status in the ERPNext system.'); + + frappe.msgprint({ + title: __('Update E-Way Bill Cancelled Status?'), + message: message, + indicator: 'orange', primary_action: function() { - const data = d.get_values(); frappe.call({ method: 'erpnext.regional.india.e_invoice.utils.cancel_eway_bill', - args: { - doctype, - docname: name, - eway_bill: ewaybill, - reason: data.reason.split('-')[0], - remark: data.remark - }, + args: { doctype, docname: name }, freeze: true, - callback: () => frm.reload_doc() || d.hide(), - error: () => d.hide() + callback: () => frm.reload_doc() }); }, - primary_action_label: __('Submit') + primary_action_label: __('Yes') }); - d.show(); }; add_custom_button(__("Cancel E-Way Bill"), action); } @@ -254,7 +235,7 @@ const get_preview_dialog = (frm, action) => { title: __("Preview"), size: "large", fields: [ - { + { "label": "Preview", "fieldname": "preview_html", "fieldtype": "HTML" diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index 3dd1b36fb6..59c098c1ca 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -15,18 +15,43 @@ import traceback import io from frappe import _, bold from pyqrcode import create as qrcreate +from frappe.utils.background_jobs import enqueue +from frappe.utils.scheduler import is_scheduler_inactive +from frappe.core.page.background_jobs.background_jobs import get_info from frappe.integrations.utils import make_post_request, make_get_request from erpnext.regional.india.utils import get_gst_accounts, get_place_of_supply -from frappe.utils.data import cstr, cint, format_date, flt, time_diff_in_seconds, now_datetime, add_to_date, get_link_to_form +from frappe.utils.data import cstr, cint, format_date, flt, time_diff_in_seconds, now_datetime, add_to_date, get_link_to_form, getdate, time_diff_in_hours + +@frappe.whitelist() +def validate_eligibility(doc): + if isinstance(doc, six.string_types): + doc = json.loads(doc) + + invalid_doctype = doc.get('doctype') != 'Sales Invoice' + if invalid_doctype: + return False + + einvoicing_enabled = cint(frappe.db.get_single_value('E Invoice Settings', 'enable')) + if not einvoicing_enabled: + return False + + einvoicing_eligible_from = frappe.db.get_single_value('E Invoice Settings', 'applicable_from') or '2021-04-01' + if getdate(doc.get('posting_date')) < getdate(einvoicing_eligible_from): + return False -def validate_einvoice_fields(doc): - einvoicing_enabled = cint(frappe.db.get_value('E Invoice Settings', 'E Invoice Settings', 'enable')) - invalid_doctype = doc.doctype != 'Sales Invoice' invalid_supply_type = doc.get('gst_category') not in ['Registered Regular', 'SEZ', 'Overseas', 'Deemed Export'] company_transaction = doc.get('billing_address_gstin') == doc.get('company_gstin') no_taxes_applied = not doc.get('taxes') - if not einvoicing_enabled or invalid_doctype or invalid_supply_type or company_transaction or no_taxes_applied: + if invalid_supply_type or company_transaction or no_taxes_applied: + return False + + return True + +def validate_einvoice_fields(doc): + invoice_eligible = validate_eligibility(doc) + + if not invoice_eligible: return if doc.docstatus == 0 and doc._action == 'save': @@ -35,6 +60,8 @@ def validate_einvoice_fields(doc): if len(doc.name) > 16: raise_document_name_too_long_error() + doc.einvoice_status = 'Pending' + elif doc.docstatus == 1 and doc._action == 'submit' and not doc.irn: frappe.throw(_('You must generate IRN before submitting the document.'), title=_('Missing IRN')) @@ -76,6 +103,9 @@ def get_transaction_details(invoice): )) def get_doc_details(invoice): + if getdate(invoice.posting_date) < getdate('2021-01-01'): + frappe.throw(_('IRN generation is not allowed for invoices dated before 1st Jan 2021'), title=_('Not Allowed')) + invoice_type = 'CRN' if invoice.is_return else 'INV' invoice_name = invoice.name @@ -87,56 +117,39 @@ def get_doc_details(invoice): invoice_date=invoice_date )) -def get_party_details(address_name, company_address=None, billing_address=None, shipping_address=None): - d = frappe.get_all('Address', filters={'name': address_name}, fields=['*'])[0] - - if ((not d.gstin and not shipping_address) - or not d.city - or not d.pincode - or not d.address_title - or not d.address_line1 - or not d.gst_state_number): +def validate_address_fields(address, is_shipping_address): + if ((not address.gstin and not is_shipping_address) + or not address.city + or not address.pincode + or not address.address_title + or not address.address_line1 + or not address.gst_state_number): frappe.throw( - msg=_('Address lines, city, pincode, gstin is mandatory for address {}. Please set them and try again.').format( - get_link_to_form('Address', address_name) - ), + msg=_('Address Lines, City, Pincode, GSTIN are mandatory for address {}. Please set them and try again.').format(address.name), title=_('Missing Address Fields') ) - if d.gst_state_number == 97: +def get_party_details(address_name, is_shipping_address=False): + addr = frappe.get_doc('Address', address_name) + + validate_address_fields(addr, is_shipping_address) + + if addr.gst_state_number == 97: # according to einvoice standard - pincode = 999999 + addr.pincode = 999999 party_address_details = frappe._dict(dict( - legal_name=sanitize_for_json(d.address_title), - location=sanitize_for_json(d.city), - pincode=d.pincode, - state_code=d.gst_state_number, - address_line1=sanitize_for_json(d.address_line1), - address_line2=sanitize_for_json(d.address_line2) + legal_name=sanitize_for_json(addr.address_title), + location=sanitize_for_json(addr.city), + pincode=addr.pincode, gstin=addr.gstin, + state_code=addr.gst_state_number, + address_line1=sanitize_for_json(addr.address_line1), + address_line2=sanitize_for_json(addr.address_line2) )) - if d.gstin: - party_address_details.gstin = d.gstin + return party_address_details -def get_gstin_details(gstin): - if not hasattr(frappe.local, 'gstin_cache'): - frappe.local.gstin_cache = {} - - key = gstin - details = frappe.local.gstin_cache.get(key) - if details: - return details - - details = frappe.cache().hget('gstin_cache', key) - if details: - frappe.local.gstin_cache[key] = details - return details - - if not details: - return GSPConnector.get_gstin_details(gstin) - def get_overseas_address_details(address_name): address_title, address_line1, address_line2, city = frappe.db.get_value( 'Address', address_name, ['address_title', 'address_line1', 'address_line2', 'city'] @@ -171,10 +184,15 @@ def get_item_list(invoice): item.description = sanitize_for_json(d.item_name) item.qty = abs(item.qty) - item.discount_amount = 0 - item.unit_rate = abs(item.base_net_amount / item.qty) - item.gross_amount = abs(item.base_net_amount) - item.taxable_value = abs(item.base_net_amount) + + if invoice.apply_discount_on == 'Net Total' and invoice.discount_amount: + item.discount_amount = abs(item.base_amount - item.base_net_amount) + else: + item.discount_amount = 0 + + item.unit_rate = abs((abs(item.taxable_value) - item.discount_amount)/ item.qty) + item.gross_amount = abs(item.taxable_value) + item.discount_amount + item.taxable_value = abs(item.taxable_value) item.batch_expiry_date = frappe.db.get_value('Batch', d.batch_no, 'expiry_date') if d.batch_no else None item.batch_expiry_date = format_date(item.batch_expiry_date, 'dd/mm/yyyy') if item.batch_expiry_date else None @@ -207,11 +225,11 @@ def update_item_taxes(invoice, item): is_applicable = t.tax_amount and t.account_head in gst_accounts_list if is_applicable: # this contains item wise tax rate & tax amount (incl. discount) - item_tax_detail = json.loads(t.item_wise_tax_detail).get(item.item_code) + item_tax_detail = json.loads(t.item_wise_tax_detail).get(item.item_code or item.item_name) item_tax_rate = item_tax_detail[0] # item tax amount excluding discount amount - item_tax_amount = (item_tax_rate / 100) * item.base_net_amount + item_tax_amount = (item_tax_rate / 100) * item.taxable_value if t.account_head in gst_accounts.cess_account: item_tax_amount_after_discount = item_tax_detail[1] @@ -225,6 +243,9 @@ def update_item_taxes(invoice, item): if t.account_head in gst_accounts[f'{tax_type}_account']: item.tax_rate += item_tax_rate item[f'{tax_type}_amount'] += abs(item_tax_amount) + else: + # TODO: other charges per item + pass return item @@ -232,10 +253,14 @@ def get_invoice_value_details(invoice): invoice_value_details = frappe._dict(dict()) if invoice.apply_discount_on == 'Net Total' and invoice.discount_amount: - invoice_value_details.base_total = abs(invoice.base_total) - invoice_value_details.invoice_discount_amt = abs(invoice.base_discount_amount) + # Discount already applied on net total which means on items + invoice_value_details.base_total = abs(sum([i.taxable_value for i in invoice.get('items')])) + invoice_value_details.invoice_discount_amt = 0 + elif invoice.apply_discount_on == 'Grand Total' and invoice.discount_amount: + invoice_value_details.invoice_discount_amt = invoice.base_discount_amount + invoice_value_details.base_total = abs(sum([i.taxable_value for i in invoice.get('items')])) else: - invoice_value_details.base_total = abs(invoice.base_net_total) + invoice_value_details.base_total = abs(sum([i.taxable_value for i in invoice.get('items')])) # since tax already considers discount amount invoice_value_details.invoice_discount_amt = 0 @@ -256,7 +281,11 @@ def update_invoice_taxes(invoice, invoice_value_details): invoice_value_details.total_igst_amt = 0 invoice_value_details.total_cess_amt = 0 invoice_value_details.total_other_charges = 0 + considered_rows = [] + for t in invoice.taxes: + tax_amount = t.base_tax_amount if (invoice.apply_discount_on == 'Grand Total' and invoice.discount_amount) \ + else t.base_tax_amount_after_discount_amount if t.account_head in gst_accounts_list: if t.account_head in gst_accounts.cess_account: # using after discount amt since item also uses after discount amt for cess calc @@ -264,12 +293,26 @@ def update_invoice_taxes(invoice, invoice_value_details): for tax_type in ['igst', 'cgst', 'sgst']: if t.account_head in gst_accounts[f'{tax_type}_account']: - invoice_value_details[f'total_{tax_type}_amt'] += abs(t.base_tax_amount_after_discount_amount) + + invoice_value_details[f'total_{tax_type}_amt'] += abs(tax_amount) + update_other_charges(t, invoice_value_details, gst_accounts_list, invoice, considered_rows) else: - invoice_value_details.total_other_charges += abs(t.base_tax_amount_after_discount_amount) + invoice_value_details.total_other_charges += abs(tax_amount) return invoice_value_details +def update_other_charges(tax_row, invoice_value_details, gst_accounts_list, invoice, considered_rows): + prev_row_id = cint(tax_row.row_id) - 1 + if tax_row.account_head in gst_accounts_list and prev_row_id not in considered_rows: + if tax_row.charge_type == 'On Previous Row Amount': + amount = invoice.get('taxes')[prev_row_id].tax_amount_after_discount_amount + invoice_value_details.total_other_charges -= abs(amount) + considered_rows.append(prev_row_id) + if tax_row.charge_type == 'On Previous Row Total': + amount = invoice.get('taxes')[prev_row_id].base_total - invoice.base_net_total + invoice_value_details.total_other_charges -= abs(amount) + considered_rows.append(prev_row_id) + def get_payment_details(invoice): payee_name = invoice.company mode_of_payment = ', '.join([d.mode_of_payment for d in invoice.payments]) @@ -282,6 +325,10 @@ def get_payment_details(invoice): )) def get_return_doc_reference(invoice): + if not invoice.return_against: + frappe.throw(_('For generating IRN, reference to the original invoice is mandatory for a credit note. Please set {} field to generate e-invoice.') + .format(frappe.bold('Return Against')), title=_('Missing Field')) + invoice_date = frappe.db.get_value('Sales Invoice', invoice.return_against, 'posting_date') return frappe._dict(dict( invoice_name=invoice.return_against, invoice_date=format_date(invoice_date, 'dd/mm/yyyy') @@ -289,7 +336,11 @@ def get_return_doc_reference(invoice): def get_eway_bill_details(invoice): if invoice.is_return: - frappe.throw(_('E-Way Bill cannot be generated for Credit Notes & Debit Notes'), title=_('E Invoice Validation Failed')) + frappe.throw(_('E-Way Bill cannot be generated for Credit Notes & Debit Notes. Please clear fields in the Transporter Section of the invoice.'), + title=_('Invalid Fields')) + + if not invoice.distance: + frappe.throw(_('Distance is mandatory for generating e-way bill for an e-invoice.'), title=_('Missing Field')) mode_of_transport = { '': '', 'Road': '1', 'Air': '2', 'Rail': '3', 'Ship': '4' } vehicle_type = { 'Regular': 'R', 'Over Dimensional Cargo (ODC)': 'O' } @@ -307,9 +358,15 @@ def get_eway_bill_details(invoice): def validate_mandatory_fields(invoice): if not invoice.company_address: - frappe.throw(_('Company Address is mandatory to fetch company GSTIN details.'), title=_('Missing Fields')) + frappe.throw( + _('Company Address is mandatory to fetch company GSTIN details. Please set Company Address and try again.'), + title=_('Missing Fields') + ) if not invoice.customer_address: - frappe.throw(_('Customer Address is mandatory to fetch customer GSTIN details.'), title=_('Missing Fields')) + frappe.throw( + _('Customer Address is mandatory to fetch customer GSTIN details. Please set Company Address and try again.'), + title=_('Missing Fields') + ) if not frappe.db.get_value('Address', invoice.company_address, 'gstin'): frappe.throw( _('GSTIN is mandatory to fetch company GSTIN details. Please enter GSTIN in selected company address.'), @@ -321,6 +378,39 @@ def validate_mandatory_fields(invoice): title=_('Missing Fields') ) +def validate_totals(einvoice): + item_list = einvoice['ItemList'] + value_details = einvoice['ValDtls'] + + total_item_ass_value = 0 + total_item_cgst_value = 0 + total_item_sgst_value = 0 + total_item_igst_value = 0 + total_item_value = 0 + for item in item_list: + total_item_ass_value += flt(item['AssAmt']) + total_item_cgst_value += flt(item['CgstAmt']) + total_item_sgst_value += flt(item['SgstAmt']) + total_item_igst_value += flt(item['IgstAmt']) + total_item_value += flt(item['TotItemVal']) + + if abs(flt(item['AssAmt']) * flt(item['GstRt']) / 100) - (flt(item['CgstAmt']) + flt(item['SgstAmt']) + flt(item['IgstAmt'])) > 1: + frappe.throw(_('Row #{}: GST rate is invalid. Please remove tax rows with zero tax amount from taxes table.').format(item.idx)) + + if abs(flt(value_details['AssVal']) - total_item_ass_value) > 1: + frappe.throw(_('Total Taxable Value of the items is not equal to the Invoice Net Total. Please check item taxes / discounts for any correction.')) + + if abs(flt(value_details['TotInvVal']) + flt(value_details['Discount']) - total_item_value) > 1: + frappe.throw(_('Total Value of the items is not equal to the Invoice Grand Total. Please check item taxes / discounts for any correction.')) + + calculated_invoice_value = \ + flt(value_details['AssVal']) + flt(value_details['CgstVal']) \ + + flt(value_details['SgstVal']) + flt(value_details['IgstVal']) \ + + flt(value_details['OthChrg']) - flt(value_details['Discount']) + + if abs(flt(value_details['TotInvVal']) - calculated_invoice_value) > 1: + frappe.throw(_('Total Item Value + Taxes - Discount is not equal to the Invoice Grand Total. Please check taxes / discounts for any correction.')) + def make_einvoice(invoice): validate_mandatory_fields(invoice) @@ -330,12 +420,12 @@ def make_einvoice(invoice): item_list = get_item_list(invoice) doc_details = get_doc_details(invoice) invoice_value_details = get_invoice_value_details(invoice) - seller_details = get_party_details(invoice.company_address, company_address=1) + seller_details = get_party_details(invoice.company_address) if invoice.gst_category == 'Overseas': buyer_details = get_overseas_address_details(invoice.customer_address) else: - buyer_details = get_party_details(invoice.customer_address, billing_address=1) + buyer_details = get_party_details(invoice.customer_address) place_of_supply = get_place_of_supply(invoice, invoice.doctype) if place_of_supply: place_of_supply = place_of_supply.split('-')[0] @@ -343,20 +433,23 @@ def make_einvoice(invoice): place_of_supply = sanitize_for_json(invoice.billing_address_gstin)[:2] buyer_details.update(dict(place_of_supply=place_of_supply)) + seller_details.update(dict(legal_name=invoice.company)) + buyer_details.update(dict(legal_name=invoice.customer_name or invoice.customer)) + shipping_details = payment_details = prev_doc_details = eway_bill_details = frappe._dict({}) if invoice.shipping_address_name and invoice.customer_address != invoice.shipping_address_name: if invoice.gst_category == 'Overseas': shipping_details = get_overseas_address_details(invoice.shipping_address_name) else: - shipping_details = get_party_details(invoice.shipping_address_name, shipping_address=1) + shipping_details = get_party_details(invoice.shipping_address_name, is_shipping_address=True) if invoice.is_pos and invoice.base_paid_amount: payment_details = get_payment_details(invoice) - if invoice.is_return and invoice.return_against: + if invoice.is_return: 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) # not yet implemented @@ -369,18 +462,73 @@ def make_einvoice(invoice): period_details=period_details, prev_doc_details=prev_doc_details, export_details=export_details, eway_bill_details=eway_bill_details ) - einvoice = safe_json_load(einvoice) - validations = json.loads(read_json('einv_validation')) - errors = validate_einvoice(validations, einvoice) - if errors: - message = "\n".join([ - "E Invoice: ", json.dumps(einvoice, indent=4), - "-" * 50, - "Errors: ", json.dumps(errors, indent=4) - ]) - frappe.log_error(title="E Invoice Validation Failed", message=message) - frappe.throw(errors, title=_('E Invoice Validation Failed'), as_list=1) + try: + einvoice = safe_json_load(einvoice) + einvoice = santize_einvoice_fields(einvoice) + except Exception: + show_link_to_error_log(invoice, einvoice) + + validate_totals(einvoice) + + return einvoice + +def show_link_to_error_log(invoice, einvoice): + err_log = log_error(einvoice) + link_to_error_log = get_link_to_form('Error Log', err_log.name, 'Error Log') + frappe.throw( + _('An error occurred while creating e-invoice for {}. Please check {} for more information.').format( + invoice.name, link_to_error_log), + title=_('E Invoice Creation Failed') + ) + +def log_error(data=None): + if isinstance(data, six.string_types): + 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 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: + einvoice.pop(key, None) + + elif not value or value == "None": + einvoice.pop(key, None) + + elif key in float_fields: + einvoice[key] = flt(value, 2) + + elif key in int_fields: + einvoice[key] = cint(value) return einvoice @@ -396,72 +544,22 @@ def safe_json_load(json_string): snippet = json_string[start:end] frappe.throw(_("Error in input data. Please check for any special characters near following input: {}").format(snippet)) -def validate_einvoice(validations, einvoice, errors=None): - if errors is None: - errors = [] - for fieldname, field_validation in validations.items(): - value = einvoice.get(fieldname, None) - if not value or value == "None": - # remove keys with empty values - einvoice.pop(fieldname, None) - continue - - value_type = field_validation.get("type").lower() - if value_type in ['object', 'array']: - child_validations = field_validation.get('properties') - - if isinstance(value, list): - for d in value: - validate_einvoice(child_validations, d, errors) - if not d: - # remove empty dicts - einvoice.pop(fieldname, None) - else: - validate_einvoice(child_validations, value, errors) - if not value: - # remove empty dicts - einvoice.pop(fieldname, None) - continue - - # convert to int or str - if value_type == 'string': - 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') - minimum = flt(field_validation.get('minimum')) - 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 - - if value_type == 'string' and len(value) > max_length: - errors.append(_('{} should not exceed {} characters').format(label, max_length)) - if value_type == 'number' and (value > maximum or value < minimum): - errors.append(_('{} {} should be between {} and {}').format(label, value, minimum, maximum)) - if pattern_str and not pattern.match(value): - errors.append(field_validation.get('validationMsg')) - - return errors - -class RequestFailed(Exception): pass +class RequestFailed(Exception): + pass +class CancellationNotAllowed(Exception): + pass class GSPConnector(): def __init__(self, doctype=None, docname=None): - self.e_invoice_settings = frappe.get_cached_doc('E Invoice Settings') - sandbox_mode = self.e_invoice_settings.sandbox_mode + self.doctype = doctype + self.docname = docname - self.invoice = frappe.get_cached_doc(doctype, docname) if doctype and docname else None - self.credentials = self.get_credentials() + self.set_invoice() + self.set_credentials() # authenticate url is same for sandbox & live self.authenticate_url = 'https://gsp.adaequare.com/gsp/authenticate?grant_type=token' - self.base_url = 'https://gsp.adaequare.com' if not sandbox_mode else 'https://gsp.adaequare.com/test' + self.base_url = 'https://gsp.adaequare.com' if not self.e_invoice_settings.sandbox_mode else 'https://gsp.adaequare.com/test' self.cancel_irn_url = self.base_url + '/enriched/ei/api/invoice/cancel' self.irn_details_url = self.base_url + '/enriched/ei/api/invoice/irn' @@ -470,18 +568,29 @@ class GSPConnector(): self.cancel_ewaybill_url = self.base_url + '/enriched/ewb/ewayapi?action=CANEWB' self.generate_ewaybill_url = self.base_url + '/enriched/ei/api/ewaybill' - def get_credentials(self): + def set_invoice(self): + self.invoice = None + if self.doctype and self.docname: + self.invoice = frappe.get_cached_doc(self.doctype, self.docname) + + def set_credentials(self): + self.e_invoice_settings = frappe.get_cached_doc('E Invoice Settings') + + if not self.e_invoice_settings.enable: + frappe.throw(_("E-Invoicing is disabled. Please enable it from {} to generate e-invoices.").format(get_link_to_form("E Invoice Settings", "E Invoice Settings"))) + if self.invoice: gstin = self.get_seller_gstin() - if not self.e_invoice_settings.enable: - frappe.throw(_("E-Invoicing is disabled. Please enable it from {} to generate e-invoices.").format(get_link_to_form("E Invoice Settings", "E Invoice Settings"))) - 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: - credentials = self.e_invoice_settings.credentials[0] if self.e_invoice_settings.credentials else None - return credentials + self.credentials = self.e_invoice_settings.credentials[0] if self.e_invoice_settings.credentials else None 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: frappe.throw(_('Cannot retrieve Company GSTIN. Please select company address with valid GSTIN.')) return gstin @@ -529,7 +638,7 @@ class GSPConnector(): self.e_invoice_settings.reload() except Exception: - self.log_error(res) + log_error(res) self.raise_error(True) def get_headers(self): @@ -551,16 +660,15 @@ class GSPConnector(): if res.get('success'): return res.get('result') else: - self.log_error(res) + log_error(res) raise RequestFailed except RequestFailed: self.raise_error() except Exception: - self.log_error() + log_error() self.raise_error(True) - @staticmethod def get_gstin_details(gstin): '''fetch and cache GSTIN details''' @@ -576,12 +684,13 @@ class GSPConnector(): return details def generate_irn(self): - headers = self.get_headers() - einvoice = make_einvoice(self.invoice) - data = json.dumps(einvoice, indent=4) - + data = {} try: + headers = self.get_headers() + einvoice = make_einvoice(self.invoice) + data = json.dumps(einvoice, indent=4) res = self.make_request('post', self.generate_irn_url, headers, data) + if res.get('success'): self.set_einvoice_data(res.get('result')) @@ -601,12 +710,36 @@ class GSPConnector(): except RequestFailed: errors = self.sanitize_error_message(res.get('message')) + self.set_failed_status(errors=errors) self.raise_error(errors=errors) - except Exception: - self.log_error(data) + except Exception as e: + self.set_failed_status(errors=str(e)) + log_error(data) self.raise_error(True) + @staticmethod + def bulk_generate_irn(invoices): + gsp_connector = GSPConnector() + gsp_connector.doctype = 'Sales Invoice' + + failed = [] + + for invoice in invoices: + try: + gsp_connector.docname = invoice + gsp_connector.set_invoice() + gsp_connector.set_credentials() + gsp_connector.generate_irn() + + except Exception as e: + failed.append({ + 'docname': invoice, + 'message': str(e) + }) + + return failed + def get_irn_details(self, irn): headers = self.get_headers() @@ -623,21 +756,30 @@ class GSPConnector(): self.raise_error(errors=errors) except Exception: - self.log_error() + log_error() self.raise_error(True) def cancel_irn(self, irn, reason, remark): - headers = self.get_headers() - data = json.dumps({ - 'Irn': irn, - 'Cnlrsn': reason, - 'Cnlrem': remark - }, indent=4) - + data, res = {}, {} try: + # validate cancellation + if time_diff_in_hours(now_datetime(), self.invoice.ack_date) > 24: + frappe.throw(_('E-Invoice cannot be cancelled after 24 hours of IRN generation.'), title=_('Not Allowed'), exc=CancellationNotAllowed) + if not irn: + frappe.throw(_('IRN not found. You must generate IRN before cancelling.'), title=_('Not Allowed'), exc=CancellationNotAllowed) + + headers = self.get_headers() + data = json.dumps({ + 'Irn': irn, + 'Cnlrsn': reason, + 'Cnlrem': remark + }, indent=4) + 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_cancel_date = res.get('result')['CancelDate'] if res.get('result') else "" + self.invoice.einvoice_status = 'Cancelled' self.invoice.flags.updater_reference = { 'doctype': self.invoice.doctype, 'docname': self.invoice.name, @@ -650,12 +792,41 @@ class GSPConnector(): except RequestFailed: errors = self.sanitize_error_message(res.get('message')) + self.set_failed_status(errors=errors) self.raise_error(errors=errors) - except Exception: - self.log_error(data) + except CancellationNotAllowed as e: + self.set_failed_status(errors=str(e)) + self.raise_error(errors=str(e)) + + except Exception as e: + self.set_failed_status(errors=str(e)) + log_error(data) self.raise_error(True) + @staticmethod + def bulk_cancel_irn(invoices, reason, remark): + gsp_connector = GSPConnector() + gsp_connector.doctype = 'Sales Invoice' + + failed = [] + + for invoice in invoices: + try: + gsp_connector.docname = invoice + gsp_connector.set_invoice() + gsp_connector.set_credentials() + irn = gsp_connector.invoice.irn + gsp_connector.cancel_irn(irn, reason, remark) + + except Exception as e: + failed.append({ + 'docname': invoice, + 'message': str(e) + }) + + return failed + def generate_eway_bill(self, **kwargs): args = frappe._dict(kwargs) @@ -694,7 +865,7 @@ class GSPConnector(): self.raise_error(errors=errors) except Exception: - self.log_error(data) + log_error(data) self.raise_error(True) def cancel_eway_bill(self, eway_bill, reason, remark): @@ -726,7 +897,7 @@ class GSPConnector(): self.raise_error(errors=errors) except Exception: - self.log_error(data) + log_error(data) self.raise_error(True) def sanitize_error_message(self, message): @@ -741,6 +912,9 @@ class GSPConnector(): ] then we trim down the message by looping over errors ''' + if not message: + return [] + errors = re.findall(': [^:]+', message) for idx, e in enumerate(errors): # remove colons @@ -752,22 +926,6 @@ class GSPConnector(): 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=[]): title = _('E Invoice Request Failed') if errors: @@ -790,7 +948,10 @@ class GSPConnector(): self.invoice.ack_no = res.get('AckNo') self.invoice.ack_date = res.get('AckDt') self.invoice.signed_einvoice = dec_signed_invoice + self.invoice.ack_no = res.get('AckNo') + self.invoice.ack_date = res.get('AckDt') self.invoice.signed_qr_code = res.get('SignedQRCode') + self.invoice.einvoice_status = 'Generated' self.attach_qrcode_image() @@ -800,7 +961,6 @@ class GSPConnector(): 'label': _('IRN Generated') } self.update_invoice() - def attach_qrcode_image(self): qrcode = self.invoice.signed_qr_code doctype = self.invoice.doctype @@ -827,6 +987,17 @@ class GSPConnector(): self.invoice.flags.ignore_validate = True self.invoice.save() + def set_failed_status(self, errors=None): + frappe.db.rollback() + self.invoice.einvoice_status = 'Failed' + self.invoice.failure_description = self.get_failure_message(errors) if errors else "" + self.update_invoice() + frappe.db.commit() + + def get_failure_message(self, errors): + if isinstance(errors, list): + errors = ', '.join(errors) + return errors def sanitize_for_json(string): """Escape JSON specific characters from a string.""" @@ -856,5 +1027,114 @@ def generate_eway_bill(doctype, docname, **kwargs): @frappe.whitelist() def cancel_eway_bill(doctype, docname, eway_bill, reason, remark): - gsp_connector = GSPConnector(doctype, docname) - gsp_connector.cancel_eway_bill(eway_bill, reason, remark) + # TODO: uncomment when eway_bill api from Adequare is enabled + # 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) + +@frappe.whitelist() +def generate_einvoices(docnames): + docnames = json.loads(docnames) or [] + + if len(docnames) < 10: + failures = GSPConnector.bulk_generate_irn(docnames) + frappe.local.message_log = [] + + if failures: + show_bulk_action_failure_message(failures) + + success = len(docnames) - len(failures) + frappe.msgprint( + _('{} e-invoices generated successfully').format(success), + title=_('Bulk E-Invoice Generation Complete') + ) + + else: + enqueue_bulk_action(schedule_bulk_generate_irn, docnames=docnames) + +def schedule_bulk_generate_irn(docnames): + failures = GSPConnector.bulk_generate_irn(docnames) + frappe.local.message_log = [] + + frappe.publish_realtime("bulk_einvoice_generation_complete", { + "user": frappe.session.user, + "failures": failures, + "invoices": docnames + }) + +def show_bulk_action_failure_message(failures): + for doc in failures: + docname = '{0}'.format(doc.get('docname')) + message = doc.get('message').replace("'", '"') + if message[0] == '[': + errors = json.loads(message) + error_list = ''.join([' +
Sent via
ERPNext """
@@ -29,6 +31,7 @@ def after_install():
add_company_to_session_defaults()
add_standard_navbar_items()
add_app_name()
+ add_non_standard_user_types()
frappe.db.commit()
@@ -164,3 +167,81 @@ def add_standard_navbar_items():
def add_app_name():
frappe.db.set_value('System Settings', None, 'app_name', 'ERPNext')
+
+def add_non_standard_user_types():
+ user_types = get_user_types_data()
+
+ user_type_limit = {}
+ for user_type, data in iteritems(user_types):
+ user_type_limit.setdefault(frappe.scrub(user_type), 10)
+
+ update_site_config('user_type_doctype_limit', user_type_limit)
+
+ for user_type, data in iteritems(user_types):
+ create_custom_role(data)
+ create_user_type(user_type, data)
+
+def get_user_types_data():
+ return {
+ 'Employee Self Service': {
+ 'role': 'Employee Self Service',
+ 'apply_user_permission_on': 'Employee',
+ 'user_id_field': 'user_id',
+ 'doctypes': {
+ 'Salary Slip': ['read'],
+ 'Employee': ['read', 'write'],
+ 'Expense Claim': ['read', 'write', 'create', 'delete'],
+ 'Leave Application': ['read', 'write', 'create', 'delete'],
+ 'Attendance Request': ['read', 'write', 'create', 'delete'],
+ 'Compensatory Leave Request': ['read', 'write', 'create', 'delete'],
+ 'Employee Tax Exemption Declaration': ['read', 'write', 'create', 'delete'],
+ 'Employee Tax Exemption Proof Submission': ['read', 'write', 'create', 'delete'],
+ 'Timesheet': ['read', 'write', 'create', 'delete', 'submit', 'cancel', 'amend']
+ }
+ }
+ }
+
+def create_custom_role(data):
+ if data.get('role') and not frappe.db.exists('Role', data.get('role')):
+ frappe.get_doc({
+ 'doctype': 'Role',
+ 'role_name': data.get('role'),
+ 'desk_access': 1,
+ 'is_custom': 1
+ }).insert(ignore_permissions=True)
+
+def create_user_type(user_type, data):
+ if frappe.db.exists('User Type', user_type):
+ doc = frappe.get_cached_doc('User Type', user_type)
+ doc.user_doctypes = []
+ else:
+ doc = frappe.new_doc('User Type')
+ doc.update({
+ 'name': user_type,
+ 'role': data.get('role'),
+ 'user_id_field': data.get('user_id_field'),
+ 'apply_user_permission_on': data.get('apply_user_permission_on')
+ })
+
+ create_role_permissions_for_doctype(doc, data)
+ doc.save(ignore_permissions=True)
+
+def create_role_permissions_for_doctype(doc, data):
+ for doctype, perms in iteritems(data.get('doctypes')):
+ args = {'document_type': doctype}
+ for perm in perms:
+ args[perm] = 1
+
+ doc.append('user_doctypes', args)
+
+def update_select_perm_after_install():
+ if not frappe.flags.update_select_perm_after_migrate:
+ return
+
+ frappe.flags.ignore_select_perm = False
+ for row in frappe.get_all('User Type', filters= {'is_standard': 0}):
+ print('Updating user type :- ', row.name)
+ doc = frappe.get_doc('User Type', row.name)
+ doc.save()
+
+ frappe.flags.update_select_perm_after_migrate = False
diff --git a/erpnext/shopping_cart/cart.py b/erpnext/shopping_cart/cart.py
index 681d161edc..8515db3300 100644
--- a/erpnext/shopping_cart/cart.py
+++ b/erpnext/shopping_cart/cart.py
@@ -112,9 +112,7 @@ def place_order():
def request_for_quotation():
quotation = _get_cart_quotation()
quotation.flags.ignore_permissions = True
- quotation.save()
- if not get_shopping_cart_settings().save_quotations_as_draft:
- quotation.submit()
+ quotation.submit()
return quotation.name
@frappe.whitelist()
diff --git a/erpnext/stock/dashboard/item_dashboard.js b/erpnext/stock/dashboard/item_dashboard.js
index 95cb92b1b3..933ca8ab3d 100644
--- a/erpnext/stock/dashboard/item_dashboard.js
+++ b/erpnext/stock/dashboard/item_dashboard.js
@@ -1,14 +1,14 @@
frappe.provide('erpnext.stock');
erpnext.stock.ItemDashboard = Class.extend({
- init: function(opts) {
+ init: function (opts) {
$.extend(this, opts);
this.make();
},
- make: function() {
+ make: function () {
var me = this;
this.start = 0;
- if(!this.sort_by) {
+ if (!this.sort_by) {
this.sort_by = 'projected_qty';
this.sort_order = 'asc';
}
@@ -16,22 +16,25 @@ erpnext.stock.ItemDashboard = Class.extend({
this.content = $(frappe.render_template('item_dashboard')).appendTo(this.parent);
this.result = this.content.find('.result');
- this.content.on('click', '.btn-move', function() {
- handle_move_add($(this), "Move")
+ this.content.on('click', '.btn-move', function () {
+ handle_move_add($(this), "Move");
});
- this.content.on('click', '.btn-add', function() {
- handle_move_add($(this), "Add")
+ this.content.on('click', '.btn-add', function () {
+ handle_move_add($(this), "Add");
});
- this.content.on('click', '.btn-edit', function() {
+ this.content.on('click', '.btn-edit', function () {
let item = unescape($(this).attr('data-item'));
let warehouse = unescape($(this).attr('data-warehouse'));
let company = unescape($(this).attr('data-company'));
- frappe.db.get_value('Putaway Rule',
- {'item_code': item, 'warehouse': warehouse, 'company': company}, 'name', (r) => {
- frappe.set_route("Form", "Putaway Rule", r.name);
- });
+ frappe.db.get_value('Putaway Rule', {
+ 'item_code': item,
+ 'warehouse': warehouse,
+ 'company': company
+ }, 'name', (r) => {
+ frappe.set_route("Form", "Putaway Rule", r.name);
+ });
});
function handle_move_add(element, action) {
@@ -39,23 +42,26 @@ erpnext.stock.ItemDashboard = Class.extend({
let warehouse = unescape(element.attr('data-warehouse'));
let actual_qty = unescape(element.attr('data-actual_qty'));
let disable_quick_entry = Number(unescape(element.attr('data-disable_quick_entry')));
- let entry_type = action === "Move" ? "Material Transfer": null;
+ let entry_type = action === "Move" ? "Material Transfer" : null;
if (disable_quick_entry) {
open_stock_entry(item, warehouse, entry_type);
} else {
if (action === "Add") {
let rate = unescape($(this).attr('data-rate'));
- erpnext.stock.move_item(item, null, warehouse, actual_qty, rate, function() { me.refresh(); });
- }
- else {
- erpnext.stock.move_item(item, warehouse, null, actual_qty, null, function() { me.refresh(); });
+ erpnext.stock.move_item(item, null, warehouse, actual_qty, rate, function () {
+ me.refresh();
+ });
+ } else {
+ erpnext.stock.move_item(item, warehouse, null, actual_qty, null, function () {
+ me.refresh();
+ });
}
}
}
function open_stock_entry(item, warehouse, entry_type) {
- frappe.model.with_doctype('Stock Entry', function() {
+ frappe.model.with_doctype('Stock Entry', function () {
var doc = frappe.model.get_new_doc('Stock Entry');
if (entry_type) doc.stock_entry_type = entry_type;
@@ -64,18 +70,18 @@ erpnext.stock.ItemDashboard = Class.extend({
row.s_warehouse = warehouse;
frappe.set_route('Form', doc.doctype, doc.name);
- })
+ });
}
// more
- this.content.find('.btn-more').on('click', function() {
+ this.content.find('.btn-more').on('click', function () {
me.start += me.page_length;
me.refresh();
});
},
- refresh: function() {
- if(this.before_refresh) {
+ refresh: function () {
+ if (this.before_refresh) {
this.before_refresh();
}
@@ -94,13 +100,13 @@ erpnext.stock.ItemDashboard = Class.extend({
frappe.call({
method: this.method,
args: args,
- callback: function(r) {
+ callback: function (r) {
me.render(r.message);
}
});
},
- render: function(data) {
- if (this.start===0) {
+ render: function (data) {
+ if (this.start === 0) {
this.max_count = 0;
this.result.empty();
}
@@ -115,7 +121,7 @@ erpnext.stock.ItemDashboard = Class.extend({
this.max_count = this.max_count;
// show more button
- if (data && data.length===(this.page_length + 1)) {
+ if (data && data.length === (this.page_length + 1)) {
this.content.find('.more').removeClass('hidden');
// remove the last element
@@ -137,15 +143,15 @@ erpnext.stock.ItemDashboard = Class.extend({
}
},
- get_item_dashboard_data: function(data, max_count, show_item) {
- if(!max_count) max_count = 0;
- if(!data) data = [];
+ get_item_dashboard_data: function (data, max_count, show_item) {
+ if (!max_count) max_count = 0;
+ if (!data) data = [];
- data.forEach(function(d) {
+ data.forEach(function (d) {
d.actual_or_pending = d.projected_qty + d.reserved_qty + d.reserved_qty_for_production + d.reserved_qty_for_sub_contract;
d.pending_qty = 0;
d.total_reserved = d.reserved_qty + d.reserved_qty_for_production + d.reserved_qty_for_sub_contract;
- if(d.actual_or_pending > d.actual_qty) {
+ if (d.actual_or_pending > d.actual_qty) {
d.pending_qty = d.actual_or_pending - d.actual_qty;
}
@@ -161,16 +167,16 @@ erpnext.stock.ItemDashboard = Class.extend({
return {
data: data,
max_count: max_count,
- can_write:can_write,
+ can_write: can_write,
show_item: show_item || false
};
},
- get_capacity_dashboard_data: function(data) {
+ get_capacity_dashboard_data: function (data) {
if (!data) data = [];
- data.forEach(function(d) {
- d.color = d.percent_occupied >=80 ? "#f8814f" : "#2490ef";
+ data.forEach(function (d) {
+ d.color = d.percent_occupied >= 80 ? "#f8814f" : "#2490ef";
});
let can_write = 0;
@@ -185,53 +191,77 @@ erpnext.stock.ItemDashboard = Class.extend({
}
});
-erpnext.stock.move_item = function(item, source, target, actual_qty, rate, callback) {
+erpnext.stock.move_item = function (item, source, target, actual_qty, rate, callback) {
var dialog = new frappe.ui.Dialog({
title: target ? __('Add Item') : __('Move Item'),
- fields: [
- {fieldname: 'item_code', label: __('Item'),
- fieldtype: 'Link', options: 'Item', read_only: 1},
- {fieldname: 'source', label: __('Source Warehouse'),
- fieldtype: 'Link', options: 'Warehouse', read_only: 1},
- {fieldname: 'target', label: __('Target Warehouse'),
- fieldtype: 'Link', options: 'Warehouse', reqd: 1},
- {fieldname: 'qty', label: __('Quantity'), reqd: 1,
- fieldtype: 'Float', description: __('Available {0}', [actual_qty]) },
- {fieldname: 'rate', label: __('Rate'), fieldtype: 'Currency', hidden: 1 },
+ fields: [{
+ fieldname: 'item_code',
+ label: __('Item'),
+ fieldtype: 'Link',
+ options: 'Item',
+ read_only: 1
+ },
+ {
+ fieldname: 'source',
+ label: __('Source Warehouse'),
+ fieldtype: 'Link',
+ options: 'Warehouse',
+ read_only: 1
+ },
+ {
+ fieldname: 'target',
+ label: __('Target Warehouse'),
+ fieldtype: 'Link',
+ options: 'Warehouse',
+ reqd: 1
+ },
+ {
+ fieldname: 'qty',
+ label: __('Quantity'),
+ reqd: 1,
+ fieldtype: 'Float',
+ description: __('Available {0}', [actual_qty])
+ },
+ {
+ fieldname: 'rate',
+ label: __('Rate'),
+ fieldtype: 'Currency',
+ hidden: 1
+ },
],
- })
+ });
dialog.show();
dialog.get_field('item_code').set_input(item);
- if(source) {
+ if (source) {
dialog.get_field('source').set_input(source);
} else {
dialog.get_field('source').df.hidden = 1;
dialog.get_field('source').refresh();
}
- if(rate) {
+ if (rate) {
dialog.get_field('rate').set_value(rate);
dialog.get_field('rate').df.hidden = 0;
dialog.get_field('rate').refresh();
}
- if(target) {
+ if (target) {
dialog.get_field('target').df.read_only = 1;
dialog.get_field('target').value = target;
dialog.get_field('target').refresh();
}
- dialog.set_primary_action(__('Submit'), function() {
+ dialog.set_primary_action(__('Submit'), function () {
var values = dialog.get_values();
- if(!values) {
+ if (!values) {
return;
}
- if(source && values.qty > actual_qty) {
+ if (source && values.qty > actual_qty) {
frappe.msgprint(__('Quantity must be less than or equal to {0}', [actual_qty]));
return;
}
- if(values.source === values.target) {
+ if (values.source === values.target) {
frappe.msgprint(__('Source and target warehouse must be different'));
}
@@ -239,21 +269,21 @@ erpnext.stock.move_item = function(item, source, target, actual_qty, rate, callb
method: 'erpnext.stock.doctype.stock_entry.stock_entry_utils.make_stock_entry',
args: values,
freeze: true,
- callback: function(r) {
+ callback: function (r) {
frappe.show_alert(__('Stock Entry {0} created',
- ['' + r.message.name+ '']));
+ ['' + r.message.name + '']));
dialog.hide();
callback(r);
},
});
});
- $('')
+ $('')
.appendTo(dialog.body)
.find('.link-open')
- .on('click', function() {
- frappe.model.with_doctype('Stock Entry', function() {
+ .on('click', function () {
+ frappe.model.with_doctype('Stock Entry', function () {
var doc = frappe.model.get_new_doc('Stock Entry');
doc.from_warehouse = dialog.get_value('source');
doc.to_warehouse = dialog.get_value('target');
@@ -266,6 +296,6 @@ erpnext.stock.move_item = function(item, source, target, actual_qty, rate, callb
row.transfer_qty = dialog.get_value('qty');
row.basic_rate = dialog.get_value('rate');
frappe.set_route('Form', doc.doctype, doc.name);
- })
+ });
});
-}
+};
diff --git a/erpnext/stock/dashboard/item_dashboard.py b/erpnext/stock/dashboard/item_dashboard.py
index cafb5c3a0a..45e662807a 100644
--- a/erpnext/stock/dashboard/item_dashboard.py
+++ b/erpnext/stock/dashboard/item_dashboard.py
@@ -2,6 +2,7 @@ from __future__ import unicode_literals
import frappe
from frappe.model.db_query import DatabaseQuery
+from frappe.utils import flt, cint
@frappe.whitelist()
def get_data(item_code=None, warehouse=None, item_group=None,
@@ -42,11 +43,20 @@ def get_data(item_code=None, warehouse=None, item_group=None,
limit_start=start,
limit_page_length='21')
+ precision = cint(frappe.db.get_single_value("System Settings", "float_precision"))
+
for item in items:
item.update({
- 'item_name': frappe.get_cached_value("Item", item.item_code, 'item_name'),
- 'disable_quick_entry': frappe.get_cached_value("Item", item.item_code, 'has_batch_no')
- or frappe.get_cached_value("Item", item.item_code, 'has_serial_no'),
+ 'item_name': frappe.get_cached_value(
+ "Item", item.item_code, 'item_name'),
+ 'disable_quick_entry': frappe.get_cached_value(
+ "Item", item.item_code, 'has_batch_no')
+ or frappe.get_cached_value(
+ "Item", item.item_code, 'has_serial_no'),
+ 'projected_qty': flt(item.projected_qty, precision),
+ 'reserved_qty': flt(item.reserved_qty, precision),
+ 'reserved_qty_for_production': flt(item.reserved_qty_for_production, precision),
+ 'reserved_qty_for_sub_contract': flt(item.reserved_qty_for_sub_contract, precision),
+ 'actual_qty': flt(item.actual_qty, precision),
})
-
return items
diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py
index 36d0de1e5d..e0b89d8e45 100644
--- a/erpnext/stock/doctype/item/test_item.py
+++ b/erpnext/stock/doctype/item/test_item.py
@@ -494,7 +494,8 @@ def make_item_variant():
test_records = frappe.get_test_records('Item')
-def create_item(item_code, is_stock_item=None, valuation_rate=0, warehouse=None, is_customer_provided_item=None, customer=None, is_purchase_item=None, opening_stock=None):
+def create_item(item_code, is_stock_item=None, valuation_rate=0, warehouse=None, is_customer_provided_item=None,
+ customer=None, is_purchase_item=None, opening_stock=None, company=None):
if not frappe.db.exists("Item", item_code):
item = frappe.new_doc("Item")
item.item_code = item_code
@@ -509,7 +510,7 @@ def create_item(item_code, is_stock_item=None, valuation_rate=0, warehouse=None,
item.customer = customer or ''
item.append("item_defaults", {
"default_warehouse": warehouse or '_Test Warehouse - _TC',
- "company": "_Test Company"
+ "company": company or "_Test Company"
})
item.save()
else:
diff --git a/erpnext/stock/doctype/item/test_records.json b/erpnext/stock/doctype/item/test_records.json
index c1f20a47b7..6cec85288f 100644
--- a/erpnext/stock/doctype/item/test_records.json
+++ b/erpnext/stock/doctype/item/test_records.json
@@ -59,6 +59,8 @@
"show_in_website": 1,
"website_warehouse": "_Test Warehouse - _TC",
"gst_hsn_code": "999800",
+ "opening_stock": 10,
+ "valuation_rate": 100,
"item_defaults": [{
"company": "_Test Company",
"default_warehouse": "_Test Warehouse - _TC",
diff --git a/erpnext/stock/doctype/item_variant_settings/item_variant_settings.js b/erpnext/stock/doctype/item_variant_settings/item_variant_settings.js
index 24f7e31a0c..e8fb34732f 100644
--- a/erpnext/stock/doctype/item_variant_settings/item_variant_settings.js
+++ b/erpnext/stock/doctype/item_variant_settings/item_variant_settings.js
@@ -15,8 +15,9 @@ frappe.ui.form.on('Item Variant Settings', {
}
});
- const child = frappe.meta.get_docfield("Variant Field", "field_name", frm.doc.name);
- child.options = allow_fields;
+ frm.fields_dict.fields.grid.update_docfield_property(
+ 'field_name', 'options', allow_fields
+ );
});
}
});
diff --git a/erpnext/stock/doctype/packing_slip/packing_slip.js b/erpnext/stock/doctype/packing_slip/packing_slip.js
index bd14e5f616..40d46852d0 100644
--- a/erpnext/stock/doctype/packing_slip/packing_slip.js
+++ b/erpnext/stock/doctype/packing_slip/packing_slip.js
@@ -110,19 +110,4 @@ cur_frm.cscript.calc_net_total_pkg = function(doc, ps_detail) {
refresh_many(['net_weight_pkg', 'net_weight_uom', 'gross_weight_uom', 'gross_weight_pkg']);
}
-var make_row = function(title,val,bold){
- var bstart = ''; var bend = '';
- return ' | |||||
'+(bold?bstart:'')+title+(bold?bend:'')+' | ' - +''+ val +' | ' - +'