Merge branch 'develop'

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

View File

@ -1,2 +1,2 @@
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
def validate_root_details(self):
#does not exists parent
# does not exists parent
if frappe.db.exists("Account", self.name):
if not frappe.db.get_value("Account", self.name, "parent_account"):
throw(_("Root cannot be edited."))

View File

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

View File

@ -66,7 +66,7 @@
"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",
"fieldtype": "Section Break",
"label": "Budget",
@ -193,4 +193,4 @@
}
],
"search_fields": "parent_cost_center, is_group"
}
}

View File

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

View File

@ -4,7 +4,7 @@
from __future__ import unicode_literals
import frappe
from frappe.utils import flt, fmt_money, getdate, formatdate, cstr
from frappe.utils import flt, fmt_money, getdate, formatdate, cstr, cint
from frappe import _
from frappe.model.document import Document
@ -139,9 +139,9 @@ def update_outstanding_amt(account, party_type, party, against_voucher_type, aga
if against_voucher_amount < 0:
bal = -bal
# Validation : Outstanding can not be negative
if bal < 0 and not on_cancel:
frappe.throw(_("Outstanding for {0} cannot be less than zero ({1})").format(against_voucher, fmt_money(bal)))
# Validation : Outstanding can not be negative for JV
if bal < 0 and not on_cancel:
frappe.throw(_("Outstanding for {0} cannot be less than zero ({1})").format(against_voucher, fmt_money(bal)))
# Update outstanding amt on against voucher
if against_voucher_type in ["Sales Invoice", "Purchase Invoice"]:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -40,7 +40,9 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte
this._super();
cur_frm.dashboard.reset();
this.frm.toggle_reqd("due_date", !this.frm.doc.is_return);
if(doc.docstatus==1) {
cur_frm.add_custom_button('View Ledger', function() {
frappe.route_options = {
@ -51,10 +53,7 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte
group_by_voucher: 0
};
frappe.set_route("query-report", "General Ledger");
}, "icon-table");
// var percent_paid = cint(flt(doc.base_grand_total - doc.outstanding_amount) / flt(doc.base_grand_total) * 100);
// cur_frm.dashboard.add_progress(percent_paid + "% Paid", percent_paid);
});
if(cint(doc.update_stock)!=1) {
// show Make Delivery Note button only if Sales Invoice is not created from Delivery Note
@ -65,13 +64,15 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte
});
if(!from_delivery_note) {
cur_frm.add_custom_button(__('Make Delivery'), cur_frm.cscript['Make Delivery Note'], "icon-truck")
cur_frm.add_custom_button(__('Make Delivery'), cur_frm.cscript['Make Delivery Note'])
}
}
if(doc.outstanding_amount!=0) {
cur_frm.add_custom_button(__('Make Payment Entry'), cur_frm.cscript.make_bank_entry, "icon-money");
if(doc.outstanding_amount!=0 && !cint(doc.is_return)) {
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
@ -205,8 +206,14 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte
items_on_form_rendered: function() {
erpnext.setup_serial_no();
},
make_sales_return: function() {
frappe.model.open_mapped_doc({
method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.make_sales_return",
frm: cur_frm
})
}
});
// for backward compatibility: combine new and previous states
@ -283,16 +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) {
return {
filters: [
@ -399,4 +396,4 @@ cur_frm.set_query("debit_to", function(doc) {
['Account', 'account_type', '=', 'Receivable']
]
}
});
});

View File

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

View File

@ -80,14 +80,16 @@ class SalesInvoice(SellingController):
self.check_prev_docstatus()
self.update_status_updater_args()
self.update_prevdoc_status()
self.update_billing_status_for_zero_amount_refdoc("Sales Order")
self.check_credit_limit()
if not self.is_return:
self.update_status_updater_args()
self.update_prevdoc_status()
self.update_billing_status_for_zero_amount_refdoc("Sales Order")
self.check_credit_limit()
# this sequence because outstanding may get -ve
self.make_gl_entries()
if not cint(self.is_pos) == 1:
if not cint(self.is_pos) == 1 and not self.is_return:
self.update_against_document_in_jv()
self.update_time_log_batch(self.name)
@ -100,13 +102,15 @@ class SalesInvoice(SellingController):
self.update_stock_ledger()
self.check_stop_sales_order("sales_order")
from erpnext.accounts.utils import remove_against_link_from_jv
remove_against_link_from_jv(self.doctype, self.name, "against_invoice")
self.update_status_updater_args()
self.update_prevdoc_status()
self.update_billing_status_for_zero_amount_refdoc("Sales Order")
if not self.is_return:
self.update_status_updater_args()
self.update_prevdoc_status()
self.update_billing_status_for_zero_amount_refdoc("Sales Order")
self.validate_c_form_on_cancel()
self.make_gl_entries_on_cancel()
@ -199,8 +203,9 @@ class SalesInvoice(SellingController):
self.set_taxes()
def get_advances(self):
super(SalesInvoice, self).get_advances(self.debit_to, "Customer", self.customer,
"Sales Invoice Advance", "advances", "credit", "sales_order")
if not self.is_return:
super(SalesInvoice, self).get_advances(self.debit_to, "Customer", self.customer,
"Sales Invoice Advance", "advances", "credit", "sales_order")
def get_company_abbr(self):
return frappe.db.sql("select abbr from tabCompany where name=%s", self.company)[0][0]
@ -285,6 +290,8 @@ class SalesInvoice(SellingController):
def so_dn_required(self):
"""check in manage account if sales order / delivery note required or not."""
if self.is_return:
return
dic = {'Sales Order':'so_required','Delivery Note':'dn_required'}
for i in dic:
if frappe.db.get_value('Selling Settings', None, dic[i]) == 'Yes':
@ -419,13 +426,16 @@ class SalesInvoice(SellingController):
def update_stock_ledger(self):
sl_entries = []
for d in self.get_item_list():
if frappe.db.get_value("Item", d.item_code, "is_stock_item") == "Yes" \
and d.warehouse:
if frappe.db.get_value("Item", d.item_code, "is_stock_item") == "Yes" and d.warehouse:
incoming_rate = 0
if cint(self.is_return) and self.return_against and self.docstatus==1:
incoming_rate = self.get_incoming_rate_for_sales_return(d.item_code, self.return_against)
sl_entries.append(self.get_sl_entries(d, {
"actual_qty": -1*flt(d.qty),
"stock_uom": frappe.db.get_value("Item", d.item_code, "stock_uom")
"stock_uom": frappe.db.get_value("Item", d.item_code, "stock_uom"),
"incoming_rate": incoming_rate
}))
self.make_sl_entries(sl_entries)
def make_gl_entries(self, repost_future_gle=True):
@ -435,8 +445,7 @@ class SalesInvoice(SellingController):
from erpnext.accounts.general_ledger import make_gl_entries
# if POS and amount is written off, there's no outstanding and hence no need to update it
update_outstanding = cint(self.is_pos) and self.write_off_account \
and 'No' or 'Yes'
update_outstanding = "No" if (cint(self.is_pos) or self.write_off_account) else "Yes"
make_gl_entries(gl_entries, cancel=(self.docstatus == 2),
update_outstanding=update_outstanding, merge_entries=False)
@ -484,7 +493,7 @@ class SalesInvoice(SellingController):
"against": self.against_income_account,
"debit": self.base_grand_total,
"remarks": self.remarks,
"against_voucher": self.name,
"against_voucher": self.return_against if cint(self.is_return) else self.name,
"against_voucher_type": self.doctype
})
)
@ -519,7 +528,6 @@ class SalesInvoice(SellingController):
# expense account gl entries
if cint(frappe.defaults.get_global_default("auto_accounting_for_stock")) \
and cint(self.update_stock):
gl_entries += super(SalesInvoice, self).get_gl_entries()
def make_pos_gl_entries(self, gl_entries):
@ -533,7 +541,7 @@ class SalesInvoice(SellingController):
"against": self.cash_bank_account,
"credit": self.paid_amount,
"remarks": self.remarks,
"against_voucher": self.name,
"against_voucher": self.return_against if cint(self.is_return) else self.name,
"against_voucher_type": self.doctype,
})
)
@ -557,7 +565,7 @@ class SalesInvoice(SellingController):
"against": self.write_off_account,
"credit": self.write_off_amount,
"remarks": self.remarks,
"against_voucher": self.name,
"against_voucher": self.return_against if cint(self.is_return) else self.name,
"against_voucher_type": self.doctype,
})
)
@ -651,3 +659,9 @@ def make_delivery_note(source_name, target_doc=None):
}, target_doc, set_missing_values)
return doclist
@frappe.whitelist()
def make_sales_return(source_name, target_doc=None):
from erpnext.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 unittest, copy
import time
from frappe.utils import nowdate, add_days
from erpnext.accounts.utils import get_stock_and_account_difference
from frappe.utils import nowdate, add_days, flt
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory
from erpnext.projects.doctype.time_log_batch.test_time_log_batch import *
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry, get_qty_after_transaction
class TestSalesInvoice(unittest.TestCase):
@ -772,6 +771,53 @@ class TestSalesInvoice(unittest.TestCase):
si1 = create_sales_invoice(posting_date="2015-07-05")
self.assertEqual(si1.due_date, "2015-08-31")
def test_return_sales_invoice(self):
set_perpetual_inventory()
make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=50, incoming_rate=100)
actual_qty_0 = get_qty_after_transaction()
si = create_sales_invoice(qty=5, rate=500, update_stock=1)
actual_qty_1 = get_qty_after_transaction()
self.assertEquals(actual_qty_0 - 5, actual_qty_1)
# outgoing_rate
outgoing_rate = frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Sales Invoice",
"voucher_no": si.name}, "stock_value_difference") / 5
# return entry
si1 = create_sales_invoice(is_return=1, return_against=si.name, qty=-2, rate=500, update_stock=1)
actual_qty_2 = get_qty_after_transaction()
self.assertEquals(actual_qty_1 + 2, actual_qty_2)
incoming_rate, stock_value_difference = frappe.db.get_value("Stock Ledger Entry",
{"voucher_type": "Sales Invoice", "voucher_no": si1.name},
["incoming_rate", "stock_value_difference"])
self.assertEquals(flt(incoming_rate, 3), abs(flt(outgoing_rate, 3)))
# Check gl entry
gle_warehouse_amount = frappe.db.get_value("GL Entry", {"voucher_type": "Sales Invoice",
"voucher_no": si1.name, "account": "_Test Warehouse - _TC"}, "debit")
self.assertEquals(gle_warehouse_amount, stock_value_difference)
party_credited = frappe.db.get_value("GL Entry", {"voucher_type": "Sales Invoice",
"voucher_no": si1.name, "account": "Debtors - _TC", "party": "_Test Customer"}, "credit")
self.assertEqual(party_credited, 1000)
# Check outstanding amount
self.assertFalse(si1.outstanding_amount)
self.assertEqual(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"), 1500)
set_perpetual_inventory(0)
def create_sales_invoice(**args):
si = frappe.new_doc("Sales Invoice")
@ -784,6 +830,10 @@ def create_sales_invoice(**args):
si.debit_to = args.debit_to or "Debtors - _TC"
si.update_stock = args.update_stock
si.is_pos = args.is_pos
si.is_return = args.is_return
si.return_against = args.return_against
si.currency="INR"
si.conversion_rate = 1
si.append("items", {
"item_code": args.item or args.item_code or "_Test Item",

View File

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

View File

@ -1,19 +1,19 @@
{
"creation": "2014-08-28 11:11:39.796473",
"disabled": 0,
"doc_type": "Journal Entry",
"docstatus": 0,
"doctype": "Print Format",
"html": "{%- from \"templates/print_formats/standard_macros.html\" import add_header -%}\n\n<div class=\"page-break\">\n {%- if not doc.get(\"print_heading\") and not doc.get(\"select_print_heading\") \n and doc.set(\"select_print_heading\", _(\"Credit Note\")) -%}{%- endif -%}\n {{ add_header(0, 1, doc, letter_head, no_letterhead) }}\n\n {%- for label, value in (\n (_(\"Credit To\"), doc.pay_to_recd_from),\n (_(\"Date\"), frappe.utils.formatdate(doc.voucher_date)),\n (_(\"Amount\"), \"<strong>\" + doc.get_formatted(\"total_amount\") + \"</strong><br>\" + (doc.total_amount_in_words or \"\") + \"<br>\"),\n (_(\"Remarks\"), doc.remark)\n ) -%}\n\n <div class=\"row\">\n <div class=\"col-xs-3\"><label class=\"text-right\">{{ label }}</label></div>\n <div class=\"col-xs-9\">{{ value }}</div>\n </div>\n\n {%- endfor -%}\n\n <hr>\n <br>\n <p class=\"strong\">\n {{ _(\"For\") }} {{ doc.company }},<br>\n <br>\n <br>\n <br>\n {{ _(\"Authorized Signatory\") }}\n </p>\n</div>\n\n\n",
"idx": 2,
"modified": "2015-01-12 11:02:25.716825",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Credit Note",
"owner": "Administrator",
"parent": "Journal Entry",
"parentfield": "__print_formats",
"parenttype": "DocType",
"print_format_type": "Server",
"creation": "2014-08-28 11:11:39.796473",
"custom_format": 0,
"disabled": 0,
"doc_type": "Journal Entry",
"docstatus": 0,
"doctype": "Print Format",
"html": "{%- from \"templates/print_formats/standard_macros.html\" import add_header -%}\n\n<div class=\"page-break\">\n {%- if not doc.get(\"print_heading\") and not doc.get(\"select_print_heading\") \n and doc.set(\"select_print_heading\", _(\"Credit Note\")) -%}{%- endif -%}\n {{ add_header(0, 1, doc, letter_head, no_letterhead) }}\n\n {%- for label, value in (\n (_(\"Credit To\"), doc.pay_to_recd_from),\n (_(\"Date\"), frappe.utils.formatdate(doc.voucher_date)),\n (_(\"Amount\"), \"<strong>\" + doc.get_formatted(\"total_amount\") + \"</strong><br>\" + (doc.total_amount_in_words or \"\") + \"<br>\"),\n (_(\"Remarks\"), doc.remark)\n ) -%}\n\n <div class=\"row\">\n <div class=\"col-xs-3\"><label class=\"text-right\">{{ label }}</label></div>\n <div class=\"col-xs-9\">{{ value }}</div>\n </div>\n\n {%- endfor -%}\n\n <hr>\n <br>\n <p class=\"strong\">\n {{ _(\"For\") }} {{ doc.company }},<br>\n <br>\n <br>\n <br>\n {{ _(\"Authorized Signatory\") }}\n </p>\n</div>\n\n\n",
"idx": 2,
"modified": "2015-07-22 17:42:01.560817",
"modified_by": "Administrator",
"name": "Credit Note",
"owner": "Administrator",
"parent": "Journal Entry",
"parentfield": "__print_formats",
"parenttype": "DocType",
"print_format_type": "Server",
"standard": "Yes"
}
}

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

View File

@ -1,5 +1,5 @@
<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>
<h2 class="text-center">{%= __("Statement of Account") %}</h2>
<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",
"label": __("Group by Account"),
"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
else:
if row.update_stock or row.dn_detail:
my_sle = self.sle.get((item_code, row.warehouse))
if (row.update_stock or row.dn_detail) and my_sle:
parenttype, parent, item_row = row.parenttype, row.parent, row.item_row
if row.dn_detail:
parenttype, parent, item_row = "Delivery Note", row.delivery_note, row.dn_detail
my_sle = self.sle.get((item_code, row.warehouse))
for i, sle in enumerate(my_sle):
# find the stock valution rate from stock ledger entry
if sle.voucher_type == parenttype and parent == sle.voucher_no and \

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

View File

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

View File

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

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",
"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",
"description":_("Send mass SMS to your contacts"),
},
{
"type": "doctype",
"name": "SMS Log",
"description":_("Logs for maintaining sms delivery status"),
},
{
"type": "doctype",
"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.utilities.transaction_base import TransactionBase
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):
def validate(self):
@ -17,10 +18,14 @@ class AccountsController(TransactionBase):
self.validate_date_with_fiscal_year()
if self.meta.get_field("currency"):
self.calculate_taxes_and_totals()
self.validate_value("base_grand_total", ">=", 0)
if not self.meta.get_field("is_return") or not self.is_return:
self.validate_value("base_grand_total", ">=", 0)
validate_return(self)
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"):
validate_recurring_document(self)
@ -74,6 +79,9 @@ class AccountsController(TransactionBase):
def validate_due_date(self):
from erpnext.accounts.party import validate_due_date
if self.doctype == "Sales Invoice":
if not self.due_date:
frappe.throw(_("Due Date is mandatory"))
validate_due_date(self.posting_date, self.due_date, "Customer", self.customer, self.company)
elif self.doctype == "Purchase Invoice":
validate_due_date(self.posting_date, self.due_date, "Supplier", self.supplier, self.company)

View File

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

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
company_currency = get_company_currency(self.company)
disable_rounded_total = cint(frappe.db.get_value("Global Defaults", None,
"disable_rounded_total"))
disable_rounded_total = cint(frappe.db.get_value("Global Defaults", None, "disable_rounded_total"))
if self.meta.get_field("base_in_words"):
self.base_in_words = money_in_words(disable_rounded_total and
self.base_grand_total or self.base_rounded_total, company_currency)
abs(self.base_grand_total) or abs(self.base_rounded_total), company_currency)
if self.meta.get_field("in_words"):
self.in_words = money_in_words(disable_rounded_total and
self.grand_total or self.rounded_total, self.currency)
abs(self.grand_total) or abs(self.rounded_total), self.currency)
def calculate_commission(self):
if self.meta.get_field("commission_rate"):
@ -175,7 +174,7 @@ class SellingController(StockController):
if flt(d.qty) > flt(d.delivered_qty):
reserved_qty_for_main_item = flt(d.qty) - flt(d.delivered_qty)
elif self.doctype == "Delivery Note" and d.against_sales_order:
elif self.doctype == "Delivery Note" and d.against_sales_order and not self.is_return:
# if SO qty is 10 and there is tolerance of 20%, then it will allow DN of 12.
# But in this case reserved qty should only be reduced by 10 and not 12
@ -211,7 +210,7 @@ class SellingController(StockController):
'qty': d.qty,
'reserved_qty': reserved_qty_for_main_item,
'uom': d.stock_uom,
'stock_uom': d.stock_uom,
'stock_uom': d.stock_uom,
'batch_no': cstr(d.get("batch_no")).strip(),
'serial_no': cstr(d.get("serial_no")).strip(),
'name': d.name

View File

@ -216,6 +216,17 @@ class StockController(AccountsController):
tuple(item_codes))
return serialized_items
def get_incoming_rate_for_sales_return(self, item_code, against_document):
incoming_rate = 0.0
if against_document and item_code:
incoming_rate = frappe.db.sql("""select abs(ifnull(stock_value_difference, 0) / actual_qty)
from `tabStock Ledger Entry`
where voucher_type = %s and voucher_no = %s and item_code = %s limit 1""",
(self.doctype, against_document, item_code))
incoming_rate = incoming_rate[0][0] if incoming_rate else 0.0
return incoming_rate
def update_gl_entries_after(posting_date, posting_time, for_warehouses=None, for_items=None,
warehouse_account=None):

View File

@ -77,6 +77,9 @@ class calculate_taxes_and_totals(object):
if not self.discount_amount_applied:
validate_taxes_and_charges(tax)
validate_inclusive_tax(tax, self.doc)
if self.doc.meta.get_field("is_return") and self.doc.is_return and tax.charge_type == "Actual":
tax.tax_amount = -1 * tax.tax_amount
tax.item_wise_tax_detail = {}
tax_fields = ["total", "tax_amount_after_discount_amount",
@ -396,13 +399,15 @@ class calculate_taxes_and_totals(object):
# total_advance is only for non POS Invoice
if self.doc.doctype == "Sales Invoice":
self.doc.round_floats_in(self.doc, ["base_grand_total", "total_advance", "write_off_amount", "paid_amount"])
total_amount_to_pay = self.doc.base_grand_total - self.doc.write_off_amount
self.doc.outstanding_amount = flt(total_amount_to_pay - self.doc.total_advance - self.doc.paid_amount,
self.doc.precision("outstanding_amount"))
if not self.doc.is_return:
self.doc.round_floats_in(self.doc, ["base_grand_total", "total_advance", "write_off_amount", "paid_amount"])
total_amount_to_pay = self.doc.base_grand_total - self.doc.write_off_amount
self.doc.outstanding_amount = flt(total_amount_to_pay - self.doc.total_advance - self.doc.paid_amount,
self.doc.precision("outstanding_amount"))
else:
self.doc.round_floats_in(self.doc, ["total_advance", "write_off_amount"])
self.doc.total_amount_to_pay = flt(self.doc.base_grand_total - self.doc.write_off_amount,
self.doc.precision("total_amount_to_pay"))
self.doc.outstanding_amount = flt(self.doc.total_amount_to_pay - self.doc.total_advance,
self.doc.precision("outstanding_amount"))
if not self.doc.is_return:
self.doc.outstanding_amount = flt(self.doc.total_amount_to_pay - self.doc.total_advance,
self.doc.precision("outstanding_amount"))

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,7 +7,8 @@ import unittest
import frappe
from frappe.utils import flt, get_datetime, time_diff_in_hours
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory
from erpnext.manufacturing.doctype.production_order.production_order import make_stock_entry, make_time_log
from erpnext.manufacturing.doctype.production_order.production_order \
import make_stock_entry, make_time_log, ProductionNotApplicableError,ItemHasVariantError
from erpnext.stock.doctype.stock_entry import test_stock_entry
from erpnext.projects.doctype.time_log.time_log import OverProductionLoggedError
@ -135,6 +136,22 @@ class TestProductionOrder(unittest.TestCase):
prod_order.set_production_order_operations()
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):
args = frappe._dict(args)

View File

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

View File

@ -9,6 +9,5 @@ Manufacturing
Stock
Support
Utilities
Contacts
Shopping Cart
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_1.fix_against_account
erpnext.patches.v5_1.fix_credit_days_based_on
erpnext.patches.v5_1.track_operations
execute:frappe.rename_doc("DocType", "Salary Manager", "Process Payroll", force=True)
erpnext.patches.v5_1.rename_roles
erpnext.patches.v5_1.default_bom
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();
// Advance calculation applicable to Sales /Purchase Invoice
if(in_list(["Sales Invoice", "Purchase Invoice"], this.frm.doc.doctype) && this.frm.doc.docstatus < 2) {
this.calculate_total_advance(update_paid_amount);
if(in_list(["Sales Invoice", "Purchase Invoice"], this.frm.doc.doctype)
&& this.frm.doc.docstatus < 2 && !this.frm.doc.is_return) {
this.calculate_total_advance(update_paid_amount);
}
// Sales person's commission
@ -93,6 +94,10 @@ erpnext.taxes_and_totals = erpnext.stock.StockController.extend({
tax_fields = ["total", "tax_amount_after_discount_amount",
"tax_amount_for_current_item", "grand_total_for_current_item",
"tax_fraction_for_current_item", "grand_total_fraction_for_current_item"]
if (frappe.meta.get_docfield(me.frm.doc.doctype, "is_return") && me.frm.doc.is_return
&& tax.charge_type == "Actual")
tax.tax_amount = -1 * tax.tax_amount;
if (cstr(tax.charge_type) != "Actual" &&
!(me.discount_amount_applied && me.frm.doc.apply_discount_on=="Grand Total"))

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

View File

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

View File

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

View File

@ -210,7 +210,7 @@ erpnext.selling.SellingController = erpnext.TransactionController.extend({
// NOTE:
// paid_amount and write_off_amount is only for POS Invoice
// total_advance is only for non POS Invoice
if(this.frm.doc.doctype == "Sales Invoice" && this.frm.doc.docstatus==0) {
if(this.frm.doc.doctype == "Sales Invoice" && this.frm.doc.docstatus==0 && !this.frm.doc.is_return) {
frappe.model.round_floats_in(this.frm.doc, ["base_grand_total", "total_advance", "write_off_amount",
"paid_amount"]);
var total_amount_to_pay = this.frm.doc.base_grand_total - this.frm.doc.write_off_amount

View File

@ -18,7 +18,7 @@
"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",
"fieldtype": "Check",
"in_list_view": 1,
@ -139,14 +139,14 @@
"permlevel": 0
},
{
"description": "To enable <b>Point of Sale</b> features",
"description": "To enable \"Point of Sale\" features",
"fieldname": "fs_pos",
"fieldtype": "Check",
"label": "Point of Sale",
"permlevel": 0
},
{
"description": "To enable <b>Point of Sale</b> view",
"description": "To enable \"Point of Sale\" view",
"fieldname": "fs_pos_view",
"fieldtype": "Check",
"label": "POS View",
@ -237,4 +237,4 @@
"write": 1
}
]
}
}

View File

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

View File

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

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

View File

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

View File

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

View File

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

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);
if (doc.docstatus==1) {
cur_frm.add_custom_button(__('Make Sales Return'), this.make_sales_return);
this.show_stock_ledger();
this.show_general_ledger();
}
if(doc.docstatus==0 && !doc.__islocal) {
cur_frm.add_custom_button(__('Make Packing Slip'),
cur_frm.cscript['Make Packing Slip'], frappe.boot.doctype_icons["Packing Slip"], "btn-default");
cur_frm.cscript['Make Packing Slip'], frappe.boot.doctype_icons["Packing Slip"]);
}
erpnext.stock.delivery_note.set_print_hide(doc, dt, dn);
@ -55,7 +56,7 @@ erpnext.stock.DeliveryNoteController = erpnext.selling.SellingController.extend(
company: cur_frm.doc.company
}
})
}, "icon-download", "btn-default");
});
}
},
@ -73,6 +74,13 @@ erpnext.stock.DeliveryNoteController = erpnext.selling.SellingController.extend(
frm: cur_frm
});
},
make_sales_return: function() {
frappe.model.open_mapped_doc({
method: "erpnext.stock.doctype.delivery_note.delivery_note.make_sales_return",
frm: cur_frm
})
},
tc_name: function() {
this.get_terms();

View File

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

View File

@ -84,7 +84,7 @@ class DeliveryNote(SellingController):
def so_required(self):
"""check in manage account if sales order required or not"""
if frappe.db.get_value("Selling Settings", None, 'so_required') == 'Yes':
if not self.is_return and frappe.db.get_value("Selling Settings", None, 'so_required') == 'Yes':
for d in self.get('items'):
if not d.against_sales_order:
frappe.throw(_("Sales Order required for Item {0}").format(d.item_code))
@ -175,17 +175,15 @@ class DeliveryNote(SellingController):
# Check for Approving Authority
frappe.get_doc('Authorization Control').validate_approving_authority(self.doctype, self.company, self.base_grand_total, self)
# update delivered qty in sales order
self.update_prevdoc_status()
if not self.is_return:
# update delivered qty in sales order
self.update_prevdoc_status()
self.check_credit_limit()
self.check_credit_limit()
# create stock ledger entry
self.update_stock_ledger()
self.make_gl_entries()
# set DN status
frappe.db.set(self, 'status', 'Submitted')
@ -193,7 +191,8 @@ class DeliveryNote(SellingController):
self.check_stop_sales_order("against_sales_order")
self.check_next_docstatus()
self.update_prevdoc_status()
if not self.is_return:
self.update_prevdoc_status()
self.update_stock_ledger()
@ -251,9 +250,14 @@ class DeliveryNote(SellingController):
if frappe.db.get_value("Item", d.item_code, "is_stock_item") == "Yes" \
and d.warehouse and flt(d['qty']):
self.update_reserved_qty(d)
incoming_rate = 0
if cint(self.is_return) and self.return_against and self.docstatus==1:
incoming_rate = self.get_incoming_rate_for_sales_return(d.item_code, self.return_against)
sl_entries.append(self.get_sl_entries(d, {
"actual_qty": -1*flt(d['qty']),
"incoming_rate": incoming_rate
}))
self.make_sl_entries(sl_entries)
@ -387,3 +391,9 @@ def make_packing_slip(source_name, target_doc=None):
}, target_doc)
return doclist
@frappe.whitelist()
def make_sales_return(source_name, target_doc=None):
from erpnext.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 \
import get_gl_entries, set_perpetual_inventory
from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry, make_serialized_item
from erpnext.stock.doctype.stock_entry.test_stock_entry \
import make_stock_entry, make_serialized_item, get_qty_after_transaction
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos, SerialNoStatusError
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import create_stock_reconciliation
class TestDeliveryNote(unittest.TestCase):
def test_over_billing_against_dn(self):
@ -175,9 +177,155 @@ class TestDeliveryNote(unittest.TestCase):
self.assertRaises(SerialNoStatusError, dn.submit)
def check_serial_no_values(self, serial_no, field_values):
serial_no = frappe.get_doc("Serial No", serial_no)
for field, value in field_values.items():
self.assertEquals(cstr(frappe.db.get_value("Serial No", serial_no, field)), value)
self.assertEquals(cstr(serial_no.get(field)), value)
def test_sales_return_for_non_bundled_items(self):
set_perpetual_inventory()
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):
dn = frappe.new_doc("Delivery Note")
@ -190,6 +338,8 @@ def create_delivery_note(**args):
dn.company = args.company or "_Test Company"
dn.customer = args.customer or "_Test Customer"
dn.currency = args.currency or "INR"
dn.is_return = args.is_return
dn.return_against = args.return_against
dn.append("items", {
"item_code": args.item or args.item_code or "_Test Item",

View File

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

View File

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

View File

@ -31,9 +31,10 @@ erpnext.stock.PurchaseReceiptController = erpnext.buying.BuyingController.extend
if(this.frm.doc.docstatus == 1) {
if(this.frm.doc.__onload && !this.frm.doc.__onload.billing_complete) {
cur_frm.add_custom_button(__('Make Purchase Invoice'), this.make_purchase_invoice,
frappe.boot.doctype_icons["Purchase Invoice"]);
cur_frm.add_custom_button(__('Make Purchase Invoice'), this.make_purchase_invoice);
}
cur_frm.add_custom_button(__('Make Purchase Return'), this.make_purchase_return);
this.show_stock_ledger();
this.show_general_ledger();
@ -51,7 +52,7 @@ erpnext.stock.PurchaseReceiptController = erpnext.buying.BuyingController.extend
company: cur_frm.doc.company
}
})
}, "icon-download", "btn-default");
});
}
this.frm.toggle_reqd("supplier_warehouse", this.frm.doc.is_subcontracted==="Yes");
@ -105,6 +106,13 @@ erpnext.stock.PurchaseReceiptController = erpnext.buying.BuyingController.extend
frm: cur_frm
})
},
make_purchase_return: function() {
frappe.model.open_mapped_doc({
method: "erpnext.stock.doctype.purchase_receipt.purchase_receipt.make_purchase_return",
frm: cur_frm
})
},
tc_name: function() {
this.get_terms();

View File

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

View File

@ -44,6 +44,7 @@ class PurchaseReceipt(BuyingController):
self.set_status()
self.po_required()
self.validate_with_previous_doc()
self.validate_purchase_return()
self.validate_rejected_warehouse()
self.validate_accepted_rejected_qty()
self.validate_inspection()
@ -60,12 +61,20 @@ class PurchaseReceipt(BuyingController):
self.set_landed_cost_voucher_amount()
self.update_valuation_rate("items")
def set_landed_cost_voucher_amount(self):
for d in self.get("items"):
lc_voucher_amount = frappe.db.sql("""select sum(ifnull(applicable_charges, 0))
from `tabLanded Cost Item`
where docstatus = 1 and purchase_receipt_item = %s""", d.name)
d.landed_cost_voucher_amount = lc_voucher_amount[0][0] if lc_voucher_amount else 0.0
def validate_purchase_return(self):
for d in self.get("items"):
if self.is_return and flt(d.rejected_qty) != 0:
frappe.throw(_("Row #{0}: Rejected Qty can not be entered in Purchase Return").format(d.idx))
# validate rate with ref PR
def validate_rejected_warehouse(self):
for d in self.get("items"):
@ -108,7 +117,7 @@ class PurchaseReceipt(BuyingController):
self.validate_rate_with_reference_doc([["Purchase Order", "prevdoc_docname", "prevdoc_detail_docname"]])
def po_required(self):
if frappe.db.get_value("Buying Settings", None, "po_required") == 'Yes':
if not self.is_return and frappe.db.get_value("Buying Settings", None, "po_required") == 'Yes':
for d in self.get('items'):
if not d.prevdoc_docname:
frappe.throw(_("Purchase Order number required for Item {0}").format(d.item_code))
@ -123,11 +132,20 @@ class PurchaseReceipt(BuyingController):
if pr_qty:
val_rate_db_precision = 6 if cint(self.precision("valuation_rate", d)) <= 6 else 9
sl_entries.append(self.get_sl_entries(d, {
rate = flt(d.valuation_rate, val_rate_db_precision)
sle = self.get_sl_entries(d, {
"actual_qty": flt(pr_qty),
"serial_no": cstr(d.serial_no).strip(),
"incoming_rate": flt(d.valuation_rate, val_rate_db_precision)
}))
"serial_no": cstr(d.serial_no).strip()
})
if self.is_return:
sle.update({
"outgoing_rate": rate
})
else:
sle.update({
"incoming_rate": rate
})
sl_entries.append(sle)
if flt(d.rejected_qty) > 0:
sl_entries.append(self.get_sl_entries(d, {
@ -176,7 +194,6 @@ class PurchaseReceipt(BuyingController):
"item_code": d.rm_item_code,
"warehouse": self.supplier_warehouse,
"actual_qty": -1*flt(d.consumed_qty),
"incoming_rate": 0
}))
def validate_inspection(self):
@ -207,17 +224,16 @@ class PurchaseReceipt(BuyingController):
# Set status as Submitted
frappe.db.set(self, 'status', 'Submitted')
self.update_prevdoc_status()
self.update_ordered_qty()
if not self.is_return:
self.update_prevdoc_status()
self.update_ordered_qty()
purchase_controller.update_last_purchase_rate(self, 1)
self.update_stock_ledger()
from erpnext.stock.doctype.serial_no.serial_no import update_serial_nos_after_submit
update_serial_nos_after_submit(self, "items")
purchase_controller.update_last_purchase_rate(self, 1)
self.make_gl_entries()
def check_next_docstatus(self):
@ -244,12 +260,13 @@ class PurchaseReceipt(BuyingController):
self.update_stock_ledger()
self.update_prevdoc_status()
if not self.is_return:
self.update_prevdoc_status()
# Must be called after updating received qty in PO
self.update_ordered_qty()
# Must be called after updating received qty in PO
self.update_ordered_qty()
pc_obj.update_last_purchase_rate(self, 0)
pc_obj.update_last_purchase_rate(self, 0)
self.make_gl_entries_on_cancel()
@ -417,7 +434,7 @@ def make_purchase_invoice(source_name, target_doc=None):
"doctype": "Purchase Invoice",
"validation": {
"docstatus": ["=", 1],
}
},
},
"Purchase Receipt Item": {
"doctype": "Purchase Invoice Item",
@ -449,3 +466,8 @@ def get_invoiced_qty_map(purchase_receipt):
invoiced_qty_map[pr_detail] += qty
return invoiced_qty_map
@frappe.whitelist()
def make_purchase_return(source_name, target_doc=None):
from erpnext.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 frappe
import frappe.defaults
from frappe.utils import cint, flt
from frappe.utils import cint, flt, cstr
class TestPurchaseReceipt(unittest.TestCase):
def test_make_purchase_invoice(self):
@ -119,6 +119,65 @@ class TestPurchaseReceipt(unittest.TestCase):
for serial_no in rejected_serial_nos:
self.assertEquals(frappe.db.get_value("Serial No", serial_no, "warehouse"),
pr.get("items")[0].rejected_warehouse)
def test_purchase_return(self):
set_perpetual_inventory()
pr = make_purchase_receipt()
return_pr = make_purchase_receipt(is_return=1, return_against=pr.name, qty=-2)
# check sle
outgoing_rate = frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Purchase Receipt",
"voucher_no": return_pr.name}, "outgoing_rate")
self.assertEqual(outgoing_rate, 50)
# check gl entries for return
gl_entries = get_gl_entries("Purchase Receipt", return_pr.name)
self.assertTrue(gl_entries)
expected_values = {
"_Test Warehouse - _TC": [0.0, 100.0],
"Stock Received But Not Billed - _TC": [100.0, 0.0],
}
for gle in gl_entries:
self.assertEquals(expected_values[gle.account][0], gle.debit)
self.assertEquals(expected_values[gle.account][1], gle.credit)
set_perpetual_inventory(0)
def test_purchase_return_for_serialized_items(self):
def _check_serial_no_values(serial_no, field_values):
serial_no = frappe.get_doc("Serial No", serial_no)
for field, value in field_values.items():
self.assertEquals(cstr(serial_no.get(field)), value)
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
pr = make_purchase_receipt(item_code="_Test Serialized Item With Series", qty=1)
serial_no = get_serial_nos(pr.get("items")[0].serial_no)[0]
_check_serial_no_values(serial_no, {
"status": "Available",
"warehouse": "_Test Warehouse - _TC",
"purchase_document_no": pr.name
})
return_pr = make_purchase_receipt(item_code="_Test Serialized Item With Series", qty=-1,
is_return=1, return_against=pr.name, serial_no=serial_no)
_check_serial_no_values(serial_no, {
"status": "Purchase Returned",
"warehouse": "",
"purchase_document_no": pr.name,
"delivery_document_no": return_pr.name
})
def get_gl_entries(voucher_type, voucher_no):
return frappe.db.sql("""select account, debit, credit
@ -142,6 +201,8 @@ def make_purchase_receipt(**args):
pr.is_subcontracted = args.is_subcontracted or "No"
pr.supplier_warehouse = "_Test Warehouse 1 - _TC"
pr.currency = args.currency or "INR"
pr.is_return = args.is_return
pr.return_against = args.return_against
pr.append("items", {
"item_code": args.item or args.item_code or "_Test Item",

View File

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

View File

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

View File

@ -7,20 +7,7 @@ frappe.provide("erpnext.stock");
erpnext.stock.StockEntry = erpnext.stock.StockController.extend({
setup: function() {
var me = this;
this.frm.fields_dict.delivery_note_no.get_query = function() {
return { query: "erpnext.stock.doctype.stock_entry.stock_entry.query_sales_return_doc" };
};
this.frm.fields_dict.sales_invoice_no.get_query =
this.frm.fields_dict.delivery_note_no.get_query;
this.frm.fields_dict.purchase_receipt_no.get_query = function() {
return {
filters:{ 'docstatus': 1 }
};
};
this.frm.fields_dict.bom_no.get_query = function() {
return {
filters:{ 'docstatus': 1 }
@ -28,20 +15,7 @@ erpnext.stock.StockEntry = erpnext.stock.StockController.extend({
};
this.frm.fields_dict.items.grid.get_field('item_code').get_query = function() {
if(in_list(["Sales Return", "Purchase Return"], me.frm.doc.purpose) &&
me.get_doctype_docname()) {
return {
query: "erpnext.stock.doctype.stock_entry.stock_entry.query_return_item",
filters: {
purpose: me.frm.doc.purpose,
delivery_note_no: me.frm.doc.delivery_note_no,
sales_invoice_no: me.frm.doc.sales_invoice_no,
purchase_receipt_no: me.frm.doc.purchase_receipt_no
}
};
} else {
return erpnext.queries.item({is_stock_item: "Yes"});
}
return erpnext.queries.item({is_stock_item: "Yes"});
};
this.frm.set_query("purchase_order", function() {
@ -84,19 +58,6 @@ erpnext.stock.StockEntry = erpnext.stock.StockController.extend({
this.toggle_enable_bom();
this.show_stock_ledger();
this.show_general_ledger();
if(this.frm.doc.docstatus === 1 && frappe.boot.user.can_create.indexOf("Journal Entry")!==-1
&& this.frm.doc.__onload.credit_debit_note_exists == 0 ) {
if(this.frm.doc.purpose === "Sales Return") {
this.frm.add_custom_button(__("Make Credit Note"),
function() { me.make_return_jv(); }, frappe.boot.doctype_icons["Journal Entry"]);
this.add_excise_button();
} else if(this.frm.doc.purpose === "Purchase Return") {
this.frm.add_custom_button(__("Make Debit Note"),
function() { me.make_return_jv(); }, frappe.boot.doctype_icons["Journal Entry"]);
this.add_excise_button();
}
}
},
on_submit: function() {
@ -111,15 +72,10 @@ erpnext.stock.StockEntry = erpnext.stock.StockController.extend({
var me = this;
if(cint(frappe.defaults.get_default("auto_accounting_for_stock")) && this.frm.doc.company) {
var account_for = "stock_adjustment_account";
if (this.frm.doc.purpose == "Purchase Return")
account_for = "stock_received_but_not_billed";
return this.frm.call({
method: "erpnext.accounts.utils.get_company_default",
args: {
"fieldname": account_for,
"fieldname": "stock_adjustment_account",
"company": this.frm.doc.company
},
callback: function(r) {
@ -192,35 +148,6 @@ erpnext.stock.StockEntry = erpnext.stock.StockController.extend({
this.frm.toggle_enable("bom_no", !in_list(["Manufacture", "Material Transfer for Manufacture"], this.frm.doc.purpose));
},
get_doctype_docname: function() {
if(this.frm.doc.purpose === "Sales Return") {
if(this.frm.doc.delivery_note_no && this.frm.doc.sales_invoice_no) {
// both specified
msgprint(__("You can not enter both Delivery Note No and Sales Invoice No. Please enter any one."));
} else if(!(this.frm.doc.delivery_note_no || this.frm.doc.sales_invoice_no)) {
// none specified
msgprint(__("Please enter Delivery Note No or Sales Invoice No to proceed"));
} else if(this.frm.doc.delivery_note_no) {
return {doctype: "Delivery Note", docname: this.frm.doc.delivery_note_no};
} else if(this.frm.doc.sales_invoice_no) {
return {doctype: "Sales Invoice", docname: this.frm.doc.sales_invoice_no};
}
} else if(this.frm.doc.purpose === "Purchase Return") {
if(this.frm.doc.purchase_receipt_no) {
return {doctype: "Purchase Receipt", docname: this.frm.doc.purchase_receipt_no};
} else {
// not specified
msgprint(__("Please enter Purchase Receipt No to proceed"));
}
}
},
add_excise_button: function() {
if(frappe.boot.sysdefaults.country === "India")
this.frm.add_custom_button(__("Make Excise Invoice"), function() {
@ -231,37 +158,16 @@ erpnext.stock.StockEntry = erpnext.stock.StockController.extend({
}, frappe.boot.doctype_icons["Journal Entry"], "btn-default");
},
make_return_jv: function() {
if(this.get_doctype_docname()) {
return this.frm.call({
method: "make_return_jv",
args: {
stock_entry: this.frm.doc.name
},
callback: function(r) {
if(!r.exc) {
var doclist = frappe.model.sync(r.message);
frappe.set_route("Form", doclist[0].doctype, doclist[0].name);
}
}
});
}
},
items_add: function(doc, cdt, cdn) {
var row = frappe.get_doc(cdt, cdn);
this.frm.script_manager.copy_from_first_row("items", row,
["expense_account", "cost_center"]);
this.frm.script_manager.copy_from_first_row("items", row, ["expense_account", "cost_center"]);
if(!row.s_warehouse) row.s_warehouse = this.frm.doc.from_warehouse;
if(!row.t_warehouse) row.t_warehouse = this.frm.doc.to_warehouse;
},
source_mandatory: ["Material Issue", "Material Transfer", "Purchase Return", "Subcontract",
"Material Transfer for Manufacture"],
target_mandatory: ["Material Receipt", "Material Transfer", "Sales Return", "Subcontract",
"Material Transfer for Manufacture"],
source_mandatory: ["Material Issue", "Material Transfer", "Subcontract", "Material Transfer for Manufacture"],
target_mandatory: ["Material Receipt", "Material Transfer", "Subcontract", "Material Transfer for Manufacture"],
from_warehouse: function(doc) {
var me = this;
@ -295,92 +201,21 @@ erpnext.stock.StockEntry = erpnext.stock.StockController.extend({
items_on_form_rendered: function(doc, grid_row) {
erpnext.setup_serial_no();
},
customer: function() {
this.get_party_details({
party: this.frm.doc.customer,
party_type:"Customer",
doctype: this.frm.doc.doctype
});
},
supplier: function() {
this.get_party_details({
party: this.frm.doc.supplier,
party_type:"Supplier",
doctype: this.frm.doc.doctype
});
},
get_party_details: function(args) {
var me = this;
frappe.call({
method: "erpnext.accounts.party.get_party_details",
args: args,
callback: function(r) {
if(r.message) {
me.frm.set_value({
"customer_name": r.message["customer_name"],
"customer_address": r.message["address_display"]
});
}
}
});
},
delivery_note_no: function() {
this.get_party_details_from_against_voucher({
ref_dt: "Delivery Note",
ref_dn: this.frm.doc.delivery_note_no
})
},
sales_invoice_no: function() {
this.get_party_details_from_against_voucher({
ref_dt: "Sales Invoice",
ref_dn: this.frm.doc.sales_invoice_no
})
},
purchase_receipt_no: function() {
this.get_party_details_from_against_voucher({
ref_dt: "Purchase Receipt",
ref_dn: this.frm.doc.purchase_receipt_no
})
},
get_party_details_from_against_voucher: function(args) {
return this.frm.call({
method: "erpnext.stock.doctype.stock_entry.stock_entry.get_party_details",
args: args,
})
}
});
cur_frm.script_manager.make(erpnext.stock.StockEntry);
cur_frm.cscript.toggle_related_fields = function(doc) {
disable_from_warehouse = inList(["Material Receipt", "Sales Return"], doc.purpose);
disable_to_warehouse = inList(["Material Issue", "Purchase Return"], doc.purpose);
cur_frm.toggle_enable("from_warehouse", doc.purpose!='Material Receipt');
cur_frm.toggle_enable("to_warehouse", doc.purpose!='Material Issue');
cur_frm.toggle_enable("from_warehouse", !disable_from_warehouse);
cur_frm.toggle_enable("to_warehouse", !disable_to_warehouse);
cur_frm.fields_dict["items"].grid.set_column_disp("s_warehouse", !disable_from_warehouse);
cur_frm.fields_dict["items"].grid.set_column_disp("t_warehouse", !disable_to_warehouse);
cur_frm.fields_dict["items"].grid.set_column_disp("s_warehouse", doc.purpose!='Material Receipt');
cur_frm.fields_dict["items"].grid.set_column_disp("t_warehouse", doc.purpose!='Material Issue');
cur_frm.cscript.toggle_enable_bom();
if(doc.purpose == 'Purchase Return') {
doc.customer = doc.customer_name = doc.customer_address =
doc.delivery_note_no = doc.sales_invoice_no = null;
doc.bom_no = doc.production_order = doc.fg_completed_qty = null;
} else if(doc.purpose == 'Sales Return') {
doc.supplier=doc.supplier_name = doc.supplier_address = doc.purchase_receipt_no=null;
doc.bom_no = doc.production_order = doc.fg_completed_qty = null;
} else if (doc.purpose == 'Subcontract') {
if (doc.purpose == 'Subcontract') {
doc.customer = doc.customer_name = doc.customer_address =
doc.delivery_note_no = doc.sales_invoice_no = null;
} else {
@ -388,7 +223,7 @@ cur_frm.cscript.toggle_related_fields = function(doc) {
doc.delivery_note_no = doc.sales_invoice_no = doc.supplier =
doc.supplier_name = doc.supplier_address = doc.purchase_receipt_no = null;
}
if(in_list(["Material Receipt", "Sales Return", "Purchase Return"], doc.purpose)) {
if(doc.purpose == "Material Receipt") {
cur_frm.set_value("from_bom", 0);
}
}
@ -505,8 +340,6 @@ cur_frm.cscript.uom = function(doc, cdt, cdn) {
}
cur_frm.cscript.validate = function(doc, cdt, cdn) {
if($.inArray(cur_frm.doc.purpose, ["Purchase Return", "Sales Return"])!==-1)
validated = cur_frm.cscript.get_doctype_docname() ? true : false;
cur_frm.cscript.validate_items(doc);
}
@ -526,14 +359,6 @@ cur_frm.cscript.cost_center = function(doc, cdt, cdn) {
erpnext.utils.copy_value_in_all_row(doc, cdt, cdn, "items", "cost_center");
}
cur_frm.fields_dict.customer.get_query = function(doc, cdt, cdn) {
return { query: "erpnext.controllers.queries.customer_query" }
}
cur_frm.fields_dict.supplier.get_query = function(doc, cdt, cdn) {
return { query: "erpnext.controllers.queries.supplier_query" }
}
cur_frm.cscript.company = function(doc, cdt, cdn) {
if(doc.company) {
erpnext.get_fiscal_year(doc.company, doc.posting_date, function() {

View File

@ -54,7 +54,7 @@
"no_copy": 0,
"oldfieldname": "purpose",
"oldfieldtype": "Select",
"options": "Material Issue\nMaterial Receipt\nMaterial Transfer\nMaterial Transfer for Manufacture\nManufacture\nRepack\nSubcontract\nSales Return\nPurchase Return",
"options": "Material Issue\nMaterial Receipt\nMaterial Transfer\nMaterial Transfer for Manufacture\nManufacture\nRepack\nSubcontract",
"permlevel": 0,
"print_hide": 0,
"read_only": 0,
@ -678,7 +678,7 @@
"is_submittable": 1,
"issingle": 0,
"max_attachments": 0,
"modified": "2015-07-13 05:28:26.085266",
"modified": "2015-07-22 18:47:20.328749",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Entry",

View File

@ -4,10 +4,8 @@
from __future__ import unicode_literals
import frappe
import frappe.defaults
from frappe.utils import cstr, cint, flt, comma_or, get_datetime, getdate
from frappe import _
from frappe.utils import cstr, cint, flt, comma_or, get_datetime, getdate
from erpnext.stock.utils import get_incoming_rate
from erpnext.stock.stock_ledger import get_previous_sle, NegativeStockError
from erpnext.controllers.queries import get_match_cond
@ -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.accounts.utils import validate_fiscal_year
class NotUpdateStockError(frappe.ValidationError): pass
class StockOverReturnError(frappe.ValidationError): pass
class IncorrectValuationRateError(frappe.ValidationError): pass
class DuplicateEntryForProductionOrderError(frappe.ValidationError): pass
class OperationsNotCompleteError(frappe.ValidationError): pass
@ -37,13 +33,6 @@ class StockEntry(StockController):
item.update(get_available_qty(item.item_code,
item.s_warehouse))
count = frappe.db.exists({
"doctype": "Journal Entry",
"stock_entry":self.name,
"docstatus":1
})
self.get("__onload").credit_debit_note_exists = 1 if count else 0
def validate(self):
self.pro_doc = None
if self.production_order:
@ -61,7 +50,6 @@ class StockEntry(StockController):
self.get_stock_and_rate()
self.validate_bom()
self.validate_finished_goods()
self.validate_return_reference_doc()
self.validate_with_material_request()
self.validate_valuation_rate()
self.set_total_incoming_outgoing_value()
@ -84,16 +72,13 @@ class StockEntry(StockController):
def validate_purpose(self):
valid_purposes = ["Material Issue", "Material Receipt", "Material Transfer", "Material Transfer for Manufacture",
"Manufacture", "Repack", "Subcontract", "Sales Return", "Purchase Return"]
"Manufacture", "Repack", "Subcontract"]
if self.purpose not in valid_purposes:
frappe.throw(_("Purpose must be one of {0}").format(comma_or(valid_purposes)))
if self.purpose in ("Manufacture", "Repack", "Sales Return") and not self.difference_account:
if self.purpose in ("Manufacture", "Repack") and not self.difference_account:
self.difference_account = frappe.db.get_value("Company", self.company, "default_expense_account")
if self.purpose in ("Purchase Return") and not self.difference_account:
frappe.throw(_("Difference Account mandatory for purpose '{0}'").format(self.purpose))
def set_transfer_qty(self):
for item in self.get("items"):
if not flt(item.qty):
@ -122,7 +107,7 @@ class StockEntry(StockController):
if not item.transfer_qty:
item.transfer_qty = item.qty * item.conversion_factor
if (self.purpose in ("Material Transfer", "Sales Return", "Purchase Return", "Material Transfer for Manufacture")
if (self.purpose in ("Material Transfer", "Material Transfer for Manufacture")
and not item.serial_no
and item.item_code in serialized_items):
frappe.throw(_("Row #{0}: Please specify Serial No for Item {1}").format(item.idx, item.item_code),
@ -131,8 +116,8 @@ class StockEntry(StockController):
def validate_warehouse(self):
"""perform various (sometimes conditional) validations on warehouse"""
source_mandatory = ["Material Issue", "Material Transfer", "Purchase Return", "Subcontract", "Material Transfer for Manufacture"]
target_mandatory = ["Material Receipt", "Material Transfer", "Sales Return", "Subcontract", "Material Transfer for Manufacture"]
source_mandatory = ["Material Issue", "Material Transfer", "Subcontract", "Material Transfer for Manufacture"]
target_mandatory = ["Material Receipt", "Material Transfer", "Subcontract", "Material Transfer for Manufacture"]
validate_for_manufacture_repack = any([d.bom_no for d in self.get("items")])
@ -201,9 +186,7 @@ class StockEntry(StockController):
def check_if_operations_completed(self):
"""Check if Time Logs are completed against before manufacturing to capture operating costs."""
prod_order = frappe.get_doc("Production Order", self.production_order)
if not prod_order.track_operations:
return
for d in prod_order.get("operations"):
total_completed_qty = flt(self.fg_completed_qty) + flt(prod_order.produced_qty)
if total_completed_qty > flt(d.completed_qty):
@ -291,8 +274,8 @@ class StockEntry(StockController):
# get incoming rate
if not d.bom_no:
if not flt(d.incoming_rate) or d.s_warehouse or self.purpose == "Sales Return" or force:
incoming_rate = flt(self.get_incoming_rate(args), self.precision("incoming_rate", d))
if not flt(d.incoming_rate) or d.s_warehouse or force:
incoming_rate = flt(get_incoming_rate(args), self.precision("incoming_rate", d))
if incoming_rate > 0:
d.incoming_rate = incoming_rate
@ -336,27 +319,6 @@ class StockEntry(StockController):
return operation_cost_per_unit + (flt(self.additional_operating_cost) / flt(qty))
def get_incoming_rate(self, args):
incoming_rate = 0
if self.purpose == "Sales Return":
incoming_rate = self.get_incoming_rate_for_sales_return(args)
else:
incoming_rate = get_incoming_rate(args)
return incoming_rate
def get_incoming_rate_for_sales_return(self, args):
incoming_rate = 0.0
if (self.delivery_note_no or self.sales_invoice_no) and args.get("item_code"):
incoming_rate = frappe.db.sql("""select abs(ifnull(stock_value_difference, 0) / actual_qty)
from `tabStock Ledger Entry`
where voucher_type = %s and voucher_no = %s and item_code = %s limit 1""",
((self.delivery_note_no and "Delivery Note" or "Sales Invoice"),
self.delivery_note_no or self.sales_invoice_no, args.item_code))
incoming_rate = incoming_rate[0][0] if incoming_rate else 0.0
return incoming_rate
def validate_purchase_order(self):
"""Throw exception if more raw material is transferred against Purchase Order than in
the raw materials supplied table"""
@ -403,55 +365,6 @@ class StockEntry(StockController):
frappe.throw(_("Finished Item {0} must be entered for Manufacture type entry")
.format(production_item))
def validate_return_reference_doc(self):
"""validate item with reference doc"""
ref = get_return_doc_and_details(self)
if ref.doc:
# validate docstatus
if ref.doc.docstatus != 1:
frappe.throw(_("{0} {1} must be submitted").format(ref.doc.doctype, ref.doc.name),
frappe.InvalidStatusError)
# update stock check
if ref.doc.doctype == "Sales Invoice" and cint(ref.doc.update_stock) != 1:
frappe.throw(_("'Update Stock' for Sales Invoice {0} must be set").format(ref.doc.name), NotUpdateStockError)
# posting date check
ref_posting_datetime = "%s %s" % (ref.doc.posting_date, ref.doc.posting_time or "00:00:00")
if get_datetime(ref_posting_datetime) < get_datetime(ref_posting_datetime):
from frappe.utils.dateutils import datetime_in_user_format
frappe.throw(_("Posting timestamp must be after {0}")
.format(datetime_in_user_format(ref_posting_datetime)))
stock_items = get_stock_items_for_return(ref.doc, ref.parentfields)
already_returned_item_qty = self.get_already_returned_item_qty(ref.fieldname)
for item in self.get("items"):
# validate if item exists in the ref doc and that it is a stock item
if item.item_code not in stock_items:
frappe.throw(_("Item {0} does not exist in {1} {2}").format(item.item_code, ref.doc.doctype, ref.doc.name),
frappe.DoesNotExistError)
# validate quantity <= ref item's qty - qty already returned
if self.purpose == "Purchase Return":
ref_item_qty = sum([flt(d.qty)*flt(d.conversion_factor) for d in ref.doc.get({"item_code": item.item_code})])
elif self.purpose == "Sales Return":
ref_item_qty = sum([flt(d.qty) for d in ref.doc.get({"item_code": item.item_code})])
returnable_qty = ref_item_qty - flt(already_returned_item_qty.get(item.item_code))
if not returnable_qty:
frappe.throw(_("Item {0} has already been returned").format(item.item_code), StockOverReturnError)
elif item.transfer_qty > returnable_qty:
frappe.throw(_("Cannot return more than {0} for Item {1}").format(returnable_qty, item.item_code),
StockOverReturnError)
def get_already_returned_item_qty(self, ref_fieldname):
return dict(frappe.db.sql("""select item_code, sum(transfer_qty) as qty
from `tabStock Entry Detail` where parent in (
select name from `tabStock Entry` where `%s`=%s and docstatus=1)
group by item_code""" % (ref_fieldname, "%s"), (self.get(ref_fieldname),)))
def update_stock_ledger(self):
sl_entries = []
for d in self.get('items'):
@ -514,6 +427,7 @@ class StockEntry(StockController):
(args.get('item_code')), as_dict = 1)
if not item:
frappe.throw(_("Item {0} is not active or end of life has been reached").format(args.get("item_code")))
item = item[0]
ret = {
@ -561,7 +475,7 @@ class StockEntry(StockController):
ret = {
"actual_qty" : get_previous_sle(args).get("qty_after_transaction") or 0,
"incoming_rate" : self.get_incoming_rate(args)
"incoming_rate" : get_incoming_rate(args)
}
return ret
@ -738,15 +652,6 @@ class StockEntry(StockController):
if getdate(self.posting_date) > getdate(expiry_date):
frappe.throw(_("Batch {0} of Item {1} has expired.").format(item.batch_no, item.item_code))
@frappe.whitelist()
def get_party_details(ref_dt, ref_dn):
if ref_dt in ["Delivery Note", "Sales Invoice"]:
res = frappe.db.get_value(ref_dt, ref_dn,
["customer", "customer_name", "address_display as customer_address"], as_dict=1)
else:
res = frappe.db.get_value(ref_dt, ref_dn,
["supplier", "supplier_name", "address_display as supplier_address"], as_dict=1)
return res or {}
@frappe.whitelist()
def get_production_order_details(production_order):
@ -756,264 +661,3 @@ def get_production_order_details(production_order):
from `tabProduction Order` where name = %s""", production_order, as_dict=1)
return res and res[0] or {}
def query_sales_return_doc(doctype, txt, searchfield, start, page_len, filters):
conditions = ""
if doctype == "Sales Invoice":
conditions = "and update_stock=1"
return frappe.db.sql("""select name, customer, customer_name
from `tab%s` where docstatus = 1
and (`%s` like %%(txt)s
or `customer` like %%(txt)s) %s %s
order by name, customer, customer_name
limit %s""" % (doctype, searchfield, conditions,
get_match_cond(doctype), "%(start)s, %(page_len)s"),
{"txt": "%%%s%%" % txt, "start": start, "page_len": page_len},
as_list=True)
def query_purchase_return_doc(doctype, txt, searchfield, start, page_len, filters):
return frappe.db.sql("""select name, supplier, supplier_name
from `tab%s` where docstatus = 1
and (`%s` like %%(txt)s
or `supplier` like %%(txt)s) %s
order by name, supplier, supplier_name
limit %s""" % (doctype, searchfield, get_match_cond(doctype),
"%(start)s, %(page_len)s"), {"txt": "%%%s%%" % txt, "start":
start, "page_len": page_len}, as_list=True)
def query_return_item(doctype, txt, searchfield, start, page_len, filters):
txt = txt.replace("%", "")
ref = get_return_doc_and_details(filters)
stock_items = get_stock_items_for_return(ref.doc, ref.parentfields)
result = []
for item in ref.doc.get_all_children():
if getattr(item, "item_code", None) in stock_items:
item.item_name = cstr(item.item_name)
item.description = cstr(item.description)
if (txt in item.item_code) or (txt in item.item_name) or (txt in item.description):
val = [
item.item_code,
(len(item.item_name) > 40) and (item.item_name[:40] + "...") or item.item_name,
(len(item.description) > 40) and (item.description[:40] + "...") or \
item.description
]
if val not in result:
result.append(val)
return result[start:start+page_len]
def get_stock_items_for_return(ref_doc, parentfields):
"""return item codes filtered from doc, which are stock items"""
if isinstance(parentfields, basestring):
parentfields = [parentfields]
all_items = list(set([d.item_code for d in
ref_doc.get_all_children() if d.get("item_code")]))
stock_items = frappe.db.sql_list("""select name from `tabItem`
where is_stock_item='Yes' and name in (%s)""" % (", ".join(["%s"] * len(all_items))),
tuple(all_items))
return stock_items
def get_return_doc_and_details(args):
ref = frappe._dict()
# get ref_doc
if args.get("purpose") in return_map:
for fieldname, val in return_map[args.get("purpose")].items():
if args.get(fieldname):
ref.fieldname = fieldname
ref.doc = frappe.get_doc(val[0], args.get(fieldname))
ref.parentfields = val[1]
break
return ref
return_map = {
"Sales Return": {
# [Ref DocType, [Item tables' parentfields]]
"delivery_note_no": ["Delivery Note", ["items", "packed_items"]],
"sales_invoice_no": ["Sales Invoice", ["items", "packed_items"]]
},
"Purchase Return": {
"purchase_receipt_no": ["Purchase Receipt", ["items"]]
}
}
@frappe.whitelist()
def make_return_jv(stock_entry):
se = frappe.get_doc("Stock Entry", stock_entry)
if not se.purpose in ["Sales Return", "Purchase Return"]:
return
ref = get_return_doc_and_details(se)
if ref.doc.doctype == "Delivery Note":
result = make_return_jv_from_delivery_note(se, ref)
elif ref.doc.doctype == "Sales Invoice":
result = make_return_jv_from_sales_invoice(se, ref)
elif ref.doc.doctype == "Purchase Receipt":
result = make_return_jv_from_purchase_receipt(se, ref)
# create jv doc and fetch balance for each unique row item
jv = frappe.new_doc("Journal Entry")
jv.update({
"posting_date": se.posting_date,
"voucher_type": se.purpose == "Sales Return" and "Credit Note" or "Debit Note",
"fiscal_year": se.fiscal_year,
"company": se.company,
"stock_entry": se.name
})
from erpnext.accounts.utils import get_balance_on
for r in result:
jv.append("accounts", {
"account": r.get("account"),
"party_type": r.get("party_type"),
"party": r.get("party"),
"balance": get_balance_on(r.get("account"), se.posting_date) if r.get("account") else 0
})
return jv
def make_return_jv_from_sales_invoice(se, ref):
# customer account entry
parent = {
"account": ref.doc.debit_to,
"party_type": "Customer",
"party": ref.doc.customer
}
# income account entries
children = []
for se_item in se.get("items"):
# find item in ref.doc
ref_item = ref.doc.get({"item_code": se_item.item_code})[0]
account = get_sales_account_from_item(ref.doc, ref_item)
if account not in children:
children.append(account)
return [parent] + [{"account": account} for account in children]
def get_sales_account_from_item(doc, ref_item):
account = None
if not getattr(ref_item, "income_account", None):
if ref_item.parent_item:
parent_item = doc.get("items", {"item_code": ref_item.parent_item})[0]
account = parent_item.income_account
else:
account = ref_item.income_account
return account
def make_return_jv_from_delivery_note(se, ref):
invoices_against_delivery = get_invoice_list("Sales Invoice Item", "delivery_note",
ref.doc.name)
if not invoices_against_delivery:
sales_orders_against_delivery = [d.against_sales_order for d in ref.doc.get_all_children() if getattr(d, "against_sales_order", None)]
if sales_orders_against_delivery:
invoices_against_delivery = get_invoice_list("Sales Invoice Item", "sales_order",
sales_orders_against_delivery)
if not invoices_against_delivery:
return []
packing_item_parent_map = dict([[d.item_code, d.parent_item] for d in ref.doc.get(ref.parentfields[1])])
parent = {}
children = []
for se_item in se.get("items"):
for sales_invoice in invoices_against_delivery:
si = frappe.get_doc("Sales Invoice", sales_invoice)
if se_item.item_code in packing_item_parent_map:
ref_item = si.get({"item_code": packing_item_parent_map[se_item.item_code]})
else:
ref_item = si.get({"item_code": se_item.item_code})
if not ref_item:
continue
ref_item = ref_item[0]
account = get_sales_account_from_item(si, ref_item)
if account not in children:
children.append(account)
if not parent:
parent = {
"account": si.debit_to,
"party_type": "Customer",
"party": si.customer
}
break
result = [parent] + [{"account": account} for account in children]
return result
def get_invoice_list(doctype, link_field, value):
if isinstance(value, basestring):
value = [value]
return frappe.db.sql_list("""select distinct parent from `tab%s`
where docstatus = 1 and `%s` in (%s)""" % (doctype, link_field,
", ".join(["%s"]*len(value))), tuple(value))
def make_return_jv_from_purchase_receipt(se, ref):
invoice_against_receipt = get_invoice_list("Purchase Invoice Item", "purchase_receipt",
ref.doc.name)
if not invoice_against_receipt:
purchase_orders_against_receipt = [d.prevdoc_docname for d in
ref.doc.get("items", {"prevdoc_doctype": "Purchase Order"})
if getattr(d, "prevdoc_docname", None)]
if purchase_orders_against_receipt:
invoice_against_receipt = get_invoice_list("Purchase Invoice Item", "purchase_order",
purchase_orders_against_receipt)
if not invoice_against_receipt:
return []
parent = {}
children = []
for se_item in se.get("items"):
for purchase_invoice in invoice_against_receipt:
pi = frappe.get_doc("Purchase Invoice", purchase_invoice)
ref_item = pi.get({"item_code": se_item.item_code})
if not ref_item:
continue
ref_item = ref_item[0]
account = ref_item.expense_account
if account not in children:
children.append(account)
if not parent:
parent = {
"account": pi.credit_to,
"party_type": "Supplier",
"party": pi.supplier
}
break
result = [parent] + [{"account": account} for account in children]
return result

View File

@ -4,15 +4,12 @@
from __future__ import unicode_literals
import frappe, unittest
import frappe.defaults
from frappe.utils import flt, nowdate, nowtime, getdate
from frappe.utils import flt, nowdate, nowtime
from erpnext.stock.doctype.serial_no.serial_no import *
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt \
import set_perpetual_inventory, make_purchase_receipt
import set_perpetual_inventory
from erpnext.stock.doctype.stock_ledger_entry.stock_ledger_entry import StockFreezeError
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_invoice
from erpnext.stock.stock_ledger import get_previous_sle
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order, create_dn_against_so
from erpnext.stock.doctype.stock_entry.stock_entry import make_return_jv, NotUpdateStockError
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import create_stock_reconciliation
def get_sle(**args):
@ -303,263 +300,6 @@ class TestStockEntry(unittest.TestCase):
self.assertEquals(expected_gl_entries[i][1], gle[1])
self.assertEquals(expected_gl_entries[i][2], gle[2])
def _test_sales_invoice_return(self, item_code, delivered_qty, returned_qty):
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
si = create_sales_invoice(item_code=item_code, qty=delivered_qty)
se = make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=returned_qty,
purpose="Sales Return", sales_invoice_no=si.name, do_not_save=True)
self.assertRaises(NotUpdateStockError, se.insert)
make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=200, incoming_rate=100)
# check currency available qty in bin
actual_qty_0 = get_qty_after_transaction()
# insert a pos invoice with update stock
si = create_sales_invoice(update_stock=1, item_code=item_code, qty=5)
# check available bin qty after invoice submission
actual_qty_1 = get_qty_after_transaction()
self.assertEquals(actual_qty_0 - delivered_qty, actual_qty_1)
# check if item is validated
se = make_stock_entry(item_code="_Test Item Home Desktop 200", target="_Test Warehouse - _TC",
qty=returned_qty, purpose="Sales Return", sales_invoice_no=si.name, do_not_save=True)
self.assertRaises(frappe.DoesNotExistError, se.insert)
# try again
se = make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC",
qty=returned_qty, purpose="Sales Return", sales_invoice_no=si.name)
# check if available qty is increased
actual_qty_2 = get_qty_after_transaction()
self.assertEquals(actual_qty_1 + returned_qty, actual_qty_2)
return se
def test_sales_invoice_return_of_non_packing_item(self):
self._test_sales_invoice_return("_Test Item", 5, 2)
def test_sales_invoice_return_of_packing_item(self):
self._test_sales_invoice_return("_Test Product Bundle Item", 25, 20)
def _test_delivery_note_return(self, item_code, delivered_qty, returned_qty):
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice
make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=50, incoming_rate=100)
actual_qty_0 = get_qty_after_transaction()
# make a delivery note based on this invoice
dn = create_delivery_note(item_code="_Test Item",
warehouse="_Test Warehouse - _TC", qty=delivered_qty)
actual_qty_1 = get_qty_after_transaction()
self.assertEquals(actual_qty_0 - delivered_qty, actual_qty_1)
si = make_sales_invoice(dn.name)
si.insert()
si.submit()
# insert and submit stock entry for sales return
se = make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC",
qty=returned_qty, purpose="Sales Return", delivery_note_no=dn.name)
actual_qty_2 = get_qty_after_transaction()
self.assertEquals(actual_qty_1 + returned_qty, actual_qty_2)
return se
def test_delivery_note_return_of_non_packing_item(self):
self._test_delivery_note_return("_Test Item", 5, 2)
def test_delivery_note_return_of_packing_item(self):
self._test_delivery_note_return("_Test Product Bundle Item", 25, 20)
def _test_sales_return_jv(self, se):
jv = make_return_jv(se.name)
self.assertEqual(len(jv.get("accounts")), 2)
self.assertEqual(jv.get("voucher_type"), "Credit Note")
self.assertEqual(jv.get("posting_date"), getdate(se.posting_date))
self.assertEqual(jv.get("accounts")[0].get("account"), "Debtors - _TC")
self.assertEqual(jv.get("accounts")[0].get("party_type"), "Customer")
self.assertEqual(jv.get("accounts")[0].get("party"), "_Test Customer")
self.assertEqual(jv.get("accounts")[1].get("account"), "Sales - _TC")
def test_make_return_jv_for_sales_invoice_non_packing_item(self):
se = self._test_sales_invoice_return("_Test Item", 5, 2)
self._test_sales_return_jv(se)
def test_make_return_jv_for_sales_invoice_packing_item(self):
se = self._test_sales_invoice_return("_Test Product Bundle Item", 25, 20)
self._test_sales_return_jv(se)
def test_make_return_jv_for_delivery_note_non_packing_item(self):
se = self._test_delivery_note_return("_Test Item", 5, 2)
self._test_sales_return_jv(se)
se = self._test_delivery_note_return_against_sales_order("_Test Item", 5, 2)
self._test_sales_return_jv(se)
def test_make_return_jv_for_delivery_note_packing_item(self):
se = self._test_delivery_note_return("_Test Product Bundle Item", 25, 20)
self._test_sales_return_jv(se)
se = self._test_delivery_note_return_against_sales_order("_Test Product Bundle Item", 25, 20)
self._test_sales_return_jv(se)
def _test_delivery_note_return_against_sales_order(self, item_code, delivered_qty, returned_qty):
from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice
actual_qty_0 = get_qty_after_transaction()
so = make_sales_order(qty=50)
dn = create_dn_against_so(so.name, delivered_qty)
actual_qty_1 = get_qty_after_transaction()
self.assertEquals(actual_qty_0 - delivered_qty, actual_qty_1)
si = make_sales_invoice(so.name)
si.insert()
si.submit()
# insert and submit stock entry for sales return
se = make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC",
qty=returned_qty, purpose="Sales Return", delivery_note_no=dn.name)
actual_qty_2 = get_qty_after_transaction()
self.assertEquals(actual_qty_1 + returned_qty, actual_qty_2)
return se
def test_purchase_receipt_return(self):
actual_qty_0 = get_qty_after_transaction()
# submit purchase receipt
pr = make_purchase_receipt(item_code="_Test Item", warehouse="_Test Warehouse - _TC", qty=5)
actual_qty_1 = get_qty_after_transaction()
self.assertEquals(actual_qty_0 + 5, actual_qty_1)
pi_doc = make_purchase_invoice(pr.name)
pi = frappe.get_doc(pi_doc)
pi.posting_date = pr.posting_date
pi.credit_to = "_Test Payable - _TC"
for d in pi.get("items"):
d.expense_account = "_Test Account Cost for Goods Sold - _TC"
d.cost_center = "_Test Cost Center - _TC"
for d in pi.get("taxes"):
d.cost_center = "_Test Cost Center - _TC"
pi.insert()
pi.submit()
# submit purchase return
se = make_stock_entry(item_code="_Test Item", source="_Test Warehouse - _TC",
qty=5, purpose="Purchase Return", purchase_receipt_no=pr.name)
actual_qty_2 = get_qty_after_transaction()
self.assertEquals(actual_qty_1 - 5, actual_qty_2)
return se, pr.name
def test_over_stock_return(self):
from erpnext.stock.doctype.stock_entry.stock_entry import StockOverReturnError
# out of 10, 5 gets returned
prev_se, pr_docname = self.test_purchase_receipt_return()
se = make_stock_entry(item_code="_Test Item", source="_Test Warehouse - _TC",
qty=6, purpose="Purchase Return", purchase_receipt_no=pr_docname, do_not_save=True)
self.assertRaises(StockOverReturnError, se.insert)
def _test_purchase_return_jv(self, se):
jv = make_return_jv(se.name)
self.assertEqual(len(jv.get("accounts")), 2)
self.assertEqual(jv.get("voucher_type"), "Debit Note")
self.assertEqual(jv.get("posting_date"), getdate(se.posting_date))
self.assertEqual(jv.get("accounts")[0].get("account"), "_Test Payable - _TC")
self.assertEqual(jv.get("accounts")[0].get("party"), "_Test Supplier")
self.assertEqual(jv.get("accounts")[1].get("account"), "_Test Account Cost for Goods Sold - _TC")
def test_make_return_jv_for_purchase_receipt(self):
se, pr_name = self.test_purchase_receipt_return()
self._test_purchase_return_jv(se)
se, pr_name = self._test_purchase_return_return_against_purchase_order()
self._test_purchase_return_jv(se)
def _test_purchase_return_return_against_purchase_order(self):
actual_qty_0 = get_qty_after_transaction()
from erpnext.buying.doctype.purchase_order.test_purchase_order \
import test_records as purchase_order_test_records
from erpnext.buying.doctype.purchase_order.purchase_order import \
make_purchase_receipt, make_purchase_invoice
# submit purchase receipt
po = frappe.copy_doc(purchase_order_test_records[0])
po.transaction_date = nowdate()
po.is_subcontracted = None
po.get("items")[0].item_code = "_Test Item"
po.get("items")[0].rate = 50
po.insert()
po.submit()
pr_doc = make_purchase_receipt(po.name)
pr = frappe.get_doc(pr_doc)
pr.posting_date = po.transaction_date
pr.insert()
pr.submit()
actual_qty_1 = get_qty_after_transaction()
self.assertEquals(actual_qty_0 + 10, actual_qty_1)
pi_doc = make_purchase_invoice(po.name)
pi = frappe.get_doc(pi_doc)
pi.posting_date = pr.posting_date
pi.credit_to = "_Test Payable - _TC"
for d in pi.get("items"):
d.expense_account = "_Test Account Cost for Goods Sold - _TC"
d.cost_center = "_Test Cost Center - _TC"
for d in pi.get("taxes"):
d.cost_center = "_Test Cost Center - _TC"
pi.run_method("calculate_taxes_and_totals")
pi.bill_no = "NA"
pi.insert()
pi.submit()
# submit purchase return
se = make_stock_entry(item_code="_Test Item", source="_Test Warehouse - _TC",
qty=5, purpose="Purchase Return", purchase_receipt_no=pr.name)
actual_qty_2 = get_qty_after_transaction()
self.assertEquals(actual_qty_1 - 5, actual_qty_2)
return se, pr.name
def test_serial_no_not_reqd(self):
se = frappe.copy_doc(test_records[0])
se.get("items")[0].serial_no = "ABCD"

View File

@ -150,6 +150,15 @@
"permlevel": 0,
"read_only": 1
},
{
"fieldname": "outgoing_rate",
"fieldtype": "Currency",
"label": "Outgoing Rate",
"options": "Company:company:default_currency",
"permlevel": 0,
"precision": "",
"read_only": 1
},
{
"fieldname": "stock_uom",
"fieldtype": "Link",
@ -266,7 +275,7 @@
"icon": "icon-list",
"idx": 1,
"in_create": 1,
"modified": "2015-07-13 05:28:27.826340",
"modified": "2015-07-16 16:37:54.452944",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Ledger Entry",

View File

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

View File

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

View File

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

View File

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

View File

@ -109,7 +109,7 @@ class update_entries_after(object):
def build(self):
# includes current entry!
entries_to_fix = self.get_sle_after_datetime()
for sle in entries_to_fix:
self.process_sle(sle)
@ -230,19 +230,21 @@ class update_entries_after(object):
self.valuation_rate = new_stock_value / new_stock_qty
def get_moving_average_values(self, sle):
incoming_rate = flt(sle.incoming_rate)
actual_qty = flt(sle.actual_qty)
if flt(sle.actual_qty) > 0:
if actual_qty > 0 or flt(sle.outgoing_rate) > 0:
rate = flt(sle.incoming_rate) if actual_qty > 0 else flt(sle.outgoing_rate)
if self.qty_after_transaction < 0 and not self.valuation_rate:
# if negative stock, take current valuation rate as incoming rate
self.valuation_rate = incoming_rate
self.valuation_rate = rate
new_stock_qty = abs(self.qty_after_transaction) + actual_qty
new_stock_value = (abs(self.qty_after_transaction) * self.valuation_rate) + (actual_qty * incoming_rate)
new_stock_value = (abs(self.qty_after_transaction) * self.valuation_rate) + (actual_qty * rate)
if new_stock_qty:
self.valuation_rate = new_stock_value / flt(new_stock_qty)
elif not self.valuation_rate and self.qty_after_transaction <= 0:
self.valuation_rate = get_valuation_rate(sle.item_code, sle.warehouse, self.allow_zero_rate)
@ -251,6 +253,7 @@ class update_entries_after(object):
def get_fifo_values(self, sle):
incoming_rate = flt(sle.incoming_rate)
actual_qty = flt(sle.actual_qty)
outgoing_rate = flt(sle.outgoing_rate)
if actual_qty > 0:
if not self.stock_queue:
@ -278,16 +281,34 @@ class update_entries_after(object):
_rate = 0
self.stock_queue.append([0, _rate])
batch = self.stock_queue[0]
index = None
if outgoing_rate > 0:
# Find the entry where rate matched with outgoing rate
for i, v in enumerate(self.stock_queue):
if v[1] == outgoing_rate:
index = i
break
# If no entry found with outgoing rate, collapse stack
if index == None:
new_stock_value = sum((d[0]*d[1] for d in self.stock_queue)) - qty_to_pop*outgoing_rate
new_stock_qty = sum((d[0] for d in self.stock_queue)) - qty_to_pop
self.stock_queue = [[new_stock_qty, new_stock_value/new_stock_qty if new_stock_qty > 0 else outgoing_rate]]
break
else:
index = 0
# select first batch or the batch with same rate
batch = self.stock_queue[index]
if qty_to_pop >= batch[0]:
# consume current batch
qty_to_pop = qty_to_pop - batch[0]
self.stock_queue.pop(0)
self.stock_queue.pop(index)
if not self.stock_queue and qty_to_pop:
# stock finished, qty still remains to be withdrawn
# negative stock, keep in as a negative batch
self.stock_queue.append([-qty_to_pop, batch[1]])
self.stock_queue.append([-qty_to_pop, outgoing_rate or batch[1]])
break
else:

View File

@ -1,6 +1,6 @@
{% macro product_image_square(website_image, 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 %}
</div>
{% endmacro %}
@ -8,7 +8,7 @@
{% macro product_image(website_image, css_class="") %}
<div class="product-image {% if not website_image -%} missing-image {%- endif %} {{ css_class }}">
{% 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 -%}
<i class="centered octicon octicon-device-camera"></i>
{%- endif %}

View File

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

View File

@ -3,12 +3,12 @@
from __future__ import unicode_literals
import frappe
import frappe.share
from frappe import _
from frappe.utils import cstr, now_datetime, cint, flt
import frappe.share
from erpnext.controllers.status_updater import StatusUpdater
class UOMMustBeIntegerError(frappe.ValidationError): pass
class TransactionBase(StatusUpdater):
def load_notification_message(self):
@ -109,8 +109,6 @@ def delete_events(ref_type, ref_name):
frappe.delete_doc("Event", frappe.db.sql_list("""select name from `tabEvent`
where ref_type=%s and ref_name=%s""", (ref_type, ref_name)), for_reload=True)
class UOMMustBeIntegerError(frappe.ValidationError): pass
def validate_uom_is_integer(doc, uom_field, qty_fields, child_dt=None):
if isinstance(qty_fields, basestring):
qty_fields = [qty_fields]

View File

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