Merge branch 'develop'

This commit is contained in:
Nabin Hait
2015-07-27 15:55:57 +05:30
90 changed files with 1598 additions and 1354 deletions

View File

@@ -1,2 +1,2 @@
from __future__ import unicode_literals from __future__ import unicode_literals
__version__ = '5.2.1' __version__ = '5.3.0'

View File

@@ -49,7 +49,7 @@ class Account(Document):
self.root_type = par.root_type self.root_type = par.root_type
def validate_root_details(self): def validate_root_details(self):
#does not exists parent # does not exists parent
if frappe.db.exists("Account", self.name): if frappe.db.exists("Account", self.name):
if not frappe.db.get_value("Account", self.name, "parent_account"): if not frappe.db.get_value("Account", self.name, "parent_account"):
throw(_("Root cannot be edited.")) throw(_("Root cannot be edited."))

View File

@@ -17,7 +17,7 @@ erpnext.accounts.CostCenterController = frappe.ui.form.Controller.extend({
return { return {
filters:[ filters:[
['Account', 'company', '=', me.frm.doc.company], ['Account', 'company', '=', me.frm.doc.company],
['Account', 'report_type', '=', 'Profit and Loss'], ['Account', 'root_type', '=', 'Expense'],
['Account', 'is_group', '=', '0'], ['Account', 'is_group', '=', '0'],
] ]
} }

View File

@@ -66,7 +66,7 @@
"precision": "" "precision": ""
}, },
{ {
"description": "Define Budget for this Cost Center. To set budget action, see <a href=\"#!List/Company\">Company Master</a>", "description": "Define Budget for this Cost Center. To set budget action, see \"Company List\"",
"fieldname": "sb1", "fieldname": "sb1",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Budget", "label": "Budget",
@@ -193,4 +193,4 @@
} }
], ],
"search_fields": "parent_cost_center, is_group" "search_fields": "parent_cost_center, is_group"
} }

View File

@@ -3,9 +3,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe import _
from frappe import msgprint, _
from frappe.utils.nestedset import NestedSet from frappe.utils.nestedset import NestedSet
class CostCenter(NestedSet): class CostCenter(NestedSet):
@@ -14,18 +12,46 @@ class CostCenter(NestedSet):
def autoname(self): def autoname(self):
self.name = self.cost_center_name.strip() + ' - ' + \ self.name = self.cost_center_name.strip() + ' - ' + \
frappe.db.get_value("Company", self.company, "abbr") frappe.db.get_value("Company", self.company, "abbr")
def validate(self):
self.validate_mandatory()
self.validate_accounts()
def validate_mandatory(self): def validate_mandatory(self):
if self.cost_center_name != self.company and not self.parent_cost_center: if self.cost_center_name != self.company and not self.parent_cost_center:
msgprint(_("Please enter parent cost center"), raise_exception=1) frappe.throw(_("Please enter parent cost center"))
elif self.cost_center_name == self.company and self.parent_cost_center: elif self.cost_center_name == self.company and self.parent_cost_center:
msgprint(_("Root cannot have a parent cost center"), raise_exception=1) frappe.throw(_("Root cannot have a parent cost center"))
def validate_accounts(self):
if self.is_group==1 and self.get("budgets"):
frappe.throw(_("Budget cannot be set for Group Cost Center"))
check_acc_list = []
for d in self.get('budgets'):
if d.account:
account_details = frappe.db.get_value("Account", d.account,
["is_group", "company", "root_type"], as_dict=1)
if account_details.is_group:
frappe.throw(_("Budget cannot be assigned against Group Account {0}").format(d.account))
elif account_details.company != self.company:
frappe.throw(_("Account {0} does not belongs to company {1}").format(d.account, self.company))
elif account_details.root_type != "Expense":
frappe.throw(_("Budget cannot be assigned against {0}, as it's not an Expense account")
.format(d.account))
if [d.account, d.fiscal_year] in check_acc_list:
frappe.throw(_("Account {0} has been entered more than once for fiscal year {1}")
.format(d.account, d.fiscal_year))
else:
check_acc_list.append([d.account, d.fiscal_year])
def convert_group_to_ledger(self): def convert_group_to_ledger(self):
if self.check_if_child_exists(): if self.check_if_child_exists():
msgprint(_("Cannot convert Cost Center to ledger as it has child nodes"), raise_exception=1) frappe.throw(_("Cannot convert Cost Center to ledger as it has child nodes"))
elif self.check_gle_exists(): elif self.check_gle_exists():
msgprint(_("Cost Center with existing transactions can not be converted to ledger"), raise_exception=1) frappe.throw(_("Cost Center with existing transactions can not be converted to ledger"))
else: else:
self.is_group = 0 self.is_group = 0
self.save() self.save()
@@ -33,7 +59,7 @@ class CostCenter(NestedSet):
def convert_ledger_to_group(self): def convert_ledger_to_group(self):
if self.check_gle_exists(): if self.check_gle_exists():
msgprint(_("Cost Center with existing transactions can not be converted to group"), raise_exception=1) frappe.throw(_("Cost Center with existing transactions can not be converted to group"))
else: else:
self.is_group = 1 self.is_group = 1
self.save() self.save()
@@ -46,21 +72,6 @@ class CostCenter(NestedSet):
return frappe.db.sql("select name from `tabCost Center` where \ return frappe.db.sql("select name from `tabCost Center` where \
parent_cost_center = %s and docstatus != 2", self.name) parent_cost_center = %s and docstatus != 2", self.name)
def validate_budget_details(self):
check_acc_list = []
for d in self.get('budgets'):
if self.is_group==1:
msgprint(_("Budget cannot be set for Group Cost Centers"), raise_exception=1)
if [d.account, d.fiscal_year] in check_acc_list:
msgprint(_("Account {0} has been entered more than once for fiscal year {1}").format(d.account, d.fiscal_year), raise_exception=1)
else:
check_acc_list.append([d.account, d.fiscal_year])
def validate(self):
self.validate_mandatory()
self.validate_budget_details()
def before_rename(self, olddn, newdn, merge=False): def before_rename(self, olddn, newdn, merge=False):
# Add company abbr if not provided # Add company abbr if not provided
from erpnext.setup.doctype.company.company import get_name_with_abbr from erpnext.setup.doctype.company.company import get_name_with_abbr

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

@@ -310,7 +310,7 @@
"depends_on": "eval:doc.voucher_type == 'Write Off Entry'", "depends_on": "eval:doc.voucher_type == 'Write Off Entry'",
"fieldname": "write_off_amount", "fieldname": "write_off_amount",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Write Off Amount <=", "label": "Write Off Amount",
"options": "Company:company:default_currency", "options": "Company:company:default_currency",
"permlevel": 0, "permlevel": 0,
"print_hide": 1, "print_hide": 1,
@@ -503,4 +503,4 @@
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"title_field": "title" "title_field": "title"
} }

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

@@ -4,10 +4,9 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from frappe.model.document import Document from frappe.model.document import Document
from erpnext.controllers.accounts_controller import validate_taxes_and_charges, validate_inclusive_tax from erpnext.accounts.doctype.sales_taxes_and_charges_template.sales_taxes_and_charges_template \
import valdiate_taxes_and_charges_template
class PurchaseTaxesandChargesTemplate(Document): class PurchaseTaxesandChargesTemplate(Document):
def validate(self): def validate(self):
for tax in self.get("taxes"): valdiate_taxes_and_charges_template(self)
validate_taxes_and_charges(tax)
validate_inclusive_tax(tax, self)

View File

@@ -40,7 +40,9 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte
this._super(); this._super();
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: [
@@ -399,4 +396,4 @@ cur_frm.set_query("debit_to", function(doc) {
['Account', 'account_type', '=', 'Receivable'] ['Account', 'account_type', '=', 'Receivable']
] ]
} }
}); });

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",
@@ -1252,8 +1274,8 @@
], ],
"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)
@@ -100,13 +102,15 @@ class SalesInvoice(SellingController):
self.update_stock_ledger() self.update_stock_ledger()
self.check_stop_sales_order("sales_order") self.check_stop_sales_order("sales_order")
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

@@ -5,21 +5,25 @@ from __future__ import unicode_literals
import frappe import frappe
from frappe.model.document import Document from frappe.model.document import Document
from erpnext.controllers.accounts_controller import validate_taxes_and_charges, validate_inclusive_tax from erpnext.controllers.accounts_controller import validate_taxes_and_charges, validate_inclusive_tax
from frappe.utils.nestedset import get_root_of
class SalesTaxesandChargesTemplate(Document): class SalesTaxesandChargesTemplate(Document):
def validate(self): def validate(self):
if self.is_default == 1: valdiate_taxes_and_charges_template(self)
frappe.db.sql("""update `tabSales Taxes and Charges Template`
set is_default = 0
where ifnull(is_default,0) = 1
and name != %s and company = %s""",
(self.name, self.company))
# at least one territory def valdiate_taxes_and_charges_template(doc):
self.validate_table_has_rows("territories") if not doc.is_default and not frappe.get_all(doc.doctype, filters={"is_default": 1}):
doc.is_default = 1
for tax in self.get("taxes"): if doc.is_default == 1:
validate_taxes_and_charges(tax) frappe.db.sql("""update `tab{0}` set is_default = 0
validate_inclusive_tax(tax, self) where ifnull(is_default,0) = 1 and name != %s and company = %s""".format(doc.doctype),
(doc.name, doc.company))
if doc.meta.get_field("territories"):
if not doc.territories:
doc.append("territories", {"territory": get_root_of("Territory") })
for tax in doc.get("taxes"):
validate_taxes_and_charges(tax)
validate_inclusive_tax(tax, doc)

View File

@@ -1,19 +1,19 @@
{ {
"creation": "2014-08-28 11:11:39.796473", "creation": "2014-08-28 11:11:39.796473",
"disabled": 0, "custom_format": 0,
"doc_type": "Journal Entry", "disabled": 0,
"docstatus": 0, "doc_type": "Journal Entry",
"doctype": "Print Format", "docstatus": 0,
"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", "doctype": "Print Format",
"idx": 2, "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",
"modified": "2015-01-12 11:02:25.716825", "idx": 2,
"modified_by": "Administrator", "modified": "2015-07-22 17:42:01.560817",
"module": "Accounts", "modified_by": "Administrator",
"name": "Credit Note", "name": "Credit Note",
"owner": "Administrator", "owner": "Administrator",
"parent": "Journal Entry", "parent": "Journal Entry",
"parentfield": "__print_formats", "parentfield": "__print_formats",
"parenttype": "DocType", "parenttype": "DocType",
"print_format_type": "Server", "print_format_type": "Server",
"standard": "Yes" "standard": "Yes"
} }

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

@@ -1,17 +1,17 @@
{ {
"add_total_row": 1, "add_total_row": 1,
"apply_user_permissions": 1, "apply_user_permissions": 1,
"creation": "2013-04-22 16:16:03", "creation": "2013-04-22 16:16:03",
"docstatus": 0, "docstatus": 0,
"doctype": "Report", "doctype": "Report",
"idx": 1, "idx": 1,
"is_standard": "Yes", "is_standard": "Yes",
"modified": "2014-06-03 07:18:10.985354", "modified": "2015-07-24 01:08:20.996267",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Accounts Payable", "name": "Accounts Payable",
"owner": "Administrator", "owner": "Administrator",
"ref_doctype": "Purchase Invoice", "ref_doctype": "Purchase Invoice",
"report_name": "Accounts Payable", "report_name": "Accounts Payable",
"report_type": "Report Builder" "report_type": "Script Report"
} }

View File

@@ -1,5 +1,5 @@
<div style="margin-bottom: 7px;" class="text-center"> <div style="margin-bottom: 7px;" class="text-center">
{%= frappe.boot.letter_heads[frappe.defaults.get_default("letter_head")] %} {%= frappe.boot.letter_heads[filters.letter_head || frappe.defaults.get_default("letter_head")] %}
</div> </div>
<h2 class="text-center">{%= __("Statement of Account") %}</h2> <h2 class="text-center">{%= __("Statement of Account") %}</h2>
<h4 class="text-center">{%= (filters.party || filters.account) && ((filters.party || filters.account) + ", ") || "" %} {%= filters.company %}</h4> <h4 class="text-center">{%= (filters.party || filters.account) && ((filters.party || filters.account) + ", ") || "" %} {%= filters.company %}</h4>

View File

@@ -80,6 +80,13 @@ frappe.query_reports["General Ledger"] = {
"fieldname":"group_by_account", "fieldname":"group_by_account",
"label": __("Group by Account"), "label": __("Group by Account"),
"fieldtype": "Check", "fieldtype": "Check",
},
{
"fieldname":"letter_head",
"label": __("Letter Head"),
"fieldtype": "Link",
"options": "Letter Head",
"default": frappe.defaults.get_default("letter_head"),
} }
] ]
} }

View File

@@ -174,12 +174,12 @@ class GrossProfitGenerator(object):
return flt(row.qty) * item_rate return flt(row.qty) * item_rate
else: else:
if row.update_stock or row.dn_detail: my_sle = self.sle.get((item_code, row.warehouse))
if (row.update_stock or row.dn_detail) and my_sle:
parenttype, parent, item_row = row.parenttype, row.parent, row.item_row parenttype, parent, item_row = row.parenttype, row.parent, row.item_row
if row.dn_detail: if row.dn_detail:
parenttype, parent, item_row = "Delivery Note", row.delivery_note, row.dn_detail parenttype, parent, item_row = "Delivery Note", row.delivery_note, row.dn_detail
my_sle = self.sle.get((item_code, row.warehouse))
for i, sle in enumerate(my_sle): for i, sle in enumerate(my_sle):
# find the stock valution rate from stock ledger entry # find the stock valution rate from stock ledger entry
if sle.voucher_type == parenttype and parent == sle.voucher_no and \ if sle.voucher_type == parenttype and parent == sle.voucher_no and \

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

@@ -0,0 +1,11 @@
- **Sales Return**: Create Delivery Note or Sales Invoice ('Updated Stock' option checked) with negative quantity.
- **Purchase Return**: Create Purchase Receipt with negative quantity
- **Credit / Debit Note**: Create Sales / Purchase Invoice with negative qtuantity against original invoice.
- Outgoing rate in Purchase Return based on reference / original Purchase Receipt rate
- Global switch added to disable capacity planning in manufacturing settings
- Opening Balance row added to Stock Ledger Report
- SMS delivery message and log
- Added users, employees, sample data via Setup Wizard
- Letter Head option in General Ledger report
- Fetch Template Bom if no BOM is set against Item Variant in Production Order
- Fetch items from Packing List while raising Material Request against SO

View File

@@ -42,6 +42,11 @@ def get_data():
"name": "SMS Center", "name": "SMS Center",
"description":_("Send mass SMS to your contacts"), "description":_("Send mass SMS to your contacts"),
}, },
{
"type": "doctype",
"name": "SMS Log",
"description":_("Logs for maintaining sms delivery status"),
}
] ]
}, },
{ {

View File

@@ -48,6 +48,11 @@ def get_data():
"name": "SMS Center", "name": "SMS Center",
"description":_("Send mass SMS to your contacts"), "description":_("Send mass SMS to your contacts"),
}, },
{
"type": "doctype",
"name": "SMS Log",
"description":_("Logs for maintaining sms delivery status"),
},
{ {
"type": "doctype", "type": "doctype",
"name": "Newsletter", "name": "Newsletter",

View File

@@ -1,92 +0,0 @@
{
"allow_rename": 1,
"autoname": "field:party_type_name",
"creation": "2014-04-07 12:32:18.010384",
"docstatus": 0,
"doctype": "DocType",
"document_type": "Master",
"fields": [
{
"fieldname": "party_type_name",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Party Type Name",
"permlevel": 0,
"reqd": 1
},
{
"fieldname": "parent_party_type",
"fieldtype": "Link",
"label": "Parent Party Type",
"options": "Party Type",
"permlevel": 0
},
{
"default": "Yes",
"fieldname": "allow_children",
"fieldtype": "Select",
"label": "Allow Children",
"options": "Yes\nNo",
"permlevel": 0
},
{
"fieldname": "default_price_list",
"fieldtype": "Link",
"ignore_user_permissions": 1,
"label": "Default Price List",
"options": "Price List",
"permlevel": 0
},
{
"fieldname": "lft",
"fieldtype": "Int",
"hidden": 1,
"label": "LFT",
"permlevel": 0,
"read_only": 1,
"search_index": 1
},
{
"fieldname": "rgt",
"fieldtype": "Int",
"hidden": 1,
"label": "RGT",
"permlevel": 0,
"read_only": 1,
"search_index": 1
},
{
"fieldname": "old_parent",
"fieldtype": "Data",
"hidden": 1,
"label": "Old Parent",
"permlevel": 0,
"read_only": 1
}
],
"modified": "2015-02-05 05:11:42.046004",
"modified_by": "Administrator",
"module": "Contacts",
"name": "Party Type",
"owner": "Administrator",
"permissions": [
{
"apply_user_permissions": 1,
"create": 1,
"permlevel": 0,
"read": 1,
"role": "Sales User",
"share": 1,
"write": 1
},
{
"apply_user_permissions": 1,
"create": 1,
"permlevel": 0,
"read": 1,
"role": "Purchase User",
"share": 1,
"write": 1
}
]
}

View File

@@ -1,9 +0,0 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe.utils.nestedset import NestedSet
class PartyType(NestedSet):
nsm_parent_field = 'parent_party_type';

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

@@ -216,6 +216,17 @@ class StockController(AccountsController):
tuple(item_codes)) tuple(item_codes))
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):

View File

@@ -77,6 +77,9 @@ class calculate_taxes_and_totals(object):
if not self.discount_amount_applied: if not self.discount_amount_applied:
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",
@@ -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

@@ -52,7 +52,7 @@
"fieldtype": "Text Editor", "fieldtype": "Text Editor",
"label": "Message", "label": "Message",
"permlevel": 0, "permlevel": 0,
"reqd": 0 "reqd": 1
}, },
{ {
"description": "", "description": "",
@@ -78,7 +78,7 @@
], ],
"icon": "icon-envelope", "icon": "icon-envelope",
"idx": 1, "idx": 1,
"modified": "2015-03-20 05:27:31.613881", "modified": "2015-07-20 05:43:33.818567",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "CRM", "module": "CRM",
"name": "Newsletter", "name": "Newsletter",

View File

@@ -1,11 +1,34 @@
from __future__ import unicode_literals from __future__ import unicode_literals
app_name = "erpnext" app_name = "erpnext"
app_title = "ERPNext" app_title = "ERPNext"
app_publisher = "Frappe Technologies Pvt. Ltd. and Contributors" app_publisher = "Frappe Technologies Pvt. Ltd."
app_description = "Open Source Enterprise Resource Planning for Small and Midsized Organizations" app_description = """## ERPNext
ERPNext is a fully featured ERP system designed for Small and Medium Sized
business. ERPNext covers a wide range of features including Accounting, CRM,
Inventory management, Selling, Purchasing, Manufacturing, Projects, HR &
Payroll, Website, E-Commerce and much more.
ERPNext is based on the Frappe Framework is highly customizable and extendable.
You can create Custom Form, Fields, Scripts and can also create your own Apps
to extend ERPNext functionality.
ERPNext is Open Source under the GNU General Public Licence v3 and has been
listed as one of the Best Open Source Softwares in the world by my online
blogs.
### Links
- Website: [https://erpnext.com](https://erpnext.com)
- GitHub: [https://github.com/frappe/erpnext](https://github.com/frappe/erpnext)
- Forum: [https://discuss.erpnext.com](https://discuss.erpnext.com)
- Frappe Framework: [https://frappe.io](https://frappe.io)
"""
app_icon = "icon-th" app_icon = "icon-th"
app_color = "#e74c3c" app_color = "#e74c3c"
app_version = "5.2.1" app_version = "5.3.0"
github_link = "https://github.com/frappe/erpnext"
error_report_email = "support@erpnext.com" error_report_email = "support@erpnext.com"

View File

@@ -15,6 +15,14 @@
"permlevel": 0, "permlevel": 0,
"precision": "" "precision": ""
}, },
{
"description": "Disables creation of time logs against Production Orders.\nOperations shall not be tracked against Production Order",
"fieldname": "disable_capacity_planning",
"fieldtype": "Check",
"label": "Disable Capacity Planning and Time Tracking",
"permlevel": 0,
"precision": ""
},
{ {
"description": "Plan time logs outside Workstation Working Hours.", "description": "Plan time logs outside Workstation Working Hours.",
"fieldname": "allow_overtime", "fieldname": "allow_overtime",
@@ -72,7 +80,7 @@
"is_submittable": 0, "is_submittable": 0,
"issingle": 1, "issingle": 1,
"istable": 0, "istable": 0,
"modified": "2015-06-15 05:52:22.986958", "modified": "2015-07-23 08:12:33.889753",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Manufacturing Settings", "name": "Manufacturing Settings",

View File

@@ -186,27 +186,16 @@ $.extend(cur_frm.cscript, {
}, },
bom_no: function() { bom_no: function() {
if (this.frm.doc.track_operations) { return this.frm.call({
return this.frm.call({ doc: this.frm.doc,
doc: this.frm.doc, method: "set_production_order_operations"
method: "set_production_order_operations" });
});
}
}, },
qty: function() { qty: function() {
frappe.ui.form.trigger("Production Order", 'bom_no') frappe.ui.form.trigger("Production Order", 'bom_no')
}, },
track_operations: function(doc) {
if (doc.track_operations) {
frappe.ui.form.trigger("Production Order", 'bom_no')
}
else {
doc.operations =[];
}
},
show_time_logs: function(doc, cdt, cdn) { show_time_logs: function(doc, cdt, cdn) {
var child = locals[cdt][cdn] var child = locals[cdt][cdn]
frappe.route_options = {"operation_id": child.name}; frappe.route_options = {"operation_id": child.name};
@@ -262,7 +251,8 @@ cur_frm.fields_dict['production_item'].get_query = function(doc) {
return { return {
filters:[ filters:[
['Item', 'is_pro_applicable', '=', 'Yes'], ['Item', 'is_pro_applicable', '=', 'Yes'],
['Item', 'has_variants', '=', 'No'] ['Item', 'has_variants', '=', 'No'],
['Item', 'end_of_life', '>=', frappe.datetime.nowdate()]
] ]
} }
} }

View File

@@ -73,14 +73,6 @@
"label": "Use Multi-Level BOM", "label": "Use Multi-Level BOM",
"permlevel": 0 "permlevel": 0
}, },
{
"default": "1",
"fieldname": "track_operations",
"fieldtype": "Check",
"label": "Track Operations",
"permlevel": 0,
"precision": ""
},
{ {
"fieldname": "column_break1", "fieldname": "column_break1",
"fieldtype": "Column Break", "fieldtype": "Column Break",
@@ -215,7 +207,7 @@
"read_only": 1 "read_only": 1
}, },
{ {
"depends_on": "track_operations", "depends_on": "",
"fieldname": "operations_section", "fieldname": "operations_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Operations", "label": "Operations",
@@ -234,7 +226,7 @@
"read_only": 1 "read_only": 1
}, },
{ {
"depends_on": "track_operations", "depends_on": "operations",
"fieldname": "section_break_22", "fieldname": "section_break_22",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Operation Cost", "label": "Operation Cost",
@@ -368,7 +360,7 @@
"idx": 1, "idx": 1,
"in_create": 0, "in_create": 0,
"is_submittable": 1, "is_submittable": 1,
"modified": "2015-07-13 05:28:23.259016", "modified": "2015-07-21 07:45:53.206902",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Production Order", "name": "Production Order",

View File

@@ -9,10 +9,13 @@ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from erpnext.manufacturing.doctype.bom.bom import validate_bom_no from erpnext.manufacturing.doctype.bom.bom import validate_bom_no
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
from erpnext.stock.doctype.item.item import validate_end_of_life
class OverProductionError(frappe.ValidationError): pass class OverProductionError(frappe.ValidationError): pass
class StockOverProductionError(frappe.ValidationError): pass class StockOverProductionError(frappe.ValidationError): pass
class OperationTooLongError(frappe.ValidationError): pass class OperationTooLongError(frappe.ValidationError): pass
class ProductionNotApplicableError(frappe.ValidationError): pass
class ItemHasVariantError(frappe.ValidationError): pass
from erpnext.manufacturing.doctype.workstation.workstation import WorkstationHolidayError, NotInWorkingHoursError from erpnext.manufacturing.doctype.workstation.workstation import WorkstationHolidayError, NotInWorkingHoursError
from erpnext.projects.doctype.time_log.time_log import OverlapError from erpnext.projects.doctype.time_log.time_log import OverlapError
@@ -174,17 +177,12 @@ class ProductionOrder(Document):
def set_production_order_operations(self): def set_production_order_operations(self):
"""Fetch operations from BOM and set in 'Production Order'""" """Fetch operations from BOM and set in 'Production Order'"""
if not self.bom_no: if not self.bom_no or cint(frappe.db.get_single_value("Manufacturing Settings", "disable_capacity_planning")):
return return
self.set('operations', []) self.set('operations', [])
operations = frappe.db.sql("""select operation, description, workstation, idx, operations = frappe.db.sql("""select operation, description, workstation, idx,
hour_rate, time_in_mins, "Pending" as status from `tabBOM Operation` hour_rate, time_in_mins, "Pending" as status from `tabBOM Operation`
where parent = %s order by idx""", self.bom_no, as_dict=1) where parent = %s order by idx""", self.bom_no, as_dict=1)
if operations:
self.track_operations=1
else:
self.track_operations=0
frappe.msgprint(_("Cannot 'track operations' as selected BOM does not have Operations."))
self.set('operations', operations) self.set('operations', operations)
self.calculate_time() self.calculate_time()
@@ -325,22 +323,27 @@ class ProductionOrder(Document):
def validate_production_item(self): def validate_production_item(self):
if frappe.db.get_value("Item", self.production_item, "is_pro_applicable")=='No': if frappe.db.get_value("Item", self.production_item, "is_pro_applicable")=='No':
frappe.throw(_("Item is not allowed to have Production Order.")) frappe.throw(_("Item is not allowed to have Production Order."), ProductionNotApplicableError)
if frappe.db.get_value("Item", self.production_item, "has_variants"): if frappe.db.get_value("Item", self.production_item, "has_variants"):
frappe.throw(_("Production Order cannot be raised against a Item Template")) frappe.throw(_("Production Order cannot be raised against a Item Template"), ItemHasVariantError)
validate_end_of_life(self.production_item)
@frappe.whitelist() @frappe.whitelist()
def get_item_details(item): def get_item_details(item):
res = frappe.db.sql("""select stock_uom, description res = frappe.db.sql("""select stock_uom, description
from `tabItem` where (ifnull(end_of_life, "0000-00-00")="0000-00-00" or end_of_life > now()) from `tabItem` where (ifnull(end_of_life, "0000-00-00")="0000-00-00" or end_of_life > now())
and name=%s""", item, as_dict=1) and name=%s""", item, as_dict=1)
if not res: if not res:
return {} return {}
res = res[0] res = res[0]
res["bom_no"] = frappe.db.get_value("BOM", filters={"item": item, "is_default": 1}) res["bom_no"] = frappe.db.get_value("BOM", filters={"item": item, "is_default": 1})
if not res["bom_no"]:
variant_of= frappe.db.get_value("Item", item, "variant_of")
if variant_of:
res["bom_no"] = frappe.db.get_value("BOM", filters={"item": variant_of, "is_default": 1})
return res return res
@frappe.whitelist() @frappe.whitelist()

View File

@@ -7,7 +7,8 @@ import unittest
import frappe import frappe
from frappe.utils import flt, get_datetime, time_diff_in_hours from frappe.utils import flt, get_datetime, time_diff_in_hours
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory
from erpnext.manufacturing.doctype.production_order.production_order import make_stock_entry, make_time_log from erpnext.manufacturing.doctype.production_order.production_order \
import make_stock_entry, make_time_log, ProductionNotApplicableError,ItemHasVariantError
from erpnext.stock.doctype.stock_entry import test_stock_entry from erpnext.stock.doctype.stock_entry import test_stock_entry
from erpnext.projects.doctype.time_log.time_log import OverProductionLoggedError from erpnext.projects.doctype.time_log.time_log import OverProductionLoggedError
@@ -135,6 +136,22 @@ class TestProductionOrder(unittest.TestCase):
prod_order.set_production_order_operations() prod_order.set_production_order_operations()
self.assertEqual(prod_order.planned_operating_cost, cost*2) self.assertEqual(prod_order.planned_operating_cost, cost*2)
def test_production_item(self):
frappe.db.set_value("Item", "_Test FG Item", "is_pro_applicable", "No")
prod_order = make_prod_order_test_record(item="_Test FG Item", qty=1, do_not_save=True)
self.assertRaises(ProductionNotApplicableError, prod_order.save)
frappe.db.set_value("Item", "_Test FG Item", "is_pro_applicable", "Yes")
frappe.db.set_value("Item", "_Test FG Item", "end_of_life", "2000-1-1")
self.assertRaises(frappe.ValidationError, prod_order.save)
frappe.db.set_value("Item", "_Test FG Item", "end_of_life", None)
prod_order = make_prod_order_test_record(item="_Test Variant Item", qty=1, do_not_save=True)
self.assertRaises(ItemHasVariantError, prod_order.save)
def make_prod_order_test_record(**args): def make_prod_order_test_record(**args):
args = frappe._dict(args) args = frappe._dict(args)

View File

@@ -9,6 +9,7 @@ from frappe import msgprint, _
from frappe.model.document import Document from frappe.model.document import Document
from erpnext.manufacturing.doctype.bom.bom import validate_bom_no from erpnext.manufacturing.doctype.bom.bom import validate_bom_no
from erpnext.manufacturing.doctype.production_order.production_order import get_item_details
class ProductionPlanningTool(Document): class ProductionPlanningTool(Document):
def __init__(self, arg1, arg2=None): def __init__(self, arg1, arg2=None):
@@ -27,16 +28,7 @@ class ProductionPlanningTool(Document):
return ret return ret
def get_item_details(self, item_code): def get_item_details(self, item_code):
""" Pull other item details from item master""" return get_item_details(item_code)
item = frappe.db.sql("""select description, stock_uom, default_bom
from `tabItem` where name = %s""", item_code, as_dict =1)
ret = {
'description' : item and item[0]['description'],
'stock_uom' : item and item[0]['stock_uom'],
'bom_no' : item and item[0]['default_bom']
}
return ret
def clear_so_table(self): def clear_so_table(self):
self.set('sales_orders', []) self.set('sales_orders', [])
@@ -142,15 +134,14 @@ class ProductionPlanningTool(Document):
self.clear_item_table() self.clear_item_table()
for p in items: for p in items:
item_details = frappe.db.sql("""select description, stock_uom, default_bom item_details = get_item_details(p['item_code'])
from tabItem where name=%s""", p['item_code'])
pi = self.append('items', {}) pi = self.append('items', {})
pi.sales_order = p['parent'] pi.sales_order = p['parent']
pi.warehouse = p['warehouse'] pi.warehouse = p['warehouse']
pi.item_code = p['item_code'] pi.item_code = p['item_code']
pi.description = item_details and item_details[0][0] or '' pi.description = item_details and item_details.description or ''
pi.stock_uom = item_details and item_details[0][1] or '' pi.stock_uom = item_details and item_details.stock_uom or ''
pi.bom_no = item_details and item_details[0][2] or '' pi.bom_no = item_details and item_details.bom_no or ''
pi.so_pending_qty = flt(p['pending_qty']) pi.so_pending_qty = flt(p['pending_qty'])
pi.planned_qty = flt(p['pending_qty']) pi.planned_qty = flt(p['pending_qty'])

View File

@@ -9,6 +9,5 @@ Manufacturing
Stock Stock
Support Support
Utilities Utilities
Contacts
Shopping Cart Shopping Cart
Hub Node Hub Node

View File

@@ -173,7 +173,7 @@ erpnext.patches.v5_0.item_variants
erpnext.patches.v5_0.update_item_desc_in_invoice erpnext.patches.v5_0.update_item_desc_in_invoice
erpnext.patches.v5_1.fix_against_account erpnext.patches.v5_1.fix_against_account
erpnext.patches.v5_1.fix_credit_days_based_on erpnext.patches.v5_1.fix_credit_days_based_on
erpnext.patches.v5_1.track_operations
execute:frappe.rename_doc("DocType", "Salary Manager", "Process Payroll", force=True) execute:frappe.rename_doc("DocType", "Salary Manager", "Process Payroll", force=True)
erpnext.patches.v5_1.rename_roles erpnext.patches.v5_1.rename_roles
erpnext.patches.v5_1.default_bom erpnext.patches.v5_1.default_bom
execute:frappe.delete_doc("DocType", "Party Type")

View File

@@ -1,8 +0,0 @@
from __future__ import unicode_literals
import frappe
def execute():
frappe.reload_doctype("Production Order")
frappe.db.sql("""Update `tabProduction Order` as po set track_operations=1 where
exists(select name from `tabProduction Order Operation` as po_operation where po_operation.parent = po.name )""")

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
@@ -93,6 +94,10 @@ erpnext.taxes_and_totals = erpnext.stock.StockController.extend({
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",
"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"))

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

@@ -272,6 +272,10 @@ def make_material_request(source_name, target_doc=None):
def postprocess(source, doc): def postprocess(source, doc):
doc.material_request_type = "Purchase" doc.material_request_type = "Purchase"
so = frappe.get_doc("Sales Order", source_name)
item_table = "Packed Item" if so.packed_items else "Sales Order Item"
doc = get_mapped_doc("Sales Order", source_name, { doc = get_mapped_doc("Sales Order", source_name, {
"Sales Order": { "Sales Order": {
"doctype": "Material Request", "doctype": "Material Request",
@@ -279,7 +283,7 @@ def make_material_request(source_name, target_doc=None):
"docstatus": ["=", 1] "docstatus": ["=", 1]
} }
}, },
"Sales Order Item": { item_table: {
"doctype": "Material Request Item", "doctype": "Material Request Item",
"field_map": { "field_map": {
"parent": "sales_order_no", "parent": "sales_order_no",

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

@@ -18,7 +18,7 @@
"permlevel": 0 "permlevel": 0
}, },
{ {
"description": "To track items in sales and purchase documents with batch nos<br><b>Preferred Industry: Chemicals etc</b>", "description": "To track items in sales and purchase documents with batch nos. \"Preferred Industry: Chemicals\"",
"fieldname": "fs_item_batch_nos", "fieldname": "fs_item_batch_nos",
"fieldtype": "Check", "fieldtype": "Check",
"in_list_view": 1, "in_list_view": 1,
@@ -139,14 +139,14 @@
"permlevel": 0 "permlevel": 0
}, },
{ {
"description": "To enable <b>Point of Sale</b> features", "description": "To enable \"Point of Sale\" features",
"fieldname": "fs_pos", "fieldname": "fs_pos",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Point of Sale", "label": "Point of Sale",
"permlevel": 0 "permlevel": 0
}, },
{ {
"description": "To enable <b>Point of Sale</b> view", "description": "To enable \"Point of Sale\" view",
"fieldname": "fs_pos_view", "fieldname": "fs_pos_view",
"fieldtype": "Check", "fieldtype": "Check",
"label": "POS View", "label": "POS View",
@@ -237,4 +237,4 @@
"write": 1 "write": 1
} }
] ]
} }

View File

@@ -5,7 +5,7 @@ from __future__ import unicode_literals
import frappe import frappe
from frappe import _, throw, msgprint from frappe import _, throw, msgprint
from frappe.utils import cstr, nowdate from frappe.utils import nowdate
from frappe.model.document import Document from frappe.model.document import Document
@@ -63,8 +63,7 @@ def send_sms(receiver_list, msg, sender_name = ''):
} }
if frappe.db.get_value('SMS Settings', None, 'sms_gateway_url'): if frappe.db.get_value('SMS Settings', None, 'sms_gateway_url'):
ret = send_via_gateway(arg) send_via_gateway(arg)
msgprint(ret)
else: else:
msgprint(_("Please Update SMS Settings")) msgprint(_("Please Update SMS Settings"))
@@ -74,12 +73,17 @@ def send_via_gateway(arg):
for d in ss.get("parameters"): for d in ss.get("parameters"):
args[d.parameter] = d.value args[d.parameter] = d.value
resp = [] success_list = []
for d in arg.get('receiver_list'): for d in arg.get('receiver_list'):
args[ss.receiver_parameter] = d args[ss.receiver_parameter] = d
resp.append(send_request(ss.sms_gateway_url, args)) status = send_request(ss.sms_gateway_url, args)
if status == 200:
success_list.append(d)
return resp if len(success_list) > 0:
args.update(arg)
create_sms_log(args, success_list)
frappe.msgprint(_("SMS sent to following numbers: {0}").format("\n" + "\n".join(success_list)))
# Send Request # Send Request
# ========================================================= # =========================================================
@@ -90,11 +94,8 @@ def send_request(gateway_url, args):
headers = {} headers = {}
headers['Accept'] = "text/plain, text/html, */*" headers['Accept'] = "text/plain, text/html, */*"
conn.request('GET', api_url + urllib.urlencode(args), headers = headers) # send request conn.request('GET', api_url + urllib.urlencode(args), headers = headers) # send request
resp = conn.getresponse() # get response resp = conn.getresponse() # get response
resp = resp.read() return resp.status
if resp.status==200:
create_sms_log()
return resp
# Split gateway url to server and api url # Split gateway url to server and api url
# ========================================================= # =========================================================
@@ -109,12 +110,13 @@ def scrub_gateway_url(url):
# Create SMS Log # Create SMS Log
# ========================================================= # =========================================================
def create_sms_log(arg, sent_sms): def create_sms_log(args, sent_to):
sl = frappe.get_doc('SMS Log') sl = frappe.new_doc('SMS Log')
sl.sender_name = arg['sender_name'] sl.sender_name = args['sender_name']
sl.sent_on = nowdate() sl.sent_on = nowdate()
sl.receiver_list = cstr(arg['receiver_list']) sl.message = args['message']
sl.message = arg['message'] sl.no_of_requested_sms = len(args['receiver_list'])
sl.no_of_requested_sms = len(arg['receiver_list']) sl.requested_numbers = "\n".join(args['receiver_list'])
sl.no_of_sent_sms = sent_sms sl.no_of_sent_sms = len(sent_to)
sl.sent_to = "\n".join(sent_to)
sl.save() sl.save()

View File

@@ -183,4 +183,4 @@ def install(country=None):
parent_link_field = ("parent_" + scrub(doc.doctype)) parent_link_field = ("parent_" + scrub(doc.doctype))
if doc.meta.get_field(parent_link_field) and not doc.get(parent_link_field): if doc.meta.get_field(parent_link_field) and not doc.get(parent_link_field):
doc.flags.ignore_mandatory = True doc.flags.ignore_mandatory = True
doc.insert() doc.insert(ignore_permissions=True)

View File

@@ -0,0 +1,122 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
import frappe
from frappe.utils.make_random import add_random_children, get_random
import frappe.utils
def make_sample_data():
"""Create a few opportunities, quotes, material requests, issues, todos, projects
to help the user get started"""
selling_items = frappe.get_all("Item", filters = {"is_sales_item": "Yes"})
buying_items = frappe.get_all("Item", filters = {"is_sales_item": "No"})
if selling_items:
for i in range(3):
make_opportunity(selling_items)
make_quote(selling_items)
make_projects()
if buying_items:
make_material_request(buying_items)
frappe.db.commit()
def make_opportunity(selling_items):
b = frappe.get_doc({
"doctype": "Opportunity",
"enquiry_from": "Customer",
"customer": get_random("Customer"),
"enquiry_type": "Sales",
"with_items": 1
})
add_random_children(b, "items", rows=len(selling_items), randomize = {
"qty": (1, 5),
"item_code": ("Item", {"is_sales_item": "Yes"})
}, unique="item_code")
b.insert(ignore_permissions=True)
b.add_comment("This is a dummy record")
def make_quote(selling_items):
qtn = frappe.get_doc({
"doctype": "Quotation",
"quotation_to": "Customer",
"customer": get_random("Customer"),
"order_type": "Sales"
})
add_random_children(qtn, "items", rows=len(selling_items), randomize = {
"qty": (1, 5),
"item_code": ("Item", {"is_sales_item": "Yes"})
}, unique="item_code")
qtn.insert(ignore_permissions=True)
qtn.add_comment("This is a dummy record")
def make_material_request(buying_items):
for i in buying_items:
mr = frappe.get_doc({
"doctype": "Material Request",
"material_request_type": "Purchase",
"items": [{
"schedule_date": frappe.utils.add_days(frappe.utils.nowdate(), 7),
"item_code": i.name,
"qty": 10
}]
})
mr.insert()
mr.submit()
mr.add_comment("This is a dummy record")
def make_issue():
pass
def make_projects():
project = frappe.get_doc({
"doctype": "Project",
"project_name": "ERPNext Implementation",
})
current_date = frappe.utils.nowdate()
project.set("tasks", [
{
"title": "Explore ERPNext",
"start_date": frappe.utils.add_days(current_date, 1),
"end_date": frappe.utils.add_days(current_date, 2)
},
{
"title": "Run Sales Cycle",
"start_date": frappe.utils.add_days(current_date, 2),
"end_date": frappe.utils.add_days(current_date, 3)
},
{
"title": "Run Billing Cycle",
"start_date": frappe.utils.add_days(current_date, 3),
"end_date": frappe.utils.add_days(current_date, 4)
},
{
"title": "Run Purchase Cycle",
"start_date": frappe.utils.add_days(current_date, 4),
"end_date": frappe.utils.add_days(current_date, 5)
},
{
"title": "Import Data",
"start_date": frappe.utils.add_days(current_date, 5),
"end_date": frappe.utils.add_days(current_date, 6)
},
{
"title": "Go Live!",
"start_date": frappe.utils.add_days(current_date, 6),
"end_date": frappe.utils.add_days(current_date, 7)
}])
project.insert(ignore_permissions=True)

View File

@@ -25,6 +25,7 @@ frappe.pages['setup-wizard'].on_page_load = function(wrapper) {
erpnext.wiz.user.slide, erpnext.wiz.user.slide,
erpnext.wiz.org.slide, erpnext.wiz.org.slide,
erpnext.wiz.branding.slide, erpnext.wiz.branding.slide,
erpnext.wiz.users.slide,
erpnext.wiz.taxes.slide, erpnext.wiz.taxes.slide,
erpnext.wiz.customers.slide, erpnext.wiz.customers.slide,
erpnext.wiz.suppliers.slide, erpnext.wiz.suppliers.slide,
@@ -137,7 +138,7 @@ erpnext.wiz.WizardSlide = Class.extend({
}); });
this.form.make(); this.form.make();
} else { } else {
$(this.body).html(this.html) $(this.body).html(this.html);
} }
if(this.id > 0) { if(this.id > 0) {
@@ -412,11 +413,30 @@ $.extend(erpnext.wiz, {
onload: function(slide) { onload: function(slide) {
erpnext.wiz.org.load_chart_of_accounts(slide); erpnext.wiz.org.load_chart_of_accounts(slide);
erpnext.wiz.org.bind_events(slide); erpnext.wiz.org.bind_events(slide);
erpnext.wiz.org.set_fy_dates(slide);
}, },
css_class: "single-column" css_class: "single-column"
}, },
set_fy_dates: function(slide) {
var country = slide.wiz.get_values().country;
if(country) {
var fy = erpnext.wiz.fiscal_years[country];
var current_year = moment(new Date()).year();
var next_year = current_year + 1;
if(!fy) {
fy = ["01-01", "12-31"];
next_year = current_year;
}
slide.get_field("fy_start_date").set_input(current_year + "-" + fy[0]);
slide.get_field("fy_end_date").set_input(next_year + "-" + fy[1]);
}
},
load_chart_of_accounts: function(slide) { load_chart_of_accounts: function(slide) {
var country = slide.wiz.get_values().country; var country = slide.wiz.get_values().country;
@@ -486,11 +506,41 @@ $.extend(erpnext.wiz, {
}, },
}, },
users: {
slide: {
icon: "icon-money",
"title": __("Add Users"),
"help": __("Add users to your organization"),
"fields": [],
before_load: function(slide) {
slide.fields = [];
for(var i=1; i<5; i++) {
slide.fields = slide.fields.concat([
{fieldtype:"Section Break"},
{fieldtype:"Data", fieldname:"user_fullname_"+ i,
label:__("Full Name")},
{fieldtype:"Data", fieldname:"user_email_" + i,
label:__("Email ID"), placeholder:__("user@example.com"),
options: "Email"},
{fieldtype:"Column Break"},
{fieldtype: "Check", fieldname: "user_sales_" + i,
label:__("Sales"), default: 1},
{fieldtype: "Check", fieldname: "user_purchaser_" + i,
label:__("Purchaser"), default: 1},
{fieldtype: "Check", fieldname: "user_accountant_" + i,
label:__("Accountant"), default: 1},
]);
}
},
css_class: "two-column"
},
},
taxes: { taxes: {
slide: { slide: {
icon: "icon-money", icon: "icon-money",
"title": __("Add Taxes"), "title": __("Add Taxes"),
"help": __("List your tax heads (e.g. VAT, Excise; they should have unique names) and their standard rates. This will create a standard template, which you can edit and add more later."), "help": __("List your tax heads (e.g. VAT, Customs etc; they should have unique names) and their standard rates. This will create a standard template, which you can edit and add more later."),
"fields": [], "fields": [],
before_load: function(slide) { before_load: function(slide) {
slide.fields = []; slide.fields = [];
@@ -526,6 +576,7 @@ $.extend(erpnext.wiz, {
label:__("Contact Name") + " " + i, placeholder:__("Contact Name")} label:__("Contact Name") + " " + i, placeholder:__("Contact Name")}
]) ])
} }
slide.fields[1].reqd = 1;
}, },
css_class: "two-column" css_class: "two-column"
}, },
@@ -549,6 +600,7 @@ $.extend(erpnext.wiz, {
label:__("Contact Name") + " " + i, placeholder:__("Contact Name")}, label:__("Contact Name") + " " + i, placeholder:__("Contact Name")},
]) ])
} }
slide.fields[1].reqd = 1;
}, },
css_class: "two-column" css_class: "two-column"
}, },
@@ -578,9 +630,11 @@ $.extend(erpnext.wiz, {
{fieldtype: "Check", fieldname: "is_sales_item_" + i, label:__("We sell this Item"), default: 1}, {fieldtype: "Check", fieldname: "is_sales_item_" + i, label:__("We sell this Item"), default: 1},
{fieldtype: "Check", fieldname: "is_purchase_item_" + i, label:__("We buy this Item")}, {fieldtype: "Check", fieldname: "is_purchase_item_" + i, label:__("We buy this Item")},
{fieldtype:"Column Break"}, {fieldtype:"Column Break"},
{fieldtype:"Currency", fieldname:"item_price_" + i, label:__("Rate")},
{fieldtype:"Attach Image", fieldname:"item_img_" + i, label:__("Attach Image")}, {fieldtype:"Attach Image", fieldname:"item_img_" + i, label:__("Attach Image")},
]) ])
} }
slide.fields[1].reqd = 1;
}, },
css_class: "two-column" css_class: "two-column"
}, },
@@ -627,3 +681,25 @@ $.extend(erpnext.wiz, {
}, },
}); });
// Source: https://en.wikipedia.org/wiki/Fiscal_year
// default 1st Jan - 31st Dec
erpnext.wiz.fiscal_years = {
"Afghanistan": ["12-20", "12-21"],
"Australia": ["07-01", "06-30"],
"Bangladesh": ["07-01", "06-30"],
"Canada": ["04-01", "03-31"],
"Costa Rica": ["10-01", "09-30"],
"Egypt": ["07-01", "06-30"],
"Hong Kong": ["04-01", "03-31"],
"India": ["04-01", "03-31"],
"Iran": ["06-23", "06-22"],
"Italy": ["07-01", "06-30"],
"Myanmar": ["04-01", "03-31"],
"New Zealand": ["04-01", "03-31"],
"Pakistan": ["07-01", "06-30"],
"Singapore": ["04-01", "03-31"],
"South Africa": ["03-01", "02-28"],
"Thailand": ["10-01", "09-30"],
"United Kingdom": ["04-01", "03-31"],
}

View File

@@ -2,7 +2,7 @@
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe, json import frappe, json, copy
from frappe.utils import cstr, flt, getdate from frappe.utils import cstr, flt, getdate
from frappe import _ from frappe import _
@@ -13,6 +13,7 @@ from frappe.geo.country_info import get_country_info
from frappe.utils.nestedset import get_root_of from frappe.utils.nestedset import get_root_of
from .default_website import website_maker from .default_website import website_maker
import install_fixtures import install_fixtures
from .sample_data import make_sample_data
@frappe.whitelist() @frappe.whitelist()
def setup_account(args=None): def setup_account(args=None):
@@ -38,6 +39,9 @@ def setup_account(args=None):
create_fiscal_year_and_company(args) create_fiscal_year_and_company(args)
frappe.local.message_log = [] frappe.local.message_log = []
create_users(args)
frappe.local.message_log = []
set_defaults(args) set_defaults(args)
frappe.local.message_log = [] frappe.local.message_log = []
@@ -81,6 +85,7 @@ def setup_account(args=None):
frappe.clear_cache() frappe.clear_cache()
make_sample_data()
except: except:
if args: if args:
traceback = frappe.get_traceback() traceback = frappe.get_traceback()
@@ -297,21 +302,45 @@ def create_taxes(args):
tax_group = frappe.db.get_value("Account", {"company": args.get("company_name"), tax_group = frappe.db.get_value("Account", {"company": args.get("company_name"),
"is_group": 1, "account_type": "Tax", "root_type": "Liability"}) "is_group": 1, "account_type": "Tax", "root_type": "Liability"})
if tax_group: if tax_group:
frappe.get_doc({ account = make_tax_head(args, i, tax_group, tax_rate)
"doctype":"Account", make_sales_and_purchase_tax_templates(account)
"company": args.get("company_name").strip(),
"parent_account": tax_group,
"account_name": args.get("tax_" + str(i)),
"is_group": 0,
"report_type": "Balance Sheet",
"account_type": "Tax",
"tax_rate": flt(tax_rate) if tax_rate else None
}).insert()
except frappe.NameError, e: except frappe.NameError, e:
if e.args[2][0]==1062: if e.args[2][0]==1062:
pass pass
else: else:
raise raise
def make_tax_head(args, i, tax_group, tax_rate):
return frappe.get_doc({
"doctype":"Account",
"company": args.get("company_name").strip(),
"parent_account": tax_group,
"account_name": args.get("tax_" + str(i)),
"is_group": 0,
"report_type": "Balance Sheet",
"account_type": "Tax",
"tax_rate": flt(tax_rate) if tax_rate else None
}).insert(ignore_permissions=True)
def make_sales_and_purchase_tax_templates(account):
doc = {
"doctype": "Sales Taxes and Charges Template",
"title": account.name,
"taxes": [{
"category": "Valuation and Total",
"charge_type": "On Net Total",
"account_head": account.name,
"description": "{0} @ {1}".format(account.account_name, account.tax_rate),
"rate": account.tax_rate
}]
}
# Sales
frappe.get_doc(copy.deepcopy(doc)).insert()
# Purchase
doc["doctype"] = "Purchase Taxes and Charges Template"
frappe.get_doc(copy.deepcopy(doc)).insert()
def create_items(args): def create_items(args):
for i in xrange(1,6): for i in xrange(1,6):
@@ -349,9 +378,30 @@ def create_items(args):
filename, filetype, content = item_image filename, filetype, content = item_image
fileurl = save_file(filename, content, "Item", item, decode=True).file_url fileurl = save_file(filename, content, "Item", item, decode=True).file_url
frappe.db.set_value("Item", item, "image", fileurl) frappe.db.set_value("Item", item, "image", fileurl)
if args.get("item_price_" + str(i)):
item_price = flt(args.get("item_price_" + str(i)))
if is_sales_item:
price_list_name = frappe.db.get_value("Price List", {"selling": 1})
make_item_price(item, price_list_name, item_price)
if is_purchase_item:
price_list_name = frappe.db.get_value("Price List", {"buying": 1})
make_item_price(item, price_list_name, item_price)
except frappe.NameError: except frappe.NameError:
pass pass
def make_item_price(item, price_list_name, item_price):
frappe.get_doc({
"doctype": "Item Price",
"price_list": price_list_name,
"item_code": item,
"price_list_rate": item_price
}).insert()
def create_customers(args): def create_customers(args):
for i in xrange(1,6): for i in xrange(1,6):
customer = args.get("customer_" + str(i)) customer = args.get("customer_" + str(i))
@@ -367,13 +417,8 @@ def create_customers(args):
}).insert() }).insert()
if args.get("customer_contact_" + str(i)): if args.get("customer_contact_" + str(i)):
contact = args.get("customer_contact_" + str(i)).split(" ") create_contact(args.get("customer_contact_" + str(i)),
frappe.get_doc({ "customer", customer)
"doctype":"Contact",
"customer": customer,
"first_name":contact[0],
"last_name": len(contact) > 1 and contact[1] or ""
}).insert()
except frappe.NameError: except frappe.NameError:
pass pass
@@ -390,16 +435,21 @@ def create_suppliers(args):
}).insert() }).insert()
if args.get("supplier_contact_" + str(i)): if args.get("supplier_contact_" + str(i)):
contact = args.get("supplier_contact_" + str(i)).split(" ") create_contact(args.get("supplier_contact_" + str(i)),
frappe.get_doc({ "supplier", supplier)
"doctype":"Contact",
"supplier": supplier,
"first_name":contact[0],
"last_name": len(contact) > 1 and contact[1] or ""
}).insert()
except frappe.NameError: except frappe.NameError:
pass pass
def create_contact(contact, party_type, party):
"""Create contact based on given contact name"""
contact = contact.strip().split(" ")
frappe.get_doc({
"doctype":"Contact",
party_type: party,
"first_name":contact[0],
"last_name": len(contact) > 1 and contact[1] or ""
}).insert()
def create_letter_head(args): def create_letter_head(args):
if args.get("attach_letterhead"): if args.get("attach_letterhead"):
@@ -451,6 +501,60 @@ def login_as_first_user(args):
if args.get("email") and hasattr(frappe.local, "login_manager"): if args.get("email") and hasattr(frappe.local, "login_manager"):
frappe.local.login_manager.login_as(args.get("email")) frappe.local.login_manager.login_as(args.get("email"))
def create_users(args):
# create employee for self
emp = frappe.get_doc({
"doctype": "Employee",
"full_name": " ".join(filter(None, [args.get("first_name"), args.get("last_name")])),
"user_id": frappe.session.user,
"status": "Active",
"company": args.get("company_name")
})
emp.flags.ignore_mandatory = True
emp.insert(ignore_permissions = True)
for i in xrange(1,5):
email = args.get("user_email_" + str(i))
fullname = args.get("user_fullname_" + str(i))
if email:
if not fullname:
fullname = email.split("@")[0]
parts = fullname.split(" ", 1)
user = frappe.get_doc({
"doctype": "User",
"email": email,
"first_name": parts[0],
"last_name": parts[1] if len(parts) > 1 else "",
"enabled": 1,
"user_type": "System User"
})
# default roles
user.append_roles("Projects User", "Stock User", "Support Team")
if args.get("user_sales_" + str(i)):
user.append_roles("Sales User", "Sales Manager", "Accounts User")
if args.get("user_purchaser_" + str(i)):
user.append_roles("Purchase User", "Purchase Manager", "Accounts User")
if args.get("user_accountant_" + str(i)):
user.append_roles("Accounts Manager", "Accounts User")
user.flags.delay_emails = True
user.insert(ignore_permissions=True)
# create employee
emp = frappe.get_doc({
"doctype": "Employee",
"full_name": fullname,
"user_id": user.name,
"status": "Active",
"company": args.get("company_name")
})
emp.flags.ignore_mandatory = True
emp.insert(ignore_permissions = True)
@frappe.whitelist() @frappe.whitelist()
def load_messages(language): def load_messages(language):
frappe.clear_cache() frappe.clear_cache()

View File

@@ -51,4 +51,15 @@ args = {
"timezone": "America/New_York", "timezone": "America/New_York",
"password": "password", "password": "password",
"email": "test@erpnext.com", "email": "test@erpnext.com",
"user_email_1": "testsetup1@example.com",
"user_fullname_1": "test setup user",
"user_sales_1": 1,
"user_purchaser_1": 1,
"user_accountant_1": 1,
"user_email_1": "testsetup2@example.com",
"user_fullname_1": "test setup user",
"user_sales_2": 1,
"user_purchaser_2": 0,
"user_accountant_2": 0
} }

View File

@@ -10,6 +10,7 @@ def get_notification_config():
"Issue": {"status": "Open"}, "Issue": {"status": "Open"},
"Warranty Claim": {"status": "Open"}, "Warranty Claim": {"status": "Open"},
"Task": {"status": "Open"}, "Task": {"status": "Open"},
"Project": {"status": "Open"},
"Lead": {"status": "Open"}, "Lead": {"status": "Open"},
"Contact": {"status": "Open"}, "Contact": {"status": "Open"},
"Opportunity": {"status": "Open"}, "Opportunity": {"status": "Open"},

View File

@@ -24,14 +24,15 @@ 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();
} }
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"); });
} }
}, },
@@ -73,6 +74,13 @@ erpnext.stock.DeliveryNoteController = erpnext.selling.SellingController.extend(
frm: cur_frm frm: cur_frm
}); });
}, },
make_sales_return: function() {
frappe.model.open_mapped_doc({
method: "erpnext.stock.doctype.delivery_note.delivery_note.make_sales_return",
frm: cur_frm
})
},
tc_name: function() { 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()
@@ -251,9 +250,14 @@ class DeliveryNote(SellingController):
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 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

@@ -86,8 +86,12 @@ frappe.ui.form.on("Item", {
}, },
manage_variants: function(frm) { manage_variants: function(frm) {
frappe.route_options = {"item_code": frm.doc.name }; if (cur_frm.doc.__unsaved==1) {
frappe.set_route("List", "Manage Variants"); frappe.throw(__("You have unsaved changes. Please save."))
} else {
frappe.route_options = {"item_code": frm.doc.name };
frappe.set_route("List", "Manage Variants");
}
} }
}); });

View File

@@ -325,7 +325,8 @@ class Item(WebsiteGenerator):
for d in variants: for d in variants:
update_variant(self.name, d) update_variant(self.name, d)
updated.append(d.item_code) updated.append(d.item_code)
frappe.msgprint(_("Item Variants {0} updated").format(", ".join(updated))) if updated:
frappe.msgprint(_("Item Variants {0} updated").format(", ".join(updated)))
def validate_has_variants(self): def validate_has_variants(self):
if not self.has_variants and frappe.db.get_value("Item", self.name, "has_variants"): if not self.has_variants and frappe.db.get_value("Item", self.name, "has_variants"):

View File

@@ -31,9 +31,10 @@ 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();
@@ -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");
@@ -105,6 +106,13 @@ erpnext.stock.PurchaseReceiptController = erpnext.buying.BuyingController.extend
frm: cur_frm frm: cur_frm
}) })
}, },
make_purchase_return: function() {
frappe.model.open_mapped_doc({
method: "erpnext.stock.doctype.purchase_receipt.purchase_receipt.make_purchase_return",
frm: cur_frm
})
},
tc_name: function() { 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,12 +61,20 @@ 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))
from `tabLanded Cost Item` from `tabLanded Cost Item`
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"):
@@ -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):
@@ -119,6 +119,65 @@ class TestPurchaseReceipt(unittest.TestCase):
for serial_no in rejected_serial_nos: for serial_no in rejected_serial_nos:
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
@@ -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

@@ -33,10 +33,7 @@ class SerialNo(StockController):
self.validate_warehouse() self.validate_warehouse()
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

@@ -7,20 +7,7 @@ frappe.provide("erpnext.stock");
erpnext.stock.StockEntry = erpnext.stock.StockController.extend({ 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")])
@@ -201,9 +186,7 @@ class StockEntry(StockController):
def check_if_operations_completed(self): def check_if_operations_completed(self):
"""Check if Time Logs are completed against before manufacturing to capture operating costs.""" """Check if Time Logs are completed against before manufacturing to capture operating costs."""
prod_order = frappe.get_doc("Production Order", self.production_order) prod_order = frappe.get_doc("Production Order", self.production_order)
if not prod_order.track_operations:
return
for d in prod_order.get("operations"): for d in prod_order.get("operations"):
total_completed_qty = flt(self.fg_completed_qty) + flt(prod_order.produced_qty) total_completed_qty = flt(self.fg_completed_qty) + flt(prod_order.produced_qty)
if total_completed_qty > flt(d.completed_qty): if total_completed_qty > flt(d.completed_qty):
@@ -291,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
@@ -336,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"""
@@ -403,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'):
@@ -514,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 = {
@@ -561,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
@@ -738,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):
@@ -756,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

@@ -98,3 +98,11 @@ cur_frm.cscript.company = function(doc, cdt, cdn) {
cur_frm.cscript.posting_date = function(doc, cdt, cdn){ cur_frm.cscript.posting_date = function(doc, cdt, cdn){
erpnext.get_fiscal_year(doc.company, doc.posting_date); erpnext.get_fiscal_year(doc.company, doc.posting_date);
} }
cur_frm.fields_dict.items.grid.get_field('item_code').get_query = function(doc, cdt, cdn) {
return {
filters:[
['Item', 'end_of_life', '>=', frappe.datetime.nowdate()]
]
}
}

View File

@@ -11,6 +11,7 @@ from erpnext.controllers.stock_controller import StockController
from erpnext.stock.utils import get_stock_balance from erpnext.stock.utils import get_stock_balance
class OpeningEntryAccountError(frappe.ValidationError): pass class OpeningEntryAccountError(frappe.ValidationError): pass
class EmptyStockReconciliationItemsError(frappe.ValidationError): pass
class StockReconciliation(StockController): class StockReconciliation(StockController):
def __init__(self, arg1, arg2=None): def __init__(self, arg1, arg2=None):
@@ -51,7 +52,11 @@ class StockReconciliation(StockController):
items = filter(lambda d: _changed(d), self.items) items = filter(lambda d: _changed(d), self.items)
if len(items) != len(self.items): if not items:
frappe.throw(_("None of the items have any change in quantity or value."),
EmptyStockReconciliationItemsError)
elif len(items) != len(self.items):
self.items = items self.items = items
for i, item in enumerate(self.items): for i, item in enumerate(self.items):
item.idx = i + 1 item.idx = i + 1

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

@@ -9,8 +9,13 @@ def execute(filters=None):
columns = get_columns() columns = get_columns()
sl_entries = get_stock_ledger_entries(filters) sl_entries = get_stock_ledger_entries(filters)
item_details = get_item_details(filters) item_details = get_item_details(filters)
opening_row = get_opening_balance(filters, columns)
data = [] data = []
if opening_row:
data.append(opening_row)
for sle in sl_entries: for sle in sl_entries:
item_detail = item_details[sle.item_code] item_detail = item_details[sle.item_code]
@@ -20,7 +25,7 @@ def execute(filters=None):
(sle.incoming_rate if sle.actual_qty > 0 else 0.0), (sle.incoming_rate if sle.actual_qty > 0 else 0.0),
sle.valuation_rate, sle.stock_value, sle.voucher_type, sle.voucher_no, sle.valuation_rate, sle.stock_value, sle.voucher_type, sle.voucher_no,
sle.batch_no, sle.serial_no, sle.company]) sle.batch_no, sle.serial_no, sle.company])
return columns, data return columns, data
def get_columns(): def get_columns():
@@ -40,7 +45,7 @@ def get_stock_ledger_entries(filters):
where company = %(company)s and where company = %(company)s and
posting_date between %(from_date)s and %(to_date)s posting_date between %(from_date)s and %(to_date)s
{sle_conditions} {sle_conditions}
order by posting_date desc, posting_time desc, name desc"""\ order by posting_date asc, posting_time asc, name asc"""\
.format(sle_conditions=get_sle_conditions(filters)), filters, as_dict=1) .format(sle_conditions=get_sle_conditions(filters)), filters, as_dict=1)
def get_item_details(filters): def get_item_details(filters):
@@ -73,3 +78,22 @@ def get_sle_conditions(filters):
conditions.append("voucher_no=%(voucher_no)s") conditions.append("voucher_no=%(voucher_no)s")
return "and {}".format(" and ".join(conditions)) if conditions else "" return "and {}".format(" and ".join(conditions)) if conditions else ""
def get_opening_balance(filters, columns):
if not (filters.item_code and filters.warehouse and filters.from_date):
return
from erpnext.stock.stock_ledger import get_previous_sle
last_entry = get_previous_sle({
"item_code": filters.item_code,
"warehouse": filters.warehouse,
"posting_date": filters.from_date,
"posting_time": "00:00:00"
})
row = [""]*len(columns)
row[1] = _("'Opening'")
for i, v in ((9, 'qty_after_transaction'), (11, 'valuation_rate'), (12, 'stock_value')):
row[i] = last_entry.get(v, 0)
return row

View File

@@ -109,7 +109,7 @@ class update_entries_after(object):
def build(self): def build(self):
# includes current entry! # includes current entry!
entries_to_fix = self.get_sle_after_datetime() entries_to_fix = self.get_sle_after_datetime()
for sle in entries_to_fix: for sle in entries_to_fix:
self.process_sle(sle) self.process_sle(sle)
@@ -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

@@ -1,6 +1,6 @@
{% macro product_image_square(website_image, css_class="") %} {% macro product_image_square(website_image, css_class="") %}
<div class="product-image product-image-square {% if not website_image -%} missing-image {%- endif %} {{ css_class }}" <div class="product-image product-image-square {% if not website_image -%} missing-image {%- endif %} {{ css_class }}"
{% if website_image -%} style="background-image: url('{{ frappe.utils.quoted(website_image) }}');" {%- endif %}> {% if website_image -%} style="background-image: url('{{ frappe.utils.quoted(website_image) | abs_url }}');" {%- endif %}>
{% if not website_image -%}<i class="centered octicon octicon-device-camera"></i>{%- endif %} {% if not website_image -%}<i class="centered octicon octicon-device-camera"></i>{%- endif %}
</div> </div>
{% endmacro %} {% endmacro %}
@@ -8,7 +8,7 @@
{% macro product_image(website_image, css_class="") %} {% macro product_image(website_image, css_class="") %}
<div class="product-image {% if not website_image -%} missing-image {%- endif %} {{ css_class }}"> <div class="product-image {% if not website_image -%} missing-image {%- endif %} {{ css_class }}">
{% if website_image -%} {% if website_image -%}
<img src="{{ frappe.utils.quoted(website_image) }}" class="img-responsive"> <img src="{{ frappe.utils.quoted(website_image) | abs_url }}" class="img-responsive">
{%- else -%} {%- else -%}
<i class="centered octicon octicon-device-camera"></i> <i class="centered octicon octicon-device-camera"></i>
{%- endif %} {%- endif %}

View File

@@ -1,32 +1,58 @@
{ {
"autoname": "SMSLOG/.########", "autoname": "SMSLOG/.########",
"creation": "2012-03-27 14:36:47.000000", "creation": "2012-03-27 14:36:47",
"docstatus": 0, "docstatus": 0,
"doctype": "DocType", "doctype": "DocType",
"fields": [ "fields": [
{
"fieldname": "column_break0",
"fieldtype": "Column Break",
"permlevel": 0,
"width": "50%"
},
{ {
"fieldname": "sender_name", "fieldname": "sender_name",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Sender Name", "label": "Sender Name",
"permlevel": 0 "permlevel": 0,
"read_only": 1
}, },
{ {
"fieldname": "sent_on", "fieldname": "sent_on",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Sent On", "label": "Sent On",
"permlevel": 0 "permlevel": 0,
"read_only": 1
}, },
{ {
"fieldname": "receiver_list", "fieldname": "column_break0",
"fieldtype": "Column Break",
"permlevel": 0,
"read_only": 0,
"width": "50%"
},
{
"fieldname": "message",
"fieldtype": "Small Text", "fieldtype": "Small Text",
"label": "Receiver List", "label": "Message",
"permlevel": 0 "permlevel": 0,
"read_only": 1
},
{
"fieldname": "sec_break1",
"fieldtype": "Section Break",
"options": "Simple",
"permlevel": 0,
"precision": "",
"read_only": 0
},
{
"fieldname": "no_of_requested_sms",
"fieldtype": "Int",
"label": "No of Requested SMS",
"permlevel": 0,
"read_only": 1
},
{
"fieldname": "requested_numbers",
"fieldtype": "Small Text",
"label": "Requested Numbers",
"permlevel": 0,
"read_only": 1
}, },
{ {
"fieldname": "column_break1", "fieldname": "column_break1",
@@ -34,28 +60,25 @@
"permlevel": 0, "permlevel": 0,
"width": "50%" "width": "50%"
}, },
{
"fieldname": "no_of_requested_sms",
"fieldtype": "Int",
"label": "No of Requested SMS",
"permlevel": 0
},
{ {
"fieldname": "no_of_sent_sms", "fieldname": "no_of_sent_sms",
"fieldtype": "Int", "fieldtype": "Int",
"label": "No of Sent SMS", "label": "No of Sent SMS",
"permlevel": 0 "permlevel": 0,
"read_only": 1
}, },
{ {
"fieldname": "message", "fieldname": "sent_to",
"fieldtype": "Small Text", "fieldtype": "Small Text",
"label": "Message", "label": "Sent To",
"permlevel": 0 "permlevel": 0,
"precision": "",
"read_only": 1
} }
], ],
"icon": "icon-mobile-phone", "icon": "icon-mobile-phone",
"idx": 1, "idx": 1,
"modified": "2013-12-20 19:24:35.000000", "modified": "2015-07-22 11:53:25.998578",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Utilities", "module": "Utilities",
"name": "SMS Log", "name": "SMS Log",

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]

View File

@@ -1,6 +1,6 @@
from setuptools import setup, find_packages from setuptools import setup, find_packages
version = "5.2.1" version = "5.3.0"
with open("requirements.txt", "r") as f: with open("requirements.txt", "r") as f:
install_requires = f.readlines() install_requires = f.readlines()