From 856ee10dc4f9cc56f1d2e9bea215f3002fb9f70b Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Fri, 17 Jul 2015 15:03:18 +0530 Subject: [PATCH 01/40] [enhancement] update to setup wizard, added users, employees, sample data --- erpnext/accounts/doctype/account/account.py | 2 +- .../purchase_taxes_and_charges_template.py | 7 +- .../sales_taxes_and_charges_template.py | 26 ++-- .../page/setup_wizard/install_fixtures.py | 2 +- .../setup/page/setup_wizard/sample_data.py | 117 ++++++++++++++++ .../setup/page/setup_wizard/setup_wizard.js | 80 ++++++++++- .../setup/page/setup_wizard/setup_wizard.py | 126 ++++++++++++++++-- .../page/setup_wizard/test_setup_data.py | 11 ++ erpnext/startup/notifications.py | 1 + 9 files changed, 342 insertions(+), 30 deletions(-) create mode 100644 erpnext/setup/page/setup_wizard/sample_data.py diff --git a/erpnext/accounts/doctype/account/account.py b/erpnext/accounts/doctype/account/account.py index 683734b879..f1d822a89c 100644 --- a/erpnext/accounts/doctype/account/account.py +++ b/erpnext/accounts/doctype/account/account.py @@ -49,7 +49,7 @@ class Account(Document): self.root_type = par.root_type def validate_root_details(self): - #does not exists parent + # does not exists parent if frappe.db.exists("Account", self.name): if not frappe.db.get_value("Account", self.name, "parent_account"): throw(_("Root cannot be edited.")) diff --git a/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template.py b/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template.py index a2ba72dba6..bffa8e6d74 100644 --- a/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template.py +++ b/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template.py @@ -4,10 +4,9 @@ from __future__ import unicode_literals from frappe.model.document import Document -from erpnext.controllers.accounts_controller import validate_taxes_and_charges, validate_inclusive_tax +from erpnext.accounts.doctype.sales_taxes_and_charges_template.sales_taxes_and_charges_template \ + import valdiate_taxes_and_charges_template class PurchaseTaxesandChargesTemplate(Document): def validate(self): - for tax in self.get("taxes"): - validate_taxes_and_charges(tax) - validate_inclusive_tax(tax, self) + valdiate_taxes_and_charges_template(self) diff --git a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py index 6721bd89e1..b36287b691 100644 --- a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py +++ b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py @@ -5,21 +5,25 @@ from __future__ import unicode_literals import frappe from frappe.model.document import Document from erpnext.controllers.accounts_controller import validate_taxes_and_charges, validate_inclusive_tax +from frappe.utils.nestedset import get_root_of class SalesTaxesandChargesTemplate(Document): def validate(self): - if self.is_default == 1: - frappe.db.sql("""update `tabSales Taxes and Charges Template` - set is_default = 0 - where ifnull(is_default,0) = 1 - and name != %s and company = %s""", - (self.name, self.company)) + valdiate_taxes_and_charges_template(self) - # at least one territory - self.validate_table_has_rows("territories") +def valdiate_taxes_and_charges_template(doc): + if not doc.is_default and not frappe.get_all(doc.doctype, filters={"is_default": 1}): + doc.is_default = 1 - for tax in self.get("taxes"): - validate_taxes_and_charges(tax) - validate_inclusive_tax(tax, self) + if doc.is_default == 1: + frappe.db.sql("""update `tab{0}` set is_default = 0 + where ifnull(is_default,0) = 1 and name != %s and company = %s""".format(doc.doctype), + (doc.name, doc.company)) + if doc.meta.get_field("territories"): + if not doc.territories: + doc.append("territories", {"territory": get_root_of("Territory") }) + for tax in doc.get("taxes"): + validate_taxes_and_charges(tax) + validate_inclusive_tax(tax, doc) diff --git a/erpnext/setup/page/setup_wizard/install_fixtures.py b/erpnext/setup/page/setup_wizard/install_fixtures.py index 629c06f6db..6265e4a36c 100644 --- a/erpnext/setup/page/setup_wizard/install_fixtures.py +++ b/erpnext/setup/page/setup_wizard/install_fixtures.py @@ -183,4 +183,4 @@ def install(country=None): parent_link_field = ("parent_" + scrub(doc.doctype)) if doc.meta.get_field(parent_link_field) and not doc.get(parent_link_field): doc.flags.ignore_mandatory = True - doc.insert() + doc.insert(ignore_permissions=True) diff --git a/erpnext/setup/page/setup_wizard/sample_data.py b/erpnext/setup/page/setup_wizard/sample_data.py new file mode 100644 index 0000000000..f7fb73b446 --- /dev/null +++ b/erpnext/setup/page/setup_wizard/sample_data.py @@ -0,0 +1,117 @@ +# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals + +import frappe +from frappe.utils.make_random import add_random_children, get_random +import frappe.utils + +def make_sample_data(): + """Create a few opportunities, quotes, material requests, issues, todos, projects + to help the user get started""" + + selling_items = frappe.get_all("Item", filters = {"is_sales_item": "Yes"}) + buying_items = frappe.get_all("Item", filters = {"is_sales_item": "No"}) + + if selling_items: + for i in range(3): + make_opportunity(selling_items) + make_quote(selling_items) + + make_projects() + + if buying_items: + make_material_request(buying_items) + + frappe.db.commit() + +def make_opportunity(selling_items): + b = frappe.get_doc({ + "doctype": "Opportunity", + "enquiry_from": "Customer", + "customer": get_random("Customer"), + "enquiry_type": "Sales", + "with_items": 1 + }) + + add_random_children(b, "items", rows=len(selling_items), randomize = { + "qty": (1, 5), + "item_code": ("Item", {"is_sales_item": "Yes"}) + }, unique="item_code") + + b.insert(ignore_permissions=True) + + b.add_comment("This is a dummy record") + +def make_quote(selling_items): + qtn = frappe.get_doc({ + "doctype": "Quotation", + "quotation_to": "Customer", + "customer": get_random("Customer"), + "order_type": "Sales" + }) + + add_random_children(qtn, "items", rows=len(selling_items), randomize = { + "qty": (1, 5), + "item_code": ("Item", {"is_sales_item": "Yes"}) + }, unique="item_code") + + qtn.insert(ignore_permissions=True) + + qtn.add_comment("This is a dummy record") + +def make_material_request(buying_items): + for i in buying_items: + mr = frappe.get_doc({ + "doctype": "Material Request", + "material_request_type": "Purchase", + "items": [{ + "schedule_date": frappe.utils.add_days(frappe.utils.nowdate(), 7), + "item_code": i.name, + "qty": 10 + }] + }) + mr.insert() + mr.submit() + + mr.add_comment("This is a dummy record") + + +def make_issue(): + pass + +def make_projects(): + project = frappe.get_doc({ + "doctype": "Project", + "project_name": "ERPNext Implementation", + }) + current_date = frappe.utils.nowdate() + project.set("tasks", [ + { + "title": "Explore ERPNext", + "start_date": frappe.utils.add_days(current_date, 1), + "end_date": frappe.utils.add_days(current_date, 2) + }, + { + "title": "Run Sales Cycle", + "start_date": frappe.utils.add_days(current_date, 2), + "end_date": frappe.utils.add_days(current_date, 3) + }, + { + "title": "Run Billing Cycle", + "start_date": frappe.utils.add_days(current_date, 3), + "end_date": frappe.utils.add_days(current_date, 4) + }, + { + "title": "Run Purchase Cycle", + "start_date": frappe.utils.add_days(current_date, 4), + "end_date": frappe.utils.add_days(current_date, 5) + }, + { + "title": "Go Live!", + "start_date": frappe.utils.add_days(current_date, 5), + "end_date": frappe.utils.add_days(current_date, 6) + }]) + + project.insert(ignore_permissions=True) diff --git a/erpnext/setup/page/setup_wizard/setup_wizard.js b/erpnext/setup/page/setup_wizard/setup_wizard.js index d521adab25..b38bd1c373 100644 --- a/erpnext/setup/page/setup_wizard/setup_wizard.js +++ b/erpnext/setup/page/setup_wizard/setup_wizard.js @@ -25,6 +25,7 @@ frappe.pages['setup-wizard'].on_page_load = function(wrapper) { erpnext.wiz.user.slide, erpnext.wiz.org.slide, erpnext.wiz.branding.slide, + erpnext.wiz.users.slide, erpnext.wiz.taxes.slide, erpnext.wiz.customers.slide, erpnext.wiz.suppliers.slide, @@ -137,7 +138,7 @@ erpnext.wiz.WizardSlide = Class.extend({ }); this.form.make(); } else { - $(this.body).html(this.html) + $(this.body).html(this.html); } if(this.id > 0) { @@ -412,11 +413,30 @@ $.extend(erpnext.wiz, { onload: function(slide) { erpnext.wiz.org.load_chart_of_accounts(slide); erpnext.wiz.org.bind_events(slide); + erpnext.wiz.org.set_fy_dates(slide); }, css_class: "single-column" }, + set_fy_dates: function(slide) { + var country = slide.wiz.get_values().country; + + if(country) { + var fy = erpnext.wiz.fiscal_years[country]; + var current_year = moment(new Date()).year(); + var next_year = current_year + 1; + if(!fy) { + fy = ["01-01", "12-31"]; + next_year = current_year; + } + + slide.get_field("fy_start_date").set_input(current_year + "-" + fy[0]); + slide.get_field("fy_end_date").set_input(next_year + "-" + fy[1]); + } + + }, + load_chart_of_accounts: function(slide) { var country = slide.wiz.get_values().country; @@ -486,11 +506,41 @@ $.extend(erpnext.wiz, { }, }, + users: { + slide: { + icon: "icon-money", + "title": __("Add Users"), + "help": __("Add users to your organization"), + "fields": [], + before_load: function(slide) { + slide.fields = []; + for(var i=1; i<5; i++) { + slide.fields = slide.fields.concat([ + {fieldtype:"Section Break"}, + {fieldtype:"Data", fieldname:"user_fullname_"+ i, + label:__("Full Name")}, + {fieldtype:"Data", fieldname:"user_email_" + i, + label:__("Email ID"), placeholder:__("user@example.com"), + options: "Email"}, + {fieldtype:"Column Break"}, + {fieldtype: "Check", fieldname: "user_sales_" + i, + label:__("Sales"), default: 1}, + {fieldtype: "Check", fieldname: "user_purchaser_" + i, + label:__("Purchaser"), default: 1}, + {fieldtype: "Check", fieldname: "user_accountant_" + i, + label:__("Accountant"), default: 1}, + ]); + } + }, + css_class: "two-column" + }, + }, + taxes: { slide: { icon: "icon-money", "title": __("Add Taxes"), - "help": __("List your tax heads (e.g. VAT, Excise; they should have unique names) and their standard rates. This will create a standard template, which you can edit and add more later."), + "help": __("List your tax heads (e.g. VAT, Customs etc; they should have unique names) and their standard rates. This will create a standard template, which you can edit and add more later."), "fields": [], before_load: function(slide) { slide.fields = []; @@ -526,6 +576,7 @@ $.extend(erpnext.wiz, { label:__("Contact Name") + " " + i, placeholder:__("Contact Name")} ]) } + slide.fields[1].reqd = 1; }, css_class: "two-column" }, @@ -549,6 +600,7 @@ $.extend(erpnext.wiz, { label:__("Contact Name") + " " + i, placeholder:__("Contact Name")}, ]) } + slide.fields[1].reqd = 1; }, css_class: "two-column" }, @@ -578,9 +630,11 @@ $.extend(erpnext.wiz, { {fieldtype: "Check", fieldname: "is_sales_item_" + i, label:__("We sell this Item"), default: 1}, {fieldtype: "Check", fieldname: "is_purchase_item_" + i, label:__("We buy this Item")}, {fieldtype:"Column Break"}, + {fieldtype:"Currency", fieldname:"item_price_" + i, label:__("Rate")}, {fieldtype:"Attach Image", fieldname:"item_img_" + i, label:__("Attach Image")}, ]) } + slide.fields[1].reqd = 1; }, css_class: "two-column" }, @@ -627,3 +681,25 @@ $.extend(erpnext.wiz, { }, }); +// Source: https://en.wikipedia.org/wiki/Fiscal_year +// default 1st Jan - 31st Dec + +erpnext.wiz.fiscal_years = { + "Afghanistan": ["12-20", "12-21"], + "Australia": ["07-01", "06-30"], + "Bangladesh": ["07-01", "06-30"], + "Canada": ["04-01", "03-31"], + "Costa Rica": ["10-01", "09-30"], + "Egypt": ["07-01", "06-30"], + "Hong Kong": ["04-01", "03-31"], + "India": ["04-01", "03-31"], + "Iran": ["06-23", "06-22"], + "Italy": ["07-01", "06-30"], + "Myanmar": ["04-01", "03-31"], + "New Zealand": ["04-01", "03-31"], + "Pakistan": ["07-01", "06-30"], + "Singapore": ["04-01", "03-31"], + "South Africa": ["03-01", "02-28"], + "Thailand": ["10-01", "09-30"], + "United Kingdom": ["04-01", "03-31"], +} diff --git a/erpnext/setup/page/setup_wizard/setup_wizard.py b/erpnext/setup/page/setup_wizard/setup_wizard.py index 4bb01d48d4..f802694d60 100644 --- a/erpnext/setup/page/setup_wizard/setup_wizard.py +++ b/erpnext/setup/page/setup_wizard/setup_wizard.py @@ -2,7 +2,7 @@ # License: GNU General Public License v3. See license.txt from __future__ import unicode_literals -import frappe, json +import frappe, json, copy from frappe.utils import cstr, flt, getdate from frappe import _ @@ -13,6 +13,7 @@ from frappe.geo.country_info import get_country_info from frappe.utils.nestedset import get_root_of from .default_website import website_maker import install_fixtures +from .sample_data import make_sample_data @frappe.whitelist() def setup_account(args=None): @@ -38,6 +39,9 @@ def setup_account(args=None): create_fiscal_year_and_company(args) frappe.local.message_log = [] + create_users(args) + frappe.local.message_log = [] + set_defaults(args) frappe.local.message_log = [] @@ -81,6 +85,7 @@ def setup_account(args=None): frappe.clear_cache() + make_sample_data() except: if args: traceback = frappe.get_traceback() @@ -297,21 +302,45 @@ def create_taxes(args): tax_group = frappe.db.get_value("Account", {"company": args.get("company_name"), "is_group": 1, "account_type": "Tax", "root_type": "Liability"}) if tax_group: - frappe.get_doc({ - "doctype":"Account", - "company": args.get("company_name").strip(), - "parent_account": tax_group, - "account_name": args.get("tax_" + str(i)), - "is_group": 0, - "report_type": "Balance Sheet", - "account_type": "Tax", - "tax_rate": flt(tax_rate) if tax_rate else None - }).insert() + account = make_tax_head(args, i, tax_group, tax_rate) + make_sales_and_purchase_tax_templates(account) + except frappe.NameError, e: if e.args[2][0]==1062: pass else: raise +def make_tax_head(args, i, tax_group, tax_rate): + return frappe.get_doc({ + "doctype":"Account", + "company": args.get("company_name").strip(), + "parent_account": tax_group, + "account_name": args.get("tax_" + str(i)), + "is_group": 0, + "report_type": "Balance Sheet", + "account_type": "Tax", + "tax_rate": flt(tax_rate) if tax_rate else None + }).insert(ignore_permissions=True) + +def make_sales_and_purchase_tax_templates(account): + doc = { + "doctype": "Sales Taxes and Charges Template", + "title": account.name, + "taxes": [{ + "category": "Valuation and Total", + "charge_type": "On Net Total", + "account_head": account.name, + "description": "{0} @ {1}".format(account.account_name, account.tax_rate), + "rate": account.tax_rate + }] + } + + # Sales + frappe.get_doc(copy.deepcopy(doc)).insert() + + # Purchase + doc["doctype"] = "Purchase Taxes and Charges Template" + frappe.get_doc(copy.deepcopy(doc)).insert() def create_items(args): for i in xrange(1,6): @@ -349,9 +378,30 @@ def create_items(args): filename, filetype, content = item_image fileurl = save_file(filename, content, "Item", item, decode=True).file_url frappe.db.set_value("Item", item, "image", fileurl) + + if args.get("item_price_" + str(i)): + item_price = flt(args.get("item_price_" + str(i))) + + if is_sales_item: + price_list_name = frappe.db.get_value("Price List", {"selling": 1}) + make_item_price(item, price_list_name, item_price) + + if is_purchase_item: + price_list_name = frappe.db.get_value("Price List", {"buying": 1}) + make_item_price(item, price_list_name, item_price) + except frappe.NameError: pass +def make_item_price(item, price_list_name, item_price): + frappe.get_doc({ + "doctype": "Item Price", + "price_list": price_list_name, + "item_code": item, + "price_list_rate": item_price + }).insert() + + def create_customers(args): for i in xrange(1,6): customer = args.get("customer_" + str(i)) @@ -451,6 +501,60 @@ def login_as_first_user(args): if args.get("email") and hasattr(frappe.local, "login_manager"): frappe.local.login_manager.login_as(args.get("email")) +def create_users(args): + # create employee for self + emp = frappe.get_doc({ + "doctype": "Employee", + "full_name": " ".join(filter(None, [args.get("first_name"), args.get("last_name")])), + "user_id": frappe.session.user, + "status": "Active", + "company": args.get("company_name") + }) + emp.flags.ignore_mandatory = True + emp.insert(ignore_permissions = True) + + for i in xrange(1,5): + email = args.get("user_email_" + str(i)) + fullname = args.get("user_fullname_" + str(i)) + if email: + if not fullname: + fullname = email.split("@")[0] + + parts = fullname.split(" ", 1) + + user = frappe.get_doc({ + "doctype": "User", + "email": email, + "first_name": parts[0], + "last_name": parts[1] if len(parts) > 1 else "", + "enabled": 1, + "user_type": "System User" + }) + + # default roles + user.append_roles("Projects User", "Stock User", "Support Team") + + if args.get("user_sales_" + str(i)): + user.append_roles("Sales User", "Sales Manager", "Accounts User") + if args.get("user_purchaser_" + str(i)): + user.append_roles("Purchase User", "Purchase Manager", "Accounts User") + if args.get("user_accountant_" + str(i)): + user.append_roles("Accounts Manager", "Accounts User") + + user.flags.delay_emails = True + user.insert(ignore_permissions=True) + + # create employee + emp = frappe.get_doc({ + "doctype": "Employee", + "full_name": fullname, + "user_id": user.name, + "status": "Active", + "company": args.get("company_name") + }) + emp.flags.ignore_mandatory = True + emp.insert(ignore_permissions = True) + @frappe.whitelist() def load_messages(language): frappe.clear_cache() diff --git a/erpnext/setup/page/setup_wizard/test_setup_data.py b/erpnext/setup/page/setup_wizard/test_setup_data.py index 43fc2cf782..de54a1d16b 100644 --- a/erpnext/setup/page/setup_wizard/test_setup_data.py +++ b/erpnext/setup/page/setup_wizard/test_setup_data.py @@ -51,4 +51,15 @@ args = { "timezone": "America/New_York", "password": "password", "email": "test@erpnext.com", +"user_email_1": "testsetup1@example.com", +"user_fullname_1": "test setup user", +"user_sales_1": 1, +"user_purchaser_1": 1, +"user_accountant_1": 1, +"user_email_1": "testsetup2@example.com", +"user_fullname_1": "test setup user", +"user_sales_2": 1, +"user_purchaser_2": 0, +"user_accountant_2": 0 + } diff --git a/erpnext/startup/notifications.py b/erpnext/startup/notifications.py index 4190f2debd..d06537066e 100644 --- a/erpnext/startup/notifications.py +++ b/erpnext/startup/notifications.py @@ -10,6 +10,7 @@ def get_notification_config(): "Issue": {"status": "Open"}, "Warranty Claim": {"status": "Open"}, "Task": {"status": "Open"}, + "Project": {"status": "Open"}, "Lead": {"status": "Open"}, "Contact": {"status": "Open"}, "Opportunity": {"status": "Open"}, From f447c8258a074ce3dd7dc29c3ce408a28cc9646d Mon Sep 17 00:00:00 2001 From: Anand Doshi Date: Mon, 20 Jul 2015 15:18:34 +0530 Subject: [PATCH 02/40] [minor] Newsletter Message should be mandatory --- erpnext/crm/doctype/newsletter/newsletter.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/crm/doctype/newsletter/newsletter.json b/erpnext/crm/doctype/newsletter/newsletter.json index f2baf2e462..715a97fb9b 100644 --- a/erpnext/crm/doctype/newsletter/newsletter.json +++ b/erpnext/crm/doctype/newsletter/newsletter.json @@ -52,7 +52,7 @@ "fieldtype": "Text Editor", "label": "Message", "permlevel": 0, - "reqd": 0 + "reqd": 1 }, { "description": "", @@ -78,7 +78,7 @@ ], "icon": "icon-envelope", "idx": 1, - "modified": "2015-03-20 05:27:31.613881", + "modified": "2015-07-20 05:43:33.818567", "modified_by": "Administrator", "module": "CRM", "name": "Newsletter", From e2b8ccf1bb229290517ad73e0867a503d32e2aa6 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Tue, 21 Jul 2015 12:02:28 +0530 Subject: [PATCH 03/40] [report] Letter Head option in General Ledger report --- erpnext/accounts/report/general_ledger/general_ledger.html | 2 +- erpnext/accounts/report/general_ledger/general_ledger.js | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.html b/erpnext/accounts/report/general_ledger/general_ledger.html index 0d3170ac73..f22e721270 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.html +++ b/erpnext/accounts/report/general_ledger/general_ledger.html @@ -1,5 +1,5 @@
- {%= frappe.boot.letter_heads[frappe.defaults.get_default("letter_head")] %} + {%= frappe.boot.letter_heads[filters.letter_head || frappe.defaults.get_default("letter_head")] %}

{%= __("Statement of Account") %}

{%= (filters.party || filters.account) && ((filters.party || filters.account) + ", ") || "" %} {%= filters.company %}

diff --git a/erpnext/accounts/report/general_ledger/general_ledger.js b/erpnext/accounts/report/general_ledger/general_ledger.js index de7027b474..b4d9b9f749 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.js +++ b/erpnext/accounts/report/general_ledger/general_ledger.js @@ -80,6 +80,13 @@ frappe.query_reports["General Ledger"] = { "fieldname":"group_by_account", "label": __("Group by Account"), "fieldtype": "Check", + }, + { + "fieldname":"letter_head", + "label": __("Letter Head"), + "fieldtype": "Link", + "options": "Letter Head", + "default": frappe.defaults.get_default("letter_head"), } ] } From cc431716edece562d5cac01c77cae6dc55a412b4 Mon Sep 17 00:00:00 2001 From: Neil Trini Lasrado Date: Wed, 22 Jul 2015 12:13:47 +0530 Subject: [PATCH 04/40] Fetch Template Bom if no BOM is set against Item Variant in Production Order --- .../doctype/production_order/production_order.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/production_order/production_order.py b/erpnext/manufacturing/doctype/production_order/production_order.py index 26af40a441..93ce5e16f6 100644 --- a/erpnext/manufacturing/doctype/production_order/production_order.py +++ b/erpnext/manufacturing/doctype/production_order/production_order.py @@ -335,12 +335,15 @@ def get_item_details(item): res = frappe.db.sql("""select stock_uom, description from `tabItem` where (ifnull(end_of_life, "0000-00-00")="0000-00-00" or end_of_life > now()) and name=%s""", item, as_dict=1) - if not res: return {} res = res[0] res["bom_no"] = frappe.db.get_value("BOM", filters={"item": item, "is_default": 1}) + if not res["bom_no"]: + variant_of= frappe.db.get_value("Item", item, "variant_of") + if variant_of: + res["bom_no"] = frappe.db.get_value("BOM", filters={"item": variant_of, "is_default": 1}) return res @frappe.whitelist() From 098760f0e20bc3a2c9823b64bbaaeaee541a9c98 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 22 Jul 2015 11:28:21 +0530 Subject: [PATCH 05/40] [fix] SMS status and log --- erpnext/config/crm.py | 5 ++ erpnext/config/selling.py | 5 ++ .../doctype/sms_settings/sms_settings.py | 38 +++++----- .../utilities/doctype/sms_log/sms_log.json | 69 ++++++++++++------- 4 files changed, 76 insertions(+), 41 deletions(-) diff --git a/erpnext/config/crm.py b/erpnext/config/crm.py index 3a7ab18143..d7a6b2e2be 100644 --- a/erpnext/config/crm.py +++ b/erpnext/config/crm.py @@ -42,6 +42,11 @@ def get_data(): "name": "SMS Center", "description":_("Send mass SMS to your contacts"), }, + { + "type": "doctype", + "name": "SMS Log", + "description":_("Logs for maintaining sms delivery status"), + } ] }, { diff --git a/erpnext/config/selling.py b/erpnext/config/selling.py index 543396462d..62dfe2326d 100644 --- a/erpnext/config/selling.py +++ b/erpnext/config/selling.py @@ -48,6 +48,11 @@ def get_data(): "name": "SMS Center", "description":_("Send mass SMS to your contacts"), }, + { + "type": "doctype", + "name": "SMS Log", + "description":_("Logs for maintaining sms delivery status"), + }, { "type": "doctype", "name": "Newsletter", diff --git a/erpnext/setup/doctype/sms_settings/sms_settings.py b/erpnext/setup/doctype/sms_settings/sms_settings.py index 1403ee5cbd..909986347f 100644 --- a/erpnext/setup/doctype/sms_settings/sms_settings.py +++ b/erpnext/setup/doctype/sms_settings/sms_settings.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import frappe from frappe import _, throw, msgprint -from frappe.utils import cstr, nowdate +from frappe.utils import nowdate from frappe.model.document import Document @@ -63,8 +63,7 @@ def send_sms(receiver_list, msg, sender_name = ''): } if frappe.db.get_value('SMS Settings', None, 'sms_gateway_url'): - ret = send_via_gateway(arg) - msgprint(ret) + send_via_gateway(arg) else: msgprint(_("Please Update SMS Settings")) @@ -74,12 +73,17 @@ def send_via_gateway(arg): for d in ss.get("parameters"): args[d.parameter] = d.value - resp = [] + success_list = [] for d in arg.get('receiver_list'): args[ss.receiver_parameter] = d - resp.append(send_request(ss.sms_gateway_url, args)) + status = send_request(ss.sms_gateway_url, args) + if status == 200: + success_list.append(d) - return resp + if len(success_list) > 0: + args.update(arg) + create_sms_log(args, success_list) + frappe.msgprint(_("SMS sent to following numbers: {0}").format("\n" + "\n".join(success_list))) # Send Request # ========================================================= @@ -90,11 +94,8 @@ def send_request(gateway_url, args): headers = {} headers['Accept'] = "text/plain, text/html, */*" conn.request('GET', api_url + urllib.urlencode(args), headers = headers) # send request - resp = conn.getresponse() # get response - resp = resp.read() - if resp.status==200: - create_sms_log() - return resp + resp = conn.getresponse() # get response + return resp.status # Split gateway url to server and api url # ========================================================= @@ -109,12 +110,13 @@ def scrub_gateway_url(url): # Create SMS Log # ========================================================= -def create_sms_log(arg, sent_sms): - sl = frappe.get_doc('SMS Log') - sl.sender_name = arg['sender_name'] +def create_sms_log(args, sent_to): + sl = frappe.new_doc('SMS Log') + sl.sender_name = args['sender_name'] sl.sent_on = nowdate() - sl.receiver_list = cstr(arg['receiver_list']) - sl.message = arg['message'] - sl.no_of_requested_sms = len(arg['receiver_list']) - sl.no_of_sent_sms = sent_sms + sl.message = args['message'] + sl.no_of_requested_sms = len(args['receiver_list']) + sl.requested_numbers = "\n".join(args['receiver_list']) + sl.no_of_sent_sms = len(sent_to) + sl.sent_to = "\n".join(sent_to) sl.save() diff --git a/erpnext/utilities/doctype/sms_log/sms_log.json b/erpnext/utilities/doctype/sms_log/sms_log.json index e3c7741676..ba88c622c3 100644 --- a/erpnext/utilities/doctype/sms_log/sms_log.json +++ b/erpnext/utilities/doctype/sms_log/sms_log.json @@ -1,32 +1,58 @@ { "autoname": "SMSLOG/.########", - "creation": "2012-03-27 14:36:47.000000", + "creation": "2012-03-27 14:36:47", "docstatus": 0, "doctype": "DocType", "fields": [ - { - "fieldname": "column_break0", - "fieldtype": "Column Break", - "permlevel": 0, - "width": "50%" - }, { "fieldname": "sender_name", "fieldtype": "Data", "label": "Sender Name", - "permlevel": 0 + "permlevel": 0, + "read_only": 1 }, { "fieldname": "sent_on", "fieldtype": "Date", "label": "Sent On", - "permlevel": 0 + "permlevel": 0, + "read_only": 1 }, { - "fieldname": "receiver_list", + "fieldname": "column_break0", + "fieldtype": "Column Break", + "permlevel": 0, + "read_only": 0, + "width": "50%" + }, + { + "fieldname": "message", "fieldtype": "Small Text", - "label": "Receiver List", - "permlevel": 0 + "label": "Message", + "permlevel": 0, + "read_only": 1 + }, + { + "fieldname": "sec_break1", + "fieldtype": "Section Break", + "options": "Simple", + "permlevel": 0, + "precision": "", + "read_only": 0 + }, + { + "fieldname": "no_of_requested_sms", + "fieldtype": "Int", + "label": "No of Requested SMS", + "permlevel": 0, + "read_only": 1 + }, + { + "fieldname": "requested_numbers", + "fieldtype": "Small Text", + "label": "Requested Numbers", + "permlevel": 0, + "read_only": 1 }, { "fieldname": "column_break1", @@ -34,28 +60,25 @@ "permlevel": 0, "width": "50%" }, - { - "fieldname": "no_of_requested_sms", - "fieldtype": "Int", - "label": "No of Requested SMS", - "permlevel": 0 - }, { "fieldname": "no_of_sent_sms", "fieldtype": "Int", "label": "No of Sent SMS", - "permlevel": 0 + "permlevel": 0, + "read_only": 1 }, { - "fieldname": "message", + "fieldname": "sent_to", "fieldtype": "Small Text", - "label": "Message", - "permlevel": 0 + "label": "Sent To", + "permlevel": 0, + "precision": "", + "read_only": 1 } ], "icon": "icon-mobile-phone", "idx": 1, - "modified": "2013-12-20 19:24:35.000000", + "modified": "2015-07-22 11:53:25.998578", "modified_by": "Administrator", "module": "Utilities", "name": "SMS Log", From d900e12ae74a36b3e0f47fef4705969f97a9d6cb Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 22 Jul 2015 12:21:55 +0530 Subject: [PATCH 06/40] Change log for sms --- erpnext/change_log/current/sms.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 erpnext/change_log/current/sms.md diff --git a/erpnext/change_log/current/sms.md b/erpnext/change_log/current/sms.md new file mode 100644 index 0000000000..bac293f20f --- /dev/null +++ b/erpnext/change_log/current/sms.md @@ -0,0 +1 @@ +- Now system will give SMS delivery message and maintain a log \ No newline at end of file From 8b48ceab8c5b830a674a1f440d0394eb84444bd7 Mon Sep 17 00:00:00 2001 From: Neil Trini Lasrado Date: Wed, 22 Jul 2015 12:42:21 +0530 Subject: [PATCH 07/40] Removed logic for get_item_details in Production Planning Tool, Used get_item_details function of Production Order instead --- .../production_planning_tool.py | 21 ++++++------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_planning_tool/production_planning_tool.py b/erpnext/manufacturing/doctype/production_planning_tool/production_planning_tool.py index 86a14d8c21..271abac10e 100644 --- a/erpnext/manufacturing/doctype/production_planning_tool/production_planning_tool.py +++ b/erpnext/manufacturing/doctype/production_planning_tool/production_planning_tool.py @@ -9,6 +9,7 @@ from frappe import msgprint, _ from frappe.model.document import Document from erpnext.manufacturing.doctype.bom.bom import validate_bom_no +from erpnext.manufacturing.doctype.production_order.production_order import get_item_details class ProductionPlanningTool(Document): def __init__(self, arg1, arg2=None): @@ -27,16 +28,7 @@ class ProductionPlanningTool(Document): return ret def get_item_details(self, item_code): - """ Pull other item details from item master""" - - item = frappe.db.sql("""select description, stock_uom, default_bom - from `tabItem` where name = %s""", item_code, as_dict =1) - ret = { - 'description' : item and item[0]['description'], - 'stock_uom' : item and item[0]['stock_uom'], - 'bom_no' : item and item[0]['default_bom'] - } - return ret + return get_item_details(item_code) def clear_so_table(self): self.set('sales_orders', []) @@ -142,15 +134,14 @@ class ProductionPlanningTool(Document): self.clear_item_table() for p in items: - item_details = frappe.db.sql("""select description, stock_uom, default_bom - from tabItem where name=%s""", p['item_code']) + item_details = get_item_details(p['item_code']) pi = self.append('items', {}) pi.sales_order = p['parent'] pi.warehouse = p['warehouse'] pi.item_code = p['item_code'] - pi.description = item_details and item_details[0][0] or '' - pi.stock_uom = item_details and item_details[0][1] or '' - pi.bom_no = item_details and item_details[0][2] or '' + pi.description = item_details and item_details.description or '' + pi.stock_uom = item_details and item_details.stock_uom or '' + pi.bom_no = item_details and item_details.bom_no or '' pi.so_pending_qty = flt(p['pending_qty']) pi.planned_qty = flt(p['pending_qty']) From 7590aa2524c92770dd16c896f245e97c0b80da09 Mon Sep 17 00:00:00 2001 From: Anand Doshi Date: Wed, 22 Jul 2015 14:42:49 +0530 Subject: [PATCH 08/40] [minor] raise EmptyStockReconciliationItemsError when no change in any of the items --- .../doctype/stock_reconciliation/stock_reconciliation.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 413f820043..efa6a8a25d 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -11,6 +11,7 @@ from erpnext.controllers.stock_controller import StockController from erpnext.stock.utils import get_stock_balance class OpeningEntryAccountError(frappe.ValidationError): pass +class EmptyStockReconciliationItemsError(frappe.ValidationError): pass class StockReconciliation(StockController): def __init__(self, arg1, arg2=None): @@ -51,7 +52,11 @@ class StockReconciliation(StockController): items = filter(lambda d: _changed(d), self.items) - if len(items) != len(self.items): + if not items: + frappe.throw(_("None of the items have any change in quantity or value."), + EmptyStockReconciliationItemsError) + + elif len(items) != len(self.items): self.items = items for i, item in enumerate(self.items): item.idx = i + 1 From 886def0a69afa97b7f275d35651437418df74c87 Mon Sep 17 00:00:00 2001 From: Anand Doshi Date: Wed, 22 Jul 2015 14:43:17 +0530 Subject: [PATCH 09/40] [fix] convert Item and Item Grid images to absolute urls --- erpnext/templates/includes/macros.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/templates/includes/macros.html b/erpnext/templates/includes/macros.html index 0967e97138..6748a5ebd2 100644 --- a/erpnext/templates/includes/macros.html +++ b/erpnext/templates/includes/macros.html @@ -1,6 +1,6 @@ {% macro product_image_square(website_image, css_class="") %}
+ {% if website_image -%} style="background-image: url('{{ frappe.utils.quoted(website_image) | abs_url }}');" {%- endif %}> {% if not website_image -%}{%- endif %}
{% endmacro %} @@ -8,7 +8,7 @@ {% macro product_image(website_image, css_class="") %}
{% if website_image -%} - + {%- else -%} {%- endif %} From ba02ce6adc73f7d0aefd602ab0049e209cb14dca Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Wed, 22 Jul 2015 15:07:25 +0530 Subject: [PATCH 10/40] [docs] added description --- erpnext/hooks.py | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index a87c8f092e..91ba19a9be 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -1,11 +1,34 @@ from __future__ import unicode_literals app_name = "erpnext" app_title = "ERPNext" -app_publisher = "Frappe Technologies Pvt. Ltd. and Contributors" -app_description = "Open Source Enterprise Resource Planning for Small and Midsized Organizations" +app_publisher = "Frappe Technologies Pvt. Ltd." +app_description = """## ERPNext + +ERPNext is a fully featured ERP system designed for Small and Medium Sized +business. ERPNext covers a wide range of features including Accounting, CRM, +Inventory management, Selling, Purchasing, Manufacturing, Projects, HR & +Payroll, Website, E-Commerce and much more. + +ERPNext is based on the Frappe Framework is highly customizable and extendable. +You can create Custom Form, Fields, Scripts and can also create your own Apps +to extend ERPNext functionality. + +ERPNext is Open Source under the GNU General Public Licence v3 and has been +listed as one of the Best Open Source Softwares in the world by my online +blogs. + +### Links + +- Website: [https://erpnext.com](https://erpnext.com) +- GitHub: [https://github.com/frappe/erpnext](https://github.com/frappe/erpnext) +- Forum: [https://discuss.erpnext.com](https://discuss.erpnext.com) +- Frappe Framework: [https://frappe.io](https://frappe.io) + +""" app_icon = "icon-th" app_color = "#e74c3c" app_version = "5.2.1" +github_link = "https://github.com/frappe/erpnext" error_report_email = "support@erpnext.com" From 05d81746961b799fbe0cfc8eba54372254b050e3 Mon Sep 17 00:00:00 2001 From: Neil Trini Lasrado Date: Wed, 22 Jul 2015 16:31:34 +0530 Subject: [PATCH 11/40] Fetch items from Packing List if Exists in Sales Order while raising Material Request against SO --- erpnext/selling/doctype/sales_order/sales_order.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index e8a772a216..d45fbba486 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -272,6 +272,10 @@ def make_material_request(source_name, target_doc=None): def postprocess(source, doc): doc.material_request_type = "Purchase" + so = frappe.get_doc("Sales Order", source_name) + + item_table = "Packed Item" if so.packed_items else "Sales Order Item" + doc = get_mapped_doc("Sales Order", source_name, { "Sales Order": { "doctype": "Material Request", @@ -279,7 +283,7 @@ def make_material_request(source_name, target_doc=None): "docstatus": ["=", 1] } }, - "Sales Order Item": { + item_table: { "doctype": "Material Request Item", "field_map": { "parent": "sales_order_no", From d0387f41dfd7578d246346f89079c40aba4d38fa Mon Sep 17 00:00:00 2001 From: Neil Trini Lasrado Date: Wed, 22 Jul 2015 18:44:47 +0530 Subject: [PATCH 12/40] Opening Balance row added to Stock Ledger Report --- .../stock/report/stock_ledger/stock_ledger.py | 30 +++++++++++++++++-- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.py b/erpnext/stock/report/stock_ledger/stock_ledger.py index 0651ae8961..c2c6d1ae7f 100644 --- a/erpnext/stock/report/stock_ledger/stock_ledger.py +++ b/erpnext/stock/report/stock_ledger/stock_ledger.py @@ -9,8 +9,13 @@ def execute(filters=None): columns = get_columns() sl_entries = get_stock_ledger_entries(filters) item_details = get_item_details(filters) - + opening_row = get_opening_balance(filters, columns) + data = [] + + if opening_row: + data.append(opening_row) + for sle in sl_entries: item_detail = item_details[sle.item_code] @@ -20,7 +25,7 @@ def execute(filters=None): (sle.incoming_rate if sle.actual_qty > 0 else 0.0), sle.valuation_rate, sle.stock_value, sle.voucher_type, sle.voucher_no, sle.batch_no, sle.serial_no, sle.company]) - + return columns, data def get_columns(): @@ -40,7 +45,7 @@ def get_stock_ledger_entries(filters): where company = %(company)s and posting_date between %(from_date)s and %(to_date)s {sle_conditions} - order by posting_date desc, posting_time desc, name desc"""\ + order by posting_date asc, posting_time asc, name asc"""\ .format(sle_conditions=get_sle_conditions(filters)), filters, as_dict=1) def get_item_details(filters): @@ -73,3 +78,22 @@ def get_sle_conditions(filters): conditions.append("voucher_no=%(voucher_no)s") return "and {}".format(" and ".join(conditions)) if conditions else "" + +def get_opening_balance(filters, columns): + if not (filters.item_code and filters.warehouse and filters.from_date): + return + + from erpnext.stock.stock_ledger import get_previous_sle + last_entry = get_previous_sle({ + "item_code": filters.item_code, + "warehouse": filters.warehouse, + "posting_date": filters.from_date, + "posting_time": "00:00:00" + }) + + row = [""]*len(columns) + row[1] = _("'Opening'") + for i, v in ((9, 'qty_after_transaction'), (11, 'valuation_rate'), (12, 'stock_value')): + row[i] = last_entry.get(v, 0) + + return row \ No newline at end of file From fec61fe33eac01d57f0249d41c3c4aa1576cb2f1 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Thu, 23 Jul 2015 10:39:13 +0530 Subject: [PATCH 13/40] [minor] fix report type for Accounts Payable --- .../accounts/report/accounts_payable/accounts_payable.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/report/accounts_payable/accounts_payable.json b/erpnext/accounts/report/accounts_payable/accounts_payable.json index 71537a837b..9be8d683b9 100644 --- a/erpnext/accounts/report/accounts_payable/accounts_payable.json +++ b/erpnext/accounts/report/accounts_payable/accounts_payable.json @@ -6,12 +6,12 @@ "doctype": "Report", "idx": 1, "is_standard": "Yes", - "modified": "2014-06-03 07:18:10.985354", + "modified": "2015-07-23 01:08:20.996267", "modified_by": "Administrator", "module": "Accounts", "name": "Accounts Payable", "owner": "Administrator", "ref_doctype": "Purchase Invoice", "report_name": "Accounts Payable", - "report_type": "Report Builder" + "report_type": "Query Report" } \ No newline at end of file From bb274fce2e30dc7524944f176da54eb9179c2e10 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 22 Jul 2015 18:54:32 +0530 Subject: [PATCH 14/40] [cleanup] Party Type deleted --- erpnext/contacts/__init__.py | 0 erpnext/contacts/doctype/__init__.py | 0 .../contacts/doctype/party_type/__init__.py | 0 .../doctype/party_type/party_type.json | 92 ------------------- .../contacts/doctype/party_type/party_type.py | 9 -- erpnext/modules.txt | 1 - erpnext/patches.txt | 1 + 7 files changed, 1 insertion(+), 102 deletions(-) delete mode 100644 erpnext/contacts/__init__.py delete mode 100644 erpnext/contacts/doctype/__init__.py delete mode 100644 erpnext/contacts/doctype/party_type/__init__.py delete mode 100644 erpnext/contacts/doctype/party_type/party_type.json delete mode 100644 erpnext/contacts/doctype/party_type/party_type.py diff --git a/erpnext/contacts/__init__.py b/erpnext/contacts/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/contacts/doctype/__init__.py b/erpnext/contacts/doctype/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/contacts/doctype/party_type/__init__.py b/erpnext/contacts/doctype/party_type/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/contacts/doctype/party_type/party_type.json b/erpnext/contacts/doctype/party_type/party_type.json deleted file mode 100644 index 19ffefba7c..0000000000 --- a/erpnext/contacts/doctype/party_type/party_type.json +++ /dev/null @@ -1,92 +0,0 @@ -{ - "allow_rename": 1, - "autoname": "field:party_type_name", - "creation": "2014-04-07 12:32:18.010384", - "docstatus": 0, - "doctype": "DocType", - "document_type": "Master", - "fields": [ - { - "fieldname": "party_type_name", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Party Type Name", - "permlevel": 0, - "reqd": 1 - }, - { - "fieldname": "parent_party_type", - "fieldtype": "Link", - "label": "Parent Party Type", - "options": "Party Type", - "permlevel": 0 - }, - { - "default": "Yes", - "fieldname": "allow_children", - "fieldtype": "Select", - "label": "Allow Children", - "options": "Yes\nNo", - "permlevel": 0 - }, - { - "fieldname": "default_price_list", - "fieldtype": "Link", - "ignore_user_permissions": 1, - "label": "Default Price List", - "options": "Price List", - "permlevel": 0 - }, - { - "fieldname": "lft", - "fieldtype": "Int", - "hidden": 1, - "label": "LFT", - "permlevel": 0, - "read_only": 1, - "search_index": 1 - }, - { - "fieldname": "rgt", - "fieldtype": "Int", - "hidden": 1, - "label": "RGT", - "permlevel": 0, - "read_only": 1, - "search_index": 1 - }, - { - "fieldname": "old_parent", - "fieldtype": "Data", - "hidden": 1, - "label": "Old Parent", - "permlevel": 0, - "read_only": 1 - } - ], - "modified": "2015-02-05 05:11:42.046004", - "modified_by": "Administrator", - "module": "Contacts", - "name": "Party Type", - "owner": "Administrator", - "permissions": [ - { - "apply_user_permissions": 1, - "create": 1, - "permlevel": 0, - "read": 1, - "role": "Sales User", - "share": 1, - "write": 1 - }, - { - "apply_user_permissions": 1, - "create": 1, - "permlevel": 0, - "read": 1, - "role": "Purchase User", - "share": 1, - "write": 1 - } - ] -} \ No newline at end of file diff --git a/erpnext/contacts/doctype/party_type/party_type.py b/erpnext/contacts/doctype/party_type/party_type.py deleted file mode 100644 index d21216f161..0000000000 --- a/erpnext/contacts/doctype/party_type/party_type.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# For license information, please see license.txt - -from __future__ import unicode_literals -import frappe -from frappe.utils.nestedset import NestedSet - -class PartyType(NestedSet): - nsm_parent_field = 'parent_party_type'; diff --git a/erpnext/modules.txt b/erpnext/modules.txt index 67c856db7f..dfca2f27c5 100644 --- a/erpnext/modules.txt +++ b/erpnext/modules.txt @@ -9,6 +9,5 @@ Manufacturing Stock Support Utilities -Contacts Shopping Cart Hub Node diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 269dcbaf9c..c7355073c4 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -177,3 +177,4 @@ erpnext.patches.v5_1.track_operations execute:frappe.rename_doc("DocType", "Salary Manager", "Process Payroll", force=True) erpnext.patches.v5_1.rename_roles erpnext.patches.v5_1.default_bom +execute:frappe.delete_doc("DocType", "Party Type") \ No newline at end of file From 7cfa5f0508be1c5247d03c61e621db0778b8f01a Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Thu, 23 Jul 2015 12:39:44 +0530 Subject: [PATCH 15/40] [fix] contact name in setup wizard --- .../setup/page/setup_wizard/setup_wizard.py | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/erpnext/setup/page/setup_wizard/setup_wizard.py b/erpnext/setup/page/setup_wizard/setup_wizard.py index 4bb01d48d4..141b914d91 100644 --- a/erpnext/setup/page/setup_wizard/setup_wizard.py +++ b/erpnext/setup/page/setup_wizard/setup_wizard.py @@ -367,13 +367,8 @@ def create_customers(args): }).insert() if args.get("customer_contact_" + str(i)): - contact = args.get("customer_contact_" + str(i)).split(" ") - frappe.get_doc({ - "doctype":"Contact", - "customer": customer, - "first_name":contact[0], - "last_name": len(contact) > 1 and contact[1] or "" - }).insert() + create_contact(args.get("customer_contact_" + str(i)), + "customer", customer) except frappe.NameError: pass @@ -390,16 +385,21 @@ def create_suppliers(args): }).insert() if args.get("supplier_contact_" + str(i)): - contact = args.get("supplier_contact_" + str(i)).split(" ") - frappe.get_doc({ - "doctype":"Contact", - "supplier": supplier, - "first_name":contact[0], - "last_name": len(contact) > 1 and contact[1] or "" - }).insert() + create_contact(args.get("supplier_contact_" + str(i)), + "supplier", supplier) except frappe.NameError: pass +def create_contact(contact, party_type, party): + """Create contact based on given contact name""" + contact = contact.strip().split(" ") + + frappe.get_doc({ + "doctype":"Contact", + party_type: party, + "first_name":contact[0], + "last_name": len(contact) > 1 and contact[1] or "" + }).insert() def create_letter_head(args): if args.get("attach_letterhead"): From 13df8a40ef209113f0e18b233f202d68f3bc3395 Mon Sep 17 00:00:00 2001 From: Neil Trini Lasrado Date: Thu, 23 Jul 2015 12:46:59 +0530 Subject: [PATCH 16/40] Validation added to prevent user to Manage Variants if Item Template is Unsaved. Prevented message stating variants updated while saving item template if there are no variants against that item Template --- erpnext/stock/doctype/item/item.js | 8 ++++++-- erpnext/stock/doctype/item/item.py | 3 ++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index 58b1adb8db..3bd5657d59 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -86,8 +86,12 @@ frappe.ui.form.on("Item", { }, manage_variants: function(frm) { - frappe.route_options = {"item_code": frm.doc.name }; - frappe.set_route("List", "Manage Variants"); + if (cur_frm.doc.__unsaved==1) { + frappe.throw(__("You have unsaved changes. Please save.")) + } else { + frappe.route_options = {"item_code": frm.doc.name }; + frappe.set_route("List", "Manage Variants"); + } } }); diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index a2e0ade50c..d3d8e9c6a1 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -325,7 +325,8 @@ class Item(WebsiteGenerator): for d in variants: update_variant(self.name, d) updated.append(d.item_code) - frappe.msgprint(_("Item Variants {0} updated").format(", ".join(updated))) + if updated: + 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"): From 08fb19ac8c0979ecada0c7253df3fb6a6a327fff Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 23 Jul 2015 12:18:46 +0530 Subject: [PATCH 17/40] Validation on Account for assigning budget --- .../doctype/cost_center/cost_center.js | 2 +- .../doctype/cost_center/cost_center.py | 57 +++++++++++-------- 2 files changed, 35 insertions(+), 24 deletions(-) diff --git a/erpnext/accounts/doctype/cost_center/cost_center.js b/erpnext/accounts/doctype/cost_center/cost_center.js index 6946bcb106..fb649e6d1b 100644 --- a/erpnext/accounts/doctype/cost_center/cost_center.js +++ b/erpnext/accounts/doctype/cost_center/cost_center.js @@ -17,7 +17,7 @@ erpnext.accounts.CostCenterController = frappe.ui.form.Controller.extend({ return { filters:[ ['Account', 'company', '=', me.frm.doc.company], - ['Account', 'report_type', '=', 'Profit and Loss'], + ['Account', 'root_type', '=', 'Expense'], ['Account', 'is_group', '=', '0'], ] } diff --git a/erpnext/accounts/doctype/cost_center/cost_center.py b/erpnext/accounts/doctype/cost_center/cost_center.py index f26c80ba59..0f51a00cd6 100644 --- a/erpnext/accounts/doctype/cost_center/cost_center.py +++ b/erpnext/accounts/doctype/cost_center/cost_center.py @@ -3,9 +3,7 @@ from __future__ import unicode_literals import frappe - -from frappe import msgprint, _ - +from frappe import _ from frappe.utils.nestedset import NestedSet class CostCenter(NestedSet): @@ -14,18 +12,46 @@ class CostCenter(NestedSet): def autoname(self): self.name = self.cost_center_name.strip() + ' - ' + \ frappe.db.get_value("Company", self.company, "abbr") + + + def validate(self): + self.validate_mandatory() + self.validate_accounts() def validate_mandatory(self): if self.cost_center_name != self.company and not self.parent_cost_center: - msgprint(_("Please enter parent cost center"), raise_exception=1) + frappe.throw(_("Please enter parent cost center")) elif self.cost_center_name == self.company and self.parent_cost_center: - msgprint(_("Root cannot have a parent cost center"), raise_exception=1) + frappe.throw(_("Root cannot have a parent cost center")) + + def validate_accounts(self): + if self.is_group==1 and self.get("budgets"): + frappe.throw(_("Budget cannot be set for Group Cost Center")) + + check_acc_list = [] + for d in self.get('budgets'): + if d.account: + account_details = frappe.db.get_value("Account", d.account, + ["is_group", "company", "root_type"], as_dict=1) + if account_details.is_group: + frappe.throw(_("Budget cannot be assigned against Group Account {0}").format(d.account)) + elif account_details.company != self.company: + frappe.throw(_("Account {0} does not belongs to company {1}").format(d.account, self.company)) + elif account_details.root_type != "Expense": + frappe.throw(_("Budget cannot be assigned against {0}, as it's not an Expense account") + .format(d.account)) + + if [d.account, d.fiscal_year] in check_acc_list: + frappe.throw(_("Account {0} has been entered more than once for fiscal year {1}") + .format(d.account, d.fiscal_year)) + else: + check_acc_list.append([d.account, d.fiscal_year]) def convert_group_to_ledger(self): if self.check_if_child_exists(): - msgprint(_("Cannot convert Cost Center to ledger as it has child nodes"), raise_exception=1) + frappe.throw(_("Cannot convert Cost Center to ledger as it has child nodes")) elif self.check_gle_exists(): - msgprint(_("Cost Center with existing transactions can not be converted to ledger"), raise_exception=1) + frappe.throw(_("Cost Center with existing transactions can not be converted to ledger")) else: self.is_group = 0 self.save() @@ -33,7 +59,7 @@ class CostCenter(NestedSet): def convert_ledger_to_group(self): if self.check_gle_exists(): - msgprint(_("Cost Center with existing transactions can not be converted to group"), raise_exception=1) + frappe.throw(_("Cost Center with existing transactions can not be converted to group")) else: self.is_group = 1 self.save() @@ -46,21 +72,6 @@ class CostCenter(NestedSet): return frappe.db.sql("select name from `tabCost Center` where \ parent_cost_center = %s and docstatus != 2", self.name) - def validate_budget_details(self): - check_acc_list = [] - for d in self.get('budgets'): - if self.is_group==1: - msgprint(_("Budget cannot be set for Group Cost Centers"), raise_exception=1) - - if [d.account, d.fiscal_year] in check_acc_list: - msgprint(_("Account {0} has been entered more than once for fiscal year {1}").format(d.account, d.fiscal_year), raise_exception=1) - else: - check_acc_list.append([d.account, d.fiscal_year]) - - def validate(self): - self.validate_mandatory() - self.validate_budget_details() - def before_rename(self, olddn, newdn, merge=False): # Add company abbr if not provided from erpnext.setup.doctype.company.company import get_name_with_abbr From f66653522358c17e946bb4181caf0ecf24392580 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 23 Jul 2015 17:08:44 +0530 Subject: [PATCH 18/40] [fix] gross profit report --- erpnext/accounts/report/gross_profit/gross_profit.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index 8153912304..75c353d170 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -174,12 +174,12 @@ class GrossProfitGenerator(object): return flt(row.qty) * item_rate else: - if row.update_stock or row.dn_detail: + my_sle = self.sle.get((item_code, row.warehouse)) + if (row.update_stock or row.dn_detail) and my_sle: parenttype, parent, item_row = row.parenttype, row.parent, row.item_row if 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 == parenttype and parent == sle.voucher_no and \ From 2771b7d82865e3316eba99a19eb894fddef2cf83 Mon Sep 17 00:00:00 2001 From: Neil Trini Lasrado Date: Tue, 21 Jul 2015 18:11:43 +0530 Subject: [PATCH 19/40] Track Operations removed and Global Switch added to disable capacity planning in manufacturing settings --- .../manufacturing_settings.json | 10 +++++++++- .../production_order/production_order.js | 19 ++++--------------- .../production_order/production_order.json | 14 +++----------- .../production_order/production_order.py | 7 +------ erpnext/patches.txt | 1 - erpnext/patches/v5_1/track_operations.py | 8 -------- .../stock/doctype/stock_entry/stock_entry.py | 4 +--- 7 files changed, 18 insertions(+), 45 deletions(-) delete mode 100644 erpnext/patches/v5_1/track_operations.py diff --git a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json index ea9569f9b4..4c0028034b 100644 --- a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json +++ b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json @@ -15,6 +15,14 @@ "permlevel": 0, "precision": "" }, + { + "description": "Disables creation of time logs against Production Orders.\nOperations shall not be tracked against Production Order", + "fieldname": "disable_capacity_planning", + "fieldtype": "Check", + "label": "Disable Capacity Planning", + "permlevel": 0, + "precision": "" + }, { "description": "Plan time logs outside Workstation Working Hours.", "fieldname": "allow_overtime", @@ -72,7 +80,7 @@ "is_submittable": 0, "issingle": 1, "istable": 0, - "modified": "2015-06-15 05:52:22.986958", + "modified": "2015-07-21 08:51:01.651774", "modified_by": "Administrator", "module": "Manufacturing", "name": "Manufacturing Settings", diff --git a/erpnext/manufacturing/doctype/production_order/production_order.js b/erpnext/manufacturing/doctype/production_order/production_order.js index 17fa202a4d..f8d76fff63 100644 --- a/erpnext/manufacturing/doctype/production_order/production_order.js +++ b/erpnext/manufacturing/doctype/production_order/production_order.js @@ -186,27 +186,16 @@ $.extend(cur_frm.cscript, { }, bom_no: function() { - if (this.frm.doc.track_operations) { - return this.frm.call({ - doc: this.frm.doc, - method: "set_production_order_operations" - }); - } + return this.frm.call({ + doc: this.frm.doc, + method: "set_production_order_operations" + }); }, qty: function() { frappe.ui.form.trigger("Production Order", 'bom_no') }, - track_operations: function(doc) { - if (doc.track_operations) { - frappe.ui.form.trigger("Production Order", 'bom_no') - } - else { - doc.operations =[]; - } - }, - show_time_logs: function(doc, cdt, cdn) { var child = locals[cdt][cdn] frappe.route_options = {"operation_id": child.name}; diff --git a/erpnext/manufacturing/doctype/production_order/production_order.json b/erpnext/manufacturing/doctype/production_order/production_order.json index 75aab9963e..e07ac5bf97 100644 --- a/erpnext/manufacturing/doctype/production_order/production_order.json +++ b/erpnext/manufacturing/doctype/production_order/production_order.json @@ -73,14 +73,6 @@ "label": "Use Multi-Level BOM", "permlevel": 0 }, - { - "default": "1", - "fieldname": "track_operations", - "fieldtype": "Check", - "label": "Track Operations", - "permlevel": 0, - "precision": "" - }, { "fieldname": "column_break1", "fieldtype": "Column Break", @@ -215,7 +207,7 @@ "read_only": 1 }, { - "depends_on": "track_operations", + "depends_on": "", "fieldname": "operations_section", "fieldtype": "Section Break", "label": "Operations", @@ -234,7 +226,7 @@ "read_only": 1 }, { - "depends_on": "track_operations", + "depends_on": "operations", "fieldname": "section_break_22", "fieldtype": "Section Break", "label": "Operation Cost", @@ -368,7 +360,7 @@ "idx": 1, "in_create": 0, "is_submittable": 1, - "modified": "2015-07-13 05:28:23.259016", + "modified": "2015-07-21 07:45:53.206902", "modified_by": "Administrator", "module": "Manufacturing", "name": "Production Order", diff --git a/erpnext/manufacturing/doctype/production_order/production_order.py b/erpnext/manufacturing/doctype/production_order/production_order.py index 93ce5e16f6..072ad41b5a 100644 --- a/erpnext/manufacturing/doctype/production_order/production_order.py +++ b/erpnext/manufacturing/doctype/production_order/production_order.py @@ -174,17 +174,12 @@ class ProductionOrder(Document): def set_production_order_operations(self): """Fetch operations from BOM and set in 'Production Order'""" - if not self.bom_no: + if not self.bom_no or cint(frappe.db.get_single_value("Manufacturing Settings", "disable_capacity_planning")): return self.set('operations', []) operations = frappe.db.sql("""select operation, description, workstation, idx, hour_rate, time_in_mins, "Pending" as status from `tabBOM Operation` where parent = %s order by idx""", self.bom_no, as_dict=1) - if operations: - self.track_operations=1 - else: - self.track_operations=0 - frappe.msgprint(_("Cannot 'track operations' as selected BOM does not have Operations.")) self.set('operations', operations) self.calculate_time() diff --git a/erpnext/patches.txt b/erpnext/patches.txt index c7355073c4..762912372b 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -173,7 +173,6 @@ erpnext.patches.v5_0.item_variants erpnext.patches.v5_0.update_item_desc_in_invoice erpnext.patches.v5_1.fix_against_account erpnext.patches.v5_1.fix_credit_days_based_on -erpnext.patches.v5_1.track_operations execute:frappe.rename_doc("DocType", "Salary Manager", "Process Payroll", force=True) erpnext.patches.v5_1.rename_roles erpnext.patches.v5_1.default_bom diff --git a/erpnext/patches/v5_1/track_operations.py b/erpnext/patches/v5_1/track_operations.py deleted file mode 100644 index 0a121420e5..0000000000 --- a/erpnext/patches/v5_1/track_operations.py +++ /dev/null @@ -1,8 +0,0 @@ -from __future__ import unicode_literals - -import frappe - -def execute(): - frappe.reload_doctype("Production Order") - frappe.db.sql("""Update `tabProduction Order` as po set track_operations=1 where - exists(select name from `tabProduction Order Operation` as po_operation where po_operation.parent = po.name )""") \ No newline at end of file diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index fb1ec3d17b..cae057123b 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -201,9 +201,7 @@ class StockEntry(StockController): def check_if_operations_completed(self): """Check if Time Logs are completed against before manufacturing to capture operating costs.""" prod_order = frappe.get_doc("Production Order", self.production_order) - if not prod_order.track_operations: - return - + for d in prod_order.get("operations"): total_completed_qty = flt(self.fg_completed_qty) + flt(prod_order.produced_qty) if total_completed_qty > flt(d.completed_qty): From c723c8b5aa45021b4310d506e2c25586ce172800 Mon Sep 17 00:00:00 2001 From: Neil Trini Lasrado Date: Thu, 23 Jul 2015 17:43:27 +0530 Subject: [PATCH 20/40] Disable Capacity Planning label changed to Disable Capacity Planning & Time Tracking in Manufacturing Settings --- .../manufacturing_settings/manufacturing_settings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json index 4c0028034b..eb770a8609 100644 --- a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json +++ b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json @@ -19,7 +19,7 @@ "description": "Disables creation of time logs against Production Orders.\nOperations shall not be tracked against Production Order", "fieldname": "disable_capacity_planning", "fieldtype": "Check", - "label": "Disable Capacity Planning", + "label": "Disable Capacity Planning and Time Tracking", "permlevel": 0, "precision": "" }, @@ -80,7 +80,7 @@ "is_submittable": 0, "issingle": 1, "istable": 0, - "modified": "2015-07-21 08:51:01.651774", + "modified": "2015-07-23 08:12:33.889753", "modified_by": "Administrator", "module": "Manufacturing", "name": "Manufacturing Settings", From 9257413b6803e5785d2dc63beb36bc1eab3ab2b1 Mon Sep 17 00:00:00 2001 From: Anand Doshi Date: Thu, 23 Jul 2015 18:16:25 +0530 Subject: [PATCH 21/40] [minor] Added 'Import Data' in sample data --- erpnext/setup/page/setup_wizard/sample_data.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/erpnext/setup/page/setup_wizard/sample_data.py b/erpnext/setup/page/setup_wizard/sample_data.py index f7fb73b446..d9f8343722 100644 --- a/erpnext/setup/page/setup_wizard/sample_data.py +++ b/erpnext/setup/page/setup_wizard/sample_data.py @@ -109,9 +109,14 @@ def make_projects(): "end_date": frappe.utils.add_days(current_date, 5) }, { - "title": "Go Live!", + "title": "Import Data", "start_date": frappe.utils.add_days(current_date, 5), "end_date": frappe.utils.add_days(current_date, 6) + }, + { + "title": "Go Live!", + "start_date": frappe.utils.add_days(current_date, 6), + "end_date": frappe.utils.add_days(current_date, 7) }]) project.insert(ignore_permissions=True) From 982f4ae44d9bd05b2b6a96a13fdd483c41753ad0 Mon Sep 17 00:00:00 2001 From: Tsutomu Mimori Date: Thu, 23 Jul 2015 22:09:35 +0900 Subject: [PATCH 22/40] Removed HTML from messages --- erpnext/accounts/doctype/cost_center/cost_center.json | 4 ++-- erpnext/accounts/doctype/journal_entry/journal_entry.json | 4 ++-- erpnext/setup/doctype/features_setup/features_setup.json | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/erpnext/accounts/doctype/cost_center/cost_center.json b/erpnext/accounts/doctype/cost_center/cost_center.json index 6177d35ced..ebba758963 100644 --- a/erpnext/accounts/doctype/cost_center/cost_center.json +++ b/erpnext/accounts/doctype/cost_center/cost_center.json @@ -66,7 +66,7 @@ "precision": "" }, { - "description": "Define Budget for this Cost Center. To set budget action, see Company Master", + "description": "Define Budget for this Cost Center. To set budget action, see \"Company Master\"", "fieldname": "sb1", "fieldtype": "Section Break", "label": "Budget", @@ -193,4 +193,4 @@ } ], "search_fields": "parent_cost_center, is_group" -} \ No newline at end of file +} diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.json b/erpnext/accounts/doctype/journal_entry/journal_entry.json index 249fcc46d3..4847a71ac2 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.json +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.json @@ -310,7 +310,7 @@ "depends_on": "eval:doc.voucher_type == 'Write Off Entry'", "fieldname": "write_off_amount", "fieldtype": "Currency", - "label": "Write Off Amount <=", + "label": "Write Off Amount", "options": "Company:company:default_currency", "permlevel": 0, "print_hide": 1, @@ -503,4 +503,4 @@ "sort_field": "modified", "sort_order": "DESC", "title_field": "title" -} \ No newline at end of file +} diff --git a/erpnext/setup/doctype/features_setup/features_setup.json b/erpnext/setup/doctype/features_setup/features_setup.json index edc88e2800..861103cdbf 100644 --- a/erpnext/setup/doctype/features_setup/features_setup.json +++ b/erpnext/setup/doctype/features_setup/features_setup.json @@ -18,7 +18,7 @@ "permlevel": 0 }, { - "description": "To track items in sales and purchase documents with batch nos
Preferred Industry: Chemicals etc", + "description": "To track items in sales and purchase documents with batch nos.\"Preferred Industry: Chemicals etc\"", "fieldname": "fs_item_batch_nos", "fieldtype": "Check", "in_list_view": 1, @@ -139,14 +139,14 @@ "permlevel": 0 }, { - "description": "To enable Point of Sale features", + "description": "To enable \"Point of Sale\" features", "fieldname": "fs_pos", "fieldtype": "Check", "label": "Point of Sale", "permlevel": 0 }, { - "description": "To enable Point of Sale view", + "description": "To enable \"Point of Sale\" view", "fieldname": "fs_pos_view", "fieldtype": "Check", "label": "POS View", @@ -237,4 +237,4 @@ "write": 1 } ] -} \ No newline at end of file +} From 9c3dca63fa252278eb9032bcb7f822cb3234e6d9 Mon Sep 17 00:00:00 2001 From: Neil Trini Lasrado Date: Tue, 21 Jul 2015 13:27:18 +0530 Subject: [PATCH 23/40] Disallowed End of Life Items from getting selected in Production Orders and Stock Reconciliation --- .../doctype/production_order/production_order.js | 3 ++- .../doctype/production_order/production_order.py | 3 +++ .../doctype/production_order/test_production_order.py | 6 ++++++ .../doctype/stock_reconciliation/stock_reconciliation.js | 8 ++++++++ 4 files changed, 19 insertions(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/production_order/production_order.js b/erpnext/manufacturing/doctype/production_order/production_order.js index 17fa202a4d..50f46ca401 100644 --- a/erpnext/manufacturing/doctype/production_order/production_order.js +++ b/erpnext/manufacturing/doctype/production_order/production_order.js @@ -262,7 +262,8 @@ cur_frm.fields_dict['production_item'].get_query = function(doc) { return { filters:[ ['Item', 'is_pro_applicable', '=', 'Yes'], - ['Item', 'has_variants', '=', 'No'] + ['Item', 'has_variants', '=', 'No'], + ['Item', 'end_of_life', '>', frappe.datetime.now_datetime()] ] } } diff --git a/erpnext/manufacturing/doctype/production_order/production_order.py b/erpnext/manufacturing/doctype/production_order/production_order.py index 93ce5e16f6..7a0921d513 100644 --- a/erpnext/manufacturing/doctype/production_order/production_order.py +++ b/erpnext/manufacturing/doctype/production_order/production_order.py @@ -9,6 +9,7 @@ from frappe import _ from frappe.model.document import Document from erpnext.manufacturing.doctype.bom.bom import validate_bom_no from dateutil.relativedelta import relativedelta +from erpnext.stock.doctype.item.item import validate_end_of_life class OverProductionError(frappe.ValidationError): pass class StockOverProductionError(frappe.ValidationError): pass @@ -329,6 +330,8 @@ class ProductionOrder(Document): if frappe.db.get_value("Item", self.production_item, "has_variants"): frappe.throw(_("Production Order cannot be raised against a Item Template")) + + validate_end_of_life(self.production_item) @frappe.whitelist() def get_item_details(item): diff --git a/erpnext/manufacturing/doctype/production_order/test_production_order.py b/erpnext/manufacturing/doctype/production_order/test_production_order.py index 34d584a94a..f9aa03194a 100644 --- a/erpnext/manufacturing/doctype/production_order/test_production_order.py +++ b/erpnext/manufacturing/doctype/production_order/test_production_order.py @@ -135,6 +135,12 @@ class TestProductionOrder(unittest.TestCase): prod_order.set_production_order_operations() self.assertEqual(prod_order.planned_operating_cost, cost*2) + def test_production_item(self): + item = frappe.get_doc("Item", "_Test FG Item") + item.end_of_life = + + prod_order = make_prod_order_test_record(item="_Test FG Item", qty=1, do_not_save=True) + def make_prod_order_test_record(**args): args = frappe._dict(args) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js index c0ae213b87..b394b71a22 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js @@ -98,3 +98,11 @@ cur_frm.cscript.company = function(doc, cdt, cdn) { cur_frm.cscript.posting_date = function(doc, cdt, cdn){ erpnext.get_fiscal_year(doc.company, doc.posting_date); } + +cur_frm.fields_dict.items.grid.get_field('item_code').get_query = function(doc, cdt, cdn) { + return { + filters:[ + ['Item', 'end_of_life', '>', frappe.datetime.now_datetime()] + ] + } +} \ No newline at end of file From 21647974c499c995d59593efaff70eccb2551ffd Mon Sep 17 00:00:00 2001 From: Neil Trini Lasrado Date: Tue, 21 Jul 2015 15:30:31 +0530 Subject: [PATCH 24/40] Test Cases Added to Production Order --- .../production_order/production_order.py | 6 ++++-- .../production_order/test_production_order.py | 19 +++++++++++++++++-- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_order/production_order.py b/erpnext/manufacturing/doctype/production_order/production_order.py index 7a0921d513..c2cbbfd9d6 100644 --- a/erpnext/manufacturing/doctype/production_order/production_order.py +++ b/erpnext/manufacturing/doctype/production_order/production_order.py @@ -14,6 +14,8 @@ from erpnext.stock.doctype.item.item import validate_end_of_life class OverProductionError(frappe.ValidationError): pass class StockOverProductionError(frappe.ValidationError): pass class OperationTooLongError(frappe.ValidationError): pass +class ProductionNotApplicableError(frappe.ValidationError): pass +class ItemHasVariantError(frappe.ValidationError): pass from erpnext.manufacturing.doctype.workstation.workstation import WorkstationHolidayError, NotInWorkingHoursError from erpnext.projects.doctype.time_log.time_log import OverlapError @@ -326,10 +328,10 @@ class ProductionOrder(Document): def validate_production_item(self): if frappe.db.get_value("Item", self.production_item, "is_pro_applicable")=='No': - frappe.throw(_("Item is not allowed to have Production Order.")) + frappe.throw(_("Item is not allowed to have Production Order."), ProductionNotApplicableError) if frappe.db.get_value("Item", self.production_item, "has_variants"): - frappe.throw(_("Production Order cannot be raised against a Item Template")) + frappe.throw(_("Production Order cannot be raised against a Item Template"), ItemHasVariantError) validate_end_of_life(self.production_item) diff --git a/erpnext/manufacturing/doctype/production_order/test_production_order.py b/erpnext/manufacturing/doctype/production_order/test_production_order.py index f9aa03194a..c62be8260a 100644 --- a/erpnext/manufacturing/doctype/production_order/test_production_order.py +++ b/erpnext/manufacturing/doctype/production_order/test_production_order.py @@ -7,7 +7,8 @@ import unittest import frappe from frappe.utils import flt, get_datetime, time_diff_in_hours from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory -from erpnext.manufacturing.doctype.production_order.production_order import make_stock_entry, make_time_log +from erpnext.manufacturing.doctype.production_order.production_order \ + import make_stock_entry, make_time_log, ProductionNotApplicableError,ItemHasVariantError from erpnext.stock.doctype.stock_entry import test_stock_entry from erpnext.projects.doctype.time_log.time_log import OverProductionLoggedError @@ -137,10 +138,24 @@ class TestProductionOrder(unittest.TestCase): def test_production_item(self): item = frappe.get_doc("Item", "_Test FG Item") - item.end_of_life = + item.is_pro_applicable= "No" + item.save() prod_order = make_prod_order_test_record(item="_Test FG Item", qty=1, do_not_save=True) + self.assertRaises(ProductionNotApplicableError, prod_order.save) + item.is_pro_applicable= "Yes" + item.end_of_life = "2000-1-1" + item.save() + + self.assertRaises(frappe.ValidationError, prod_order.save) + + item.end_of_life=None + item.save() + + prod_order = make_prod_order_test_record(item="_Test Variant Item", qty=1, do_not_save=True) + self.assertRaises(ItemHasVariantError, prod_order.save) + def make_prod_order_test_record(**args): args = frappe._dict(args) From f965c5d20327e0e580a9deb248bb681702734ff3 Mon Sep 17 00:00:00 2001 From: Neil Trini Lasrado Date: Thu, 23 Jul 2015 16:56:27 +0530 Subject: [PATCH 25/40] now_datetime changed to nowdate --- .../manufacturing/doctype/production_order/production_order.js | 2 +- .../stock/doctype/stock_reconciliation/stock_reconciliation.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_order/production_order.js b/erpnext/manufacturing/doctype/production_order/production_order.js index 50f46ca401..151854c30a 100644 --- a/erpnext/manufacturing/doctype/production_order/production_order.js +++ b/erpnext/manufacturing/doctype/production_order/production_order.js @@ -263,7 +263,7 @@ cur_frm.fields_dict['production_item'].get_query = function(doc) { filters:[ ['Item', 'is_pro_applicable', '=', 'Yes'], ['Item', 'has_variants', '=', 'No'], - ['Item', 'end_of_life', '>', frappe.datetime.now_datetime()] + ['Item', 'end_of_life', '>=', frappe.datetime.nowdate()] ] } } diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js index b394b71a22..f833a251be 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js @@ -102,7 +102,7 @@ cur_frm.cscript.posting_date = function(doc, cdt, cdn){ cur_frm.fields_dict.items.grid.get_field('item_code').get_query = function(doc, cdt, cdn) { return { filters:[ - ['Item', 'end_of_life', '>', frappe.datetime.now_datetime()] + ['Item', 'end_of_life', '>=', frappe.datetime.nowdate()] ] } } \ No newline at end of file From 8a9d41a92edae41aacc959a4799f4dd8d74ff5a1 Mon Sep 17 00:00:00 2001 From: Neil Trini Lasrado Date: Fri, 24 Jul 2015 13:18:45 +0530 Subject: [PATCH 26/40] Modified Test Cases for Production Order --- .../production_order/test_production_order.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_order/test_production_order.py b/erpnext/manufacturing/doctype/production_order/test_production_order.py index c62be8260a..b91b2e12c8 100644 --- a/erpnext/manufacturing/doctype/production_order/test_production_order.py +++ b/erpnext/manufacturing/doctype/production_order/test_production_order.py @@ -137,21 +137,17 @@ class TestProductionOrder(unittest.TestCase): self.assertEqual(prod_order.planned_operating_cost, cost*2) def test_production_item(self): - item = frappe.get_doc("Item", "_Test FG Item") - item.is_pro_applicable= "No" - item.save() - + frappe.db.set_value("Item", "_Test FG Item", "is_pro_applicable", "No") + prod_order = make_prod_order_test_record(item="_Test FG Item", qty=1, do_not_save=True) self.assertRaises(ProductionNotApplicableError, prod_order.save) - item.is_pro_applicable= "Yes" - item.end_of_life = "2000-1-1" - item.save() + frappe.db.set_value("Item", "_Test FG Item", "is_pro_applicable", "Yes") + frappe.db.set_value("Item", "_Test FG Item", "end_of_life", "2000-1-1") self.assertRaises(frappe.ValidationError, prod_order.save) - item.end_of_life=None - item.save() + frappe.db.set_value("Item", "_Test FG Item", "end_of_life", None) prod_order = make_prod_order_test_record(item="_Test Variant Item", qty=1, do_not_save=True) self.assertRaises(ItemHasVariantError, prod_order.save) From ada485f09600fe3e0436795adc3bb052c471bf42 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 17 Jul 2015 15:09:56 +0530 Subject: [PATCH 27/40] Outgoing rate in Purchase Return based on reference/original Purchase Receipt rate --- .../stock_ledger_entry.json | 11 +++++- erpnext/stock/stock_ledger.py | 39 ++++++++++++++----- 2 files changed, 40 insertions(+), 10 deletions(-) diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json index 780bcc9c33..bb6f4098d7 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json +++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json @@ -150,6 +150,15 @@ "permlevel": 0, "read_only": 1 }, + { + "fieldname": "outgoing_rate", + "fieldtype": "Currency", + "label": "Outgoing Rate", + "options": "Company:company:default_currency", + "permlevel": 0, + "precision": "", + "read_only": 1 + }, { "fieldname": "stock_uom", "fieldtype": "Link", @@ -266,7 +275,7 @@ "icon": "icon-list", "idx": 1, "in_create": 1, - "modified": "2015-07-13 05:28:27.826340", + "modified": "2015-07-16 16:37:54.452944", "modified_by": "Administrator", "module": "Stock", "name": "Stock Ledger Entry", diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 954a03b3bf..a5deb3075b 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -109,7 +109,7 @@ class update_entries_after(object): def build(self): # includes current entry! entries_to_fix = self.get_sle_after_datetime() - + for sle in entries_to_fix: self.process_sle(sle) @@ -230,19 +230,21 @@ class update_entries_after(object): self.valuation_rate = new_stock_value / new_stock_qty def get_moving_average_values(self, sle): - incoming_rate = flt(sle.incoming_rate) actual_qty = flt(sle.actual_qty) - - if flt(sle.actual_qty) > 0: + + if actual_qty > 0 or flt(sle.outgoing_rate) > 0: + rate = flt(sle.incoming_rate) if actual_qty > 0 else flt(sle.outgoing_rate) + if self.qty_after_transaction < 0 and not self.valuation_rate: # if negative stock, take current valuation rate as incoming rate - self.valuation_rate = incoming_rate + self.valuation_rate = rate new_stock_qty = abs(self.qty_after_transaction) + actual_qty - new_stock_value = (abs(self.qty_after_transaction) * self.valuation_rate) + (actual_qty * incoming_rate) + new_stock_value = (abs(self.qty_after_transaction) * self.valuation_rate) + (actual_qty * rate) if new_stock_qty: self.valuation_rate = new_stock_value / flt(new_stock_qty) + elif not self.valuation_rate and self.qty_after_transaction <= 0: self.valuation_rate = get_valuation_rate(sle.item_code, sle.warehouse, self.allow_zero_rate) @@ -251,6 +253,7 @@ class update_entries_after(object): def get_fifo_values(self, sle): incoming_rate = flt(sle.incoming_rate) actual_qty = flt(sle.actual_qty) + outgoing_rate = flt(sle.outgoing_rate) if actual_qty > 0: if not self.stock_queue: @@ -278,16 +281,34 @@ class update_entries_after(object): _rate = 0 self.stock_queue.append([0, _rate]) - batch = self.stock_queue[0] + index = None + if outgoing_rate > 0: + # Find the entry where rate matched with outgoing rate + for i, v in enumerate(self.stock_queue): + if v[1] == outgoing_rate: + index = i + break + + # If no entry found with outgoing rate, collapse stack + if index == None: + new_stock_value = sum((d[0]*d[1] for d in self.stock_queue)) - qty_to_pop*outgoing_rate + new_stock_qty = sum((d[0] for d in self.stock_queue)) - qty_to_pop + self.stock_queue = [[new_stock_qty, new_stock_value/new_stock_qty if new_stock_qty > 0 else outgoing_rate]] + break + else: + index = 0 + + # select first batch or the batch with same rate + batch = self.stock_queue[index] if qty_to_pop >= batch[0]: # consume current batch qty_to_pop = qty_to_pop - batch[0] - self.stock_queue.pop(0) + self.stock_queue.pop(index) if not self.stock_queue and qty_to_pop: # stock finished, qty still remains to be withdrawn # negative stock, keep in as a negative batch - self.stock_queue.append([-qty_to_pop, batch[1]]) + self.stock_queue.append([-qty_to_pop, outgoing_rate or batch[1]]) break else: From 1d21842f68ebccbcff09dd9c57d0a325da66b512 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 17 Jul 2015 15:19:02 +0530 Subject: [PATCH 28/40] Sales / Purchase Return redesigned via negative DN / SI / PR / PI --- erpnext/accounts/doctype/gl_entry/gl_entry.py | 8 +- .../purchase_invoice/purchase_invoice.js | 12 ++- .../purchase_invoice/purchase_invoice.json | 26 ++++- .../purchase_invoice/purchase_invoice.py | 37 ++++--- .../doctype/sales_invoice/sales_invoice.js | 28 +++-- .../doctype/sales_invoice/sales_invoice.json | 28 ++++- .../doctype/sales_invoice/sales_invoice.py | 58 ++++++---- .../purchase_common/purchase_common.js | 6 +- .../purchase_common/purchase_common.py | 3 +- erpnext/controllers/accounts_controller.py | 101 +++++++++++++++++- erpnext/controllers/buying_controller.py | 3 +- erpnext/controllers/selling_controller.py | 4 +- erpnext/controllers/stock_controller.py | 11 ++ erpnext/controllers/taxes_and_totals.py | 17 +-- .../public/js/controllers/taxes_and_totals.js | 9 +- erpnext/public/js/controllers/transaction.js | 20 +++- erpnext/selling/sales_common.js | 2 +- .../doctype/delivery_note/delivery_note.js | 11 +- .../doctype/delivery_note/delivery_note.json | 26 ++++- .../doctype/delivery_note/delivery_note.py | 28 +++-- .../purchase_receipt/purchase_receipt.js | 10 ++ .../purchase_receipt/purchase_receipt.json | 27 ++++- .../purchase_receipt/purchase_receipt.py | 55 +++++++--- erpnext/utilities/transaction_base.py | 33 ++++++ 24 files changed, 451 insertions(+), 112 deletions(-) diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py index 3d306fb8d5..edee1226eb 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.py +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals import frappe -from frappe.utils import flt, fmt_money, getdate, formatdate, cstr +from frappe.utils import flt, fmt_money, getdate, formatdate, cstr, cint from frappe import _ from frappe.model.document import Document @@ -139,9 +139,9 @@ def update_outstanding_amt(account, party_type, party, against_voucher_type, aga if against_voucher_amount < 0: bal = -bal - # Validation : Outstanding can not be negative - if bal < 0 and not on_cancel: - frappe.throw(_("Outstanding for {0} cannot be less than zero ({1})").format(against_voucher, fmt_money(bal))) + # Validation : Outstanding can not be negative for JV + if bal < 0 and not on_cancel: + frappe.throw(_("Outstanding for {0} cannot be less than zero ({1})").format(against_voucher, fmt_money(bal))) # Update outstanding amt on against voucher if against_voucher_type in ["Sales Invoice", "Purchase Invoice"]: diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index 07dbf721f5..548abb7cbf 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -25,6 +25,9 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({ frappe.boot.doctype_icons["Journal Entry"]); if(doc.docstatus==1) { + cur_frm.add_custom_button(__('Make Purchase Return'), this.make_purchase_return, + frappe.boot.doctype_icons["Purchase Invoice"]); + cur_frm.add_custom_button(__('View Ledger'), function() { frappe.route_options = { "voucher_no": doc.name, @@ -109,7 +112,14 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({ $.each(this.frm.doc["items"] || [], function(i, row) { if(row.purchase_receipt) frappe.model.clear_doc("Purchase Receipt", row.purchase_receipt) }) - } + }, + + make_purchase_return: function() { + frappe.model.open_mapped_doc({ + method: "erpnext.accounts.doctype.purchase_invoice.purchase_invoice.make_purchase_return", + frm: cur_frm + }) + }, }); cur_frm.script_manager.make(erpnext.accounts.PurchaseInvoice); diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index 69b0708f1a..c5797162db 100755 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -12,7 +12,7 @@ "no_copy": 1, "oldfieldname": "naming_series", "oldfieldtype": "Select", - "options": "PINV-", + "options": "PINV-\nPINV-RET-", "permlevel": 0, "print_hide": 1, "read_only": 0, @@ -154,6 +154,28 @@ "read_only": 0, "search_index": 0 }, + { + "fieldname": "is_return", + "fieldtype": "Check", + "label": "Is Return", + "no_copy": 1, + "permlevel": 0, + "precision": "", + "print_hide": 1, + "read_only": 1 + }, + { + "depends_on": "is_return", + "fieldname": "return_against", + "fieldtype": "Link", + "label": "Return Against Purchase Invoice", + "no_copy": 1, + "options": "Purchase Invoice", + "permlevel": 0, + "precision": "", + "print_hide": 1, + "read_only": 1 + }, { "fieldname": "currency_and_price_list", "fieldtype": "Section Break", @@ -940,7 +962,7 @@ "icon": "icon-file-text", "idx": 1, "is_submittable": 1, - "modified": "2015-07-03 03:26:32.934540", + "modified": "2015-07-17 14:09:19.666457", "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 1ac0f5acd8..b34f8452e2 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -37,14 +37,16 @@ class PurchaseInvoice(BuyingController): super(PurchaseInvoice, self).validate() - self.po_required() - self.pr_required() - self.validate_supplier_invoice() + if not self.is_return: + self.po_required() + self.pr_required() + self.validate_supplier_invoice() + self.validate_advance_jv("advances", "purchase_order") + self.check_active_purchase_items() self.check_conversion_rate() self.validate_credit_to_acc() self.clear_unallocated_advances("Purchase Invoice Advance", "advances") - self.validate_advance_jv("advances", "purchase_order") self.check_for_stopped_status() self.validate_with_previous_doc() self.validate_uom_is_integer("uom", "qty") @@ -71,8 +73,9 @@ class PurchaseInvoice(BuyingController): super(PurchaseInvoice, self).set_missing_values(for_validate) def get_advances(self): - super(PurchaseInvoice, self).get_advances(self.credit_to, "Supplier", self.supplier, - "Purchase Invoice Advance", "advances", "debit", "purchase_order") + if not self.is_return: + super(PurchaseInvoice, self).get_advances(self.credit_to, "Supplier", self.supplier, + "Purchase Invoice Advance", "advances", "debit", "purchase_order") def check_active_purchase_items(self): for d in self.get('items'): @@ -226,9 +229,11 @@ class PurchaseInvoice(BuyingController): # this sequence because outstanding may get -negative self.make_gl_entries() - self.update_against_document_in_jv() - self.update_prevdoc_status() - self.update_billing_status_for_zero_amount_refdoc("Purchase Order") + if not self.is_return: + self.update_against_document_in_jv() + self.update_prevdoc_status() + self.update_billing_status_for_zero_amount_refdoc("Purchase Order") + self.update_project() def make_gl_entries(self): @@ -358,11 +363,12 @@ class PurchaseInvoice(BuyingController): make_gl_entries(gl_entries, cancel=(self.docstatus == 2)) def on_cancel(self): - from erpnext.accounts.utils import remove_against_link_from_jv - remove_against_link_from_jv(self.doctype, self.name, "against_voucher") + if not self.is_return: + from erpnext.accounts.utils import remove_against_link_from_jv + remove_against_link_from_jv(self.doctype, self.name, "against_voucher") - self.update_prevdoc_status() - self.update_billing_status_for_zero_amount_refdoc("Purchase Order") + self.update_prevdoc_status() + self.update_billing_status_for_zero_amount_refdoc("Purchase Order") self.make_gl_entries_on_cancel() self.update_project() @@ -403,3 +409,8 @@ def get_expense_account(doctype, txt, searchfield, start, page_len, filters): and tabAccount.%(key)s LIKE '%(txt)s' %(mcond)s""" % {'company': filters['company'], 'key': searchfield, 'txt': "%%%s%%" % frappe.db.escape(txt), 'mcond':get_match_cond(doctype)}) + +@frappe.whitelist() +def make_purchase_return(source_name, target_doc=None): + from erpnext.utilities.transaction_base import make_return_doc + return make_return_doc("Purchase Invoice", source_name, target_doc) \ No newline at end of file diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 5b2f3483e7..8daf3f667f 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -53,9 +53,6 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte frappe.set_route("query-report", "General Ledger"); }, "icon-table"); - // var percent_paid = cint(flt(doc.base_grand_total - doc.outstanding_amount) / flt(doc.base_grand_total) * 100); - // cur_frm.dashboard.add_progress(percent_paid + "% Paid", percent_paid); - if(cint(doc.update_stock)!=1) { // show Make Delivery Note button only if Sales Invoice is not created from Delivery Note var from_delivery_note = false; @@ -69,9 +66,12 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte } } - if(doc.outstanding_amount!=0) { + if(doc.outstanding_amount!=0 && !cint(doc.is_return)) { cur_frm.add_custom_button(__('Make Payment Entry'), cur_frm.cscript.make_bank_entry, "icon-money"); } + + cur_frm.add_custom_button(__('Make Sales Return'), this.make_sales_return, + frappe.boot.doctype_icons["Sales Invoice"]); } // Show buttons only when pos view is active @@ -205,8 +205,14 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte items_on_form_rendered: function() { erpnext.setup_serial_no(); + }, + + make_sales_return: function() { + frappe.model.open_mapped_doc({ + method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.make_sales_return", + frm: cur_frm + }) } - }); // for backward compatibility: combine new and previous states @@ -283,16 +289,6 @@ cur_frm.cscript.make_bank_entry = function() { }); } -cur_frm.fields_dict.debit_to.get_query = function(doc) { - return{ - filters: { - 'report_type': 'Balance Sheet', - 'is_group': 0, - 'company': doc.company - } - } -} - cur_frm.fields_dict.cash_bank_account.get_query = function(doc) { return { filters: [ @@ -399,4 +395,4 @@ cur_frm.set_query("debit_to", function(doc) { ['Account', 'account_type', '=', 'Receivable'] ] } -}); +}); \ No newline at end of file diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index 25dd3988f1..b983d99024 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -21,7 +21,7 @@ "no_copy": 1, "oldfieldname": "naming_series", "oldfieldtype": "Select", - "options": "SINV-", + "options": "SINV-\nSINV-RET-", "permlevel": 0, "print_hide": 1, "read_only": 0, @@ -169,6 +169,28 @@ "print_hide": 1, "read_only": 0 }, + { + "fieldname": "is_return", + "fieldtype": "Check", + "label": "Is Return", + "no_copy": 1, + "permlevel": 0, + "precision": "", + "print_hide": 1, + "read_only": 1 + }, + { + "depends_on": "is_return", + "fieldname": "return_against", + "fieldtype": "Link", + "label": "Return Against Sales Invoice", + "no_copy": 1, + "options": "Sales Invoice", + "permlevel": 0, + "precision": "", + "print_hide": 1, + "read_only": 1 + }, { "fieldname": "shipping_address_name", "fieldtype": "Link", @@ -1252,8 +1274,8 @@ ], "icon": "icon-file-text", "idx": 1, - "is_submittable": 1, - "modified": "2015-07-09 17:33:28.583808", + "is_submittable": 1, + "modified": "2015-07-17 13:29:36.922418", "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 829478df79..9129e1f7f0 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -80,14 +80,16 @@ class SalesInvoice(SellingController): self.check_prev_docstatus() - self.update_status_updater_args() - self.update_prevdoc_status() - self.update_billing_status_for_zero_amount_refdoc("Sales Order") - self.check_credit_limit() + if not self.is_return: + self.update_status_updater_args() + self.update_prevdoc_status() + self.update_billing_status_for_zero_amount_refdoc("Sales Order") + self.check_credit_limit() + # this sequence because outstanding may get -ve self.make_gl_entries() - if not cint(self.is_pos) == 1: + if not cint(self.is_pos) == 1 and not self.is_return: self.update_against_document_in_jv() self.update_time_log_batch(self.name) @@ -100,13 +102,15 @@ class SalesInvoice(SellingController): self.update_stock_ledger() self.check_stop_sales_order("sales_order") - + from erpnext.accounts.utils import remove_against_link_from_jv remove_against_link_from_jv(self.doctype, self.name, "against_invoice") - - self.update_status_updater_args() - self.update_prevdoc_status() - self.update_billing_status_for_zero_amount_refdoc("Sales Order") + + if not self.is_return: + self.update_status_updater_args() + self.update_prevdoc_status() + self.update_billing_status_for_zero_amount_refdoc("Sales Order") + self.validate_c_form_on_cancel() self.make_gl_entries_on_cancel() @@ -199,8 +203,9 @@ class SalesInvoice(SellingController): self.set_taxes() def get_advances(self): - super(SalesInvoice, self).get_advances(self.debit_to, "Customer", self.customer, - "Sales Invoice Advance", "advances", "credit", "sales_order") + if not self.is_return: + super(SalesInvoice, self).get_advances(self.debit_to, "Customer", self.customer, + "Sales Invoice Advance", "advances", "credit", "sales_order") def get_company_abbr(self): return frappe.db.sql("select abbr from tabCompany where name=%s", self.company)[0][0] @@ -285,6 +290,8 @@ class SalesInvoice(SellingController): def so_dn_required(self): """check in manage account if sales order / delivery note required or not.""" + if self.is_return: + return dic = {'Sales Order':'so_required','Delivery Note':'dn_required'} for i in dic: if frappe.db.get_value('Selling Settings', None, dic[i]) == 'Yes': @@ -419,13 +426,16 @@ class SalesInvoice(SellingController): def update_stock_ledger(self): sl_entries = [] for d in self.get_item_list(): - if frappe.db.get_value("Item", d.item_code, "is_stock_item") == "Yes" \ - and d.warehouse: + if frappe.db.get_value("Item", d.item_code, "is_stock_item") == "Yes" and d.warehouse: + incoming_rate = 0 + if cint(self.is_return) and self.return_against and self.docstatus==1: + incoming_rate = self.get_incoming_rate_for_sales_return(d.item_code, self.return_against) + sl_entries.append(self.get_sl_entries(d, { "actual_qty": -1*flt(d.qty), - "stock_uom": frappe.db.get_value("Item", d.item_code, "stock_uom") + "stock_uom": frappe.db.get_value("Item", d.item_code, "stock_uom"), + "incoming_rate": incoming_rate })) - self.make_sl_entries(sl_entries) def make_gl_entries(self, repost_future_gle=True): @@ -435,8 +445,7 @@ class SalesInvoice(SellingController): from erpnext.accounts.general_ledger import make_gl_entries # if POS and amount is written off, there's no outstanding and hence no need to update it - update_outstanding = cint(self.is_pos) and self.write_off_account \ - and 'No' or 'Yes' + update_outstanding = "No" if (cint(self.is_pos) or self.write_off_account) else "Yes" make_gl_entries(gl_entries, cancel=(self.docstatus == 2), update_outstanding=update_outstanding, merge_entries=False) @@ -484,7 +493,7 @@ class SalesInvoice(SellingController): "against": self.against_income_account, "debit": self.base_grand_total, "remarks": self.remarks, - "against_voucher": self.name, + "against_voucher": self.against_invoice if cint(self.is_return) else self.name, "against_voucher_type": self.doctype }) ) @@ -519,7 +528,6 @@ class SalesInvoice(SellingController): # expense account gl entries if cint(frappe.defaults.get_global_default("auto_accounting_for_stock")) \ and cint(self.update_stock): - gl_entries += super(SalesInvoice, self).get_gl_entries() def make_pos_gl_entries(self, gl_entries): @@ -533,7 +541,7 @@ class SalesInvoice(SellingController): "against": self.cash_bank_account, "credit": self.paid_amount, "remarks": self.remarks, - "against_voucher": self.name, + "against_voucher": self.against_invoice if cint(self.is_return) else self.name, "against_voucher_type": self.doctype, }) ) @@ -557,7 +565,7 @@ class SalesInvoice(SellingController): "against": self.write_off_account, "credit": self.write_off_amount, "remarks": self.remarks, - "against_voucher": self.name, + "against_voucher": self.against_invoice if cint(self.is_return) else self.name, "against_voucher_type": self.doctype, }) ) @@ -651,3 +659,9 @@ def make_delivery_note(source_name, target_doc=None): }, target_doc, set_missing_values) return doclist + + +@frappe.whitelist() +def make_sales_return(source_name, target_doc=None): + from erpnext.utilities.transaction_base import make_return_doc + return make_return_doc("Sales Invoice", source_name, target_doc) \ No newline at end of file diff --git a/erpnext/buying/doctype/purchase_common/purchase_common.js b/erpnext/buying/doctype/purchase_common/purchase_common.js index 1b7d20ae82..19ad9ab651 100644 --- a/erpnext/buying/doctype/purchase_common/purchase_common.js +++ b/erpnext/buying/doctype/purchase_common/purchase_common.js @@ -164,8 +164,10 @@ erpnext.buying.BuyingController = erpnext.TransactionController.extend({ frappe.model.round_floats_in(this.frm.doc, ["base_grand_total", "total_advance", "write_off_amount"]); this.frm.doc.total_amount_to_pay = flt(this.frm.doc.base_grand_total - this.frm.doc.write_off_amount, precision("total_amount_to_pay")); - this.frm.doc.outstanding_amount = flt(this.frm.doc.total_amount_to_pay - this.frm.doc.total_advance, - precision("outstanding_amount")); + if (!this.frm.doc.is_return) { + this.frm.doc.outstanding_amount = flt(this.frm.doc.total_amount_to_pay - this.frm.doc.total_advance, + precision("outstanding_amount")); + } } } }); diff --git a/erpnext/buying/doctype/purchase_common/purchase_common.py b/erpnext/buying/doctype/purchase_common/purchase_common.py index 476aa92f67..1bf6f8fe67 100644 --- a/erpnext/buying/doctype/purchase_common/purchase_common.py +++ b/erpnext/buying/doctype/purchase_common/purchase_common.py @@ -41,8 +41,7 @@ class PurchaseCommon(BuyingController): def validate_for_items(self, obj): items = [] for d in obj.get("items"): - # validation for valid qty - if flt(d.qty) < 0 or (d.parenttype != 'Purchase Receipt' and not flt(d.qty)): + if not d.qty: frappe.throw(_("Please enter quantity for Item {0}").format(d.item_code)) # udpate with latest quantities diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 98f240958f..d1ce3c6fdc 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -4,12 +4,15 @@ from __future__ import unicode_literals import frappe from frappe import _, throw -from frappe.utils import today, flt, cint +from frappe.utils import today, flt, cint, format_datetime, get_datetime from erpnext.setup.utils import get_company_currency, get_exchange_rate from erpnext.accounts.utils import get_fiscal_year, validate_fiscal_year from erpnext.utilities.transaction_base import TransactionBase from erpnext.controllers.recurring_document import convert_to_recurring, validate_recurring_document +class StockOverReturnError(frappe.ValidationError): pass + + class AccountsController(TransactionBase): def validate(self): if self.get("_action") and self._action != "update_after_submit": @@ -17,10 +20,14 @@ class AccountsController(TransactionBase): self.validate_date_with_fiscal_year() if self.meta.get_field("currency"): self.calculate_taxes_and_totals() - self.validate_value("base_grand_total", ">=", 0) + if not self.meta.get_field("is_return") or not self.is_return: + self.validate_value("base_grand_total", ">=", 0) + + self.validate_return_doc() self.set_total_in_words() - self.validate_due_date() + if not self.is_return: + self.validate_due_date() if self.meta.get_field("is_recurring"): validate_recurring_document(self) @@ -51,6 +58,94 @@ class AccountsController(TransactionBase): self.fiscal_year = get_fiscal_year(self.get(fieldname))[0] break + def validate_return_doc(self): + if not self.meta.get_field("is_return") or not self.is_return: + return + + self.validate_return_against() + self.validate_returned_items() + + def validate_return_against(self): + if not self.return_against: + frappe.throw(_("{0} is mandatory for Return").format(self.meta.get_label("return_against"))) + else: + filters = {"doctype": self.doctype, "docstatus": 1, "company": self.company} + if self.meta.get_field("customer"): + filters["customer"] = self.customer + elif self.meta.get_field("supplier"): + filters["supplier"] = self.supplier + + if not frappe.db.exists(filters): + frappe.throw(_("Invalid {0}: {1}") + .format(self.meta.get_label("return_against"), self.return_against)) + else: + ref_doc = frappe.get_doc(self.doctype, self.return_against) + + # validate posting date time + return_posting_datetime = "%s %s" % (self.posting_date, self.get("posting_time") or "00:00:00") + ref_posting_datetime = "%s %s" % (ref_doc.posting_date, ref_doc.get("posting_time") or "00:00:00") + + if get_datetime(return_posting_datetime) < get_datetime(ref_posting_datetime): + frappe.throw(_("Posting timestamp must be after {0}") + .format(datetime_in_user_format(ref_posting_datetime))) + + # validate same exchange rate + if self.conversion_rate != ref_doc.conversion_rate: + frappe.throw(_("Exchange Rate must be same as {0} {1} ({2})") + .format(self.doctype, self.return_against, ref_doc.conversion_rate)) + + # validate update stock + if self.doctype == "Sales Invoice" and self.update_stock \ + and not frappe.db.get_value("Sales Invoice", self.return_against, "update_stock"): + frappe.throw(_("'Update Stock' can not be checked because items are not delivered via {0}") + .format(self.return_against)) + + def validate_returned_items(self): + valid_items = frappe._dict() + for d in frappe.db.sql("""select item_code, sum(qty) as qty, rate from `tab{0} Item` + where parent = %s group by item_code""".format(self.doctype), self.return_against, as_dict=1): + valid_items.setdefault(d.item_code, d) + + already_returned_items = self.get_already_returned_items() + + items_returned = False + for d in self.get("items"): + if flt(d.qty) < 0: + if d.item_code not in valid_items: + frappe.throw(_("Row # {0}: Returned Item {1} does not exists in {2} {3}") + .format(d.idx, d.item_code, self.doctype, self.return_against)) + else: + ref = valid_items.get(d.item_code, frappe._dict()) + already_returned_qty = flt(already_returned_items.get(d.item_code)) + max_return_qty = flt(ref.qty) - already_returned_qty + + if already_returned_qty >= ref.qty: + frappe.throw(_("Item {0} has already been returned").format(d.item_code), StockOverReturnError) + elif abs(d.qty) > max_return_qty: + frappe.throw(_("Row # {0}: Cannot return more than {1} for Item {2}") + .format(d.idx, ref.qty, d.item_code), StockOverReturnError) + elif flt(d.rate) != ref.rate: + frappe.throw(_("Row # {0}: Rate must be same as {1} {2}") + .format(d.idx, self.doctype, self.return_against)) + + + items_returned = True + + if not items_returned: + frappe.throw(_("Atleast one item should be entered with negative quantity in return document")) + + def get_already_returned_items(self): + return frappe._dict(frappe.db.sql(""" + select + child.item_code, sum(abs(child.qty)) as qty + from + `tab{0} Item` child, `tab{1}` par + where + child.parent = par.name and par.docstatus = 1 + and ifnull(par.is_return, 0) = 1 and par.return_against = %s and child.qty < 0 + group by item_code + """.format(self.doctype, self.doctype), self.return_against)) + def calculate_taxes_and_totals(self): from erpnext.controllers.taxes_and_totals import calculate_taxes_and_totals calculate_taxes_and_totals(self) diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 9867973258..0b60473b8b 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -26,8 +26,7 @@ class BuyingController(StockController): def validate(self): super(BuyingController, self).validate() if getattr(self, "supplier", None) and not self.supplier_name: - self.supplier_name = frappe.db.get_value("Supplier", - self.supplier, "supplier_name") + self.supplier_name = frappe.db.get_value("Supplier", self.supplier, "supplier_name") self.is_item_table_empty() self.set_qty_as_per_stock_uom() self.validate_stock_or_nonstock_items() diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index b2a9f0317f..01ef605b63 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -175,7 +175,7 @@ class SellingController(StockController): if flt(d.qty) > flt(d.delivered_qty): reserved_qty_for_main_item = flt(d.qty) - flt(d.delivered_qty) - elif self.doctype == "Delivery Note" and d.against_sales_order: + elif self.doctype == "Delivery Note" and d.against_sales_order and not self.is_return: # if SO qty is 10 and there is tolerance of 20%, then it will allow DN of 12. # But in this case reserved qty should only be reduced by 10 and not 12 @@ -211,7 +211,7 @@ class SellingController(StockController): 'qty': d.qty, 'reserved_qty': reserved_qty_for_main_item, 'uom': d.stock_uom, - 'stock_uom': d.stock_uom, + 'stock_uom': d.stock_uom, 'batch_no': cstr(d.get("batch_no")).strip(), 'serial_no': cstr(d.get("serial_no")).strip(), 'name': d.name diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 6678007489..19440e24a7 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -216,6 +216,17 @@ class StockController(AccountsController): tuple(item_codes)) return serialized_items + + def get_incoming_rate_for_sales_return(self, item_code, against_document): + incoming_rate = 0.0 + if against_document and item_code: + incoming_rate = frappe.db.sql("""select abs(ifnull(stock_value_difference, 0) / actual_qty) + from `tabStock Ledger Entry` + where voucher_type = %s and voucher_no = %s and item_code = %s limit 1""", + (self.doctype, against_document, item_code)) + incoming_rate = incoming_rate[0][0] if incoming_rate else 0.0 + + return incoming_rate def update_gl_entries_after(posting_date, posting_time, for_warehouses=None, for_items=None, warehouse_account=None): diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index e77a9a6619..f22b62488b 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -77,6 +77,9 @@ class calculate_taxes_and_totals(object): if not self.discount_amount_applied: validate_taxes_and_charges(tax) validate_inclusive_tax(tax, self.doc) + + if self.doc.meta.get_field("is_return") and self.doc.is_return and tax.charge_type == "Actual": + tax.tax_amount = -1 * tax.tax_amount tax.item_wise_tax_detail = {} tax_fields = ["total", "tax_amount_after_discount_amount", @@ -396,13 +399,15 @@ class calculate_taxes_and_totals(object): # total_advance is only for non POS Invoice if self.doc.doctype == "Sales Invoice": - self.doc.round_floats_in(self.doc, ["base_grand_total", "total_advance", "write_off_amount", "paid_amount"]) - total_amount_to_pay = self.doc.base_grand_total - self.doc.write_off_amount - self.doc.outstanding_amount = flt(total_amount_to_pay - self.doc.total_advance - self.doc.paid_amount, - self.doc.precision("outstanding_amount")) + if not self.doc.is_return: + self.doc.round_floats_in(self.doc, ["base_grand_total", "total_advance", "write_off_amount", "paid_amount"]) + total_amount_to_pay = self.doc.base_grand_total - self.doc.write_off_amount + self.doc.outstanding_amount = flt(total_amount_to_pay - self.doc.total_advance - self.doc.paid_amount, + self.doc.precision("outstanding_amount")) else: self.doc.round_floats_in(self.doc, ["total_advance", "write_off_amount"]) self.doc.total_amount_to_pay = flt(self.doc.base_grand_total - self.doc.write_off_amount, self.doc.precision("total_amount_to_pay")) - self.doc.outstanding_amount = flt(self.doc.total_amount_to_pay - self.doc.total_advance, - self.doc.precision("outstanding_amount")) + if not self.doc.is_return: + self.doc.outstanding_amount = flt(self.doc.total_amount_to_pay - self.doc.total_advance, + self.doc.precision("outstanding_amount")) diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 4a26d6d39c..07c2d56cf1 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -13,8 +13,9 @@ erpnext.taxes_and_totals = erpnext.stock.StockController.extend({ this.apply_discount_amount(); // Advance calculation applicable to Sales /Purchase Invoice - if(in_list(["Sales Invoice", "Purchase Invoice"], this.frm.doc.doctype) && this.frm.doc.docstatus < 2) { - this.calculate_total_advance(update_paid_amount); + if(in_list(["Sales Invoice", "Purchase Invoice"], this.frm.doc.doctype) + && this.frm.doc.docstatus < 2 && !this.frm.doc.is_return) { + this.calculate_total_advance(update_paid_amount); } // Sales person's commission @@ -93,6 +94,10 @@ erpnext.taxes_and_totals = erpnext.stock.StockController.extend({ tax_fields = ["total", "tax_amount_after_discount_amount", "tax_amount_for_current_item", "grand_total_for_current_item", "tax_fraction_for_current_item", "grand_total_fraction_for_current_item"] + + if (frappe.meta.get_docfield(me.frm.doc.doctype, "is_return") && me.frm.doc.is_return + && tax.charge_type == "Actual") + tax.tax_amount = -1 * tax.tax_amount; if (cstr(tax.charge_type) != "Actual" && !(me.discount_amount_applied && me.frm.doc.apply_discount_on=="Grand Total")) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 0a75dad09e..01e5781a4e 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -46,6 +46,23 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ } }); } + + if(this.frm.fields_dict["return_against"]) { + this.frm.set_query("return_against", function(doc) { + var filters = { + "docstatus": 1, + "is_return": 0, + "company": doc.company + }; + if (me.frm.fields_dict["customer"] && doc.customer) filters["customer"] = doc.customer; + if (me.frm.fields_dict["supplier"] && doc.supplier) filters["supplier"] = doc.supplier; + + return { + filters: filters + } + }); + } + }, onload_post_render: function() { @@ -354,7 +371,8 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ plc_conversion_rate: function() { if(this.frm.doc.price_list_currency === this.get_company_currency()) { this.frm.set_value("plc_conversion_rate", 1.0); - } else if(this.frm.doc.price_list_currency === this.frm.doc.currency && this.frm.doc.plc_conversion_rate && cint(this.frm.doc.plc_conversion_rate) != 1 && + } else if(this.frm.doc.price_list_currency === this.frm.doc.currency + && this.frm.doc.plc_conversion_rate && cint(this.frm.doc.plc_conversion_rate) != 1 && cint(this.frm.doc.plc_conversion_rate) != cint(this.frm.doc.conversion_rate)) { this.frm.set_value("conversion_rate", this.frm.doc.plc_conversion_rate); } diff --git a/erpnext/selling/sales_common.js b/erpnext/selling/sales_common.js index f3cd8a7833..e8d8fd5f7a 100644 --- a/erpnext/selling/sales_common.js +++ b/erpnext/selling/sales_common.js @@ -210,7 +210,7 @@ erpnext.selling.SellingController = erpnext.TransactionController.extend({ // NOTE: // paid_amount and write_off_amount is only for POS Invoice // total_advance is only for non POS Invoice - if(this.frm.doc.doctype == "Sales Invoice" && this.frm.doc.docstatus==0) { + if(this.frm.doc.doctype == "Sales Invoice" && this.frm.doc.docstatus==0 && !this.frm.doc.is_return) { frappe.model.round_floats_in(this.frm.doc, ["base_grand_total", "total_advance", "write_off_amount", "paid_amount"]); var total_amount_to_pay = this.frm.doc.base_grand_total - this.frm.doc.write_off_amount diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.js b/erpnext/stock/doctype/delivery_note/delivery_note.js index 631009fa08..26adf4e232 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.js +++ b/erpnext/stock/doctype/delivery_note/delivery_note.js @@ -24,7 +24,9 @@ erpnext.stock.DeliveryNoteController = erpnext.selling.SellingController.extend( cur_frm.add_custom_button(__('Make Installation Note'), this.make_installation_note); if (doc.docstatus==1) { - + cur_frm.add_custom_button(__('Make Sales Return'), this.make_sales_return, + frappe.boot.doctype_icons["Delivery Note"]); + this.show_stock_ledger(); this.show_general_ledger(); } @@ -73,6 +75,13 @@ erpnext.stock.DeliveryNoteController = erpnext.selling.SellingController.extend( frm: cur_frm }); }, + + make_sales_return: function() { + frappe.model.open_mapped_doc({ + method: "erpnext.stock.doctype.delivery_note.delivery_note.make_sales_return", + frm: cur_frm + }) + }, tc_name: function() { this.get_terms(); diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json index 72a72278a0..89da4806ba 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.json +++ b/erpnext/stock/doctype/delivery_note/delivery_note.json @@ -29,7 +29,7 @@ "no_copy": 1, "oldfieldname": "naming_series", "oldfieldtype": "Select", - "options": "DN-", + "options": "DN-\nDN-RET-", "permlevel": 0, "print_hide": 1, "read_only": 0, @@ -205,6 +205,28 @@ "read_only": 1, "width": "100px" }, + { + "fieldname": "is_return", + "fieldtype": "Check", + "label": "Is Return", + "no_copy": 1, + "permlevel": 0, + "precision": "", + "print_hide": 1, + "read_only": 1 + }, + { + "depends_on": "is_return", + "fieldname": "return_against", + "fieldtype": "Link", + "label": "Return Against Delivery Note", + "no_copy": 1, + "options": "Delivery Note", + "permlevel": 0, + "precision": "", + "print_hide": 1, + "read_only": 1 + }, { "fieldname": "cusrrency_and_price_list", "fieldtype": "Section Break", @@ -1070,7 +1092,7 @@ "idx": 1, "in_create": 0, "is_submittable": 1, - "modified": "2015-07-13 05:28:29.814096", + "modified": "2015-07-17 13:29:28.019506", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note", diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 90a8a6c720..cd501dad88 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -84,7 +84,7 @@ class DeliveryNote(SellingController): def so_required(self): """check in manage account if sales order required or not""" - if frappe.db.get_value("Selling Settings", None, 'so_required') == 'Yes': + if not self.is_return and frappe.db.get_value("Selling Settings", None, 'so_required') == 'Yes': for d in self.get('items'): if not d.against_sales_order: frappe.throw(_("Sales Order required for Item {0}").format(d.item_code)) @@ -175,17 +175,15 @@ class DeliveryNote(SellingController): # Check for Approving Authority frappe.get_doc('Authorization Control').validate_approving_authority(self.doctype, self.company, self.base_grand_total, self) - # update delivered qty in sales order - self.update_prevdoc_status() + if not self.is_return: + # update delivered qty in sales order + self.update_prevdoc_status() - self.check_credit_limit() + self.check_credit_limit() - # create stock ledger entry self.update_stock_ledger() - self.make_gl_entries() - # set DN status frappe.db.set(self, 'status', 'Submitted') @@ -193,7 +191,8 @@ class DeliveryNote(SellingController): self.check_stop_sales_order("against_sales_order") self.check_next_docstatus() - self.update_prevdoc_status() + if not self.is_return: + self.update_prevdoc_status() self.update_stock_ledger() @@ -251,9 +250,14 @@ class DeliveryNote(SellingController): if frappe.db.get_value("Item", d.item_code, "is_stock_item") == "Yes" \ and d.warehouse and flt(d['qty']): self.update_reserved_qty(d) - + + incoming_rate = 0 + if cint(self.is_return) and self.return_against and self.docstatus==1: + incoming_rate = self.get_incoming_rate_for_sales_return(d.item_code, self.return_against) + sl_entries.append(self.get_sl_entries(d, { "actual_qty": -1*flt(d['qty']), + incoming_rate: incoming_rate })) self.make_sl_entries(sl_entries) @@ -387,3 +391,9 @@ def make_packing_slip(source_name, target_doc=None): }, target_doc) return doclist + + +@frappe.whitelist() +def make_sales_return(source_name, target_doc=None): + from erpnext.utilities.transaction_base import make_return_doc + return make_return_doc("Delivery Note", source_name, target_doc) \ No newline at end of file diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js index fe41b4fb93..727d38ec2c 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js @@ -34,6 +34,9 @@ erpnext.stock.PurchaseReceiptController = erpnext.buying.BuyingController.extend cur_frm.add_custom_button(__('Make Purchase Invoice'), this.make_purchase_invoice, frappe.boot.doctype_icons["Purchase Invoice"]); } + + cur_frm.add_custom_button(__('Make Purchase Return'), this.make_purchase_return, + frappe.boot.doctype_icons["Purchase Receipt"]); this.show_stock_ledger(); this.show_general_ledger(); @@ -105,6 +108,13 @@ erpnext.stock.PurchaseReceiptController = erpnext.buying.BuyingController.extend frm: cur_frm }) }, + + make_purchase_return: function() { + frappe.model.open_mapped_doc({ + method: "erpnext.stock.doctype.purchase_receipt.purchase_receipt.make_purchase_return", + frm: cur_frm + }) + }, tc_name: function() { this.get_terms(); diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json index 6e344b61e0..c44923abb1 100755 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json @@ -21,13 +21,14 @@ "width": "50%" }, { + "default": "", "fieldname": "naming_series", "fieldtype": "Select", "label": "Series", "no_copy": 1, "oldfieldname": "naming_series", "oldfieldtype": "Select", - "options": "PREC-", + "options": "PREC-\nPREC-RET-", "permlevel": 0, "print_hide": 1, "reqd": 1 @@ -130,6 +131,28 @@ "search_index": 0, "width": "100px" }, + { + "fieldname": "is_return", + "fieldtype": "Check", + "label": "Is Return", + "no_copy": 1, + "permlevel": 0, + "precision": "", + "print_hide": 1, + "read_only": 1 + }, + { + "depends_on": "is_return", + "fieldname": "return_against", + "fieldtype": "Link", + "label": "Return Against Purchase Receipt", + "no_copy": 1, + "options": "Purchase Receipt", + "permlevel": 0, + "precision": "", + "print_hide": 1, + "read_only": 1 + }, { "fieldname": "currency_and_price_list", "fieldtype": "Section Break", @@ -854,7 +877,7 @@ "icon": "icon-truck", "idx": 1, "is_submittable": 1, - "modified": "2015-07-13 05:28:27.389559", + "modified": "2015-07-17 13:29:10.298448", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt", diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index e78288908d..31a2f50a12 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -44,6 +44,7 @@ class PurchaseReceipt(BuyingController): self.set_status() self.po_required() self.validate_with_previous_doc() + self.validate_purchase_return() self.validate_rejected_warehouse() self.validate_accepted_rejected_qty() self.validate_inspection() @@ -60,12 +61,21 @@ class PurchaseReceipt(BuyingController): self.set_landed_cost_voucher_amount() self.update_valuation_rate("items") + def set_landed_cost_voucher_amount(self): for d in self.get("items"): lc_voucher_amount = frappe.db.sql("""select sum(ifnull(applicable_charges, 0)) from `tabLanded Cost Item` where docstatus = 1 and purchase_receipt_item = %s""", d.name) d.landed_cost_voucher_amount = lc_voucher_amount[0][0] if lc_voucher_amount else 0.0 + + def validate_purchase_return(self): + for d in self.get("items"): + print flt(d.rejected_qty) + if self.is_return and flt(d.rejected_qty) != 0: + frappe.throw(_("Row #{0}: Rejected Qty can not be entered in Purchase Return").format(d.idx)) + + # validate rate with ref PR def validate_rejected_warehouse(self): for d in self.get("items"): @@ -108,7 +118,7 @@ class PurchaseReceipt(BuyingController): self.validate_rate_with_reference_doc([["Purchase Order", "prevdoc_docname", "prevdoc_detail_docname"]]) def po_required(self): - if frappe.db.get_value("Buying Settings", None, "po_required") == 'Yes': + if not self.is_return and frappe.db.get_value("Buying Settings", None, "po_required") == 'Yes': for d in self.get('items'): if not d.prevdoc_docname: frappe.throw(_("Purchase Order number required for Item {0}").format(d.item_code)) @@ -123,11 +133,20 @@ class PurchaseReceipt(BuyingController): if pr_qty: val_rate_db_precision = 6 if cint(self.precision("valuation_rate", d)) <= 6 else 9 - sl_entries.append(self.get_sl_entries(d, { + rate = flt(d.valuation_rate, val_rate_db_precision) + sle = self.get_sl_entries(d, { "actual_qty": flt(pr_qty), - "serial_no": cstr(d.serial_no).strip(), - "incoming_rate": flt(d.valuation_rate, val_rate_db_precision) - })) + "serial_no": cstr(d.serial_no).strip() + }) + if self.is_return: + sle.update({ + "outgoing_rate": rate + }) + else: + sle.update({ + "incoming_rate": rate + }) + sl_entries.append(sle) if flt(d.rejected_qty) > 0: sl_entries.append(self.get_sl_entries(d, { @@ -176,7 +195,6 @@ class PurchaseReceipt(BuyingController): "item_code": d.rm_item_code, "warehouse": self.supplier_warehouse, "actual_qty": -1*flt(d.consumed_qty), - "incoming_rate": 0 })) def validate_inspection(self): @@ -207,17 +225,16 @@ class PurchaseReceipt(BuyingController): # Set status as Submitted frappe.db.set(self, 'status', 'Submitted') - self.update_prevdoc_status() - - self.update_ordered_qty() + if not self.is_return: + self.update_prevdoc_status() + self.update_ordered_qty() + purchase_controller.update_last_purchase_rate(self, 1) self.update_stock_ledger() from erpnext.stock.doctype.serial_no.serial_no import update_serial_nos_after_submit update_serial_nos_after_submit(self, "items") - purchase_controller.update_last_purchase_rate(self, 1) - self.make_gl_entries() def check_next_docstatus(self): @@ -244,12 +261,13 @@ class PurchaseReceipt(BuyingController): self.update_stock_ledger() - self.update_prevdoc_status() + if not self.is_return: + self.update_prevdoc_status() - # Must be called after updating received qty in PO - self.update_ordered_qty() + # Must be called after updating received qty in PO + self.update_ordered_qty() - pc_obj.update_last_purchase_rate(self, 0) + pc_obj.update_last_purchase_rate(self, 0) self.make_gl_entries_on_cancel() @@ -417,7 +435,7 @@ def make_purchase_invoice(source_name, target_doc=None): "doctype": "Purchase Invoice", "validation": { "docstatus": ["=", 1], - } + }, }, "Purchase Receipt Item": { "doctype": "Purchase Invoice Item", @@ -449,3 +467,8 @@ def get_invoiced_qty_map(purchase_receipt): invoiced_qty_map[pr_detail] += qty return invoiced_qty_map + +@frappe.whitelist() +def make_purchase_return(source_name, target_doc=None): + from erpnext.utilities.transaction_base import make_return_doc + return make_return_doc("Purchase Receipt", source_name, target_doc) \ No newline at end of file diff --git a/erpnext/utilities/transaction_base.py b/erpnext/utilities/transaction_base.py index 50b0319d3f..fd2aaabca8 100644 --- a/erpnext/utilities/transaction_base.py +++ b/erpnext/utilities/transaction_base.py @@ -128,3 +128,36 @@ def validate_uom_is_integer(doc, uom_field, qty_fields, child_dt=None): if d.get(f): if cint(d.get(f))!=d.get(f): frappe.throw(_("Quantity cannot be a fraction in row {0}").format(d.idx), UOMMustBeIntegerError) + +def make_return_doc(doctype, source_name, target_doc=None): + from frappe.model.mapper import get_mapped_doc + def set_missing_values(source, target): + doc = frappe.get_doc(target) + doc.is_return = 1 + doc.return_against = source.name + doc.ignore_pricing_rule = 1 + doc.run_method("calculate_taxes_and_totals") + + def update_item(source_doc, target_doc, source_parent): + target_doc.qty = -1* source_doc.qty + if doctype == "Purchase Receipt": + target_doc.received_qty = -1* source_doc.qty + elif doctype == "Purchase Invoice": + target_doc.purchase_receipt = source_doc.purchase_receipt + target_doc.pr_detail = source_doc.pr_detail + + doclist = get_mapped_doc(doctype, source_name, { + doctype: { + "doctype": doctype, + + "validation": { + "docstatus": ["=", 1], + } + }, + doctype +" Item": { + "doctype": doctype + " Item", + "postprocess": update_item + }, + }, target_doc, set_missing_values) + + return doclist From 623ed576632bb9f6556ed49061e55475ecdf56af Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 17 Jul 2015 16:28:10 +0530 Subject: [PATCH 29/40] Removed Sales/Purchase Return option from Stock Entry --- erpnext/stock/doctype/serial_no/serial_no.py | 9 +- .../stock/doctype/stock_entry/stock_entry.js | 199 +------ .../doctype/stock_entry/stock_entry.json | 8 +- .../stock/doctype/stock_entry/stock_entry.py | 372 +------------ .../doctype/stock_entry/test_stock_entry.py | 512 +++++++++--------- 5 files changed, 286 insertions(+), 814 deletions(-) diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py index bac544194a..452182198c 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.py +++ b/erpnext/stock/doctype/serial_no/serial_no.py @@ -81,20 +81,19 @@ class SerialNo(StockController): def set_status(self, last_sle): if last_sle: if last_sle.voucher_type == "Stock Entry": - document_type = frappe.db.get_value("Stock Entry", last_sle.voucher_no, - "purpose") + document_type = frappe.db.get_value("Stock Entry", last_sle.voucher_no, "purpose") else: document_type = last_sle.voucher_type if last_sle.actual_qty > 0: - if document_type == "Sales Return": + if document_type in ("Delivery Note", "Sales Invoice", "Sales Return"): self.status = "Sales Returned" else: self.status = "Available" else: - if document_type == "Purchase Return": + if document_type in ("Purchase Receipt", "Purchase Invoice", "Purchase Return"): self.status = "Purchase Returned" - elif last_sle.voucher_type in ("Delivery Note", "Sales Invoice"): + elif document_type in ("Delivery Note", "Sales Invoice"): self.status = "Delivered" else: self.status = "Not Available" diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 6958ea03ca..8526117223 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -7,20 +7,7 @@ frappe.provide("erpnext.stock"); erpnext.stock.StockEntry = erpnext.stock.StockController.extend({ setup: function() { var me = this; - - this.frm.fields_dict.delivery_note_no.get_query = function() { - return { query: "erpnext.stock.doctype.stock_entry.stock_entry.query_sales_return_doc" }; - }; - - this.frm.fields_dict.sales_invoice_no.get_query = - this.frm.fields_dict.delivery_note_no.get_query; - - this.frm.fields_dict.purchase_receipt_no.get_query = function() { - return { - filters:{ 'docstatus': 1 } - }; - }; - + this.frm.fields_dict.bom_no.get_query = function() { return { filters:{ 'docstatus': 1 } @@ -28,20 +15,7 @@ erpnext.stock.StockEntry = erpnext.stock.StockController.extend({ }; this.frm.fields_dict.items.grid.get_field('item_code').get_query = function() { - if(in_list(["Sales Return", "Purchase Return"], me.frm.doc.purpose) && - me.get_doctype_docname()) { - return { - query: "erpnext.stock.doctype.stock_entry.stock_entry.query_return_item", - filters: { - purpose: me.frm.doc.purpose, - delivery_note_no: me.frm.doc.delivery_note_no, - sales_invoice_no: me.frm.doc.sales_invoice_no, - purchase_receipt_no: me.frm.doc.purchase_receipt_no - } - }; - } else { - return erpnext.queries.item({is_stock_item: "Yes"}); - } + return erpnext.queries.item({is_stock_item: "Yes"}); }; this.frm.set_query("purchase_order", function() { @@ -84,19 +58,6 @@ erpnext.stock.StockEntry = erpnext.stock.StockController.extend({ this.toggle_enable_bom(); this.show_stock_ledger(); this.show_general_ledger(); - - if(this.frm.doc.docstatus === 1 && frappe.boot.user.can_create.indexOf("Journal Entry")!==-1 - && this.frm.doc.__onload.credit_debit_note_exists == 0 ) { - if(this.frm.doc.purpose === "Sales Return") { - this.frm.add_custom_button(__("Make Credit Note"), - function() { me.make_return_jv(); }, frappe.boot.doctype_icons["Journal Entry"]); - this.add_excise_button(); - } else if(this.frm.doc.purpose === "Purchase Return") { - this.frm.add_custom_button(__("Make Debit Note"), - function() { me.make_return_jv(); }, frappe.boot.doctype_icons["Journal Entry"]); - this.add_excise_button(); - } - } }, on_submit: function() { @@ -111,15 +72,10 @@ erpnext.stock.StockEntry = erpnext.stock.StockController.extend({ var me = this; if(cint(frappe.defaults.get_default("auto_accounting_for_stock")) && this.frm.doc.company) { - var account_for = "stock_adjustment_account"; - - if (this.frm.doc.purpose == "Purchase Return") - account_for = "stock_received_but_not_billed"; - return this.frm.call({ method: "erpnext.accounts.utils.get_company_default", args: { - "fieldname": account_for, + "fieldname": "stock_adjustment_account", "company": this.frm.doc.company }, callback: function(r) { @@ -192,35 +148,6 @@ erpnext.stock.StockEntry = erpnext.stock.StockController.extend({ this.frm.toggle_enable("bom_no", !in_list(["Manufacture", "Material Transfer for Manufacture"], this.frm.doc.purpose)); }, - get_doctype_docname: function() { - if(this.frm.doc.purpose === "Sales Return") { - if(this.frm.doc.delivery_note_no && this.frm.doc.sales_invoice_no) { - // both specified - msgprint(__("You can not enter both Delivery Note No and Sales Invoice No. Please enter any one.")); - - } else if(!(this.frm.doc.delivery_note_no || this.frm.doc.sales_invoice_no)) { - // none specified - msgprint(__("Please enter Delivery Note No or Sales Invoice No to proceed")); - - } else if(this.frm.doc.delivery_note_no) { - return {doctype: "Delivery Note", docname: this.frm.doc.delivery_note_no}; - - } else if(this.frm.doc.sales_invoice_no) { - return {doctype: "Sales Invoice", docname: this.frm.doc.sales_invoice_no}; - - } - } else if(this.frm.doc.purpose === "Purchase Return") { - if(this.frm.doc.purchase_receipt_no) { - return {doctype: "Purchase Receipt", docname: this.frm.doc.purchase_receipt_no}; - - } else { - // not specified - msgprint(__("Please enter Purchase Receipt No to proceed")); - - } - } - }, - add_excise_button: function() { if(frappe.boot.sysdefaults.country === "India") this.frm.add_custom_button(__("Make Excise Invoice"), function() { @@ -231,37 +158,16 @@ erpnext.stock.StockEntry = erpnext.stock.StockController.extend({ }, frappe.boot.doctype_icons["Journal Entry"], "btn-default"); }, - make_return_jv: function() { - if(this.get_doctype_docname()) { - return this.frm.call({ - method: "make_return_jv", - args: { - stock_entry: this.frm.doc.name - }, - callback: function(r) { - if(!r.exc) { - var doclist = frappe.model.sync(r.message); - frappe.set_route("Form", doclist[0].doctype, doclist[0].name); - - } - } - }); - } - }, - items_add: function(doc, cdt, cdn) { var row = frappe.get_doc(cdt, cdn); - this.frm.script_manager.copy_from_first_row("items", row, - ["expense_account", "cost_center"]); + this.frm.script_manager.copy_from_first_row("items", row, ["expense_account", "cost_center"]); if(!row.s_warehouse) row.s_warehouse = this.frm.doc.from_warehouse; if(!row.t_warehouse) row.t_warehouse = this.frm.doc.to_warehouse; }, - source_mandatory: ["Material Issue", "Material Transfer", "Purchase Return", "Subcontract", - "Material Transfer for Manufacture"], - target_mandatory: ["Material Receipt", "Material Transfer", "Sales Return", "Subcontract", - "Material Transfer for Manufacture"], + source_mandatory: ["Material Issue", "Material Transfer", "Subcontract", "Material Transfer for Manufacture"], + target_mandatory: ["Material Receipt", "Material Transfer", "Subcontract", "Material Transfer for Manufacture"], from_warehouse: function(doc) { var me = this; @@ -295,92 +201,21 @@ erpnext.stock.StockEntry = erpnext.stock.StockController.extend({ items_on_form_rendered: function(doc, grid_row) { erpnext.setup_serial_no(); - }, - - customer: function() { - this.get_party_details({ - party: this.frm.doc.customer, - party_type:"Customer", - doctype: this.frm.doc.doctype - }); - }, - - supplier: function() { - this.get_party_details({ - party: this.frm.doc.supplier, - party_type:"Supplier", - doctype: this.frm.doc.doctype - }); - }, - - get_party_details: function(args) { - var me = this; - frappe.call({ - method: "erpnext.accounts.party.get_party_details", - args: args, - callback: function(r) { - if(r.message) { - me.frm.set_value({ - "customer_name": r.message["customer_name"], - "customer_address": r.message["address_display"] - }); - } - } - }); - }, - - delivery_note_no: function() { - this.get_party_details_from_against_voucher({ - ref_dt: "Delivery Note", - ref_dn: this.frm.doc.delivery_note_no - }) - }, - - sales_invoice_no: function() { - this.get_party_details_from_against_voucher({ - ref_dt: "Sales Invoice", - ref_dn: this.frm.doc.sales_invoice_no - }) - }, - - purchase_receipt_no: function() { - this.get_party_details_from_against_voucher({ - ref_dt: "Purchase Receipt", - ref_dn: this.frm.doc.purchase_receipt_no - }) - }, - - get_party_details_from_against_voucher: function(args) { - return this.frm.call({ - method: "erpnext.stock.doctype.stock_entry.stock_entry.get_party_details", - args: args, - }) } - }); cur_frm.script_manager.make(erpnext.stock.StockEntry); cur_frm.cscript.toggle_related_fields = function(doc) { - disable_from_warehouse = inList(["Material Receipt", "Sales Return"], doc.purpose); - disable_to_warehouse = inList(["Material Issue", "Purchase Return"], doc.purpose); + cur_frm.toggle_enable("from_warehouse", doc.purpose!='Material Receipt'); + cur_frm.toggle_enable("to_warehouse", doc.purpose!='Material Issue'); - cur_frm.toggle_enable("from_warehouse", !disable_from_warehouse); - cur_frm.toggle_enable("to_warehouse", !disable_to_warehouse); - - cur_frm.fields_dict["items"].grid.set_column_disp("s_warehouse", !disable_from_warehouse); - cur_frm.fields_dict["items"].grid.set_column_disp("t_warehouse", !disable_to_warehouse); + cur_frm.fields_dict["items"].grid.set_column_disp("s_warehouse", doc.purpose!='Material Receipt'); + cur_frm.fields_dict["items"].grid.set_column_disp("t_warehouse", doc.purpose!='Material Issue'); cur_frm.cscript.toggle_enable_bom(); - if(doc.purpose == 'Purchase Return') { - doc.customer = doc.customer_name = doc.customer_address = - doc.delivery_note_no = doc.sales_invoice_no = null; - doc.bom_no = doc.production_order = doc.fg_completed_qty = null; - } else if(doc.purpose == 'Sales Return') { - doc.supplier=doc.supplier_name = doc.supplier_address = doc.purchase_receipt_no=null; - doc.bom_no = doc.production_order = doc.fg_completed_qty = null; - } else if (doc.purpose == 'Subcontract') { + if (doc.purpose == 'Subcontract') { doc.customer = doc.customer_name = doc.customer_address = doc.delivery_note_no = doc.sales_invoice_no = null; } else { @@ -388,7 +223,7 @@ cur_frm.cscript.toggle_related_fields = function(doc) { doc.delivery_note_no = doc.sales_invoice_no = doc.supplier = doc.supplier_name = doc.supplier_address = doc.purchase_receipt_no = null; } - if(in_list(["Material Receipt", "Sales Return", "Purchase Return"], doc.purpose)) { + if(doc.purpose == "Material Receipt") { cur_frm.set_value("from_bom", 0); } } @@ -505,8 +340,6 @@ cur_frm.cscript.uom = function(doc, cdt, cdn) { } cur_frm.cscript.validate = function(doc, cdt, cdn) { - if($.inArray(cur_frm.doc.purpose, ["Purchase Return", "Sales Return"])!==-1) - validated = cur_frm.cscript.get_doctype_docname() ? true : false; cur_frm.cscript.validate_items(doc); } @@ -526,14 +359,6 @@ cur_frm.cscript.cost_center = function(doc, cdt, cdn) { erpnext.utils.copy_value_in_all_row(doc, cdt, cdn, "items", "cost_center"); } -cur_frm.fields_dict.customer.get_query = function(doc, cdt, cdn) { - return { query: "erpnext.controllers.queries.customer_query" } -} - -cur_frm.fields_dict.supplier.get_query = function(doc, cdt, cdn) { - return { query: "erpnext.controllers.queries.supplier_query" } -} - cur_frm.cscript.company = function(doc, cdt, cdn) { if(doc.company) { erpnext.get_fiscal_year(doc.company, doc.posting_date, function() { diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.json b/erpnext/stock/doctype/stock_entry/stock_entry.json index 06dec5808b..ca84db9e31 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.json +++ b/erpnext/stock/doctype/stock_entry/stock_entry.json @@ -54,7 +54,7 @@ "no_copy": 0, "oldfieldname": "purpose", "oldfieldtype": "Select", - "options": "Material Issue\nMaterial Receipt\nMaterial Transfer\nMaterial Transfer for Manufacture\nManufacture\nRepack\nSubcontract\nSales Return\nPurchase Return", + "options": "Material Issue\nMaterial Receipt\nMaterial Transfer\nMaterial Transfer for Manufacture\nManufacture\nRepack\nSubcontract", "permlevel": 0, "print_hide": 0, "read_only": 0, @@ -678,7 +678,7 @@ "is_submittable": 1, "issingle": 0, "max_attachments": 0, - "modified": "2015-07-13 05:28:26.085266", + "modified": "2015-07-17 15:41:00.980883", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry", @@ -695,7 +695,7 @@ "print": 1, "read": 1, "report": 1, - "role": "Stock User", + "role": "Material User", "share": 1, "submit": 1, "write": 1 @@ -741,7 +741,7 @@ "print": 1, "read": 1, "report": 1, - "role": "Stock Manager", + "role": "Material Manager", "share": 1, "submit": 1, "write": 1 diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index fb1ec3d17b..78e1038292 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -4,10 +4,8 @@ from __future__ import unicode_literals import frappe import frappe.defaults - -from frappe.utils import cstr, cint, flt, comma_or, get_datetime, getdate - from frappe import _ +from frappe.utils import cstr, cint, flt, comma_or, get_datetime, getdate from erpnext.stock.utils import get_incoming_rate from erpnext.stock.stock_ledger import get_previous_sle, NegativeStockError from erpnext.controllers.queries import get_match_cond @@ -16,7 +14,6 @@ from erpnext.manufacturing.doctype.bom.bom import validate_bom_no from erpnext.accounts.utils import validate_fiscal_year class NotUpdateStockError(frappe.ValidationError): pass -class StockOverReturnError(frappe.ValidationError): pass class IncorrectValuationRateError(frappe.ValidationError): pass class DuplicateEntryForProductionOrderError(frappe.ValidationError): pass class OperationsNotCompleteError(frappe.ValidationError): pass @@ -37,13 +34,6 @@ class StockEntry(StockController): item.update(get_available_qty(item.item_code, item.s_warehouse)) - count = frappe.db.exists({ - "doctype": "Journal Entry", - "stock_entry":self.name, - "docstatus":1 - }) - self.get("__onload").credit_debit_note_exists = 1 if count else 0 - def validate(self): self.pro_doc = None if self.production_order: @@ -84,16 +74,13 @@ class StockEntry(StockController): def validate_purpose(self): valid_purposes = ["Material Issue", "Material Receipt", "Material Transfer", "Material Transfer for Manufacture", - "Manufacture", "Repack", "Subcontract", "Sales Return", "Purchase Return"] + "Manufacture", "Repack", "Subcontract"] if self.purpose not in valid_purposes: frappe.throw(_("Purpose must be one of {0}").format(comma_or(valid_purposes))) - if self.purpose in ("Manufacture", "Repack", "Sales Return") and not self.difference_account: + if self.purpose in ("Manufacture", "Repack") and not self.difference_account: self.difference_account = frappe.db.get_value("Company", self.company, "default_expense_account") - if self.purpose in ("Purchase Return") and not self.difference_account: - frappe.throw(_("Difference Account mandatory for purpose '{0}'").format(self.purpose)) - def set_transfer_qty(self): for item in self.get("items"): if not flt(item.qty): @@ -122,7 +109,7 @@ class StockEntry(StockController): if not item.transfer_qty: item.transfer_qty = item.qty * item.conversion_factor - if (self.purpose in ("Material Transfer", "Sales Return", "Purchase Return", "Material Transfer for Manufacture") + if (self.purpose in ("Material Transfer", "Material Transfer for Manufacture") and not item.serial_no and item.item_code in serialized_items): frappe.throw(_("Row #{0}: Please specify Serial No for Item {1}").format(item.idx, item.item_code), @@ -131,8 +118,8 @@ class StockEntry(StockController): def validate_warehouse(self): """perform various (sometimes conditional) validations on warehouse""" - source_mandatory = ["Material Issue", "Material Transfer", "Purchase Return", "Subcontract", "Material Transfer for Manufacture"] - target_mandatory = ["Material Receipt", "Material Transfer", "Sales Return", "Subcontract", "Material Transfer for Manufacture"] + source_mandatory = ["Material Issue", "Material Transfer", "Subcontract", "Material Transfer for Manufacture"] + target_mandatory = ["Material Receipt", "Material Transfer", "Subcontract", "Material Transfer for Manufacture"] validate_for_manufacture_repack = any([d.bom_no for d in self.get("items")]) @@ -291,8 +278,8 @@ class StockEntry(StockController): # get incoming rate if not d.bom_no: - if not flt(d.incoming_rate) or d.s_warehouse or self.purpose == "Sales Return" or force: - incoming_rate = flt(self.get_incoming_rate(args), self.precision("incoming_rate", d)) + if not flt(d.incoming_rate) or d.s_warehouse or force: + incoming_rate = flt(get_incoming_rate(args), self.precision("incoming_rate", d)) if incoming_rate > 0: d.incoming_rate = incoming_rate @@ -336,27 +323,6 @@ class StockEntry(StockController): return operation_cost_per_unit + (flt(self.additional_operating_cost) / flt(qty)) - def get_incoming_rate(self, args): - incoming_rate = 0 - if self.purpose == "Sales Return": - incoming_rate = self.get_incoming_rate_for_sales_return(args) - else: - incoming_rate = get_incoming_rate(args) - - return incoming_rate - - def get_incoming_rate_for_sales_return(self, args): - incoming_rate = 0.0 - if (self.delivery_note_no or self.sales_invoice_no) and args.get("item_code"): - incoming_rate = frappe.db.sql("""select abs(ifnull(stock_value_difference, 0) / actual_qty) - from `tabStock Ledger Entry` - where voucher_type = %s and voucher_no = %s and item_code = %s limit 1""", - ((self.delivery_note_no and "Delivery Note" or "Sales Invoice"), - self.delivery_note_no or self.sales_invoice_no, args.item_code)) - incoming_rate = incoming_rate[0][0] if incoming_rate else 0.0 - - return incoming_rate - def validate_purchase_order(self): """Throw exception if more raw material is transferred against Purchase Order than in the raw materials supplied table""" @@ -403,55 +369,6 @@ class StockEntry(StockController): frappe.throw(_("Finished Item {0} must be entered for Manufacture type entry") .format(production_item)) - def validate_return_reference_doc(self): - """validate item with reference doc""" - ref = get_return_doc_and_details(self) - - if ref.doc: - # validate docstatus - if ref.doc.docstatus != 1: - frappe.throw(_("{0} {1} must be submitted").format(ref.doc.doctype, ref.doc.name), - frappe.InvalidStatusError) - - # update stock check - if ref.doc.doctype == "Sales Invoice" and cint(ref.doc.update_stock) != 1: - frappe.throw(_("'Update Stock' for Sales Invoice {0} must be set").format(ref.doc.name), NotUpdateStockError) - - # posting date check - ref_posting_datetime = "%s %s" % (ref.doc.posting_date, ref.doc.posting_time or "00:00:00") - - if get_datetime(ref_posting_datetime) < get_datetime(ref_posting_datetime): - from frappe.utils.dateutils import datetime_in_user_format - frappe.throw(_("Posting timestamp must be after {0}") - .format(datetime_in_user_format(ref_posting_datetime))) - - stock_items = get_stock_items_for_return(ref.doc, ref.parentfields) - already_returned_item_qty = self.get_already_returned_item_qty(ref.fieldname) - - for item in self.get("items"): - # validate if item exists in the ref doc and that it is a stock item - if item.item_code not in stock_items: - frappe.throw(_("Item {0} does not exist in {1} {2}").format(item.item_code, ref.doc.doctype, ref.doc.name), - frappe.DoesNotExistError) - - # validate quantity <= ref item's qty - qty already returned - if self.purpose == "Purchase Return": - ref_item_qty = sum([flt(d.qty)*flt(d.conversion_factor) for d in ref.doc.get({"item_code": item.item_code})]) - elif self.purpose == "Sales Return": - ref_item_qty = sum([flt(d.qty) for d in ref.doc.get({"item_code": item.item_code})]) - returnable_qty = ref_item_qty - flt(already_returned_item_qty.get(item.item_code)) - if not returnable_qty: - frappe.throw(_("Item {0} has already been returned").format(item.item_code), StockOverReturnError) - elif item.transfer_qty > returnable_qty: - frappe.throw(_("Cannot return more than {0} for Item {1}").format(returnable_qty, item.item_code), - StockOverReturnError) - - def get_already_returned_item_qty(self, ref_fieldname): - return dict(frappe.db.sql("""select item_code, sum(transfer_qty) as qty - from `tabStock Entry Detail` where parent in ( - select name from `tabStock Entry` where `%s`=%s and docstatus=1) - group by item_code""" % (ref_fieldname, "%s"), (self.get(ref_fieldname),))) - def update_stock_ledger(self): sl_entries = [] for d in self.get('items'): @@ -514,6 +431,7 @@ class StockEntry(StockController): (args.get('item_code')), as_dict = 1) if not item: frappe.throw(_("Item {0} is not active or end of life has been reached").format(args.get("item_code"))) + item = item[0] ret = { @@ -561,7 +479,7 @@ class StockEntry(StockController): ret = { "actual_qty" : get_previous_sle(args).get("qty_after_transaction") or 0, - "incoming_rate" : self.get_incoming_rate(args) + "incoming_rate" : get_incoming_rate(args) } return ret @@ -738,15 +656,6 @@ class StockEntry(StockController): if getdate(self.posting_date) > getdate(expiry_date): frappe.throw(_("Batch {0} of Item {1} has expired.").format(item.batch_no, item.item_code)) -@frappe.whitelist() -def get_party_details(ref_dt, ref_dn): - if ref_dt in ["Delivery Note", "Sales Invoice"]: - res = frappe.db.get_value(ref_dt, ref_dn, - ["customer", "customer_name", "address_display as customer_address"], as_dict=1) - else: - res = frappe.db.get_value(ref_dt, ref_dn, - ["supplier", "supplier_name", "address_display as supplier_address"], as_dict=1) - return res or {} @frappe.whitelist() def get_production_order_details(production_order): @@ -756,264 +665,3 @@ def get_production_order_details(production_order): from `tabProduction Order` where name = %s""", production_order, as_dict=1) return res and res[0] or {} - -def query_sales_return_doc(doctype, txt, searchfield, start, page_len, filters): - conditions = "" - if doctype == "Sales Invoice": - conditions = "and update_stock=1" - - return frappe.db.sql("""select name, customer, customer_name - from `tab%s` where docstatus = 1 - and (`%s` like %%(txt)s - or `customer` like %%(txt)s) %s %s - order by name, customer, customer_name - limit %s""" % (doctype, searchfield, conditions, - get_match_cond(doctype), "%(start)s, %(page_len)s"), - {"txt": "%%%s%%" % txt, "start": start, "page_len": page_len}, - as_list=True) - -def query_purchase_return_doc(doctype, txt, searchfield, start, page_len, filters): - return frappe.db.sql("""select name, supplier, supplier_name - from `tab%s` where docstatus = 1 - and (`%s` like %%(txt)s - or `supplier` like %%(txt)s) %s - order by name, supplier, supplier_name - limit %s""" % (doctype, searchfield, get_match_cond(doctype), - "%(start)s, %(page_len)s"), {"txt": "%%%s%%" % txt, "start": - start, "page_len": page_len}, as_list=True) - -def query_return_item(doctype, txt, searchfield, start, page_len, filters): - txt = txt.replace("%", "") - - ref = get_return_doc_and_details(filters) - - stock_items = get_stock_items_for_return(ref.doc, ref.parentfields) - - result = [] - for item in ref.doc.get_all_children(): - if getattr(item, "item_code", None) in stock_items: - item.item_name = cstr(item.item_name) - item.description = cstr(item.description) - if (txt in item.item_code) or (txt in item.item_name) or (txt in item.description): - val = [ - item.item_code, - (len(item.item_name) > 40) and (item.item_name[:40] + "...") or item.item_name, - (len(item.description) > 40) and (item.description[:40] + "...") or \ - item.description - ] - if val not in result: - result.append(val) - - return result[start:start+page_len] - -def get_stock_items_for_return(ref_doc, parentfields): - """return item codes filtered from doc, which are stock items""" - if isinstance(parentfields, basestring): - parentfields = [parentfields] - - all_items = list(set([d.item_code for d in - ref_doc.get_all_children() if d.get("item_code")])) - stock_items = frappe.db.sql_list("""select name from `tabItem` - where is_stock_item='Yes' and name in (%s)""" % (", ".join(["%s"] * len(all_items))), - tuple(all_items)) - - return stock_items - -def get_return_doc_and_details(args): - ref = frappe._dict() - - # get ref_doc - if args.get("purpose") in return_map: - for fieldname, val in return_map[args.get("purpose")].items(): - if args.get(fieldname): - ref.fieldname = fieldname - ref.doc = frappe.get_doc(val[0], args.get(fieldname)) - ref.parentfields = val[1] - break - - return ref - -return_map = { - "Sales Return": { - # [Ref DocType, [Item tables' parentfields]] - "delivery_note_no": ["Delivery Note", ["items", "packed_items"]], - "sales_invoice_no": ["Sales Invoice", ["items", "packed_items"]] - }, - "Purchase Return": { - "purchase_receipt_no": ["Purchase Receipt", ["items"]] - } -} - -@frappe.whitelist() -def make_return_jv(stock_entry): - se = frappe.get_doc("Stock Entry", stock_entry) - if not se.purpose in ["Sales Return", "Purchase Return"]: - return - - ref = get_return_doc_and_details(se) - - if ref.doc.doctype == "Delivery Note": - result = make_return_jv_from_delivery_note(se, ref) - elif ref.doc.doctype == "Sales Invoice": - result = make_return_jv_from_sales_invoice(se, ref) - elif ref.doc.doctype == "Purchase Receipt": - result = make_return_jv_from_purchase_receipt(se, ref) - - # create jv doc and fetch balance for each unique row item - jv = frappe.new_doc("Journal Entry") - jv.update({ - "posting_date": se.posting_date, - "voucher_type": se.purpose == "Sales Return" and "Credit Note" or "Debit Note", - "fiscal_year": se.fiscal_year, - "company": se.company, - "stock_entry": se.name - }) - - from erpnext.accounts.utils import get_balance_on - for r in result: - jv.append("accounts", { - "account": r.get("account"), - "party_type": r.get("party_type"), - "party": r.get("party"), - "balance": get_balance_on(r.get("account"), se.posting_date) if r.get("account") else 0 - }) - - return jv - -def make_return_jv_from_sales_invoice(se, ref): - # customer account entry - parent = { - "account": ref.doc.debit_to, - "party_type": "Customer", - "party": ref.doc.customer - } - - # income account entries - children = [] - for se_item in se.get("items"): - # find item in ref.doc - ref_item = ref.doc.get({"item_code": se_item.item_code})[0] - - account = get_sales_account_from_item(ref.doc, ref_item) - - if account not in children: - children.append(account) - - return [parent] + [{"account": account} for account in children] - -def get_sales_account_from_item(doc, ref_item): - account = None - if not getattr(ref_item, "income_account", None): - if ref_item.parent_item: - parent_item = doc.get("items", {"item_code": ref_item.parent_item})[0] - account = parent_item.income_account - else: - account = ref_item.income_account - - return account - -def make_return_jv_from_delivery_note(se, ref): - invoices_against_delivery = get_invoice_list("Sales Invoice Item", "delivery_note", - ref.doc.name) - - if not invoices_against_delivery: - sales_orders_against_delivery = [d.against_sales_order for d in ref.doc.get_all_children() if getattr(d, "against_sales_order", None)] - - if sales_orders_against_delivery: - invoices_against_delivery = get_invoice_list("Sales Invoice Item", "sales_order", - sales_orders_against_delivery) - - if not invoices_against_delivery: - return [] - - packing_item_parent_map = dict([[d.item_code, d.parent_item] for d in ref.doc.get(ref.parentfields[1])]) - - parent = {} - children = [] - - for se_item in se.get("items"): - for sales_invoice in invoices_against_delivery: - si = frappe.get_doc("Sales Invoice", sales_invoice) - - if se_item.item_code in packing_item_parent_map: - ref_item = si.get({"item_code": packing_item_parent_map[se_item.item_code]}) - else: - ref_item = si.get({"item_code": se_item.item_code}) - - if not ref_item: - continue - - ref_item = ref_item[0] - - account = get_sales_account_from_item(si, ref_item) - - if account not in children: - children.append(account) - - if not parent: - parent = { - "account": si.debit_to, - "party_type": "Customer", - "party": si.customer - } - - break - - result = [parent] + [{"account": account} for account in children] - - return result - -def get_invoice_list(doctype, link_field, value): - if isinstance(value, basestring): - value = [value] - - return frappe.db.sql_list("""select distinct parent from `tab%s` - where docstatus = 1 and `%s` in (%s)""" % (doctype, link_field, - ", ".join(["%s"]*len(value))), tuple(value)) - -def make_return_jv_from_purchase_receipt(se, ref): - invoice_against_receipt = get_invoice_list("Purchase Invoice Item", "purchase_receipt", - ref.doc.name) - - if not invoice_against_receipt: - purchase_orders_against_receipt = [d.prevdoc_docname for d in - ref.doc.get("items", {"prevdoc_doctype": "Purchase Order"}) - if getattr(d, "prevdoc_docname", None)] - - if purchase_orders_against_receipt: - invoice_against_receipt = get_invoice_list("Purchase Invoice Item", "purchase_order", - purchase_orders_against_receipt) - - if not invoice_against_receipt: - return [] - - parent = {} - children = [] - - for se_item in se.get("items"): - for purchase_invoice in invoice_against_receipt: - pi = frappe.get_doc("Purchase Invoice", purchase_invoice) - ref_item = pi.get({"item_code": se_item.item_code}) - - if not ref_item: - continue - - ref_item = ref_item[0] - - account = ref_item.expense_account - - if account not in children: - children.append(account) - - if not parent: - parent = { - "account": pi.credit_to, - "party_type": "Supplier", - "party": pi.supplier - } - - break - - result = [parent] + [{"account": account} for account in children] - - return result diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 70d6413ce1..5406bdccfb 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -303,262 +303,262 @@ class TestStockEntry(unittest.TestCase): self.assertEquals(expected_gl_entries[i][1], gle[1]) self.assertEquals(expected_gl_entries[i][2], gle[2]) - def _test_sales_invoice_return(self, item_code, delivered_qty, returned_qty): - from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice - - si = create_sales_invoice(item_code=item_code, qty=delivered_qty) - - se = make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=returned_qty, - purpose="Sales Return", sales_invoice_no=si.name, do_not_save=True) - self.assertRaises(NotUpdateStockError, se.insert) - - make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=200, incoming_rate=100) - - # check currency available qty in bin - actual_qty_0 = get_qty_after_transaction() - - # insert a pos invoice with update stock - si = create_sales_invoice(update_stock=1, item_code=item_code, qty=5) - - # check available bin qty after invoice submission - actual_qty_1 = get_qty_after_transaction() - - self.assertEquals(actual_qty_0 - delivered_qty, actual_qty_1) - - # check if item is validated - se = make_stock_entry(item_code="_Test Item Home Desktop 200", target="_Test Warehouse - _TC", - qty=returned_qty, purpose="Sales Return", sales_invoice_no=si.name, do_not_save=True) - - self.assertRaises(frappe.DoesNotExistError, se.insert) - - # try again - se = make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", - qty=returned_qty, purpose="Sales Return", sales_invoice_no=si.name) - - # check if available qty is increased - actual_qty_2 = get_qty_after_transaction() - - self.assertEquals(actual_qty_1 + returned_qty, actual_qty_2) - - return se - - def test_sales_invoice_return_of_non_packing_item(self): - self._test_sales_invoice_return("_Test Item", 5, 2) - - def test_sales_invoice_return_of_packing_item(self): - self._test_sales_invoice_return("_Test Product Bundle Item", 25, 20) - - def _test_delivery_note_return(self, item_code, delivered_qty, returned_qty): - from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note - - from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice - - make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=50, incoming_rate=100) - - actual_qty_0 = get_qty_after_transaction() - # make a delivery note based on this invoice - dn = create_delivery_note(item_code="_Test Item", - warehouse="_Test Warehouse - _TC", qty=delivered_qty) - - actual_qty_1 = get_qty_after_transaction() - - self.assertEquals(actual_qty_0 - delivered_qty, actual_qty_1) - - si = make_sales_invoice(dn.name) - si.insert() - si.submit() - - # insert and submit stock entry for sales return - se = make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", - qty=returned_qty, purpose="Sales Return", delivery_note_no=dn.name) - - actual_qty_2 = get_qty_after_transaction() - self.assertEquals(actual_qty_1 + returned_qty, actual_qty_2) - - return se - - def test_delivery_note_return_of_non_packing_item(self): - self._test_delivery_note_return("_Test Item", 5, 2) - - def test_delivery_note_return_of_packing_item(self): - self._test_delivery_note_return("_Test Product Bundle Item", 25, 20) - - def _test_sales_return_jv(self, se): - jv = make_return_jv(se.name) - - self.assertEqual(len(jv.get("accounts")), 2) - self.assertEqual(jv.get("voucher_type"), "Credit Note") - self.assertEqual(jv.get("posting_date"), getdate(se.posting_date)) - self.assertEqual(jv.get("accounts")[0].get("account"), "Debtors - _TC") - self.assertEqual(jv.get("accounts")[0].get("party_type"), "Customer") - self.assertEqual(jv.get("accounts")[0].get("party"), "_Test Customer") - self.assertEqual(jv.get("accounts")[1].get("account"), "Sales - _TC") - - def test_make_return_jv_for_sales_invoice_non_packing_item(self): - se = self._test_sales_invoice_return("_Test Item", 5, 2) - self._test_sales_return_jv(se) - - def test_make_return_jv_for_sales_invoice_packing_item(self): - se = self._test_sales_invoice_return("_Test Product Bundle Item", 25, 20) - self._test_sales_return_jv(se) - - def test_make_return_jv_for_delivery_note_non_packing_item(self): - se = self._test_delivery_note_return("_Test Item", 5, 2) - self._test_sales_return_jv(se) - - se = self._test_delivery_note_return_against_sales_order("_Test Item", 5, 2) - self._test_sales_return_jv(se) - - def test_make_return_jv_for_delivery_note_packing_item(self): - se = self._test_delivery_note_return("_Test Product Bundle Item", 25, 20) - self._test_sales_return_jv(se) - - se = self._test_delivery_note_return_against_sales_order("_Test Product Bundle Item", 25, 20) - self._test_sales_return_jv(se) - - def _test_delivery_note_return_against_sales_order(self, item_code, delivered_qty, returned_qty): - from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice - - actual_qty_0 = get_qty_after_transaction() - - so = make_sales_order(qty=50) - - dn = create_dn_against_so(so.name, delivered_qty) - - actual_qty_1 = get_qty_after_transaction() - self.assertEquals(actual_qty_0 - delivered_qty, actual_qty_1) - - si = make_sales_invoice(so.name) - si.insert() - si.submit() - - # insert and submit stock entry for sales return - se = make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", - qty=returned_qty, purpose="Sales Return", delivery_note_no=dn.name) - - actual_qty_2 = get_qty_after_transaction() - self.assertEquals(actual_qty_1 + returned_qty, actual_qty_2) - - return se - - def test_purchase_receipt_return(self): - actual_qty_0 = get_qty_after_transaction() - - # submit purchase receipt - pr = make_purchase_receipt(item_code="_Test Item", warehouse="_Test Warehouse - _TC", qty=5) - - actual_qty_1 = get_qty_after_transaction() - - self.assertEquals(actual_qty_0 + 5, actual_qty_1) - - pi_doc = make_purchase_invoice(pr.name) - - pi = frappe.get_doc(pi_doc) - pi.posting_date = pr.posting_date - pi.credit_to = "_Test Payable - _TC" - for d in pi.get("items"): - d.expense_account = "_Test Account Cost for Goods Sold - _TC" - d.cost_center = "_Test Cost Center - _TC" - - for d in pi.get("taxes"): - d.cost_center = "_Test Cost Center - _TC" - - pi.insert() - pi.submit() - - # submit purchase return - se = make_stock_entry(item_code="_Test Item", source="_Test Warehouse - _TC", - qty=5, purpose="Purchase Return", purchase_receipt_no=pr.name) - - actual_qty_2 = get_qty_after_transaction() - - self.assertEquals(actual_qty_1 - 5, actual_qty_2) - - return se, pr.name - - def test_over_stock_return(self): - from erpnext.stock.doctype.stock_entry.stock_entry import StockOverReturnError - - # out of 10, 5 gets returned - prev_se, pr_docname = self.test_purchase_receipt_return() - - se = make_stock_entry(item_code="_Test Item", source="_Test Warehouse - _TC", - qty=6, purpose="Purchase Return", purchase_receipt_no=pr_docname, do_not_save=True) - - self.assertRaises(StockOverReturnError, se.insert) - - def _test_purchase_return_jv(self, se): - jv = make_return_jv(se.name) - - self.assertEqual(len(jv.get("accounts")), 2) - self.assertEqual(jv.get("voucher_type"), "Debit Note") - self.assertEqual(jv.get("posting_date"), getdate(se.posting_date)) - self.assertEqual(jv.get("accounts")[0].get("account"), "_Test Payable - _TC") - self.assertEqual(jv.get("accounts")[0].get("party"), "_Test Supplier") - self.assertEqual(jv.get("accounts")[1].get("account"), "_Test Account Cost for Goods Sold - _TC") - - def test_make_return_jv_for_purchase_receipt(self): - se, pr_name = self.test_purchase_receipt_return() - self._test_purchase_return_jv(se) - - se, pr_name = self._test_purchase_return_return_against_purchase_order() - self._test_purchase_return_jv(se) - - def _test_purchase_return_return_against_purchase_order(self): - - actual_qty_0 = get_qty_after_transaction() - - from erpnext.buying.doctype.purchase_order.test_purchase_order \ - import test_records as purchase_order_test_records - - from erpnext.buying.doctype.purchase_order.purchase_order import \ - make_purchase_receipt, make_purchase_invoice - - # submit purchase receipt - po = frappe.copy_doc(purchase_order_test_records[0]) - po.transaction_date = nowdate() - po.is_subcontracted = None - po.get("items")[0].item_code = "_Test Item" - po.get("items")[0].rate = 50 - po.insert() - po.submit() - - pr_doc = make_purchase_receipt(po.name) - - pr = frappe.get_doc(pr_doc) - pr.posting_date = po.transaction_date - pr.insert() - pr.submit() - - actual_qty_1 = get_qty_after_transaction() - - self.assertEquals(actual_qty_0 + 10, actual_qty_1) - - pi_doc = make_purchase_invoice(po.name) - - pi = frappe.get_doc(pi_doc) - pi.posting_date = pr.posting_date - pi.credit_to = "_Test Payable - _TC" - for d in pi.get("items"): - d.expense_account = "_Test Account Cost for Goods Sold - _TC" - d.cost_center = "_Test Cost Center - _TC" - for d in pi.get("taxes"): - d.cost_center = "_Test Cost Center - _TC" - - pi.run_method("calculate_taxes_and_totals") - pi.bill_no = "NA" - pi.insert() - pi.submit() - - # submit purchase return - se = make_stock_entry(item_code="_Test Item", source="_Test Warehouse - _TC", - qty=5, purpose="Purchase Return", purchase_receipt_no=pr.name) - - actual_qty_2 = get_qty_after_transaction() - - self.assertEquals(actual_qty_1 - 5, actual_qty_2) - - return se, pr.name + # def _test_sales_invoice_return(self, item_code, delivered_qty, returned_qty): + # from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice + # + # si = create_sales_invoice(item_code=item_code, qty=delivered_qty) + # + # se = make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=returned_qty, + # purpose="Sales Return", sales_invoice_no=si.name, do_not_save=True) + # self.assertRaises(NotUpdateStockError, se.insert) + # + # make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=200, incoming_rate=100) + # + # # check currency available qty in bin + # actual_qty_0 = get_qty_after_transaction() + # + # # insert a pos invoice with update stock + # si = create_sales_invoice(update_stock=1, item_code=item_code, qty=5) + # + # # check available bin qty after invoice submission + # actual_qty_1 = get_qty_after_transaction() + # + # self.assertEquals(actual_qty_0 - delivered_qty, actual_qty_1) + # + # # check if item is validated + # se = make_stock_entry(item_code="_Test Item Home Desktop 200", target="_Test Warehouse - _TC", + # qty=returned_qty, purpose="Sales Return", sales_invoice_no=si.name, do_not_save=True) + # + # self.assertRaises(frappe.DoesNotExistError, se.insert) + # + # # try again + # se = make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", + # qty=returned_qty, purpose="Sales Return", sales_invoice_no=si.name) + # + # # check if available qty is increased + # actual_qty_2 = get_qty_after_transaction() + # + # self.assertEquals(actual_qty_1 + returned_qty, actual_qty_2) + # + # return se + # + # def test_sales_invoice_return_of_non_packing_item(self): + # self._test_sales_invoice_return("_Test Item", 5, 2) + # + # def test_sales_invoice_return_of_packing_item(self): + # self._test_sales_invoice_return("_Test Product Bundle Item", 25, 20) + # + # def _test_delivery_note_return(self, item_code, delivered_qty, returned_qty): + # from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note + # + # from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice + # + # make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=50, incoming_rate=100) + # + # actual_qty_0 = get_qty_after_transaction() + # # make a delivery note based on this invoice + # dn = create_delivery_note(item_code="_Test Item", + # warehouse="_Test Warehouse - _TC", qty=delivered_qty) + # + # actual_qty_1 = get_qty_after_transaction() + # + # self.assertEquals(actual_qty_0 - delivered_qty, actual_qty_1) + # + # si = make_sales_invoice(dn.name) + # si.insert() + # si.submit() + # + # # insert and submit stock entry for sales return + # se = make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", + # qty=returned_qty, purpose="Sales Return", delivery_note_no=dn.name) + # + # actual_qty_2 = get_qty_after_transaction() + # self.assertEquals(actual_qty_1 + returned_qty, actual_qty_2) + # + # return se + # + # def test_delivery_note_return_of_non_packing_item(self): + # self._test_delivery_note_return("_Test Item", 5, 2) + # + # def test_delivery_note_return_of_packing_item(self): + # self._test_delivery_note_return("_Test Product Bundle Item", 25, 20) + # + # def _test_sales_return_jv(self, se): + # jv = make_return_jv(se.name) + # + # self.assertEqual(len(jv.get("accounts")), 2) + # self.assertEqual(jv.get("voucher_type"), "Credit Note") + # self.assertEqual(jv.get("posting_date"), getdate(se.posting_date)) + # self.assertEqual(jv.get("accounts")[0].get("account"), "Debtors - _TC") + # self.assertEqual(jv.get("accounts")[0].get("party_type"), "Customer") + # self.assertEqual(jv.get("accounts")[0].get("party"), "_Test Customer") + # self.assertEqual(jv.get("accounts")[1].get("account"), "Sales - _TC") + # + # def test_make_return_jv_for_sales_invoice_non_packing_item(self): + # se = self._test_sales_invoice_return("_Test Item", 5, 2) + # self._test_sales_return_jv(se) + # + # def test_make_return_jv_for_sales_invoice_packing_item(self): + # se = self._test_sales_invoice_return("_Test Product Bundle Item", 25, 20) + # self._test_sales_return_jv(se) + # + # def test_make_return_jv_for_delivery_note_non_packing_item(self): + # se = self._test_delivery_note_return("_Test Item", 5, 2) + # self._test_sales_return_jv(se) + # + # se = self._test_delivery_note_return_against_sales_order("_Test Item", 5, 2) + # self._test_sales_return_jv(se) + # + # def test_make_return_jv_for_delivery_note_packing_item(self): + # se = self._test_delivery_note_return("_Test Product Bundle Item", 25, 20) + # self._test_sales_return_jv(se) + # + # se = self._test_delivery_note_return_against_sales_order("_Test Product Bundle Item", 25, 20) + # self._test_sales_return_jv(se) + # + # def _test_delivery_note_return_against_sales_order(self, item_code, delivered_qty, returned_qty): + # from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice + # + # actual_qty_0 = get_qty_after_transaction() + # + # so = make_sales_order(qty=50) + # + # dn = create_dn_against_so(so.name, delivered_qty) + # + # actual_qty_1 = get_qty_after_transaction() + # self.assertEquals(actual_qty_0 - delivered_qty, actual_qty_1) + # + # si = make_sales_invoice(so.name) + # si.insert() + # si.submit() + # + # # insert and submit stock entry for sales return + # se = make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", + # qty=returned_qty, purpose="Sales Return", delivery_note_no=dn.name) + # + # actual_qty_2 = get_qty_after_transaction() + # self.assertEquals(actual_qty_1 + returned_qty, actual_qty_2) + # + # return se + # + # def test_purchase_receipt_return(self): + # actual_qty_0 = get_qty_after_transaction() + # + # # submit purchase receipt + # pr = make_purchase_receipt(item_code="_Test Item", warehouse="_Test Warehouse - _TC", qty=5) + # + # actual_qty_1 = get_qty_after_transaction() + # + # self.assertEquals(actual_qty_0 + 5, actual_qty_1) + # + # pi_doc = make_purchase_invoice(pr.name) + # + # pi = frappe.get_doc(pi_doc) + # pi.posting_date = pr.posting_date + # pi.credit_to = "_Test Payable - _TC" + # for d in pi.get("items"): + # d.expense_account = "_Test Account Cost for Goods Sold - _TC" + # d.cost_center = "_Test Cost Center - _TC" + # + # for d in pi.get("taxes"): + # d.cost_center = "_Test Cost Center - _TC" + # + # pi.insert() + # pi.submit() + # + # # submit purchase return + # se = make_stock_entry(item_code="_Test Item", source="_Test Warehouse - _TC", + # qty=5, purpose="Purchase Return", purchase_receipt_no=pr.name) + # + # actual_qty_2 = get_qty_after_transaction() + # + # self.assertEquals(actual_qty_1 - 5, actual_qty_2) + # + # return se, pr.name + # + # def test_over_stock_return(self): + # from erpnext.stock.doctype.stock_entry.stock_entry import StockOverReturnError + # + # # out of 10, 5 gets returned + # prev_se, pr_docname = self.test_purchase_receipt_return() + # + # se = make_stock_entry(item_code="_Test Item", source="_Test Warehouse - _TC", + # qty=6, purpose="Purchase Return", purchase_receipt_no=pr_docname, do_not_save=True) + # + # self.assertRaises(StockOverReturnError, se.insert) + # + # def _test_purchase_return_jv(self, se): + # jv = make_return_jv(se.name) + # + # self.assertEqual(len(jv.get("accounts")), 2) + # self.assertEqual(jv.get("voucher_type"), "Debit Note") + # self.assertEqual(jv.get("posting_date"), getdate(se.posting_date)) + # self.assertEqual(jv.get("accounts")[0].get("account"), "_Test Payable - _TC") + # self.assertEqual(jv.get("accounts")[0].get("party"), "_Test Supplier") + # self.assertEqual(jv.get("accounts")[1].get("account"), "_Test Account Cost for Goods Sold - _TC") + # + # def test_make_return_jv_for_purchase_receipt(self): + # se, pr_name = self.test_purchase_receipt_return() + # self._test_purchase_return_jv(se) + # + # se, pr_name = self._test_purchase_return_return_against_purchase_order() + # self._test_purchase_return_jv(se) + # + # def _test_purchase_return_return_against_purchase_order(self): + # + # actual_qty_0 = get_qty_after_transaction() + # + # from erpnext.buying.doctype.purchase_order.test_purchase_order \ + # import test_records as purchase_order_test_records + # + # from erpnext.buying.doctype.purchase_order.purchase_order import \ + # make_purchase_receipt, make_purchase_invoice + # + # # submit purchase receipt + # po = frappe.copy_doc(purchase_order_test_records[0]) + # po.transaction_date = nowdate() + # po.is_subcontracted = None + # po.get("items")[0].item_code = "_Test Item" + # po.get("items")[0].rate = 50 + # po.insert() + # po.submit() + # + # pr_doc = make_purchase_receipt(po.name) + # + # pr = frappe.get_doc(pr_doc) + # pr.posting_date = po.transaction_date + # pr.insert() + # pr.submit() + # + # actual_qty_1 = get_qty_after_transaction() + # + # self.assertEquals(actual_qty_0 + 10, actual_qty_1) + # + # pi_doc = make_purchase_invoice(po.name) + # + # pi = frappe.get_doc(pi_doc) + # pi.posting_date = pr.posting_date + # pi.credit_to = "_Test Payable - _TC" + # for d in pi.get("items"): + # d.expense_account = "_Test Account Cost for Goods Sold - _TC" + # d.cost_center = "_Test Cost Center - _TC" + # for d in pi.get("taxes"): + # d.cost_center = "_Test Cost Center - _TC" + # + # pi.run_method("calculate_taxes_and_totals") + # pi.bill_no = "NA" + # pi.insert() + # pi.submit() + # + # # submit purchase return + # se = make_stock_entry(item_code="_Test Item", source="_Test Warehouse - _TC", + # qty=5, purpose="Purchase Return", purchase_receipt_no=pr.name) + # + # actual_qty_2 = get_qty_after_transaction() + # + # self.assertEquals(actual_qty_1 - 5, actual_qty_2) + # + # return se, pr.name def test_serial_no_not_reqd(self): se = frappe.copy_doc(test_records[0]) From 6b25708b7a47a7a366cb5cb1a0f68b27f20cab89 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 17 Jul 2015 16:54:25 +0530 Subject: [PATCH 30/40] Minor fixes --- erpnext/controllers/accounts_controller.py | 2 +- erpnext/stock/doctype/purchase_receipt/purchase_receipt.py | 1 - erpnext/stock/doctype/stock_entry/stock_entry.py | 1 - erpnext/stock/doctype/stock_entry/test_stock_entry.py | 2 +- .../doctype/stock_reconciliation/test_stock_reconciliation.py | 2 -- 5 files changed, 2 insertions(+), 6 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index d1ce3c6fdc..61b3c5213f 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -26,7 +26,7 @@ class AccountsController(TransactionBase): self.validate_return_doc() self.set_total_in_words() - if not self.is_return: + if self.doctype in ("Sales Invoice", "Purchase Invoice") and not self.is_return: self.validate_due_date() if self.meta.get_field("is_recurring"): diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 31a2f50a12..a94856d44b 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -71,7 +71,6 @@ class PurchaseReceipt(BuyingController): def validate_purchase_return(self): for d in self.get("items"): - print flt(d.rejected_qty) if self.is_return and flt(d.rejected_qty) != 0: frappe.throw(_("Row #{0}: Rejected Qty can not be entered in Purchase Return").format(d.idx)) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 78e1038292..58a133132f 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -51,7 +51,6 @@ class StockEntry(StockController): self.get_stock_and_rate() self.validate_bom() self.validate_finished_goods() - self.validate_return_reference_doc() self.validate_with_material_request() self.validate_valuation_rate() self.set_total_incoming_outgoing_value() diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 5406bdccfb..95196ccf29 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -12,7 +12,7 @@ from erpnext.stock.doctype.stock_ledger_entry.stock_ledger_entry import StockFre from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_invoice from erpnext.stock.stock_ledger import get_previous_sle from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order, create_dn_against_so -from erpnext.stock.doctype.stock_entry.stock_entry import make_return_jv, NotUpdateStockError +from erpnext.stock.doctype.stock_entry.stock_entry import NotUpdateStockError from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import create_stock_reconciliation def get_sle(**args): diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index eaa82dd23f..dde338611b 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -107,8 +107,6 @@ def create_stock_reconciliation(**args): "valuation_rate": args.rate }) - sr.insert() - sr.submit() return sr From 246ed3f12242fd66994c64957b4bfc09227e0a72 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 20 Jul 2015 18:39:44 +0530 Subject: [PATCH 31/40] Test cases for sales return --- .../doctype/sales_invoice/sales_invoice.py | 6 +- .../sales_invoice/test_sales_invoice.py | 56 +++- erpnext/controllers/accounts_controller.py | 10 +- .../doctype/delivery_note/delivery_note.py | 2 +- .../delivery_note/test_delivery_note.py | 112 +++++++- .../stock/doctype/stock_entry/stock_entry.py | 1 - .../doctype/stock_entry/test_stock_entry.py | 264 +----------------- 7 files changed, 177 insertions(+), 274 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 9129e1f7f0..dd60085b85 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -493,7 +493,7 @@ class SalesInvoice(SellingController): "against": self.against_income_account, "debit": self.base_grand_total, "remarks": self.remarks, - "against_voucher": self.against_invoice if cint(self.is_return) else self.name, + "against_voucher": self.return_against if cint(self.is_return) else self.name, "against_voucher_type": self.doctype }) ) @@ -541,7 +541,7 @@ class SalesInvoice(SellingController): "against": self.cash_bank_account, "credit": self.paid_amount, "remarks": self.remarks, - "against_voucher": self.against_invoice if cint(self.is_return) else self.name, + "against_voucher": self.return_against if cint(self.is_return) else self.name, "against_voucher_type": self.doctype, }) ) @@ -565,7 +565,7 @@ class SalesInvoice(SellingController): "against": self.write_off_account, "credit": self.write_off_amount, "remarks": self.remarks, - "against_voucher": self.against_invoice if cint(self.is_return) else self.name, + "against_voucher": self.return_against if cint(self.is_return) else self.name, "against_voucher_type": self.doctype, }) ) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index cf752afc9d..6d54f0ab6c 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -4,11 +4,10 @@ from __future__ import unicode_literals import frappe import unittest, copy -import time -from frappe.utils import nowdate, add_days -from erpnext.accounts.utils import get_stock_and_account_difference +from frappe.utils import nowdate, add_days, flt from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory from erpnext.projects.doctype.time_log_batch.test_time_log_batch import * +from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry, get_qty_after_transaction class TestSalesInvoice(unittest.TestCase): @@ -772,6 +771,53 @@ class TestSalesInvoice(unittest.TestCase): si1 = create_sales_invoice(posting_date="2015-07-05") self.assertEqual(si1.due_date, "2015-08-31") + def test_return_sales_invoice(self): + set_perpetual_inventory() + + make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=50, incoming_rate=100) + + actual_qty_0 = get_qty_after_transaction() + + si = create_sales_invoice(qty=5, rate=500, update_stock=1) + + actual_qty_1 = get_qty_after_transaction() + self.assertEquals(actual_qty_0 - 5, actual_qty_1) + + # outgoing_rate + outgoing_rate = frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Sales Invoice", + "voucher_no": si.name}, "stock_value_difference") / 5 + + # return entry + si1 = create_sales_invoice(is_return=1, return_against=si.name, qty=-2, rate=500, update_stock=1) + + actual_qty_2 = get_qty_after_transaction() + + self.assertEquals(actual_qty_1 + 2, actual_qty_2) + + incoming_rate, stock_value_difference = frappe.db.get_value("Stock Ledger Entry", + {"voucher_type": "Sales Invoice", "voucher_no": si1.name}, + ["incoming_rate", "stock_value_difference"]) + + self.assertEquals(flt(incoming_rate, 3), abs(flt(outgoing_rate, 3))) + + + # Check gl entry + gle_warehouse_amount = frappe.db.get_value("GL Entry", {"voucher_type": "Sales Invoice", + "voucher_no": si1.name, "account": "_Test Warehouse - _TC"}, "debit") + + self.assertEquals(gle_warehouse_amount, stock_value_difference) + + party_credited = frappe.db.get_value("GL Entry", {"voucher_type": "Sales Invoice", + "voucher_no": si1.name, "account": "Debtors - _TC", "party": "_Test Customer"}, "credit") + + self.assertEqual(party_credited, 1000) + + # Check outstanding amount + self.assertFalse(si1.outstanding_amount) + self.assertEqual(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"), 1500) + + set_perpetual_inventory(0) + def create_sales_invoice(**args): si = frappe.new_doc("Sales Invoice") @@ -784,6 +830,10 @@ def create_sales_invoice(**args): si.debit_to = args.debit_to or "Debtors - _TC" si.update_stock = args.update_stock si.is_pos = args.is_pos + si.is_return = args.is_return + si.return_against = args.return_against + si.currency="INR" + si.conversion_rate = 1 si.append("items", { "item_code": args.item or args.item_code or "_Test Item", diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 61b3c5213f..eb7b73f7f2 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -86,8 +86,7 @@ class AccountsController(TransactionBase): ref_posting_datetime = "%s %s" % (ref_doc.posting_date, ref_doc.get("posting_time") or "00:00:00") if get_datetime(return_posting_datetime) < get_datetime(ref_posting_datetime): - frappe.throw(_("Posting timestamp must be after {0}") - .format(datetime_in_user_format(ref_posting_datetime))) + frappe.throw(_("Posting timestamp must be after {0}").format(format_datetime(ref_posting_datetime))) # validate same exchange rate if self.conversion_rate != ref_doc.conversion_rate: @@ -105,6 +104,11 @@ class AccountsController(TransactionBase): for d in frappe.db.sql("""select item_code, sum(qty) as qty, rate from `tab{0} Item` where parent = %s group by item_code""".format(self.doctype), self.return_against, as_dict=1): valid_items.setdefault(d.item_code, d) + + if self.doctype in ("Delivery Note", "Sales Invoice"): + for d in frappe.db.sql("""select item_code, sum(qty) as qty from `tabPacked Item` + where parent = %s group by item_code""".format(self.doctype), self.return_against, as_dict=1): + valid_items.setdefault(d.item_code, d) already_returned_items = self.get_already_returned_items() @@ -124,7 +128,7 @@ class AccountsController(TransactionBase): elif abs(d.qty) > max_return_qty: frappe.throw(_("Row # {0}: Cannot return more than {1} for Item {2}") .format(d.idx, ref.qty, d.item_code), StockOverReturnError) - elif flt(d.rate) != ref.rate: + elif ref.rate and flt(d.rate) != ref.rate: frappe.throw(_("Row # {0}: Rate must be same as {1} {2}") .format(d.idx, self.doctype, self.return_against)) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index cd501dad88..d0fff0b811 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -257,7 +257,7 @@ class DeliveryNote(SellingController): sl_entries.append(self.get_sl_entries(d, { "actual_qty": -1*flt(d['qty']), - incoming_rate: incoming_rate + "incoming_rate": incoming_rate })) self.make_sl_entries(sl_entries) diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 978e968c4a..6f2196aed9 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -13,8 +13,10 @@ from erpnext.accounts.utils import get_balance_on from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt \ import get_gl_entries, set_perpetual_inventory from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice -from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry, make_serialized_item +from erpnext.stock.doctype.stock_entry.test_stock_entry \ + import make_stock_entry, make_serialized_item, get_qty_after_transaction from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos, SerialNoStatusError +from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import create_stock_reconciliation class TestDeliveryNote(unittest.TestCase): def test_over_billing_against_dn(self): @@ -177,7 +179,113 @@ class TestDeliveryNote(unittest.TestCase): def check_serial_no_values(self, serial_no, field_values): for field, value in field_values.items(): self.assertEquals(cstr(frappe.db.get_value("Serial No", serial_no, field)), value) + + def test_sales_return_for_non_bundled_items(self): + set_perpetual_inventory() + + make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=50, incoming_rate=100) + + actual_qty_0 = get_qty_after_transaction() + + dn = create_delivery_note(qty=5, rate=500) + actual_qty_1 = get_qty_after_transaction() + self.assertEquals(actual_qty_0 - 5, actual_qty_1) + + # outgoing_rate + outgoing_rate = frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Delivery Note", + "voucher_no": dn.name}, "stock_value_difference") / 5 + + # return entry + dn1 = create_delivery_note(is_return=1, return_against=dn.name, qty=-2, rate=500) + + actual_qty_2 = get_qty_after_transaction() + + self.assertEquals(actual_qty_1 + 2, actual_qty_2) + + incoming_rate, stock_value_difference = frappe.db.get_value("Stock Ledger Entry", + {"voucher_type": "Delivery Note", "voucher_no": dn1.name}, + ["incoming_rate", "stock_value_difference"]) + + self.assertEquals(flt(incoming_rate, 3), abs(flt(outgoing_rate, 3))) + + gle_warehouse_amount = frappe.db.get_value("GL Entry", {"voucher_type": "Delivery Note", + "voucher_no": dn1.name, "account": "_Test Warehouse - _TC"}, "debit") + + self.assertEquals(gle_warehouse_amount, stock_value_difference) + + set_perpetual_inventory(0) + + def test_return_single_item_from_bundled_items(self): + set_perpetual_inventory() + + create_stock_reconciliation(item_code="_Test Item", target="_Test Warehouse - _TC", qty=50, rate=100) + create_stock_reconciliation(item_code="_Test Item Home Desktop 100", target="_Test Warehouse - _TC", + qty=50, rate=100) + + dn = create_delivery_note(item_code="_Test Product Bundle Item", qty=5, rate=500) + + # Qty after delivery + actual_qty_1 = get_qty_after_transaction() + self.assertEquals(actual_qty_1, 25) + + # outgoing_rate + outgoing_rate = frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Delivery Note", + "voucher_no": dn.name, "item_code": "_Test Item"}, "stock_value_difference") / 25 + + # return 'test item' from packed items + dn1 = create_delivery_note(is_return=1, return_against=dn.name, qty=-10, rate=500) + + # qty after return + actual_qty_2 = get_qty_after_transaction() + self.assertEquals(actual_qty_2, 35) + + # Check incoming rate for return entry + incoming_rate, stock_value_difference = frappe.db.get_value("Stock Ledger Entry", + {"voucher_type": "Delivery Note", "voucher_no": dn1.name}, + ["incoming_rate", "stock_value_difference"]) + + self.assertEquals(flt(incoming_rate, 3), abs(flt(outgoing_rate, 3))) + + # Check gl entry for warehouse + gle_warehouse_amount = frappe.db.get_value("GL Entry", {"voucher_type": "Delivery Note", + "voucher_no": dn1.name, "account": "_Test Warehouse - _TC"}, "debit") + + self.assertEquals(gle_warehouse_amount, stock_value_difference) + + set_perpetual_inventory(0) + + def test_return_entire_bundled_items(self): + set_perpetual_inventory() + + create_stock_reconciliation(item_code="_Test Item", target="_Test Warehouse - _TC", qty=50, rate=100) + create_stock_reconciliation(item_code="_Test Item Home Desktop 100", target="_Test Warehouse - _TC", + qty=50, rate=100) + + dn = create_delivery_note(item_code="_Test Product Bundle Item", qty=5, rate=500) + + # return bundled item + dn1 = create_delivery_note(item_code='_Test Product Bundle Item', is_return=1, + return_against=dn.name, qty=-2, rate=500) + + # qty after return + actual_qty = get_qty_after_transaction() + self.assertEquals(actual_qty, 35) + + # Check incoming rate for return entry + incoming_rate, stock_value_difference = frappe.db.get_value("Stock Ledger Entry", + {"voucher_type": "Delivery Note", "voucher_no": dn1.name}, + ["incoming_rate", "stock_value_difference"]) + + self.assertEquals(incoming_rate, 100) + + # Check gl entry for warehouse + gle_warehouse_amount = frappe.db.get_value("GL Entry", {"voucher_type": "Delivery Note", + "voucher_no": dn1.name, "account": "_Test Warehouse - _TC"}, "debit") + + self.assertEquals(gle_warehouse_amount, 1400) + + set_perpetual_inventory(0) def create_delivery_note(**args): dn = frappe.new_doc("Delivery Note") @@ -190,6 +298,8 @@ def create_delivery_note(**args): dn.company = args.company or "_Test Company" dn.customer = args.customer or "_Test Customer" dn.currency = args.currency or "INR" + dn.is_return = args.is_return + dn.return_against = args.return_against dn.append("items", { "item_code": args.item or args.item_code or "_Test Item", diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 58a133132f..b290a07247 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -13,7 +13,6 @@ from erpnext.stock.get_item_details import get_available_qty, get_default_cost_c from erpnext.manufacturing.doctype.bom.bom import validate_bom_no from erpnext.accounts.utils import validate_fiscal_year -class NotUpdateStockError(frappe.ValidationError): pass class IncorrectValuationRateError(frappe.ValidationError): pass class DuplicateEntryForProductionOrderError(frappe.ValidationError): pass class OperationsNotCompleteError(frappe.ValidationError): pass diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 95196ccf29..d283c3dd14 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -4,15 +4,12 @@ from __future__ import unicode_literals import frappe, unittest import frappe.defaults -from frappe.utils import flt, nowdate, nowtime, getdate +from frappe.utils import flt, nowdate, nowtime from erpnext.stock.doctype.serial_no.serial_no import * from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt \ - import set_perpetual_inventory, make_purchase_receipt + import set_perpetual_inventory from erpnext.stock.doctype.stock_ledger_entry.stock_ledger_entry import StockFreezeError -from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_invoice from erpnext.stock.stock_ledger import get_previous_sle -from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order, create_dn_against_so -from erpnext.stock.doctype.stock_entry.stock_entry import NotUpdateStockError from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import create_stock_reconciliation def get_sle(**args): @@ -303,263 +300,6 @@ class TestStockEntry(unittest.TestCase): self.assertEquals(expected_gl_entries[i][1], gle[1]) self.assertEquals(expected_gl_entries[i][2], gle[2]) - # def _test_sales_invoice_return(self, item_code, delivered_qty, returned_qty): - # from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice - # - # si = create_sales_invoice(item_code=item_code, qty=delivered_qty) - # - # se = make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=returned_qty, - # purpose="Sales Return", sales_invoice_no=si.name, do_not_save=True) - # self.assertRaises(NotUpdateStockError, se.insert) - # - # make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=200, incoming_rate=100) - # - # # check currency available qty in bin - # actual_qty_0 = get_qty_after_transaction() - # - # # insert a pos invoice with update stock - # si = create_sales_invoice(update_stock=1, item_code=item_code, qty=5) - # - # # check available bin qty after invoice submission - # actual_qty_1 = get_qty_after_transaction() - # - # self.assertEquals(actual_qty_0 - delivered_qty, actual_qty_1) - # - # # check if item is validated - # se = make_stock_entry(item_code="_Test Item Home Desktop 200", target="_Test Warehouse - _TC", - # qty=returned_qty, purpose="Sales Return", sales_invoice_no=si.name, do_not_save=True) - # - # self.assertRaises(frappe.DoesNotExistError, se.insert) - # - # # try again - # se = make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", - # qty=returned_qty, purpose="Sales Return", sales_invoice_no=si.name) - # - # # check if available qty is increased - # actual_qty_2 = get_qty_after_transaction() - # - # self.assertEquals(actual_qty_1 + returned_qty, actual_qty_2) - # - # return se - # - # def test_sales_invoice_return_of_non_packing_item(self): - # self._test_sales_invoice_return("_Test Item", 5, 2) - # - # def test_sales_invoice_return_of_packing_item(self): - # self._test_sales_invoice_return("_Test Product Bundle Item", 25, 20) - # - # def _test_delivery_note_return(self, item_code, delivered_qty, returned_qty): - # from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note - # - # from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice - # - # make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=50, incoming_rate=100) - # - # actual_qty_0 = get_qty_after_transaction() - # # make a delivery note based on this invoice - # dn = create_delivery_note(item_code="_Test Item", - # warehouse="_Test Warehouse - _TC", qty=delivered_qty) - # - # actual_qty_1 = get_qty_after_transaction() - # - # self.assertEquals(actual_qty_0 - delivered_qty, actual_qty_1) - # - # si = make_sales_invoice(dn.name) - # si.insert() - # si.submit() - # - # # insert and submit stock entry for sales return - # se = make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", - # qty=returned_qty, purpose="Sales Return", delivery_note_no=dn.name) - # - # actual_qty_2 = get_qty_after_transaction() - # self.assertEquals(actual_qty_1 + returned_qty, actual_qty_2) - # - # return se - # - # def test_delivery_note_return_of_non_packing_item(self): - # self._test_delivery_note_return("_Test Item", 5, 2) - # - # def test_delivery_note_return_of_packing_item(self): - # self._test_delivery_note_return("_Test Product Bundle Item", 25, 20) - # - # def _test_sales_return_jv(self, se): - # jv = make_return_jv(se.name) - # - # self.assertEqual(len(jv.get("accounts")), 2) - # self.assertEqual(jv.get("voucher_type"), "Credit Note") - # self.assertEqual(jv.get("posting_date"), getdate(se.posting_date)) - # self.assertEqual(jv.get("accounts")[0].get("account"), "Debtors - _TC") - # self.assertEqual(jv.get("accounts")[0].get("party_type"), "Customer") - # self.assertEqual(jv.get("accounts")[0].get("party"), "_Test Customer") - # self.assertEqual(jv.get("accounts")[1].get("account"), "Sales - _TC") - # - # def test_make_return_jv_for_sales_invoice_non_packing_item(self): - # se = self._test_sales_invoice_return("_Test Item", 5, 2) - # self._test_sales_return_jv(se) - # - # def test_make_return_jv_for_sales_invoice_packing_item(self): - # se = self._test_sales_invoice_return("_Test Product Bundle Item", 25, 20) - # self._test_sales_return_jv(se) - # - # def test_make_return_jv_for_delivery_note_non_packing_item(self): - # se = self._test_delivery_note_return("_Test Item", 5, 2) - # self._test_sales_return_jv(se) - # - # se = self._test_delivery_note_return_against_sales_order("_Test Item", 5, 2) - # self._test_sales_return_jv(se) - # - # def test_make_return_jv_for_delivery_note_packing_item(self): - # se = self._test_delivery_note_return("_Test Product Bundle Item", 25, 20) - # self._test_sales_return_jv(se) - # - # se = self._test_delivery_note_return_against_sales_order("_Test Product Bundle Item", 25, 20) - # self._test_sales_return_jv(se) - # - # def _test_delivery_note_return_against_sales_order(self, item_code, delivered_qty, returned_qty): - # from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice - # - # actual_qty_0 = get_qty_after_transaction() - # - # so = make_sales_order(qty=50) - # - # dn = create_dn_against_so(so.name, delivered_qty) - # - # actual_qty_1 = get_qty_after_transaction() - # self.assertEquals(actual_qty_0 - delivered_qty, actual_qty_1) - # - # si = make_sales_invoice(so.name) - # si.insert() - # si.submit() - # - # # insert and submit stock entry for sales return - # se = make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", - # qty=returned_qty, purpose="Sales Return", delivery_note_no=dn.name) - # - # actual_qty_2 = get_qty_after_transaction() - # self.assertEquals(actual_qty_1 + returned_qty, actual_qty_2) - # - # return se - # - # def test_purchase_receipt_return(self): - # actual_qty_0 = get_qty_after_transaction() - # - # # submit purchase receipt - # pr = make_purchase_receipt(item_code="_Test Item", warehouse="_Test Warehouse - _TC", qty=5) - # - # actual_qty_1 = get_qty_after_transaction() - # - # self.assertEquals(actual_qty_0 + 5, actual_qty_1) - # - # pi_doc = make_purchase_invoice(pr.name) - # - # pi = frappe.get_doc(pi_doc) - # pi.posting_date = pr.posting_date - # pi.credit_to = "_Test Payable - _TC" - # for d in pi.get("items"): - # d.expense_account = "_Test Account Cost for Goods Sold - _TC" - # d.cost_center = "_Test Cost Center - _TC" - # - # for d in pi.get("taxes"): - # d.cost_center = "_Test Cost Center - _TC" - # - # pi.insert() - # pi.submit() - # - # # submit purchase return - # se = make_stock_entry(item_code="_Test Item", source="_Test Warehouse - _TC", - # qty=5, purpose="Purchase Return", purchase_receipt_no=pr.name) - # - # actual_qty_2 = get_qty_after_transaction() - # - # self.assertEquals(actual_qty_1 - 5, actual_qty_2) - # - # return se, pr.name - # - # def test_over_stock_return(self): - # from erpnext.stock.doctype.stock_entry.stock_entry import StockOverReturnError - # - # # out of 10, 5 gets returned - # prev_se, pr_docname = self.test_purchase_receipt_return() - # - # se = make_stock_entry(item_code="_Test Item", source="_Test Warehouse - _TC", - # qty=6, purpose="Purchase Return", purchase_receipt_no=pr_docname, do_not_save=True) - # - # self.assertRaises(StockOverReturnError, se.insert) - # - # def _test_purchase_return_jv(self, se): - # jv = make_return_jv(se.name) - # - # self.assertEqual(len(jv.get("accounts")), 2) - # self.assertEqual(jv.get("voucher_type"), "Debit Note") - # self.assertEqual(jv.get("posting_date"), getdate(se.posting_date)) - # self.assertEqual(jv.get("accounts")[0].get("account"), "_Test Payable - _TC") - # self.assertEqual(jv.get("accounts")[0].get("party"), "_Test Supplier") - # self.assertEqual(jv.get("accounts")[1].get("account"), "_Test Account Cost for Goods Sold - _TC") - # - # def test_make_return_jv_for_purchase_receipt(self): - # se, pr_name = self.test_purchase_receipt_return() - # self._test_purchase_return_jv(se) - # - # se, pr_name = self._test_purchase_return_return_against_purchase_order() - # self._test_purchase_return_jv(se) - # - # def _test_purchase_return_return_against_purchase_order(self): - # - # actual_qty_0 = get_qty_after_transaction() - # - # from erpnext.buying.doctype.purchase_order.test_purchase_order \ - # import test_records as purchase_order_test_records - # - # from erpnext.buying.doctype.purchase_order.purchase_order import \ - # make_purchase_receipt, make_purchase_invoice - # - # # submit purchase receipt - # po = frappe.copy_doc(purchase_order_test_records[0]) - # po.transaction_date = nowdate() - # po.is_subcontracted = None - # po.get("items")[0].item_code = "_Test Item" - # po.get("items")[0].rate = 50 - # po.insert() - # po.submit() - # - # pr_doc = make_purchase_receipt(po.name) - # - # pr = frappe.get_doc(pr_doc) - # pr.posting_date = po.transaction_date - # pr.insert() - # pr.submit() - # - # actual_qty_1 = get_qty_after_transaction() - # - # self.assertEquals(actual_qty_0 + 10, actual_qty_1) - # - # pi_doc = make_purchase_invoice(po.name) - # - # pi = frappe.get_doc(pi_doc) - # pi.posting_date = pr.posting_date - # pi.credit_to = "_Test Payable - _TC" - # for d in pi.get("items"): - # d.expense_account = "_Test Account Cost for Goods Sold - _TC" - # d.cost_center = "_Test Cost Center - _TC" - # for d in pi.get("taxes"): - # d.cost_center = "_Test Cost Center - _TC" - # - # pi.run_method("calculate_taxes_and_totals") - # pi.bill_no = "NA" - # pi.insert() - # pi.submit() - # - # # submit purchase return - # se = make_stock_entry(item_code="_Test Item", source="_Test Warehouse - _TC", - # qty=5, purpose="Purchase Return", purchase_receipt_no=pr.name) - # - # actual_qty_2 = get_qty_after_transaction() - # - # self.assertEquals(actual_qty_1 - 5, actual_qty_2) - # - # return se, pr.name - def test_serial_no_not_reqd(self): se = frappe.copy_doc(test_records[0]) se.get("items")[0].serial_no = "ABCD" From 061f7079edb362030475cb439716326639816b67 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 20 Jul 2015 19:18:59 +0530 Subject: [PATCH 32/40] Test case for purchase return --- .../purchase_receipt/test_purchase_receipt.py | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 141bcd4d0d..9cdbc2c995 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -119,6 +119,38 @@ class TestPurchaseReceipt(unittest.TestCase): for serial_no in rejected_serial_nos: self.assertEquals(frappe.db.get_value("Serial No", serial_no, "warehouse"), pr.get("items")[0].rejected_warehouse) + + def test_purchase_return(self): + set_perpetual_inventory() + + pr = make_purchase_receipt() + + return_pr = make_purchase_receipt(is_return=1, return_against=pr.name, qty=-2) + + + # check sle + outgoing_rate = frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Purchase Receipt", + "voucher_no": return_pr.name}, "outgoing_rate") + + self.assertEqual(outgoing_rate, 50) + + + # check gl entries for return + gl_entries = get_gl_entries("Purchase Receipt", return_pr.name) + + self.assertTrue(gl_entries) + + expected_values = { + "_Test Warehouse - _TC": [0.0, 100.0], + "Stock Received But Not Billed - _TC": [100.0, 0.0], + } + + for gle in gl_entries: + self.assertEquals(expected_values[gle.account][0], gle.debit) + self.assertEquals(expected_values[gle.account][1], gle.credit) + + set_perpetual_inventory(0) + def get_gl_entries(voucher_type, voucher_no): return frappe.db.sql("""select account, debit, credit @@ -142,6 +174,8 @@ def make_purchase_receipt(**args): pr.is_subcontracted = args.is_subcontracted or "No" pr.supplier_warehouse = "_Test Warehouse 1 - _TC" pr.currency = args.currency or "INR" + pr.is_return = args.is_return + pr.return_against = args.return_against pr.append("items", { "item_code": args.item or args.item_code or "_Test Item", From b74999da82e5a1fc29fcd9bf9d4d784a55d92d41 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 22 Jul 2015 13:12:26 +0530 Subject: [PATCH 33/40] [testcase] Testcase for return purchase invoice --- .../purchase_invoice/test_purchase_invoice.py | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 7f46b083d8..5f3d4c8a04 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -275,5 +275,58 @@ class TestPurchaseInvoice(unittest.TestCase): purchase_invoice.cancel() self.assertEqual(frappe.db.get_value("Project", "_Test Project", "total_purchase_cost"), 0) + def test_return_purchase_invoice(self): + set_perpetual_inventory() + + pi = make_purchase_invoice() + + return_pi = make_purchase_invoice(is_return=1, return_against=pi.name, qty=-2) + + + # check gl entries for return + gl_entries = frappe.db.sql("""select account, debit, credit + from `tabGL Entry` where voucher_type=%s and voucher_no=%s + order by account desc""", ("Purchase Invoice", return_pi.name), as_dict=1) + + self.assertTrue(gl_entries) + + expected_values = { + "Creditors - _TC": [100.0, 0.0], + "Stock Received But Not Billed - _TC": [0.0, 100.0], + } + + for gle in gl_entries: + self.assertEquals(expected_values[gle.account][0], gle.debit) + self.assertEquals(expected_values[gle.account][1], gle.credit) + + set_perpetual_inventory(0) + +def make_purchase_invoice(**args): + pi = frappe.new_doc("Purchase Invoice") + args = frappe._dict(args) + if args.posting_date: + pi.posting_date = args.posting_date + if args.posting_time: + pi.posting_time = args.posting_time + pi.company = args.company or "_Test Company" + pi.supplier = args.supplier or "_Test Supplier" + pi.currency = args.currency or "INR" + pi.is_return = args.is_return + pi.return_against = args.return_against + + pi.append("items", { + "item_code": args.item or args.item_code or "_Test Item", + "warehouse": args.warehouse or "_Test Warehouse - _TC", + "qty": args.qty or 5, + "rate": args.rate or 50, + "conversion_factor": 1.0, + "serial_no": args.serial_no, + "stock_uom": "_Test UOM" + }) + if not args.do_not_save: + pi.insert() + if not args.do_not_submit: + pi.submit() + return pi test_records = frappe.get_test_records('Purchase Invoice') From 04d244a360a016c4ccbaf49cc21c0db0bb577494 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 22 Jul 2015 17:47:49 +0530 Subject: [PATCH 34/40] Credit Note print format --- .../doctype/sales_invoice/sales_invoice.js | 4 ++- .../doctype/sales_invoice/sales_invoice.json | 4 +-- .../print_format/credit_note/credit_note.json | 34 +++++++++---------- .../__init__.py | 0 .../credit_note___negative_invoice.json | 17 ++++++++++ erpnext/controllers/accounts_controller.py | 3 ++ erpnext/controllers/selling_controller.py | 7 ++-- 7 files changed, 45 insertions(+), 24 deletions(-) create mode 100644 erpnext/accounts/print_format/credit_note___negative_invoice/__init__.py create mode 100644 erpnext/accounts/print_format/credit_note___negative_invoice/credit_note___negative_invoice.json diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 8daf3f667f..d3f142e12f 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -40,7 +40,9 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte this._super(); cur_frm.dashboard.reset(); - + + this.frm.toggle_reqd("due_date", !this.frm.doc.is_return); + if(doc.docstatus==1) { cur_frm.add_custom_button('View Ledger', function() { frappe.route_options = { diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index b983d99024..bc6a2275f4 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -156,7 +156,7 @@ "oldfieldtype": "Date", "permlevel": 0, "read_only": 0, - "reqd": 1, + "reqd": 0, "search_index": 0 }, { @@ -1275,7 +1275,7 @@ "icon": "icon-file-text", "idx": 1, "is_submittable": 1, - "modified": "2015-07-17 13:29:36.922418", + "modified": "2015-07-22 16:53:52.995407", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", diff --git a/erpnext/accounts/print_format/credit_note/credit_note.json b/erpnext/accounts/print_format/credit_note/credit_note.json index de405e694f..863d4aa607 100644 --- a/erpnext/accounts/print_format/credit_note/credit_note.json +++ b/erpnext/accounts/print_format/credit_note/credit_note.json @@ -1,19 +1,19 @@ { - "creation": "2014-08-28 11:11:39.796473", - "disabled": 0, - "doc_type": "Journal Entry", - "docstatus": 0, - "doctype": "Print Format", - "html": "{%- from \"templates/print_formats/standard_macros.html\" import add_header -%}\n\n
\n {%- if not doc.get(\"print_heading\") and not doc.get(\"select_print_heading\") \n and doc.set(\"select_print_heading\", _(\"Credit Note\")) -%}{%- endif -%}\n {{ add_header(0, 1, doc, letter_head, no_letterhead) }}\n\n {%- for label, value in (\n (_(\"Credit To\"), doc.pay_to_recd_from),\n (_(\"Date\"), frappe.utils.formatdate(doc.voucher_date)),\n (_(\"Amount\"), \"\" + doc.get_formatted(\"total_amount\") + \"
\" + (doc.total_amount_in_words or \"\") + \"
\"),\n (_(\"Remarks\"), doc.remark)\n ) -%}\n\n
\n
\n
{{ value }}
\n
\n\n {%- endfor -%}\n\n
\n
\n

\n {{ _(\"For\") }} {{ doc.company }},
\n
\n
\n
\n {{ _(\"Authorized Signatory\") }}\n

\n
\n\n\n", - "idx": 2, - "modified": "2015-01-12 11:02:25.716825", - "modified_by": "Administrator", - "module": "Accounts", - "name": "Credit Note", - "owner": "Administrator", - "parent": "Journal Entry", - "parentfield": "__print_formats", - "parenttype": "DocType", - "print_format_type": "Server", + "creation": "2014-08-28 11:11:39.796473", + "custom_format": 0, + "disabled": 0, + "doc_type": "Journal Entry", + "docstatus": 0, + "doctype": "Print Format", + "html": "{%- from \"templates/print_formats/standard_macros.html\" import add_header -%}\n\n
\n {%- if not doc.get(\"print_heading\") and not doc.get(\"select_print_heading\") \n and doc.set(\"select_print_heading\", _(\"Credit Note\")) -%}{%- endif -%}\n {{ add_header(0, 1, doc, letter_head, no_letterhead) }}\n\n {%- for label, value in (\n (_(\"Credit To\"), doc.pay_to_recd_from),\n (_(\"Date\"), frappe.utils.formatdate(doc.voucher_date)),\n (_(\"Amount\"), \"\" + doc.get_formatted(\"total_amount\") + \"
\" + (doc.total_amount_in_words or \"\") + \"
\"),\n (_(\"Remarks\"), doc.remark)\n ) -%}\n\n
\n
\n
{{ value }}
\n
\n\n {%- endfor -%}\n\n
\n
\n

\n {{ _(\"For\") }} {{ doc.company }},
\n
\n
\n
\n {{ _(\"Authorized Signatory\") }}\n

\n
\n\n\n", + "idx": 2, + "modified": "2015-07-22 17:42:01.560817", + "modified_by": "Administrator", + "name": "Credit Note", + "owner": "Administrator", + "parent": "Journal Entry", + "parentfield": "__print_formats", + "parenttype": "DocType", + "print_format_type": "Server", "standard": "Yes" -} +} \ No newline at end of file diff --git a/erpnext/accounts/print_format/credit_note___negative_invoice/__init__.py b/erpnext/accounts/print_format/credit_note___negative_invoice/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/accounts/print_format/credit_note___negative_invoice/credit_note___negative_invoice.json b/erpnext/accounts/print_format/credit_note___negative_invoice/credit_note___negative_invoice.json new file mode 100644 index 0000000000..e7d7eabaff --- /dev/null +++ b/erpnext/accounts/print_format/credit_note___negative_invoice/credit_note___negative_invoice.json @@ -0,0 +1,17 @@ +{ + "creation": "2015-07-22 17:45:22.220567", + "custom_format": 1, + "disabled": 0, + "doc_type": "Sales Invoice", + "docstatus": 0, + "doctype": "Print Format", + "font": "Default", + "html": "\n\n

\n\t{{ doc.company }}
\n\t{{ doc.select_print_heading or _(\"Credit Note\") }}
\n

\n\n
\n\n{%- for label, value in (\n (_(\"Receipt No\"), doc.name),\n (_(\"Date\"), doc.get_formatted(\"posting_date\")),\n\t(_(\"Customer\"), doc.customer_name),\n (_(\"Amount\"), \"\" + doc.get_formatted(\"grand_total\", absolute_value=True) + \"
\" + (doc.in_words or \"\")),\n\t(_(\"Against\"), doc.return_against),\n (_(\"Remarks\"), doc.remarks)\n) -%}\n\n\t\t
\n\t\t
\n\t\t
{{ value }}
\n\t\t
\n{%- endfor -%}\n\n
\n
\n

\n {{ _(\"For\") }} {{ doc.company }},
\n
\n
\n
\n {{ _(\"Authorized Signatory\") }}\n

", + "modified": "2015-07-22 17:45:22.220567", + "modified_by": "Administrator", + "name": "Credit Note - Negative Invoice", + "owner": "Administrator", + "print_format_builder": 0, + "print_format_type": "Server", + "standard": "Yes" +} \ No newline at end of file diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index eb7b73f7f2..c094771189 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -173,6 +173,9 @@ class AccountsController(TransactionBase): def validate_due_date(self): from erpnext.accounts.party import validate_due_date if self.doctype == "Sales Invoice": + if not self.due_date: + frappe.throw(_("Due Date is mandatory")) + validate_due_date(self.posting_date, self.due_date, "Customer", self.customer, self.company) elif self.doctype == "Purchase Invoice": validate_due_date(self.posting_date, self.due_date, "Supplier", self.supplier, self.company) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 01ef605b63..5ad0a25af3 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -110,15 +110,14 @@ class SellingController(StockController): from frappe.utils import money_in_words company_currency = get_company_currency(self.company) - disable_rounded_total = cint(frappe.db.get_value("Global Defaults", None, - "disable_rounded_total")) + disable_rounded_total = cint(frappe.db.get_value("Global Defaults", None, "disable_rounded_total")) if self.meta.get_field("base_in_words"): self.base_in_words = money_in_words(disable_rounded_total and - self.base_grand_total or self.base_rounded_total, company_currency) + abs(self.base_grand_total) or abs(self.base_rounded_total), company_currency) if self.meta.get_field("in_words"): self.in_words = money_in_words(disable_rounded_total and - self.grand_total or self.rounded_total, self.currency) + abs(self.grand_total) or abs(self.rounded_total), self.currency) def calculate_commission(self): if self.meta.get_field("commission_rate"): From f061877b4fbfa59e65f3ec4459e01825b9d791b7 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 22 Jul 2015 18:49:52 +0530 Subject: [PATCH 35/40] [fix] Stock Entry permissions --- erpnext/stock/doctype/stock_entry/stock_entry.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.json b/erpnext/stock/doctype/stock_entry/stock_entry.json index ca84db9e31..3c39d42ff6 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.json +++ b/erpnext/stock/doctype/stock_entry/stock_entry.json @@ -678,7 +678,7 @@ "is_submittable": 1, "issingle": 0, "max_attachments": 0, - "modified": "2015-07-17 15:41:00.980883", + "modified": "2015-07-22 18:47:20.328749", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry", @@ -695,7 +695,7 @@ "print": 1, "read": 1, "report": 1, - "role": "Material User", + "role": "Stock User", "share": 1, "submit": 1, "write": 1 @@ -741,7 +741,7 @@ "print": 1, "read": 1, "report": 1, - "role": "Material Manager", + "role": "Stock Manager", "share": 1, "submit": 1, "write": 1 From 3cf67a462b1994b0f4f47a636ae9e01a230e6d0d Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 24 Jul 2015 13:26:36 +0530 Subject: [PATCH 36/40] Cleanup and test cases for serialized item --- .../purchase_invoice/purchase_invoice.js | 12 +- .../purchase_invoice/purchase_invoice.json | 6 +- .../purchase_invoice/purchase_invoice.py | 2 +- .../doctype/sales_invoice/sales_invoice.js | 9 +- .../doctype/sales_invoice/sales_invoice.json | 6 +- .../doctype/sales_invoice/sales_invoice.py | 2 +- .../doctype/purchase_order/purchase_order.js | 33 ++--- erpnext/controllers/accounts_controller.py | 100 +------------ .../controllers/sales_and_purchase_return.py | 138 ++++++++++++++++++ .../doctype/sales_order/sales_order.js | 20 +-- .../doctype/delivery_note/delivery_note.js | 7 +- .../doctype/delivery_note/delivery_note.json | 6 +- .../doctype/delivery_note/delivery_note.py | 2 +- .../delivery_note/test_delivery_note.py | 42 +++++- .../purchase_receipt/purchase_receipt.js | 8 +- .../purchase_receipt/purchase_receipt.json | 6 +- .../purchase_receipt/purchase_receipt.py | 2 +- .../purchase_receipt/test_purchase_receipt.py | 31 +++- .../stock/doctype/serial_no/serial_no.json | 4 +- erpnext/stock/doctype/serial_no/serial_no.py | 18 +-- erpnext/utilities/transaction_base.py | 39 +---- 21 files changed, 275 insertions(+), 218 deletions(-) create mode 100644 erpnext/controllers/sales_and_purchase_return.py diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index 548abb7cbf..6a02706532 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -21,12 +21,10 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({ // Show / Hide button if(doc.docstatus==1 && doc.outstanding_amount > 0) - this.frm.add_custom_button(__('Make Payment Entry'), this.make_bank_entry, - frappe.boot.doctype_icons["Journal Entry"]); + this.frm.add_custom_button(__('Make Payment Entry'), this.make_bank_entry); if(doc.docstatus==1) { - cur_frm.add_custom_button(__('Make Purchase Return'), this.make_purchase_return, - frappe.boot.doctype_icons["Purchase Invoice"]); + cur_frm.add_custom_button(__('Make Purchase Return'), this.make_purchase_return); cur_frm.add_custom_button(__('View Ledger'), function() { frappe.route_options = { @@ -37,7 +35,7 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({ group_by_voucher: 0 }; frappe.set_route("query-report", "General Ledger"); - }, "icon-table"); + }); } if(doc.docstatus===0) { @@ -54,7 +52,7 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({ company: cur_frm.doc.company } }) - }, "icon-download", "btn-default"); + }); cur_frm.add_custom_button(__('From Purchase Receipt'), function() { @@ -67,7 +65,7 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({ company: cur_frm.doc.company } }) - }, "icon-download", "btn-default"); + }); } }, diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index c5797162db..f8101dc7af 100755 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -158,7 +158,7 @@ "fieldname": "is_return", "fieldtype": "Check", "label": "Is Return", - "no_copy": 1, + "no_copy": 0, "permlevel": 0, "precision": "", "print_hide": 1, @@ -169,7 +169,7 @@ "fieldname": "return_against", "fieldtype": "Link", "label": "Return Against Purchase Invoice", - "no_copy": 1, + "no_copy": 0, "options": "Purchase Invoice", "permlevel": 0, "precision": "", @@ -962,7 +962,7 @@ "icon": "icon-file-text", "idx": 1, "is_submittable": 1, - "modified": "2015-07-17 14:09:19.666457", + "modified": "2015-07-24 11:49:59.762109", "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 b34f8452e2..006470f860 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -412,5 +412,5 @@ def get_expense_account(doctype, txt, searchfield, start, page_len, filters): @frappe.whitelist() def make_purchase_return(source_name, target_doc=None): - from erpnext.utilities.transaction_base import make_return_doc + from erpnext.controllers.sales_and_purchase_return import make_return_doc return make_return_doc("Purchase Invoice", source_name, target_doc) \ No newline at end of file diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index d3f142e12f..fdc1a58f6b 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -53,7 +53,7 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte group_by_voucher: 0 }; frappe.set_route("query-report", "General Ledger"); - }, "icon-table"); + }); if(cint(doc.update_stock)!=1) { // show Make Delivery Note button only if Sales Invoice is not created from Delivery Note @@ -64,16 +64,15 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte }); if(!from_delivery_note) { - cur_frm.add_custom_button(__('Make Delivery'), cur_frm.cscript['Make Delivery Note'], "icon-truck") + cur_frm.add_custom_button(__('Make Delivery'), cur_frm.cscript['Make Delivery Note']) } } if(doc.outstanding_amount!=0 && !cint(doc.is_return)) { - cur_frm.add_custom_button(__('Make Payment Entry'), cur_frm.cscript.make_bank_entry, "icon-money"); + cur_frm.add_custom_button(__('Make Payment Entry'), cur_frm.cscript.make_bank_entry); } - cur_frm.add_custom_button(__('Make Sales Return'), this.make_sales_return, - frappe.boot.doctype_icons["Sales Invoice"]); + cur_frm.add_custom_button(__('Make Sales Return'), this.make_sales_return); } // Show buttons only when pos view is active diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index bc6a2275f4..cd70a46c5d 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -173,7 +173,7 @@ "fieldname": "is_return", "fieldtype": "Check", "label": "Is Return", - "no_copy": 1, + "no_copy": 0, "permlevel": 0, "precision": "", "print_hide": 1, @@ -184,7 +184,7 @@ "fieldname": "return_against", "fieldtype": "Link", "label": "Return Against Sales Invoice", - "no_copy": 1, + "no_copy": 0, "options": "Sales Invoice", "permlevel": 0, "precision": "", @@ -1275,7 +1275,7 @@ "icon": "icon-file-text", "idx": 1, "is_submittable": 1, - "modified": "2015-07-22 16:53:52.995407", + "modified": "2015-07-24 11:48:07.544569", "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 dd60085b85..5a9ccea1d2 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -663,5 +663,5 @@ def make_delivery_note(source_name, target_doc=None): @frappe.whitelist() def make_sales_return(source_name, target_doc=None): - from erpnext.utilities.transaction_base import make_return_doc + from erpnext.controllers.sales_and_purchase_return import make_return_doc return make_return_doc("Sales Invoice", source_name, target_doc) \ No newline at end of file diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index 20edbca0e5..6049810213 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -11,39 +11,32 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend( this._super(); // this.frm.dashboard.reset(); - if(doc.docstatus == 1 && doc.status != 'Stopped'){ - // cur_frm.dashboard.add_progress(cint(doc.per_received) + __("% Received"), - // doc.per_received); - // cur_frm.dashboard.add_progress(cint(doc.per_billed) + __("% Billed"), - // doc.per_billed); - + if(doc.docstatus == 1 && doc.status != 'Stopped') { if(flt(doc.per_received, 2) < 100) { - cur_frm.add_custom_button(__('Make Purchase Receipt'), - this.make_purchase_receipt); + cur_frm.add_custom_button(__('Make Purchase Receipt'), this.make_purchase_receipt); + if(doc.is_subcontracted==="Yes") { - cur_frm.add_custom_button(__('Transfer Material to Supplier'), - function() { me.make_stock_entry() }); + cur_frm.add_custom_button(__('Transfer Material to Supplier'), this.make_stock_entry); } } if(flt(doc.per_billed, 2) < 100) - cur_frm.add_custom_button(__('Make Invoice'), this.make_purchase_invoice, - frappe.boot.doctype_icons["Purchase Invoice"]); + cur_frm.add_custom_button(__('Make Invoice'), this.make_purchase_invoice); + if(flt(doc.per_billed, 2) < 100 || doc.per_received < 100) - cur_frm.add_custom_button(__('Stop'), cur_frm.cscript['Stop Purchase Order'], - "icon-exclamation", "btn-default"); + cur_frm.add_custom_button(__('Stop'), cur_frm.cscript['Stop Purchase Order']); } else if(doc.docstatus===0) { cur_frm.cscript.add_from_mappers(); } if(doc.docstatus == 1 && doc.status == 'Stopped') - cur_frm.add_custom_button(__('Unstop Purchase Order'), - cur_frm.cscript['Unstop Purchase Order'], "icon-check"); + cur_frm.add_custom_button(__('Unstop Purchase Order'), cur_frm.cscript['Unstop Purchase Order']); }, make_stock_entry: function() { var items = $.map(cur_frm.doc.items, function(d) { return d.bom ? d.item_code : false; }), - me = this; + var me = this; + if(items.length===1) { me._make_stock_entry(items[0]); return; @@ -96,7 +89,7 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend( company: cur_frm.doc.company } }) - }, "icon-download", "btn-default" + } ); cur_frm.add_custom_button(__('From Supplier Quotation'), @@ -110,7 +103,7 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend( company: cur_frm.doc.company } }) - }, "icon-download", "btn-default" + } ); cur_frm.add_custom_button(__('For Supplier'), @@ -122,7 +115,7 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend( docstatus: ["!=", 2], } }) - }, "icon-download", "btn-default" + } ); }, diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index c094771189..7610042b5f 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -4,14 +4,12 @@ from __future__ import unicode_literals import frappe from frappe import _, throw -from frappe.utils import today, flt, cint, format_datetime, get_datetime +from frappe.utils import today, flt, cint from erpnext.setup.utils import get_company_currency, get_exchange_rate from erpnext.accounts.utils import get_fiscal_year, validate_fiscal_year from erpnext.utilities.transaction_base import TransactionBase from erpnext.controllers.recurring_document import convert_to_recurring, validate_recurring_document - -class StockOverReturnError(frappe.ValidationError): pass - +from erpnext.controllers.sales_and_purchase_return import validate_return class AccountsController(TransactionBase): def validate(self): @@ -23,7 +21,7 @@ class AccountsController(TransactionBase): if not self.meta.get_field("is_return") or not self.is_return: self.validate_value("base_grand_total", ">=", 0) - self.validate_return_doc() + validate_return(self) self.set_total_in_words() if self.doctype in ("Sales Invoice", "Purchase Invoice") and not self.is_return: @@ -58,98 +56,6 @@ class AccountsController(TransactionBase): self.fiscal_year = get_fiscal_year(self.get(fieldname))[0] break - def validate_return_doc(self): - if not self.meta.get_field("is_return") or not self.is_return: - return - - self.validate_return_against() - self.validate_returned_items() - - def validate_return_against(self): - if not self.return_against: - frappe.throw(_("{0} is mandatory for Return").format(self.meta.get_label("return_against"))) - else: - filters = {"doctype": self.doctype, "docstatus": 1, "company": self.company} - if self.meta.get_field("customer"): - filters["customer"] = self.customer - elif self.meta.get_field("supplier"): - filters["supplier"] = self.supplier - - if not frappe.db.exists(filters): - frappe.throw(_("Invalid {0}: {1}") - .format(self.meta.get_label("return_against"), self.return_against)) - else: - ref_doc = frappe.get_doc(self.doctype, self.return_against) - - # validate posting date time - return_posting_datetime = "%s %s" % (self.posting_date, self.get("posting_time") or "00:00:00") - ref_posting_datetime = "%s %s" % (ref_doc.posting_date, ref_doc.get("posting_time") or "00:00:00") - - if get_datetime(return_posting_datetime) < get_datetime(ref_posting_datetime): - frappe.throw(_("Posting timestamp must be after {0}").format(format_datetime(ref_posting_datetime))) - - # validate same exchange rate - if self.conversion_rate != ref_doc.conversion_rate: - frappe.throw(_("Exchange Rate must be same as {0} {1} ({2})") - .format(self.doctype, self.return_against, ref_doc.conversion_rate)) - - # validate update stock - if self.doctype == "Sales Invoice" and self.update_stock \ - and not frappe.db.get_value("Sales Invoice", self.return_against, "update_stock"): - frappe.throw(_("'Update Stock' can not be checked because items are not delivered via {0}") - .format(self.return_against)) - - def validate_returned_items(self): - valid_items = frappe._dict() - for d in frappe.db.sql("""select item_code, sum(qty) as qty, rate from `tab{0} Item` - where parent = %s group by item_code""".format(self.doctype), self.return_against, as_dict=1): - valid_items.setdefault(d.item_code, d) - - if self.doctype in ("Delivery Note", "Sales Invoice"): - for d in frappe.db.sql("""select item_code, sum(qty) as qty from `tabPacked Item` - where parent = %s group by item_code""".format(self.doctype), self.return_against, as_dict=1): - valid_items.setdefault(d.item_code, d) - - already_returned_items = self.get_already_returned_items() - - items_returned = False - for d in self.get("items"): - if flt(d.qty) < 0: - if d.item_code not in valid_items: - frappe.throw(_("Row # {0}: Returned Item {1} does not exists in {2} {3}") - .format(d.idx, d.item_code, self.doctype, self.return_against)) - else: - ref = valid_items.get(d.item_code, frappe._dict()) - already_returned_qty = flt(already_returned_items.get(d.item_code)) - max_return_qty = flt(ref.qty) - already_returned_qty - - if already_returned_qty >= ref.qty: - frappe.throw(_("Item {0} has already been returned").format(d.item_code), StockOverReturnError) - elif abs(d.qty) > max_return_qty: - frappe.throw(_("Row # {0}: Cannot return more than {1} for Item {2}") - .format(d.idx, ref.qty, d.item_code), StockOverReturnError) - elif ref.rate and flt(d.rate) != ref.rate: - frappe.throw(_("Row # {0}: Rate must be same as {1} {2}") - .format(d.idx, self.doctype, self.return_against)) - - - items_returned = True - - if not items_returned: - frappe.throw(_("Atleast one item should be entered with negative quantity in return document")) - - def get_already_returned_items(self): - return frappe._dict(frappe.db.sql(""" - select - child.item_code, sum(abs(child.qty)) as qty - from - `tab{0} Item` child, `tab{1}` par - where - child.parent = par.name and par.docstatus = 1 - and ifnull(par.is_return, 0) = 1 and par.return_against = %s and child.qty < 0 - group by item_code - """.format(self.doctype, self.doctype), self.return_against)) - def calculate_taxes_and_totals(self): from erpnext.controllers.taxes_and_totals import calculate_taxes_and_totals calculate_taxes_and_totals(self) diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py new file mode 100644 index 0000000000..899d1c1165 --- /dev/null +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -0,0 +1,138 @@ +# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals +import frappe +from frappe import _ +from frappe.utils import flt, get_datetime, format_datetime + +class StockOverReturnError(frappe.ValidationError): pass + + +def validate_return(doc): + if not doc.meta.get_field("is_return") or not doc.is_return: + return + + validate_return_against(doc) + validate_returned_items(doc) + +def validate_return_against(doc): + if not doc.return_against: + frappe.throw(_("{0} is mandatory for Return").format(doc.meta.get_label("return_against"))) + else: + filters = {"doctype": doc.doctype, "docstatus": 1, "company": doc.company} + if doc.meta.get_field("customer"): + filters["customer"] = doc.customer + elif doc.meta.get_field("supplier"): + filters["supplier"] = doc.supplier + + if not frappe.db.exists(filters): + frappe.throw(_("Invalid {0}: {1}") + .format(doc.meta.get_label("return_against"), doc.return_against)) + else: + ref_doc = frappe.get_doc(doc.doctype, doc.return_against) + + # validate posting date time + return_posting_datetime = "%s %s" % (doc.posting_date, doc.get("posting_time") or "00:00:00") + ref_posting_datetime = "%s %s" % (ref_doc.posting_date, ref_doc.get("posting_time") or "00:00:00") + + if get_datetime(return_posting_datetime) < get_datetime(ref_posting_datetime): + frappe.throw(_("Posting timestamp must be after {0}").format(format_datetime(ref_posting_datetime))) + + # validate same exchange rate + if doc.conversion_rate != ref_doc.conversion_rate: + frappe.throw(_("Exchange Rate must be same as {0} {1} ({2})") + .format(doc.doctype, doc.return_against, ref_doc.conversion_rate)) + + # validate update stock + if doc.doctype == "Sales Invoice" and doc.update_stock and not ref_doc.update_stock: + frappe.throw(_("'Update Stock' can not be checked because items are not delivered via {0}") + .format(doc.return_against)) + +def validate_returned_items(doc): + valid_items = frappe._dict() + for d in frappe.db.sql("""select item_code, sum(qty) as qty, rate from `tab{0} Item` + where parent = %s group by item_code""".format(doc.doctype), doc.return_against, as_dict=1): + valid_items.setdefault(d.item_code, d) + + if doc.doctype in ("Delivery Note", "Sales Invoice"): + for d in frappe.db.sql("""select item_code, sum(qty) as qty from `tabPacked Item` + where parent = %s group by item_code""".format(doc.doctype), doc.return_against, as_dict=1): + valid_items.setdefault(d.item_code, d) + + already_returned_items = get_already_returned_items(doc) + + items_returned = False + for d in doc.get("items"): + if flt(d.qty) < 0: + if d.item_code not in valid_items: + frappe.throw(_("Row # {0}: Returned Item {1} does not exists in {2} {3}") + .format(d.idx, d.item_code, doc.doctype, doc.return_against)) + else: + ref = valid_items.get(d.item_code, frappe._dict()) + already_returned_qty = flt(already_returned_items.get(d.item_code)) + max_return_qty = flt(ref.qty) - already_returned_qty + + if already_returned_qty >= ref.qty: + frappe.throw(_("Item {0} has already been returned").format(d.item_code), StockOverReturnError) + elif abs(d.qty) > max_return_qty: + frappe.throw(_("Row # {0}: Cannot return more than {1} for Item {2}") + .format(d.idx, ref.qty, d.item_code), StockOverReturnError) + elif ref.rate and flt(d.rate) != ref.rate: + frappe.throw(_("Row # {0}: Rate must be same as {1} {2}") + .format(d.idx, doc.doctype, doc.return_against)) + + + items_returned = True + + if not items_returned: + frappe.throw(_("Atleast one item should be entered with negative quantity in return document")) + +def get_already_returned_items(doc): + return frappe._dict(frappe.db.sql(""" + select + child.item_code, sum(abs(child.qty)) as qty + from + `tab{0} Item` child, `tab{1}` par + where + child.parent = par.name and par.docstatus = 1 + and ifnull(par.is_return, 0) = 1 and par.return_against = %s and child.qty < 0 + group by item_code + """.format(doc.doctype, doc.doctype), doc.return_against)) + +def make_return_doc(doctype, source_name, target_doc=None): + from frappe.model.mapper import get_mapped_doc + def set_missing_values(source, target): + doc = frappe.get_doc(target) + doc.is_return = 1 + doc.return_against = source.name + doc.ignore_pricing_rule = 1 + doc.run_method("calculate_taxes_and_totals") + + def update_item(source_doc, target_doc, source_parent): + target_doc.qty = -1* source_doc.qty + if doctype == "Purchase Receipt": + target_doc.received_qty = -1* source_doc.qty + elif doctype == "Purchase Invoice": + target_doc.purchase_receipt = source_doc.purchase_receipt + target_doc.pr_detail = source_doc.pr_detail + + doclist = get_mapped_doc(doctype, source_name, { + doctype: { + "doctype": doctype, + + "validation": { + "docstatus": ["=", 1], + } + }, + doctype +" Item": { + "doctype": doctype + " Item", + "fields": { + "purchase_order": "purchase_order", + "purchase_receipt": "purchase_receipt" + }, + "postprocess": update_item + }, + }, target_doc, set_missing_values) + + return doclist diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index fcdae4d0b4..d06d550b94 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -18,35 +18,31 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( // delivery note if(flt(doc.per_delivered, 2) < 100 && ["Sales", "Shopping Cart"].indexOf(doc.order_type)!==-1) - cur_frm.add_custom_button(__('Make Delivery'), this.make_delivery_note, "icon-truck"); + cur_frm.add_custom_button(__('Make Delivery'), this.make_delivery_note); // indent if(!doc.order_type || ["Sales", "Shopping Cart"].indexOf(doc.order_type)!==-1) cur_frm.add_custom_button(__('Make ') + __('Material Request'), - this.make_material_request, "icon-ticket"); + this.make_material_request); // sales invoice if(flt(doc.per_billed, 2) < 100) { - cur_frm.add_custom_button(__('Make Invoice'), this.make_sales_invoice, - frappe.boot.doctype_icons["Sales Invoice"]); + cur_frm.add_custom_button(__('Make Invoice'), this.make_sales_invoice); } // stop if(flt(doc.per_delivered, 2) < 100 || doc.per_billed < 100) - cur_frm.add_custom_button(__('Stop'), cur_frm.cscript['Stop Sales Order'], - "icon-exclamation", "btn-default") + cur_frm.add_custom_button(__('Stop'), cur_frm.cscript['Stop Sales Order']) // maintenance if(flt(doc.per_delivered, 2) < 100 && ["Sales", "Shopping Cart"].indexOf(doc.order_type)===-1) { - cur_frm.add_custom_button(__('Make Maint. Visit'), - this.make_maintenance_visit, null, "btn-default"); - cur_frm.add_custom_button(__('Make Maint. Schedule'), - this.make_maintenance_schedule, null, "btn-default"); + cur_frm.add_custom_button(__('Make Maint. Visit'), this.make_maintenance_visit); + cur_frm.add_custom_button(__('Make Maint. Schedule'), this.make_maintenance_schedule); } } else { // un-stop - cur_frm.add_custom_button(__('Unstop'), cur_frm.cscript['Unstop Sales Order'], "icon-check"); + cur_frm.add_custom_button(__('Unstop'), cur_frm.cscript['Unstop Sales Order']); } } @@ -64,7 +60,7 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( company: cur_frm.doc.company } }) - }, "icon-download", "btn-default"); + }); } this.order_type(doc); diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.js b/erpnext/stock/doctype/delivery_note/delivery_note.js index 26adf4e232..794d6fd811 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.js +++ b/erpnext/stock/doctype/delivery_note/delivery_note.js @@ -24,8 +24,7 @@ erpnext.stock.DeliveryNoteController = erpnext.selling.SellingController.extend( cur_frm.add_custom_button(__('Make Installation Note'), this.make_installation_note); if (doc.docstatus==1) { - cur_frm.add_custom_button(__('Make Sales Return'), this.make_sales_return, - frappe.boot.doctype_icons["Delivery Note"]); + cur_frm.add_custom_button(__('Make Sales Return'), this.make_sales_return); this.show_stock_ledger(); this.show_general_ledger(); @@ -33,7 +32,7 @@ erpnext.stock.DeliveryNoteController = erpnext.selling.SellingController.extend( if(doc.docstatus==0 && !doc.__islocal) { cur_frm.add_custom_button(__('Make Packing Slip'), - cur_frm.cscript['Make Packing Slip'], frappe.boot.doctype_icons["Packing Slip"], "btn-default"); + cur_frm.cscript['Make Packing Slip'], frappe.boot.doctype_icons["Packing Slip"]); } erpnext.stock.delivery_note.set_print_hide(doc, dt, dn); @@ -57,7 +56,7 @@ erpnext.stock.DeliveryNoteController = erpnext.selling.SellingController.extend( company: cur_frm.doc.company } }) - }, "icon-download", "btn-default"); + }); } }, diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json index 89da4806ba..0ca85c9e59 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.json +++ b/erpnext/stock/doctype/delivery_note/delivery_note.json @@ -209,7 +209,7 @@ "fieldname": "is_return", "fieldtype": "Check", "label": "Is Return", - "no_copy": 1, + "no_copy": 0, "permlevel": 0, "precision": "", "print_hide": 1, @@ -220,7 +220,7 @@ "fieldname": "return_against", "fieldtype": "Link", "label": "Return Against Delivery Note", - "no_copy": 1, + "no_copy": 0, "options": "Delivery Note", "permlevel": 0, "precision": "", @@ -1092,7 +1092,7 @@ "idx": 1, "in_create": 0, "is_submittable": 1, - "modified": "2015-07-17 13:29:28.019506", + "modified": "2015-07-24 11:49:15.056249", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note", diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index d0fff0b811..e3058822f6 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -395,5 +395,5 @@ def make_packing_slip(source_name, target_doc=None): @frappe.whitelist() def make_sales_return(source_name, target_doc=None): - from erpnext.utilities.transaction_base import make_return_doc + from erpnext.controllers.sales_and_purchase_return import make_return_doc return make_return_doc("Delivery Note", source_name, target_doc) \ No newline at end of file diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 6f2196aed9..eb80014e59 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -177,8 +177,9 @@ class TestDeliveryNote(unittest.TestCase): self.assertRaises(SerialNoStatusError, dn.submit) def check_serial_no_values(self, serial_no, field_values): + serial_no = frappe.get_doc("Serial No", serial_no) for field, value in field_values.items(): - self.assertEquals(cstr(frappe.db.get_value("Serial No", serial_no, field)), value) + self.assertEquals(cstr(serial_no.get(field)), value) def test_sales_return_for_non_bundled_items(self): set_perpetual_inventory() @@ -286,6 +287,45 @@ class TestDeliveryNote(unittest.TestCase): self.assertEquals(gle_warehouse_amount, 1400) set_perpetual_inventory(0) + + def test_return_for_serialized_items(self): + se = make_serialized_item() + serial_no = get_serial_nos(se.get("items")[0].serial_no)[0] + + dn = create_delivery_note(item_code="_Test Serialized Item With Series", rate=500, serial_no=serial_no) + + self.check_serial_no_values(serial_no, { + "status": "Delivered", + "warehouse": "", + "delivery_document_no": dn.name + }) + + # return entry + dn1 = create_delivery_note(item_code="_Test Serialized Item With Series", + is_return=1, return_against=dn.name, qty=-1, rate=500, serial_no=serial_no) + + self.check_serial_no_values(serial_no, { + "status": "Sales Returned", + "warehouse": "_Test Warehouse - _TC", + "delivery_document_no": "" + }) + + dn1.cancel() + + self.check_serial_no_values(serial_no, { + "status": "Delivered", + "warehouse": "", + "delivery_document_no": dn.name + }) + + dn.cancel() + + self.check_serial_no_values(serial_no, { + "status": "Available", + "warehouse": "_Test Warehouse - _TC", + "delivery_document_no": "", + "purchase_document_no": se.name + }) def create_delivery_note(**args): dn = frappe.new_doc("Delivery Note") diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js index 727d38ec2c..13e104e15a 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js @@ -31,12 +31,10 @@ erpnext.stock.PurchaseReceiptController = erpnext.buying.BuyingController.extend if(this.frm.doc.docstatus == 1) { if(this.frm.doc.__onload && !this.frm.doc.__onload.billing_complete) { - cur_frm.add_custom_button(__('Make Purchase Invoice'), this.make_purchase_invoice, - frappe.boot.doctype_icons["Purchase Invoice"]); + cur_frm.add_custom_button(__('Make Purchase Invoice'), this.make_purchase_invoice); } - cur_frm.add_custom_button(__('Make Purchase Return'), this.make_purchase_return, - frappe.boot.doctype_icons["Purchase Receipt"]); + cur_frm.add_custom_button(__('Make Purchase Return'), this.make_purchase_return); this.show_stock_ledger(); this.show_general_ledger(); @@ -54,7 +52,7 @@ erpnext.stock.PurchaseReceiptController = erpnext.buying.BuyingController.extend company: cur_frm.doc.company } }) - }, "icon-download", "btn-default"); + }); } this.frm.toggle_reqd("supplier_warehouse", this.frm.doc.is_subcontracted==="Yes"); diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json index c44923abb1..8e32281d59 100755 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json @@ -135,7 +135,7 @@ "fieldname": "is_return", "fieldtype": "Check", "label": "Is Return", - "no_copy": 1, + "no_copy": 0, "permlevel": 0, "precision": "", "print_hide": 1, @@ -146,7 +146,7 @@ "fieldname": "return_against", "fieldtype": "Link", "label": "Return Against Purchase Receipt", - "no_copy": 1, + "no_copy": 0, "options": "Purchase Receipt", "permlevel": 0, "precision": "", @@ -877,7 +877,7 @@ "icon": "icon-truck", "idx": 1, "is_submittable": 1, - "modified": "2015-07-17 13:29:10.298448", + "modified": "2015-07-24 11:49:35.580382", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt", diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index a94856d44b..034eb07830 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -469,5 +469,5 @@ def get_invoiced_qty_map(purchase_receipt): @frappe.whitelist() def make_purchase_return(source_name, target_doc=None): - from erpnext.utilities.transaction_base import make_return_doc + from erpnext.controllers.sales_and_purchase_return import make_return_doc return make_return_doc("Purchase Receipt", source_name, target_doc) \ No newline at end of file diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 9cdbc2c995..343d51acc7 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -6,7 +6,7 @@ from __future__ import unicode_literals import unittest import frappe import frappe.defaults -from frappe.utils import cint, flt +from frappe.utils import cint, flt, cstr class TestPurchaseReceipt(unittest.TestCase): def test_make_purchase_invoice(self): @@ -127,7 +127,6 @@ class TestPurchaseReceipt(unittest.TestCase): return_pr = make_purchase_receipt(is_return=1, return_against=pr.name, qty=-2) - # check sle outgoing_rate = frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Purchase Receipt", "voucher_no": return_pr.name}, "outgoing_rate") @@ -151,6 +150,34 @@ class TestPurchaseReceipt(unittest.TestCase): set_perpetual_inventory(0) + def test_purchase_return_for_serialized_items(self): + def _check_serial_no_values(serial_no, field_values): + serial_no = frappe.get_doc("Serial No", serial_no) + for field, value in field_values.items(): + self.assertEquals(cstr(serial_no.get(field)), value) + + from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos + + pr = make_purchase_receipt(item_code="_Test Serialized Item With Series", qty=1) + + serial_no = get_serial_nos(pr.get("items")[0].serial_no)[0] + + _check_serial_no_values(serial_no, { + "status": "Available", + "warehouse": "_Test Warehouse - _TC", + "purchase_document_no": pr.name + }) + + return_pr = make_purchase_receipt(item_code="_Test Serialized Item With Series", qty=-1, + is_return=1, return_against=pr.name, serial_no=serial_no) + + _check_serial_no_values(serial_no, { + "status": "Purchase Returned", + "warehouse": "", + "purchase_document_no": pr.name, + "delivery_document_no": return_pr.name + }) + def get_gl_entries(voucher_type, voucher_no): return frappe.db.sql("""select account, debit, credit diff --git a/erpnext/stock/doctype/serial_no/serial_no.json b/erpnext/stock/doctype/serial_no/serial_no.json index 8ffe7ed9dd..97754e9d1d 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.json +++ b/erpnext/stock/doctype/serial_no/serial_no.json @@ -244,7 +244,7 @@ "in_filter": 1, "label": "Delivery Document Type", "no_copy": 1, - "options": "\nDelivery Note\nSales Invoice\nStock Entry", + "options": "\nDelivery Note\nSales Invoice\nStock Entry\nPurchase Receipt", "permlevel": 0, "read_only": 1 }, @@ -418,7 +418,7 @@ "icon": "icon-barcode", "idx": 1, "in_create": 0, - "modified": "2015-07-13 05:28:27.961178", + "modified": "2015-07-24 03:55:29.946944", "modified_by": "Administrator", "module": "Stock", "name": "Serial No", diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py index 452182198c..6b5054b902 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.py +++ b/erpnext/stock/doctype/serial_no/serial_no.py @@ -33,10 +33,7 @@ class SerialNo(StockController): self.validate_warehouse() self.validate_item() self.on_stock_ledger_entry() - - valid_purchase_document_type = ("Purchase Receipt", "Stock Entry", "Serial No") - self.validate_value("purchase_document_type", "in", valid_purchase_document_type) - + def set_maintenance_status(self): if not self.warranty_expiry_date and not self.amc_expiry_date: self.maintenance_status = None @@ -122,9 +119,10 @@ class SerialNo(StockController): self.delivery_document_no = delivery_sle.voucher_no self.delivery_date = delivery_sle.posting_date self.delivery_time = delivery_sle.posting_time - self.customer, self.customer_name = \ - frappe.db.get_value(delivery_sle.voucher_type, delivery_sle.voucher_no, - ["customer", "customer_name"]) + if delivery_sle.voucher_type in ("Delivery Note", "Sales Invoice"): + self.customer, self.customer_name = \ + frappe.db.get_value(delivery_sle.voucher_type, delivery_sle.voucher_no, + ["customer", "customer_name"]) if self.warranty_period: self.warranty_expiry_date = add_days(cstr(delivery_sle.posting_date), cint(self.warranty_period)) @@ -234,10 +232,10 @@ def validate_serial_no(sle, item_det): frappe.throw(_("Serial No {0} does not belong to Warehouse {1}").format(serial_no, sle.warehouse), SerialNoWarehouseError) - if sle.voucher_type in ("Delivery Note", "Sales Invoice") \ + if sle.voucher_type in ("Delivery Note", "Sales Invoice") and sle.is_cancelled=="No" \ and sr.status != "Available": - frappe.throw(_("Serial No {0} status must be 'Available' to Deliver").format(serial_no), - SerialNoStatusError) + frappe.throw(_("Serial No {0} status must be 'Available' to Deliver").format(serial_no), + SerialNoStatusError) elif sle.actual_qty < 0: # transfer out diff --git a/erpnext/utilities/transaction_base.py b/erpnext/utilities/transaction_base.py index fd2aaabca8..6c9b9a428d 100644 --- a/erpnext/utilities/transaction_base.py +++ b/erpnext/utilities/transaction_base.py @@ -3,12 +3,12 @@ from __future__ import unicode_literals import frappe +import frappe.share from frappe import _ from frappe.utils import cstr, now_datetime, cint, flt -import frappe.share - from erpnext.controllers.status_updater import StatusUpdater +class UOMMustBeIntegerError(frappe.ValidationError): pass class TransactionBase(StatusUpdater): def load_notification_message(self): @@ -109,8 +109,6 @@ def delete_events(ref_type, ref_name): frappe.delete_doc("Event", frappe.db.sql_list("""select name from `tabEvent` where ref_type=%s and ref_name=%s""", (ref_type, ref_name)), for_reload=True) -class UOMMustBeIntegerError(frappe.ValidationError): pass - def validate_uom_is_integer(doc, uom_field, qty_fields, child_dt=None): if isinstance(qty_fields, basestring): qty_fields = [qty_fields] @@ -128,36 +126,3 @@ def validate_uom_is_integer(doc, uom_field, qty_fields, child_dt=None): if d.get(f): if cint(d.get(f))!=d.get(f): frappe.throw(_("Quantity cannot be a fraction in row {0}").format(d.idx), UOMMustBeIntegerError) - -def make_return_doc(doctype, source_name, target_doc=None): - from frappe.model.mapper import get_mapped_doc - def set_missing_values(source, target): - doc = frappe.get_doc(target) - doc.is_return = 1 - doc.return_against = source.name - doc.ignore_pricing_rule = 1 - doc.run_method("calculate_taxes_and_totals") - - def update_item(source_doc, target_doc, source_parent): - target_doc.qty = -1* source_doc.qty - if doctype == "Purchase Receipt": - target_doc.received_qty = -1* source_doc.qty - elif doctype == "Purchase Invoice": - target_doc.purchase_receipt = source_doc.purchase_receipt - target_doc.pr_detail = source_doc.pr_detail - - doclist = get_mapped_doc(doctype, source_name, { - doctype: { - "doctype": doctype, - - "validation": { - "docstatus": ["=", 1], - } - }, - doctype +" Item": { - "doctype": doctype + " Item", - "postprocess": update_item - }, - }, target_doc, set_missing_values) - - return doclist From 196a0bc67578b210afb74bcff185dc494b88db94 Mon Sep 17 00:00:00 2001 From: Tsutomu Mimori Date: Fri, 24 Jul 2015 17:06:00 +0900 Subject: [PATCH 37/40] Amend for frappe/erpnext/pull/3716 --- erpnext/accounts/doctype/cost_center/cost_center.json | 2 +- erpnext/setup/doctype/features_setup/features_setup.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/cost_center/cost_center.json b/erpnext/accounts/doctype/cost_center/cost_center.json index ebba758963..186283a0a2 100644 --- a/erpnext/accounts/doctype/cost_center/cost_center.json +++ b/erpnext/accounts/doctype/cost_center/cost_center.json @@ -66,7 +66,7 @@ "precision": "" }, { - "description": "Define Budget for this Cost Center. To set budget action, see \"Company Master\"", + "description": "Define Budget for this Cost Center. To set budget action, see \"Company List\"", "fieldname": "sb1", "fieldtype": "Section Break", "label": "Budget", diff --git a/erpnext/setup/doctype/features_setup/features_setup.json b/erpnext/setup/doctype/features_setup/features_setup.json index 861103cdbf..f9bbad076e 100644 --- a/erpnext/setup/doctype/features_setup/features_setup.json +++ b/erpnext/setup/doctype/features_setup/features_setup.json @@ -18,7 +18,7 @@ "permlevel": 0 }, { - "description": "To track items in sales and purchase documents with batch nos.\"Preferred Industry: Chemicals etc\"", + "description": "To track items in sales and purchase documents with batch nos. \"Preferred Industry: Chemicals\"", "fieldname": "fs_item_batch_nos", "fieldtype": "Check", "in_list_view": 1, From 14859faf1751f10ec1b0c2ea5a581ee1b508addd Mon Sep 17 00:00:00 2001 From: Anand Doshi Date: Fri, 24 Jul 2015 16:01:16 +0530 Subject: [PATCH 38/40] [minor] Accounts Payable Report Type='Script Report' --- .../accounts_payable/accounts_payable.json | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/erpnext/accounts/report/accounts_payable/accounts_payable.json b/erpnext/accounts/report/accounts_payable/accounts_payable.json index 9be8d683b9..13d28a4bea 100644 --- a/erpnext/accounts/report/accounts_payable/accounts_payable.json +++ b/erpnext/accounts/report/accounts_payable/accounts_payable.json @@ -1,17 +1,17 @@ { - "add_total_row": 1, - "apply_user_permissions": 1, - "creation": "2013-04-22 16:16:03", - "docstatus": 0, - "doctype": "Report", - "idx": 1, - "is_standard": "Yes", - "modified": "2015-07-23 01:08:20.996267", - "modified_by": "Administrator", - "module": "Accounts", - "name": "Accounts Payable", - "owner": "Administrator", - "ref_doctype": "Purchase Invoice", - "report_name": "Accounts Payable", - "report_type": "Query Report" -} \ No newline at end of file + "add_total_row": 1, + "apply_user_permissions": 1, + "creation": "2013-04-22 16:16:03", + "docstatus": 0, + "doctype": "Report", + "idx": 1, + "is_standard": "Yes", + "modified": "2015-07-24 01:08:20.996267", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Accounts Payable", + "owner": "Administrator", + "ref_doctype": "Purchase Invoice", + "report_name": "Accounts Payable", + "report_type": "Script Report" +} From 8d8655e1cdb4fca6e661907acb21ab96d8302b2a Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 27 Jul 2015 15:54:39 +0530 Subject: [PATCH 39/40] Change log added --- erpnext/change_log/current/sms.md | 1 - erpnext/change_log/v5/v5_3_0.md | 11 +++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) delete mode 100644 erpnext/change_log/current/sms.md create mode 100644 erpnext/change_log/v5/v5_3_0.md diff --git a/erpnext/change_log/current/sms.md b/erpnext/change_log/current/sms.md deleted file mode 100644 index bac293f20f..0000000000 --- a/erpnext/change_log/current/sms.md +++ /dev/null @@ -1 +0,0 @@ -- Now system will give SMS delivery message and maintain a log \ No newline at end of file diff --git a/erpnext/change_log/v5/v5_3_0.md b/erpnext/change_log/v5/v5_3_0.md new file mode 100644 index 0000000000..e36909080d --- /dev/null +++ b/erpnext/change_log/v5/v5_3_0.md @@ -0,0 +1,11 @@ +- **Sales Return**: Create Delivery Note or Sales Invoice ('Updated Stock' option checked) with negative quantity. +- **Purchase Return**: Create Purchase Receipt with negative quantity +- **Credit / Debit Note**: Create Sales / Purchase Invoice with negative qtuantity against original invoice. +- Outgoing rate in Purchase Return based on reference / original Purchase Receipt rate +- Global switch added to disable capacity planning in manufacturing settings +- Opening Balance row added to Stock Ledger Report +- SMS delivery message and log +- Added users, employees, sample data via Setup Wizard +- Letter Head option in General Ledger report +- Fetch Template Bom if no BOM is set against Item Variant in Production Order +- Fetch items from Packing List while raising Material Request against SO \ No newline at end of file From 228ff87ea22cc1592496b1129d32609d6a43cfe9 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 27 Jul 2015 16:25:56 +0600 Subject: [PATCH 40/40] bumped to version 5.3.0 --- erpnext/__version__.py | 2 +- erpnext/hooks.py | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/__version__.py b/erpnext/__version__.py index 9049b135b4..3a243f5bb7 100644 --- a/erpnext/__version__.py +++ b/erpnext/__version__.py @@ -1,2 +1,2 @@ from __future__ import unicode_literals -__version__ = '5.2.1' +__version__ = '5.3.0' diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 91ba19a9be..811e4c01e2 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -27,7 +27,7 @@ blogs. """ app_icon = "icon-th" app_color = "#e74c3c" -app_version = "5.2.1" +app_version = "5.3.0" github_link = "https://github.com/frappe/erpnext" error_report_email = "support@erpnext.com" diff --git a/setup.py b/setup.py index 9cb39e1fe5..020dfa3c43 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ from setuptools import setup, find_packages -version = "5.2.1" +version = "5.3.0" with open("requirements.txt", "r") as f: install_requires = f.readlines()