diff --git a/erpnext/__init__.py b/erpnext/__init__.py index bb0baebc410..018c87aa172 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -5,7 +5,7 @@ import frappe from erpnext.hooks import regional_overrides from frappe.utils import getdate -__version__ = '12.16.2' +__version__ = '12.17.0' def get_default_company(user=None): '''Get default company for user''' diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py index 077a11a9be6..4702b8a434f 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.py +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py @@ -138,7 +138,8 @@ class GLEntry(Document): 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)) - 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 be used in transactions""").format(self.voucher_type, self.voucher_no, frappe.bold(self.cost_center))) diff --git a/erpnext/accounts/doctype/loyalty_program/loyalty_program.py b/erpnext/accounts/doctype/loyalty_program/loyalty_program.py index d2d852229e2..e2435defa97 100644 --- a/erpnext/accounts/doctype/loyalty_program/loyalty_program.py +++ b/erpnext/accounts/doctype/loyalty_program/loyalty_program.py @@ -38,7 +38,10 @@ def get_loyalty_details(customer, loyalty_program, expiry_date=None, company=Non @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): 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)) # sort collection rule, first item on list will be lowest min_spent diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index 3883637e363..0bd54cd6055 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -187,7 +187,7 @@ frappe.ui.form.on('Payment Entry', { frm.toggle_display("base_received_amount", ( 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 )); @@ -386,6 +386,8 @@ frappe.ui.form.on('Payment Entry', { set_account_currency_and_balance: function(frm, account, currency_field, balance_field, callback_function) { + + var company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency; if (frm.doc.posting_date && account) { frappe.call({ 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) 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; + } + } } }, () => { diff --git a/erpnext/accounts/doctype/sales_invoice/pos.py b/erpnext/accounts/doctype/sales_invoice/pos.py index 64c7f491bb7..140b63fd334 100755 --- a/erpnext/accounts/doctype/sales_invoice/pos.py +++ b/erpnext/accounts/doctype/sales_invoice/pos.py @@ -153,8 +153,8 @@ def update_multi_mode_option(doc, pos_profile): def get_mode_of_payment(doc): return frappe.db.sql(""" - select mpa.default_account, mpa.parent, mp.type as type - from `tabMode of Payment Account` mpa,`tabMode of Payment` mp + select mpa.default_account, mpa.parent, mp.type as type + from `tabMode of Payment Account` mpa,`tabMode of Payment` mp where mpa.parent = mp.name and mpa.company = %(company)s and mp.enabled = 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') order by priority desc, name desc""", {'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 @@ -434,10 +442,10 @@ def make_invoice(pos_profile, doc_list={}, email_queue_list={}, customers_list={ name_list.append(name) email_queue = make_email_queue(email_queue_list) - + if isinstance(pos_profile, string_types): pos_profile = json.loads(pos_profile) - + customers = get_customers_list(pos_profile) return { 'invoice': name_list, diff --git a/erpnext/accounts/doctype/sales_invoice/regional/india.js b/erpnext/accounts/doctype/sales_invoice/regional/india.js index ba672c9368d..ca15626e19c 100644 --- a/erpnext/accounts/doctype/sales_invoice/regional/india.js +++ b/erpnext/accounts/doctype/sales_invoice/regional/india.js @@ -14,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) { return { filters: { diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 2ff5dd70c0c..124daa36be3 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals import frappe, erpnext 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 erpnext.accounts.party import get_party_account, get_due_date from erpnext.controllers.stock_controller import update_gl_entries_after @@ -537,7 +537,12 @@ class SalesInvoice(SellingController): self.against_income_account = ','.join(against_acc) 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): # Don't auto set the posting date and time if invoice is amended diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 315d6a86b1b..4db739e09df 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -1877,23 +1877,6 @@ class TestSalesInvoice(unittest.TestCase): 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 = [] @@ -1941,19 +1924,17 @@ class TestSalesInvoice(unittest.TestCase): value_details = einvoice['ValDtls'] self.assertEqual(einvoice['Version'], '1.1') - self.assertEqual(value_details['AssVal'], total_item_ass_value) - self.assertEqual(value_details['CgstVal'], total_item_cgst_value) - self.assertEqual(value_details['SgstVal'], total_item_sgst_value) - self.assertEqual(value_details['IgstVal'], total_item_igst_value) + self.assertTrue(abs(value_details['AssVal'] - total_item_ass_value) <= 1) + self.assertTrue(abs(value_details['CgstVal'] - total_item_cgst_value) <= 1) + self.assertTrue(abs(value_details['SgstVal'] - total_item_sgst_value) <= 1) + self.assertTrue(abs(value_details['IgstVal'] - total_item_igst_value) <= 1) - self.assertEqual( - value_details['TotInvVal'], - value_details['AssVal'] + value_details['CgstVal'] - + value_details['SgstVal'] + value_details['IgstVal'] + calculated_invoice_value = \ + value_details['AssVal'] + value_details['CgstVal'] \ + + value_details['SgstVal'] + value_details['IgstVal'] \ + value_details['OthChrg'] - value_details['Discount'] - ) - self.assertEqual(value_details['TotInvVal'], si.base_grand_total) + self.assertTrue(abs(value_details['TotInvVal'] - calculated_invoice_value) <= 1) self.assertTrue(einvoice['EwbDtls']) def make_test_address_for_ewaybill(): @@ -2066,27 +2047,6 @@ def make_sales_invoice_for_ewaybill(): return si - def test_item_tax_validity(self): - item = frappe.get_doc("Item", "_Test Item 2") - - if item.taxes: - item.taxes = [] - item.save() - - item.append("taxes", { - "item_tax_template": "_Test Item Tax Template 1", - "valid_from": add_days(nowdate(), 1) - }) - - item.save() - - sales_invoice = create_sales_invoice(item = "_Test Item 2", do_not_save=1) - sales_invoice.items[0].item_tax_template = "_Test Item Tax Template 1" - self.assertRaises(frappe.ValidationError, sales_invoice.save) - - item.taxes = [] - item.save() - def create_sales_invoice(**args): si = frappe.new_doc("Sales Invoice") args = frappe._dict(args) diff --git a/erpnext/accounts/doctype/subscription/subscription.py b/erpnext/accounts/doctype/subscription/subscription.py index ae0059cd3cd..8cc5cb8e99b 100644 --- a/erpnext/accounts/doctype/subscription/subscription.py +++ b/erpnext/accounts/doctype/subscription/subscription.py @@ -333,7 +333,7 @@ class Subscription(Document): if not self.generate_invoice_at_period_start: return False - if self.is_new_subscription(): + if self.is_new_subscription() and getdate(nowdate()) >= getdate(self.current_invoice_start): return True # Check invoice dates and make sure it doesn't have outstanding invoices diff --git a/erpnext/accounts/page/pos/pos.js b/erpnext/accounts/page/pos/pos.js index b9b1d293d47..2819371f3e3 100755 --- a/erpnext/accounts/page/pos/pos.js +++ b/erpnext/accounts/page/pos/pos.js @@ -2018,34 +2018,57 @@ erpnext.pos.PointOfSale = erpnext.taxes_and_totals.extend({ apply_pricing_rule: function () { var me = this; + + var remove_item = false; $.each(this.frm.doc["items"], function (n, item) { var pricing_rule = me.get_pricing_rule(item) me.validate_pricing_rule(pricing_rule) if (pricing_rule.length) { - item.pricing_rule = pricing_rule[0].name; - item.margin_type = pricing_rule[0].margin_type; - item.price_list_rate = pricing_rule[0].price || item.price_list_rate; - item.margin_rate_or_amount = pricing_rule[0].margin_rate_or_amount; - item.discount_percentage = pricing_rule[0].discount_percentage || 0.0; - me.apply_pricing_rule_on_item(item) + if (pricing_rule[0].price_or_product_discount == "Price") { + item.pricing_rule = pricing_rule[0].name; + item.margin_type = pricing_rule[0].margin_type; + item.price_list_rate = pricing_rule[0].price || item.price_list_rate; + item.margin_rate_or_amount = pricing_rule[0].margin_rate_or_amount; + 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) { item.price_list_rate = me.price_list_data[item.item_code] item.margin_rate_or_amount = 0.0; item.discount_percentage = 0.0; item.pricing_rule = null; me.apply_pricing_rule_on_item(item) + } else if (item.is_free_item) { + remove_item = true; + item.qty = 0 } if(item.discount_percentage > 0) { me.apply_pricing_rule_on_item(item) } - }) + }); + + if (remove_item) { + this.remove_zero_qty_items_from_cart(); + } }, get_pricing_rule: function (item) { var me = this; 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 (in_list(['Customer', 'Customer Group', 'Territory', 'Campaign'], data.applicable_for)) { 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) { var apply_on = frappe.model.scrub(data.apply_on); - return (data.apply_on == 'Item Group') - ? this.validate_item_group(data.item_group, item.item_group) : (data[apply_on] == item[apply_on]); + return in_list(data[apply_on], item[apply_on]); }, validate_item_group: function (pr_item_group, cart_item_group) { diff --git a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py index 1f78c7a006f..b435a9a53cc 100644 --- a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py +++ b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py @@ -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, get_grand_total, add_total_row, get_display_value, get_group_by_and_display_fields, add_sub_total_row, get_group_by_conditions) +from erpnext.selling.report.item_wise_sales_history.item_wise_sales_history import get_item_details def execute(filters=None): return _execute(filters) @@ -23,7 +24,7 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum aii_account_map = get_aii_accounts() if item_list: 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) @@ -35,10 +36,14 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum if filters.get('group_by'): grand_total = get_grand_total(filters, 'Purchase Invoice') + item_details = get_item_details() + for d in item_list: if not d.stock_qty: continue + item_record = item_details.get(d.item_code) + purchase_receipt = None if d.purchase_receipt: purchase_receipt = d.purchase_receipt @@ -49,8 +54,8 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum row = { 'item_code': d.item_code, - 'item_name': d.item_name, - 'item_group': d.item_group, + 'item_name': item_record.item_name, + 'item_group': item_record.item_group, 'description': d.description, 'invoice': d.parent, '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: item_tax = itemised_tax.get(d.name, {}).get(tax, {}) row.update({ - frappe.scrub(tax + ' Rate'): item_tax.get("tax_rate", 0), - frappe.scrub(tax + ' Amount'): item_tax.get("tax_amount", 0), + frappe.scrub(tax + ' Rate'): item_tax.get('tax_rate', 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({ 'total_tax': total_tax, @@ -317,8 +322,8 @@ def get_items(filters, additional_query_columns): select `tabPurchase Invoice Item`.`name`, `tabPurchase Invoice Item`.`parent`, `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 Item`.`item_name`, `tabPurchase Invoice Item`.`item_group`, `tabPurchase Invoice Item`.description, + `tabPurchase Invoice`.supplier, `tabPurchase Invoice`.remarks, `tabPurchase Invoice`.base_net_total, + `tabPurchase Invoice Item`.`item_code`, `tabPurchase Invoice Item`.description, `tabPurchase Invoice Item`.`project`, `tabPurchase Invoice Item`.`purchase_order`, `tabPurchase Invoice Item`.`purchase_receipt`, `tabPurchase Invoice Item`.`po_detail`, `tabPurchase Invoice Item`.`expense_account`, `tabPurchase Invoice Item`.`stock_qty`, diff --git a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py index 92a22e62f14..d22111c9f5a 100644 --- a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py +++ b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py @@ -8,6 +8,7 @@ from frappe.utils import flt, cstr from frappe.model.meta import get_field_precision from frappe.utils.xlsxutils import handle_html 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): 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]}) 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) if item_list: @@ -34,7 +35,13 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum if filters.get('group_by'): grand_total = get_grand_total(filters, 'Sales Invoice') + customer_details = get_customer_details() + item_details = get_item_details() + for d in item_list: + customer_record = customer_details.get(d.customer) + item_record = item_details.get(d.item_code) + delivery_note = None if d.delivery_note: delivery_note = d.delivery_note @@ -46,14 +53,14 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum row = { 'item_code': d.item_code, - 'item_name': d.item_name, - 'item_group': d.item_group, + 'item_name': item_record.item_name, + 'item_group': item_record.item_group, 'description': d.description, 'invoice': d.parent, 'posting_date': d.posting_date, 'customer': d.customer, - 'customer_name': d.customer_name, - 'customer_group': d.customer_group, + 'customer_name': customer_record.customer_name, + 'customer_group': customer_record.customer_group, } if additional_query_columns: @@ -91,10 +98,10 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum for tax in tax_columns: item_tax = itemised_tax.get(d.name, {}).get(tax, {}) row.update({ - frappe.scrub(tax + ' Rate'): item_tax.get("tax_rate", 0), - frappe.scrub(tax + ' Amount'): item_tax.get("tax_amount", 0), + frappe.scrub(tax + ' Rate'): item_tax.get('tax_rate', 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({ 'total_tax': total_tax, @@ -227,7 +234,7 @@ def get_columns(additional_table_columns, filters): if filters.get('group_by') != 'Terriotory': columns.extend([ { - 'label': _("Territory"), + 'label': _('Territory'), 'fieldname': 'territory', 'fieldtype': 'Link', 'options': 'Territory', @@ -382,13 +389,12 @@ def get_items(filters, additional_query_columns): `tabSales Invoice`.posting_date, `tabSales Invoice`.debit_to, `tabSales Invoice`.project, `tabSales Invoice`.customer, `tabSales Invoice`.remarks, `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_group, `tabSales Invoice Item`.description, `tabSales Invoice Item`.sales_order, - `tabSales Invoice Item`.delivery_note, `tabSales Invoice Item`.income_account, - `tabSales Invoice Item`.cost_center, `tabSales Invoice Item`.stock_qty, - `tabSales Invoice Item`.stock_uom, `tabSales Invoice Item`.base_net_rate, - `tabSales Invoice Item`.base_net_amount, `tabSales Invoice`.customer_name, - `tabSales Invoice`.customer_group, `tabSales Invoice Item`.so_detail, + `tabSales Invoice Item`.item_code, `tabSales Invoice Item`.description, + `tabSales Invoice Item`.sales_order, `tabSales Invoice Item`.delivery_note, + `tabSales Invoice Item`.income_account, `tabSales Invoice Item`.cost_center, + `tabSales Invoice Item`.stock_qty, `tabSales Invoice Item`.stock_uom, + `tabSales Invoice Item`.base_net_rate, `tabSales Invoice Item`.base_net_amount, + `tabSales Invoice`.customer_name, `tabSales Invoice`.customer_group, `tabSales Invoice Item`.so_detail, `tabSales Invoice`.update_stock, `tabSales Invoice Item`.uom, `tabSales Invoice Item`.qty {0} from `tabSales Invoice`, `tabSales Invoice Item` 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'") 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 item_row_map = {} tax_columns = [] invoice_item_row = {} 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 for d in item_list: @@ -477,8 +483,8 @@ def get_tax_accounts(item_list, columns, company_currency, tax_rate = tax_data tax_amount = 0 - if charge_type == "Actual" and not tax_rate: - tax_rate = "NA" + if charge_type == 'Actual' and not tax_rate: + tax_rate = 'NA' item_net_amount = sum([flt(d.base_net_amount) 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) itemised_tax.setdefault(d.name, {})[description] = frappe._dict({ - "tax_rate": tax_rate, - "tax_amount": tax_value + 'tax_rate': tax_rate, + 'tax_amount': tax_value }) except ValueError: continue - elif charge_type == "Actual" and tax_amount: + elif charge_type == 'Actual' and tax_amount: for d in invoice_item_row.get(parent, []): itemised_tax.setdefault(d.name, {})[description] = frappe._dict({ - "tax_rate": "NA", - "tax_amount": flt((tax_amount * d.base_net_amount) / d.base_net_total, + 'tax_rate': 'NA', + 'tax_amount': flt((tax_amount * d.base_net_amount) / d.base_net_total, 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', { - subtotal_display_field: "Total", + subtotal_display_field: 'Total', 'stock_qty': 0.0, 'amount': 0.0, 'bold': 1, diff --git a/erpnext/accounts/report/payment_period_based_on_invoice_date/payment_period_based_on_invoice_date.py b/erpnext/accounts/report/payment_period_based_on_invoice_date/payment_period_based_on_invoice_date.py index 57a1231f5a9..98731d32ae9 100644 --- a/erpnext/accounts/report/payment_period_based_on_invoice_date/payment_period_based_on_invoice_date.py +++ b/erpnext/accounts/report/payment_period_based_on_invoice_date/payment_period_based_on_invoice_date.py @@ -59,23 +59,111 @@ def validate_filters(filters): def get_columns(filters): return [ - _("Payment Document") + ":: 100", - _("Payment Entry") + ":Dynamic Link/"+_("Payment Document")+":140", - _("Party Type") + "::100", - _("Party") + ":Dynamic Link/Party Type:140", - _("Posting Date") + ":Date: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", - _("Debit") + ":Currency:120", - _("Credit") + ":Currency:120", - _("Remarks") + "::150", - _("Age") +":Int:40", - "0-30:Currency:100", - "30-60:Currency:100", - "60-90:Currency:100", - _("90-Above") + ":Currency:100", - _("Delay in payment (Days)") + "::150" + { + "fieldname": "payment_document", + "label": _("Payment Document Type"), + "fieldtype": "Data", + "width": 100 + }, + { + "fieldname": "payment_entry", + "label": _("Payment Document"), + "fieldtype": "Dynamic Link", + "options": "payment_document", + "width": 160 + }, + { + "fieldname": "party_type", + "label": _("Party Type"), + "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): diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index 78d36f14d82..93781d9e0ef 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -136,6 +136,8 @@ frappe.ui.form.on('Asset', { if (frm.doc.docstatus == 0) { 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'); } }, diff --git a/erpnext/assets/doctype/asset_category/asset_category.js b/erpnext/assets/doctype/asset_category/asset_category.js index 74963c2aa96..0adcf64bfc2 100644 --- a/erpnext/assets/doctype/asset_category/asset_category.js +++ b/erpnext/assets/doctype/asset_category/asset_category.js @@ -50,6 +50,5 @@ frappe.ui.form.on('Asset Category', { } }; }); - } }); diff --git a/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json b/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json index d422876047e..89d88d950ac 100644 --- a/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json +++ b/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json @@ -1,5 +1,4 @@ { - "actions": [], "creation": "2018-05-08 14:44:37.095570", "doctype": "DocType", "editable_grid": 1, @@ -54,9 +53,7 @@ "fieldname": "depreciation_start_date", "fieldtype": "Date", "in_list_view": 1, - "label": "Depreciation Posting Date", - "mandatory_depends_on": "eval:parent.doctype == 'Asset'", - "reqd": 1 + "label": "Depreciation Posting Date" }, { "default": "0", @@ -84,10 +81,8 @@ "label": "Rate of Depreciation" } ], - "index_web_pages_for_search": 1, "istable": 1, - "links": [], - "modified": "2020-10-30 15:22:29.119868", + "modified": "2020-12-30 15:43:03.188256", "modified_by": "Administrator", "module": "Assets", "name": "Asset Finance Book", diff --git a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py index 96e38a9b779..cce987950b2 100644 --- a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py +++ b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py @@ -141,7 +141,7 @@ def get_data(filters): assets_record = frappe.db.get_all("Asset", filters=conditions, - fields=["name", "asset_name", "department", "cost_center", "purchase_receipt", + fields=["name as asset_id", "asset_name", "department", "cost_center", "purchase_receipt", "asset_category", "purchase_date", "gross_purchase_amount", "location", "available_for_use_date", "status", "purchase_invoice", "opening_accumulated_depreciation"]) diff --git a/erpnext/buying/utils.py b/erpnext/buying/utils.py index 47b48665b60..79542d482ad 100644 --- a/erpnext/buying/utils.py +++ b/erpnext/buying/utils.py @@ -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)) # update last purchsae rate - if last_purchase_rate: - frappe.db.sql("""update `tabItem` set last_purchase_rate = %s where name = %s""", - (flt(last_purchase_rate), d.item_code)) + frappe.db.set_value('Item', d.item_code, 'last_purchase_rate', flt(last_purchase_rate)) def validate_for_items(doc): items = [] diff --git a/erpnext/change_log/v12/v12_17_0.md b/erpnext/change_log/v12/v12_17_0.md new file mode 100644 index 00000000000..72c31ce9a62 --- /dev/null +++ b/erpnext/change_log/v12/v12_17_0.md @@ -0,0 +1,24 @@ +## ERPNext v12.17.0 Release Notes + +### Features + +- Separated equity tree in COA SKR04 ([#24094](https://github.com/frappe/erpnext/pull/24094)) +- Display transporter address in sales invoice ([#23731](https://github.com/frappe/erpnext/pull/23731)) +- Introduced GST E-Invoicing ([#24184](https://github.com/frappe/erpnext/pull/24184)) + +### Fixes + +- Fixed Payment Entry multi-currency issue ([#24331](https://github.com/frappe/erpnext/pull/24331)) +- Fixed an issue where user could have manufactured same serial no multiple times ([#24163](https://github.com/frappe/erpnext/pull/24163)) +- Back Update from QC based on Batch No ([#24368](https://github.com/frappe/erpnext/pull/24368)) +- Fixed tax calculation on salary slip for the first month ([#24309](https://github.com/frappe/erpnext/pull/24309)) ([#24272](https://github.com/frappe/erpnext/pull/24272)) +- Fixed issues related to e-invoicing ([#24366](https://github.com/frappe/erpnext/pull/24366)) ([#24421](https://github.com/frappe/erpnext/pull/24421)) ([#24284](https://github.com/frappe/erpnext/pull/24284)) +- Added a validation to restrict manual overriding of valuation rate in Stock Entry ([#24221](https://github.com/frappe/erpnext/pull/24221)) +- Fixed incorrect serial no. in the subcontracted Purchase Receipt ([#24353](https://github.com/frappe/erpnext/pull/24353)) +- Fixed an issue where Stock Ledger entry was not getting created against Stock Reconciliation ([#24384](https://github.com/frappe/erpnext/pull/24384)) +- Fixed company wise Valuation Rate for raw material in BOM ([#24367](https://github.com/frappe/erpnext/pull/24367)) +- Fixed Taxation related issue ([#24159](https://github.com/frappe/erpnext/pull/24159)) +- Allowed to override the basic rate for the finished good ([#24301](https://github.com/frappe/erpnext/pull/24301)) +- Fixed Loyalty Program related issues ([#24188](https://github.com/frappe/erpnext/pull/24188)) +- Fixed an issue where last purchase rate was not getting updated on canceling last voucher ([#24323](https://github.com/frappe/erpnext/pull/24323)) +- Fixed issue with pricing rule for offline POS ([#24288](https://github.com/frappe/erpnext/pull/24288)) \ No newline at end of file diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 68fc331e218..59c371200c8 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -296,7 +296,7 @@ class BuyingController(StockController): raw_material_data = backflushed_raw_materials_map.get(rm_item_key, {}) 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', '') transferred_qty = raw_material.qty diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index 18b5daf128a..c3a15c58ce9 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -279,6 +279,7 @@ def make_return_doc(doctype, source_name, target_doc=None): target_doc.po_detail = source_doc.po_detail target_doc.pr_detail = source_doc.pr_detail target_doc.purchase_invoice_item = source_doc.name + target_doc.price_list_rate = 0 elif doctype == "Delivery Note": 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.expense_account = source_doc.expense_account target_doc.sales_invoice_item = source_doc.name + target_doc.price_list_rate = 0 if default_warehouse_for_sales_return: target_doc.warehouse = default_warehouse_for_sales_return diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py index b34432ae202..66d0e5f77db 100644 --- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py +++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py @@ -31,11 +31,12 @@ class PlaidConnector(): return access_token def get_token_request(self, update_mode=False): + country_codes = ["US", "CA", "FR", "IE", "NL", "ES", "GB"] if self.settings.enable_european_access else ["US", "CA"] args = { "client_name": self.client_name, # only allow Plaid-supported languages and countries (LAST: Sep-19-2020) "language": frappe.local.lang if frappe.local.lang in ["en", "fr", "es", "nl"] else "en", - "country_codes": ["US", "CA", "ES", "FR", "GB", "IE", "NL"], + "country_codes": country_codes, "user": { "client_user_id": frappe.generate_hash(frappe.session.user, length=32) } diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.json b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.json index 27062172239..122aa41f4b9 100644 --- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.json +++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.json @@ -1,4 +1,5 @@ { + "actions": [], "creation": "2018-10-25 10:02:48.656165", "doctype": "DocType", "editable_grid": 1, @@ -11,7 +12,8 @@ "plaid_client_id", "plaid_secret", "column_break_7", - "plaid_env" + "plaid_env", + "enable_european_access" ], "fields": [ { @@ -58,10 +60,17 @@ { "fieldname": "column_break_7", "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "enable_european_access", + "fieldtype": "Check", + "label": "Enable European Access" } ], "issingle": 1, - "modified": "2020-09-12 02:31:44.542385", + "links": [], + "modified": "2020-10-29 20:24:56.916104", "modified_by": "Administrator", "module": "ERPNext Integrations", "name": "Plaid Settings", diff --git a/erpnext/hooks.py b/erpnext/hooks.py index bf81805833b..219e426d782 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -245,7 +245,8 @@ doc_events = { "Sales Invoice": { "on_submit": ["erpnext.regional.create_transaction_log", "erpnext.regional.italy.utils.sales_invoice_on_submit"], "on_cancel": "erpnext.regional.italy.utils.sales_invoice_on_cancel", - "on_trash": "erpnext.regional.check_deletion_permission" + "on_trash": "erpnext.regional.check_deletion_permission", + "validate": "erpnext.regional.india.utils.set_transporter_address" }, "Purchase Invoice": { "validate": "erpnext.regional.india.utils.update_grand_total_for_rcm" diff --git a/erpnext/hr/doctype/employee/employee.json b/erpnext/hr/doctype/employee/employee.json index 4a60dba30e5..fa02ac4ea7e 100644 --- a/erpnext/hr/doctype/employee/employee.json +++ b/erpnext/hr/doctype/employee/employee.json @@ -782,7 +782,7 @@ "icon": "fa fa-user", "idx": 24, "image_field": "image", - "modified": "2020-01-09 04:23:55.611366", + "modified": "2020-01-09 05:23:55.611366", "modified_by": "Administrator", "module": "HR", "name": "Employee", @@ -824,7 +824,6 @@ "write": 1 } ], - "quick_entry": 1, "search_fields": "employee_name", "show_name_in_global_search": 1, "sort_field": "modified", diff --git a/erpnext/hr/doctype/payroll_period/payroll_period.py b/erpnext/hr/doctype/payroll_period/payroll_period.py index 6956c382854..0b500ea60d6 100644 --- a/erpnext/hr/doctype/payroll_period/payroll_period.py +++ b/erpnext/hr/doctype/payroll_period/payroll_period.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import frappe 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 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 if relieving_date and getdate(relieving_date) < getdate(period_end): 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 diff --git a/erpnext/hr/doctype/salary_slip/salary_slip.py b/erpnext/hr/doctype/salary_slip/salary_slip.py index 774aa51daa1..6e6ae4351c8 100644 --- a/erpnext/hr/doctype/salary_slip/salary_slip.py +++ b/erpnext/hr/doctype/salary_slip/salary_slip.py @@ -299,14 +299,17 @@ class SalarySlip(TransactionBase): def calculate_net_pay(self): if self.salary_structure: 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: self.calculate_component_amounts("deductions") - self.total_deduction = self.get_component_totals("deductions") - + 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.rounded_total = rounded(self.net_pay) @@ -323,8 +326,6 @@ class SalarySlip(TransactionBase): else: self.add_tax_components(payroll_period) - self.set_component_amounts_based_on_payment_days(component_type) - def add_structure_components(self, component_type): data = self.get_data_for_eval() 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 (not self.salary_slip_based_on_timesheet 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) / 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 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 for d in self.get(component_type): if not d.do_not_include_in_total: - d.amount = flt(d.amount, d.precision("amount")) - total += d.amount + if depends_on_payment_days: + 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 - 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, ["date_of_joining", "relieving_date"]) @@ -830,8 +837,9 @@ class SalarySlip(TransactionBase): if not joining_date: frappe.throw(_("Please set the Date Of Joining for employee {0}").format(frappe.bold(self.employee_name))) - for d in self.get(component_type): - d.amount = self.get_amount_based_on_payment_days(d, joining_date, relieving_date)[0] + for component_type in ("earnings", "deductions"): + 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): self.set('loans', []) diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js index 74169c80d35..89291263b64 100644 --- a/erpnext/manufacturing/doctype/bom/bom.js +++ b/erpnext/manufacturing/doctype/bom/bom.js @@ -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.bom_no = function(doc, cdt, cdn) { +cur_frm.cscript.bom_no = function(doc, cdt, cdn) { 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); }; -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]; if (d.item_code) { return frappe.call({ doc: doc, method: "get_bom_material_detail", args: { - 'item_code': d.item_code, - 'bom_no': d.bom_no != null ? d.bom_no: '', + "company": doc.company, + "item_code": d.item_code, + "bom_no": d.bom_no != null ? d.bom_no: '', "scrap_items": scrap_items, - 'qty': d.qty, + "qty": d.qty, "stock_qty": d.stock_qty, "include_item_in_manufacturing": d.include_item_in_manufacturing, "uom": d.uom, @@ -309,7 +314,7 @@ cur_frm.cscript.rate = function(doc, cdt, cdn) { } 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); } else { erpnext.bom.calculate_rm_cost(doc); diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 14bcbaac6ff..0bc6a70376b 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -51,6 +51,10 @@ class BOM(WebsiteGenerator): def validate(self): self.route = frappe.scrub(self.name).replace('_', '-') + + if not self.company: + frappe.throw(_("Please select a Company first."), title=_("Mandatory")) + self.clear_operations() self.validate_main_item() self.validate_currency() @@ -122,6 +126,7 @@ class BOM(WebsiteGenerator): self.validate_bom_currecny(item) ret = self.get_bom_material_detail({ + "company": self.company, "item_code": item.item_code, "item_name": item.item_name, "bom_no": item.bom_no, @@ -236,6 +241,7 @@ class BOM(WebsiteGenerator): for d in self.get("items"): rate = self.get_rm_rate({ + "company": self.company, "item_code": d.item_code, "bom_no": d.bom_no, "qty": d.qty, @@ -288,10 +294,20 @@ class BOM(WebsiteGenerator): """ Get weighted average of valuation rate from all warehouses """ 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` - where item_code=%s""", args['item_code'], as_dict=1): - total_qty += flt(d.actual_qty) - total_value += flt(d.stock_value) + item_bins = frappe.db.sql(""" + select + bin.actual_qty, bin.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: valuation_rate = total_value / total_qty diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index dac61b8e974..b11fdb04745 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -526,7 +526,6 @@ class TestWorkOrder(unittest.TestCase): ste1.submit() ste_cancel_list.append(ste1) - print(wo_order.name) ste3 = frappe.get_doc(make_stock_entry(wo_order.name, "Material Consumption for Manufacture", 2)) self.assertEquals(ste3.fg_completed_qty, 2) @@ -539,6 +538,48 @@ class TestWorkOrder(unittest.TestCase): 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): scrap_items = {} for item in frappe.db.sql("""select item_code, stock_qty from `tabBOM Scrap Item` diff --git a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py index 75ebcbc971b..1c6758e6f36 100644 --- a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py +++ b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py @@ -20,6 +20,7 @@ def get_columns(): _("Item") + ":Link/Item:150", _("Description") + "::300", _("BOM Qty") + ":Float:160", + _("BOM UoM") + "::160", _("Required Qty") + ":Float:120", _("In Stock Qty") + ":Float:120", _("Enough Parts to Build") + ":Float:200", @@ -32,7 +33,7 @@ def get_bom_stock(filters): bom = filters.get("bom") table = "`tabBOM Item`" - qty_field = "qty" + qty_field = "stock_qty" qty_to_produce = filters.get("qty_to_produce", 1) if int(qty_to_produce) <= 0: @@ -40,7 +41,6 @@ def get_bom_stock(filters): if filters.get("show_exploded_view"): table = "`tabBOM Explosion Item`" - qty_field = "stock_qty" if filters.get("warehouse"): 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.description , bom_item.{qty_field}, + bom_item.stock_uom, bom_item.{qty_field} * {qty_to_produce} / bom.quantity, sum(ledger.actual_qty) as actual_qty, sum(FLOOR(ledger.actual_qty / (bom_item.{qty_field} * {qty_to_produce} / bom.quantity))) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index e6066a9fead..08eda7e2c43 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -678,4 +678,5 @@ erpnext.patches.v12_0.update_state_code_for_daman_and_diu erpnext.patches.v12_0.rename_lost_reason_detail erpnext.patches.v12_0.update_leave_application_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 diff --git a/erpnext/patches/v12_0/add_transporter_address_field.py b/erpnext/patches/v12_0/add_transporter_address_field.py new file mode 100644 index 00000000000..78da58f4e03 --- /dev/null +++ b/erpnext/patches/v12_0/add_transporter_address_field.py @@ -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') \ No newline at end of file diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index e16cb5fe6e5..3cd8e27bc40 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -522,6 +522,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ company: me.frm.doc.company, order_type: me.frm.doc.order_type, is_pos: cint(me.frm.doc.is_pos), + is_return: cint(me.frm.doc.is_return), is_subcontracted: me.frm.doc.is_subcontracted, transaction_date: me.frm.doc.transaction_date || me.frm.doc.posting_date, ignore_pricing_rule: me.frm.doc.ignore_pricing_rule, diff --git a/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.json b/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.json index 5c1c79dc047..3034370feac 100644 --- a/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.json +++ b/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.json @@ -24,9 +24,8 @@ }, { "fieldname": "reference_invoice", - "fieldtype": "Link", - "label": "Reference Invoice", - "options": "Sales Invoice" + "fieldtype": "Data", + "label": "Reference Invoice" }, { "fieldname": "headers", @@ -64,7 +63,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2020-12-24 21:09:38.882866", + "modified": "2021-01-13 12:06:57.253111", "modified_by": "Administrator", "module": "Regional", "name": "E Invoice Request Log", diff --git a/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.json b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.json index 4dcb22a54c7..db8bda75bfd 100644 --- a/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.json +++ b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.json @@ -7,6 +7,7 @@ "field_order": [ "enable", "section_break_2", + "sandbox_mode", "credentials", "auth_token", "token_expiry" @@ -41,12 +42,18 @@ "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": "2020-12-22 15:34:57.280044", + "modified": "2021-01-13 12:04:49.449199", "modified_by": "Administrator", "module": "Regional", "name": "E Invoice Settings", diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index 225e6cdd786..79d554c0f96 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -15,7 +15,7 @@ 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 +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')) @@ -84,29 +84,32 @@ def get_doc_details(invoice): )) def get_party_details(address_name): - address = frappe.get_all('Address', filters={'name': address_name}, fields=['*'])[0] - gstin = address.get('gstin') + d = frappe.get_all('Address', filters={'name': address_name}, fields=['*'])[0] - gstin_details = get_gstin_details(gstin) - legal_name = gstin_details.get('LegalName') - location = gstin_details.get('AddrLoc') or address.get('city') - state_code = gstin_details.get('StateCode') - pincode = gstin_details.get('AddrPncd') - address_line1 = '{} {}'.format(gstin_details.get('AddrBno'), gstin_details.get('AddrFlno')) - address_line2 = '{} {}'.format(gstin_details.get('AddrBnm'), gstin_details.get('AddrSt')) - email_id = address.get('email_id') - phone = address.get('phone') - # get last 10 digit - phone = phone.replace(" ", "")[-10:] if phone else '' + 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): - if state_code == 97: + 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=gstin, legal_name=legal_name, location=location, - pincode=pincode, state_code=state_code, address_line1=address_line1, - address_line2=address_line2, email=email_id, phone=phone + 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): @@ -127,14 +130,22 @@ def get_gstin_details(gstin): return GSPConnector.get_gstin_details(gstin) def get_overseas_address_details(address_name): - address_title, address_line1, address_line2, city, phone, email_id = frappe.db.get_value( - 'Address', address_name, ['address_title', 'address_line1', 'address_line2', 'city', 'phone', 'email_id'] + 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, address_line1=address_line1, - address_line2=address_line2, email=email_id, phone=phone, - pincode=999999, state_code=96, place_of_supply=96, location=city + 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): @@ -146,16 +157,18 @@ def get_item_list(invoice): item.update(d.as_dict()) item.sr_no = d.idx - item.discount_amount = abs(item.discount_amount * item.qty) - item.description = d.item_name + item.description = d.item_name.replace('"', '\\"') + item.qty = abs(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.discount_amount = abs(item.discount_amount * item.qty) + item.unit_rate = abs(item.base_net_amount / item.qty) + item.gross_amount = abs(item.base_net_amount) + item.taxable_value = abs(item.base_net_amount) 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) @@ -185,7 +198,7 @@ def update_item_taxes(invoice, item): if t.account_head in gst_accounts_list: item_tax_rate = item_tax_detail[0] # item tax amount excluding discount amount - item_tax_amount = (item_tax_rate / 100) * item.base_amount + item_tax_amount = (item_tax_rate / 100) * item.base_net_amount if t.account_head in gst_accounts.cess_account: item_tax_amount_after_discount = item_tax_detail[1] @@ -204,9 +217,15 @@ def update_item_taxes(invoice, item): def get_invoice_value_details(invoice): invoice_value_details = frappe._dict(dict()) - invoice_value_details.base_total = abs(invoice.base_total) - invoice_value_details.invoice_discount_amt = invoice.discount_amount - invoice_value_details.round_off = invoice.rounding_adjustment + + if invoice.apply_discount_on == 'Net Total' and invoice.discount_amount: + invoice_value_details.base_total = abs(invoice.base_total) + else: + invoice_value_details.base_total = abs(invoice.base_net_total) + + # since tax already considers discount amount + invoice_value_details.invoice_discount_amt = 0 # invoice.base_discount_amount + invoice_value_details.round_off = invoice.base_rounding_adjustment 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) @@ -231,9 +250,9 @@ def update_invoice_taxes(invoice, invoice_value_details): for tax_type in ['igst', 'cgst', 'sgst']: if t.account_head in gst_accounts['{}_account'.format(tax_type)]: - invoice_value_details['total_{}_amt'.format(tax_type)] += abs(t.base_tax_amount) + invoice_value_details['total_{}_amt'.format(tax_type)] += abs(t.base_tax_amount_after_discount_amount) else: - invoice_value_details.total_other_charges += abs(t.base_tax_amount) + invoice_value_details.total_other_charges += abs(t.base_tax_amount_after_discount_amount) return invoice_value_details @@ -272,7 +291,25 @@ def get_eway_bill_details(invoice): 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) @@ -358,7 +395,7 @@ def validate_einvoice(validations, einvoice, errors=[]): # remove empty dicts einvoice.pop(fieldname, None) continue - + # convert to int or str if value_type == 'string': einvoice[fieldname] = str(value) @@ -390,15 +427,19 @@ 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() - self.base_url = 'https://gsp.adaequare.com' - self.authenticate_url = self.base_url + '/gsp/authenticate?grant_type=token' - self.gstin_details_url = self.base_url + '/enriched/ei/api/master/gstin' - self.generate_irn_url = self.base_url + '/enriched/ei/api/invoice' - self.irn_details_url = self.base_url + '/enriched/ei/api/invoice/irn' + # 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' @@ -442,7 +483,7 @@ class GSPConnector(): "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) + request_log.save(ignore_permissions=True) frappe.db.commit() def fetch_auth_token(self): @@ -455,7 +496,8 @@ class GSPConnector(): 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() + self.e_invoice_settings.save(ignore_permissions=True) + self.e_invoice_settings.reload() except Exception: self.log_error(res) @@ -726,7 +768,7 @@ class GSPConnector(): 'label': _('IRN Generated') } self.update_invoice() - + def attach_qrcode_image(self): qrcode = self.invoice.signed_qr_code doctype = self.invoice.doctype @@ -734,10 +776,10 @@ class GSPConnector(): _file = frappe.new_doc('File') _file.update({ - 'file_name': 'QRCode_{}.png'.format(docname), + 'file_name': 'QRCode_{}.png'.format(docname.replace('/', '-')), 'attached_to_doctype': doctype, 'attached_to_name': docname, - 'content': 'qrcode', + 'content': str(base64.b64encode(os.urandom(64))), 'is_private': 1 }) _file.insert() diff --git a/erpnext/regional/india/party.js b/erpnext/regional/india/party.js index 402a387c4a1..8e9c9a5f94e 100644 --- a/erpnext/regional/india/party.js +++ b/erpnext/regional/india/party.js @@ -22,4 +22,4 @@ erpnext.setup_gst_reminder_button = (doctype) => { } } }); -}; +}; \ No newline at end of file diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index 3cfcaa4d9c8..4954b3438dc 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -7,7 +7,7 @@ import frappe, os, json from frappe.custom.doctype.custom_field.custom_field import create_custom_fields from frappe.permissions import add_permission, update_permission_property 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 def setup(company=None, patch=True): @@ -273,11 +273,21 @@ def make_custom_fields(update=True): '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', + 'insert_after': 'transporter_name', 'fetch_from': 'transporter.gst_transporter_id', 'print_hide': 1, 'translatable': 0 @@ -319,11 +329,18 @@ def make_custom_fields(update=True): 'insert_after': 'distance' }, { - 'fieldname': 'transporter_name', - 'label': 'Transporter Name', - 'fieldtype': 'Data', + 'fieldname': 'transporter_address', + 'label': 'Transporter Address Name', + 'fieldtype': 'Link', '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, 'print_hide': 1, 'translatable': 0 @@ -333,7 +350,8 @@ def make_custom_fields(update=True): 'label': 'Mode of Transport', 'fieldtype': 'Select', 'options': '\nRoad\nAir\nRail\nShip', - 'insert_after': 'transporter_name', + 'default': 'Road', + 'insert_after': 'transporter_address_display', 'print_hide': 1, 'translatable': 0 }, @@ -567,13 +585,18 @@ def set_salary_components(docs): def set_tax_withholding_category(company): accounts = [] + fiscal_year = None abbr = frappe.get_value("Company", company, "abbr") tds_account = frappe.get_value("Account", 'TDS Payable - {0}'.format(abbr), 'name') if company and 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) for d in docs: @@ -588,11 +611,14 @@ def set_tax_withholding_category(company): if accounts: doc.append("accounts", accounts[0]) - # if fiscal year don't match with any of the already entered data, append rate row - fy_exist = [k for k in doc.get('rates') if k.get('fiscal_year')==fiscal_year] - if not fy_exist: - doc.append("rates", d.get('rates')[0]) - + if fiscal_year: + # if fiscal year don't match with any of the already entered data, append rate row + fy_exist = [k for k in doc.get('rates') if k.get('fiscal_year')==fiscal_year] + if not fy_exist: + doc.append("rates", d.get('rates')[0]) + + doc.flags.ignore_permissions = True + doc.flags.ignore_mandatory = True doc.save() def set_tds_account(docs, company): diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index bac8ca109e3..89677433392 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -12,6 +12,7 @@ from erpnext.regional.india import number_state_mapping from six import string_types from erpnext.accounts.general_ledger import make_gl_entries 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): @@ -93,8 +94,7 @@ def validate_gstin_check_digit(gstin, label='GSTIN'): total += digit factor = 2 if factor == 1 else 1 if gstin[-1] != code_point_chars[((mod - (total % mod)) % mod)]: - frappe.throw(_("""Invalid {0}! The check digit validation has failed. - Please ensure you've typed the {0} correctly.""").format(label)) + frappe.throw(_("""Invalid {0}! The check digit validation has failed. Please ensure you've typed the {0} correctly.""").format(label)) def get_itemised_tax_breakup_header(item_doctype, tax_accounts): if frappe.get_meta(item_doctype).has_field('gst_hsn_code'): @@ -138,6 +138,30 @@ def get_itemised_tax_breakup_data(doc, account_wise=False): def set_place_of_supply(doc, method=None): 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 def test_method(): '''test function''' diff --git a/erpnext/regional/report/gstr_1/gstr_1.py b/erpnext/regional/report/gstr_1/gstr_1.py index 28b77c5b694..61c41bbddf4 100644 --- a/erpnext/regional/report/gstr_1/gstr_1.py +++ b/erpnext/regional/report/gstr_1/gstr_1.py @@ -255,15 +255,16 @@ class Gstr1Report(object): for item_code, tax_amounts in item_wise_tax_detail.items(): tax_rate = tax_amounts[0] - if cgst_or_sgst: - tax_rate *= 2 - if parent not in self.cgst_sgst_invoices: - self.cgst_sgst_invoices.append(parent) + if tax_rate: + if cgst_or_sgst: + tax_rate *= 2 + if parent not in self.cgst_sgst_invoices: + self.cgst_sgst_invoices.append(parent) - rate_based_dict = self.items_based_on_tax_rate\ - .setdefault(parent, {}).setdefault(tax_rate, []) - if item_code not in rate_based_dict: - rate_based_dict.append(item_code) + rate_based_dict = self.items_based_on_tax_rate\ + .setdefault(parent, {}).setdefault(tax_rate, []) + if item_code not in rate_based_dict: + rate_based_dict.append(item_code) except ValueError: continue if unidentified_gst_accounts: diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.js b/erpnext/selling/page/point_of_sale/point_of_sale.js index a4a39ca4d03..e426c932ffb 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.js +++ b/erpnext/selling/page/point_of_sale/point_of_sale.js @@ -155,7 +155,7 @@ erpnext.pos.PointOfSale = class PointOfSale { var me = this; if (this.frm.doc.customer) { 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: { "customer": me.frm.doc.customer, "expiry_date": me.frm.doc.posting_date, diff --git a/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py b/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py index f1b8bc34efb..12c072b9f4a 100644 --- a/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py +++ b/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py @@ -10,8 +10,8 @@ from frappe.utils.nestedset import get_descendants_of def execute(filters=None): filters = frappe._dict(filters or {}) 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) data = get_data(filters) return columns, data @@ -145,14 +145,16 @@ def get_data(filters): company_list.append(filters.get("company")) customer_details = get_customer_details() + item_details = get_item_details() sales_order_records = get_sales_order_details(company_list, filters) for record in sales_order_records: customer_record = customer_details.get(record.customer) + item_record = item_details.get(record.item_code) row = { "item_code": record.item_code, - "item_name": record.item_name, - "item_group": record.item_group, + "item_name": item_record.item_name, + "item_group": item_record.item_group, "description": record.description, "quantity": record.qty, "uom": record.uom, @@ -187,8 +189,8 @@ def get_conditions(filters): return conditions def get_customer_details(): - details = frappe.get_all('Customer', - fields=['name', 'customer_name', "customer_group"]) + details = frappe.get_all("Customer", + fields=["name", "customer_name", "customer_group"]) customer_details = {} for d in details: customer_details.setdefault(d.name, frappe._dict({ @@ -197,15 +199,25 @@ def get_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): conditions = get_conditions(filters) return frappe.db.sql(""" SELECT - so_item.item_code, so_item.item_name, so_item.item_group, - so_item.description, so_item.qty, so_item.uom, - so_item.base_rate, so_item.base_amount, so.name, - so.transaction_date, so.customer, so.territory, + so_item.item_code, so_item.description, so_item.qty, + so_item.uom, so_item.base_rate, so_item.base_amount, + so.name, so.transaction_date, so.customer,so.territory, so.project, so_item.delivered_qty, so_item.billed_amt, so.company FROM diff --git a/erpnext/setup/doctype/company/delete_company_transactions.py b/erpnext/setup/doctype/company/delete_company_transactions.py index 9979bc51e3a..15b6d522e1d 100644 --- a/erpnext/setup/doctype/company/delete_company_transactions.py +++ b/erpnext/setup/doctype/company/delete_company_transactions.py @@ -26,7 +26,8 @@ def delete_company_transactions(company_name): tabDocField where fieldtype='Link' and options='Company'"""): if doctype not in ("Account", "Cost Center", "Warehouse", "Budget", "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) # reset company values diff --git a/erpnext/stock/doctype/material_request/test_material_request.py b/erpnext/stock/doctype/material_request/test_material_request.py index 19924b16363..dccfc047354 100644 --- a/erpnext/stock/doctype/material_request/test_material_request.py +++ b/erpnext/stock/doctype/material_request/test_material_request.py @@ -427,6 +427,7 @@ class TestMaterialRequest(unittest.TestCase): "basic_rate": 1.0 }) se_doc.get("items")[1].update({ + "item_code": "_Test Item Home Desktop 100", "qty": 3.0, "transfer_qty": 3.0, "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', uom="_Test UOM 1", conversion_factor=12) - + requested_qty = self._get_requested_qty('_Test FG Item', '_Test Warehouse - _TC') self.assertEqual(requested_qty, existing_requested_qty + 120) diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.py b/erpnext/stock/doctype/quality_inspection/quality_inspection.py index c3bb5141849..3bdecafdaba 100644 --- a/erpnext/stock/doctype/quality_inspection/quality_inspection.py +++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.py @@ -52,10 +52,24 @@ class QualityInspection(Document): doctype = 'Stock Entry Detail' if self.reference_type and self.reference_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""" - .format(parent_doc=self.reference_type, child_doc=doctype), + conditions = "" + if self.batch_no and self.docstatus == 1: + conditions += " and t1.batch_no = '%s'"%(self.batch_no) + + 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)) @frappe.whitelist() diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 117802297b6..87019cbc728 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -488,6 +488,8 @@ class StockEntry(StockController): if self.purpose in ["Manufacture", "Repack"]: 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) 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": 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")) - 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_amount = d.basic_rate * flt(d.qty) @@ -1010,7 +1012,7 @@ class StockEntry(StockController): wo = frappe.get_doc("Work Order", self.work_order) wo_items = frappe.get_all('Work Order Item', filters={'parent': self.work_order}, - fields=["item_code", "required_qty", "consumed_qty", "transferred_qty"] + fields=["item_code", "required_qty", "consumed_qty", "transferred_qty", "source_warehouse"] ) work_order_qty = wo.material_transferred_for_manufacturing or wo.qty @@ -1028,9 +1030,13 @@ class StockEntry(StockController): qty = req_qty_each * flt(self.fg_completed_qty) 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({ item.item_code: { - "from_warehouse": wo.wip_warehouse, + "from_warehouse": from_warehouse, "to_warehouse": "", "qty": qty, "item_name": item.item_name, @@ -1109,7 +1115,10 @@ class StockEntry(StockController): else: qty = (req_qty_each * flt(self.fg_completed_qty)) - remaining_qty 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): for d in backflushed_materials.get(item.item_code): @@ -1117,7 +1126,8 @@ class StockEntry(StockController): if (qty > req_qty): qty = (qty/trans_qty) * flt(self.fg_completed_qty) - if consumed_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')): @@ -1241,9 +1251,8 @@ class StockEntry(StockController): mreq_item = frappe.db.get_value("Material Request Item", {"name": item.material_request_item, "parent": item.material_request}, ["item_code", "warehouse", "idx"], as_dict=True) - if mreq_item.item_code != item.item_code or \ - mreq_item.warehouse != (item.s_warehouse if self.purpose== "Material Issue" else item.t_warehouse): - frappe.throw(_("Item or Warehouse for row {0} does not match Material Request").format(item.idx), + if mreq_item.item_code != item.item_code: + frappe.throw(_("Item for row {0} does not match Material Request").format(item.idx), frappe.MappingMismatchError) def validate_batch(self): diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json index 9d397df8bcd..29869d01968 100644 --- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json +++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json @@ -494,7 +494,7 @@ }, { "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", "fieldtype": "Check", "label": "Set Basic Rate Manually" @@ -502,7 +502,7 @@ ], "idx": 1, "istable": 1, - "modified": "2020-09-04 12:12:35.668198", + "modified": "2021-01-05 15:05:04.891447", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry Detail", diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index ed29316030f..1b81b97392c 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -216,7 +216,7 @@ class StockReconciliation(StockController): 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)) - 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)) or (not previous_sle and not row.qty)): continue diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 48b4dc858c4..8e61c461d51 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -72,7 +72,9 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru 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): out.update(get_pos_profile_item_details(args.company, args)) diff --git a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.js b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.js index 4204aee342b..2a68fb6f4e0 100644 --- a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.js +++ b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.js @@ -61,9 +61,11 @@ frappe.query_reports["Batch-Wise Balance History"] = { "options": "Batch", "get_query": function() { let item_code = frappe.query_report.get_filter_value('item_code'); - return { - filters: { - "item": item_code + if (item_code) { + return { + filters: { + "item": item_code + } } } }