From 3cf67a462b1994b0f4f47a636ae9e01a230e6d0d Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 24 Jul 2015 13:26:36 +0530 Subject: [PATCH] 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