Fixed conflict

This commit is contained in:
Nabin Hait
2015-09-03 16:04:11 +05:30
23 changed files with 1194 additions and 1086 deletions

View File

@@ -49,6 +49,20 @@ DocTypes are easy to create but hard to maintain. If you find that there is a an
Tabs! Tabs!
#### Release Checklist
- Describe, in detail, what is in the pull request
- How to use the new feature?
- Test cases
- Change log
- Manual Pull Request Link
- Screencast. Should include:
- New Forms
- Linked Forms
- Linked Reports
- Print Views
### Copyright ### Copyright
Please see README.md Please see README.md

View File

@@ -1,2 +1,2 @@
from __future__ import unicode_literals from __future__ import unicode_literals
__version__ = '5.8.2' __version__ = '6.0.1'

View File

@@ -1,2 +1,4 @@
- Added Calendar and Gantt Views for Sales Order based on Delivery Date
- Most transaction forms will now have collapsibe sections so that forms appear to be more organized and you can easily locate parts to be edited. - Most transaction forms will now have collapsibe sections so that forms appear to be more organized and you can easily locate parts to be edited.
- The document title on most transactions can be edited so that you can set meaningful titles to all transactions like Sales Invoice and also edit it to denote status. - The document title on most transactions can be edited so that you can set meaningful titles to all transactions like Sales Invoice and also edit it to denote status.
- Allow user to disable warnings for "Multiple Items" and "Multiple Sales Order against a Customer's Purchase Order" via Sales and Purchase Settings. Sponsored by: **[McLean Images](http://www.mcleanimages.com.au/)**

View File

@@ -0,0 +1,168 @@
# 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
from frappe import _
from frappe.utils import cstr, flt
import json
class ItemVariantExistsError(frappe.ValidationError): pass
class InvalidItemAttributeValueError(frappe.ValidationError): pass
class ItemTemplateCannotHaveStock(frappe.ValidationError): pass
@frappe.whitelist()
def get_variant(item, args):
"""Validates Attributes and their Values, then looks for an exactly matching Item Variant
:param item: Template Item
:param args: A dictionary with "Attribute" as key and "Attribute Value" as value
"""
if isinstance(args, basestring):
args = json.loads(args)
if not args:
frappe.throw(_("Please specify at least one attribute in the Attributes table"))
validate_item_variant_attributes(item, args)
return find_variant(item, args)
def validate_item_variant_attributes(item, args):
attribute_values = {}
for t in frappe.get_all("Item Attribute Value", fields=["parent", "attribute_value"],
filters={"parent": ["in", args.keys()]}):
(attribute_values.setdefault(t.parent, [])).append(t.attribute_value)
numeric_attributes = frappe._dict((t.attribute, t) for t in \
frappe.db.sql("""select attribute, from_range, to_range, increment from `tabItem Variant Attribute`
where parent = %s and numeric_values=1""", (item), as_dict=1))
for attribute, value in args.items():
if attribute in numeric_attributes:
numeric_attribute = numeric_attributes[attribute]
from_range = numeric_attribute.from_range
to_range = numeric_attribute.to_range
increment = numeric_attribute.increment
if increment == 0:
# defensive validation to prevent ZeroDivisionError
frappe.throw(_("Increment for Attribute {0} cannot be 0").format(attribute))
is_in_range = from_range <= flt(value) <= to_range
precision = len(cstr(increment).split(".")[-1].rstrip("0"))
#avoid precision error by rounding the remainder
remainder = flt((flt(value) - from_range) % increment, precision)
is_incremental = remainder==0 or remainder==0 or remainder==increment
if not (is_in_range and is_incremental):
frappe.throw(_("Value for Attribute {0} must be within the range of {1} to {2} in the increments of {3}")\
.format(attribute, from_range, to_range, increment), InvalidItemAttributeValueError)
elif value not in attribute_values[attribute]:
frappe.throw(_("Value {0} for Attribute {1} does not exist in the list of valid Item Attribute Values").format(
value, attribute))
def find_variant(item, args):
conditions = ["""(iv_attribute.attribute="{0}" and iv_attribute.attribute_value="{1}")"""\
.format(frappe.db.escape(key), frappe.db.escape(cstr(value))) for key, value in args.items()]
conditions = " or ".join(conditions)
# use approximate match and shortlist possible variant matches
# it is approximate because we are matching using OR condition
# and it need not be exact match at this stage
# this uses a simpler query instead of using multiple exists conditions
possible_variants = frappe.db.sql_list("""select name from `tabItem` item
where variant_of=%s and exists (
select name from `tabItem Variant Attribute` iv_attribute
where iv_attribute.parent=item.name
and ({conditions})
)""".format(conditions=conditions), item)
for variant in possible_variants:
variant = frappe.get_doc("Item", variant)
if len(args.keys()) == len(variant.get("attributes")):
# has the same number of attributes and values
# assuming no duplication as per the validation in Item
match_count = 0
for attribute, value in args.items():
for row in variant.attributes:
if row.attribute==attribute and row.attribute_value== cstr(value):
# this row matches
match_count += 1
break
if match_count == len(args.keys()):
return variant.name
@frappe.whitelist()
def create_variant(item, args):
if isinstance(args, basestring):
args = json.loads(args)
template = frappe.get_doc("Item", item)
variant = frappe.new_doc("Item")
variant_attributes = []
for d in template.attributes:
variant_attributes.append({
"attribute": d.attribute,
"attribute_value": args.get(d.attribute)
})
variant.set("attributes", variant_attributes)
copy_attributes_to_variant(template, variant)
make_variant_item_code(template, variant)
return variant
def copy_attributes_to_variant(item, variant):
from frappe.model import no_value_fields
for field in item.meta.fields:
if field.fieldtype not in no_value_fields and (not field.no_copy)\
and field.fieldname not in ("item_code", "item_name"):
if variant.get(field.fieldname) != item.get(field.fieldname):
variant.set(field.fieldname, item.get(field.fieldname))
variant.variant_of = item.name
variant.has_variants = 0
variant.show_in_website = 0
if variant.attributes:
variant.description += "\n"
for d in variant.attributes:
variant.description += "<p>" + d.attribute + ": " + cstr(d.attribute_value) + "</p>"
def make_variant_item_code(template, variant):
"""Uses template's item code and abbreviations to make variant's item code"""
if variant.item_code:
return
abbreviations = []
for attr in variant.attributes:
item_attribute = frappe.db.sql("""select i.numeric_values, v.abbr
from `tabItem Attribute` i left join `tabItem Attribute Value` v
on (i.name=v.parent)
where i.name=%(attribute)s and v.attribute_value=%(attribute_value)s""", {
"attribute": attr.attribute,
"attribute_value": attr.attribute_value
}, as_dict=True)
if not item_attribute:
# somehow an invalid item attribute got used
return
if item_attribute[0].numeric_values:
# don't generate item code if one of the attributes is numeric
return
abbreviations.append(item_attribute[0].abbr)
if abbreviations:
variant.item_code = "{0}-{1}".format(template.item_code, "-".join(abbreviations))
if variant.item_code:
variant.item_name = variant.item_code

View File

@@ -268,7 +268,7 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters):
{0} {0}
{match_conditions} {match_conditions}
order by expiry_date, name desc order by expiry_date, name desc
limit %(start)s, %(page_len)s""".format(cond, match_conditions=get_match_cond(doctype)), args, debug=1) limit %(start)s, %(page_len)s""".format(cond, match_conditions=get_match_cond(doctype)), args)
def get_account_list(doctype, txt, searchfield, start, page_len, filters): def get_account_list(doctype, txt, searchfield, start, page_len, filters):
filter_list = [] filter_list = []

View File

@@ -186,7 +186,6 @@
"in_list_view": 0, "in_list_view": 0,
"label": "Title", "label": "Title",
"no_copy": 0, "no_copy": 0,
"options": "[{status}] {enquiry_type} from {customer}",
"permlevel": 0, "permlevel": 0,
"precision": "", "precision": "",
"print_hide": 0, "print_hide": 0,
@@ -890,7 +889,7 @@
"is_submittable": 0, "is_submittable": 0,
"issingle": 0, "issingle": 0,
"istable": 0, "istable": 0,
"modified": "2015-08-26 06:56:09.914606", "modified": "2015-08-26 06:56:09.914607",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "CRM", "module": "CRM",
"name": "Opportunity", "name": "Opportunity",

View File

@@ -27,7 +27,7 @@ blogs.
""" """
app_icon = "icon-th" app_icon = "icon-th"
app_color = "#e74c3c" app_color = "#e74c3c"
app_version = "5.8.2" app_version = "6.0.1"
github_link = "https://github.com/frappe/erpnext" github_link = "https://github.com/frappe/erpnext"
error_report_email = "support@erpnext.com" error_report_email = "support@erpnext.com"
@@ -51,7 +51,7 @@ my_account_context = "erpnext.shopping_cart.utils.update_my_account_context"
email_append_to = ["Job Applicant", "Opportunity", "Issue"] email_append_to = ["Job Applicant", "Opportunity", "Issue"]
calendars = ["Task", "Production Order", "Time Log", "Leave Application"] calendars = ["Task", "Production Order", "Time Log", "Leave Application", "Sales Order"]
website_generators = ["Item Group", "Item", "Sales Partner"] website_generators = ["Item Group", "Item", "Sales Partner"]

View File

@@ -381,19 +381,17 @@ def make_stock_entry(production_order_id, purpose, qty=None):
@frappe.whitelist() @frappe.whitelist()
def get_events(start, end, filters=None): def get_events(start, end, filters=None):
from frappe.desk.reportview import build_match_conditions """Returns events for Gantt / Calendar view rendering.
if not frappe.has_permission("Production Order"):
frappe.msgprint(_("No Permission"), raise_exception=1)
conditions = build_match_conditions("Production Order") :param start: Start date-time.
conditions = conditions and (" and " + conditions) or "" :param end: End date-time.
if filters: :param filters: Filters (JSON).
filters = json.loads(filters) """
for key in filters: from frappe.desk.calendar import get_event_conditions
if filters[key]: conditions = get_event_conditions("Production Order", filters)
conditions += " and " + key + ' = "' + filters[key].replace('"', '\"') + '"'
data = frappe.db.sql("""select name, production_item, planned_start_date, planned_end_date data = frappe.db.sql("""select name, production_item, planned_start_date,
planned_end_date, status
from `tabProduction Order` from `tabProduction Order`
where ((ifnull(planned_start_date, '0000-00-00')!= '0000-00-00') \ where ((ifnull(planned_start_date, '0000-00-00')!= '0000-00-00') \
and (planned_start_date between %(start)s and %(end)s) \ and (planned_start_date between %(start)s and %(end)s) \

View File

@@ -10,6 +10,15 @@ frappe.views.calendar["Production Order"] = {
"allDay": "allDay" "allDay": "allDay"
}, },
gantt: true, gantt: true,
get_css_class: function(data) {
if(data.status==="Completed") {
return "success";
} else if(data.status==="In Process") {
return "warning";
} else {
return "danger";
}
},
filters: [ filters: [
{ {
"fieldtype": "Link", "fieldtype": "Link",

View File

@@ -200,5 +200,5 @@ erpnext.patches.v5_8.add_credit_note_print_heading
execute:frappe.delete_doc_if_exists("Print Format", "Credit Note - Negative Invoice") execute:frappe.delete_doc_if_exists("Print Format", "Credit Note - Negative Invoice")
# V6.0 # V6.0
erpnext.patches.v6_0.set_default_title erpnext.patches.v6_0.set_default_title # 2015-09-03
erpnext.patches.v6_0.multi_currency erpnext.patches.v6_0.multi_currency

View File

@@ -107,18 +107,14 @@ class Task(Document):
@frappe.whitelist() @frappe.whitelist()
def get_events(start, end, filters=None): def get_events(start, end, filters=None):
from frappe.desk.reportview import build_match_conditions """Returns events for Gantt / Calendar view rendering.
if not frappe.has_permission("Task"):
frappe.msgprint(_("No Permission"), raise_exception=1)
conditions = build_match_conditions("Task") :param start: Start date-time.
conditions = conditions and (" and " + conditions) or "" :param end: End date-time.
:param filters: Filters (JSON).
if filters: """
filters = json.loads(filters) from frappe.desk.calendar import get_event_conditions
for key in filters: conditions = get_event_conditions("Task", filters)
if filters[key]:
conditions += " and " + key + ' = "' + filters[key].replace('"', '\"') + '"'
data = frappe.db.sql("""select name, exp_start_date, exp_end_date, data = frappe.db.sql("""select name, exp_start_date, exp_end_date,
subject, status, project from `tabTask` subject, status, project from `tabTask`

View File

@@ -2,7 +2,7 @@
# 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, json import frappe
from frappe import _ from frappe import _
from frappe.utils import cstr, flt, get_datetime, get_time, getdate from frappe.utils import cstr, flt, get_datetime, get_time, getdate
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
@@ -248,17 +248,8 @@ def get_events(start, end, filters=None):
:param end: End date-time. :param end: End date-time.
:param filters: Filters like workstation, project etc. :param filters: Filters like workstation, project etc.
""" """
from frappe.desk.reportview import build_match_conditions from frappe.desk.calendar import get_event_conditions
if not frappe.has_permission("Time Log"): conditions = get_event_conditions("Time Log", filters)
frappe.msgprint(_("No Permission"), raise_exception=1)
conditions = build_match_conditions("Time Log")
conditions = conditions and (" and " + conditions) or ""
if filters:
filters = json.loads(filters)
for key in filters:
if filters[key]:
conditions += " and " + key + ' = "' + filters[key].replace('"', '\"') + '"'
data = frappe.db.sql("""select name, from_time, to_time, data = frappe.db.sql("""select name, from_time, to_time,
activity_type, task, project, production_order, workstation from `tabTime Log` activity_type, task, project, production_order, workstation from `tabTime Log`

View File

@@ -471,3 +471,24 @@ def make_maintenance_visit(source_name, target_doc=None):
}, target_doc) }, target_doc)
return doclist return doclist
@frappe.whitelist()
def get_events(start, end, filters=None):
"""Returns events for Gantt / Calendar view rendering.
:param start: Start date-time.
:param end: End date-time.
:param filters: Filters (JSON).
"""
from frappe.desk.calendar import get_event_conditions
conditions = get_event_conditions("Sales Order", filters)
data = frappe.db.sql("""select name, customer_name, delivery_status, billing_status, delivery_date
from `tabSales Order`
where (ifnull(delivery_date, '0000-00-00')!= '0000-00-00') \
and (delivery_date between %(start)s and %(end)s) {conditions}
""".format(conditions=conditions), {
"start": start,
"end": end
}, as_dict=True, update={"allDay": 0})
return data

View File

@@ -0,0 +1,45 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
frappe.views.calendar["Sales Order"] = {
field_map: {
"start": "delivery_date",
"end": "delivery_date",
"id": "name",
"title": "customer_name",
"allDay": "allDay"
},
gantt: true,
filters: [
{
"fieldtype": "Link",
"fieldname": "customer",
"options": "Customer",
"label": __("Customer")
},
{
"fieldtype": "Select",
"fieldname": "delivery_status",
"options": "Not Delivered\nFully Delivered\nPartly Delivered\nClosed\nNot Applicable",
"label": __("Delivery Status")
},
{
"fieldtype": "Select",
"fieldname": "billing_status",
"options": "Not Billed\nFully Billed\nPartly Billed\nClosed",
"label": __("Billing Status")
},
],
get_events_method: "erpnext.selling.doctype.sales_order.sales_order.get_events",
get_css_class: function(data) {
if(data.status=="Stopped") {
return "";
} if(data.delivery_status=="Not Delivered") {
return "danger";
} else if(data.delivery_status=="Partly Delivered") {
return "warning";
} else if(data.delivery_status=="Fully Delivered") {
return "success";
}
}
}

View File

@@ -4,7 +4,7 @@
# For license information, please see license.txt # For license information, please see license.txt
from __future__ import unicode_literals from __future__ import unicode_literals
from frappe.utils import get_site_path from frappe.utils import get_site_path, cint
from frappe.utils.data import convert_utc_to_user_timezone from frappe.utils.data import convert_utc_to_user_timezone
import os import os
import datetime import datetime
@@ -42,7 +42,7 @@ def take_backups_weekly():
take_backups_if("Weekly") take_backups_if("Weekly")
def take_backups_if(freq): def take_backups_if(freq):
if frappe.db.get_value("Backup Manager", None, "send_backups_to_dropbox"): if cint(frappe.db.get_value("Backup Manager", None, "send_backups_to_dropbox")):
if frappe.db.get_value("Backup Manager", None, "upload_backups_to_dropbox")==freq: if frappe.db.get_value("Backup Manager", None, "upload_backups_to_dropbox")==freq:
take_backups_dropbox() take_backups_dropbox()

View File

@@ -6,6 +6,7 @@ import frappe
from frappe.utils import cint from frappe.utils import cint
from frappe import _ from frappe import _
from frappe.desk.notifications import clear_notifications
@frappe.whitelist() @frappe.whitelist()
def delete_company_transactions(company_name): def delete_company_transactions(company_name):
@@ -17,11 +18,16 @@ def delete_company_transactions(company_name):
delete_bins(company_name) delete_bins(company_name)
delete_time_logs(company_name)
for doctype in frappe.db.sql_list("""select parent from for doctype in frappe.db.sql_list("""select parent from
tabDocField where fieldtype='Link' and options='Company'"""): tabDocField where fieldtype='Link' and options='Company'"""):
if doctype not in ("Account", "Cost Center", "Warehouse", "Budget Detail", "Party Account"): if doctype not in ("Account", "Cost Center", "Warehouse", "Budget Detail", "Party Account", "Employee"):
delete_for_doctype(doctype, company_name) delete_for_doctype(doctype, company_name)
# Clear notification counts
clear_notifications()
def delete_for_doctype(doctype, company_name): def delete_for_doctype(doctype, company_name):
meta = frappe.get_meta(doctype) meta = frappe.get_meta(doctype)
company_fieldname = meta.get("fields", {"fieldtype": "Link", company_fieldname = meta.get("fields", {"fieldtype": "Link",
@@ -60,3 +66,20 @@ def delete_for_doctype(doctype, company_name):
def delete_bins(company_name): def delete_bins(company_name):
frappe.db.sql("""delete from tabBin where warehouse in frappe.db.sql("""delete from tabBin where warehouse in
(select name from tabWarehouse where company=%s)""", company_name) (select name from tabWarehouse where company=%s)""", company_name)
def delete_time_logs(company_name):
# Delete Time Logs as it is linked to Production Order / Project / Task, which are linked to company
frappe.db.sql("""
delete from `tabTime Log`
where
(ifnull(project, '') != ''
and exists(select name from `tabProject` where name=`tabTime Log`.project and company=%(company)s))
or (ifnull(task, '') != ''
and exists(select name from `tabTask` where name=`tabTime Log`.task and company=%(company)s))
or (ifnull(production_order, '') != ''
and exists(select name from `tabProduction Order`
where name=`tabTime Log`.production_order and company=%(company)s))
or (ifnull(sales_invoice, '') != ''
and exists(select name from `tabSales Invoice`
where name=`tabTime Log`.sales_invoice and company=%(company)s))
""", {"company": company_name})

View File

@@ -231,7 +231,7 @@ $.extend(erpnext.item, {
args = d.get_values(); args = d.get_values();
if(!args) return; if(!args) return;
frappe.call({ frappe.call({
method:"erpnext.stock.doctype.item.item.get_variant", method:"erpnext.controllers.item_variant.get_variant",
args: { args: {
"item": cur_frm.doc.name, "item": cur_frm.doc.name,
"args": d.get_values() "args": d.get_values()
@@ -253,7 +253,7 @@ $.extend(erpnext.item, {
} else { } else {
d.hide(); d.hide();
frappe.call({ frappe.call({
method:"erpnext.stock.doctype.item.item.create_variant", method:"erpnext.controllers.item_variant.create_variant",
args: { args: {
"item": cur_frm.doc.name, "item": cur_frm.doc.name,
"args": d.get_values() "args": d.get_values()

View File

@@ -3,18 +3,15 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
import json
from frappe import msgprint, _ from frappe import msgprint, _
from frappe.utils import cstr, flt, cint, getdate, now_datetime, formatdate from frappe.utils import cstr, flt, cint, getdate, now_datetime, formatdate
from frappe.website.website_generator import WebsiteGenerator from frappe.website.website_generator import WebsiteGenerator
from erpnext.setup.doctype.item_group.item_group import invalidate_cache_for, get_parent_item_groups from erpnext.setup.doctype.item_group.item_group import invalidate_cache_for, get_parent_item_groups
from frappe.website.render import clear_cache from frappe.website.render import clear_cache
from frappe.website.doctype.website_slideshow.website_slideshow import get_slideshow from frappe.website.doctype.website_slideshow.website_slideshow import get_slideshow
from erpnext.controllers.item_variant import get_variant, copy_attributes_to_variant, ItemVariantExistsError
class WarehouseNotSet(frappe.ValidationError): pass class WarehouseNotSet(frappe.ValidationError): pass
class ItemTemplateCannotHaveStock(frappe.ValidationError): pass
class ItemVariantExistsError(frappe.ValidationError): pass
class InvalidItemAttributeValueError(frappe.ValidationError): pass
class Item(WebsiteGenerator): class Item(WebsiteGenerator):
website = frappe._dict( website = frappe._dict(
@@ -63,8 +60,8 @@ class Item(WebsiteGenerator):
self.validate_warehouse_for_reorder() self.validate_warehouse_for_reorder()
self.update_item_desc() self.update_item_desc()
self.synced_with_hub = 0 self.synced_with_hub = 0
self.validate_has_variants() self.validate_has_variants()
# self.validate_stock_for_template_must_be_zero()
self.validate_attributes() self.validate_attributes()
self.validate_variant_attributes() self.validate_variant_attributes()
@@ -314,14 +311,6 @@ class Item(WebsiteGenerator):
if frappe.db.exists("Item", {"variant_of": self.name}): if frappe.db.exists("Item", {"variant_of": self.name}):
frappe.throw(_("Item has variants.")) frappe.throw(_("Item has variants."))
def validate_stock_for_template_must_be_zero(self):
if self.has_variants:
stock_in = frappe.db.sql_list("""select warehouse from tabBin
where item_code=%s and (ifnull(actual_qty, 0) > 0 or ifnull(ordered_qty, 0) > 0
or ifnull(reserved_qty, 0) > 0 or ifnull(indented_qty, 0) > 0 or ifnull(planned_qty, 0) > 0)""", self.name)
if stock_in:
frappe.throw(_("Item Template cannot have stock or Open Sales/Purchase/Production Orders."), ItemTemplateCannotHaveStock)
def validate_uom(self): def validate_uom(self):
if not self.get("__islocal"): if not self.get("__islocal"):
check_stock_uom_with_bin(self.name, self.stock_uom) check_stock_uom_with_bin(self.name, self.stock_uom)
@@ -490,158 +479,3 @@ def check_stock_uom_with_bin(item, stock_uom):
you have already made some transaction(s) with another UOM. To change default UOM, \ you have already made some transaction(s) with another UOM. To change default UOM, \
use 'UOM Replace Utility' tool under Stock module.").format(item)) use 'UOM Replace Utility' tool under Stock module.").format(item))
@frappe.whitelist()
def get_variant(item, args):
"""Validates Attributes and their Values, then looks for an exactly matching Item Variant
:param item: Template Item
:param args: A dictionary with "Attribute" as key and "Attribute Value" as value
"""
if isinstance(args, basestring):
args = json.loads(args)
if not args:
frappe.throw(_("Please specify at least one attribute in the Attributes table"))
validate_item_variant_attributes(item, args)
return find_variant(item, args)
def validate_item_variant_attributes(item, args):
attribute_values = {}
for t in frappe.get_all("Item Attribute Value", fields=["parent", "attribute_value"],
filters={"parent": ["in", args.keys()]}):
(attribute_values.setdefault(t.parent, [])).append(t.attribute_value)
numeric_attributes = frappe._dict((t.attribute, t) for t in \
frappe.db.sql("""select attribute, from_range, to_range, increment from `tabItem Variant Attribute`
where parent = %s and numeric_values=1""", (item), as_dict=1))
for attribute, value in args.items():
if attribute in numeric_attributes:
numeric_attribute = numeric_attributes[attribute]
from_range = numeric_attribute.from_range
to_range = numeric_attribute.to_range
increment = numeric_attribute.increment
if increment == 0:
# defensive validation to prevent ZeroDivisionError
frappe.throw(_("Increment for Attribute {0} cannot be 0").format(attribute))
is_in_range = from_range <= flt(value) <= to_range
precision = len(cstr(increment).split(".")[-1].rstrip("0"))
#avoid precision error by rounding the remainder
remainder = flt((flt(value) - from_range) % increment, precision)
is_incremental = remainder==0 or remainder==0 or remainder==increment
if not (is_in_range and is_incremental):
frappe.throw(_("Value for Attribute {0} must be within the range of {1} to {2} in the increments of {3}")\
.format(attribute, from_range, to_range, increment), InvalidItemAttributeValueError)
elif value not in attribute_values[attribute]:
frappe.throw(_("Value {0} for Attribute {1} does not exist in the list of valid Item Attribute Values").format(
value, attribute))
def find_variant(item, args):
conditions = ["""(iv_attribute.attribute="{0}" and iv_attribute.attribute_value="{1}")"""\
.format(frappe.db.escape(key), frappe.db.escape(cstr(value))) for key, value in args.items()]
conditions = " or ".join(conditions)
# use approximate match and shortlist possible variant matches
# it is approximate because we are matching using OR condition
# and it need not be exact match at this stage
# this uses a simpler query instead of using multiple exists conditions
possible_variants = frappe.db.sql_list("""select name from `tabItem` item
where variant_of=%s and exists (
select name from `tabItem Variant Attribute` iv_attribute
where iv_attribute.parent=item.name
and ({conditions})
)""".format(conditions=conditions), item)
for variant in possible_variants:
variant = frappe.get_doc("Item", variant)
if len(args.keys()) == len(variant.get("attributes")):
# has the same number of attributes and values
# assuming no duplication as per the validation in Item
match_count = 0
for attribute, value in args.items():
for row in variant.attributes:
if row.attribute==attribute and row.attribute_value== cstr(value):
# this row matches
match_count += 1
break
if match_count == len(args.keys()):
return variant.name
@frappe.whitelist()
def create_variant(item, args):
if isinstance(args, basestring):
args = json.loads(args)
variant = frappe.new_doc("Item")
variant_attributes = []
for d in args:
variant_attributes.append({
"attribute": d,
"attribute_value": args[d]
})
variant.set("attributes", variant_attributes)
template = frappe.get_doc("Item", item)
copy_attributes_to_variant(template, variant)
make_variant_item_code(template, variant)
return variant
def copy_attributes_to_variant(item, variant):
from frappe.model import no_value_fields
for field in item.meta.fields:
if field.fieldtype not in no_value_fields and (not field.no_copy)\
and field.fieldname not in ("item_code", "item_name"):
if variant.get(field.fieldname) != item.get(field.fieldname):
variant.set(field.fieldname, item.get(field.fieldname))
variant.variant_of = item.name
variant.has_variants = 0
variant.show_in_website = 0
if variant.attributes:
variant.description += "\n"
for d in variant.attributes:
variant.description += "<p>" + d.attribute + ": " + cstr(d.attribute_value) + "</p>"
def make_variant_item_code(template, variant):
"""Uses template's item code and abbreviations to make variant's item code"""
if variant.item_code:
return
abbreviations = []
for attr in variant.attributes:
item_attribute = frappe.db.sql("""select i.numeric_values, v.abbr
from `tabItem Attribute` i left join `tabItem Attribute Value` v
on (i.name=v.parent)
where i.name=%(attribute)s and v.attribute_value=%(attribute_value)s""", {
"attribute": attr.attribute,
"attribute_value": attr.attribute_value
}, as_dict=True)
if not item_attribute:
# somehow an invalid item attribute got used
return
if item_attribute[0].numeric_values:
# don't generate item code if one of the attributes is numeric
return
abbreviations.append(item_attribute[0].abbr)
if abbreviations:
variant.item_code = "{0}-{1}".format(template.item_code, "-".join(abbreviations))
if variant.item_code:
variant.item_name = variant.item_code

View File

@@ -6,8 +6,8 @@ import unittest
import frappe import frappe
from frappe.test_runner import make_test_records from frappe.test_runner import make_test_records
from erpnext.stock.doctype.item.item import (WarehouseNotSet, create_variant, from erpnext.stock.doctype.item.item import WarehouseNotSet
ItemVariantExistsError, InvalidItemAttributeValueError) from erpnext.controllers.item_variant import create_variant, ItemVariantExistsError, InvalidItemAttributeValueError
test_ignore = ["BOM"] test_ignore = ["BOM"]
test_dependencies = ["Warehouse"] test_dependencies = ["Warehouse"]

View File

@@ -41,6 +41,10 @@ class ItemAttribute(Document):
abbrs.append(d.abbr) abbrs.append(d.abbr)
def validate_attribute_values(self): def validate_attribute_values(self):
# don't validate numeric values
if self.numeric_values:
return
attribute_values = [] attribute_values = []
for d in self.item_attribute_values: for d in self.item_attribute_values:
attribute_values.append(d.attribute_value) attribute_values.append(d.attribute_value)

View File

@@ -8,7 +8,7 @@ from frappe import _
from frappe.utils import flt, getdate, add_days, formatdate from frappe.utils import flt, getdate, add_days, formatdate
from frappe.model.document import Document from frappe.model.document import Document
from datetime import date from datetime import date
from erpnext.stock.doctype.item.item import ItemTemplateCannotHaveStock from erpnext.controllers.item_variant import ItemTemplateCannotHaveStock
class StockFreezeError(frappe.ValidationError): pass class StockFreezeError(frappe.ValidationError): pass

View File

@@ -231,12 +231,16 @@ def insert_item_price(args):
if frappe.db.get_value("Price List", args.price_list, "currency") == args.currency \ if frappe.db.get_value("Price List", args.price_list, "currency") == args.currency \
and cint(frappe.db.get_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing")): and cint(frappe.db.get_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing")):
if frappe.has_permission("Item Price", "write"): if frappe.has_permission("Item Price", "write"):
price_list_rate = args.rate / args.conversion_factor \
if args.get("conversion_factor") else args.rate
item_price = frappe.get_doc({ item_price = frappe.get_doc({
"doctype": "Item Price", "doctype": "Item Price",
"price_list": args.price_list, "price_list": args.price_list,
"item_code": args.item_code, "item_code": args.item_code,
"currency": args.currency, "currency": args.currency,
"price_list_rate": args.rate "price_list_rate": price_list_rate
}) })
item_price.insert() item_price.insert()
frappe.msgprint("Item Price added for {0} in Price List {1}".format(args.item_code, frappe.msgprint("Item Price added for {0} in Price List {1}".format(args.item_code,

View File

@@ -1,6 +1,6 @@
from setuptools import setup, find_packages from setuptools import setup, find_packages
version = "5.8.2" version = "6.0.1"
with open("requirements.txt", "r") as f: with open("requirements.txt", "r") as f:
install_requires = f.readlines() install_requires = f.readlines()