Merge pull request #3683 from nabinhait/return

Sales / Purchase Return Enhancement
This commit is contained in:
Nabin Hait
2015-07-24 16:53:01 +05:30
43 changed files with 938 additions and 1033 deletions

View File

@@ -4,7 +4,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe 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 import _
from frappe.model.document import Document 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: if against_voucher_amount < 0:
bal = -bal bal = -bal
# Validation : Outstanding can not be negative # Validation : Outstanding can not be negative for JV
if bal < 0 and not on_cancel: if bal < 0 and not on_cancel:
frappe.throw(_("Outstanding for {0} cannot be less than zero ({1})").format(against_voucher, fmt_money(bal))) frappe.throw(_("Outstanding for {0} cannot be less than zero ({1})").format(against_voucher, fmt_money(bal)))
# Update outstanding amt on against voucher # Update outstanding amt on against voucher
if against_voucher_type in ["Sales Invoice", "Purchase Invoice"]: if against_voucher_type in ["Sales Invoice", "Purchase Invoice"]:

View File

@@ -21,10 +21,11 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({
// Show / Hide button // Show / Hide button
if(doc.docstatus==1 && doc.outstanding_amount > 0) if(doc.docstatus==1 && doc.outstanding_amount > 0)
this.frm.add_custom_button(__('Make Payment Entry'), this.make_bank_entry, this.frm.add_custom_button(__('Make Payment Entry'), this.make_bank_entry);
frappe.boot.doctype_icons["Journal Entry"]);
if(doc.docstatus==1) { if(doc.docstatus==1) {
cur_frm.add_custom_button(__('Make Purchase Return'), this.make_purchase_return);
cur_frm.add_custom_button(__('View Ledger'), function() { cur_frm.add_custom_button(__('View Ledger'), function() {
frappe.route_options = { frappe.route_options = {
"voucher_no": doc.name, "voucher_no": doc.name,
@@ -34,7 +35,7 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({
group_by_voucher: 0 group_by_voucher: 0
}; };
frappe.set_route("query-report", "General Ledger"); frappe.set_route("query-report", "General Ledger");
}, "icon-table"); });
} }
if(doc.docstatus===0) { if(doc.docstatus===0) {
@@ -51,7 +52,7 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({
company: cur_frm.doc.company company: cur_frm.doc.company
} }
}) })
}, "icon-download", "btn-default"); });
cur_frm.add_custom_button(__('From Purchase Receipt'), cur_frm.add_custom_button(__('From Purchase Receipt'),
function() { function() {
@@ -64,7 +65,7 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({
company: cur_frm.doc.company company: cur_frm.doc.company
} }
}) })
}, "icon-download", "btn-default"); });
} }
}, },
@@ -109,7 +110,14 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({
$.each(this.frm.doc["items"] || [], function(i, row) { $.each(this.frm.doc["items"] || [], function(i, row) {
if(row.purchase_receipt) frappe.model.clear_doc("Purchase Receipt", row.purchase_receipt) 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); cur_frm.script_manager.make(erpnext.accounts.PurchaseInvoice);

View File

@@ -12,7 +12,7 @@
"no_copy": 1, "no_copy": 1,
"oldfieldname": "naming_series", "oldfieldname": "naming_series",
"oldfieldtype": "Select", "oldfieldtype": "Select",
"options": "PINV-", "options": "PINV-\nPINV-RET-",
"permlevel": 0, "permlevel": 0,
"print_hide": 1, "print_hide": 1,
"read_only": 0, "read_only": 0,
@@ -154,6 +154,28 @@
"read_only": 0, "read_only": 0,
"search_index": 0 "search_index": 0
}, },
{
"fieldname": "is_return",
"fieldtype": "Check",
"label": "Is Return",
"no_copy": 0,
"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": 0,
"options": "Purchase Invoice",
"permlevel": 0,
"precision": "",
"print_hide": 1,
"read_only": 1
},
{ {
"fieldname": "currency_and_price_list", "fieldname": "currency_and_price_list",
"fieldtype": "Section Break", "fieldtype": "Section Break",
@@ -940,7 +962,7 @@
"icon": "icon-file-text", "icon": "icon-file-text",
"idx": 1, "idx": 1,
"is_submittable": 1, "is_submittable": 1,
"modified": "2015-07-03 03:26:32.934540", "modified": "2015-07-24 11:49:59.762109",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Purchase Invoice", "name": "Purchase Invoice",

View File

@@ -37,14 +37,16 @@ class PurchaseInvoice(BuyingController):
super(PurchaseInvoice, self).validate() super(PurchaseInvoice, self).validate()
self.po_required() if not self.is_return:
self.pr_required() self.po_required()
self.validate_supplier_invoice() self.pr_required()
self.validate_supplier_invoice()
self.validate_advance_jv("advances", "purchase_order")
self.check_active_purchase_items() self.check_active_purchase_items()
self.check_conversion_rate() self.check_conversion_rate()
self.validate_credit_to_acc() self.validate_credit_to_acc()
self.clear_unallocated_advances("Purchase Invoice Advance", "advances") self.clear_unallocated_advances("Purchase Invoice Advance", "advances")
self.validate_advance_jv("advances", "purchase_order")
self.check_for_stopped_status() self.check_for_stopped_status()
self.validate_with_previous_doc() self.validate_with_previous_doc()
self.validate_uom_is_integer("uom", "qty") self.validate_uom_is_integer("uom", "qty")
@@ -71,8 +73,9 @@ class PurchaseInvoice(BuyingController):
super(PurchaseInvoice, self).set_missing_values(for_validate) super(PurchaseInvoice, self).set_missing_values(for_validate)
def get_advances(self): def get_advances(self):
super(PurchaseInvoice, self).get_advances(self.credit_to, "Supplier", self.supplier, if not self.is_return:
"Purchase Invoice Advance", "advances", "debit", "purchase_order") super(PurchaseInvoice, self).get_advances(self.credit_to, "Supplier", self.supplier,
"Purchase Invoice Advance", "advances", "debit", "purchase_order")
def check_active_purchase_items(self): def check_active_purchase_items(self):
for d in self.get('items'): for d in self.get('items'):
@@ -226,9 +229,11 @@ class PurchaseInvoice(BuyingController):
# this sequence because outstanding may get -negative # this sequence because outstanding may get -negative
self.make_gl_entries() self.make_gl_entries()
self.update_against_document_in_jv() if not self.is_return:
self.update_prevdoc_status() self.update_against_document_in_jv()
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.update_project() self.update_project()
def make_gl_entries(self): def make_gl_entries(self):
@@ -358,11 +363,12 @@ class PurchaseInvoice(BuyingController):
make_gl_entries(gl_entries, cancel=(self.docstatus == 2)) make_gl_entries(gl_entries, cancel=(self.docstatus == 2))
def on_cancel(self): def on_cancel(self):
from erpnext.accounts.utils import remove_against_link_from_jv if not self.is_return:
remove_against_link_from_jv(self.doctype, self.name, "against_voucher") 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_prevdoc_status()
self.update_billing_status_for_zero_amount_refdoc("Purchase Order") self.update_billing_status_for_zero_amount_refdoc("Purchase Order")
self.make_gl_entries_on_cancel() self.make_gl_entries_on_cancel()
self.update_project() 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' and tabAccount.%(key)s LIKE '%(txt)s'
%(mcond)s""" % {'company': filters['company'], 'key': searchfield, %(mcond)s""" % {'company': filters['company'], 'key': searchfield,
'txt': "%%%s%%" % frappe.db.escape(txt), 'mcond':get_match_cond(doctype)}) 'txt': "%%%s%%" % frappe.db.escape(txt), 'mcond':get_match_cond(doctype)})
@frappe.whitelist()
def make_purchase_return(source_name, target_doc=None):
from erpnext.controllers.sales_and_purchase_return import make_return_doc
return make_return_doc("Purchase Invoice", source_name, target_doc)

View File

@@ -275,5 +275,58 @@ class TestPurchaseInvoice(unittest.TestCase):
purchase_invoice.cancel() purchase_invoice.cancel()
self.assertEqual(frappe.db.get_value("Project", "_Test Project", "total_purchase_cost"), 0) 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') test_records = frappe.get_test_records('Purchase Invoice')

View File

@@ -41,6 +41,8 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte
cur_frm.dashboard.reset(); cur_frm.dashboard.reset();
this.frm.toggle_reqd("due_date", !this.frm.doc.is_return);
if(doc.docstatus==1) { if(doc.docstatus==1) {
cur_frm.add_custom_button('View Ledger', function() { cur_frm.add_custom_button('View Ledger', function() {
frappe.route_options = { frappe.route_options = {
@@ -51,10 +53,7 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte
group_by_voucher: 0 group_by_voucher: 0
}; };
frappe.set_route("query-report", "General Ledger"); 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) { if(cint(doc.update_stock)!=1) {
// show Make Delivery Note button only if Sales Invoice is not created from Delivery Note // show Make Delivery Note button only if Sales Invoice is not created from Delivery Note
@@ -65,13 +64,15 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte
}); });
if(!from_delivery_note) { 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) { 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);
} }
// Show buttons only when pos view is active // Show buttons only when pos view is active
@@ -205,8 +206,14 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte
items_on_form_rendered: function() { items_on_form_rendered: function() {
erpnext.setup_serial_no(); 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 // for backward compatibility: combine new and previous states
@@ -283,16 +290,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) { cur_frm.fields_dict.cash_bank_account.get_query = function(doc) {
return { return {
filters: [ filters: [

View File

@@ -21,7 +21,7 @@
"no_copy": 1, "no_copy": 1,
"oldfieldname": "naming_series", "oldfieldname": "naming_series",
"oldfieldtype": "Select", "oldfieldtype": "Select",
"options": "SINV-", "options": "SINV-\nSINV-RET-",
"permlevel": 0, "permlevel": 0,
"print_hide": 1, "print_hide": 1,
"read_only": 0, "read_only": 0,
@@ -156,7 +156,7 @@
"oldfieldtype": "Date", "oldfieldtype": "Date",
"permlevel": 0, "permlevel": 0,
"read_only": 0, "read_only": 0,
"reqd": 1, "reqd": 0,
"search_index": 0 "search_index": 0
}, },
{ {
@@ -169,6 +169,28 @@
"print_hide": 1, "print_hide": 1,
"read_only": 0 "read_only": 0
}, },
{
"fieldname": "is_return",
"fieldtype": "Check",
"label": "Is Return",
"no_copy": 0,
"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": 0,
"options": "Sales Invoice",
"permlevel": 0,
"precision": "",
"print_hide": 1,
"read_only": 1
},
{ {
"fieldname": "shipping_address_name", "fieldname": "shipping_address_name",
"fieldtype": "Link", "fieldtype": "Link",
@@ -1253,7 +1275,7 @@
"icon": "icon-file-text", "icon": "icon-file-text",
"idx": 1, "idx": 1,
"is_submittable": 1, "is_submittable": 1,
"modified": "2015-07-09 17:33:28.583808", "modified": "2015-07-24 11:48:07.544569",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Invoice", "name": "Sales Invoice",

View File

@@ -80,14 +80,16 @@ class SalesInvoice(SellingController):
self.check_prev_docstatus() self.check_prev_docstatus()
self.update_status_updater_args() if not self.is_return:
self.update_prevdoc_status() self.update_status_updater_args()
self.update_billing_status_for_zero_amount_refdoc("Sales Order") self.update_prevdoc_status()
self.check_credit_limit() self.update_billing_status_for_zero_amount_refdoc("Sales Order")
self.check_credit_limit()
# this sequence because outstanding may get -ve # this sequence because outstanding may get -ve
self.make_gl_entries() 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_against_document_in_jv()
self.update_time_log_batch(self.name) self.update_time_log_batch(self.name)
@@ -104,9 +106,11 @@ class SalesInvoice(SellingController):
from erpnext.accounts.utils import remove_against_link_from_jv from erpnext.accounts.utils import remove_against_link_from_jv
remove_against_link_from_jv(self.doctype, self.name, "against_invoice") remove_against_link_from_jv(self.doctype, self.name, "against_invoice")
self.update_status_updater_args() if not self.is_return:
self.update_prevdoc_status() self.update_status_updater_args()
self.update_billing_status_for_zero_amount_refdoc("Sales Order") self.update_prevdoc_status()
self.update_billing_status_for_zero_amount_refdoc("Sales Order")
self.validate_c_form_on_cancel() self.validate_c_form_on_cancel()
self.make_gl_entries_on_cancel() self.make_gl_entries_on_cancel()
@@ -199,8 +203,9 @@ class SalesInvoice(SellingController):
self.set_taxes() self.set_taxes()
def get_advances(self): def get_advances(self):
super(SalesInvoice, self).get_advances(self.debit_to, "Customer", self.customer, if not self.is_return:
"Sales Invoice Advance", "advances", "credit", "sales_order") super(SalesInvoice, self).get_advances(self.debit_to, "Customer", self.customer,
"Sales Invoice Advance", "advances", "credit", "sales_order")
def get_company_abbr(self): def get_company_abbr(self):
return frappe.db.sql("select abbr from tabCompany where name=%s", self.company)[0][0] 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): def so_dn_required(self):
"""check in manage account if sales order / delivery note required or not.""" """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'} dic = {'Sales Order':'so_required','Delivery Note':'dn_required'}
for i in dic: for i in dic:
if frappe.db.get_value('Selling Settings', None, dic[i]) == 'Yes': if frappe.db.get_value('Selling Settings', None, dic[i]) == 'Yes':
@@ -419,13 +426,16 @@ class SalesInvoice(SellingController):
def update_stock_ledger(self): def update_stock_ledger(self):
sl_entries = [] sl_entries = []
for d in self.get_item_list(): for d in self.get_item_list():
if frappe.db.get_value("Item", d.item_code, "is_stock_item") == "Yes" \ if frappe.db.get_value("Item", d.item_code, "is_stock_item") == "Yes" and d.warehouse:
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, { sl_entries.append(self.get_sl_entries(d, {
"actual_qty": -1*flt(d.qty), "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) self.make_sl_entries(sl_entries)
def make_gl_entries(self, repost_future_gle=True): 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 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 # 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 \ update_outstanding = "No" if (cint(self.is_pos) or self.write_off_account) else "Yes"
and 'No' or 'Yes'
make_gl_entries(gl_entries, cancel=(self.docstatus == 2), make_gl_entries(gl_entries, cancel=(self.docstatus == 2),
update_outstanding=update_outstanding, merge_entries=False) update_outstanding=update_outstanding, merge_entries=False)
@@ -484,7 +493,7 @@ class SalesInvoice(SellingController):
"against": self.against_income_account, "against": self.against_income_account,
"debit": self.base_grand_total, "debit": self.base_grand_total,
"remarks": self.remarks, "remarks": self.remarks,
"against_voucher": self.name, "against_voucher": self.return_against if cint(self.is_return) else self.name,
"against_voucher_type": self.doctype "against_voucher_type": self.doctype
}) })
) )
@@ -519,7 +528,6 @@ class SalesInvoice(SellingController):
# expense account gl entries # expense account gl entries
if cint(frappe.defaults.get_global_default("auto_accounting_for_stock")) \ if cint(frappe.defaults.get_global_default("auto_accounting_for_stock")) \
and cint(self.update_stock): and cint(self.update_stock):
gl_entries += super(SalesInvoice, self).get_gl_entries() gl_entries += super(SalesInvoice, self).get_gl_entries()
def make_pos_gl_entries(self, gl_entries): def make_pos_gl_entries(self, gl_entries):
@@ -533,7 +541,7 @@ class SalesInvoice(SellingController):
"against": self.cash_bank_account, "against": self.cash_bank_account,
"credit": self.paid_amount, "credit": self.paid_amount,
"remarks": self.remarks, "remarks": self.remarks,
"against_voucher": self.name, "against_voucher": self.return_against if cint(self.is_return) else self.name,
"against_voucher_type": self.doctype, "against_voucher_type": self.doctype,
}) })
) )
@@ -557,7 +565,7 @@ class SalesInvoice(SellingController):
"against": self.write_off_account, "against": self.write_off_account,
"credit": self.write_off_amount, "credit": self.write_off_amount,
"remarks": self.remarks, "remarks": self.remarks,
"against_voucher": self.name, "against_voucher": self.return_against if cint(self.is_return) else self.name,
"against_voucher_type": self.doctype, "against_voucher_type": self.doctype,
}) })
) )
@@ -651,3 +659,9 @@ def make_delivery_note(source_name, target_doc=None):
}, target_doc, set_missing_values) }, target_doc, set_missing_values)
return doclist return doclist
@frappe.whitelist()
def make_sales_return(source_name, target_doc=None):
from erpnext.controllers.sales_and_purchase_return import make_return_doc
return make_return_doc("Sales Invoice", source_name, target_doc)

View File

@@ -4,11 +4,10 @@ from __future__ import unicode_literals
import frappe import frappe
import unittest, copy import unittest, copy
import time from frappe.utils import nowdate, add_days, flt
from frappe.utils import nowdate, add_days
from erpnext.accounts.utils import get_stock_and_account_difference
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory 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.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): class TestSalesInvoice(unittest.TestCase):
@@ -772,6 +771,53 @@ class TestSalesInvoice(unittest.TestCase):
si1 = create_sales_invoice(posting_date="2015-07-05") si1 = create_sales_invoice(posting_date="2015-07-05")
self.assertEqual(si1.due_date, "2015-08-31") 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): def create_sales_invoice(**args):
si = frappe.new_doc("Sales Invoice") 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.debit_to = args.debit_to or "Debtors - _TC"
si.update_stock = args.update_stock si.update_stock = args.update_stock
si.is_pos = args.is_pos 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", { si.append("items", {
"item_code": args.item or args.item_code or "_Test Item", "item_code": args.item or args.item_code or "_Test Item",

View File

@@ -1,14 +1,14 @@
{ {
"creation": "2014-08-28 11:11:39.796473", "creation": "2014-08-28 11:11:39.796473",
"custom_format": 0,
"disabled": 0, "disabled": 0,
"doc_type": "Journal Entry", "doc_type": "Journal Entry",
"docstatus": 0, "docstatus": 0,
"doctype": "Print Format", "doctype": "Print Format",
"html": "{%- from \"templates/print_formats/standard_macros.html\" import add_header -%}\n\n<div class=\"page-break\">\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\"), \"<strong>\" + doc.get_formatted(\"total_amount\") + \"</strong><br>\" + (doc.total_amount_in_words or \"\") + \"<br>\"),\n (_(\"Remarks\"), doc.remark)\n ) -%}\n\n <div class=\"row\">\n <div class=\"col-xs-3\"><label class=\"text-right\">{{ label }}</label></div>\n <div class=\"col-xs-9\">{{ value }}</div>\n </div>\n\n {%- endfor -%}\n\n <hr>\n <br>\n <p class=\"strong\">\n {{ _(\"For\") }} {{ doc.company }},<br>\n <br>\n <br>\n <br>\n {{ _(\"Authorized Signatory\") }}\n </p>\n</div>\n\n\n", "html": "{%- from \"templates/print_formats/standard_macros.html\" import add_header -%}\n\n<div class=\"page-break\">\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\"), \"<strong>\" + doc.get_formatted(\"total_amount\") + \"</strong><br>\" + (doc.total_amount_in_words or \"\") + \"<br>\"),\n (_(\"Remarks\"), doc.remark)\n ) -%}\n\n <div class=\"row\">\n <div class=\"col-xs-3\"><label class=\"text-right\">{{ label }}</label></div>\n <div class=\"col-xs-9\">{{ value }}</div>\n </div>\n\n {%- endfor -%}\n\n <hr>\n <br>\n <p class=\"strong\">\n {{ _(\"For\") }} {{ doc.company }},<br>\n <br>\n <br>\n <br>\n {{ _(\"Authorized Signatory\") }}\n </p>\n</div>\n\n\n",
"idx": 2, "idx": 2,
"modified": "2015-01-12 11:02:25.716825", "modified": "2015-07-22 17:42:01.560817",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts",
"name": "Credit Note", "name": "Credit Note",
"owner": "Administrator", "owner": "Administrator",
"parent": "Journal Entry", "parent": "Journal Entry",

View File

@@ -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": "<style>\n\t.print-format table, .print-format tr, \n\t.print-format td, .print-format div, .print-format p {\n\t\tfont-family: Monospace;\n\t\tline-height: 200%;\n\t\tvertical-align: middle;\n\t}\n\t@media screen {\n\t\t.print-format {\n\t\t\twidth: 4in;\n\t\t\tpadding: 0.25in;\n\t\t\tmin-height: 6in;\n\t\t}\n\t}\n</style>\n\n<p class=\"text-center\">\n\t{{ doc.company }}<br>\n\t{{ doc.select_print_heading or _(\"Credit Note\") }}<br>\n</p>\n\n<hr>\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\"), \"<strong>\" + doc.get_formatted(\"grand_total\", absolute_value=True) + \"</strong><br>\" + (doc.in_words or \"\")),\n\t(_(\"Against\"), doc.return_against),\n (_(\"Remarks\"), doc.remarks)\n) -%}\n\n\t\t<div class=\"row\">\n\t\t <div class=\"col-xs-4\"><label class=\"text-right\">{{ label }}</label></div>\n\t\t <div class=\"col-xs-8\">{{ value }}</div>\n\t\t</div>\n{%- endfor -%}\n\n<hr>\n<br>\n<p class=\"strong\">\n {{ _(\"For\") }} {{ doc.company }},<br>\n <br>\n <br>\n <br>\n {{ _(\"Authorized Signatory\") }}\n</p>",
"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"
}

View File

@@ -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"]); 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, 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")); precision("total_amount_to_pay"));
this.frm.doc.outstanding_amount = flt(this.frm.doc.total_amount_to_pay - this.frm.doc.total_advance, if (!this.frm.doc.is_return) {
precision("outstanding_amount")); this.frm.doc.outstanding_amount = flt(this.frm.doc.total_amount_to_pay - this.frm.doc.total_advance,
precision("outstanding_amount"));
}
} }
} }
}); });

View File

@@ -41,8 +41,7 @@ class PurchaseCommon(BuyingController):
def validate_for_items(self, obj): def validate_for_items(self, obj):
items = [] items = []
for d in obj.get("items"): for d in obj.get("items"):
# validation for valid qty if not d.qty:
if flt(d.qty) < 0 or (d.parenttype != 'Purchase Receipt' and not flt(d.qty)):
frappe.throw(_("Please enter quantity for Item {0}").format(d.item_code)) frappe.throw(_("Please enter quantity for Item {0}").format(d.item_code))
# udpate with latest quantities # udpate with latest quantities

View File

@@ -11,39 +11,32 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend(
this._super(); this._super();
// this.frm.dashboard.reset(); // this.frm.dashboard.reset();
if(doc.docstatus == 1 && doc.status != 'Stopped'){ 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(flt(doc.per_received, 2) < 100) { if(flt(doc.per_received, 2) < 100) {
cur_frm.add_custom_button(__('Make Purchase Receipt'), cur_frm.add_custom_button(__('Make Purchase Receipt'), this.make_purchase_receipt);
this.make_purchase_receipt);
if(doc.is_subcontracted==="Yes") { if(doc.is_subcontracted==="Yes") {
cur_frm.add_custom_button(__('Transfer Material to Supplier'), cur_frm.add_custom_button(__('Transfer Material to Supplier'), this.make_stock_entry);
function() { me.make_stock_entry() });
} }
} }
if(flt(doc.per_billed, 2) < 100) if(flt(doc.per_billed, 2) < 100)
cur_frm.add_custom_button(__('Make Invoice'), this.make_purchase_invoice, cur_frm.add_custom_button(__('Make Invoice'), this.make_purchase_invoice);
frappe.boot.doctype_icons["Purchase Invoice"]);
if(flt(doc.per_billed, 2) < 100 || doc.per_received < 100) if(flt(doc.per_billed, 2) < 100 || doc.per_received < 100)
cur_frm.add_custom_button(__('Stop'), cur_frm.cscript['Stop Purchase Order'], cur_frm.add_custom_button(__('Stop'), cur_frm.cscript['Stop Purchase Order']);
"icon-exclamation", "btn-default");
} else if(doc.docstatus===0) { } else if(doc.docstatus===0) {
cur_frm.cscript.add_from_mappers(); cur_frm.cscript.add_from_mappers();
} }
if(doc.docstatus == 1 && doc.status == 'Stopped') if(doc.docstatus == 1 && doc.status == 'Stopped')
cur_frm.add_custom_button(__('Unstop Purchase Order'), cur_frm.add_custom_button(__('Unstop Purchase Order'), cur_frm.cscript['Unstop Purchase Order']);
cur_frm.cscript['Unstop Purchase Order'], "icon-check");
}, },
make_stock_entry: function() { make_stock_entry: function() {
var items = $.map(cur_frm.doc.items, function(d) { return d.bom ? d.item_code : false; }), 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) { if(items.length===1) {
me._make_stock_entry(items[0]); me._make_stock_entry(items[0]);
return; return;
@@ -96,7 +89,7 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend(
company: cur_frm.doc.company company: cur_frm.doc.company
} }
}) })
}, "icon-download", "btn-default" }
); );
cur_frm.add_custom_button(__('From Supplier Quotation'), cur_frm.add_custom_button(__('From Supplier Quotation'),
@@ -110,7 +103,7 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend(
company: cur_frm.doc.company company: cur_frm.doc.company
} }
}) })
}, "icon-download", "btn-default" }
); );
cur_frm.add_custom_button(__('For Supplier'), cur_frm.add_custom_button(__('For Supplier'),
@@ -122,7 +115,7 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend(
docstatus: ["!=", 2], docstatus: ["!=", 2],
} }
}) })
}, "icon-download", "btn-default" }
); );
}, },

View File

@@ -9,6 +9,7 @@ from erpnext.setup.utils import get_company_currency, get_exchange_rate
from erpnext.accounts.utils import get_fiscal_year, validate_fiscal_year from erpnext.accounts.utils import get_fiscal_year, validate_fiscal_year
from erpnext.utilities.transaction_base import TransactionBase from erpnext.utilities.transaction_base import TransactionBase
from erpnext.controllers.recurring_document import convert_to_recurring, validate_recurring_document from erpnext.controllers.recurring_document import convert_to_recurring, validate_recurring_document
from erpnext.controllers.sales_and_purchase_return import validate_return
class AccountsController(TransactionBase): class AccountsController(TransactionBase):
def validate(self): def validate(self):
@@ -17,10 +18,14 @@ class AccountsController(TransactionBase):
self.validate_date_with_fiscal_year() self.validate_date_with_fiscal_year()
if self.meta.get_field("currency"): if self.meta.get_field("currency"):
self.calculate_taxes_and_totals() 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)
validate_return(self)
self.set_total_in_words() self.set_total_in_words()
self.validate_due_date() if self.doctype in ("Sales Invoice", "Purchase Invoice") and not self.is_return:
self.validate_due_date()
if self.meta.get_field("is_recurring"): if self.meta.get_field("is_recurring"):
validate_recurring_document(self) validate_recurring_document(self)
@@ -74,6 +79,9 @@ class AccountsController(TransactionBase):
def validate_due_date(self): def validate_due_date(self):
from erpnext.accounts.party import validate_due_date from erpnext.accounts.party import validate_due_date
if self.doctype == "Sales Invoice": 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) validate_due_date(self.posting_date, self.due_date, "Customer", self.customer, self.company)
elif self.doctype == "Purchase Invoice": elif self.doctype == "Purchase Invoice":
validate_due_date(self.posting_date, self.due_date, "Supplier", self.supplier, self.company) validate_due_date(self.posting_date, self.due_date, "Supplier", self.supplier, self.company)

View File

@@ -26,8 +26,7 @@ class BuyingController(StockController):
def validate(self): def validate(self):
super(BuyingController, self).validate() super(BuyingController, self).validate()
if getattr(self, "supplier", None) and not self.supplier_name: if getattr(self, "supplier", None) and not self.supplier_name:
self.supplier_name = frappe.db.get_value("Supplier", self.supplier_name = frappe.db.get_value("Supplier", self.supplier, "supplier_name")
self.supplier, "supplier_name")
self.is_item_table_empty() self.is_item_table_empty()
self.set_qty_as_per_stock_uom() self.set_qty_as_per_stock_uom()
self.validate_stock_or_nonstock_items() self.validate_stock_or_nonstock_items()

View File

@@ -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

View File

@@ -110,15 +110,14 @@ class SellingController(StockController):
from frappe.utils import money_in_words from frappe.utils import money_in_words
company_currency = get_company_currency(self.company) company_currency = get_company_currency(self.company)
disable_rounded_total = cint(frappe.db.get_value("Global Defaults", None, disable_rounded_total = cint(frappe.db.get_value("Global Defaults", None, "disable_rounded_total"))
"disable_rounded_total"))
if self.meta.get_field("base_in_words"): if self.meta.get_field("base_in_words"):
self.base_in_words = money_in_words(disable_rounded_total and 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"): if self.meta.get_field("in_words"):
self.in_words = money_in_words(disable_rounded_total and 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): def calculate_commission(self):
if self.meta.get_field("commission_rate"): if self.meta.get_field("commission_rate"):
@@ -175,7 +174,7 @@ class SellingController(StockController):
if flt(d.qty) > flt(d.delivered_qty): if flt(d.qty) > flt(d.delivered_qty):
reserved_qty_for_main_item = 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. # 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 # But in this case reserved qty should only be reduced by 10 and not 12
@@ -211,7 +210,7 @@ class SellingController(StockController):
'qty': d.qty, 'qty': d.qty,
'reserved_qty': reserved_qty_for_main_item, 'reserved_qty': reserved_qty_for_main_item,
'uom': d.stock_uom, 'uom': d.stock_uom,
'stock_uom': d.stock_uom, 'stock_uom': d.stock_uom,
'batch_no': cstr(d.get("batch_no")).strip(), 'batch_no': cstr(d.get("batch_no")).strip(),
'serial_no': cstr(d.get("serial_no")).strip(), 'serial_no': cstr(d.get("serial_no")).strip(),
'name': d.name 'name': d.name

View File

@@ -217,6 +217,17 @@ class StockController(AccountsController):
return serialized_items 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, def update_gl_entries_after(posting_date, posting_time, for_warehouses=None, for_items=None,
warehouse_account=None): warehouse_account=None):
def _delete_gl_entries(voucher_type, voucher_no): def _delete_gl_entries(voucher_type, voucher_no):

View File

@@ -78,6 +78,9 @@ class calculate_taxes_and_totals(object):
validate_taxes_and_charges(tax) validate_taxes_and_charges(tax)
validate_inclusive_tax(tax, self.doc) 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.item_wise_tax_detail = {}
tax_fields = ["total", "tax_amount_after_discount_amount", tax_fields = ["total", "tax_amount_after_discount_amount",
"tax_amount_for_current_item", "grand_total_for_current_item", "tax_amount_for_current_item", "grand_total_for_current_item",
@@ -396,13 +399,15 @@ class calculate_taxes_and_totals(object):
# total_advance is only for non POS Invoice # total_advance is only for non POS Invoice
if self.doc.doctype == "Sales Invoice": if self.doc.doctype == "Sales Invoice":
self.doc.round_floats_in(self.doc, ["base_grand_total", "total_advance", "write_off_amount", "paid_amount"]) if not self.doc.is_return:
total_amount_to_pay = self.doc.base_grand_total - self.doc.write_off_amount self.doc.round_floats_in(self.doc, ["base_grand_total", "total_advance", "write_off_amount", "paid_amount"])
self.doc.outstanding_amount = flt(total_amount_to_pay - self.doc.total_advance - self.doc.paid_amount, total_amount_to_pay = self.doc.base_grand_total - self.doc.write_off_amount
self.doc.precision("outstanding_amount")) self.doc.outstanding_amount = flt(total_amount_to_pay - self.doc.total_advance - self.doc.paid_amount,
self.doc.precision("outstanding_amount"))
else: else:
self.doc.round_floats_in(self.doc, ["total_advance", "write_off_amount"]) 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.total_amount_to_pay = flt(self.doc.base_grand_total - self.doc.write_off_amount,
self.doc.precision("total_amount_to_pay")) self.doc.precision("total_amount_to_pay"))
self.doc.outstanding_amount = flt(self.doc.total_amount_to_pay - self.doc.total_advance, if not self.doc.is_return:
self.doc.precision("outstanding_amount")) self.doc.outstanding_amount = flt(self.doc.total_amount_to_pay - self.doc.total_advance,
self.doc.precision("outstanding_amount"))

View File

@@ -13,8 +13,9 @@ erpnext.taxes_and_totals = erpnext.stock.StockController.extend({
this.apply_discount_amount(); this.apply_discount_amount();
// Advance calculation applicable to Sales /Purchase Invoice // Advance calculation applicable to Sales /Purchase Invoice
if(in_list(["Sales Invoice", "Purchase Invoice"], this.frm.doc.doctype) && this.frm.doc.docstatus < 2) { if(in_list(["Sales Invoice", "Purchase Invoice"], this.frm.doc.doctype)
this.calculate_total_advance(update_paid_amount); && this.frm.doc.docstatus < 2 && !this.frm.doc.is_return) {
this.calculate_total_advance(update_paid_amount);
} }
// Sales person's commission // Sales person's commission
@@ -94,6 +95,10 @@ erpnext.taxes_and_totals = erpnext.stock.StockController.extend({
"tax_amount_for_current_item", "grand_total_for_current_item", "tax_amount_for_current_item", "grand_total_for_current_item",
"tax_fraction_for_current_item", "grand_total_fraction_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" && if (cstr(tax.charge_type) != "Actual" &&
!(me.discount_amount_applied && me.frm.doc.apply_discount_on=="Grand Total")) !(me.discount_amount_applied && me.frm.doc.apply_discount_on=="Grand Total"))
tax_fields.push("tax_amount"); tax_fields.push("tax_amount");

View File

@@ -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() { onload_post_render: function() {
@@ -354,7 +371,8 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
plc_conversion_rate: function() { plc_conversion_rate: function() {
if(this.frm.doc.price_list_currency === this.get_company_currency()) { if(this.frm.doc.price_list_currency === this.get_company_currency()) {
this.frm.set_value("plc_conversion_rate", 1.0); 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)) { 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); this.frm.set_value("conversion_rate", this.frm.doc.plc_conversion_rate);
} }

View File

@@ -18,35 +18,31 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend(
// delivery note // delivery note
if(flt(doc.per_delivered, 2) < 100 && ["Sales", "Shopping Cart"].indexOf(doc.order_type)!==-1) 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 // indent
if(!doc.order_type || ["Sales", "Shopping Cart"].indexOf(doc.order_type)!==-1) if(!doc.order_type || ["Sales", "Shopping Cart"].indexOf(doc.order_type)!==-1)
cur_frm.add_custom_button(__('Make ') + __('Material Request'), cur_frm.add_custom_button(__('Make ') + __('Material Request'),
this.make_material_request, "icon-ticket"); this.make_material_request);
// sales invoice // sales invoice
if(flt(doc.per_billed, 2) < 100) { if(flt(doc.per_billed, 2) < 100) {
cur_frm.add_custom_button(__('Make Invoice'), this.make_sales_invoice, cur_frm.add_custom_button(__('Make Invoice'), this.make_sales_invoice);
frappe.boot.doctype_icons["Sales Invoice"]);
} }
// stop // stop
if(flt(doc.per_delivered, 2) < 100 || doc.per_billed < 100) if(flt(doc.per_delivered, 2) < 100 || doc.per_billed < 100)
cur_frm.add_custom_button(__('Stop'), cur_frm.cscript['Stop Sales Order'], cur_frm.add_custom_button(__('Stop'), cur_frm.cscript['Stop Sales Order'])
"icon-exclamation", "btn-default")
// maintenance // maintenance
if(flt(doc.per_delivered, 2) < 100 && ["Sales", "Shopping Cart"].indexOf(doc.order_type)===-1) { if(flt(doc.per_delivered, 2) < 100 && ["Sales", "Shopping Cart"].indexOf(doc.order_type)===-1) {
cur_frm.add_custom_button(__('Make Maint. Visit'), cur_frm.add_custom_button(__('Make Maint. Visit'), this.make_maintenance_visit);
this.make_maintenance_visit, null, "btn-default"); cur_frm.add_custom_button(__('Make Maint. Schedule'), this.make_maintenance_schedule);
cur_frm.add_custom_button(__('Make Maint. Schedule'),
this.make_maintenance_schedule, null, "btn-default");
} }
} else { } else {
// un-stop // 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 company: cur_frm.doc.company
} }
}) })
}, "icon-download", "btn-default"); });
} }
this.order_type(doc); this.order_type(doc);

View File

@@ -210,7 +210,7 @@ erpnext.selling.SellingController = erpnext.TransactionController.extend({
// NOTE: // NOTE:
// paid_amount and write_off_amount is only for POS Invoice // paid_amount and write_off_amount is only for POS Invoice
// total_advance is only for non 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", frappe.model.round_floats_in(this.frm.doc, ["base_grand_total", "total_advance", "write_off_amount",
"paid_amount"]); "paid_amount"]);
var total_amount_to_pay = this.frm.doc.base_grand_total - this.frm.doc.write_off_amount var total_amount_to_pay = this.frm.doc.base_grand_total - this.frm.doc.write_off_amount

View File

@@ -24,6 +24,7 @@ erpnext.stock.DeliveryNoteController = erpnext.selling.SellingController.extend(
cur_frm.add_custom_button(__('Make Installation Note'), this.make_installation_note); cur_frm.add_custom_button(__('Make Installation Note'), this.make_installation_note);
if (doc.docstatus==1) { if (doc.docstatus==1) {
cur_frm.add_custom_button(__('Make Sales Return'), this.make_sales_return);
this.show_stock_ledger(); this.show_stock_ledger();
this.show_general_ledger(); this.show_general_ledger();
@@ -31,7 +32,7 @@ erpnext.stock.DeliveryNoteController = erpnext.selling.SellingController.extend(
if(doc.docstatus==0 && !doc.__islocal) { if(doc.docstatus==0 && !doc.__islocal) {
cur_frm.add_custom_button(__('Make Packing Slip'), 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); erpnext.stock.delivery_note.set_print_hide(doc, dt, dn);
@@ -55,7 +56,7 @@ erpnext.stock.DeliveryNoteController = erpnext.selling.SellingController.extend(
company: cur_frm.doc.company company: cur_frm.doc.company
} }
}) })
}, "icon-download", "btn-default"); });
} }
}, },
@@ -74,6 +75,13 @@ erpnext.stock.DeliveryNoteController = erpnext.selling.SellingController.extend(
}); });
}, },
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() { tc_name: function() {
this.get_terms(); this.get_terms();
}, },

View File

@@ -29,7 +29,7 @@
"no_copy": 1, "no_copy": 1,
"oldfieldname": "naming_series", "oldfieldname": "naming_series",
"oldfieldtype": "Select", "oldfieldtype": "Select",
"options": "DN-", "options": "DN-\nDN-RET-",
"permlevel": 0, "permlevel": 0,
"print_hide": 1, "print_hide": 1,
"read_only": 0, "read_only": 0,
@@ -205,6 +205,28 @@
"read_only": 1, "read_only": 1,
"width": "100px" "width": "100px"
}, },
{
"fieldname": "is_return",
"fieldtype": "Check",
"label": "Is Return",
"no_copy": 0,
"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": 0,
"options": "Delivery Note",
"permlevel": 0,
"precision": "",
"print_hide": 1,
"read_only": 1
},
{ {
"fieldname": "cusrrency_and_price_list", "fieldname": "cusrrency_and_price_list",
"fieldtype": "Section Break", "fieldtype": "Section Break",
@@ -1070,7 +1092,7 @@
"idx": 1, "idx": 1,
"in_create": 0, "in_create": 0,
"is_submittable": 1, "is_submittable": 1,
"modified": "2015-07-13 05:28:29.814096", "modified": "2015-07-24 11:49:15.056249",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Delivery Note", "name": "Delivery Note",

View File

@@ -84,7 +84,7 @@ class DeliveryNote(SellingController):
def so_required(self): def so_required(self):
"""check in manage account if sales order required or not""" """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'): for d in self.get('items'):
if not d.against_sales_order: if not d.against_sales_order:
frappe.throw(_("Sales Order required for Item {0}").format(d.item_code)) frappe.throw(_("Sales Order required for Item {0}").format(d.item_code))
@@ -175,17 +175,15 @@ class DeliveryNote(SellingController):
# Check for Approving Authority # Check for Approving Authority
frappe.get_doc('Authorization Control').validate_approving_authority(self.doctype, self.company, self.base_grand_total, self) frappe.get_doc('Authorization Control').validate_approving_authority(self.doctype, self.company, self.base_grand_total, self)
# update delivered qty in sales order if not self.is_return:
self.update_prevdoc_status() # 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.update_stock_ledger()
self.make_gl_entries() self.make_gl_entries()
# set DN status
frappe.db.set(self, 'status', 'Submitted') frappe.db.set(self, 'status', 'Submitted')
@@ -193,7 +191,8 @@ class DeliveryNote(SellingController):
self.check_stop_sales_order("against_sales_order") self.check_stop_sales_order("against_sales_order")
self.check_next_docstatus() self.check_next_docstatus()
self.update_prevdoc_status() if not self.is_return:
self.update_prevdoc_status()
self.update_stock_ledger() self.update_stock_ledger()
@@ -252,8 +251,13 @@ class DeliveryNote(SellingController):
and d.warehouse and flt(d['qty']): and d.warehouse and flt(d['qty']):
self.update_reserved_qty(d) 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, { sl_entries.append(self.get_sl_entries(d, {
"actual_qty": -1*flt(d['qty']), "actual_qty": -1*flt(d['qty']),
"incoming_rate": incoming_rate
})) }))
self.make_sl_entries(sl_entries) self.make_sl_entries(sl_entries)
@@ -387,3 +391,9 @@ def make_packing_slip(source_name, target_doc=None):
}, target_doc) }, target_doc)
return doclist return doclist
@frappe.whitelist()
def make_sales_return(source_name, target_doc=None):
from erpnext.controllers.sales_and_purchase_return import make_return_doc
return make_return_doc("Delivery Note", source_name, target_doc)

View File

@@ -13,8 +13,10 @@ from erpnext.accounts.utils import get_balance_on
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt \ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt \
import get_gl_entries, set_perpetual_inventory import get_gl_entries, set_perpetual_inventory
from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice 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.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): class TestDeliveryNote(unittest.TestCase):
def test_over_billing_against_dn(self): def test_over_billing_against_dn(self):
@@ -175,9 +177,155 @@ class TestDeliveryNote(unittest.TestCase):
self.assertRaises(SerialNoStatusError, dn.submit) self.assertRaises(SerialNoStatusError, dn.submit)
def check_serial_no_values(self, serial_no, field_values): 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(): 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()
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 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): def create_delivery_note(**args):
dn = frappe.new_doc("Delivery Note") dn = frappe.new_doc("Delivery Note")
@@ -190,6 +338,8 @@ def create_delivery_note(**args):
dn.company = args.company or "_Test Company" dn.company = args.company or "_Test Company"
dn.customer = args.customer or "_Test Customer" dn.customer = args.customer or "_Test Customer"
dn.currency = args.currency or "INR" dn.currency = args.currency or "INR"
dn.is_return = args.is_return
dn.return_against = args.return_against
dn.append("items", { dn.append("items", {
"item_code": args.item or args.item_code or "_Test Item", "item_code": args.item or args.item_code or "_Test Item",

View File

@@ -31,10 +31,11 @@ erpnext.stock.PurchaseReceiptController = erpnext.buying.BuyingController.extend
if(this.frm.doc.docstatus == 1) { if(this.frm.doc.docstatus == 1) {
if(this.frm.doc.__onload && !this.frm.doc.__onload.billing_complete) { if(this.frm.doc.__onload && !this.frm.doc.__onload.billing_complete) {
cur_frm.add_custom_button(__('Make Purchase Invoice'), this.make_purchase_invoice, 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);
this.show_stock_ledger(); this.show_stock_ledger();
this.show_general_ledger(); this.show_general_ledger();
} else { } else {
@@ -51,7 +52,7 @@ erpnext.stock.PurchaseReceiptController = erpnext.buying.BuyingController.extend
company: cur_frm.doc.company company: cur_frm.doc.company
} }
}) })
}, "icon-download", "btn-default"); });
} }
this.frm.toggle_reqd("supplier_warehouse", this.frm.doc.is_subcontracted==="Yes"); this.frm.toggle_reqd("supplier_warehouse", this.frm.doc.is_subcontracted==="Yes");
@@ -106,6 +107,13 @@ erpnext.stock.PurchaseReceiptController = erpnext.buying.BuyingController.extend
}) })
}, },
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() { tc_name: function() {
this.get_terms(); this.get_terms();
}, },

View File

@@ -21,13 +21,14 @@
"width": "50%" "width": "50%"
}, },
{ {
"default": "",
"fieldname": "naming_series", "fieldname": "naming_series",
"fieldtype": "Select", "fieldtype": "Select",
"label": "Series", "label": "Series",
"no_copy": 1, "no_copy": 1,
"oldfieldname": "naming_series", "oldfieldname": "naming_series",
"oldfieldtype": "Select", "oldfieldtype": "Select",
"options": "PREC-", "options": "PREC-\nPREC-RET-",
"permlevel": 0, "permlevel": 0,
"print_hide": 1, "print_hide": 1,
"reqd": 1 "reqd": 1
@@ -130,6 +131,28 @@
"search_index": 0, "search_index": 0,
"width": "100px" "width": "100px"
}, },
{
"fieldname": "is_return",
"fieldtype": "Check",
"label": "Is Return",
"no_copy": 0,
"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": 0,
"options": "Purchase Receipt",
"permlevel": 0,
"precision": "",
"print_hide": 1,
"read_only": 1
},
{ {
"fieldname": "currency_and_price_list", "fieldname": "currency_and_price_list",
"fieldtype": "Section Break", "fieldtype": "Section Break",
@@ -854,7 +877,7 @@
"icon": "icon-truck", "icon": "icon-truck",
"idx": 1, "idx": 1,
"is_submittable": 1, "is_submittable": 1,
"modified": "2015-07-13 05:28:27.389559", "modified": "2015-07-24 11:49:35.580382",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Purchase Receipt", "name": "Purchase Receipt",

View File

@@ -44,6 +44,7 @@ class PurchaseReceipt(BuyingController):
self.set_status() self.set_status()
self.po_required() self.po_required()
self.validate_with_previous_doc() self.validate_with_previous_doc()
self.validate_purchase_return()
self.validate_rejected_warehouse() self.validate_rejected_warehouse()
self.validate_accepted_rejected_qty() self.validate_accepted_rejected_qty()
self.validate_inspection() self.validate_inspection()
@@ -60,6 +61,7 @@ class PurchaseReceipt(BuyingController):
self.set_landed_cost_voucher_amount() self.set_landed_cost_voucher_amount()
self.update_valuation_rate("items") self.update_valuation_rate("items")
def set_landed_cost_voucher_amount(self): def set_landed_cost_voucher_amount(self):
for d in self.get("items"): for d in self.get("items"):
lc_voucher_amount = frappe.db.sql("""select sum(ifnull(applicable_charges, 0)) lc_voucher_amount = frappe.db.sql("""select sum(ifnull(applicable_charges, 0))
@@ -67,6 +69,13 @@ class PurchaseReceipt(BuyingController):
where docstatus = 1 and purchase_receipt_item = %s""", d.name) 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 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"):
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): def validate_rejected_warehouse(self):
for d in self.get("items"): for d in self.get("items"):
if flt(d.rejected_qty) and not d.rejected_warehouse: if flt(d.rejected_qty) and not d.rejected_warehouse:
@@ -108,7 +117,7 @@ class PurchaseReceipt(BuyingController):
self.validate_rate_with_reference_doc([["Purchase Order", "prevdoc_docname", "prevdoc_detail_docname"]]) self.validate_rate_with_reference_doc([["Purchase Order", "prevdoc_docname", "prevdoc_detail_docname"]])
def po_required(self): 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'): for d in self.get('items'):
if not d.prevdoc_docname: if not d.prevdoc_docname:
frappe.throw(_("Purchase Order number required for Item {0}").format(d.item_code)) frappe.throw(_("Purchase Order number required for Item {0}").format(d.item_code))
@@ -123,11 +132,20 @@ class PurchaseReceipt(BuyingController):
if pr_qty: if pr_qty:
val_rate_db_precision = 6 if cint(self.precision("valuation_rate", d)) <= 6 else 9 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), "actual_qty": flt(pr_qty),
"serial_no": cstr(d.serial_no).strip(), "serial_no": cstr(d.serial_no).strip()
"incoming_rate": flt(d.valuation_rate, val_rate_db_precision) })
})) 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: if flt(d.rejected_qty) > 0:
sl_entries.append(self.get_sl_entries(d, { sl_entries.append(self.get_sl_entries(d, {
@@ -176,7 +194,6 @@ class PurchaseReceipt(BuyingController):
"item_code": d.rm_item_code, "item_code": d.rm_item_code,
"warehouse": self.supplier_warehouse, "warehouse": self.supplier_warehouse,
"actual_qty": -1*flt(d.consumed_qty), "actual_qty": -1*flt(d.consumed_qty),
"incoming_rate": 0
})) }))
def validate_inspection(self): def validate_inspection(self):
@@ -207,17 +224,16 @@ class PurchaseReceipt(BuyingController):
# Set status as Submitted # Set status as Submitted
frappe.db.set(self, 'status', 'Submitted') frappe.db.set(self, 'status', 'Submitted')
self.update_prevdoc_status() if not self.is_return:
self.update_prevdoc_status()
self.update_ordered_qty() self.update_ordered_qty()
purchase_controller.update_last_purchase_rate(self, 1)
self.update_stock_ledger() self.update_stock_ledger()
from erpnext.stock.doctype.serial_no.serial_no import update_serial_nos_after_submit from erpnext.stock.doctype.serial_no.serial_no import update_serial_nos_after_submit
update_serial_nos_after_submit(self, "items") update_serial_nos_after_submit(self, "items")
purchase_controller.update_last_purchase_rate(self, 1)
self.make_gl_entries() self.make_gl_entries()
def check_next_docstatus(self): def check_next_docstatus(self):
@@ -244,12 +260,13 @@ class PurchaseReceipt(BuyingController):
self.update_stock_ledger() 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 # Must be called after updating received qty in PO
self.update_ordered_qty() 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() self.make_gl_entries_on_cancel()
@@ -417,7 +434,7 @@ def make_purchase_invoice(source_name, target_doc=None):
"doctype": "Purchase Invoice", "doctype": "Purchase Invoice",
"validation": { "validation": {
"docstatus": ["=", 1], "docstatus": ["=", 1],
} },
}, },
"Purchase Receipt Item": { "Purchase Receipt Item": {
"doctype": "Purchase Invoice Item", "doctype": "Purchase Invoice Item",
@@ -449,3 +466,8 @@ def get_invoiced_qty_map(purchase_receipt):
invoiced_qty_map[pr_detail] += qty invoiced_qty_map[pr_detail] += qty
return invoiced_qty_map return invoiced_qty_map
@frappe.whitelist()
def make_purchase_return(source_name, target_doc=None):
from erpnext.controllers.sales_and_purchase_return import make_return_doc
return make_return_doc("Purchase Receipt", source_name, target_doc)

View File

@@ -6,7 +6,7 @@ from __future__ import unicode_literals
import unittest import unittest
import frappe import frappe
import frappe.defaults import frappe.defaults
from frappe.utils import cint, flt from frappe.utils import cint, flt, cstr
class TestPurchaseReceipt(unittest.TestCase): class TestPurchaseReceipt(unittest.TestCase):
def test_make_purchase_invoice(self): def test_make_purchase_invoice(self):
@@ -120,6 +120,65 @@ class TestPurchaseReceipt(unittest.TestCase):
self.assertEquals(frappe.db.get_value("Serial No", serial_no, "warehouse"), self.assertEquals(frappe.db.get_value("Serial No", serial_no, "warehouse"),
pr.get("items")[0].rejected_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 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): def get_gl_entries(voucher_type, voucher_no):
return frappe.db.sql("""select account, debit, credit return frappe.db.sql("""select account, debit, credit
from `tabGL Entry` where voucher_type=%s and voucher_no=%s from `tabGL Entry` where voucher_type=%s and voucher_no=%s
@@ -142,6 +201,8 @@ def make_purchase_receipt(**args):
pr.is_subcontracted = args.is_subcontracted or "No" pr.is_subcontracted = args.is_subcontracted or "No"
pr.supplier_warehouse = "_Test Warehouse 1 - _TC" pr.supplier_warehouse = "_Test Warehouse 1 - _TC"
pr.currency = args.currency or "INR" pr.currency = args.currency or "INR"
pr.is_return = args.is_return
pr.return_against = args.return_against
pr.append("items", { pr.append("items", {
"item_code": args.item or args.item_code or "_Test Item", "item_code": args.item or args.item_code or "_Test Item",

View File

@@ -244,7 +244,7 @@
"in_filter": 1, "in_filter": 1,
"label": "Delivery Document Type", "label": "Delivery Document Type",
"no_copy": 1, "no_copy": 1,
"options": "\nDelivery Note\nSales Invoice\nStock Entry", "options": "\nDelivery Note\nSales Invoice\nStock Entry\nPurchase Receipt",
"permlevel": 0, "permlevel": 0,
"read_only": 1 "read_only": 1
}, },
@@ -418,7 +418,7 @@
"icon": "icon-barcode", "icon": "icon-barcode",
"idx": 1, "idx": 1,
"in_create": 0, "in_create": 0,
"modified": "2015-07-13 05:28:27.961178", "modified": "2015-07-24 03:55:29.946944",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Serial No", "name": "Serial No",

View File

@@ -34,9 +34,6 @@ class SerialNo(StockController):
self.validate_item() self.validate_item()
self.on_stock_ledger_entry() 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): def set_maintenance_status(self):
if not self.warranty_expiry_date and not self.amc_expiry_date: if not self.warranty_expiry_date and not self.amc_expiry_date:
self.maintenance_status = None self.maintenance_status = None
@@ -81,20 +78,19 @@ class SerialNo(StockController):
def set_status(self, last_sle): def set_status(self, last_sle):
if last_sle: if last_sle:
if last_sle.voucher_type == "Stock Entry": if last_sle.voucher_type == "Stock Entry":
document_type = frappe.db.get_value("Stock Entry", last_sle.voucher_no, document_type = frappe.db.get_value("Stock Entry", last_sle.voucher_no, "purpose")
"purpose")
else: else:
document_type = last_sle.voucher_type document_type = last_sle.voucher_type
if last_sle.actual_qty > 0: 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" self.status = "Sales Returned"
else: else:
self.status = "Available" self.status = "Available"
else: else:
if document_type == "Purchase Return": if document_type in ("Purchase Receipt", "Purchase Invoice", "Purchase Return"):
self.status = "Purchase Returned" 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" self.status = "Delivered"
else: else:
self.status = "Not Available" self.status = "Not Available"
@@ -123,9 +119,10 @@ class SerialNo(StockController):
self.delivery_document_no = delivery_sle.voucher_no self.delivery_document_no = delivery_sle.voucher_no
self.delivery_date = delivery_sle.posting_date self.delivery_date = delivery_sle.posting_date
self.delivery_time = delivery_sle.posting_time self.delivery_time = delivery_sle.posting_time
self.customer, self.customer_name = \ if delivery_sle.voucher_type in ("Delivery Note", "Sales Invoice"):
frappe.db.get_value(delivery_sle.voucher_type, delivery_sle.voucher_no, self.customer, self.customer_name = \
["customer", "customer_name"]) frappe.db.get_value(delivery_sle.voucher_type, delivery_sle.voucher_no,
["customer", "customer_name"])
if self.warranty_period: if self.warranty_period:
self.warranty_expiry_date = add_days(cstr(delivery_sle.posting_date), self.warranty_expiry_date = add_days(cstr(delivery_sle.posting_date),
cint(self.warranty_period)) cint(self.warranty_period))
@@ -235,10 +232,10 @@ def validate_serial_no(sle, item_det):
frappe.throw(_("Serial No {0} does not belong to Warehouse {1}").format(serial_no, frappe.throw(_("Serial No {0} does not belong to Warehouse {1}").format(serial_no,
sle.warehouse), SerialNoWarehouseError) 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": and sr.status != "Available":
frappe.throw(_("Serial No {0} status must be 'Available' to Deliver").format(serial_no), frappe.throw(_("Serial No {0} status must be 'Available' to Deliver").format(serial_no),
SerialNoStatusError) SerialNoStatusError)
elif sle.actual_qty < 0: elif sle.actual_qty < 0:
# transfer out # transfer out

View File

@@ -8,19 +8,6 @@ erpnext.stock.StockEntry = erpnext.stock.StockController.extend({
setup: function() { setup: function() {
var me = this; 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() { this.frm.fields_dict.bom_no.get_query = function() {
return { return {
filters:{ 'docstatus': 1 } 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() { this.frm.fields_dict.items.grid.get_field('item_code').get_query = function() {
if(in_list(["Sales Return", "Purchase Return"], me.frm.doc.purpose) && return erpnext.queries.item({is_stock_item: "Yes"});
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"});
}
}; };
this.frm.set_query("purchase_order", function() { this.frm.set_query("purchase_order", function() {
@@ -84,19 +58,6 @@ erpnext.stock.StockEntry = erpnext.stock.StockController.extend({
this.toggle_enable_bom(); this.toggle_enable_bom();
this.show_stock_ledger(); this.show_stock_ledger();
this.show_general_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() { on_submit: function() {
@@ -111,15 +72,10 @@ erpnext.stock.StockEntry = erpnext.stock.StockController.extend({
var me = this; var me = this;
if(cint(frappe.defaults.get_default("auto_accounting_for_stock")) && this.frm.doc.company) { 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({ return this.frm.call({
method: "erpnext.accounts.utils.get_company_default", method: "erpnext.accounts.utils.get_company_default",
args: { args: {
"fieldname": account_for, "fieldname": "stock_adjustment_account",
"company": this.frm.doc.company "company": this.frm.doc.company
}, },
callback: function(r) { 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)); 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() { add_excise_button: function() {
if(frappe.boot.sysdefaults.country === "India") if(frappe.boot.sysdefaults.country === "India")
this.frm.add_custom_button(__("Make Excise Invoice"), function() { 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"); }, 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) { items_add: function(doc, cdt, cdn) {
var row = frappe.get_doc(cdt, cdn); var row = frappe.get_doc(cdt, cdn);
this.frm.script_manager.copy_from_first_row("items", row, this.frm.script_manager.copy_from_first_row("items", row, ["expense_account", "cost_center"]);
["expense_account", "cost_center"]);
if(!row.s_warehouse) row.s_warehouse = this.frm.doc.from_warehouse; if(!row.s_warehouse) row.s_warehouse = this.frm.doc.from_warehouse;
if(!row.t_warehouse) row.t_warehouse = this.frm.doc.to_warehouse; if(!row.t_warehouse) row.t_warehouse = this.frm.doc.to_warehouse;
}, },
source_mandatory: ["Material Issue", "Material Transfer", "Purchase Return", "Subcontract", source_mandatory: ["Material Issue", "Material Transfer", "Subcontract", "Material Transfer for Manufacture"],
"Material Transfer for Manufacture"], target_mandatory: ["Material Receipt", "Material Transfer", "Subcontract", "Material Transfer for Manufacture"],
target_mandatory: ["Material Receipt", "Material Transfer", "Sales Return", "Subcontract",
"Material Transfer for Manufacture"],
from_warehouse: function(doc) { from_warehouse: function(doc) {
var me = this; var me = this;
@@ -295,92 +201,21 @@ erpnext.stock.StockEntry = erpnext.stock.StockController.extend({
items_on_form_rendered: function(doc, grid_row) { items_on_form_rendered: function(doc, grid_row) {
erpnext.setup_serial_no(); 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.script_manager.make(erpnext.stock.StockEntry);
cur_frm.cscript.toggle_related_fields = function(doc) { cur_frm.cscript.toggle_related_fields = function(doc) {
disable_from_warehouse = inList(["Material Receipt", "Sales Return"], doc.purpose); cur_frm.toggle_enable("from_warehouse", doc.purpose!='Material Receipt');
disable_to_warehouse = inList(["Material Issue", "Purchase Return"], doc.purpose); cur_frm.toggle_enable("to_warehouse", doc.purpose!='Material Issue');
cur_frm.toggle_enable("from_warehouse", !disable_from_warehouse); cur_frm.fields_dict["items"].grid.set_column_disp("s_warehouse", doc.purpose!='Material Receipt');
cur_frm.toggle_enable("to_warehouse", !disable_to_warehouse); cur_frm.fields_dict["items"].grid.set_column_disp("t_warehouse", doc.purpose!='Material Issue');
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.cscript.toggle_enable_bom(); cur_frm.cscript.toggle_enable_bom();
if(doc.purpose == 'Purchase Return') { if (doc.purpose == 'Subcontract') {
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') {
doc.customer = doc.customer_name = doc.customer_address = doc.customer = doc.customer_name = doc.customer_address =
doc.delivery_note_no = doc.sales_invoice_no = null; doc.delivery_note_no = doc.sales_invoice_no = null;
} else { } else {
@@ -388,7 +223,7 @@ cur_frm.cscript.toggle_related_fields = function(doc) {
doc.delivery_note_no = doc.sales_invoice_no = doc.supplier = doc.delivery_note_no = doc.sales_invoice_no = doc.supplier =
doc.supplier_name = doc.supplier_address = doc.purchase_receipt_no = null; 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); 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) { 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); 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"); 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) { cur_frm.cscript.company = function(doc, cdt, cdn) {
if(doc.company) { if(doc.company) {
erpnext.get_fiscal_year(doc.company, doc.posting_date, function() { erpnext.get_fiscal_year(doc.company, doc.posting_date, function() {

View File

@@ -54,7 +54,7 @@
"no_copy": 0, "no_copy": 0,
"oldfieldname": "purpose", "oldfieldname": "purpose",
"oldfieldtype": "Select", "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, "permlevel": 0,
"print_hide": 0, "print_hide": 0,
"read_only": 0, "read_only": 0,
@@ -678,7 +678,7 @@
"is_submittable": 1, "is_submittable": 1,
"issingle": 0, "issingle": 0,
"max_attachments": 0, "max_attachments": 0,
"modified": "2015-07-13 05:28:26.085266", "modified": "2015-07-22 18:47:20.328749",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Stock Entry", "name": "Stock Entry",

View File

@@ -4,10 +4,8 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
import frappe.defaults import frappe.defaults
from frappe.utils import cstr, cint, flt, comma_or, get_datetime, getdate
from frappe import _ 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.utils import get_incoming_rate
from erpnext.stock.stock_ledger import get_previous_sle, NegativeStockError from erpnext.stock.stock_ledger import get_previous_sle, NegativeStockError
from erpnext.controllers.queries import get_match_cond from erpnext.controllers.queries import get_match_cond
@@ -15,8 +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.manufacturing.doctype.bom.bom import validate_bom_no
from erpnext.accounts.utils import validate_fiscal_year from erpnext.accounts.utils import validate_fiscal_year
class NotUpdateStockError(frappe.ValidationError): pass
class StockOverReturnError(frappe.ValidationError): pass
class IncorrectValuationRateError(frappe.ValidationError): pass class IncorrectValuationRateError(frappe.ValidationError): pass
class DuplicateEntryForProductionOrderError(frappe.ValidationError): pass class DuplicateEntryForProductionOrderError(frappe.ValidationError): pass
class OperationsNotCompleteError(frappe.ValidationError): pass class OperationsNotCompleteError(frappe.ValidationError): pass
@@ -37,13 +33,6 @@ class StockEntry(StockController):
item.update(get_available_qty(item.item_code, item.update(get_available_qty(item.item_code,
item.s_warehouse)) 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): def validate(self):
self.pro_doc = None self.pro_doc = None
if self.production_order: if self.production_order:
@@ -61,7 +50,6 @@ class StockEntry(StockController):
self.get_stock_and_rate() self.get_stock_and_rate()
self.validate_bom() self.validate_bom()
self.validate_finished_goods() self.validate_finished_goods()
self.validate_return_reference_doc()
self.validate_with_material_request() self.validate_with_material_request()
self.validate_valuation_rate() self.validate_valuation_rate()
self.set_total_incoming_outgoing_value() self.set_total_incoming_outgoing_value()
@@ -84,16 +72,13 @@ class StockEntry(StockController):
def validate_purpose(self): def validate_purpose(self):
valid_purposes = ["Material Issue", "Material Receipt", "Material Transfer", "Material Transfer for Manufacture", 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: if self.purpose not in valid_purposes:
frappe.throw(_("Purpose must be one of {0}").format(comma_or(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") 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): def set_transfer_qty(self):
for item in self.get("items"): for item in self.get("items"):
if not flt(item.qty): if not flt(item.qty):
@@ -122,7 +107,7 @@ class StockEntry(StockController):
if not item.transfer_qty: if not item.transfer_qty:
item.transfer_qty = item.qty * item.conversion_factor 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 not item.serial_no
and item.item_code in serialized_items): and item.item_code in serialized_items):
frappe.throw(_("Row #{0}: Please specify Serial No for Item {1}").format(item.idx, item.item_code), frappe.throw(_("Row #{0}: Please specify Serial No for Item {1}").format(item.idx, item.item_code),
@@ -131,8 +116,8 @@ class StockEntry(StockController):
def validate_warehouse(self): def validate_warehouse(self):
"""perform various (sometimes conditional) validations on warehouse""" """perform various (sometimes conditional) validations on warehouse"""
source_mandatory = ["Material Issue", "Material Transfer", "Purchase Return", "Subcontract", "Material Transfer for Manufacture"] source_mandatory = ["Material Issue", "Material Transfer", "Subcontract", "Material Transfer for Manufacture"]
target_mandatory = ["Material Receipt", "Material Transfer", "Sales Return", "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")]) validate_for_manufacture_repack = any([d.bom_no for d in self.get("items")])
@@ -289,8 +274,8 @@ class StockEntry(StockController):
# get incoming rate # get incoming rate
if not d.bom_no: if not d.bom_no:
if not flt(d.incoming_rate) or d.s_warehouse or self.purpose == "Sales Return" or force: if not flt(d.incoming_rate) or d.s_warehouse or force:
incoming_rate = flt(self.get_incoming_rate(args), self.precision("incoming_rate", d)) incoming_rate = flt(get_incoming_rate(args), self.precision("incoming_rate", d))
if incoming_rate > 0: if incoming_rate > 0:
d.incoming_rate = incoming_rate d.incoming_rate = incoming_rate
@@ -334,27 +319,6 @@ class StockEntry(StockController):
return operation_cost_per_unit + (flt(self.additional_operating_cost) / flt(qty)) 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): def validate_purchase_order(self):
"""Throw exception if more raw material is transferred against Purchase Order than in """Throw exception if more raw material is transferred against Purchase Order than in
the raw materials supplied table""" the raw materials supplied table"""
@@ -401,55 +365,6 @@ class StockEntry(StockController):
frappe.throw(_("Finished Item {0} must be entered for Manufacture type entry") frappe.throw(_("Finished Item {0} must be entered for Manufacture type entry")
.format(production_item)) .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): def update_stock_ledger(self):
sl_entries = [] sl_entries = []
for d in self.get('items'): for d in self.get('items'):
@@ -512,6 +427,7 @@ class StockEntry(StockController):
(args.get('item_code')), as_dict = 1) (args.get('item_code')), as_dict = 1)
if not item: if not item:
frappe.throw(_("Item {0} is not active or end of life has been reached").format(args.get("item_code"))) frappe.throw(_("Item {0} is not active or end of life has been reached").format(args.get("item_code")))
item = item[0] item = item[0]
ret = { ret = {
@@ -559,7 +475,7 @@ class StockEntry(StockController):
ret = { ret = {
"actual_qty" : get_previous_sle(args).get("qty_after_transaction") or 0, "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 return ret
@@ -736,15 +652,6 @@ class StockEntry(StockController):
if getdate(self.posting_date) > getdate(expiry_date): 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.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() @frappe.whitelist()
def get_production_order_details(production_order): def get_production_order_details(production_order):
@@ -754,264 +661,3 @@ def get_production_order_details(production_order):
from `tabProduction Order` where name = %s""", production_order, as_dict=1) from `tabProduction Order` where name = %s""", production_order, as_dict=1)
return res and res[0] or {} 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

View File

@@ -4,15 +4,12 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe, unittest import frappe, unittest
import frappe.defaults 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.serial_no.serial_no import *
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt \ 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.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.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_reconciliation.test_stock_reconciliation import create_stock_reconciliation from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import create_stock_reconciliation
def get_sle(**args): 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][1], gle[1])
self.assertEquals(expected_gl_entries[i][2], gle[2]) 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): def test_serial_no_not_reqd(self):
se = frappe.copy_doc(test_records[0]) se = frappe.copy_doc(test_records[0])
se.get("items")[0].serial_no = "ABCD" se.get("items")[0].serial_no = "ABCD"

View File

@@ -150,6 +150,15 @@
"permlevel": 0, "permlevel": 0,
"read_only": 1 "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", "fieldname": "stock_uom",
"fieldtype": "Link", "fieldtype": "Link",
@@ -266,7 +275,7 @@
"icon": "icon-list", "icon": "icon-list",
"idx": 1, "idx": 1,
"in_create": 1, "in_create": 1,
"modified": "2015-07-13 05:28:27.826340", "modified": "2015-07-16 16:37:54.452944",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Stock Ledger Entry", "name": "Stock Ledger Entry",

View File

@@ -107,8 +107,6 @@ def create_stock_reconciliation(**args):
"valuation_rate": args.rate "valuation_rate": args.rate
}) })
sr.insert()
sr.submit() sr.submit()
return sr return sr

View File

@@ -230,19 +230,21 @@ class update_entries_after(object):
self.valuation_rate = new_stock_value / new_stock_qty self.valuation_rate = new_stock_value / new_stock_qty
def get_moving_average_values(self, sle): def get_moving_average_values(self, sle):
incoming_rate = flt(sle.incoming_rate)
actual_qty = flt(sle.actual_qty) 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 self.qty_after_transaction < 0 and not self.valuation_rate:
# if negative stock, take current valuation rate as incoming 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_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: if new_stock_qty:
self.valuation_rate = new_stock_value / flt(new_stock_qty) self.valuation_rate = new_stock_value / flt(new_stock_qty)
elif not self.valuation_rate and self.qty_after_transaction <= 0: 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) 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): def get_fifo_values(self, sle):
incoming_rate = flt(sle.incoming_rate) incoming_rate = flt(sle.incoming_rate)
actual_qty = flt(sle.actual_qty) actual_qty = flt(sle.actual_qty)
outgoing_rate = flt(sle.outgoing_rate)
if actual_qty > 0: if actual_qty > 0:
if not self.stock_queue: if not self.stock_queue:
@@ -278,16 +281,34 @@ class update_entries_after(object):
_rate = 0 _rate = 0
self.stock_queue.append([0, _rate]) 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]: if qty_to_pop >= batch[0]:
# consume current batch # consume current batch
qty_to_pop = qty_to_pop - batch[0] 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: if not self.stock_queue and qty_to_pop:
# stock finished, qty still remains to be withdrawn # stock finished, qty still remains to be withdrawn
# negative stock, keep in as a negative batch # 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 break
else: else:

View File

@@ -3,12 +3,12 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
import frappe.share
from frappe import _ from frappe import _
from frappe.utils import cstr, now_datetime, cint, flt from frappe.utils import cstr, now_datetime, cint, flt
import frappe.share
from erpnext.controllers.status_updater import StatusUpdater from erpnext.controllers.status_updater import StatusUpdater
class UOMMustBeIntegerError(frappe.ValidationError): pass
class TransactionBase(StatusUpdater): class TransactionBase(StatusUpdater):
def load_notification_message(self): 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` 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) 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): def validate_uom_is_integer(doc, uom_field, qty_fields, child_dt=None):
if isinstance(qty_fields, basestring): if isinstance(qty_fields, basestring):
qty_fields = [qty_fields] qty_fields = [qty_fields]