Merge branch 'develop'
diff --git a/erpnext/__version__.py b/erpnext/__version__.py
index b0b41d3..5069b2f 100644
--- a/erpnext/__version__.py
+++ b/erpnext/__version__.py
@@ -1,2 +1,2 @@
from __future__ import unicode_literals
-__version__ = '5.0.29'
+__version__ = '5.1.0'
diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py
index 138cf23..3d306fb 100644
--- a/erpnext/accounts/doctype/gl_entry/gl_entry.py
+++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py
@@ -127,7 +127,7 @@
against_voucher_amount = flt(frappe.db.sql("""
select sum(ifnull(debit, 0)) - sum(ifnull(credit, 0))
from `tabGL Entry` where voucher_type = 'Journal Entry' and voucher_no = %s
- and account = %s and ifnull(party_type, '')=%s and ifnull(party, '')=%s
+ and account = %s and ifnull(party_type, '')=%s and ifnull(party, '')=%s
and ifnull(against_voucher, '') = ''""",
(against_voucher, account, cstr(party_type), cstr(party)))[0][0])
@@ -158,3 +158,22 @@
frappe.throw(_("Account {0} is frozen").format(account))
elif frozen_accounts_modifier not in frappe.get_roles():
frappe.throw(_("Not authorized to edit frozen Account {0}").format(account))
+
+def update_against_account(voucher_type, voucher_no):
+ entries = frappe.db.get_all("GL Entry",
+ filters={"voucher_type": voucher_type, "voucher_no": voucher_no},
+ fields=["name", "party", "against", "debit", "credit", "account"])
+
+ accounts_debited, accounts_credited = [], []
+ for d in entries:
+ if flt(d.debit > 0): accounts_debited.append(d.party or d.account)
+ if flt(d.credit) > 0: accounts_credited.append(d.party or d.account)
+
+ for d in entries:
+ if flt(d.debit > 0):
+ new_against = ", ".join(list(set(accounts_credited)))
+ if flt(d.credit > 0):
+ new_against = ", ".join(list(set(accounts_debited)))
+
+ if d.against != new_against:
+ frappe.db.set_value("GL Entry", d.name, "against", new_against)
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.json b/erpnext/accounts/doctype/journal_entry/journal_entry.json
index 6eb395d..249fcc4 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.json
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.json
@@ -53,7 +53,7 @@
"fieldname": "posting_date",
"fieldtype": "Date",
"in_filter": 1,
- "in_list_view": 1,
+ "in_list_view": 0,
"label": "Posting Date",
"no_copy": 1,
"oldfieldname": "posting_date",
@@ -445,7 +445,7 @@
"icon": "icon-file-text",
"idx": 1,
"is_submittable": 1,
- "modified": "2015-04-27 20:32:31.655580",
+ "modified": "2015-06-29 15:28:12.529019",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Journal Entry",
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py
index 7bf6c56..489d488 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.py
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py
@@ -81,7 +81,7 @@
frappe.throw(_("Row {0}: Party Type and Party is only applicable against Receivable / Payable account").format(d.idx))
def check_credit_limit(self):
- customers = list(set([d.party for d in self.get("accounts")
+ customers = list(set([d.party for d in self.get("accounts")
if d.party_type=="Customer" and d.party and flt(d.debit) > 0]))
if customers:
from erpnext.selling.doctype.customer.customer import check_credit_limit
@@ -243,8 +243,8 @@
def set_against_account(self):
accounts_debited, accounts_credited = [], []
for d in self.get("accounts"):
- if flt(d.debit > 0): accounts_debited.append(d.account)
- if flt(d.credit) > 0: accounts_credited.append(d.account)
+ if flt(d.debit > 0): accounts_debited.append(d.party or d.account)
+ if flt(d.credit) > 0: accounts_credited.append(d.party or d.account)
for d in self.get("accounts"):
if flt(d.debit > 0): d.against_account = ", ".join(list(set(accounts_credited)))
@@ -274,9 +274,9 @@
r.append(_('Reference #{0} dated {1}').format(self.cheque_no, formatdate(self.cheque_date)))
else:
msgprint(_("Please enter Reference date"), raise_exception=frappe.MandatoryError)
-
+
company_currency = get_company_currency(self.company)
-
+
for d in self.get('accounts'):
if d.against_invoice and d.credit:
r.append(_("{0} against Sales Invoice {1}").format(fmt_money(flt(d.credit), currency = company_currency), \
@@ -426,7 +426,7 @@
def validate_expense_claim(self):
for d in self.accounts:
if d.against_expense_claim:
- sanctioned_amount, reimbursed_amount = frappe.db.get_value("Expense Claim",
+ sanctioned_amount, reimbursed_amount = frappe.db.get_value("Expense Claim",
d.against_expense_claim, ("total_sanctioned_amount", "total_amount_reimbursed"))
pending_amount = flt(sanctioned_amount) - flt(reimbursed_amount)
if d.debit > pending_amount:
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
index 3e16a31..d404851 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
@@ -224,3 +224,4 @@
else
cur_frm.pformat.print_heading = __("Purchase Invoice");
}
+
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
index e192e76..dcbc605 100755
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
@@ -926,12 +926,21 @@
"no_copy": 1,
"permlevel": 0,
"print_hide": 1
+ },
+ {
+ "depends_on": "eval:doc.is_recurring==1",
+ "fieldname": "recurring_print_format",
+ "fieldtype": "Link",
+ "label": "Recurring Print Format",
+ "options": "Print Format",
+ "permlevel": 0,
+ "precision": ""
}
],
"icon": "icon-file-text",
"idx": 1,
"is_submittable": 1,
- "modified": "2015-06-16 16:46:47.308287",
+ "modified": "2015-06-22 07:30:06.743438",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index 660b221..50a79ec 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -164,7 +164,7 @@
elif item.expense_account not in against_accounts:
# if no auto_accounting_for_stock or not a stock item
against_accounts.append(item.expense_account)
-
+
self.against_expense_account = ",".join(against_accounts)
def po_required(self):
@@ -271,7 +271,7 @@
gl_entries.append(
self.get_gl_dict({
"account": tax.account_head,
- "against": self.credit_to,
+ "against": self.supplier,
"debit": tax.add_deduct_tax == "Add" and tax.base_tax_amount_after_discount_amount or 0,
"credit": tax.add_deduct_tax == "Deduct" and tax.base_tax_amount_after_discount_amount or 0,
"remarks": self.remarks,
@@ -295,7 +295,7 @@
gl_entries.append(
self.get_gl_dict({
"account": item.expense_account,
- "against": self.credit_to,
+ "against": self.supplier,
"debit": item.base_net_amount,
"remarks": self.remarks,
"cost_center": item.cost_center
@@ -315,7 +315,7 @@
gl_entries.append(
self.get_gl_dict({
"account": stock_received_but_not_billed,
- "against": self.credit_to,
+ "against": self.supplier,
"debit": flt(item.item_tax_amount, self.precision("item_tax_amount", item)),
"remarks": self.remarks or "Accounting Entry for Stock"
})
@@ -341,7 +341,7 @@
self.get_gl_dict({
"account": expenses_included_in_valuation,
"cost_center": cost_center,
- "against": self.credit_to,
+ "against": self.supplier,
"credit": applicable_amount,
"remarks": self.remarks or "Accounting Entry for Stock"
})
@@ -355,7 +355,7 @@
gl_entries.append(
self.get_gl_dict({
"account": self.write_off_account,
- "against": self.credit_to,
+ "against": self.supplier,
"credit": flt(self.write_off_amount),
"remarks": self.remarks,
"cost_center": self.write_off_cost_center
@@ -374,7 +374,7 @@
self.update_billing_status_for_zero_amount_refdoc("Purchase Order")
self.make_gl_entries_on_cancel()
self.update_project()
-
+
def update_project(self):
project_list = []
for d in self.items:
@@ -384,7 +384,7 @@
project.update_purchase_costing()
project.save()
project_list.append(d.project_name)
-
+
def validate_supplier_invoice(self):
if self.bill_date:
if getdate(self.bill_date) > getdate(self.posting_date):
diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json
index 1280cc0..ce6ac7a 100755
--- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json
+++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json
@@ -50,6 +50,23 @@
"width": "300px"
},
{
+ "fieldname": "image",
+ "fieldtype": "Attach",
+ "hidden": 1,
+ "label": "Image",
+ "permlevel": 0,
+ "precision": ""
+ },
+ {
+ "fieldname": "image_view",
+ "fieldtype": "Image",
+ "label": "Image View",
+ "options": "image",
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 1
+ },
+ {
"fieldname": "quantity_and_rate",
"fieldtype": "Section Break",
"label": "Quantity and Rate",
@@ -452,7 +469,7 @@
],
"idx": 1,
"istable": 1,
- "modified": "2015-06-02 14:18:56.294949",
+ "modified": "2015-07-02 03:00:44.496683",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice Item",
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
index b35fa8a..3bb9aa0 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
@@ -392,8 +392,6 @@
}
}
-
-
cur_frm.set_query("debit_to", function(doc) {
return{
filters: [
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
index a021be4..045678d 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
@@ -1228,6 +1228,15 @@
"read_only": 0
},
{
+ "depends_on": "eval:doc.is_recurring==1",
+ "fieldname": "recurring_print_format",
+ "fieldtype": "Link",
+ "label": "Recurring Print Format",
+ "options": "Print Format",
+ "permlevel": 0,
+ "precision": ""
+ },
+ {
"fieldname": "against_income_account",
"fieldtype": "Small Text",
"hidden": 1,
@@ -1244,7 +1253,7 @@
"icon": "icon-file-text",
"idx": 1,
"is_submittable": 1,
- "modified": "2015-06-16 16:45:06.618286",
+ "modified": "2015-06-22 06:39:22.072544",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index 87f723d..604370b 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -503,7 +503,7 @@
gl_entries.append(
self.get_gl_dict({
"account": tax.account_head,
- "against": self.debit_to,
+ "against": self.customer,
"credit": flt(tax.base_tax_amount_after_discount_amount),
"remarks": self.remarks,
"cost_center": tax.cost_center
@@ -517,7 +517,7 @@
gl_entries.append(
self.get_gl_dict({
"account": item.income_account,
- "against": self.debit_to,
+ "against": self.customer,
"credit": item.base_net_amount,
"remarks": self.remarks,
"cost_center": item.cost_center
@@ -548,7 +548,7 @@
gl_entries.append(
self.get_gl_dict({
"account": self.cash_bank_account,
- "against": self.debit_to,
+ "against": self.customer,
"debit": self.paid_amount,
"remarks": self.remarks,
})
@@ -572,7 +572,7 @@
gl_entries.append(
self.get_gl_dict({
"account": self.write_off_account,
- "against": self.debit_to,
+ "against": self.customer,
"debit": self.write_off_amount,
"remarks": self.remarks,
"cost_center": self.write_off_cost_center
@@ -587,7 +587,7 @@
@frappe.whitelist()
def get_bank_cash_account(mode_of_payment, company):
- account = frappe.db.get_value("Mode of Payment Account",
+ account = frappe.db.get_value("Mode of Payment Account",
{"parent": mode_of_payment, "company": company}, "default_account")
if not account:
frappe.msgprint(_("Please set default Cash or Bank account in Mode of Payment {0}").format(mode_of_payment))
diff --git a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json
index 4ace40a..6dcd591 100644
--- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json
+++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json
@@ -69,6 +69,23 @@
"width": "200px"
},
{
+ "fieldname": "image",
+ "fieldtype": "Attach",
+ "hidden": 1,
+ "label": "Image",
+ "permlevel": 0,
+ "precision": ""
+ },
+ {
+ "fieldname": "image_view",
+ "fieldtype": "Image",
+ "label": "Image View",
+ "options": "image",
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 1
+ },
+ {
"fieldname": "quantity_and_rate",
"fieldtype": "Section Break",
"label": "Quantity and Rate",
@@ -505,7 +522,7 @@
],
"idx": 1,
"istable": 1,
- "modified": "2015-06-02 14:18:45.176726",
+ "modified": "2015-07-02 02:59:08.413213",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice Item",
diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py
index 716cc3d..9b68810 100644
--- a/erpnext/accounts/report/gross_profit/gross_profit.py
+++ b/erpnext/accounts/report/gross_profit/gross_profit.py
@@ -175,16 +175,15 @@
else:
if row.update_stock or row.dn_detail:
+ parenttype, parent, item_row = row.parenttype, row.parent, row.item_row
if row.dn_detail:
- row.parenttype = "Delivery Note"
- row.parent = row.delivery_note
- row.item_row = row.dn_detail
+ parenttype, parent, item_row = "Delivery Note", row.delivery_note, row.dn_detail
my_sle = self.sle.get((item_code, row.warehouse))
for i, sle in enumerate(my_sle):
# find the stock valution rate from stock ledger entry
- if sle.voucher_type == row.parenttype and row.parent == sle.voucher_no and \
- sle.voucher_detail_no == row.item_row:
+ if sle.voucher_type == parenttype and parent == sle.voucher_no and \
+ sle.voucher_detail_no == item_row:
previous_stock_value = len(my_sle) > i+1 and \
flt(my_sle[i+1].stock_value) or 0.0
return previous_stock_value - flt(sle.stock_value)
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json
index dc1dfa4..f65ba40 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.json
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.json
@@ -868,12 +868,21 @@
"no_copy": 1,
"permlevel": 0,
"print_hide": 1
+ },
+ {
+ "depends_on": "eval:doc.is_recurring==1",
+ "fieldname": "recurring_print_format",
+ "fieldtype": "Link",
+ "label": "Recurring Print Format",
+ "options": "Print Format",
+ "permlevel": 0,
+ "precision": ""
}
],
"icon": "icon-file-text",
"idx": 1,
"is_submittable": 1,
- "modified": "2015-06-15 15:38:56.794601",
+ "modified": "2015-06-22 07:30:36.259753",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order",
diff --git a/erpnext/change_log/v5_1_0.md b/erpnext/change_log/v5_1_0.md
new file mode 100644
index 0000000..9a35002
--- /dev/null
+++ b/erpnext/change_log/v5_1_0.md
@@ -0,0 +1,8 @@
+- Item variants is now manageable via dedicated tool **Manage Variants**. To learn about it, check https://manual.erpnext.com/contents/stock/item/item-variants
+- Against account in General Ledger will show Party instead of Account (which is not useful)
+- Print format for recurring documents can be set by the users
+- Recurring documents won't be created for Stopped Sales / Purchase Orders.
+- Lead status will be changed to 'Opportunity' when Lead converted to Opportunity
+- Amount in Journal Entry list view
+- Currency exchange rate is now automatically fetched from fixer.io, instead of jsonrates.com
+- Item image is now available in Sales / Purchase Invoice
\ No newline at end of file
diff --git a/erpnext/controllers/recurring_document.py b/erpnext/controllers/recurring_document.py
index a46fa32..f9ab509 100644
--- a/erpnext/controllers/recurring_document.py
+++ b/erpnext/controllers/recurring_document.py
@@ -33,11 +33,13 @@
next_date = next_date or nowdate()
date_field = date_field_map[doctype]
+
+ condition = " and ifnull(status, '') != 'Stopped'" if doctype in ("Sales Order", "Purchase Order") else ""
recurring_documents = frappe.db.sql("""select name, recurring_id
- from `tab{}` where ifnull(is_recurring, 0)=1
- and docstatus=1 and next_date='{}'
- and next_date <= ifnull(end_date, '2199-12-31')""".format(doctype, next_date))
+ from `tab{0}` where ifnull(is_recurring, 0)=1
+ and docstatus=1 and next_date=%s
+ and next_date <= ifnull(end_date, '2199-12-31') {1}""".format(doctype, condition), next_date)
exception_list = []
for ref_document, recurring_id in recurring_documents:
@@ -124,7 +126,7 @@
frappe.sendmail(new_rv.notification_email_address,
subject= _("New {0}: #{1}").format(new_rv.doctype, new_rv.name),
message = _("Please find attached {0} #{1}").format(new_rv.doctype, new_rv.name),
- attachments = [frappe.attach_print(new_rv.doctype, new_rv.name, file_name=new_rv.name)])
+ attachments = [frappe.attach_print(new_rv.doctype, new_rv.name, file_name=new_rv.name, print_format=new_rv.recurring_print_format)])
def notify_errors(doc, doctype, party, owner):
from frappe.utils.user import get_system_managers
diff --git a/erpnext/crm/doctype/lead/lead.py b/erpnext/crm/doctype/lead/lead.py
index b523416..12de2e5 100644
--- a/erpnext/crm/doctype/lead/lead.py
+++ b/erpnext/crm/doctype/lead/lead.py
@@ -75,8 +75,7 @@
return frappe.db.get_value("Customer", {"lead_name": self.name})
def has_opportunity(self):
- return frappe.db.get_value("Opportunity", {"lead": self.name, "docstatus": 1,
- "status": ["!=", "Lost"]})
+ return frappe.db.get_value("Opportunity", {"lead": self.name, "status": ["!=", "Lost"]})
@frappe.whitelist()
def make_customer(source_name, target_doc=None):
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index d9262d4..8ee63fe 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -5,7 +5,7 @@
app_description = "Open Source Enterprise Resource Planning for Small and Midsized Organizations"
app_icon = "icon-th"
app_color = "#e74c3c"
-app_version = "5.0.29"
+app_version = "5.1.0"
error_report_email = "support@erpnext.com"
@@ -78,8 +78,10 @@
}
scheduler_events = {
+ "hourly": [
+ "erpnext.controllers.recurring_document.create_recurring_documents"
+ ],
"daily": [
- "erpnext.controllers.recurring_document.create_recurring_documents",
"erpnext.stock.reorder_item.reorder_item",
"erpnext.setup.doctype.email_digest.email_digest.send",
"erpnext.support.doctype.issue.issue.auto_close_tickets",
diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js
index 65ecfd8..f0eebb1 100644
--- a/erpnext/manufacturing/doctype/bom/bom.js
+++ b/erpnext/manufacturing/doctype/bom/bom.js
@@ -61,7 +61,7 @@
var d = locals[cdt][cdn];
if (d.item_code) {
return frappe.call({
- doc: cur_frm.doc,
+ doc: doc,
method: "get_bom_material_detail",
args: {
'item_code': d.item_code,
@@ -234,5 +234,3 @@
cur_frm.cscript.image = function() {
refresh_field("image_view");
}
-
-
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index c647210..b21d31b 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -166,4 +166,8 @@
erpnext.patches.v5_0.reset_values_in_tools
execute:frappe.delete_doc("Page", "users")
erpnext.patches.v5_0.update_material_transferred_for_manufacturing_again
-erpnext.patches.v5_0.index_on_account_and_gl_entry
\ No newline at end of file
+erpnext.patches.v5_0.index_on_account_and_gl_entry
+execute:frappe.db.sql("""delete from `tabProject Task`""")
+erpnext.patches.v5_0.item_variants
+erpnext.patches.v5_0.update_item_desc_in_invoice
+erpnext.patches.v5_1.fix_against_account
diff --git a/erpnext/patches/v5_0/item_variants.py b/erpnext/patches/v5_0/item_variants.py
new file mode 100644
index 0000000..62e9ac9
--- /dev/null
+++ b/erpnext/patches/v5_0/item_variants.py
@@ -0,0 +1,16 @@
+import frappe
+
+def execute():
+ frappe.reload_doctype("Item")
+ for dt in ["manage_variants", "manage_variants_item", "variant_attribute"]:
+ frappe.reload_doc("stock", "doctype", dt)
+
+ for d in frappe.get_list("Item", filters={"has_variants":1}):
+ manage_variant = frappe.new_doc("Manage Variants")
+ manage_variant.item_code = d.name
+ manage_variant.attributes = frappe.db.sql("select item_attribute as attribute, item_attribute_value as attribute_value \
+ from `tabItem Variant` where parent = %s", d.name, as_dict=1)
+ if manage_variant.attributes:
+ manage_variant.generate_combinations()
+ manage_variant.create_variants()
+ frappe.delete_doc("DocType", "Item Variant")
\ No newline at end of file
diff --git a/erpnext/patches/v5_0/update_item_desc_in_invoice.py b/erpnext/patches/v5_0/update_item_desc_in_invoice.py
new file mode 100644
index 0000000..b7a071c
--- /dev/null
+++ b/erpnext/patches/v5_0/update_item_desc_in_invoice.py
@@ -0,0 +1,50 @@
+# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
+# License: GNU General Public License v3. See license.txt
+
+import frappe
+from frappe.website.utils import find_first_image
+from frappe.utils import cstr
+import re
+
+def execute():
+ item_details = frappe._dict()
+ for d in frappe.db.sql("select name, description, image from `tabItem`", as_dict=1):
+ description = cstr(d.description).strip()
+ item_details.setdefault(d.name, frappe._dict({
+ "description": description,
+ "image": d.image
+ }))
+
+
+ dt_list= ["Sales Invoice Item","Purchase Invoice Item"]
+ for dt in dt_list:
+ frappe.reload_doctype(dt)
+ records = frappe.db.sql("""select name, item_code, description from `tab{0}`
+ where description is not null """.format(dt), as_dict=1)
+
+ count = 1
+ for d in records:
+ if d.item_code and item_details.get(d.item_code) \
+ and cstr(d.description) == item_details.get(d.item_code).description:
+ desc = item_details.get(d.item_code).description
+ image = item_details.get(d.item_code).image
+ else:
+ desc, image = extract_image_and_description(cstr(d.description))
+
+ if not image:
+ image = item_details.get(d.item_code).image
+
+ frappe.db.sql("""update `tab{0}` set description = %s, image = %s
+ where name = %s """.format(dt), (desc, image, d.name))
+
+ count += 1
+ if count % 500 == 0:
+ frappe.db.commit()
+
+
+def extract_image_and_description(data):
+ image_url = find_first_image(data)
+ desc = data
+ for tag in ("img", "table", "tr", "td"):
+ desc = re.sub("\</*{0}[^>]*\>".format(tag), "", desc)
+ return desc, image_url
\ No newline at end of file
diff --git a/erpnext/patches/v5_1/__init__.py b/erpnext/patches/v5_1/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/patches/v5_1/__init__.py
diff --git a/erpnext/patches/v5_1/fix_against_account.py b/erpnext/patches/v5_1/fix_against_account.py
new file mode 100644
index 0000000..a62c15b
--- /dev/null
+++ b/erpnext/patches/v5_1/fix_against_account.py
@@ -0,0 +1,37 @@
+from __future__ import unicode_literals
+
+import frappe
+
+from erpnext.accounts.doctype.gl_entry.gl_entry import update_against_account
+
+def execute():
+ from_date = "2015-05-01"
+
+ for doc in frappe.get_all("Journal Entry",
+ filters={"creation": (">", from_date), "docstatus": "1"}):
+
+ # update in gl_entry
+ update_against_account("Journal Entry", doc.name)
+
+ # update in jv
+ doc = frappe.get_doc("Journal Entry", doc.name)
+ doc.set_against_account()
+ doc.db_update()
+
+ for doc in frappe.get_all("Sales Invoice",
+ filters={"creation": (">", from_date), "docstatus": "1"},
+ fields=["name", "customer"]):
+
+ frappe.db.sql("""update `tabGL Entry` set against=%s
+ where voucher_type='Sales Invoice' and voucher_no=%s
+ and credit > 0 and ifnull(party, '')=''""",
+ (doc.customer, doc.name))
+
+ for doc in frappe.get_all("Purchase Invoice",
+ filters={"creation": (">", from_date), "docstatus": "1"},
+ fields=["name", "supplier"]):
+
+ frappe.db.sql("""update `tabGL Entry` set against=%s
+ where voucher_type='Purchase Invoice' and voucher_no=%s
+ and debit > 0 and ifnull(party, '')=''""",
+ (doc.supplier, doc.name))
diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py
index 05e4038..6ebafdb 100644
--- a/erpnext/projects/doctype/project/project.py
+++ b/erpnext/projects/doctype/project/project.py
@@ -35,6 +35,7 @@
def validate(self):
self.validate_dates()
self.sync_tasks()
+ self.tasks = []
def validate_dates(self):
if self.expected_start_date and self.expected_end_date:
@@ -45,6 +46,8 @@
"""sync tasks and remove table"""
if self.flags.dont_sync_tasks: return
+
+ task_added_or_deleted = False
task_names = []
for t in self.tasks:
if t.task_id:
@@ -52,6 +55,7 @@
else:
task = frappe.new_doc("Task")
task.project = self.name
+ task_added_or_deleted = True
task.update({
"subject": t.title,
@@ -69,17 +73,22 @@
# delete
for t in frappe.get_all("Task", ["name"], {"project": self.name, "name": ("not in", task_names)}):
frappe.delete_doc("Task", t.name)
+ task_added_or_deleted = True
+
+ if task_added_or_deleted:
+ self.update_project()
- self.tasks = []
+ def update_project(self):
+ self.update_percent_complete()
+ self.update_costing()
def update_percent_complete(self):
- total = frappe.db.sql("""select count(*) from tabTask where project=%s""",
- self.name)[0][0]
+ total = frappe.db.sql("""select count(*) from tabTask where project=%s""", self.name)[0][0]
if total:
completed = frappe.db.sql("""select count(*) from tabTask where
project=%s and status in ('Closed', 'Cancelled')""", self.name)[0][0]
- frappe.db.set_value("Project", self.name, "percent_complete",
- int(float(completed) / total * 100))
+
+ self.percent_complete = flt(completed) / total * 100
def update_costing(self):
total_cost = frappe.db.sql("""select sum(total_costing_amount) as costing_amount,
diff --git a/erpnext/projects/doctype/project/test_project.py b/erpnext/projects/doctype/project/test_project.py
index f69ce80..f1be544 100644
--- a/erpnext/projects/doctype/project/test_project.py
+++ b/erpnext/projects/doctype/project/test_project.py
@@ -5,3 +5,4 @@
import frappe
test_records = frappe.get_test_records('Project')
+test_ignore = ["Sales Order"]
diff --git a/erpnext/projects/doctype/task/task.py b/erpnext/projects/doctype/task/task.py
index f5541cc..7229203 100644
--- a/erpnext/projects/doctype/task/task.py
+++ b/erpnext/projects/doctype/task/task.py
@@ -43,15 +43,8 @@
def on_update(self):
self.check_recursion()
self.reschedule_dependent_tasks()
- self.update_percentage()
self.update_project()
- def update_percentage(self):
- """update percent complete in project"""
- if self.project and not self.flags.from_project:
- project = frappe.get_doc("Project", self.project)
- project.run_method("update_percent_complete")
-
def update_total_expense_claim(self):
self.total_expense_claim = frappe.db.sql("""select sum(total_sanctioned_amount) from `tabExpense Claim`
where project = %s and task = %s and approval_status = "Approved" and docstatus=1""",(self.project, self.name))
@@ -70,10 +63,10 @@
self.act_end_date= tl.end_date
def update_project(self):
- if self.project and frappe.db.exists("Project", self.project):
+ if self.project and not self.flags.from_project:
project = frappe.get_doc("Project", self.project)
project.flags.dont_sync_tasks = True
- project.update_costing()
+ project.update_project()
project.save()
def check_recursion(self):
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index ca0a6cb..91601a4 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -37,6 +37,16 @@
if(this.frm.fields_dict["items"]) {
this["items_remove"] = this.calculate_taxes_and_totals;
}
+
+ if(this.frm.fields_dict["recurring_print_format"]) {
+ this.frm.set_query("recurring_print_format", function(doc) {
+ return{
+ filters: [
+ ['Print Format', 'doc_type', '=', cur_frm.doctype],
+ ]
+ }
+ });
+ }
},
onload_post_render: function() {
@@ -782,3 +792,5 @@
cur_frm.cscript.set_dynamic_labels();
cur_frm.cscript.calculate_taxes_and_totals();
})
+
+
diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js
index 7ede9d4..7f0b386 100644
--- a/erpnext/selling/doctype/sales_order/sales_order.js
+++ b/erpnext/selling/doctype/sales_order/sales_order.js
@@ -183,5 +183,3 @@
cur_frm.email_doc(frappe.boot.notification_settings.sales_order_message);
}
};
-
-;
diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json
index ad62d57..59f54fb 100644
--- a/erpnext/selling/doctype/sales_order/sales_order.json
+++ b/erpnext/selling/doctype/sales_order/sales_order.json
@@ -1074,13 +1074,22 @@
"no_copy": 1,
"permlevel": 0,
"print_hide": 1
+ },
+ {
+ "depends_on": "eval:doc.is_recurring==1",
+ "fieldname": "recurring_print_format",
+ "fieldtype": "Link",
+ "label": "Recurring Print Format",
+ "options": "Print Format",
+ "permlevel": 0,
+ "precision": ""
}
],
"icon": "icon-file-text",
"idx": 1,
"is_submittable": 1,
"issingle": 0,
- "modified": "2015-06-15 15:36:38.898462",
+ "modified": "2015-06-22 07:29:24.379272",
"modified_by": "Administrator",
"module": "Selling",
"name": "Sales Order",
diff --git a/erpnext/selling/doctype/sales_order_item/sales_order_item.json b/erpnext/selling/doctype/sales_order_item/sales_order_item.json
index b4a9c0a..8012bba 100644
--- a/erpnext/selling/doctype/sales_order_item/sales_order_item.json
+++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.json
@@ -58,7 +58,7 @@
"fieldname": "description",
"fieldtype": "Small Text",
"in_filter": 1,
- "in_list_view": 1,
+ "in_list_view": 0,
"label": "Description",
"oldfieldname": "description",
"oldfieldtype": "Small Text",
@@ -498,7 +498,7 @@
],
"idx": 1,
"istable": 1,
- "modified": "2015-05-27 02:47:15.134435",
+ "modified": "2015-07-02 05:37:29.289574",
"modified_by": "Administrator",
"module": "Selling",
"name": "Sales Order Item",
diff --git a/erpnext/selling/page/sales_browser/sales_browser.js b/erpnext/selling/page/sales_browser/sales_browser.js
index 3b9e25a..98d34be 100644
--- a/erpnext/selling/page/sales_browser/sales_browser.js
+++ b/erpnext/selling/page/sales_browser/sales_browser.js
@@ -122,7 +122,7 @@
if(me.ctype == "Sales Person") {
fields.splice(-1, 0, {fieldtype:'Link', fieldname:'employee', label:__('Employee'),
- options:'Employee', description: __("Please enter Employee Id of this sales parson")});
+ options:'Employee', description: __("Please enter Employee Id of this sales person")});
}
// the dialog
diff --git a/erpnext/setup/doctype/global_defaults/global_defaults.json b/erpnext/setup/doctype/global_defaults/global_defaults.json
index 777ae3b..c7330d0 100644
--- a/erpnext/setup/doctype/global_defaults/global_defaults.json
+++ b/erpnext/setup/doctype/global_defaults/global_defaults.json
@@ -66,20 +66,6 @@
"label": "Disable Rounded Total",
"permlevel": 0,
"read_only": 0
- },
- {
- "fieldname": "section_break_8",
- "fieldtype": "Section Break",
- "permlevel": 0,
- "precision": ""
- },
- {
- "description": "For automatic exchange rates go to jsonrates.com and signup for an API key",
- "fieldname": "jsonrates_api_key",
- "fieldtype": "Data",
- "label": "jsonrates.com API Key",
- "permlevel": 0,
- "precision": ""
}
],
"hide_toolbar": 0,
@@ -87,7 +73,7 @@
"idx": 1,
"in_create": 1,
"issingle": 1,
- "modified": "2015-05-07 05:43:49.760061",
+ "modified": "2015-06-30 03:00:26.420003",
"modified_by": "Administrator",
"module": "Setup",
"name": "Global Defaults",
diff --git a/erpnext/setup/doctype/global_defaults/global_defaults.py b/erpnext/setup/doctype/global_defaults/global_defaults.py
index a3fa3a9..efdc875 100644
--- a/erpnext/setup/doctype/global_defaults/global_defaults.py
+++ b/erpnext/setup/doctype/global_defaults/global_defaults.py
@@ -17,7 +17,6 @@
'hide_currency_symbol':'hide_currency_symbol',
'account_url':'account_url',
'disable_rounded_total': 'disable_rounded_total',
- 'jsonrates_api_key': 'jsonrates_api_key'
}
from frappe.model.document import Document
diff --git a/erpnext/setup/utils.py b/erpnext/setup/utils.py
index cef6079..f661edb 100644
--- a/erpnext/setup/utils.py
+++ b/erpnext/setup/utils.py
@@ -60,20 +60,21 @@
@frappe.whitelist()
def get_exchange_rate(from_currency, to_currency):
- jsonrates_api_key = frappe.conf.jsonrates_api_key or frappe.db.get_default("jsonrates_api_key")
-
- if jsonrates_api_key:
+ try:
cache = frappe.cache()
key = "currency_exchange_rate:{0}:{1}".format(from_currency, to_currency)
value = cache.get(key)
if not value:
import requests
- response = requests.get("http://jsonrates.com/get/?from={0}&to={1}&apiKey={2}".format(from_currency,
- to_currency, jsonrates_api_key))
+ response = requests.get("http://api.fixer.io/latest", params={
+ "base": from_currency,
+ "symbols": to_currency
+ })
# expire in 24 hours
- value = response.json().get("rate")
+ response.raise_for_status()
+ value = response.json()["rates"][to_currency]
cache.setex(key, value, 24 * 60 * 60)
return flt(value)
- else:
+ except:
exchange = "%s-%s" % (from_currency, to_currency)
return flt(frappe.db.get_value("Currency Exchange", exchange, "exchange_rate"))
diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js
index ed1367e..58b1adb 100644
--- a/erpnext/stock/doctype/item/item.js
+++ b/erpnext/stock/doctype/item/item.js
@@ -5,39 +5,6 @@
frappe.ui.form.on("Item", {
onload: function(frm) {
- var df = frappe.meta.get_docfield("Item Variant", "item_attribute_value");
- df.on_make = function(field) {
- field.$input.autocomplete({
- minLength: 0,
- minChars: 0,
- source: function(request, response) {
- frappe.call({
- method:"frappe.client.get_list",
- args:{
- doctype:"Item Attribute Value",
- filters: [
- ["parent","=", field.doc.item_attribute],
- ["attribute_value", "like", request.term + "%"]
- ],
- fields: ["attribute_value"]
- },
- callback: function(r) {
- response($.map(r.message, function(d) { return d.attribute_value; }));
- }
- });
- },
- select: function(event, ui) {
- field.$input.val(ui.item.value);
- field.$input.trigger("change");
- },
- focus: function( event, ui ) {
- if(ui.item.action) {
- return false;
- }
- },
- });
- }
-
erpnext.item.setup_queries(frm);
},
@@ -113,8 +80,14 @@
method: "copy_specification_from_item_group"
});
},
+
is_stock_item: function(frm) {
erpnext.item.toggle_reqd(frm);
+ },
+
+ manage_variants: function(frm) {
+ frappe.route_options = {"item_code": frm.doc.name };
+ frappe.set_route("List", "Manage Variants");
}
});
diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json
index 8b10319..acf2764 100644
--- a/erpnext/stock/doctype/item/item.json
+++ b/erpnext/stock/doctype/item/item.json
@@ -12,7 +12,7 @@
{
"fieldname": "name_and_description_section",
"fieldtype": "Section Break",
- "label": "Name and Description",
+ "label": "",
"no_copy": 0,
"oldfieldtype": "Section Break",
"options": "icon-flag",
@@ -167,16 +167,17 @@
"search_index": 0
},
{
- "depends_on": "eval:!!!doc.variant_of",
+ "depends_on": "eval:!doc.variant_of",
"fieldname": "variants_section",
"fieldtype": "Section Break",
- "label": "Variants",
+ "label": "Variant",
"permlevel": 0,
"precision": ""
},
{
"default": "0",
- "description": "Automatically set. If this item has variants, then it cannot be selected in sales orders etc.",
+ "depends_on": "",
+ "description": "If this item has variants, then it cannot be selected in sales orders etc.",
"fieldname": "has_variants",
"fieldtype": "Check",
"label": "Has Variants",
@@ -186,16 +187,38 @@
"read_only": 0
},
{
- "depends_on": "has_variants",
- "description": "A new variant (Item) will be created for each attribute value combination",
- "fieldname": "variants",
- "fieldtype": "Table",
- "label": "Variants",
- "options": "Item Variant",
+ "fieldname": "column_break_18",
+ "fieldtype": "Column Break",
"permlevel": 0,
"precision": ""
},
{
+ "depends_on": "has_variants",
+ "fieldname": "manage_variants",
+ "fieldtype": "Button",
+ "label": "Manage Variants",
+ "permlevel": 0,
+ "precision": ""
+ },
+ {
+ "fieldname": "section_break_20",
+ "fieldtype": "Section Break",
+ "permlevel": 0,
+ "precision": ""
+ },
+ {
+ "depends_on": "variant_of",
+ "fieldname": "attributes",
+ "fieldtype": "Table",
+ "hidden": 0,
+ "label": "Attributes",
+ "no_copy": 1,
+ "options": "Variant Attribute",
+ "permlevel": 0,
+ "precision": "",
+ "read_only": 0
+ },
+ {
"fieldname": "inventory",
"fieldtype": "Section Break",
"label": "Inventory",
@@ -877,9 +900,9 @@
}
],
"icon": "icon-tag",
- "idx": 1,
+ "idx": 1,
"max_attachments": 1,
- "modified": "2015-06-26 17:20:18.204558",
+ "modified": "2015-07-01 17:20:18.204558",
"modified_by": "Administrator",
"module": "Stock",
"name": "Item",
diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py
index aa463ee..ccb0fab 100644
--- a/erpnext/stock/doctype/item/item.py
+++ b/erpnext/stock/doctype/item/item.py
@@ -9,10 +9,9 @@
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
-import copy
+from erpnext.stock.doctype.manage_variants.manage_variants import update_variant
class WarehouseNotSet(frappe.ValidationError): pass
-class DuplicateVariant(frappe.ValidationError): pass
class ItemTemplateCannotHaveStock(frappe.ValidationError): pass
class Item(WebsiteGenerator):
@@ -48,9 +47,6 @@
if self.image and not self.website_image:
self.website_image = self.image
- if self.variant_of:
- self.copy_attributes_to_variant(frappe.get_doc("Item", self.variant_of), self)
-
self.check_warehouse_is_set_for_stock_item()
self.check_stock_uom_with_bin()
self.add_default_uom_in_conversion_factor_table()
@@ -63,9 +59,10 @@
self.cant_change()
self.validate_reorder_level()
self.validate_warehouse_for_reorder()
- self.validate_variants()
self.update_item_desc()
self.synced_with_hub = 0
+ self.validate_has_variants()
+ self.validate_stock_for_template_must_be_zero()
if not self.get("__islocal"):
self.old_item_group = frappe.db.get_value(self.doctype, self.name, "item_group")
@@ -77,7 +74,7 @@
invalidate_cache_for_item(self)
self.validate_name_with_item_group()
self.update_item_price()
- self.sync_variants()
+ self.update_variants()
def get_context(self, context):
context["parent_groups"] = get_parent_item_groups(self.item_group) + \
@@ -133,142 +130,6 @@
if not matched:
frappe.throw(_("Default Unit of Measure can not be changed directly because you have already made some transaction(s) with another UOM. To change default UOM, use 'UOM Replace Utility' tool under Stock module."))
- def validate_variants(self):
- self.validate_variants_are_unique()
- self.validate_stock_for_template_must_be_zero()
-
- 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""", self.name)
- if stock_in:
- frappe.throw(_("Item Template cannot have stock and varaiants. Please remove stock from warehouses {0}").format(", ".join(stock_in)),
- ItemTemplateCannotHaveStock)
-
- def validate_variants_are_unique(self):
- if not self.has_variants:
- self.variants = []
- return
-
- if self.variants:
- if self.variant_of:
- frappe.throw(_("Item cannot be a variant of a variant"))
-
- variants, attributes = [], {}
- for d in self.variants:
- key = (d.item_attribute, d.item_attribute_value)
- if key in variants:
- frappe.throw(_("{0} {1} is entered more than once in Item Variants table")
- .format(d.item_attribute, d.item_attribute_value), DuplicateVariant)
- variants.append(key)
-
- attributes.setdefault(d.item_attribute, [t.attribute_value for t in frappe.db.get_all("Item Attribute Value",
- fields=["attribute_value"], filters={"parent": d.item_attribute })])
-
- if d.item_attribute_value not in attributes.get(d.item_attribute):
- frappe.throw(_("Attribute value {0} does not exist in Item Attribute Master.").format(d.item_attribute_value))
- else:
- frappe.throw(_("Please enter atleast one attribute row in Item Variants table"))
-
- def sync_variants(self):
- variant_item_codes = self.get_variant_item_codes()
-
- # delete missing variants
- existing_variants = [d.name for d in frappe.get_all("Item",
- filters={"variant_of":self.name})]
-
- updated, deleted = [], []
- for existing_variant in existing_variants:
- if existing_variant not in variant_item_codes:
- frappe.delete_doc("Item", existing_variant)
- deleted.append(existing_variant)
- else:
- self.update_variant(existing_variant)
- updated.append(existing_variant)
-
- inserted = []
- for item_code in variant_item_codes:
- if item_code not in existing_variants:
- self.make_variant(item_code)
- inserted.append(item_code)
-
- if inserted:
- frappe.msgprint(_("Item Variants {0} created").format(", ".join(inserted)))
-
- if updated:
- frappe.msgprint(_("Item Variants {0} updated").format(", ".join(updated)))
-
- if deleted:
- frappe.msgprint(_("Item Variants {0} deleted").format(", ".join(deleted)))
-
- def get_variant_item_codes(self):
- """Get all possible suffixes for variants"""
- if not self.variants:
- return []
-
- self.variant_attributes = {}
- variant_dict = {}
- variant_item_codes = []
-
- for d in self.variants:
- variant_dict.setdefault(d.item_attribute, []).append(d.item_attribute_value)
-
- all_attributes = [d.name for d in frappe.get_all("Item Attribute", order_by = "priority asc")]
-
- # sort attributes by their priority
- attributes = filter(None, map(lambda d: d if d in variant_dict else None, all_attributes))
-
- def add_attribute_suffixes(item_code, my_attributes, attributes):
- attr = frappe.get_doc("Item Attribute", attributes[0])
- for value in attr.item_attribute_values:
- if value.attribute_value in variant_dict[attr.name]:
- _my_attributes = copy.deepcopy(my_attributes)
- _my_attributes.append([attr.name, value.attribute_value])
- if len(attributes) > 1:
- add_attribute_suffixes(item_code + "-" + value.abbr, _my_attributes, attributes[1:])
- else:
- variant_item_codes.append(item_code + "-" + value.abbr)
- self.variant_attributes[item_code + "-" + value.abbr] = _my_attributes
-
- add_attribute_suffixes(self.name, [], attributes)
-
- return variant_item_codes
-
- def make_variant(self, item_code):
- item = frappe.new_doc("Item")
- item.item_code = item_code
- self.copy_attributes_to_variant(self, item, insert=True)
- item.insert()
-
- def update_variant(self, item_code):
- item = frappe.get_doc("Item", item_code)
- item.item_code = item_code
- self.copy_attributes_to_variant(self, item)
- item.save()
-
- def copy_attributes_to_variant(self, template, variant, insert=False):
- from frappe.model import no_value_fields
- for field in self.meta.fields:
- if field.fieldtype not in no_value_fields and (insert or not field.no_copy)\
- and field.fieldname not in ("item_code", "item_name"):
- if variant.get(field.fieldname) != template.get(field.fieldname):
- variant.set(field.fieldname, template.get(field.fieldname))
- variant.__dirty = True
-
- variant.description += "\n"
-
- if not getattr(template, "variant_attributes", None):
- template.get_variant_item_codes()
-
- for attr in template.variant_attributes[variant.item_code]:
- variant.description += "<p>" + attr[0] + ": " + attr[1] + "</p>"
-
- variant.item_name = self.item_name + variant.item_code[len(self.name):]
-
- variant.variant_of = template.name
- variant.has_variants = 0
- variant.show_in_website = 0
-
def update_template_tables(self):
template = frappe.get_doc("Item", self.variant_of)
@@ -361,7 +222,8 @@
vals.has_batch_no != self.has_batch_no or
cstr(vals.valuation_method) != cstr(self.valuation_method)):
if self.check_if_sle_exists() == "exists":
- frappe.throw(_("As there are existing stock transactions for this item, you can not change the values of 'Has Serial No', 'Has Batch No', 'Is Stock Item' and 'Valuation Method'"))
+ frappe.throw(_("As there are existing stock transactions for this item, \
+ you can not change the values of 'Has Serial No', 'Has Batch No', 'Is Stock Item' and 'Valuation Method'"))
def validate_reorder_level(self):
if cint(self.apply_warehouse_wise_reorder_level):
@@ -460,9 +322,33 @@
def update_item_desc(self):
if frappe.db.get_value('BOM',self.name, 'description') != self.description:
frappe.db.sql("""update `tabBOM` set description = %s where item = %s and docstatus < 2""",(self.description, self.name))
- frappe.db.sql("""update `tabBOM Item` set description = %s where item_code = %s and docstatus < 2""",(self.description, self.name))
- frappe.db.sql("""update `tabBOM Explosion Item` set description = %s where item_code = %s and docstatus < 2""",(self.description, self.name))
+ frappe.db.sql("""update `tabBOM Item` set description = %s where
+ item_code = %s and docstatus < 2""",(self.description, self.name))
+ frappe.db.sql("""update `tabBOM Explosion Item` set description = %s where
+ item_code = %s and docstatus < 2""",(self.description, self.name))
+
+ def update_variants(self):
+ if self.has_variants:
+ updated = []
+ variants = frappe.db.get_all("Item", fields=["item_code"], filters={"variant_of": self.name })
+ for d in variants:
+ update_variant(self.name, d)
+ updated.append(d.item_code)
+ frappe.msgprint(_("Item Variants {0} updated").format(", ".join(updated)))
+
+ def validate_has_variants(self):
+ if not self.has_variants and frappe.db.get_value("Item", self.name, "has_variants"):
+ if frappe.db.exists("Item", {"variant_of": self.name}):
+ 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_end_of_life(item_code, end_of_life=None, verbose=1):
if not end_of_life:
end_of_life = frappe.db.get_value("Item", item_code, "end_of_life")
@@ -567,3 +453,4 @@
if doc.get("old_item_group") and doc.get("old_item_group") != doc.item_group:
invalidate_cache_for(doc, doc.old_item_group)
+
diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py
index 02ff714..9cf3c07 100644
--- a/erpnext/stock/doctype/item/test_item.py
+++ b/erpnext/stock/doctype/item/test_item.py
@@ -6,7 +6,7 @@
import frappe
from frappe.test_runner import make_test_records
-from erpnext.stock.doctype.item.item import WarehouseNotSet, DuplicateVariant, ItemTemplateCannotHaveStock
+from erpnext.stock.doctype.item.item import WarehouseNotSet, ItemTemplateCannotHaveStock
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
test_ignore = ["BOM"]
@@ -20,60 +20,14 @@
item.insert()
else:
item = frappe.get_doc("Item", item_code)
-
return item
-
- def test_duplicate_variant(self):
- item = frappe.copy_doc(test_records[11])
- item.append("variants", {"item_attribute": "Test Size", "item_attribute_value": "Small"})
- self.assertRaises(DuplicateVariant, item.insert)
-
+
def test_template_cannot_have_stock(self):
- item = self.get_item(10)
-
- se = make_stock_entry(item_code=item.name, target="Stores - _TC", qty=1, incoming_rate=1)
-
- item.has_variants = 1
- item.append("variants", {"item_attribute": "Test Size", "item_attribute_value": "Small"})
-
- self.assertRaises(ItemTemplateCannotHaveStock, item.save)
-
- def test_variant_item_codes(self):
- item = self.get_item(11)
-
- variants = ['_Test Variant Item-S', '_Test Variant Item-M', '_Test Variant Item-L']
- self.assertEqual(item.get_variant_item_codes(), variants)
- for v in variants:
- self.assertTrue(frappe.db.get_value("Item", {"variant_of": item.name, "name": v}))
-
- item.append("variants", {"item_attribute": "Test Colour", "item_attribute_value": "Red"})
- item.append("variants", {"item_attribute": "Test Colour", "item_attribute_value": "Blue"})
- item.append("variants", {"item_attribute": "Test Colour", "item_attribute_value": "Green"})
-
- self.assertEqual(item.get_variant_item_codes(), ['_Test Variant Item-S-R',
- '_Test Variant Item-S-G', '_Test Variant Item-S-B',
- '_Test Variant Item-M-R', '_Test Variant Item-M-G',
- '_Test Variant Item-M-B', '_Test Variant Item-L-R',
- '_Test Variant Item-L-G', '_Test Variant Item-L-B'])
-
- self.assertEqual(item.variant_attributes['_Test Variant Item-L-R'], [['Test Size', 'Large'], ['Test Colour', 'Red']])
- self.assertEqual(item.variant_attributes['_Test Variant Item-S-G'], [['Test Size', 'Small'], ['Test Colour', 'Green']])
-
- # check stock entry cannot be made
- def test_stock_entry_cannot_be_made_for_template(self):
- item = self.get_item(11)
-
- se = frappe.new_doc("Stock Entry")
- se.purpose = "Material Receipt"
- se.append("items", {
- "item_code": item.name,
- "t_warehouse": "Stores - _TC",
- "qty": 1,
- "incoming_rate": 1
- })
- se.insert()
- self.assertRaises(ItemTemplateCannotHaveStock, se.submit)
-
+ item = self.get_item(10)
+ se = make_stock_entry(item_code=item.name, target="Stores - _TC", qty=1, incoming_rate=1)
+ item.has_variants = 1
+ self.assertRaises(ItemTemplateCannotHaveStock, item.save)
+
def test_default_warehouse(self):
item = frappe.copy_doc(test_records[0])
item.is_stock_item = "Yes"
diff --git a/erpnext/stock/doctype/item/test_records.json b/erpnext/stock/doctype/item/test_records.json
index dc095c6..5a02c6b 100644
--- a/erpnext/stock/doctype/item/test_records.json
+++ b/erpnext/stock/doctype/item/test_records.json
@@ -273,11 +273,6 @@
"item_name": "_Test Variant Item",
"stock_uom": "_Test UOM",
"has_variants": 1,
- "variants": [
- {"item_attribute": "Test Size", "item_attribute_value": "Small"},
- {"item_attribute": "Test Size", "item_attribute_value": "Medium"},
- {"item_attribute": "Test Size", "item_attribute_value": "Large"}
- ],
"apply_warehouse_wise_reorder_level": 1,
"reorder_levels": [
{
diff --git a/erpnext/stock/doctype/manage_variants/__init__.py b/erpnext/stock/doctype/manage_variants/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/stock/doctype/manage_variants/__init__.py
diff --git a/erpnext/stock/doctype/manage_variants/manage_variants.js b/erpnext/stock/doctype/manage_variants/manage_variants.js
new file mode 100644
index 0000000..ba5c46e
--- /dev/null
+++ b/erpnext/stock/doctype/manage_variants/manage_variants.js
@@ -0,0 +1,56 @@
+// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
+// License: GNU General Public License v3. See license.txt
+
+frappe.ui.form.on("Manage Variants", {
+ onload: function(frm) {
+ var df = frappe.meta.get_docfield("Variant Attribute", "attribute_value");
+ df.on_make = function(field) {
+ $(field.input_area).addClass("ui-front");
+ field.$input.autocomplete({
+ minLength: 0,
+ minChars: 0,
+ source: function(request, response) {
+ frappe.call({
+ method:"frappe.client.get_list",
+ args:{
+ doctype:"Item Attribute Value",
+ filters: [
+ ["parent","=", field.doc.attribute],
+ ["attribute_value", "like", request.term + "%"]
+ ],
+ fields: ["attribute_value"]
+ },
+ callback: function(r) {
+ response($.map(r.message, function(d) { return d.attribute_value; }));
+ }
+ });
+ },
+ select: function(event, ui) {
+ field.$input.val(ui.item.value);
+ field.$input.trigger("change");
+ }
+ });
+ }
+ },
+
+ refresh: function(frm) {
+ frm.disable_save();
+ frm.page.set_primary_action(__("Create Variants"), function() {
+ frappe.call({
+ method: "create_variants",
+ doc:frm.doc
+ })
+ });
+ },
+
+ item_code:function(frm) {
+ return frappe.call({
+ method: "get_item_details",
+ doc:frm.doc,
+ callback: function(r) {
+ refresh_field('attributes');
+ refresh_field('variants');
+ }
+ })
+ }
+});
diff --git a/erpnext/stock/doctype/manage_variants/manage_variants.json b/erpnext/stock/doctype/manage_variants/manage_variants.json
new file mode 100644
index 0000000..7c61620
--- /dev/null
+++ b/erpnext/stock/doctype/manage_variants/manage_variants.json
@@ -0,0 +1,103 @@
+{
+ "allow_copy": 0,
+ "allow_import": 0,
+ "allow_rename": 0,
+ "creation": "2015-05-19 05:39:59.345901",
+ "custom": 0,
+ "docstatus": 0,
+ "doctype": "DocType",
+ "document_type": "",
+ "fields": [
+ {
+ "fieldname": "item_code",
+ "fieldtype": "Link",
+ "label": "Item Code",
+ "options": "Item",
+ "permlevel": 0,
+ "precision": "",
+ "reqd": 1
+ },
+ {
+ "fieldname": "section_break_2",
+ "fieldtype": "Section Break",
+ "label": "Item Variant Attributes",
+ "permlevel": 0,
+ "precision": ""
+ },
+ {
+ "allow_on_submit": 0,
+ "fieldname": "attributes",
+ "fieldtype": "Table",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "in_filter": 0,
+ "in_list_view": 0,
+ "label": "Attributes",
+ "no_copy": 0,
+ "options": "Variant Attribute",
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "read_only": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0
+ },
+ {
+ "fieldname": "generate_combinations",
+ "fieldtype": "Button",
+ "label": "Generate Combinations",
+ "options": "generate_combinations",
+ "permlevel": 0,
+ "precision": ""
+ },
+ {
+ "fieldname": "section_break_4",
+ "fieldtype": "Section Break",
+ "label": "Item Variants",
+ "permlevel": 0,
+ "precision": ""
+ },
+ {
+ "fieldname": "variants",
+ "fieldtype": "Table",
+ "label": "Variants",
+ "options": "Manage Variants Item",
+ "permlevel": 0,
+ "precision": ""
+ }
+ ],
+ "hide_heading": 0,
+ "hide_toolbar": 0,
+ "in_create": 1,
+ "in_dialog": 0,
+ "is_submittable": 0,
+ "issingle": 1,
+ "istable": 0,
+ "modified": "2015-06-30 13:40:59.946655",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "Manage Variants",
+ "name_case": "",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 0,
+ "permlevel": 0,
+ "print": 1,
+ "read": 1,
+ "report": 0,
+ "role": "Material Master Manager",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "read_only": 1,
+ "read_only_onload": 0,
+ "sort_field": "modified",
+ "sort_order": "DESC"
+}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/manage_variants/manage_variants.py b/erpnext/stock/doctype/manage_variants/manage_variants.py
new file mode 100644
index 0000000..b6784d3
--- /dev/null
+++ b/erpnext/stock/doctype/manage_variants/manage_variants.py
@@ -0,0 +1,207 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe
+from frappe import _
+from frappe.model.document import Document
+import copy
+import json
+
+class DuplicateAttribute(frappe.ValidationError): pass
+
+class ManageVariants(Document):
+
+ def get_item_details(self):
+ self.clear_tables()
+ if self.item_code:
+ self.get_attributes()
+ self.get_variants()
+
+ def generate_combinations(self):
+ self.validate_attributes()
+ self.validate_template_item()
+ self.validate_attribute_values()
+ self.validate_attributes_are_unique()
+ self.get_variant_item_codes()
+
+ def create_variants(self):
+ self.sync_variants()
+
+ def clear_tables(self):
+ self.set('attributes', [])
+ self.set('variants', [])
+
+ def get_attributes(self):
+ attributes = {}
+ self.set('attributes', [])
+ for d in frappe.db.sql("""select attribute, attribute_value from `tabVariant Attribute` as attribute,
+ `tabItem` as item where attribute.parent= item.name and item.variant_of = %s""", self.item_code, as_dict=1):
+ attributes.setdefault(d.attribute, []).append(d.attribute_value)
+ for d in attributes:
+ attribute_values = set(attributes[d])
+ for value in attribute_values:
+ self.append('attributes',{"attribute": d, "attribute_value": value})
+
+ def get_variants(self):
+ variants = [d.name for d in frappe.get_all("Item",
+ filters={"variant_of":self.item_code})]
+ data = frappe.db.sql("""select parent, attribute, attribute_value from `tabVariant Attribute`""", as_dict=1)
+ for d in variants:
+ variant_attributes, attributes = "", []
+ for attribute in data:
+ if attribute.parent == d:
+ variant_attributes += attribute.attribute_value + " | "
+ attributes.append([attribute.attribute, attribute.attribute_value])
+ self.append('variants',{"variant": d, "variant_attributes": variant_attributes[: -3], "attributes": json.dumps(attributes)})
+
+ def validate_attributes(self):
+ if not self.attributes:
+ frappe.throw(_("Enter atleast one Attribute & its Value in Attribute table."))
+
+ def validate_template_item(self):
+ if not frappe.db.get_value("Item", self.item_code, "has_variants"):
+ frappe.throw(_("Selected Item cannot have Variants."))
+
+ if frappe.db.get_value("Item", self.item_code, "variant_of"):
+ frappe.throw(_("Item cannot be a variant of a variant"))
+
+ def validate_attribute_values(self):
+ attributes = {}
+ for t in frappe.db.get_all("Item Attribute Value", fields=["parent", "attribute_value"]):
+ attributes.setdefault(t.parent, []).append(t.attribute_value)
+
+ for d in self.attributes:
+ if d.attribute_value not in attributes.get(d.attribute):
+ frappe.throw(_("Attribute value {0} does not exist in Item Attribute Master.").format(d.attribute_value))
+
+ def validate_attributes_are_unique(self):
+ attributes = []
+ for d in self.attributes:
+ key = (d.attribute, d.attribute_value)
+ if key in attributes:
+ frappe.throw(_("{0} {1} is entered more than once in Attributes table")
+ .format(d.attribute, d.attribute_value), DuplicateAttribute)
+ attributes.append(key)
+
+ def get_variant_item_codes(self):
+ """Get all possible suffixes for variants"""
+ variant_dict = {}
+ self.set('variants', [])
+
+ for d in self.attributes:
+ variant_dict.setdefault(d.attribute, []).append(d.attribute_value)
+
+ all_attributes = [d.name for d in frappe.get_all("Item Attribute", order_by = "priority asc")]
+
+ # sort attributes by their priority
+ attributes = filter(None, map(lambda d: d if d in variant_dict else None, all_attributes))
+
+ def add_attribute_suffixes(item_code, my_attributes, attributes):
+ attr = frappe.get_doc("Item Attribute", attributes[0])
+ for value in attr.item_attribute_values:
+ if value.attribute_value in variant_dict[attr.name]:
+ _my_attributes = copy.deepcopy(my_attributes)
+ _my_attributes.append([attr.name, value.attribute_value])
+ if len(attributes) > 1:
+ add_attribute_suffixes(item_code + "-" + value.abbr, _my_attributes, attributes[1:])
+ else:
+ variant_attributes = ""
+ for d in _my_attributes:
+ variant_attributes += d[1] + " | "
+ self.append('variants', {"variant": item_code + "-" + value.abbr,
+ "attributes": json.dumps(_my_attributes), "variant_attributes": variant_attributes[: -3]})
+ add_attribute_suffixes(self.item_code, [], attributes)
+
+ def sync_variants(self):
+ variant_item_codes = []
+ item_variants_attributes = {}
+ inserted, updated, old_variant_name, new_variant_name, deleted = [], [], [], [], []
+
+ for v in self.variants:
+ variant_item_codes.append(v.variant)
+
+ existing_variants = [d.name for d in frappe.get_all("Item",
+ filters={"variant_of":self.item_code})]
+
+ for d in existing_variants:
+ attributes = []
+ for attribute in frappe.db.sql("""select attribute, attribute_value from `tabVariant Attribute` where parent = %s""", d):
+ attributes.append([attribute[0], attribute[1]])
+ item_variants_attributes.setdefault(d, []).append(attributes)
+
+ for existing_variant in existing_variants:
+ if existing_variant not in variant_item_codes:
+ att = item_variants_attributes[existing_variant][0]
+ for variant in self.variants:
+ if sorted(json.loads(variant.attributes) ,key=lambda x: x[0]) == \
+ sorted(att ,key=lambda x: x[0]):
+ rename_variant(existing_variant, variant.variant)
+ old_variant_name.append(existing_variant)
+ new_variant_name.append(variant.variant)
+
+ if existing_variant not in old_variant_name:
+ delete_variant(existing_variant)
+ deleted.append(existing_variant)
+
+ for item_code in variant_item_codes:
+ if item_code not in existing_variants:
+ if item_code not in new_variant_name:
+ make_variant(self.item_code, item_code, self.variants)
+ inserted.append(item_code)
+ else:
+ update_variant(self.item_code, item_code, self.variants)
+ updated.append(item_code)
+
+ if inserted:
+ frappe.msgprint(_("Item Variants {0} created").format(", ".join(inserted)))
+
+ if updated:
+ frappe.msgprint(_("Item Variants {0} updated").format(", ".join(updated)))
+
+ if old_variant_name:
+ frappe.msgprint(_("Item Variants {0} renamed").format(", ".join(old_variant_name)))
+
+ if deleted:
+ frappe.msgprint(_("Item Variants {0} deleted").format(", ".join(deleted)))
+
+def make_variant(item, variant_code, variant_attribute):
+ variant = frappe.new_doc("Item")
+ variant.item_code = variant_code
+ copy_attributes_to_variant(item, variant, variant_attribute, insert=True)
+ variant.insert()
+
+def update_variant(item, variant_code, variant_attribute=None):
+ variant = frappe.get_doc("Item", variant_code)
+ copy_attributes_to_variant(item, variant, variant_attribute, insert=True)
+ variant.save()
+
+def rename_variant(old_variant_code, new_variant_code):
+ frappe.rename_doc("Item", old_variant_code, new_variant_code)
+
+def delete_variant(variant_code):
+ frappe.delete_doc("Item", variant_code)
+
+def copy_attributes_to_variant(item, variant, variant_attribute=None, insert=False):
+ template = frappe.get_doc("Item", item)
+ from frappe.model import no_value_fields
+ for field in template.meta.fields:
+ if field.fieldtype not in no_value_fields and (insert or not field.no_copy)\
+ and field.fieldname not in ("item_code", "item_name"):
+ if variant.get(field.fieldname) != template.get(field.fieldname):
+ variant.set(field.fieldname, template.get(field.fieldname))
+ variant.item_name = template.item_name + variant.item_code[len(template.name):]
+ variant.variant_of = template.name
+ variant.has_variants = 0
+ variant.show_in_website = 0
+ if variant_attribute:
+ for d in variant_attribute:
+ if d.variant == variant.item_code:
+ variant.attributes= []
+ for a in json.loads(d.attributes):
+ variant.append('attributes', {"attribute": a[0], "attribute_value": a[1]})
+ if variant.attributes:
+ variant.description += "\n"
+ for d in variant.attributes:
+ variant.description += "<p>" + d.attribute + ": " + d.attribute_value + "</p>"
\ No newline at end of file
diff --git a/erpnext/stock/doctype/manage_variants/test_manage_variants.py b/erpnext/stock/doctype/manage_variants/test_manage_variants.py
new file mode 100644
index 0000000..9952ba9
--- /dev/null
+++ b/erpnext/stock/doctype/manage_variants/test_manage_variants.py
@@ -0,0 +1,49 @@
+# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
+# License: GNU General Public License v3. See license.txt
+
+from __future__ import unicode_literals
+import unittest
+import frappe
+
+from erpnext.stock.doctype.manage_variants.manage_variants import DuplicateAttribute
+
+class TestManageVariants(unittest.TestCase):
+ def test_variant_item_codes(self):
+ manage_variant = frappe.new_doc("Manage Variants")
+ manage_variant.update({
+ "item": "_Test Variant Item",
+ "attributes": [
+ {
+ "attribute": "Test Size",
+ "attribute_value": "Small"
+ },
+ {
+ "attribute": "Test Size",
+ "attribute_value": "Large"
+ }
+ ]
+ })
+ manage_variant.generate_combinations()
+ self.assertEqual(manage_variant.variants[0].variant, "_Test Variant Item-S")
+ self.assertEqual(manage_variant.variants[1].variant, "_Test Variant Item-L")
+
+ self.assertEqual(manage_variant.variants[0].variant_attributes, "Small")
+ self.assertEqual(manage_variant.variants[1].variant_attributes, "Large")
+ manage_variant.create_variants()
+
+ def test_attributes_are_unique(self):
+ manage_variant = frappe.new_doc("Manage Variants")
+ manage_variant.update({
+ "item": "_Test Variant Item",
+ "attributes": [
+ {
+ "attribute": "Test Size",
+ "attribute_value": "Small"
+ },
+ {
+ "attribute": "Test Size",
+ "attribute_value": "Small"
+ }
+ ]
+ })
+ self.assertRaises(DuplicateAttribute, manage_variant.generate_combinations)
diff --git a/erpnext/stock/doctype/manage_variants_item/__init__.py b/erpnext/stock/doctype/manage_variants_item/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/stock/doctype/manage_variants_item/__init__.py
diff --git a/erpnext/stock/doctype/manage_variants_item/manage_variants_item.json b/erpnext/stock/doctype/manage_variants_item/manage_variants_item.json
new file mode 100644
index 0000000..a8bb61d
--- /dev/null
+++ b/erpnext/stock/doctype/manage_variants_item/manage_variants_item.json
@@ -0,0 +1,76 @@
+{
+ "allow_copy": 0,
+ "allow_import": 1,
+ "allow_rename": 0,
+ "autoname": "",
+ "creation": "2015-05-19 05:55:31.155672",
+ "custom": 0,
+ "docstatus": 0,
+ "doctype": "DocType",
+ "document_type": "Other",
+ "fields": [
+ {
+ "allow_on_submit": 0,
+ "fieldname": "variant",
+ "fieldtype": "Data",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "in_filter": 0,
+ "in_list_view": 1,
+ "label": "Variant",
+ "no_copy": 0,
+ "options": "",
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "read_only": 0,
+ "report_hide": 0,
+ "reqd": 1,
+ "search_index": 0,
+ "set_only_once": 0
+ },
+ {
+ "fieldname": "column_break_2",
+ "fieldtype": "Column Break",
+ "permlevel": 0,
+ "precision": ""
+ },
+ {
+ "fieldname": "variant_attributes",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Variant Attributes",
+ "permlevel": 0,
+ "precision": "",
+ "read_only": 1
+ },
+ {
+ "fieldname": "attributes",
+ "fieldtype": "Text",
+ "hidden": 1,
+ "label": "attributes",
+ "permlevel": 0,
+ "precision": "",
+ "read_only": 1
+ }
+ ],
+ "hide_heading": 0,
+ "hide_toolbar": 0,
+ "icon": "",
+ "in_create": 0,
+ "in_dialog": 0,
+ "is_submittable": 0,
+ "issingle": 0,
+ "istable": 1,
+ "modified": "2015-06-30 03:19:07.548196",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "Manage Variants Item",
+ "name_case": "",
+ "owner": "Administrator",
+ "permissions": [],
+ "read_only": 0,
+ "read_only_onload": 0,
+ "sort_field": "modified",
+ "sort_order": "DESC"
+}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/manage_variants_item/manage_variants_item.py b/erpnext/stock/doctype/manage_variants_item/manage_variants_item.py
new file mode 100644
index 0000000..800888a
--- /dev/null
+++ b/erpnext/stock/doctype/manage_variants_item/manage_variants_item.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe
+from frappe.model.document import Document
+
+class ManageVariantsItem(Document):
+ pass
diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
index ff0b272..8114bef 100644
--- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
@@ -72,6 +72,18 @@
self._test_auto_material_request("_Test Item")
def test_auto_material_request_for_variant(self):
+ manage_variant = frappe.new_doc("Manage Variants")
+ manage_variant.update({
+ "item": "_Test Variant Item",
+ "attributes": [
+ {
+ "attribute": "Test Size",
+ "attribute_value": "Small"
+ }
+ ]
+ })
+ manage_variant.generate_combinations()
+ manage_variant.create_variants()
self._test_auto_material_request("_Test Variant Item-S")
def _test_auto_material_request(self, item_code):
diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json
index fb654c2..a373185 100644
--- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json
+++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json
@@ -100,7 +100,7 @@
{
"fieldname": "description",
"fieldtype": "Text",
- "in_list_view": 1,
+ "in_list_view": 0,
"label": "Description",
"oldfieldname": "description",
"oldfieldtype": "Text",
@@ -344,7 +344,7 @@
],
"idx": 1,
"istable": 1,
- "modified": "2015-02-25 04:31:21.801200",
+ "modified": "2015-07-02 05:32:56.511570",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Entry Detail",
diff --git a/erpnext/stock/doctype/variant_attribute/__init__.py b/erpnext/stock/doctype/variant_attribute/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/stock/doctype/variant_attribute/__init__.py
diff --git a/erpnext/stock/doctype/variant_attribute/variant_attribute.json b/erpnext/stock/doctype/variant_attribute/variant_attribute.json
new file mode 100644
index 0000000..5ab3d73
--- /dev/null
+++ b/erpnext/stock/doctype/variant_attribute/variant_attribute.json
@@ -0,0 +1,78 @@
+{
+ "allow_copy": 0,
+ "allow_import": 1,
+ "allow_rename": 0,
+ "autoname": "",
+ "creation": "2015-05-19 05:12:30.344797",
+ "custom": 0,
+ "docstatus": 0,
+ "doctype": "DocType",
+ "document_type": "Other",
+ "fields": [
+ {
+ "allow_on_submit": 0,
+ "fieldname": "attribute",
+ "fieldtype": "Link",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "in_filter": 0,
+ "in_list_view": 1,
+ "label": "Attribute",
+ "no_copy": 0,
+ "options": "Item Attribute",
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "read_only": 0,
+ "report_hide": 0,
+ "reqd": 1,
+ "search_index": 0,
+ "set_only_once": 0
+ },
+ {
+ "fieldname": "column_break_2",
+ "fieldtype": "Column Break",
+ "permlevel": 0,
+ "precision": ""
+ },
+ {
+ "allow_on_submit": 0,
+ "fieldname": "attribute_value",
+ "fieldtype": "Data",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "in_filter": 0,
+ "in_list_view": 1,
+ "label": "Attribute Value",
+ "no_copy": 0,
+ "options": "",
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "read_only": 0,
+ "report_hide": 0,
+ "reqd": 1,
+ "search_index": 0,
+ "set_only_once": 0
+ }
+ ],
+ "hide_heading": 0,
+ "hide_toolbar": 0,
+ "icon": "",
+ "in_create": 0,
+ "in_dialog": 0,
+ "is_submittable": 0,
+ "issingle": 0,
+ "istable": 1,
+ "modified": "2015-05-20 06:16:16.803578",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "Variant Attribute",
+ "name_case": "",
+ "owner": "Administrator",
+ "permissions": [],
+ "read_only": 0,
+ "read_only_onload": 0,
+ "sort_field": "modified",
+ "sort_order": "DESC"
+}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/variant_attribute/variant_attribute.py b/erpnext/stock/doctype/variant_attribute/variant_attribute.py
new file mode 100644
index 0000000..9c35732
--- /dev/null
+++ b/erpnext/stock/doctype/variant_attribute/variant_attribute.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe
+from frappe.model.document import Document
+
+class VariantAttribute(Document):
+ pass
diff --git a/erpnext/support/doctype/maintenance_schedule/maintenance_schedule.py b/erpnext/support/doctype/maintenance_schedule/maintenance_schedule.py
index 2398950..0ecc9c0 100644
--- a/erpnext/support/doctype/maintenance_schedule/maintenance_schedule.py
+++ b/erpnext/support/doctype/maintenance_schedule/maintenance_schedule.py
@@ -4,7 +4,7 @@
from __future__ import unicode_literals
import frappe
-from frappe.utils import add_days, getdate, cint
+from frappe.utils import add_days, getdate, cint, cstr
from frappe import throw, _
from erpnext.utilities.transaction_base import TransactionBase, delete_events
@@ -73,7 +73,7 @@
"owner": email_map[d.sales_person] or self.owner,
"subject": description,
"description": description,
- "starts_on": key["scheduled_date"] + " 10:00:00",
+ "starts_on": cstr(key["scheduled_date"]) + " 10:00:00",
"event_type": "Private",
"ref_type": self.doctype,
"ref_name": self.name
diff --git a/erpnext/templates/form_grid/includes/visible_cols.html b/erpnext/templates/form_grid/includes/visible_cols.html
index c26f279..caa1366 100644
--- a/erpnext/templates/form_grid/includes/visible_cols.html
+++ b/erpnext/templates/form_grid/includes/visible_cols.html
@@ -9,5 +9,7 @@
{%= doc.get_formatted(df.fieldname) %}
</div>
</div>
+ {% } else if (df.fieldname==="description") { %}
+ <p>{{ doc.description }}</p>
{% } %}
{% }); %}
diff --git a/erpnext/templates/form_grid/item_grid.html b/erpnext/templates/form_grid/item_grid.html
index d2db463..197cef4 100644
--- a/erpnext/templates/form_grid/item_grid.html
+++ b/erpnext/templates/form_grid/item_grid.html
@@ -1,4 +1,6 @@
-{% var visible_columns = row.get_visible_columns(["item_code", "qty", "rate", "amount", "stock_uom", "uom", "discount_percentage", "schedule_date", "warehouse", "against_sales_order", "sales_order"]); %}
+{% var visible_columns = row.get_visible_columns(["item_code", "qty", "rate", "amount",
+ "stock_uom", "uom", "discount_percentage", "schedule_date", "warehouse",
+ "against_sales_order", "sales_order"]); %}
{% if(!doc) { %}
<div class="row">
@@ -48,12 +50,7 @@
{% if(doc.item_name != doc.item_code && in_list(visible_column_fieldnames, "item_name")) { %}
<br>{%= doc.item_name %}{% } %}
-
- {% if((doc.description != doc.item_code != doc.item_name) &&
- in_list(visible_column_fieldnames, "description")) { %}
- <br>
- <strong>Description: </strong>
- {%= doc.get_formatted("description") %}{% } %}
+
{% include "templates/form_grid/includes/visible_cols.html" %}
</div>
diff --git a/erpnext/utilities/transaction_base.py b/erpnext/utilities/transaction_base.py
index 60889c5..be74aeb 100644
--- a/erpnext/utilities/transaction_base.py
+++ b/erpnext/utilities/transaction_base.py
@@ -22,16 +22,21 @@
self.posting_time = now_datetime().strftime('%H:%M:%S')
def add_calendar_event(self, opts, force=False):
- if self.contact_by != cstr(self._prev.contact_by) or \
- self.contact_date != cstr(self._prev.contact_date) or force:
+ if cstr(self.contact_by) != cstr(self._prev.contact_by) or \
+ cstr(self.contact_date) != cstr(self._prev.contact_date) or force:
self.delete_events()
self._add_calendar_event(opts)
def delete_events(self):
- frappe.delete_doc("Event", frappe.db.sql_list("""select name from `tabEvent`
- where ref_type=%s and ref_name=%s""", (self.doctype, self.name)),
- ignore_permissions=True)
+ events = frappe.db.sql_list("""select name from `tabEvent`
+ where ref_type=%s and ref_name=%s""", (self.doctype, self.name))
+ if events:
+ frappe.db.sql("delete from `tabEvent` where name in (%s)"
+ .format(", ".join(['%s']*len(events))), tuple(events))
+
+ frappe.db.sql("delete from `tabEvent Role` where parent in (%s)"
+ .format(", ".join(['%s']*len(events))), tuple(events))
def _add_calendar_event(self, opts):
opts = frappe._dict(opts)
diff --git a/setup.py b/setup.py
index ea74fa8..0694a82 100644
--- a/setup.py
+++ b/setup.py
@@ -1,6 +1,6 @@
from setuptools import setup, find_packages
-version = "5.0.29"
+version = "5.1.0"
with open("requirements.txt", "r") as f:
install_requires = f.readlines()