Merge pull request #5 from frappe/version-12-hotfix

Version 12 hotfix
This commit is contained in:
Jannat Patel
2021-01-18 15:58:26 +05:30
committed by GitHub
90 changed files with 4079 additions and 438 deletions

View File

@@ -910,75 +910,8 @@
}, },
"is_group": 1 "is_group": 1
}, },
"Passiva": { "Passiva - Verbindlichkeiten": {
"root_type": "Liability", "root_type": "Liability",
"A - Eigenkapital": {
"account_type": "Equity",
"is_group": 1,
"I - Gezeichnetes Kapital": {
"account_type": "Equity",
"is_group": 1
},
"II - Kapitalr\u00fccklage": {
"account_type": "Equity",
"is_group": 1
},
"III - Gewinnr\u00fccklagen": {
"account_type": "Equity",
"1 - gesetzliche R\u00fccklage": {
"account_type": "Equity",
"is_group": 1
},
"2 - R\u00fccklage f. Anteile an einem herrschenden oder mehrheitlich beteiligten Unternehmen": {
"account_type": "Equity",
"is_group": 1
},
"3 - satzungsm\u00e4\u00dfige R\u00fccklagen": {
"account_type": "Equity",
"is_group": 1
},
"4 - andere Gewinnr\u00fccklagen": {
"account_type": "Equity",
"is_group": 1,
"Gewinnr\u00fccklagen aus den \u00dcbergangsvorschriften BilMoG": {
"is_group": 1,
"Gewinnr\u00fccklagen (BilMoG)": {
"account_number": "2963"
},
"Gewinnr\u00fccklagen aus Zuschreibung Sachanlageverm\u00f6gen (BilMoG)": {
"account_number": "2964"
},
"Gewinnr\u00fccklagen aus Zuschreibung Finanzanlageverm\u00f6gen (BilMoG)": {
"account_number": "2965"
},
"Gewinnr\u00fccklagen aus Aufl\u00f6sung der Sonderposten mit R\u00fccklageanteil (BilMoG)": {
"account_number": "2966"
}
},
"Latente Steuern (Gewinnr\u00fccklage Haben) aus erfolgsneutralen Verrechnungen": {
"account_number": "2967"
},
"Latente Steuern (Gewinnr\u00fccklage Soll) aus erfolgsneutralen Verrechnungen": {
"account_number": "2968"
},
"Rechnungsabgrenzungsposten (Gewinnr\u00fccklage Soll) aus erfolgsneutralen Verrechnungen": {
"account_number": "2969"
}
},
"is_group": 1
},
"IV - Gewinnvortrag/Verlustvortrag": {
"account_type": "Equity",
"is_group": 1
},
"V - Jahres\u00fcberschu\u00df/Jahresfehlbetrag": {
"account_type": "Equity",
"is_group": 1
},
"Einlagen stiller Gesellschafter": {
"account_number": "9295"
}
},
"B - R\u00fcckstellungen": { "B - R\u00fcckstellungen": {
"is_group": 1, "is_group": 1,
"1 - R\u00fcckstellungen f. Pensionen und \u00e4hnliche Verplicht.": { "1 - R\u00fcckstellungen f. Pensionen und \u00e4hnliche Verplicht.": {
@@ -1595,6 +1528,143 @@
}, },
"is_group": 1 "is_group": 1
}, },
"Passiva - Eigenkapital": {
"root_type": "Equity",
"A - Eigenkapital": {
"account_type": "Equity",
"is_group": 1,
"I - Gezeichnetes Kapital": {
"account_type": "Equity",
"is_group": 1,
"Gezeichnetes Kapital": {
"account_number": "2900",
"account_type": "Equity"
},
"Gesch\u00e4ftsguthaben der verbleibenden Mitglieder": {
"account_number": "2901"
},
"Gesch\u00e4ftsguthaben der ausscheidenden Mitglieder": {
"account_number": "2902"
},
"Gesch\u00e4ftsguthaben aus gek\u00fcndigten Gesch\u00e4ftsanteilen": {
"account_number": "2903"
},
"R\u00fcckst\u00e4ndige f\u00e4llige Einzahlungen auf Gesch\u00e4ftsanteile, vermerkt": {
"account_number": "2906"
},
"Gegenkonto R\u00fcckst\u00e4ndige f\u00e4llige Einzahlungen auf Gesch\u00e4ftsanteile, vermerkt": {
"account_number": "2907"
},
"Kapitalerh\u00f6hung aus Gesellschaftsmitteln": {
"account_number": "2908"
},
"Ausstehende Einlagen auf das gezeichnete Kapital, nicht eingefordert": {
"account_number": "2910"
}
},
"II - Kapitalr\u00fccklage": {
"account_type": "Equity",
"is_group": 1,
"Kapitalr\u00fccklage": {
"account_number": "2920"
},
"Kapitalr\u00fccklage durch Ausgabe von Anteilen \u00fcber Nennbetrag": {
"account_number": "2925"
},
"Kapitalr\u00fccklage durch Ausgabe von Schuldverschreibungen": {
"account_number": "2926"
},
"Kapitalr\u00fccklage durch Zuzahlungen gegen Gew\u00e4hrung eines Vorzugs": {
"account_number": "2927"
},
"Kapitalr\u00fccklage durch Zuzahlungen in das Eigenkapital": {
"account_number": "2928"
},
"Nachschusskapital (Gegenkonto 1299)": {
"account_number": "2929"
}
},
"III - Gewinnr\u00fccklagen": {
"account_type": "Equity",
"1 - gesetzliche R\u00fccklage": {
"account_type": "Equity",
"is_group": 1,
"Gesetzliche R\u00fccklage": {
"account_number": "2930"
}
},
"2 - R\u00fccklage f. Anteile an einem herrschenden oder mehrheitlich beteiligten Unternehmen": {
"account_type": "Equity",
"is_group": 1,
"R\u00fccklage f. Anteile an einem herrschenden oder mehrheitlich beteiligten Unternehmen": {
"account_number": "2935"
}
},
"3 - satzungsm\u00e4\u00dfige R\u00fccklagen": {
"account_type": "Equity",
"is_group": 1,
"Satzungsm\u00e4\u00dfige R\u00fccklagen": {
"account_number": "2950"
}
},
"4 - andere Gewinnr\u00fccklagen": {
"account_type": "Equity",
"is_group": 1,
"Andere Gewinnr\u00fccklagen": {
"account_number": "2960"
},
"Andere Gewinnr\u00fccklagen aus dem Erwerb eigener Anteile": {
"account_number": "2961"
},
"Eigenkapitalanteil von Wertaufholungen": {
"account_number": "2962"
},
"Gewinnr\u00fccklagen aus den \u00dcbergangsvorschriften BilMoG": {
"is_group": 1,
"Gewinnr\u00fccklagen (BilMoG)": {
"account_number": "2963"
},
"Gewinnr\u00fccklagen aus Zuschreibung Sachanlageverm\u00f6gen (BilMoG)": {
"account_number": "2964"
},
"Gewinnr\u00fccklagen aus Zuschreibung Finanzanlageverm\u00f6gen (BilMoG)": {
"account_number": "2965"
},
"Gewinnr\u00fccklagen aus Aufl\u00f6sung der Sonderposten mit R\u00fccklageanteil (BilMoG)": {
"account_number": "2966"
}
},
"Latente Steuern (Gewinnr\u00fccklage Haben) aus erfolgsneutralen Verrechnungen": {
"account_number": "2967"
},
"Latente Steuern (Gewinnr\u00fccklage Soll) aus erfolgsneutralen Verrechnungen": {
"account_number": "2968"
},
"Rechnungsabgrenzungsposten (Gewinnr\u00fccklage Soll) aus erfolgsneutralen Verrechnungen": {
"account_number": "2969"
}
},
"is_group": 1
},
"IV - Gewinnvortrag/Verlustvortrag": {
"account_type": "Equity",
"is_group": 1,
"Gewinnvortrag vor Verwendung": {
"account_number": "2970"
},
"Verlustvortrag vor Verwendung": {
"account_number": "2978"
}
},
"V - Jahres\u00fcberschu\u00df/Jahresfehlbetrag": {
"account_type": "Equity",
"is_group": 1
},
"Einlagen stiller Gesellschafter": {
"account_number": "9295"
}
}
},
"1 - Umsatzerl\u00f6se": { "1 - Umsatzerl\u00f6se": {
"root_type": "Income", "root_type": "Income",
"is_group": 1, "is_group": 1,

View File

@@ -245,6 +245,9 @@ def get():
"account_number": "2200" "account_number": "2200"
}, },
_("Duties and Taxes"): { _("Duties and Taxes"): {
_("TDS Payable"): {
"account_number": "2310"
},
"account_type": "Tax", "account_type": "Tax",
"is_group": 1, "is_group": 1,
"account_number": "2300" "account_number": "2300"

View File

@@ -138,7 +138,8 @@ class GLEntry(Document):
frappe.throw(_("{0} {1}: Cost Center {2} does not belong to Company {3}") frappe.throw(_("{0} {1}: Cost Center {2} does not belong to Company {3}")
.format(self.voucher_type, self.voucher_no, self.cost_center, self.company)) .format(self.voucher_type, self.voucher_no, self.cost_center, self.company))
if not self.flags.from_repost and self.cost_center and _check_is_group(): if not self.flags.from_repost and not self.voucher_type == 'Period Closing Voucher' \
and self.cost_center and _check_is_group():
frappe.throw(_("""{0} {1}: Cost Center {2} is a group cost center and group cost centers cannot frappe.throw(_("""{0} {1}: Cost Center {2} is a group cost center and group cost centers cannot
be used in transactions""").format(self.voucher_type, self.voucher_no, frappe.bold(self.cost_center))) be used in transactions""").format(self.voucher_type, self.voucher_no, frappe.bold(self.cost_center)))

View File

@@ -38,7 +38,10 @@ def get_loyalty_details(customer, loyalty_program, expiry_date=None, company=Non
@frappe.whitelist() @frappe.whitelist()
def get_loyalty_program_details_with_points(customer, loyalty_program=None, expiry_date=None, company=None, silent=False, include_expired_entry=False, current_transaction_amount=0): def get_loyalty_program_details_with_points(customer, loyalty_program=None, expiry_date=None, company=None, silent=False, include_expired_entry=False, current_transaction_amount=0):
lp_details = get_loyalty_program_details(customer, loyalty_program, company=company, silent=silent) lp_details = get_loyalty_program_details(customer, loyalty_program, company=company, silent=silent)
loyalty_program = frappe.get_doc("Loyalty Program", loyalty_program) loyalty_program_name = loyalty_program or lp_details.loyalty_program
if not loyalty_program_name: return
loyalty_program = frappe.get_doc("Loyalty Program", loyalty_program_name)
lp_details.update(get_loyalty_details(customer, loyalty_program.name, expiry_date, company, include_expired_entry)) lp_details.update(get_loyalty_details(customer, loyalty_program.name, expiry_date, company, include_expired_entry))
# sort collection rule, first item on list will be lowest min_spent # sort collection rule, first item on list will be lowest min_spent

View File

@@ -181,7 +181,8 @@ class OpeningInvoiceCreationTool(Document):
"due_date": row.due_date, "due_date": row.due_date,
"posting_date": row.posting_date, "posting_date": row.posting_date,
frappe.scrub(party_type): row.party, frappe.scrub(party_type): row.party,
"doctype": "Sales Invoice" if self.invoice_type == "Sales" else "Purchase Invoice" "doctype": "Sales Invoice" if self.invoice_type == "Sales" else "Purchase Invoice",
"update_stock": 0
}) })
accounting_dimension = get_accounting_dimensions() accounting_dimension = get_accounting_dimensions()

View File

@@ -7,17 +7,25 @@ import frappe
import unittest import unittest
test_dependencies = ["Customer", "Supplier"] test_dependencies = ["Customer", "Supplier"]
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
from erpnext.accounts.doctype.opening_invoice_creation_tool.opening_invoice_creation_tool import get_temporary_opening_account from erpnext.accounts.doctype.opening_invoice_creation_tool.opening_invoice_creation_tool import get_temporary_opening_account
from erpnext.controllers.accounts_controller import AccountMissingError
class TestOpeningInvoiceCreationTool(unittest.TestCase): class TestOpeningInvoiceCreationTool(unittest.TestCase):
def make_invoices(self, invoice_type="Sales"): def setUp(self):
if not frappe.db.exists("Company", "_Test Opening Invoice Company"):
make_company()
def make_invoices(self, invoice_type="Sales", company=None, party_1=None, party_2=None):
doc = frappe.get_single("Opening Invoice Creation Tool") doc = frappe.get_single("Opening Invoice Creation Tool")
args = get_opening_invoice_creation_dict(invoice_type=invoice_type) args = get_opening_invoice_creation_dict(invoice_type=invoice_type, company=company,
party_1=party_1, party_2=party_2)
doc.update(args) doc.update(args)
return doc.make_invoices() return doc.make_invoices()
def test_opening_sales_invoice_creation(self): def test_opening_sales_invoice_creation(self):
invoices = self.make_invoices() property_setter = make_property_setter("Sales Invoice", "update_stock", "default", 1, "Check")
invoices = self.make_invoices(company="_Test Opening Invoice Company")
self.assertEqual(len(invoices), 2) self.assertEqual(len(invoices), 2)
expected_value = { expected_value = {
@@ -27,6 +35,13 @@ class TestOpeningInvoiceCreationTool(unittest.TestCase):
} }
self.check_expected_values(invoices, expected_value) self.check_expected_values(invoices, expected_value)
si = frappe.get_doc("Sales Invoice", invoices[0])
# Check if update stock is not enabled
self.assertEqual(si.update_stock, 0)
property_setter.delete()
def check_expected_values(self, invoices, expected_value, invoice_type="Sales"): def check_expected_values(self, invoices, expected_value, invoice_type="Sales"):
doctype = "Sales Invoice" if invoice_type == "Sales" else "Purchase Invoice" doctype = "Sales Invoice" if invoice_type == "Sales" else "Purchase Invoice"
@@ -36,7 +51,7 @@ class TestOpeningInvoiceCreationTool(unittest.TestCase):
self.assertEqual(si.get(field, ""), expected_value[invoice_idx][field_idx]) self.assertEqual(si.get(field, ""), expected_value[invoice_idx][field_idx])
def test_opening_purchase_invoice_creation(self): def test_opening_purchase_invoice_creation(self):
invoices = self.make_invoices(invoice_type="Purchase") invoices = self.make_invoices(invoice_type="Purchase", company="_Test Opening Invoice Company")
self.assertEqual(len(invoices), 2) self.assertEqual(len(invoices), 2)
expected_value = { expected_value = {
@@ -46,6 +61,28 @@ class TestOpeningInvoiceCreationTool(unittest.TestCase):
} }
self.check_expected_values(invoices, expected_value, invoice_type="Purchase", ) self.check_expected_values(invoices, expected_value, invoice_type="Purchase", )
def test_opening_sales_invoice_creation_with_missing_debit_account(self):
company = "_Test Opening Invoice Company"
party_1, party_2 = make_customer("Customer A"), make_customer("Customer B")
old_default_receivable_account = frappe.db.get_value("Company", company, "default_receivable_account")
frappe.db.set_value("Company", company, "default_receivable_account", "")
if not frappe.db.exists("Cost Center", "_Test Opening Invoice Company - _TOIC"):
cc = frappe.get_doc({"doctype": "Cost Center", "cost_center_name": "_Test Opening Invoice Company",
"is_group": 1, "company": "_Test Opening Invoice Company"})
cc.insert(ignore_mandatory=True)
cc2 = frappe.get_doc({"doctype": "Cost Center", "cost_center_name": "Main", "is_group": 0,
"company": "_Test Opening Invoice Company", "parent_cost_center": cc.name})
cc2.insert()
frappe.db.set_value("Company", company, "cost_center", "Main - _TOIC")
self.assertRaises(AccountMissingError, self.make_invoices, company="_Test Opening Invoice Company", party_1=party_1, party_2=party_2)
# teardown
frappe.db.set_value("Company", company, "default_receivable_account", old_default_receivable_account)
def get_opening_invoice_creation_dict(**args): def get_opening_invoice_creation_dict(**args):
party = "Customer" if args.get("invoice_type", "Sales") == "Sales" else "Supplier" party = "Customer" if args.get("invoice_type", "Sales") == "Sales" else "Supplier"
company = args.get("company", "_Test Company") company = args.get("company", "_Test Company")
@@ -57,7 +94,7 @@ def get_opening_invoice_creation_dict(**args):
{ {
"qty": 1.0, "qty": 1.0,
"outstanding_amount": 300, "outstanding_amount": 300,
"party": "_Test {0}".format(party), "party": args.get("party_1") or "_Test {0}".format(party),
"item_name": "Opening Item", "item_name": "Opening Item",
"due_date": "2016-09-10", "due_date": "2016-09-10",
"posting_date": "2016-09-05", "posting_date": "2016-09-05",
@@ -66,7 +103,7 @@ def get_opening_invoice_creation_dict(**args):
{ {
"qty": 2.0, "qty": 2.0,
"outstanding_amount": 250, "outstanding_amount": 250,
"party": "_Test {0} 1".format(party), "party": args.get("party_2") or "_Test {0} 1".format(party),
"item_name": "Opening Item", "item_name": "Opening Item",
"due_date": "2016-09-10", "due_date": "2016-09-10",
"posting_date": "2016-09-05", "posting_date": "2016-09-05",
@@ -76,4 +113,31 @@ def get_opening_invoice_creation_dict(**args):
}) })
invoice_dict.update(args) invoice_dict.update(args)
return invoice_dict return invoice_dict
def make_company():
if frappe.db.exists("Company", "_Test Opening Invoice Company"):
return frappe.get_doc("Company", "_Test Opening Invoice Company")
company = frappe.new_doc("Company")
company.company_name = "_Test Opening Invoice Company"
company.abbr = "_TOIC"
company.default_currency = "INR"
company.country = "India"
company.insert()
return company
def make_customer(customer=None):
customer_name = customer or "Opening Customer"
customer = frappe.get_doc({
"doctype": "Customer",
"customer_name": customer_name,
"customer_group": "All Customer Groups",
"customer_type": "Company",
"territory": "All Territories"
})
if not frappe.db.exists("Customer", customer_name):
customer.insert(ignore_permissions=True)
return customer.name
else:
return frappe.db.exists("Customer", customer_name)

View File

@@ -187,7 +187,7 @@ frappe.ui.form.on('Payment Entry', {
frm.toggle_display("base_received_amount", ( frm.toggle_display("base_received_amount", (
frm.doc.paid_to_account_currency != company_currency && frm.doc.paid_to_account_currency != company_currency &&
frm.doc.paid_from_account_currency != frm.doc.paid_to_account_currency frm.doc.paid_from_account_currency != frm.doc.paid_to_account_currency
&& frm.doc.base_paid_amount != frm.doc.base_received_amount && frm.doc.base_paid_amount != frm.doc.base_received_amount
)); ));
@@ -386,6 +386,8 @@ frappe.ui.form.on('Payment Entry', {
set_account_currency_and_balance: function(frm, account, currency_field, set_account_currency_and_balance: function(frm, account, currency_field,
balance_field, callback_function) { balance_field, callback_function) {
var company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
if (frm.doc.posting_date && account) { if (frm.doc.posting_date && account) {
frappe.call({ frappe.call({
method: "erpnext.accounts.doctype.payment_entry.payment_entry.get_account_details", method: "erpnext.accounts.doctype.payment_entry.payment_entry.get_account_details",
@@ -412,6 +414,14 @@ frappe.ui.form.on('Payment Entry', {
if(!frm.doc.paid_amount && frm.doc.received_amount) if(!frm.doc.paid_amount && frm.doc.received_amount)
frm.events.received_amount(frm); frm.events.received_amount(frm);
if (frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency
&& frm.doc.paid_amount != frm.doc.received_amount) {
if (company_currency != frm.doc.paid_from_account_currency &&
frm.doc.payment_type == "Pay") {
frm.doc.paid_amount = frm.doc.received_amount;
}
}
} }
}, },
() => { () => {

View File

@@ -142,6 +142,11 @@ class PurchaseInvoice(BuyingController):
throw(_("Conversion rate cannot be 0 or 1")) throw(_("Conversion rate cannot be 0 or 1"))
def validate_credit_to_acc(self): def validate_credit_to_acc(self):
if not self.credit_to:
self.credit_to = get_party_account("Supplier", self.supplier, self.company)
if not self.credit_to:
self.raise_missing_debit_credit_account_error("Supplier", self.supplier)
account = frappe.db.get_value("Account", self.credit_to, account = frappe.db.get_value("Account", self.credit_to,
["account_type", "report_type", "account_currency"], as_dict=True) ["account_type", "report_type", "account_currency"], as_dict=True)

View File

@@ -153,8 +153,8 @@ def update_multi_mode_option(doc, pos_profile):
def get_mode_of_payment(doc): def get_mode_of_payment(doc):
return frappe.db.sql(""" return frappe.db.sql("""
select mpa.default_account, mpa.parent, mp.type as type select mpa.default_account, mpa.parent, mp.type as type
from `tabMode of Payment Account` mpa,`tabMode of Payment` mp from `tabMode of Payment Account` mpa,`tabMode of Payment` mp
where mpa.parent = mp.name and mpa.company = %(company)s and mp.enabled = 1""", where mpa.parent = mp.name and mpa.company = %(company)s and mp.enabled = 1""",
{'company': doc.company}, as_dict=1) {'company': doc.company}, as_dict=1)
@@ -394,6 +394,14 @@ def get_pricing_rule_data(doc):
between ifnull(valid_from, '2000-01-01') and ifnull(valid_upto, '2500-12-31') between ifnull(valid_from, '2000-01-01') and ifnull(valid_upto, '2500-12-31')
order by priority desc, name desc""", order by priority desc, name desc""",
{'company': doc.company, 'price_list': doc.selling_price_list, 'date': nowdate()}, as_dict=1) {'company': doc.company, 'price_list': doc.selling_price_list, 'date': nowdate()}, as_dict=1)
for row in pricing_rules:
if row.apply_on:
doctype = "Pricing Rule " + row.apply_on
apply_on = frappe.scrub(row.apply_on)
row[apply_on] = [d.get(apply_on) for d in frappe.get_all(doctype,
filters = {"parent": row.name}, fields = [apply_on])]
return pricing_rules return pricing_rules
@@ -434,10 +442,10 @@ def make_invoice(pos_profile, doc_list={}, email_queue_list={}, customers_list={
name_list.append(name) name_list.append(name)
email_queue = make_email_queue(email_queue_list) email_queue = make_email_queue(email_queue_list)
if isinstance(pos_profile, string_types): if isinstance(pos_profile, string_types):
pos_profile = json.loads(pos_profile) pos_profile = json.loads(pos_profile)
customers = get_customers_list(pos_profile) customers = get_customers_list(pos_profile)
return { return {
'invoice': name_list, 'invoice': name_list,

View File

@@ -1,6 +1,8 @@
{% include "erpnext/regional/india/taxes.js" %} {% include "erpnext/regional/india/taxes.js" %}
{% include "erpnext/regional/india/e_invoice/einvoice.js" %}
erpnext.setup_auto_gst_taxation('Sales Invoice'); erpnext.setup_auto_gst_taxation('Sales Invoice');
erpnext.setup_einvoice_actions('Sales Invoice')
frappe.ui.form.on("Sales Invoice", { frappe.ui.form.on("Sales Invoice", {
setup: function(frm) { setup: function(frm) {
@@ -12,6 +14,16 @@ frappe.ui.form.on("Sales Invoice", {
}; };
}); });
frm.set_query('transporter_address', function (doc) {
return {
query: 'frappe.contacts.doctype.address.address.address_query',
filters: {
link_doctype: 'Supplier',
link_name: doc.transporter
}
}
});
frm.set_query('driver', function(doc) { frm.set_query('driver', function(doc) {
return { return {
filters: { filters: {

View File

@@ -4,7 +4,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe, erpnext import frappe, erpnext
import frappe.defaults import frappe.defaults
from frappe.utils import cint, flt, add_months, today, date_diff, getdate, add_days, cstr, nowdate from frappe.utils import cint, flt, getdate, add_days, cstr, nowdate, formatdate
from frappe import _, msgprint, throw from frappe import _, msgprint, throw
from erpnext.accounts.party import get_party_account, get_due_date from erpnext.accounts.party import get_party_account, get_due_date
from erpnext.controllers.stock_controller import update_gl_entries_after from erpnext.controllers.stock_controller import update_gl_entries_after
@@ -225,9 +225,9 @@ class SalesInvoice(SellingController):
frappe.throw(_("At least one mode of payment is required for POS invoice.")) frappe.throw(_("At least one mode of payment is required for POS invoice."))
def before_cancel(self): def before_cancel(self):
super(SalesInvoice, self).before_cancel()
self.update_time_sheet(None) self.update_time_sheet(None)
def on_cancel(self): def on_cancel(self):
super(SalesInvoice, self).on_cancel() super(SalesInvoice, self).on_cancel()
@@ -469,6 +469,11 @@ class SalesInvoice(SellingController):
return frappe.db.sql("select abbr from tabCompany where name=%s", self.company)[0][0] return frappe.db.sql("select abbr from tabCompany where name=%s", self.company)[0][0]
def validate_debit_to_acc(self): def validate_debit_to_acc(self):
if not self.debit_to:
self.debit_to = get_party_account("Customer", self.customer, self.company)
if not self.debit_to:
self.raise_missing_debit_credit_account_error("Customer", self.customer)
account = frappe.get_cached_value("Account", self.debit_to, account = frappe.get_cached_value("Account", self.debit_to,
["account_type", "report_type", "account_currency"], as_dict=True) ["account_type", "report_type", "account_currency"], as_dict=True)
@@ -532,7 +537,12 @@ class SalesInvoice(SellingController):
self.against_income_account = ','.join(against_acc) self.against_income_account = ','.join(against_acc)
def add_remarks(self): def add_remarks(self):
if not self.remarks: self.remarks = 'No Remarks' if not self.remarks:
if self.po_no and self.po_date:
self.remarks = _("Against Customer Order {0} dated {1}").format(self.po_no,
formatdate(self.po_date))
else:
self.remarks = _("No Remarks")
def validate_auto_set_posting_time(self): def validate_auto_set_posting_time(self):
# Don't auto set the posting date and time if invoice is amended # Don't auto set the posting date and time if invoice is amended

View File

@@ -1841,93 +1841,7 @@ class TestSalesInvoice(unittest.TestCase):
self.assertEqual(target_doc.supplier, "_Test Internal Supplier") self.assertEqual(target_doc.supplier, "_Test Internal Supplier")
def test_eway_bill_json(self): def test_eway_bill_json(self):
if not frappe.db.exists('Address', '_Test Address for Eway bill-Billing'): si = make_sales_invoice_for_ewaybill()
address = frappe.get_doc({
"address_line1": "_Test Address Line 1",
"address_title": "_Test Address for Eway bill",
"address_type": "Billing",
"city": "_Test City",
"state": "Test State",
"country": "India",
"doctype": "Address",
"is_primary_address": 1,
"phone": "+91 0000000000",
"gstin": "27AAECE4835E1ZR",
"gst_state": "Maharashtra",
"gst_state_number": "27",
"pincode": "401108"
}).insert()
address.append("links", {
"link_doctype": "Company",
"link_name": "_Test Company"
})
address.save()
if not frappe.db.exists('Address', '_Test Customer-Address for Eway bill-Shipping'):
address = frappe.get_doc({
"address_line1": "_Test Address Line 1",
"address_title": "_Test Customer-Address for Eway bill",
"address_type": "Shipping",
"city": "_Test City",
"state": "Test State",
"country": "India",
"doctype": "Address",
"is_primary_address": 1,
"phone": "+91 0000000000",
"gst_state": "Maharashtra",
"gst_state_number": "27",
"pincode": "410038"
}).insert()
address.append("links", {
"link_doctype": "Customer",
"link_name": "_Test Customer"
})
address.save()
gst_settings = frappe.get_doc("GST Settings")
gst_account = frappe.get_all(
"GST Account",
fields=["cgst_account", "sgst_account", "igst_account"],
filters = {"company": "_Test Company"})
if not gst_account:
gst_settings.append("gst_accounts", {
"company": "_Test Company",
"cgst_account": "CGST - _TC",
"sgst_account": "SGST - _TC",
"igst_account": "IGST - _TC",
})
gst_settings.save()
si = create_sales_invoice(do_not_save =1, rate = '60000')
si.distance = 2000
si.company_address = "_Test Address for Eway bill-Billing"
si.customer_address = "_Test Customer-Address for Eway bill-Shipping"
si.vehicle_no = "KA12KA1234"
si.gst_category = "Registered Regular"
si.append("taxes", {
"charge_type": "On Net Total",
"account_head": "CGST - _TC",
"cost_center": "Main - _TC",
"description": "CGST @ 9.0",
"rate": 9
})
si.append("taxes", {
"charge_type": "On Net Total",
"account_head": "SGST - _TC",
"cost_center": "Main - _TC",
"description": "SGST @ 9.0",
"rate": 9
})
si.submit() si.submit()
@@ -1943,6 +1857,187 @@ class TestSalesInvoice(unittest.TestCase):
self.assertEqual(data['billLists'][0]['sgstValue'], 5400) self.assertEqual(data['billLists'][0]['sgstValue'], 5400)
self.assertEqual(data['billLists'][0]['vehicleNo'], 'KA12KA1234') self.assertEqual(data['billLists'][0]['vehicleNo'], 'KA12KA1234')
self.assertEqual(data['billLists'][0]['itemList'][0]['taxableAmount'], 60000) self.assertEqual(data['billLists'][0]['itemList'][0]['taxableAmount'], 60000)
def test_einvoice_submission_without_irn(self):
# init
frappe.db.set_value('E Invoice Settings', 'E Invoice Settings', 'enable', 1)
country = frappe.flags.country
frappe.flags.country = 'India'
si = make_sales_invoice_for_ewaybill()
self.assertRaises(frappe.ValidationError, si.submit)
si.irn = 'test_irn'
si.submit()
# reset
frappe.db.set_value('E Invoice Settings', 'E Invoice Settings', 'enable', 0)
frappe.flags.country = country
def test_einvoice_json(self):
from erpnext.regional.india.e_invoice.utils import make_einvoice
customer_gstin = '27AACCM7806M1Z3'
customer_gstin_dtls = {
'LegalName': '_Test Customer', 'TradeName': '_Test Customer', 'AddrLoc': '_Test City',
'StateCode': '27', 'AddrPncd': '410038', 'AddrBno': '_Test Bldg',
'AddrBnm': '100', 'AddrFlno': '200', 'AddrSt': '_Test Street'
}
company_gstin = '27AAECE4835E1ZR'
company_gstin_dtls = {
'LegalName': '_Test Company', 'TradeName': '_Test Company', 'AddrLoc': '_Test City',
'StateCode': '27', 'AddrPncd': '401108', 'AddrBno': '_Test Bldg',
'AddrBnm': '100', 'AddrFlno': '200', 'AddrSt': '_Test Street'
}
# set cache gstin details to avoid fetching details which will require connection to GSP servers
frappe.local.gstin_cache = {}
frappe.local.gstin_cache[customer_gstin] = customer_gstin_dtls
frappe.local.gstin_cache[company_gstin] = company_gstin_dtls
si = make_sales_invoice_for_ewaybill()
si.naming_series = 'INV-2020-.#####'
si.items = []
si.append("items", {
"item_code": "_Test Item",
"uom": "Nos",
"warehouse": "_Test Warehouse - _TC",
"qty": 2,
"rate": 100,
"income_account": "Sales - _TC",
"expense_account": "Cost of Goods Sold - _TC",
"cost_center": "_Test Cost Center - _TC",
})
si.append("items", {
"item_code": "_Test Item 2",
"uom": "Nos",
"warehouse": "_Test Warehouse - _TC",
"qty": 4,
"rate": 150,
"income_account": "Sales - _TC",
"expense_account": "Cost of Goods Sold - _TC",
"cost_center": "_Test Cost Center - _TC",
})
si.save()
einvoice = make_einvoice(si)
total_item_ass_value = sum([d['AssAmt'] for d in einvoice['ItemList']])
total_item_cgst_value = sum([d['CgstAmt'] for d in einvoice['ItemList']])
total_item_sgst_value = sum([d['SgstAmt'] for d in einvoice['ItemList']])
total_item_igst_value = sum([d['IgstAmt'] for d in einvoice['ItemList']])
total_item_value = sum([d['TotItemVal'] for d in einvoice['ItemList']])
self.assertEqual(einvoice['Version'], '1.1')
self.assertEqual(einvoice['ValDtls']['AssVal'], total_item_ass_value)
self.assertEqual(einvoice['ValDtls']['CgstVal'], total_item_cgst_value)
self.assertEqual(einvoice['ValDtls']['SgstVal'], total_item_sgst_value)
self.assertEqual(einvoice['ValDtls']['IgstVal'], total_item_igst_value)
self.assertEqual(einvoice['ValDtls']['TotInvVal'], total_item_value)
self.assertTrue(einvoice['EwbDtls'])
def make_sales_invoice_for_ewaybill():
if not frappe.db.exists('Address', '_Test Address for Eway bill-Billing'):
address = frappe.get_doc({
"address_line1": "_Test Address Line 1",
"address_title": "_Test Address for Eway bill",
"address_type": "Billing",
"city": "_Test City",
"state": "Test State",
"country": "India",
"doctype": "Address",
"is_primary_address": 1,
"phone": "+910000000000",
"gstin": "27AAECE4835E1ZR",
"gst_state": "Maharashtra",
"gst_state_number": "27",
"pincode": "401108"
}).insert()
address.append("links", {
"link_doctype": "Company",
"link_name": "_Test Company"
})
address.save()
if not frappe.db.exists('Address', '_Test Customer-Address for Eway bill-Shipping'):
address = frappe.get_doc({
"address_line1": "_Test Address Line 1",
"address_title": "_Test Customer-Address for Eway bill",
"address_type": "Shipping",
"city": "_Test City",
"state": "Test State",
"country": "India",
"doctype": "Address",
"is_primary_address": 1,
"phone": "+910000000000",
"gstin": "27AACCM7806M1Z3",
"gst_state": "Maharashtra",
"gst_state_number": "27",
"pincode": "410038"
}).insert()
address.append("links", {
"link_doctype": "Customer",
"link_name": "_Test Customer"
})
address.save()
if not frappe.db.exists('Supplier', '_Test Transporter'):
frappe.get_doc({
"doctype": "Supplier",
"supplier_name": "_Test Transporter",
"country": "India",
"supplier_group": "_Test Supplier Group",
"supplier_type": "Company",
"is_transporter": 1
}).insert()
gst_settings = frappe.get_doc("GST Settings")
gst_account = frappe.get_all(
"GST Account",
fields=["cgst_account", "sgst_account", "igst_account"],
filters = {"company": "_Test Company"})
if not gst_account:
gst_settings.append("gst_accounts", {
"company": "_Test Company",
"cgst_account": "CGST - _TC",
"sgst_account": "SGST - _TC",
"igst_account": "IGST - _TC",
})
gst_settings.save()
si = create_sales_invoice(do_not_save =1, rate = '60000')
si.distance = 2000
si.company_address = "_Test Address for Eway bill-Billing"
si.customer_address = "_Test Customer-Address for Eway bill-Shipping"
si.vehicle_no = "KA12KA1234"
si.gst_category = "Registered Regular"
si.mode_of_transport = 'Road'
si.transporter = '_Test Transporter'
si.append("taxes", {
"charge_type": "On Net Total",
"account_head": "CGST - _TC",
"cost_center": "Main - _TC",
"description": "CGST @ 9.0",
"rate": 9
})
si.append("taxes", {
"charge_type": "On Net Total",
"account_head": "SGST - _TC",
"cost_center": "Main - _TC",
"description": "SGST @ 9.0",
"rate": 9
})
return si
def test_item_tax_validity(self): def test_item_tax_validity(self):
item = frappe.get_doc("Item", "_Test Item 2") item = frappe.get_doc("Item", "_Test Item 2")

View File

@@ -333,7 +333,7 @@ class Subscription(Document):
if not self.generate_invoice_at_period_start: if not self.generate_invoice_at_period_start:
return False return False
if self.is_new_subscription(): if self.is_new_subscription() and getdate(nowdate()) >= getdate(self.current_invoice_start):
return True return True
# Check invoice dates and make sure it doesn't have outstanding invoices # Check invoice dates and make sure it doesn't have outstanding invoices

View File

@@ -2018,34 +2018,57 @@ erpnext.pos.PointOfSale = erpnext.taxes_and_totals.extend({
apply_pricing_rule: function () { apply_pricing_rule: function () {
var me = this; var me = this;
var remove_item = false;
$.each(this.frm.doc["items"], function (n, item) { $.each(this.frm.doc["items"], function (n, item) {
var pricing_rule = me.get_pricing_rule(item) var pricing_rule = me.get_pricing_rule(item)
me.validate_pricing_rule(pricing_rule) me.validate_pricing_rule(pricing_rule)
if (pricing_rule.length) { if (pricing_rule.length) {
item.pricing_rule = pricing_rule[0].name; if (pricing_rule[0].price_or_product_discount == "Price") {
item.margin_type = pricing_rule[0].margin_type; item.pricing_rule = pricing_rule[0].name;
item.price_list_rate = pricing_rule[0].price || item.price_list_rate; item.margin_type = pricing_rule[0].margin_type;
item.margin_rate_or_amount = pricing_rule[0].margin_rate_or_amount; item.price_list_rate = pricing_rule[0].price || item.price_list_rate;
item.discount_percentage = pricing_rule[0].discount_percentage || 0.0; item.margin_rate_or_amount = pricing_rule[0].margin_rate_or_amount;
me.apply_pricing_rule_on_item(item) item.discount_percentage = pricing_rule[0].discount_percentage || 0.0;
me.apply_pricing_rule_on_item(item)
} else {
me.child = frappe.model.add_child(me.frm.doc, me.frm.doc.doctype + " Item", "items");
me.child.item_code = pricing_rule[0].same_item ? item.item_code : pricing_rule[0].free_item;
me.child.item_name = pricing_rule[0].same_item ? item.item_name : pricing_rule[0].free_item;
me.child.stock_uom = pricing_rule[0].same_item ? item.stock_uom : pricing_rule[0].free_item_uom;
me.child.uom = pricing_rule[0].same_item ? item.uom : pricing_rule[0].free_item_uom;
me.child.conversion_factor = 1;
me.child.qty = pricing_rule.qty || 1;
me.child.is_free_item = 1;
me.child.brand = pricing_rule[0].same_item ? item.brand : "";
me.child.description = pricing_rule[0].same_item ? item.description : pricing_rule[0].free_item;
}
} else if (item.pricing_rule) { } else if (item.pricing_rule) {
item.price_list_rate = me.price_list_data[item.item_code] item.price_list_rate = me.price_list_data[item.item_code]
item.margin_rate_or_amount = 0.0; item.margin_rate_or_amount = 0.0;
item.discount_percentage = 0.0; item.discount_percentage = 0.0;
item.pricing_rule = null; item.pricing_rule = null;
me.apply_pricing_rule_on_item(item) me.apply_pricing_rule_on_item(item)
} else if (item.is_free_item) {
remove_item = true;
item.qty = 0
} }
if(item.discount_percentage > 0) { if(item.discount_percentage > 0) {
me.apply_pricing_rule_on_item(item) me.apply_pricing_rule_on_item(item)
} }
}) });
if (remove_item) {
this.remove_zero_qty_items_from_cart();
}
}, },
get_pricing_rule: function (item) { get_pricing_rule: function (item) {
var me = this; var me = this;
return $.grep(this.pricing_rules, function (data) { return $.grep(this.pricing_rules, function (data) {
if (item.qty >= data.min_qty && (item.qty <= (data.max_qty ? data.max_qty : item.qty))) { me.get_mixed_min_max_qty_and_amt(data, item);
if (data.mixed_qty >= data.min_qty && (data.mixed_qty <= (data.max_qty ? data.max_qty : data.mixed_qty))) {
if (me.validate_item_condition(data, item)) { if (me.validate_item_condition(data, item)) {
if (in_list(['Customer', 'Customer Group', 'Territory', 'Campaign'], data.applicable_for)) { if (in_list(['Customer', 'Customer Group', 'Territory', 'Campaign'], data.applicable_for)) {
return me.validate_condition(data) return me.validate_condition(data)
@@ -2057,11 +2080,26 @@ erpnext.pos.PointOfSale = erpnext.taxes_and_totals.extend({
}) })
}, },
get_mixed_min_max_qty_and_amt: function(data, item) {
var apply_on = frappe.model.scrub(data.apply_on);
data.mixed_qty = 0.0
if (data.mixed_conditions && in_list(data[apply_on], item[apply_on])) {
this.frm.doc.items.forEach(d => {
if (in_list(data[apply_on], d[apply_on])) {
data.mixed_qty += d.qty;
data.mixed_amt += d.amount;
}
});
} else {
data.mixed_qty = item.qty;
data.mixed_amt = item.amount;
}
},
validate_item_condition: function (data, item) { validate_item_condition: function (data, item) {
var apply_on = frappe.model.scrub(data.apply_on); var apply_on = frappe.model.scrub(data.apply_on);
return (data.apply_on == 'Item Group') return in_list(data[apply_on], item[apply_on]);
? this.validate_item_group(data.item_group, item.item_group) : (data[apply_on] == item[apply_on]);
}, },
validate_item_group: function (pr_item_group, cart_item_group) { validate_item_group: function (pr_item_group, cart_item_group) {

View File

@@ -0,0 +1,166 @@
{%- from "templates/print_formats/standard_macros.html" import add_header, render_field, print_value -%}
{%- set einvoice = json.loads(doc.signed_einvoice) -%}
<div class="page-break">
<div {% if print_settings.repeat_header_footer %} id="header-html" class="hidden-pdf" {% endif %}>
{% if letter_head and not no_letterhead %}
<div class="letter-head">{{ letter_head }}</div>
{% endif %}
<div class="print-heading">
<h2>E Invoice<br><small>{{ doc.name }}</small></h2>
</div>
</div>
{% if print_settings.repeat_header_footer %}
<div id="footer-html" class="visible-pdf">
{% if not no_letterhead and footer %}
<div class="letter-head-footer">
{{ footer }}
</div>
{% endif %}
<p class="text-center small page-number visible-pdf">
{{ _("Page {0} of {1}").format('<span class="page"></span>', '<span class="topage"></span>') }}
</p>
</div>
{% endif %}
<div class="row section-break" style="border-bottom: 1px solid #d1d8dd; padding-bottom: 10px;">
<h5 class="font-bold" style="margin-left: 15px; margin-top: 0px;">1. Transaction Details</h5>
<div class="col-xs-7 column-break">
<div class="row data-field">
<div class="col-xs-4"><label>IRN</label></div>
<div class="col-xs-8 value">{{ einvoice.Irn }}</div>
</div>
<div class="row data-field">
<div class="col-xs-4"><label>Ack. No</label></div>
<div class="col-xs-8 value">{{ einvoice.AckNo }}</div>
</div>
<div class="row data-field">
<div class="col-xs-4"><label>Ack. Date</label></div>
<div class="col-xs-8 value">{{ frappe.utils.format_datetime(einvoice.AckDt, "dd/MM/yyyy hh:mm:ss") }}</div>
</div>
<div class="row data-field">
<div class="col-xs-4"><label>Category</label></div>
<div class="col-xs-8 value">{{ einvoice.TranDtls.SupTyp }}</div>
</div>
<div class="row data-field">
<div class="col-xs-4"><label>Document Type</label></div>
<div class="col-xs-8 value">{{ einvoice.DocDtls.Typ }}</div>
</div>
<div class="row data-field">
<div class="col-xs-4"><label>Document No</label></div>
<div class="col-xs-8 value">{{ einvoice.DocDtls.No }}</div>
</div>
</div>
<div class="col-xs-5 column-break">
<img src="{{ doc.qrcode_image }}" alt="QRCode Image" style="
width: 175px; height: 175px;
float: right; border: 1px solid gray;
display: flex; align-items: center; justify-content: center;
">
</div>
</div>
<div class="row section-break" style="border-bottom: 1px solid #d1d8dd; padding-bottom: 10px;">
<h5 class="font-bold" style="margin-left: 15px; margin-bottom: 0px;">2. Party Details</h5>
{%- set seller = einvoice.SellerDtls -%}
<div class="col-xs-6 column-break">
<h5 style="margin-bottom: 5px;">Seller</h5>
<p>{{ seller.Gstin }}</p>
<p>{{ seller.LglNm }}</p>
<p>{{ seller.Addr1 }}</p>
{%- if seller.Addr2 -%} <p>{{ seller.Addr2 }}</p> {% endif %}
<p>{{ seller.Loc }}</p>
<p>{{ frappe.db.get_value("Address", doc.company_address, "gst_state") }} - {{ seller.Pin }}</p>
{%- if einvoice.ShipDtls -%}
{%- set shipping = einvoice.ShipDtls -%}
<h5 style="margin-bottom: 5px;">Shipping</h5>
<p>{{ shipping.Gstin }}</p>
<p>{{ shipping.LglNm }}</p>
<p>{{ shipping.Addr1 }}</p>
{%- if shipping.Addr2 -%} <p>{{ shipping.Addr2 }}</p> {% endif %}
<p>{{ shipping.Loc }}</p>
<p>{{ frappe.db.get_value("Address", doc.shipping_address_name, "gst_state") }} - {{ shipping.Pin }}</p>
{% endif %}
</div>
{%- set buyer = einvoice.BuyerDtls -%}
<div class="col-xs-6 column-break">
<h5 style="margin-bottom: 5px;">Buyer</h5>
<p>{{ buyer.Gstin }}</p>
<p>{{ buyer.LglNm }}</p>
<p>{{ buyer.Addr1 }}</p>
{%- if buyer.Addr2 -%} <p>{{ buyer.Addr2 }}</p> {% endif %}
<p>{{ buyer.Loc }}</p>
<p>{{ frappe.db.get_value("Address", doc.customer_address, "gst_state") }} - {{ buyer.Pin }}</p>
</div>
</div>
<div style="overflow-x: auto;">
<h5 class="font-bold" style="margin-bottom: 0px;">3. Item Details</h5>
<table class="table table-bordered">
<thead>
<tr>
<th class="text-left" style="width: 3%;">Sr. No.</th>
<th class="text-left">Item</th>
<th class="text-left" style="width: 10%;">HSN Code</th>
<th class="text-left" style="width: 5%;">Qty</th>
<th class="text-left" style="width: 5%;">UOM</th>
<th class="text-left">Rate</th>
<th class="text-left" style="width: 5%;">Discount</th>
<th class="text-left">Taxable Amount</th>
<th class="text-left" style="width: 7%;">Tax Rate</th>
<th class="text-left" style="width: 5%;">Other Charges</th>
<th class="text-left">Total</th>
</tr>
</thead>
<tbody>
{% for item in einvoice.ItemList %}
<tr>
<td class="text-left" style="width: 3%;">{{ item.SlNo }}</td>
<td class="text-left">{{ item.PrdDesc }}</td>
<td class="text-left" style="width: 10%;">{{ item.HsnCd }}</td>
<td class="text-right" style="width: 5%;">{{ item.Qty }}</td>
<td class="text-left" style="width: 5%;">{{ item.Unit }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(item.UnitPrice, None, "INR") }}</td>
<td class="text-right" style="width: 5%;">{{ frappe.utils.fmt_money(item.Discount, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(item.AssAmt, None, "INR") }}</td>
<td class="text-right" style="width: 7%;">{{ item.GstRt + item.CesRt }} %</td>
<td class="text-right" style="width: 5%;">{{ frappe.utils.fmt_money(0, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(item.TotItemVal, None, "INR") }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div style="overflow-x: auto;">
<h5 class="font-bold" style="margin-bottom: 0px;">4. Value Details</h5>
<table class="table table-bordered">
<thead>
<tr>
<th class="text-left">Taxable Amount</th>
<th class="text-left">CGST</th>
<th class="text-left"">SGST</th>
<th class="text-left">IGST</th>
<th class="text-left">CESS</th>
<th class="text-left" style="width: 10%;">State CESS</th>
<th class="text-left">Discount</th>
<th class="text-left" style="width: 10%;">Other Charges</th>
<th class="text-left" style="width: 10%;">Round Off</th>
<th class="text-left">Total Value</th>
</tr>
</thead>
<tbody>
{%- set value_details = einvoice.ValDtls -%}
<tr>
<td class="text-right">{{ frappe.utils.fmt_money(value_details.AssVal, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(value_details.CgstVal, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(value_details.SgstVal, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(value_details.IgstVal, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(value_details.CesVal, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(0, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(value_details.Discount, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(0, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(value_details.RndOffAmt, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(value_details.TotInvVal, None, "INR") }}</td>
</tr>
</tbody>
</table>
</div>
</div>

View File

@@ -0,0 +1,24 @@
{
"align_labels_right": 1,
"creation": "2020-10-10 18:01:21.032914",
"custom_format": 0,
"default_print_language": "en-US",
"disabled": 1,
"doc_type": "Sales Invoice",
"docstatus": 0,
"doctype": "Print Format",
"font": "Default",
"html": "",
"idx": 0,
"line_breaks": 1,
"modified": "2020-10-23 19:54:40.634936",
"modified_by": "Administrator",
"module": "Accounts",
"name": "GST E-Invoice",
"owner": "Administrator",
"print_format_builder": 0,
"print_format_type": "Jinja",
"raw_printing": 0,
"show_section_headings": 1,
"standard": "Yes"
}

View File

@@ -42,11 +42,13 @@
{% if(filters.show_future_payments) { %} {% if(filters.show_future_payments) { %}
{% var balance_row = data.slice(-1).pop(); {% var balance_row = data.slice(-1).pop();
var range1 = report.columns[11].label; var start = filters.based_on_payment_terms ? 13 : 11;
var range2 = report.columns[12].label; var range1 = report.columns[start].label;
var range3 = report.columns[13].label; var range2 = report.columns[start+1].label;
var range4 = report.columns[14].label; var range3 = report.columns[start+2].label;
var range5 = report.columns[15].label; var range4 = report.columns[start+3].label;
var range5 = report.columns[start+4].label;
var range6 = report.columns[start+5].label;
%} %}
{% if(balance_row) { %} {% if(balance_row) { %}
<table class="table table-bordered table-condensed"> <table class="table table-bordered table-condensed">
@@ -70,20 +72,34 @@
<th>{%= __(range3) %}</th> <th>{%= __(range3) %}</th>
<th>{%= __(range4) %}</th> <th>{%= __(range4) %}</th>
<th>{%= __(range5) %}</th> <th>{%= __(range5) %}</th>
<th>{%= __(range6) %}</th>
<th>{%= __("Total") %}</th> <th>{%= __("Total") %}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr> <tr>
<td>{%= __("Total Outstanding") %}</td> <td>{%= __("Total Outstanding") %}</td>
<td class="text-right">{%= format_number(balance_row["range1"], null, 2) %}</td> <td class="text-right">
<td class="text-right">{%= format_currency(balance_row["range2"]) %}</td> {%= format_number(balance_row["age"], null, 2) %}
<td class="text-right">{%= format_currency(balance_row["range3"]) %}</td> </td>
<td class="text-right">{%= format_currency(balance_row["range4"]) %}</td> <td class="text-right">
<td class="text-right">{%= format_currency(balance_row["range5"]) %}</td> {%= format_currency(balance_row["range1"], data[data.length-1]["currency"]) %}
</td>
<td class="text-right">
{%= format_currency(balance_row["range2"], data[data.length-1]["currency"]) %}
</td>
<td class="text-right">
{%= format_currency(balance_row["range3"], data[data.length-1]["currency"]) %}
</td>
<td class="text-right">
{%= format_currency(balance_row["range4"], data[data.length-1]["currency"]) %}
</td>
<td class="text-right">
{%= format_currency(balance_row["range5"], data[data.length-1]["currency"]) %}
</td>
<td class="text-right"> <td class="text-right">
{%= format_currency(flt(balance_row["outstanding"]), data[data.length-1]["currency"]) %} {%= format_currency(flt(balance_row["outstanding"]), data[data.length-1]["currency"]) %}
</td> </td>
</tr> </tr>
<td>{%= __("Future Payments") %}</td> <td>{%= __("Future Payments") %}</td>
<td></td> <td></td>
@@ -91,6 +107,7 @@
<td></td> <td></td>
<td></td> <td></td>
<td></td> <td></td>
<td></td>
<td class="text-right"> <td class="text-right">
{%= format_currency(flt(balance_row[("future_amount")]), data[data.length-1]["currency"]) %} {%= format_currency(flt(balance_row[("future_amount")]), data[data.length-1]["currency"]) %}
</td> </td>
@@ -101,6 +118,7 @@
<th></th> <th></th>
<th></th> <th></th>
<th></th> <th></th>
<th></th>
<th class="text-right"> <th class="text-right">
{%= format_currency(flt(balance_row["outstanding"] - balance_row[("future_amount")]), data[data.length-1]["currency"]) %}</th> {%= format_currency(flt(balance_row["outstanding"] - balance_row[("future_amount")]), data[data.length-1]["currency"]) %}</th>
</tr> </tr>
@@ -218,15 +236,15 @@
<td></td> <td></td>
<td style="text-align: right"><b>{%= __("Total") %}</b></td> <td style="text-align: right"><b>{%= __("Total") %}</b></td>
<td style="text-align: right"> <td style="text-align: right">
{%= format_currency(data[i]["invoiced"], data[0]["currency"] ) %}</td> {%= format_currency(data[i]["invoiced"], data[i]["currency"] ) %}</td>
{% if(!filters.show_future_payments) { %} {% if(!filters.show_future_payments) { %}
<td style="text-align: right"> <td style="text-align: right">
{%= format_currency(data[i]["paid"], data[0]["currency"]) %}</td> {%= format_currency(data[i]["paid"], data[i]["currency"]) %}</td>
<td style="text-align: right">{%= format_currency(data[i]["credit_note"], data[0]["currency"]) %} </td> <td style="text-align: right">{%= format_currency(data[i]["credit_note"], data[i]["currency"]) %} </td>
{% } %} {% } %}
<td style="text-align: right"> <td style="text-align: right">
{%= format_currency(data[i]["outstanding"], data[0]["currency"]) %}</td> {%= format_currency(data[i]["outstanding"], data[i]["currency"]) %}</td>
{% if(filters.show_future_payments) { %} {% if(filters.show_future_payments) { %}
{% if(report.report_name === "Accounts Receivable") { %} {% if(report.report_name === "Accounts Receivable") { %}
@@ -234,8 +252,8 @@
{%= data[i]["po_no"] %}</td> {%= data[i]["po_no"] %}</td>
{% } %} {% } %}
<td style="text-align: right">{%= data[i]["future_ref"] %}</td> <td style="text-align: right">{%= data[i]["future_ref"] %}</td>
<td style="text-align: right">{%= format_currency(data[i]["future_amount"], data[0]["currency"]) %}</td> <td style="text-align: right">{%= format_currency(data[i]["future_amount"], data[i]["currency"]) %}</td>
<td style="text-align: right">{%= format_currency(data[i]["remaining_balance"], data[0]["currency"]) %}</td> <td style="text-align: right">{%= format_currency(data[i]["remaining_balance"], data[i]["currency"]) %}</td>
{% } %} {% } %}
{% } %} {% } %}
{% } else { %} {% } else { %}
@@ -256,10 +274,10 @@
{% } else { %} {% } else { %}
<td><b>{%= __("Total") %}</b></td> <td><b>{%= __("Total") %}</b></td>
{% } %} {% } %}
<td style="text-align: right">{%= format_currency(data[i]["invoiced"], data[0]["currency"]) %}</td> <td style="text-align: right">{%= format_currency(data[i]["invoiced"], data[i]["currency"]) %}</td>
<td style="text-align: right">{%= format_currency(data[i]["paid"], data[0]["currency"]) %}</td> <td style="text-align: right">{%= format_currency(data[i]["paid"], data[i]["currency"]) %}</td>
<td style="text-align: right">{%= format_currency(data[i]["credit_note"], data[0]["currency"]) %}</td> <td style="text-align: right">{%= format_currency(data[i]["credit_note"], data[i]["currency"]) %}</td>
<td style="text-align: right">{%= format_currency(data[i]["outstanding"], data[0]["currency"]) %}</td> <td style="text-align: right">{%= format_currency(data[i]["outstanding"], data[i]["currency"]) %}</td>
{% } %} {% } %}
{% } %} {% } %}
</tr> </tr>

View File

@@ -8,6 +8,7 @@ from frappe.utils import flt
from erpnext.accounts.report.item_wise_sales_register.item_wise_sales_register import (get_tax_accounts, from erpnext.accounts.report.item_wise_sales_register.item_wise_sales_register import (get_tax_accounts,
get_grand_total, add_total_row, get_display_value, get_group_by_and_display_fields, add_sub_total_row, get_grand_total, add_total_row, get_display_value, get_group_by_and_display_fields, add_sub_total_row,
get_group_by_conditions) get_group_by_conditions)
from erpnext.selling.report.item_wise_sales_history.item_wise_sales_history import get_item_details
def execute(filters=None): def execute(filters=None):
return _execute(filters) return _execute(filters)
@@ -23,7 +24,7 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum
aii_account_map = get_aii_accounts() aii_account_map = get_aii_accounts()
if item_list: if item_list:
itemised_tax, tax_columns = get_tax_accounts(item_list, columns, company_currency, itemised_tax, tax_columns = get_tax_accounts(item_list, columns, company_currency,
doctype="Purchase Invoice", tax_doctype="Purchase Taxes and Charges") doctype='Purchase Invoice', tax_doctype='Purchase Taxes and Charges')
po_pr_map = get_purchase_receipts_against_purchase_order(item_list) po_pr_map = get_purchase_receipts_against_purchase_order(item_list)
@@ -35,10 +36,14 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum
if filters.get('group_by'): if filters.get('group_by'):
grand_total = get_grand_total(filters, 'Purchase Invoice') grand_total = get_grand_total(filters, 'Purchase Invoice')
item_details = get_item_details()
for d in item_list: for d in item_list:
if not d.stock_qty: if not d.stock_qty:
continue continue
item_record = item_details.get(d.item_code)
purchase_receipt = None purchase_receipt = None
if d.purchase_receipt: if d.purchase_receipt:
purchase_receipt = d.purchase_receipt purchase_receipt = d.purchase_receipt
@@ -49,8 +54,8 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum
row = { row = {
'item_code': d.item_code, 'item_code': d.item_code,
'item_name': d.item_name, 'item_name': item_record.item_name,
'item_group': d.item_group, 'item_group': item_record.item_group,
'description': d.description, 'description': d.description,
'invoice': d.parent, 'invoice': d.parent,
'posting_date': d.posting_date, 'posting_date': d.posting_date,
@@ -82,10 +87,10 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum
for tax in tax_columns: for tax in tax_columns:
item_tax = itemised_tax.get(d.name, {}).get(tax, {}) item_tax = itemised_tax.get(d.name, {}).get(tax, {})
row.update({ row.update({
frappe.scrub(tax + ' Rate'): item_tax.get("tax_rate", 0), frappe.scrub(tax + ' Rate'): item_tax.get('tax_rate', 0),
frappe.scrub(tax + ' Amount'): item_tax.get("tax_amount", 0), frappe.scrub(tax + ' Amount'): item_tax.get('tax_amount', 0),
}) })
total_tax += flt(item_tax.get("tax_amount")) total_tax += flt(item_tax.get('tax_amount'))
row.update({ row.update({
'total_tax': total_tax, 'total_tax': total_tax,
@@ -317,8 +322,8 @@ def get_items(filters, additional_query_columns):
select select
`tabPurchase Invoice Item`.`name`, `tabPurchase Invoice Item`.`parent`, `tabPurchase Invoice Item`.`name`, `tabPurchase Invoice Item`.`parent`,
`tabPurchase Invoice`.posting_date, `tabPurchase Invoice`.credit_to, `tabPurchase Invoice`.company, `tabPurchase Invoice`.posting_date, `tabPurchase Invoice`.credit_to, `tabPurchase Invoice`.company,
`tabPurchase Invoice`.supplier, `tabPurchase Invoice`.remarks, `tabPurchase Invoice`.base_net_total, `tabPurchase Invoice Item`.`item_code`, `tabPurchase Invoice`.supplier, `tabPurchase Invoice`.remarks, `tabPurchase Invoice`.base_net_total,
`tabPurchase Invoice Item`.`item_name`, `tabPurchase Invoice Item`.`item_group`, `tabPurchase Invoice Item`.description, `tabPurchase Invoice Item`.`item_code`, `tabPurchase Invoice Item`.description,
`tabPurchase Invoice Item`.`project`, `tabPurchase Invoice Item`.`purchase_order`, `tabPurchase Invoice Item`.`project`, `tabPurchase Invoice Item`.`purchase_order`,
`tabPurchase Invoice Item`.`purchase_receipt`, `tabPurchase Invoice Item`.`po_detail`, `tabPurchase Invoice Item`.`purchase_receipt`, `tabPurchase Invoice Item`.`po_detail`,
`tabPurchase Invoice Item`.`expense_account`, `tabPurchase Invoice Item`.`stock_qty`, `tabPurchase Invoice Item`.`expense_account`, `tabPurchase Invoice Item`.`stock_qty`,

View File

@@ -8,6 +8,7 @@ from frappe.utils import flt, cstr
from frappe.model.meta import get_field_precision from frappe.model.meta import get_field_precision
from frappe.utils.xlsxutils import handle_html from frappe.utils.xlsxutils import handle_html
from erpnext.accounts.report.sales_register.sales_register import get_mode_of_payments from erpnext.accounts.report.sales_register.sales_register import get_mode_of_payments
from erpnext.selling.report.item_wise_sales_history.item_wise_sales_history import get_item_details, get_customer_details
def execute(filters=None): def execute(filters=None):
return _execute(filters) return _execute(filters)
@@ -17,7 +18,7 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum
filters.update({"from_date": filters.get("date_range") and filters.get("date_range")[0], "to_date": filters.get("date_range") and filters.get("date_range")[1]}) filters.update({"from_date": filters.get("date_range") and filters.get("date_range")[0], "to_date": filters.get("date_range") and filters.get("date_range")[1]})
columns = get_columns(additional_table_columns, filters) columns = get_columns(additional_table_columns, filters)
company_currency = frappe.get_cached_value('Company', filters.get("company"), "default_currency") company_currency = frappe.get_cached_value('Company', filters.get('company'), 'default_currency')
item_list = get_items(filters, additional_query_columns) item_list = get_items(filters, additional_query_columns)
if item_list: if item_list:
@@ -34,7 +35,13 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum
if filters.get('group_by'): if filters.get('group_by'):
grand_total = get_grand_total(filters, 'Sales Invoice') grand_total = get_grand_total(filters, 'Sales Invoice')
customer_details = get_customer_details()
item_details = get_item_details()
for d in item_list: for d in item_list:
customer_record = customer_details.get(d.customer)
item_record = item_details.get(d.item_code)
delivery_note = None delivery_note = None
if d.delivery_note: if d.delivery_note:
delivery_note = d.delivery_note delivery_note = d.delivery_note
@@ -46,14 +53,14 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum
row = { row = {
'item_code': d.item_code, 'item_code': d.item_code,
'item_name': d.item_name, 'item_name': item_record.item_name,
'item_group': d.item_group, 'item_group': item_record.item_group,
'description': d.description, 'description': d.description,
'invoice': d.parent, 'invoice': d.parent,
'posting_date': d.posting_date, 'posting_date': d.posting_date,
'customer': d.customer, 'customer': d.customer,
'customer_name': d.customer_name, 'customer_name': customer_record.customer_name,
'customer_group': d.customer_group, 'customer_group': customer_record.customer_group,
} }
if additional_query_columns: if additional_query_columns:
@@ -91,10 +98,10 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum
for tax in tax_columns: for tax in tax_columns:
item_tax = itemised_tax.get(d.name, {}).get(tax, {}) item_tax = itemised_tax.get(d.name, {}).get(tax, {})
row.update({ row.update({
frappe.scrub(tax + ' Rate'): item_tax.get("tax_rate", 0), frappe.scrub(tax + ' Rate'): item_tax.get('tax_rate', 0),
frappe.scrub(tax + ' Amount'): item_tax.get("tax_amount", 0), frappe.scrub(tax + ' Amount'): item_tax.get('tax_amount', 0),
}) })
total_tax += flt(item_tax.get("tax_amount")) total_tax += flt(item_tax.get('tax_amount'))
row.update({ row.update({
'total_tax': total_tax, 'total_tax': total_tax,
@@ -227,7 +234,7 @@ def get_columns(additional_table_columns, filters):
if filters.get('group_by') != 'Terriotory': if filters.get('group_by') != 'Terriotory':
columns.extend([ columns.extend([
{ {
'label': _("Territory"), 'label': _('Territory'),
'fieldname': 'territory', 'fieldname': 'territory',
'fieldtype': 'Link', 'fieldtype': 'Link',
'options': 'Territory', 'options': 'Territory',
@@ -382,13 +389,12 @@ def get_items(filters, additional_query_columns):
`tabSales Invoice`.posting_date, `tabSales Invoice`.debit_to, `tabSales Invoice`.posting_date, `tabSales Invoice`.debit_to,
`tabSales Invoice`.project, `tabSales Invoice`.customer, `tabSales Invoice`.remarks, `tabSales Invoice`.project, `tabSales Invoice`.customer, `tabSales Invoice`.remarks,
`tabSales Invoice`.territory, `tabSales Invoice`.company, `tabSales Invoice`.base_net_total, `tabSales Invoice`.territory, `tabSales Invoice`.company, `tabSales Invoice`.base_net_total,
`tabSales Invoice Item`.item_code, `tabSales Invoice Item`.item_name, `tabSales Invoice Item`.item_code, `tabSales Invoice Item`.description,
`tabSales Invoice Item`.item_group, `tabSales Invoice Item`.description, `tabSales Invoice Item`.sales_order, `tabSales Invoice Item`.sales_order, `tabSales Invoice Item`.delivery_note,
`tabSales Invoice Item`.delivery_note, `tabSales Invoice Item`.income_account, `tabSales Invoice Item`.income_account, `tabSales Invoice Item`.cost_center,
`tabSales Invoice Item`.cost_center, `tabSales Invoice Item`.stock_qty, `tabSales Invoice Item`.stock_qty, `tabSales Invoice Item`.stock_uom,
`tabSales Invoice Item`.stock_uom, `tabSales Invoice Item`.base_net_rate, `tabSales Invoice Item`.base_net_rate, `tabSales Invoice Item`.base_net_amount,
`tabSales Invoice Item`.base_net_amount, `tabSales Invoice`.customer_name, `tabSales Invoice`.customer_name, `tabSales Invoice`.customer_group, `tabSales Invoice Item`.so_detail,
`tabSales Invoice`.customer_group, `tabSales Invoice Item`.so_detail,
`tabSales Invoice`.update_stock, `tabSales Invoice Item`.uom, `tabSales Invoice Item`.qty {0} `tabSales Invoice`.update_stock, `tabSales Invoice Item`.uom, `tabSales Invoice Item`.qty {0}
from `tabSales Invoice`, `tabSales Invoice Item` from `tabSales Invoice`, `tabSales Invoice Item`
where `tabSales Invoice`.name = `tabSales Invoice Item`.parent where `tabSales Invoice`.name = `tabSales Invoice Item`.parent
@@ -425,14 +431,14 @@ def get_deducted_taxes():
return frappe.db.sql_list("select name from `tabPurchase Taxes and Charges` where add_deduct_tax = 'Deduct'") return frappe.db.sql_list("select name from `tabPurchase Taxes and Charges` where add_deduct_tax = 'Deduct'")
def get_tax_accounts(item_list, columns, company_currency, def get_tax_accounts(item_list, columns, company_currency,
doctype="Sales Invoice", tax_doctype="Sales Taxes and Charges"): doctype='Sales Invoice', tax_doctype='Sales Taxes and Charges'):
import json import json
item_row_map = {} item_row_map = {}
tax_columns = [] tax_columns = []
invoice_item_row = {} invoice_item_row = {}
itemised_tax = {} itemised_tax = {}
tax_amount_precision = get_field_precision(frappe.get_meta(tax_doctype).get_field("tax_amount"), tax_amount_precision = get_field_precision(frappe.get_meta(tax_doctype).get_field('tax_amount'),
currency=company_currency) or 2 currency=company_currency) or 2
for d in item_list: for d in item_list:
@@ -477,8 +483,8 @@ def get_tax_accounts(item_list, columns, company_currency,
tax_rate = tax_data tax_rate = tax_data
tax_amount = 0 tax_amount = 0
if charge_type == "Actual" and not tax_rate: if charge_type == 'Actual' and not tax_rate:
tax_rate = "NA" tax_rate = 'NA'
item_net_amount = sum([flt(d.base_net_amount) item_net_amount = sum([flt(d.base_net_amount)
for d in item_row_map.get(parent, {}).get(item_code, [])]) for d in item_row_map.get(parent, {}).get(item_code, [])])
@@ -492,17 +498,17 @@ def get_tax_accounts(item_list, columns, company_currency,
if (doctype == 'Purchase Invoice' and name in deducted_tax) else tax_value) if (doctype == 'Purchase Invoice' and name in deducted_tax) else tax_value)
itemised_tax.setdefault(d.name, {})[description] = frappe._dict({ itemised_tax.setdefault(d.name, {})[description] = frappe._dict({
"tax_rate": tax_rate, 'tax_rate': tax_rate,
"tax_amount": tax_value 'tax_amount': tax_value
}) })
except ValueError: except ValueError:
continue continue
elif charge_type == "Actual" and tax_amount: elif charge_type == 'Actual' and tax_amount:
for d in invoice_item_row.get(parent, []): for d in invoice_item_row.get(parent, []):
itemised_tax.setdefault(d.name, {})[description] = frappe._dict({ itemised_tax.setdefault(d.name, {})[description] = frappe._dict({
"tax_rate": "NA", 'tax_rate': 'NA',
"tax_amount": flt((tax_amount * d.base_net_amount) / d.base_net_total, 'tax_amount': flt((tax_amount * d.base_net_amount) / d.base_net_total,
tax_amount_precision) tax_amount_precision)
}) })
@@ -564,7 +570,7 @@ def add_total_row(data, filters, prev_group_by_value, item, total_row_map,
}) })
total_row_map.setdefault('total_row', { total_row_map.setdefault('total_row', {
subtotal_display_field: "Total", subtotal_display_field: 'Total',
'stock_qty': 0.0, 'stock_qty': 0.0,
'amount': 0.0, 'amount': 0.0,
'bold': 1, 'bold': 1,

View File

@@ -59,23 +59,111 @@ def validate_filters(filters):
def get_columns(filters): def get_columns(filters):
return [ return [
_("Payment Document") + ":: 100", {
_("Payment Entry") + ":Dynamic Link/"+_("Payment Document")+":140", "fieldname": "payment_document",
_("Party Type") + "::100", "label": _("Payment Document Type"),
_("Party") + ":Dynamic Link/Party Type:140", "fieldtype": "Data",
_("Posting Date") + ":Date:100", "width": 100
_("Invoice") + (":Link/Purchase Invoice:130" if filters.get("payment_type") == _("Outgoing") else ":Link/Sales Invoice:130"), },
_("Invoice Posting Date") + ":Date:130", {
_("Payment Due Date") + ":Date:130", "fieldname": "payment_entry",
_("Debit") + ":Currency:120", "label": _("Payment Document"),
_("Credit") + ":Currency:120", "fieldtype": "Dynamic Link",
_("Remarks") + "::150", "options": "payment_document",
_("Age") +":Int:40", "width": 160
"0-30:Currency:100", },
"30-60:Currency:100", {
"60-90:Currency:100", "fieldname": "party_type",
_("90-Above") + ":Currency:100", "label": _("Party Type"),
_("Delay in payment (Days)") + "::150" "fieldtype": "Data",
"width": 100
},
{
"fieldname": "party",
"label": _("Party"),
"fieldtype": "Dynamic Link",
"options": "party_type",
"width": 160
},
{
"fieldname": "posting_date",
"label": _("Posting Date"),
"fieldtype": "Date",
"width": 100
},
{
"fieldname": "invoice",
"label": _("Invoice"),
"fieldtype": "Link",
"options": "Purchase Invoice" if filters.get("payment_type") == _("Outgoing") else "Sales Invoice",
"width": 160
},
{
"fieldname": "invoice_posting_date",
"label": _("Invoice Posting Date"),
"fieldtype": "Date",
"width": 100
},
{
"fieldname": "due_date",
"label": _("Payment Due Date"),
"fieldtype": "Date",
"width": 100
},
{
"fieldname": "debit",
"label": _("Debit"),
"fieldtype": "Currency",
"width": 140
},
{
"fieldname": "credit",
"label": _("Credit"),
"fieldtype": "Currency",
"width": 140
},
{
"fieldname": "remarks",
"label": _("Remarks"),
"fieldtype": "Data",
"width": 200
},
{
"fieldname": "age",
"label": _("Age"),
"fieldtype": "Int",
"width": 50
},
{
"fieldname": "range1",
"label": _("0-30"),
"fieldtype": "Currency",
"width": 140
},
{
"fieldname": "range2",
"label": _("30-60"),
"fieldtype": "Currency",
"width": 140
},
{
"fieldname": "range3",
"label": _("60-90"),
"fieldtype": "Currency",
"width": 140
},
{
"fieldname": "range4",
"label": _("90 Above"),
"fieldtype": "Currency",
"width": 140
},
{
"fieldname": "delay_in_payment",
"label": _("Delay in payment (Days)"),
"fieldtype": "Int",
"width": 100
}
] ]
def get_conditions(filters): def get_conditions(filters):

View File

@@ -75,7 +75,10 @@ def get_fiscal_years(transaction_date=None, fiscal_year=None, label="Date", verb
else: else:
return ((fy.name, fy.year_start_date, fy.year_end_date),) return ((fy.name, fy.year_start_date, fy.year_end_date),)
error_msg = _("""{0} {1} not in any active Fiscal Year.""").format(label, formatdate(transaction_date)) error_msg = _("""{0} {1} is not in any active Fiscal Year""").format(label, formatdate(transaction_date))
if company:
error_msg = _("""{0} for {1}""").format(error_msg, frappe.bold(company))
if verbose==1: frappe.msgprint(error_msg) if verbose==1: frappe.msgprint(error_msg)
raise FiscalYearError(error_msg) raise FiscalYearError(error_msg)

View File

@@ -136,6 +136,8 @@ frappe.ui.form.on('Asset', {
if (frm.doc.docstatus == 0) { if (frm.doc.docstatus == 0) {
frm.toggle_reqd("finance_books", frm.doc.calculate_depreciation); frm.toggle_reqd("finance_books", frm.doc.calculate_depreciation);
frm.set_df_property('depreciation_start_date', 'reqd', 1, frm.doc.name, 'finance_books');
frm.refresh_field('finance_books');
} }
}, },

View File

@@ -50,6 +50,5 @@ frappe.ui.form.on('Asset Category', {
} }
}; };
}); });
} }
}); });

View File

@@ -1,5 +1,4 @@
{ {
"actions": [],
"creation": "2018-05-08 14:44:37.095570", "creation": "2018-05-08 14:44:37.095570",
"doctype": "DocType", "doctype": "DocType",
"editable_grid": 1, "editable_grid": 1,
@@ -54,9 +53,7 @@
"fieldname": "depreciation_start_date", "fieldname": "depreciation_start_date",
"fieldtype": "Date", "fieldtype": "Date",
"in_list_view": 1, "in_list_view": 1,
"label": "Depreciation Posting Date", "label": "Depreciation Posting Date"
"mandatory_depends_on": "eval:parent.doctype == 'Asset'",
"reqd": 1
}, },
{ {
"default": "0", "default": "0",
@@ -84,10 +81,8 @@
"label": "Rate of Depreciation" "label": "Rate of Depreciation"
} }
], ],
"index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "modified": "2020-12-30 15:43:03.188256",
"modified": "2020-10-30 15:22:29.119868",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Assets", "module": "Assets",
"name": "Asset Finance Book", "name": "Asset Finance Book",

View File

@@ -12,8 +12,8 @@ from frappe.model.document import Document
class AssetValueAdjustment(Document): class AssetValueAdjustment(Document):
def validate(self): def validate(self):
self.validate_date() self.validate_date()
self.set_difference_amount()
self.set_current_asset_value() self.set_current_asset_value()
self.set_difference_amount()
def on_submit(self): def on_submit(self):
self.make_depreciation_entry() self.make_depreciation_entry()

View File

@@ -148,24 +148,23 @@ def get_data(filters):
for asset in assets_record: for asset in assets_record:
asset_value = asset.gross_purchase_amount - flt(asset.opening_accumulated_depreciation) \ asset_value = asset.gross_purchase_amount - flt(asset.opening_accumulated_depreciation) \
- flt(depreciation_amount_map.get(asset.name)) - flt(depreciation_amount_map.get(asset.name))
if asset_value: row = {
row = { "asset_id": asset.asset_id,
"asset_id": asset.name, "asset_name": asset.asset_name,
"asset_name": asset.asset_name, "status": asset.status,
"status": asset.status, "department": asset.department,
"department": asset.department, "cost_center": asset.cost_center,
"cost_center": asset.cost_center, "vendor_name": pr_supplier_map.get(asset.purchase_receipt) or pi_supplier_map.get(asset.purchase_invoice),
"vendor_name": pr_supplier_map.get(asset.purchase_receipt) or pi_supplier_map.get(asset.purchase_invoice), "gross_purchase_amount": asset.gross_purchase_amount,
"gross_purchase_amount": asset.gross_purchase_amount, "opening_accumulated_depreciation": asset.opening_accumulated_depreciation,
"opening_accumulated_depreciation": asset.opening_accumulated_depreciation, "depreciated_amount": depreciation_amount_map.get(asset.asset_id) or 0.0,
"depreciated_amount": depreciation_amount_map.get(asset.name) or 0.0, "available_for_use_date": asset.available_for_use_date,
"available_for_use_date": asset.available_for_use_date, "location": asset.location,
"location": asset.location, "asset_category": asset.asset_category,
"asset_category": asset.asset_category, "purchase_date": asset.purchase_date,
"purchase_date": asset.purchase_date, "asset_value": asset_value
"asset_value": asset_value }
} data.append(row)
data.append(row)
return data return data

View File

@@ -35,9 +35,7 @@ def update_last_purchase_rate(doc, is_submit):
frappe.throw(_("UOM Conversion factor is required in row {0}").format(d.idx)) frappe.throw(_("UOM Conversion factor is required in row {0}").format(d.idx))
# update last purchsae rate # update last purchsae rate
if last_purchase_rate: frappe.db.set_value('Item', d.item_code, 'last_purchase_rate', flt(last_purchase_rate))
frappe.db.sql("""update `tabItem` set last_purchase_rate = %s where name = %s""",
(flt(last_purchase_rate), d.item_code))
def validate_for_items(doc): def validate_for_items(doc):
items = [] items = []

View File

@@ -23,6 +23,8 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import g
from erpnext.stock.get_item_details import get_item_warehouse, _get_item_tax_template, get_item_tax_map from erpnext.stock.get_item_details import get_item_warehouse, _get_item_tax_template, get_item_tax_map
from erpnext.stock.doctype.packed_item.packed_item import make_packing_list from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
class AccountMissingError(frappe.ValidationError): pass
force_item_fields = ("item_group", "brand", "stock_uom", "is_fixed_asset", "item_tax_rate", "pricing_rules") force_item_fields = ("item_group", "brand", "stock_uom", "is_fixed_asset", "item_tax_rate", "pricing_rules")
class AccountsController(TransactionBase): class AccountsController(TransactionBase):
@@ -106,8 +108,14 @@ class AccountsController(TransactionBase):
self.validate_deferred_start_and_end_date() self.validate_deferred_start_and_end_date()
validate_regional(self) validate_regional(self)
validate_einvoice_fields(self)
if self.doctype != 'Material Request': if self.doctype != 'Material Request':
apply_pricing_rule_on_transaction(self) apply_pricing_rule_on_transaction(self)
def before_cancel(self):
validate_einvoice_fields(self)
def validate_deferred_start_and_end_date(self): def validate_deferred_start_and_end_date(self):
for d in self.items: for d in self.items:
@@ -711,6 +719,21 @@ class AccountsController(TransactionBase):
return self._abbr return self._abbr
def raise_missing_debit_credit_account_error(self, party_type, party):
"""Raise an error if debit to/credit to account does not exist."""
db_or_cr = frappe.bold("Debit To") if self.doctype == "Sales Invoice" else frappe.bold("Credit To")
rec_or_pay = "Receivable" if self.doctype == "Sales Invoice" else "Payable"
link_to_party = frappe.utils.get_link_to_form(party_type, party)
link_to_company = frappe.utils.get_link_to_form("Company", self.company)
message = _("{0} Account not found against Customer {1}.").format(db_or_cr, frappe.bold(party) or '')
message += "<br>" + _("Please set one of the following:") + "<br>"
message += "<br><ul><li>" + _("'Account' in the Accounting section of Customer {0}").format(link_to_party) + "</li>"
message += "<li>" + _("'Default {0} Account' in Company {1}").format(rec_or_pay, link_to_company) + "</li></ul>"
frappe.throw(message, title=_("Account Missing"), exc=AccountMissingError)
def validate_party(self): def validate_party(self):
party_type, party = self.get_party() party_type, party = self.get_party()
validate_party_frozen_disabled(party_type, party) validate_party_frozen_disabled(party_type, party)
@@ -1406,3 +1429,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
@erpnext.allow_regional @erpnext.allow_regional
def validate_regional(doc): def validate_regional(doc):
pass pass
@erpnext.allow_regional
def validate_einvoice_fields(doc):
pass

View File

@@ -296,7 +296,7 @@ class BuyingController(StockController):
raw_material_data = backflushed_raw_materials_map.get(rm_item_key, {}) raw_material_data = backflushed_raw_materials_map.get(rm_item_key, {})
consumed_qty = raw_material_data.get('qty', 0) consumed_qty = raw_material_data.get('qty', 0)
consumed_serial_nos = raw_material_data.get('serial_nos', '') consumed_serial_nos = raw_material_data.get('serial_no', '')
consumed_batch_nos = raw_material_data.get('batch_nos', '') consumed_batch_nos = raw_material_data.get('batch_nos', '')
transferred_qty = raw_material.qty transferred_qty = raw_material.qty

View File

@@ -279,6 +279,7 @@ def make_return_doc(doctype, source_name, target_doc=None):
target_doc.po_detail = source_doc.po_detail target_doc.po_detail = source_doc.po_detail
target_doc.pr_detail = source_doc.pr_detail target_doc.pr_detail = source_doc.pr_detail
target_doc.purchase_invoice_item = source_doc.name target_doc.purchase_invoice_item = source_doc.name
target_doc.price_list_rate = 0
elif doctype == "Delivery Note": elif doctype == "Delivery Note":
target_doc.against_sales_order = source_doc.against_sales_order target_doc.against_sales_order = source_doc.against_sales_order
@@ -297,6 +298,7 @@ def make_return_doc(doctype, source_name, target_doc=None):
target_doc.dn_detail = source_doc.dn_detail target_doc.dn_detail = source_doc.dn_detail
target_doc.expense_account = source_doc.expense_account target_doc.expense_account = source_doc.expense_account
target_doc.sales_invoice_item = source_doc.name target_doc.sales_invoice_item = source_doc.name
target_doc.price_list_rate = 0
if default_warehouse_for_sales_return: if default_warehouse_for_sales_return:
target_doc.warehouse = default_warehouse_for_sales_return target_doc.warehouse = default_warehouse_for_sales_return

View File

@@ -244,6 +244,15 @@ def update_taxes_with_shipping_lines(taxes, shipping_lines, shopify_settings):
"""Shipping lines represents the shipping details, """Shipping lines represents the shipping details,
each such shipping detail consists of a list of tax_lines""" each such shipping detail consists of a list of tax_lines"""
for shipping_charge in shipping_lines: for shipping_charge in shipping_lines:
if shipping_charge.get("price"):
taxes.append({
"charge_type": _("Actual"),
"account_head": get_tax_account_head(shipping_charge),
"description": shipping_charge["title"],
"tax_amount": shipping_charge["price"],
"cost_center": shopify_settings.cost_center
})
for tax in shipping_charge.get("tax_lines"): for tax in shipping_charge.get("tax_lines"):
taxes.append({ taxes.append({
"charge_type": _("Actual"), "charge_type": _("Actual"),

View File

@@ -239,10 +239,14 @@ doc_events = {
"Website Settings": { "Website Settings": {
"validate": "erpnext.portal.doctype.products_settings.products_settings.home_page_is_products" "validate": "erpnext.portal.doctype.products_settings.products_settings.home_page_is_products"
}, },
"Tax Category": {
"validate": "erpnext.regional.india.utils.validate_tax_category"
},
"Sales Invoice": { "Sales Invoice": {
"on_submit": ["erpnext.regional.create_transaction_log", "erpnext.regional.italy.utils.sales_invoice_on_submit"], "on_submit": ["erpnext.regional.create_transaction_log", "erpnext.regional.italy.utils.sales_invoice_on_submit"],
"on_cancel": "erpnext.regional.italy.utils.sales_invoice_on_cancel", "on_cancel": "erpnext.regional.italy.utils.sales_invoice_on_cancel",
"on_trash": "erpnext.regional.check_deletion_permission" "on_trash": "erpnext.regional.check_deletion_permission",
"validate": "erpnext.regional.india.utils.set_transporter_address"
}, },
"Purchase Invoice": { "Purchase Invoice": {
"validate": "erpnext.regional.india.utils.update_grand_total_for_rcm" "validate": "erpnext.regional.india.utils.update_grand_total_for_rcm"
@@ -357,7 +361,8 @@ regional_overrides = {
'erpnext.accounts.party.get_regional_address_details': 'erpnext.regional.india.utils.get_regional_address_details', 'erpnext.accounts.party.get_regional_address_details': 'erpnext.regional.india.utils.get_regional_address_details',
'erpnext.hr.utils.calculate_annual_eligible_hra_exemption': 'erpnext.regional.india.utils.calculate_annual_eligible_hra_exemption', 'erpnext.hr.utils.calculate_annual_eligible_hra_exemption': 'erpnext.regional.india.utils.calculate_annual_eligible_hra_exemption',
'erpnext.hr.utils.calculate_hra_exemption_for_period': 'erpnext.regional.india.utils.calculate_hra_exemption_for_period', 'erpnext.hr.utils.calculate_hra_exemption_for_period': 'erpnext.regional.india.utils.calculate_hra_exemption_for_period',
'erpnext.accounts.doctype.purchase_invoice.purchase_invoice.make_regional_gl_entries': 'erpnext.regional.india.utils.make_regional_gl_entries' 'erpnext.accounts.doctype.purchase_invoice.purchase_invoice.make_regional_gl_entries': 'erpnext.regional.india.utils.make_regional_gl_entries',
'erpnext.controllers.accounts_controller.validate_einvoice_fields': 'erpnext.regional.india.e_invoice.utils.validate_einvoice_fields'
}, },
'United Arab Emirates': { 'United Arab Emirates': {
'erpnext.controllers.taxes_and_totals.update_itemised_tax_data': 'erpnext.regional.united_arab_emirates.utils.update_itemised_tax_data' 'erpnext.controllers.taxes_and_totals.update_itemised_tax_data': 'erpnext.regional.united_arab_emirates.utils.update_itemised_tax_data'

View File

@@ -782,7 +782,7 @@
"icon": "fa fa-user", "icon": "fa fa-user",
"idx": 24, "idx": 24,
"image_field": "image", "image_field": "image",
"modified": "2020-01-09 04:23:55.611366", "modified": "2020-01-09 05:23:55.611366",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "Employee", "name": "Employee",
@@ -824,7 +824,6 @@
"write": 1 "write": 1
} }
], ],
"quick_entry": 1,
"search_fields": "employee_name", "search_fields": "employee_name",
"show_name_in_global_search": 1, "show_name_in_global_search": 1,
"sort_field": "modified", "sort_field": "modified",

View File

@@ -5,7 +5,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils import date_diff, getdate, formatdate, cint, month_diff, flt from frappe.utils import date_diff, getdate, formatdate, cint, month_diff, flt, add_months
from frappe.model.document import Document from frappe.model.document import Document
from erpnext.hr.utils import get_holidays_for_employee from erpnext.hr.utils import get_holidays_for_employee
@@ -88,6 +88,8 @@ def get_period_factor(employee, start_date, end_date, payroll_frequency, payroll
period_start = joining_date period_start = joining_date
if relieving_date and getdate(relieving_date) < getdate(period_end): if relieving_date and getdate(relieving_date) < getdate(period_end):
period_end = relieving_date period_end = relieving_date
if month_diff(period_end, start_date) > 1:
start_date = add_months(start_date, - (month_diff(period_end, start_date)+1))
total_sub_periods, remaining_sub_periods = 0.0, 0.0 total_sub_periods, remaining_sub_periods = 0.0, 0.0

View File

@@ -299,14 +299,17 @@ class SalarySlip(TransactionBase):
def calculate_net_pay(self): def calculate_net_pay(self):
if self.salary_structure: if self.salary_structure:
self.calculate_component_amounts("earnings") self.calculate_component_amounts("earnings")
self.gross_pay = self.get_component_totals("earnings") self.gross_pay = self.get_component_totals("earnings", depends_on_payment_days=1)
if self.salary_structure: if self.salary_structure:
self.calculate_component_amounts("deductions") self.calculate_component_amounts("deductions")
self.total_deduction = self.get_component_totals("deductions")
self.set_loan_repayment() self.set_loan_repayment()
self.set_component_amounts_based_on_payment_days()
self.set_net_pay()
def set_net_pay(self):
self.total_deduction = self.get_component_totals("deductions")
self.net_pay = flt(self.gross_pay) - (flt(self.total_deduction) + flt(self.total_loan_repayment)) self.net_pay = flt(self.gross_pay) - (flt(self.total_deduction) + flt(self.total_loan_repayment))
self.rounded_total = rounded(self.net_pay) self.rounded_total = rounded(self.net_pay)
@@ -323,8 +326,6 @@ class SalarySlip(TransactionBase):
else: else:
self.add_tax_components(payroll_period) self.add_tax_components(payroll_period)
self.set_component_amounts_based_on_payment_days(component_type)
def add_structure_components(self, component_type): def add_structure_components(self, component_type):
data = self.get_data_for_eval() data = self.get_data_for_eval()
for struct_row in self._salary_structure_doc.get(component_type): for struct_row in self._salary_structure_doc.get(component_type):
@@ -679,7 +680,7 @@ class SalarySlip(TransactionBase):
cint(row.depends_on_payment_days) and cint(self.total_working_days) and cint(row.depends_on_payment_days) and cint(self.total_working_days) and
(not self.salary_slip_based_on_timesheet or (not self.salary_slip_based_on_timesheet or
getdate(self.start_date) < joining_date or getdate(self.start_date) < joining_date or
getdate(self.end_date) > relieving_date (relieving_date and getdate(self.end_date) > relieving_date)
)): )):
additional_amount = flt((flt(row.additional_amount) * flt(self.payment_days) additional_amount = flt((flt(row.additional_amount) * flt(self.payment_days)
/ cint(self.total_working_days)), row.precision("additional_amount")) / cint(self.total_working_days)), row.precision("additional_amount"))
@@ -812,15 +813,21 @@ class SalarySlip(TransactionBase):
struct_row['variable_based_on_taxable_salary'] = component.variable_based_on_taxable_salary struct_row['variable_based_on_taxable_salary'] = component.variable_based_on_taxable_salary
return struct_row return struct_row
def get_component_totals(self, component_type): def get_component_totals(self, component_type, depends_on_payment_days=0):
joining_date, relieving_date = frappe.get_cached_value("Employee", self.employee,
["date_of_joining", "relieving_date"])
total = 0.0 total = 0.0
for d in self.get(component_type): for d in self.get(component_type):
if not d.do_not_include_in_total: if not d.do_not_include_in_total:
d.amount = flt(d.amount, d.precision("amount")) if depends_on_payment_days:
total += d.amount amount = self.get_amount_based_on_payment_days(d, joining_date, relieving_date)[0]
else:
amount = flt(d.amount, d.precision("amount"))
total += amount
return total return total
def set_component_amounts_based_on_payment_days(self, component_type): def set_component_amounts_based_on_payment_days(self):
joining_date, relieving_date = frappe.get_cached_value("Employee", self.employee, joining_date, relieving_date = frappe.get_cached_value("Employee", self.employee,
["date_of_joining", "relieving_date"]) ["date_of_joining", "relieving_date"])
@@ -830,8 +837,9 @@ class SalarySlip(TransactionBase):
if not joining_date: if not joining_date:
frappe.throw(_("Please set the Date Of Joining for employee {0}").format(frappe.bold(self.employee_name))) frappe.throw(_("Please set the Date Of Joining for employee {0}").format(frappe.bold(self.employee_name)))
for d in self.get(component_type): for component_type in ("earnings", "deductions"):
d.amount = self.get_amount_based_on_payment_days(d, joining_date, relieving_date)[0] for d in self.get(component_type):
d.amount = flt(self.get_amount_based_on_payment_days(d, joining_date, relieving_date)[0], d.precision("amount"))
def set_loan_repayment(self): def set_loan_repayment(self):
self.set('loans', []) self.set('loans', [])

View File

@@ -253,7 +253,7 @@ cur_frm.cscript.hour_rate = function(doc) {
cur_frm.cscript.time_in_mins = cur_frm.cscript.hour_rate; cur_frm.cscript.time_in_mins = cur_frm.cscript.hour_rate;
cur_frm.cscript.bom_no = function(doc, cdt, cdn) { cur_frm.cscript.bom_no = function(doc, cdt, cdn) {
get_bom_material_detail(doc, cdt, cdn, false); get_bom_material_detail(doc, cdt, cdn, false);
}; };
@@ -261,17 +261,22 @@ cur_frm.cscript.is_default = function(doc) {
if (doc.is_default) cur_frm.set_value("is_active", 1); if (doc.is_default) cur_frm.set_value("is_active", 1);
}; };
var get_bom_material_detail= function(doc, cdt, cdn, scrap_items) { var get_bom_material_detail = function(doc, cdt, cdn, scrap_items) {
if (!doc.company) {
frappe.throw({message: __("Please select a Company first."), title: __("Mandatory")});
}
var d = locals[cdt][cdn]; var d = locals[cdt][cdn];
if (d.item_code) { if (d.item_code) {
return frappe.call({ return frappe.call({
doc: doc, doc: doc,
method: "get_bom_material_detail", method: "get_bom_material_detail",
args: { args: {
'item_code': d.item_code, "company": doc.company,
'bom_no': d.bom_no != null ? d.bom_no: '', "item_code": d.item_code,
"bom_no": d.bom_no != null ? d.bom_no: '',
"scrap_items": scrap_items, "scrap_items": scrap_items,
'qty': d.qty, "qty": d.qty,
"stock_qty": d.stock_qty, "stock_qty": d.stock_qty,
"include_item_in_manufacturing": d.include_item_in_manufacturing, "include_item_in_manufacturing": d.include_item_in_manufacturing,
"uom": d.uom, "uom": d.uom,
@@ -309,7 +314,7 @@ cur_frm.cscript.rate = function(doc, cdt, cdn) {
} }
if (d.bom_no) { if (d.bom_no) {
frappe.msgprint(__("You can not change rate if BOM mentioned agianst any item")); frappe.msgprint(__("You cannot change the rate if BOM is mentioned against any Item."));
get_bom_material_detail(doc, cdt, cdn, scrap_items); get_bom_material_detail(doc, cdt, cdn, scrap_items);
} else { } else {
erpnext.bom.calculate_rm_cost(doc); erpnext.bom.calculate_rm_cost(doc);

View File

@@ -51,6 +51,10 @@ class BOM(WebsiteGenerator):
def validate(self): def validate(self):
self.route = frappe.scrub(self.name).replace('_', '-') self.route = frappe.scrub(self.name).replace('_', '-')
if not self.company:
frappe.throw(_("Please select a Company first."), title=_("Mandatory"))
self.clear_operations() self.clear_operations()
self.validate_main_item() self.validate_main_item()
self.validate_currency() self.validate_currency()
@@ -122,6 +126,7 @@ class BOM(WebsiteGenerator):
self.validate_bom_currecny(item) self.validate_bom_currecny(item)
ret = self.get_bom_material_detail({ ret = self.get_bom_material_detail({
"company": self.company,
"item_code": item.item_code, "item_code": item.item_code,
"item_name": item.item_name, "item_name": item.item_name,
"bom_no": item.bom_no, "bom_no": item.bom_no,
@@ -236,6 +241,7 @@ class BOM(WebsiteGenerator):
for d in self.get("items"): for d in self.get("items"):
rate = self.get_rm_rate({ rate = self.get_rm_rate({
"company": self.company,
"item_code": d.item_code, "item_code": d.item_code,
"bom_no": d.bom_no, "bom_no": d.bom_no,
"qty": d.qty, "qty": d.qty,
@@ -288,10 +294,20 @@ class BOM(WebsiteGenerator):
""" Get weighted average of valuation rate from all warehouses """ """ Get weighted average of valuation rate from all warehouses """
total_qty, total_value, valuation_rate = 0.0, 0.0, 0.0 total_qty, total_value, valuation_rate = 0.0, 0.0, 0.0
for d in frappe.db.sql("""select actual_qty, stock_value from `tabBin` item_bins = frappe.db.sql("""
where item_code=%s""", args['item_code'], as_dict=1): select
total_qty += flt(d.actual_qty) bin.actual_qty, bin.stock_value
total_value += flt(d.stock_value) from
`tabBin` bin, `tabWarehouse` warehouse
where
bin.item_code=%(item)s
and bin.warehouse = warehouse.name
and warehouse.company=%(company)s""",
{"item": args['item_code'], "company": args['company']}, as_dict=1)
for d in item_bins:
total_qty += flt(d.actual_qty)
total_value += flt(d.stock_value)
if total_qty: if total_qty:
valuation_rate = total_value / total_qty valuation_rate = total_value / total_qty

View File

@@ -506,6 +506,80 @@ class TestWorkOrder(unittest.TestCase):
work_order1.save() work_order1.save()
self.assertEqual(work_order1.operations[0].time_in_mins, 40.0) self.assertEqual(work_order1.operations[0].time_in_mins, 40.0)
def test_partial_material_consumption(self):
frappe.db.set_value("Manufacturing Settings", None, "material_consumption", 1)
wo_order = make_wo_order_test_record(planned_start_date=now(), qty=4)
ste_cancel_list = []
ste1 = test_stock_entry.make_stock_entry(item_code="_Test Item",
target="_Test Warehouse - _TC", qty=20, basic_rate=5000.0)
ste2 = test_stock_entry.make_stock_entry(item_code="_Test Item Home Desktop 100",
target="_Test Warehouse - _TC", qty=20, basic_rate=1000.0)
ste_cancel_list.extend([ste1, ste2])
s = frappe.get_doc(make_stock_entry(wo_order.name, "Material Transfer for Manufacture", 4))
s.submit()
ste_cancel_list.append(s)
ste1 = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 2))
ste1.submit()
ste_cancel_list.append(ste1)
ste3 = frappe.get_doc(make_stock_entry(wo_order.name, "Material Consumption for Manufacture", 2))
self.assertEquals(ste3.fg_completed_qty, 2)
expected_qty = {"_Test Item": 2, "_Test Item Home Desktop 100": 4}
for row in ste3.items:
self.assertEquals(row.qty, expected_qty.get(row.item_code))
for ste_doc in ste_cancel_list:
ste_doc.cancel()
frappe.db.set_value("Manufacturing Settings", None, "material_consumption", 0)
def test_extra_material_transfer(self):
frappe.db.set_value("Manufacturing Settings", None, "material_consumption", 0)
frappe.db.set_value("Manufacturing Settings", None, "backflush_raw_materials_based_on",
"Material Transferred for Manufacture")
wo_order = make_wo_order_test_record(planned_start_date=now(), qty=4)
ste_cancel_list = []
ste1 = test_stock_entry.make_stock_entry(item_code="_Test Item",
target="_Test Warehouse - _TC", qty=20, basic_rate=5000.0)
ste2 = test_stock_entry.make_stock_entry(item_code="_Test Item Home Desktop 100",
target="_Test Warehouse - _TC", qty=20, basic_rate=1000.0)
ste_cancel_list.extend([ste1, ste2])
itemwise_qty = {}
s = frappe.get_doc(make_stock_entry(wo_order.name, "Material Transfer for Manufacture", 4))
for row in s.items:
row.qty = row.qty + 2
itemwise_qty.setdefault(row.item_code, row.qty)
s.submit()
ste_cancel_list.append(s)
ste3 = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 2))
for ste_row in ste3.items:
if itemwise_qty.get(ste_row.item_code) and ste_row.s_warehouse:
self.assertEquals(ste_row.qty, itemwise_qty.get(ste_row.item_code) / 2)
ste3.submit()
ste_cancel_list.append(ste3)
ste2 = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 2))
for ste_row in ste2.items:
if itemwise_qty.get(ste_row.item_code) and ste_row.s_warehouse:
self.assertEquals(ste_row.qty, itemwise_qty.get(ste_row.item_code) / 2)
for ste_doc in ste_cancel_list:
ste_doc.cancel()
frappe.db.set_value("Manufacturing Settings", None, "backflush_raw_materials_based_on", "BOM")
def get_scrap_item_details(bom_no): def get_scrap_item_details(bom_no):
scrap_items = {} scrap_items = {}
for item in frappe.db.sql("""select item_code, stock_qty from `tabBOM Scrap Item` for item in frappe.db.sql("""select item_code, stock_qty from `tabBOM Scrap Item`

View File

@@ -521,7 +521,8 @@ erpnext.work_order = {
var tbl = frm.doc.required_items || []; var tbl = frm.doc.required_items || [];
var tbl_lenght = tbl.length; var tbl_lenght = tbl.length;
for (var i = 0, len = tbl_lenght; i < len; i++) { for (var i = 0, len = tbl_lenght; i < len; i++) {
if (flt(frm.doc.required_items[i].required_qty) > flt(frm.doc.required_items[i].consumed_qty)) { let wo_item_qty = frm.doc.required_items[i].transferred_qty || frm.doc.required_items[i].required_qty;
if (flt(wo_item_qty) > flt(frm.doc.required_items[i].consumed_qty)) {
counter += 1; counter += 1;
} }
} }

View File

@@ -529,7 +529,7 @@ class WorkOrder(Document):
and (entry.purpose = "Material Consumption for Manufacture" and (entry.purpose = "Material Consumption for Manufacture"
or entry.purpose = "Manufacture") or entry.purpose = "Manufacture")
and entry.docstatus = 1 and entry.docstatus = 1
and detail.parent = entry.name and detail.parent = entry.name and IFNULL(t_warehouse, "") = ""
and (detail.item_code = %(item)s or detail.original_item = %(item)s)''', { and (detail.item_code = %(item)s or detail.original_item = %(item)s)''', {
'name': self.name, 'name': self.name,
'item': d.item_code 'item': d.item_code

View File

@@ -20,6 +20,7 @@ def get_columns():
_("Item") + ":Link/Item:150", _("Item") + ":Link/Item:150",
_("Description") + "::300", _("Description") + "::300",
_("BOM Qty") + ":Float:160", _("BOM Qty") + ":Float:160",
_("BOM UoM") + "::160",
_("Required Qty") + ":Float:120", _("Required Qty") + ":Float:120",
_("In Stock Qty") + ":Float:120", _("In Stock Qty") + ":Float:120",
_("Enough Parts to Build") + ":Float:200", _("Enough Parts to Build") + ":Float:200",
@@ -32,7 +33,7 @@ def get_bom_stock(filters):
bom = filters.get("bom") bom = filters.get("bom")
table = "`tabBOM Item`" table = "`tabBOM Item`"
qty_field = "qty" qty_field = "stock_qty"
qty_to_produce = filters.get("qty_to_produce", 1) qty_to_produce = filters.get("qty_to_produce", 1)
if int(qty_to_produce) <= 0: if int(qty_to_produce) <= 0:
@@ -40,7 +41,6 @@ def get_bom_stock(filters):
if filters.get("show_exploded_view"): if filters.get("show_exploded_view"):
table = "`tabBOM Explosion Item`" table = "`tabBOM Explosion Item`"
qty_field = "stock_qty"
if filters.get("warehouse"): if filters.get("warehouse"):
warehouse_details = frappe.db.get_value("Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1) warehouse_details = frappe.db.get_value("Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1)
@@ -59,6 +59,7 @@ def get_bom_stock(filters):
bom_item.item_code, bom_item.item_code,
bom_item.description , bom_item.description ,
bom_item.{qty_field}, bom_item.{qty_field},
bom_item.stock_uom,
bom_item.{qty_field} * {qty_to_produce} / bom.quantity, bom_item.{qty_field} * {qty_to_produce} / bom.quantity,
sum(ledger.actual_qty) as actual_qty, sum(ledger.actual_qty) as actual_qty,
sum(FLOOR(ledger.actual_qty / (bom_item.{qty_field} * {qty_to_produce} / bom.quantity))) sum(FLOOR(ledger.actual_qty / (bom_item.{qty_field} * {qty_to_produce} / bom.quantity)))

View File

@@ -677,4 +677,6 @@ erpnext.patches.v12_0.set_multi_uom_in_rfq
erpnext.patches.v12_0.update_state_code_for_daman_and_diu erpnext.patches.v12_0.update_state_code_for_daman_and_diu
erpnext.patches.v12_0.rename_lost_reason_detail erpnext.patches.v12_0.rename_lost_reason_detail
erpnext.patches.v12_0.update_leave_application_status erpnext.patches.v12_0.update_leave_application_status
erpnext.patches.v12_0.update_payment_entry_status erpnext.patches.v12_0.update_payment_entry_status
erpnext.patches.v12_0.add_transporter_address_field #2020-10-27
erpnext.patches.v12_0.setup_einvoice_fields #2020-12-02

View File

@@ -0,0 +1,150 @@
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
fields = [
{
'fieldname': 'transporter_info',
'label': 'Transporter Info',
'fieldtype': 'Section Break',
'insert_after': 'terms',
'collapsible': 1,
'collapsible_depends_on': 'transporter',
'print_hide': 1
},
{
'fieldname': 'transporter',
'label': 'Transporter',
'fieldtype': 'Link',
'insert_after': 'transporter_info',
'options': 'Supplier',
'print_hide': 1
},
{
'fieldname': 'transporter_name',
'label': 'Transporter Name',
'fieldtype': 'Data',
'insert_after': 'transporter',
'fetch_from': 'transporter.name',
'read_only': 1,
'print_hide': 1,
'translatable': 0
},
{
'fieldname': 'gst_transporter_id',
'label': 'GST Transporter ID',
'fieldtype': 'Data',
'insert_after': 'transporter_name',
'fetch_from': 'transporter.gst_transporter_id',
'print_hide': 1,
'translatable': 0
},
{
'fieldname': 'driver',
'label': 'Driver',
'fieldtype': 'Link',
'insert_after': 'gst_transporter_id',
'options': 'Driver',
'print_hide': 1
},
{
'fieldname': 'lr_no',
'label': 'Transport Receipt No',
'fieldtype': 'Data',
'insert_after': 'driver',
'print_hide': 1,
'translatable': 0
},
{
'fieldname': 'vehicle_no',
'label': 'Vehicle No',
'fieldtype': 'Data',
'insert_after': 'lr_no',
'print_hide': 1,
'translatable': 0
},
{
'fieldname': 'distance',
'label': 'Distance (in km)',
'fieldtype': 'Float',
'insert_after': 'vehicle_no',
'print_hide': 1
},
{
'fieldname': 'transporter_col_break',
'fieldtype': 'Column Break',
'insert_after': 'distance'
},
{
'fieldname': 'transporter_address',
'label': 'Transporter Address Name',
'fieldtype': 'Link',
'insert_after': 'transporter_col_break',
'options': 'Address',
'print_hide': 1
},
{
'fieldname': 'transporter_address_display',
'label': 'Transporter Address Preview',
'fieldtype': 'Small Text',
'insert_after': 'transporter_address',
'read_only': 1,
'print_hide': 1,
'translatable': 0
},
{
'fieldname': 'mode_of_transport',
'label': 'Mode of Transport',
'fieldtype': 'Select',
'options': '\nRoad\nAir\nRail\nShip',
'default': 'Road',
'insert_after': 'transporter_address_display',
'print_hide': 1,
'translatable': 0
},
{
'fieldname': 'driver_name',
'label': 'Driver Name',
'fieldtype': 'Data',
'insert_after': 'mode_of_transport',
'fetch_from': 'driver.full_name',
'print_hide': 1,
'translatable': 0
},
{
'fieldname': 'lr_date',
'label': 'Transport Receipt Date',
'fieldtype': 'Date',
'insert_after': 'driver_name',
'default': 'Today',
'print_hide': 1
},
{
'fieldname': 'gst_vehicle_type',
'label': 'GST Vehicle Type',
'fieldtype': 'Select',
'options': 'Regular\nOver Dimensional Cargo (ODC)',
'depends_on': 'eval:(doc.mode_of_transport === "Road")',
'default': 'Regular',
'insert_after': 'lr_date',
'print_hide': 1,
'translatable': 0
},
{
'fieldname': 'ewaybill',
'label': 'e-Way Bill No.',
'fieldtype': 'Data',
'depends_on': 'eval:(doc.docstatus === 1)',
'allow_on_submit': 1,
'insert_after': 'tax_id',
'translatable': 0
}
]
create_custom_fields({ 'Sales Invoice': fields }, update=True)
frappe.reload_doctype('Sales Invoice')

View File

@@ -0,0 +1,48 @@
from __future__ import unicode_literals
import frappe
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
from erpnext.regional.india.setup import add_permissions, add_print_formats
def execute():
company = frappe.get_all('Company', filters = {'country': 'India'})
if not company:
return
frappe.reload_doc("regional", "doctype", "e_invoice_settings")
custom_fields = {
'Sales Invoice': [
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='signed_qr_code', fieldtype='Code', options='JSON', hidden=1, no_copy=1, print_hide=1, read_only=1),
dict(fieldname='qrcode_image', label='QRCode', fieldtype='Attach Image', hidden=1, no_copy=1, print_hide=1, read_only=1)
]
}
create_custom_fields(custom_fields, update=True)
add_permissions()
add_print_formats()
t = {
'mode_of_transport': [{'default': None}],
'ewaybill': [
{'depends_on': 'eval:((doc.docstatus === 1 || doc.ewaybill) && doc.eway_bill_cancelled === 0)'}
]
}
for field, conditions in t.items():
for c in conditions:
[(prop, value)] = c.items()
frappe.db.set_value('Custom Field', { 'fieldname': field }, prop, value)

View File

@@ -2,6 +2,7 @@ import frappe
import numpy as np import numpy as np
from frappe.utils import cint from frappe.utils import cint
from erpnext.portal.product_configurator.item_variants_cache import ItemVariantsCacheManager from erpnext.portal.product_configurator.item_variants_cache import ItemVariantsCacheManager
from erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings import get_shopping_cart_settings
def get_field_filter_data(): def get_field_filter_data():
product_settings = get_product_settings() product_settings = get_product_settings()
@@ -249,6 +250,8 @@ def get_next_attribute_and_values(item_code, selected_attributes):
optional_attributes = item_cache.get_optional_attributes() optional_attributes = item_cache.get_optional_attributes()
exact_match = [] exact_match = []
shopping_cart_settings = get_shopping_cart_settings()
allow_items_not_in_stock = cint(shopping_cart_settings.allow_items_not_in_stock)
# search for exact match if all selected attributes are required attributes # search for exact match if all selected attributes are required attributes
if len(selected_attributes.keys()) >= (len(attribute_list) - len(optional_attributes)): if len(selected_attributes.keys()) >= (len(attribute_list) - len(optional_attributes)):
item_attribute_value_map = item_cache.get_item_attribute_value_map() item_attribute_value_map = item_cache.get_item_attribute_value_map()
@@ -263,7 +266,6 @@ def get_next_attribute_and_values(item_code, selected_attributes):
if exact_match: if exact_match:
data = get_product_info_for_website(exact_match[0]) data = get_product_info_for_website(exact_match[0])
product_info = data.product_info product_info = data.product_info
product_info["allow_items_not_in_stock"] = cint(data.cart_settings.allow_items_not_in_stock)
if not data.cart_settings.show_price: if not data.cart_settings.show_price:
product_info = None product_info = None
else: else:
@@ -275,6 +277,7 @@ def get_next_attribute_and_values(item_code, selected_attributes):
'filtered_items_count': filtered_items_count, 'filtered_items_count': filtered_items_count,
'filtered_items': filtered_items if filtered_items_count < 10 else [], 'filtered_items': filtered_items if filtered_items_count < 10 else [],
'exact_match': exact_match, 'exact_match': exact_match,
'allow_items_not_in_stock': allow_items_not_in_stock,
'product_info': product_info 'product_info': product_info
} }

View File

@@ -521,6 +521,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
company: me.frm.doc.company, company: me.frm.doc.company,
order_type: me.frm.doc.order_type, order_type: me.frm.doc.order_type,
is_pos: cint(me.frm.doc.is_pos), is_pos: cint(me.frm.doc.is_pos),
is_return: cint(me.frm.doc.is_return),
is_subcontracted: me.frm.doc.is_subcontracted, is_subcontracted: me.frm.doc.is_subcontracted,
transaction_date: me.frm.doc.transaction_date || me.frm.doc.posting_date, transaction_date: me.frm.doc.transaction_date || me.frm.doc.posting_date,
ignore_pricing_rule: me.frm.doc.ignore_pricing_rule, ignore_pricing_rule: me.frm.doc.ignore_pricing_rule,

View File

@@ -0,0 +1,8 @@
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('E Invoice Request Log', {
// refresh: function(frm) {
// }
});

View File

@@ -0,0 +1,102 @@
{
"actions": [],
"autoname": "EINV-REQ-.#####",
"creation": "2020-12-08 12:54:08.175992",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"user",
"url",
"headers",
"response",
"column_break_7",
"timestamp",
"reference_invoice",
"data"
],
"fields": [
{
"fieldname": "user",
"fieldtype": "Link",
"label": "User",
"options": "User"
},
{
"fieldname": "reference_invoice",
"fieldtype": "Data",
"label": "Reference Invoice"
},
{
"fieldname": "headers",
"fieldtype": "Code",
"label": "Headers",
"options": "JSON"
},
{
"fieldname": "data",
"fieldtype": "Code",
"label": "Data",
"options": "JSON"
},
{
"default": "Now",
"fieldname": "timestamp",
"fieldtype": "Datetime",
"label": "Timestamp"
},
{
"fieldname": "response",
"fieldtype": "Code",
"label": "Response",
"options": "JSON"
},
{
"fieldname": "url",
"fieldtype": "Data",
"label": "URL"
},
{
"fieldname": "column_break_7",
"fieldtype": "Column Break"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-01-13 12:06:57.253111",
"modified_by": "Administrator",
"module": "Regional",
"name": "E Invoice Request Log",
"owner": "Administrator",
"permissions": [
{
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1
},
{
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts User",
"share": 1
},
{
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts Manager",
"share": 1
}
],
"sort_field": "modified",
"sort_order": "DESC"
}

View File

@@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
class EInvoiceRequestLog(Document):
pass

View File

@@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
# import frappe
import unittest
class TestEInvoiceRequestLog(unittest.TestCase):
pass

View File

@@ -0,0 +1,11 @@
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('E Invoice Settings', {
refresh(frm) {
const docs_link = 'https://docs.erpnext.com/docs/user/manual/en/regional/india/setup-e-invoicing';
frm.dashboard.set_headline(
__("Read {0} for more information on E Invoicing features.", [`<a href='${docs_link}'>documentation</a>`])
);
}
});

View File

@@ -0,0 +1,65 @@
{
"actions": [],
"creation": "2020-09-24 16:23:16.235722",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"enable",
"section_break_2",
"sandbox_mode",
"credentials",
"auth_token",
"token_expiry"
],
"fields": [
{
"default": "0",
"fieldname": "enable",
"fieldtype": "Check",
"label": "Enable"
},
{
"depends_on": "enable",
"fieldname": "section_break_2",
"fieldtype": "Section Break"
},
{
"fieldname": "auth_token",
"fieldtype": "Data",
"hidden": 1,
"read_only": 1
},
{
"fieldname": "token_expiry",
"fieldtype": "Datetime",
"hidden": 1,
"read_only": 1
},
{
"fieldname": "credentials",
"fieldtype": "Table",
"label": "Credentials",
"mandatory_depends_on": "enable",
"options": "E Invoice User"
},
{
"default": "0",
"fieldname": "sandbox_mode",
"fieldtype": "Check",
"label": "Sandbox Mode"
}
],
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2021-01-13 12:04:49.449199",
"modified_by": "Administrator",
"module": "Regional",
"name": "E Invoice Settings",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@@ -0,0 +1,14 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.model.document import Document
class EInvoiceSettings(Document):
def validate(self):
if self.enable and not self.credentials:
frappe.throw(_('You must add atleast one credentials to be able to use E Invoicing.'))

View File

@@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
# import frappe
import unittest
class TestEInvoiceSettings(unittest.TestCase):
pass

View File

@@ -0,0 +1,48 @@
{
"actions": [],
"creation": "2020-12-22 15:02:46.229474",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"gstin",
"username",
"password"
],
"fields": [
{
"fieldname": "gstin",
"fieldtype": "Data",
"in_list_view": 1,
"label": "GSTIN",
"reqd": 1
},
{
"fieldname": "username",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Username",
"reqd": 1
},
{
"fieldname": "password",
"fieldtype": "Password",
"in_list_view": 1,
"label": "Password",
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2020-12-22 15:10:53.466205",
"modified_by": "Administrator",
"module": "Regional",
"name": "E Invoice User",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
class EInvoiceUser(Document):
pass

View File

@@ -0,0 +1,31 @@
{{
"SlNo": "{item.sr_no}",
"PrdDesc": "{item.description}",
"IsServc": "{item.is_service_item}",
"HsnCd": "{item.gst_hsn_code}",
"Barcde": "{item.barcode}",
"Unit": "{item.uom}",
"Qty": "{item.qty}",
"FreeQty": "{item.free_qty}",
"UnitPrice": "{item.unit_rate}",
"TotAmt": "{item.gross_amount}",
"Discount": "{item.discount_amount}",
"AssAmt": "{item.taxable_value}",
"PrdSlNo": "{item.serial_no}",
"GstRt": "{item.tax_rate}",
"IgstAmt": "{item.igst_amount}",
"CgstAmt": "{item.cgst_amount}",
"SgstAmt": "{item.sgst_amount}",
"CesRt": "{item.cess_rate}",
"CesAmt": "{item.cess_amount}",
"CesNonAdvlAmt": "{item.cess_nadv_amount}",
"StateCesRt": "{item.state_cess_rate}",
"StateCesAmt": "{item.state_cess_amount}",
"StateCesNonAdvlAmt": "{item.state_cess_nadv_amount}",
"OthChrg": "{item.other_charges}",
"TotItemVal": "{item.total_value}",
"BchDtls": {{
"Nm": "{item.batch_no}",
"ExpDt": "{item.batch_expiry_date}"
}}
}}

View File

@@ -0,0 +1,110 @@
{{
"Version": "1.1",
"TranDtls": {{
"TaxSch": "{transaction_details.tax_scheme}",
"SupTyp": "{transaction_details.supply_type}",
"RegRev": "{transaction_details.reverse_charge}",
"EcmGstin": "{transaction_details.ecom_gstin}",
"IgstOnIntra": "{transaction_details.igst_on_intra}"
}},
"DocDtls": {{
"Typ": "{doc_details.invoice_type}",
"No": "{doc_details.invoice_name}",
"Dt": "{doc_details.invoice_date}"
}},
"SellerDtls": {{
"Gstin": "{seller_details.gstin}",
"LglNm": "{seller_details.legal_name}",
"TrdNm": "{seller_details.trade_name}",
"Loc": "{seller_details.location}",
"Pin": "{seller_details.pincode}",
"Stcd": "{seller_details.state_code}",
"Addr1": "{seller_details.address_line1}",
"Addr2": "{seller_details.address_line2}",
"Ph": "{seller_details.phone}",
"Em": "{seller_details.email}"
}},
"BuyerDtls": {{
"Gstin": "{buyer_details.gstin}",
"LglNm": "{buyer_details.legal_name}",
"TrdNm": "{buyer_details.trade_name}",
"Addr1": "{buyer_details.address_line1}",
"Addr2": "{buyer_details.address_line2}",
"Loc": "{buyer_details.location}",
"Pin": "{buyer_details.pincode}",
"Stcd": "{buyer_details.state_code}",
"Ph": "{buyer_details.phone}",
"Em": "{buyer_details.email}",
"Pos": "{buyer_details.place_of_supply}"
}},
"DispDtls": {{
"Nm": "{dispatch_details.company_name}",
"Addr1": "{dispatch_details.address_line1}",
"Addr2": "{dispatch_details.address_line2}",
"Loc": "{dispatch_details.location}",
"Pin": "{dispatch_details.pincode}",
"Stcd": "{dispatch_details.state_code}"
}},
"ShipDtls": {{
"Gstin": "{shipping_details.gstin}",
"LglNm": "{shipping_details.legal_name}",
"TrdNm": "{shipping_details.trader_name}",
"Addr1": "{shipping_details.address_line1}",
"Addr2": "{shipping_details.address_line2}",
"Loc": "{shipping_details.location}",
"Pin": "{shipping_details.pincode}",
"Stcd": "{shipping_details.state_code}"
}},
"ItemList": [
{item_list}
],
"ValDtls": {{
"AssVal": "{invoice_value_details.base_net_total}",
"CgstVal": "{invoice_value_details.total_cgst_amt}",
"SgstVal": "{invoice_value_details.total_sgst_amt}",
"IgstVal": "{invoice_value_details.total_igst_amt}",
"CesVal": "{invoice_value_details.total_cess_amt}",
"Discount": "{invoice_value_details.invoice_discount_amt}",
"RndOffAmt": "{invoice_value_details.round_off}",
"OthChrg": "{invoice_value_details.total_other_charges}",
"TotInvVal": "{invoice_value_details.base_grand_total}",
"TotInvValFc": "{invoice_value_details.grand_total}"
}},
"PayDtls": {{
"Nm": "{payment_details.payee_name}",
"AccDet": "{payment_details.account_no}",
"Mode": "{payment_details.mode_of_payment}",
"FinInsBr": "{payment_details.ifsc_code}",
"PayTerm": "{payment_details.terms}",
"PaidAmt": "{payment_details.paid_amount}",
"PaymtDue": "{payment_details.outstanding_amount}"
}},
"RefDtls": {{
"DocPerdDtls": {{
"InvStDt": "{period_details.start_date}",
"InvEndDt": "{period_details.end_date}"
}},
"PrecDocDtls": [{{
"InvNo": "{prev_doc_details.invoice_name}",
"InvDt": "{prev_doc_details.invoice_date}"
}}]
}},
"ExpDtls": {{
"ShipBNo": "{export_details.bill_no}",
"ShipBDt": "{export_details.bill_date}",
"Port": "{export_details.port}",
"ForCur": "{export_details.foreign_curr_code}",
"CntCode": "{export_details.country_code}",
"ExpDuty": "{export_details.export_duty}"
}},
"EwbDtls": {{
"TransId": "{eway_bill_details.gstin}",
"TransName": "{eway_bill_details.name}",
"TransMode": "{eway_bill_details.mode_of_transport}",
"Distance": "{eway_bill_details.distance}",
"TransDocNo": "{eway_bill_details.document_name}",
"TransDocDt": "{eway_bill_details.document_date}",
"VehNo": "{eway_bill_details.vehicle_no}",
"VehType": "{eway_bill_details.vehicle_type}"
}}
}}

View File

@@ -0,0 +1,956 @@
{
"Version": {
"type": "string",
"minLength": 1,
"maxLength": 6,
"description": "Version of the schema"
},
"Irn": {
"type": "string",
"minLength": 64,
"maxLength": 64,
"description": "Invoice Reference Number"
},
"TranDtls": {
"type": "object",
"properties": {
"TaxSch": {
"type": "string",
"minLength": 3,
"maxLength": 10,
"enum": ["GST"],
"description": "GST- Goods and Services Tax Scheme"
},
"SupTyp": {
"type": "string",
"minLength": 3,
"maxLength": 10,
"enum": ["B2B", "SEZWP", "SEZWOP", "EXPWP", "EXPWOP", "DEXP"],
"description": "Type of Supply: B2B-Business to Business, SEZWP - SEZ with payment, SEZWOP - SEZ without payment, EXPWP - Export with Payment, EXPWOP - Export without payment,DEXP - Deemed Export"
},
"RegRev": {
"type": "string",
"minLength": 1,
"maxLength": 1,
"enum": ["Y", "N"],
"description": "Y- whether the tax liability is payable under reverse charge"
},
"EcmGstin": {
"type": "string",
"minLength": 15,
"maxLength": 15,
"pattern": "([0-9]{2}[0-9A-Z]{13})",
"description": "E-Commerce GSTIN",
"validationMsg": "E-Commerce GSTIN is invalid"
},
"IgstOnIntra": {
"type": "string",
"minLength": 1,
"maxLength": 1,
"enum": ["Y", "N"],
"description": "Y- indicates the supply is intra state but chargeable to IGST"
}
},
"required": ["TaxSch", "SupTyp"]
},
"DocDtls": {
"type": "object",
"properties": {
"Typ": {
"type": "string",
"minLength": 3,
"maxLength": 3,
"enum": ["INV", "CRN", "DBN"],
"description": "Document Type"
},
"No": {
"type": "string",
"minLength": 1,
"maxLength": 16,
"pattern": "^([A-Z1-9]{1}[A-Z0-9/-]{0,15})$",
"description": "Document Number",
"validationMsg": "Document Number should not be starting with 0, / and -"
},
"Dt": {
"type": "string",
"minLength": 10,
"maxLength": 10,
"pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]",
"description": "Document Date"
}
},
"required": ["Typ", "No", "Dt"]
},
"SellerDtls": {
"type": "object",
"properties": {
"Gstin": {
"type": "string",
"minLength": 15,
"maxLength": 15,
"pattern": "([0-9]{2}[0-9A-Z]{13})",
"description": "Supplier GSTIN",
"validationMsg": "Company GSTIN is invalid"
},
"LglNm": {
"type": "string",
"minLength": 3,
"maxLength": 100,
"description": "Legal Name"
},
"TrdNm": {
"type": "string",
"minLength": 3,
"maxLength": 100,
"description": "Tradename"
},
"Addr1": {
"type": "string",
"minLength": 1,
"maxLength": 100,
"description": "Address Line 1"
},
"Addr2": {
"type": "string",
"minLength": 3,
"maxLength": 100,
"description": "Address Line 2"
},
"Loc": {
"type": "string",
"minLength": 3,
"maxLength": 50,
"description": "Location"
},
"Pin": {
"type": "number",
"minimum": 100000,
"maximum": 999999,
"description": "Pincode"
},
"Stcd": {
"type": "string",
"minLength": 1,
"maxLength": 2,
"description": "Supplier State Code"
},
"Ph": {
"type": "string",
"minLength": 6,
"maxLength": 12,
"description": "Phone"
},
"Em": {
"type": "string",
"minLength": 6,
"maxLength": 100,
"description": "Email-Id"
}
},
"required": ["Gstin", "LglNm", "Addr1", "Loc", "Pin", "Stcd"]
},
"BuyerDtls": {
"type": "object",
"properties": {
"Gstin": {
"type": "string",
"minLength": 3,
"maxLength": 15,
"pattern": "^(([0-9]{2}[0-9A-Z]{13})|URP)$",
"description": "Buyer GSTIN",
"validationMsg": "Customer GSTIN is invalid"
},
"LglNm": {
"type": "string",
"minLength": 3,
"maxLength": 100,
"description": "Legal Name"
},
"TrdNm": {
"type": "string",
"minLength": 3,
"maxLength": 100,
"description": "Trade Name"
},
"Pos": {
"type": "string",
"minLength": 1,
"maxLength": 2,
"description": "Place of Supply State code"
},
"Addr1": {
"type": "string",
"minLength": 1,
"maxLength": 100,
"description": "Address Line 1"
},
"Addr2": {
"type": "string",
"minLength": 3,
"maxLength": 100,
"description": "Address Line 2"
},
"Loc": {
"type": "string",
"minLength": 3,
"maxLength": 100,
"description": "Location"
},
"Pin": {
"type": "number",
"minimum": 100000,
"maximum": 999999,
"description": "Pincode"
},
"Stcd": {
"type": "string",
"minLength": 1,
"maxLength": 2,
"description": "Buyer State Code"
},
"Ph": {
"type": "string",
"minLength": 6,
"maxLength": 12,
"description": "Phone"
},
"Em": {
"type": "string",
"minLength": 6,
"maxLength": 100,
"description": "Email-Id"
}
},
"required": ["Gstin", "LglNm", "Pos", "Addr1", "Loc", "Stcd"]
},
"DispDtls": {
"type": "object",
"properties": {
"Nm": {
"type": "string",
"minLength": 3,
"maxLength": 100,
"description": "Dispatch Address Name"
},
"Addr1": {
"type": "string",
"minLength": 1,
"maxLength": 100,
"description": "Address Line 1"
},
"Addr2": {
"type": "string",
"minLength": 3,
"maxLength": 100,
"description": "Address Line 2"
},
"Loc": {
"type": "string",
"minLength": 3,
"maxLength": 100,
"description": "Location"
},
"Pin": {
"type": "number",
"minimum": 100000,
"maximum": 999999,
"description": "Pincode"
},
"Stcd": {
"type": "string",
"minLength": 1,
"maxLength": 2,
"description": "State Code"
}
},
"required": ["Nm", "Addr1", "Loc", "Pin", "Stcd"]
},
"ShipDtls": {
"type": "object",
"properties": {
"Gstin": {
"type": "string",
"maxLength": 15,
"minLength": 3,
"pattern": "^(([0-9]{2}[0-9A-Z]{13})|URP)$",
"description": "Shipping Address GSTIN",
"validationMsg": "Shipping Address GSTIN is invalid"
},
"LglNm": {
"type": "string",
"minLength": 3,
"maxLength": 100,
"description": "Legal Name"
},
"TrdNm": {
"type": "string",
"minLength": 3,
"maxLength": 100,
"description": "Trade Name"
},
"Addr1": {
"type": "string",
"minLength": 1,
"maxLength": 100,
"description": "Address Line 1"
},
"Addr2": {
"type": "string",
"minLength": 3,
"maxLength": 100,
"description": "Address Line 2"
},
"Loc": {
"type": "string",
"minLength": 3,
"maxLength": 100,
"description": "Location"
},
"Pin": {
"type": "number",
"minimum": 100000,
"maximum": 999999,
"description": "Pincode"
},
"Stcd": {
"type": "string",
"minLength": 1,
"maxLength": 2,
"description": "State Code"
}
},
"required": ["LglNm", "Addr1", "Loc", "Pin", "Stcd"]
},
"ItemList": {
"type": "Array",
"properties": {
"SlNo": {
"type": "string",
"minLength": 1,
"maxLength": 6,
"description": "Serial No. of Item"
},
"PrdDesc": {
"type": "string",
"minLength": 3,
"maxLength": 300,
"description": "Item Name"
},
"IsServc": {
"type": "string",
"minLength": 1,
"maxLength": 1,
"enum": ["Y", "N"],
"description": "Is Service Item"
},
"HsnCd": {
"type": "string",
"minLength": 4,
"maxLength": 8,
"description": "HSN Code"
},
"Barcde": {
"type": "string",
"minLength": 3,
"maxLength": 30,
"description": "Barcode"
},
"Qty": {
"type": "number",
"minimum": 0,
"maximum": 9999999999.999,
"description": "Quantity"
},
"FreeQty": {
"type": "number",
"minimum": 0,
"maximum": 9999999999.999,
"description": "Free Quantity"
},
"Unit": {
"type": "string",
"minLength": 3,
"maxLength": 8,
"description": "UOM"
},
"UnitPrice": {
"type": "number",
"minimum": 0,
"maximum": 999999999999.999,
"description": "Rate"
},
"TotAmt": {
"type": "number",
"minimum": 0,
"maximum": 999999999999.99,
"description": "Gross Amount"
},
"Discount": {
"type": "number",
"minimum": 0,
"maximum": 999999999999.99,
"description": "Discount"
},
"PreTaxVal": {
"type": "number",
"minimum": 0,
"maximum": 999999999999.99,
"description": "Pre tax value"
},
"AssAmt": {
"type": "number",
"minimum": 0,
"maximum": 999999999999.99,
"description": "Taxable Value"
},
"GstRt": {
"type": "number",
"minimum": 0,
"maximum": 999.999,
"description": "GST Rate"
},
"IgstAmt": {
"type": "number",
"minimum": 0,
"maximum": 999999999999.99,
"description": "IGST Amount"
},
"CgstAmt": {
"type": "number",
"minimum": 0,
"maximum": 999999999999.99,
"description": "CGST Amount"
},
"SgstAmt": {
"type": "number",
"minimum": 0,
"maximum": 999999999999.99,
"description": "SGST Amount"
},
"CesRt": {
"type": "number",
"minimum": 0,
"maximum": 999.999,
"description": "Cess Rate"
},
"CesAmt": {
"type": "number",
"minimum": 0,
"maximum": 999999999999.99,
"description": "Cess Amount (Advalorem)"
},
"CesNonAdvlAmt": {
"type": "number",
"minimum": 0,
"maximum": 999999999999.99,
"description": "Cess Amount (Non-Advalorem)"
},
"StateCesRt": {
"type": "number",
"minimum": 0,
"maximum": 999.999,
"description": "State CESS Rate"
},
"StateCesAmt": {
"type": "number",
"minimum": 0,
"maximum": 999999999999.99,
"description": "State CESS Amount"
},
"StateCesNonAdvlAmt": {
"type": "number",
"minimum": 0,
"maximum": 999999999999.99,
"description": "State CESS Amount (Non Advalorem)"
},
"OthChrg": {
"type": "number",
"minimum": 0,
"maximum": 999999999999.99,
"description": "Other Charges"
},
"TotItemVal": {
"type": "number",
"minimum": 0,
"maximum": 999999999999.99,
"description": "Total Item Value"
},
"OrdLineRef": {
"type": "string",
"minLength": 1,
"maxLength": 50,
"description": "Order line reference"
},
"OrgCntry": {
"type": "string",
"minLength": 2,
"maxLength": 2,
"description": "Origin Country"
},
"PrdSlNo": {
"type": "string",
"minLength": 1,
"maxLength": 20,
"description": "Serial number"
},
"BchDtls": {
"type": "object",
"properties": {
"Nm": {
"type": "string",
"minLength": 3,
"maxLength": 20,
"description": "Batch number"
},
"ExpDt": {
"type": "string",
"maxLength": 10,
"minLength": 10,
"pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]",
"description": "Batch Expiry Date"
},
"WrDt": {
"type": "string",
"maxLength": 10,
"minLength": 10,
"pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]",
"description": "Warranty Date"
}
},
"required": ["Nm"]
},
"AttribDtls": {
"type": "Array",
"Attribute": {
"type": "object",
"properties": {
"Nm": {
"type": "string",
"minLength": 1,
"maxLength": 100,
"description": "Attribute name of the item"
},
"Val": {
"type": "string",
"minLength": 1,
"maxLength": 100,
"description": "Attribute value of the item"
}
}
}
}
},
"required": [
"SlNo",
"IsServc",
"HsnCd",
"UnitPrice",
"TotAmt",
"AssAmt",
"GstRt",
"TotItemVal"
]
},
"ValDtls": {
"type": "object",
"properties": {
"AssVal": {
"type": "number",
"minimum": 0,
"maximum": 99999999999999.99,
"description": "Total Assessable value of all items"
},
"CgstVal": {
"type": "number",
"maximum": 99999999999999.99,
"minimum": 0,
"description": "Total CGST value of all items"
},
"SgstVal": {
"type": "number",
"minimum": 0,
"maximum": 99999999999999.99,
"description": "Total SGST value of all items"
},
"IgstVal": {
"type": "number",
"minimum": 0,
"maximum": 99999999999999.99,
"description": "Total IGST value of all items"
},
"CesVal": {
"type": "number",
"minimum": 0,
"maximum": 99999999999999.99,
"description": "Total CESS value of all items"
},
"StCesVal": {
"type": "number",
"minimum": 0,
"maximum": 99999999999999.99,
"description": "Total State CESS value of all items"
},
"Discount": {
"type": "number",
"minimum": 0,
"maximum": 99999999999999.99,
"description": "Invoice Discount"
},
"OthChrg": {
"type": "number",
"minimum": 0,
"maximum": 99999999999999.99,
"description": "Other Charges"
},
"RndOffAmt": {
"type": "number",
"minimum": -99.99,
"maximum": 99.99,
"description": "Rounded off Amount"
},
"TotInvVal": {
"type": "number",
"minimum": 0,
"maximum": 99999999999999.99,
"description": "Final Invoice Value "
},
"TotInvValFc": {
"type": "number",
"minimum": 0,
"maximum": 99999999999999.99,
"description": "Final Invoice value in Foreign Currency"
}
},
"required": ["AssVal", "TotInvVal"]
},
"PayDtls": {
"type": "object",
"properties": {
"Nm": {
"type": "string",
"minLength": 1,
"maxLength": 100,
"description": "Payee Name"
},
"AccDet": {
"type": "string",
"minLength": 1,
"maxLength": 18,
"description": "Bank Account Number of Payee"
},
"Mode": {
"type": "string",
"minLength": 1,
"maxLength": 18,
"description": "Mode of Payment"
},
"FinInsBr": {
"type": "string",
"minLength": 1,
"maxLength": 11,
"description": "Branch or IFSC code"
},
"PayTerm": {
"type": "string",
"minLength": 1,
"maxLength": 100,
"description": "Terms of Payment"
},
"PayInstr": {
"type": "string",
"minLength": 1,
"maxLength": 100,
"description": "Payment Instruction"
},
"CrTrn": {
"type": "string",
"minLength": 1,
"maxLength": 100,
"description": "Credit Transfer"
},
"DirDr": {
"type": "string",
"minLength": 1,
"maxLength": 100,
"description": "Direct Debit"
},
"CrDay": {
"type": "number",
"minimum": 0,
"maximum": 9999,
"description": "Credit Days"
},
"PaidAmt": {
"type": "number",
"minimum": 0,
"maximum": 99999999999999.99,
"description": "Advance Amount"
},
"PaymtDue": {
"type": "number",
"minimum": 0,
"maximum": 99999999999999.99,
"description": "Outstanding Amount"
}
}
},
"RefDtls": {
"type": "object",
"properties": {
"InvRm": {
"type": "string",
"maxLength": 100,
"minLength": 3,
"pattern": "^[0-9A-Za-z/-]{3,100}$",
"description": "Remarks/Note"
},
"DocPerdDtls": {
"type": "object",
"properties": {
"InvStDt": {
"type": "string",
"maxLength": 10,
"minLength": 10,
"pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]",
"description": "Invoice Period Start Date"
},
"InvEndDt": {
"type": "string",
"maxLength": 10,
"minLength": 10,
"pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]",
"description": "Invoice Period End Date"
}
},
"required": ["InvStDt ", "InvEndDt "]
},
"PrecDocDtls": {
"type": "object",
"properties": {
"InvNo": {
"type": "string",
"minLength": 1,
"maxLength": 16,
"pattern": "^[1-9A-Z]{1}[0-9A-Z/-]{1,15}$",
"description": "Reference of Original Invoice"
},
"InvDt": {
"type": "string",
"maxLength": 10,
"minLength": 10,
"pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]",
"description": "Date of Orginal Invoice"
},
"OthRefNo": {
"type": "string",
"minLength": 1,
"maxLength": 20,
"description": "Other Reference"
}
}
},
"required": ["InvNo", "InvDt"],
"ContrDtls": {
"type": "object",
"properties": {
"RecAdvRefr": {
"type": "string",
"minLength": 1,
"maxLength": 20,
"pattern": "^([0-9A-Za-z/-]){1,20}$",
"description": "Receipt Advice No."
},
"RecAdvDt": {
"type": "string",
"minLength": 10,
"maxLength": 10,
"pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]",
"description": "Date of receipt advice"
},
"TendRefr": {
"type": "string",
"minLength": 1,
"maxLength": 20,
"pattern": "^([0-9A-Za-z/-]){1,20}$",
"description": "Lot/Batch Reference No."
},
"ContrRefr": {
"type": "string",
"minLength": 1,
"maxLength": 20,
"pattern": "^([0-9A-Za-z/-]){1,20}$",
"description": "Contract Reference Number"
},
"ExtRefr": {
"type": "string",
"minLength": 1,
"maxLength": 20,
"pattern": "^([0-9A-Za-z/-]){1,20}$",
"description": "Any other reference"
},
"ProjRefr": {
"type": "string",
"minLength": 1,
"maxLength": 20,
"pattern": "^([0-9A-Za-z/-]){1,20}$",
"description": "Project Reference Number"
},
"PORefr": {
"type": "string",
"minLength": 1,
"maxLength": 16,
"pattern": "^([0-9A-Za-z/-]){1,16}$",
"description": "PO Reference Number"
},
"PORefDt": {
"type": "string",
"minLength": 10,
"maxLength": 10,
"pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]",
"description": "PO Reference date"
}
}
}
}
},
"AddlDocDtls": {
"type": "Array",
"properties": {
"Url": {
"type": "string",
"minLength": 3,
"maxLength": 100,
"description": "Supporting document URL"
},
"Docs": {
"type": "string",
"minLength": 3,
"maxLength": 1000,
"description": "Supporting document in Base64 Format"
},
"Info": {
"type": "string",
"minLength": 3,
"maxLength": 1000,
"description": "Any additional information"
}
}
},
"ExpDtls": {
"type": "object",
"properties": {
"ShipBNo": {
"type": "string",
"minLength": 1,
"maxLength": 20,
"description": "Shipping Bill No."
},
"ShipBDt": {
"type": "string",
"minLength": 10,
"maxLength": 10,
"pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]",
"description": "Shipping Bill Date"
},
"Port": {
"type": "string",
"minLength": 2,
"maxLength": 10,
"pattern": "^[0-9A-Za-z]{2,10}$",
"description": "Port Code. Refer the master"
},
"RefClm": {
"type": "string",
"minLength": 1,
"maxLength": 1,
"description": "Claiming Refund. Y/N"
},
"ForCur": {
"type": "string",
"minLength": 3,
"maxLength": 16,
"description": "Additional Currency Code. Refer the master"
},
"CntCode": {
"type": "string",
"minLength": 2,
"maxLength": 2,
"description": "Country Code. Refer the master"
},
"ExpDuty": {
"type": "number",
"minimum": 0,
"maximum": 999999999999.99,
"description": "Export Duty"
}
}
},
"EwbDtls": {
"type": "object",
"properties": {
"TransId": {
"type": "string",
"minLength": 15,
"maxLength": 15,
"description": "Transporter GSTIN"
},
"TransName": {
"type": "string",
"minLength": 3,
"maxLength": 100,
"description": "Transporter Name"
},
"TransMode": {
"type": "string",
"maxLength": 1,
"minLength": 1,
"enum": ["1", "2", "3", "4"],
"description": "Mode of Transport"
},
"Distance": {
"type": "number",
"minimum": 1,
"maximum": 9999,
"description": "Distance"
},
"TransDocNo": {
"type": "string",
"minLength": 1,
"maxLength": 15,
"pattern": "^([0-9A-Z/-]){1,15}$",
"description": "Tranport Document Number"
},
"TransDocDt": {
"type": "string",
"minLength": 10,
"maxLength": 10,
"pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]",
"description": "Transport Document Date"
},
"VehNo": {
"type": "string",
"minLength": 4,
"maxLength": 20,
"description": "Vehicle Number"
},
"VehType": {
"type": "string",
"minLength": 1,
"maxLength": 1,
"enum": ["O", "R"],
"description": "Vehicle Type"
}
},
"required": ["Distance"]
},
"required": [
"Version",
"TranDtls",
"DocDtls",
"SellerDtls",
"BuyerDtls",
"ItemList",
"ValDtls"
]
}

View File

@@ -0,0 +1,309 @@
erpnext.setup_einvoice_actions = (doctype) => {
frappe.ui.form.on(doctype, {
refresh(frm) {
const einvoicing_enabled = frappe.db.get_value("E Invoice Settings", "E Invoice Settings", "enable");
const supply_type = frm.doc.gst_category;
const valid_supply_type = ['Registered Regular', 'SEZ', 'Overseas', 'Deemed Export'].includes(supply_type);
const company_transaction = frm.doc.billing_address_gstin == frm.doc.company_gstin;
if (!einvoicing_enabled || !valid_supply_type || company_transaction) return;
const { doctype, irn, irn_cancelled, ewaybill, eway_bill_cancelled, name, __unsaved } = frm.doc;
const add_custom_button = (label, action) => {
if (!frm.custom_buttons[label]) {
frm.add_custom_button(label, action, __('E Invoicing'));
}
};
if (ewaybill && irn) {
frm.set_df_property('ewaybill', 'read_only', 1);
}
if (!irn && !__unsaved) {
const action = () => {
frappe.call({
method: 'erpnext.regional.india.e_invoice.utils.get_einvoice',
args: { doctype, docname: name },
freeze: true,
callback: (res) => {
const einvoice = res.message;
show_einvoice_preview(frm, einvoice);
}
});
};
add_custom_button(__("Generate IRN"), action);
}
if (irn && !irn_cancelled && !ewaybill) {
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 IRN"),
fields: fields,
primary_action: function() {
const data = d.get_values();
frappe.call({
method: 'erpnext.regional.india.e_invoice.utils.cancel_irn',
args: {
doctype,
docname: name,
irn: irn,
reason: data.reason.split('-')[0],
remark: data.remark
},
freeze: true,
callback: () => frm.reload_doc() || d.hide(),
error: () => d.hide()
});
},
primary_action_label: __('Submit')
});
d.show();
};
add_custom_button(__("Cancel IRN"), action);
}
if (irn && !irn_cancelled && !ewaybill) {
const action = () => {
const d = new frappe.ui.Dialog({
title: __('Generate E-Way Bill'),
wide: 1,
fields: get_ewaybill_fields(frm),
primary_action: function() {
const data = d.get_values();
frappe.call({
method: 'erpnext.regional.india.e_invoice.utils.generate_eway_bill',
args: {
doctype,
docname: name,
irn,
...data
},
freeze: true,
callback: () => frm.reload_doc() || d.hide(),
error: () => d.hide()
});
},
primary_action_label: __('Submit')
});
d.show();
};
add_custom_button(__("Generate E-Way Bill"), action);
}
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,
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
},
freeze: true,
callback: () => frm.reload_doc() || d.hide(),
error: () => d.hide()
});
},
primary_action_label: __('Submit')
});
d.show();
};
add_custom_button(__("Cancel E-Way Bill"), action);
}
}
});
};
const get_ewaybill_fields = (frm) => {
return [
{
'fieldname': 'transporter',
'label': 'Transporter',
'fieldtype': 'Link',
'options': 'Supplier',
'default': frm.doc.transporter
},
{
'fieldname': 'gst_transporter_id',
'label': 'GST Transporter ID',
'fieldtype': 'Data',
'fetch_from': 'transporter.gst_transporter_id',
'default': frm.doc.gst_transporter_id
},
{
'fieldname': 'driver',
'label': 'Driver',
'fieldtype': 'Link',
'options': 'Driver',
'default': frm.doc.driver
},
{
'fieldname': 'lr_no',
'label': 'Transport Receipt No',
'fieldtype': 'Data',
'default': frm.doc.lr_no
},
{
'fieldname': 'vehicle_no',
'label': 'Vehicle No',
'fieldtype': 'Data',
'depends_on': 'eval:(doc.mode_of_transport === "Road")',
'default': frm.doc.vehicle_no
},
{
'fieldname': 'distance',
'label': 'Distance (in km)',
'fieldtype': 'Float',
'default': frm.doc.distance
},
{
'fieldname': 'transporter_col_break',
'fieldtype': 'Column Break',
},
{
'fieldname': 'transporter_name',
'label': 'Transporter Name',
'fieldtype': 'Data',
'fetch_from': 'transporter.name',
'read_only': 1,
'default': frm.doc.transporter_name
},
{
'fieldname': 'mode_of_transport',
'label': 'Mode of Transport',
'fieldtype': 'Select',
'options': `\nRoad\nAir\nRail\nShip`,
'default': frm.doc.mode_of_transport
},
{
'fieldname': 'driver_name',
'label': 'Driver Name',
'fieldtype': 'Data',
'fetch_from': 'driver.full_name',
'read_only': 1,
'default': frm.doc.driver_name
},
{
'fieldname': 'lr_date',
'label': 'Transport Receipt Date',
'fieldtype': 'Date',
'default': frm.doc.lr_date
},
{
'fieldname': 'gst_vehicle_type',
'label': 'GST Vehicle Type',
'fieldtype': 'Select',
'options': `Regular\nOver Dimensional Cargo (ODC)`,
'depends_on': 'eval:(doc.mode_of_transport === "Road")',
'default': frm.doc.gst_vehicle_type
}
];
};
const request_irn_generation = (frm) => {
frappe.call({
method: 'erpnext.regional.india.e_invoice.utils.generate_irn',
args: { doctype: frm.doc.doctype, docname: frm.doc.name },
freeze: true,
callback: () => frm.reload_doc()
});
};
const get_preview_dialog = (frm, action) => {
const dialog = new frappe.ui.Dialog({
title: __("Preview"),
wide: 1,
fields: [
{
"label": "Preview",
"fieldname": "preview_html",
"fieldtype": "HTML"
}
],
primary_action: () => action(frm) || dialog.hide(),
primary_action_label: __('Generate IRN')
});
return dialog;
};
const show_einvoice_preview = (frm, einvoice) => {
const preview_dialog = get_preview_dialog(frm, request_irn_generation);
// initialize e-invoice fields
einvoice["Irn"] = einvoice["AckNo"] = ''; einvoice["AckDt"] = frappe.datetime.nowdate();
frm.doc.signed_einvoice = JSON.stringify(einvoice);
// initialize preview wrapper
const $preview_wrapper = preview_dialog.get_field("preview_html").$wrapper;
$preview_wrapper.html(
`<div>
<div class="print-preview">
<div class="print-format"></div>
</div>
<div class="page-break-message text-muted text-center text-medium margin-top"></div>
</div>`
);
frappe.call({
method: "frappe.www.printview.get_html_and_style",
args: {
doc: frm.doc,
print_format: "GST E-Invoice",
no_letterhead: 1
},
callback: function (r) {
if (!r.exc) {
$preview_wrapper.find(".print-format").html(r.message.html);
const style = `
.print-format { box-shadow: 0px 0px 5px rgba(0,0,0,0.2); padding: 0.30in; min-height: 80vh; }
.print-preview { min-height: 0px; }
.modal-dialog { width: 720px; }`;
frappe.dom.set_style(style, "custom-print-style");
preview_dialog.show();
}
}
});
};

View File

@@ -0,0 +1,814 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import os
import re
import jwt
import sys
import json
import base64
import frappe
import traceback
from frappe import _, bold
from pyqrcode import create as qrcreate
from frappe.integrations.utils import make_post_request, make_get_request
from erpnext.regional.india.utils import get_gst_accounts, get_place_of_supply
from frappe.utils.data import cstr, cint, formatdate as format_date, flt, time_diff_in_seconds, now_datetime, add_to_date, get_link_to_form
def validate_einvoice_fields(doc):
einvoicing_enabled = cint(frappe.db.get_value('E Invoice Settings', 'E Invoice Settings', 'enable'))
invalid_doctype = doc.doctype not in ['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')
if not einvoicing_enabled or invalid_doctype or invalid_supply_type or company_transaction: return
if doc.docstatus == 0 and doc._action == 'save':
if doc.irn:
frappe.throw(_('You cannot edit the invoice after generating IRN'), title=_('Edit Not Allowed'))
if len(doc.name) > 16:
raise_document_name_too_long_error()
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'))
elif doc.docstatus == 2 and doc._action == 'cancel' and not doc.irn_cancelled:
frappe.throw(_('You must cancel IRN before cancelling the document.'), title=_('Cancel Not Allowed'))
def raise_document_name_too_long_error():
title = _('Document ID Too Long')
msg = _('As you have E-Invoicing enabled, to be able to generate IRN for this invoice, ')
msg += _('document id {} exceed 16 letters. ').format(bold(_('should not')))
msg += '<br><br>'
msg += _('You must {} your {} in order to have document id of {} length 16. ').format(
bold(_('modify')), bold(_('naming series')), bold(_('maximum'))
)
msg += _('Please account for ammended documents too. ')
frappe.throw(msg, title=title)
def read_json(name):
file_path = os.path.join(os.path.dirname(__file__), '{name}.json'.format(name=name))
with open(file_path, 'r') as f:
return cstr(f.read())
def get_transaction_details(invoice):
supply_type = ''
if invoice.gst_category == 'Registered Regular': supply_type = 'B2B'
elif invoice.gst_category == 'SEZ': supply_type = 'SEZWOP'
elif invoice.gst_category == 'Overseas': supply_type = 'EXPWOP'
elif invoice.gst_category == 'Deemed Export': supply_type = 'DEXP'
if not supply_type:
rr, sez, overseas, export = bold('Registered Regular'), bold('SEZ'), bold('Overseas'), bold('Deemed Export')
frappe.throw(_('GST category should be one of {}, {}, {}, {}').format(rr, sez, overseas, export),
title=_('Invalid Supply Type'))
return frappe._dict(dict(
tax_scheme='GST',
supply_type=supply_type,
reverse_charge=invoice.reverse_charge
))
def get_doc_details(invoice):
invoice_type = 'CRN' if invoice.is_return else 'INV'
invoice_name = invoice.name
invoice_date = format_date(invoice.posting_date, 'dd/mm/yyyy')
return frappe._dict(dict(
invoice_type=invoice_type,
invoice_name=invoice_name,
invoice_date=invoice_date
))
def get_party_details(address_name):
d = frappe.get_all('Address', filters={'name': address_name}, fields=['*'])[0]
if (not d.gstin
or not d.city
or not d.pincode
or not d.address_title
or not d.address_line1
or not d.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)
),
title=_('Missing Address Fields')
)
if d.gst_state_number == 97:
# according to einvoice standard
pincode = 999999
return frappe._dict(dict(
gstin=d.gstin, legal_name=d.address_title,
location=d.city, pincode=d.pincode,
state_code=d.gst_state_number,
address_line1=d.address_line1,
address_line2=d.address_line2
))
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']
)
if not address_title or not address_line1 or not city:
frappe.throw(
msg=_('Address lines and city is mandatory for address {}. Please set them and try again.').format(
get_link_to_form('Address', address_name)
),
title=_('Missing Address Fields')
)
return frappe._dict(dict(
gstin='URP', legal_name=address_title, location=city,
address_line1=address_line1, address_line2=address_line2,
pincode=999999, state_code=96, place_of_supply=96
))
def get_item_list(invoice):
item_list = []
for d in invoice.items:
einvoice_item_schema = read_json('einv_item_template')
item = frappe._dict({})
item.update(d.as_dict())
item.sr_no = d.idx
item.description = d.item_name.replace('"', '\\"')
item.qty = abs(item.qty)
item.discount_amount = abs(item.discount_amount * item.qty)
item.unit_rate = abs(item.base_amount / item.qty)
item.gross_amount = abs(item.base_amount)
item.taxable_value = abs(item.base_amount)
item.batch_expiry_date = frappe.db.get_value('Batch', d.batch_no, 'expiry_date') if d.batch_no else None
item.batch_expiry_date = format_date(item.batch_expiry_date, 'dd/mm/yyyy') if item.batch_expiry_date else None
item.is_service_item = 'N' if frappe.db.get_value('Item', d.item_code, 'is_stock_item') else 'Y'
item.serial_no = ""
item = update_item_taxes(invoice, item)
item.total_value = abs(
item.taxable_value + item.igst_amount + item.sgst_amount +
item.cgst_amount + item.cess_amount + item.cess_nadv_amount + item.other_charges
)
einv_item = einvoice_item_schema.format(item=item)
item_list.append(einv_item)
return ', '.join(item_list)
def update_item_taxes(invoice, item):
gst_accounts = get_gst_accounts(invoice.company)
gst_accounts_list = [d for accounts in gst_accounts.values() for d in accounts if d]
for attr in [
'tax_rate', 'cess_rate', 'cess_nadv_amount',
'cgst_amount', 'sgst_amount', 'igst_amount',
'cess_amount', 'cess_nadv_amount', 'other_charges'
]:
item[attr] = 0
for t in invoice.taxes:
item_tax_detail = json.loads(t.item_wise_tax_detail).get(item.item_code)
if t.account_head in gst_accounts_list:
if t.account_head in gst_accounts.cess_account:
if t.charge_type == 'On Item Quantity':
item.cess_nadv_amount += abs(item_tax_detail[1])
else:
item.cess_rate += item_tax_detail[0]
item.cess_amount += abs(item_tax_detail[1])
elif t.account_head in gst_accounts.igst_account:
item.tax_rate += item_tax_detail[0]
item.igst_amount += abs(item_tax_detail[1])
elif t.account_head in gst_accounts.sgst_account:
item.tax_rate += item_tax_detail[0]
item.sgst_amount += abs(item_tax_detail[1])
elif t.account_head in gst_accounts.cgst_account:
item.tax_rate += item_tax_detail[0]
item.cgst_amount += abs(item_tax_detail[1])
return item
def get_invoice_value_details(invoice):
invoice_value_details = frappe._dict(dict())
invoice_value_details.base_net_total = abs(invoice.base_net_total)
invoice_value_details.invoice_discount_amt = invoice.discount_amount if invoice.discount_amount and invoice.discount_amount > 0 else 0
# discount amount cannnot be -ve in an e-invoice, so if -ve include discount in round_off
invoice_value_details.round_off = invoice.rounding_adjustment - (invoice.discount_amount if invoice.discount_amount and invoice.discount_amount < 0 else 0)
invoice_value_details.base_grand_total = abs(invoice.base_rounded_total) or abs(invoice.base_grand_total)
invoice_value_details.grand_total = abs(invoice.rounded_total) or abs(invoice.grand_total)
invoice_value_details = update_invoice_taxes(invoice, invoice_value_details)
return invoice_value_details
def update_invoice_taxes(invoice, invoice_value_details):
gst_accounts = get_gst_accounts(invoice.company)
gst_accounts_list = [d for accounts in gst_accounts.values() for d in accounts if d]
invoice_value_details.total_cgst_amt = 0
invoice_value_details.total_sgst_amt = 0
invoice_value_details.total_igst_amt = 0
invoice_value_details.total_cess_amt = 0
invoice_value_details.total_other_charges = 0
for t in invoice.taxes:
if t.account_head in gst_accounts_list:
if t.account_head in gst_accounts.cess_account:
invoice_value_details.total_cess_amt += abs(t.base_tax_amount_after_discount_amount)
elif t.account_head in gst_accounts.igst_account:
invoice_value_details.total_igst_amt += abs(t.base_tax_amount_after_discount_amount)
elif t.account_head in gst_accounts.sgst_account:
invoice_value_details.total_sgst_amt += abs(t.base_tax_amount_after_discount_amount)
elif t.account_head in gst_accounts.cgst_account:
invoice_value_details.total_cgst_amt += abs(t.base_tax_amount_after_discount_amount)
else:
invoice_value_details.total_other_charges += abs(t.base_tax_amount_after_discount_amount)
return invoice_value_details
def get_payment_details(invoice):
payee_name = invoice.company
mode_of_payment = ', '.join([d.mode_of_payment for d in invoice.payments])
paid_amount = invoice.base_paid_amount
outstanding_amount = invoice.outstanding_amount
return frappe._dict(dict(
payee_name=payee_name, mode_of_payment=mode_of_payment,
paid_amount=paid_amount, outstanding_amount=outstanding_amount
))
def get_return_doc_reference(invoice):
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')
))
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'))
mode_of_transport = { '': '', 'Road': '1', 'Air': '2', 'Rail': '3', 'Ship': '4' }
vehicle_type = { 'Regular': 'R', 'Over Dimensional Cargo (ODC)': 'O' }
return frappe._dict(dict(
gstin=invoice.gst_transporter_id,
name=invoice.transporter_name,
mode_of_transport=mode_of_transport[invoice.mode_of_transport],
distance=invoice.distance or 0,
document_name=invoice.lr_no,
document_date=format_date(invoice.lr_date, 'dd/mm/yyyy'),
vehicle_no=invoice.vehicle_no,
vehicle_type=vehicle_type[invoice.gst_vehicle_type]
))
def validate_mandatory_fields(invoice):
if not invoice.company_address:
frappe.throw(_('Company Address is mandatory to fetch company GSTIN details.'), title=_('Missing Fields'))
if not invoice.customer_address:
frappe.throw(_('Customer Address is mandatory to fetch customer GSTIN details.'), 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.'),
title=_('Missing Fields')
)
if not frappe.db.get_value('Address', invoice.customer_address, 'gstin'):
frappe.throw(
_('GSTIN is mandatory to fetch customer GSTIN details. Please enter GSTIN in selected customer address.'),
title=_('Missing Fields')
)
def make_einvoice(invoice):
validate_mandatory_fields(invoice)
schema = read_json('einv_template')
transaction_details = get_transaction_details(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)
if invoice.gst_category == 'Overseas':
buyer_details = get_overseas_address_details(invoice.customer_address)
else:
buyer_details = get_party_details(invoice.customer_address)
place_of_supply = get_place_of_supply(invoice, invoice.doctype) or invoice.billing_address_gstin
place_of_supply = place_of_supply[:2]
buyer_details.update(dict(place_of_supply=place_of_supply))
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:
shipping_details = get_party_details(invoice.shipping_address_name)
if invoice.is_pos and invoice.base_paid_amount:
payment_details = get_payment_details(invoice)
if invoice.is_return and invoice.return_against:
prev_doc_details = get_return_doc_reference(invoice)
if invoice.transporter:
eway_bill_details = get_eway_bill_details(invoice)
# not yet implemented
dispatch_details = period_details = export_details = frappe._dict({})
einvoice = schema.format(
transaction_details=transaction_details, doc_details=doc_details, dispatch_details=dispatch_details,
seller_details=seller_details, buyer_details=buyer_details, shipping_details=shipping_details,
item_list=item_list, invoice_value_details=invoice_value_details, payment_details=payment_details,
period_details=period_details, prev_doc_details=prev_doc_details,
export_details=export_details, eway_bill_details=eway_bill_details
)
einvoice = json.loads(einvoice)
validations = json.loads(read_json('einv_validation'))
errors = validate_einvoice(validations, einvoice)
if errors:
message = "\n".join([
"E Invoice: ", json.dumps(einvoice, indent=4),
"-" * 50,
"Errors: ", json.dumps(errors, indent=4)
])
frappe.log_error(title="E Invoice Validation Failed", message=message)
throw_error_list(errors, _('E Invoice Validation Failed'))
return einvoice
def throw_error_list(errors, title):
if len(errors) > 1:
li = ['<li>'+ d +'</li>' for d in errors]
frappe.throw("<ul style='padding-left: 20px'>{}</ul>".format(''.join(li)), title=title)
else:
frappe.throw(errors[0], title=title)
def validate_einvoice(validations, einvoice, 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 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.invoice = frappe.get_cached_doc(doctype, docname) if doctype and docname else None
self.credentials = self.get_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.cancel_irn_url = self.base_url + '/enriched/ei/api/invoice/cancel'
self.irn_details_url = self.base_url + '/enriched/ei/api/invoice/irn'
self.generate_irn_url = self.base_url + '/enriched/ei/api/invoice'
self.gstin_details_url = self.base_url + '/enriched/ei/api/master/gstin'
self.cancel_ewaybill_url = self.base_url + '/enriched/ei/api/ewayapi'
self.generate_ewaybill_url = self.base_url + '/enriched/ei/api/ewaybill'
def get_credentials(self):
if self.invoice:
gstin = self.get_seller_gstin()
credentials = next(d for d in self.e_invoice_settings.credentials if d.gstin == gstin)
else:
credentials = self.e_invoice_settings.credentials[0] if self.e_invoice_settings.credentials else None
return credentials
def get_seller_gstin(self):
gstin = self.invoice.company_gstin or frappe.db.get_value('Address', self.invoice.company_address, 'gstin')
if not gstin:
frappe.throw(_('Cannot retrieve Company GSTIN. Please select company address with valid GSTIN.'))
return gstin
def get_auth_token(self):
if time_diff_in_seconds(self.e_invoice_settings.token_expiry, now_datetime()) < 150.0:
self.fetch_auth_token()
return self.e_invoice_settings.auth_token
def make_request(self, request_type, url, headers=None, data=None):
if request_type == 'post':
res = make_post_request(url, headers=headers, data=data)
else:
res = make_get_request(url, headers=headers, data=data)
self.log_request(url, headers, data, res)
return res
def log_request(self, url, headers, data, res):
headers.update({ 'password': self.credentials.password })
request_log = frappe.get_doc({
"doctype": "E Invoice Request Log",
"user": frappe.session.user,
"reference_invoice": self.invoice.name if self.invoice else None,
"url": url,
"headers": json.dumps(headers, indent=4) if headers else None,
"data": json.dumps(data, indent=4) if isinstance(data, dict) else data,
"response": json.dumps(res, indent=4) if res else None
})
request_log.insert(ignore_permissions=True)
frappe.db.commit()
def fetch_auth_token(self):
headers = {
'gspappid': frappe.conf.einvoice_client_id,
'gspappsecret': frappe.conf.einvoice_client_secret
}
res = {}
try:
res = self.make_request('post', self.authenticate_url, headers)
self.e_invoice_settings.auth_token = "{} {}".format(res.get('token_type'), res.get('access_token'))
self.e_invoice_settings.token_expiry = add_to_date(None, seconds=res.get('expires_in'))
self.e_invoice_settings.save()
except Exception:
self.log_error(res)
self.raise_error(True)
def get_headers(self):
return {
'content-type': 'application/json',
'user_name': self.credentials.username,
'password': self.credentials.get_password(),
'gstin': self.credentials.gstin,
'authorization': self.get_auth_token(),
'requestid': str(base64.b64encode(os.urandom(18))),
}
def fetch_gstin_details(self, gstin):
headers = self.get_headers()
try:
params = '?gstin={gstin}'.format(gstin=gstin)
res = self.make_request('get', self.gstin_details_url + params, headers)
if res.get('success'):
return res.get('result')
else:
self.log_error(res)
raise RequestFailed
except RequestFailed:
self.raise_error()
except Exception:
self.log_error()
self.raise_error(True)
@staticmethod
def get_gstin_details(gstin):
'''fetch and cache GSTIN details'''
if not hasattr(frappe.local, 'gstin_cache'):
frappe.local.gstin_cache = {}
key = gstin
gsp_connector = GSPConnector()
details = gsp_connector.fetch_gstin_details(gstin)
frappe.local.gstin_cache[key] = details
frappe.cache().hset('gstin_cache', key, details)
return details
def generate_irn(self):
headers = self.get_headers()
einvoice = make_einvoice(self.invoice)
data = json.dumps(einvoice, indent=4)
try:
res = self.make_request('post', self.generate_irn_url, headers, data)
if res.get('success'):
self.set_einvoice_data(res.get('result'))
elif '2150' in res.get('message'):
# IRN already generated but not updated in invoice
# Extract the IRN from the response description and fetch irn details
irn = res.get('result')[0].get('Desc').get('Irn')
irn_details = self.get_irn_details(irn)
if irn_details:
self.set_einvoice_data(irn_details)
else:
raise RequestFailed('IRN has already been generated for the invoice but cannot fetch details for the it. \
Contact ERPNext support to resolve the issue.')
else:
raise RequestFailed
except RequestFailed:
errors = self.sanitize_error_message(res.get('message'))
self.raise_error(errors=errors)
except Exception:
self.log_error(data)
self.raise_error(True)
def get_irn_details(self, irn):
headers = self.get_headers()
try:
params = '?irn={irn}'.format(irn=irn)
res = self.make_request('get', self.irn_details_url + params, headers)
if res.get('success'):
return res.get('result')
else:
raise RequestFailed
except RequestFailed:
errors = self.sanitize_error_message(res.get('message'))
self.raise_error(errors=errors)
except Exception:
self.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)
try:
res = self.make_request('post', self.cancel_irn_url, headers, data)
if res.get('success'):
self.invoice.irn_cancelled = 1
self.invoice.flags.updater_reference = {
'doctype': self.invoice.doctype,
'docname': self.invoice.name,
'label': _('IRN Cancelled - {}').format(remark)
}
self.update_invoice()
else:
raise RequestFailed
except RequestFailed:
errors = self.sanitize_error_message(res.get('message'))
self.raise_error(errors=errors)
except Exception:
self.log_error(data)
self.raise_error(True)
def generate_eway_bill(self, **kwargs):
args = frappe._dict(kwargs)
headers = self.get_headers()
eway_bill_details = get_eway_bill_details(args)
data = json.dumps({
'Irn': args.irn,
'Distance': cint(eway_bill_details.distance),
'TransMode': eway_bill_details.mode_of_transport,
'TransId': eway_bill_details.gstin,
'TransName': eway_bill_details.transporter,
'TrnDocDt': eway_bill_details.document_date,
'TrnDocNo': eway_bill_details.document_name,
'VehNo': eway_bill_details.vehicle_no,
'VehType': eway_bill_details.vehicle_type
}, indent=4)
try:
res = self.make_request('post', self.generate_ewaybill_url, headers, data)
if res.get('success'):
self.invoice.ewaybill = res.get('result').get('EwbNo')
self.invoice.eway_bill_cancelled = 0
self.invoice.update(args)
self.invoice.flags.updater_reference = {
'doctype': self.invoice.doctype,
'docname': self.invoice.name,
'label': _('E-Way Bill Generated')
}
self.update_invoice()
else:
raise RequestFailed
except RequestFailed:
errors = self.sanitize_error_message(res.get('message'))
self.raise_error(errors=errors)
except Exception:
self.log_error(data)
self.raise_error(True)
def cancel_eway_bill(self, eway_bill, reason, remark):
headers = self.get_headers()
data = json.dumps({
'ewbNo': eway_bill,
'cancelRsnCode': reason,
'cancelRmrk': remark
}, indent=4)
try:
res = self.make_request('post', self.cancel_ewaybill_url, headers, data)
if res.get('success'):
self.invoice.ewaybill = ''
self.invoice.eway_bill_cancelled = 1
self.invoice.flags.updater_reference = {
'doctype': self.invoice.doctype,
'docname': self.invoice.name,
'label': _('E-Way Bill Cancelled - {}').format(remark)
}
self.update_invoice()
else:
raise RequestFailed
except RequestFailed:
errors = self.sanitize_error_message(res.get('message'))
self.raise_error(errors=errors)
except Exception:
self.log_error(data)
self.raise_error(True)
def sanitize_error_message(self, message):
'''
On validation errors, response message looks something like this:
message = '2174 : For inter-state transaction, CGST and SGST amounts are not applicable; only IGST amount is applicable,
3095 : Supplier GSTIN is inactive'
we search for string between ':' to extract the error messages
errors = [
': For inter-state transaction, CGST and SGST amounts are not applicable; only IGST amount is applicable, 3095 ',
': Test'
]
then we trim down the message by looping over errors
'''
errors = re.findall(': [^:]+', message)
for idx, e in enumerate(errors):
# remove colons
errors[idx] = errors[idx].replace(':', '').strip()
# if not last
if idx != len(errors) - 1:
# remove last 7 chars eg: ', 3095 '
errors[idx] = errors[idx][:-6]
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:
throw_error_list(errors, title)
else:
link_to_error_list = '<a href="desk#List/Error Log/List?method=E Invoice Request Failed">Error Log</a>'
frappe.msgprint(
_('An error occurred while making e-invoicing request. Please check {} for more information.').format(link_to_error_list),
title=title,
raise_exception=raise_exception,
indicator='red'
)
def set_einvoice_data(self, res):
enc_signed_invoice = res.get('SignedInvoice')
dec_signed_invoice = jwt.decode(enc_signed_invoice, verify=False)['data']
self.invoice.irn = res.get('Irn')
self.invoice.ewaybill = res.get('EwbNo')
self.invoice.signed_einvoice = dec_signed_invoice
self.invoice.signed_qr_code = res.get('SignedQRCode')
self.attach_qrcode_image()
self.invoice.flags.updater_reference = {
'doctype': self.invoice.doctype,
'docname': self.invoice.name,
'label': _('IRN Generated')
}
self.update_invoice()
def attach_qrcode_image(self):
qrcode = self.invoice.signed_qr_code
doctype = self.invoice.doctype
docname = self.invoice.name
_file = frappe.new_doc('File')
_file.update({
'file_name': 'QRCode_{}.png'.format(docname.replace('/', '-')),
'attached_to_doctype': doctype,
'attached_to_name': docname,
'content': 'qrcode',
'is_private': 1
})
_file.insert()
frappe.db.commit()
url = qrcreate(qrcode, error='L')
abs_file_path = os.path.abspath(_file.get_full_path())
url.png(abs_file_path, scale=2, quiet_zone=1)
self.invoice.qrcode_image = _file.file_url
def update_invoice(self):
self.invoice.flags.ignore_validate_update_after_submit = True
self.invoice.flags.ignore_validate = True
self.invoice.save()
@frappe.whitelist()
def get_einvoice(doctype, docname):
invoice = frappe.get_doc(doctype, docname)
return make_einvoice(invoice)
@frappe.whitelist()
def generate_irn(doctype, docname):
gsp_connector = GSPConnector(doctype, docname)
gsp_connector.generate_irn()
@frappe.whitelist()
def cancel_irn(doctype, docname, irn, reason, remark):
gsp_connector = GSPConnector(doctype, docname)
gsp_connector.cancel_irn(irn, reason, remark)
@frappe.whitelist()
def generate_eway_bill(doctype, docname, **kwargs):
gsp_connector = GSPConnector(doctype, docname)
gsp_connector.generate_eway_bill(**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)

View File

@@ -22,4 +22,4 @@ erpnext.setup_gst_reminder_button = (doctype) => {
} }
} }
}); });
}; };

View File

@@ -7,7 +7,7 @@ import frappe, os, json
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
from frappe.permissions import add_permission, update_permission_property from frappe.permissions import add_permission, update_permission_property
from erpnext.regional.india import states from erpnext.regional.india import states
from erpnext.accounts.utils import get_fiscal_year from erpnext.accounts.utils import get_fiscal_year, FiscalYearError
from frappe.utils import today from frappe.utils import today
def setup(company=None, patch=True): def setup(company=None, patch=True):
@@ -77,7 +77,7 @@ def add_custom_roles_for_reports():
)).insert() )).insert()
def add_permissions(): def add_permissions():
for doctype in ('GST HSN Code', 'GST Settings', 'GSTR 3B Report', 'Lower Deduction Certificate'): for doctype in ('GST HSN Code', 'GST Settings', 'GSTR 3B Report', 'Lower Deduction Certificate', 'E Invoice Settings'):
add_permission(doctype, 'All', 0) add_permission(doctype, 'All', 0)
for role in ('Accounts Manager', 'Accounts User', 'System Manager'): for role in ('Accounts Manager', 'Accounts User', 'System Manager'):
add_permission(doctype, role, 0) add_permission(doctype, role, 0)
@@ -93,9 +93,10 @@ def add_permissions():
def add_print_formats(): def add_print_formats():
frappe.reload_doc("regional", "print_format", "gst_tax_invoice") frappe.reload_doc("regional", "print_format", "gst_tax_invoice")
frappe.reload_doc("accounts", "print_format", "gst_pos_invoice") frappe.reload_doc("accounts", "print_format", "gst_pos_invoice")
frappe.reload_doc("accounts", "print_format", "GST E-Invoice")
frappe.db.sql(""" update `tabPrint Format` set disabled = 0 where frappe.db.sql(""" update `tabPrint Format` set disabled = 0 where
name in('GST POS Invoice', 'GST Tax Invoice') """) name in('GST POS Invoice', 'GST Tax Invoice', 'GST E-Invoice') """)
def make_custom_fields(update=True): def make_custom_fields(update=True):
hsn_sac_field = dict(fieldname='gst_hsn_code', label='HSN/SAC', hsn_sac_field = dict(fieldname='gst_hsn_code', label='HSN/SAC',
@@ -272,11 +273,21 @@ def make_custom_fields(update=True):
'options': 'Supplier', 'options': 'Supplier',
'print_hide': 1 'print_hide': 1
}, },
{
'fieldname': 'transporter_name',
'label': 'Transporter Name',
'fieldtype': 'Data',
'insert_after': 'transporter',
'fetch_from': 'transporter.name',
'read_only': 1,
'print_hide': 1,
'translatable': 0
},
{ {
'fieldname': 'gst_transporter_id', 'fieldname': 'gst_transporter_id',
'label': 'GST Transporter ID', 'label': 'GST Transporter ID',
'fieldtype': 'Data', 'fieldtype': 'Data',
'insert_after': 'transporter', 'insert_after': 'transporter_name',
'fetch_from': 'transporter.gst_transporter_id', 'fetch_from': 'transporter.gst_transporter_id',
'print_hide': 1, 'print_hide': 1,
'translatable': 0 'translatable': 0
@@ -318,11 +329,18 @@ def make_custom_fields(update=True):
'insert_after': 'distance' 'insert_after': 'distance'
}, },
{ {
'fieldname': 'transporter_name', 'fieldname': 'transporter_address',
'label': 'Transporter Name', 'label': 'Transporter Address Name',
'fieldtype': 'Data', 'fieldtype': 'Link',
'insert_after': 'transporter_col_break', 'insert_after': 'transporter_col_break',
'fetch_from': 'transporter.name', 'options': 'Address',
'print_hide': 1
},
{
'fieldname': 'transporter_address_display',
'label': 'Transporter Address Preview',
'fieldtype': 'Small Text',
'insert_after': 'transporter_address',
'read_only': 1, 'read_only': 1,
'print_hide': 1, 'print_hide': 1,
'translatable': 0 'translatable': 0
@@ -333,7 +351,7 @@ def make_custom_fields(update=True):
'fieldtype': 'Select', 'fieldtype': 'Select',
'options': '\nRoad\nAir\nRail\nShip', 'options': '\nRoad\nAir\nRail\nShip',
'default': 'Road', 'default': 'Road',
'insert_after': 'transporter_name', 'insert_after': 'transporter_address_display',
'print_hide': 1, 'print_hide': 1,
'translatable': 0 'translatable': 0
}, },
@@ -369,13 +387,34 @@ def make_custom_fields(update=True):
'fieldname': 'ewaybill', 'fieldname': 'ewaybill',
'label': 'e-Way Bill No.', 'label': 'e-Way Bill No.',
'fieldtype': 'Data', 'fieldtype': 'Data',
'depends_on': 'eval:(doc.docstatus === 1)', 'depends_on': 'eval:((doc.docstatus === 1 || doc.ewaybill) && doc.eway_bill_cancelled === 0)',
'allow_on_submit': 1, 'allow_on_submit': 1,
'insert_after': 'tax_id', 'insert_after': 'tax_id',
'translatable': 0 'translatable': 0
} }
] ]
si_einvoice_fields = [
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='signed_qr_code', fieldtype='Code', options='JSON', hidden=1, no_copy=1, print_hide=1, read_only=1),
dict(fieldname='qrcode_image', label='QRCode', fieldtype='Attach Image', hidden=1, no_copy=1, print_hide=1, read_only=1)
]
custom_fields = { custom_fields = {
'Address': [ 'Address': [
dict(fieldname='gstin', label='Party GSTIN', fieldtype='Data', dict(fieldname='gstin', label='Party GSTIN', fieldtype='Data',
@@ -388,7 +427,7 @@ def make_custom_fields(update=True):
'Purchase Invoice': purchase_invoice_gst_category + invoice_gst_fields + purchase_invoice_itc_fields + purchase_invoice_gst_fields, 'Purchase Invoice': purchase_invoice_gst_category + invoice_gst_fields + purchase_invoice_itc_fields + purchase_invoice_gst_fields,
'Purchase Order': purchase_invoice_gst_fields, 'Purchase Order': purchase_invoice_gst_fields,
'Purchase Receipt': 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, '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,
'Sales Order': sales_invoice_gst_fields, 'Sales Order': sales_invoice_gst_fields,
'Tax Category': inter_state_gst_field, 'Tax Category': inter_state_gst_field,
@@ -546,13 +585,18 @@ def set_salary_components(docs):
def set_tax_withholding_category(company): def set_tax_withholding_category(company):
accounts = [] accounts = []
fiscal_year = None
abbr = frappe.get_value("Company", company, "abbr") abbr = frappe.get_value("Company", company, "abbr")
tds_account = frappe.get_value("Account", 'TDS Payable - {0}'.format(abbr), 'name') tds_account = frappe.get_value("Account", 'TDS Payable - {0}'.format(abbr), 'name')
if company and tds_account: if company and tds_account:
accounts = [dict(company=company, account=tds_account)] accounts = [dict(company=company, account=tds_account)]
fiscal_year = get_fiscal_year(today(), company=company)[0] try:
fiscal_year = get_fiscal_year(today(), verbose=0, company=company)[0]
except FiscalYearError:
pass
docs = get_tds_details(accounts, fiscal_year) docs = get_tds_details(accounts, fiscal_year)
for d in docs: for d in docs:
@@ -567,11 +611,14 @@ def set_tax_withholding_category(company):
if accounts: if accounts:
doc.append("accounts", accounts[0]) doc.append("accounts", accounts[0])
# if fiscal year don't match with any of the already entered data, append rate row if fiscal_year:
fy_exist = [k for k in doc.get('rates') if k.get('fiscal_year')==fiscal_year] # if fiscal year don't match with any of the already entered data, append rate row
if not fy_exist: fy_exist = [k for k in doc.get('rates') if k.get('fiscal_year')==fiscal_year]
doc.append("rates", d.get('rates')[0]) if not fy_exist:
doc.append("rates", d.get('rates')[0])
doc.flags.ignore_permissions = True
doc.flags.ignore_mandatory = True
doc.save() doc.save()
def set_tds_account(docs, company): def set_tds_account(docs, company):

View File

@@ -9,6 +9,9 @@ erpnext.setup_auto_gst_taxation = (doctype) => {
tax_category: function(frm) { tax_category: function(frm) {
frm.trigger('get_tax_template'); frm.trigger('get_tax_template');
}, },
customer_address: function(frm) {
frm.trigger('get_tax_template');
},
get_tax_template: function(frm) { get_tax_template: function(frm) {
if (!frm.doc.company) return; if (!frm.doc.company) return;
@@ -16,6 +19,7 @@ erpnext.setup_auto_gst_taxation = (doctype) => {
'shipping_address': frm.doc.shipping_address || '', 'shipping_address': frm.doc.shipping_address || '',
'shipping_address_name': frm.doc.shipping_address_name || '', 'shipping_address_name': frm.doc.shipping_address_name || '',
'customer_address': frm.doc.customer_address || '', 'customer_address': frm.doc.customer_address || '',
'supplier_address': frm.doc.supplier_address,
'customer': frm.doc.customer, 'customer': frm.doc.customer,
'supplier': frm.doc.supplier, 'supplier': frm.doc.supplier,
'supplier_gstin': frm.doc.supplier_gstin, 'supplier_gstin': frm.doc.supplier_gstin,
@@ -28,12 +32,15 @@ erpnext.setup_auto_gst_taxation = (doctype) => {
args: { args: {
party_details: JSON.stringify(party_details), party_details: JSON.stringify(party_details),
doctype: frm.doc.doctype, doctype: frm.doc.doctype,
company: frm.doc.company, company: frm.doc.company
return_taxes: 1
}, },
callback: function(r) { callback: function(r) {
if(r.message) { if(r.message) {
frm.set_value('taxes_and_charges', r.message.taxes_and_charges); frm.set_value('taxes_and_charges', r.message.taxes_and_charges);
frm.set_value('place_of_supply', r.message.place_of_supply);
} else if (frm.doc.is_internal_supplier || frm.doc.is_internal_customer) {
frm.set_value('taxes_and_charges', '');
frm.set_value('taxes', []);
} }
} }
}); });

View File

@@ -12,6 +12,8 @@ from erpnext.regional.india import number_state_mapping
from six import string_types from six import string_types
from erpnext.accounts.general_ledger import make_gl_entries from erpnext.accounts.general_ledger import make_gl_entries
from erpnext.accounts.utils import get_account_currency from erpnext.accounts.utils import get_account_currency
from frappe.contacts.doctype.address.address import get_address_display
from frappe.model.utils import get_fetch_values
def validate_gstin_for_india(doc, method): def validate_gstin_for_india(doc, method):
if hasattr(doc, 'gst_state') and doc.gst_state: if hasattr(doc, 'gst_state') and doc.gst_state:
@@ -51,6 +53,13 @@ def validate_gstin_for_india(doc, method):
frappe.throw(_("Invalid GSTIN! First 2 digits of GSTIN should match with State number {0}.") frappe.throw(_("Invalid GSTIN! First 2 digits of GSTIN should match with State number {0}.")
.format(doc.gst_state_number)) .format(doc.gst_state_number))
def validate_tax_category(doc, method):
if doc.get('gst_state') and frappe.db.get_value('Tax Category', {'gst_state': doc.gst_state, 'is_inter_state': doc.is_inter_state}):
if doc.is_inter_state:
frappe.throw(_("Inter State tax category for GST State {0} already exists").format(doc.gst_state))
else:
frappe.throw(_("Intra State tax category for GST State {0} already exists").format(doc.gst_state))
def update_gst_category(doc, method): def update_gst_category(doc, method):
for link in doc.links: for link in doc.links:
if link.link_doctype in ['Customer', 'Supplier']: if link.link_doctype in ['Customer', 'Supplier']:
@@ -85,8 +94,7 @@ def validate_gstin_check_digit(gstin, label='GSTIN'):
total += digit total += digit
factor = 2 if factor == 1 else 1 factor = 2 if factor == 1 else 1
if gstin[-1] != code_point_chars[((mod - (total % mod)) % mod)]: if gstin[-1] != code_point_chars[((mod - (total % mod)) % mod)]:
frappe.throw(_("""Invalid {0}! The check digit validation has failed. frappe.throw(_("""Invalid {0}! The check digit validation has failed. Please ensure you've typed the {0} correctly.""").format(label))
Please ensure you've typed the {0} correctly.""".format(label)))
def get_itemised_tax_breakup_header(item_doctype, tax_accounts): def get_itemised_tax_breakup_header(item_doctype, tax_accounts):
if frappe.get_meta(item_doctype).has_field('gst_hsn_code'): if frappe.get_meta(item_doctype).has_field('gst_hsn_code'):
@@ -130,6 +138,30 @@ def get_itemised_tax_breakup_data(doc, account_wise=False):
def set_place_of_supply(doc, method=None): def set_place_of_supply(doc, method=None):
doc.place_of_supply = get_place_of_supply(doc, doc.doctype) doc.place_of_supply = get_place_of_supply(doc, doc.doctype)
def set_transporter_address(doc, method=None):
country = frappe.get_cached_value('Company', doc.company, 'country')
if country != 'India':
return
if doc.get("transporter_address"):
# once supplier is set, address can be selected from multiple transporter addresses
doc.transporter_address_display = get_address_display(doc.get("transporter_address"))
return
transporter_address = frappe.db.get_value("Dynamic Link", {
'link_doctype': 'Supplier',
'link_name': doc.get('transporter'),
'parenttype': 'Address'
}, "parent")
if not transporter_address:
doc.transporter_address = ""
doc.transporter_address_display = ""
return
doc.transporter_address = transporter_address
doc.transporter_address_display = get_address_display(transporter_address)
# don't remove this function it is used in tests # don't remove this function it is used in tests
def test_method(): def test_method():
'''test function''' '''test function'''
@@ -149,24 +181,31 @@ def get_place_of_supply(party_details, doctype):
return cstr(address.gst_state_number) + "-" + cstr(address.gst_state) return cstr(address.gst_state_number) + "-" + cstr(address.gst_state)
@frappe.whitelist() @frappe.whitelist()
def get_regional_address_details(party_details, doctype, company, return_taxes=None): def get_regional_address_details(party_details, doctype, company):
if isinstance(party_details, string_types): if isinstance(party_details, string_types):
party_details = json.loads(party_details) party_details = json.loads(party_details)
party_details = frappe._dict(party_details) party_details = frappe._dict(party_details)
update_party_details(party_details, doctype)
party_details.place_of_supply = get_place_of_supply(party_details, doctype) party_details.place_of_supply = get_place_of_supply(party_details, doctype)
if is_internal_transfer(party_details, doctype):
party_details.taxes_and_charges = ''
party_details.taxes = ''
return party_details
if doctype in ("Sales Invoice", "Delivery Note", "Sales Order"): if doctype in ("Sales Invoice", "Delivery Note", "Sales Order"):
master_doctype = "Sales Taxes and Charges Template" master_doctype = "Sales Taxes and Charges Template"
get_tax_template_for_sez(party_details, master_doctype, company, 'Customer') get_tax_template_for_sez(party_details, master_doctype, company, 'Customer')
get_tax_template_based_on_category(master_doctype, company, party_details) get_tax_template_based_on_category(master_doctype, company, party_details)
if party_details.get('taxes_and_charges') and return_taxes: if party_details.get('taxes_and_charges'):
return party_details return party_details
if not party_details.company_gstin: if not party_details.company_gstin:
return return party_details
elif doctype in ("Purchase Invoice", "Purchase Order", "Purchase Receipt"): elif doctype in ("Purchase Invoice", "Purchase Order", "Purchase Receipt"):
master_doctype = "Purchase Taxes and Charges Template" master_doctype = "Purchase Taxes and Charges Template"
@@ -174,15 +213,15 @@ def get_regional_address_details(party_details, doctype, company, return_taxes=N
get_tax_template_for_sez(party_details, master_doctype, company, 'Supplier') get_tax_template_for_sez(party_details, master_doctype, company, 'Supplier')
get_tax_template_based_on_category(master_doctype, company, party_details) get_tax_template_based_on_category(master_doctype, company, party_details)
if party_details.get('taxes_and_charges') and return_taxes: if party_details.get('taxes_and_charges'):
return party_details return party_details
if not party_details.supplier_gstin: if not party_details.supplier_gstin:
return return party_details
if not party_details.place_of_supply: return if not party_details.place_of_supply: return party_details
if not party_details.company_gstin: return if not party_details.company_gstin: return party_details
if ((doctype in ("Sales Invoice", "Delivery Note", "Sales Order") and party_details.company_gstin if ((doctype in ("Sales Invoice", "Delivery Note", "Sales Order") and party_details.company_gstin
and party_details.company_gstin[:2] != party_details.place_of_supply[:2]) or (doctype in ("Purchase Invoice", and party_details.company_gstin[:2] != party_details.place_of_supply[:2]) or (doctype in ("Purchase Invoice",
@@ -192,12 +231,27 @@ def get_regional_address_details(party_details, doctype, company, return_taxes=N
default_tax = get_tax_template(master_doctype, company, 0, party_details.company_gstin[:2]) default_tax = get_tax_template(master_doctype, company, 0, party_details.company_gstin[:2])
if not default_tax: if not default_tax:
return return party_details
party_details["taxes_and_charges"] = default_tax party_details["taxes_and_charges"] = default_tax
party_details.taxes = get_taxes_and_charges(master_doctype, default_tax) party_details.taxes = get_taxes_and_charges(master_doctype, default_tax)
if return_taxes: return party_details
return party_details
def update_party_details(party_details, doctype):
for address_field in ['shipping_address', 'company_address', 'supplier_address', 'shipping_address_name', 'customer_address']:
if party_details.get(address_field):
party_details.update(get_fetch_values(doctype, address_field, party_details.get(address_field)))
def is_internal_transfer(party_details, doctype):
if doctype in ("Sales Invoice", "Delivery Note", "Sales Order"):
destination_gstin = party_details.company_gstin
elif doctype in ("Purchase Invoice", "Purchase Order", "Purchase Receipt"):
destination_gstin = party_details.supplier_gstin
if party_details.gstin == destination_gstin:
return True
else:
False
def get_tax_template_based_on_category(master_doctype, company, party_details): def get_tax_template_based_on_category(master_doctype, company, party_details):
if not party_details.get('tax_category'): if not party_details.get('tax_category'):
@@ -500,7 +554,7 @@ def get_address_details(data, doc, company_address, billing_address):
data.transType = 1 data.transType = 1
data.actualToStateCode = data.toStateCode data.actualToStateCode = data.toStateCode
shipping_address = billing_address shipping_address = billing_address
if doc.gst_category == 'SEZ': if doc.gst_category == 'SEZ':
data.toStateCode = 99 data.toStateCode = 99
@@ -737,4 +791,4 @@ def make_regional_gl_entries(gl_entries, doc):
}, account_currency, item=tax) }, account_currency, item=tax)
) )
return gl_entries return gl_entries

View File

@@ -255,15 +255,16 @@ class Gstr1Report(object):
for item_code, tax_amounts in item_wise_tax_detail.items(): for item_code, tax_amounts in item_wise_tax_detail.items():
tax_rate = tax_amounts[0] tax_rate = tax_amounts[0]
if cgst_or_sgst: if tax_rate:
tax_rate *= 2 if cgst_or_sgst:
if parent not in self.cgst_sgst_invoices: tax_rate *= 2
self.cgst_sgst_invoices.append(parent) if parent not in self.cgst_sgst_invoices:
self.cgst_sgst_invoices.append(parent)
rate_based_dict = self.items_based_on_tax_rate\ rate_based_dict = self.items_based_on_tax_rate\
.setdefault(parent, {}).setdefault(tax_rate, []) .setdefault(parent, {}).setdefault(tax_rate, [])
if item_code not in rate_based_dict: if item_code not in rate_based_dict:
rate_based_dict.append(item_code) rate_based_dict.append(item_code)
except ValueError: except ValueError:
continue continue
if unidentified_gst_accounts: if unidentified_gst_accounts:

View File

@@ -8,7 +8,7 @@ frappe.ui.form.on("Sales Order", {
frm.custom_make_buttons = { frm.custom_make_buttons = {
'Delivery Note': 'Delivery Note', 'Delivery Note': 'Delivery Note',
'Pick List': 'Pick List', 'Pick List': 'Pick List',
'Sales Invoice': 'Invoice', 'Sales Invoice': 'Sales Invoice',
'Material Request': 'Material Request', 'Material Request': 'Material Request',
'Purchase Order': 'Purchase Order', 'Purchase Order': 'Purchase Order',
'Project': 'Project', 'Project': 'Project',

View File

@@ -155,7 +155,7 @@ erpnext.pos.PointOfSale = class PointOfSale {
var me = this; var me = this;
if (this.frm.doc.customer) { if (this.frm.doc.customer) {
frappe.call({ frappe.call({
method: "erpnext.accounts.doctype.loyalty_program.loyalty_program.get_loyalty_program_details", method: "erpnext.accounts.doctype.loyalty_program.loyalty_program.get_loyalty_program_details_with_points",
args: { args: {
"customer": me.frm.doc.customer, "customer": me.frm.doc.customer,
"expiry_date": me.frm.doc.posting_date, "expiry_date": me.frm.doc.posting_date,

View File

@@ -10,8 +10,8 @@ from frappe.utils.nestedset import get_descendants_of
def execute(filters=None): def execute(filters=None):
filters = frappe._dict(filters or {}) filters = frappe._dict(filters or {})
if filters.from_date > filters.to_date: if filters.from_date > filters.to_date:
frappe.throw(_('From Date cannot be greater than To Date')) frappe.throw(_("From Date cannot be greater than To Date"))
columns = get_columns(filters) columns = get_columns(filters)
data = get_data(filters) data = get_data(filters)
return columns, data return columns, data
@@ -145,14 +145,16 @@ def get_data(filters):
company_list.append(filters.get("company")) company_list.append(filters.get("company"))
customer_details = get_customer_details() customer_details = get_customer_details()
item_details = get_item_details()
sales_order_records = get_sales_order_details(company_list, filters) sales_order_records = get_sales_order_details(company_list, filters)
for record in sales_order_records: for record in sales_order_records:
customer_record = customer_details.get(record.customer) customer_record = customer_details.get(record.customer)
item_record = item_details.get(record.item_code)
row = { row = {
"item_code": record.item_code, "item_code": record.item_code,
"item_name": record.item_name, "item_name": item_record.item_name,
"item_group": record.item_group, "item_group": item_record.item_group,
"description": record.description, "description": record.description,
"quantity": record.qty, "quantity": record.qty,
"uom": record.uom, "uom": record.uom,
@@ -187,8 +189,8 @@ def get_conditions(filters):
return conditions return conditions
def get_customer_details(): def get_customer_details():
details = frappe.get_all('Customer', details = frappe.get_all("Customer",
fields=['name', 'customer_name', "customer_group"]) fields=["name", "customer_name", "customer_group"])
customer_details = {} customer_details = {}
for d in details: for d in details:
customer_details.setdefault(d.name, frappe._dict({ customer_details.setdefault(d.name, frappe._dict({
@@ -197,15 +199,25 @@ def get_customer_details():
})) }))
return customer_details return customer_details
def get_item_details():
details = frappe.db.get_all("Item",
fields=["item_code", "item_name", "item_group"])
item_details = {}
for d in details:
item_details.setdefault(d.item_code, frappe._dict({
"item_name": d.item_name,
"item_group": d.item_group
}))
return item_details
def get_sales_order_details(company_list, filters): def get_sales_order_details(company_list, filters):
conditions = get_conditions(filters) conditions = get_conditions(filters)
return frappe.db.sql(""" return frappe.db.sql("""
SELECT SELECT
so_item.item_code, so_item.item_name, so_item.item_group, so_item.item_code, so_item.description, so_item.qty,
so_item.description, so_item.qty, so_item.uom, so_item.uom, so_item.base_rate, so_item.base_amount,
so_item.base_rate, so_item.base_amount, so.name, so.name, so.transaction_date, so.customer,so.territory,
so.transaction_date, so.customer, so.territory,
so.project, so_item.delivered_qty, so.project, so_item.delivered_qty,
so_item.billed_amt, so.company so_item.billed_amt, so.company
FROM FROM

View File

@@ -26,7 +26,8 @@ def delete_company_transactions(company_name):
tabDocField where fieldtype='Link' and options='Company'"""): tabDocField where fieldtype='Link' and options='Company'"""):
if doctype not in ("Account", "Cost Center", "Warehouse", "Budget", if doctype not in ("Account", "Cost Center", "Warehouse", "Budget",
"Party Account", "Employee", "Sales Taxes and Charges Template", "Party Account", "Employee", "Sales Taxes and Charges Template",
"Purchase Taxes and Charges Template", "POS Profile", 'BOM'): "Purchase Taxes and Charges Template", "POS Profile", 'BOM',
"Item default", "Customer", "Supplier"):
delete_for_doctype(doctype, company_name) delete_for_doctype(doctype, company_name)
# reset company values # reset company values

View File

@@ -427,6 +427,7 @@ class TestMaterialRequest(unittest.TestCase):
"basic_rate": 1.0 "basic_rate": 1.0
}) })
se_doc.get("items")[1].update({ se_doc.get("items")[1].update({
"item_code": "_Test Item Home Desktop 100",
"qty": 3.0, "qty": 3.0,
"transfer_qty": 3.0, "transfer_qty": 3.0,
"s_warehouse": "_Test Warehouse 1 - _TC", "s_warehouse": "_Test Warehouse 1 - _TC",
@@ -537,7 +538,7 @@ class TestMaterialRequest(unittest.TestCase):
mr = make_material_request(item_code='_Test FG Item', material_request_type='Manufacture', mr = make_material_request(item_code='_Test FG Item', material_request_type='Manufacture',
uom="_Test UOM 1", conversion_factor=12) uom="_Test UOM 1", conversion_factor=12)
requested_qty = self._get_requested_qty('_Test FG Item', '_Test Warehouse - _TC') requested_qty = self._get_requested_qty('_Test FG Item', '_Test Warehouse - _TC')
self.assertEqual(requested_qty, existing_requested_qty + 120) self.assertEqual(requested_qty, existing_requested_qty + 120)

View File

@@ -453,7 +453,7 @@ class TestPurchaseReceipt(unittest.TestCase):
se = make_stock_entry(item_code=item_code, target="_Test Warehouse - _TC", qty=1, se = make_stock_entry(item_code=item_code, target="_Test Warehouse - _TC", qty=1,
serial_no=serial_no, basic_rate=100, do_not_submit=True) serial_no=serial_no, basic_rate=100, do_not_submit=True)
self.assertRaises(SerialNoDuplicateError, se.submit) se.submit()
def test_auto_asset_creation(self): def test_auto_asset_creation(self):
asset_item = "Test Asset Item" asset_item = "Test Asset Item"

View File

@@ -52,10 +52,24 @@ class QualityInspection(Document):
doctype = 'Stock Entry Detail' doctype = 'Stock Entry Detail'
if self.reference_type and self.reference_name: if self.reference_type and self.reference_name:
frappe.db.sql("""update `tab{child_doc}` t1, `tab{parent_doc}` t2 conditions = ""
set t1.quality_inspection = %s, t2.modified = %s if self.batch_no and self.docstatus == 1:
where t1.parent = %s and t1.item_code = %s and t1.parent = t2.name""" conditions += " and t1.batch_no = '%s'"%(self.batch_no)
.format(parent_doc=self.reference_type, child_doc=doctype),
if self.docstatus == 2: # if cancel, then remove qi link wherever same name
conditions += " and t1.quality_inspection = '%s'"%(self.name)
frappe.db.sql("""
UPDATE
`tab{child_doc}` t1, `tab{parent_doc}` t2
SET
t1.quality_inspection = %s, t2.modified = %s
WHERE
t1.parent = %s
and t1.item_code = %s
and t1.parent = t2.name
{conditions}
""".format(parent_doc=self.reference_type, child_doc=doctype, conditions=conditions),
(quality_inspection, self.modified, self.reference_name, self.item_code)) (quality_inspection, self.modified, self.reference_name, self.item_code))
@frappe.whitelist() @frappe.whitelist()

View File

@@ -5,7 +5,7 @@ from __future__ import unicode_literals
import frappe import frappe
from frappe.model.naming import make_autoname from frappe.model.naming import make_autoname
from frappe.utils import cint, cstr, flt, add_days, nowdate, getdate from frappe.utils import cint, cstr, flt, add_days, nowdate, getdate, get_link_to_form
from erpnext.stock.get_item_details import get_reserved_qty_for_so from erpnext.stock.get_item_details import get_reserved_qty_for_so
from frappe import _, ValidationError from frappe import _, ValidationError
@@ -238,7 +238,7 @@ def validate_serial_no(sle, item_det):
for serial_no in serial_nos: for serial_no in serial_nos:
if frappe.db.exists("Serial No", serial_no): if frappe.db.exists("Serial No", serial_no):
sr = frappe.db.get_value("Serial No", serial_no, ["name", "item_code", "batch_no", "sales_order", sr = frappe.db.get_value("Serial No", serial_no, ["name", "item_code", "batch_no", "sales_order",
"delivery_document_no", "delivery_document_type", "warehouse", "delivery_document_no", "delivery_document_type", "warehouse", "purchase_document_type",
"purchase_document_no", "company"], as_dict=1) "purchase_document_no", "company"], as_dict=1)
if sr.item_code!=sle.item_code: if sr.item_code!=sle.item_code:
@@ -246,9 +246,10 @@ def validate_serial_no(sle, item_det):
frappe.throw(_("Serial No {0} does not belong to Item {1}").format(serial_no, frappe.throw(_("Serial No {0} does not belong to Item {1}").format(serial_no,
sle.item_code), SerialNoItemError) sle.item_code), SerialNoItemError)
if cint(sle.actual_qty) > 0 and has_duplicate_serial_no(sr, sle): if cint(sle.actual_qty) > 0 and has_serial_no_exists(sr, sle):
frappe.throw(_("Serial No {0} has already been received").format(serial_no), doc_name = frappe.bold(get_link_to_form(sr.purchase_document_type, sr.purchase_document_no))
SerialNoDuplicateError) frappe.throw(_("Serial No {0} has already been received in the {1} #{2}")
.format(frappe.bold(serial_no), sr.purchase_document_type, doc_name), SerialNoDuplicateError)
if (sr.delivery_document_no and sle.voucher_type not in ['Stock Entry', 'Stock Reconciliation'] if (sr.delivery_document_no and sle.voucher_type not in ['Stock Entry', 'Stock Reconciliation']
and sle.voucher_type == sr.delivery_document_type): and sle.voucher_type == sr.delivery_document_type):
@@ -339,7 +340,7 @@ def validate_so_serial_no(sr, sales_order,):
only deliver reserved {1} against {0}. Serial No {2} cannot only deliver reserved {1} against {0}. Serial No {2} cannot
be delivered""").format(sales_order, sr.item_code, sr.name)) be delivered""").format(sales_order, sr.item_code, sr.name))
def has_duplicate_serial_no(sn, sle): def has_serial_no_exists(sn, sle):
if (sn.warehouse and not sle.skip_serial_no_validaiton if (sn.warehouse and not sle.skip_serial_no_validaiton
and sle.voucher_type != 'Stock Reconciliation'): and sle.voucher_type != 'Stock Reconciliation'):
return True return True
@@ -349,12 +350,13 @@ def has_duplicate_serial_no(sn, sle):
status = False status = False
if sn.purchase_document_no: if sn.purchase_document_no:
if sle.voucher_type in ['Purchase Receipt', 'Stock Entry', "Purchase Invoice"] and \ if (sle.voucher_type in ['Purchase Receipt', 'Stock Entry', "Purchase Invoice"] and
sn.delivery_document_type not in ['Purchase Receipt', 'Stock Entry', "Purchase Invoice"]: sn.delivery_document_type not in ['Purchase Receipt', 'Stock Entry', "Purchase Invoice"]):
status = True status = True
if status and sle.voucher_type == 'Stock Entry' and \ # If status is receipt then system will allow to in-ward the delivered serial no
frappe.db.get_value('Stock Entry', sle.voucher_no, 'purpose') != 'Material Receipt': if (status and sle.voucher_type == 'Stock Entry' and frappe.db.get_value('Stock Entry',
sle.voucher_no, 'purpose') in ("Material Receipt", "Material Transfer")):
status = False status = False
return status return status
@@ -408,7 +410,7 @@ def auto_make_serial_nos(args):
if is_new: if is_new:
created_numbers.append(sr.name) created_numbers.append(sr.name)
form_links = list(map(lambda d: frappe.utils.get_link_to_form('Serial No', d), created_numbers)) form_links = list(map(lambda d: get_link_to_form('Serial No', d), created_numbers))
if len(form_links) == 1: if len(form_links) == 1:
frappe.msgprint(_("Serial No {0} created").format(form_links[0])) frappe.msgprint(_("Serial No {0} created").format(form_links[0]))
elif len(form_links) > 0: elif len(form_links) > 0:

View File

@@ -322,11 +322,12 @@ frappe.ui.form.on('Stock Entry', {
method: "erpnext.stock.get_item_details.get_serial_no", method: "erpnext.stock.get_item_details.get_serial_no",
args: {"args": args}, args: {"args": args},
callback: function(r) { callback: function(r) {
if (!r.exe && r.message){ if (!r.exe && r.message) {
frappe.model.set_value(cdt, cdn, "serial_no", r.message); frappe.model.set_value(cdt, cdn, "serial_no", r.message);
}
if (callback) { if (callback) {
callback(); callback();
}
} }
} }
}); });
@@ -736,6 +737,10 @@ erpnext.stock.StockEntry = erpnext.stock.StockController.extend({
} }
}, },
fg_completed_qty: function() {
this.get_items();
},
get_items: function() { get_items: function() {
var me = this; var me = this;
if(!this.frm.doc.fg_completed_qty || !this.frm.doc.bom_no) if(!this.frm.doc.fg_completed_qty || !this.frm.doc.bom_no)
@@ -745,6 +750,7 @@ erpnext.stock.StockEntry = erpnext.stock.StockController.extend({
// if work order / bom is mentioned, get items // if work order / bom is mentioned, get items
return this.frm.call({ return this.frm.call({
doc: me.frm.doc, doc: me.frm.doc,
freeze: true,
method: "get_items", method: "get_items",
callback: function(r) { callback: function(r) {
if(!r.exc) refresh_field("items"); if(!r.exc) refresh_field("items");

View File

@@ -83,7 +83,7 @@ class StockEntry(StockController):
self.set_incoming_rate() self.set_incoming_rate()
self.validate_serialized_batch() self.validate_serialized_batch()
self.set_actual_qty() self.set_actual_qty()
self.calculate_rate_and_amount() self.calculate_rate_and_amount(update_finished_item_rate=False)
def on_submit(self): def on_submit(self):
@@ -460,7 +460,7 @@ class StockEntry(StockController):
scrap_material_cost += flt(d.basic_amount) scrap_material_cost += flt(d.basic_amount)
number_of_fg_items = len([t.t_warehouse for t in self.get("items") if t.t_warehouse]) number_of_fg_items = len([t.t_warehouse for t in self.get("items") if t.t_warehouse])
if (fg_basic_rate == 0.0 and number_of_fg_items == 1) or update_finished_item_rate: if number_of_fg_items == 1 or update_finished_item_rate:
self.set_basic_rate_for_finished_goods(raw_material_cost, scrap_material_cost) self.set_basic_rate_for_finished_goods(raw_material_cost, scrap_material_cost)
def get_args_for_incoming_rate(self, item): def get_args_for_incoming_rate(self, item):
@@ -488,6 +488,8 @@ class StockEntry(StockController):
if self.purpose in ["Manufacture", "Repack"]: if self.purpose in ["Manufacture", "Repack"]:
for d in self.get("items"): for d in self.get("items"):
if d.set_basic_rate_manually: continue
if (d.transfer_qty and (d.bom_no or d.t_warehouse) if (d.transfer_qty and (d.bom_no or d.t_warehouse)
and (getattr(self, "pro_doc", frappe._dict()).scrap_warehouse != d.t_warehouse)): and (getattr(self, "pro_doc", frappe._dict()).scrap_warehouse != d.t_warehouse)):
@@ -499,7 +501,7 @@ class StockEntry(StockController):
if raw_material_cost and self.purpose == "Manufacture": if raw_material_cost and self.purpose == "Manufacture":
d.basic_rate = flt((raw_material_cost - scrap_material_cost) / flt(d.transfer_qty), d.precision("basic_rate")) d.basic_rate = flt((raw_material_cost - scrap_material_cost) / flt(d.transfer_qty), d.precision("basic_rate"))
d.basic_amount = flt((raw_material_cost - scrap_material_cost), d.precision("basic_amount")) d.basic_amount = flt((raw_material_cost - scrap_material_cost), d.precision("basic_amount"))
elif self.purpose == "Repack" and total_fg_qty and not d.set_basic_rate_manually: elif self.purpose == "Repack" and total_fg_qty:
d.basic_rate = flt(raw_material_cost) / flt(total_fg_qty) d.basic_rate = flt(raw_material_cost) / flt(total_fg_qty)
d.basic_amount = d.basic_rate * flt(d.qty) d.basic_amount = d.basic_rate * flt(d.qty)
@@ -1010,31 +1012,31 @@ class StockEntry(StockController):
wo = frappe.get_doc("Work Order", self.work_order) wo = frappe.get_doc("Work Order", self.work_order)
wo_items = frappe.get_all('Work Order Item', wo_items = frappe.get_all('Work Order Item',
filters={'parent': self.work_order}, filters={'parent': self.work_order},
fields=["item_code", "required_qty", "consumed_qty"] fields=["item_code", "required_qty", "consumed_qty", "transferred_qty", "source_warehouse"]
) )
work_order_qty = wo.material_transferred_for_manufacturing or wo.qty
for item in wo_items: for item in wo_items:
qty = item.required_qty
item_account_details = get_item_defaults(item.item_code, self.company) item_account_details = get_item_defaults(item.item_code, self.company)
# Take into account consumption if there are any. # Take into account consumption if there are any.
if self.purpose == 'Manufacture':
req_qty_each = flt(item.required_qty / wo.qty) wo_item_qty = item.transferred_qty or item.required_qty
if (flt(item.consumed_qty) != 0):
remaining_qty = flt(item.consumed_qty) - (flt(wo.produced_qty) * req_qty_each) req_qty_each = (
exhaust_qty = req_qty_each * wo.produced_qty (flt(wo_item_qty) - flt(item.consumed_qty)) /
if remaining_qty > exhaust_qty : (flt(work_order_qty) - flt(wo.produced_qty))
if (remaining_qty/(req_qty_each * flt(self.fg_completed_qty))) >= 1: )
qty =0
else: qty = req_qty_each * flt(self.fg_completed_qty)
qty = (req_qty_each * flt(self.fg_completed_qty)) - remaining_qty
else:
qty = req_qty_each * flt(self.fg_completed_qty)
if qty > 0: if qty > 0:
from_warehouse = wo.wip_warehouse
if wo.skip_transfer and not wo.from_wip_warehouse:
from_warehouse = item.source_warehouse
self.add_to_stock_entry_detail({ self.add_to_stock_entry_detail({
item.item_code: { item.item_code: {
"from_warehouse": wo.wip_warehouse, "from_warehouse": from_warehouse,
"to_warehouse": "", "to_warehouse": "",
"qty": qty, "qty": qty,
"item_name": item.item_name, "item_name": item.item_name,
@@ -1113,8 +1115,10 @@ class StockEntry(StockController):
else: else:
qty = (req_qty_each * flt(self.fg_completed_qty)) - remaining_qty qty = (req_qty_each * flt(self.fg_completed_qty)) - remaining_qty
else: else:
qty = req_qty_each * flt(self.fg_completed_qty) if self.flags.backflush_based_on == "Material Transferred for Manufacture":
qty = (item.qty/trans_qty) * flt(self.fg_completed_qty)
else:
qty = req_qty_each * flt(self.fg_completed_qty)
elif backflushed_materials.get(item.item_code): elif backflushed_materials.get(item.item_code):
for d in backflushed_materials.get(item.item_code): for d in backflushed_materials.get(item.item_code):
@@ -1122,6 +1126,10 @@ class StockEntry(StockController):
if (qty > req_qty): if (qty > req_qty):
qty = (qty/trans_qty) * flt(self.fg_completed_qty) qty = (qty/trans_qty) * flt(self.fg_completed_qty)
if consumed_qty and frappe.db.get_single_value("Manufacturing Settings",
"material_consumption"):
qty -= consumed_qty
if cint(frappe.get_cached_value('UOM', item.stock_uom, 'must_be_whole_number')): if cint(frappe.get_cached_value('UOM', item.stock_uom, 'must_be_whole_number')):
qty = frappe.utils.ceil(qty) qty = frappe.utils.ceil(qty)
@@ -1243,9 +1251,8 @@ class StockEntry(StockController):
mreq_item = frappe.db.get_value("Material Request Item", mreq_item = frappe.db.get_value("Material Request Item",
{"name": item.material_request_item, "parent": item.material_request}, {"name": item.material_request_item, "parent": item.material_request},
["item_code", "warehouse", "idx"], as_dict=True) ["item_code", "warehouse", "idx"], as_dict=True)
if mreq_item.item_code != item.item_code or \ if mreq_item.item_code != item.item_code:
mreq_item.warehouse != (item.s_warehouse if self.purpose== "Material Issue" else item.t_warehouse): frappe.throw(_("Item for row {0} does not match Material Request").format(item.idx),
frappe.throw(_("Item or Warehouse for row {0} does not match Material Request").format(item.idx),
frappe.MappingMismatchError) frappe.MappingMismatchError)
def validate_batch(self): def validate_batch(self):

View File

@@ -494,7 +494,7 @@
}, },
{ {
"default": "0", "default": "0",
"depends_on": "eval:parent.purpose===\"Repack\" && doc.t_warehouse", "depends_on": "eval:in_list([\"Repack\", \"Manufacture\"], parent.purpose) && doc.t_warehouse",
"fieldname": "set_basic_rate_manually", "fieldname": "set_basic_rate_manually",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Set Basic Rate Manually" "label": "Set Basic Rate Manually"
@@ -502,7 +502,7 @@
], ],
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"modified": "2020-09-04 12:12:35.668198", "modified": "2021-01-05 15:05:04.891447",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Stock Entry Detail", "name": "Stock Entry Detail",

View File

@@ -216,7 +216,7 @@ class StockReconciliation(StockController):
if row.qty and not row.valuation_rate: if row.qty and not row.valuation_rate:
frappe.throw(_("Valuation Rate required for Item {0} at row {1}").format(row.item_code, row.idx)) frappe.throw(_("Valuation Rate required for Item {0} at row {1}").format(row.item_code, row.idx))
if ((previous_sle and row.qty == previous_sle.get("qty_after_transaction") if (not item.has_batch_no and (previous_sle and row.qty == previous_sle.get("qty_after_transaction")
and (row.valuation_rate == previous_sle.get("valuation_rate") or row.qty == 0)) and (row.valuation_rate == previous_sle.get("valuation_rate") or row.qty == 0))
or (not previous_sle and not row.qty)): or (not previous_sle and not row.qty)):
continue continue

View File

@@ -918,7 +918,7 @@
"issingle": 1, "issingle": 1,
"istable": 0, "istable": 0,
"max_attachments": 0, "max_attachments": 0,
"modified": "2019-07-04 01:19:07.738045", "modified": "2020-12-18 19:56:06.343314",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Stock Settings", "name": "Stock Settings",
@@ -949,7 +949,7 @@
"read_only_onload": 0, "read_only_onload": 0,
"show_name_in_global_search": 0, "show_name_in_global_search": 0,
"sort_order": "ASC", "sort_order": "ASC",
"track_changes": 0, "track_changes": 1,
"track_seen": 0, "track_seen": 0,
"track_views": 0 "track_views": 0
} }

View File

@@ -72,7 +72,9 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru
update_party_blanket_order(args, out) update_party_blanket_order(args, out)
get_price_list_rate(args, item, out) if not doc or cint(doc.get('is_return')) == 0:
# get price list rate only if the invoice is not a credit or debit note
get_price_list_rate(args, item, out)
if args.customer and cint(args.is_pos): if args.customer and cint(args.is_pos):
out.update(get_pos_profile_item_details(args.company, args)) out.update(get_pos_profile_item_details(args.company, args))

View File

@@ -61,9 +61,11 @@ frappe.query_reports["Batch-Wise Balance History"] = {
"options": "Batch", "options": "Batch",
"get_query": function() { "get_query": function() {
let item_code = frappe.query_report.get_filter_value('item_code'); let item_code = frappe.query_report.get_filter_value('item_code');
return { if (item_code) {
filters: { return {
"item": item_code filters: {
"item": item_code
}
} }
} }
} }

View File

@@ -186,7 +186,7 @@ class ItemConfigure {
this.dialog.$status_area.empty(); this.dialog.$status_area.empty();
} }
get_html_for_item_found({ filtered_items_count, filtered_items, exact_match, product_info }) { get_html_for_item_found({ filtered_items_count, filtered_items, exact_match, product_info, allow_items_not_in_stock }) {
const exact_match_message = __('1 exact match.'); const exact_match_message = __('1 exact match.');
const one_item = exact_match.length === 1 ? const one_item = exact_match.length === 1 ?
exact_match[0] : exact_match[0] :
@@ -194,7 +194,7 @@ class ItemConfigure {
filtered_items[0] : ''; filtered_items[0] : '';
// Allow Add to Cart if adding out of stock items enabled in Shopping Cart else check stock. // Allow Add to Cart if adding out of stock items enabled in Shopping Cart else check stock.
const in_stock = product_info.allow_items_not_in_stock ? 1 : product_info.in_stock; const in_stock = allow_items_not_in_stock ? 1 : product_info && product_info.in_stock;
const add_to_cart = `<a href data-action="btn_add_to_cart" data-item-code="${one_item}">${__('Add to cart')}</a>`; const add_to_cart = `<a href data-action="btn_add_to_cart" data-item-code="${one_item}">${__('Add to cart')}</a>`;
const product_action = in_stock ? add_to_cart : `<a style="color:#74808b;">${__('Not in Stock')}</a>`; const product_action = in_stock ? add_to_cart : `<a style="color:#74808b;">${__('Not in Stock')}</a>`;

View File

@@ -109,7 +109,7 @@ frappe.ready(() => {
reqd: 1 reqd: 1
}, },
{ {
label: __('Pin Code'), label: __('Postal Code'),
fieldname: 'pincode', fieldname: 'pincode',
fieldtype: 'Data' fieldtype: 'Data'
}, },

View File

@@ -20,10 +20,10 @@
{%- if (charge.tax_amount or doc.flags.print_taxes_with_zero_amount) and (not charge.included_in_print_rate or doc.flags.show_inclusive_tax_in_print) -%} {%- if (charge.tax_amount or doc.flags.print_taxes_with_zero_amount) and (not charge.included_in_print_rate or doc.flags.show_inclusive_tax_in_print) -%}
<div class="row"> <div class="row">
<div class="col-xs-5 {%- if doc.align_labels_right %} text-right{%- endif -%}"> <div class="col-xs-5 {%- if doc.align_labels_right %} text-right{%- endif -%}">
<label>{{ charge.get_formatted("description") }}</label></div> <label>{{ charge.get_formatted("description") }}</label>
</div>
<div class="col-xs-7 text-right"> <div class="col-xs-7 text-right">
{{ frappe.format_value(frappe.utils.flt(charge.tax_amount), {{ charge.get_formatted('tax_amount', doc) }}
table_meta.get_field("tax_amount"), doc, currency=doc.currency) }}
</div> </div>
</div> </div>
{%- endif -%} {%- endif -%}

View File

@@ -8,3 +8,4 @@ PyGithub==1.44.1
python-stdnum==1.12 python-stdnum==1.12
Unidecode==1.1.1 Unidecode==1.1.1
WooCommerce==2.1.1 WooCommerce==2.1.1
pycryptodome==3.9.8