Merge branch 'develop' into healthcare_refactor

This commit is contained in:
Rucha Mahabal 2020-04-09 19:16:31 +05:30 committed by GitHub
commit 41520f9fca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 486 additions and 1048 deletions

19
CODEOWNERS Normal file
View File

@ -0,0 +1,19 @@
# Each line is a file pattern followed by one or more owners.
# These owners will be the default owners for everything in
# the repo. Unless a later match takes precedence,
* @nabinhait
manufacturing/ @rohitwaghchaure
accounts/ @deepeshgarg007 @nextchamp-saqib
loan_management/ @deepeshgarg007
pos* @nextchamp-saqib
assets/ @nextchamp-saqib
stock/ @marination @rohitwaghchaure
buying/ @marination @rohitwaghchaure
hr/ @Anurag810
projects/ @hrwX
support/ @hrwX
healthcare/ @ruchamahabal
erpnext_integrations/ @Mangesh-Khairnar
requirements.txt @gavindsouza

View File

@ -1,7 +1,7 @@
frappe.provide("frappe.treeview_settings")
frappe.treeview_settings["Account"] = {
breadcrumbs: "Accounts",
breadcrumb: "Accounts",
title: __("Chart Of Accounts"),
get_tree_root: false,
filters: [

View File

@ -1,5 +1,5 @@
frappe.treeview_settings["Cost Center"] = {
breadcrumbs: "Accounts",
breadcrumb: "Accounts",
get_tree_root: false,
filters: [{
fieldname: "company",

View File

@ -15,11 +15,11 @@ frappe.ui.form.on('Payment Order', {
if (frm.doc.docstatus == 0) {
frm.add_custom_button(__('Payment Request'), function() {
frm.trigger("get_from_payment_request");
}, __("Get from"));
}, __("Get Payments from"));
frm.add_custom_button(__('Payment Entry'), function() {
frm.trigger("get_from_payment_entry");
}, __("Get from"));
}, __("Get Payments from"));
frm.trigger('remove_button');
}

View File

@ -59,7 +59,6 @@
"fieldtype": "Section Break"
},
{
"allow_bulk_edit": 1,
"fieldname": "references",
"fieldtype": "Table",
"label": "Payment Order Reference",
@ -108,7 +107,7 @@
}
],
"is_submittable": 1,
"modified": "2019-05-14 17:12:24.912666",
"modified": "2020-04-06 18:00:56.022642",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Order",

View File

@ -1926,6 +1926,16 @@ class TestSalesInvoice(unittest.TestCase):
item.taxes = []
item.save()
def test_customer_provided_parts_si(self):
create_item('CUST-0987', is_customer_provided_item = 1, customer = '_Test Customer', is_purchase_item = 0)
si = create_sales_invoice(item_code='CUST-0987', rate=0)
self.assertEqual(si.get("items")[0].allow_zero_valuation_rate, 1)
self.assertEqual(si.get("items")[0].amount, 0)
# test if Sales Invoice with rate is allowed
si2 = create_sales_invoice(item_code='CUST-0987', do_not_save=True)
self.assertRaises(frappe.ValidationError, si2.save)
def create_sales_invoice(**args):
si = frappe.new_doc("Sales Invoice")
args = frappe._dict(args)
@ -1948,7 +1958,7 @@ def create_sales_invoice(**args):
"gst_hsn_code": "999800",
"warehouse": args.warehouse or "_Test Warehouse - _TC",
"qty": args.qty or 1,
"rate": args.rate or 100,
"rate": args.rate if args.get("rate") is not None else 100,
"income_account": args.income_account or "Sales - _TC",
"expense_account": args.expense_account or "Cost of Goods Sold - _TC",
"cost_center": args.cost_center or "_Test Cost Center - _TC",

View File

@ -82,7 +82,7 @@ class ShippingRule(Document):
if not shipping_country:
frappe.throw(_('Shipping Address does not have country, which is required for this Shipping Rule'))
if shipping_country not in [d.country for d in self.countries]:
frappe.throw(_('Shipping rule not applicable for country {0}').format(shipping_country))
frappe.throw(_('Shipping rule not applicable for country {0} in Shipping Address').format(shipping_country))
def add_shipping_rule_to_tax_table(self, doc, shipping_amount):
shipping_charge = {

View File

@ -439,7 +439,7 @@ class AccountsController(TransactionBase):
if account_currency not in valid_currency:
frappe.throw(_("Account {0} is invalid. Account Currency must be {1}")
.format(account, _(" or ").join(valid_currency)))
.format(account, (' ' + _("or") + ' ').join(valid_currency)))
def clear_unallocated_advances(self, childtype, parentfield):
self.set(parentfield, self.get(parentfield, {"allocated_amount": ["not in", [0, None, ""]]}))
@ -834,7 +834,7 @@ class AccountsController(TransactionBase):
for d in self.get("payment_schedule"):
if self.doctype == "Sales Order" and getdate(d.due_date) < getdate(self.transaction_date):
frappe.throw(_("Row {0}: Due Date cannot be before posting date").format(d.idx))
frappe.throw(_("Row {0}: Due Date in the Payment Terms table cannot be before Posting Date").format(d.idx))
elif d.due_date in dates:
li.append(_("{0} in row {1}").format(d.due_date, d.idx))
dates.append(d.due_date)

View File

@ -45,6 +45,7 @@ class BuyingController(StockController):
self.validate_warehouse()
self.validate_from_warehouse()
self.set_supplier_address()
self.validate_asset_return()
if self.doctype=="Purchase Invoice":
self.validate_purchase_receipt_if_update_stock()
@ -101,6 +102,19 @@ class BuyingController(StockController):
d.category = 'Total'
msgprint(_('Tax Category has been changed to "Total" because all the Items are non-stock items'))
def validate_asset_return(self):
if self.doctype not in ['Purchase Receipt', 'Purchase Invoice'] or not self.is_return:
return
purchase_doc_field = 'purchase_receipt' if self.doctype == 'Purchase Receipt' else 'purchase_invoice'
not_cancelled_asset = [d.name for d in frappe.db.get_all("Asset", {
purchase_doc_field: self.return_against,
"docstatus": 1
})]
if self.is_return and len(not_cancelled_asset):
frappe.throw(_("{} has submitted assets linked to it. You need to cancel the assets to create purchase return.".format(self.return_against)),
title=_("Not Allowed"))
def get_asset_items(self):
if self.doctype not in ['Purchase Order', 'Purchase Invoice', 'Purchase Receipt']:
return []

View File

@ -21,6 +21,7 @@ class StockController(AccountsController):
super(StockController, self).validate()
self.validate_inspection()
self.validate_serialized_batch()
self.validate_customer_provided_item()
def make_gl_entries(self, gl_entries=None, repost_future_gle=True, from_repost=False):
if self.docstatus == 2:
@ -377,6 +378,15 @@ class StockController(AccountsController):
for blanket_order in blanket_orders:
frappe.get_doc("Blanket Order", blanket_order).update_ordered_qty()
def validate_customer_provided_item(self):
for d in self.get('items'):
# Customer Provided parts will have zero valuation rate
if frappe.db.get_value('Item', d.item_code, 'is_customer_provided_item'):
d.allow_zero_valuation_rate = 1
if d.parenttype in ["Delivery Note", "Sales Invoice"] and d.rate:
frappe.throw(_("Row #{0}: {1} cannot have {2} as it is a Customer Provided Item")
.format(d.idx, frappe.bold(d.item_code), frappe.bold("Rate")))
def update_gl_entries_after(posting_date, posting_time, for_warehouses=None, for_items=None,
warehouse_account=None, company=None):
def _delete_gl_entries(voucher_type, voucher_no):

View File

@ -284,8 +284,7 @@
"depends_on": "eval: doc.__islocal",
"fieldname": "pincode",
"fieldtype": "Data",
"label": "Postal Code",
"options": "Country"
"label": "Postal Code"
},
{
"fieldname": "column_break2",
@ -303,7 +302,8 @@
"fieldtype": "Data",
"label": "Phone",
"oldfieldname": "contact_no",
"oldfieldtype": "Data"
"oldfieldtype": "Data",
"options": "Phone"
},
{
"depends_on": "eval: doc.__islocal",
@ -311,7 +311,8 @@
"fieldtype": "Data",
"label": "Mobile No.",
"oldfieldname": "mobile_no",
"oldfieldtype": "Data"
"oldfieldtype": "Data",
"options": "Phone"
},
{
"depends_on": "eval: doc.__islocal",
@ -445,7 +446,8 @@
"icon": "fa fa-user",
"idx": 5,
"image_field": "image",
"modified": "2020-04-06 20:26:11.687110",
"links": [],
"modified": "2020-04-08 22:26:11.687110",
"modified_by": "Administrator",
"module": "CRM",
"name": "Lead",

View File

@ -46,7 +46,7 @@ def execute(filters=None):
def get_columns():
columns = [
_(" Group Roll No") + "::60",
_("Group Roll No") + "::60",
_("Student ID") + ":Link/Student:90",
_("Student Name") + "::150",
_("Student Mobile No.") + "::110",

View File

@ -121,7 +121,7 @@ def call_mws_method(mws_method, *args, **kwargs):
time.sleep(delay)
continue
mws_settings.enable_synch = 0
mws_settings.enable_sync = 0
mws_settings.save()
frappe.throw(_("Sync has been temporarily disabled because maximum retries have been exceeded"))

View File

@ -39,16 +39,19 @@ __all__ = [
# for a list of the end points and marketplace IDs
MARKETPLACES = {
"CA" : "https://mws.amazonservices.ca", #A2EUQ1WTGCTBG2
"US" : "https://mws.amazonservices.com", #ATVPDKIKX0DER",
"DE" : "https://mws-eu.amazonservices.com", #A1PA6795UKMFR9
"ES" : "https://mws-eu.amazonservices.com", #A1RKKUPIHCS9HS
"FR" : "https://mws-eu.amazonservices.com", #A13V1IB3VIYZZH
"IN" : "https://mws.amazonservices.in", #A21TJRUUN4KGV
"IT" : "https://mws-eu.amazonservices.com", #APJ6JRA9NG5V4
"UK" : "https://mws-eu.amazonservices.com", #A1F83G8C2ARO7P
"JP" : "https://mws.amazonservices.jp", #A1VC38T7YXB528
"CN" : "https://mws.amazonservices.com.cn", #AAHKV2X7AFYLW
"CA": "https://mws.amazonservices.ca", #A2EUQ1WTGCTBG2
"US": "https://mws.amazonservices.com", #ATVPDKIKX0DER",
"DE": "https://mws-eu.amazonservices.com", #A1PA6795UKMFR9
"ES": "https://mws-eu.amazonservices.com", #A1RKKUPIHCS9HS
"FR": "https://mws-eu.amazonservices.com", #A13V1IB3VIYZZH
"IN": "https://mws.amazonservices.in", #A21TJRUUN4KGV
"IT": "https://mws-eu.amazonservices.com", #APJ6JRA9NG5V4
"UK": "https://mws-eu.amazonservices.com", #A1F83G8C2ARO7P
"JP": "https://mws.amazonservices.jp", #A1VC38T7YXB528
"CN": "https://mws.amazonservices.com.cn", #AAHKV2X7AFYLW
"AE": " https://mws.amazonservices.ae", #A2VIGQ35RCS4UG
"MX": "https://mws.amazonservices.com.mx", #A1AM78C64UM0Y8
"BR": "https://mws.amazonservices.com", #A2Q3Y263D00KWC
}

View File

@ -7,14 +7,15 @@ import frappe
from frappe.model.document import Document
import dateutil
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
from erpnext.erpnext_integrations.doctype.amazon_mws_settings.amazon_methods import get_orders
class AmazonMWSSettings(Document):
def validate(self):
if self.enable_amazon == 1:
self.enable_synch = 1
self.enable_sync = 1
setup_custom_fields()
else:
self.enable_synch = 0
self.enable_sync = 0
def get_products_details(self):
if self.enable_amazon == 1:
@ -27,7 +28,7 @@ class AmazonMWSSettings(Document):
def schedule_get_order_details():
mws_settings = frappe.get_doc("Amazon MWS Settings")
if mws_settings.enable_synch and mws_settings.enable_amazon:
if mws_settings.enable_sync and mws_settings.enable_amazon:
after_date = dateutil.parser.parse(mws_settings.after_date).strftime("%Y-%m-%d")
get_orders(after_date = after_date)

View File

@ -259,7 +259,8 @@
"bold": 1,
"fieldname": "emergency_phone_number",
"fieldtype": "Data",
"label": "Emergency Phone"
"label": "Emergency Phone",
"options": "Phone"
},
{
"bold": 1,
@ -480,7 +481,8 @@
{
"fieldname": "cell_number",
"fieldtype": "Data",
"label": "Mobile"
"label": "Mobile",
"options": "Phone"
},
{
"fieldname": "prefered_contact_email",
@ -787,7 +789,7 @@
"idx": 24,
"image_field": "image",
"links": [],
"modified": "2020-01-09 04:23:55.611366",
"modified": "2020-04-08 12:25:34.306695",
"modified_by": "Administrator",
"module": "HR",
"name": "Employee",
@ -834,6 +836,5 @@
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "employee_name",
"track_changes": 1
"title_field": "employee_name"
}

View File

@ -687,8 +687,7 @@ def add_leaves(events, start, end, filter_conditions=None):
"to_date": d.to_date,
"docstatus": d.docstatus,
"color": d.color,
"title": cstr(d.employee_name) + \
(d.half_day and _(" (Half Day)") or ""),
"title": cstr(d.employee_name) + (' ' + _('(Half Day)') if d.half_day else ''),
}
if e not in events:
events.append(e)

View File

@ -356,13 +356,13 @@ class SalarySlip(TransactionBase):
def eval_condition_and_formula(self, d, data):
try:
condition = d.condition.strip() if d.condition else None
condition = d.condition.strip().replace("\n", " ") if d.condition else None
if condition:
if not frappe.safe_eval(condition, self.whitelisted_globals, data):
return None
amount = d.amount
if d.amount_based_on_formula:
formula = d.formula.strip() if d.formula else None
formula = d.formula.strip().replace("\n", " ") if d.formula else None
if formula:
amount = flt(frappe.safe_eval(formula, self.whitelisted_globals, data), d.precision("amount"))
if amount:

View File

@ -665,3 +665,5 @@ erpnext.patches.v12_0.set_received_qty_in_material_request_as_per_stock_uom
erpnext.patches.v12_0.recalculate_requested_qty_in_bin
erpnext.patches.v12_0.update_healthcare_refactored_changes
erpnext.patches.v12_0.set_total_batch_quantity
erpnext.patches.v12_0.rename_mws_settings_fields
erpnext.patches.v12_0.set_updated_purpose_in_pick_list

View File

@ -0,0 +1,11 @@
# Copyright (c) 2020, Frappe and Contributors
# License: GNU General Public License v3. See license.txt
import frappe
def execute():
count = frappe.db.sql("SELECT COUNT(*) FROM `tabSingles` WHERE doctype='Amazon MWS Settings' AND field='enable_sync';")[0][0]
if count == 0:
frappe.db.sql("UPDATE `tabSingles` SET field='enable_sync' WHERE doctype='Amazon MWS Settings' AND field='enable_synch';")
frappe.reload_doc("ERPNext Integrations", "doctype", "Amazon MWS Settings")

View File

@ -1,16 +1,15 @@
import frappe
def execute():
frappe.reload_doctype('Task')
frappe.reload_doctype('Task')
# add "Completed" if customized
for doctype in ('Task'):
property_setter_name = frappe.db.exists('Property Setter', dict(doc_type = doctype, field_name = 'status', property = 'options'))
if property_setter_name:
property_setter = frappe.get_doc('Property Setter', property_setter_name)
if not "Completed" in property_setter.value:
property_setter.value = property_setter.value + '\nCompleted'
property_setter.save()
# add "Completed" if customized
property_setter_name = frappe.db.exists('Property Setter', dict(doc_type='Task', field_name = 'status', property = 'options'))
if property_setter_name:
property_setter = frappe.get_doc('Property Setter', property_setter_name)
if not "Completed" in property_setter.value:
property_setter.value = property_setter.value + '\nCompleted'
property_setter.save()
# renamed default status to Completed as status "Closed" is ambiguous
frappe.db.sql('update tabTask set status = "Completed" where status = "Closed"')
# renamed default status to Completed as status "Closed" is ambiguous
frappe.db.sql('update tabTask set status = "Completed" where status = "Closed"')

View File

@ -0,0 +1,11 @@
# Copyright (c) 2019, Frappe and Contributors
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
import frappe
def execute():
frappe.reload_doc("stock", "doctype", "pick_list")
frappe.db.sql("""UPDATE `tabPick List` set purpose = 'Delivery'
WHERE docstatus = 1 and purpose = 'Delivery against Sales Order' """)

View File

@ -53,7 +53,7 @@ def get_columns():
def get_data(filters):
data = []
if(filters.from_date > filters.to_date):
frappe.msgprint(_(" From Date can not be greater than To Date"))
frappe.msgprint(_("From Date can not be greater than To Date"))
return data
timesheets = get_timesheets(filters)

View File

@ -43,7 +43,6 @@ $.extend(frappe.breadcrumbs.preferred, {
$.extend(frappe.breadcrumbs.module_map, {
'ERPNext Integrations': 'Integrations',
'Geo': 'Settings',
'Accounts': 'Accounting',
'Portal': 'Website',
'Utilities': 'Settings',
'Shopping Cart': 'Website',

View File

@ -894,7 +894,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
shipping_rule: function() {
var me = this;
if(this.frm.doc.shipping_rule) {
if(this.frm.doc.shipping_rule && this.frm.doc.shipping_address) {
return this.frm.call({
doc: this.frm.doc,
method: "apply_shipping_rule",

View File

@ -13,6 +13,7 @@ import json
import zlib
import zipfile
import six
from csv import QUOTE_NONNUMERIC
from six import BytesIO
from six import string_types
import frappe
@ -80,7 +81,7 @@ def get_transactions(filters, as_dict=1):
gl.posting_date as 'Belegdatum',
gl.voucher_no as 'Belegfeld 1',
gl.remarks as 'Buchungstext',
LEFT(gl.remarks, 60) as 'Buchungstext',
gl.voucher_type as 'Beleginfo - Art 1',
gl.voucher_no as 'Beleginfo - Inhalt 1',
gl.against_voucher_type as 'Beleginfo - Art 2',
@ -268,7 +269,9 @@ def get_datev_csv(data, filters, csv_class):
# Do not number rows
index=False,
# Use all columns defined above
columns=csv_class.COLUMNS
columns=csv_class.COLUMNS,
# Quote most fields, even currency values with "," separator
quoting=QUOTE_NONNUMERIC
)
if not six.PY2:
@ -285,6 +288,7 @@ def get_datev_csv(data, filters, csv_class):
def get_header(filters, csv_class):
coa = frappe.get_value("Company", filters.get("company"), "chart_of_accounts")
description = filters.get("voucher_type", csv_class.FORMAT_NAME)
coa_used = "04" if "SKR04" in coa else ("03" if "SKR03" in coa else "")
header = [
@ -323,12 +327,8 @@ def get_header(filters, csv_class):
frappe.utils.formatdate(filters.get('from_date'), "yyyyMMdd"),
# P = Transaction batch end date (YYYYMMDD)
frappe.utils.formatdate(filters.get('to_date'), "yyyyMMdd"),
# Q = Description (for example, "January - February 2019 Transactions")
'"{} - {} {}"'.format(
frappe.utils.formatdate(filters.get('from_date'), "MMMM yyyy"),
frappe.utils.formatdate(filters.get('to_date'), "MMMM yyyy"),
csv_class.FORMAT_NAME
),
# Q = Description (for example, "Sales Invoice") Max. 30 chars
'"{}"'.format(_(description)),
# R = Diktatkürzel
'',
# S = Buchungstyp

View File

@ -1036,7 +1036,7 @@ def create_pick_list(source_name, target_doc=None):
},
}, target_doc)
doc.purpose = 'Delivery against Sales Order'
doc.purpose = 'Delivery'
doc.set_item_locations()

View File

@ -108,8 +108,8 @@ erpnext.stock.ItemDashboard = Class.extend({
if (context.data.length > 0) {
$(frappe.render_template('item_dashboard_list', context)).appendTo(this.result);
} else {
var message = __(" Currently no stock available in any warehouse")
$("<span class='text-muted small'>"+message+"</span>").appendTo(this.result);
var message = __("Currently no stock available in any warehouse");
$(`<span class='text-muted small'> ${message} </span>`).appendTo(this.result);
}
},
get_item_dashboard_data: function(data, max_count, show_item) {

View File

@ -112,7 +112,6 @@ class DeliveryNote(SellingController):
self.so_required()
self.validate_proj_cust()
self.check_sales_order_on_hold_or_close("against_sales_order")
self.validate_for_items()
self.validate_warehouse()
self.validate_uom_is_integer("stock_uom", "stock_qty")
self.validate_uom_is_integer("uom", "qty")
@ -166,12 +165,6 @@ class DeliveryNote(SellingController):
if not res:
frappe.throw(_("Customer {0} does not belong to project {1}").format(self.customer, self.project))
def validate_for_items(self):
for d in self.get('items'):
#Customer Provided parts will have zero valuation rate
if frappe.db.get_value('Item', d.item_code, 'is_customer_provided_item'):
d.allow_zero_valuation_rate = 1
def validate_warehouse(self):
super(DeliveryNote, self).validate_warehouse()

View File

@ -21,6 +21,7 @@ from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation \
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order, create_dn_against_so
from erpnext.accounts.doctype.account.test_account import get_inventory_account, create_account
from erpnext.stock.doctype.warehouse.test_warehouse import get_warehouse
from erpnext.stock.doctype.item.test_item import create_item
class TestDeliveryNote(unittest.TestCase):
def setUp(self):
@ -433,6 +434,15 @@ class TestDeliveryNote(unittest.TestCase):
update_delivery_note_status(dn.name, "Closed")
self.assertEqual(frappe.db.get_value("Delivery Note", dn.name, "Status"), "Closed")
def test_customer_provided_parts_dn(self):
create_item('CUST-0987', is_customer_provided_item = 1, customer = '_Test Customer', is_purchase_item = 0)
dn = create_delivery_note(item_code='CUST-0987', rate=0)
self.assertEqual(dn.get("items")[0].allow_zero_valuation_rate, 1)
# test if Delivery Note with rate is allowed against Customer Provided Item
dn2 = create_delivery_note(item_code='CUST-0987', do_not_save=True)
self.assertRaises(frappe.ValidationError, dn2.save)
def test_dn_billing_status_case1(self):
# SO -> DN -> SI
so = make_sales_order()
@ -671,7 +681,7 @@ def create_delivery_note(**args):
"item_code": args.item or args.item_code or "_Test Item",
"warehouse": args.warehouse or "_Test Warehouse - _TC",
"qty": args.qty or 1,
"rate": args.rate or 100,
"rate": args.rate if args.get("rate") is not None else 100,
"conversion_factor": 1.0,
"allow_zero_valuation_rate": args.allow_zero_valuation_rate or 1,
"expense_account": args.expense_account or "Cost of Goods Sold - _TC",

View File

@ -308,7 +308,7 @@ class Item(WebsiteGenerator):
if self.retain_sample and not frappe.db.get_single_value('Stock Settings', 'sample_retention_warehouse'):
frappe.throw(_("Please select Sample Retention Warehouse in Stock Settings first"))
if self.retain_sample and not self.has_batch_no:
frappe.throw(_(" {0} Retain Sample is based on batch, please check Has Batch No to retain sample of item").format(
frappe.throw(_("{0} Retain Sample is based on batch, please check Has Batch No to retain sample of item").format(
self.item_code))
def get_context(self, context):

View File

@ -454,6 +454,9 @@ def make_stock_entry(source_name, target_doc=None):
else:
target.s_warehouse = obj.warehouse
if source_parent.material_request_type == "Customer Provided":
target.allow_zero_valuation_rate = 1
def set_missing_values(source, target):
target.purpose = source.material_request_type
if source.job_card:
@ -471,7 +474,7 @@ def make_stock_entry(source_name, target_doc=None):
"doctype": "Stock Entry",
"validation": {
"docstatus": ["=", 1],
"material_request_type": ["in", ["Material Transfer", "Material Issue"]]
"material_request_type": ["in", ["Material Transfer", "Material Issue", "Customer Provided"]]
}
},
"Material Request Item": {

View File

@ -38,13 +38,17 @@ frappe.ui.form.on('Pick List', {
};
});
},
get_item_locations: (frm) => {
if (!frm.doc.locations || !frm.doc.locations.length) {
frappe.msgprint(__('First add items in the Item Locations table'));
set_item_locations:(frm, save) => {
if (!(frm.doc.locations && frm.doc.locations.length)) {
frappe.msgprint(__('Add items in the Item Locations table'));
} else {
frm.call('set_item_locations');
frm.call('set_item_locations', {save: save});
}
},
get_item_locations: (frm) => {
// Button on the form
frm.events.set_item_locations(frm, false);
},
refresh: (frm) => {
frm.trigger('add_get_items_button');
if (frm.doc.docstatus === 1) {
@ -52,8 +56,13 @@ frappe.ui.form.on('Pick List', {
'pick_list_name': frm.doc.name,
'purpose': frm.doc.purpose
}).then(target_document_exists => {
frm.set_df_property("locations", "allow_on_submit", target_document_exists ? 0 : 1);
if (target_document_exists) return;
if (frm.doc.purpose === 'Delivery against Sales Order') {
frm.add_custom_button(__('Update Current Stock'), () => frm.trigger('update_pick_list_stock'));
if (frm.doc.purpose === 'Delivery') {
frm.add_custom_button(__('Delivery Note'), () => frm.trigger('create_delivery_note'), __('Create'));
} else {
frm.add_custom_button(__('Stock Entry'), () => frm.trigger('create_stock_entry'), __('Create'));
@ -105,6 +114,7 @@ frappe.ui.form.on('Pick List', {
method: 'erpnext.stock.doctype.pick_list.pick_list.create_delivery_note',
frm: frm
});
},
create_stock_entry: (frm) => {
frappe.xcall('erpnext.stock.doctype.pick_list.pick_list.create_stock_entry', {
@ -114,9 +124,12 @@ frappe.ui.form.on('Pick List', {
frappe.set_route("Form", 'Stock Entry', stock_entry.name);
});
},
update_pick_list_stock: (frm) => {
frm.events.set_item_locations(frm, true);
},
add_get_items_button: (frm) => {
let purpose = frm.doc.purpose;
if (purpose != 'Delivery against Sales Order' || frm.doc.docstatus !== 0) return;
if (purpose != 'Delivery' || frm.doc.docstatus !== 0) return;
let get_query_filters = {
docstatus: 1,
per_delivered: ['<', 100],

View File

@ -1,4 +1,5 @@
{
"actions": [],
"autoname": "naming_series:",
"creation": "2019-07-11 16:03:13.681045",
"doctype": "DocType",
@ -44,7 +45,7 @@
"options": "Warehouse"
},
{
"depends_on": "eval:doc.purpose==='Delivery against Sales Order'",
"depends_on": "eval:doc.purpose==='Delivery'",
"fieldname": "customer",
"fieldtype": "Link",
"in_list_view": 1,
@ -59,6 +60,7 @@
"options": "Work Order"
},
{
"allow_on_submit": 1,
"fieldname": "locations",
"fieldtype": "Table",
"label": "Item Locations",
@ -86,7 +88,7 @@
"fieldname": "purpose",
"fieldtype": "Select",
"label": "Purpose",
"options": "Material Transfer for Manufacture\nMaterial Transfer\nDelivery against Sales Order"
"options": "Material Transfer for Manufacture\nMaterial Transfer\nDelivery"
},
{
"depends_on": "eval:['Material Transfer', 'Material Issue'].includes(doc.purpose)",
@ -111,7 +113,8 @@
}
],
"is_submittable": 1,
"modified": "2019-08-29 21:10:11.572387",
"links": [],
"modified": "2020-03-17 11:38:41.932875",
"modified_by": "Administrator",
"module": "Stock",
"name": "Pick List",

View File

@ -29,7 +29,7 @@ class PickList(Document):
frappe.throw(_('For item {0} at row {1}, count of serial numbers does not match with the picked quantity')
.format(frappe.bold(item.item_code), frappe.bold(item.idx)))
def set_item_locations(self):
def set_item_locations(self, save=False):
items = self.aggregate_item_qty()
self.item_location_map = frappe._dict()
@ -43,7 +43,7 @@ class PickList(Document):
item_code = item_doc.item_code
self.item_location_map.setdefault(item_code,
get_available_item_locations(item_code, from_warehouses, self.item_count_map.get(item_code)))
get_available_item_locations(item_code, from_warehouses, self.item_count_map.get(item_code), self.company))
locations = get_items_with_location_and_quantity(item_doc, self.item_location_map)
@ -59,12 +59,17 @@ class PickList(Document):
location.update(row)
self.append('locations', location)
if save:
self.save()
def aggregate_item_qty(self):
locations = self.get('locations')
self.item_count_map = {}
# aggregate qty for same item
item_map = OrderedDict()
for item in locations:
if not item.item_code:
frappe.throw("Row #{0}: Item Code is Mandatory".format(item.idx))
item_code = item.item_code
reference = item.sales_order_item or item.material_request_item
key = (item_code, item.uom, reference)
@ -85,6 +90,10 @@ class PickList(Document):
return item_map.values()
def validate_item_locations(pick_list):
if not pick_list.locations:
frappe.throw(_("Add items in the Item Locations table"))
def get_items_with_location_and_quantity(item_doc, item_location_map):
available_locations = item_location_map.get(item_doc.item_code)
locations = []
@ -130,14 +139,14 @@ def get_items_with_location_and_quantity(item_doc, item_location_map):
item_location_map[item_doc.item_code] = available_locations
return locations
def get_available_item_locations(item_code, from_warehouses, required_qty):
def get_available_item_locations(item_code, from_warehouses, required_qty, company):
locations = []
if frappe.get_cached_value('Item', item_code, 'has_serial_no'):
locations = get_available_item_locations_for_serialized_item(item_code, from_warehouses, required_qty)
locations = get_available_item_locations_for_serialized_item(item_code, from_warehouses, required_qty, company)
elif frappe.get_cached_value('Item', item_code, 'has_batch_no'):
locations = get_available_item_locations_for_batched_item(item_code, from_warehouses, required_qty)
locations = get_available_item_locations_for_batched_item(item_code, from_warehouses, required_qty, company)
else:
locations = get_available_item_locations_for_other_item(item_code, from_warehouses, required_qty)
locations = get_available_item_locations_for_other_item(item_code, from_warehouses, required_qty, company)
total_qty_available = sum(location.get('qty') for location in locations)
@ -150,9 +159,10 @@ def get_available_item_locations(item_code, from_warehouses, required_qty):
return locations
def get_available_item_locations_for_serialized_item(item_code, from_warehouses, required_qty):
def get_available_item_locations_for_serialized_item(item_code, from_warehouses, required_qty, company):
filters = frappe._dict({
'item_code': item_code,
'company': company,
'warehouse': ['!=', '']
})
@ -180,7 +190,7 @@ def get_available_item_locations_for_serialized_item(item_code, from_warehouses,
return locations
def get_available_item_locations_for_batched_item(item_code, from_warehouses, required_qty):
def get_available_item_locations_for_batched_item(item_code, from_warehouses, required_qty, company):
warehouse_condition = 'and warehouse in %(warehouses)s' if from_warehouses else ''
batch_locations = frappe.db.sql("""
SELECT
@ -192,6 +202,7 @@ def get_available_item_locations_for_batched_item(item_code, from_warehouses, re
WHERE
sle.batch_no = batch.name
and sle.`item_code`=%(item_code)s
and sle.`company` = %(company)s
and IFNULL(batch.`expiry_date`, '2200-01-01') > %(today)s
{warehouse_condition}
GROUP BY
@ -202,16 +213,20 @@ def get_available_item_locations_for_batched_item(item_code, from_warehouses, re
ORDER BY IFNULL(batch.`expiry_date`, '2200-01-01'), batch.`creation`
""".format(warehouse_condition=warehouse_condition), { #nosec
'item_code': item_code,
'company': company,
'today': today(),
'warehouses': from_warehouses
}, as_dict=1)
return batch_locations
def get_available_item_locations_for_other_item(item_code, from_warehouses, required_qty):
def get_available_item_locations_for_other_item(item_code, from_warehouses, required_qty, company):
# gets all items available in different warehouses
warehouses = [x.get('name') for x in frappe.get_list("Warehouse", {'company': company}, "name")]
filters = frappe._dict({
'item_code': item_code,
'warehouse': ['in', warehouses],
'actual_qty': ['>', 0]
})
@ -230,7 +245,9 @@ def get_available_item_locations_for_other_item(item_code, from_warehouses, requ
@frappe.whitelist()
def create_delivery_note(source_name, target_doc=None):
pick_list = frappe.get_doc('Pick List', source_name)
sales_orders = [d.sales_order for d in pick_list.locations]
validate_item_locations(pick_list)
sales_orders = [d.sales_order for d in pick_list.locations if d.sales_order]
sales_orders = set(sales_orders)
delivery_note = None
@ -238,6 +255,10 @@ def create_delivery_note(source_name, target_doc=None):
delivery_note = create_delivery_note_from_sales_order(sales_order,
delivery_note, skip_item_mapping=True)
# map rows without sales orders as well
if not delivery_note:
delivery_note = frappe.new_doc("Delivery Note")
item_table_mapper = {
'doctype': 'Delivery Note Item',
'field_map': {
@ -248,9 +269,25 @@ def create_delivery_note(source_name, target_doc=None):
'condition': lambda doc: abs(doc.delivered_qty) < abs(doc.qty) and doc.delivered_by_supplier!=1
}
item_table_mapper_without_so = {
'doctype': 'Delivery Note Item',
'field_map': {
'rate': 'rate',
'name': 'name',
'parent': '',
}
}
for location in pick_list.locations:
sales_order_item = frappe.get_cached_doc('Sales Order Item', location.sales_order_item)
dn_item = map_child_doc(sales_order_item, delivery_note, item_table_mapper)
if location.sales_order_item:
sales_order_item = frappe.get_cached_doc('Sales Order Item', {'name':location.sales_order_item})
else:
sales_order_item = None
source_doc, table_mapper = [sales_order_item, item_table_mapper] if sales_order_item \
else [location, item_table_mapper_without_so]
dn_item = map_child_doc(source_doc, delivery_note, table_mapper)
if dn_item:
dn_item.warehouse = location.warehouse
@ -258,7 +295,7 @@ def create_delivery_note(source_name, target_doc=None):
dn_item.batch_no = location.batch_no
dn_item.serial_no = location.serial_no
update_delivery_note_item(sales_order_item, dn_item, delivery_note)
update_delivery_note_item(source_doc, dn_item, delivery_note)
set_delivery_note_missing_values(delivery_note)
@ -269,6 +306,7 @@ def create_delivery_note(source_name, target_doc=None):
@frappe.whitelist()
def create_stock_entry(pick_list):
pick_list = frappe.get_doc(json.loads(pick_list))
validate_item_locations(pick_list)
if stock_entry_exists(pick_list.get('name')):
return frappe.msgprint(_('Stock Entry has been already created against this Pick List'))
@ -318,7 +356,7 @@ def get_pending_work_orders(doctype, txt, searchfield, start, page_length, filte
@frappe.whitelist()
def target_document_exists(pick_list_name, purpose):
if purpose == 'Delivery against Sales Order':
if purpose == 'Delivery':
return frappe.db.exists('Delivery Note', {
'pick_list': pick_list_name
})

View File

@ -1,4 +1,5 @@
{
"actions": [],
"creation": "2019-07-11 16:01:22.832885",
"doctype": "DocType",
"editable_grid": 1,
@ -8,6 +9,7 @@
"item_name",
"column_break_2",
"description",
"item_group",
"section_break_5",
"warehouse",
"quantity_section",
@ -120,7 +122,8 @@
"fieldtype": "Link",
"in_list_view": 1,
"label": "Item",
"options": "Item"
"options": "Item",
"reqd": 1
},
{
"fieldname": "quantity_section",
@ -166,10 +169,18 @@
"fieldtype": "Data",
"label": "Material Request Item",
"read_only": 1
},
{
"fetch_from": "item_code.item_group",
"fieldname": "item_group",
"fieldtype": "Data",
"label": "Item Group",
"read_only": 1
}
],
"istable": 1,
"modified": "2019-08-29 21:28:39.539007",
"links": [],
"modified": "2020-03-13 19:08:21.995986",
"modified_by": "Administrator",
"module": "Stock",
"name": "Pick List Item",

View File

@ -376,6 +376,33 @@ class TestPurchaseReceipt(unittest.TestCase):
location = frappe.db.get_value('Asset', assets[0].name, 'location')
self.assertEquals(location, "Test Location")
def test_purchase_return_with_submitted_asset(self):
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_return
pr = make_purchase_receipt(item_code="Test Asset Item", qty=1)
asset = frappe.get_doc("Asset", {
'purchase_receipt': pr.name
})
asset.available_for_use_date = frappe.utils.nowdate()
asset.gross_purchase_amount = 50.0
asset.append("finance_books", {
"expected_value_after_useful_life": 10,
"depreciation_method": "Straight Line",
"total_number_of_depreciations": 3,
"frequency_of_depreciation": 1,
"depreciation_start_date": frappe.utils.nowdate()
})
asset.submit()
pr_return = make_purchase_return(pr.name)
self.assertRaises(frappe.exceptions.ValidationError, pr_return.submit)
asset.load_from_db()
asset.cancel()
pr_return.submit()
def test_purchase_receipt_for_enable_allow_cost_center_in_entry_of_bs_account(self):
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
accounts_settings = frappe.get_doc('Accounts Settings', 'Accounts Settings')

View File

@ -50,6 +50,7 @@ class StockEntry(StockController):
self.validate_posting_time()
self.validate_purpose()
self.validate_item()
self.validate_customer_provided_item()
self.validate_qty()
self.set_transfer_qty()
self.validate_uom_is_integer("uom", "qty")
@ -203,10 +204,6 @@ class StockEntry(StockController):
frappe.throw(_("Row #{0}: Please specify Serial No for Item {1}").format(item.idx, item.item_code),
frappe.MandatoryError)
#Customer Provided parts will have zero valuation rate
if frappe.db.get_value('Item', item.item_code, 'is_customer_provided_item'):
item.allow_zero_valuation_rate = 1
def validate_qty(self):
manufacture_purpose = ["Manufacture", "Material Consumption for Manufacture"]

View File

@ -744,7 +744,7 @@ class TestStockEntry(unittest.TestCase):
def test_customer_provided_parts_se(self):
create_item('CUST-0987', is_customer_provided_item = 1, customer = '_Test Customer', is_purchase_item = 0)
se = make_stock_entry(item_code='CUST-0987', purporse = 'Material Receipt', qty=4, to_warehouse = "_Test Warehouse - _TC")
se = make_stock_entry(item_code='CUST-0987', purpose = 'Material Receipt', qty=4, to_warehouse = "_Test Warehouse - _TC")
self.assertEqual(se.get("items")[0].allow_zero_valuation_rate, 1)
self.assertEqual(se.get("items")[0].amount, 0)