Merge branch 'develop' into payment-terms

This commit is contained in:
tunde 2017-09-21 11:21:05 +01:00
commit 82de375ba0
38 changed files with 751 additions and 32 deletions

View File

@ -286,6 +286,99 @@
"search_index": 0, "search_index": 0,
"set_only_once": 0, "set_only_once": 0,
"unique": 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, "has_web_view": 0,
@ -299,7 +392,7 @@
"issingle": 1, "issingle": 1,
"istable": 0, "istable": 0,
"max_attachments": 0, "max_attachments": 0,
"modified": "2017-06-16 17:39:50.614522", "modified": "2017-09-05 10:10:03.117505",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Accounts Settings", "name": "Accounts Settings",

View File

@ -5,10 +5,20 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe import _ from frappe.utils import cint
from frappe.utils import cint, comma_and
from frappe.model.document import Document from frappe.model.document import Document
class AccountsSettings(Document): class AccountsSettings(Document):
def on_update(self): def on_update(self):
pass 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)

View File

@ -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);
}
};

View File

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

View File

@ -402,6 +402,13 @@ frappe.ui.form.on('Payment Entry', {
frm.events.set_difference_amount(frm); 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) { target_exchange_rate: function(frm) {
@ -420,6 +427,13 @@ frappe.ui.form.on('Payment Entry', {
frm.events.set_difference_amount(frm); frm.events.set_difference_amount(frm);
} }
frm.set_paid_amount_based_on_received_amount = false; 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) { paid_amount: function(frm) {

View File

@ -3,15 +3,6 @@
frappe.ui.form.on('Subscription', { frappe.ui.form.on('Subscription', {
setup: function(frm) { setup: function(frm) {
if(frm.doc.__islocal) {
var last_route = frappe.route_history.slice(-2, -1)[0];
if(frappe.dynamic_link && frappe.dynamic_link.doc
&& frappe.dynamic_link.doc.name==last_route[2]) {
frm.set_value('reference_doctype', last_route[1]);
frm.set_value('reference_document', last_route[2]);
}
}
frm.fields_dict['reference_document'].get_query = function() { frm.fields_dict['reference_document'].get_query = function() {
return { return {
filters: { filters: {

View File

@ -0,0 +1,8 @@
#### POS
- Now user has an option to enable or disable Offline POS mode from POS Settings
- Provision to select the Item's serial number from the dropdown while adding item in the cart
- Indicator for stock availability in Online POS Mode.
#### Subscription
- Setup recurring documents using **Subscription**
- User can schedule the subscription for doctypes other than Sales Invoice, Purchase Invoice etc.

View File

@ -105,6 +105,11 @@ def get_data():
"name": "Pricing Rule", "name": "Pricing Rule",
"description": _("Rules for applying pricing and discount.") "description": _("Rules for applying pricing and discount.")
}, },
{
"type": "doctype",
"name": "Item Variant Settings",
"description": _("Item Variant Settings."),
},
] ]
}, },

View File

@ -180,10 +180,10 @@ def copy_attributes_to_variant(item, variant):
# don't copy manufacturer values if based on part no # don't copy manufacturer values if based on part no
exclude_fields += ['manufacturer', 'manufacturer_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: for field in item.meta.fields:
# "Table" is part of `no_value_field` but we shouldn't ignore tables # "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) \ if (field.reqd or field.fieldname in allow_fields) and field.fieldname not in exclude_fields:
and (not field.no_copy) and field.fieldname not in exclude_fields:
if variant.get(field.fieldname) != item.get(field.fieldname): if variant.get(field.fieldname) != item.get(field.fieldname):
variant.set(field.fieldname, item.get(field.fieldname)) variant.set(field.fieldname, item.get(field.fieldname))
variant.variant_of = item.name variant.variant_of = item.name

View File

@ -4,6 +4,7 @@ import frappe
import json import json
import unittest 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 from erpnext.controllers.item_variant import copy_attributes_to_variant, make_variant_item_code
# python 3 compatibility stuff # python 3 compatibility stuff
@ -54,5 +55,7 @@ def make_item_variant():
class TestItemVariant(unittest.TestCase): class TestItemVariant(unittest.TestCase):
def test_tables_in_template_copied_to_variant(self): def test_tables_in_template_copied_to_variant(self):
fields = [{'field_name': 'quality_parameters'}]
set_item_variant_settings(fields)
variant = make_item_variant() variant = make_item_variant()
self.assertNotEqual(variant.get("quality_parameters"), []) self.assertNotEqual(variant.get("quality_parameters"), [])

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

View File

@ -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. * 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} {next}

View File

@ -49,3 +49,10 @@ When you make a new Variant, the system will prompt you to select a Manufacturer
src='/docs/assets/img/stock/set-variant-by-mfg.png'> 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" 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'>

View File

@ -439,12 +439,14 @@ erpnext.patches.v8_7.set_offline_in_pos_settings #11-09-17
erpnext.patches.v8_9.add_setup_progress_actions #08-09-2017 erpnext.patches.v8_9.add_setup_progress_actions #08-09-2017
erpnext.patches.v8_9.rename_company_sales_target_field erpnext.patches.v8_9.rename_company_sales_target_field
erpnext.patches.v8_8.set_bom_rate_as_per_uom 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_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_print_zero_amount_taxes
erpnext.patches.v8_9.set_default_customer_group erpnext.patches.v8_9.set_default_customer_group
erpnext.patches.v8_9.remove_employee_from_salary_structure_parent 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.delete_gst_doctypes_for_outside_india_accounts
erpnext.patches.v8_9.set_default_fields_in_variant_settings
erpnext.patches.v8_10.add_due_date_to_gle erpnext.patches.v8_10.add_due_date_to_gle
erpnext.patches.v8_10.update_gl_due_date_for_pi_and_si erpnext.patches.v8_10.update_gl_due_date_for_pi_and_si
erpnext.patches.v8_10.add_payment_terms_field_to_supplier erpnext.patches.v8_10.add_payment_terms_field_to_supplier

View File

@ -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')"
)

View File

@ -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()

View File

@ -523,6 +523,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
}, },
conversion_rate: function() { conversion_rate: function() {
const me = this.frm;
if(this.frm.doc.currency === this.get_company_currency()) { if(this.frm.doc.currency === this.get_company_currency()) {
this.frm.set_value("conversion_rate", 1.0); this.frm.set_value("conversion_rate", 1.0);
} }
@ -540,6 +541,12 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
} }
} }
// 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() { set_actual_charges_based_on_currency: function() {

View File

@ -1,9 +1,9 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe, unittest import frappe, unittest
from erpnext.setup.utils import get_exchange_rate
test_records = frappe.get_test_records('Currency Exchange') test_records = frappe.get_test_records('Currency Exchange')
@ -28,11 +28,21 @@ def save_new_records(test_records):
class TestCurrencyExchange(unittest.TestCase): class TestCurrencyExchange(unittest.TestCase):
def test_exchnage_rate(self): def clear_cache(self):
from erpnext.setup.utils import get_exchange_rate 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) 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") exchange_rate = get_exchange_rate("USD", "INR", "2016-01-01")
self.assertEqual(exchange_rate, 60.0) self.assertEqual(exchange_rate, 60.0)
@ -43,6 +53,51 @@ class TestCurrencyExchange(unittest.TestCase):
self.assertEqual(exchange_rate, 62.9) self.assertEqual(exchange_rate, 62.9)
# Exchange rate as on 15th Dec, 2015, should be fetched from fixer.io # 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") exchange_rate = get_exchange_rate("USD", "INR", "2015-12-15")
self.assertFalse(exchange_rate == 60) self.assertFalse(exchange_rate == 60)
self.assertEqual(exchange_rate, 66.894) 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)

View File

@ -33,5 +33,12 @@
"exchange_rate": 62.9, "exchange_rate": 62.9,
"from_currency": "USD", "from_currency": "USD",
"to_currency": "INR" "to_currency": "INR"
},
{
"doctype": "Currency Exchange",
"date": "2016-01-10",
"exchange_rate": 65.1,
"from_currency": "INR",
"to_currency": "NGN"
} }
] ]

View File

@ -33,6 +33,7 @@ def setup_complete(args=None):
create_feed_and_todo() create_feed_and_todo()
create_email_digest() create_email_digest()
create_letter_head(args) create_letter_head(args)
set_no_copy_fields_in_variant_settings()
if args.get('domain').lower() == 'education': if args.get('domain').lower() == 'education':
create_academic_year() create_academic_year()
@ -354,6 +355,12 @@ def create_letter_head(args):
fileurl = save_file(filename, content, "Letter Head", _("Standard"), decode=True).file_url 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) 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): def create_logo(args):
if args.get("attach_logo"): if args.get("attach_logo"):
attach_logo = args.get("attach_logo").split(",") attach_logo = args.get("attach_logo").split(",")

View File

@ -4,7 +4,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils import flt from frappe.utils import flt, add_days
from frappe.utils import get_datetime_str, nowdate from frappe.utils import get_datetime_str, nowdate
def get_root_of(doctype): def get_root_of(doctype):
@ -56,8 +56,6 @@ def before_tests():
@frappe.whitelist() @frappe.whitelist()
def get_exchange_rate(from_currency, to_currency, transaction_date=None): 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): if not (from_currency and to_currency):
# manqala 19/09/2016: Should this be an empty return or should it throw and exception? # manqala 19/09/2016: Should this be an empty return or should it throw and exception?
return return
@ -65,13 +63,27 @@ def get_exchange_rate(from_currency, to_currency, transaction_date=None):
if from_currency == to_currency: if from_currency == to_currency:
return 1 return 1
# cksgb 19/09/2016: get last entry in Currency Exchange with from_currency and to_currency. if not transaction_date:
entries = frappe.get_all("Currency Exchange", fields = ["exchange_rate"], transaction_date = nowdate()
filters=[
currency_settings = frappe.get_doc("Accounts Settings").as_dict()
allow_stale_rates = currency_settings.get("allow_stale")
filters = [
["date", "<=", get_datetime_str(transaction_date)], ["date", "<=", get_datetime_str(transaction_date)],
["from_currency", "=", from_currency], ["from_currency", "=", from_currency],
["to_currency", "=", to_currency] ["to_currency", "=", to_currency]
], order_by="date desc", limit=1) ]
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=filters, order_by="date desc",
limit=1)
if entries: if entries:
return flt(entries[0].exchange_rate) return flt(entries[0].exchange_rate)

View File

@ -97,6 +97,12 @@ frappe.ui.form.on("Item", {
} }
frappe.set_route('Form', 'Item', new_item.name); 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){ validate: function(frm){

View File

@ -100,6 +100,7 @@ class Item(WebsiteGenerator):
def on_update(self): def on_update(self):
invalidate_cache_for_item(self) invalidate_cache_for_item(self)
self.validate_name_with_item_group() self.validate_name_with_item_group()
self.update_variants()
self.update_item_price() self.update_item_price()
self.update_template_item() self.update_template_item()
@ -607,9 +608,24 @@ class Item(WebsiteGenerator):
if not template_item.show_in_website: if not template_item.show_in_website:
template_item.show_in_website = 1 template_item.show_in_website = 1
template_item.flags.dont_update_variants = True
template_item.flags.ignore_permissions = True template_item.flags.ignore_permissions = True
template_item.save() 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): def validate_has_variants(self):
if not self.has_variants and frappe.db.get_value("Item", self.name, "has_variants"): if not self.has_variants and frappe.db.get_value("Item", self.name, "has_variants"):
if frappe.db.exists("Item", {"variant_of": self.name}): if frappe.db.exists("Item", {"variant_of": self.name}):

View File

@ -119,6 +119,37 @@ class TestItem(unittest.TestCase):
variant.item_code = "_Test Variant Item-L-duplicate" variant.item_code = "_Test Variant Item-L-duplicate"
self.assertRaises(ItemVariantExistsError, variant.save) 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): def test_make_item_variant_with_numeric_values(self):
# cleanup # cleanup
for d in frappe.db.get_all('Item', filters={'variant_of': for d in frappe.db.get_all('Item', filters={'variant_of':
@ -194,6 +225,9 @@ class TestItem(unittest.TestCase):
{"item_code": "Test Item for Merging 2", "warehouse": "_Test Warehouse 1 - _TC"})) {"item_code": "Test Item for Merging 2", "warehouse": "_Test Warehouse 1 - _TC"}))
def test_item_variant_by_manufacturer(self): 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'): if frappe.db.exists('Item', '_Test Variant Mfg'):
frappe.delete_doc('Item', '_Test Variant Mfg') frappe.delete_doc('Item', '_Test Variant Mfg')
if frappe.db.exists('Item', '_Test Variant Mfg-1'): if frappe.db.exists('Item', '_Test Variant Mfg-1'):
@ -227,6 +261,10 @@ class TestItem(unittest.TestCase):
self.assertEquals(variant.manufacturer, 'MSG1') self.assertEquals(variant.manufacturer, 'MSG1')
self.assertEquals(variant.manufacturer_part_no, '007') 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(): def make_item_variant():
if not frappe.db.exists("Item", "_Test Variant Item-S"): if not frappe.db.exists("Item", "_Test Variant Item-S"):

View File

@ -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;
});
}
});

View File

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

View File

@ -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
})

View File

@ -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()
]);
});

View File

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

View File

@ -11,6 +11,7 @@ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt \
from erpnext.stock.doctype.stock_ledger_entry.stock_ledger_entry import StockFreezeError from erpnext.stock.doctype.stock_ledger_entry.stock_ledger_entry import StockFreezeError
from erpnext.stock.stock_ledger import get_previous_sle 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.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 frappe.tests.test_permissions import set_user_permission_doctypes
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
from erpnext.accounts.doctype.account.test_account import get_inventory_account from erpnext.accounts.doctype.account.test_account import get_inventory_account
@ -79,6 +80,19 @@ class TestStockEntry(unittest.TestCase):
self._test_auto_material_request("_Test Item", material_request_type="Transfer") self._test_auto_material_request("_Test Item", material_request_type="Transfer")
def test_auto_material_request_for_variant(self): 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") self._test_auto_material_request("_Test Variant Item-S")
def test_auto_material_request_for_warehouse_group(self): def test_auto_material_request_for_warehouse_group(self):

View File

@ -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()
]);
});

View File

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

View File

@ -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() {
}
});

View File

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

View File

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