Merge branch 'develop' of https://github.com/frappe/erpnext into stock_ageing_fix

This commit is contained in:
deepeshgarg007 2019-07-26 10:30:22 +05:30
commit 15154d4bd0
28 changed files with 874 additions and 663 deletions

View File

@ -58,8 +58,7 @@ def get_columns():
{ {
"fieldname": "payment_document", "fieldname": "payment_document",
"label": _("Payment Document Type"), "label": _("Payment Document Type"),
"fieldtype": "Link", "fieldtype": "Data",
"options": "DocType",
"width": 220 "width": 220
}, },
{ {

View File

@ -141,6 +141,11 @@ def get_data():
"name": "Campaign", "name": "Campaign",
"description": _("Sales campaigns."), "description": _("Sales campaigns."),
}, },
{
"type": "doctype",
"name": "Email Campaign",
"description": _("Sends Mails to lead or contact based on a Campaign schedule"),
},
{ {
"type": "doctype", "type": "doctype",
"name": "SMS Center", "name": "SMS Center",

View File

@ -40,7 +40,6 @@ status_map = {
["To Bill", "eval:self.per_delivered == 100 and self.per_billed < 100 and self.docstatus == 1"], ["To Bill", "eval:self.per_delivered == 100 and self.per_billed < 100 and self.docstatus == 1"],
["To Deliver", "eval:self.per_delivered < 100 and self.per_billed == 100 and self.docstatus == 1"], ["To Deliver", "eval:self.per_delivered < 100 and self.per_billed == 100 and self.docstatus == 1"],
["Completed", "eval:self.per_delivered == 100 and self.per_billed == 100 and self.docstatus == 1"], ["Completed", "eval:self.per_delivered == 100 and self.per_billed == 100 and self.docstatus == 1"],
["Completed", "eval:self.order_type == 'Maintenance' and self.per_billed == 100 and self.docstatus == 1"],
["Cancelled", "eval:self.docstatus==2"], ["Cancelled", "eval:self.docstatus==2"],
["Closed", "eval:self.status=='Closed'"], ["Closed", "eval:self.status=='Closed'"],
["On Hold", "eval:self.status=='On Hold'"], ["On Hold", "eval:self.status=='On Hold'"],

View File

@ -0,0 +1,38 @@
{
"creation": "2019-06-30 15:56:20.306901",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"email_template",
"send_after_days"
],
"fields": [
{
"fieldname": "send_after_days",
"fieldtype": "Int",
"in_list_view": 1,
"label": "Send After (days)",
"reqd": 1
},
{
"fieldname": "email_template",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Email Template",
"options": "Email Template",
"reqd": 1
}
],
"istable": 1,
"modified": "2019-07-12 11:46:43.184123",
"modified_by": "Administrator",
"module": "CRM",
"name": "Campaign Email Schedule",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
class CampaignEmailSchedule(Document):
pass

View File

@ -0,0 +1,8 @@
// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Email Campaign', {
email_campaign_for: function(frm) {
frm.set_value('recipient', '');
}
});

View File

@ -0,0 +1,95 @@
{
"autoname": "format:MAIL-CAMP-{YYYY}-{#####}",
"creation": "2019-06-30 16:05:30.015615",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"campaign_name",
"email_campaign_for",
"recipient",
"sender",
"column_break_4",
"start_date",
"end_date",
"status"
],
"fields": [
{
"fieldname": "campaign_name",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Campaign",
"options": "Campaign",
"reqd": 1
},
{
"fieldname": "status",
"fieldtype": "Select",
"label": "Status",
"options": "\nScheduled\nIn Progress\nCompleted\nUnsubscribed",
"read_only": 1
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"fieldname": "start_date",
"fieldtype": "Date",
"label": "Start Date",
"reqd": 1
},
{
"fieldname": "end_date",
"fieldtype": "Date",
"label": "End Date",
"read_only": 1
},
{
"default": "Lead",
"fieldname": "email_campaign_for",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Email Campaign For ",
"options": "\nLead\nContact"
},
{
"fieldname": "recipient",
"fieldtype": "Dynamic Link",
"label": "Recipient",
"options": "email_campaign_for",
"reqd": 1
},
{
"default": "__user",
"fieldname": "sender",
"fieldtype": "Link",
"label": "Sender",
"options": "User"
}
],
"modified": "2019-07-12 13:47:37.261213",
"modified_by": "Administrator",
"module": "CRM",
"name": "Email Campaign",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -0,0 +1,102 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.utils import getdate, add_days, today, nowdate, cstr
from frappe.model.document import Document
from frappe.core.doctype.communication.email import make
class EmailCampaign(Document):
def validate(self):
self.set_date()
#checking if email is set for lead. Not checking for contact as email is a mandatory field for contact.
if self.email_campaign_for == "Lead":
self.validate_lead()
self.validate_email_campaign_already_exists()
self.update_status()
def set_date(self):
if getdate(self.start_date) < getdate(today()):
frappe.throw(_("Start Date cannot be before the current date"))
#set the end date as start date + max(send after days) in campaign schedule
send_after_days = []
campaign = frappe.get_doc("Campaign", self.campaign_name)
for entry in campaign.get("campaign_schedules"):
send_after_days.append(entry.send_after_days)
try:
end_date = add_days(getdate(self.start_date), max(send_after_days))
except ValueError:
frappe.throw(_("Please set up the Campaign Schedule in the Campaign {0}").format(self.campaign_name))
def validate_lead(self):
lead_email_id = frappe.db.get_value("Lead", self.recipient, 'email_id')
if not lead_email_id:
lead_name = frappe.db.get_value("Lead", self.recipient, 'lead_name')
frappe.throw(_("Please set an email id for the Lead {0}").format(lead_name))
def validate_email_campaign_already_exists(self):
email_campaign_exists = frappe.db.exists("Email Campaign", {
"campaign_name": self.campaign_name,
"recipient": self.recipient,
"status": ("in", ["In Progress", "Scheduled"])
})
if email_campaign_exists:
frappe.throw(_("The Campaign '{0}' already exists for the {1} '{2}'").format(self.campaign_name, self.email_campaign_for, self.recipient))
def update_status(self):
start_date = getdate(self.start_date)
end_date = getdate(self.end_date)
today_date = getdate(today())
if start_date > today_date:
self.status = "Scheduled"
elif end_date >= today_date:
self.status = "In Progress"
elif end_date < today_date:
self.status = "Completed"
#called through hooks to send campaign mails to leads
def send_email_to_leads_or_contacts():
email_campaigns = frappe.get_all("Email Campaign", filters = { 'status': ('not in', ['Unsubscribed', 'Completed', 'Scheduled']) })
for camp in email_campaigns:
email_campaign = frappe.get_doc("Email Campaign", camp.name)
campaign = frappe.get_cached_doc("Campaign", email_campaign.campaign_name)
for entry in campaign.get("campaign_schedules"):
scheduled_date = add_days(email_campaign.get('start_date'), entry.get('send_after_days'))
if scheduled_date == getdate(today()):
send_mail(entry, email_campaign)
def send_mail(entry, email_campaign):
recipient = frappe.db.get_value(email_campaign.email_campaign_for, email_campaign.get("recipient"), 'email_id')
email_template = frappe.get_doc("Email Template", entry.get("email_template"))
sender = frappe.db.get_value("User", email_campaign.get("sender"), 'email')
# send mail and link communication to document
comm = make(
doctype = "Email Campaign",
name = email_campaign.name,
subject = email_template.get("subject"),
content = email_template.get("response"),
sender = sender,
recipients = recipient,
communication_medium = "Email",
sent_or_received = "Sent",
send_email = True,
email_template = email_template.name
)
return comm
#called from hooks on doc_event Email Unsubscribe
def unsubscribe_recipient(unsubscribe, method):
if unsubscribe.reference_doctype == 'Email Campaign':
frappe.db.set_value("Email Campaign", unsubscribe.reference_name, "status", "Unsubscribed")
#called through hooks to update email campaign status daily
def set_email_campaign_status():
email_campaigns = frappe.get_all("Email Campaign", filters = { 'status': ('!=', 'Unsubscribed')})
for entry in email_campaigns:
email_campaign = frappe.get_doc("Email Campaign", entry.name)
email_campaign.update_status()

View File

@ -0,0 +1,11 @@
frappe.listview_settings['Email Campaign'] = {
get_indicator: function(doc) {
var colors = {
"Unsubscribed": "red",
"Scheduled": "blue",
"In Progress": "orange",
"Completed": "green"
};
return [__(doc.status), colors[doc.status], "status,=," + doc.status];
}
};

View File

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
# import frappe
import unittest
class TestEmailCampaign(unittest.TestCase):
pass

View File

@ -233,6 +233,9 @@ doc_events = {
}, },
"Contact":{ "Contact":{
"on_trash": "erpnext.support.doctype.issue.issue.update_issue" "on_trash": "erpnext.support.doctype.issue.issue.update_issue"
},
"Email Unsubscribe": {
"after_insert": "erpnext.crm.doctype.email_campaign.email_campaign.unsubscribe_recipient"
} }
} }
@ -272,6 +275,8 @@ scheduler_events = {
"erpnext.projects.doctype.project.project.send_project_status_email_to_users", "erpnext.projects.doctype.project.project.send_project_status_email_to_users",
"erpnext.quality_management.doctype.quality_review.quality_review.review", "erpnext.quality_management.doctype.quality_review.quality_review.review",
"erpnext.support.doctype.service_level_agreement.service_level_agreement.check_agreement_status", "erpnext.support.doctype.service_level_agreement.service_level_agreement.check_agreement_status",
"erpnext.crm.doctype.email_campaign.email_campaign.send_email_to_leads_or_contacts",
"erpnext.crm.doctype.email_campaign.email_campaign.set_email_campaign_status"
], ],
"daily_long": [ "daily_long": [
"erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.update_latest_price_in_all_boms" "erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.update_latest_price_in_all_boms"

View File

@ -161,6 +161,7 @@ class Gstr1Report(object):
"gst_category": ["in", ["Registered Regular", "Deemed Export", "SEZ"]] "gst_category": ["in", ["Registered Regular", "Deemed Export", "SEZ"]]
}) })
if customers:
conditions += """ and ifnull(gst_category, '') != 'Overseas' and is_return != 1 conditions += """ and ifnull(gst_category, '') != 'Overseas' and is_return != 1
and customer in ({0})""".format(", ".join([frappe.db.escape(c.name) for c in customers])) and customer in ({0})""".format(", ".join([frappe.db.escape(c.name) for c in customers]))
@ -174,11 +175,11 @@ class Gstr1Report(object):
"gst_category": ["in", ["Unregistered"]] "gst_category": ["in", ["Unregistered"]]
}) })
if self.filters.get("type_of_business") == "B2C Large": if self.filters.get("type_of_business") == "B2C Large" and customers:
conditions += """ and SUBSTR(place_of_supply, 1, 2) != SUBSTR(company_gstin, 1, 2) conditions += """ and SUBSTR(place_of_supply, 1, 2) != SUBSTR(company_gstin, 1, 2)
and grand_total > {0} and is_return != 1 and customer in ({1})""".\ and grand_total > {0} and is_return != 1 and customer in ({1})""".\
format(flt(b2c_limit), ", ".join([frappe.db.escape(c.name) for c in customers])) format(flt(b2c_limit), ", ".join([frappe.db.escape(c.name) for c in customers]))
elif self.filters.get("type_of_business") == "B2C Small": elif self.filters.get("type_of_business") == "B2C Small" and customers:
conditions += """ and ( conditions += """ and (
SUBSTR(place_of_supply, 1, 2) = SUBSTR(company_gstin, 1, 2) SUBSTR(place_of_supply, 1, 2) = SUBSTR(company_gstin, 1, 2)
or grand_total <= {0}) and is_return != 1 and customer in ({1})""".\ or grand_total <= {0}) and is_return != 1 and customer in ({1})""".\

View File

@ -6,18 +6,13 @@
"description": "Keep Track of Sales Campaigns. Keep track of Leads, Quotations, Sales Order etc from Campaigns to gauge Return on Investment. ", "description": "Keep Track of Sales Campaigns. Keep track of Leads, Quotations, Sales Order etc from Campaigns to gauge Return on Investment. ",
"doctype": "DocType", "doctype": "DocType",
"document_type": "Setup", "document_type": "Setup",
"engine": "InnoDB",
"field_order": [ "field_order": [
"campaign", "campaign",
"campaign_name", "campaign_name",
"naming_series", "naming_series",
"from_date", "campaign_schedules_section",
"column_break1", "campaign_schedules",
"status",
"to_date",
"budget_section",
"currency",
"column_break2",
"budget",
"description_section", "description_section",
"description" "description"
], ],
@ -52,57 +47,25 @@
"oldfieldtype": "Text", "oldfieldtype": "Text",
"width": "300px" "width": "300px"
}, },
{
"fieldname": "status",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Status",
"options": "\nPlanned\nIn Progress\nCompleted\nCancelled",
"reqd": 1,
"default": "Planned"
},
{
"fieldname": "from_date",
"fieldtype": "Date",
"label": "From Date"
},
{
"fieldname": "to_date",
"fieldtype": "Date",
"label": "To Date"
},
{
"fieldname": "column_break1",
"fieldtype": "Column Break"
},
{
"fieldname": "budget",
"fieldtype": "Currency",
"label": "Budget"
},
{ {
"fieldname": "description_section", "fieldname": "description_section",
"fieldtype": "Section Break" "fieldtype": "Section Break"
}, },
{ {
"fieldname": "currency", "fieldname": "campaign_schedules",
"fieldtype": "Link", "fieldtype": "Table",
"label": "Currency", "label": "Campaign Schedules",
"options": "Currency" "options": "Campaign Email Schedule"
}, },
{ {
"fieldname": "column_break2", "fieldname": "campaign_schedules_section",
"fieldtype": "Column Break"
},
{
"fieldname": "budget_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "BUDGET" "label": "Campaign Schedules"
} }
], ],
"icon": "fa fa-bullhorn", "icon": "fa fa-bullhorn",
"idx": 1, "idx": 1,
"modified": "2019-04-29 22:09:39.251884", "modified": "2019-07-22 12:03:39.832342",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Campaign", "name": "Campaign",
@ -140,5 +103,7 @@
"write": 1 "write": 1
} }
], ],
"quick_entry": 1 "quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC"
} }

View File

@ -0,0 +1,13 @@
from __future__ import unicode_literals
from frappe import _
def get_data():
return {
'fieldname': 'campaign_name',
'transactions': [
{
'label': _('Email Campaigns'),
'items': ['Email Campaign']
}
],
}

View File

@ -69,8 +69,7 @@ class ItemGroup(NestedSet, WebsiteGenerator):
"items": get_product_list_for_group(product_group = self.name, start=start, "items": get_product_list_for_group(product_group = self.name, start=start,
limit=context.page_length + 1, search=frappe.form_dict.get("search")), limit=context.page_length + 1, search=frappe.form_dict.get("search")),
"parents": get_parent_item_groups(self.parent_item_group), "parents": get_parent_item_groups(self.parent_item_group),
"title": self.name, "title": self.name
"products_as_list": cint(frappe.db.get_single_value('Products Settings', 'products_as_list'))
}) })
if self.slideshow: if self.slideshow:

View File

@ -122,8 +122,8 @@ class Batch(Document):
self.expiry_date = add_days(self.manufacturing_date, shelf_life_in_days) self.expiry_date = add_days(self.manufacturing_date, shelf_life_in_days)
if has_expiry_date and not self.expiry_date: if has_expiry_date and not self.expiry_date:
frappe.throw(_('Expiry date is mandatory for selected item')) frappe.msgprint(_('Expiry date is mandatory for selected item.'))
frappe.msgprint(_('Set items shelf life in days, to set expiry based on manufacturing_date plus self life')) frappe.throw(_("Set item's shelf life in days, to set expiry based on manufacturing date plus shelf-life."))
def get_name_from_naming_series(self): def get_name_from_naming_series(self):
""" """

View File

@ -222,7 +222,7 @@ def validate_serial_no(sle, item_det):
frappe.throw(_("Serial No {0} has already been received").format(serial_no), frappe.throw(_("Serial No {0} has already been received").format(serial_no),
SerialNoDuplicateError) SerialNoDuplicateError)
if (sr.delivery_document_no and sle.voucher_type != 'Stock Entry' if (sr.delivery_document_no and sle.voucher_type not in ['Stock Entry', 'Stock Reconciliation']
and sle.voucher_type == sr.delivery_document_type): and sle.voucher_type == sr.delivery_document_type):
return_against = frappe.db.get_value(sle.voucher_type, sle.voucher_no, 'return_against') return_against = frappe.db.get_value(sle.voucher_type, sle.voucher_no, 'return_against')
if return_against and return_against != sr.delivery_document_no: if return_against and return_against != sr.delivery_document_no:
@ -299,7 +299,7 @@ def validate_so_serial_no(sr, sales_order,):
be delivered""").format(sales_order, sr.item_code, sr.name)) be delivered""").format(sales_order, sr.item_code, sr.name))
def has_duplicate_serial_no(sn, sle): def has_duplicate_serial_no(sn, sle):
if sn.warehouse: if sn.warehouse and sle.voucher_type != 'Stock Reconciliation':
return True return True
if sn.company != sle.company: if sn.company != sle.company:
@ -415,16 +415,20 @@ def update_serial_nos_after_submit(controller, parentfield):
if not stock_ledger_entries: return if not stock_ledger_entries: return
for d in controller.get(parentfield): for d in controller.get(parentfield):
if d.serial_no:
continue
update_rejected_serial_nos = True if (controller.doctype in ("Purchase Receipt", "Purchase Invoice") update_rejected_serial_nos = True if (controller.doctype in ("Purchase Receipt", "Purchase Invoice")
and d.rejected_qty) else False and d.rejected_qty) else False
accepted_serial_nos_updated = False accepted_serial_nos_updated = False
if controller.doctype == "Stock Entry": if controller.doctype == "Stock Entry":
warehouse = d.t_warehouse warehouse = d.t_warehouse
qty = d.transfer_qty qty = d.transfer_qty
else: else:
warehouse = d.warehouse warehouse = d.warehouse
qty = d.stock_qty qty = (d.qty if controller.doctype == "Stock Reconciliation"
else d.stock_qty)
for sle in stock_ledger_entries: for sle in stock_ledger_entries:
if sle.voucher_detail_no==d.name: if sle.voucher_detail_no==d.name:
if not accepted_serial_nos_updated and qty and abs(sle.actual_qty)==qty \ if not accepted_serial_nos_updated and qty and abs(sle.actual_qty)==qty \

View File

@ -359,7 +359,7 @@ class StockEntry(StockController):
d.basic_rate = 0.0 d.basic_rate = 0.0
elif d.t_warehouse and not d.basic_rate: elif d.t_warehouse and not d.basic_rate:
d.basic_rate = get_valuation_rate(d.item_code, d.t_warehouse, d.basic_rate = get_valuation_rate(d.item_code, d.t_warehouse,
self.doctype, d.name, d.allow_zero_valuation_rate, self.doctype, self.name, d.allow_zero_valuation_rate,
currency=erpnext.get_company_currency(self.company)) currency=erpnext.get_company_currency(self.company))
def set_actual_qty(self): def set_actual_qty(self):

View File

@ -38,7 +38,7 @@ class StockLedgerEntry(Document):
self.check_stock_frozen_date() self.check_stock_frozen_date()
self.actual_amt_check() self.actual_amt_check()
if not self.get("via_landed_cost_voucher") and self.voucher_type != 'Stock Reconciliation': if not self.get("via_landed_cost_voucher"):
from erpnext.stock.doctype.serial_no.serial_no import process_serial_no from erpnext.stock.doctype.serial_no.serial_no import process_serial_no
process_serial_no(self) process_serial_no(self)

View File

@ -12,8 +12,7 @@ frappe.ui.form.on("Stock Reconciliation", {
return { return {
query: "erpnext.controllers.queries.item_query", query: "erpnext.controllers.queries.item_query",
filters:{ filters:{
"is_stock_item": 1, "is_stock_item": 1
"has_serial_no": 0
} }
} }
}); });
@ -77,6 +76,7 @@ frappe.ui.form.on("Stock Reconciliation", {
set_valuation_rate_and_qty: function(frm, cdt, cdn) { set_valuation_rate_and_qty: function(frm, cdt, cdn) {
var d = frappe.model.get_doc(cdt, cdn); var d = frappe.model.get_doc(cdt, cdn);
if(d.item_code && d.warehouse) { if(d.item_code && d.warehouse) {
frappe.call({ frappe.call({
method: "erpnext.stock.doctype.stock_reconciliation.stock_reconciliation.get_stock_balance_for", method: "erpnext.stock.doctype.stock_reconciliation.stock_reconciliation.get_stock_balance_for",
@ -84,7 +84,8 @@ frappe.ui.form.on("Stock Reconciliation", {
item_code: d.item_code, item_code: d.item_code,
warehouse: d.warehouse, warehouse: d.warehouse,
posting_date: frm.doc.posting_date, posting_date: frm.doc.posting_date,
posting_time: frm.doc.posting_time posting_time: frm.doc.posting_time,
batch_no: d.batch_no
}, },
callback: function(r) { callback: function(r) {
frappe.model.set_value(cdt, cdn, "qty", r.message.qty); frappe.model.set_value(cdt, cdn, "qty", r.message.qty);
@ -93,7 +94,7 @@ frappe.ui.form.on("Stock Reconciliation", {
frappe.model.set_value(cdt, cdn, "current_valuation_rate", r.message.rate); frappe.model.set_value(cdt, cdn, "current_valuation_rate", r.message.rate);
frappe.model.set_value(cdt, cdn, "current_amount", r.message.rate * r.message.qty); frappe.model.set_value(cdt, cdn, "current_amount", r.message.rate * r.message.qty);
frappe.model.set_value(cdt, cdn, "amount", r.message.rate * r.message.qty); frappe.model.set_value(cdt, cdn, "amount", r.message.rate * r.message.qty);
frappe.model.set_value(cdt, cdn, "current_serial_no", r.message.serial_nos);
} }
}); });
} }
@ -152,17 +153,44 @@ frappe.ui.form.on("Stock Reconciliation Item", {
barcode: function(frm, cdt, cdn) { barcode: function(frm, cdt, cdn) {
frm.events.set_item_code(frm, cdt, cdn); frm.events.set_item_code(frm, cdt, cdn);
}, },
warehouse: function(frm, cdt, cdn) { warehouse: function(frm, cdt, cdn) {
var child = locals[cdt][cdn];
if (child.batch_no) {
frappe.model.set_value(child.cdt, child.cdn, "batch_no", "");
}
frm.events.set_valuation_rate_and_qty(frm, cdt, cdn); frm.events.set_valuation_rate_and_qty(frm, cdt, cdn);
}, },
item_code: function(frm, cdt, cdn) { item_code: function(frm, cdt, cdn) {
var child = locals[cdt][cdn];
if (child.batch_no) {
frappe.model.set_value(cdt, cdn, "batch_no", "");
}
frm.events.set_valuation_rate_and_qty(frm, cdt, cdn); frm.events.set_valuation_rate_and_qty(frm, cdt, cdn);
}, },
batch_no: function(frm, cdt, cdn) {
frm.events.set_valuation_rate_and_qty(frm, cdt, cdn);
},
qty: function(frm, cdt, cdn) { qty: function(frm, cdt, cdn) {
frm.events.set_amount_quantity(frm, cdt, cdn); frm.events.set_amount_quantity(frm, cdt, cdn);
}, },
valuation_rate: function(frm, cdt, cdn) { valuation_rate: function(frm, cdt, cdn) {
frm.events.set_amount_quantity(frm, cdt, cdn); frm.events.set_amount_quantity(frm, cdt, cdn);
},
serial_no: function(frm, cdt, cdn) {
var child = locals[cdt][cdn];
if (child.serial_no) {
const serial_nos = child.serial_no.trim().split('\n');
frappe.model.set_value(cdt, cdn, "qty", serial_nos.length);
}
} }
}); });

View File

@ -9,7 +9,9 @@ from frappe.utils import cstr, flt, cint
from erpnext.stock.stock_ledger import update_entries_after from erpnext.stock.stock_ledger import update_entries_after
from erpnext.controllers.stock_controller import StockController from erpnext.controllers.stock_controller import StockController
from erpnext.accounts.utils import get_company_default from erpnext.accounts.utils import get_company_default
from erpnext.stock.utils import get_stock_balance from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
from erpnext.stock.utils import get_stock_balance, get_incoming_rate, get_available_serial_nos
from erpnext.stock.doctype.batch.batch import get_batch_qty
class OpeningEntryAccountError(frappe.ValidationError): pass class OpeningEntryAccountError(frappe.ValidationError): pass
class EmptyStockReconciliationItemsError(frappe.ValidationError): pass class EmptyStockReconciliationItemsError(frappe.ValidationError): pass
@ -30,10 +32,16 @@ class StockReconciliation(StockController):
self.validate_expense_account() self.validate_expense_account()
self.set_total_qty_and_amount() self.set_total_qty_and_amount()
if self._action=="submit":
self.make_batches('warehouse')
def on_submit(self): def on_submit(self):
self.update_stock_ledger() self.update_stock_ledger()
self.make_gl_entries() self.make_gl_entries()
from erpnext.stock.doctype.serial_no.serial_no import update_serial_nos_after_submit
update_serial_nos_after_submit(self, "items")
def on_cancel(self): def on_cancel(self):
self.delete_and_repost_sle() self.delete_and_repost_sle()
self.make_gl_entries_on_cancel() self.make_gl_entries_on_cancel()
@ -42,23 +50,28 @@ class StockReconciliation(StockController):
"""Remove items if qty or rate is not changed""" """Remove items if qty or rate is not changed"""
self.difference_amount = 0.0 self.difference_amount = 0.0
def _changed(item): def _changed(item):
qty, rate = get_stock_balance(item.item_code, item.warehouse, item_dict = get_stock_balance_for(item.item_code, item.warehouse,
self.posting_date, self.posting_time, with_valuation_rate=True) self.posting_date, self.posting_time, batch_no=item.batch_no)
if (item.qty==None or item.qty==qty) and (item.valuation_rate==None or item.valuation_rate==rate): if (((item.qty is None or item.qty==item_dict.get("qty")) and
(item.valuation_rate is None or item.valuation_rate==item_dict.get("rate")) and not item.serial_no)
or (item.serial_no and item.serial_no == item_dict.get("serial_nos"))):
return False return False
else: else:
# set default as current rates # set default as current rates
if item.qty==None: if item.qty is None:
item.qty = qty item.qty = item_dict.get("qty")
if item.valuation_rate==None: if item.valuation_rate is None:
item.valuation_rate = rate item.valuation_rate = item_dict.get("rate")
item.current_qty = qty if item_dict.get("serial_nos"):
item.current_valuation_rate = rate item.current_serial_no = item_dict.get("serial_nos")
item.current_qty = item_dict.get("qty")
item.current_valuation_rate = item_dict.get("rate")
self.difference_amount += (flt(item.qty, item.precision("qty")) * \ self.difference_amount += (flt(item.qty, item.precision("qty")) * \
flt(item.valuation_rate or rate, item.precision("valuation_rate")) \ flt(item.valuation_rate or item_dict.get("rate"), item.precision("valuation_rate")) \
- flt(qty, item.precision("qty")) * flt(rate, item.precision("valuation_rate"))) - flt(item_dict.get("qty"), item.precision("qty")) * flt(item_dict.get("rate"), item.precision("valuation_rate")))
return True return True
items = list(filter(lambda d: _changed(d), self.items)) items = list(filter(lambda d: _changed(d), self.items))
@ -84,12 +97,17 @@ class StockReconciliation(StockController):
for row_num, row in enumerate(self.items): for row_num, row in enumerate(self.items):
# find duplicates # find duplicates
if [row.item_code, row.warehouse] in item_warehouse_combinations: key = [row.item_code, row.warehouse]
for field in ['serial_no', 'batch_no']:
if row.get(field):
key.append(row.get(field))
if key in item_warehouse_combinations:
self.validation_messages.append(_get_msg(row_num, _("Duplicate entry"))) self.validation_messages.append(_get_msg(row_num, _("Duplicate entry")))
else: else:
item_warehouse_combinations.append([row.item_code, row.warehouse]) item_warehouse_combinations.append(key)
self.validate_item(row.item_code, row_num+1) self.validate_item(row.item_code, row)
# validate warehouse # validate warehouse
if not frappe.db.get_value("Warehouse", row.warehouse): if not frappe.db.get_value("Warehouse", row.warehouse):
@ -131,7 +149,7 @@ class StockReconciliation(StockController):
raise frappe.ValidationError(self.validation_messages) raise frappe.ValidationError(self.validation_messages)
def validate_item(self, item_code, row_num): def validate_item(self, item_code, row):
from erpnext.stock.doctype.item.item import validate_end_of_life, \ from erpnext.stock.doctype.item.item import validate_end_of_life, \
validate_is_stock_item, validate_cancelled_item validate_is_stock_item, validate_cancelled_item
@ -145,31 +163,37 @@ class StockReconciliation(StockController):
validate_is_stock_item(item_code, item.is_stock_item, verbose=0) validate_is_stock_item(item_code, item.is_stock_item, verbose=0)
# item should not be serialized # item should not be serialized
if item.has_serial_no == 1: if item.has_serial_no and not row.serial_no and not item.serial_no_series:
raise frappe.ValidationError(_("Serialized Item {0} cannot be updated using Stock Reconciliation, please use Stock Entry").format(item_code)) raise frappe.ValidationError(_("Serial no(s) required for serialized item {0}").format(item_code))
# item managed batch-wise not allowed # item managed batch-wise not allowed
if item.has_batch_no == 1: if item.has_batch_no and not row.batch_no and not item.create_new_batch:
raise frappe.ValidationError(_("Batched Item {0} cannot be updated using Stock Reconciliation, instead use Stock Entry").format(item_code)) raise frappe.ValidationError(_("Batch no is required for batched item {0}").format(item_code))
# docstatus should be < 2 # docstatus should be < 2
validate_cancelled_item(item_code, item.docstatus, verbose=0) validate_cancelled_item(item_code, item.docstatus, verbose=0)
except Exception as e: except Exception as e:
self.validation_messages.append(_("Row # ") + ("%d: " % (row_num)) + cstr(e)) self.validation_messages.append(_("Row # ") + ("%d: " % (row.idx)) + cstr(e))
def update_stock_ledger(self): def update_stock_ledger(self):
""" find difference between current and expected entries """ find difference between current and expected entries
and create stock ledger entries based on the difference""" and create stock ledger entries based on the difference"""
from erpnext.stock.stock_ledger import get_previous_sle from erpnext.stock.stock_ledger import get_previous_sle
sl_entries = []
for row in self.items: for row in self.items:
item = frappe.get_doc("Item", row.item_code)
if item.has_serial_no or item.has_batch_no:
self.get_sle_for_serialized_items(row, sl_entries)
else:
previous_sle = get_previous_sle({ previous_sle = get_previous_sle({
"item_code": row.item_code, "item_code": row.item_code,
"warehouse": row.warehouse, "warehouse": row.warehouse,
"posting_date": self.posting_date, "posting_date": self.posting_date,
"posting_time": self.posting_time "posting_time": self.posting_time
}) })
if previous_sle: if previous_sle:
if row.qty in ("", None): if row.qty in ("", None):
row.qty = previous_sle.get("qty_after_transaction", 0) row.qty = previous_sle.get("qty_after_transaction", 0)
@ -178,18 +202,91 @@ class StockReconciliation(StockController):
row.valuation_rate = previous_sle.get("valuation_rate", 0) row.valuation_rate = previous_sle.get("valuation_rate", 0)
if row.qty and not row.valuation_rate: if row.qty and not row.valuation_rate:
frappe.throw(_("Valuation Rate required for Item in row {0}").format(row.idx)) frappe.throw(_("Valuation Rate required for Item {0} at row {1}").format(row.item_code, row.idx))
if ((previous_sle and row.qty == previous_sle.get("qty_after_transaction") if ((previous_sle and row.qty == previous_sle.get("qty_after_transaction")
and (row.valuation_rate == previous_sle.get("valuation_rate") or row.qty == 0)) and (row.valuation_rate == previous_sle.get("valuation_rate") or row.qty == 0))
or (not previous_sle and not row.qty)): or (not previous_sle and not row.qty)):
continue continue
self.insert_entries(row) sl_entries.append(self.get_sle_for_items(row))
def insert_entries(self, row): if sl_entries:
self.make_sl_entries(sl_entries)
def get_sle_for_serialized_items(self, row, sl_entries):
from erpnext.stock.stock_ledger import get_previous_sle
serial_nos = get_serial_nos(row.serial_no)
# To issue existing serial nos
if row.current_qty and (row.current_serial_no or row.batch_no):
args = self.get_sle_for_items(row)
args.update({
'actual_qty': -1 * row.current_qty,
'serial_no': row.current_serial_no,
'batch_no': row.batch_no,
'valuation_rate': row.current_valuation_rate
})
if row.current_serial_no:
args.update({
'qty_after_transaction': 0,
})
sl_entries.append(args)
for serial_no in serial_nos:
args = self.get_sle_for_items(row, [serial_no])
previous_sle = get_previous_sle({
"item_code": row.item_code,
"posting_date": self.posting_date,
"posting_time": self.posting_time,
"serial_no": serial_no
})
if previous_sle and row.warehouse != previous_sle.get("warehouse"):
# If serial no exists in different warehouse
new_args = args.copy()
new_args.update({
'actual_qty': -1,
'qty_after_transaction': cint(previous_sle.get('qty_after_transaction')) - 1,
'warehouse': previous_sle.get("warehouse", '') or row.warehouse,
'valuation_rate': previous_sle.get("valuation_rate")
})
sl_entries.append(new_args)
if row.qty:
args = self.get_sle_for_items(row)
args.update({
'actual_qty': row.qty,
'incoming_rate': row.valuation_rate,
'valuation_rate': row.valuation_rate
})
sl_entries.append(args)
if serial_nos == get_serial_nos(row.current_serial_no):
# update valuation rate
self.update_valuation_rate_for_serial_nos(row, serial_nos)
def update_valuation_rate_for_serial_nos(self, row, serial_nos):
valuation_rate = row.valuation_rate if self.docstatus == 1 else row.current_valuation_rate
for d in serial_nos:
frappe.db.set_value("Serial No", d, 'purchase_rate', valuation_rate)
def get_sle_for_items(self, row, serial_nos=None):
"""Insert Stock Ledger Entries""" """Insert Stock Ledger Entries"""
args = frappe._dict({
if not serial_nos and row.serial_no:
serial_nos = get_serial_nos(row.serial_no)
data = frappe._dict({
"doctype": "Stock Ledger Entry", "doctype": "Stock Ledger Entry",
"item_code": row.item_code, "item_code": row.item_code,
"warehouse": row.warehouse, "warehouse": row.warehouse,
@ -197,13 +294,19 @@ class StockReconciliation(StockController):
"posting_time": self.posting_time, "posting_time": self.posting_time,
"voucher_type": self.doctype, "voucher_type": self.doctype,
"voucher_no": self.name, "voucher_no": self.name,
"voucher_detail_no": row.name,
"company": self.company, "company": self.company,
"stock_uom": frappe.db.get_value("Item", row.item_code, "stock_uom"), "stock_uom": frappe.db.get_value("Item", row.item_code, "stock_uom"),
"is_cancelled": "No", "is_cancelled": "No" if self.docstatus != 2 else "Yes",
"qty_after_transaction": flt(row.qty, row.precision("qty")), "serial_no": '\n'.join(serial_nos) if serial_nos else '',
"batch_no": row.batch_no,
"valuation_rate": flt(row.valuation_rate, row.precision("valuation_rate")) "valuation_rate": flt(row.valuation_rate, row.precision("valuation_rate"))
}) })
self.make_sl_entries([args])
if not row.batch_no:
data.qty_after_transaction = flt(row.qty, row.precision("qty"))
return data
def delete_and_repost_sle(self): def delete_and_repost_sle(self):
""" Delete Stock Ledger Entries related to this voucher """ Delete Stock Ledger Entries related to this voucher
@ -217,6 +320,15 @@ class StockReconciliation(StockController):
frappe.db.sql("""delete from `tabStock Ledger Entry` frappe.db.sql("""delete from `tabStock Ledger Entry`
where voucher_type=%s and voucher_no=%s""", (self.doctype, self.name)) where voucher_type=%s and voucher_no=%s""", (self.doctype, self.name))
sl_entries = []
for row in self.items:
if row.serial_no or row.batch_no or row.current_serial_no:
self.get_sle_for_serialized_items(row, sl_entries)
if sl_entries:
sl_entries.reverse()
self.make_sl_entries(sl_entries)
# repost future entries for selected item_code, warehouse # repost future entries for selected item_code, warehouse
for entries in existing_entries: for entries in existing_entries:
update_entries_after({ update_entries_after({
@ -310,17 +422,52 @@ def get_items(warehouse, posting_date, posting_time, company):
return res return res
@frappe.whitelist() @frappe.whitelist()
def get_stock_balance_for(item_code, warehouse, posting_date, posting_time): def get_stock_balance_for(item_code, warehouse,
posting_date, posting_time, batch_no=None, with_valuation_rate= True):
frappe.has_permission("Stock Reconciliation", "write", throw = True) frappe.has_permission("Stock Reconciliation", "write", throw = True)
item_dict = frappe.db.get_value("Item", item_code,
["has_serial_no", "has_batch_no"], as_dict=1)
serial_nos = ""
if item_dict.get("has_serial_no"):
qty, rate, serial_nos = get_qty_rate_for_serial_nos(item_code,
warehouse, posting_date, posting_time, item_dict)
else:
qty, rate = get_stock_balance(item_code, warehouse, qty, rate = get_stock_balance(item_code, warehouse,
posting_date, posting_time, with_valuation_rate=True) posting_date, posting_time, with_valuation_rate=with_valuation_rate)
if item_dict.get("has_batch_no"):
qty = get_batch_qty(batch_no, warehouse) or 0
return { return {
'qty': qty, 'qty': qty,
'rate': rate 'rate': rate,
'serial_nos': serial_nos
} }
def get_qty_rate_for_serial_nos(item_code, warehouse, posting_date, posting_time, item_dict):
args = {
"item_code": item_code,
"warehouse": warehouse,
"posting_date": posting_date,
"posting_time": posting_time,
}
serial_nos_list = [serial_no.get("name")
for serial_no in get_available_serial_nos(item_code, warehouse)]
qty = len(serial_nos_list)
serial_nos = '\n'.join(serial_nos_list)
args.update({
'qty': qty,
"serial_nos": serial_nos
})
rate = get_incoming_rate(args, raise_error_if_no_rate=False) or 0
return qty, rate, serial_nos
@frappe.whitelist() @frappe.whitelist()
def get_difference_account(purpose, company): def get_difference_account(purpose, company):
if purpose == 'Stock Reconciliation': if purpose == 'Stock Reconciliation':

View File

@ -13,9 +13,12 @@ from erpnext.stock.stock_ledger import get_previous_sle, update_entries_after
from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import EmptyStockReconciliationItemsError, get_items from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import EmptyStockReconciliationItemsError, get_items
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
from erpnext.stock.doctype.item.test_item import create_item from erpnext.stock.doctype.item.test_item import create_item
from erpnext.stock.utils import get_stock_balance, get_incoming_rate, get_available_serial_nos, get_stock_value_on
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
class TestStockReconciliation(unittest.TestCase): class TestStockReconciliation(unittest.TestCase):
def setUp(self): def setUp(self):
create_batch_or_serial_no_items()
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1) frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1)
self.insert_existing_sle() self.insert_existing_sle()
@ -106,6 +109,135 @@ class TestStockReconciliation(unittest.TestCase):
make_stock_entry(posting_date="2013-01-05", posting_time="07:00", item_code="_Test Item", make_stock_entry(posting_date="2013-01-05", posting_time="07:00", item_code="_Test Item",
target="_Test Warehouse - _TC", qty=15, basic_rate=1200) target="_Test Warehouse - _TC", qty=15, basic_rate=1200)
def test_stock_reco_for_serialized_item(self):
set_perpetual_inventory()
to_delete_records = []
to_delete_serial_nos = []
# Add new serial nos
serial_item_code = "Stock-Reco-Serial-Item-1"
serial_warehouse = "_Test Warehouse for Stock Reco1 - _TC"
sr = create_stock_reconciliation(item_code=serial_item_code,
warehouse = serial_warehouse, qty=5, rate=200)
# print(sr.name)
serial_nos = get_serial_nos(sr.items[0].serial_no)
self.assertEqual(len(serial_nos), 5)
args = {
"item_code": serial_item_code,
"warehouse": serial_warehouse,
"posting_date": nowdate(),
"posting_time": nowtime(),
"serial_no": sr.items[0].serial_no
}
valuation_rate = get_incoming_rate(args)
self.assertEqual(valuation_rate, 200)
to_delete_records.append(sr.name)
sr = create_stock_reconciliation(item_code=serial_item_code,
warehouse = serial_warehouse, qty=5, rate=300, serial_no = '\n'.join(serial_nos))
# print(sr.name)
serial_nos1 = get_serial_nos(sr.items[0].serial_no)
self.assertEqual(len(serial_nos1), 5)
args = {
"item_code": serial_item_code,
"warehouse": serial_warehouse,
"posting_date": nowdate(),
"posting_time": nowtime(),
"serial_no": sr.items[0].serial_no
}
valuation_rate = get_incoming_rate(args)
self.assertEqual(valuation_rate, 300)
to_delete_records.append(sr.name)
to_delete_records.reverse()
for d in to_delete_records:
stock_doc = frappe.get_doc("Stock Reconciliation", d)
stock_doc.cancel()
frappe.delete_doc("Stock Reconciliation", stock_doc.name)
for d in serial_nos + serial_nos1:
if frappe.db.exists("Serial No", d):
frappe.delete_doc("Serial No", d)
def test_stock_reco_for_batch_item(self):
set_perpetual_inventory()
to_delete_records = []
to_delete_serial_nos = []
# Add new serial nos
item_code = "Stock-Reco-batch-Item-1"
warehouse = "_Test Warehouse for Stock Reco2 - _TC"
sr = create_stock_reconciliation(item_code=item_code,
warehouse = warehouse, qty=5, rate=200, do_not_submit=1)
sr.save(ignore_permissions=True)
sr.submit()
self.assertTrue(sr.items[0].batch_no)
to_delete_records.append(sr.name)
sr1 = create_stock_reconciliation(item_code=item_code,
warehouse = warehouse, qty=6, rate=300, batch_no=sr.items[0].batch_no)
args = {
"item_code": item_code,
"warehouse": warehouse,
"posting_date": nowdate(),
"posting_time": nowtime(),
}
valuation_rate = get_incoming_rate(args)
self.assertEqual(valuation_rate, 300)
to_delete_records.append(sr1.name)
sr2 = create_stock_reconciliation(item_code=item_code,
warehouse = warehouse, qty=0, rate=0, batch_no=sr.items[0].batch_no)
stock_value = get_stock_value_on(warehouse, nowdate(), item_code)
self.assertEqual(stock_value, 0)
to_delete_records.append(sr2.name)
to_delete_records.reverse()
for d in to_delete_records:
stock_doc = frappe.get_doc("Stock Reconciliation", d)
stock_doc.cancel()
frappe.delete_doc("Batch", sr.items[0].batch_no)
for d in to_delete_records:
frappe.delete_doc("Stock Reconciliation", d)
def create_batch_or_serial_no_items():
create_warehouse("_Test Warehouse for Stock Reco1",
{"is_group": 0, "parent_warehouse": "_Test Warehouse Group - _TC"})
create_warehouse("_Test Warehouse for Stock Reco2",
{"is_group": 0, "parent_warehouse": "_Test Warehouse Group - _TC"})
serial_item_doc = create_item("Stock-Reco-Serial-Item-1", is_stock_item=1)
if not serial_item_doc.has_serial_no:
serial_item_doc.has_serial_no = 1
serial_item_doc.serial_no_series = "SRSI.####"
serial_item_doc.save(ignore_permissions=True)
batch_item_doc = create_item("Stock-Reco-batch-Item-1", is_stock_item=1)
if not batch_item_doc.has_batch_no:
batch_item_doc.has_batch_no = 1
batch_item_doc.create_new_batch = 1
serial_item_doc.batch_number_series = "BASR.#####"
batch_item_doc.save(ignore_permissions=True)
def create_stock_reconciliation(**args): def create_stock_reconciliation(**args):
args = frappe._dict(args) args = frappe._dict(args)
sr = frappe.new_doc("Stock Reconciliation") sr = frappe.new_doc("Stock Reconciliation")
@ -120,10 +252,13 @@ def create_stock_reconciliation(**args):
"item_code": args.item_code or "_Test Item", "item_code": args.item_code or "_Test Item",
"warehouse": args.warehouse or "_Test Warehouse - _TC", "warehouse": args.warehouse or "_Test Warehouse - _TC",
"qty": args.qty, "qty": args.qty,
"valuation_rate": args.rate "valuation_rate": args.rate,
"serial_no": args.serial_no,
"batch_no": args.batch_no
}) })
try: try:
if not args.do_not_submit:
sr.submit() sr.submit()
except EmptyStockReconciliationItemsError: except EmptyStockReconciliationItemsError:
pass pass
@ -140,3 +275,4 @@ def set_valuation_method(item_code, valuation_method):
}, allow_negative_stock=1) }, allow_negative_stock=1)
test_dependencies = ["Item", "Warehouse"] test_dependencies = ["Item", "Warehouse"]

View File

@ -1,560 +1,182 @@
{ {
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2015-02-17 01:06:05.072764", "creation": "2015-02-17 01:06:05.072764",
"custom": 0,
"docstatus": 0,
"doctype": "DocType", "doctype": "DocType",
"document_type": "Other", "document_type": "Other",
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [
"barcode",
"item_code",
"item_name",
"warehouse",
"column_break_6",
"qty",
"valuation_rate",
"amount",
"serial_no_and_batch_section",
"serial_no",
"column_break_11",
"batch_no",
"section_break_3",
"current_qty",
"current_serial_no",
"column_break_9",
"current_valuation_rate",
"current_amount",
"section_break_14",
"quantity_difference",
"column_break_16",
"amount_difference"
],
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "barcode", "fieldname": "barcode",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Barcode", "label": "Barcode",
"length": 0, "print_hide": 1
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 1,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 3, "columns": 3,
"fieldname": "item_code", "fieldname": "item_code",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 1, "in_global_search": 1,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Item Code", "label": "Item Code",
"length": 0,
"no_copy": 0,
"options": "Item", "options": "Item",
"permlevel": 0, "reqd": 1
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "item_name", "fieldname": "item_name",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 1, "in_global_search": 1,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Item Name", "label": "Item Name",
"length": 0,
"no_copy": 1, "no_copy": 1,
"permlevel": 0,
"precision": "",
"print_hide": 1, "print_hide": 1,
"print_hide_if_no_value": 0, "read_only": 1
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 3, "columns": 3,
"fieldname": "warehouse", "fieldname": "warehouse",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Warehouse", "label": "Warehouse",
"length": 0,
"no_copy": 0,
"options": "Warehouse", "options": "Warehouse",
"permlevel": 0, "reqd": 1
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_6", "fieldname": "column_break_6",
"fieldtype": "Column Break", "fieldtype": "Column Break"
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 2, "columns": 2,
"description": "",
"fieldname": "qty", "fieldname": "qty",
"fieldtype": "Float", "fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0, "label": "Quantity"
"label": "Quantity",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 2, "columns": 2,
"description": "",
"fieldname": "valuation_rate", "fieldname": "valuation_rate",
"fieldtype": "Currency", "fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Valuation Rate", "label": "Valuation Rate",
"length": 0, "options": "Company:company:default_currency"
"no_copy": 0,
"options": "Company:company:default_currency",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "amount", "fieldname": "amount",
"fieldtype": "Currency", "fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Amount", "label": "Amount",
"length": 0,
"no_copy": 0,
"options": "Company:company:default_currency", "options": "Company:company:default_currency",
"permlevel": 0, "read_only": 1
"precision": "", },
"print_hide": 0, {
"print_hide_if_no_value": 0, "fieldname": "serial_no_and_batch_section",
"read_only": 1, "fieldtype": "Section Break",
"remember_last_selected_value": 0, "label": "Serial No and Batch"
"report_hide": 0, },
"reqd": 0, {
"search_index": 0, "fieldname": "serial_no",
"set_only_once": 0, "fieldtype": "Small Text",
"unique": 0 "label": "Serial No"
},
{
"fieldname": "column_break_11",
"fieldtype": "Column Break"
}, },
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "section_break_3", "fieldname": "section_break_3",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"hidden": 0, "label": "Before reconciliation"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Before reconciliation",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0, "default": "0",
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "",
"fieldname": "current_qty", "fieldname": "current_qty",
"fieldtype": "Float", "fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Current Qty", "label": "Current Qty",
"length": 0, "read_only": 1
"no_copy": 0, },
"permlevel": 0, {
"precision": "", "fieldname": "current_serial_no",
"print_hide": 0, "fieldtype": "Small Text",
"print_hide_if_no_value": 0, "label": "Current Serial No",
"read_only": 1, "no_copy": 1,
"remember_last_selected_value": 0, "print_hide": 1,
"report_hide": 0, "read_only": 1
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_9", "fieldname": "column_break_9",
"fieldtype": "Column Break", "fieldtype": "Column Break"
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "",
"fieldname": "current_valuation_rate", "fieldname": "current_valuation_rate",
"fieldtype": "Currency", "fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Current Valuation Rate", "label": "Current Valuation Rate",
"length": 0,
"no_copy": 0,
"options": "Company:company:default_currency", "options": "Company:company:default_currency",
"permlevel": 0, "read_only": 1
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "",
"fieldname": "current_amount", "fieldname": "current_amount",
"fieldtype": "Currency", "fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Current Amount", "label": "Current Amount",
"length": 0,
"no_copy": 0,
"options": "Company:company:default_currency", "options": "Company:company:default_currency",
"permlevel": 0, "read_only": 1
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "section_break_14", "fieldname": "section_break_14",
"fieldtype": "Section Break", "fieldtype": "Section Break"
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "quantity_difference", "fieldname": "quantity_difference",
"fieldtype": "Read Only", "fieldtype": "Read Only",
"hidden": 0, "label": "Quantity Difference"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Quantity Difference",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_16", "fieldname": "column_break_16",
"fieldtype": "Column Break", "fieldtype": "Column Break"
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "amount_difference", "fieldname": "amount_difference",
"fieldtype": "Currency", "fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Amount Difference", "label": "Amount Difference",
"length": 0,
"no_copy": 0,
"options": "Company:company:default_currency", "options": "Company:company:default_currency",
"permlevel": 0, "read_only": 1
"precision": "", },
"print_hide": 0, {
"print_hide_if_no_value": 0, "fieldname": "batch_no",
"read_only": 1, "fieldtype": "Link",
"remember_last_selected_value": 0, "label": "Batch No",
"report_hide": 0, "options": "Batch"
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
} }
], ],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1, "istable": 1,
"max_attachments": 0, "modified": "2019-06-14 17:10:53.188305",
"menu_index": 0,
"modified": "2017-08-03 00:03:40.412071",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Stock Reconciliation Item", "name": "Stock Reconciliation Item",
"name_case": "",
"owner": "Administrator", "owner": "Administrator",
"permissions": [], "permissions": [],
"quick_entry": 1, "quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"track_changes": 1, "track_changes": 1
"track_seen": 0
} }

View File

@ -55,10 +55,12 @@ def get_conditions(filters):
#get all details #get all details
def get_stock_ledger_entries(filters): def get_stock_ledger_entries(filters):
conditions = get_conditions(filters) conditions = get_conditions(filters)
return frappe.db.sql("""select item_code, batch_no, warehouse, return frappe.db.sql("""
posting_date, actual_qty select item_code, batch_no, warehouse, posting_date, sum(actual_qty) as actual_qty
from `tabStock Ledger Entry` from `tabStock Ledger Entry`
where docstatus < 2 and ifnull(batch_no, '') != '' %s order by item_code, warehouse""" % where docstatus < 2 and ifnull(batch_no, '') != '' %s
group by voucher_no, batch_no, item_code, warehouse
order by item_code, warehouse""" %
conditions, as_dict=1) conditions, as_dict=1)
def get_item_warehouse_batch_map(filters, float_precision): def get_item_warehouse_batch_map(filters, float_precision):

View File

@ -157,9 +157,12 @@ class update_entries_after(object):
if sle.serial_no: if sle.serial_no:
self.get_serialized_values(sle) self.get_serialized_values(sle)
self.qty_after_transaction += flt(sle.actual_qty) self.qty_after_transaction += flt(sle.actual_qty)
if sle.voucher_type == "Stock Reconciliation":
self.qty_after_transaction = sle.qty_after_transaction
self.stock_value = flt(self.qty_after_transaction) * flt(self.valuation_rate) self.stock_value = flt(self.qty_after_transaction) * flt(self.valuation_rate)
else: else:
if sle.voucher_type=="Stock Reconciliation": if sle.voucher_type=="Stock Reconciliation" and not sle.batch_no:
# assert # assert
self.valuation_rate = sle.valuation_rate self.valuation_rate = sle.valuation_rate
self.qty_after_transaction = sle.qty_after_transaction self.qty_after_transaction = sle.qty_after_transaction
@ -371,7 +374,7 @@ class update_entries_after(object):
"""get Stock Ledger Entries after a particular datetime, for reposting""" """get Stock Ledger Entries after a particular datetime, for reposting"""
return get_stock_ledger_entries(self.previous_sle or frappe._dict({ return get_stock_ledger_entries(self.previous_sle or frappe._dict({
"item_code": self.args.get("item_code"), "warehouse": self.args.get("warehouse") }), "item_code": self.args.get("item_code"), "warehouse": self.args.get("warehouse") }),
">", "asc", for_update=True) ">", "asc", for_update=True, check_serial_no=False)
def raise_exceptions(self): def raise_exceptions(self):
deficiency = min(e["diff"] for e in self.exceptions) deficiency = min(e["diff"] for e in self.exceptions)
@ -412,7 +415,8 @@ def get_previous_sle(args, for_update=False):
sle = get_stock_ledger_entries(args, "<=", "desc", "limit 1", for_update=for_update) sle = get_stock_ledger_entries(args, "<=", "desc", "limit 1", for_update=for_update)
return sle and sle[0] or {} return sle and sle[0] or {}
def get_stock_ledger_entries(previous_sle, operator=None, order="desc", limit=None, for_update=False, debug=False): def get_stock_ledger_entries(previous_sle, operator=None,
order="desc", limit=None, for_update=False, debug=False, check_serial_no=True):
"""get stock ledger entries filtered by specific posting datetime conditions""" """get stock ledger entries filtered by specific posting datetime conditions"""
conditions = " and timestamp(posting_date, posting_time) {0} timestamp(%(posting_date)s, %(posting_time)s)".format(operator) conditions = " and timestamp(posting_date, posting_time) {0} timestamp(%(posting_date)s, %(posting_time)s)".format(operator)
if previous_sle.get("warehouse"): if previous_sle.get("warehouse"):
@ -420,6 +424,9 @@ def get_stock_ledger_entries(previous_sle, operator=None, order="desc", limit=No
elif previous_sle.get("warehouse_condition"): elif previous_sle.get("warehouse_condition"):
conditions += " and " + previous_sle.get("warehouse_condition") conditions += " and " + previous_sle.get("warehouse_condition")
if check_serial_no and previous_sle.get("serial_no"):
conditions += " and serial_no like {}".format(frappe.db.escape('%{0}%'.format(previous_sle.get("serial_no"))))
if not previous_sle.get("posting_date"): if not previous_sle.get("posting_date"):
previous_sle["posting_date"] = "1900-01-01" previous_sle["posting_date"] = "1900-01-01"
if not previous_sle.get("posting_time"): if not previous_sle.get("posting_time"):
@ -479,6 +486,7 @@ def get_valuation_rate(item_code, warehouse, voucher_type, voucher_no,
if not allow_zero_rate and not valuation_rate and raise_error_if_no_rate \ if not allow_zero_rate and not valuation_rate and raise_error_if_no_rate \
and cint(erpnext.is_perpetual_inventory_enabled(company)): and cint(erpnext.is_perpetual_inventory_enabled(company)):
frappe.local.message_log = [] frappe.local.message_log = []
frappe.throw(_("Valuation rate not found for the Item {0}, which is required to do accounting entries for {1} {2}. If the item is transacting as a zero valuation rate item in the {1}, please mention that in the {1} Item table. Otherwise, please create an incoming stock transaction for the item or mention valuation rate in the Item record, and then try submiting/cancelling this entry").format(item_code, voucher_type, voucher_no)) frappe.throw(_("Valuation rate not found for the Item {0}, which is required to do accounting entries for {1} {2}. If the item is transacting as a zero valuation rate item in the {1}, please mention that in the {1} Item table. Otherwise, please create an incoming stock transaction for the item or mention valuation rate in the Item record, and then try submiting / cancelling this entry.")
.format(item_code, voucher_type, voucher_no))
return valuation_rate return valuation_rate

View File

@ -173,7 +173,7 @@ def get_incoming_rate(args, raise_error_if_no_rate=True):
in_rate = get_valuation_rate(args.get('item_code'), args.get('warehouse'), in_rate = get_valuation_rate(args.get('item_code'), args.get('warehouse'),
args.get('voucher_type'), voucher_no, args.get('allow_zero_valuation'), args.get('voucher_type'), voucher_no, args.get('allow_zero_valuation'),
currency=erpnext.get_company_currency(args.get('company')), company=args.get('company'), currency=erpnext.get_company_currency(args.get('company')), company=args.get('company'),
raise_error_if_no_rate=True) raise_error_if_no_rate=raise_error_if_no_rate)
return in_rate return in_rate
@ -277,3 +277,7 @@ def update_included_uom_in_report(columns, result, include_uom, conversion_facto
new_row.append(None) new_row.append(None)
result[row_idx] = new_row result[row_idx] = new_row
def get_available_serial_nos(item_code, warehouse):
return frappe.get_all("Serial No", filters = {'item_code': item_code,
'warehouse': warehouse, 'delivery_document_no': ''}) or []