diff --git a/erpnext/__version__.py b/erpnext/__version__.py index 9049b135b4..3a243f5bb7 100644 --- a/erpnext/__version__.py +++ b/erpnext/__version__.py @@ -1,2 +1,2 @@ from __future__ import unicode_literals -__version__ = '5.2.1' +__version__ = '5.3.0' diff --git a/erpnext/accounts/doctype/account/account.py b/erpnext/accounts/doctype/account/account.py index 683734b879..f1d822a89c 100644 --- a/erpnext/accounts/doctype/account/account.py +++ b/erpnext/accounts/doctype/account/account.py @@ -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.")) diff --git a/erpnext/accounts/doctype/cost_center/cost_center.js b/erpnext/accounts/doctype/cost_center/cost_center.js index 6946bcb106..fb649e6d1b 100644 --- a/erpnext/accounts/doctype/cost_center/cost_center.js +++ b/erpnext/accounts/doctype/cost_center/cost_center.js @@ -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'], ] } diff --git a/erpnext/accounts/doctype/cost_center/cost_center.json b/erpnext/accounts/doctype/cost_center/cost_center.json index 6177d35ced..186283a0a2 100644 --- a/erpnext/accounts/doctype/cost_center/cost_center.json +++ b/erpnext/accounts/doctype/cost_center/cost_center.json @@ -66,7 +66,7 @@ "precision": "" }, { - "description": "Define Budget for this Cost Center. To set budget action, see Company Master", + "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" -} \ No newline at end of file +} diff --git a/erpnext/accounts/doctype/cost_center/cost_center.py b/erpnext/accounts/doctype/cost_center/cost_center.py index f26c80ba59..0f51a00cd6 100644 --- a/erpnext/accounts/doctype/cost_center/cost_center.py +++ b/erpnext/accounts/doctype/cost_center/cost_center.py @@ -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 diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py index 3d306fb8d5..edee1226eb 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.py +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals import frappe -from frappe.utils import flt, fmt_money, getdate, formatdate, cstr +from frappe.utils import flt, fmt_money, getdate, formatdate, cstr, cint from frappe import _ from frappe.model.document import Document @@ -139,9 +139,9 @@ def update_outstanding_amt(account, party_type, party, against_voucher_type, aga if against_voucher_amount < 0: bal = -bal - # Validation : Outstanding can not be negative - if bal < 0 and not on_cancel: - frappe.throw(_("Outstanding for {0} cannot be less than zero ({1})").format(against_voucher, fmt_money(bal))) + # Validation : Outstanding can not be negative for JV + if bal < 0 and not on_cancel: + frappe.throw(_("Outstanding for {0} cannot be less than zero ({1})").format(against_voucher, fmt_money(bal))) # Update outstanding amt on against voucher if against_voucher_type in ["Sales Invoice", "Purchase Invoice"]: diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.json b/erpnext/accounts/doctype/journal_entry/journal_entry.json index 249fcc46d3..4847a71ac2 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.json +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.json @@ -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" -} \ No newline at end of file +} diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index 07dbf721f5..6a02706532 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -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); diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index 69b0708f1a..f8101dc7af 100755 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -12,7 +12,7 @@ "no_copy": 1, "oldfieldname": "naming_series", "oldfieldtype": "Select", - "options": "PINV-", + "options": "PINV-\nPINV-RET-", "permlevel": 0, "print_hide": 1, "read_only": 0, @@ -154,6 +154,28 @@ "read_only": 0, "search_index": 0 }, + { + "fieldname": "is_return", + "fieldtype": "Check", + "label": "Is Return", + "no_copy": 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", diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 1ac0f5acd8..006470f860 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -37,14 +37,16 @@ class PurchaseInvoice(BuyingController): super(PurchaseInvoice, self).validate() - self.po_required() - self.pr_required() - self.validate_supplier_invoice() + if not self.is_return: + self.po_required() + self.pr_required() + self.validate_supplier_invoice() + self.validate_advance_jv("advances", "purchase_order") + self.check_active_purchase_items() self.check_conversion_rate() self.validate_credit_to_acc() self.clear_unallocated_advances("Purchase Invoice Advance", "advances") - self.validate_advance_jv("advances", "purchase_order") self.check_for_stopped_status() self.validate_with_previous_doc() self.validate_uom_is_integer("uom", "qty") @@ -71,8 +73,9 @@ class PurchaseInvoice(BuyingController): super(PurchaseInvoice, self).set_missing_values(for_validate) def get_advances(self): - super(PurchaseInvoice, self).get_advances(self.credit_to, "Supplier", self.supplier, - "Purchase Invoice Advance", "advances", "debit", "purchase_order") + if not self.is_return: + super(PurchaseInvoice, self).get_advances(self.credit_to, "Supplier", self.supplier, + "Purchase Invoice Advance", "advances", "debit", "purchase_order") def check_active_purchase_items(self): for d in self.get('items'): @@ -226,9 +229,11 @@ class PurchaseInvoice(BuyingController): # this sequence because outstanding may get -negative self.make_gl_entries() - self.update_against_document_in_jv() - self.update_prevdoc_status() - self.update_billing_status_for_zero_amount_refdoc("Purchase Order") + if not self.is_return: + self.update_against_document_in_jv() + self.update_prevdoc_status() + self.update_billing_status_for_zero_amount_refdoc("Purchase Order") + self.update_project() def make_gl_entries(self): @@ -358,11 +363,12 @@ class PurchaseInvoice(BuyingController): make_gl_entries(gl_entries, cancel=(self.docstatus == 2)) def on_cancel(self): - from erpnext.accounts.utils import remove_against_link_from_jv - remove_against_link_from_jv(self.doctype, self.name, "against_voucher") + if not self.is_return: + from erpnext.accounts.utils import remove_against_link_from_jv + remove_against_link_from_jv(self.doctype, self.name, "against_voucher") - self.update_prevdoc_status() - self.update_billing_status_for_zero_amount_refdoc("Purchase Order") + self.update_prevdoc_status() + self.update_billing_status_for_zero_amount_refdoc("Purchase Order") self.make_gl_entries_on_cancel() self.update_project() @@ -403,3 +409,8 @@ def get_expense_account(doctype, txt, searchfield, start, page_len, filters): and tabAccount.%(key)s LIKE '%(txt)s' %(mcond)s""" % {'company': filters['company'], 'key': searchfield, 'txt': "%%%s%%" % frappe.db.escape(txt), 'mcond':get_match_cond(doctype)}) + +@frappe.whitelist() +def make_purchase_return(source_name, target_doc=None): + from erpnext.controllers.sales_and_purchase_return import make_return_doc + return make_return_doc("Purchase Invoice", source_name, target_doc) \ No newline at end of file diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 7f46b083d8..5f3d4c8a04 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -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') diff --git a/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template.py b/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template.py index a2ba72dba6..bffa8e6d74 100644 --- a/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template.py +++ b/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template.py @@ -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) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 5b2f3483e7..fdc1a58f6b 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -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'] ] } -}); +}); \ No newline at end of file diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index 25dd3988f1..cd70a46c5d 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -21,7 +21,7 @@ "no_copy": 1, "oldfieldname": "naming_series", "oldfieldtype": "Select", - "options": "SINV-", + "options": "SINV-\nSINV-RET-", "permlevel": 0, "print_hide": 1, "read_only": 0, @@ -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", diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 829478df79..5a9ccea1d2 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -80,14 +80,16 @@ class SalesInvoice(SellingController): self.check_prev_docstatus() - self.update_status_updater_args() - self.update_prevdoc_status() - self.update_billing_status_for_zero_amount_refdoc("Sales Order") - self.check_credit_limit() + if not self.is_return: + self.update_status_updater_args() + self.update_prevdoc_status() + self.update_billing_status_for_zero_amount_refdoc("Sales Order") + self.check_credit_limit() + # this sequence because outstanding may get -ve self.make_gl_entries() - if not cint(self.is_pos) == 1: + if not cint(self.is_pos) == 1 and not self.is_return: self.update_against_document_in_jv() self.update_time_log_batch(self.name) @@ -100,13 +102,15 @@ class SalesInvoice(SellingController): self.update_stock_ledger() self.check_stop_sales_order("sales_order") - + from erpnext.accounts.utils import remove_against_link_from_jv remove_against_link_from_jv(self.doctype, self.name, "against_invoice") - - self.update_status_updater_args() - self.update_prevdoc_status() - self.update_billing_status_for_zero_amount_refdoc("Sales Order") + + if not self.is_return: + self.update_status_updater_args() + self.update_prevdoc_status() + self.update_billing_status_for_zero_amount_refdoc("Sales Order") + self.validate_c_form_on_cancel() self.make_gl_entries_on_cancel() @@ -199,8 +203,9 @@ class SalesInvoice(SellingController): self.set_taxes() def get_advances(self): - super(SalesInvoice, self).get_advances(self.debit_to, "Customer", self.customer, - "Sales Invoice Advance", "advances", "credit", "sales_order") + if not self.is_return: + super(SalesInvoice, self).get_advances(self.debit_to, "Customer", self.customer, + "Sales Invoice Advance", "advances", "credit", "sales_order") def get_company_abbr(self): return frappe.db.sql("select abbr from tabCompany where name=%s", self.company)[0][0] @@ -285,6 +290,8 @@ class SalesInvoice(SellingController): def so_dn_required(self): """check in manage account if sales order / delivery note required or not.""" + if self.is_return: + return dic = {'Sales Order':'so_required','Delivery Note':'dn_required'} for i in dic: if frappe.db.get_value('Selling Settings', None, dic[i]) == 'Yes': @@ -419,13 +426,16 @@ class SalesInvoice(SellingController): def update_stock_ledger(self): sl_entries = [] for d in self.get_item_list(): - if frappe.db.get_value("Item", d.item_code, "is_stock_item") == "Yes" \ - and d.warehouse: + if frappe.db.get_value("Item", d.item_code, "is_stock_item") == "Yes" and d.warehouse: + incoming_rate = 0 + if cint(self.is_return) and self.return_against and self.docstatus==1: + incoming_rate = self.get_incoming_rate_for_sales_return(d.item_code, self.return_against) + sl_entries.append(self.get_sl_entries(d, { "actual_qty": -1*flt(d.qty), - "stock_uom": frappe.db.get_value("Item", d.item_code, "stock_uom") + "stock_uom": frappe.db.get_value("Item", d.item_code, "stock_uom"), + "incoming_rate": incoming_rate })) - self.make_sl_entries(sl_entries) def make_gl_entries(self, repost_future_gle=True): @@ -435,8 +445,7 @@ class SalesInvoice(SellingController): from erpnext.accounts.general_ledger import make_gl_entries # if POS and amount is written off, there's no outstanding and hence no need to update it - update_outstanding = cint(self.is_pos) and self.write_off_account \ - and 'No' or 'Yes' + update_outstanding = "No" if (cint(self.is_pos) or self.write_off_account) else "Yes" make_gl_entries(gl_entries, cancel=(self.docstatus == 2), update_outstanding=update_outstanding, merge_entries=False) @@ -484,7 +493,7 @@ class SalesInvoice(SellingController): "against": self.against_income_account, "debit": self.base_grand_total, "remarks": self.remarks, - "against_voucher": self.name, + "against_voucher": self.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) \ No newline at end of file diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index cf752afc9d..6d54f0ab6c 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -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", diff --git a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py index 6721bd89e1..b36287b691 100644 --- a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py +++ b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py @@ -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) diff --git a/erpnext/accounts/print_format/credit_note/credit_note.json b/erpnext/accounts/print_format/credit_note/credit_note.json index de405e694f..863d4aa607 100644 --- a/erpnext/accounts/print_format/credit_note/credit_note.json +++ b/erpnext/accounts/print_format/credit_note/credit_note.json @@ -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
\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\"), \"\" + doc.get_formatted(\"total_amount\") + \"
\" + (doc.total_amount_in_words or \"\") + \"
\"),\n (_(\"Remarks\"), doc.remark)\n ) -%}\n\n
\n
\n
{{ value }}
\n
\n\n {%- endfor -%}\n\n
\n
\n

\n {{ _(\"For\") }} {{ doc.company }},
\n
\n
\n
\n {{ _(\"Authorized Signatory\") }}\n

\n
\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
\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\"), \"\" + doc.get_formatted(\"total_amount\") + \"
\" + (doc.total_amount_in_words or \"\") + \"
\"),\n (_(\"Remarks\"), doc.remark)\n ) -%}\n\n
\n
\n
{{ value }}
\n
\n\n {%- endfor -%}\n\n
\n
\n

\n {{ _(\"For\") }} {{ doc.company }},
\n
\n
\n
\n {{ _(\"Authorized Signatory\") }}\n

\n
\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" -} +} \ No newline at end of file diff --git a/erpnext/contacts/__init__.py b/erpnext/accounts/print_format/credit_note___negative_invoice/__init__.py similarity index 100% rename from erpnext/contacts/__init__.py rename to erpnext/accounts/print_format/credit_note___negative_invoice/__init__.py diff --git a/erpnext/accounts/print_format/credit_note___negative_invoice/credit_note___negative_invoice.json b/erpnext/accounts/print_format/credit_note___negative_invoice/credit_note___negative_invoice.json new file mode 100644 index 0000000000..e7d7eabaff --- /dev/null +++ b/erpnext/accounts/print_format/credit_note___negative_invoice/credit_note___negative_invoice.json @@ -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": "\n\n

\n\t{{ doc.company }}
\n\t{{ doc.select_print_heading or _(\"Credit Note\") }}
\n

\n\n
\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\"), \"\" + doc.get_formatted(\"grand_total\", absolute_value=True) + \"
\" + (doc.in_words or \"\")),\n\t(_(\"Against\"), doc.return_against),\n (_(\"Remarks\"), doc.remarks)\n) -%}\n\n\t\t
\n\t\t
\n\t\t
{{ value }}
\n\t\t
\n{%- endfor -%}\n\n
\n
\n

\n {{ _(\"For\") }} {{ doc.company }},
\n
\n
\n
\n {{ _(\"Authorized Signatory\") }}\n

", + "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" +} \ No newline at end of file diff --git a/erpnext/accounts/report/accounts_payable/accounts_payable.json b/erpnext/accounts/report/accounts_payable/accounts_payable.json index 71537a837b..13d28a4bea 100644 --- a/erpnext/accounts/report/accounts_payable/accounts_payable.json +++ b/erpnext/accounts/report/accounts_payable/accounts_payable.json @@ -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" -} \ No newline at end of file + "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" +} diff --git a/erpnext/accounts/report/general_ledger/general_ledger.html b/erpnext/accounts/report/general_ledger/general_ledger.html index 0d3170ac73..f22e721270 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.html +++ b/erpnext/accounts/report/general_ledger/general_ledger.html @@ -1,5 +1,5 @@
- {%= frappe.boot.letter_heads[frappe.defaults.get_default("letter_head")] %} + {%= frappe.boot.letter_heads[filters.letter_head || frappe.defaults.get_default("letter_head")] %}

{%= __("Statement of Account") %}

{%= (filters.party || filters.account) && ((filters.party || filters.account) + ", ") || "" %} {%= filters.company %}

diff --git a/erpnext/accounts/report/general_ledger/general_ledger.js b/erpnext/accounts/report/general_ledger/general_ledger.js index de7027b474..b4d9b9f749 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.js +++ b/erpnext/accounts/report/general_ledger/general_ledger.js @@ -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"), } ] } diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index 8153912304..75c353d170 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -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 \ diff --git a/erpnext/buying/doctype/purchase_common/purchase_common.js b/erpnext/buying/doctype/purchase_common/purchase_common.js index 1b7d20ae82..19ad9ab651 100644 --- a/erpnext/buying/doctype/purchase_common/purchase_common.js +++ b/erpnext/buying/doctype/purchase_common/purchase_common.js @@ -164,8 +164,10 @@ erpnext.buying.BuyingController = erpnext.TransactionController.extend({ frappe.model.round_floats_in(this.frm.doc, ["base_grand_total", "total_advance", "write_off_amount"]); this.frm.doc.total_amount_to_pay = flt(this.frm.doc.base_grand_total - this.frm.doc.write_off_amount, precision("total_amount_to_pay")); - this.frm.doc.outstanding_amount = flt(this.frm.doc.total_amount_to_pay - this.frm.doc.total_advance, - precision("outstanding_amount")); + if (!this.frm.doc.is_return) { + this.frm.doc.outstanding_amount = flt(this.frm.doc.total_amount_to_pay - this.frm.doc.total_advance, + precision("outstanding_amount")); + } } } }); diff --git a/erpnext/buying/doctype/purchase_common/purchase_common.py b/erpnext/buying/doctype/purchase_common/purchase_common.py index 476aa92f67..1bf6f8fe67 100644 --- a/erpnext/buying/doctype/purchase_common/purchase_common.py +++ b/erpnext/buying/doctype/purchase_common/purchase_common.py @@ -41,8 +41,7 @@ class PurchaseCommon(BuyingController): def validate_for_items(self, obj): items = [] for d in obj.get("items"): - # validation for valid qty - if flt(d.qty) < 0 or (d.parenttype != 'Purchase Receipt' and not flt(d.qty)): + if not d.qty: frappe.throw(_("Please enter quantity for Item {0}").format(d.item_code)) # udpate with latest quantities diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index 20edbca0e5..6049810213 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -11,39 +11,32 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend( this._super(); // this.frm.dashboard.reset(); - if(doc.docstatus == 1 && doc.status != 'Stopped'){ - // cur_frm.dashboard.add_progress(cint(doc.per_received) + __("% Received"), - // doc.per_received); - // cur_frm.dashboard.add_progress(cint(doc.per_billed) + __("% Billed"), - // doc.per_billed); - + if(doc.docstatus == 1 && doc.status != 'Stopped') { if(flt(doc.per_received, 2) < 100) { - cur_frm.add_custom_button(__('Make Purchase Receipt'), - this.make_purchase_receipt); + cur_frm.add_custom_button(__('Make Purchase Receipt'), this.make_purchase_receipt); + if(doc.is_subcontracted==="Yes") { - cur_frm.add_custom_button(__('Transfer Material to Supplier'), - function() { me.make_stock_entry() }); + cur_frm.add_custom_button(__('Transfer Material to Supplier'), this.make_stock_entry); } } if(flt(doc.per_billed, 2) < 100) - cur_frm.add_custom_button(__('Make Invoice'), this.make_purchase_invoice, - frappe.boot.doctype_icons["Purchase Invoice"]); + cur_frm.add_custom_button(__('Make Invoice'), this.make_purchase_invoice); + if(flt(doc.per_billed, 2) < 100 || doc.per_received < 100) - cur_frm.add_custom_button(__('Stop'), cur_frm.cscript['Stop Purchase Order'], - "icon-exclamation", "btn-default"); + cur_frm.add_custom_button(__('Stop'), cur_frm.cscript['Stop Purchase Order']); } else if(doc.docstatus===0) { cur_frm.cscript.add_from_mappers(); } if(doc.docstatus == 1 && doc.status == 'Stopped') - cur_frm.add_custom_button(__('Unstop Purchase Order'), - cur_frm.cscript['Unstop Purchase Order'], "icon-check"); + cur_frm.add_custom_button(__('Unstop Purchase Order'), cur_frm.cscript['Unstop Purchase Order']); }, make_stock_entry: function() { var items = $.map(cur_frm.doc.items, function(d) { return d.bom ? d.item_code : false; }), - me = this; + var me = this; + if(items.length===1) { me._make_stock_entry(items[0]); return; @@ -96,7 +89,7 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend( company: cur_frm.doc.company } }) - }, "icon-download", "btn-default" + } ); cur_frm.add_custom_button(__('From Supplier Quotation'), @@ -110,7 +103,7 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend( company: cur_frm.doc.company } }) - }, "icon-download", "btn-default" + } ); cur_frm.add_custom_button(__('For Supplier'), @@ -122,7 +115,7 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend( docstatus: ["!=", 2], } }) - }, "icon-download", "btn-default" + } ); }, diff --git a/erpnext/change_log/v5/v5_3_0.md b/erpnext/change_log/v5/v5_3_0.md new file mode 100644 index 0000000000..e36909080d --- /dev/null +++ b/erpnext/change_log/v5/v5_3_0.md @@ -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 \ No newline at end of file diff --git a/erpnext/config/crm.py b/erpnext/config/crm.py index 3a7ab18143..d7a6b2e2be 100644 --- a/erpnext/config/crm.py +++ b/erpnext/config/crm.py @@ -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"), + } ] }, { diff --git a/erpnext/config/selling.py b/erpnext/config/selling.py index 543396462d..62dfe2326d 100644 --- a/erpnext/config/selling.py +++ b/erpnext/config/selling.py @@ -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", diff --git a/erpnext/contacts/doctype/__init__.py b/erpnext/contacts/doctype/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/contacts/doctype/party_type/__init__.py b/erpnext/contacts/doctype/party_type/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/contacts/doctype/party_type/party_type.json b/erpnext/contacts/doctype/party_type/party_type.json deleted file mode 100644 index 19ffefba7c..0000000000 --- a/erpnext/contacts/doctype/party_type/party_type.json +++ /dev/null @@ -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 - } - ] -} \ No newline at end of file diff --git a/erpnext/contacts/doctype/party_type/party_type.py b/erpnext/contacts/doctype/party_type/party_type.py deleted file mode 100644 index d21216f161..0000000000 --- a/erpnext/contacts/doctype/party_type/party_type.py +++ /dev/null @@ -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'; diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 98f240958f..7610042b5f 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -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) diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 9867973258..0b60473b8b 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -26,8 +26,7 @@ class BuyingController(StockController): def validate(self): super(BuyingController, self).validate() if getattr(self, "supplier", None) and not self.supplier_name: - self.supplier_name = frappe.db.get_value("Supplier", - self.supplier, "supplier_name") + self.supplier_name = frappe.db.get_value("Supplier", self.supplier, "supplier_name") self.is_item_table_empty() self.set_qty_as_per_stock_uom() self.validate_stock_or_nonstock_items() diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py new file mode 100644 index 0000000000..899d1c1165 --- /dev/null +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -0,0 +1,138 @@ +# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals +import frappe +from frappe import _ +from frappe.utils import flt, get_datetime, format_datetime + +class StockOverReturnError(frappe.ValidationError): pass + + +def validate_return(doc): + if not doc.meta.get_field("is_return") or not doc.is_return: + return + + validate_return_against(doc) + validate_returned_items(doc) + +def validate_return_against(doc): + if not doc.return_against: + frappe.throw(_("{0} is mandatory for Return").format(doc.meta.get_label("return_against"))) + else: + filters = {"doctype": doc.doctype, "docstatus": 1, "company": doc.company} + if doc.meta.get_field("customer"): + filters["customer"] = doc.customer + elif doc.meta.get_field("supplier"): + filters["supplier"] = doc.supplier + + if not frappe.db.exists(filters): + frappe.throw(_("Invalid {0}: {1}") + .format(doc.meta.get_label("return_against"), doc.return_against)) + else: + ref_doc = frappe.get_doc(doc.doctype, doc.return_against) + + # validate posting date time + return_posting_datetime = "%s %s" % (doc.posting_date, doc.get("posting_time") or "00:00:00") + ref_posting_datetime = "%s %s" % (ref_doc.posting_date, ref_doc.get("posting_time") or "00:00:00") + + if get_datetime(return_posting_datetime) < get_datetime(ref_posting_datetime): + frappe.throw(_("Posting timestamp must be after {0}").format(format_datetime(ref_posting_datetime))) + + # validate same exchange rate + if doc.conversion_rate != ref_doc.conversion_rate: + frappe.throw(_("Exchange Rate must be same as {0} {1} ({2})") + .format(doc.doctype, doc.return_against, ref_doc.conversion_rate)) + + # validate update stock + if doc.doctype == "Sales Invoice" and doc.update_stock and not ref_doc.update_stock: + frappe.throw(_("'Update Stock' can not be checked because items are not delivered via {0}") + .format(doc.return_against)) + +def validate_returned_items(doc): + valid_items = frappe._dict() + for d in frappe.db.sql("""select item_code, sum(qty) as qty, rate from `tab{0} Item` + where parent = %s group by item_code""".format(doc.doctype), doc.return_against, as_dict=1): + valid_items.setdefault(d.item_code, d) + + if doc.doctype in ("Delivery Note", "Sales Invoice"): + for d in frappe.db.sql("""select item_code, sum(qty) as qty from `tabPacked Item` + where parent = %s group by item_code""".format(doc.doctype), doc.return_against, as_dict=1): + valid_items.setdefault(d.item_code, d) + + already_returned_items = get_already_returned_items(doc) + + items_returned = False + for d in doc.get("items"): + if flt(d.qty) < 0: + if d.item_code not in valid_items: + frappe.throw(_("Row # {0}: Returned Item {1} does not exists in {2} {3}") + .format(d.idx, d.item_code, doc.doctype, doc.return_against)) + else: + ref = valid_items.get(d.item_code, frappe._dict()) + already_returned_qty = flt(already_returned_items.get(d.item_code)) + max_return_qty = flt(ref.qty) - already_returned_qty + + if already_returned_qty >= ref.qty: + frappe.throw(_("Item {0} has already been returned").format(d.item_code), StockOverReturnError) + elif abs(d.qty) > max_return_qty: + frappe.throw(_("Row # {0}: Cannot return more than {1} for Item {2}") + .format(d.idx, ref.qty, d.item_code), StockOverReturnError) + elif ref.rate and flt(d.rate) != ref.rate: + frappe.throw(_("Row # {0}: Rate must be same as {1} {2}") + .format(d.idx, doc.doctype, doc.return_against)) + + + items_returned = True + + if not items_returned: + frappe.throw(_("Atleast one item should be entered with negative quantity in return document")) + +def get_already_returned_items(doc): + return frappe._dict(frappe.db.sql(""" + select + child.item_code, sum(abs(child.qty)) as qty + from + `tab{0} Item` child, `tab{1}` par + where + child.parent = par.name and par.docstatus = 1 + and ifnull(par.is_return, 0) = 1 and par.return_against = %s and child.qty < 0 + group by item_code + """.format(doc.doctype, doc.doctype), doc.return_against)) + +def make_return_doc(doctype, source_name, target_doc=None): + from frappe.model.mapper import get_mapped_doc + def set_missing_values(source, target): + doc = frappe.get_doc(target) + doc.is_return = 1 + doc.return_against = source.name + doc.ignore_pricing_rule = 1 + doc.run_method("calculate_taxes_and_totals") + + def update_item(source_doc, target_doc, source_parent): + target_doc.qty = -1* source_doc.qty + if doctype == "Purchase Receipt": + target_doc.received_qty = -1* source_doc.qty + elif doctype == "Purchase Invoice": + target_doc.purchase_receipt = source_doc.purchase_receipt + target_doc.pr_detail = source_doc.pr_detail + + doclist = get_mapped_doc(doctype, source_name, { + doctype: { + "doctype": doctype, + + "validation": { + "docstatus": ["=", 1], + } + }, + doctype +" Item": { + "doctype": doctype + " Item", + "fields": { + "purchase_order": "purchase_order", + "purchase_receipt": "purchase_receipt" + }, + "postprocess": update_item + }, + }, target_doc, set_missing_values) + + return doclist diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index b2a9f0317f..5ad0a25af3 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -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 diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 6678007489..19440e24a7 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -216,6 +216,17 @@ class StockController(AccountsController): tuple(item_codes)) return serialized_items + + def get_incoming_rate_for_sales_return(self, item_code, against_document): + incoming_rate = 0.0 + if against_document and item_code: + incoming_rate = frappe.db.sql("""select abs(ifnull(stock_value_difference, 0) / actual_qty) + from `tabStock Ledger Entry` + where voucher_type = %s and voucher_no = %s and item_code = %s limit 1""", + (self.doctype, against_document, item_code)) + incoming_rate = incoming_rate[0][0] if incoming_rate else 0.0 + + return incoming_rate def update_gl_entries_after(posting_date, posting_time, for_warehouses=None, for_items=None, warehouse_account=None): diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index e77a9a6619..f22b62488b 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -77,6 +77,9 @@ class calculate_taxes_and_totals(object): if not self.discount_amount_applied: validate_taxes_and_charges(tax) validate_inclusive_tax(tax, self.doc) + + if self.doc.meta.get_field("is_return") and self.doc.is_return and tax.charge_type == "Actual": + tax.tax_amount = -1 * tax.tax_amount tax.item_wise_tax_detail = {} tax_fields = ["total", "tax_amount_after_discount_amount", @@ -396,13 +399,15 @@ class calculate_taxes_and_totals(object): # total_advance is only for non POS Invoice if self.doc.doctype == "Sales Invoice": - self.doc.round_floats_in(self.doc, ["base_grand_total", "total_advance", "write_off_amount", "paid_amount"]) - total_amount_to_pay = self.doc.base_grand_total - self.doc.write_off_amount - self.doc.outstanding_amount = flt(total_amount_to_pay - self.doc.total_advance - self.doc.paid_amount, - self.doc.precision("outstanding_amount")) + if not self.doc.is_return: + self.doc.round_floats_in(self.doc, ["base_grand_total", "total_advance", "write_off_amount", "paid_amount"]) + total_amount_to_pay = self.doc.base_grand_total - self.doc.write_off_amount + self.doc.outstanding_amount = flt(total_amount_to_pay - self.doc.total_advance - self.doc.paid_amount, + self.doc.precision("outstanding_amount")) else: self.doc.round_floats_in(self.doc, ["total_advance", "write_off_amount"]) self.doc.total_amount_to_pay = flt(self.doc.base_grand_total - self.doc.write_off_amount, self.doc.precision("total_amount_to_pay")) - self.doc.outstanding_amount = flt(self.doc.total_amount_to_pay - self.doc.total_advance, - self.doc.precision("outstanding_amount")) + if not self.doc.is_return: + self.doc.outstanding_amount = flt(self.doc.total_amount_to_pay - self.doc.total_advance, + self.doc.precision("outstanding_amount")) diff --git a/erpnext/crm/doctype/newsletter/newsletter.json b/erpnext/crm/doctype/newsletter/newsletter.json index f2baf2e462..715a97fb9b 100644 --- a/erpnext/crm/doctype/newsletter/newsletter.json +++ b/erpnext/crm/doctype/newsletter/newsletter.json @@ -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", diff --git a/erpnext/hooks.py b/erpnext/hooks.py index a87c8f092e..811e4c01e2 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -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" diff --git a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json index ea9569f9b4..eb770a8609 100644 --- a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json +++ b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json @@ -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", diff --git a/erpnext/manufacturing/doctype/production_order/production_order.js b/erpnext/manufacturing/doctype/production_order/production_order.js index 17fa202a4d..d36637784a 100644 --- a/erpnext/manufacturing/doctype/production_order/production_order.js +++ b/erpnext/manufacturing/doctype/production_order/production_order.js @@ -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()] ] } } diff --git a/erpnext/manufacturing/doctype/production_order/production_order.json b/erpnext/manufacturing/doctype/production_order/production_order.json index 75aab9963e..e07ac5bf97 100644 --- a/erpnext/manufacturing/doctype/production_order/production_order.json +++ b/erpnext/manufacturing/doctype/production_order/production_order.json @@ -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", diff --git a/erpnext/manufacturing/doctype/production_order/production_order.py b/erpnext/manufacturing/doctype/production_order/production_order.py index 26af40a441..13cc52374d 100644 --- a/erpnext/manufacturing/doctype/production_order/production_order.py +++ b/erpnext/manufacturing/doctype/production_order/production_order.py @@ -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() diff --git a/erpnext/manufacturing/doctype/production_order/test_production_order.py b/erpnext/manufacturing/doctype/production_order/test_production_order.py index 34d584a94a..b91b2e12c8 100644 --- a/erpnext/manufacturing/doctype/production_order/test_production_order.py +++ b/erpnext/manufacturing/doctype/production_order/test_production_order.py @@ -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) diff --git a/erpnext/manufacturing/doctype/production_planning_tool/production_planning_tool.py b/erpnext/manufacturing/doctype/production_planning_tool/production_planning_tool.py index 86a14d8c21..271abac10e 100644 --- a/erpnext/manufacturing/doctype/production_planning_tool/production_planning_tool.py +++ b/erpnext/manufacturing/doctype/production_planning_tool/production_planning_tool.py @@ -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']) diff --git a/erpnext/modules.txt b/erpnext/modules.txt index 67c856db7f..dfca2f27c5 100644 --- a/erpnext/modules.txt +++ b/erpnext/modules.txt @@ -9,6 +9,5 @@ Manufacturing Stock Support Utilities -Contacts Shopping Cart Hub Node diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 269dcbaf9c..762912372b 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -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") \ No newline at end of file diff --git a/erpnext/patches/v5_1/track_operations.py b/erpnext/patches/v5_1/track_operations.py deleted file mode 100644 index 0a121420e5..0000000000 --- a/erpnext/patches/v5_1/track_operations.py +++ /dev/null @@ -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 )""") \ No newline at end of file diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 4a26d6d39c..07c2d56cf1 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -13,8 +13,9 @@ erpnext.taxes_and_totals = erpnext.stock.StockController.extend({ this.apply_discount_amount(); // Advance calculation applicable to Sales /Purchase Invoice - if(in_list(["Sales Invoice", "Purchase Invoice"], this.frm.doc.doctype) && this.frm.doc.docstatus < 2) { - this.calculate_total_advance(update_paid_amount); + if(in_list(["Sales Invoice", "Purchase Invoice"], this.frm.doc.doctype) + && this.frm.doc.docstatus < 2 && !this.frm.doc.is_return) { + this.calculate_total_advance(update_paid_amount); } // Sales person's commission @@ -93,6 +94,10 @@ erpnext.taxes_and_totals = erpnext.stock.StockController.extend({ tax_fields = ["total", "tax_amount_after_discount_amount", "tax_amount_for_current_item", "grand_total_for_current_item", "tax_fraction_for_current_item", "grand_total_fraction_for_current_item"] + + if (frappe.meta.get_docfield(me.frm.doc.doctype, "is_return") && me.frm.doc.is_return + && tax.charge_type == "Actual") + tax.tax_amount = -1 * tax.tax_amount; if (cstr(tax.charge_type) != "Actual" && !(me.discount_amount_applied && me.frm.doc.apply_discount_on=="Grand Total")) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 0a75dad09e..01e5781a4e 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -46,6 +46,23 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ } }); } + + if(this.frm.fields_dict["return_against"]) { + this.frm.set_query("return_against", function(doc) { + var filters = { + "docstatus": 1, + "is_return": 0, + "company": doc.company + }; + if (me.frm.fields_dict["customer"] && doc.customer) filters["customer"] = doc.customer; + if (me.frm.fields_dict["supplier"] && doc.supplier) filters["supplier"] = doc.supplier; + + return { + filters: filters + } + }); + } + }, onload_post_render: function() { @@ -354,7 +371,8 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ plc_conversion_rate: function() { if(this.frm.doc.price_list_currency === this.get_company_currency()) { this.frm.set_value("plc_conversion_rate", 1.0); - } else if(this.frm.doc.price_list_currency === this.frm.doc.currency && this.frm.doc.plc_conversion_rate && cint(this.frm.doc.plc_conversion_rate) != 1 && + } else if(this.frm.doc.price_list_currency === this.frm.doc.currency + && this.frm.doc.plc_conversion_rate && cint(this.frm.doc.plc_conversion_rate) != 1 && cint(this.frm.doc.plc_conversion_rate) != cint(this.frm.doc.conversion_rate)) { this.frm.set_value("conversion_rate", this.frm.doc.plc_conversion_rate); } diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index fcdae4d0b4..d06d550b94 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -18,35 +18,31 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( // delivery note if(flt(doc.per_delivered, 2) < 100 && ["Sales", "Shopping Cart"].indexOf(doc.order_type)!==-1) - cur_frm.add_custom_button(__('Make Delivery'), this.make_delivery_note, "icon-truck"); + cur_frm.add_custom_button(__('Make Delivery'), this.make_delivery_note); // indent if(!doc.order_type || ["Sales", "Shopping Cart"].indexOf(doc.order_type)!==-1) cur_frm.add_custom_button(__('Make ') + __('Material Request'), - this.make_material_request, "icon-ticket"); + this.make_material_request); // sales invoice if(flt(doc.per_billed, 2) < 100) { - cur_frm.add_custom_button(__('Make Invoice'), this.make_sales_invoice, - frappe.boot.doctype_icons["Sales Invoice"]); + cur_frm.add_custom_button(__('Make Invoice'), this.make_sales_invoice); } // stop if(flt(doc.per_delivered, 2) < 100 || doc.per_billed < 100) - cur_frm.add_custom_button(__('Stop'), cur_frm.cscript['Stop Sales Order'], - "icon-exclamation", "btn-default") + cur_frm.add_custom_button(__('Stop'), cur_frm.cscript['Stop Sales Order']) // maintenance if(flt(doc.per_delivered, 2) < 100 && ["Sales", "Shopping Cart"].indexOf(doc.order_type)===-1) { - cur_frm.add_custom_button(__('Make Maint. Visit'), - this.make_maintenance_visit, null, "btn-default"); - cur_frm.add_custom_button(__('Make Maint. Schedule'), - this.make_maintenance_schedule, null, "btn-default"); + cur_frm.add_custom_button(__('Make Maint. Visit'), this.make_maintenance_visit); + cur_frm.add_custom_button(__('Make Maint. Schedule'), this.make_maintenance_schedule); } } else { // un-stop - cur_frm.add_custom_button(__('Unstop'), cur_frm.cscript['Unstop Sales Order'], "icon-check"); + cur_frm.add_custom_button(__('Unstop'), cur_frm.cscript['Unstop Sales Order']); } } @@ -64,7 +60,7 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( company: cur_frm.doc.company } }) - }, "icon-download", "btn-default"); + }); } this.order_type(doc); diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index e8a772a216..d45fbba486 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -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", diff --git a/erpnext/selling/sales_common.js b/erpnext/selling/sales_common.js index f3cd8a7833..e8d8fd5f7a 100644 --- a/erpnext/selling/sales_common.js +++ b/erpnext/selling/sales_common.js @@ -210,7 +210,7 @@ erpnext.selling.SellingController = erpnext.TransactionController.extend({ // NOTE: // paid_amount and write_off_amount is only for POS Invoice // total_advance is only for non POS Invoice - if(this.frm.doc.doctype == "Sales Invoice" && this.frm.doc.docstatus==0) { + if(this.frm.doc.doctype == "Sales Invoice" && this.frm.doc.docstatus==0 && !this.frm.doc.is_return) { frappe.model.round_floats_in(this.frm.doc, ["base_grand_total", "total_advance", "write_off_amount", "paid_amount"]); var total_amount_to_pay = this.frm.doc.base_grand_total - this.frm.doc.write_off_amount diff --git a/erpnext/setup/doctype/features_setup/features_setup.json b/erpnext/setup/doctype/features_setup/features_setup.json index edc88e2800..f9bbad076e 100644 --- a/erpnext/setup/doctype/features_setup/features_setup.json +++ b/erpnext/setup/doctype/features_setup/features_setup.json @@ -18,7 +18,7 @@ "permlevel": 0 }, { - "description": "To track items in sales and purchase documents with batch nos
Preferred Industry: Chemicals etc", + "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 Point of Sale features", + "description": "To enable \"Point of Sale\" features", "fieldname": "fs_pos", "fieldtype": "Check", "label": "Point of Sale", "permlevel": 0 }, { - "description": "To enable Point of Sale view", + "description": "To enable \"Point of Sale\" view", "fieldname": "fs_pos_view", "fieldtype": "Check", "label": "POS View", @@ -237,4 +237,4 @@ "write": 1 } ] -} \ No newline at end of file +} diff --git a/erpnext/setup/doctype/sms_settings/sms_settings.py b/erpnext/setup/doctype/sms_settings/sms_settings.py index 1403ee5cbd..909986347f 100644 --- a/erpnext/setup/doctype/sms_settings/sms_settings.py +++ b/erpnext/setup/doctype/sms_settings/sms_settings.py @@ -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() diff --git a/erpnext/setup/page/setup_wizard/install_fixtures.py b/erpnext/setup/page/setup_wizard/install_fixtures.py index 629c06f6db..6265e4a36c 100644 --- a/erpnext/setup/page/setup_wizard/install_fixtures.py +++ b/erpnext/setup/page/setup_wizard/install_fixtures.py @@ -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) diff --git a/erpnext/setup/page/setup_wizard/sample_data.py b/erpnext/setup/page/setup_wizard/sample_data.py new file mode 100644 index 0000000000..d9f8343722 --- /dev/null +++ b/erpnext/setup/page/setup_wizard/sample_data.py @@ -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) diff --git a/erpnext/setup/page/setup_wizard/setup_wizard.js b/erpnext/setup/page/setup_wizard/setup_wizard.js index d521adab25..b38bd1c373 100644 --- a/erpnext/setup/page/setup_wizard/setup_wizard.js +++ b/erpnext/setup/page/setup_wizard/setup_wizard.js @@ -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"], +} diff --git a/erpnext/setup/page/setup_wizard/setup_wizard.py b/erpnext/setup/page/setup_wizard/setup_wizard.py index 4bb01d48d4..9ddd2dc03e 100644 --- a/erpnext/setup/page/setup_wizard/setup_wizard.py +++ b/erpnext/setup/page/setup_wizard/setup_wizard.py @@ -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() diff --git a/erpnext/setup/page/setup_wizard/test_setup_data.py b/erpnext/setup/page/setup_wizard/test_setup_data.py index 43fc2cf782..de54a1d16b 100644 --- a/erpnext/setup/page/setup_wizard/test_setup_data.py +++ b/erpnext/setup/page/setup_wizard/test_setup_data.py @@ -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 + } diff --git a/erpnext/startup/notifications.py b/erpnext/startup/notifications.py index 4190f2debd..d06537066e 100644 --- a/erpnext/startup/notifications.py +++ b/erpnext/startup/notifications.py @@ -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"}, diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.js b/erpnext/stock/doctype/delivery_note/delivery_note.js index 631009fa08..794d6fd811 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.js +++ b/erpnext/stock/doctype/delivery_note/delivery_note.js @@ -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(); diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json index 72a72278a0..0ca85c9e59 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.json +++ b/erpnext/stock/doctype/delivery_note/delivery_note.json @@ -29,7 +29,7 @@ "no_copy": 1, "oldfieldname": "naming_series", "oldfieldtype": "Select", - "options": "DN-", + "options": "DN-\nDN-RET-", "permlevel": 0, "print_hide": 1, "read_only": 0, @@ -205,6 +205,28 @@ "read_only": 1, "width": "100px" }, + { + "fieldname": "is_return", + "fieldtype": "Check", + "label": "Is Return", + "no_copy": 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", diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 90a8a6c720..e3058822f6 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -84,7 +84,7 @@ class DeliveryNote(SellingController): def so_required(self): """check in manage account if sales order required or not""" - if frappe.db.get_value("Selling Settings", None, 'so_required') == 'Yes': + if not self.is_return and frappe.db.get_value("Selling Settings", None, 'so_required') == 'Yes': for d in self.get('items'): if not d.against_sales_order: frappe.throw(_("Sales Order required for Item {0}").format(d.item_code)) @@ -175,17 +175,15 @@ class DeliveryNote(SellingController): # Check for Approving Authority frappe.get_doc('Authorization Control').validate_approving_authority(self.doctype, self.company, self.base_grand_total, self) - # update delivered qty in sales order - self.update_prevdoc_status() + if not self.is_return: + # update delivered qty in sales order + self.update_prevdoc_status() - self.check_credit_limit() + self.check_credit_limit() - # create stock ledger entry self.update_stock_ledger() - self.make_gl_entries() - # set DN status frappe.db.set(self, 'status', 'Submitted') @@ -193,7 +191,8 @@ class DeliveryNote(SellingController): self.check_stop_sales_order("against_sales_order") self.check_next_docstatus() - self.update_prevdoc_status() + if not self.is_return: + self.update_prevdoc_status() self.update_stock_ledger() @@ -251,9 +250,14 @@ class DeliveryNote(SellingController): if frappe.db.get_value("Item", d.item_code, "is_stock_item") == "Yes" \ and d.warehouse and flt(d['qty']): self.update_reserved_qty(d) - + + incoming_rate = 0 + if cint(self.is_return) and self.return_against and self.docstatus==1: + incoming_rate = self.get_incoming_rate_for_sales_return(d.item_code, self.return_against) + sl_entries.append(self.get_sl_entries(d, { "actual_qty": -1*flt(d['qty']), + "incoming_rate": incoming_rate })) self.make_sl_entries(sl_entries) @@ -387,3 +391,9 @@ def make_packing_slip(source_name, target_doc=None): }, target_doc) return doclist + + +@frappe.whitelist() +def make_sales_return(source_name, target_doc=None): + from erpnext.controllers.sales_and_purchase_return import make_return_doc + return make_return_doc("Delivery Note", source_name, target_doc) \ No newline at end of file diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 978e968c4a..eb80014e59 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -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", diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index 58b1adb8db..3bd5657d59 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -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"); + } } }); diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index a2e0ade50c..d3d8e9c6a1 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -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"): diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js index fe41b4fb93..13e104e15a 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js @@ -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(); diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json index 6e344b61e0..8e32281d59 100755 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json @@ -21,13 +21,14 @@ "width": "50%" }, { + "default": "", "fieldname": "naming_series", "fieldtype": "Select", "label": "Series", "no_copy": 1, "oldfieldname": "naming_series", "oldfieldtype": "Select", - "options": "PREC-", + "options": "PREC-\nPREC-RET-", "permlevel": 0, "print_hide": 1, "reqd": 1 @@ -130,6 +131,28 @@ "search_index": 0, "width": "100px" }, + { + "fieldname": "is_return", + "fieldtype": "Check", + "label": "Is Return", + "no_copy": 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", diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index e78288908d..034eb07830 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -44,6 +44,7 @@ class PurchaseReceipt(BuyingController): self.set_status() self.po_required() self.validate_with_previous_doc() + self.validate_purchase_return() self.validate_rejected_warehouse() self.validate_accepted_rejected_qty() self.validate_inspection() @@ -60,12 +61,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) \ No newline at end of file diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 141bcd4d0d..343d51acc7 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -6,7 +6,7 @@ from __future__ import unicode_literals import unittest import frappe import frappe.defaults -from frappe.utils import cint, flt +from frappe.utils import cint, flt, cstr class TestPurchaseReceipt(unittest.TestCase): def test_make_purchase_invoice(self): @@ -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", diff --git a/erpnext/stock/doctype/serial_no/serial_no.json b/erpnext/stock/doctype/serial_no/serial_no.json index 8ffe7ed9dd..97754e9d1d 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.json +++ b/erpnext/stock/doctype/serial_no/serial_no.json @@ -244,7 +244,7 @@ "in_filter": 1, "label": "Delivery Document Type", "no_copy": 1, - "options": "\nDelivery Note\nSales Invoice\nStock Entry", + "options": "\nDelivery Note\nSales Invoice\nStock Entry\nPurchase Receipt", "permlevel": 0, "read_only": 1 }, @@ -418,7 +418,7 @@ "icon": "icon-barcode", "idx": 1, "in_create": 0, - "modified": "2015-07-13 05:28:27.961178", + "modified": "2015-07-24 03:55:29.946944", "modified_by": "Administrator", "module": "Stock", "name": "Serial No", diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py index bac544194a..6b5054b902 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.py +++ b/erpnext/stock/doctype/serial_no/serial_no.py @@ -33,10 +33,7 @@ class SerialNo(StockController): self.validate_warehouse() self.validate_item() self.on_stock_ledger_entry() - - valid_purchase_document_type = ("Purchase Receipt", "Stock Entry", "Serial No") - self.validate_value("purchase_document_type", "in", valid_purchase_document_type) - + def set_maintenance_status(self): if not self.warranty_expiry_date and not self.amc_expiry_date: self.maintenance_status = None @@ -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 diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 6958ea03ca..8526117223 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -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() { diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.json b/erpnext/stock/doctype/stock_entry/stock_entry.json index 06dec5808b..3c39d42ff6 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.json +++ b/erpnext/stock/doctype/stock_entry/stock_entry.json @@ -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", diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index fb1ec3d17b..1b01f3aa68 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -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 diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 70d6413ce1..d283c3dd14 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -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" diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json index 780bcc9c33..bb6f4098d7 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json +++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json @@ -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", diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js index c0ae213b87..f833a251be 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js @@ -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()] + ] + } +} \ No newline at end of file diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 413f820043..efa6a8a25d 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -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 diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index eaa82dd23f..dde338611b 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -107,8 +107,6 @@ def create_stock_reconciliation(**args): "valuation_rate": args.rate }) - sr.insert() - sr.submit() return sr diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.py b/erpnext/stock/report/stock_ledger/stock_ledger.py index 0651ae8961..c2c6d1ae7f 100644 --- a/erpnext/stock/report/stock_ledger/stock_ledger.py +++ b/erpnext/stock/report/stock_ledger/stock_ledger.py @@ -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 \ No newline at end of file diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 954a03b3bf..a5deb3075b 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -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: diff --git a/erpnext/templates/includes/macros.html b/erpnext/templates/includes/macros.html index 0967e97138..6748a5ebd2 100644 --- a/erpnext/templates/includes/macros.html +++ b/erpnext/templates/includes/macros.html @@ -1,6 +1,6 @@ {% macro product_image_square(website_image, css_class="") %}
+ {% if website_image -%} style="background-image: url('{{ frappe.utils.quoted(website_image) | abs_url }}');" {%- endif %}> {% if not website_image -%}{%- endif %}
{% endmacro %} @@ -8,7 +8,7 @@ {% macro product_image(website_image, css_class="") %}
{% if website_image -%} - + {%- else -%} {%- endif %} diff --git a/erpnext/utilities/doctype/sms_log/sms_log.json b/erpnext/utilities/doctype/sms_log/sms_log.json index e3c7741676..ba88c622c3 100644 --- a/erpnext/utilities/doctype/sms_log/sms_log.json +++ b/erpnext/utilities/doctype/sms_log/sms_log.json @@ -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", diff --git a/erpnext/utilities/transaction_base.py b/erpnext/utilities/transaction_base.py index 50b0319d3f..6c9b9a428d 100644 --- a/erpnext/utilities/transaction_base.py +++ b/erpnext/utilities/transaction_base.py @@ -3,12 +3,12 @@ from __future__ import unicode_literals import frappe +import frappe.share from frappe import _ from frappe.utils import cstr, now_datetime, cint, flt -import frappe.share - from erpnext.controllers.status_updater import StatusUpdater +class UOMMustBeIntegerError(frappe.ValidationError): pass class TransactionBase(StatusUpdater): def load_notification_message(self): @@ -109,8 +109,6 @@ def delete_events(ref_type, ref_name): frappe.delete_doc("Event", frappe.db.sql_list("""select name from `tabEvent` where ref_type=%s and ref_name=%s""", (ref_type, ref_name)), for_reload=True) -class UOMMustBeIntegerError(frappe.ValidationError): pass - def validate_uom_is_integer(doc, uom_field, qty_fields, child_dt=None): if isinstance(qty_fields, basestring): qty_fields = [qty_fields] diff --git a/setup.py b/setup.py index 9cb39e1fe5..020dfa3c43 100644 --- a/setup.py +++ b/setup.py @@ -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()