Merge branch 'develop' into purchase-invoice-to-purchase-receipt-develop

This commit is contained in:
Nabin Hait 2021-04-14 11:23:34 +05:30 committed by GitHub
commit 1bc65ddbe4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
129 changed files with 2329 additions and 743 deletions

View File

@ -5,7 +5,7 @@ import frappe
from erpnext.hooks import regional_overrides
from frappe.utils import getdate
__version__ = '13.0.0-dev'
__version__ = '13.0.1'
def get_default_company(user=None):
'''Get default company for user'''

View File

@ -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 {

View File

@ -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 ? ('<label>' + (value > 0.0 ? __("Dr") : __("Cr")) + '</label>') : "";
return "<div style='text-align: right'>"
+ ((value==null || value==="") ? "" : format_currency(Math.abs(value), currency))
+ " " + dr_or_cr
+ "</div>";
}
})
const formatter = function(value, df, options, doc) {
var currency = frappe.meta.get_field_currency(df, doc);
var dr_or_cr = value ? ('<label>' + (value > 0.0 ? __("Dr") : __("Cr")) + '</label>') : "";
return "<div style='text-align: right'>"
+ ((value==null || value==="") ? "" : format_currency(Math.abs(value), currency))
+ " " + dr_or_cr
+ "</div>";
};
this.frm.fields_dict.accounts.grid.update_docfield_property('balance', 'formatter', formatter);
this.frm.fields_dict.accounts.grid.update_docfield_property('party_balance', 'formatter', formatter);
},
reference_name: function(doc, cdt, cdn) {
@ -431,15 +429,6 @@ cur_frm.cscript.validate = function(doc,cdt,cdn) {
cur_frm.cscript.update_totals(doc);
}
cur_frm.cscript.select_print_heading = function(doc,cdt,cdn){
if(doc.select_print_heading){
// print heading
cur_frm.pformat.print_heading = doc.select_print_heading;
}
else
cur_frm.pformat.print_heading = __("Journal Entry");
}
frappe.ui.form.on("Journal Entry Account", {
party: function(frm, cdt, cdn) {
var d = frappe.get_doc(cdt, cdn);
@ -511,8 +500,11 @@ $.extend(erpnext.journal_entry, {
};
$.each(field_label_map, function (fieldname, label) {
var df = frappe.meta.get_docfield("Journal Entry Account", fieldname, frm.doc.name);
df.label = frm.doc.multi_currency ? (label + " in Account Currency") : label;
frm.fields_dict.accounts.grid.update_docfield_property(
fieldname,
'label',
frm.doc.multi_currency ? (label + " in Account Currency") : label
);
})
},

View File

@ -280,7 +280,7 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2020-06-24 14:06:54.833738",
"modified": "2020-06-26 14:06:54.833738",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Journal Entry Account",

View File

@ -234,8 +234,9 @@ erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.ext
});
if (invoices) {
frappe.meta.get_docfield("Payment Reconciliation Payment", "invoice_number",
me.frm.doc.name).options = "\n" + invoices.join("\n");
this.frm.fields_dict.payment.grid.update_docfield_property(
'invoice_number', 'options', "\n" + invoices.join("\n")
);
$.each(me.frm.doc.payments || [], function(i, p) {
if(!in_list(invoices, cstr(p.invoice_number))) p.invoice_number = null;

View File

@ -108,7 +108,6 @@ class POSInvoice(SalesInvoice):
filters = { "item_code": d.item_code, "warehouse": d.warehouse }
if d.batch_no:
filters["batch_no"] = d.batch_no
reserved_serial_nos = get_pos_reserved_serial_nos(filters)
serial_nos = get_serial_nos(d.serial_no)
invalid_serial_nos = [s for s in serial_nos if s in reserved_serial_nos]

View File

@ -12,6 +12,7 @@ from frappe.utils.background_jobs import enqueue
from frappe.model.mapper import map_doc, map_child_doc
from frappe.utils.scheduler import is_scheduler_inactive
from frappe.core.page.background_jobs.background_jobs import get_info
import json
from six import iteritems
@ -78,8 +79,11 @@ class POSInvoiceMergeLog(Document):
sales_invoice = self.merge_pos_invoice_into(sales_invoice, data)
sales_invoice.is_consolidated = 1
sales_invoice.set_posting_time = 1
sales_invoice.posting_date = getdate(self.posting_date)
sales_invoice.save()
sales_invoice.submit()
self.consolidated_invoice = sales_invoice.name
return sales_invoice.name
@ -91,10 +95,13 @@ class POSInvoiceMergeLog(Document):
credit_note = self.merge_pos_invoice_into(credit_note, data)
credit_note.is_consolidated = 1
credit_note.set_posting_time = 1
credit_note.posting_date = getdate(self.posting_date)
# TODO: return could be against multiple sales invoice which could also have been consolidated?
# credit_note.return_against = self.consolidated_invoice
credit_note.save()
credit_note.submit()
self.consolidated_credit_note = credit_note.name
return credit_note.name
@ -131,12 +138,14 @@ class POSInvoiceMergeLog(Document):
if t.account_head == tax.account_head and t.cost_center == tax.cost_center:
t.tax_amount = flt(t.tax_amount) + flt(tax.tax_amount_after_discount_amount)
t.base_tax_amount = flt(t.base_tax_amount) + flt(tax.base_tax_amount_after_discount_amount)
update_item_wise_tax_detail(t, tax)
found = True
if not found:
tax.charge_type = 'Actual'
tax.included_in_print_rate = 0
tax.tax_amount = tax.tax_amount_after_discount_amount
tax.base_tax_amount = tax.base_tax_amount_after_discount_amount
tax.item_wise_tax_detail = tax.item_wise_tax_detail
taxes.append(tax)
for payment in doc.get('payments'):
@ -168,11 +177,9 @@ class POSInvoiceMergeLog(Document):
sales_invoice = frappe.new_doc('Sales Invoice')
sales_invoice.customer = self.customer
sales_invoice.is_pos = 1
# date can be pos closing date?
sales_invoice.posting_date = getdate(nowdate())
return sales_invoice
def update_pos_invoices(self, invoice_docs, sales_invoice='', credit_note=''):
for doc in invoice_docs:
doc.load_from_db()
@ -187,6 +194,26 @@ class POSInvoiceMergeLog(Document):
si.flags.ignore_validate = True
si.cancel()
def update_item_wise_tax_detail(consolidate_tax_row, tax_row):
consolidated_tax_detail = json.loads(consolidate_tax_row.item_wise_tax_detail)
tax_row_detail = json.loads(tax_row.item_wise_tax_detail)
if not consolidated_tax_detail:
consolidated_tax_detail = {}
for item_code, tax_data in tax_row_detail.items():
if consolidated_tax_detail.get(item_code):
consolidated_tax_data = consolidated_tax_detail.get(item_code)
consolidated_tax_detail.update({
item_code: [consolidated_tax_data[0], consolidated_tax_data[1] + tax_data[1]]
})
else:
consolidated_tax_detail.update({
item_code: [tax_data[0], tax_data[1]]
})
consolidate_tax_row.item_wise_tax_detail = json.dumps(consolidated_tax_detail, separators=(',', ':'))
def get_all_unconsolidated_invoices():
filters = {
'consolidated_invoice': [ 'in', [ '', None ]],
@ -214,7 +241,7 @@ def consolidate_pos_invoices(pos_invoices=[], closing_entry={}):
if len(invoices) >= 5 and closing_entry:
closing_entry.set_status(update=True, status='Queued')
enqueue_job(create_merge_logs, invoice_by_customer, closing_entry)
enqueue_job(create_merge_logs, invoice_by_customer=invoice_by_customer, closing_entry=closing_entry)
else:
create_merge_logs(invoice_by_customer, closing_entry)
@ -227,21 +254,21 @@ def unconsolidate_pos_invoices(closing_entry):
if len(merge_logs) >= 5:
closing_entry.set_status(update=True, status='Queued')
enqueue_job(cancel_merge_logs, merge_logs, closing_entry)
enqueue_job(cancel_merge_logs, merge_logs=merge_logs, closing_entry=closing_entry)
else:
cancel_merge_logs(merge_logs, closing_entry)
def create_merge_logs(invoice_by_customer, closing_entry={}):
for customer, invoices in iteritems(invoice_by_customer):
merge_log = frappe.new_doc('POS Invoice Merge Log')
merge_log.posting_date = getdate(nowdate())
merge_log.posting_date = getdate(closing_entry.get('posting_date'))
merge_log.customer = customer
merge_log.pos_closing_entry = closing_entry.get('name', None)
merge_log.set('pos_invoices', invoices)
merge_log.save(ignore_permissions=True)
merge_log.submit()
if closing_entry:
closing_entry.set_status(update=True, status='Submitted')
closing_entry.update_opening_entry()
@ -256,7 +283,7 @@ def cancel_merge_logs(merge_logs, closing_entry={}):
closing_entry.set_status(update=True, status='Cancelled')
closing_entry.update_opening_entry(for_cancel=True)
def enqueue_job(job, invoice_by_customer, closing_entry):
def enqueue_job(job, merge_logs=None, invoice_by_customer=None, closing_entry=None):
check_scheduler_status()
job_name = closing_entry.get("name")
@ -269,6 +296,7 @@ def enqueue_job(job, invoice_by_customer, closing_entry):
job_name=job_name,
closing_entry=closing_entry,
invoice_by_customer=invoice_by_customer,
merge_logs=merge_logs,
now=frappe.conf.developer_mode or frappe.flags.in_test
)

View File

@ -5,6 +5,7 @@ from __future__ import unicode_literals
import frappe
import unittest
import json
from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import create_pos_invoice
from erpnext.accounts.doctype.pos_invoice.pos_invoice import make_sales_return
from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import consolidate_pos_invoices
@ -99,4 +100,51 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
frappe.db.sql("delete from `tabPOS Profile`")
frappe.db.sql("delete from `tabPOS Invoice`")
def test_consolidated_invoice_item_taxes(self):
frappe.db.sql("delete from `tabPOS Invoice`")
try:
inv = create_pos_invoice(qty=1, rate=100, do_not_save=True)
inv.append("taxes", {
"account_head": "_Test Account VAT - _TC",
"charge_type": "On Net Total",
"cost_center": "_Test Cost Center - _TC",
"description": "VAT",
"doctype": "Sales Taxes and Charges",
"rate": 9
})
inv.insert()
inv.submit()
inv2 = create_pos_invoice(qty=1, rate=100, do_not_save=True)
inv2.get('items')[0].item_code = '_Test Item 2'
inv2.append("taxes", {
"account_head": "_Test Account VAT - _TC",
"charge_type": "On Net Total",
"cost_center": "_Test Cost Center - _TC",
"description": "VAT",
"doctype": "Sales Taxes and Charges",
"rate": 5
})
inv2.insert()
inv2.submit()
consolidate_pos_invoices()
inv.load_from_db()
consolidated_invoice = frappe.get_doc('Sales Invoice', inv.consolidated_invoice)
item_wise_tax_detail = json.loads(consolidated_invoice.get('taxes')[0].item_wise_tax_detail)
tax_rate, amount = item_wise_tax_detail.get('_Test Item')
self.assertEqual(tax_rate, 9)
self.assertEqual(amount, 9)
tax_rate2, amount2 = item_wise_tax_detail.get('_Test Item 2')
self.assertEqual(tax_rate2, 5)
self.assertEqual(amount2, 5)
finally:
frappe.set_user("Administrator")
frappe.db.sql("delete from `tabPOS Profile`")
frappe.db.sql("delete from `tabPOS Invoice`")

View File

@ -62,14 +62,15 @@ class POSProfile(Document):
if len(default_mode) > 1:
frappe.throw(_("You can only select one mode of payment as default"))
invalid_modes = []
for d in self.payments:
account = frappe.db.get_value(
"Mode of Payment Account",
"Mode of Payment Account",
{"parent": d.mode_of_payment, "company": self.company},
"default_account"
)
if not account:
invalid_modes.append(get_link_to_form("Mode of Payment", d.mode_of_payment))

View File

@ -92,11 +92,21 @@ def make_pos_profile(**args):
"write_off_cost_center": args.write_off_cost_center or "_Test Write Off Cost Center - _TC"
})
payments = [{
mode_of_payment = frappe.get_doc("Mode of Payment", "Cash")
company = args.company or "_Test Company"
default_account = args.income_account or "Sales - _TC"
if not frappe.db.get_value("Mode of Payment Account", {"company": company, "parent": "Cash"}):
mode_of_payment.append("accounts", {
"company": company,
"default_account": default_account
})
mode_of_payment.save()
pos_profile.append("payments", {
'mode_of_payment': 'Cash',
'default': 1
}]
pos_profile.set("payments", payments)
})
if not frappe.db.exists("POS Profile", args.name or "_Test POS Profile"):
pos_profile.insert()

View File

@ -16,8 +16,11 @@ frappe.ui.form.on('POS Settings', {
}
});
frappe.meta.get_docfield("POS Field", "fieldname", frm.doc.name).options = [""].concat(fields);
frm.fields_dict.invoice_fields.grid.update_docfield_property(
'fieldname', 'options', [""].concat(fields)
);
});
}
});

View File

@ -328,6 +328,21 @@ class TestPricingRule(unittest.TestCase):
self.assertEquals(item.discount_amount, 110)
self.assertEquals(item.rate, 990)
def test_pricing_rule_with_margin_and_discount_amount(self):
frappe.delete_doc_if_exists('Pricing Rule', '_Test Pricing Rule')
make_pricing_rule(selling=1, margin_type="Percentage", margin_rate_or_amount=10,
rate_or_discount="Discount Amount", discount_amount=110)
si = create_sales_invoice(do_not_save=True)
si.items[0].price_list_rate = 1000
si.payment_schedule = []
si.insert(ignore_permissions=True)
item = si.items[0]
self.assertEquals(item.margin_rate_or_amount, 10)
self.assertEquals(item.rate_with_margin, 1100)
self.assertEquals(item.discount_amount, 110)
self.assertEquals(item.rate, 990)
def test_pricing_rule_for_product_discount_on_same_item(self):
frappe.delete_doc_if_exists('Pricing Rule', '_Test Pricing Rule')
test_record = {
@ -560,6 +575,7 @@ def make_pricing_rule(**args):
"margin_rate_or_amount": args.margin_rate_or_amount or 0.0,
"condition": args.condition or '',
"priority": 1,
"discount_amount": args.discount_amount or 0.0,
"apply_multiple_pricing_rules": args.apply_multiple_pricing_rules or 0
})

View File

@ -471,7 +471,7 @@ def apply_pricing_rule_on_transaction(doc):
if not d.get(pr_field): continue
if d.validate_applied_rule and doc.get(field) < d.get(pr_field):
if d.validate_applied_rule and doc.get(field) is not None and doc.get(field) < d.get(pr_field):
frappe.msgprint(_("User has not applied rule on the invoice {0}")
.format(doc.name))
else:

View File

@ -92,7 +92,7 @@ frappe.ui.form.on('Process Statement Of Accounts', {
frm.refresh_field('customers');
}
else{
frappe.msgprint('No Customers found with selected options.');
frappe.throw('No Customers found with selected options.');
}
}
}

View File

@ -126,9 +126,11 @@ def get_customers_based_on_sales_person(sales_person):
sales_person_records = frappe._dict()
for d in records:
sales_person_records.setdefault(d.parenttype, set()).add(d.parent)
customers = frappe.get_list('Customer', fields=['name', 'email_id'], \
if sales_person_records.get('Customer'):
return frappe.get_list('Customer', fields=['name', 'email_id'], \
filters=[['name', 'in', list(sales_person_records['Customer'])]])
return customers
else:
return []
def get_recipients_and_cc(customer, doc):
recipients = []

View File

@ -496,15 +496,6 @@ cur_frm.fields_dict['items'].grid.get_field('project').get_query = function(doc,
}
}
cur_frm.cscript.select_print_heading = function(doc,cdt,cdn){
if(doc.select_print_heading){
// print heading
cur_frm.pformat.print_heading = doc.select_print_heading;
}
else
cur_frm.pformat.print_heading = __("Purchase Invoice");
}
frappe.ui.form.on("Purchase Invoice", {
setup: function(frm) {
frm.custom_make_buttons = {

View File

@ -1380,7 +1380,7 @@
"idx": 204,
"is_submittable": 1,
"links": [],
"modified": "2021-03-30 21:45:58.334107",
"modified": "2021-03-30 22:45:58.334107",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",

View File

@ -1,14 +1,14 @@
var globalOnload = frappe.listview_settings['Sales Invoice'].onload;
frappe.listview_settings['Sales Invoice'].onload = function (doclist) {
frappe.listview_settings['Sales Invoice'].onload = function (list_view) {
// Provision in case onload event is added to sales_invoice.js in future
if (globalOnload) {
globalOnload(doclist);
globalOnload(list_view);
}
const action = () => {
const selected_docs = doclist.get_checked_items();
const docnames = doclist.get_checked_items(true);
const selected_docs = list_view.get_checked_items();
const docnames = list_view.get_checked_items(true);
for (let doc of selected_docs) {
if (doc.docstatus !== 1) {
@ -19,7 +19,7 @@ frappe.listview_settings['Sales Invoice'].onload = function (doclist) {
frappe.call({
method: 'erpnext.regional.india.utils.generate_ewb_json',
args: {
'dt': doclist.doctype,
'dt': list_view.doctype,
'dn': docnames
},
callback: function(r) {
@ -35,5 +35,140 @@ frappe.listview_settings['Sales Invoice'].onload = function (doclist) {
});
};
doclist.page.add_actions_menu_item(__('Generate E-Way Bill JSON'), action, false);
list_view.page.add_actions_menu_item(__('Generate E-Way Bill JSON'), action, false);
const generate_irns = () => {
const docnames = list_view.get_checked_items(true);
if (docnames && docnames.length) {
frappe.call({
method: 'erpnext.regional.india.e_invoice.utils.generate_einvoices',
args: { docnames },
freeze: true,
freeze_message: __('Generating E-Invoices...')
});
} else {
frappe.msgprint({
message: __('Please select at least one sales invoice to generate IRN'),
title: __('No Invoice Selected'),
indicator: 'red'
});
}
};
const cancel_irns = () => {
const docnames = list_view.get_checked_items(true);
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 d = new frappe.ui.Dialog({
title: __("Cancel IRN"),
fields: fields,
primary_action: function() {
const data = d.get_values();
frappe.call({
method: 'erpnext.regional.india.e_invoice.utils.cancel_irns',
args: {
doctype: list_view.doctype,
docnames,
reason: data.reason.split('-')[0],
remark: data.remark
},
freeze: true,
freeze_message: __('Cancelling E-Invoices...'),
});
d.hide();
},
primary_action_label: __('Submit')
});
d.show();
};
let einvoicing_enabled = false;
frappe.db.get_single_value("E Invoice Settings", "enable").then(enabled => {
einvoicing_enabled = enabled;
});
list_view.$result.on("change", "input[type=checkbox]", () => {
if (einvoicing_enabled) {
const docnames = list_view.get_checked_items(true);
// show/hide e-invoicing actions when no sales invoices are checked
if (docnames && docnames.length) {
// prevent adding actions twice if e-invoicing action group already exists
if (list_view.page.get_inner_group_button(__('E-Invoicing')).length == 0) {
list_view.page.add_inner_button(__('Generate IRNs'), generate_irns, __('E-Invoicing'));
list_view.page.add_inner_button(__('Cancel IRNs'), cancel_irns, __('E-Invoicing'));
}
} else {
list_view.page.remove_inner_button(__('Generate IRNs'), __('E-Invoicing'));
list_view.page.remove_inner_button(__('Cancel IRNs'), __('E-Invoicing'));
}
}
});
frappe.realtime.on("bulk_einvoice_generation_complete", (data) => {
const { failures, user, invoices } = data;
if (invoices.length != failures.length) {
frappe.msgprint({
message: __('{0} e-invoices generated successfully', [invoices.length]),
title: __('Bulk E-Invoice Generation Complete'),
indicator: 'orange'
});
}
if (failures && failures.length && user == frappe.session.user) {
let message = `
Failed to generate IRNs for following ${failures.length} sales invoices:
<ul style="padding-left: 20px; padding-top: 5px;">
${failures.map(d => `<li>${d.docname}</li>`).join('')}
</ul>
`;
frappe.msgprint({
message: message,
title: __('Bulk E-Invoice Generation Complete'),
indicator: 'orange'
});
}
});
frappe.realtime.on("bulk_einvoice_cancellation_complete", (data) => {
const { failures, user, invoices } = data;
if (invoices.length != failures.length) {
frappe.msgprint({
message: __('{0} e-invoices cancelled successfully', [invoices.length]),
title: __('Bulk E-Invoice Cancellation Complete'),
indicator: 'orange'
});
}
if (failures && failures.length && user == frappe.session.user) {
let message = `
Failed to cancel IRNs for following ${failures.length} sales invoices:
<ul style="padding-left: 20px; padding-top: 5px;">
${failures.map(d => `<li>${d.docname}</li>`).join('')}
</ul>
`;
frappe.msgprint({
message: message,
title: __('Bulk E-Invoice Cancellation Complete'),
indicator: 'orange'
});
}
});
};

View File

@ -1,9 +1,6 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
// print heading
cur_frm.pformat.print_heading = 'Invoice';
{% include 'erpnext/selling/sales_common.js' %};
frappe.provide("erpnext.accounts");
@ -916,7 +913,7 @@ frappe.ui.form.on('Sales Invoice Timesheet', {
},
callback: function(r, rt) {
if(r.message){
data = r.message;
let data = r.message;
frappe.model.set_value(cdt, cdn, "billing_hours", data.billing_hours);
frappe.model.set_value(cdt, cdn, "billing_amount", data.billing_amount);
frappe.model.set_value(cdt, cdn, "timesheet_detail", data.timesheet_detail);

View File

@ -24,6 +24,7 @@ from erpnext.accounts.deferred_revenue import validate_service_stop_date
from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import get_party_tax_withholding_details
from frappe.model.utils import get_fetch_values
from frappe.contacts.doctype.address.address import get_address_display
from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import get_party_tax_withholding_details
from erpnext.healthcare.utils import manage_invoice_submit_cancel
@ -211,6 +212,9 @@ class SalesInvoice(SellingController):
# this sequence because outstanding may get -ve
self.make_gl_entries()
if self.update_stock == 1:
self.repost_future_sle_and_gle()
if self.update_stock == 1:
self.repost_future_sle_and_gle()

View File

@ -1166,10 +1166,12 @@ class TestSalesInvoice(unittest.TestCase):
def test_create_so_with_margin(self):
si = create_sales_invoice(item_code="_Test Item", qty=1, do_not_submit=True)
price_list_rate = 100
price_list_rate = flt(100) * flt(si.plc_conversion_rate)
si.items[0].price_list_rate = price_list_rate
si.items[0].margin_type = 'Percentage'
si.items[0].margin_rate_or_amount = 25
si.items[0].discount_amount = 0.0
si.items[0].discount_percentage = 0.0
si.save()
self.assertEqual(si.get("items")[0].rate, flt((price_list_rate*25)/100 + price_list_rate))

View File

@ -443,6 +443,16 @@
"onboard": 0,
"type": "Link"
},
{
"dependencies": "GL Entry",
"hidden": 0,
"is_query_report": 1,
"label": "UAE VAT 201",
"link_to": "UAE VAT 201",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,

View File

@ -13,6 +13,8 @@
"po_required",
"pr_required",
"maintain_same_rate",
"maintain_same_rate_action",
"role_to_override_stop_action",
"allow_multiple_items",
"subcontract",
"backflush_raw_materials_of_subcontract_based_on",
@ -89,6 +91,23 @@
{
"fieldname": "column_break_11",
"fieldtype": "Column Break"
},
{
"default": "Stop",
"depends_on": "maintain_same_rate",
"description": "Configure the action to stop the transaction or just warn if the same rate is not maintained.",
"fieldname": "maintain_same_rate_action",
"fieldtype": "Select",
"label": "Action If Same Rate is Not Maintained",
"mandatory_depends_on": "maintain_same_rate",
"options": "Stop\nWarn"
},
{
"depends_on": "eval:doc.maintain_same_rate_action == 'Stop'",
"fieldname": "role_to_override_stop_action",
"fieldtype": "Link",
"label": "Role Allowed to Override Stop Action",
"options": "Role"
}
],
"icon": "fa fa-cog",
@ -96,7 +115,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2021-03-02 17:34:04.190677",
"modified": "2021-04-04 20:01:44.087066",
"modified_by": "Administrator",
"module": "Buying",
"name": "Buying Settings",

View File

@ -0,0 +1,471 @@
# Version 13.0.0 Release Notes
### Accounting
- [New and refreshed POS](https://github.com/frappe/erpnext/pull/20789)
- [GST E-invoicing for India](https://docs.erpnext.com/docs/user/manual/en/regional/india/setup-e-invoicing)
- [Distributed Cost Center](https://docs.erpnext.com/docs/user/manual/en/accounts/distributed-cost-center)
- [Process Bulk Statement Of Accounts](https://docs.erpnext.com/docs/user/manual/en/accounts/process-statement-of-accounts)
- [More controlled deferred revenue booking](https://docs.erpnext.com/docs/user/manual/en/accounts/process-deferred-accounting)
- [Dunning](https://docs.erpnext.com/docs/user/manual/en/accounts/dunning)
- [Journal Entry Template](https://docs.erpnext.com/docs/user/manual/en/accounts/journal-entry-template)
- [POS Register report](https://github.com/frappe/erpnext/pull/23313)
- [UAE VAT 201 Report](https://github.com/frappe/erpnext/pull/23447)
### Loan Management
- [Loan Application](https://docs.erpnext.com/docs/user/manual/en/loan-management/loan-application)
- [Loan](https://docs.erpnext.com/docs/user/manual/en/loan-management/loan)
- [Loan Security Pledge](https://docs.erpnext.com/docs/user/manual/en/loan-management/loan-security-pledge)
- [Loan Disbursement](https://docs.erpnext.com/docs/user/manual/en/loan-management/loan-disbursement)
- [Loan Repayment](https://docs.erpnext.com/docs/user/manual/en/loan-management/loan-repayment)
- [Loan Interest Accrual](https://docs.erpnext.com/docs/user/manual/en/loan-management/loan-interest-accrual)
- [Loan Write Off](https://docs.erpnext.com/docs/user/manual/en/loan-management/loan-write-off)
### Healthcare
- [Refactored Healthcare Module](https://docs.erpnext.com/docs/user/manual/en/healthcare)
- [Rehabilitation Module](https://docs.erpnext.com/docs/user/manual/en/healthcare/exercise_type)
- [Laboratory Module](https://docs.erpnext.com/docs/user/manual/en/healthcare/setup_laboratory)
- [Patient Progress Page](https://github.com/frappe/erpnext/pull/22474)
- [Inpatient Medication Order and Entry](https://docs.erpnext.com/docs/user/manual/en/healthcare/inpatient_medication_entry)
- [Therapy Plan Template](https://docs.erpnext.com/docs/user/manual/en/healthcare/therapy_plan)
- [Multi company support in Healthcare](https://github.com/frappe/erpnext/pull/21290)
- [Inpatient Medication Orders Script Report](https://github.com/frappe/erpnext/pull/23984)
- [Patient History Enhancements](https://github.com/frappe/erpnext/pull/24033)
### Stock
- [Putaway](https://docs.erpnext.com/docs/user/manual/en/stock/putaway-rule)
- [More accurate stock valuation in case of back-dated stock transactions](https://github.com/frappe/erpnext/pull/24183)
- [Repost item costing via background job](https://github.com/frappe/erpnext/pull/24183)
- [Item valuation for internal stock transfers](https://github.com/frappe/erpnext/pull/24200)
- [Multi currency in Landed Cost Voucher](https://github.com/frappe/erpnext/pull/24127)
- [Formula based Quality Inspection](https://docs.erpnext.com/docs/user/manual/en/stock/quality-inspection)
- [Value Based and Numeric Quality Inspection](https://github.com/frappe/erpnext/pull/24181)
- [Shipment](https://github.com/frappe/erpnext/pull/22914)
- [Return tracking in PR/DN](https://github.com/frappe/erpnext/pull/22859)
### Manufacturing
- [Production forecasting using Exponential Smoothing method](https://docs.erpnext.com/docs/user/manual/en/manufacturing/reports/demand-driven-forecasting)
- [BOM Template](https://docs.erpnext.com/docs/user/manual/en/manufacturing/bill-of-materials#34-bom-template)
- [Downtime Entry](https://docs.erpnext.com/docs/user/manual/en/manufacturing/downtime-entry)
- [Quality Inspection on Job Card](https://github.com/frappe/erpnext/pull/23964)
- New Reports
- Production Planning Report ([#21763](https://github.com/frappe/erpnext/pull/21763))
- BOM Operations Time ([#21763](https://github.com/frappe/erpnext/pull/21763))
- Work Order Summary ([#21430](https://github.com/frappe/erpnext/pull/21430))
- Job card Summary ([#21430](https://github.com/frappe/erpnext/pull/21430))
- Downtime Analysis ([#21430](https://github.com/frappe/erpnext/pull/21430))
- Quality Inspection ([#21430](https://github.com/frappe/erpnext/pull/21430))
### HR
- [Leave policy assignment](https://github.com/frappe/erpnext/pull/23112)
- [In and Out time in attendance](https://github.com/frappe/erpnext/pull/21547)
- [Shift management](https://docs.erpnext.com/docs/user/manual/en/human-resources/shift-management)
- [Recruitment analytics](https://github.com/frappe/erpnext/pull/21732)
- [Bulk Mark Attendance](https://github.com/frappe/erpnext/pull/20062)
- [Leave type with partial payment](https://github.com/frappe/erpnext/pull/23173)
- New and enhanced reports
- Employee Analytics ([#21705](https://github.com/frappe/erpnext/pull/21705))
- Employee Leave Balance ([#20754](https://github.com/frappe/erpnext/pull/20754))
- Employee Leave Balance Summary ([#20754](https://github.com/frappe/erpnext/pull/20754))
### Payroll
- [Multi-currency payroll](https://github.com/frappe/erpnext/pull/23519)
- [Payroll based on attendance](https://github.com/frappe/erpnext/pull/21258)
- [Payroll based on employee cost center](https://github.com/frappe/erpnext/pull/21609)
- [Recurring Additional Salary](https://github.com/frappe/erpnext/pull/20936)
- [Compute Year to Date for Salary Slip components](https://github.com/frappe/erpnext/pull/24362)
- New Reports
- Income Tax Deductions
- Professional Tax Deductions
- Provident Fund Deductions
- Total Salary Payments Based on Payment Mode
- Salary Payments via ECS
### CRM
- [Social Media Post](https://docs.erpnext.com/docs/user/manual/en/CRM/social-media-post)
- [Make Quotation against Blanket Order](https://docs.erpnext.com/docs/user/manual/en/selling/blanket-order)
- [Calendar View for Opportunity](https://github.com/frappe/erpnext/pull/21280)
### Selling
- [Batch wise item pricing](https://github.com/frappe/erpnext/pull/24470)
- [Refreshed shopping cart](https://github.com/frappe/erpnext/pull/22617)
- [Territory-wise Sales Report](https://github.com/frappe/erpnext/pull/20428)
#### Buying
- [Multi UOM support in Request for Quotation](https://github.com/frappe/erpnext/pull/22249)
- [Provision to make RFQ against Opportunity](https://github.com/frappe/erpnext/pull/22765)
- [Item Rate in Stock UOM in purchase cycle](https://github.com/frappe/erpnext/pull/24315)
- New Reports
- Requested Items To Order ([#21611](https://github.com/frappe/erpnext/pull/21611))
- Purchase Order Analysis ([#21611](https://github.com/frappe/erpnext/pull/21611))
- Supplier Quotation Comparison report ([#23323](https://github.com/frappe/erpnext/pull/23323))
### Project
- [Project template with dependent tasks](https://github.com/frappe/erpnext/pull/24092)
- [Project Summary Report](https://github.com/frappe/erpnext/pull/21587)
### Support
- [Help Articles on support portal](https://github.com/frappe/erpnext/pull/22194)
- [Issue Metrics and SLA Enhancements](https://github.com/frappe/erpnext/pull/21617)
- [Issue Summary Script Report](https://docs.erpnext.com/docs/user/manual/en/support/support_reports)
- [Issue Analytics Script Report](https://docs.erpnext.com/docs/user/manual/en/support/support_reports)
### Non-Profits
- [80G Certificates and Donations](https://docs.erpnext.com/docs/user/manual/en/non_profit/tax_exemption_80g_certificate)
#### Integrations
- [Woocommerce Integration](https://docs.erpnext.com/docs/user/manual/en/erpnext_integration/woocommerce_integration)
- [Taxjar Integration](https://github.com/frappe/erpnext/pull/21047)
- [M-pesa Integration](https://docs.erpnext.com/docs/user/manual/en/erpnext_integration/mpesa-integration)
- [Telephony feature using Twillio](https://github.com/frappe/erpnext/pull/24032)
- [Voice Call Settings](https://github.com/frappe/erpnext/pull/24126)
#### Other Enhancements and Fixes
- Accounting Dimensions in Budget Variance Report ([#19973](https://github.com/frappe/erpnext/pull/19973))
- "Sync Now" option in Plaid Settings ([#23602](https://github.com/frappe/erpnext/pull/23602))
- Custom Fields in POS ([#19876](https://github.com/frappe/erpnext/pull/19876))
- [Inter Warehouse Stock Transfer in Purchase Receipt](https://docs.erpnext.com/docs/user/manual/en/stock/articles/material-transfer-from-delivery-note)
- [Accounts Payable Report based on Payment Terms](https://docs.erpnext.com/docs/user/manual/en/accounts/accounting-reports)
- Configurable accounting dimension filters and validations ([#23912](https://github.com/frappe/erpnext/pull/23912))
- Territory tree in Customer Acquisition and Loyalty report ([#21668](https://github.com/frappe/erpnext/pull/21668))
- Allow Purchase Invoice Creation Without Purchase Order Checkbox in Supplier ([#20864](https://github.com/frappe/erpnext/pull/20864))
- Gross Profit In Quotation ([#21795](https://github.com/frappe/erpnext/pull/21795))
- Notify credit controller users for credit limit extension via Email ([#22213](https://github.com/frappe/erpnext/pull/22213))
- Run MRP at parent level in the production plan and make material transfer based upon materials availability ([#21545](https://github.com/frappe/erpnext/pull/21545))
- Balance Serial Nos in Stock Ledger report ([#23675](https://github.com/frappe/erpnext/pull/23675))
- Youtube interactions via Video ([#22867](https://github.com/frappe/erpnext/pull/22867))
- Consider Holiday List in Student Leave Application and Attendance ([#23388](https://github.com/frappe/erpnext/pull/23388))
- Patient appointment status changes ([#24201](https://github.com/frappe/erpnext/pull/24201))
- Sales order status filter added for production plan ([#23805](https://github.com/frappe/erpnext/pull/23805))
- Monthly attendance sheet report group by Department, Designation, Employee Grade and Branch ([#21331](https://github.com/frappe/erpnext/pull/21331))
- Upload Attendance template now have pre-filled holiday status ([#20947](https://github.com/frappe/erpnext/pull/20947))
- Provision to disable serial no and batch selector ([#24398](https://github.com/frappe/erpnext/pull/24398))
<details>
<summary>More</summary>
- Fetch Items from BOM in Stock Entry([#19498](https://github.com/frappe/erpnext/pull/19498))
- Supplier Sourced Items in BOM ([#23557](https://github.com/frappe/erpnext/pull/23557))
- Close Production Plan ([#23728](https://github.com/frappe/erpnext/pull/23728))
- Button to create Stock Entry for Drug Shortage ([#24012](https://github.com/frappe/erpnext/pull/24012))
- Added column cost center in Accounts Receivable report ([#23835](https://github.com/frappe/erpnext/pull/23835))
- Added jinja templating in Contract Template ([#24046](https://github.com/frappe/erpnext/pull/24046))
- Make account number length configurable ([#23845](https://github.com/frappe/erpnext/pull/23845))
- Add company and correct filter in bank reconciliation statement ([#23614](https://github.com/frappe/erpnext/pull/23614))
- Added Condition field in Pricing Rule ([#23014](https://github.com/frappe/erpnext/pull/23014))
- Open lead status on next contact date ([#23445](https://github.com/frappe/erpnext/pull/23445))
- [Tax Category in POS Profile](https://docs.erpnext.com/docs/user/manual/en/accounts/pos-profile)
- Added phone field in product Inquiry ([#23170](https://github.com/frappe/erpnext/pull/23170))
- Allow Discharge despite Unbilled Healthcare Services ([#24281](https://github.com/frappe/erpnext/pull/24281))
- Do Not Bill Patient Encounters for Inpatients ([#24355](https://github.com/frappe/erpnext/pull/24355))
- Autofill Supplier pop-up when only 1 Supplier in RFQ ([#22512](https://github.com/frappe/erpnext/pull/22512))
- Accounting entries for service item in Purchase receipt ([#22223](https://github.com/frappe/erpnext/pull/22223))
- Added Project in Sales Analytics report ([#23309](https://github.com/frappe/erpnext/pull/23309))
- Added all companies option in employee tree to view employee across all companies ([#22573](https://github.com/frappe/erpnext/pull/22573))
- Email Group Option In Email Campaign ([#22731](https://github.com/frappe/erpnext/pull/22731))
- Stock Report Enhancements ([#21727](https://github.com/frappe/erpnext/pull/21727))
- Added range for age in stock ageing ([#22622](https://github.com/frappe/erpnext/pull/22622))
- Report Summary in Financial Statement([#20876](https://github.com/frappe/erpnext/pull/20876))
- Added sequence id in routing for the completion of operations sequentially ([#23641](https://github.com/frappe/erpnext/pull/23641))
- Nested Set filtering for Accounting Dimension
- Add/Remove Items from submitted Sales/Purchase Order
- Provision to edit Item Details from Marketplace
- Scan Barcode in Purchase Receipt
- Disable Rounded Totals Checkbox for Salary Slips in HR Settings
- Renamed Loan Management to Loan on Desk Page ([#21877](https://github.com/frappe/erpnext/pull/21877))
- Added Expense Approver field in Employee master ([#22244](https://github.com/frappe/erpnext/pull/22244))
- Bill all hours by default on Timesheet ([#22155](https://github.com/frappe/erpnext/pull/22155))
- Unable to cancel employee advance ([#22374](https://github.com/frappe/erpnext/pull/22374))
- Status error in purchase invoice ([#22351](https://github.com/frappe/erpnext/pull/22351))
- Item-wise sales and purchase register export ([#22184](https://github.com/frappe/erpnext/pull/22184))
- Billing address in for Purchase documents ([#22233](https://github.com/frappe/erpnext/pull/22233))
- Handle canceled entries in financial statements ([#22231](https://github.com/frappe/erpnext/pull/22231))
- Default period start date and period end date for financial statements ([#22011](https://github.com/frappe/erpnext/pull/22011))
- Update Packed Items via Update Items in Sales Order ([#22392](https://github.com/frappe/erpnext/pull/22392))
- Hide delete company transactions button if not system manager ([#21839](https://github.com/frappe/erpnext/pull/21839))
- Skipping total row for tree-view reports ([#22350](https://github.com/frappe/erpnext/pull/22350))
- Cancelled entries in tds payable monthly report ([#22131](https://github.com/frappe/erpnext/pull/22131))
- Inter-company Invoice currency for multicurrency transactions ([#21984](https://github.com/frappe/erpnext/pull/21984))
- Filter batches based on item and warehouse in Pick List (develop) ([#21780](https://github.com/frappe/erpnext/pull/21780))
- Set cost center in Expense Claim child based on parent (if missing) ([#22175](https://github.com/frappe/erpnext/pull/22175))
- Item wise backdated stock entry posting for immutable ledger ([#22366](https://github.com/frappe/erpnext/pull/22366))
- Shopping cart UI fixes ([#22137](https://github.com/frappe/erpnext/pull/22137))
- Filter Leave Type based on allocation for a particular employee ([#22050](https://github.com/frappe/erpnext/pull/22050))
- Party validation for inter-warehouse transaction ([#22186](https://github.com/frappe/erpnext/pull/22186))
- Manufacturing dashboard and work order summary chart ([#21946](https://github.com/frappe/erpnext/pull/21946))
- IP Admission and Discharge, Minor fixes ([#21817](https://github.com/frappe/erpnext/pull/21817))
- Validation of Purchase Order against Material Request missing ([#22192](https://github.com/frappe/erpnext/pull/22192))
- Staffing Plan validation ([#22379](https://github.com/frappe/erpnext/pull/22379))
- Do not allow backdated stock transactions in previous fiscal year ([#21967](https://github.com/frappe/erpnext/pull/21967))
- Employee Advance Return not working ([#21812](https://github.com/frappe/erpnext/pull/21812))
- Added card for reports on education desk ([#21853](https://github.com/frappe/erpnext/pull/21853))
- Refactored project summary report ([#21943](https://github.com/frappe/erpnext/pull/21943))
- Revenue and Customer Count only in date range in Customer Acquitition Report ([#22210](https://github.com/frappe/erpnext/pull/22210))
- Alternative item not working for subcontract ([#22386](https://github.com/frappe/erpnext/pull/22386))
- Unable to create batched Item ([#22393](https://github.com/frappe/erpnext/pull/22393))
- Filters for the manufacturing reports ([#21960](https://github.com/frappe/erpnext/pull/21960))
- Raw material warehouse in Production Planning Report ([#21982](https://github.com/frappe/erpnext/pull/21982))
- Allowed LWP leave types to select in Leave Application even if there is no allocation against them ([#22197](https://github.com/frappe/erpnext/pull/22197))
- Report not working on parameter Grade ([#21951](https://github.com/frappe/erpnext/pull/21951))
- Allow to enter Relieving date if employee status is Left ([#22242](https://github.com/frappe/erpnext/pull/22242))
- Resetting lost reason in opportunity and quotation ([#22378](https://github.com/frappe/erpnext/pull/22378))
- Filtering issues in opening invoice creation tool ([#21969](https://github.com/frappe/erpnext/pull/21969))
- Set default reference Id for "On Previous Row Amount" and "On Previous Row Total" ([#22346](https://github.com/frappe/erpnext/pull/22346))
- UX date range field separated in from and to date fields. ([#21765](https://github.com/frappe/erpnext/pull/21765))
- Enable show_configure_button when shopping cart is enabled ([#22468](https://github.com/frappe/erpnext/pull/22468))
- Setup status indicators for Job Offer and Job Applicant (develop) ([#22445](https://github.com/frappe/erpnext/pull/22445))
- Item-wise sales history report ([#22783](https://github.com/frappe/erpnext/pull/22783))
- Setting filter for project in kanban board ([#22717](https://github.com/frappe/erpnext/pull/22717))
- Dashboard For Timesheet ([#22750](https://github.com/frappe/erpnext/pull/22750))
- Handle custom statuses for the pause SLA configuration ([#22349](https://github.com/frappe/erpnext/pull/22349))
- Quality Feedback and Template ([#22571](https://github.com/frappe/erpnext/pull/22571))
- Unable to change link from new lead to existing customer ([#22787](https://github.com/frappe/erpnext/pull/22787))
- Move Issue List actions under 'Actions' dropdown (ux) ([#22710](https://github.com/frappe/erpnext/pull/22710))
- Cost center should only show option of selected company ([#22598](https://github.com/frappe/erpnext/pull/22598))
- Serial No Rename does not affect Stock Ledger Entry ([#22746](https://github.com/frappe/erpnext/pull/22746))
- Descriptions not copied while creating Fees from Fee Structure ([#22792](https://github.com/frappe/erpnext/pull/22792))
- Company filter for cost_center and expense_account in all sales and purchase transactions ([#22478](https://github.com/frappe/erpnext/pull/22478))
- Arrangements of filters for reports accounts payable & receivable ([#22636](https://github.com/frappe/erpnext/pull/22636))
- Update the project after task deletion so that the % completed shows correct value ([#22591](https://github.com/frappe/erpnext/pull/22591))
- Block Invalid Serial No updates in Maintenance Schedule ([#22665](https://github.com/frappe/erpnext/pull/22665))
- Fetch item price in sales invoice based on it's validity ([#22563](https://github.com/frappe/erpnext/pull/22563))
- Add view ledger button for cancelled docs ([#22432](https://github.com/frappe/erpnext/pull/22432))
- Allow creating SLA documents even if SLA tracking is not enabled ([#22608](https://github.com/frappe/erpnext/pull/22608))
- Quotation list view blank if quotation_to field not set as a standard filter ([#22672](https://github.com/frappe/erpnext/pull/22672))
- Salary deductions report fixes ([#22397](https://github.com/frappe/erpnext/pull/22397))
22727))
- Incorrect delivered qty in Supplier-Wise Sales Analytics ([#22631](https://github.com/frappe/erpnext/pull/22631))
- Moved parent warehouse to top section also added a section break ([#22708](https://github.com/frappe/erpnext/pull/22708))
- Skip Progress and Completed by fields on Task Duplication ([#22565](https://github.com/frappe/erpnext/pull/22565))
- Incorrect stock after merging the items ([#22526](https://github.com/frappe/erpnext/pull/22526))
- Letter head not found in opening invoice creation tool ([#22488](https://github.com/frappe/erpnext/pull/22488))
- Cannot cancel asset and asset movement ([#22441](https://github.com/frappe/erpnext/pull/22441))
- Fetch project-related info in Timesheet ([#22423](https://github.com/frappe/erpnext/pull/22423))
- Currency symbol not showing as per company currency in stock balance report ([#22724](https://github.com/frappe/erpnext/pull/22724))
- Add default cost center in payment reconciliation JV ([#22614](https://github.com/frappe/erpnext/pull/22614))
- Stock Reconciliation Invalid Quantity for Batched Item ([#22726](https://github.com/frappe/erpnext/pull/22726))
- Project link not set in accounts other than profit and loss accounts ([#22051](https://github.com/frappe/erpnext/pull/22051))
- Buying price for non stock item in gross profit report ([#22616](https://github.com/frappe/erpnext/pull/22616))
- Multi currency payment reconciliation ([#22738](https://github.com/frappe/erpnext/pull/22738))
- Cannot cancel assets with repair pending ([#22440](https://github.com/frappe/erpnext/pull/22440))
- Reset homepage to home after unchecking products page ([#22736](https://github.com/frappe/erpnext/pull/22736))
- Generic Message in previous doc validation for buying and selling ([#22546](https://github.com/frappe/erpnext/pull/22546))
- Expense claim outstanding while making payment entry ([#22735](https://github.com/frappe/erpnext/pull/22735))
- Take parent cost center for child if no cost center at child in expense claim ([#22496](https://github.com/frappe/erpnext/pull/22496))
- Consider company fiscal year for getting balance ([#22577](https://github.com/frappe/erpnext/pull/22577))
- Pick List empty table and Serial-Batch items handling ([#22426](https://github.com/frappe/erpnext/pull/22426))
- Show total row in print format of financial statement ([#22693](https://github.com/frappe/erpnext/pull/22693))
- Set Root as Parent if no parent in new tree view node ([#22497](https://github.com/frappe/erpnext/pull/22497))
- Multiple pos issues ([#23725](https://github.com/frappe/erpnext/pull/23725))
- Calculate taxes if tax is based on item quantity and inclusive on item price ([#23001](https://github.com/frappe/erpnext/pull/23001))
- Contact us button not visible in the website for the non variant items ([#23217](https://github.com/frappe/erpnext/pull/23217))
- Not able to make Material Request from Sales Order ([#23669](https://github.com/frappe/erpnext/pull/23669))
- Capture advance payments in payment order ([#23256](https://github.com/frappe/erpnext/pull/23256))
- Program and Course Enrollment fixes ([#23333](https://github.com/frappe/erpnext/pull/23333))
- Cannot create asset if cwip disabled and account not set ([#23580](https://github.com/frappe/erpnext/pull/23580))
- Cannot merge pos invoices with inclusive tax ([#23541](https://github.com/frappe/erpnext/pull/23541))
- Do not allow Company as accounting dimension ([#23755](https://github.com/frappe/erpnext/pull/23755))
- Set value of wrong Bank Account field in Payment Entry ([#22302](https://github.com/frappe/erpnext/pull/22302))
- Reverse journal entry for multi-currency ([#23165](https://github.com/frappe/erpnext/pull/23165))
- Updated integrations desk page ([#23772](https://github.com/frappe/erpnext/pull/23772))
- Assessment Result child table not visible when accessed via Assessment Plan dashboard ([#22880](https://github.com/frappe/erpnext/pull/22880))
- Conversion factor fixes in Stock Entry ([#23407](https://github.com/frappe/erpnext/pull/23407))
- Total calculations for multi-currency RCM invoices ([#23072](https://github.com/frappe/erpnext/pull/23072))
- Show accounts in financial statements upto level 20 ([#23718](https://github.com/frappe/erpnext/pull/23718))
- Consolidated financial statement sums values into wrong parent ([#23288](https://github.com/frappe/erpnext/pull/23288))
- Set SLA variance in seconds for Duration fieldtype ([#23765](https://github.com/frappe/erpnext/pull/23765))
- Added missing reports on selling desk ([#23548](https://github.com/frappe/erpnext/pull/23548))
- Fixed heading in the mobile view ([#23145](https://github.com/frappe/erpnext/pull/23145))
- Misleading filters on Item tax Template Link field ([#22918](https://github.com/frappe/erpnext/pull/22918))
- Do not consider opening entries for TDS calculation ([#23597](https://github.com/frappe/erpnext/pull/23597))
- Attendance calendar map fix ([#23245](https://github.com/frappe/erpnext/pull/23245))
- Post cancellation accounting entry on posting date instead of current ([#23361](https://github.com/frappe/erpnext/pull/23361))
- Set Customer only if Contact is present ([#23704](https://github.com/frappe/erpnext/pull/23704))
- Add Delivery Note Count in Sales Invoice Dashboard ([#23161](https://github.com/frappe/erpnext/pull/23161))
- Breadcrumbs for Maintenance Visit and Schedule ([#23369](https://github.com/frappe/erpnext/pull/23369))
- Raise Error on over receipt/consumption for sub-contracted PR ([#23195](https://github.com/frappe/erpnext/pull/23195))
- Validate if company not set in the Payment Entry ([#23419](https://github.com/frappe/erpnext/pull/23419))
- Ignore company and bank account doctype while deleting company transactions ([#22953](https://github.com/frappe/erpnext/pull/22953))
- Sales funnel data is inconsistent ([#23110](https://github.com/frappe/erpnext/pull/23110))
- Credit Limit Email not working ([#23059](https://github.com/frappe/erpnext/pull/23059))
- Add Company in list fields to fetch for Expense Claim ([#23007](https://github.com/frappe/erpnext/pull/23007))
- Issue form cleaned up and renamed Minutes to First Response field ([#23066](https://github.com/frappe/erpnext/pull/23066))
- Quotation lost reason options fix ([#22814](https://github.com/frappe/erpnext/pull/22814))
- Tax amounts in HSN Wise Outward summary ([#23076](https://github.com/frappe/erpnext/pull/23076))
- Patient Appointment not able to save ([#23434](https://github.com/frappe/erpnext/pull/23434))
- Removed Working Hours field from Company ([#23009](https://github.com/frappe/erpnext/pull/23009))
- Added check-in time validation in the Inpatient Record - Transfer ([#22958](https://github.com/frappe/erpnext/pull/22958))
- Handle Blank from/to range in Numeric Item Attribute ([#23483](https://github.com/frappe/erpnext/pull/23483))
- Sequence Matcher error in Bank Reconciliation ([#23539](https://github.com/frappe/erpnext/pull/23539))
- Fixed Conversion Factor rate for the BOM Exploded Item ([#23151](https://github.com/frappe/erpnext/pull/23151))
- Payment Schedule not fetching ([#23476](https://github.com/frappe/erpnext/pull/23476))
- Validate if removed Item Attributes exist in variant items ([#22911](https://github.com/frappe/erpnext/pull/22911))
- Set default billing address for purchase documents ([#22950](https://github.com/frappe/erpnext/pull/22950))
- Added help link in navbar settings ([#22943](https://github.com/frappe/erpnext/pull/22943))
- Apply TDS on Purchase Invoice creation from Purchase Order and Purchase Receipt ([#23282](https://github.com/frappe/erpnext/pull/23282))
- Education Module fixes ([#23714](https://github.com/frappe/erpnext/pull/23714))
- Filter out cancelled entries in customer ledger summary ([#23205](https://github.com/frappe/erpnext/pull/23205))
- Fiscal Year and Tax Rates for Italy ([#23623](https://github.com/frappe/erpnext/pull/23623))
- Production Plan incorrect Work Order qty ([#23264](https://github.com/frappe/erpnext/pull/23264))
- Added new filters in the Batch-wise Balance History report ([#23676](https://github.com/frappe/erpnext/pull/23676))
- Update state code and union territory for Daman and Diu ([#22988](https://github.com/frappe/erpnext/pull/22988))
- Set Stock UOM in item while creating Material Request from Stock Entry ([#23436](https://github.com/frappe/erpnext/pull/23436))
- Sales Order to Purchase Order flow improvement ([#23357](https://github.com/frappe/erpnext/pull/23357))
- Student Admission and Student Applicant fixes ([#23515](https://github.com/frappe/erpnext/pull/23515))
- Loan disbursement amount validation ([#24000](https://github.com/frappe/erpnext/pull/24000))
- Making company address read-only in delivery note ([#23890](https://github.com/frappe/erpnext/pull/23890))
- BOM stock report color showing always red ([#23994](https://github.com/frappe/erpnext/pull/23994))
- Added filter for customer field in Issue ([#24051](https://github.com/frappe/erpnext/pull/24051))
- Added project link in timesheet form ([#23764](https://github.com/frappe/erpnext/pull/23764))
- Update integrations desk page ([#23767](https://github.com/frappe/erpnext/pull/23767))
- Place of supply change on address change ([#23941](https://github.com/frappe/erpnext/pull/23941))
- TDS calculation, skip invoices with "Apply Tax Withholding Amount" has disabled ([#23672](https://github.com/frappe/erpnext/pull/23672))
- Auto fetch serial nos with modified conversion factor ([#23854](https://github.com/frappe/erpnext/pull/23854))
- Default cost center in item master not set in stock entry ([#23877](https://github.com/frappe/erpnext/pull/23877))
- Incorrect de-link serial no and batch ([#23947](https://github.com/frappe/erpnext/pull/23947))
- Accounting for internal transfer invoices within same company ([#24021](https://github.com/frappe/erpnext/pull/24021))
- Multiple pricing rule with margin type as Percentage is not working ([#24205](https://github.com/frappe/erpnext/pull/24205))
- Added Purchase Order to Global Search ([#24055](https://github.com/frappe/erpnext/pull/24055))
- Cannot expand row in update items dialog ([#23839](https://github.com/frappe/erpnext/pull/23839))
- Maintain stock can't be changed it there is product bundle ([#23989](https://github.com/frappe/erpnext/pull/23989))
- SO to PO Mapping Issue ([#23820](https://github.com/frappe/erpnext/pull/23820))
- Asset with value zero doesn't show up in fixed asset register ([#24091](https://github.com/frappe/erpnext/pull/24091))
- Cannot save customer email & phone ([#23797](https://github.com/frappe/erpnext/pull/23797))
- Incorrect balance value in stock balance report ([#24048](https://github.com/frappe/erpnext/pull/24048))
- Payment Terms not fetched in Purchase Invoice from Purchase Receipt ([#23735](https://github.com/frappe/erpnext/pull/23735))
- Fix for LMS Sign Up link ([#23743](https://github.com/frappe/erpnext/pull/23743))
- Incorrect stock quantity if 'Allow Multiple Material Consumption… ([#24116](https://github.com/frappe/erpnext/pull/24116))
- Added wrong absent days calculation in salary slip ([#23897](https://github.com/frappe/erpnext/pull/23897))
- Purchase receipt to purchase invoice bill date mapping ([#23967](https://github.com/frappe/erpnext/pull/23967))
- Overriding po ([#24022](https://github.com/frappe/erpnext/pull/24022))
- Do not cancel reference document on Quality Inspection cancellation ([#24198](https://github.com/frappe/erpnext/pull/24198))
- Get formatted value in 'taxes' print template ([#24035](https://github.com/frappe/erpnext/pull/24035))
- Don't overrule Item Price via Pricing Rule Rate if 0 ([#23636](https://github.com/frappe/erpnext/pull/23636))
- Job card error handling for operations field ([#23991](https://github.com/frappe/erpnext/pull/23991))
- Validation for journal entry with 0 debit and credit values ([#23975](https://github.com/frappe/erpnext/pull/23975))
- Check if customer exists in product listing ([#24030](https://github.com/frappe/erpnext/pull/24030))
- Asset finance book posting date fix ([#23778](https://github.com/frappe/erpnext/pull/23778))
- Same source and target tables in Status Updater's update query ([#24110](https://github.com/frappe/erpnext/pull/24110))
- Asset finance book depreciation posting date fix ([#23833](https://github.com/frappe/erpnext/pull/23833))
- Ignore exception during leave ledger creation from patch ([#24005](https://github.com/frappe/erpnext/pull/24005))
- Added link of bank reconciliation and clearance in accounting desk page ([#23850](https://github.com/frappe/erpnext/pull/23850))
- Sales invoice add button from sales order dashboard ([#24077](https://github.com/frappe/erpnext/pull/24077))
- Incorrect calculation for consumed qty for subcontract item ([#23257](https://github.com/frappe/erpnext/pull/23257))
- Incorrect required_qty in Production Planning Report ([#24074](https://github.com/frappe/erpnext/pull/24074))
- Email digest user not found ([#23949](https://github.com/frappe/erpnext/pull/23949))
- Delete Receive at Warehouse entry on cancellation of Send to War… ([#24115](https://github.com/frappe/erpnext/pull/24115))
- Added TDS Payable account number and an error message ([#24065](https://github.com/frappe/erpnext/pull/24065))
- Override field_map for job card gantt ([#24155](https://github.com/frappe/erpnext/pull/24155))
- Old shopify order syncing date ([#23990](https://github.com/frappe/erpnext/pull/23990))
- Shipping chanrges not sync in erpnext from shopify ([#24114](https://github.com/frappe/erpnext/pull/24114))
- GSTR B2C report ([#24039](https://github.com/frappe/erpnext/pull/24039))
- Ignore cancelled entries in stock balance report ([#23757](https://github.com/frappe/erpnext/pull/23757))
- Stock ageing report not working ([#23923](https://github.com/frappe/erpnext/pull/23923))
- Incorrect assign to in Maintenance Schedule ([#23831](https://github.com/frappe/erpnext/pull/23831))
- Improve UX of DATEV report ([#23892](https://github.com/frappe/erpnext/pull/23892))
- Set SLA variance in seconds for Duration fieldtype ([#23765](https://github.com/frappe/erpnext/pull/23765))
- dDouble exception in payroll ([#24078](https://github.com/frappe/erpnext/pull/24078))
- Make asset dashboard charts public ([#23751](https://github.com/frappe/erpnext/pull/23751))
- Don't copy terms and discount from SO to PO ([#23903](https://github.com/frappe/erpnext/pull/23903))
- Ignore doctypes on company transaction delete ([#23864](https://github.com/frappe/erpnext/pull/23864))
- Error handling in Upload Attendance ([#23907](https://github.com/frappe/erpnext/pull/23907))
- Tax template update on customer address change ([#24160](https://github.com/frappe/erpnext/pull/24160))
- Not able to save bom ([#23910](https://github.com/frappe/erpnext/pull/23910))
- Enable Allow Auto Repeat for standard doctypes having auto_repeat field ([#23776](https://github.com/frappe/erpnext/pull/23776))
- Place of Supply fix in Sales Invoices ([#23785](https://github.com/frappe/erpnext/pull/23785))
- Opening invoices in GSTR-1 report ([#24117](https://github.com/frappe/erpnext/pull/24117))
- Partial serial no return issue ([#24208](https://github.com/frappe/erpnext/pull/24208))
- Import taxjar globally in the taxjar_integration module ([#24027](https://github.com/frappe/erpnext/pull/24027))
- Payroll attendance error ([#23887](https://github.com/frappe/erpnext/pull/23887))
- Loan application link on creating loan ([#23937](https://github.com/frappe/erpnext/pull/23937))
- POS item search includes non stock items ([#23914](https://github.com/frappe/erpnext/pull/23914))
- Paid amount in Sales Invoice POS return resets to 0 ([#24057](https://github.com/frappe/erpnext/pull/24057))
- Fiscal year can be shorter than 12 months ([#23838](https://github.com/frappe/erpnext/pull/23838))
- Loan repayment type option remove ([#23582](https://github.com/frappe/erpnext/pull/23582))
- Item wise tax calculation ([#23744](https://github.com/frappe/erpnext/pull/23744))
- Enabling track changes for stock settings ([#23982](https://github.com/frappe/erpnext/pull/23982))
- Added link of bank reconciliation and clearance in accounting desk page ([#23809](https://github.com/frappe/erpnext/pull/23809))
- Location data on Asset to use command(make_demo) ([#23825](https://github.com/frappe/erpnext/pull/23825))
- Handle Account and Item None not found in Opening Invoice Creation Tool ([#23559](https://github.com/frappe/erpnext/pull/23559))
- Multiple subcontracting issues ([#23662](https://github.com/frappe/erpnext/pull/23662))
- Sequence id override with workstation column ([#23810](https://github.com/frappe/erpnext/pull/23810))
- Leave policy dashboard fix and roles ([#24170](https://github.com/frappe/erpnext/pull/24170))
- Scan barcode does not update barcode item field in sales order ([#24090](https://github.com/frappe/erpnext/pull/24090))
- Item price duplicate checking ([#23408](https://github.com/frappe/erpnext/pull/23408))
- Tax template update on supplier change for India ([#24060](https://github.com/frappe/erpnext/pull/24060))
- Consumed qty logic for subcontracted raw materials ([#23314](https://github.com/frappe/erpnext/pull/23314))
- Finance book not getting added in journal Entry of asset value adjustment ([#24100](https://github.com/frappe/erpnext/pull/24100))
- Set proper state code in ewaybill JSON when GST category is SEZ ([#23953](https://github.com/frappe/erpnext/pull/23953))
- Copying po no when mapping doc ([#23729](https://github.com/frappe/erpnext/pull/23729))
- Duplicate items validation for POS Invoice when allow multiple items is disabled ([#23896](https://github.com/frappe/erpnext/pull/23896))
- Do not allow Company as accounting dimension ([#23749](https://github.com/frappe/erpnext/pull/23749))
- Validation for duplicate Tax Category ([#23978](https://github.com/frappe/erpnext/pull/23978))
- Therapy plan and session fixes ([#23817](https://github.com/frappe/erpnext/pull/23817))
- Pricing rule with transaction not working for additional product ([#24053](https://github.com/frappe/erpnext/pull/24053))
- Inpatient Medication Order and Entry fixes ([#23799](https://github.com/frappe/erpnext/pull/23799))
- Avoid using SQL query to get fiscal year dates ([#24050](https://github.com/frappe/erpnext/pull/24050))
- Auto Statewise gst tax template ([#23832](https://github.com/frappe/erpnext/pull/23832))
- On save sequence id column override with workstation ([#23812](https://github.com/frappe/erpnext/pull/23812))
- Multiple pricing rules are not working on selling side ([#22711](https://github.com/frappe/erpnext/pull/22711))
- Salary slip popup error ([#24192](https://github.com/frappe/erpnext/pull/24192))
- Multiple pricing rule with margin type as Percentage is not working ([#24204](https://github.com/frappe/erpnext/pull/24204))
- Allow statistical component in salary structure. ([#24424](https://github.com/frappe/erpnext/pull/24424))
- Set current asset value before calculating difference amount ([#24119](https://github.com/frappe/erpnext/pull/24119))
- To use Stock UoM in BOM Stock Report ([#24339](https://github.com/frappe/erpnext/pull/24339))
- Accounting entries of asset when submitting purchase receipt ([#24191](https://github.com/frappe/erpnext/pull/24191))
- Batch/Serial Selector for Scanned Batched Item ([#24338](https://github.com/frappe/erpnext/pull/24338))
- Link timesheets with corresponding projects ([#24346](https://github.com/frappe/erpnext/pull/24346))
- Material request wrong status issue ([#24019](https://github.com/frappe/erpnext/pull/24019))
- UX issues in e-invoicing ([#24358](https://github.com/frappe/erpnext/pull/24358))
- Company Wise Valuation Rate for RM in BOM ([#24324](https://github.com/frappe/erpnext/pull/24324))
- Stock ageing should not take cancelled stock entries. ([#24437](https://github.com/frappe/erpnext/pull/24437))
- Partial loan security unpledging ([#24252](https://github.com/frappe/erpnext/pull/24252))
- Asset depreciation ledger ([#24226](https://github.com/frappe/erpnext/pull/24226))
- Back Update from QC based on Batch No ([#24329](https://github.com/frappe/erpnext/pull/24329))
- Fix for not having fiscal year while creating new company ([#24130](https://github.com/frappe/erpnext/pull/24130))
- E-invoice print format not showing other charges ([#24474](https://github.com/frappe/erpnext/pull/24474))
- Tax template update on customer address change ([#24146](https://github.com/frappe/erpnext/pull/24146))
- Do not manufacture same serial no multiple times ([#24164](https://github.com/frappe/erpnext/pull/24164))
- Ignore group cost center validation for period closing voucher ([#24375](https://github.com/frappe/erpnext/pull/24375))
- Partial serial no return issue ([#24207](https://github.com/frappe/erpnext/pull/24207))
- GSTR-1 double entry issue ([#24376](https://github.com/frappe/erpnext/pull/24376))
- Not able to create dunning from sales invoice ([#24349](https://github.com/frappe/erpnext/pull/24349))
- Set company in leave allocation and leave ledger entry ([#24296](https://github.com/frappe/erpnext/pull/24296))
- Allow leave policy assignment to be canceled. ([#24265](https://github.com/frappe/erpnext/pull/24265))
- Removed all day event from shift assignment calendar ([#24397](https://github.com/frappe/erpnext/pull/24397))
- Tax calculation on salary slip for the first month ([#24272](https://github.com/frappe/erpnext/pull/24272))
- Validate tax template for tax category ([#24402](https://github.com/frappe/erpnext/pull/24402))
- Numeric/Non-numeric QI UX ([#24517](https://github.com/frappe/erpnext/pull/24517))
- Finished good produced qty validation ([#24220](https://github.com/frappe/erpnext/pull/24220))
- Incorrect serial no in the subcontracted purchase receipt ([#24354](https://github.com/frappe/erpnext/pull/24354))
- Don't validate warehouse values between Material Request and Stock Entry ([#24294](https://github.com/frappe/erpnext/pull/24294))
- Don't cancel job card if manufacturing entry has made ([#24063](https://github.com/frappe/erpnext/pull/24063))
- Subscription prepaid date validation ([#24356](https://github.com/frappe/erpnext/pull/24356))
- Payment Period based on invoice date report fix/refactor ([#24378](https://github.com/frappe/erpnext/pull/24378))
- Drop ship partial order fixed ([#24072](https://github.com/frappe/erpnext/pull/24072))
- Payment entry multi-currency issue ([#24332](https://github.com/frappe/erpnext/pull/24332))
- Multiple pricing rule issue ([#24515](https://github.com/frappe/erpnext/pull/24515))
- Last purchase rate not updating when voucher cancelled if only one voucher is present ([#24322](https://github.com/frappe/erpnext/pull/24322))
- Do not cancel reference document on Quality Inspection cancellation ([#24197](https://github.com/frappe/erpnext/pull/24197))
- Refactored fetching & validating address from erpnext rather than gst portal ([#24297](https://github.com/frappe/erpnext/pull/24297))
- Opportunity Status fix ([#22944](https://github.com/frappe/erpnext/pull/22944))
- Fixed stock and account balance syncing ([#24644](https://github.com/frappe/erpnext/pull/24644))
- Fixed incorrect stock ledger qty in the stock ledger report and bin ([#24649](https://github.com/frappe/erpnext/pull/24649))
- Fixed Consolidated Financial Statement report ([#24580](https://github.com/frappe/erpnext/pull/24580))
- Repost incompleted backdated transactions ([#24991](https://github.com/frappe/erpnext/pull/24991))
- Unequal debit and credit issue on RCM Invoice ([#24838](https://github.com/frappe/erpnext/pull/24838))
- Period list for exponential smoothing forecasting report ([#24983](https://github.com/frappe/erpnext/pull/24983))
- POS Opening Entry with empty balance detail rows ([#24891](https://github.com/frappe/erpnext/pull/24891))
- Use account_name only in consolidated report ([#24840](https://github.com/frappe/erpnext/pull/24840))
- Validation of job card in stock entry ([#24882](https://github.com/frappe/erpnext/pull/24882))
- Incorrect Nil Exempt and Non GST amount in GSTR3B report ([#24918](https://github.com/frappe/erpnext/pull/24918))
- TDS check getting checked after reload ([#24973](https://github.com/frappe/erpnext/pull/24973))
- Membership and Donation API fixes ([#24900](https://github.com/frappe/erpnext/pull/24900))
- Allow zero valuation in stock reconciliation ([#24985](https://github.com/frappe/erpnext/pull/24985))
- Simplified logic for additional salary ([#24907](https://github.com/frappe/erpnext/pull/24907))
- Allow to select item code in batch naming ([#24825](https://github.com/frappe/erpnext/pull/24825))
- Membership renewal validation (#24963) ([#24964](https://github.com/frappe/erpnext/pull/24964))
</details>

View File

@ -325,7 +325,7 @@ def get_delivery_notes_to_be_billed(doctype, txt, searchfield, start, page_len,
and status not in ("Stopped", "Closed") %(fcond)s
and (
(`tabDelivery Note`.is_return = 0 and `tabDelivery Note`.per_billed < 100)
or `tabDelivery Note`.grand_total = 0
or (`tabDelivery Note`.grand_total = 0 and `tabDelivery Note`.per_billed < 100)
or (
`tabDelivery Note`.is_return = 1
and return_against in (select name from `tabDelivery Note` where per_billed < 100)

View File

@ -406,8 +406,7 @@ class StockController(AccountsController):
def set_rate_of_stock_uom(self):
if self.doctype in ["Purchase Receipt", "Purchase Invoice", "Purchase Order", "Sales Invoice", "Sales Order", "Delivery Note", "Quotation"]:
for d in self.get("items"):
if d.conversion_factor:
d.stock_uom_rate = d.rate / d.conversion_factor
d.stock_uom_rate = d.rate / (d.conversion_factor or 1)
def validate_internal_transfer(self):
if self.doctype in ('Sales Invoice', 'Delivery Note', 'Purchase Invoice', 'Purchase Receipt') \

View File

@ -113,10 +113,12 @@ class calculate_taxes_and_totals(object):
item.rate_with_margin, item.base_rate_with_margin = self.calculate_margin(item)
if flt(item.rate_with_margin) > 0:
item.rate = flt(item.rate_with_margin * (1.0 - (item.discount_percentage / 100.0)), item.precision("rate"))
if item.discount_amount and not item.discount_percentage:
item.rate -= item.discount_amount
item.rate = item.rate_with_margin - item.discount_amount
else:
item.discount_amount = item.rate_with_margin - item.rate
elif flt(item.price_list_rate) > 0:
item.discount_amount = item.price_list_rate - item.rate
elif flt(item.price_list_rate) > 0 and not item.discount_amount:
@ -147,7 +149,9 @@ class calculate_taxes_and_totals(object):
validate_taxes_and_charges(tax)
validate_inclusive_tax(tax, self.doc)
tax.item_wise_tax_detail = {}
if not self.doc.get('is_consolidated'):
tax.item_wise_tax_detail = {}
tax_fields = ["total", "tax_amount_after_discount_amount",
"tax_amount_for_current_item", "grand_total_for_current_item",
"tax_fraction_for_current_item", "grand_total_fraction_for_current_item"]
@ -287,10 +291,13 @@ class calculate_taxes_and_totals(object):
# set precision in the last item iteration
if n == len(self.doc.get("items")) - 1:
self.round_off_totals(tax)
self._set_in_company_currency(tax,
["tax_amount", "tax_amount_after_discount_amount"])
self.round_off_base_values(tax)
self.set_cumulative_total(i, tax)
self._set_in_company_currency(tax,
["total", "tax_amount", "tax_amount_after_discount_amount"])
self._set_in_company_currency(tax, ["total"])
# adjust Discount Amount loss in last tax iteration
if i == (len(self.doc.get("taxes")) - 1) and self.discount_amount_applied \
@ -337,18 +344,11 @@ class calculate_taxes_and_totals(object):
elif tax.charge_type == "On Item Quantity":
current_tax_amount = tax_rate * item.qty
current_tax_amount = self.get_final_current_tax_amount(tax, current_tax_amount)
self.set_item_wise_tax(item, tax, tax_rate, current_tax_amount)
if not self.doc.get("is_consolidated"):
self.set_item_wise_tax(item, tax, tax_rate, current_tax_amount)
return current_tax_amount
def get_final_current_tax_amount(self, tax, current_tax_amount):
# Some countries need individual tax components to be rounded
# Handeled via regional doctypess
if tax.account_head in frappe.flags.round_off_applicable_accounts:
current_tax_amount = round(current_tax_amount, 0)
return current_tax_amount
def set_item_wise_tax(self, item, tax, tax_rate, current_tax_amount):
# store tax breakup for each item
key = item.item_code or item.item_name
@ -359,10 +359,20 @@ class calculate_taxes_and_totals(object):
tax.item_wise_tax_detail[key] = [tax_rate,flt(item_wise_tax_amount)]
def round_off_totals(self, tax):
if tax.account_head in frappe.flags.round_off_applicable_accounts:
tax.tax_amount = round(tax.tax_amount, 0)
tax.tax_amount_after_discount_amount = round(tax.tax_amount_after_discount_amount, 0)
tax.tax_amount = flt(tax.tax_amount, tax.precision("tax_amount"))
tax.tax_amount_after_discount_amount = flt(tax.tax_amount_after_discount_amount,
tax.precision("tax_amount"))
def round_off_base_values(self, tax):
# Round off to nearest integer based on regional settings
if tax.account_head in frappe.flags.round_off_applicable_accounts:
tax.base_tax_amount = round(tax.base_tax_amount, 0)
tax.base_tax_amount_after_discount_amount = round(tax.base_tax_amount_after_discount_amount, 0)
def manipulate_grand_total_for_inclusive_tax(self):
# if fully inclusive taxes and diff
if self.doc.get("taxes") and any([cint(t.included_in_print_rate) for t in self.doc.get("taxes")]):
@ -440,8 +450,9 @@ class calculate_taxes_and_totals(object):
self._set_in_company_currency(self.doc, ["rounding_adjustment", "rounded_total"])
def _cleanup(self):
for tax in self.doc.get("taxes"):
tax.item_wise_tax_detail = json.dumps(tax.item_wise_tax_detail, separators=(',', ':'))
if not self.doc.get('is_consolidated'):
for tax in self.doc.get("taxes"):
tax.item_wise_tax_detail = json.dumps(tax.item_wise_tax_detail, separators=(',', ':'))
def set_discount_amount(self):
if self.doc.additional_discount_percentage:

View File

@ -25,7 +25,7 @@ def get_transaction_list(doctype, txt=None, filters=None, limit_start=0, limit_p
if not filters: filters = []
if doctype in ['Supplier Quotation', 'Purchase Invoice', 'Quotation']:
if doctype in ['Supplier Quotation', 'Purchase Invoice']:
filters.append((doctype, 'docstatus', '<', 2))
else:
filters.append((doctype, 'docstatus', '=', 1))

View File

@ -22,7 +22,7 @@ class ShopifySettings(unittest.TestCase):
frappe.db.set_value('Stock Settings', None, 'allow_negative_stock', 1)
# use the fixture data
import_doc(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"))
frappe.reload_doctype("Customer")
frappe.reload_doctype("Sales Order")

View File

@ -39,11 +39,13 @@ frappe.ui.form.on('Patient Assessment', {
},
set_score_range: function(frm) {
let options = [];
let options = [''];
for(let i = frm.doc.scale_min; i <= frm.doc.scale_max; i++) {
options.push(i);
}
frappe.meta.get_docfield('Patient Assessment Sheet', 'score', frm.doc.name).options = [''].concat(options);
frm.fields_dict.assessment_sheet.grid.update_docfield_property(
'score', 'options', options
);
},
calculate_total_score: function(frm, cdt, cdn) {
@ -83,4 +85,4 @@ frappe.ui.form.on('Patient Assessment Sheet', {
score: function(frm, cdt, cdn) {
frm.events.calculate_total_score(frm, cdt, cdn);
}
});
});

View File

@ -58,8 +58,12 @@ frappe.ui.form.on('Therapy Plan', {
}
if (frm.doc.therapy_plan_template) {
frappe.meta.get_docfield('Therapy Plan Detail', 'therapy_type', frm.doc.name).read_only = 1;
frappe.meta.get_docfield('Therapy Plan Detail', 'no_of_sessions', frm.doc.name).read_only = 1;
frm.fields_dict.therapy_plan_details.grid.update_docfield_property(
'therapy_type', 'read_only', 1
);
frm.fields_dict.therapy_plan_details.grid.update_docfield_property(
'no_of_sessions', 'read_only', 1
);
}
},
@ -126,4 +130,4 @@ frappe.ui.form.on('Therapy Plan Detail', {
frm.set_value('total_sessions', total);
refresh_field('total_sessions');
}
});
});

View File

@ -262,7 +262,8 @@ doc_events = {
],
"on_trash": "erpnext.regional.check_deletion_permission",
"validate": [
"erpnext.regional.india.utils.validate_document_name"
"erpnext.regional.india.utils.validate_document_name",
"erpnext.regional.india.utils.update_taxable_values"
]
},
"Purchase Invoice": {

View File

@ -35,7 +35,8 @@ class Attendance(Document):
and docstatus != 2
""", (self.employee, getdate(self.attendance_date), self.name))
if res:
frappe.throw(_("Attendance for employee {0} is already marked").format(self.employee))
frappe.throw(_("Attendance for employee {0} is already marked for the date {1}").format(
frappe.bold(self.employee), frappe.bold(self.attendance_date)))
def check_leave_record(self):
leave_record = frappe.db.sql("""

View File

@ -200,7 +200,7 @@
],
"is_submittable": 1,
"links": [],
"modified": "2021-03-31 14:42:47.321368",
"modified": "2021-03-31 22:31:53.746659",
"modified_by": "Administrator",
"module": "HR",
"name": "Employee Advance",

View File

@ -154,7 +154,7 @@
],
"is_submittable": 1,
"links": [],
"modified": "2021-03-31 14:45:27.948207",
"modified": "2021-03-31 22:32:55.492327",
"modified_by": "Administrator",
"module": "HR",
"name": "Leave Encashment",

View File

@ -187,4 +187,4 @@ def get_job_offer(ja_list):
else:
ja_joff_map[offer.job_applicant].append(offer)
return ja_joff_map
return ja_joff_map

View File

@ -23,6 +23,7 @@
"rate_of_interest",
"is_secured_loan",
"disbursement_date",
"closure_date",
"disbursed_amount",
"column_break_11",
"maximum_loan_amount",
@ -348,12 +349,18 @@
"no_copy": 1,
"options": "Company:company:default_currency",
"read_only": 1
},
{
"fieldname": "closure_date",
"fieldtype": "Date",
"label": "Closure Date",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2020-11-24 12:27:23.208240",
"modified": "2021-04-10 09:28:21.946972",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan",

View File

@ -523,33 +523,7 @@ class TestLoan(unittest.TestCase):
self.assertEqual(flt(repayment_entry.total_interest_paid, 0), flt(interest_amount, 0))
def test_penalty(self):
pledge = [{
"loan_security": "Test Security 1",
"qty": 4000.00
}]
loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge)
create_pledge(loan_application)
loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01')
loan.submit()
self.assertEquals(loan.loan_amount, 1000000)
first_date = '2019-10-01'
last_date = '2019-10-30'
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date)
process_loan_interest_accrual_for_demand_loans(posting_date = last_date)
amounts = calculate_amounts(loan.name, add_days(last_date, 1))
paid_amount = amounts['interest_amount']/2
repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5),
paid_amount)
repayment_entry.submit()
loan, amounts = create_loan_scenario_for_penalty(self)
# 30 days - grace period
penalty_days = 30 - 4
penalty_applicable_amount = flt(amounts['interest_amount']/2)
@ -559,8 +533,28 @@ class TestLoan(unittest.TestCase):
calculated_penalty_amount = frappe.db.get_value('Loan Interest Accrual',
{'process_loan_interest_accrual': process, 'loan': loan.name}, 'penalty_amount')
self.assertEquals(loan.loan_amount, 1000000)
self.assertEquals(calculated_penalty_amount, penalty_amount)
def test_penalty_repayment(self):
loan, dummy = create_loan_scenario_for_penalty(self)
amounts = calculate_amounts(loan.name, '2019-11-30 00:00:00')
first_penalty = 10000
second_penalty = amounts['penalty_amount'] - 10000
repayment_entry = create_repayment_entry(loan.name, self.applicant2, '2019-11-30 00:00:00', 10000)
repayment_entry.submit()
amounts = calculate_amounts(loan.name, '2019-11-30 00:00:01')
self.assertEquals(amounts['penalty_amount'], second_penalty)
repayment_entry = create_repayment_entry(loan.name, self.applicant2, '2019-11-30 00:00:01', second_penalty)
repayment_entry.submit()
amounts = calculate_amounts(loan.name, '2019-11-30 00:00:02')
self.assertEquals(amounts['penalty_amount'], 0)
def test_loan_write_off_limit(self):
pledge = [{
"loan_security": "Test Security 1",
@ -651,6 +645,32 @@ class TestLoan(unittest.TestCase):
amounts = calculate_amounts(loan.name, add_days(last_date, 5))
self.assertEquals(flt(amounts['pending_principal_amount'], 0), 0)
def create_loan_scenario_for_penalty(doc):
pledge = [{
"loan_security": "Test Security 1",
"qty": 4000.00
}]
loan_application = create_loan_application('_Test Company', doc.applicant2, 'Demand Loan', pledge)
create_pledge(loan_application)
loan = create_demand_loan(doc.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01')
loan.submit()
first_date = '2019-10-01'
last_date = '2019-10-30'
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date)
process_loan_interest_accrual_for_demand_loans(posting_date = last_date)
amounts = calculate_amounts(loan.name, add_days(last_date, 1))
paid_amount = amounts['interest_amount']/2
repayment_entry = create_repayment_entry(loan.name, doc.applicant2, add_days(last_date, 5),
paid_amount)
repayment_entry.submit()
return loan, amounts
def create_loan_accounts():
if not frappe.db.exists("Account", "Loans and Advances (Assets) - _TC"):

View File

@ -20,6 +20,10 @@
"cost_center",
"customer_details_section",
"bank_account",
"disbursement_references_section",
"reference_date",
"column_break_17",
"reference_number",
"amended_from"
],
"fields": [
@ -126,12 +130,31 @@
{
"fieldname": "column_break_8",
"fieldtype": "Column Break"
},
{
"fieldname": "disbursement_references_section",
"fieldtype": "Section Break",
"label": "Disbursement References"
},
{
"fieldname": "reference_date",
"fieldtype": "Date",
"label": "Reference Date"
},
{
"fieldname": "column_break_17",
"fieldtype": "Column Break"
},
{
"fieldname": "reference_number",
"fieldtype": "Data",
"label": "Reference Number"
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2020-11-06 10:04:30.882322",
"modified": "2021-04-10 10:03:41.502210",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Disbursement",

View File

@ -239,14 +239,16 @@
{
"fieldname": "total_penalty_paid",
"fieldtype": "Currency",
"hidden": 1,
"label": "Total Penalty Paid",
"options": "Company:company:default_currency"
"options": "Company:company:default_currency",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2021-04-05 13:45:19.137896",
"modified": "2021-04-10 10:00:31.859076",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Repayment",

View File

@ -75,7 +75,7 @@ class LoanRepayment(AccountsController):
"docstatus": 1, "against_loan": self.against_loan}, 'posting_date')
if future_repayment_date:
frappe.throw("Repayment already made till date {0}".format(getdate(future_repayment_date)))
frappe.throw("Repayment already made till date {0}".format(get_datetime(future_repayment_date)))
def validate_amount(self):
precision = cint(frappe.db.get_default("currency_precision")) or 2
@ -83,10 +83,6 @@ class LoanRepayment(AccountsController):
if not self.amount_paid:
frappe.throw(_("Amount paid cannot be zero"))
if not self.shortfall_amount and self.amount_paid < self.penalty_amount:
msg = _("Paid amount cannot be less than {0}").format(self.penalty_amount)
frappe.throw(msg)
def book_unaccrued_interest(self):
precision = cint(frappe.db.get_default("currency_precision")) or 2
if self.total_interest_paid > self.interest_payable:
@ -231,6 +227,14 @@ class LoanRepayment(AccountsController):
gle_map = []
loan_details = frappe.get_doc("Loan", self.against_loan)
if self.shortfall_amount and self.amount_paid > self.shortfall_amount:
remarks = _("Shortfall Repayment of {0}.\nRepayment against Loan: {1}").format(self.shortfall_amount,
self.against_loan)
elif self.shortfall_amount:
remarks = _("Shortfall Repayment of {0}").format(self.shortfall_amount)
else:
remarks = _("Repayment against Loan: ") + self.against_loan
if self.total_penalty_paid:
gle_map.append(
self.get_gl_dict({
@ -271,7 +275,7 @@ class LoanRepayment(AccountsController):
"debit_in_account_currency": self.amount_paid,
"against_voucher_type": "Loan",
"against_voucher": self.against_loan,
"remarks": _("Repayment against Loan: ") + self.against_loan,
"remarks": remarks,
"cost_center": self.cost_center,
"posting_date": getdate(self.posting_date)
})
@ -287,7 +291,7 @@ class LoanRepayment(AccountsController):
"credit_in_account_currency": self.amount_paid,
"against_voucher_type": "Loan",
"against_voucher": self.against_loan,
"remarks": _("Repayment against Loan: ") + self.against_loan,
"remarks": remarks,
"cost_center": self.cost_center,
"posting_date": getdate(self.posting_date)
})
@ -338,6 +342,18 @@ def get_accrued_interest_entries(against_loan, posting_date=None):
return unpaid_accrued_entries
def get_penalty_details(against_loan):
penalty_details = frappe.db.sql("""
SELECT posting_date, (penalty_amount - total_penalty_paid) as pending_penalty_amount
FROM `tabLoan Repayment` where posting_date >= (SELECT MAX(posting_date) from `tabLoan Repayment`
where against_loan = %s) and docstatus = 1 and against_loan = %s
""", (against_loan, against_loan))
if penalty_details:
return penalty_details[0][0], flt(penalty_details[0][1])
else:
return None, 0
# This function returns the amounts that are payable at the time of loan repayment based on posting date
# So it pulls all the unpaid Loan Interest Accrual Entries and calculates the penalty if applicable
@ -348,6 +364,7 @@ def get_amounts(amounts, against_loan, posting_date):
loan_type_details = frappe.get_doc("Loan Type", against_loan_doc.loan_type)
accrued_interest_entries = get_accrued_interest_entries(against_loan_doc.name, posting_date)
computed_penalty_date, pending_penalty_amount = get_penalty_details(against_loan)
pending_accrual_entries = {}
total_pending_interest = 0
@ -362,8 +379,13 @@ def get_amounts(amounts, against_loan, posting_date):
# and if no_of_late days are positive then penalty is levied
due_date = add_days(entry.posting_date, 1)
no_of_late_days = date_diff(posting_date,
add_days(due_date, loan_type_details.grace_period_in_days)) + 1
due_date_after_grace_period = add_days(due_date, loan_type_details.grace_period_in_days)
# Consider one day after already calculated penalty
if computed_penalty_date and getdate(computed_penalty_date) >= due_date_after_grace_period:
due_date_after_grace_period = add_days(computed_penalty_date, 1)
no_of_late_days = date_diff(posting_date, due_date_after_grace_period) + 1
if no_of_late_days > 0 and (not against_loan_doc.repay_from_salary) and entry.accrual_type == 'Regular':
penalty_amount += (entry.interest_amount * (loan_type_details.penalty_interest_rate / 100) * no_of_late_days)
@ -401,7 +423,7 @@ def get_amounts(amounts, against_loan, posting_date):
amounts["pending_principal_amount"] = flt(pending_principal_amount, precision)
amounts["payable_principal_amount"] = flt(payable_principal_amount, precision)
amounts["interest_amount"] = flt(total_pending_interest, precision)
amounts["penalty_amount"] = flt(penalty_amount, precision)
amounts["penalty_amount"] = flt(penalty_amount + pending_penalty_amount, precision)
amounts["payable_amount"] = flt(payable_principal_amount + total_pending_interest + penalty_amount, precision)
amounts["pending_accrual_entries"] = pending_accrual_entries
amounts["unaccrued_interest"] = flt(unaccrued_interest, precision)

View File

@ -6,7 +6,7 @@ from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import get_datetime, flt
from frappe.utils import get_datetime, flt, getdate
import json
from six import iteritems
from erpnext.loan_management.doctype.loan_security_price.loan_security_price import get_loan_security_price
@ -113,7 +113,11 @@ class LoanSecurityUnpledge(Document):
pledged_qty += qty
if not pledged_qty:
frappe.db.set_value('Loan', self.loan, 'status', 'Closed')
frappe.db.set_value('Loan', self.loan,
{
'status': 'Closed',
'closure_date': getdate()
})
@frappe.whitelist()
def get_pledged_security_qty(loan):

View File

@ -93,15 +93,15 @@ class TestBOM(unittest.TestCase):
base_raw_material_cost = raw_material_cost * flt(bom.conversion_rate, bom.precision("conversion_rate"))
base_op_cost = op_cost * flt(bom.conversion_rate, bom.precision("conversion_rate"))
# test amounts in selected currency
self.assertEqual(bom.operating_cost, op_cost)
self.assertEqual(bom.raw_material_cost, raw_material_cost)
self.assertEqual(bom.total_cost, raw_material_cost + op_cost)
# test amounts in selected currency, almostEqual checks for 7 digits by default
self.assertAlmostEqual(bom.operating_cost, op_cost)
self.assertAlmostEqual(bom.raw_material_cost, raw_material_cost)
self.assertAlmostEqual(bom.total_cost, raw_material_cost + op_cost)
# test amounts in selected currency
self.assertEqual(bom.base_operating_cost, base_op_cost)
self.assertEqual(bom.base_raw_material_cost, base_raw_material_cost)
self.assertEqual(bom.base_total_cost, base_raw_material_cost + base_op_cost)
self.assertAlmostEqual(bom.base_operating_cost, base_op_cost)
self.assertAlmostEqual(bom.base_raw_material_cost, base_raw_material_cost)
self.assertAlmostEqual(bom.base_total_cost, base_raw_material_cost + base_op_cost)
def test_bom_cost_multi_uom_multi_currency_based_on_price_list(self):
frappe.db.set_value("Price List", "_Test Price List", "price_not_uom_dependent", 1)

View File

@ -433,6 +433,7 @@ def make_material_request(source_name, target_doc=None):
def make_stock_entry(source_name, target_doc=None):
def update_item(obj, target, source_parent):
target.t_warehouse = source_parent.wip_warehouse
target.conversion_factor = 1
def set_missing_values(source, target):
target.purpose = "Material Transfer for Manufacture"

View File

@ -11,10 +11,9 @@ frappe.ui.form.on('Routing', {
},
display_sequence_id_column: function(frm) {
frappe.meta.get_docfield("BOM Operation", "sequence_id",
frm.doc.name).in_list_view = true;
frm.fields_dict.operations.grid.refresh();
frm.fields_dict.operations.grid.update_docfield_property(
'sequence_id', 'in_list_view', 1
);
},
calculate_operating_cost: function(frm, child) {
@ -69,4 +68,4 @@ frappe.ui.form.on('BOM Operation', {
const d = locals[cdt][cdn];
frm.events.calculate_operating_cost(frm, d);
}
});
});

View File

@ -19,7 +19,7 @@
"documentation_url": "https://docs.erpnext.com/docs/user/manual/en/manufacturing",
"idx": 0,
"is_complete": 0,
"modified": "2020-07-08 14:05:56.197563",
"modified": "2020-06-29 20:25:36.899106",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Manufacturing",
@ -53,4 +53,4 @@
"subtitle": "Products, Raw Materials, BOM, Work Order, and more.",
"success_message": "Manufacturing module is all set up!",
"title": "Let's Set Up the Manufacturing Module."
}
}

View File

@ -756,11 +756,18 @@ erpnext.patches.v13_0.update_payment_terms_outstanding
erpnext.patches.v12_0.add_state_code_for_ladakh
erpnext.patches.v13_0.item_reposting_for_incorrect_sl_and_gl
erpnext.patches.v13_0.delete_old_bank_reconciliation_doctypes
erpnext.patches.v13_0.update_vehicle_no_reqd_condition
erpnext.patches.v12_0.update_vehicle_no_reqd_condition
erpnext.patches.v12_0.add_einvoice_status_field #2021-03-17
erpnext.patches.v12_0.add_einvoice_summary_report_permissions
erpnext.patches.v13_0.setup_fields_for_80g_certificate_and_donation
erpnext.patches.v13_0.rename_membership_settings_to_non_profit_settings
erpnext.patches.v13_0.setup_gratuity_rule_for_india_and_uae
erpnext.patches.v13_0.setup_uae_vat_fields
execute:frappe.db.set_value('System Settings', None, 'app_name', 'ERPNext')
erpnext.patches.v12_0.add_company_link_to_einvoice_settings
erpnext.patches.v13_0.rename_discharge_date_in_ip_record
erpnext.patches.v12_0.create_taxable_value_field
erpnext.patches.v12_0.add_gst_category_in_delivery_note
erpnext.patches.v12_0.purchase_receipt_status
erpnext.patches.v13_0.fix_non_unique_represents_company
erpnext.patches.v12_0.add_document_type_field_for_italy_einvoicing

View File

@ -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) == 1:
frappe.db.set_value('E Invoice User', creds.get('name'), 'company', company_name[0][0])

View File

@ -0,0 +1,18 @@
from __future__ import unicode_literals
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
import frappe
def execute():
company = frappe.get_all('Company', filters = {'country': 'Italy'})
if not company:
return
custom_fields = {
'Sales Invoice': [
dict(fieldname='type_of_document', label='Type of Document',
fieldtype='Select', insert_after='customer_fiscal_code',
options='\nTD01\nTD02\nTD03\nTD04\nTD05\nTD06\nTD16\nTD17\nTD18\nTD19\nTD20\nTD21\nTD22\nTD23\nTD24\nTD25\nTD26\nTD27'),
]
}
create_custom_fields(custom_fields, update=True)

View File

@ -0,0 +1,69 @@
from __future__ import unicode_literals
import json
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
# move hidden einvoice fields to a different section
custom_fields = {
'Sales Invoice': [
dict(fieldname='einvoice_section', label='E-Invoice Fields', fieldtype='Section Break', insert_after='gst_vehicle_type',
print_hide=1, hidden=1),
dict(fieldname='ack_no', label='Ack. No.', fieldtype='Data', read_only=1, hidden=1, insert_after='einvoice_section',
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_cancel_date', label='Cancel Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_date',
no_copy=1, print_hide=1),
dict(fieldname='signed_einvoice', label='Signed E-Invoice', fieldtype='Code', options='JSON', hidden=1, insert_after='irn_cancel_date',
no_copy=1, print_hide=1, read_only=1),
dict(fieldname='signed_qr_code', label='Signed QRCode', fieldtype='Code', options='JSON', hidden=1, insert_after='signed_einvoice',
no_copy=1, print_hide=1, read_only=1),
dict(fieldname='qrcode_image', label='QRCode', fieldtype='Attach Image', hidden=1, insert_after='signed_qr_code',
no_copy=1, print_hide=1, read_only=1),
dict(fieldname='einvoice_status', label='E-Invoice Status', fieldtype='Select', insert_after='qrcode_image',
options='\nPending\nGenerated\nCancelled\nFailed', default=None, hidden=1, no_copy=1, print_hide=1, read_only=1),
dict(fieldname='failure_description', label='E-Invoice Failure Description', fieldtype='Code', options='JSON',
hidden=1, insert_after='einvoice_status', no_copy=1, print_hide=1, read_only=1)
]
}
create_custom_fields(custom_fields, update=True)
if frappe.db.exists('E Invoice Settings') and frappe.db.get_single_value('E Invoice Settings', 'enable'):
frappe.db.sql('''
UPDATE `tabSales Invoice` SET einvoice_status = 'Pending'
WHERE
posting_date >= '2021-04-01'
AND ifnull(irn, '') = ''
AND ifnull(`billing_address_gstin`, '') != ifnull(`company_gstin`, '')
AND ifnull(gst_category, '') in ('Registered Regular', 'SEZ', 'Overseas', 'Deemed Export')
''')
# set appropriate statuses
frappe.db.sql('''UPDATE `tabSales Invoice` SET einvoice_status = 'Generated'
WHERE ifnull(irn, '') != '' AND ifnull(irn_cancelled, 0) = 0''')
frappe.db.sql('''UPDATE `tabSales Invoice` SET einvoice_status = 'Cancelled'
WHERE ifnull(irn_cancelled, 0) = 1''')
# set correct acknowledgement in e-invoices
einvoices = frappe.get_all('Sales Invoice', {'irn': ['is', 'set']}, ['name', 'signed_einvoice'])
if einvoices:
for inv in einvoices:
signed_einvoice = inv.get('signed_einvoice')
if signed_einvoice:
signed_einvoice = json.loads(signed_einvoice)
frappe.db.set_value('Sales Invoice', inv.get('name'), 'ack_no', signed_einvoice.get('AckNo'), update_modified=False)
frappe.db.set_value('Sales Invoice', inv.get('name'), 'ack_date', signed_einvoice.get('AckDt'), update_modified=False)

View File

@ -0,0 +1,18 @@
from __future__ import unicode_literals
import frappe
def execute():
company = frappe.get_all('Company', filters = {'country': 'India'})
if not company:
return
if frappe.db.exists('Report', 'E-Invoice Summary') and \
not frappe.db.get_value('Custom Role', dict(report='E-Invoice Summary')):
frappe.get_doc(dict(
doctype='Custom Role',
report='E-Invoice Summary',
roles= [
dict(role='Accounts User'),
dict(role='Accounts Manager')
]
)).insert()

View File

@ -0,0 +1,19 @@
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 = {
'Delivery Note': [
dict(fieldname='gst_category', label='GST Category',
fieldtype='Select', insert_after='gst_vehicle_type', print_hide=1,
options='\nRegistered Regular\nRegistered Composition\nUnregistered\nSEZ\nOverseas\nConsumer\nDeemed Export\nUIN Holders',
fetch_from='customer.gst_category', fetch_if_empty=1),
]
}
create_custom_fields(custom_fields, update=True)

View 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)

View File

@ -1,6 +1,7 @@
import frappe
def execute():
frappe.reload_doc('custom', 'doctype', 'custom_field')
company = frappe.get_all('Company', filters = {'country': 'India'})
if not company:
return

View File

@ -8,36 +8,39 @@ from erpnext.regional.india.setup import setup
def execute():
doctypes = ['salary_component',
'Employee Tax Exemption Declaration',
'Employee Tax Exemption Proof Submission',
'Employee Tax Exemption Declaration Category',
'Employee Tax Exemption Proof Submission Detail'
]
doctypes = ['salary_component',
'Employee Tax Exemption Declaration',
'Employee Tax Exemption Proof Submission',
'Employee Tax Exemption Declaration Category',
'Employee Tax Exemption Proof Submission Detail',
'gratuity_rule',
'gratuity_rule_slab',
'gratuity_applicable_component'
]
for doctype in doctypes:
frappe.reload_doc('Payroll', 'doctype', doctype)
for doctype in doctypes:
frappe.reload_doc('Payroll', 'doctype', doctype)
reports = ['Professional Tax Deductions', 'Provident Fund Deductions']
for report in reports:
frappe.reload_doc('Regional', 'Report', report)
frappe.reload_doc('Regional', 'Report', report)
reports = ['Professional Tax Deductions', 'Provident Fund Deductions']
for report in reports:
frappe.reload_doc('Regional', 'Report', report)
frappe.reload_doc('Regional', 'Report', report)
if erpnext.get_region() == "India":
setup(patch=True)
if erpnext.get_region() == "India":
setup(patch=True)
if frappe.db.exists("Salary Component", "Income Tax"):
frappe.db.set_value("Salary Component", "Income Tax", "is_income_tax_component", 1)
if frappe.db.exists("Salary Component", "TDS"):
frappe.db.set_value("Salary Component", "TDS", "is_income_tax_component", 1)
if frappe.db.exists("Salary Component", "Income Tax"):
frappe.db.set_value("Salary Component", "Income Tax", "is_income_tax_component", 1)
if frappe.db.exists("Salary Component", "TDS"):
frappe.db.set_value("Salary Component", "TDS", "is_income_tax_component", 1)
components = frappe.db.sql("select name from `tabSalary Component` where variable_based_on_taxable_salary = 1", as_dict=1)
for component in components:
frappe.db.set_value("Salary Component", component.name, "is_income_tax_component", 1)
components = frappe.db.sql("select name from `tabSalary Component` where variable_based_on_taxable_salary = 1", as_dict=1)
for component in components:
frappe.db.set_value("Salary Component", component.name, "is_income_tax_component", 1)
if erpnext.get_region() == "India":
if frappe.db.exists("Salary Component", "Provident Fund"):
frappe.db.set_value("Salary Component", "Provident Fund", "component_type", "Provident Fund")
if frappe.db.exists("Salary Component", "Professional Tax"):
frappe.db.set_value("Salary Component", "Professional Tax", "component_type", "Professional Tax")
if erpnext.get_region() == "India":
if frappe.db.exists("Salary Component", "Provident Fund"):
frappe.db.set_value("Salary Component", "Provident Fund", "component_type", "Provident Fund")
if frappe.db.exists("Salary Component", "Professional Tax"):
frappe.db.set_value("Salary Component", "Professional Tax", "component_type", "Professional Tax")

View File

@ -11,4 +11,8 @@ def execute():
if not company:
return
frappe.reload_doc('accounts', 'doctype', 'pos_invoice')
frappe.reload_doc('accounts', 'doctype', 'pos_invoice_item')
make_custom_fields()

View File

@ -0,0 +1,8 @@
import frappe
def execute():
frappe.db.sql("""
update tabCustomer
set represents_company = NULL
where represents_company = ''
""")

View File

@ -18,6 +18,7 @@ def execute():
for old_dt, new_dt in doctypes.items():
if not frappe.db.table_exists(new_dt) and frappe.db.table_exists(old_dt):
frappe.reload_doc('healthcare', 'doctype', frappe.scrub(old_dt))
frappe.rename_doc('DocType', old_dt, new_dt, force=True)
frappe.reload_doc('healthcare', 'doctype', frappe.scrub(new_dt))
frappe.delete_doc_if_exists('DocType', old_dt)
@ -36,6 +37,18 @@ def execute():
SET parentfield = %(parentfield)s
""".format(doctype), {'parentfield': parentfield})
# copy renamed child table fields (fields were already renamed in old doctype json, hence sql)
frappe.db.sql("""UPDATE `tabNormal Test Result` SET lab_test_name = test_name""")
frappe.db.sql("""UPDATE `tabNormal Test Result` SET lab_test_event = test_event""")
frappe.db.sql("""UPDATE `tabNormal Test Result` SET lab_test_uom = test_uom""")
frappe.db.sql("""UPDATE `tabNormal Test Result` SET lab_test_comment = test_comment""")
frappe.db.sql("""UPDATE `tabNormal Test Template` SET lab_test_event = test_event""")
frappe.db.sql("""UPDATE `tabNormal Test Template` SET lab_test_uom = test_uom""")
frappe.db.sql("""UPDATE `tabDescriptive Test Result` SET lab_test_particulars = test_particulars""")
frappe.db.sql("""UPDATE `tabLab Test Group Template` SET lab_test_template = test_template""")
frappe.db.sql("""UPDATE `tabLab Test Group Template` SET lab_test_description = test_description""")
frappe.db.sql("""UPDATE `tabLab Test Group Template` SET lab_test_rate = test_rate""")
# rename field
frappe.reload_doc('healthcare', 'doctype', 'lab_test')
if frappe.db.has_column('Lab Test', 'special_toggle'):

View File

@ -20,9 +20,11 @@ def execute():
frappe.clear_cache()
frappe.flags.warehouse_account_map = {}
company_list = []
data = frappe.db.sql('''
SELECT
name, item_code, warehouse, voucher_type, voucher_no, posting_date, posting_time
name, item_code, warehouse, voucher_type, voucher_no, posting_date, posting_time, company
FROM
`tabStock Ledger Entry`
WHERE
@ -36,6 +38,9 @@ def execute():
total_sle = len(data)
i = 0
for d in data:
if d.company not in company_list:
company_list.append(d.company)
update_entries_after({
"item_code": d.item_code,
"warehouse": d.warehouse,
@ -53,8 +58,10 @@ def execute():
print("Reposting General Ledger Entries...")
for row in frappe.get_all('Company', filters= {'enable_perpetual_inventory': 1}):
update_gl_entries_after(posting_date, posting_time, company=row.name)
if data:
for row in frappe.get_all('Company', filters= {'enable_perpetual_inventory': 1}):
if row.name in company_list:
update_gl_entries_after(posting_date, posting_time, company=row.name)
frappe.db.auto_commit_on_many_writes = 0

View File

@ -6,8 +6,9 @@ def execute():
if "Healthcare" not in frappe.get_active_domains():
return
frappe.reload_doc("healthcare", "doctype", "Therapy Session")
frappe.reload_doc("healthcare", "doctype", "Inpatient Medication Order")
frappe.reload_doc("healthcare", "doctype", "Therapy Session")
frappe.reload_doc("healthcare", "doctype", "Clinical Procedure")
frappe.reload_doc("healthcare", "doctype", "Patient History Settings")
frappe.reload_doc("healthcare", "doctype", "Patient History Standard Document Type")
frappe.reload_doc("healthcare", "doctype", "Patient History Custom Document Type")

View File

@ -2,11 +2,15 @@
# License: GNU General Public License v3. See license.txt
import frappe
from erpnext.regional.united_arab_emirates.setup import setup
from erpnext.regional.united_arab_emirates.setup import setup
def execute():
company = frappe.get_all('Company', filters = {'country': 'United Arab Emirates'})
if not company:
return
setup()
frappe.reload_doc('regional', 'report', 'uae_vat_201')
frappe.reload_doc('regional', 'doctype', 'uae_vat_settings')
frappe.reload_doc('regional', 'doctype', 'uae_vat_account')
setup()

View File

@ -175,7 +175,7 @@
],
"is_submittable": 1,
"links": [],
"modified": "2021-03-31 14:45:48.566756",
"modified": "2021-03-31 22:33:59.098532",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Additional Salary",

View File

@ -147,7 +147,7 @@
],
"is_submittable": 1,
"links": [],
"modified": "2021-03-31 14:46:22.465521",
"modified": "2021-03-31 22:35:08.940087",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Employee Benefit Application",

View File

@ -144,7 +144,7 @@
],
"is_submittable": 1,
"links": [],
"modified": "2021-03-31 15:51:51.489269",
"modified": "2021-03-31 22:37:21.024625",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Employee Benefit Claim",

View File

@ -94,7 +94,7 @@
],
"is_submittable": 1,
"links": [],
"modified": "2021-03-31 14:48:00.919839",
"modified": "2021-03-31 22:38:20.332316",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Employee Incentive",

View File

@ -119,7 +119,7 @@
],
"is_submittable": 1,
"links": [],
"modified": "2021-03-31 20:41:57.387749",
"modified": "2021-03-31 22:39:59.237361",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Employee Tax Exemption Declaration",

View File

@ -142,7 +142,7 @@
],
"is_submittable": 1,
"links": [],
"modified": "2021-03-31 20:48:32.639885",
"modified": "2021-03-31 22:41:13.723339",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Employee Tax Exemption Proof Submission",

View File

@ -104,7 +104,7 @@
],
"is_submittable": 1,
"links": [],
"modified": "2021-03-31 20:53:33.323712",
"modified": "2021-03-31 22:42:08.139520",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Income Tax Slab",

View File

@ -567,6 +567,7 @@ def create_salary_slips_for_employees(employees, args, publish_progress=True):
if publish_progress:
frappe.publish_progress(count*100/len(set(employees) - set(salary_slips_exists_for)),
title = _("Creating Salary Slips..."))
else:
salary_slips_not_created.append(emp)

View File

@ -105,7 +105,7 @@
],
"is_submittable": 1,
"links": [],
"modified": "2021-03-31 14:50:29.401020",
"modified": "2021-03-31 22:43:28.363644",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Retention Bonus",

View File

@ -119,6 +119,7 @@ frappe.ui.form.on("Salary Slip", {
frm.set_df_property('exchange_rate', 'hidden', 1);
frm.set_df_property("exchange_rate", "description", "" );
}
}
}
},

View File

@ -631,7 +631,7 @@
"idx": 9,
"is_submittable": 1,
"links": [],
"modified": "2021-03-31 15:39:28.817166",
"modified": "2021-03-31 22:44:09.772331",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Salary Slip",

View File

@ -598,10 +598,10 @@ class SalarySlip(TransactionBase):
continue
if (
not d.additional_salary
and (not additional_salary or additional_salary.overwrite)
or additional_salary
and additional_salary.name == d.additional_salary
(not d.additional_salary
and (not additional_salary or additional_salary.overwrite))
or (additional_salary
and additional_salary.name == d.additional_salary)
):
component_row = d
break
@ -611,7 +611,7 @@ class SalarySlip(TransactionBase):
self.set(component_type, [
d for d in self.get(component_type)
if d.salary_component != component_data.salary_component
or d.additional_salary and additional_salary.name != d.additional_salary
or (d.additional_salary and additional_salary.name != d.additional_salary)
or d == component_row
])

View File

@ -312,7 +312,7 @@ class TestSalarySlip(unittest.TestCase):
frappe.db.sql("DELETE FROM `tabSalary Slip` where employee_name = 'test_ytd@salary.com'")
create_salary_slips_for_payroll_period(applicant, salary_structure.name,
payroll_period, deduct_random=False)
payroll_period, deduct_random=False, num=6)
salary_slips = frappe.get_all('Salary Slip', fields=['year_to_date', 'net_pay'], filters={'employee_name':
'test_ytd@salary.com'}, order_by = 'posting_date')

View File

@ -111,12 +111,19 @@ frappe.ui.form.on('Salary Structure', {
frappe.set_route('Form', 'Salary Structure Assignment', doc.name);
});
frm.add_custom_button(__("Assign to Employees"),function () {
frm.trigger('assign_to_employees')
})
frm.trigger('assign_to_employees')
})
}
// set columns read-only
let fields_read_only = ["is_tax_applicable", "is_flexible_benefit", "variable_based_on_taxable_salary"];
fields_read_only.forEach(function(field) {
frappe.meta.get_docfield("Salary Detail", field, frm.doc.name).read_only = 1;
frm.fields_dict.earnings.grid.update_docfield_property(
field, 'read_only', 1
);
frm.fields_dict.deductions.grid.update_docfield_property(
field, 'read_only', 1
);
});
frm.trigger('set_earning_deduction_component');
},

View File

@ -164,7 +164,13 @@ def create_salary_structure_assignment(employee, salary_structure, from_date=Non
salary_structure_assignment.employee = employee
salary_structure_assignment.base = 50000
salary_structure_assignment.variable = 5000
salary_structure_assignment.from_date = from_date or add_days(nowdate(), -1)
if getdate(nowdate()).day == 1:
date = from_date or nowdate()
else:
date = from_date or add_days(nowdate(), -1)
salary_structure_assignment.from_date = date
salary_structure_assignment.salary_structure = salary_structure
salary_structure_assignment.currency = currency
salary_structure_assignment.payroll_payable_account = get_payable_account(company)

View File

@ -145,7 +145,7 @@
],
"is_submittable": 1,
"links": [],
"modified": "2021-03-31 15:49:36.361253",
"modified": "2021-03-31 22:44:46.267974",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Salary Structure Assignment",

View File

@ -8,7 +8,7 @@
"is_mandatory": 1,
"is_single": 0,
"is_skipped": 0,
"modified": "2020-06-01 11:53:54.553947",
"modified": "2020-06-29 11:53:54.553947",
"modified_by": "Administrator",
"name": "Create Payroll Period",
"owner": "Administrator",

View File

@ -1,19 +1,19 @@
{
"action": "Go to Page",
"action": "Update Settings",
"creation": "2020-06-04 16:34:29.664917",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
"is_mandatory": 0,
"is_single": 0,
"is_single": 1,
"is_skipped": 0,
"modified": "2020-06-04 16:34:29.664917",
"modified": "2020-06-29 16:34:29.664917",
"modified_by": "Administrator",
"name": "Payroll Settings",
"owner": "Administrator",
"path": "#Form/Payroll Settings",
"reference_document": "Payroll Settings",
"show_full_form": 0,
"title": "Payroll Settings",
"validate_action": 1
"validate_action": 0
}

View File

@ -10,10 +10,12 @@ frappe.ui.form.on('Products Settings', {
df => ['Link', 'Table MultiSelect'].includes(df.fieldtype) && !df.hidden
).map(df => ({ label: df.label, value: df.fieldname }));
const field = frappe.meta.get_docfield("Website Filter Field", "fieldname", frm.docname);
field.fieldtype = 'Select';
field.options = valid_fields;
frm.fields_dict.filter_fields.grid.refresh();
frm.fields_dict.filter_fields.grid.update_docfield_property(
'fieldname', 'fieldtype', 'Select'
);
frm.fields_dict.filter_fields.grid.update_docfield_property(
'fieldname', 'options', valid_fields
);
});
}
});

View File

@ -276,74 +276,3 @@ erpnext.taxes.set_conditional_mandatory_rate_or_amount = function(grid_row) {
}
}
}
// For customizing print
cur_frm.pformat.total = function(doc) { return ''; }
cur_frm.pformat.discount_amount = function(doc) { return ''; }
cur_frm.pformat.grand_total = function(doc) { return ''; }
cur_frm.pformat.rounded_total = function(doc) { return ''; }
cur_frm.pformat.in_words = function(doc) { return ''; }
cur_frm.pformat.taxes= function(doc){
//function to make row of table
var make_row = function(title, val, bold, is_negative) {
var bstart = '<b>'; var bend = '</b>';
return '<tr><td style="width:50%;">' + (bold?bstart:'') + title + (bold?bend:'') + '</td>'
+ '<td style="width:50%;text-align:right;">' + (is_negative ? '- ' : '')
+ format_currency(val, doc.currency) + '</td></tr>';
}
function print_hide(fieldname) {
var doc_field = frappe.meta.get_docfield(doc.doctype, fieldname, doc.name);
return doc_field.print_hide;
}
out ='';
if (!doc.print_without_amount) {
var cl = doc.taxes || [];
// outer table
var out='<div><table class="noborder" style="width:100%"><tr><td style="width: 60%"></td><td>';
// main table
out +='<table class="noborder" style="width:100%">';
if(!print_hide('total')) {
out += make_row('Total', doc.total, 1);
}
// Discount Amount on net total
if(!print_hide('discount_amount') && doc.apply_discount_on == "Net Total" && doc.discount_amount)
out += make_row('Discount Amount', doc.discount_amount, 0, 1);
// add rows
if(cl.length){
for(var i=0;i<cl.length;i++) {
if(cl[i].tax_amount!=0 && !cl[i].included_in_print_rate)
out += make_row(cl[i].description, cl[i].tax_amount, 0);
}
}
// Discount Amount on grand total
if(!print_hide('discount_amount') && doc.apply_discount_on == "Grand Total" && doc.discount_amount)
out += make_row('Discount Amount', doc.discount_amount, 0, 1);
// grand total
if(!print_hide('grand_total'))
out += make_row('Grand Total', doc.grand_total, 1);
if(!print_hide('rounded_total'))
out += make_row('Rounded Total', doc.rounded_total, 1);
if(doc.in_words && !print_hide('in_words')) {
out +='</table></td></tr>';
out += '<tr><td colspan = "2">';
out += '<table><tr><td style="width:25%;"><b>In Words</b></td>';
out += '<td style="width:50%;">' + doc.in_words + '</td></tr>';
}
out += '</table></td></tr></table></div>';
}
return out;
}

View File

@ -323,12 +323,15 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
// set precision in the last item iteration
if (n == me.frm.doc["items"].length - 1) {
me.round_off_totals(tax);
me.set_in_company_currency(tax,
["tax_amount", "tax_amount_after_discount_amount"]);
me.round_off_base_values(tax);
// in tax.total, accumulate grand total for each item
me.set_cumulative_total(i, tax);
me.set_in_company_currency(tax,
["total", "tax_amount", "tax_amount_after_discount_amount"]);
me.set_in_company_currency(tax, ["total"]);
// adjust Discount Amount loss in last tax iteration
if ((i == me.frm.doc["taxes"].length - 1) && me.discount_amount_applied
@ -393,20 +396,11 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
current_tax_amount = tax_rate * item.qty;
}
current_tax_amount = this.get_final_tax_amount(tax, current_tax_amount);
this.set_item_wise_tax(item, tax, tax_rate, current_tax_amount);
return current_tax_amount;
},
get_final_tax_amount: function(tax, current_tax_amount) {
if (frappe.flags.round_off_applicable_accounts.includes(tax.account_head)) {
current_tax_amount = Math.round(current_tax_amount);
}
return current_tax_amount;
},
set_item_wise_tax: function(item, tax, tax_rate, current_tax_amount) {
// store tax breakup for each item
let tax_detail = tax.item_wise_tax_detail;
@ -420,10 +414,22 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
},
round_off_totals: function(tax) {
if (frappe.flags.round_off_applicable_accounts.includes(tax.account_head)) {
tax.tax_amount= Math.round(tax.tax_amount);
tax.tax_amount_after_discount_amount = Math.round(tax.tax_amount_after_discount_amount);
}
tax.tax_amount = flt(tax.tax_amount, precision("tax_amount", tax));
tax.tax_amount_after_discount_amount = flt(tax.tax_amount_after_discount_amount, precision("tax_amount", tax));
},
round_off_base_values: function(tax) {
if (frappe.flags.round_off_applicable_accounts.includes(tax.account_head)) {
tax.base_tax_amount= Math.round(tax.base_tax_amount);
tax.base_tax_amount_after_discount_amount = Math.round(tax.base_tax_amount_after_discount_amount);
}
},
manipulate_grand_total_for_inclusive_tax: function() {
var me = this;
// if fully inclusive taxes and diff

View File

@ -0,0 +1,14 @@
// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
// MIT License. See license.txt
frappe.ui.form.on('Website Theme', {
validate(frm) {
let theme_scss = frm.doc.theme_scss;
if (theme_scss && theme_scss.includes('frappe/public/scss/website')
&& !theme_scss.includes('erpnext/public/scss/website')
) {
frm.set_value('theme_scss',
`${frm.doc.theme_scss}\n@import "erpnext/public/scss/website";`);
}
}
});

View File

@ -8,6 +8,7 @@
"enable",
"section_break_2",
"sandbox_mode",
"applicable_from",
"credentials",
"auth_token",
"token_expiry"
@ -48,12 +49,19 @@
"fieldname": "sandbox_mode",
"fieldtype": "Check",
"label": "Sandbox Mode"
},
{
"fieldname": "applicable_from",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Applicable From",
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2021-01-13 12:04:49.449199",
"modified": "2021-03-30 12:26:25.538294",
"modified_by": "Administrator",
"module": "Regional",
"name": "E Invoice Settings",

View File

@ -5,6 +5,7 @@
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"company",
"gstin",
"username",
"password"
@ -30,12 +31,20 @@
"in_list_view": 1,
"label": "Password",
"reqd": 1
},
{
"fieldname": "company",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Company",
"options": "Company",
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2020-12-22 15:10:53.466205",
"modified": "2021-03-22 12:16:56.365616",
"modified_by": "Administrator",
"module": "Regional",
"name": "E Invoice User",

View File

@ -3,4 +3,17 @@ import frappe
def setup(company=None, patch=True):
pass
add_custom_roles_for_reports()
def add_custom_roles_for_reports():
"""Add Access Control to UAE VAT 201."""
if not frappe.db.get_value('Custom Role', dict(report='DATEV')):
frappe.get_doc(dict(
doctype='Custom Role',
report='DATEV',
roles= [
dict(role='Accounts User'),
dict(role='Accounts Manager')
]
)).insert()

View File

@ -69,7 +69,7 @@ state_numbers = {
"Mizoram": "15",
"Nagaland": "13",
"Odisha": "21",
"Other Territory": "98",
"Other Territory": "97",
"Pondicherry": "34",
"Punjab": "03",
"Rajasthan": "08",

View File

@ -1,12 +1,13 @@
erpnext.setup_einvoice_actions = (doctype) => {
frappe.ui.form.on(doctype, {
async refresh(frm) {
const einvoicing_enabled = await frappe.db.get_single_value("E Invoice Settings", "enable");
const supply_type = frm.doc.gst_category;
const valid_supply_type = ['Registered Regular', 'SEZ', 'Overseas', 'Deemed Export'].includes(supply_type);
const company_transaction = frm.doc.billing_address_gstin == frm.doc.company_gstin;
const res = await frappe.call({
method: 'erpnext.regional.india.e_invoice.utils.validate_eligibility',
args: { doc: frm.doc }
});
const invoice_eligible = res.message;
if (cint(einvoicing_enabled) == 0 || !valid_supply_type || company_transaction) return;
if (!invoice_eligible) return;
const { doctype, irn, irn_cancelled, ewaybill, eway_bill_cancelled, name, __unsaved } = frm.doc;
@ -45,7 +46,7 @@ erpnext.setup_einvoice_actions = (doctype) => {
"default": "1-Duplicate",
"options": ["1-Duplicate", "2-Data Entry Error", "3-Order Cancelled", "4-Other"]
},
{
{
"label": "Remark",
"fieldname": "remark",
"fieldtype": "Data",
@ -60,7 +61,7 @@ erpnext.setup_einvoice_actions = (doctype) => {
const data = d.get_values();
frappe.call({
method: 'erpnext.regional.india.e_invoice.utils.cancel_irn',
args: {
args: {
doctype,
docname: name,
irn: irn,
@ -109,45 +110,25 @@ erpnext.setup_einvoice_actions = (doctype) => {
}
if (irn && ewaybill && !irn_cancelled && !eway_bill_cancelled) {
const fields = [
{
"label": "Reason",
"fieldname": "reason",
"fieldtype": "Select",
"reqd": 1,
"default": "1-Duplicate",
"options": ["1-Duplicate", "2-Data Entry Error", "3-Order Cancelled", "4-Other"]
},
{
"label": "Remark",
"fieldname": "remark",
"fieldtype": "Data",
"reqd": 1
}
];
const action = () => {
const d = new frappe.ui.Dialog({
title: __('Cancel E-Way Bill'),
fields: fields,
let message = __('Cancellation of e-way bill is currently not supported. ');
message += '<br><br>';
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"

View File

@ -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,70 @@ 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)
validate_totals(einvoice)
except Exception:
log_error(einvoice)
link_to_error_list = '<a href="List/Error Log/List?method=E Invoice Request Failed">Error Log</a>'
frappe.throw(
_('An error occurred while creating e-invoice for {}. Please check {} for more information.').format(
invoice.name, link_to_error_list),
title=_('E Invoice Creation Failed')
)
return einvoice
def log_error(data=None):
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 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 +541,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: <br> {}").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,15 +565,26 @@ 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')
@ -529,7 +635,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 +657,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 +681,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 +707,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 +753,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 +789,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 +862,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 +894,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 +909,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 +923,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 +945,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 +958,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 +984,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 +1024,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 = '<a href="sales-invoice/{0}">{0}</a>'.format(doc.get('docname'))
message = doc.get('message').replace("'", '"')
if message[0] == '[':
errors = json.loads(message)
error_list = ''.join(['<li>{}</li>'.format(err) for err in errors])
message = '''{} has following errors:<br>
<ul style="padding-left: 20px; padding-top: 5px">{}</ul>'''.format(docname, error_list)
else:
message = '{} - {}'.format(docname, message)
frappe.msgprint(
message,
title=_('Bulk E-Invoice Generation Complete'),
indicator='red'
)
@frappe.whitelist()
def cancel_irns(docnames, reason, remark):
docnames = json.loads(docnames) or []
if len(docnames) < 10:
failures = GSPConnector.bulk_cancel_irn(docnames, reason, remark)
frappe.local.message_log = []
if failures:
show_bulk_action_failure_message(failures)
success = len(docnames) - len(failures)
frappe.msgprint(
_('{} e-invoices cancelled successfully').format(success),
title=_('Bulk E-Invoice Cancellation Complete')
)
else:
enqueue_bulk_action(schedule_bulk_cancel_irn, docnames=docnames, reason=reason, remark=remark)
def schedule_bulk_cancel_irn(docnames, reason, remark):
failures = GSPConnector.bulk_cancel_irn(docnames, reason, remark)
frappe.local.message_log = []
frappe.publish_realtime("bulk_einvoice_cancellation_complete", {
"user": frappe.session.user,
"failures": failures,
"invoices": docnames
})
def enqueue_bulk_action(job, **kwargs):
check_scheduler_status()
enqueue(
job,
**kwargs,
queue="long",
timeout=10000,
event="processing_bulk_einvoice_action",
now=frappe.conf.developer_mode or frappe.flags.in_test,
)
if job == schedule_bulk_generate_irn:
msg = _('E-Invoices will be generated in a background process.')
else:
msg = _('E-Invoices will be cancelled in a background process.')
frappe.msgprint(msg, alert=1)
def check_scheduler_status():
if is_scheduler_inactive() and not frappe.flags.in_test:
frappe.throw(_("Scheduler is inactive. Cannot enqueue job."), title=_("Scheduler Inactive"))
def job_already_enqueued(job_name):
enqueued_jobs = [d.get("job_name") for d in get_info()]
if job_name in enqueued_jobs:
return True

View File

@ -12,14 +12,14 @@ from erpnext.accounts.utils import get_fiscal_year, FiscalYearError
from frappe.utils import today
def setup(company=None, patch=True):
setup_company_independent_fixtures()
setup_company_independent_fixtures(patch=patch)
if not patch:
make_fixtures(company)
# TODO: for all countries
def setup_company_independent_fixtures():
def setup_company_independent_fixtures(patch=False):
make_custom_fields()
make_property_setters()
make_property_setters(patch=patch)
add_permissions()
add_custom_roles_for_reports()
frappe.enqueue('erpnext.regional.india.setup.add_hsn_sac_codes', now=frappe.flags.in_test)
@ -51,7 +51,7 @@ def create_hsn_codes(data, code_field):
def add_custom_roles_for_reports():
for report_name in ('GST Sales Register', 'GST Purchase Register',
'GST Itemised Sales Register', 'GST Itemised Purchase Register', 'Eway Bill'):
'GST Itemised Sales Register', 'GST Itemised Purchase Register', 'Eway Bill', 'E-Invoice Summary'):
if not frappe.db.get_value('Custom Role', dict(report=report_name)):
frappe.get_doc(dict(
@ -112,10 +112,11 @@ def add_print_formats():
frappe.db.set_value("Print Format", "GST Tax Invoice", "disabled", 0)
frappe.db.set_value("Print Format", "GST E-Invoice", "disabled", 0)
def make_property_setters():
def make_property_setters(patch=False):
# GST rules do not allow for an invoice no. bigger than 16 characters
make_property_setter('Sales Invoice', 'naming_series', 'options', 'SINV-.YY.-\nSRET-.YY.-', '')
make_property_setter('Purchase Invoice', 'naming_series', 'options', 'PINV-.YY.-\nPRET-.YY.-', '')
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):
hsn_sac_field = dict(fieldname='gst_hsn_code', label='HSN/SAC',
@ -127,6 +128,9 @@ def make_custom_fields(update=True):
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',
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 = [
dict(fieldname='gst_section', label='GST Details', fieldtype='Section Break',
@ -156,6 +160,13 @@ def make_custom_fields(update=True):
fetch_if_empty=1),
]
delivery_note_gst_category = [
dict(fieldname='gst_category', label='GST Category',
fieldtype='Select', insert_after='gst_vehicle_type', print_hide=1,
options='\nRegistered Regular\nRegistered Composition\nUnregistered\nSEZ\nOverseas\nConsumer\nDeemed Export\nUIN Holders',
fetch_from='customer.gst_category', fetch_if_empty=1),
]
invoice_gst_fields = [
dict(fieldname='invoice_copy', label='Invoice Copy',
fieldtype='Select', insert_after='export_type', print_hide=1, allow_on_submit=1,
@ -280,7 +291,7 @@ def make_custom_fields(update=True):
'allow_on_submit': 1,
'insert_after': 'customer_name_in_arabic',
'translatable': 0,
}
}
]
si_ewaybill_fields = [
@ -408,21 +419,37 @@ def make_custom_fields(update=True):
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'),
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='irn_cancelled', label='IRN Cancelled', fieldtype='Check', no_copy=1, print_hide=1,
depends_on='eval:(doc.irn_cancelled === 1)', read_only=1, allow_on_submit=1, insert_after='customer'),
dict(fieldname='eway_bill_cancelled', label='E-Way Bill Cancelled', fieldtype='Check', no_copy=1, print_hide=1,
depends_on='eval:(doc.eway_bill_cancelled === 1)', read_only=1, allow_on_submit=1, insert_after='customer'),
dict(fieldname='signed_einvoice', fieldtype='Code', options='JSON', hidden=1, no_copy=1, print_hide=1, read_only=1),
dict(fieldname='einvoice_section', label='E-Invoice Fields', fieldtype='Section Break', insert_after='gst_vehicle_type',
print_hide=1, hidden=1),
dict(fieldname='ack_no', label='Ack. No.', fieldtype='Data', read_only=1, hidden=1, insert_after='einvoice_section',
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='signed_qr_code', fieldtype='Code', options='JSON', hidden=1, no_copy=1, print_hide=1, read_only=1),
dict(fieldname='irn_cancel_date', label='Cancel Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_date',
no_copy=1, print_hide=1),
dict(fieldname='qrcode_image', label='QRCode', fieldtype='Attach Image', hidden=1, no_copy=1, print_hide=1, read_only=1)
dict(fieldname='signed_einvoice', label='Signed E-Invoice', fieldtype='Code', options='JSON', hidden=1, insert_after='irn_cancel_date',
no_copy=1, print_hide=1, read_only=1),
dict(fieldname='signed_qr_code', label='Signed QRCode', fieldtype='Code', options='JSON', hidden=1, insert_after='signed_einvoice',
no_copy=1, print_hide=1, read_only=1),
dict(fieldname='qrcode_image', label='QRCode', fieldtype='Attach Image', hidden=1, insert_after='signed_qr_code',
no_copy=1, print_hide=1, read_only=1),
dict(fieldname='einvoice_status', label='E-Invoice Status', fieldtype='Select', insert_after='qrcode_image',
options='\nPending\nGenerated\nCancelled\nFailed', default=None, hidden=1, no_copy=1, print_hide=1, read_only=1),
dict(fieldname='failure_description', label='E-Invoice Failure Description', fieldtype='Code', options='JSON',
hidden=1, insert_after='einvoice_status', no_copy=1, print_hide=1, read_only=1)
]
custom_fields = {
@ -438,7 +465,7 @@ def make_custom_fields(update=True):
'Purchase Order': purchase_invoice_gst_fields,
'Purchase Receipt': purchase_invoice_gst_fields,
'Sales Invoice': sales_invoice_gst_category + invoice_gst_fields + sales_invoice_shipping_fields + sales_invoice_gst_fields + si_ewaybill_fields + si_einvoice_fields,
'Delivery Note': sales_invoice_gst_fields + ewaybill_fields + sales_invoice_shipping_fields,
'Delivery Note': sales_invoice_gst_fields + ewaybill_fields + sales_invoice_shipping_fields + delivery_note_gst_category,
'Sales Order': sales_invoice_gst_fields,
'Tax Category': inter_state_gst_field,
'Item': [
@ -453,7 +480,7 @@ def make_custom_fields(update=True):
'Supplier Quotation 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],
'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 Receipt Item': [hsn_sac_field, nil_rated_exempt, is_non_gst],
'Purchase Invoice Item': [hsn_sac_field, nil_rated_exempt, is_non_gst],

View File

@ -12,14 +12,14 @@ class TestIndiaUtils(unittest.TestCase):
mock_get_cached.return_value = "India" # mock country
posting_date = "2021-05-01"
invalid_names = [ "SI$1231", "012345678901234567", "SI 2020 05",
"SI.2020.0001", "PI2021 - 001" ]
invalid_names = ["SI$1231", "012345678901234567", "SI 2020 05",
"SI.2020.0001", "PI2021 - 001"]
for name in invalid_names:
doc = frappe._dict(name=name, posting_date=posting_date)
self.assertRaises(frappe.ValidationError, validate_document_name, doc)
valid_names = [ "012345678901236", "SI/2020/0001", "SI/2020-0001",
"2020-PI-0001", "PI2020-0001" ]
valid_names = ["012345678901236", "SI/2020/0001", "SI/2020-0001",
"2020-PI-0001", "PI2020-0001"]
for name in valid_names:
doc = frappe._dict(name=name, posting_date=posting_date)
try:

View File

@ -2,7 +2,7 @@ from __future__ import unicode_literals
import frappe, re, json
from frappe import _
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.controllers.taxes_and_totals import get_itemised_tax, get_itemised_taxable_amount
from erpnext.controllers.accounts_controller import get_taxes_and_charges
@ -41,24 +41,25 @@ def validate_gstin_for_india(doc, method):
return
if len(doc.gstin) != 15:
frappe.throw(_("Invalid GSTIN! A GSTIN must have 15 characters."))
frappe.throw(_("A GSTIN must have 15 characters."), title=_("Invalid GSTIN"))
if gst_category and gst_category == 'UIN Holders':
if not GSTIN_UIN_FORMAT.match(doc.gstin):
frappe.throw(_("Invalid GSTIN! The input you've entered doesn't match the GSTIN format for UIN Holders or Non-Resident OIDAR Service Providers"))
frappe.throw(_("The input you've entered doesn't match the GSTIN format for UIN Holders or Non-Resident OIDAR Service Providers"),
title=_("Invalid GSTIN"))
else:
if not GSTIN_FORMAT.match(doc.gstin):
frappe.throw(_("Invalid GSTIN! The input you've entered doesn't match the format of GSTIN."))
frappe.throw(_("The input you've entered doesn't match the format of GSTIN."), title=_("Invalid GSTIN"))
validate_gstin_check_digit(doc.gstin)
set_gst_state_and_state_number(doc)
if not doc.gst_state:
frappe.throw(_("Please Enter GST state"))
frappe.throw(_("Please enter GST state"), title=_("Invalid State"))
if doc.gst_state_number != doc.gstin[:2]:
frappe.throw(_("Invalid GSTIN! First 2 digits of GSTIN should match with State number {0}.")
.format(doc.gst_state_number))
frappe.throw(_("First 2 digits of GSTIN should match with State number {0}.")
.format(doc.gst_state_number), title=_("Invalid GSTIN"))
def validate_pan_for_india(doc, method):
if doc.get('country') != 'India' or not doc.pan:
@ -154,6 +155,7 @@ def set_place_of_supply(doc, method=None):
def validate_document_name(doc, method=None):
"""Validate GST invoice number requirements."""
country = frappe.get_cached_value("Company", doc.company, "country")
# Date was chosen as start of next FY to avoid irritating current users.
@ -832,3 +834,48 @@ def get_regional_round_off_accounts(company, account_list):
account_list.extend(gst_account_list)
return account_list
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

View File

@ -139,6 +139,9 @@ def make_custom_fields(update=True):
dict(fieldname='customer_fiscal_code', label='Customer Fiscal Code',
fieldtype='Data', insert_after='cb_e_invoicing_reference', read_only=1,
fetch_from="customer.fiscal_code"),
dict(fieldname='type_of_document', label='Type of Document',
fieldtype='Select', insert_after='customer_fiscal_code',
options='\nTD01\nTD02\nTD03\nTD04\nTD05\nTD06\nTD16\nTD17\nTD18\nTD19\nTD20\nTD21\nTD22\nTD23\nTD24\nTD25\nTD26\nTD27'),
],
'Purchase Invoice Item': invoice_item_fields,
'Sales Order Item': invoice_item_fields,

View File

@ -57,11 +57,12 @@ def prepare_invoice(invoice, progressive_number):
invoice.company_address_data = company_address
#Set invoice type
if invoice.is_return and invoice.return_against:
invoice.type_of_document = "TD04" #Credit Note (Nota di Credito)
invoice.return_against_unamended = get_unamended_name(frappe.get_doc("Sales Invoice", invoice.return_against))
else:
invoice.type_of_document = "TD01" #Sales Invoice (Fattura)
if not invoice.type_of_document:
if invoice.is_return and invoice.return_against:
invoice.type_of_document = "TD04" #Credit Note (Nota di Credito)
invoice.return_against_unamended = get_unamended_name(frappe.get_doc("Sales Invoice", invoice.return_against))
else:
invoice.type_of_document = "TD01" #Sales Invoice (Fattura)
#set customer information
invoice.customer_data = frappe.get_doc("Customer", invoice.customer)

View File

@ -1,29 +1,22 @@
{
"add_total_row": 0,
"apply_user_permissions": 0,
"creation": "2019-04-24 08:45:16.650129",
"disabled": 0,
"icon": "octicon octicon-repo-pull",
"color": "#4CB944",
"docstatus": 0,
"doctype": "Report",
"idx": 0,
"is_standard": "Yes",
"module": "Regional",
"name": "DATEV",
"owner": "Administrator",
"ref_doctype": "GL Entry",
"report_name": "DATEV",
"report_type": "Script Report",
"roles": [
{
"role": "Accounts User"
},
{
"role": "Accounts Manager"
},
{
"role": "Auditor"
}
]
}
"add_total_row": 0,
"columns": [],
"creation": "2019-04-24 08:45:16.650129",
"disable_prepared_report": 0,
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 0,
"is_standard": "Yes",
"modified": "2021-04-06 12:23:00.379517",
"modified_by": "Administrator",
"module": "Regional",
"name": "DATEV",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "GL Entry",
"report_name": "DATEV",
"report_type": "Script Report",
"roles": []
}

View File

@ -0,0 +1,55 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
/* eslint-disable */
frappe.query_reports["E-Invoice Summary"] = {
"filters": [
{
"fieldtype": "Link",
"options": "Company",
"reqd": 1,
"fieldname": "company",
"label": __("Company"),
"default": frappe.defaults.get_user_default("Company"),
},
{
"fieldtype": "Link",
"options": "Customer",
"fieldname": "customer",
"label": __("Customer")
},
{
"fieldtype": "Date",
"reqd": 1,
"fieldname": "from_date",
"label": __("From Date"),
"default": frappe.datetime.add_months(frappe.datetime.get_today(), -1),
},
{
"fieldtype": "Date",
"reqd": 1,
"fieldname": "to_date",
"label": __("To Date"),
"default": frappe.datetime.get_today(),
},
{
"fieldtype": "Select",
"fieldname": "status",
"label": __("Status"),
"options": "\nPending\nGenerated\nCancelled\nFailed"
}
],
"formatter": function (value, row, column, data, default_formatter) {
value = default_formatter(value, row, column, data);
if (column.fieldname == "einvoice_status" && value) {
if (value == 'Pending') value = `<span class="bold" style="color: var(--text-on-orange)">${value}</span>`;
else if (value == 'Generated') value = `<span class="bold" style="color: var(--text-on-green)">${value}</span>`;
else if (value == 'Cancelled') value = `<span class="bold" style="color: var(--text-on-red)">${value}</span>`;
else if (value == 'Failed') value = `<span class="bold" style="color: var(--text-on-red)">${value}</span>`;
}
return value;
}
};

View File

@ -0,0 +1,28 @@
{
"add_total_row": 0,
"columns": [],
"creation": "2021-03-12 11:23:37.312294",
"disable_prepared_report": 0,
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 0,
"is_standard": "Yes",
"json": "{}",
"letter_head": "Logo",
"modified": "2021-03-12 12:36:48.689413",
"modified_by": "Administrator",
"module": "Regional",
"name": "E-Invoice Summary",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "Sales Invoice",
"report_name": "E-Invoice Summary",
"report_type": "Script Report",
"roles": [
{
"role": "Administrator"
}
]
}

View File

@ -0,0 +1,106 @@
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe import _
def execute(filters=None):
validate_filters(filters)
columns = get_columns()
data = get_data(filters)
return columns, data
def validate_filters(filters={}):
filters = frappe._dict(filters)
if not filters.company:
frappe.throw(_('{} is mandatory for generating E-Invoice Summary Report').format(_('Company')), title=_('Invalid Filter'))
if filters.company:
# validate if company has e-invoicing enabled
pass
if not filters.from_date or not filters.to_date:
frappe.throw(_('From Date & To Date is mandatory for generating E-Invoice Summary Report'), title=_('Invalid Filter'))
if filters.from_date > filters.to_date:
frappe.throw(_('From Date must be before To Date'), title=_('Invalid Filter'))
def get_data(filters={}):
query_filters = {
'posting_date': ['between', [filters.from_date, filters.to_date]],
'einvoice_status': ['is', 'set'],
'company': filters.company
}
if filters.customer:
query_filters['customer'] = filters.customer
if filters.status:
query_filters['einvoice_status'] = filters.status
data = frappe.get_all(
'Sales Invoice',
filters=query_filters,
fields=[d.get('fieldname') for d in get_columns()]
)
return data
def get_columns():
return [
{
"fieldtype": "Date",
"fieldname": "posting_date",
"label": _("Posting Date"),
"width": 0
},
{
"fieldtype": "Link",
"fieldname": "name",
"label": _("Sales Invoice"),
"options": "Sales Invoice",
"width": 140
},
{
"fieldtype": "Data",
"fieldname": "einvoice_status",
"label": _("Status"),
"width": 100
},
{
"fieldtype": "Link",
"fieldname": "customer",
"options": "Customer",
"label": _("Customer")
},
{
"fieldtype": "Check",
"fieldname": "is_return",
"label": _("Is Return"),
"width": 85
},
{
"fieldtype": "Data",
"fieldname": "ack_no",
"label": "Ack. No.",
"width": 145
},
{
"fieldtype": "Data",
"fieldname": "ack_date",
"label": "Ack. Date",
"width": 165
},
{
"fieldtype": "Data",
"fieldname": "irn",
"label": _("IRN No."),
"width": 250
},
{
"fieldtype": "Currency",
"options": "Company:company:default_currency",
"fieldname": "base_grand_total",
"label": _("Grand Total"),
"width": 120
}
]

View File

@ -199,7 +199,7 @@ class Gstr1Report(object):
self.item_tax_rate = frappe._dict()
items = frappe.db.sql("""
select item_code, parent, base_net_amount, item_tax_rate
select item_code, parent, taxable_value, item_tax_rate
from `tab%s Item`
where parent in (%s)
""" % (self.doctype, ', '.join(['%s']*len(self.invoices))), tuple(self.invoices), as_dict=1)
@ -207,7 +207,7 @@ class Gstr1Report(object):
for d in items:
if d.item_code not in self.invoice_items.get(d.parent, {}):
self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code,
sum(i.get('base_net_amount', 0) for i in items
sum(i.get('taxable_value', 0) for i in items
if i.item_code == d.item_code and i.parent == d.parent))
item_tax_rate = {}

Some files were not shown because too many files have changed in this diff Show More