Fixed conflict
This commit is contained in:
commit
11a746d363
@ -49,6 +49,20 @@ DocTypes are easy to create but hard to maintain. If you find that there is a an
|
||||
|
||||
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
|
||||
|
||||
Please see README.md
|
||||
|
||||
|
@ -1,2 +1,2 @@
|
||||
from __future__ import unicode_literals
|
||||
__version__ = '5.8.2'
|
||||
__version__ = '6.0.1'
|
||||
|
@ -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.
|
||||
- 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/)**
|
168
erpnext/controllers/item_variant.py
Normal file
168
erpnext/controllers/item_variant.py
Normal 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
|
@ -268,7 +268,7 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters):
|
||||
{0}
|
||||
{match_conditions}
|
||||
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):
|
||||
filter_list = []
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -27,7 +27,7 @@ blogs.
|
||||
"""
|
||||
app_icon = "icon-th"
|
||||
app_color = "#e74c3c"
|
||||
app_version = "5.8.2"
|
||||
app_version = "6.0.1"
|
||||
github_link = "https://github.com/frappe/erpnext"
|
||||
|
||||
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"]
|
||||
|
||||
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"]
|
||||
|
||||
|
@ -329,15 +329,15 @@ class ProductionOrder(Document):
|
||||
frappe.throw(_("Production Order cannot be raised against a Item Template"), ItemHasVariantError)
|
||||
|
||||
validate_end_of_life(self.production_item)
|
||||
|
||||
|
||||
def validate_qty(self):
|
||||
if not self.qty > 0:
|
||||
frappe.throw(_("Quantity to Manufacture must be greater than 0."))
|
||||
|
||||
|
||||
def validate_operation_time(self):
|
||||
for d in self.operations:
|
||||
if not d.time_in_mins > 0:
|
||||
frappe.throw(_("Operation Time must be greater than 0 for Operation {0}".format(d.operation)))
|
||||
frappe.throw(_("Operation Time must be greater than 0 for Operation {0}".format(d.operation)))
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_item_details(item):
|
||||
@ -381,19 +381,17 @@ def make_stock_entry(production_order_id, purpose, qty=None):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_events(start, end, filters=None):
|
||||
from frappe.desk.reportview import build_match_conditions
|
||||
if not frappe.has_permission("Production Order"):
|
||||
frappe.msgprint(_("No Permission"), raise_exception=1)
|
||||
"""Returns events for Gantt / Calendar view rendering.
|
||||
|
||||
conditions = build_match_conditions("Production Order")
|
||||
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('"', '\"') + '"'
|
||||
: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("Production Order", filters)
|
||||
|
||||
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`
|
||||
where ((ifnull(planned_start_date, '0000-00-00')!= '0000-00-00') \
|
||||
and (planned_start_date between %(start)s and %(end)s) \
|
||||
@ -427,4 +425,4 @@ def make_time_log(name, operation, from_time=None, to_time=None, qty=None, proj
|
||||
def get_default_warehouse():
|
||||
wip_warehouse = frappe.db.get_single_value("Manufacturing Settings", "default_wip_warehouse")
|
||||
fg_warehouse = frappe.db.get_single_value("Manufacturing Settings", "default_fg_warehouse")
|
||||
return {"wip_warehouse": wip_warehouse, "fg_warehouse": fg_warehouse}
|
||||
return {"wip_warehouse": wip_warehouse, "fg_warehouse": fg_warehouse}
|
||||
|
@ -10,6 +10,15 @@ frappe.views.calendar["Production Order"] = {
|
||||
"allDay": "allDay"
|
||||
},
|
||||
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: [
|
||||
{
|
||||
"fieldtype": "Link",
|
||||
|
@ -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")
|
||||
|
||||
# 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
|
||||
|
@ -42,7 +42,7 @@ class Task(Document):
|
||||
for d in self.depends_on:
|
||||
if frappe.db.get_value("Task", d.task, "status") != "Closed":
|
||||
frappe.throw(_("Cannot close task as its dependant task {0} is not closed.").format(d.task))
|
||||
|
||||
|
||||
from frappe.desk.form.assign_to import clear
|
||||
clear(self.doctype, self.name)
|
||||
|
||||
@ -107,18 +107,14 @@ class Task(Document):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_events(start, end, filters=None):
|
||||
from frappe.desk.reportview import build_match_conditions
|
||||
if not frappe.has_permission("Task"):
|
||||
frappe.msgprint(_("No Permission"), raise_exception=1)
|
||||
"""Returns events for Gantt / Calendar view rendering.
|
||||
|
||||
conditions = build_match_conditions("Task")
|
||||
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('"', '\"') + '"'
|
||||
: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("Task", filters)
|
||||
|
||||
data = frappe.db.sql("""select name, exp_start_date, exp_end_date,
|
||||
subject, status, project from `tabTask`
|
||||
|
@ -2,7 +2,7 @@
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import frappe, json
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import cstr, flt, get_datetime, get_time, getdate
|
||||
from dateutil.relativedelta import relativedelta
|
||||
@ -248,17 +248,8 @@ def get_events(start, end, filters=None):
|
||||
:param end: End date-time.
|
||||
:param filters: Filters like workstation, project etc.
|
||||
"""
|
||||
from frappe.desk.reportview import build_match_conditions
|
||||
if not frappe.has_permission("Time Log"):
|
||||
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('"', '\"') + '"'
|
||||
from frappe.desk.calendar import get_event_conditions
|
||||
conditions = get_event_conditions("Time Log", filters)
|
||||
|
||||
data = frappe.db.sql("""select name, from_time, to_time,
|
||||
activity_type, task, project, production_order, workstation from `tabTime Log`
|
||||
|
@ -471,3 +471,24 @@ def make_maintenance_visit(source_name, target_doc=None):
|
||||
}, target_doc)
|
||||
|
||||
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
|
||||
|
45
erpnext/selling/doctype/sales_order/sales_order_calendar.js
Normal file
45
erpnext/selling/doctype/sales_order/sales_order_calendar.js
Normal 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";
|
||||
}
|
||||
}
|
||||
}
|
@ -4,7 +4,7 @@
|
||||
# For license information, please see license.txt
|
||||
|
||||
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
|
||||
import os
|
||||
import datetime
|
||||
@ -42,7 +42,7 @@ def take_backups_weekly():
|
||||
take_backups_if("Weekly")
|
||||
|
||||
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:
|
||||
take_backups_dropbox()
|
||||
|
||||
|
@ -6,6 +6,7 @@ import frappe
|
||||
|
||||
from frappe.utils import cint
|
||||
from frappe import _
|
||||
from frappe.desk.notifications import clear_notifications
|
||||
|
||||
@frappe.whitelist()
|
||||
def delete_company_transactions(company_name):
|
||||
@ -16,11 +17,16 @@ def delete_company_transactions(company_name):
|
||||
frappe.throw(_("Transactions can only be deleted by the creator of the Company"), frappe.PermissionError)
|
||||
|
||||
delete_bins(company_name)
|
||||
|
||||
delete_time_logs(company_name)
|
||||
|
||||
for doctype in frappe.db.sql_list("""select parent from
|
||||
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)
|
||||
|
||||
# Clear notification counts
|
||||
clear_notifications()
|
||||
|
||||
def delete_for_doctype(doctype, company_name):
|
||||
meta = frappe.get_meta(doctype)
|
||||
@ -60,3 +66,20 @@ def delete_for_doctype(doctype, company_name):
|
||||
def delete_bins(company_name):
|
||||
frappe.db.sql("""delete from tabBin where warehouse in
|
||||
(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})
|
@ -231,7 +231,7 @@ $.extend(erpnext.item, {
|
||||
args = d.get_values();
|
||||
if(!args) return;
|
||||
frappe.call({
|
||||
method:"erpnext.stock.doctype.item.item.get_variant",
|
||||
method:"erpnext.controllers.item_variant.get_variant",
|
||||
args: {
|
||||
"item": cur_frm.doc.name,
|
||||
"args": d.get_values()
|
||||
@ -253,7 +253,7 @@ $.extend(erpnext.item, {
|
||||
} else {
|
||||
d.hide();
|
||||
frappe.call({
|
||||
method:"erpnext.stock.doctype.item.item.create_variant",
|
||||
method:"erpnext.controllers.item_variant.create_variant",
|
||||
args: {
|
||||
"item": cur_frm.doc.name,
|
||||
"args": d.get_values()
|
||||
|
@ -3,18 +3,15 @@
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
import json
|
||||
from frappe import msgprint, _
|
||||
from frappe.utils import cstr, flt, cint, getdate, now_datetime, formatdate
|
||||
from frappe.website.website_generator import WebsiteGenerator
|
||||
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.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 ItemTemplateCannotHaveStock(frappe.ValidationError): pass
|
||||
class ItemVariantExistsError(frappe.ValidationError): pass
|
||||
class InvalidItemAttributeValueError(frappe.ValidationError): pass
|
||||
|
||||
class Item(WebsiteGenerator):
|
||||
website = frappe._dict(
|
||||
@ -63,8 +60,8 @@ class Item(WebsiteGenerator):
|
||||
self.validate_warehouse_for_reorder()
|
||||
self.update_item_desc()
|
||||
self.synced_with_hub = 0
|
||||
|
||||
self.validate_has_variants()
|
||||
# self.validate_stock_for_template_must_be_zero()
|
||||
self.validate_attributes()
|
||||
self.validate_variant_attributes()
|
||||
|
||||
@ -314,14 +311,6 @@ class Item(WebsiteGenerator):
|
||||
if frappe.db.exists("Item", {"variant_of": self.name}):
|
||||
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):
|
||||
if not self.get("__islocal"):
|
||||
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, \
|
||||
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
|
||||
|
@ -6,8 +6,8 @@ import unittest
|
||||
import frappe
|
||||
|
||||
from frappe.test_runner import make_test_records
|
||||
from erpnext.stock.doctype.item.item import (WarehouseNotSet, create_variant,
|
||||
ItemVariantExistsError, InvalidItemAttributeValueError)
|
||||
from erpnext.stock.doctype.item.item import WarehouseNotSet
|
||||
from erpnext.controllers.item_variant import create_variant, ItemVariantExistsError, InvalidItemAttributeValueError
|
||||
|
||||
test_ignore = ["BOM"]
|
||||
test_dependencies = ["Warehouse"]
|
||||
@ -132,7 +132,7 @@ class TestItem(unittest.TestCase):
|
||||
"attribute": "Test Size"
|
||||
},
|
||||
{
|
||||
"attribute": "Test Item Length",
|
||||
"attribute": "Test Item Length",
|
||||
"numeric_values": 1,
|
||||
"from_range": 0.0,
|
||||
"to_range": 100.0,
|
||||
|
@ -41,6 +41,10 @@ class ItemAttribute(Document):
|
||||
abbrs.append(d.abbr)
|
||||
|
||||
def validate_attribute_values(self):
|
||||
# don't validate numeric values
|
||||
if self.numeric_values:
|
||||
return
|
||||
|
||||
attribute_values = []
|
||||
for d in self.item_attribute_values:
|
||||
attribute_values.append(d.attribute_value)
|
||||
|
@ -8,7 +8,7 @@ from frappe import _
|
||||
from frappe.utils import flt, getdate, add_days, formatdate
|
||||
from frappe.model.document import Document
|
||||
from datetime import date
|
||||
from erpnext.stock.doctype.item.item import ItemTemplateCannotHaveStock
|
||||
from erpnext.controllers.item_variant import ItemTemplateCannotHaveStock
|
||||
|
||||
class StockFreezeError(frappe.ValidationError): pass
|
||||
|
||||
|
@ -212,7 +212,7 @@ def get_price_list_rate(args, item_doc, out):
|
||||
price_list_rate = get_price_list_rate_for(args, item_doc.name)
|
||||
if not price_list_rate and item_doc.variant_of:
|
||||
price_list_rate = get_price_list_rate_for(args, item_doc.variant_of)
|
||||
|
||||
|
||||
if not price_list_rate:
|
||||
if args.price_list and args.rate:
|
||||
insert_item_price(args)
|
||||
@ -231,12 +231,16 @@ def insert_item_price(args):
|
||||
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")):
|
||||
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({
|
||||
"doctype": "Item Price",
|
||||
"price_list": args.price_list,
|
||||
"item_code": args.item_code,
|
||||
"currency": args.currency,
|
||||
"price_list_rate": args.rate
|
||||
"price_list_rate": price_list_rate
|
||||
})
|
||||
item_price.insert()
|
||||
frappe.msgprint("Item Price added for {0} in Price List {1}".format(args.item_code,
|
||||
|
Loading…
Reference in New Issue
Block a user