diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py index 3d306fb8d5f..edee1226eb5 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 07dbf721f50..548abb7cbf6 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 69b0708f1a5..c5797162db8 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 1ac0f5acd85..b34f8452e2e 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 5b2f3483e72..8daf3f667f6 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 25dd3988f14..b983d990244 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 829478df799..9129e1f7f09 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 1b7d20ae822..19ad9ab6519 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 476aa92f67e..1bf6f8fe67b 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 98f240958f8..d1ce3c6fdcd 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 98679732588..0b60473b8ba 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 b2a9f0317f0..01ef605b635 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 66780074893..19440e24a73 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 e77a9a66197..f22b62488bb 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 4a26d6d39cb..07c2d56cf15 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 0a75dad09eb..01e5781a4e8 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 f3cd8a78338..e8d8fd5f7a3 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 631009fa087..26adf4e232d 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 72a72278a0e..89da4806ba3 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 90a8a6c7200..cd501dad886 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 fe41b4fb93a..727d38ec2c9 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 6e344b61e0e..c44923abb1c 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 e78288908d9..31a2f50a12c 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 50b0319d3fb..fd2aaabca8f 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