Merge branch 'develop' into fix-minor-custom-cash-flow

This commit is contained in:
Saqib Ansari 2022-03-25 17:16:42 +05:30 committed by GitHub
commit 80c7d7064b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
77 changed files with 778 additions and 690 deletions

View File

@ -29,6 +29,8 @@ ignore =
B950,
W191,
E124, # closing bracket, irritating while writing QB code
E131, # continuation line unaligned for hanging indent
E123, # closing bracket does not match indentation of opening bracket's line
max-line-length = 200
exclude=.github/helper/semgrep_rules

View File

@ -134,7 +134,8 @@
{
"fieldname": "allocated_amount",
"fieldtype": "Currency",
"label": "Allocated Amount"
"label": "Allocated Amount",
"options": "currency"
},
{
"fieldname": "amended_from",
@ -152,7 +153,8 @@
{
"fieldname": "unallocated_amount",
"fieldtype": "Currency",
"label": "Unallocated Amount"
"label": "Unallocated Amount",
"options": "currency"
},
{
"fieldname": "party_section",
@ -192,10 +194,11 @@
],
"is_submittable": 1,
"links": [],
"modified": "2021-04-14 17:31:58.963529",
"modified": "2022-03-21 19:05:04.208222",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bank Transaction",
"naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{
@ -242,6 +245,7 @@
],
"sort_field": "date",
"sort_order": "DESC",
"states": [],
"title_field": "bank_account",
"track_changes": 1
}

View File

@ -110,13 +110,12 @@
"description": "Reference number of the invoice from the previous system",
"fieldname": "invoice_number",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Invoice Number"
}
],
"istable": 1,
"links": [],
"modified": "2021-12-17 19:25:06.053187",
"modified": "2022-03-21 19:31:45.382656",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Opening Invoice Creation Tool Item",
@ -125,5 +124,6 @@
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@ -264,7 +264,6 @@
"print_hide": 1
},
{
"allow_on_submit": 1,
"default": "0",
"fieldname": "is_return",
"fieldtype": "Check",
@ -1573,7 +1572,7 @@
"icon": "fa fa-file-text",
"is_submittable": 1,
"links": [],
"modified": "2021-10-05 12:11:53.871828",
"modified": "2022-03-22 13:00:24.166684",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Invoice",
@ -1623,6 +1622,7 @@
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"timeline_field": "customer",
"title_field": "title",
"track_changes": 1,

View File

@ -16,7 +16,11 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
)
from erpnext.accounts.party import get_due_date, get_party_account
from erpnext.stock.doctype.batch.batch import get_batch_qty, get_pos_reserved_batch_qty
from erpnext.stock.doctype.serial_no.serial_no import get_pos_reserved_serial_nos, get_serial_nos
from erpnext.stock.doctype.serial_no.serial_no import (
get_delivered_serial_nos,
get_pos_reserved_serial_nos,
get_serial_nos,
)
class POSInvoice(SalesInvoice):
@ -145,12 +149,7 @@ class POSInvoice(SalesInvoice):
.format(item.idx, bold_invalid_batch_no, bold_item_name, bold_extra_batch_qty_needed), title=_("Item Unavailable"))
def validate_delivered_serial_nos(self, item):
serial_nos = get_serial_nos(item.serial_no)
delivered_serial_nos = frappe.db.get_list('Serial No', {
'item_code': item.item_code,
'name': ['in', serial_nos],
'sales_invoice': ['is', 'set']
}, pluck='name')
delivered_serial_nos = get_delivered_serial_nos(item.serial_no)
if delivered_serial_nos:
bold_delivered_serial_nos = frappe.bold(', '.join(delivered_serial_nos))
@ -172,10 +171,14 @@ class POSInvoice(SalesInvoice):
frappe.throw(error_msg, title=_("Invalid Item"), as_list=True)
def validate_stock_availablility(self):
if self.is_return:
return
if self.docstatus.is_draft() and not frappe.db.get_value('POS Profile', self.pos_profile, 'validate_stock_on_save'):
return
from erpnext.stock.stock_ledger import is_negative_stock_allowed
if self.is_return or self.docstatus != 1:
return
for d in self.get('items'):
is_service_item = not (frappe.db.get_value('Item', d.get('item_code'), 'is_stock_item'))
if is_service_item:
@ -485,16 +488,15 @@ class POSInvoice(SalesInvoice):
"payment_account": pay.account,
}, ["name"])
args = {
'doctype': 'Payment Request',
filters = {
'reference_doctype': 'POS Invoice',
'reference_name': self.name,
'payment_gateway_account': payment_gateway_account,
'email_to': self.contact_mobile
}
pr = frappe.db.exists(args)
pr = frappe.db.get_value('Payment Request', filters=filters)
if pr:
return frappe.get_doc('Payment Request', pr[0][0])
return frappe.get_doc('Payment Request', pr)
@frappe.whitelist()
def get_stock_availability(item_code, warehouse):

View File

@ -340,6 +340,7 @@ class TestPOSInvoice(unittest.TestCase):
item=se.get("items")[0].item_code, rate=1000, do_not_save=1)
si.get("items")[0].serial_no = serial_nos[0]
si.update_stock = 1
si.insert()
si.submit()
@ -610,6 +611,78 @@ class TestPOSInvoice(unittest.TestCase):
pos_inv.delete()
pr.delete()
def test_delivered_serial_no_case(self):
from erpnext.accounts.doctype.pos_invoice_merge_log.test_pos_invoice_merge_log import (
init_user_and_profile,
)
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
from erpnext.stock.doctype.serial_no.test_serial_no import get_serial_nos
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
frappe.db.savepoint('before_test_delivered_serial_no_case')
try:
se = make_serialized_item()
serial_no = get_serial_nos(se.get("items")[0].serial_no)[0]
dn = create_delivery_note(
item_code="_Test Serialized Item With Series", serial_no=serial_no
)
delivery_document_no = frappe.db.get_value("Serial No", serial_no, "delivery_document_no")
self.assertEquals(delivery_document_no, dn.name)
init_user_and_profile()
pos_inv = create_pos_invoice(
item_code="_Test Serialized Item With Series",
serial_no=serial_no,
qty=1,
rate=100,
do_not_submit=True
)
self.assertRaises(frappe.ValidationError, pos_inv.submit)
finally:
frappe.db.rollback(save_point='before_test_delivered_serial_no_case')
frappe.set_user("Administrator")
def test_returned_serial_no_case(self):
from erpnext.accounts.doctype.pos_invoice_merge_log.test_pos_invoice_merge_log import (
init_user_and_profile,
)
from erpnext.stock.doctype.serial_no.serial_no import get_pos_reserved_serial_nos
from erpnext.stock.doctype.serial_no.test_serial_no import get_serial_nos
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
frappe.db.savepoint('before_test_returned_serial_no_case')
try:
se = make_serialized_item()
serial_no = get_serial_nos(se.get("items")[0].serial_no)[0]
init_user_and_profile()
pos_inv = create_pos_invoice(
item_code="_Test Serialized Item With Series",
serial_no=serial_no,
qty=1,
rate=100,
)
pos_return = make_sales_return(pos_inv.name)
pos_return.flags.ignore_validate = True
pos_return.insert()
pos_return.submit()
pos_reserved_serial_nos = get_pos_reserved_serial_nos({
'item_code': '_Test Serialized Item With Series',
'warehouse': '_Test Warehouse - _TC'
})
self.assertTrue(serial_no not in pos_reserved_serial_nos)
finally:
frappe.db.rollback(save_point='before_test_returned_serial_no_case')
frappe.set_user("Administrator")
def create_pos_invoice(**args):
args = frappe._dict(args)

View File

@ -53,7 +53,7 @@ class POSInvoiceMergeLog(Document):
frappe.throw(msg)
def on_submit(self):
pos_invoice_docs = [frappe.get_doc("POS Invoice", d.pos_invoice) for d in self.pos_invoices]
pos_invoice_docs = [frappe.get_cached_doc("POS Invoice", d.pos_invoice) for d in self.pos_invoices]
returns = [d for d in pos_invoice_docs if d.get('is_return') == 1]
sales = [d for d in pos_invoice_docs if d.get('is_return') == 0]
@ -70,7 +70,7 @@ class POSInvoiceMergeLog(Document):
self.update_pos_invoices(pos_invoice_docs, sales_invoice, credit_note)
def on_cancel(self):
pos_invoice_docs = [frappe.get_doc("POS Invoice", d.pos_invoice) for d in self.pos_invoices]
pos_invoice_docs = [frappe.get_cached_doc("POS Invoice", d.pos_invoice) for d in self.pos_invoices]
self.update_pos_invoices(pos_invoice_docs)
self.cancel_linked_invoices()
@ -254,7 +254,7 @@ def get_all_unconsolidated_invoices():
'docstatus': 1
}
pos_invoices = frappe.db.get_all('POS Invoice', filters=filters,
fields=["name as pos_invoice", 'posting_date', 'grand_total', 'customer'])
fields=["name as pos_invoice", 'posting_date', 'grand_total', 'customer', 'is_return', 'return_against'])
return pos_invoices
@ -294,17 +294,62 @@ def unconsolidate_pos_invoices(closing_entry):
else:
cancel_merge_logs(merge_logs, closing_entry)
def split_invoices(invoices):
'''
Splits invoices into multiple groups
Use-case:
If a serial no is sold and later it is returned
then split the invoices such that the selling entry is merged first and then the return entry
'''
# Input
# invoices = [
# {'pos_invoice': 'Invoice with SR#1 & SR#2', 'is_return': 0},
# {'pos_invoice': 'Invoice with SR#1', 'is_return': 1},
# {'pos_invoice': 'Invoice with SR#2', 'is_return': 0}
# ]
# Output
# _invoices = [
# [{'pos_invoice': 'Invoice with SR#1 & SR#2', 'is_return': 0}],
# [{'pos_invoice': 'Invoice with SR#1', 'is_return': 1}, {'pos_invoice': 'Invoice with SR#2', 'is_return': 0}],
# ]
_invoices = []
special_invoices = []
pos_return_docs = [frappe.get_cached_doc("POS Invoice", d.pos_invoice) for d in invoices if d.is_return and d.return_against]
for pos_invoice in pos_return_docs:
for item in pos_invoice.items:
if not item.serial_no:
continue
return_against_is_added = any(d for d in _invoices if d.pos_invoice == pos_invoice.return_against)
if return_against_is_added:
break
return_against_is_consolidated = frappe.db.get_value('POS Invoice', pos_invoice.return_against, 'status', cache=True) == 'Consolidated'
if return_against_is_consolidated:
break
pos_invoice_row = [d for d in invoices if d.pos_invoice == pos_invoice.return_against]
_invoices.append(pos_invoice_row)
special_invoices.append(pos_invoice.return_against)
break
_invoices.append([d for d in invoices if d.pos_invoice not in special_invoices])
return _invoices
def create_merge_logs(invoice_by_customer, closing_entry=None):
try:
for customer, invoices in invoice_by_customer.items():
merge_log = frappe.new_doc('POS Invoice Merge Log')
merge_log.posting_date = getdate(closing_entry.get('posting_date')) if closing_entry else nowdate()
merge_log.customer = customer
merge_log.pos_closing_entry = closing_entry.get('name') if closing_entry else None
for _invoices in split_invoices(invoices):
merge_log = frappe.new_doc('POS Invoice Merge Log')
merge_log.posting_date = getdate(closing_entry.get('posting_date')) if closing_entry else nowdate()
merge_log.customer = customer
merge_log.pos_closing_entry = closing_entry.get('name') if closing_entry else None
merge_log.set('pos_invoices', invoices)
merge_log.save(ignore_permissions=True)
merge_log.submit()
merge_log.set('pos_invoices', _invoices)
merge_log.save(ignore_permissions=True)
merge_log.submit()
if closing_entry:
closing_entry.set_status(update=True, status='Submitted')

View File

@ -382,6 +382,68 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
consolidated_invoice = frappe.get_doc('Sales Invoice', inv.consolidated_invoice)
self.assertEqual(consolidated_invoice.rounding_adjustment, 1)
finally:
frappe.set_user("Administrator")
frappe.db.sql("delete from `tabPOS Profile`")
frappe.db.sql("delete from `tabPOS Invoice`")
def test_serial_no_case_1(self):
'''
Create a POS Invoice with serial no
Create a Return Invoice with serial no
Create a POS Invoice with serial no again
Consolidate the invoices
The first POS Invoice should be consolidated with a separate single Merge Log
The second and third POS Invoice should be consolidated with a single Merge Log
'''
from erpnext.stock.doctype.serial_no.test_serial_no import get_serial_nos
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
frappe.db.sql("delete from `tabPOS Invoice`")
try:
se = make_serialized_item()
serial_no = get_serial_nos(se.get("items")[0].serial_no)[0]
init_user_and_profile()
pos_inv = create_pos_invoice(
item_code="_Test Serialized Item With Series",
serial_no=serial_no,
qty=1,
rate=100,
do_not_submit=1
)
pos_inv.append('payments', {
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 100
})
pos_inv.submit()
pos_inv_cn = make_sales_return(pos_inv.name)
pos_inv_cn.paid_amount = -100
pos_inv_cn.submit()
pos_inv2 = create_pos_invoice(
item_code="_Test Serialized Item With Series",
serial_no=serial_no,
qty=1,
rate=100,
do_not_submit=1
)
pos_inv2.append('payments', {
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 100
})
pos_inv2.submit()
consolidate_pos_invoices()
pos_inv.load_from_db()
pos_inv2.load_from_db()
self.assertNotEqual(pos_inv.consolidated_invoice, pos_inv2.consolidated_invoice)
finally:
frappe.set_user("Administrator")
frappe.db.sql("delete from `tabPOS Profile`")

View File

@ -9,7 +9,9 @@
"posting_date",
"column_break_3",
"customer",
"grand_total"
"grand_total",
"is_return",
"return_against"
],
"fields": [
{
@ -48,11 +50,27 @@
"in_list_view": 1,
"label": "Amount",
"reqd": 1
},
{
"default": "0",
"fetch_from": "pos_invoice.is_return",
"fieldname": "is_return",
"fieldtype": "Check",
"label": "Is Return",
"read_only": 1
},
{
"fetch_from": "pos_invoice.return_against",
"fieldname": "return_against",
"fieldtype": "Link",
"label": "Return Against",
"options": "POS Invoice",
"read_only": 1
}
],
"istable": 1,
"links": [],
"modified": "2020-05-29 15:08:42.194979",
"modified": "2022-03-24 13:32:02.366257",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Invoice Reference",
@ -61,5 +79,6 @@
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@ -22,6 +22,7 @@
"hide_images",
"hide_unavailable_items",
"auto_add_item_to_cart",
"validate_stock_on_save",
"column_break_16",
"update_stock",
"ignore_pricing_rule",
@ -351,6 +352,12 @@
{
"fieldname": "column_break_25",
"fieldtype": "Column Break"
},
{
"default": "0",
"fieldname": "validate_stock_on_save",
"fieldtype": "Check",
"label": "Validate Stock on Save"
}
],
"icon": "icon-cog",
@ -378,10 +385,11 @@
"link_fieldname": "pos_profile"
}
],
"modified": "2021-10-14 14:17:00.469298",
"modified": "2022-03-21 13:29:28.480533",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Profile",
"naming_rule": "Set by user",
"owner": "Administrator",
"permissions": [
{
@ -404,5 +412,6 @@
}
],
"sort_field": "modified",
"sort_order": "DESC"
"sort_order": "DESC",
"states": []
}

View File

@ -1711,6 +1711,7 @@ def make_delivery_note(source_name, target_doc=None):
}
}, target_doc, set_missing_values)
doclist.set_onload('ignore_price_list', True)
return doclist
@frappe.whitelist()

View File

@ -442,6 +442,8 @@ def make_purchase_receipt(source_name, target_doc=None):
}
}, target_doc, set_missing_values)
doc.set_onload('ignore_price_list', True)
return doc
@frappe.whitelist()
@ -509,6 +511,7 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions
doc = get_mapped_doc("Purchase Order", source_name, fields,
target_doc, postprocess, ignore_permissions=ignore_permissions)
doc.set_onload('ignore_price_list', True)
return doc

View File

@ -139,6 +139,7 @@ def make_purchase_order(source_name, target_doc=None):
},
}, target_doc, set_missing_values)
doclist.set_onload('ignore_price_list', True)
return doclist
@frappe.whitelist()

View File

@ -168,7 +168,7 @@ def tax_account_query(doctype, txt, searchfield, start, page_len, filters):
{account_type_condition}
AND is_group = 0
AND company = %(company)s
AND account_currency = %(currency)s
AND (account_currency = %(currency)s or ifnull(account_currency, '') = '')
AND `{searchfield}` LIKE %(txt)s
{mcond}
ORDER BY idx DESC, name

View File

@ -399,6 +399,8 @@ def make_return_doc(doctype, source_name, target_doc=None):
}
}, target_doc, set_missing_values)
doclist.set_onload('ignore_price_list', True)
return doclist
def get_rate_for_return(voucher_type, voucher_no, item_code, return_against=None,

View File

@ -114,20 +114,16 @@ class calculate_taxes_and_totals(object):
for item in self.doc.get("items"):
self.doc.round_floats_in(item)
if not item.rate:
item.rate = item.price_list_rate
if item.discount_percentage == 100:
item.rate = 0.0
elif item.price_list_rate:
if item.pricing_rules or abs(item.discount_percentage) > 0:
if not item.rate or (item.pricing_rules and item.discount_percentage > 0):
item.rate = flt(item.price_list_rate *
(1.0 - (item.discount_percentage / 100.0)), item.precision("rate"))
if abs(item.discount_percentage) > 0:
item.discount_amount = item.price_list_rate * (item.discount_percentage / 100.0)
item.discount_amount = item.price_list_rate * (item.discount_percentage / 100.0)
elif item.discount_amount or item.pricing_rules:
elif item.discount_amount and item.pricing_rules:
item.rate = item.price_list_rate - item.discount_amount
if item.doctype in ['Quotation Item', 'Sales Order Item', 'Delivery Note Item', 'Sales Invoice Item',

View File

@ -225,9 +225,7 @@ def _check_agent_availability(agent_email, scheduled_time):
def _get_employee_from_user(user):
employee_docname = frappe.db.exists(
{'doctype': 'Employee', 'user_id': user})
employee_docname = frappe.db.get_value('Employee', {'user_id': user})
if employee_docname:
# frappe.db.exists returns a tuple of a tuple
return frappe.get_doc('Employee', employee_docname[0][0])
return frappe.get_doc('Employee', employee_docname)
return None

View File

@ -8,50 +8,44 @@ import frappe
def create_test_lead():
test_lead = frappe.db.exists({'doctype': 'Lead', 'email_id':'test@example.com'})
if test_lead:
return frappe.get_doc('Lead', test_lead[0][0])
test_lead = frappe.get_doc({
'doctype': 'Lead',
'lead_name': 'Test Lead',
'email_id': 'test@example.com'
})
test_lead.insert(ignore_permissions=True)
return test_lead
test_lead = frappe.db.get_value("Lead", {"email_id": "test@example.com"})
if test_lead:
return frappe.get_doc("Lead", test_lead)
test_lead = frappe.get_doc(
{"doctype": "Lead", "lead_name": "Test Lead", "email_id": "test@example.com"}
)
test_lead.insert(ignore_permissions=True)
return test_lead
def create_test_appointments():
test_appointment = frappe.db.exists(
{'doctype': 'Appointment', 'scheduled_time':datetime.datetime.now(),'email':'test@example.com'})
if test_appointment:
return frappe.get_doc('Appointment', test_appointment[0][0])
test_appointment = frappe.get_doc({
'doctype': 'Appointment',
'email': 'test@example.com',
'status': 'Open',
'customer_name': 'Test Lead',
'customer_phone_number': '666',
'customer_skype': 'test',
'customer_email': 'test@example.com',
'scheduled_time': datetime.datetime.now()
})
test_appointment.insert()
return test_appointment
test_appointment = frappe.get_doc(
{
"doctype": "Appointment",
"email": "test@example.com",
"status": "Open",
"customer_name": "Test Lead",
"customer_phone_number": "666",
"customer_skype": "test",
"customer_email": "test@example.com",
"scheduled_time": datetime.datetime.now(),
}
)
test_appointment.insert()
return test_appointment
class TestAppointment(unittest.TestCase):
test_appointment = test_lead = None
test_appointment = test_lead = None
def setUp(self):
self.test_lead = create_test_lead()
self.test_appointment = create_test_appointments()
def setUp(self):
self.test_lead = create_test_lead()
self.test_appointment = create_test_appointments()
def test_calendar_event_created(self):
cal_event = frappe.get_doc(
'Event', self.test_appointment.calendar_event)
self.assertEqual(cal_event.starts_on,
self.test_appointment.scheduled_time)
def test_calendar_event_created(self):
cal_event = frappe.get_doc("Event", self.test_appointment.calendar_event)
self.assertEqual(cal_event.starts_on, self.test_appointment.scheduled_time)
def test_lead_linked(self):
lead = frappe.get_doc('Lead', self.test_lead.name)
self.assertIsNotNone(lead)
def test_lead_linked(self):
lead = frappe.get_doc("Lead", self.test_lead.name)
self.assertIsNotNone(lead)

View File

@ -5,6 +5,12 @@ frappe.ui.form.on('Website Item', {
onload: function(frm) {
// should never check Private
frm.fields_dict["website_image"].df.is_private = 0;
frm.set_query("website_warehouse", () => {
return {
filters: {"is_group": 0}
};
});
},
image: function() {

View File

@ -418,6 +418,22 @@ erpnext.ProductView = class {
me.change_route_with_filters();
});
// bind filter lookup input box
$('.filter-lookup-input').on('keydown', frappe.utils.debounce((e) => {
const $input = $(e.target);
const keyword = ($input.val() || '').toLowerCase();
const $filter_options = $input.next('.filter-options');
$filter_options.find('.filter-lookup-wrapper').show();
$filter_options.find('.filter-lookup-wrapper').each((i, el) => {
const $el = $(el);
const value = $el.data('value').toLowerCase();
if (!value.includes(keyword)) {
$el.hide();
}
});
}, 300));
}
change_route_with_filters() {

View File

@ -15,6 +15,7 @@ class TestVariantSelector(FrappeTestCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
template_item = make_item("Test-Tshirt-Temp", {
"has_variant": 1,
"variant_based_on": "Item Attribute",

View File

@ -3,7 +3,6 @@
import frappe
from erpnext.setup.utils import insert_record
def setup_education():
@ -13,6 +12,21 @@ def setup_education():
return
create_academic_sessions()
def insert_record(records):
for r in records:
doc = frappe.new_doc(r.get("doctype"))
doc.update(r)
try:
doc.insert(ignore_permissions=True)
except frappe.DuplicateEntryError as e:
# pass DuplicateEntryError and continue
if e.args and e.args[0]==doc.doctype and e.args[1]==doc.name:
# make sure DuplicateEntryError is for the exact same doc and not a related doc
pass
else:
raise
def create_academic_sessions():
data = [
{"doctype": "Academic Year", "academic_year_name": "2015-16"},

View File

@ -25,6 +25,7 @@ from erpnext.hr.doctype.leave_application.leave_application import (
LeaveDayBlockedError,
NotAnOptionalHoliday,
OverlapError,
get_leave_allocation_records,
get_leave_balance_on,
get_leave_details,
)
@ -882,6 +883,27 @@ class TestLeaveApplication(unittest.TestCase):
self.assertEqual(leave_allocation['leaves_pending_approval'], 1)
self.assertEqual(leave_allocation['remaining_leaves'], 26)
@set_holiday_list('Salary Slip Test Holiday List', '_Test Company')
def test_get_leave_allocation_records(self):
employee = get_employee()
leave_type = create_leave_type(
leave_type_name="_Test_CF_leave_expiry",
is_carry_forward=1,
expire_carry_forwarded_leaves_after_days=90)
leave_type.insert()
leave_alloc = create_carry_forwarded_allocation(employee, leave_type)
details = get_leave_allocation_records(employee.name, getdate(), leave_type.name)
expected_data = {
"from_date": getdate(leave_alloc.from_date),
"to_date": getdate(leave_alloc.to_date),
"total_leaves_allocated": 30.0,
"unused_leaves": 15.0,
"new_leaves_allocated": 15.0,
"leave_type": leave_type.name
}
self.assertEqual(details.get(leave_type.name), expected_data)
def create_carry_forwarded_allocation(employee, leave_type):
# initial leave allocation
@ -903,6 +925,8 @@ def create_carry_forwarded_allocation(employee, leave_type):
carry_forward=1)
leave_allocation.submit()
return leave_allocation
def make_allocation_record(employee=None, leave_type=None, from_date=None, to_date=None, carry_forward=False, leaves=None):
allocation = frappe.get_doc({
"doctype": "Leave Allocation",
@ -931,12 +955,9 @@ def set_leave_approver():
dept_doc.save(ignore_permissions=True)
def get_leave_period():
leave_period_name = frappe.db.exists({
"doctype": "Leave Period",
"company": "_Test Company"
})
leave_period_name = frappe.db.get_value("Leave Period", {"company": "_Test Company"})
if leave_period_name:
return frappe.get_doc("Leave Period", leave_period_name[0][0])
return frappe.get_doc("Leave Period", leave_period_name)
else:
return frappe.get_doc(dict(
name = 'Test Leave Period',

View File

@ -500,6 +500,9 @@ class JobCard(Document):
2: "Cancelled"
}[self.docstatus or 0]
if self.for_quantity <= self.transferred_qty:
self.status = 'Material Transferred'
if self.time_logs:
self.status = 'Work In Progress'
@ -507,10 +510,6 @@ class JobCard(Document):
(self.for_quantity <= self.total_completed_qty or not self.items)):
self.status = 'Completed'
if self.status != 'Completed':
if self.for_quantity <= self.transferred_qty:
self.status = 'Material Transferred'
if update_status:
self.db_set('status', self.status)

View File

@ -169,6 +169,7 @@ class TestJobCard(FrappeTestCase):
job_card_name = frappe.db.get_value("Job Card", {'work_order': self.work_order.name})
job_card = frappe.get_doc("Job Card", job_card_name)
self.assertEqual(job_card.status, "Open")
# fully transfer both RMs
transfer_entry_1 = make_stock_entry_from_jc(job_card_name)

View File

@ -2,6 +2,13 @@
// For license information, please see license.txt
frappe.ui.form.on('Production Plan', {
before_save: function(frm) {
// preserve temporary names on production plan item to re-link sub-assembly items
frm.doc.po_items.forEach(item => {
item.temporary_name = item.name;
});
},
setup: function(frm) {
frm.custom_make_buttons = {
'Work Order': 'Work Order / Subcontract PO',

View File

@ -32,6 +32,7 @@ class ProductionPlan(Document):
self.set_pending_qty_in_row_without_reference()
self.calculate_total_planned_qty()
self.set_status()
self._rename_temporary_references()
def set_pending_qty_in_row_without_reference(self):
"Set Pending Qty in independent rows (not from SO or MR)."
@ -57,6 +58,18 @@ class ProductionPlan(Document):
if not flt(d.planned_qty):
frappe.throw(_("Please enter Planned Qty for Item {0} at row {1}").format(d.item_code, d.idx))
def _rename_temporary_references(self):
""" po_items and sub_assembly_items items are both constructed client side without saving.
Attempt to fix linkages by using temporary names to map final row names.
"""
new_name_map = {d.temporary_name: d.name for d in self.po_items if d.temporary_name}
actual_names = {d.name for d in self.po_items}
for sub_assy in self.sub_assembly_items:
if sub_assy.production_plan_item not in actual_names:
sub_assy.production_plan_item = new_name_map.get(sub_assy.production_plan_item)
@frappe.whitelist()
def get_open_sales_orders(self):
""" Pull sales orders which are pending to deliver based on criteria selected"""

View File

@ -667,6 +667,39 @@ class TestProductionPlan(FrappeTestCase):
wo_doc.submit()
self.assertEqual(wo_doc.qty, 0.55)
def test_temporary_name_relinking(self):
pp = frappe.new_doc("Production Plan")
# this can not be unittested so mocking data that would be expected
# from client side.
for _ in range(10):
po_item = pp.append("po_items", {
"name": frappe.generate_hash(length=10),
"temporary_name": frappe.generate_hash(length=10),
})
pp.append("sub_assembly_items", {
"production_plan_item": po_item.temporary_name
})
pp._rename_temporary_references()
for po_item, subassy_item in zip(pp.po_items, pp.sub_assembly_items):
self.assertEqual(po_item.name, subassy_item.production_plan_item)
# bad links should be erased
pp.append("sub_assembly_items", {
"production_plan_item": frappe.generate_hash(length=16)
})
pp._rename_temporary_references()
self.assertIsNone(pp.sub_assembly_items[-1].production_plan_item)
pp.sub_assembly_items.pop()
# reattempting on same doc shouldn't change anything
pp._rename_temporary_references()
for po_item, subassy_item in zip(pp.po_items, pp.sub_assembly_items):
self.assertEqual(po_item.name, subassy_item.production_plan_item)
def create_production_plan(**args):
"""
sales_order (obj): Sales Order Doc Object

View File

@ -27,7 +27,8 @@
"material_request",
"material_request_item",
"product_bundle_item",
"item_reference"
"item_reference",
"temporary_name"
],
"fields": [
{
@ -204,17 +205,25 @@
"fieldtype": "Data",
"hidden": 1,
"label": "Item Reference"
},
{
"fieldname": "temporary_name",
"fieldtype": "Data",
"hidden": 1,
"label": "temporary name"
}
],
"idx": 1,
"istable": 1,
"links": [],
"modified": "2021-06-28 18:31:06.822168",
"modified": "2022-03-24 04:54:09.940224",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Production Plan Item",
"naming_rule": "Random",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "ASC"
"sort_order": "ASC",
"states": []
}

View File

@ -457,7 +457,8 @@ class WorkOrder(Document):
mr_obj.update_requested_qty([self.material_request_item])
def update_ordered_qty(self):
if self.production_plan and self.production_plan_item:
if self.production_plan and self.production_plan_item \
and not self.production_plan_sub_assembly_item:
qty = frappe.get_value("Production Plan Item", self.production_plan_item, "ordered_qty") or 0.0
if self.docstatus == 1:
@ -644,9 +645,13 @@ class WorkOrder(Document):
if not self.qty > 0:
frappe.throw(_("Quantity to Manufacture must be greater than 0."))
if self.production_plan and self.production_plan_item:
if self.production_plan and self.production_plan_item \
and not self.production_plan_sub_assembly_item:
qty_dict = frappe.db.get_value("Production Plan Item", self.production_plan_item, ["planned_qty", "ordered_qty"], as_dict=1)
if not qty_dict:
return
allowance_qty = flt(frappe.db.get_single_value("Manufacturing Settings",
"overproduction_percentage_for_work_order"))/100 * qty_dict.get("planned_qty", 0)
@ -1150,6 +1155,10 @@ def create_job_card(work_order, row, enable_capacity_planning=False, auto_create
doc.insert()
frappe.msgprint(_("Job card {0} created").format(get_link_to_form("Job Card", doc.name)), alert=True)
if enable_capacity_planning:
# automatically added scheduling rows shouldn't change status to WIP
doc.db_set("status", "Open")
return doc
def get_work_order_operation_data(work_order, operation, workstation):

View File

@ -1,7 +1,7 @@
[pre_model_sync]
erpnext.patches.v12_0.update_is_cancelled_field
erpnext.patches.v13_0.add_bin_unique_constraint
erpnext.patches.v11_0.rename_production_order_to_work_order
erpnext.patches.v13_0.add_bin_unique_constraint
erpnext.patches.v11_0.refactor_naming_series
erpnext.patches.v11_0.refactor_autoname_naming
execute:frappe.reload_doc("accounts", "doctype", "POS Payment Method") #2020-05-28
@ -360,4 +360,5 @@ erpnext.patches.v14_0.update_batch_valuation_flag
erpnext.patches.v14_0.delete_non_profit_doctypes
erpnext.patches.v14_0.update_employee_advance_status
erpnext.patches.v13_0.add_cost_center_in_loans
erpnext.patches.v13_0.remove_unknown_links_to_prod_plan_items
erpnext.patches.v13_0.set_return_against_in_pos_invoice_references
erpnext.patches.v13_0.remove_unknown_links_to_prod_plan_items # 24-03-2022

View File

@ -60,7 +60,7 @@ def execute():
def convert_to_seconds(value, unit):
seconds = 0
if value == 0:
if not value:
return seconds
if unit == 'Hours':
seconds = value * 3600

View File

@ -0,0 +1,38 @@
import frappe
def execute():
'''
Fetch and Set is_return & return_against from POS Invoice in POS Invoice References table.
'''
POSClosingEntry = frappe.qb.DocType("POS Closing Entry")
open_pos_closing_entries = (
frappe.qb
.from_(POSClosingEntry)
.select(POSClosingEntry.name)
.where(POSClosingEntry.docstatus == 0)
.run(pluck=True)
)
if not open_pos_closing_entries:
return
POSInvoiceReference = frappe.qb.DocType("POS Invoice Reference")
POSInvoice = frappe.qb.DocType("POS Invoice")
pos_invoice_references = (
frappe.qb
.from_(POSInvoiceReference)
.join(POSInvoice)
.on(POSInvoiceReference.pos_invoice == POSInvoice.name)
.select(POSInvoiceReference.name, POSInvoice.is_return, POSInvoice.return_against)
.where(POSInvoiceReference.parent.isin(open_pos_closing_entries))
.run(as_dict=True)
)
for row in pos_invoice_references:
frappe.db.set_value("POS Invoice Reference", row.name, "is_return", row.is_return)
if row.is_return:
frappe.db.set_value("POS Invoice Reference", row.name, "return_against", row.return_against)
else:
frappe.db.set_value("POS Invoice Reference", row.name, "return_against", None)

View File

@ -708,6 +708,8 @@ def submit_salary_slips_for_employees(payroll_entry, salary_slips, publish_progr
if not_submitted_ss:
frappe.msgprint(_("Could not submit some Salary Slips"))
frappe.flags.via_payroll_entry = False
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_payroll_entries_for_jv(doctype, txt, searchfield, start, page_len, filters):

View File

@ -38,6 +38,8 @@ from erpnext.payroll.doctype.salary_structure.salary_structure import make_salar
class TestSalarySlip(unittest.TestCase):
def setUp(self):
setup_test()
frappe.flags.pop("via_payroll_entry", None)
def tearDown(self):
frappe.db.rollback()
@ -409,15 +411,17 @@ class TestSalarySlip(unittest.TestCase):
"email_salary_slip_to_employee": 1
})
def test_email_salary_slip(self):
frappe.db.sql("delete from `tabEmail Queue`")
frappe.db.delete("Email Queue")
make_employee("test_email_salary_slip@salary.com", company="_Test Company")
ss = make_employee_salary_slip("test_email_salary_slip@salary.com", "Monthly", "Test Salary Slip Email")
user_id = "test_email_salary_slip@salary.com"
make_employee(user_id, company="_Test Company")
ss = make_employee_salary_slip(user_id, "Monthly", "Test Salary Slip Email")
ss.company = "_Test Company"
ss.save()
ss.submit()
email_queue = frappe.db.sql("""select name from `tabEmail Queue`""")
email_queue = frappe.db.a_row_exists("Email Queue")
self.assertTrue(email_queue)
def test_loan_repayment_salary_slip(self):

View File

@ -1042,9 +1042,9 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
var me = this;
this.set_dynamic_labels();
var company_currency = this.get_company_currency();
// Added `ignore_pricing_rule` to determine if document is loading after mapping from another doc
// Added `ignore_price_list` to determine if document is loading after mapping from another doc
if(this.frm.doc.currency && this.frm.doc.currency !== company_currency
&& !this.frm.doc.ignore_pricing_rule) {
&& !this.frm.doc.__onload.ignore_price_list) {
this.get_exchange_rate(transaction_date, this.frm.doc.currency, company_currency,
function(exchange_rate) {
@ -1070,7 +1070,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
}
if(flt(this.frm.doc.conversion_rate)>0.0) {
if(this.frm.doc.ignore_pricing_rule) {
if(this.frm.doc.__onload && this.frm.doc.__onload.ignore_price_list) {
this.calculate_taxes_and_totals();
} else if (!this.in_apply_price_list){
this.apply_price_list();
@ -1144,8 +1144,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
this.set_dynamic_labels();
var company_currency = this.get_company_currency();
// Added `ignore_pricing_rule` to determine if document is loading after mapping from another doc
if(this.frm.doc.price_list_currency !== company_currency && !this.frm.doc.ignore_pricing_rule) {
// Added `ignore_price_list` to determine if document is loading after mapping from another doc
if(this.frm.doc.price_list_currency !== company_currency && !this.frm.doc.__onload.ignore_price_list) {
this.get_exchange_rate(this.frm.doc.posting_date, this.frm.doc.price_list_currency, company_currency,
function(exchange_rate) {
me.frm.set_value("plc_conversion_rate", exchange_rate);
@ -1884,6 +1884,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
callback: function(r) {
if(!r.exc) {
item.item_tax_rate = r.message;
me.add_taxes_from_item_tax_template(item.item_tax_rate);
me.calculate_taxes_and_totals();
}
}

View File

@ -608,6 +608,11 @@ function check_can_calculate_pending_qty(me) {
&& doc.fg_completed_qty
&& erpnext.stock.bom
&& erpnext.stock.bom.name === doc.bom_no;
const itemChecks = !!item && !item.allow_alternative_item;
const itemChecks = !!item
&& !item.allow_alternative_item
&& erpnext.stock.bom && erpnext.stock.items
&& (item.item_code in erpnext.stock.bom.items);
return docChecks && itemChecks;
}
//# sourceURL=serial_no_batch_selector.js

View File

@ -264,6 +264,15 @@ body.product-page {
font-size: 13px;
}
.filter-lookup-input {
background-color: white;
border: 1px solid var(--gray-300);
&:focus {
border: 1px solid var(--primary);
}
}
.filter-label {
font-size: 11px;
font-weight: 600;

View File

@ -19,8 +19,9 @@ PAN_NUMBER_FORMAT = re.compile("[A-Z]{5}[0-9]{4}[A-Z]{1}")
def validate_gstin_for_india(doc, method):
if hasattr(doc, 'gst_state') and doc.gst_state:
doc.gst_state_number = state_numbers[doc.gst_state]
if hasattr(doc, 'gst_state'):
set_gst_state_and_state_number(doc)
if not hasattr(doc, 'gstin') or not doc.gstin:
return
@ -50,7 +51,6 @@ def validate_gstin_for_india(doc, method):
frappe.throw(_("The input you've entered doesn't match the format of GSTIN."), title=_("Invalid GSTIN"))
validate_gstin_check_digit(doc.gstin)
set_gst_state_and_state_number(doc)
if not doc.gst_state:
frappe.throw(_("Please enter GST state"), title=_("Invalid State"))
@ -82,17 +82,14 @@ def update_gst_category(doc, method):
frappe.db.set_value(link.link_doctype, {'name': link.link_name, 'gst_category': 'Unregistered'}, 'gst_category', 'Registered Regular')
def set_gst_state_and_state_number(doc):
if not doc.gst_state:
if not doc.state:
return
if not doc.gst_state and doc.state:
state = doc.state.lower()
states_lowercase = {s.lower():s for s in states}
if state in states_lowercase:
doc.gst_state = states_lowercase[state]
else:
return
doc.gst_state_number = state_numbers[doc.gst_state]
doc.gst_state_number = state_numbers.get(doc.gst_state)
def validate_gstin_check_digit(gstin, label='GSTIN'):
''' Function to validate the check digit of the GSTIN.'''

View File

@ -206,6 +206,7 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False):
}, target_doc, set_missing_values, ignore_permissions=ignore_permissions)
# postprocess: fetch shipping address, set missing values
doclist.set_onload('ignore_price_list', True)
return doclist
@ -269,6 +270,8 @@ def _make_sales_invoice(source_name, target_doc=None, ignore_permissions=False):
}
}, target_doc, set_missing_values, ignore_permissions=ignore_permissions)
doclist.set_onload('ignore_price_list', True)
return doclist
def _make_customer(source_name, ignore_permissions=False):

View File

@ -584,6 +584,8 @@ def make_delivery_note(source_name, target_doc=None, skip_item_mapping=False):
target_doc = get_mapped_doc("Sales Order", source_name, mapper, target_doc, set_missing_values)
target_doc.set_onload('ignore_price_list', True)
return target_doc
@frappe.whitelist()
@ -664,6 +666,8 @@ def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False):
if automatically_fetch_payment_terms:
doclist.set_payment_schedule()
doclist.set_onload('ignore_price_list', True)
return doclist
@frappe.whitelist()

View File

@ -8,7 +8,7 @@ import frappe
from frappe.utils.nestedset import get_root_of
from erpnext.accounts.doctype.pos_invoice.pos_invoice import get_stock_availability
from erpnext.accounts.doctype.pos_profile.pos_profile import get_item_groups
from erpnext.accounts.doctype.pos_profile.pos_profile import get_child_nodes, get_item_groups
def search_by_term(search_term, warehouse, price_list):
@ -275,3 +275,16 @@ def set_customer_info(fieldname, customer, value=""):
contact_doc.set('phone_nos', [{ 'phone': value, 'is_primary_mobile_no': 1}])
frappe.db.set_value('Customer', customer, 'mobile_no', value)
contact_doc.save()
@frappe.whitelist()
def get_pos_profile_data(pos_profile):
pos_profile = frappe.get_doc('POS Profile', pos_profile)
pos_profile = pos_profile.as_dict()
_customer_groups_with_children = []
for row in pos_profile.customer_groups:
children = get_child_nodes('Customer Group', row.customer_group)
_customer_groups_with_children.extend(children)
pos_profile.customer_groups = _customer_groups_with_children
return pos_profile

View File

@ -119,10 +119,15 @@ erpnext.PointOfSale.Controller = class {
this.allow_negative_stock = flt(message.allow_negative_stock) || false;
});
frappe.db.get_doc("POS Profile", this.pos_profile).then((profile) => {
Object.assign(this.settings, profile);
this.settings.customer_groups = profile.customer_groups.map(group => group.customer_group);
this.make_app();
frappe.call({
method: "erpnext.selling.page.point_of_sale.point_of_sale.get_pos_profile_data",
args: { "pos_profile": this.pos_profile },
callback: (res) => {
const profile = res.message;
Object.assign(this.settings, profile);
this.settings.customer_groups = profile.customer_groups.map(group => group.name);
this.make_app();
}
});
}
@ -555,7 +560,7 @@ erpnext.PointOfSale.Controller = class {
if (this.item_details.$component.is(':visible'))
this.edit_item_details_of(item_row);
if (this.check_serial_batch_selection_needed(item_row))
if (this.check_serial_batch_selection_needed(item_row) && !this.item_details.$component.is(':visible'))
this.edit_item_details_of(item_row);
}
@ -704,7 +709,7 @@ erpnext.PointOfSale.Controller = class {
frappe.dom.freeze();
const { doctype, name, current_item } = this.item_details;
frappe.model.set_value(doctype, name, 'qty', 0)
return frappe.model.set_value(doctype, name, 'qty', 0)
.then(() => {
frappe.model.clear_doc(doctype, name);
this.update_cart_html(current_item, true);
@ -715,7 +720,14 @@ erpnext.PointOfSale.Controller = class {
}
async save_and_checkout() {
this.frm.is_dirty() && await this.frm.save();
this.payment.checkout();
if (this.frm.is_dirty()) {
// only move to payment section if save is successful
frappe.route_hooks.after_save = () => this.payment.checkout();
return this.frm.save(
null, null, null, () => this.cart.toggle_checkout_btn(true) // show checkout button on error
);
} else {
this.payment.checkout();
}
}
};

View File

@ -60,12 +60,18 @@ erpnext.PointOfSale.ItemDetails = class {
return item && item.name == this.current_item.name;
}
toggle_item_details_section(item) {
async toggle_item_details_section(item) {
const current_item_changed = !this.compare_with_current_item(item);
// if item is null or highlighted cart item is clicked twice
const hide_item_details = !Boolean(item) || !current_item_changed;
if ((!hide_item_details && current_item_changed) || hide_item_details) {
// if item details is being closed OR if item details is opened but item is changed
// in both cases, if the current item is a serialized item, then validate and remove the item
await this.validate_serial_batch_item();
}
this.events.toggle_item_selector(!hide_item_details);
this.toggle_component(!hide_item_details);
@ -83,7 +89,6 @@ erpnext.PointOfSale.ItemDetails = class {
this.render_form(item);
this.events.highlight_cart_item(item);
} else {
this.validate_serial_batch_item();
this.current_item = {};
}
}
@ -103,11 +108,11 @@ erpnext.PointOfSale.ItemDetails = class {
(serialized && batched && (no_batch_selected || no_serial_selected))) {
frappe.show_alert({
message: __("Item will be removed since no serial / batch no selected."),
message: __("Item is removed since no serial / batch no selected."),
indicator: 'orange'
});
frappe.utils.play_sound("cancel");
this.events.remove_item_from_cart();
return this.events.remove_item_from_cart();
}
}

View File

@ -170,20 +170,24 @@ erpnext.PointOfSale.Payment = class {
});
frappe.ui.form.on('POS Invoice', 'coupon_code', (frm) => {
if (!frm.doc.ignore_pricing_rule && frm.doc.coupon_code) {
frappe.run_serially([
() => frm.doc.ignore_pricing_rule=1,
() => frm.trigger('ignore_pricing_rule'),
() => frm.doc.ignore_pricing_rule=0,
() => frm.trigger('apply_pricing_rule'),
() => frm.save(),
() => this.update_totals_section(frm.doc)
]);
} else if (frm.doc.ignore_pricing_rule && frm.doc.coupon_code) {
frappe.show_alert({
message: __("Ignore Pricing Rule is enabled. Cannot apply coupon code."),
indicator: "orange"
});
if (frm.doc.coupon_code && !frm.applying_pos_coupon_code) {
if (!frm.doc.ignore_pricing_rule) {
frm.applying_pos_coupon_code = true;
frappe.run_serially([
() => frm.doc.ignore_pricing_rule=1,
() => frm.trigger('ignore_pricing_rule'),
() => frm.doc.ignore_pricing_rule=0,
() => frm.trigger('apply_pricing_rule'),
() => frm.save(),
() => this.update_totals_section(frm.doc),
() => (frm.applying_pos_coupon_code = false)
]);
} else if (frm.doc.ignore_pricing_rule) {
frappe.show_alert({
message: __("Ignore Pricing Rule is enabled. Cannot apply coupon code."),
indicator: "orange"
});
}
}
});

View File

@ -4,8 +4,12 @@
frappe.ui.form.on('Sales Person', {
refresh: function(frm) {
if(frm.doc.__onload && frm.doc.__onload.dashboard_info) {
var info = frm.doc.__onload.dashboard_info;
frm.dashboard.add_indicator(__('Total Contribution Amount: {0}', [format_currency(info.allocated_amount, info.currency)]), 'blue');
let info = frm.doc.__onload.dashboard_info;
frm.dashboard.add_indicator(__('Total Contribution Amount Against Orders: {0}',
[format_currency(info.allocated_amount_against_order, info.currency)]), 'blue');
frm.dashboard.add_indicator(__('Total Contribution Amount Against Invoices: {0}',
[format_currency(info.allocated_amount_against_invoice, info.currency)]), 'blue');
}
},

View File

@ -28,14 +28,17 @@ class SalesPerson(NestedSet):
def load_dashboard_info(self):
company_default_currency = get_default_currency()
allocated_amount = frappe.db.sql("""
select sum(allocated_amount)
from `tabSales Team`
where sales_person = %s and docstatus=1 and parenttype = 'Sales Order'
""",(self.sales_person_name))
allocated_amount_against_order = flt(frappe.db.get_value('Sales Team',
{'docstatus': 1, 'parenttype': 'Sales Order', 'sales_person': self.sales_person_name},
'sum(allocated_amount)'))
allocated_amount_against_invoice = flt(frappe.db.get_value('Sales Team',
{'docstatus': 1, 'parenttype': 'Sales Invoice', 'sales_person': self.sales_person_name},
'sum(allocated_amount)'))
info = {}
info["allocated_amount"] = flt(allocated_amount[0][0]) if allocated_amount else 0
info["allocated_amount_against_order"] = allocated_amount_against_order
info["allocated_amount_against_invoice"] = allocated_amount_against_invoice
info["currency"] = company_default_currency
self.set_onload('dashboard_info', info)

View File

@ -21,9 +21,7 @@ default_mail_footer = """<div style="padding: 7px; text-align: right; color: #88
def after_install():
frappe.get_doc({'doctype': "Role", "role_name": "Analytics"}).insert()
set_single_defaults()
create_compact_item_print_custom_field()
create_print_uom_after_qty_custom_field()
create_print_zero_amount_taxes_custom_field()
create_print_setting_custom_fields()
add_all_roles_to("Administrator")
create_default_cash_flow_mapper_templates()
create_default_success_action()
@ -77,7 +75,7 @@ def setup_currency_exchange():
except frappe.ValidationError:
pass
def create_compact_item_print_custom_field():
def create_print_setting_custom_fields():
create_custom_field('Print Settings', {
'label': _('Compact Item Print'),
'fieldname': 'compact_item_print',
@ -85,9 +83,6 @@ def create_compact_item_print_custom_field():
'default': 1,
'insert_after': 'with_letterhead'
})
def create_print_uom_after_qty_custom_field():
create_custom_field('Print Settings', {
'label': _('Print UOM after Quantity'),
'fieldname': 'print_uom_after_quantity',
@ -95,9 +90,6 @@ def create_print_uom_after_qty_custom_field():
'default': 0,
'insert_after': 'compact_item_print'
})
def create_print_zero_amount_taxes_custom_field():
create_custom_field('Print Settings', {
'label': _('Print taxes with zero amount'),
'fieldname': 'print_taxes_with_zero_amount',

View File

@ -1,56 +0,0 @@
{
"add_sample_data": 1,
"bank_account": "HDFC",
"company_abbr": "FT",
"company_name": "For Testing",
"company_tagline": "Just for GST",
"country": "India",
"currency": "INR",
"customer_1": "Test Customer 1",
"customer_2": "Test Customer 2",
"domains": ["Manufacturing"],
"email": "great@example.com",
"full_name": "Great Tester",
"fy_end_date": "2018-03-31",
"fy_start_date": "2017-04-01",
"is_purchase_item_1": 1,
"is_purchase_item_2": 1,
"is_purchase_item_3": 0,
"is_purchase_item_4": 0,
"is_purchase_item_5": 0,
"is_sales_item_1": 1,
"is_sales_item_2": 1,
"is_sales_item_3": 1,
"is_sales_item_4": 1,
"is_sales_item_5": 1,
"item_1": "Test Item 1",
"item_2": "Test Item 2",
"item_group_1": "Products",
"item_group_2": "Products",
"item_group_3": "Products",
"item_group_4": "Products",
"item_group_5": "Products",
"item_uom_1": "Unit",
"item_uom_2": "Unit",
"item_uom_3": "Unit",
"item_uom_4": "Unit",
"item_uom_5": "Unit",
"language": "English (United States)",
"password": "test",
"setup_website": 1,
"supplier_1": "Test Supplier 1",
"supplier_2": "Test Supplier 2",
"timezone": "Asia/Kolkata",
"user_accountant_1": 1,
"user_accountant_2": 1,
"user_accountant_3": 1,
"user_accountant_4": 1,
"user_purchaser_1": 1,
"user_purchaser_2": 1,
"user_purchaser_3": 1,
"user_purchaser_4": 1,
"user_sales_1": 1,
"user_sales_2": 1,
"user_sales_3": 1,
"user_sales_4": 1
}

View File

@ -1,179 +0,0 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
import json
import os
import random
import frappe
import frappe.utils
from frappe import _
from frappe.utils.make_random import add_random_children
def make_sample_data(domains, make_dependent = False):
"""Create a few opportunities, quotes, material requests, issues, todos, projects
to help the user get started"""
if make_dependent:
items = frappe.get_all("Item", {'is_sales_item': 1})
customers = frappe.get_all("Customer")
warehouses = frappe.get_all("Warehouse")
if items and customers:
for i in range(3):
customer = random.choice(customers).name
make_opportunity(items, customer)
make_quote(items, customer)
if items and warehouses:
make_material_request(frappe.get_all("Item"))
make_projects(domains)
import_notification()
def make_opportunity(items, customer):
b = frappe.get_doc({
"doctype": "Opportunity",
"opportunity_from": "Customer",
"customer": customer,
"opportunity_type": _("Sales"),
"with_items": 1
})
add_random_children(b, "items", rows=len(items), randomize = {
"qty": (1, 5),
"item_code": ["Item"]
}, unique="item_code")
b.insert(ignore_permissions=True)
b.add_comment('Comment', text="This is a dummy record")
def make_quote(items, customer):
qtn = frappe.get_doc({
"doctype": "Quotation",
"quotation_to": "Customer",
"party_name": customer,
"order_type": "Sales"
})
add_random_children(qtn, "items", rows=len(items), randomize = {
"qty": (1, 5),
"item_code": ["Item"]
}, unique="item_code")
qtn.insert(ignore_permissions=True)
qtn.add_comment('Comment', text="This is a dummy record")
def make_material_request(items):
for i in items:
mr = frappe.get_doc({
"doctype": "Material Request",
"material_request_type": "Purchase",
"schedule_date": frappe.utils.add_days(frappe.utils.nowdate(), 7),
"items": [{
"schedule_date": frappe.utils.add_days(frappe.utils.nowdate(), 7),
"item_code": i.name,
"qty": 10
}]
})
mr.insert()
mr.submit()
mr.add_comment('Comment', text="This is a dummy record")
def make_issue():
pass
def make_projects(domains):
current_date = frappe.utils.nowdate()
project = frappe.get_doc({
"doctype": "Project",
"project_name": "ERPNext Implementation",
})
tasks = [
{
"title": "Explore ERPNext",
"start_date": current_date,
"end_date": current_date,
"file": "explore.md"
}]
if 'Education' in domains:
tasks += [
{
"title": _("Setup your Institute in ERPNext"),
"start_date": current_date,
"end_date": frappe.utils.add_days(current_date, 1),
"file": "education_masters.md"
},
{
"title": "Setup Master Data",
"start_date": current_date,
"end_date": frappe.utils.add_days(current_date, 1),
"file": "education_masters.md"
}]
else:
tasks += [
{
"title": "Setup Your Company",
"start_date": current_date,
"end_date": frappe.utils.add_days(current_date, 1),
"file": "masters.md"
},
{
"title": "Start Tracking your Sales",
"start_date": current_date,
"end_date": frappe.utils.add_days(current_date, 2),
"file": "sales.md"
},
{
"title": "Start Managing Purchases",
"start_date": current_date,
"end_date": frappe.utils.add_days(current_date, 3),
"file": "purchase.md"
},
{
"title": "Import Data",
"start_date": current_date,
"end_date": frappe.utils.add_days(current_date, 4),
"file": "import_data.md"
},
{
"title": "Go Live!",
"start_date": current_date,
"end_date": frappe.utils.add_days(current_date, 5),
"file": "go_live.md"
}]
for t in tasks:
with open (os.path.join(os.path.dirname(__file__), "tasks", t['file'])) as f:
t['description'] = frappe.utils.md_to_html(f.read())
del t['file']
project.append('tasks', t)
project.insert(ignore_permissions=True)
def import_notification():
'''Import notification for task start'''
with open (os.path.join(os.path.dirname(__file__), "tasks/task_alert.json")) as f:
notification = frappe.get_doc(json.loads(f.read())[0])
notification.insert()
# trigger the first message!
from frappe.email.doctype.notification.notification import trigger_daily_alerts
trigger_daily_alerts()
def test_sample():
frappe.db.sql('delete from `tabNotification`')
frappe.db.sql('delete from tabProject')
frappe.db.sql('delete from tabTask')
make_projects('Education')
import_notification()

View File

@ -7,7 +7,6 @@ from frappe import _
from .operations import company_setup
from .operations import install_fixtures as fixtures
from .operations import sample_data
def get_setup_stages(args=None):
@ -103,16 +102,6 @@ def fin(args):
frappe.local.message_log = []
login_as_first_user(args)
make_sample_data(args.get('domains'))
def make_sample_data(domains):
try:
sample_data.make_sample_data(domains)
except Exception:
# clear message
if frappe.message_log:
frappe.message_log.pop()
pass
def login_as_first_user(args):
if args.get("email") and hasattr(frappe.local, "login_manager"):

View File

@ -1,9 +0,0 @@
Lets start making things in ERPNext that are representative of your institution.
1. Make a list of **Programs** that you offer
1. Add a few **Courses** that your programs cover
1. Create **Academic Terms** and **Academic Years**
1. Start adding **Students**
1. Group your students into **Batches**
Watch this video to learn more about ERPNext Education: https://www.youtube.com/watch?v=f6foQOyGzdA

View File

@ -1,7 +0,0 @@
Thanks for checking this out! ❤️
If you are evaluating an ERP system for the first time, this is going to be quite a task! But don't worry, ERPNext is awesome.
First, get familiar with the surroundings. ERPNext covers a *lot of features*, go to the home page and click on the "Explore" icon.
All the best!

View File

@ -1,18 +0,0 @@
Ready to go live with ERPNext? 🏁🏁🏁
Here are the steps:
1. Sync up your **Chart of Accounts**
3. Add your opening stock using **Stock Reconciliation**
4. Add your open invoices (both sales and purchase)
3. Add your opening account balances by making a **Journal Entry**
If you need help for going live, sign up for an account at erpnext.com or find a partner to help you with this.
Or you can watch these videos 📺:
Setup your chart of accounts: https://www.youtube.com/watch?v=AcfMCT7wLLo
Add Open Stock: https://www.youtube.com/watch?v=nlHX0ZZ84Lw
Add Opening Balances: https://www.youtube.com/watch?v=nlHX0ZZ84Lw

View File

@ -1,5 +0,0 @@
Lets import some data! 💪💪
If you are already running a business, you most likely have your Items, Customers or Suppliers in some spreadsheet file somewhere, import it into ERPNext with the Data Import Tool.
Watch this video to get started: https://www.youtube.com/watch?v=Ta2Xx3QoK3E

View File

@ -1,7 +0,0 @@
Start building a model of your business in ERPNext by adding your Items and Customers.
These videos 📺 will help you get started:
Adding Customers and Suppliers: https://www.youtube.com/watch?v=zsrrVDk6VBs
Adding Items and Prices: https://www.youtube.com/watch?v=FcOsV-e8ymE

View File

@ -1,10 +0,0 @@
How to manage your purchasing in ERPNext 🛒🛒🛒:
1. Add a few **Suppliers**
2. Find out what you need by making **Material Requests**.
3. Now start placing orders via **Purchase Order**.
4. When your suppliers deliver, make **Purchase Receipts**
Now never run out of stock again! 😎
Watch this video 📺 to get an overview: https://www.youtube.com/watch?v=4TN9kPyfIqM

View File

@ -1,8 +0,0 @@
Start managing your sales with ERPNext 🔔🔔🔔:
1. Add potential business contacts as **Leads**
2. Udpate your deals in pipeline in **Opportunities**
3. Send proposals to your leads or customers with **Quotations**
4. Track confirmed orders with **Sales Orders**
Watch this video 📺 to get an overview: https://www.youtube.com/watch?v=o9XCSZHJfpA

View File

@ -1,5 +0,0 @@
Lets import some data! 💪💪
If you are already running a Institute, you most likely have your Students in some spreadsheet file somewhere. Import it into ERPNext with the Data Import Tool.
Watch this video to get started: https://www.youtube.com/watch?v=Ta2Xx3QoK3E

View File

@ -1,28 +0,0 @@
[
{
"attach_print": 0,
"condition": "doc.status in ('Open', 'Overdue')",
"date_changed": "exp_end_date",
"days_in_advance": 0,
"docstatus": 0,
"doctype": "Notification",
"document_type": "Task",
"enabled": 1,
"event": "Days After",
"is_standard": 0,
"message": "<p>Task due today:</p>\n\n<div>\n{{ doc.description }}\n</div>\n\n<hr>\n<p style=\"font-size: 85%\">\nThis is a notification for a task that is due today, and a sample <b>Notification</b>. In ERPNext you can setup notifications on anything, Invoices, Orders, Leads, Opportunities, so you never miss a thing.\n<br>To edit this, and setup other alerts, just type <b>Notification</b> in the search bar.</p>",
"method": null,
"modified": "2017-03-09 07:34:58.168370",
"module": null,
"name": "Task Due Alert",
"recipients": [
{
"cc": null,
"condition": null,
"email_by_document_field": "owner"
}
],
"subject": "{{ doc.subject }}",
"value_changed": null
}
]

View File

@ -1,12 +0,0 @@
import json
import os
from frappe.desk.page.setup_wizard.setup_wizard import setup_complete
def complete():
with open(os.path.join(os.path.dirname(__file__),
'data', 'test_mfg.json'), 'r') as f:
data = json.loads(f.read())
setup_complete(data)

View File

@ -5,28 +5,17 @@
import frappe
from frappe import _
from frappe.utils import add_days, flt, get_datetime_str, nowdate
from frappe.utils.data import now_datetime
from frappe.utils.nestedset import get_ancestors_of, get_root_of # noqa
from erpnext import get_default_company
def get_root_of(doctype):
"""Get root element of a DocType with a tree structure"""
result = frappe.db.sql_list("""select name from `tab%s`
where lft=1 and rgt=(select max(rgt) from `tab%s` where docstatus < 2)""" %
(doctype, doctype))
return result[0] if result else None
def get_ancestors_of(doctype, name):
"""Get ancestor elements of a DocType with a tree structure"""
lft, rgt = frappe.db.get_value(doctype, name, ["lft", "rgt"])
result = frappe.db.sql_list("""select name from `tab%s`
where lft<%s and rgt>%s order by lft desc""" % (doctype, "%s", "%s"), (lft, rgt))
return result or []
def before_tests():
frappe.clear_cache()
# complete setup if missing
from frappe.desk.page.setup_wizard.setup_wizard import setup_complete
current_year = now_datetime().year
if not frappe.get_list("Company"):
setup_complete({
"currency" :"USD",
@ -36,8 +25,8 @@ def before_tests():
"company_abbr" :"WP",
"industry" :"Manufacturing",
"country" :"United States",
"fy_start_date" :"2021-01-01",
"fy_end_date" :"2021-12-31",
"fy_start_date" :f"{current_year}-01-01",
"fy_end_date" :f"{current_year}-12-31",
"language" :"english",
"company_tagline" :"Testing",
"email" :"test@erpnext.com",
@ -51,7 +40,6 @@ def before_tests():
frappe.db.sql("delete from `tabSalary Slip`")
frappe.db.sql("delete from `tabItem Price`")
frappe.db.set_value("Stock Settings", None, "auto_insert_price_list_rate_if_missing", 0)
enable_all_roles_and_domains()
set_defaults_for_tests()
@ -142,13 +130,13 @@ def enable_all_roles_and_domains():
add_all_roles_to('Administrator')
def set_defaults_for_tests():
from frappe.utils.nestedset import get_root_of
selling_settings = frappe.get_single("Selling Settings")
selling_settings.customer_group = get_root_of("Customer Group")
selling_settings.territory = get_root_of("Territory")
selling_settings.save()
frappe.db.set_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing", 0)
def insert_record(records):
for r in records:

View File

@ -40,18 +40,15 @@ def get_data(item_code=None, warehouse=None, item_group=None,
filters=filters,
order_by=sort_by + ' ' + sort_order,
limit_start=start,
limit_page_length='21')
limit_page_length=21)
precision = cint(frappe.db.get_single_value("System Settings", "float_precision"))
for item in items:
item.update({
'item_name': frappe.get_cached_value(
"Item", item.item_code, 'item_name'),
'disable_quick_entry': frappe.get_cached_value(
"Item", item.item_code, 'has_batch_no')
or frappe.get_cached_value(
"Item", item.item_code, 'has_serial_no'),
'item_name': frappe.get_cached_value("Item", item.item_code, 'item_name'),
'disable_quick_entry': frappe.get_cached_value( "Item", item.item_code, 'has_batch_no')
or frappe.get_cached_value( "Item", item.item_code, 'has_serial_no'),
'projected_qty': flt(item.projected_qty, precision),
'reserved_qty': flt(item.reserved_qty, precision),
'reserved_qty_for_production': flt(item.reserved_qty_for_production, precision),

View File

@ -519,6 +519,8 @@ def make_sales_invoice(source_name, target_doc=None):
if automatically_fetch_payment_terms:
doc.set_payment_schedule()
doc.set_onload('ignore_price_list', True)
return doc
@frappe.whitelist()

View File

@ -422,7 +422,6 @@
"fieldname": "has_batch_no",
"fieldtype": "Check",
"label": "Has Batch No",
"no_copy": 1,
"oldfieldname": "has_batch_no",
"oldfieldtype": "Select"
},
@ -472,7 +471,6 @@
"fieldname": "has_serial_no",
"fieldtype": "Check",
"label": "Has Serial No",
"no_copy": 1,
"oldfieldname": "has_serial_no",
"oldfieldtype": "Select"
},
@ -921,7 +919,7 @@
"image_field": "image",
"index_web_pages_for_search": 1,
"links": [],
"modified": "2022-02-11 08:07:46.663220",
"modified": "2022-03-25 06:38:55.942304",
"modified_by": "Administrator",
"module": "Stock",
"name": "Item",
@ -994,4 +992,4 @@
"states": [],
"title_field": "item_name",
"track_changes": 1
}
}

View File

@ -685,6 +685,12 @@ class TestItem(FrappeTestCase):
# standalone return
make_purchase_receipt(is_return=True, qty=-1, **typical_args)
def test_item_dashboard(self):
from erpnext.stock.dashboard.item_dashboard import get_data
self.assertTrue(get_data(item_code="_Test Item"))
self.assertTrue(get_data(warehouse="_Test Warehouse - _TC"))
self.assertTrue(get_data(item_group="All Item Groups"))
def set_item_variant_settings(fields):

View File

@ -13,10 +13,16 @@ frappe.ui.form.on('Item Variant Settings', {
const exclude_field_types = ['HTML', 'Section Break', 'Column Break', 'Button', 'Read Only'];
frappe.model.with_doctype('Item', () => {
const field_label_map = {};
frappe.get_meta('Item').fields.forEach(d => {
field_label_map[d.fieldname] = __(d.label) + ` (${d.fieldname})`;
if (!in_list(exclude_field_types, d.fieldtype)
&& !d.no_copy && !in_list(exclude_fields, d.fieldname)) {
allow_fields.push(d.fieldname);
allow_fields.push({
label: field_label_map[d.fieldname],
value: d.fieldname,
});
}
});

View File

@ -823,6 +823,7 @@ def make_purchase_invoice(source_name, target_doc=None):
}
}, target_doc, set_missing_values)
doclist.set_onload('ignore_price_list', True)
return doclist
def get_invoiced_qty_map(purchase_receipt):

View File

@ -396,6 +396,21 @@ class TestPutawayRule(FrappeTestCase):
rule_1.delete()
rule_2.delete()
def test_warehouse_capacity_dashbord(self):
from erpnext.stock.dashboard.warehouse_capacity_dashboard import get_data
item = "_Rice"
rule = create_putaway_rule(item_code=item, warehouse=self.warehouse_1, capacity=500,
uom="Kg")
capacities = get_data(warehouse=self.warehouse_1)
for capacity in capacities:
if capacity.item_code == item and capacity.warehouse == self.warehouse_1:
self.assertEqual(capacity.stock_capacity, 500)
get_data(warehouse=self.warehouse_1)
rule.delete()
def create_putaway_rule(**args):
args = frappe._dict(args)
putaway = frappe.new_doc("Putaway Rule")

View File

@ -3,6 +3,7 @@
import json
from typing import List, Optional, Union
import frappe
from frappe import ValidationError, _
@ -574,22 +575,30 @@ def get_delivery_note_serial_no(item_code, qty, delivery_note):
return serial_nos
@frappe.whitelist()
def auto_fetch_serial_number(qty, item_code, warehouse,
posting_date=None, batch_nos=None, for_doctype=None, exclude_sr_nos=None):
def auto_fetch_serial_number(
qty: float,
item_code: str,
warehouse: str,
posting_date: Optional[str] = None,
batch_nos: Optional[Union[str, List[str]]] = None,
for_doctype: Optional[str] = None,
exclude_sr_nos: Optional[List[str]] = None
) -> List[str]:
filters = frappe._dict({"item_code": item_code, "warehouse": warehouse})
if exclude_sr_nos is None:
exclude_sr_nos = []
else:
exclude_sr_nos = safe_json_loads(exclude_sr_nos)
exclude_sr_nos = get_serial_nos(clean_serial_no_string("\n".join(exclude_sr_nos)))
if batch_nos:
batch_nos = safe_json_loads(batch_nos)
if isinstance(batch_nos, list):
filters.batch_no = batch_nos
elif isinstance(batch_nos, str):
filters.batch_no = [batch_nos]
else:
filters.batch_no = [str(batch_nos)]
if posting_date:
filters.expiry_date = posting_date
@ -602,25 +611,60 @@ def auto_fetch_serial_number(qty, item_code, warehouse,
return sorted([d.get('name') for d in serial_numbers])
def get_delivered_serial_nos(serial_nos):
'''
Returns serial numbers that delivered from the list of serial numbers
'''
from frappe.query_builder.functions import Coalesce
SerialNo = frappe.qb.DocType("Serial No")
serial_nos = get_serial_nos(serial_nos)
query = frappe.qb.select(SerialNo.name).from_(SerialNo).where(
(SerialNo.name.isin(serial_nos))
& (Coalesce(SerialNo.delivery_document_type, "") != "")
)
result = query.run()
if result and len(result) > 0:
delivered_serial_nos = [row[0] for row in result]
return delivered_serial_nos
@frappe.whitelist()
def get_pos_reserved_serial_nos(filters):
if isinstance(filters, str):
filters = json.loads(filters)
pos_transacted_sr_nos = frappe.db.sql("""select item.serial_no as serial_no
from `tabPOS Invoice` p, `tabPOS Invoice Item` item
where p.name = item.parent
and p.consolidated_invoice is NULL
and p.docstatus = 1
and item.docstatus = 1
and item.item_code = %(item_code)s
and item.warehouse = %(warehouse)s
and item.serial_no is NOT NULL and item.serial_no != ''
""", filters, as_dict=1)
POSInvoice = frappe.qb.DocType("POS Invoice")
POSInvoiceItem = frappe.qb.DocType("POS Invoice Item")
query = frappe.qb.from_(
POSInvoice
).from_(
POSInvoiceItem
).select(
POSInvoice.is_return,
POSInvoiceItem.serial_no
).where(
(POSInvoice.name == POSInvoiceItem.parent)
& (POSInvoice.docstatus == 1)
& (POSInvoiceItem.docstatus == 1)
& (POSInvoiceItem.item_code == filters.get('item_code'))
& (POSInvoiceItem.warehouse == filters.get('warehouse'))
& (POSInvoiceItem.serial_no.isnotnull())
& (POSInvoiceItem.serial_no != '')
)
pos_transacted_sr_nos = query.run(as_dict=True)
reserved_sr_nos = []
returned_sr_nos = []
for d in pos_transacted_sr_nos:
reserved_sr_nos += get_serial_nos(d.serial_no)
if d.is_return == 0:
reserved_sr_nos += get_serial_nos(d.serial_no)
elif d.is_return == 1:
returned_sr_nos += get_serial_nos(d.serial_no)
for sr_no in returned_sr_nos:
reserved_sr_nos.remove(sr_no)
return reserved_sr_nos

View File

@ -274,7 +274,8 @@ class TestSerialNo(FrappeTestCase):
msg=f"{partial_fetch} should be subset of {first_fetch}")
# exclusion
remaining = auto_fetch_serial_number(3, item_code, warehouse, exclude_sr_nos=partial_fetch)
remaining = auto_fetch_serial_number(3, item_code, warehouse,
exclude_sr_nos=json.dumps(partial_fetch))
self.assertEqual(sorted(remaining + partial_fetch), first_fetch)
# batchwise

View File

@ -1,72 +1,32 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2017-08-29 16:33:33.978574",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"actions": [],
"creation": "2022-02-11 11:26:20.611960",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"field_name"
],
"fields": [
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "field_name",
"fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Field Name",
"length": 0,
"no_copy": 0,
"options": "",
"permlevel": 0,
"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
"fieldname": "field_name",
"fieldtype": "Autocomplete",
"in_list_view": 1,
"label": "Field Name",
"reqd": 1
}
],
"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,
"max_attachments": 0,
"modified": "2017-08-29 17:19:20.353197",
"modified_by": "Administrator",
"module": "Stock",
"name": "Variant Field",
"name_case": "",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0
],
"istable": 1,
"links": [],
"modified": "2022-03-25 05:48:30.946201",
"modified_by": "Administrator",
"module": "Stock",
"name": "Variant Field",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@ -52,24 +52,6 @@
</div>
<script>
frappe.ready(() => {
$('.product-filter-filter').on('keydown', frappe.utils.debounce((e) => {
const $input = $(e.target);
const keyword = ($input.val() || '').toLowerCase();
const $filter_options = $input.next('.filter-options');
$filter_options.find('.custom-control').show();
$filter_options.find('.custom-control').each((i, el) => {
const $el = $(el);
const value = $el.data('value').toLowerCase();
if (!value.includes(keyword)) {
$el.hide();
}
});
}, 300));
})
</script>
</div>
</div>
</div>

View File

@ -300,13 +300,13 @@
{% if values | len > 20 %}
<!-- show inline filter if values more than 20 -->
<input type="text" class="form-control form-control-sm mb-2 product-filter-filter"/>
<input type="text" class="form-control form-control-sm mb-2 filter-lookup-input" placeholder="Search {{ item_field.label + 's' }}"/>
{% endif %}
{% if values %}
<div class="filter-options">
{% for value in values %}
<div class="checkbox" data-value="{{ value }}">
<div class="filter-lookup-wrapper checkbox" data-value="{{ value }}">
<label for="{{value}}">
<input type="checkbox"
class="product-filter field-filter"
@ -329,16 +329,16 @@
{%- macro attribute_filter_section(filters)-%}
{% for attribute in filters %}
<div class="mb-4 filter-block pb-5">
<div class="filter-label mb-3">{{ attribute.name}}</div>
{% if values | len > 20 %}
<div class="filter-label mb-3">{{ attribute.name }}</div>
{% if attribute.item_attribute_values | len > 20 %}
<!-- show inline filter if values more than 20 -->
<input type="text" class="form-control form-control-sm mb-2 product-filter-filter"/>
<input type="text" class="form-control form-control-sm mb-2 filter-lookup-input" placeholder="Search {{ attribute.name + 's' }}"/>
{% endif %}
{% if attribute.item_attribute_values %}
<div class="filter-options">
{% for attr_value in attribute.item_attribute_values %}
<div class="checkbox">
<div class="filter-lookup-wrapper checkbox" data-value="{{ attr_value }}">
<label data-value="{{ attr_value }}">
<input type="checkbox"
class="product-filter attribute-filter"

View File

@ -1,13 +1,14 @@
""" dumb test to check all function calls on known form loads """
import unittest
""" smoak tests to check basic functionality calls on known form loads."""
import frappe
from frappe.desk.form.load import getdoc
from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.www.printview import get_html_and_style
class TestFormLoads(unittest.TestCase):
class TestFormLoads(FrappeTestCase):
@change_settings("Print Settings", {"allow_print_for_cancelled": 1})
def test_load(self):
erpnext_modules = frappe.get_all("Module Def", filters={"app_name": "erpnext"}, pluck="name")
doctypes = frappe.get_all("DocType", {"istable": 0, "issingle": 0, "is_virtual": 0, "module": ("in", erpnext_modules)}, pluck="name")
@ -17,14 +18,35 @@ class TestFormLoads(unittest.TestCase):
if not last_doc:
continue
with self.subTest(msg=f"Loading {doctype} - {last_doc}", doctype=doctype, last_doc=last_doc):
try:
# reset previous response
frappe.response = frappe._dict({"docs":[]})
frappe.response.docinfo = None
self.assertFormLoad(doctype, last_doc)
self.assertDocPrint(doctype, last_doc)
getdoc(doctype, last_doc)
except Exception as e:
self.fail(f"Failed to load {doctype} - {last_doc}: {e}")
def assertFormLoad(self, doctype, docname):
# reset previous response
frappe.response = frappe._dict({"docs":[]})
frappe.response.docinfo = None
self.assertTrue(frappe.response.docs, msg=f"expected document in reponse, found: {frappe.response.docs}")
self.assertTrue(frappe.response.docinfo, msg=f"expected docinfo in reponse, found: {frappe.response.docinfo}")
try:
getdoc(doctype, docname)
except Exception as e:
self.fail(f"Failed to load {doctype}-{docname}: {e}")
self.assertTrue(frappe.response.docs, msg=f"expected document in reponse, found: {frappe.response.docs}")
self.assertTrue(frappe.response.docinfo, msg=f"expected docinfo in reponse, found: {frappe.response.docinfo}")
def assertDocPrint(self, doctype, docname):
doc = frappe.get_doc(doctype, docname)
doc.set("__onload", frappe._dict())
doc.run_method("onload")
messages_before = frappe.get_message_log()
ret = get_html_and_style(doc=doc.as_json(), print_format="Standard", no_letterhead=1)
messages_after = frappe.get_message_log()
if len(messages_after) > len(messages_before):
new_messages = messages_after[len(messages_before):]
self.fail("Print view showing error/warnings: \n"
+ "\n".join(str(msg) for msg in new_messages))
# html should exist
self.assertTrue(bool(ret["html"]))

View File

@ -1163,7 +1163,8 @@ Government,Regierung,
Grand Total,Gesamtbetrag,
Grant,Gewähren,
Grant Application,Antrag bewilligen,
Grant Leaves,Grant Blätter,
Grant Commission,Provision gewähren,
Grant Leaves,Meldungen gewähren,
Grant information.,Gewähren Sie Informationen.,
Grocery,Lebensmittelgeschäft,
Gross Pay,Bruttolohn,
@ -1332,6 +1333,7 @@ Invoices for Costumers.,Rechnungen für Kunden.,
Inward supplies from ISD,Nachschub von ISD,
Inward supplies liable to reverse charge (other than 1 & 2 above),Rückbelastungspflichtige Lieferungen (außer 1 &amp; 2 oben),
Is Active,Ist aktiv(iert),
Is Debit Note,Ist Lastschrift,
Is Default,Ist Standard,
Is Existing Asset,Vermögenswert existiert bereits.,
Is Frozen,Ist gesperrt,

Can't render this file because it is too large.

View File

@ -31,24 +31,6 @@
{% endif %}
</div>
<script>
frappe.ready(() => {
$('.product-filter-filter').on('keydown', frappe.utils.debounce((e) => {
const $input = $(e.target);
const keyword = ($input.val() || '').toLowerCase();
const $filter_options = $input.next('.filter-options');
$filter_options.find('.custom-control').show();
$filter_options.find('.custom-control').each((i, el) => {
const $el = $(el);
const value = $el.data('value').toLowerCase();
if (!value.includes(keyword)) {
$el.hide();
}
});
}, 300));
})
</script>
</div>
</div>