Merge branch 'master' into develop
diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json
index 0ce3d5d..42cd44a 100644
--- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json
+++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json
@@ -286,6 +286,99 @@
"search_index": 0,
"set_only_once": 0,
"unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "currency_exchange_section",
+ "fieldtype": "Section Break",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Currency Exchange Settings",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "default": "1",
+ "fieldname": "allow_stale",
+ "fieldtype": "Check",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 1,
+ "in_standard_filter": 0,
+ "label": "Allow Stale Exchange Rates",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "default": "1",
+ "depends_on": "eval:doc.allow_stale==0",
+ "fieldname": "stale_days",
+ "fieldtype": "Int",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Stale Days",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "unique": 0
}
],
"has_web_view": 0,
@@ -299,7 +392,7 @@
"issingle": 1,
"istable": 0,
"max_attachments": 0,
- "modified": "2017-06-16 17:39:50.614522",
+ "modified": "2017-09-05 10:10:03.117505",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",
diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py
index dd33ff1..8431173 100644
--- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py
+++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py
@@ -5,10 +5,20 @@
from __future__ import unicode_literals
import frappe
-from frappe import _
-from frappe.utils import cint, comma_and
+from frappe.utils import cint
from frappe.model.document import Document
+
class AccountsSettings(Document):
def on_update(self):
- pass
\ No newline at end of file
+ pass
+
+ def validate(self):
+ self.validate_stale_days()
+
+ def validate_stale_days(self):
+ if not self.allow_stale and cint(self.stale_days) <= 0:
+ frappe.msgprint(
+ "Stale Days should start from 1.", title='Error', indicator='red',
+ raise_exception=1)
+
diff --git a/erpnext/accounts/doctype/accounts_settings/test_accounts_settings.js b/erpnext/accounts/doctype/accounts_settings/test_accounts_settings.js
new file mode 100644
index 0000000..f9aa166
--- /dev/null
+++ b/erpnext/accounts/doctype/accounts_settings/test_accounts_settings.js
@@ -0,0 +1,35 @@
+QUnit.module('accounts');
+
+QUnit.test("test: Accounts Settings doesn't allow negatives", function (assert) {
+ let done = assert.async();
+
+ assert.expect(2);
+
+ frappe.run_serially([
+ () => frappe.set_route('Form', 'Accounts Settings', 'Accounts Settings'),
+ () => frappe.timeout(2),
+ () => unchecked_if_checked(cur_frm, 'Allow Stale Exchange Rates', frappe.click_check),
+ () => cur_frm.set_value('stale_days', 0),
+ () => frappe.click_button('Save'),
+ () => frappe.timeout(2),
+ () => {
+ assert.ok(cur_dialog);
+ },
+ () => frappe.click_button('Close'),
+ () => cur_frm.set_value('stale_days', -1),
+ () => frappe.click_button('Save'),
+ () => frappe.timeout(2),
+ () => {
+ assert.ok(cur_dialog);
+ },
+ () => frappe.click_button('Close'),
+ () => done()
+ ]);
+
+});
+
+const unchecked_if_checked = function(frm, field_name, fn){
+ if (frm.doc.allow_stale) {
+ return fn(field_name);
+ }
+};
diff --git a/erpnext/accounts/doctype/accounts_settings/test_accounts_settings.py b/erpnext/accounts/doctype/accounts_settings/test_accounts_settings.py
new file mode 100644
index 0000000..bf1e967
--- /dev/null
+++ b/erpnext/accounts/doctype/accounts_settings/test_accounts_settings.py
@@ -0,0 +1,22 @@
+import unittest
+
+import frappe
+
+
+class TestAccountsSettings(unittest.TestCase):
+ def tearDown(self):
+ # Just in case `save` method succeeds, we need to take things back to default so that other tests
+ # don't break
+ cur_settings = frappe.get_doc('Accounts Settings', 'Accounts Settings')
+ cur_settings.allow_stale = 1
+ cur_settings.save()
+
+ def test_stale_days(self):
+ cur_settings = frappe.get_doc('Accounts Settings', 'Accounts Settings')
+ cur_settings.allow_stale = 0
+ cur_settings.stale_days = 0
+
+ self.assertRaises(frappe.ValidationError, cur_settings.save)
+
+ cur_settings.stale_days = -1
+ self.assertRaises(frappe.ValidationError, cur_settings.save)
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js
index 61ede97..04db9e2 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.js
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js
@@ -403,6 +403,13 @@
frm.events.set_difference_amount(frm);
}
+
+ // Make read only if Accounts Settings doesn't allow stale rates
+ frappe.model.get_value("Accounts Settings", null, "allow_stale",
+ function(d){
+ frm.set_df_property("source_exchange_rate", "read_only", cint(d.allow_stale) ? 0 : 1);
+ }
+ );
},
target_exchange_rate: function(frm) {
@@ -421,6 +428,13 @@
frm.events.set_difference_amount(frm);
}
frm.set_paid_amount_based_on_received_amount = false;
+
+ // Make read only if Accounts Settings doesn't allow stale rates
+ frappe.model.get_value("Accounts Settings", null, "allow_stale",
+ function(d){
+ frm.set_df_property("target_exchange_rate", "read_only", cint(d.allow_stale) ? 0 : 1);
+ }
+ );
},
paid_amount: function(frm) {
diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js
index f6d9ca9..3801141 100644
--- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js
+++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js
@@ -254,6 +254,21 @@
}
})
}, __("Get items from"));
+ // Get items from Opportunity
+ this.frm.add_custom_button(__('Opportunity'),
+ function() {
+ erpnext.utils.map_current_doc({
+ method: "erpnext.crm.doctype.opportunity.opportunity.make_request_for_quotation",
+ source_doctype: "Opportunity",
+ target: me.frm,
+ setters: {
+ company: me.frm.doc.company
+ },
+ get_query_filters: {
+ enquiry_type: "Sales"
+ }
+ })
+ }, __("Get items from"));
// Get items from open Material Requests based on supplier
this.frm.add_custom_button(__('Possible Supplier'), function() {
// Create a dialog window for the user to pick their supplier
diff --git a/erpnext/config/stock.py b/erpnext/config/stock.py
index a98c40e..d6b18fd 100644
--- a/erpnext/config/stock.py
+++ b/erpnext/config/stock.py
@@ -105,6 +105,11 @@
"name": "Pricing Rule",
"description": _("Rules for applying pricing and discount.")
},
+ {
+ "type": "doctype",
+ "name": "Item Variant Settings",
+ "description": _("Item Variant Settings."),
+ },
]
},
diff --git a/erpnext/controllers/item_variant.py b/erpnext/controllers/item_variant.py
index 967c133..ff11eb2 100644
--- a/erpnext/controllers/item_variant.py
+++ b/erpnext/controllers/item_variant.py
@@ -180,10 +180,10 @@
# don't copy manufacturer values if based on part no
exclude_fields += ['manufacturer', 'manufacturer_part_no']
+ allow_fields = [d.field_name for d in frappe.get_all("Variant Field", fields = ['field_name'])]
for field in item.meta.fields:
# "Table" is part of `no_value_field` but we shouldn't ignore tables
- if (field.fieldtype == 'Table' or field.fieldtype not in no_value_fields) \
- and (not field.no_copy) and field.fieldname not in exclude_fields:
+ if (field.reqd or field.fieldname in allow_fields) and field.fieldname not in exclude_fields:
if variant.get(field.fieldname) != item.get(field.fieldname):
variant.set(field.fieldname, item.get(field.fieldname))
variant.variant_of = item.name
diff --git a/erpnext/controllers/tests/test_item_variant.py b/erpnext/controllers/tests/test_item_variant.py
index 9fc45d2..34d6360 100644
--- a/erpnext/controllers/tests/test_item_variant.py
+++ b/erpnext/controllers/tests/test_item_variant.py
@@ -4,6 +4,7 @@
import json
import unittest
+from erpnext.stock.doctype.item.test_item import set_item_variant_settings
from erpnext.controllers.item_variant import copy_attributes_to_variant, make_variant_item_code
# python 3 compatibility stuff
@@ -54,5 +55,7 @@
class TestItemVariant(unittest.TestCase):
def test_tables_in_template_copied_to_variant(self):
+ fields = [{'field_name': 'quality_parameters'}]
+ set_item_variant_settings(fields)
variant = make_item_variant()
self.assertNotEqual(variant.get("quality_parameters"), [])
diff --git a/erpnext/crm/doctype/opportunity/opportunity.py b/erpnext/crm/doctype/opportunity/opportunity.py
index 8bc7ad8..4251cae 100644
--- a/erpnext/crm/doctype/opportunity/opportunity.py
+++ b/erpnext/crm/doctype/opportunity/opportunity.py
@@ -246,6 +246,27 @@
return doclist
@frappe.whitelist()
+def make_request_for_quotation(source_name, target_doc=None):
+ doclist = get_mapped_doc("Opportunity", source_name, {
+ "Opportunity": {
+ "doctype": "Request for Quotation",
+ "validation": {
+ "enquiry_type": ["=", "Sales"]
+ }
+ },
+ "Opportunity Item": {
+ "doctype": "Request for Quotation Item",
+ "field_map": [
+ ["name", "opportunity_item"],
+ ["parent", "opportunity"],
+ ["uom", "uom"]
+ ]
+ }
+ }, target_doc)
+
+ return doclist
+
+@frappe.whitelist()
def make_supplier_quotation(source_name, target_doc=None):
doclist = get_mapped_doc("Opportunity", source_name, {
"Opportunity": {
@@ -284,4 +305,4 @@
doc.status = "Closed"
doc.flags.ignore_permissions = True
doc.flags.ignore_mandatory = True
- doc.save()
\ No newline at end of file
+ doc.save()
diff --git a/erpnext/docs/assets/img/stock/item_variants_settings.png b/erpnext/docs/assets/img/stock/item_variants_settings.png
new file mode 100644
index 0000000..82b909f
--- /dev/null
+++ b/erpnext/docs/assets/img/stock/item_variants_settings.png
Binary files differ
diff --git a/erpnext/docs/user/manual/en/accounts/setup/accounts-settings.md b/erpnext/docs/user/manual/en/accounts/setup/accounts-settings.md
index 3bada56..f47f6e6 100644
--- a/erpnext/docs/user/manual/en/accounts/setup/accounts-settings.md
+++ b/erpnext/docs/user/manual/en/accounts/setup/accounts-settings.md
@@ -13,4 +13,8 @@
* Unlink Payment on Cancellation of Invoice: If checked, system will unlink the payment against the invoice. Otherwise, it will show the link error.
+* Allow Stale Exchange Rate: This should be unchecked if you want ERPNext to check the age of records fetched from Currency Exchange in foreign currency transactions. If it is unchecked, the exchange rate field will be read-only in documents.
+
+* Stale Days: The number of days to use when deciding if a Currency Exchange record is stale. E.g If Currency Exchange records are to be updated every day, the Stale Days should be set as 1.
+
{next}
diff --git a/erpnext/docs/user/manual/en/stock/item/item-variants.md b/erpnext/docs/user/manual/en/stock/item/item-variants.md
index 8b6da16..eeee0e1 100644
--- a/erpnext/docs/user/manual/en/stock/item/item-variants.md
+++ b/erpnext/docs/user/manual/en/stock/item/item-variants.md
@@ -48,4 +48,11 @@
<img class='screenshot' alt='Setup Item Variant by Manufacturer'
src='/docs/assets/img/stock/set-variant-by-mfg.png'>
-The naming of the variant will be the name (ID) of the template Item with a number suffix. e.g. "ITEM000" will have variant "ITEM000-1"
\ No newline at end of file
+The naming of the variant will be the name (ID) of the template Item with a number suffix. e.g. "ITEM000" will have variant "ITEM000-1"
+
+### Update Variants Based on Template
+To update the value in the variants items from the template item, select the respective fields first in the Item Variant Settings page. After that system will update the value of that fields in the variants if that values has been changed in the template item.
+
+To set the fields Goto Stock > Item Variant Settings
+<img class='screenshot' alt='Item Variant Settings'
+ src='/docs/assets/img/stock/item_variants_settings.png'>
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 19d713e..030d44c 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -439,9 +439,11 @@
erpnext.patches.v8_9.add_setup_progress_actions #08-09-2017 #26-09-2017
erpnext.patches.v8_9.rename_company_sales_target_field
erpnext.patches.v8_8.set_bom_rate_as_per_uom
-erpnext.patches.v9_0.remove_subscription_module
erpnext.patches.v8_7.make_subscription_from_recurring_data
+erpnext.patches.v8_8.add_new_fields_in_accounts_settings
+erpnext.patches.v9_0.remove_subscription_module
erpnext.patches.v8_9.set_print_zero_amount_taxes
erpnext.patches.v8_9.set_default_customer_group
erpnext.patches.v8_9.remove_employee_from_salary_structure_parent
erpnext.patches.v8_9.delete_gst_doctypes_for_outside_india_accounts
+erpnext.patches.v8_9.set_default_fields_in_variant_settings
\ No newline at end of file
diff --git a/erpnext/patches/v8_8/add_new_fields_in_accounts_settings.py b/erpnext/patches/v8_8/add_new_fields_in_accounts_settings.py
new file mode 100644
index 0000000..bd25f15
--- /dev/null
+++ b/erpnext/patches/v8_8/add_new_fields_in_accounts_settings.py
@@ -0,0 +1,9 @@
+from __future__ import unicode_literals
+import frappe
+
+
+def execute():
+ frappe.db.sql(
+ "INSERT INTO `tabSingles` (`doctype`, `field`, `value`) VALUES ('Accounts Settings', 'allow_stale', '1'), "
+ "('Accounts Settings', 'stale_days', '1')"
+ )
diff --git a/erpnext/patches/v8_9/set_default_fields_in_variant_settings.py b/erpnext/patches/v8_9/set_default_fields_in_variant_settings.py
new file mode 100644
index 0000000..a550d09
--- /dev/null
+++ b/erpnext/patches/v8_9/set_default_fields_in_variant_settings.py
@@ -0,0 +1,13 @@
+# Copyright (c) 2017, Frappe and Contributors
+# License: GNU General Public License v3. See license.txt
+
+from __future__ import unicode_literals
+import frappe
+
+def execute():
+ frappe.reload_doc('stock', 'doctype', 'item_variant_settings')
+ frappe.reload_doc('stock', 'doctype', 'variant_field')
+
+ doc = frappe.get_doc('Item Variant Settings')
+ doc.set_default_fields()
+ doc.save()
\ No newline at end of file
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index a5ef15e..5f35ea4 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -519,6 +519,7 @@
},
conversion_rate: function() {
+ const me = this.frm;
if(this.frm.doc.currency === this.get_company_currency()) {
this.frm.set_value("conversion_rate", 1.0);
}
@@ -536,6 +537,12 @@
}
}
+ // Make read only if Accounts Settings doesn't allow stale rates
+ frappe.model.get_value("Accounts Settings", null, "allow_stale",
+ function(d){
+ me.set_df_property("conversion_rate", "read_only", cint(d.allow_stale) ? 0 : 1);
+ }
+ );
},
set_actual_charges_based_on_currency: function() {
diff --git a/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py b/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py
index a477379..6d5848a 100644
--- a/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py
+++ b/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py
@@ -1,9 +1,9 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
-
-
import frappe, unittest
+from erpnext.setup.utils import get_exchange_rate
+
test_records = frappe.get_test_records('Currency Exchange')
@@ -28,11 +28,21 @@
class TestCurrencyExchange(unittest.TestCase):
- def test_exchnage_rate(self):
- from erpnext.setup.utils import get_exchange_rate
+ def clear_cache(self):
+ cache = frappe.cache()
+ key = "currency_exchange_rate:{0}:{1}".format("USD", "INR")
+ cache.delete(key)
+ def tearDown(self):
+ frappe.db.set_value("Accounts Settings", None, "allow_stale", 1)
+ self.clear_cache()
+
+ def test_exchange_rate(self):
save_new_records(test_records)
+ frappe.db.set_value("Accounts Settings", None, "allow_stale", 1)
+
+ # Start with allow_stale is True
exchange_rate = get_exchange_rate("USD", "INR", "2016-01-01")
self.assertEqual(exchange_rate, 60.0)
@@ -43,6 +53,51 @@
self.assertEqual(exchange_rate, 62.9)
# Exchange rate as on 15th Dec, 2015, should be fetched from fixer.io
+ self.clear_cache()
exchange_rate = get_exchange_rate("USD", "INR", "2015-12-15")
self.assertFalse(exchange_rate == 60)
- self.assertEqual(exchange_rate, 66.894)
\ No newline at end of file
+ self.assertEqual(exchange_rate, 66.894)
+
+ def test_exchange_rate_strict(self):
+ # strict currency settings
+ frappe.db.set_value("Accounts Settings", None, "allow_stale", 0)
+ frappe.db.set_value("Accounts Settings", None, "stale_days", 1)
+
+ exchange_rate = get_exchange_rate("USD", "INR", "2016-01-01")
+ self.assertEqual(exchange_rate, 60.0)
+
+ # Will fetch from fixer.io
+ self.clear_cache()
+ exchange_rate = get_exchange_rate("USD", "INR", "2016-01-15")
+ self.assertEqual(exchange_rate, 67.79)
+
+ exchange_rate = get_exchange_rate("USD", "INR", "2016-01-30")
+ self.assertEqual(exchange_rate, 62.9)
+
+ # Exchange rate as on 15th Dec, 2015, should be fetched from fixer.io
+ self.clear_cache()
+ exchange_rate = get_exchange_rate("USD", "INR", "2015-12-15")
+ self.assertEqual(exchange_rate, 66.894)
+
+ exchange_rate = get_exchange_rate("INR", "NGN", "2016-01-10")
+ self.assertEqual(exchange_rate, 65.1)
+
+ # NGN is not available on fixer.io so these should return 0
+ exchange_rate = get_exchange_rate("INR", "NGN", "2016-01-09")
+ self.assertEqual(exchange_rate, 0)
+
+ exchange_rate = get_exchange_rate("INR", "NGN", "2016-01-11")
+ self.assertEqual(exchange_rate, 0)
+
+ def test_exchange_rate_strict_switched(self):
+ # Start with allow_stale is True
+ exchange_rate = get_exchange_rate("USD", "INR", "2016-01-15")
+ self.assertEqual(exchange_rate, 65.1)
+
+ frappe.db.set_value("Accounts Settings", None, "allow_stale", 0)
+ frappe.db.set_value("Accounts Settings", None, "stale_days", 1)
+
+ # Will fetch from fixer.io
+ self.clear_cache()
+ exchange_rate = get_exchange_rate("USD", "INR", "2016-01-15")
+ self.assertEqual(exchange_rate, 67.79)
\ No newline at end of file
diff --git a/erpnext/setup/doctype/currency_exchange/test_records.json b/erpnext/setup/doctype/currency_exchange/test_records.json
index d2f658b..0c9cfbb 100644
--- a/erpnext/setup/doctype/currency_exchange/test_records.json
+++ b/erpnext/setup/doctype/currency_exchange/test_records.json
@@ -33,5 +33,12 @@
"exchange_rate": 62.9,
"from_currency": "USD",
"to_currency": "INR"
+ },
+ {
+ "doctype": "Currency Exchange",
+ "date": "2016-01-10",
+ "exchange_rate": 65.1,
+ "from_currency": "INR",
+ "to_currency": "NGN"
}
]
\ No newline at end of file
diff --git a/erpnext/setup/setup_wizard/setup_wizard.py b/erpnext/setup/setup_wizard/setup_wizard.py
index d3e4a08..bf92217 100644
--- a/erpnext/setup/setup_wizard/setup_wizard.py
+++ b/erpnext/setup/setup_wizard/setup_wizard.py
@@ -33,6 +33,7 @@
create_feed_and_todo()
create_email_digest()
create_letter_head(args)
+ set_no_copy_fields_in_variant_settings()
if args.get('domain').lower() == 'education':
create_academic_year()
@@ -354,6 +355,12 @@
fileurl = save_file(filename, content, "Letter Head", _("Standard"), decode=True).file_url
frappe.db.set_value("Letter Head", _("Standard"), "content", "<img src='%s' style='max-width: 100%%;'>" % fileurl)
+def set_no_copy_fields_in_variant_settings():
+ # set no copy fields of an item doctype to item variant settings
+ doc = frappe.get_doc('Item Variant Settings')
+ doc.set_default_fields()
+ doc.save()
+
def create_logo(args):
if args.get("attach_logo"):
attach_logo = args.get("attach_logo").split(",")
diff --git a/erpnext/setup/utils.py b/erpnext/setup/utils.py
index bdbf3f4..f003ce4 100644
--- a/erpnext/setup/utils.py
+++ b/erpnext/setup/utils.py
@@ -4,7 +4,7 @@
from __future__ import unicode_literals
import frappe
from frappe import _
-from frappe.utils import flt
+from frappe.utils import flt, add_days
from frappe.utils import get_datetime_str, nowdate
def get_root_of(doctype):
@@ -56,8 +56,6 @@
@frappe.whitelist()
def get_exchange_rate(from_currency, to_currency, transaction_date=None):
- if not transaction_date:
- transaction_date = nowdate()
if not (from_currency and to_currency):
# manqala 19/09/2016: Should this be an empty return or should it throw and exception?
return
@@ -65,13 +63,27 @@
if from_currency == to_currency:
return 1
+ if not transaction_date:
+ transaction_date = nowdate()
+
+ currency_settings = frappe.get_doc("Accounts Settings").as_dict()
+ allow_stale_rates = currency_settings.get("allow_stale")
+
+ filters = [
+ ["date", "<=", get_datetime_str(transaction_date)],
+ ["from_currency", "=", from_currency],
+ ["to_currency", "=", to_currency]
+ ]
+
+ if not allow_stale_rates:
+ stale_days = currency_settings.get("stale_days")
+ checkpoint_date = add_days(transaction_date, -stale_days)
+ filters.append(["date", ">", get_datetime_str(checkpoint_date)])
+
# cksgb 19/09/2016: get last entry in Currency Exchange with from_currency and to_currency.
- entries = frappe.get_all("Currency Exchange", fields = ["exchange_rate"],
- filters=[
- ["date", "<=", get_datetime_str(transaction_date)],
- ["from_currency", "=", from_currency],
- ["to_currency", "=", to_currency]
- ], order_by="date desc", limit=1)
+ entries = frappe.get_all(
+ "Currency Exchange", fields=["exchange_rate"], filters=filters, order_by="date desc",
+ limit=1)
if entries:
return flt(entries[0].exchange_rate)
diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js
index 7837f8c..03b93c0 100644
--- a/erpnext/stock/doctype/item/item.js
+++ b/erpnext/stock/doctype/item/item.js
@@ -97,6 +97,12 @@
}
frappe.set_route('Form', 'Item', new_item.name);
});
+
+ if(frm.doc.has_variants) {
+ frm.add_custom_button(__("Item Variant Settings"), function() {
+ frappe.set_route("Form", "Item Variant Settings");
+ }, __("View"));
+ }
},
validate: function(frm){
diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py
index f2ea1d8..a810665 100644
--- a/erpnext/stock/doctype/item/item.py
+++ b/erpnext/stock/doctype/item/item.py
@@ -100,6 +100,7 @@
def on_update(self):
invalidate_cache_for_item(self)
self.validate_name_with_item_group()
+ self.update_variants()
self.update_item_price()
self.update_template_item()
@@ -607,9 +608,24 @@
if not template_item.show_in_website:
template_item.show_in_website = 1
+ template_item.flags.dont_update_variants = True
template_item.flags.ignore_permissions = True
template_item.save()
+ def update_variants(self):
+ if self.flags.dont_update_variants:
+ return
+ if self.has_variants:
+ updated = []
+ variants = frappe.db.get_all("Item", fields=["item_code"], filters={"variant_of": self.name })
+ for d in variants:
+ variant = frappe.get_doc("Item", d)
+ copy_attributes_to_variant(self, variant)
+ variant.save()
+ updated.append(d.item_code)
+ if updated:
+ frappe.msgprint(_("Item Variants {0} updated").format(", ".join(updated)))
+
def validate_has_variants(self):
if not self.has_variants and frappe.db.get_value("Item", self.name, "has_variants"):
if frappe.db.exists("Item", {"variant_of": self.name}):
diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py
index 2a8e434..34e3af6 100644
--- a/erpnext/stock/doctype/item/test_item.py
+++ b/erpnext/stock/doctype/item/test_item.py
@@ -119,6 +119,37 @@
variant.item_code = "_Test Variant Item-L-duplicate"
self.assertRaises(ItemVariantExistsError, variant.save)
+ def test_copy_fields_from_template_to_variants(self):
+ fields = [{'field_name': 'item_group'}, {'field_name': 'is_stock_item'}]
+ allow_fields = [d.get('field_name') for d in fields]
+ set_item_variant_settings(fields)
+
+ if not frappe.db.get_value('Item Attribute Value',
+ {'parent': 'Test Size', 'attribute_value': 'Extra Large'}, 'name'):
+ item_attribute = frappe.get_doc('Item Attribute', 'Test Size')
+ item_attribute.append('item_attribute_values', {
+ 'attribute_value' : 'Extra Large',
+ 'abbr': 'XL'
+ })
+ item_attribute.save()
+
+ variant = create_variant("_Test Variant Item", {"Test Size": "Extra Large"})
+ variant.item_code = "_Test Variant Item-XL"
+ variant.item_name = "_Test Variant Item-XL"
+ variant.save()
+
+ template = frappe.get_doc('Item', '_Test Variant Item')
+ template.item_group = "_Test Item Group D"
+ template.save()
+
+ variant = frappe.get_doc('Item', '_Test Variant Item-XL')
+ for fieldname in allow_fields:
+ self.assertEquals(template.get(fieldname), variant.get(fieldname))
+
+ template = frappe.get_doc('Item', '_Test Variant Item')
+ template.item_group = "_Test Item Group Desktops"
+ template.save()
+
def test_make_item_variant_with_numeric_values(self):
# cleanup
for d in frappe.db.get_all('Item', filters={'variant_of':
@@ -194,6 +225,9 @@
{"item_code": "Test Item for Merging 2", "warehouse": "_Test Warehouse 1 - _TC"}))
def test_item_variant_by_manufacturer(self):
+ fields = [{'field_name': 'description'}, {'field_name': 'variant_based_on'}]
+ set_item_variant_settings(fields)
+
if frappe.db.exists('Item', '_Test Variant Mfg'):
frappe.delete_doc('Item', '_Test Variant Mfg')
if frappe.db.exists('Item', '_Test Variant Mfg-1'):
@@ -227,6 +261,10 @@
self.assertEquals(variant.manufacturer, 'MSG1')
self.assertEquals(variant.manufacturer_part_no, '007')
+def set_item_variant_settings(fields):
+ doc = frappe.get_doc('Item Variant Settings')
+ doc.set('fields', fields)
+ doc.save()
def make_item_variant():
if not frappe.db.exists("Item", "_Test Variant Item-S"):
diff --git a/erpnext/stock/doctype/item_variant_settings/__init__.py b/erpnext/stock/doctype/item_variant_settings/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/stock/doctype/item_variant_settings/__init__.py
diff --git a/erpnext/stock/doctype/item_variant_settings/item_variant_settings.js b/erpnext/stock/doctype/item_variant_settings/item_variant_settings.js
new file mode 100644
index 0000000..f3404cc
--- /dev/null
+++ b/erpnext/stock/doctype/item_variant_settings/item_variant_settings.js
@@ -0,0 +1,22 @@
+// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Item Variant Settings', {
+ setup: function(frm) {
+ const allow_fields = [];
+ const exclude_fields = ["item_code", "item_name", "show_in_website", "show_variant_in_website",
+ "opening_stock", "variant_of", "valuation_rate", "variant_based_on"];
+
+ frappe.model.with_doctype('Item', () => {
+ frappe.get_meta('Item').fields.forEach(d => {
+ if(!in_list(['HTML', 'Section Break', 'Column Break', 'Button', 'Read Only'], d.fieldtype)
+ && !d.no_copy && !in_list(exclude_fields, d.fieldname)) {
+ allow_fields.push(d.fieldname);
+ }
+ });
+
+ const child = frappe.meta.get_docfield("Variant Field", "field_name", frm.doc.name);
+ child.options = allow_fields;
+ });
+ }
+});
diff --git a/erpnext/stock/doctype/item_variant_settings/item_variant_settings.json b/erpnext/stock/doctype/item_variant_settings/item_variant_settings.json
new file mode 100644
index 0000000..a29137c
--- /dev/null
+++ b/erpnext/stock/doctype/item_variant_settings/item_variant_settings.json
@@ -0,0 +1,143 @@
+{
+ "allow_copy": 0,
+ "allow_guest_to_view": 0,
+ "allow_import": 0,
+ "allow_rename": 0,
+ "beta": 0,
+ "creation": "2017-08-29 16:38:31.173830",
+ "custom": 0,
+ "docstatus": 0,
+ "doctype": "DocType",
+ "document_type": "",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "fields": [
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "copy_fields_to_variant",
+ "fieldtype": "Section Break",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Copy Fields to Variant",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "fields",
+ "fieldtype": "Table",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Fields",
+ "length": 0,
+ "no_copy": 0,
+ "options": "Variant Field",
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "unique": 0
+ }
+ ],
+ "has_web_view": 0,
+ "hide_heading": 0,
+ "hide_toolbar": 0,
+ "idx": 0,
+ "image_view": 0,
+ "in_create": 0,
+ "is_submittable": 0,
+ "issingle": 1,
+ "istable": 0,
+ "max_attachments": 0,
+ "modified": "2017-09-11 12:05:16.288601",
+ "modified_by": "rohit@erpnext.com",
+ "module": "Stock",
+ "name": "Item Variant Settings",
+ "name_case": "",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "amend": 0,
+ "apply_user_permissions": 0,
+ "cancel": 0,
+ "create": 1,
+ "delete": 0,
+ "email": 1,
+ "export": 0,
+ "if_owner": 0,
+ "import": 0,
+ "permlevel": 0,
+ "print": 1,
+ "read": 1,
+ "report": 0,
+ "role": "System Manager",
+ "set_user_permissions": 0,
+ "share": 1,
+ "submit": 0,
+ "write": 1
+ },
+ {
+ "amend": 0,
+ "apply_user_permissions": 0,
+ "cancel": 0,
+ "create": 1,
+ "delete": 0,
+ "email": 1,
+ "export": 0,
+ "if_owner": 0,
+ "import": 0,
+ "permlevel": 0,
+ "print": 1,
+ "read": 1,
+ "report": 0,
+ "role": "Item Manager",
+ "set_user_permissions": 0,
+ "share": 1,
+ "submit": 0,
+ "write": 1
+ }
+ ],
+ "quick_entry": 1,
+ "read_only": 0,
+ "read_only_onload": 0,
+ "show_name_in_global_search": 0,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1,
+ "track_seen": 0
+}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/item_variant_settings/item_variant_settings.py b/erpnext/stock/doctype/item_variant_settings/item_variant_settings.py
new file mode 100644
index 0000000..80462d1
--- /dev/null
+++ b/erpnext/stock/doctype/item_variant_settings/item_variant_settings.py
@@ -0,0 +1,23 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2017, 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 ItemVariantSettings(Document):
+ def set_default_fields(self):
+ self.fields = []
+ fields = frappe.get_meta('Item').fields
+ exclude_fields = ["item_code", "item_name", "show_in_website", "show_variant_in_website",
+ "standard_rate", "opening_stock", "image", "description",
+ "variant_of", "valuation_rate", "description", "variant_based_on",
+ "website_image", "thumbnail", "website_specifiations", "web_long_description"]
+
+ for d in fields:
+ if not d.no_copy and d.fieldname not in exclude_fields and \
+ d.fieldtype not in ['HTML', 'Section Break', 'Column Break', 'Button', 'Read Only']:
+ self.append('fields', {
+ 'field_name': d.fieldname
+ })
\ No newline at end of file
diff --git a/erpnext/stock/doctype/item_variant_settings/test_item_variant_settings.js b/erpnext/stock/doctype/item_variant_settings/test_item_variant_settings.js
new file mode 100644
index 0000000..3b3bf94
--- /dev/null
+++ b/erpnext/stock/doctype/item_variant_settings/test_item_variant_settings.js
@@ -0,0 +1,23 @@
+/* eslint-disable */
+// rename this file from _test_[name] to test_[name] to activate
+// and remove above this line
+
+QUnit.test("test: Item Variant Settings", function (assert) {
+ let done = assert.async();
+
+ // number of asserts
+ assert.expect(1);
+
+ frappe.run_serially([
+ // insert a new Item Variant Settings
+ () => frappe.tests.make('Item Variant Settings', [
+ // values to be set
+ {key: 'value'}
+ ]),
+ () => {
+ assert.equal(cur_frm.doc.key, 'value');
+ },
+ () => done()
+ ]);
+
+});
diff --git a/erpnext/stock/doctype/item_variant_settings/test_item_variant_settings.py b/erpnext/stock/doctype/item_variant_settings/test_item_variant_settings.py
new file mode 100644
index 0000000..9a800c0
--- /dev/null
+++ b/erpnext/stock/doctype/item_variant_settings/test_item_variant_settings.py
@@ -0,0 +1,9 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+import unittest
+
+class TestItemVariantSettings(unittest.TestCase):
+ pass
diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
index 7fa232e..4bcbcc4 100644
--- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
@@ -11,6 +11,7 @@
from erpnext.stock.doctype.stock_ledger_entry.stock_ledger_entry import StockFreezeError
from erpnext.stock.stock_ledger import get_previous_sle
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import create_stock_reconciliation
+from erpnext.stock.doctype.item.test_item import set_item_variant_settings
from frappe.tests.test_permissions import set_user_permission_doctypes
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
from erpnext.accounts.doctype.account.test_account import get_inventory_account
@@ -79,6 +80,19 @@
self._test_auto_material_request("_Test Item", material_request_type="Transfer")
def test_auto_material_request_for_variant(self):
+ fields = [{'field_name': 'reorder_levels'}]
+ set_item_variant_settings(fields)
+ template = frappe.get_doc("Item", "_Test Variant Item")
+
+ if not template.reorder_levels:
+ template.append('reorder_levels', {
+ "material_request_type": "Purchase",
+ "warehouse": "_Test Warehouse - _TC",
+ "warehouse_reorder_level": 20,
+ "warehouse_reorder_qty": 20
+ })
+
+ template.save()
self._test_auto_material_request("_Test Variant Item-S")
def test_auto_material_request_for_warehouse_group(self):
diff --git a/erpnext/stock/doctype/variant_field/__init__.py b/erpnext/stock/doctype/variant_field/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/stock/doctype/variant_field/__init__.py
diff --git a/erpnext/stock/doctype/variant_field/test_variant_field.js b/erpnext/stock/doctype/variant_field/test_variant_field.js
new file mode 100644
index 0000000..2600a10
--- /dev/null
+++ b/erpnext/stock/doctype/variant_field/test_variant_field.js
@@ -0,0 +1,23 @@
+/* eslint-disable */
+// rename this file from _test_[name] to test_[name] to activate
+// and remove above this line
+
+QUnit.test("test: Variant Field", function (assert) {
+ let done = assert.async();
+
+ // number of asserts
+ assert.expect(1);
+
+ frappe.run_serially([
+ // insert a new Variant Field
+ () => frappe.tests.make('Variant Field', [
+ // values to be set
+ {key: 'value'}
+ ]),
+ () => {
+ assert.equal(cur_frm.doc.key, 'value');
+ },
+ () => done()
+ ]);
+
+});
diff --git a/erpnext/stock/doctype/variant_field/test_variant_field.py b/erpnext/stock/doctype/variant_field/test_variant_field.py
new file mode 100644
index 0000000..53024bd
--- /dev/null
+++ b/erpnext/stock/doctype/variant_field/test_variant_field.py
@@ -0,0 +1,9 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+import unittest
+
+class TestVariantField(unittest.TestCase):
+ pass
diff --git a/erpnext/stock/doctype/variant_field/variant_field.js b/erpnext/stock/doctype/variant_field/variant_field.js
new file mode 100644
index 0000000..13db3f9
--- /dev/null
+++ b/erpnext/stock/doctype/variant_field/variant_field.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Variant Field', {
+ refresh: function() {
+
+ }
+});
diff --git a/erpnext/stock/doctype/variant_field/variant_field.json b/erpnext/stock/doctype/variant_field/variant_field.json
new file mode 100644
index 0000000..ae90884
--- /dev/null
+++ b/erpnext/stock/doctype/variant_field/variant_field.json
@@ -0,0 +1,72 @@
+{
+ "allow_copy": 0,
+ "allow_guest_to_view": 0,
+ "allow_import": 0,
+ "allow_rename": 0,
+ "beta": 0,
+ "creation": "2017-08-29 16:33:33.978574",
+ "custom": 0,
+ "docstatus": 0,
+ "doctype": "DocType",
+ "document_type": "",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "fields": [
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "field_name",
+ "fieldtype": "Select",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 1,
+ "in_standard_filter": 0,
+ "label": "Field Name",
+ "length": 0,
+ "no_copy": 0,
+ "options": "",
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 1,
+ "search_index": 0,
+ "set_only_once": 0,
+ "unique": 0
+ }
+ ],
+ "has_web_view": 0,
+ "hide_heading": 0,
+ "hide_toolbar": 0,
+ "idx": 0,
+ "image_view": 0,
+ "in_create": 0,
+ "is_submittable": 0,
+ "issingle": 0,
+ "istable": 1,
+ "max_attachments": 0,
+ "modified": "2017-08-29 17:19:20.353197",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "Variant Field",
+ "name_case": "",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "read_only": 0,
+ "read_only_onload": 0,
+ "show_name_in_global_search": 0,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1,
+ "track_seen": 0
+}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/variant_field/variant_field.py b/erpnext/stock/doctype/variant_field/variant_field.py
new file mode 100644
index 0000000..a77301e
--- /dev/null
+++ b/erpnext/stock/doctype/variant_field/variant_field.py
@@ -0,0 +1,9 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+from frappe.model.document import Document
+
+class VariantField(Document):
+ pass