diff --git a/erpnext/__version__.py b/erpnext/__version__.py index b0b41d353db..5069b2fe32c 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 138cf23d381..3d306fb8d5f 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.py +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py @@ -127,7 +127,7 @@ def update_outstanding_amt(account, party_type, party, against_voucher_type, aga 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 @@ def validate_frozen_account(account, adv_adj=None): 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 6eb395d4861..249fcc46d30 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 7bf6c56dc52..489d4889a11 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -81,7 +81,7 @@ class JournalEntry(AccountsController): 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 @@ class JournalEntry(AccountsController): 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 @@ class JournalEntry(AccountsController): 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 @@ class JournalEntry(AccountsController): 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 3e16a312cbc..d404851a187 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -224,3 +224,4 @@ cur_frm.cscript.select_print_heading = function(doc,cdt,cdn){ 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 e192e763180..dcbc60521e8 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 660b221e7bd..50a79ec01be 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -164,7 +164,7 @@ class PurchaseInvoice(BuyingController): 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 @@ class PurchaseInvoice(BuyingController): 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 @@ class PurchaseInvoice(BuyingController): 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 @@ class PurchaseInvoice(BuyingController): 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 @@ class PurchaseInvoice(BuyingController): 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 @@ class PurchaseInvoice(BuyingController): 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 @@ class PurchaseInvoice(BuyingController): 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 @@ class PurchaseInvoice(BuyingController): 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 1280cc0c1e6..ce6ac7aa3b6 100755 --- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json +++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json @@ -49,6 +49,23 @@ "read_only": 0, "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", @@ -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 b35fa8ae84b..3bb9aa0bac3 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.cscript.on_submit = function(doc, cdt, cdn) { } } - - 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 a021be41d2f..045678dc545 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -1227,6 +1227,15 @@ "print_hide": 1, "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", @@ -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 87f723d4f82..604370b3262 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -503,7 +503,7 @@ class SalesInvoice(SellingController): 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 @@ class SalesInvoice(SellingController): 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 @@ class SalesInvoice(SellingController): 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 @@ class SalesInvoice(SellingController): 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 @@ def get_list_context(context=None): @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 4ace40a672a..6dcd59137a6 100644 --- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json +++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json @@ -68,6 +68,23 @@ "reqd": 1, "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", @@ -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 716cc3d4f62..9b688109d97 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -175,16 +175,15 @@ class GrossProfitGenerator(object): 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 dc1dfa42bde..f65ba40a92c 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 00000000000..9a350025f44 --- /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 a46fa324398..f9ab50965c5 100644 --- a/erpnext/controllers/recurring_document.py +++ b/erpnext/controllers/recurring_document.py @@ -33,11 +33,13 @@ def manage_recurring_documents(doctype, next_date=None, commit=True): 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 @@ def send_notification(new_rv): 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 b523416484d..12de2e58193 100644 --- a/erpnext/crm/doctype/lead/lead.py +++ b/erpnext/crm/doctype/lead/lead.py @@ -75,8 +75,7 @@ class Lead(SellingController): 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 d9262d483cf..8ee63feaf36 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -5,7 +5,7 @@ app_publisher = "Frappe Technologies Pvt. Ltd. and Contributors" 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 @@ doc_events = { } 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 65ecfd8b90b..f0eebb1a2a2 100644 --- a/erpnext/manufacturing/doctype/bom/bom.js +++ b/erpnext/manufacturing/doctype/bom/bom.js @@ -61,7 +61,7 @@ var get_bom_material_detail= function(doc, cdt, cdn) { 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 @@ frappe.ui.form.on("BOM", "with_operations", function(frm) { cur_frm.cscript.image = function() { refresh_field("image_view"); } - - diff --git a/erpnext/patches.txt b/erpnext/patches.txt index c6472109b16..b21d31b5c86 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -166,4 +166,8 @@ erpnext.patches.v5_0.portal_fixes 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 00000000000..62e9ac9e1f0 --- /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 00000000000..b7a071cbfea --- /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("\]*\>".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 00000000000..e69de29bb2d 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 00000000000..a62c15b7d19 --- /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 05e40388bf9..6ebafdba59e 100644 --- a/erpnext/projects/doctype/project/project.py +++ b/erpnext/projects/doctype/project/project.py @@ -35,6 +35,7 @@ class Project(Document): 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 @@ class Project(Document): """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 @@ class Project(Document): else: task = frappe.new_doc("Task") task.project = self.name + task_added_or_deleted = True task.update({ "subject": t.title, @@ -69,17 +73,22 @@ class Project(Document): # 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 f69ce808248..f1be54492f5 100644 --- a/erpnext/projects/doctype/project/test_project.py +++ b/erpnext/projects/doctype/project/test_project.py @@ -5,3 +5,4 @@ from __future__ import unicode_literals 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 f5541cc0e5e..7229203395c 100644 --- a/erpnext/projects/doctype/task/task.py +++ b/erpnext/projects/doctype/task/task.py @@ -43,15 +43,8 @@ class Task(Document): 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 @@ class Task(Document): 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 ca0a6cb8d2d..91601a43127 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -37,6 +37,16 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ 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 @@ frappe.ui.form.on(cur_frm.doctype, "discount_amount", function(frm) { 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 7ede9d45059..7f0b3861e7c 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.cscript.on_submit = function(doc, cdt, cdn) { 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 ad62d578d46..59f54fbadcc 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 b4a9c0a7ca8..8012bba7af4 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 3b9e25aa1c7..98d34be40d3 100644 --- a/erpnext/selling/page/sales_browser/sales_browser.js +++ b/erpnext/selling/page/sales_browser/sales_browser.js @@ -122,7 +122,7 @@ erpnext.SalesChart = Class.extend({ 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 777ae3b78b6..c7330d0c897 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 a3fa3a9c5f0..efdc875249f 100644 --- a/erpnext/setup/doctype/global_defaults/global_defaults.py +++ b/erpnext/setup/doctype/global_defaults/global_defaults.py @@ -17,7 +17,6 @@ keydict = { '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 cef60799a13..f661edb2b6a 100644 --- a/erpnext/setup/utils.py +++ b/erpnext/setup/utils.py @@ -60,20 +60,21 @@ def before_tests(): @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 ed1367e0b61..58b1adb8db7 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -5,39 +5,6 @@ frappe.provide("erpnext.item"); 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 @@ frappe.ui.form.on("Item", { 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 8b10319f86e..acf2764e936 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,15 +187,37 @@ "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", @@ -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 aa463eee775..ccb0fab9a1d 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -9,10 +9,9 @@ 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 -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 @@ class Item(WebsiteGenerator): 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 @@ class Item(WebsiteGenerator): 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 @@ class Item(WebsiteGenerator): 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 @@ class Item(WebsiteGenerator): 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 += "

" + attr[0] + ": " + attr[1] + "

" - - 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 @@ class Item(WebsiteGenerator): 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 @@ class Item(WebsiteGenerator): 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 @@ def invalidate_cache_for_item(doc): 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 02ff714bdaf..9cf3c077b41 100644 --- a/erpnext/stock/doctype/item/test_item.py +++ b/erpnext/stock/doctype/item/test_item.py @@ -6,7 +6,7 @@ import unittest 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 @@ class TestItem(unittest.TestCase): 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 dc095c6b14b..5a02c6bde56 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 00000000000..e69de29bb2d 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 00000000000..ba5c46e8eb9 --- /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 00000000000..7c61620b083 --- /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 00000000000..b6784d3a2e3 --- /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 += "

" + d.attribute + ": " + d.attribute_value + "

" \ 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 00000000000..9952ba9a2b4 --- /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 00000000000..e69de29bb2d 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 00000000000..a8bb61d5e48 --- /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 00000000000..800888a864d --- /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 ff0b272fd52..8114befa2f7 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 @@ class TestStockEntry(unittest.TestCase): 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 fb654c20db8..a373185c238 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 00000000000..e69de29bb2d 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 00000000000..5ab3d73239e --- /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 00000000000..9c35732bf2f --- /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 23989507ac9..0ecc9c0c948 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 @@ class MaintenanceSchedule(TransactionBase): "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 c26f2793379..caa13667db2 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) %} + {% } else if (df.fieldname==="description") { %} +

{{ doc.description }}

{% } %} {% }); %} diff --git a/erpnext/templates/form_grid/item_grid.html b/erpnext/templates/form_grid/item_grid.html index d2db463dcb5..197cef461bf 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) { %}
@@ -48,12 +50,7 @@ {% if(doc.item_name != doc.item_code && in_list(visible_column_fieldnames, "item_name")) { %}
{%= doc.item_name %}{% } %} - - {% if((doc.description != doc.item_code != doc.item_name) && - in_list(visible_column_fieldnames, "description")) { %} -
- Description: - {%= doc.get_formatted("description") %}{% } %} + {% include "templates/form_grid/includes/visible_cols.html" %}
diff --git a/erpnext/utilities/transaction_base.py b/erpnext/utilities/transaction_base.py index 60889c54155..be74aeb920d 100644 --- a/erpnext/utilities/transaction_base.py +++ b/erpnext/utilities/transaction_base.py @@ -22,16 +22,21 @@ class TransactionBase(StatusUpdater): 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 ea74fa83051..0694a82ce12 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()