From 2ce39cf7705e2f7840ec666793d89220d3a4011b Mon Sep 17 00:00:00 2001 From: Anand Doshi Date: Mon, 7 Apr 2014 18:51:58 +0530 Subject: [PATCH] Fixed Stock Entry Test Cases frappe/frappe#478 --- .../journal_voucher/journal_voucher.py | 182 ++++---- .../purchase_invoice/purchase_invoice.py | 160 +++---- erpnext/controllers/stock_controller.py | 150 +++--- .../stock/doctype/stock_entry/stock_entry.py | 436 +++++++++--------- .../doctype/stock_entry/test_stock_entry.py | 43 +- .../stock_ledger_entry/stock_ledger_entry.py | 14 +- .../stock/doctype/warehouse/test_records.json | 30 +- erpnext/stock/stock_ledger.py | 12 +- erpnext/stock/utils.py | 119 +++-- 9 files changed, 573 insertions(+), 573 deletions(-) diff --git a/erpnext/accounts/doctype/journal_voucher/journal_voucher.py b/erpnext/accounts/doctype/journal_voucher/journal_voucher.py index 0ba0208ecf..f11589bdf1 100644 --- a/erpnext/accounts/doctype/journal_voucher/journal_voucher.py +++ b/erpnext/accounts/doctype/journal_voucher/journal_voucher.py @@ -21,22 +21,22 @@ class JournalVoucher(AccountsController): def validate(self): if not self.is_opening: self.is_opening='No' - + self.clearance_date = None - + super(JournalVoucher, self).validate_date_with_fiscal_year() - + self.validate_debit_credit() self.validate_cheque_info() self.validate_entries_for_advance() self.validate_against_jv() - + self.set_against_account() self.create_remarks() self.set_aging_date() self.set_print_format_fields() - + def on_submit(self): if self.voucher_type in ['Bank Voucher', 'Contra Voucher', 'Journal Entry']: self.check_credit_days() @@ -46,9 +46,9 @@ class JournalVoucher(AccountsController): def on_cancel(self): from erpnext.accounts.utils import remove_against_link_from_jv remove_against_link_from_jv(self.doctype, self.name, "against_jv") - + self.make_gl_entries(1) - + def on_trash(self): pass #if self.amended_from: @@ -57,7 +57,7 @@ class JournalVoucher(AccountsController): def validate_debit_credit(self): for d in self.get('entries'): if d.debit and d.credit: - msgprint("You cannot credit and debit same account at the same time.", + msgprint("You cannot credit and debit same account at the same time.", raise_exception=1) def validate_cheque_info(self): @@ -65,7 +65,7 @@ class JournalVoucher(AccountsController): if not self.cheque_no or not self.cheque_date: msgprint("Reference No & Reference Date is required for %s" % self.voucher_type, raise_exception=1) - + if self.cheque_date and not self.cheque_no: msgprint("Reference No is mandatory if you entered Reference Date", raise_exception=1) @@ -85,11 +85,11 @@ class JournalVoucher(AccountsController): if d.against_jv == self.name: msgprint("You can not enter current voucher in 'Against JV' column", raise_exception=1) - elif not frappe.db.sql("""select name from `tabJournal Voucher Detail` - where account = %s and docstatus = 1 and parent = %s""", + elif not frappe.db.sql("""select name from `tabJournal Voucher Detail` + where account = %s and docstatus = 1 and parent = %s""", (d.account, d.against_jv)): msgprint("Against JV: %s is not valid." % d.against_jv, raise_exception=1) - + def set_against_account(self): # Debit = Credit debit, credit = 0.0, 0.0 @@ -104,9 +104,9 @@ class JournalVoucher(AccountsController): self.total_credit = credit if abs(self.total_debit-self.total_credit) > 0.001: - msgprint("Debit must be equal to Credit. The difference is %s" % + msgprint("Debit must be equal to Credit. The difference is %s" % (self.total_debit-self.total_credit), raise_exception=1) - + # update against account for d in self.get('entries'): if flt(d.debit) > 0: d.against_account = ', '.join(credit_list) @@ -114,28 +114,28 @@ class JournalVoucher(AccountsController): def create_remarks(self): r = [] - if self.cheque_no : + if self.cheque_no: if self.cheque_date: - r.append('Via Reference #%s dated %s' % + r.append('Via Reference #%s dated %s' % (self.cheque_no, formatdate(self.cheque_date))) else : msgprint("Please enter Reference date", raise_exception=1) - + for d in self.get('entries'): if d.against_invoice and d.credit: currency = frappe.db.get_value("Sales Invoice", d.against_invoice, "currency") - r.append('%s %s against Invoice: %s' % + r.append('%s %s against Invoice: %s' % (cstr(currency), fmt_money(flt(d.credit)), d.against_invoice)) - + if d.against_voucher and d.debit: - bill_no = frappe.db.sql("""select bill_no, bill_date, currency + bill_no = frappe.db.sql("""select bill_no, bill_date, currency from `tabPurchase Invoice` where name=%s""", d.against_voucher) if bill_no and bill_no[0][0] and bill_no[0][0].lower().strip() \ not in ['na', 'not applicable', 'none']: - r.append('%s %s against Bill %s dated %s' % - (cstr(bill_no[0][2]), fmt_money(flt(d.debit)), bill_no[0][0], + r.append('%s %s against Bill %s dated %s' % + (cstr(bill_no[0][2]), fmt_money(flt(d.debit)), bill_no[0][0], bill_no[0][1] and formatdate(bill_no[0][1].strftime('%Y-%m-%d')) or '')) - + if self.user_remark: r.append("User Remark : %s"%self.user_remark) @@ -157,25 +157,25 @@ class JournalVoucher(AccountsController): break # If customer/supplier account, aging date is mandatory - if exists and not self.aging_date: + if exists and not self.aging_date: msgprint("Aging Date is mandatory for opening entry", raise_exception=1) else: self.aging_date = self.posting_date def set_print_format_fields(self): for d in self.get('entries'): - account_type, master_type = frappe.db.get_value("Account", d.account, + account_type, master_type = frappe.db.get_value("Account", d.account, ["account_type", "master_type"]) - + if master_type in ['Supplier', 'Customer']: if not self.pay_to_recd_from: - self.pay_to_recd_from = frappe.db.get_value(master_type, - ' - '.join(d.account.split(' - ')[:-1]), + self.pay_to_recd_from = frappe.db.get_value(master_type, + ' - '.join(d.account.split(' - ')[:-1]), master_type == 'Customer' and 'customer_name' or 'supplier_name') - + if account_type in ['Bank', 'Cash']: company_currency = get_company_currency(self.company) - amt = flt(d.debit) and d.debit or d.credit + amt = flt(d.debit) and d.debit or d.credit self.total_amount = company_currency + ' ' + cstr(amt) from frappe.utils import money_in_words self.total_amount_in_words = money_in_words(amt, company_currency) @@ -184,29 +184,29 @@ class JournalVoucher(AccountsController): date_diff = 0 if self.cheque_date: date_diff = (getdate(self.cheque_date)-getdate(self.posting_date)).days - + if date_diff <= 0: return - + # Get List of Customer Account - acc_list = filter(lambda d: frappe.db.get_value("Account", d.account, + acc_list = filter(lambda d: frappe.db.get_value("Account", d.account, "master_type")=='Customer', self.get('entries')) - + for d in acc_list: credit_days = self.get_credit_days_for(d.account) # Check credit days if credit_days > 0 and not self.get_authorized_user() and cint(date_diff) > credit_days: msgprint("Credit Not Allowed: Cannot allow a check that is dated \ more than %s days after the posting date" % credit_days, raise_exception=1) - + def get_credit_days_for(self, ac): if not self.credit_days_for.has_key(ac): self.credit_days_for[ac] = cint(frappe.db.get_value("Account", ac, "credit_days")) if not self.credit_days_for[ac]: if self.credit_days_global==-1: - self.credit_days_global = cint(frappe.db.get_value("Company", + self.credit_days_global = cint(frappe.db.get_value("Company", self.company, "credit_days")) - + return self.credit_days_global else: return self.credit_days_for[ac] @@ -216,33 +216,33 @@ class JournalVoucher(AccountsController): self.is_approving_authority = 0 # Fetch credit controller role - approving_authority = frappe.db.get_value("Global Defaults", None, + approving_authority = frappe.db.get_value("Global Defaults", None, "credit_controller") - + # Check logged-in user is authorized if approving_authority in frappe.user.get_roles(): self.is_approving_authority = 1 - + return self.is_approving_authority def check_account_against_entries(self): for d in self.get("entries"): - if d.against_invoice and frappe.db.get_value("Sales Invoice", + if d.against_invoice and frappe.db.get_value("Sales Invoice", d.against_invoice, "debit_to") != d.account: frappe.throw(_("Row #") + cstr(d.idx) + ": " + _("Account is not matching with Debit To account of Sales Invoice")) - - if d.against_voucher and frappe.db.get_value("Purchase Invoice", + + if d.against_voucher and frappe.db.get_value("Purchase Invoice", d.against_voucher, "credit_to") != d.account: frappe.throw(_("Row #") + cstr(d.idx) + ": " + _("Account is not matching with Credit To account of Purchase Invoice")) def make_gl_entries(self, cancel=0, adv_adj=0): from erpnext.accounts.general_ledger import make_gl_entries - + if not cancel: self.check_account_against_entries() - + gl_map = [] for d in self.get("entries"): if d.debit or d.credit: @@ -252,8 +252,8 @@ class JournalVoucher(AccountsController): "against": d.against_account, "debit": d.debit, "credit": d.credit, - "against_voucher_type": ((d.against_voucher and "Purchase Invoice") - or (d.against_invoice and "Sales Invoice") + "against_voucher_type": ((d.against_voucher and "Purchase Invoice") + or (d.against_invoice and "Sales Invoice") or (d.against_jv and "Journal Voucher")), "against_voucher": d.against_voucher or d.against_invoice or d.against_jv, "remarks": self.remark, @@ -262,10 +262,10 @@ class JournalVoucher(AccountsController): ) if gl_map: make_gl_entries(gl_map, cancel=cancel, adv_adj=adv_adj) - + def check_credit_limit(self): for d in self.get("entries"): - master_type, master_name = frappe.db.get_value("Account", d.account, + master_type, master_name = frappe.db.get_value("Account", d.account, ["master_type", "master_name"]) if master_type == "Customer" and master_name: super(JournalVoucher, self).check_credit_limit(d.account) @@ -276,7 +276,7 @@ class JournalVoucher(AccountsController): else: flag, self.total_debit, self.total_credit = 0, 0, 0 diff = flt(self.difference, 2) - + # If any row without amount, set the diff on that row for d in self.get('entries'): if not d.credit and not d.debit and diff != 0: @@ -285,7 +285,7 @@ class JournalVoucher(AccountsController): elif diff<0: d.debit = diff flag = 1 - + # Set the diff in a new row if flag == 0 and diff != 0: jd = self.append('entries', {}) @@ -293,7 +293,7 @@ class JournalVoucher(AccountsController): jd.credit = abs(diff) elif diff<0: jd.debit = abs(diff) - + # Set the total debit, total credit and difference for d in self.get('entries'): self.total_debit += flt(d.debit, 2) @@ -326,12 +326,12 @@ class JournalVoucher(AccountsController): cond = (flt(self.write_off_amount) > 0) and \ ' and outstanding_amount <= '+ self.write_off_amount or '' if self.write_off_based_on == 'Accounts Receivable': - return frappe.db.sql("""select name, debit_to, outstanding_amount - from `tabSales Invoice` where docstatus = 1 and company = %s + return frappe.db.sql("""select name, debit_to, outstanding_amount + from `tabSales Invoice` where docstatus = 1 and company = %s and outstanding_amount > 0 %s""" % ('%s', cond), self.company) elif self.write_off_based_on == 'Accounts Payable': - return frappe.db.sql("""select name, credit_to, outstanding_amount - from `tabPurchase Invoice` where docstatus = 1 and company = %s + return frappe.db.sql("""select name, credit_to, outstanding_amount + from `tabPurchase Invoice` where docstatus = 1 and company = %s and outstanding_amount > 0 %s""" % ('%s', cond), self.company) @frappe.whitelist() @@ -344,7 +344,7 @@ def get_default_bank_cash_account(company, voucher_type): "account": account, "balance": get_balance_on(account) } - + @frappe.whitelist() def get_payment_entry_from_sales_invoice(sales_invoice): from erpnext.accounts.utils import get_balance_on @@ -360,7 +360,7 @@ def get_payment_entry_from_sales_invoice(sales_invoice): # debit bank jv.get("entries")[1].debit = si.outstanding_amount - + return jv.as_dict() @frappe.whitelist() @@ -369,7 +369,7 @@ def get_payment_entry_from_purchase_invoice(purchase_invoice): pi = frappe.get_doc("Purchase Invoice", purchase_invoice) jv = get_payment_entry(pi) jv.remark = 'Payment against Purchase Invoice {0}. {1}'.format(pi.name, pi.remarks) - + # credit supplier jv.get("entries")[0].account = pi.credit_to jv.get("entries")[0].balance = get_balance_on(pi.credit_to) @@ -378,12 +378,12 @@ def get_payment_entry_from_purchase_invoice(purchase_invoice): # credit bank jv.get("entries")[1].credit = pi.outstanding_amount - + return jv.as_dict() def get_payment_entry(doc): bank_account = get_default_bank_cash_account(doc.company, "Bank Voucher") - + jv = frappe.new_doc('Journal Voucher') jv.voucher_type = 'Bank Voucher' @@ -396,63 +396,63 @@ def get_payment_entry(doc): if bank_account: d2.account = bank_account["account"] d2.balance = bank_account["balance"] - + return jv - + @frappe.whitelist() def get_opening_accounts(company): """get all balance sheet accounts for opening entry""" from erpnext.accounts.utils import get_balance_on - accounts = frappe.db.sql_list("""select name from tabAccount + accounts = frappe.db.sql_list("""select name from tabAccount where group_or_ledger='Ledger' and report_type='Profit and Loss' and company=%s""", company) - + return [{"account": a, "balance": get_balance_on(a)} for a in accounts] - + def get_against_purchase_invoice(doctype, txt, searchfield, start, page_len, filters): - return frappe.db.sql("""select name, credit_to, outstanding_amount, bill_no, bill_date - from `tabPurchase Invoice` where credit_to = %s and docstatus = 1 + return frappe.db.sql("""select name, credit_to, outstanding_amount, bill_no, bill_date + from `tabPurchase Invoice` where credit_to = %s and docstatus = 1 and outstanding_amount > 0 and %s like %s order by name desc limit %s, %s""" % - ("%s", searchfield, "%s", "%s", "%s"), - (filters["account"], "%%%s%%" % txt, start, page_len)) - -def get_against_sales_invoice(doctype, txt, searchfield, start, page_len, filters): - return frappe.db.sql("""select name, debit_to, outstanding_amount - from `tabSales Invoice` where debit_to = %s and docstatus = 1 - and outstanding_amount > 0 and `%s` like %s order by name desc limit %s, %s""" % - ("%s", searchfield, "%s", "%s", "%s"), - (filters["account"], "%%%s%%" % txt, start, page_len)) - -def get_against_jv(doctype, txt, searchfield, start, page_len, filters): - return frappe.db.sql("""select jv.name, jv.posting_date, jv.user_remark - from `tabJournal Voucher` jv, `tabJournal Voucher Detail` jv_detail - where jv_detail.parent = jv.name and jv_detail.account = %s and jv.docstatus = 1 - and jv.%s like %s order by jv.name desc limit %s, %s""" % - ("%s", searchfield, "%s", "%s", "%s"), + ("%s", searchfield, "%s", "%s", "%s"), (filters["account"], "%%%s%%" % txt, start, page_len)) -@frappe.whitelist() +def get_against_sales_invoice(doctype, txt, searchfield, start, page_len, filters): + return frappe.db.sql("""select name, debit_to, outstanding_amount + from `tabSales Invoice` where debit_to = %s and docstatus = 1 + and outstanding_amount > 0 and `%s` like %s order by name desc limit %s, %s""" % + ("%s", searchfield, "%s", "%s", "%s"), + (filters["account"], "%%%s%%" % txt, start, page_len)) + +def get_against_jv(doctype, txt, searchfield, start, page_len, filters): + return frappe.db.sql("""select jv.name, jv.posting_date, jv.user_remark + from `tabJournal Voucher` jv, `tabJournal Voucher Detail` jv_detail + where jv_detail.parent = jv.name and jv_detail.account = %s and jv.docstatus = 1 + and jv.%s like %s order by jv.name desc limit %s, %s""" % + ("%s", searchfield, "%s", "%s", "%s"), + (filters["account"], "%%%s%%" % txt, start, page_len)) + +@frappe.whitelist() def get_outstanding(args): args = eval(args) if args.get("doctype") == "Journal Voucher" and args.get("account"): against_jv_amount = frappe.db.sql(""" - select sum(ifnull(debit, 0)) - sum(ifnull(credit, 0)) - from `tabJournal Voucher Detail` where parent=%s and account=%s + select sum(ifnull(debit, 0)) - sum(ifnull(credit, 0)) + from `tabJournal Voucher Detail` where parent=%s and account=%s and ifnull(against_invoice, '')='' and ifnull(against_voucher, '')='' and ifnull(against_jv, '')=''""", (args['docname'], args['account'])) - + against_jv_amount = flt(against_jv_amount[0][0]) if against_jv_amount else 0 if against_jv_amount > 0: return {"credit": against_jv_amount} else: return {"debit": -1* against_jv_amount} - + elif args.get("doctype") == "Sales Invoice": return { - "credit": flt(frappe.db.get_value("Sales Invoice", args["docname"], + "credit": flt(frappe.db.get_value("Sales Invoice", args["docname"], "outstanding_amount")) } elif args.get("doctype") == "Purchase Invoice": return { - "debit": flt(frappe.db.get_value("Purchase Invoice", args["docname"], + "debit": flt(frappe.db.get_value("Purchase Invoice", args["docname"], "outstanding_amount")) } diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 88df6bdce5..e178d03b57 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -28,13 +28,13 @@ class PurchaseInvoice(BuyingController): 'source_field': 'amount', 'percent_join_field': 'purchase_order', }] - + def validate(self): if not self.is_opening: self.is_opening = 'No' - + super(PurchaseInvoice, self).validate() - + self.po_required() self.pr_required() self.check_active_purchase_items() @@ -51,22 +51,22 @@ class PurchaseInvoice(BuyingController): self.validate_write_off_account() self.update_raw_material_cost() self.update_valuation_rate("entries") - self.validate_multiple_billing("Purchase Receipt", "pr_detail", "amount", + self.validate_multiple_billing("Purchase Receipt", "pr_detail", "amount", "purchase_receipt_details") - + def set_missing_values(self, for_validate=False): if not self.credit_to: self.credit_to = get_party_account(self.company, self.supplier, "Supplier") if not self.due_date: self.due_date = get_due_date(self.posting_date, self.supplier, "Supplier", self.credit_to, self.company) - + super(PurchaseInvoice, self).set_missing_values(for_validate) - + def get_advances(self): - super(PurchaseInvoice, self).get_advances(self.credit_to, + super(PurchaseInvoice, self).get_advances(self.credit_to, "Purchase Invoice Advance", "advance_allocation_details", "debit") - + def check_active_purchase_items(self): for d in self.get('entries'): if d.item_code: # extra condn coz item_code is not mandatory in PV @@ -77,29 +77,29 @@ class PurchaseInvoice(BuyingController): if not valid_item[0][1] == 'Yes': msgprint("Item : '%s' is not Purchase Item"%(d.item_code)) raise Exception - + def check_conversion_rate(self): - default_currency = get_company_currency(self.company) + default_currency = get_company_currency(self.company) if not default_currency: msgprint('Message: Please enter default currency in Company Master') raise Exception if (self.currency == default_currency and flt(self.conversion_rate) != 1.00) or not self.conversion_rate or (self.currency != default_currency and flt(self.conversion_rate) == 1.00): msgprint("Message: Please Enter Appropriate Conversion Rate.") - raise Exception - + raise Exception + def validate_bill_no(self): if self.bill_no and self.bill_no.lower().strip() \ not in ['na', 'not applicable', 'none']: - b_no = frappe.db.sql("""select bill_no, name, ifnull(is_opening,'') from `tabPurchase Invoice` - where bill_no = %s and credit_to = %s and docstatus = 1 and name != %s""", + b_no = frappe.db.sql("""select bill_no, name, ifnull(is_opening,'') from `tabPurchase Invoice` + where bill_no = %s and credit_to = %s and docstatus = 1 and name != %s""", (self.bill_no, self.credit_to, self.name)) if b_no and cstr(b_no[0][2]) == cstr(self.is_opening): msgprint("Please check you have already booked expense against Bill No. %s \ - in Purchase Invoice %s" % (cstr(b_no[0][0]), cstr(b_no[0][1])), + in Purchase Invoice %s" % (cstr(b_no[0][0]), cstr(b_no[0][1])), raise_exception=1) - + if not self.remarks and self.bill_date: - self.remarks = (self.remarks or '') + "\n" + ("Against Bill %s dated %s" + self.remarks = (self.remarks or '') + "\n" + ("Against Bill %s dated %s" % (self.bill_no, formatdate(self.bill_date))) if not self.remarks: @@ -108,28 +108,28 @@ class PurchaseInvoice(BuyingController): def validate_credit_acc(self): if frappe.db.get_value("Account", self.credit_to, "report_type") != "Balance Sheet": frappe.throw(_("Account must be a balance sheet account")) - + # Validate Acc Head of Supplier and Credit To Account entered # ------------------------------------------------------------ - def check_for_acc_head_of_supplier(self): + def check_for_acc_head_of_supplier(self): if self.supplier and self.credit_to: acc_head = frappe.db.sql("select master_name from `tabAccount` where name = %s", self.credit_to) - + if (acc_head and cstr(acc_head[0][0]) != cstr(self.supplier)) or (not acc_head and (self.credit_to != cstr(self.supplier) + " - " + self.company_abbr)): msgprint("Credit To: %s do not match with Supplier: %s for Company: %s.\n If both correctly entered, please select Master Type and Master Name in account master." %(self.credit_to,self.supplier,self.company), raise_exception=1) - + # Check for Stopped PO # --------------------- def check_for_stopped_status(self): check_list = [] for d in self.get('entries'): if d.purchase_order and not d.purchase_order in check_list and not d.purchase_receipt: - check_list.append(d.purhcase_order) + check_list.append(d.purchase_order) stopped = frappe.db.sql("select name from `tabPurchase Order` where status = 'Stopped' and name = %s", d.purchase_order) if stopped: msgprint("One cannot do any transaction against 'Purchase Order' : %s, it's status is 'Stopped'" % (d.purhcase_order)) raise Exception - + def validate_with_previous_doc(self): super(PurchaseInvoice, self).validate_with_previous_doc(self.tname, { "Purchase Order": { @@ -152,7 +152,7 @@ class PurchaseInvoice(BuyingController): "is_child_table": True } }) - + if cint(frappe.defaults.get_global_default('maintain_same_rate')): super(PurchaseInvoice, self).validate_with_previous_doc(self.tname, { "Purchase Order Item": { @@ -167,21 +167,21 @@ class PurchaseInvoice(BuyingController): "is_child_table": True } }) - - + + def set_aging_date(self): if self.is_opening != 'Yes': self.aging_date = self.posting_date elif not self.aging_date: msgprint("Aging Date is mandatory for opening entry") raise Exception - + def set_against_expense_account(self): auto_accounting_for_stock = cint(frappe.defaults.get_global_default("auto_accounting_for_stock")) if auto_accounting_for_stock: stock_not_billed_account = self.get_company_default("stock_received_but_not_billed") - + against_accounts = [] stock_items = self.get_stock_items() for item in self.get("entries"): @@ -191,18 +191,18 @@ class PurchaseInvoice(BuyingController): # Stock Received But Not Billed for a stock item item.expense_account = stock_not_billed_account item.cost_center = None - + if stock_not_billed_account not in against_accounts: against_accounts.append(stock_not_billed_account) - + elif not item.expense_account: - msgprint(_("Expense account is mandatory for item") + ": " + + msgprint(_("Expense account is mandatory for item") + ": " + (item.item_code or item.item_name), raise_exception=1) - + elif item.expense_account not in against_accounts: # if no auto_accounting_for_stock or not a stock item against_accounts.append(item.expense_account) - + self.against_expense_account = ",".join(against_accounts) def po_required(self): @@ -233,8 +233,8 @@ class PurchaseInvoice(BuyingController): submitted = frappe.db.sql("select name from `tabPurchase Receipt` where docstatus = 1 and name = %s", d.purchase_receipt) if not submitted: frappe.throw("Purchase Receipt : "+ cstr(d.purchase_receipt) +" is not submitted") - - + + def update_against_document_in_jv(self): """ Links invoice and advance voucher: @@ -242,33 +242,33 @@ class PurchaseInvoice(BuyingController): 2. split into multiple rows if partially adjusted, assign against voucher 3. submit advance voucher """ - + lst = [] for d in self.get('advance_allocation_details'): if flt(d.allocated_amount) > 0: args = { - 'voucher_no' : d.journal_voucher, - 'voucher_detail_no' : d.jv_detail_no, - 'against_voucher_type' : 'Purchase Invoice', + 'voucher_no' : d.journal_voucher, + 'voucher_detail_no' : d.jv_detail_no, + 'against_voucher_type' : 'Purchase Invoice', 'against_voucher' : self.name, - 'account' : self.credit_to, - 'is_advance' : 'Yes', - 'dr_or_cr' : 'debit', + 'account' : self.credit_to, + 'is_advance' : 'Yes', + 'dr_or_cr' : 'debit', 'unadjusted_amt' : flt(d.advance_amount), 'allocated_amt' : flt(d.allocated_amount) } lst.append(args) - + if lst: from erpnext.accounts.utils import reconcile_against_document reconcile_against_document(lst) def on_submit(self): self.check_prev_docstatus() - - frappe.get_doc('Authorization Control').validate_approving_authority(self.doctype, + + frappe.get_doc('Authorization Control').validate_approving_authority(self.doctype, self.company, self.grand_total) - + # this sequence because outstanding may get -negative self.make_gl_entries() self.update_against_document_in_jv() @@ -278,9 +278,9 @@ class PurchaseInvoice(BuyingController): def make_gl_entries(self): auto_accounting_for_stock = \ cint(frappe.defaults.get_global_default("auto_accounting_for_stock")) - + gl_entries = [] - + # parent's gl entry if self.grand_total: gl_entries.append( @@ -293,7 +293,7 @@ class PurchaseInvoice(BuyingController): "against_voucher_type": self.doctype, }) ) - + # tax table gl entries valuation_tax = {} for tax in self.get("other_charges"): @@ -308,31 +308,31 @@ class PurchaseInvoice(BuyingController): "cost_center": tax.cost_center }) ) - + # accumulate valuation tax if tax.category in ("Valuation", "Valuation and Total") and flt(tax.tax_amount): if auto_accounting_for_stock and not tax.cost_center: frappe.throw(_("Row %(row)s: Cost Center is mandatory \ - if tax/charges category is Valuation or Valuation and Total" % + if tax/charges category is Valuation or Valuation and Total" % {"row": tax.idx})) valuation_tax.setdefault(tax.cost_center, 0) valuation_tax[tax.cost_center] += \ (tax.add_deduct_tax == "Add" and 1 or -1) * flt(tax.tax_amount) - + # item gl entries stock_item_and_auto_accounting_for_stock = False stock_items = self.get_stock_items() for item in self.get("entries"): if auto_accounting_for_stock and item.item_code in stock_items: if flt(item.valuation_rate): - # if auto inventory accounting enabled and stock item, + # if auto inventory accounting enabled and stock item, # then do stock related gl entries # expense will be booked in sales invoice stock_item_and_auto_accounting_for_stock = True - - valuation_amt = flt(item.base_amount + item.item_tax_amount + item.rm_supp_cost, + + valuation_amt = flt(item.base_amount + item.item_tax_amount + item.rm_supp_cost, self.precision("base_amount", item)) - + gl_entries.append( self.get_gl_dict({ "account": item.expense_account, @@ -341,7 +341,7 @@ class PurchaseInvoice(BuyingController): "remarks": self.remarks or "Accounting Entry for Stock" }) ) - + elif flt(item.base_amount): # if not a stock item or auto inventory accounting disabled, book the expense gl_entries.append( @@ -353,13 +353,13 @@ class PurchaseInvoice(BuyingController): "cost_center": item.cost_center }) ) - + if stock_item_and_auto_accounting_for_stock and valuation_tax: # credit valuation tax amount in "Expenses Included In Valuation" # this will balance out valuation amount included in cost of goods sold expenses_included_in_valuation = \ self.get_company_default("expenses_included_in_valuation") - + for cost_center, amount in valuation_tax.items(): gl_entries.append( self.get_gl_dict({ @@ -370,8 +370,8 @@ class PurchaseInvoice(BuyingController): "remarks": self.remarks or "Accounting Entry for Stock" }) ) - - # writeoff account includes petty difference in the invoice amount + + # writeoff account includes petty difference in the invoice amount # and the amount that is paid if self.write_off_account and flt(self.write_off_amount): gl_entries.append( @@ -383,7 +383,7 @@ class PurchaseInvoice(BuyingController): "cost_center": self.write_off_cost_center }) ) - + if gl_entries: from erpnext.accounts.general_ledger import make_gl_entries make_gl_entries(gl_entries, cancel=(self.docstatus == 2)) @@ -391,43 +391,43 @@ class PurchaseInvoice(BuyingController): 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") - + self.update_prevdoc_status() self.update_billing_status_for_zero_amount_refdoc("Purchase Order") self.make_cancel_gl_entries() - + def on_update(self): pass - + def update_raw_material_cost(self): if self.sub_contracted_items: for d in self.get("entries"): - rm_cost = frappe.db.sql("""select raw_material_cost / quantity - from `tabBOM` where item = %s and is_default = 1 and docstatus = 1 + rm_cost = frappe.db.sql("""select raw_material_cost / quantity + from `tabBOM` where item = %s and is_default = 1 and docstatus = 1 and is_active = 1 """, (d.item_code,)) rm_cost = rm_cost and flt(rm_cost[0][0]) or 0 - + d.conversion_factor = d.conversion_factor or flt(frappe.db.get_value( - "UOM Conversion Detail", {"parent": d.item_code, "uom": d.uom}, + "UOM Conversion Detail", {"parent": d.item_code, "uom": d.uom}, "conversion_factor")) or 1 - + d.rm_supp_cost = rm_cost * flt(d.qty) * flt(d.conversion_factor) - + @frappe.whitelist() def get_expense_account(doctype, txt, searchfield, start, page_len, filters): from erpnext.controllers.queries import get_match_cond - - # expense account can be any Debit account, - # but can also be a Liability account with account_type='Expense Account' in special circumstances. + + # expense account can be any Debit account, + # but can also be a Liability account with account_type='Expense Account' in special circumstances. # Hence the first condition is an "OR" - return frappe.db.sql("""select tabAccount.name from `tabAccount` + return frappe.db.sql("""select tabAccount.name from `tabAccount` where (tabAccount.report_type = "Profit and Loss" or tabAccount.account_type = "Expense Account") - and tabAccount.group_or_ledger="Ledger" - and tabAccount.docstatus!=2 + and tabAccount.group_or_ledger="Ledger" + and tabAccount.docstatus!=2 and ifnull(tabAccount.master_type, "")="" and ifnull(tabAccount.master_name, "")="" - and tabAccount.company = '%(company)s' + and tabAccount.company = '%(company)s' and tabAccount.%(key)s LIKE '%(txt)s' - %(mcond)s""" % {'company': filters['company'], 'key': searchfield, + %(mcond)s""" % {'company': filters['company'], 'key': searchfield, 'txt': "%%%s%%" % txt, 'mcond':get_match_cond(doctype)}) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index e5ad8c69d1..6aeb9402f6 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -14,29 +14,29 @@ class StockController(AccountsController): def make_gl_entries(self, repost_future_gle=True): if self.docstatus == 2: delete_gl_entries(voucher_type=self.doctype, voucher_no=self.name) - + if cint(frappe.defaults.get_global_default("auto_accounting_for_stock")): warehouse_account = self.get_warehouse_account() - + if self.docstatus==1: gl_entries = self.get_gl_entries(warehouse_account) make_gl_entries(gl_entries) if repost_future_gle: items, warehouse_account = self.get_items_and_warehouse_accounts(warehouse_account) - update_gl_entries_after(self.posting_date, self.posting_time, + update_gl_entries_after(self.posting_date, self.posting_time, warehouse_account, items) - + def get_gl_entries(self, warehouse_account=None, default_expense_account=None, default_cost_center=None): from erpnext.accounts.general_ledger import process_gl_map if not warehouse_account: warehouse_account = get_warehouse_account() - + stock_ledger = self.get_stock_ledger_details() - voucher_details = self.get_voucher_details(stock_ledger, default_expense_account, + voucher_details = self.get_voucher_details(stock_ledger, default_expense_account, default_cost_center) - + gl_list = [] warehouse_with_no_account = [] for detail in voucher_details: @@ -63,13 +63,13 @@ class StockController(AccountsController): })) elif sle.warehouse not in warehouse_with_no_account: warehouse_with_no_account.append(sle.warehouse) - - if warehouse_with_no_account: - msgprint(_("No accounting entries for following warehouses") + ": \n" + + + if warehouse_with_no_account: + msgprint(_("No accounting entries for following warehouses") + ": \n" + "\n".join(warehouse_with_no_account)) - + return process_gl_map(gl_list) - + def get_voucher_details(self, stock_ledger, default_expense_account, default_cost_center): if not default_expense_account: details = self.get(self.fname) @@ -77,18 +77,18 @@ class StockController(AccountsController): self.check_expense_account(d) else: details = [frappe._dict({ - "name":d, - "expense_account": default_expense_account, + "name":d, + "expense_account": default_expense_account, "cost_center": default_cost_center }) for d in stock_ledger.keys()] - + return details - + def get_items_and_warehouse_accounts(self, warehouse_account=None): items, warehouses = [], [] if not warehouse_account: warehouse_account = get_warehouse_account() - + if hasattr(self, "fname"): item_doclist = self.get(self.fname) elif self.doctype == "Stock Reconciliation": @@ -98,26 +98,26 @@ class StockController(AccountsController): for row in data[data.index(self.head_row)+1:]: d = frappe._dict(zip(["item_code", "warehouse", "qty", "valuation_rate"], row)) item_doclist.append(d) - + if item_doclist: for d in item_doclist: if d.item_code and d.item_code not in items: items.append(d.item_code) - + if d.get("warehouse") and d.warehouse not in warehouses: warehouses.append(d.warehouse) - + if self.doctype == "Stock Entry": if d.get("s_warehouse") and d.s_warehouse not in warehouses: warehouses.append(d.s_warehouse) if d.get("t_warehouse") and d.t_warehouse not in warehouses: warehouses.append(d.t_warehouse) - warehouse_account = {wh: warehouse_account[wh] for wh in warehouses + warehouse_account = {wh: warehouse_account[wh] for wh in warehouses if warehouse_account.get(wh)} - + return items, warehouse_account - + def get_stock_ledger_details(self): stock_ledger = {} for sle in frappe.db.sql("""select warehouse, stock_value_difference, voucher_detail_no @@ -125,12 +125,12 @@ class StockController(AccountsController): (self.doctype, self.name), as_dict=True): stock_ledger.setdefault(sle.voucher_detail_no, []).append(sle) return stock_ledger - + def get_warehouse_account(self): - warehouse_account = dict(frappe.db.sql("""select master_name, name from tabAccount + warehouse_account = dict(frappe.db.sql("""select master_name, name from tabAccount where account_type = 'Warehouse' and ifnull(master_name, '') != ''""")) return warehouse_account - + def update_gl_entries_after(self, warehouse_account=None): future_stock_vouchers = self.get_future_stock_vouchers() gle = self.get_voucherwise_gl_entries(future_stock_vouchers) @@ -153,53 +153,53 @@ class StockController(AccountsController): break else: matched = False - + if not matched: self.delete_gl_entries(voucher_type, voucher_no) voucher_obj.make_gl_entries(repost_future_gle=False) else: self.delete_gl_entries(voucher_type, voucher_no) - - + + def get_future_stock_vouchers(self): future_stock_vouchers = [] - + if hasattr(self, "fname"): item_list = [d.item_code for d in self.get(self.fname)] condition = ''.join(['and item_code in (\'', '\', \''.join(item_list) ,'\')']) else: condition = "" - - for d in frappe.db.sql("""select distinct sle.voucher_type, sle.voucher_no + + for d in frappe.db.sql("""select distinct sle.voucher_type, sle.voucher_no from `tabStock Ledger Entry` sle where timestamp(sle.posting_date, sle.posting_time) >= timestamp(%s, %s) %s - order by timestamp(sle.posting_date, sle.posting_time) asc, name asc""" % - ('%s', '%s', condition), (self.posting_date, self.posting_time), + order by timestamp(sle.posting_date, sle.posting_time) asc, name asc""" % + ('%s', '%s', condition), (self.posting_date, self.posting_time), as_dict=True): future_stock_vouchers.append([d.voucher_type, d.voucher_no]) - + return future_stock_vouchers - + def get_voucherwise_gl_entries(self, future_stock_vouchers): gl_entries = {} if future_stock_vouchers: - for d in frappe.db.sql("""select * from `tabGL Entry` - where posting_date >= %s and voucher_no in (%s)""" % - ('%s', ', '.join(['%s']*len(future_stock_vouchers))), + for d in frappe.db.sql("""select * from `tabGL Entry` + where posting_date >= %s and voucher_no in (%s)""" % + ('%s', ', '.join(['%s']*len(future_stock_vouchers))), tuple([self.posting_date] + [d[1] for d in future_stock_vouchers]), as_dict=1): gl_entries.setdefault((d.voucher_type, d.voucher_no), []).append(d) - + return gl_entries - + def delete_gl_entries(self, voucher_type, voucher_no): - frappe.db.sql("""delete from `tabGL Entry` + frappe.db.sql("""delete from `tabGL Entry` where voucher_type=%s and voucher_no=%s""", (voucher_type, voucher_no)) - + def make_adjustment_entry(self, expected_gle, voucher_obj): from erpnext.accounts.utils import get_stock_and_account_difference account_list = [d.account for d in expected_gle] acc_diff = get_stock_and_account_difference(account_list, expected_gle[0].posting_date) - + cost_center = self.get_company_default("cost_center") stock_adjustment_account = self.get_company_default("stock_adjustment_account") @@ -214,7 +214,7 @@ class StockController(AccountsController): "debit": diff, "remarks": "Adjustment Accounting Entry for Stock", }), - + # account against stock in hand voucher_obj.get_gl_dict({ "account": stock_adjustment_account, @@ -224,21 +224,21 @@ class StockController(AccountsController): "remarks": "Adjustment Accounting Entry for Stock", }), ]) - + if gl_entries: from erpnext.accounts.general_ledger import make_gl_entries make_gl_entries(gl_entries) - + def check_expense_account(self, item): if item.meta.get_field("expense_account") and not item.expense_account: - msgprint(_("""Expense/Difference account is mandatory for item: """) + item.item_code, + msgprint(_("""Expense/Difference account is mandatory for item: """) + item.item_code, raise_exception=1) - + if item.meta.get_field("expense_account") and not item.cost_center: - msgprint(_("""Cost Center is mandatory for item: """) + item.item_code, + msgprint(_("""Cost Center is mandatory for item: """) + item.item_code, raise_exception=1) - - def get_sl_entries(self, d, args): + + def get_sl_entries(self, d, args): sl_dict = { "item_code": d.item_code, "warehouse": d.get("warehouse", None), @@ -257,27 +257,27 @@ class StockController(AccountsController): "project": d.get("project_name"), "is_cancelled": self.docstatus==2 and "Yes" or "No" } - + sl_dict.update(args) return sl_dict - + def make_sl_entries(self, sl_entries, is_amended=None): from erpnext.stock.stock_ledger import make_sl_entries make_sl_entries(sl_entries, is_amended) - + def make_cancel_gl_entries(self): - if frappe.db.sql("""select name from `tabGL Entry` where voucher_type=%s + if frappe.db.sql("""select name from `tabGL Entry` where voucher_type=%s and voucher_no=%s""", (self.doctype, self.name)): self.make_gl_entries() - + def update_gl_entries_after(posting_date, posting_time, warehouse_account=None, for_items=None): def _delete_gl_entries(voucher_type, voucher_no): - frappe.db.sql("""delete from `tabGL Entry` + frappe.db.sql("""delete from `tabGL Entry` where voucher_type=%s and voucher_no=%s""", (voucher_type, voucher_no)) - + if not warehouse_account: warehouse_account = get_warehouse_account() - future_stock_vouchers = get_future_stock_vouchers(posting_date, posting_time, + future_stock_vouchers = get_future_stock_vouchers(posting_date, posting_time, warehouse_account, for_items) gle = get_voucherwise_gl_entries(future_stock_vouchers, posting_date) @@ -286,13 +286,13 @@ def update_gl_entries_after(posting_date, posting_time, warehouse_account=None, voucher_obj = frappe.get_doc(voucher_type, voucher_no) expected_gle = voucher_obj.get_gl_entries(warehouse_account) if expected_gle: - if not existing_gle or not compare_existing_and_expected_gle(existing_gle, + if not existing_gle or not compare_existing_and_expected_gle(existing_gle, expected_gle): _delete_gl_entries(voucher_type, voucher_no) voucher_obj.make_gl_entries(repost_future_gle=False) else: _delete_gl_entries(voucher_type, voucher_no) - + def compare_existing_and_expected_gle(existing_gle, expected_gle): matched = True for entry in expected_gle: @@ -306,36 +306,36 @@ def compare_existing_and_expected_gle(existing_gle, expected_gle): def get_future_stock_vouchers(posting_date, posting_time, warehouse_account=None, for_items=None): future_stock_vouchers = [] - + condition = "" if for_items: condition = ''.join([' and item_code in (\'', '\', \''.join(for_items) ,'\')']) - + if warehouse_account: condition += ''.join([' and warehouse in (\'', '\', \''.join(warehouse_account.keys()) ,'\')']) - - for d in frappe.db.sql("""select distinct sle.voucher_type, sle.voucher_no + + for d in frappe.db.sql("""select distinct sle.voucher_type, sle.voucher_no from `tabStock Ledger Entry` sle where timestamp(sle.posting_date, sle.posting_time) >= timestamp(%s, %s) %s - order by timestamp(sle.posting_date, sle.posting_time) asc, name asc""" % - ('%s', '%s', condition), (posting_date, posting_time), + order by timestamp(sle.posting_date, sle.posting_time) asc, name asc""" % + ('%s', '%s', condition), (posting_date, posting_time), as_dict=True): future_stock_vouchers.append([d.voucher_type, d.voucher_no]) - + return future_stock_vouchers - + def get_voucherwise_gl_entries(future_stock_vouchers, posting_date): gl_entries = {} if future_stock_vouchers: - for d in frappe.db.sql("""select * from `tabGL Entry` - where posting_date >= %s and voucher_no in (%s)""" % - ('%s', ', '.join(['%s']*len(future_stock_vouchers))), + for d in frappe.db.sql("""select * from `tabGL Entry` + where posting_date >= %s and voucher_no in (%s)""" % + ('%s', ', '.join(['%s']*len(future_stock_vouchers))), tuple([posting_date] + [d[1] for d in future_stock_vouchers]), as_dict=1): gl_entries.setdefault((d.voucher_type, d.voucher_no), []).append(d) - + return gl_entries def get_warehouse_account(): - warehouse_account = dict(frappe.db.sql("""select master_name, name from tabAccount + warehouse_account = dict(frappe.db.sql("""select master_name, name from tabAccount where account_type = 'Warehouse' and ifnull(master_name, '') != ''""")) return warehouse_account diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index b5d46a2c0f..9a28033912 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -19,12 +19,12 @@ class StockOverReturnError(frappe.ValidationError): pass class IncorrectValuationRateError(frappe.ValidationError): pass class DuplicateEntryForProductionOrderError(frappe.ValidationError): pass class StockOverProductionError(frappe.ValidationError): pass - + from erpnext.controllers.stock_controller import StockController class StockEntry(StockController): - fname = 'mtn_details' - + fname = 'mtn_details' + def validate(self): self.validate_posting_time() self.validate_purpose() @@ -44,7 +44,7 @@ class StockEntry(StockController): self.validate_with_material_request() self.validate_fiscal_year() self.set_total_amount() - + def on_submit(self): self.update_stock_ledger() @@ -57,32 +57,32 @@ class StockEntry(StockController): self.update_stock_ledger() self.update_production_order() self.make_cancel_gl_entries() - + def validate_fiscal_year(self): from erpnext.accounts.utils import validate_fiscal_year validate_fiscal_year(self.posting_date, self.fiscal_year, self.meta.get_label("posting_date")) - + def validate_purpose(self): - valid_purposes = ["Material Issue", "Material Receipt", "Material Transfer", + valid_purposes = ["Material Issue", "Material Receipt", "Material Transfer", "Manufacture/Repack", "Subcontract", "Sales Return", "Purchase Return"] if self.purpose not in valid_purposes: msgprint(_("Purpose must be one of ") + comma_or(valid_purposes), raise_exception=True) - + def validate_item(self): stock_items = self.get_stock_items() for item in self.get("mtn_details"): if item.item_code not in stock_items: msgprint(_("""Only Stock Items are allowed for Stock Entry"""), raise_exception=True) - + def validate_warehouse(self, pro_obj): """perform various (sometimes conditional) validations on warehouse""" - + source_mandatory = ["Material Issue", "Material Transfer", "Purchase Return"] target_mandatory = ["Material Receipt", "Material Transfer", "Sales Return"] - + validate_for_manufacture_repack = any([d.bom_no for d in self.get("mtn_details")]) if self.purpose in source_mandatory and self.purpose not in target_mandatory: @@ -101,11 +101,11 @@ class StockEntry(StockController): if not (d.s_warehouse or d.t_warehouse): msgprint(_("Atleast one warehouse is mandatory"), raise_exception=1) - + if self.purpose in source_mandatory and not d.s_warehouse: msgprint(_("Row # ") + "%s: " % cint(d.idx) + _("Source Warehouse") + _(" is mandatory"), raise_exception=1) - + if self.purpose in target_mandatory and not d.t_warehouse: msgprint(_("Row # ") + "%s: " % cint(d.idx) + _("Target Warehouse") + _(" is mandatory"), raise_exception=1) @@ -114,39 +114,39 @@ class StockEntry(StockController): if validate_for_manufacture_repack: if d.bom_no: d.s_warehouse = None - + if not d.t_warehouse: msgprint(_("Row # ") + "%s: " % cint(d.idx) + _("Target Warehouse") + _(" is mandatory"), raise_exception=1) - + elif pro_obj and cstr(d.t_warehouse) != pro_obj.fg_warehouse: msgprint(_("Row # ") + "%s: " % cint(d.idx) + _("Target Warehouse") + _(" should be same as that in ") + _("Production Order"), raise_exception=1) - + else: d.t_warehouse = None if not d.s_warehouse: msgprint(_("Row # ") + "%s: " % cint(d.idx) + _("Source Warehouse") + _(" is mandatory"), raise_exception=1) - + if cstr(d.s_warehouse) == cstr(d.t_warehouse): - msgprint(_("Source and Target Warehouse cannot be same"), + msgprint(_("Source and Target Warehouse cannot be same"), raise_exception=1) - + def validate_production_order(self, pro_obj=None): if not pro_obj: if self.production_order: pro_obj = frappe.get_doc('Production Order', self.production_order) else: return - + if self.purpose == "Manufacture/Repack": # check for double entry self.check_duplicate_entry_for_production_order() elif self.purpose != "Material Transfer": self.production_order = None - + def check_duplicate_entry_for_production_order(self): other_ste = [t[0] for t in frappe.db.get_values("Stock Entry", { "production_order": self.production_order, @@ -154,24 +154,24 @@ class StockEntry(StockController): "docstatus": ["!=", 2], "name": ["!=", self.name] }, "name")] - + if other_ste: - production_item, qty = frappe.db.get_value("Production Order", + production_item, qty = frappe.db.get_value("Production Order", self.production_order, ["production_item", "qty"]) args = other_ste + [production_item] fg_qty_already_entered = frappe.db.sql("""select sum(actual_qty) - from `tabStock Entry Detail` - where parent in (%s) - and item_code = %s + from `tabStock Entry Detail` + where parent in (%s) + and item_code = %s and ifnull(s_warehouse,'')='' """ % (", ".join(["%s" * len(other_ste)]), "%s"), args)[0][0] - + if fg_qty_already_entered >= qty: - frappe.throw(_("Stock Entries already created for Production Order ") + frappe.throw(_("Stock Entries already created for Production Order ") + self.production_order + ":" + ", ".join(other_ste), DuplicateEntryForProductionOrderError) def set_total_amount(self): self.total_amount = sum([flt(item.amount) for item in self.get("mtn_details")]) - + def get_stock_and_rate(self): """get stock and incoming rate on posting date""" for d in self.get('mtn_details'): @@ -186,21 +186,21 @@ class StockEntry(StockController): }) # get actual stock at source warehouse d.actual_qty = get_previous_sle(args).get("qty_after_transaction") or 0 - + # get incoming rate if not flt(d.incoming_rate): d.incoming_rate = self.get_incoming_rate(args) - + d.amount = flt(d.transfer_qty) * flt(d.incoming_rate) - + def get_incoming_rate(self, args): incoming_rate = 0 if self.purpose == "Sales Return" and \ (self.delivery_note_no or self.sales_invoice_no): - sle = frappe.db.sql("""select name, posting_date, posting_time, - actual_qty, stock_value, warehouse from `tabStock Ledger Entry` - where voucher_type = %s and voucher_no = %s and - item_code = %s limit 1""", + sle = frappe.db.sql("""select name, posting_date, posting_time, + actual_qty, stock_value, warehouse 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), as_dict=1) if sle: @@ -215,14 +215,14 @@ class StockEntry(StockController): flt(sle[0].actual_qty) else: incoming_rate = get_incoming_rate(args) - + return incoming_rate - + def validate_incoming_rate(self): for d in self.get('mtn_details'): if d.t_warehouse: self.validate_value("incoming_rate", ">", 0, d, raise_exception=IncorrectValuationRateError) - + def validate_bom(self): for d in self.get('mtn_details'): if d.bom_no and not frappe.db.sql("""select name from `tabBOM` @@ -231,72 +231,72 @@ class StockEntry(StockController): msgprint(_("Item") + " %s: " % cstr(d.item_code) + _("does not belong to BOM: ") + cstr(d.bom_no) + _(" or the BOM is cancelled or inactive"), raise_exception=1) - + def validate_finished_goods(self): """validation: finished good quantity should be same as manufacturing quantity""" for d in self.get('mtn_details'): if d.bom_no and flt(d.transfer_qty) != flt(self.fg_completed_qty): - msgprint(_("Row #") + " %s: " % d.idx + msgprint(_("Row #") + " %s: " % d.idx + _("Quantity should be equal to Manufacturing Quantity. To fetch items again, click on 'Get Items' button or update the Quantity manually."), raise_exception=1) - + 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.msgprint(_(ref.doc.doctype) + ' "' + ref.doc.name + '": ' + frappe.msgprint(_(ref.doc.doctype) + ' "' + ref.doc.name + '": ' + _("Status should be Submitted"), raise_exception=frappe.InvalidStatusError) - + # update stock check if ref.doc.doctype == "Sales Invoice" and cint(ref.doc.update_stock) != 1: - frappe.msgprint(_(ref.doc.doctype) + ' "' + ref.doc.name + '": ' - + _("Update Stock should be checked."), + frappe.msgprint(_(ref.doc.doctype) + ' "' + ref.doc.name + '": ' + + _("Update Stock should be checked."), raise_exception=NotUpdateStockError) - + # posting date check - ref_posting_datetime = "%s %s" % (cstr(ref.doc.posting_date), + ref_posting_datetime = "%s %s" % (cstr(ref.doc.posting_date), cstr(ref.doc.posting_time) or "00:00:00") - this_posting_datetime = "%s %s" % (cstr(self.posting_date), + this_posting_datetime = "%s %s" % (cstr(self.posting_date), cstr(self.posting_time)) if this_posting_datetime < ref_posting_datetime: from frappe.utils.dateutils import datetime_in_user_format frappe.msgprint(_("Posting Date Time cannot be before") + ": " + datetime_in_user_format(ref_posting_datetime), raise_exception=True) - + 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("mtn_details"): # validate if item exists in the ref doc and that it is a stock item if item.item_code not in stock_items: msgprint(_("Item") + ': "' + item.item_code + _("\" does not exist in ") + - ref.doc.doctype + ": " + ref.doc.name, + ref.doc.doctype + ": " + ref.doc.name, raise_exception=frappe.DoesNotExistError) - + # validate quantity <= ref item's qty - qty already returned ref_item = ref.doc.getone({"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}: {item_code} {returned}".format( - item=_("Item"), item_code=item.item_code, - returned=_("already returned though some other documents")), + item=_("Item"), item_code=item.item_code, + returned=_("already returned though some other documents")), StockOverReturnError) elif item.transfer_qty > returnable_qty: frappe.throw("{item}: {item_code}, {returned}: {qty}".format( item=_("Item"), item_code=item.item_code, returned=_("Max Returnable Qty"), qty=returnable_qty), 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 = [] + sl_entries = [] for d in self.get('mtn_details'): if cstr(d.s_warehouse) and self.docstatus == 1: sl_entries.append(self.get_sl_entries(d, { @@ -304,57 +304,57 @@ class StockEntry(StockController): "actual_qty": -flt(d.transfer_qty), "incoming_rate": 0 })) - + if cstr(d.t_warehouse): sl_entries.append(self.get_sl_entries(d, { "warehouse": cstr(d.t_warehouse), "actual_qty": flt(d.transfer_qty), "incoming_rate": flt(d.incoming_rate) })) - - # On cancellation, make stock ledger entry for + + # On cancellation, make stock ledger entry for # target warehouse first, to update serial no values properly - + if cstr(d.s_warehouse) and self.docstatus == 2: sl_entries.append(self.get_sl_entries(d, { "warehouse": cstr(d.s_warehouse), "actual_qty": -flt(d.transfer_qty), "incoming_rate": 0 })) - + self.make_sl_entries(sl_entries, self.amended_from and 'Yes' or 'No') def update_production_order(self): def _validate_production_order(pro_doc): if flt(pro_doc.docstatus) != 1: - frappe.throw(_("Production Order must be submitted") + ": " + + frappe.throw(_("Production Order must be submitted") + ": " + self.production_order) - + if pro_doc.status == 'Stopped': - msgprint(_("Transaction not allowed against stopped Production Order") + ": " + + msgprint(_("Transaction not allowed against stopped Production Order") + ": " + self.production_order) - + if self.production_order: pro_doc = frappe.get_doc("Production Order", self.production_order) _validate_production_order(pro_doc) self.update_produced_qty(pro_doc) if self.purpose == "Manufacture/Repack": self.update_planned_qty(pro_doc) - + def update_produced_qty(self, pro_doc): if self.purpose == "Manufacture/Repack": produced_qty = flt(pro_doc.produced_qty) + \ (self.docstatus==1 and 1 or -1 ) * flt(self.fg_completed_qty) - + if produced_qty > flt(pro_doc.qty): frappe.throw(_("Production Order") + ": " + self.production_order + "\n" + - _("Total Manufactured Qty can not be greater than Planned qty to manufacture") + _("Total Manufactured Qty can not be greater than Planned qty to manufacture") + "(%s/%s)" % (produced_qty, flt(pro_doc.qty)), StockOverProductionError) - + status = 'Completed' if flt(produced_qty) >= flt(pro_doc.qty) else 'In Process' - frappe.db.sql("""update `tabProduction Order` set status=%s, produced_qty=%s + frappe.db.sql("""update `tabProduction Order` set status=%s, produced_qty=%s where name=%s""", (status, produced_qty, self.production_order)) - + def update_planned_qty(self, pro_doc): from erpnext.stock.utils import update_bin update_bin({ @@ -363,16 +363,16 @@ class StockEntry(StockController): "posting_date": self.posting_date, "planned_qty": (self.docstatus==1 and -1 or 1 ) * flt(self.fg_completed_qty) }) - + def get_item_details(self, arg): arg = json.loads(arg) - item = frappe.db.sql("""select stock_uom, description, item_name, - expense_account, buying_cost_center from `tabItem` - where name = %s and (ifnull(end_of_life,'')='' or end_of_life ='0000-00-00' + item = frappe.db.sql("""select stock_uom, description, item_name, + expense_account, buying_cost_center from `tabItem` + where name = %s and (ifnull(end_of_life,'')='' or end_of_life ='0000-00-00' or end_of_life > now())""", (arg.get('item_code')), as_dict = 1) - if not item: + if not item: msgprint("Item is not active", raise_exception=1) - + ret = { 'uom' : item and item[0]['stock_uom'] or '', 'stock_uom' : item and item[0]['stock_uom'] or '', @@ -394,7 +394,7 @@ class StockEntry(StockController): def get_uom_details(self, arg = ''): arg, ret = eval(arg), {} - uom = frappe.db.sql("""select conversion_factor from `tabUOM Conversion Detail` + uom = frappe.db.sql("""select conversion_factor from `tabUOM Conversion Detail` where parent = %s and uom = %s""", (arg['item_code'], arg['uom']), as_dict = 1) if not uom or not flt(uom[0].conversion_factor): msgprint("There is no Conversion Factor for UOM '%s' in Item '%s'" % (arg['uom'], @@ -406,7 +406,7 @@ class StockEntry(StockController): 'transfer_qty' : flt(arg['qty']) * flt(uom[0]['conversion_factor']), } return ret - + def get_warehouse_details(self, args): args = json.loads(args) ret = {} @@ -416,14 +416,14 @@ class StockEntry(StockController): "posting_time": self.posting_time, }) args = frappe._dict(args) - + ret = { "actual_qty" : get_previous_sle(args).get("qty_after_transaction") or 0, "incoming_rate" : self.get_incoming_rate(args) } return ret - - def get_items(self): + + def get_items(self): pro_obj = None if self.production_order: # common validations @@ -434,7 +434,7 @@ class StockEntry(StockController): else: # invalid production order self.production_order = None - + if self.bom_no: if self.purpose in ["Material Issue", "Material Transfer", "Manufacture/Repack", "Subcontract"]: @@ -451,10 +451,10 @@ class StockEntry(StockController): # add raw materials to Stock Entry Detail table idx = self.add_to_stock_entry_detail(item_dict) - + # add finished good item to Stock Entry Detail table -- along with bom_no if self.production_order and self.purpose == "Manufacture/Repack": - item = frappe.db.get_value("Item", pro_obj.production_item, ["item_name", + item = frappe.db.get_value("Item", pro_obj.production_item, ["item_name", "description", "stock_uom", "expense_account", "buying_cost_center"], as_dict=1) self.add_to_stock_entry_detail({ cstr(pro_obj.production_item): { @@ -468,14 +468,14 @@ class StockEntry(StockController): "cost_center": item.buying_cost_center, } }, bom_no=pro_obj.bom_no, idx=idx) - + elif self.purpose in ["Material Receipt", "Manufacture/Repack"]: if self.purpose=="Material Receipt": self.from_warehouse = "" - - item = frappe.db.sql("""select name, item_name, description, - stock_uom, expense_account, buying_cost_center from `tabItem` - where name=(select item from tabBOM where name=%s)""", + + item = frappe.db.sql("""select name, item_name, description, + stock_uom, expense_account, buying_cost_center from `tabItem` + where name=(select item from tabBOM where name=%s)""", self.bom_no, as_dict=1) self.add_to_stock_entry_detail({ item[0]["name"] : { @@ -488,20 +488,20 @@ class StockEntry(StockController): "cost_center": item[0].buying_cost_center, } }, bom_no=self.bom_no, idx=idx) - + self.get_stock_and_rate() - + def get_bom_raw_materials(self, qty): from erpnext.manufacturing.doctype.bom.bom import get_bom_items_as_dict - + # item dict = { item_code: {qty, description, stock_uom} } item_dict = get_bom_items_as_dict(self.bom_no, qty=qty, fetch_exploded = self.use_multi_level_bom) - + for item in item_dict.values(): item.from_warehouse = item.default_warehouse - + return item_dict - + def get_pending_raw_materials(self, pro_obj): """ issue (item quantity) that is pending to issue or desire to transfer, @@ -509,10 +509,10 @@ class StockEntry(StockController): """ item_dict = self.get_bom_raw_materials(1) issued_item_qty = self.get_issued_qty() - + max_qty = flt(pro_obj.qty) only_pending_fetched = [] - + for item in item_dict: pending_to_issue = (max_qty * item_dict[item]["qty"]) - issued_item_qty.get(item, 0) desire_to_transfer = flt(self.fg_completed_qty) * item_dict[item]["qty"] @@ -522,17 +522,17 @@ class StockEntry(StockController): item_dict[item]["qty"] = pending_to_issue if pending_to_issue: only_pending_fetched.append(item) - + # delete items with 0 qty for item in item_dict.keys(): if not item_dict[item]["qty"]: del item_dict[item] - + # show some message if not len(item_dict): frappe.msgprint(_("""All items have already been transferred \ for this Production Order.""")) - + elif only_pending_fetched: frappe.msgprint(_("""Only quantities pending to be transferred \ were fetched for the following items:\n""" + "\n".join(only_pending_fetched))) @@ -548,7 +548,7 @@ class StockEntry(StockController): group by t1.item_code""", self.production_order) for t in result: issued_item_qty[t[0]] = flt(t[1]) - + return issued_item_qty def add_to_stock_entry_detail(self, item_dict, bom_no=None, idx=None): @@ -569,79 +569,79 @@ class StockEntry(StockController): se_child.qty = flt(item_dict[d]["qty"]) se_child.expense_account = item_dict[d]["expense_account"] or expense_account se_child.cost_center = item_dict[d]["cost_center"] or cost_center - + # in stock uom se_child.transfer_qty = flt(item_dict[d]["qty"]) se_child.conversion_factor = 1.00 - + # to be assigned for finished item se_child.bom_no = bom_no # increment idx by 1 idx += 1 return idx - + def validate_with_material_request(self): for item in self.get("mtn_details"): if item.material_request: - mreq_item = frappe.db.get_value("Material Request Item", + mreq_item = frappe.db.get_value("Material Request Item", {"name": item.material_request_item, "parent": item.material_request}, ["item_code", "warehouse", "idx"], as_dict=True) if mreq_item.item_code != item.item_code or mreq_item.warehouse != item.t_warehouse: msgprint(_("Row #") + (" %d: " % item.idx) + _("does not match") + " " + _("Row #") + (" %d %s " % (mreq_item.idx, _("of"))) - + _("Material Request") + (" - %s" % item.material_request), + + _("Material Request") + (" - %s" % item.material_request), raise_exception=frappe.MappingMismatchError) - -@frappe.whitelist() + +@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, + 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, + 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): - result = frappe.db.sql("""select bom_no, - ifnull(qty, 0) - ifnull(produced_qty, 0) as fg_completed_qty, use_multi_level_bom, + result = frappe.db.sql("""select bom_no, + ifnull(qty, 0) - ifnull(produced_qty, 0) as fg_completed_qty, use_multi_level_bom, wip_warehouse from `tabProduction Order` where name = %s""", production_order, as_dict=1) return result and result[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 + 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}, + 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 + 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": + 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: @@ -649,8 +649,8 @@ def query_return_item(doctype, txt, searchfield, start, page_len, filters): 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, + 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 ] @@ -662,45 +662,45 @@ def query_return_item(doctype, txt, searchfield, start, page_len, filters): def get_batch_no(doctype, txt, searchfield, start, page_len, filters): if not filters.get("posting_date"): filters["posting_date"] = nowdate() - + batch_nos = None args = { - 'item_code': filters['item_code'], - 's_warehouse': filters['s_warehouse'], - 'posting_date': filters['posting_date'], - 'txt': "%%%s%%" % txt, - 'mcond':get_match_cond(doctype), - "start": start, + 'item_code': filters['item_code'], + 's_warehouse': filters['s_warehouse'], + 'posting_date': filters['posting_date'], + 'txt': "%%%s%%" % txt, + 'mcond':get_match_cond(doctype), + "start": start, "page_len": page_len } - + if filters.get("s_warehouse"): - batch_nos = frappe.db.sql("""select batch_no - from `tabStock Ledger Entry` sle - where item_code = '%(item_code)s' + batch_nos = frappe.db.sql("""select batch_no + from `tabStock Ledger Entry` sle + where item_code = '%(item_code)s' and warehouse = '%(s_warehouse)s' - and batch_no like '%(txt)s' - and exists(select * from `tabBatch` - where name = sle.batch_no - and (ifnull(expiry_date, '2099-12-31') >= %(posting_date)s + and batch_no like '%(txt)s' + and exists(select * from `tabBatch` + where name = sle.batch_no + and (ifnull(expiry_date, '2099-12-31') >= %(posting_date)s or expiry_date = '') - and docstatus != 2) + and docstatus != 2) %(mcond)s - group by batch_no having sum(actual_qty) > 0 - order by batch_no desc - limit %(start)s, %(page_len)s """ + group by batch_no having sum(actual_qty) > 0 + order by batch_no desc + limit %(start)s, %(page_len)s """ % args) - + if batch_nos: return batch_nos else: - return frappe.db.sql("""select name from `tabBatch` + return frappe.db.sql("""select name from `tabBatch` where item = '%(item_code)s' and docstatus < 2 - and (ifnull(expiry_date, '2099-12-31') >= %(posting_date)s + and (ifnull(expiry_date, '2099-12-31') >= %(posting_date)s or expiry_date = '' or expiry_date = "0000-00-00") %(mcond)s - order by name desc + order by name desc limit %(start)s, %(page_len)s """ % args) @@ -708,19 +708,19 @@ 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 + + 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 + + # get ref_doc if args.get("purpose") in return_map: for fieldname, val in return_map[args.get("purpose")].items(): if args.get(fieldname): @@ -728,9 +728,9 @@ def get_return_doc_and_details(args): 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]] @@ -747,29 +747,28 @@ 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_list = [{ - "__islocal": 1, - "doctype": "Journal Voucher", + jv = frappe.new_doc("Journal Voucher") + 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 - }] - + }) + from erpnext.accounts.utils import get_balance_on for r in result: - jv_list.append({ + jv.append("entries", { "__islocal": 1, "doctype": "Journal Voucher Detail", "parentfield": "entries", @@ -779,139 +778,140 @@ def make_return_jv(stock_entry): "balance": get_balance_on(r.get("account"), se.posting_date) \ if r.get("account") else 0 }) - - return jv_list - + + return jv + def make_return_jv_from_sales_invoice(se, ref): # customer account entry parent = { "account": ref.doc.debit_to, "against_invoice": ref.doc.name, } - + # income account entries children = [] for se_item in se.get("mtn_details"): # 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 ref_item.income_account: + if not getattr(ref_item, "income_account", None): if ref_item.parent_item: - parent_item = doc.get({"item_code": ref_item.parent_item})[0] + parent_item = doc.get(doc.fname, {"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 d.against_sales_order] - + 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("mtn_details"): 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} break - + if len(invoices_against_delivery) == 1: parent["against_invoice"] = invoices_against_delivery[0] - + 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.get({"prevdoc_doctype": "Purchase Order"}) if d.prevdoc_docname] - + purchase_orders_against_receipt = [d.prevdoc_docname for d in + ref.doc.get(ref.doc.fname, {"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("mtn_details"): 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} break - + if len(invoice_against_receipt) == 1: parent["against_voucher"] = invoice_against_receipt[0] - + result = [parent] + [{"account": account} for account in children] - - return result \ No newline at end of file + + 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 0aa043333e..f8cdeb79d1 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals import frappe, unittest +import frappe.defaults from frappe.utils import flt, getdate from erpnext.stock.doctype.serial_no.serial_no import * from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory @@ -31,7 +32,6 @@ class TestStockEntry(unittest.TestCase): st2.submit() from erpnext.stock.utils import reorder_item - reorder_item() mr_name = frappe.db.sql("""select parent from `tabMaterial Request Item` @@ -39,8 +39,6 @@ class TestStockEntry(unittest.TestCase): self.assertTrue(mr_name) - frappe.db.set_default("company", self.old_default_company) - def test_material_receipt_gl_entry(self): self._clear_stock_account_balance() set_perpetual_inventory() @@ -367,14 +365,14 @@ class TestStockEntry(unittest.TestCase): def _test_sales_return_jv(self, se): from erpnext.stock.doctype.stock_entry.stock_entry import make_return_jv - jv_list = make_return_jv(se.name) + jv = make_return_jv(se.name) - self.assertEqual(len(jv_list), 3) - self.assertEqual(jv_list[0].get("voucher_type"), "Credit Note") - self.assertEqual(jv_list[0].get("posting_date"), se.posting_date) - self.assertEqual(jv_list[1].get("account"), "_Test Customer - _TC") - self.assertEqual(jv_list[2].get("account"), "Sales - _TC") - self.assertTrue(jv_list[1].get("against_invoice")) + self.assertEqual(len(jv.get("entries")), 2) + self.assertEqual(jv.get("voucher_type"), "Credit Note") + self.assertEqual(jv.get("posting_date"), se.posting_date) + self.assertEqual(jv.get("entries")[0].get("account"), "_Test Customer - _TC") + self.assertEqual(jv.get("entries")[1].get("account"), "Sales - _TC") + self.assertTrue(jv.get("entries")[0].get("against_invoice")) def test_make_return_jv_for_sales_invoice_non_packing_item(self): self._clear_stock_account_balance() @@ -527,14 +525,14 @@ class TestStockEntry(unittest.TestCase): def _test_purchase_return_jv(self, se): from erpnext.stock.doctype.stock_entry.stock_entry import make_return_jv - jv_list = make_return_jv(se.name) + jv = make_return_jv(se.name) - self.assertEqual(len(jv_list), 3) - self.assertEqual(jv_list[0].get("voucher_type"), "Debit Note") - self.assertEqual(jv_list[0].get("posting_date"), se.posting_date) - self.assertEqual(jv_list[1].get("account"), "_Test Supplier - _TC") - self.assertEqual(jv_list[2].get("account"), "_Test Account Cost for Goods Sold - _TC") - self.assertTrue(jv_list[1].get("against_voucher")) + self.assertEqual(len(jv.get("entries")), 2) + self.assertEqual(jv.get("voucher_type"), "Debit Note") + self.assertEqual(jv.get("posting_date"), se.posting_date) + self.assertEqual(jv.get("entries")[0].get("account"), "_Test Supplier - _TC") + self.assertEqual(jv.get("entries")[1].get("account"), "_Test Account Cost for Goods Sold - _TC") + self.assertTrue(jv.get("entries")[0].get("against_voucher")) def test_make_return_jv_for_purchase_receipt(self): self._clear_stock_account_balance() @@ -774,10 +772,9 @@ class TestStockEntry(unittest.TestCase): # permission tests def test_warehouse_user(self): - import frappe.defaults set_perpetual_inventory(0) - frappe.defaults.add_default("Warehouse", "_Test Warehouse 1 - _TC1", "test@example.com", "Restriction") + frappe.defaults.add_default("Warehouse", "_Test Warehouse 1 - _TC", "test@example.com", "Restriction") frappe.defaults.add_default("Warehouse", "_Test Warehouse 2 - _TC1", "test2@example.com", "Restriction") frappe.get_doc("User", "test@example.com")\ .add_roles("Sales User", "Sales Manager", "Material User", "Material Manager") @@ -797,15 +794,17 @@ class TestStockEntry(unittest.TestCase): st1.insert() st1.submit() - frappe.defaults.clear_default("Warehouse", "_Test Warehouse 1 - _TC1", "test@example.com", parenttype="Restriction") - frappe.defaults.clear_default("Warehouse", "_Test Warehouse 2 - _TC1", "test2@example.com", parenttype="Restriction") + frappe.defaults.clear_default("Warehouse", "_Test Warehouse 1 - _TC", + "test@example.com", parenttype="Restriction") + frappe.defaults.clear_default("Warehouse", "_Test Warehouse 2 - _TC1", + "test2@example.com", parenttype="Restriction") def test_freeze_stocks (self): self._clear_stock_account_balance() frappe.db.set_value('Stock Settings', None,'stock_auth_role', '') # test freeze_stocks_upto - date_newer_than_test_records = add_days(getdate(test_records[0][0]['posting_date']), 5) + date_newer_than_test_records = add_days(getdate(test_records[0]['posting_date']), 5) frappe.db.set_value("Stock Settings", None, "stock_frozen_upto", date_newer_than_test_records) se = frappe.copy_doc(test_records[0]).insert() self.assertRaises (StockFreezeError, se.submit) diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py index 17d4282f49..17d683f988 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py @@ -19,15 +19,15 @@ class StockLedgerEntry(DocListController): self.validate_item() validate_warehouse_company(self.warehouse, self.company) self.scrub_posting_time() - + from erpnext.accounts.utils import validate_fiscal_year - validate_fiscal_year(self.posting_date, self.fiscal_year, + validate_fiscal_year(self.posting_date, self.fiscal_year, self.meta.get_label("posting_date")) def on_submit(self): self.check_stock_frozen_date() self.actual_amt_check() - + from erpnext.stock.doctype.serial_no.serial_no import process_serial_no process_serial_no(self) @@ -58,7 +58,7 @@ class StockLedgerEntry(DocListController): msgprint("Stock Ledger Entry: '%s' is mandatory" % k, raise_exception = 1) elif k == 'warehouse': if not frappe.db.exists("Warehouse", self.get(k)): - msgprint("Warehouse: '%s' does not exist in the system. Please check." % + msgprint("Warehouse: '%s' does not exist in the system. Please check." % self.get(k), raise_exception = 1) def validate_item(self): @@ -76,9 +76,9 @@ class StockLedgerEntry(DocListController): frappe.throw("Batch number is mandatory for Item '%s'" % self.item_code) # check if batch belongs to item - if not frappe.db.get_value("Batch", + if not frappe.db.get_value("Batch", {"item": self.item_code, "name": self.batch_no}): - frappe.throw("'%s' is not a valid Batch Number for Item '%s'" % + frappe.throw("'%s' is not a valid Batch Number for Item '%s'" % (self.batch_no, self.item_code)) if not self.stock_uom: @@ -108,4 +108,4 @@ def on_doctype_update(): where Key_name="posting_sort_index" """): frappe.db.commit() frappe.db.sql("""alter table `tabStock Ledger Entry` - add index posting_sort_index(posting_date, posting_time, name)""") \ No newline at end of file + add index posting_sort_index(posting_date, posting_time, name)""") diff --git a/erpnext/stock/doctype/warehouse/test_records.json b/erpnext/stock/doctype/warehouse/test_records.json index 05d30d6eab..e0941af26c 100644 --- a/erpnext/stock/doctype/warehouse/test_records.json +++ b/erpnext/stock/doctype/warehouse/test_records.json @@ -1,25 +1,25 @@ [ { - "company": "_Test Company", - "create_account_under": "Stock Assets - _TC", - "doctype": "Warehouse", + "company": "_Test Company", + "create_account_under": "Stock Assets - _TC", + "doctype": "Warehouse", "warehouse_name": "_Test Warehouse" - }, + }, { - "company": "_Test Company", - "create_account_under": "Fixed Assets - _TC", - "doctype": "Warehouse", + "company": "_Test Company", + "create_account_under": "Fixed Assets - _TC", + "doctype": "Warehouse", "warehouse_name": "_Test Warehouse 1" - }, + }, { - "company": "_Test Company 1", - "create_account_under": "Stock Assets - _TC", - "doctype": "Warehouse", + "company": "_Test Company 1", + "create_account_under": "Stock Assets - _TC", + "doctype": "Warehouse", "warehouse_name": "_Test Warehouse 2" - }, + }, { - "company": "_Test Company", - "doctype": "Warehouse", + "company": "_Test Company", + "doctype": "Warehouse", "warehouse_name": "_Test Warehouse No Account" } -] \ No newline at end of file +] diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 8a19bb19a7..34558fbbc4 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -169,9 +169,13 @@ def get_sle_before_datetime(args, for_update=False): def get_sle_after_datetime(args, for_update=False): """get Stock Ledger Entries after a particular datetime, for reposting""" # NOTE: using for update of - return get_stock_ledger_entries(args, - ["timestamp(posting_date, posting_time) > timestamp(%(posting_date)s, %(posting_time)s)"], - "asc", for_update=for_update) + conditions = ["timestamp(posting_date, posting_time) > timestamp(%(posting_date)s, %(posting_time)s)"] + + # Excluding name: Workaround for MariaDB timestamp() floating microsecond issue + if args.get("name"): + conditions.append("name!=%(name)s") + + return get_stock_ledger_entries(args, conditions, "asc", for_update=for_update) def get_stock_ledger_entries(args, conditions=None, order="desc", limit=None, for_update=False): """get stock ledger entries filtered by specific posting datetime conditions""" @@ -180,7 +184,7 @@ def get_stock_ledger_entries(args, conditions=None, order="desc", limit=None, fo if not args.get("posting_time"): args["posting_time"] = "00:00" - return frappe.db.sql("""select * from `tabStock Ledger Entry` + return frappe.db.sql("""select *, timestamp(posting_date, posting_time) as "timestamp" from `tabStock Ledger Entry` where item_code = %%(item_code)s and warehouse = %%(warehouse)s and ifnull(is_cancelled, 'No')='No' diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index 0345a7edf9..bce94f389d 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -9,34 +9,34 @@ from frappe.defaults import get_global_default from frappe.utils.email_lib import sendmail class InvalidWarehouseCompany(frappe.ValidationError): pass - + def get_stock_balance_on(warehouse, posting_date=None): if not posting_date: posting_date = nowdate() - + stock_ledger_entries = frappe.db.sql(""" - SELECT + SELECT item_code, stock_value - FROM + FROM `tabStock Ledger Entry` - WHERE + WHERE warehouse=%s AND posting_date <= %s ORDER BY timestamp(posting_date, posting_time) DESC, name DESC """, (warehouse, posting_date), as_dict=1) - + sle_map = {} for sle in stock_ledger_entries: sle_map.setdefault(sle.item_code, flt(sle.stock_value)) - + return sum(sle_map.values()) - + def get_latest_stock_balance(): bin_map = {} - for d in frappe.db.sql("""SELECT item_code, warehouse, stock_value as stock_value + for d in frappe.db.sql("""SELECT item_code, warehouse, stock_value as stock_value FROM tabBin""", as_dict=1): bin_map.setdefault(d.warehouse, {}).setdefault(d.item_code, flt(d.stock_value)) - + return bin_map - + def get_bin(item_code, warehouse): bin = frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse}) if not bin: @@ -58,18 +58,18 @@ def update_bin(args): bin.update_stock(args) return bin else: - msgprint("[Stock Update] Ignored %s since it is not a stock item" + msgprint("[Stock Update] Ignored %s since it is not a stock item" % args.get("item_code")) def get_incoming_rate(args): """Get Incoming Rate based on valuation method""" from erpnext.stock.stock_ledger import get_previous_sle - + in_rate = 0 if args.get("serial_no"): in_rate = get_avg_purchase_rate(args.get("serial_no")) elif args.get("bom_no"): - result = frappe.db.sql("""select ifnull(total_cost, 0) / ifnull(quantity, 1) + result = frappe.db.sql("""select ifnull(total_cost, 0) / ifnull(quantity, 1) from `tabBOM` where name = %s and docstatus=1 and is_active=1""", args.get("bom_no")) in_rate = result and flt(result[0][0]) or 0 else: @@ -84,12 +84,12 @@ def get_incoming_rate(args): elif valuation_method == 'Moving Average': in_rate = previous_sle.get('valuation_rate') or 0 return in_rate - + def get_avg_purchase_rate(serial_nos): """get average value of serial numbers""" - + serial_nos = get_valid_serial_nos(serial_nos) - return flt(frappe.db.sql("""select avg(ifnull(purchase_rate, 0)) from `tabSerial No` + return flt(frappe.db.sql("""select avg(ifnull(purchase_rate, 0)) from `tabSerial No` where name in (%s)""" % ", ".join(["%s"] * len(serial_nos)), tuple(serial_nos))[0][0]) @@ -99,11 +99,11 @@ def get_valuation_method(item_code): if not val_method: val_method = get_global_default('valuation_method') or "FIFO" return val_method - + def get_fifo_rate(previous_stock_queue, qty): """get FIFO (average) Rate from Queue""" if qty >= 0: - total = sum(f[0] for f in previous_stock_queue) + total = sum(f[0] for f in previous_stock_queue) return total and sum(f[0] * f[1] for f in previous_stock_queue) / flt(total) or 0.0 else: outgoing_cost = 0 @@ -123,12 +123,12 @@ def get_fifo_rate(previous_stock_queue, qty): qty_to_pop = 0 # if queue gets blank and qty_to_pop remaining, get average rate of full queue return outgoing_cost / abs(qty) - qty_to_pop - + def get_valid_serial_nos(sr_nos, qty=0, item_code=''): """split serial nos, validate and return list of valid serial nos""" # TODO: remove duplicates in client side serial_nos = cstr(sr_nos).strip().replace(',', '\n').split('\n') - + valid_serial_nos = [] for val in serial_nos: if val: @@ -137,12 +137,12 @@ def get_valid_serial_nos(sr_nos, qty=0, item_code=''): msgprint("You have entered duplicate serial no: '%s'" % val, raise_exception=1) else: valid_serial_nos.append(val) - + if qty and len(valid_serial_nos) != abs(qty): msgprint("Please enter serial nos for " + cstr(abs(qty)) + " quantity against item code: " + item_code, raise_exception=1) - + return valid_serial_nos def validate_warehouse_company(warehouse, company): @@ -151,48 +151,48 @@ def validate_warehouse_company(warehouse, company): frappe.msgprint(_("Warehouse does not belong to company.") + " (" + \ warehouse + ", " + company +")", raise_exception=InvalidWarehouseCompany) -def get_sales_bom_buying_amount(item_code, warehouse, voucher_type, voucher_no, voucher_detail_no, +def get_sales_bom_buying_amount(item_code, warehouse, voucher_type, voucher_no, voucher_detail_no, stock_ledger_entries, item_sales_bom): # sales bom item buying_amount = 0.0 for bom_item in item_sales_bom[item_code]: if bom_item.get("parent_detail_docname")==voucher_detail_no: - buying_amount += get_buying_amount(voucher_type, voucher_no, voucher_detail_no, + buying_amount += get_buying_amount(voucher_type, voucher_no, voucher_detail_no, stock_ledger_entries.get((bom_item.item_code, warehouse), [])) return buying_amount - + def get_buying_amount(voucher_type, voucher_no, item_row, stock_ledger_entries): # IMP NOTE - # stock_ledger_entries should already be filtered by item_code and warehouse and + # stock_ledger_entries should already be filtered by item_code and warehouse and # sorted by posting_date desc, posting_time desc for i, sle in enumerate(stock_ledger_entries): if sle.voucher_type == voucher_type and sle.voucher_no == voucher_no and \ sle.voucher_detail_no == item_row: previous_stock_value = len(stock_ledger_entries) > i+1 and \ flt(stock_ledger_entries[i+1].stock_value) or 0.0 - buying_amount = previous_stock_value - flt(sle.stock_value) - + buying_amount = previous_stock_value - flt(sle.stock_value) + return buying_amount return 0.0 - + def reorder_item(): """ Reorder item if stock reaches reorder level""" if getattr(frappe.local, "auto_indent", None) is None: frappe.local.auto_indent = cint(frappe.db.get_value('Stock Settings', None, 'auto_indent')) - + if frappe.local.auto_indent: material_requests = {} bin_list = frappe.db.sql("""select item_code, warehouse, projected_qty from tabBin where ifnull(item_code, '') != '' and ifnull(warehouse, '') != '' - and exists (select name from `tabItem` - where `tabItem`.name = `tabBin`.item_code and + and exists (select name from `tabItem` + where `tabItem`.name = `tabBin`.item_code and is_stock_item='Yes' and (is_purchase_item='Yes' or is_sub_contracted_item='Yes') and - (ifnull(end_of_life, '')='' or end_of_life > now()))""", as_dict=True) + (ifnull(end_of_life, '')='' or end_of_life > curdate()))""", as_dict=True) for bin in bin_list: #check if re-order is required - item_reorder = frappe.db.get("Item Reorder", + item_reorder = frappe.db.get("Item Reorder", {"parent": bin.item_code, "warehouse": bin.warehouse}) if item_reorder: reorder_level = item_reorder.warehouse_reorder_level @@ -202,15 +202,15 @@ def reorder_item(): reorder_level, reorder_qty = frappe.db.get_value("Item", bin.item_code, ["re_order_level", "re_order_qty"]) material_request_type = "Purchase" - + if flt(reorder_level) and flt(bin.projected_qty) < flt(reorder_level): if flt(reorder_level) - flt(bin.projected_qty) > flt(reorder_qty): reorder_qty = flt(reorder_level) - flt(bin.projected_qty) - + company = frappe.db.get_value("Warehouse", bin.warehouse, "company") or \ frappe.defaults.get_defaults()["company"] or \ frappe.db.sql("""select name from tabCompany limit 1""")[0][0] - + material_requests.setdefault(material_request_type, frappe._dict()).setdefault( company, []).append(frappe._dict({ "item_code": bin.item_code, @@ -218,7 +218,7 @@ def reorder_item(): "reorder_qty": reorder_qty }) ) - + create_material_request(material_requests) def create_material_request(material_requests): @@ -234,21 +234,19 @@ def create_material_request(material_requests): items = material_requests[request_type][company] if not items: continue - - mr = [{ - "doctype": "Material Request", + + mr = frappe.new_doc("Material Request") + mr.update({ "company": company, "fiscal_year": current_fiscal_year, "transaction_date": nowdate(), "material_request_type": request_type - }] - + }) + for d in items: item = frappe.get_doc("Item", d.item_code) - mr.append({ + mr.append("indent_details", { "doctype": "Material Request Item", - "parenttype": "Material Request", - "parentfield": "indent_details", "item_code": d.item_code, "schedule_date": add_days(nowdate(),cint(item.lead_time_days)), "uom": item.stock_uom, @@ -259,11 +257,10 @@ def create_material_request(material_requests): "qty": d.reorder_qty, "brand": item.brand, }) - - mr_doc = frappe.get_doc(mr) - mr_doc.insert() - mr_doc.submit() - mr_list.append(mr_doc) + + mr.insert() + mr.submit() + mr_list.append(mr) except: if frappe.local.message_log: @@ -274,24 +271,24 @@ def create_material_request(material_requests): if mr_list: if getattr(frappe.local, "reorder_email_notify", None) is None: - frappe.local.reorder_email_notify = cint(frappe.db.get_value('Stock Settings', None, + frappe.local.reorder_email_notify = cint(frappe.db.get_value('Stock Settings', None, 'reorder_email_notify')) - + if(frappe.local.reorder_email_notify): send_email_notification(mr_list) if exceptions_list: notify_errors(exceptions_list) - + def send_email_notification(mr_list): """ Notify user about auto creation of indent""" - - email_list = frappe.db.sql_list("""select distinct r.parent + + email_list = frappe.db.sql_list("""select distinct r.parent from tabUserRole r, tabUser p where p.name = r.parent and p.enabled = 1 and p.docstatus < 2 - and r.role in ('Purchase Manager','Material Manager') + and r.role in ('Purchase Manager','Material Manager') and p.name not in ('Administrator', 'All', 'Guest')""") - + msg="""

Following Material Requests has been raised automatically \ based on item reorder level:

""" for mr in mr_list: @@ -302,13 +299,13 @@ def send_email_notification(mr_list): cstr(item.qty) + "" + cstr(item.uom) + "" msg += "" sendmail(email_list, subject='Auto Material Request Generation Notification', msg = msg) - + def notify_errors(exceptions_list): subject = "[Important] [ERPNext] Error(s) while creating Material Requests based on Re-order Levels" msg = """Dear System Manager, An error occured for certain Items while creating Material Requests based on Re-order level. - + Please rectify these issues: ---