Merge branch 'develop' into dev_enhance_upgrade_process

This commit is contained in:
HENRY Florian 2022-08-31 13:52:28 +02:00 committed by GitHub
commit b05fdb28ff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 208 additions and 275 deletions

View File

@ -22,7 +22,8 @@
"amount", "amount",
"account_currency", "account_currency",
"amount_in_account_currency", "amount_in_account_currency",
"delinked" "delinked",
"remarks"
], ],
"fields": [ "fields": [
{ {
@ -136,12 +137,17 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "Finance Book", "label": "Finance Book",
"options": "Finance Book" "options": "Finance Book"
},
{
"fieldname": "remarks",
"fieldtype": "Text",
"label": "Remarks"
} }
], ],
"in_create": 1, "in_create": 1,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2022-07-11 09:13:54.379168", "modified": "2022-08-22 15:32:56.629430",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Payment Ledger Entry", "name": "Payment Ledger Entry",

View File

@ -178,6 +178,11 @@ frappe.query_reports["Accounts Receivable"] = {
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 1 "hidden": 1
}, },
{
"fieldname": "show_remarks",
"label": __("Show Remarks"),
"fieldtype": "Check",
},
{ {
"fieldname": "customer_name", "fieldname": "customer_name",
"label": __("Customer Name"), "label": __("Customer Name"),

View File

@ -119,6 +119,7 @@ class ReceivablePayableReport(object):
party_account=ple.account, party_account=ple.account,
posting_date=ple.posting_date, posting_date=ple.posting_date,
account_currency=ple.account_currency, account_currency=ple.account_currency,
remarks=ple.remarks,
invoiced=0.0, invoiced=0.0,
paid=0.0, paid=0.0,
credit_note=0.0, credit_note=0.0,
@ -697,6 +698,7 @@ class ReceivablePayableReport(object):
ple.account_currency, ple.account_currency,
ple.amount, ple.amount,
ple.amount_in_account_currency, ple.amount_in_account_currency,
ple.remarks,
) )
.where(ple.delinked == 0) .where(ple.delinked == 0)
.where(Criterion.all(self.qb_selection_filter)) .where(Criterion.all(self.qb_selection_filter))
@ -975,6 +977,9 @@ class ReceivablePayableReport(object):
options="Supplier Group", options="Supplier Group",
) )
if self.filters.show_remarks:
self.add_column(label=_("Remarks"), fieldname="remarks", fieldtype="Text", width=200),
def add_column(self, label, fieldname=None, fieldtype="Currency", options=None, width=120): def add_column(self, label, fieldname=None, fieldtype="Currency", options=None, width=120):
if not fieldname: if not fieldname:
fieldname = scrub(label) fieldname = scrub(label)

View File

@ -1424,6 +1424,7 @@ def create_payment_ledger_entry(
"amount": dr_or_cr, "amount": dr_or_cr,
"amount_in_account_currency": dr_or_cr_account_currency, "amount_in_account_currency": dr_or_cr_account_currency,
"delinked": True if cancel else False, "delinked": True if cancel else False,
"remarks": gle.remarks,
} }
) )

View File

@ -41,6 +41,7 @@ class calculate_taxes_and_totals(object):
if self.doc.apply_discount_on == "Grand Total" and self.doc.get("is_cash_or_non_trade_discount"): if self.doc.apply_discount_on == "Grand Total" and self.doc.get("is_cash_or_non_trade_discount"):
self.doc.grand_total -= self.doc.discount_amount self.doc.grand_total -= self.doc.discount_amount
self.doc.base_grand_total -= self.doc.base_discount_amount self.doc.base_grand_total -= self.doc.base_discount_amount
self.set_rounded_total()
self.calculate_shipping_charges() self.calculate_shipping_charges()

View File

@ -200,7 +200,7 @@ erpnext.ProductSearch = class {
let thumbnail = res.thumbnail || '/assets/erpnext/images/ui-states/cart-empty-state.png'; let thumbnail = res.thumbnail || '/assets/erpnext/images/ui-states/cart-empty-state.png';
html += ` html += `
<div class="dropdown-item" style="display: flex;"> <div class="dropdown-item" style="display: flex;">
<img class="item-thumb col-2" src=${thumbnail} /> <img class="item-thumb col-2" src=${encodeURI(thumbnail)} />
<div class="col-9" style="white-space: normal;"> <div class="col-9" style="white-space: normal;">
<a href="/${res.route}">${res.web_item_name}</a><br> <a href="/${res.route}">${res.web_item_name}</a><br>
<span class="brand-line">${res.brand ? "by " + res.brand : ""}</span> <span class="brand-line">${res.brand ? "by " + res.brand : ""}</span>
@ -241,4 +241,4 @@ erpnext.ProductSearch = class {
this.category_container.html(html); this.category_container.html(html);
} }
}; };

View File

@ -7,7 +7,9 @@ import frappe
from frappe import _ from frappe import _
from frappe.utils.redis_wrapper import RedisWrapper from frappe.utils.redis_wrapper import RedisWrapper
from redis import ResponseError from redis import ResponseError
from redisearch import AutoCompleter, Client, IndexDefinition, Suggestion, TagField, TextField from redis.commands.search.field import TagField, TextField
from redis.commands.search.indexDefinition import IndexDefinition
from redis.commands.search.suggestion import Suggestion
WEBSITE_ITEM_INDEX = "website_items_index" WEBSITE_ITEM_INDEX = "website_items_index"
WEBSITE_ITEM_KEY_PREFIX = "website_item:" WEBSITE_ITEM_KEY_PREFIX = "website_item:"
@ -35,12 +37,9 @@ def is_redisearch_enabled():
def is_search_module_loaded(): def is_search_module_loaded():
try: try:
cache = frappe.cache() cache = frappe.cache()
out = cache.execute_command("MODULE LIST") for module in cache.module_list():
if module.get(b"name") == b"search":
parsed_output = " ".join( return True
(" ".join([frappe.as_unicode(s) for s in o if not isinstance(s, int)]) for o in out)
)
return "search" in parsed_output
except Exception: except Exception:
return False # handling older redis versions return False # handling older redis versions
@ -58,18 +57,18 @@ def if_redisearch_enabled(function):
def make_key(key): def make_key(key):
return "{0}|{1}".format(frappe.conf.db_name, key).encode("utf-8") return frappe.cache().make_key(key)
@if_redisearch_enabled @if_redisearch_enabled
def create_website_items_index(): def create_website_items_index():
"Creates Index Definition." "Creates Index Definition."
# CREATE index redis = frappe.cache()
client = Client(make_key(WEBSITE_ITEM_INDEX), conn=frappe.cache()) index = redis.ft(WEBSITE_ITEM_INDEX)
try: try:
client.drop_index() # drop if already exists index.dropindex() # drop if already exists
except ResponseError: except ResponseError:
# will most likely raise a ResponseError if index does not exist # will most likely raise a ResponseError if index does not exist
# ignore and create index # ignore and create index
@ -86,9 +85,10 @@ def create_website_items_index():
if "web_item_name" in idx_fields: if "web_item_name" in idx_fields:
idx_fields.remove("web_item_name") idx_fields.remove("web_item_name")
idx_fields = list(map(to_search_field, idx_fields)) idx_fields = [to_search_field(f) for f in idx_fields]
client.create_index( # TODO: sortable?
index.create_index(
[TextField("web_item_name", sortable=True)] + idx_fields, [TextField("web_item_name", sortable=True)] + idx_fields,
definition=idx_def, definition=idx_def,
) )
@ -119,8 +119,8 @@ def insert_item_to_index(website_item_doc):
@if_redisearch_enabled @if_redisearch_enabled
def insert_to_name_ac(web_name, doc_name): def insert_to_name_ac(web_name, doc_name):
ac = AutoCompleter(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE), conn=frappe.cache()) ac = frappe.cache().ft()
ac.add_suggestions(Suggestion(web_name, payload=doc_name)) ac.sugadd(WEBSITE_ITEM_NAME_AUTOCOMPLETE, Suggestion(web_name, payload=doc_name))
def create_web_item_map(website_item_doc): def create_web_item_map(website_item_doc):
@ -157,9 +157,8 @@ def delete_item_from_index(website_item_doc):
@if_redisearch_enabled @if_redisearch_enabled
def delete_from_ac_dict(website_item_doc): def delete_from_ac_dict(website_item_doc):
"""Removes this items's name from autocomplete dictionary""" """Removes this items's name from autocomplete dictionary"""
cache = frappe.cache() ac = frappe.cache().ft()
name_ac = AutoCompleter(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE), conn=cache) ac.sugdel(website_item_doc.web_item_name)
name_ac.delete(website_item_doc.web_item_name)
@if_redisearch_enabled @if_redisearch_enabled
@ -170,8 +169,6 @@ def define_autocomplete_dictionary():
""" """
cache = frappe.cache() cache = frappe.cache()
item_ac = AutoCompleter(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE), conn=cache)
item_group_ac = AutoCompleter(make_key(WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE), conn=cache)
# Delete both autocomplete dicts # Delete both autocomplete dicts
try: try:
@ -180,38 +177,43 @@ def define_autocomplete_dictionary():
except Exception: except Exception:
raise_redisearch_error() raise_redisearch_error()
create_items_autocomplete_dict(autocompleter=item_ac) create_items_autocomplete_dict()
create_item_groups_autocomplete_dict(autocompleter=item_group_ac) create_item_groups_autocomplete_dict()
@if_redisearch_enabled @if_redisearch_enabled
def create_items_autocomplete_dict(autocompleter): def create_items_autocomplete_dict():
"Add items as suggestions in Autocompleter." "Add items as suggestions in Autocompleter."
ac = frappe.cache().ft()
items = frappe.get_all( items = frappe.get_all(
"Website Item", fields=["web_item_name", "item_group"], filters={"published": 1} "Website Item", fields=["web_item_name", "item_group"], filters={"published": 1}
) )
for item in items: for item in items:
autocompleter.add_suggestions(Suggestion(item.web_item_name)) ac.sugadd(WEBSITE_ITEM_NAME_AUTOCOMPLETE, Suggestion(item.web_item_name))
@if_redisearch_enabled @if_redisearch_enabled
def create_item_groups_autocomplete_dict(autocompleter): def create_item_groups_autocomplete_dict():
"Add item groups with weightage as suggestions in Autocompleter." "Add item groups with weightage as suggestions in Autocompleter."
published_item_groups = frappe.get_all( published_item_groups = frappe.get_all(
"Item Group", fields=["name", "route", "weightage"], filters={"show_in_website": 1} "Item Group", fields=["name", "route", "weightage"], filters={"show_in_website": 1}
) )
if not published_item_groups: if not published_item_groups:
return return
ac = frappe.cache().ft()
for item_group in published_item_groups: for item_group in published_item_groups:
payload = json.dumps({"name": item_group.name, "route": item_group.route}) payload = json.dumps({"name": item_group.name, "route": item_group.route})
autocompleter.add_suggestions( ac.sugadd(
WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE,
Suggestion( Suggestion(
string=item_group.name, string=item_group.name,
score=frappe.utils.flt(item_group.weightage) or 1.0, score=frappe.utils.flt(item_group.weightage) or 1.0,
payload=payload, # additional info that can be retrieved later payload=payload, # additional info that can be retrieved later
) ),
) )

View File

@ -236,7 +236,7 @@ def get_term_loans(date, term_loan=None, loan_type=None):
AND l.is_term_loan =1 AND l.is_term_loan =1
AND rs.payment_date <= %s AND rs.payment_date <= %s
AND rs.is_accrued=0 {0} AND rs.is_accrued=0 {0}
AND rs.interest_amount > 0 AND rs.principal_amount > 0
AND l.status = 'Disbursed' AND l.status = 'Disbursed'
ORDER BY rs.payment_date""".format( ORDER BY rs.payment_date""".format(
condition condition

View File

@ -735,6 +735,7 @@ def get_amounts(amounts, against_loan, posting_date):
) )
amounts["pending_accrual_entries"] = pending_accrual_entries amounts["pending_accrual_entries"] = pending_accrual_entries
amounts["unaccrued_interest"] = flt(unaccrued_interest, precision) amounts["unaccrued_interest"] = flt(unaccrued_interest, precision)
amounts["written_off_amount"] = flt(against_loan_doc.written_off_amount, precision)
if final_due_date: if final_due_date:
amounts["due_date"] = final_due_date amounts["due_date"] = final_due_date

View File

@ -57,7 +57,7 @@ def process_loan_interest_accrual_for_demand_loans(
def process_loan_interest_accrual_for_term_loans(posting_date=None, loan_type=None, loan=None): def process_loan_interest_accrual_for_term_loans(posting_date=None, loan_type=None, loan=None):
if not term_loan_accrual_pending(posting_date or nowdate()): if not term_loan_accrual_pending(posting_date or nowdate(), loan=loan):
return return
loan_process = frappe.new_doc("Process Loan Interest Accrual") loan_process = frappe.new_doc("Process Loan Interest Accrual")
@ -71,9 +71,12 @@ def process_loan_interest_accrual_for_term_loans(posting_date=None, loan_type=No
return loan_process.name return loan_process.name
def term_loan_accrual_pending(date): def term_loan_accrual_pending(date, loan=None):
pending_accrual = frappe.db.get_value( filters = {"payment_date": ("<=", date), "is_accrued": 0}
"Repayment Schedule", {"payment_date": ("<=", date), "is_accrued": 0}
) if loan:
filters.update({"parent": loan})
pending_accrual = frappe.db.get_value("Repayment Schedule", filters)
return pending_accrual return pending_accrual

View File

@ -311,4 +311,5 @@ erpnext.patches.v14_0.remove_india_localisation # 14-07-2022
erpnext.patches.v13_0.fix_number_and_frequency_for_monthly_depreciation erpnext.patches.v13_0.fix_number_and_frequency_for_monthly_depreciation
erpnext.patches.v14_0.remove_hr_and_payroll_modules # 20-07-2022 erpnext.patches.v14_0.remove_hr_and_payroll_modules # 20-07-2022
erpnext.patches.v14_0.fix_crm_no_of_employees erpnext.patches.v14_0.fix_crm_no_of_employees
erpnext.patches.v14_0.create_accounting_dimensions_in_subcontracting_doctypes erpnext.patches.v14_0.create_accounting_dimensions_in_subcontracting_doctypes
erpnext.patches.v14_0.migrate_remarks_from_gl_to_payment_ledger

View File

@ -14,7 +14,8 @@ def execute():
for sla in frappe.get_all("Service Level Agreement"): for sla in frappe.get_all("Service Level Agreement"):
agreement = frappe.get_doc("Service Level Agreement", sla.name) agreement = frappe.get_doc("Service Level Agreement", sla.name)
agreement.document_type = "Issue" agreement.db_set("document_type", "Issue")
agreement.reload()
agreement.apply_sla_for_resolution = 1 agreement.apply_sla_for_resolution = 1
agreement.append("sla_fulfilled_on", {"status": "Resolved"}) agreement.append("sla_fulfilled_on", {"status": "Resolved"})
agreement.append("sla_fulfilled_on", {"status": "Closed"}) agreement.append("sla_fulfilled_on", {"status": "Closed"})

View File

@ -16,18 +16,18 @@ def execute():
delete_auto_email_reports(report) delete_auto_email_reports(report)
check_and_delete_linked_reports(report) check_and_delete_linked_reports(report)
frappe.delete_doc("Report", report) frappe.delete_doc("Report", report, force=True)
def delete_auto_email_reports(report): def delete_auto_email_reports(report):
"""Check for one or multiple Auto Email Reports and delete""" """Check for one or multiple Auto Email Reports and delete"""
auto_email_reports = frappe.db.get_values("Auto Email Report", {"report": report}, ["name"]) auto_email_reports = frappe.db.get_values("Auto Email Report", {"report": report}, ["name"])
for auto_email_report in auto_email_reports: for auto_email_report in auto_email_reports:
frappe.delete_doc("Auto Email Report", auto_email_report[0]) frappe.delete_doc("Auto Email Report", auto_email_report[0], force=True)
def delete_links_from_desktop_icons(report): def delete_links_from_desktop_icons(report):
"""Check for one or multiple Desktop Icons and delete""" """Check for one or multiple Desktop Icons and delete"""
desktop_icons = frappe.db.get_values("Desktop Icon", {"_report": report}, ["name"]) desktop_icons = frappe.db.get_values("Desktop Icon", {"_report": report}, ["name"])
for desktop_icon in desktop_icons: for desktop_icon in desktop_icons:
frappe.delete_doc("Desktop Icon", desktop_icon[0]) frappe.delete_doc("Desktop Icon", desktop_icon[0], force=True)

View File

@ -0,0 +1,56 @@
import frappe
from frappe import qb
from frappe.utils import create_batch
def execute():
if frappe.reload_doc("accounts", "doctype", "payment_ledger_entry"):
gle = qb.DocType("GL Entry")
ple = qb.DocType("Payment Ledger Entry")
# get ple and their remarks from GL Entry
pl_entries = (
qb.from_(ple)
.left_join(gle)
.on(
(ple.account == gle.account)
& (ple.party_type == gle.party_type)
& (ple.party == gle.party)
& (ple.voucher_type == gle.voucher_type)
& (ple.voucher_no == gle.voucher_no)
& (ple.company == gle.company)
)
.select(
ple.company,
ple.account,
ple.party_type,
ple.party,
ple.voucher_type,
ple.voucher_no,
gle.remarks.as_("gle_remarks"),
)
.where((ple.delinked == 0) & (gle.is_cancelled == 0))
.run(as_dict=True)
)
if pl_entries:
# split into multiple batches, update and commit for each batch
batch_size = 1000
for batch in create_batch(pl_entries, batch_size):
for entry in batch:
query = (
qb.update(ple)
.set(ple.remarks, entry.gle_remarks)
.where(
(ple.company == entry.company)
& (ple.account == entry.account)
& (ple.party_type == entry.party_type)
& (ple.party == entry.party)
& (ple.voucher_type == entry.voucher_type)
& (ple.voucher_no == entry.voucher_no)
)
)
query.run()
frappe.db.commit()

View File

@ -1,127 +1,70 @@
{ {
"allow_copy": 0, "actions": [],
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"autoname": "Prompt", "autoname": "Prompt",
"beta": 0,
"creation": "2019-04-19 15:04:05.317138", "creation": "2019-04-19 15:04:05.317138",
"custom": 0,
"docstatus": 0,
"doctype": "DocType", "doctype": "DocType",
"document_type": "",
"editable_grid": 0,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [
"weight",
"description"
],
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "weight", "fieldname": "weight",
"fieldtype": "Float", "fieldtype": "Float",
"hidden": 0, "label": "Weight"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Weight",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "description", "fieldname": "description",
"fieldtype": "Small Text", "fieldtype": "Small Text",
"hidden": 0, "label": "Description"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Description",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
} }
], ],
"has_web_view": 0, "links": [],
"hide_toolbar": 0, "modified": "2022-08-29 17:46:41.342979",
"idx": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2019-04-19 15:31:48.080164",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Projects", "module": "Projects",
"name": "Task Type", "name": "Task Type",
"name_case": "", "naming_rule": "Set by user",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
"amend": 0,
"cancel": 0,
"create": 1, "create": 1,
"delete": 1, "delete": 1,
"email": 1, "email": 1,
"export": 1, "export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1, "print": 1,
"read": 1, "read": 1,
"report": 1, "report": 1,
"role": "System Manager", "role": "System Manager",
"set_user_permissions": 0,
"share": 1, "share": 1,
"submit": 0,
"write": 1 "write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Projects Manager",
"share": 1,
"write": 1
},
{
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Projects User",
"share": 1
} }
], ],
"quick_entry": 1, "quick_entry": 1,
"read_only": 0,
"show_name_in_global_search": 0,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "ASC", "sort_order": "ASC",
"track_changes": 1, "states": [],
"track_seen": 0, "track_changes": 1
"track_views": 0
} }

View File

@ -268,7 +268,7 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False):
def set_expired_status(): def set_expired_status():
# filter out submitted non expired quotations whose validity has been ended # filter out submitted non expired quotations whose validity has been ended
cond = "`tabQuotation`.docstatus = 1 and `tabQuotation`.status != 'Expired' and `tabQuotation`.valid_till < %s" cond = "`tabQuotation`.docstatus = 1 and `tabQuotation`.status NOT IN ('Expired', 'Lost') and `tabQuotation`.valid_till < %s"
# check if those QUO have SO against it # check if those QUO have SO against it
so_against_quo = """ so_against_quo = """
SELECT SELECT

View File

@ -2568,27 +2568,26 @@ def get_supplied_items(
@frappe.whitelist() @frappe.whitelist()
def get_items_from_subcontracting_order(source_name, target_doc=None): def get_items_from_subcontracting_order(source_name, target_doc=None):
sco = frappe.get_doc("Subcontracting Order", source_name) def post_process(source, target):
target.stock_entry_type = target.purpose = "Send to Subcontractor"
target.subcontracting_order = source_name
if sco.docstatus == 1: if target.items:
if target_doc and isinstance(target_doc, str): target.items = []
target_doc = frappe.get_doc(json.loads(target_doc))
if target_doc.items:
target_doc.items = []
warehouses = {} warehouses = {}
for item in sco.items: for item in source.items:
warehouses[item.name] = item.warehouse warehouses[item.name] = item.warehouse
for item in sco.supplied_items: for item in source.supplied_items:
target_doc.append( target.append(
"items", "items",
{ {
"s_warehouse": warehouses.get(item.reference_name), "s_warehouse": warehouses.get(item.reference_name),
"t_warehouse": sco.supplier_warehouse, "t_warehouse": source.supplier_warehouse,
"subcontracted_item": item.main_item_code,
"item_code": item.rm_item_code, "item_code": item.rm_item_code,
"qty": item.required_qty, "qty": max(item.required_qty - item.total_supplied_qty, 0),
"transfer_qty": item.required_qty, "transfer_qty": item.required_qty,
"uom": item.stock_uom, "uom": item.stock_uom,
"stock_uom": item.stock_uom, "stock_uom": item.stock_uom,
@ -2596,6 +2595,23 @@ def get_items_from_subcontracting_order(source_name, target_doc=None):
}, },
) )
target_doc = get_mapped_doc(
"Subcontracting Order",
source_name,
{
"Subcontracting Order": {
"doctype": "Stock Entry",
"field_no_map": ["purchase_order"],
"validation": {
"docstatus": ["=", 1],
},
},
},
target_doc,
post_process,
ignore_child_tables=True,
)
return target_doc return target_doc

View File

@ -164,10 +164,7 @@ erpnext.buying.SubcontractingOrderController = class SubcontractingOrderControll
if (flt(doc.per_received) < 100) { if (flt(doc.per_received) < 100) {
cur_frm.add_custom_button(__('Subcontracting Receipt'), this.make_subcontracting_receipt, __('Create')); cur_frm.add_custom_button(__('Subcontracting Receipt'), this.make_subcontracting_receipt, __('Create'));
if (me.has_unsupplied_items()) { if (me.has_unsupplied_items()) {
cur_frm.add_custom_button(__('Material to Supplier'), cur_frm.add_custom_button(__('Material to Supplier'), this.make_stock_entry, __('Transfer'));
() => {
me.make_stock_entry();
}, __('Transfer'));
} }
} }
cur_frm.page.set_inner_btn_group_as_primary(__('Create')); cur_frm.page.set_inner_btn_group_as_primary(__('Create'));
@ -195,120 +192,6 @@ erpnext.buying.SubcontractingOrderController = class SubcontractingOrderControll
transaction_controller.autofill_warehouse(child_table, warehouse_field, warehouse); transaction_controller.autofill_warehouse(child_table, warehouse_field, warehouse);
} }
make_stock_entry() {
var items = $.map(cur_frm.doc.items, (d) => d.bom ? d.item_code : false);
var me = this;
if (items.length >= 1) {
me.raw_material_data = [];
me.show_dialog = 1;
let title = __('Transfer Material to Supplier');
let fields = [
{ fieldtype: 'Section Break', label: __('Raw Materials') },
{
fieldname: 'sub_con_rm_items', fieldtype: 'Table', label: __('Items'),
fields: [
{
fieldtype: 'Data',
fieldname: 'item_code',
label: __('Item'),
read_only: 1,
in_list_view: 1
},
{
fieldtype: 'Data',
fieldname: 'rm_item_code',
label: __('Raw Material'),
read_only: 1,
in_list_view: 1
},
{
fieldtype: 'Float',
read_only: 1,
fieldname: 'qty',
label: __('Quantity'),
in_list_view: 1
},
{
fieldtype: 'Data',
read_only: 1,
fieldname: 'warehouse',
label: __('Reserve Warehouse'),
in_list_view: 1
},
{
fieldtype: 'Float',
read_only: 1,
fieldname: 'rate',
label: __('Rate'),
hidden: 1
},
{
fieldtype: 'Float',
read_only: 1,
fieldname: 'amount',
label: __('Amount'),
hidden: 1
},
{
fieldtype: 'Link',
read_only: 1,
fieldname: 'uom',
label: __('UOM'),
hidden: 1
}
],
data: me.raw_material_data,
get_data: () => me.raw_material_data
}
];
me.dialog = new frappe.ui.Dialog({
title: title, fields: fields
});
if (me.frm.doc['supplied_items']) {
me.frm.doc['supplied_items'].forEach((item) => {
if (item.rm_item_code && item.main_item_code && item.required_qty - item.supplied_qty != 0) {
me.raw_material_data.push({
'name': item.name,
'item_code': item.main_item_code,
'rm_item_code': item.rm_item_code,
'item_name': item.rm_item_code,
'qty': item.required_qty - item.supplied_qty,
'warehouse': item.reserve_warehouse,
'rate': item.rate,
'amount': item.amount,
'stock_uom': item.stock_uom
});
me.dialog.fields_dict.sub_con_rm_items.grid.refresh();
}
});
}
me.dialog.get_field('sub_con_rm_items').check_all_rows();
me.dialog.show();
this.dialog.set_primary_action(__('Transfer'), () => {
me.values = me.dialog.get_values();
if (me.values) {
me.values.sub_con_rm_items.map((row, i) => {
if (!row.item_code || !row.rm_item_code || !row.warehouse || !row.qty || row.qty === 0) {
let row_id = i + 1;
frappe.throw(__('Item Code, warehouse and quantity are required on row {0}', [row_id]));
}
});
me.make_rm_stock_entry(me.dialog.fields_dict.sub_con_rm_items.grid.get_selected_children());
me.dialog.hide();
}
});
}
me.dialog.get_close_btn().on('click', () => {
me.dialog.hide();
});
}
has_unsupplied_items() { has_unsupplied_items() {
return this.frm.doc['supplied_items'].some(item => item.required_qty > item.supplied_qty); return this.frm.doc['supplied_items'].some(item => item.required_qty > item.supplied_qty);
} }
@ -321,6 +204,15 @@ erpnext.buying.SubcontractingOrderController = class SubcontractingOrderControll
}); });
} }
make_stock_entry() {
frappe.model.open_mapped_doc({
method: 'erpnext.stock.doctype.stock_entry.stock_entry.get_items_from_subcontracting_order',
source_name: cur_frm.doc.name,
freeze: true,
freeze_message: __('Creating Stock Entry ...')
});
}
make_rm_stock_entry(rm_items) { make_rm_stock_entry(rm_items) {
frappe.call({ frappe.call({
method: 'erpnext.controllers.subcontracting_controller.make_rm_stock_entry', method: 'erpnext.controllers.subcontracting_controller.make_rm_stock_entry',

View File

@ -5,14 +5,13 @@ import json
import frappe import frappe
from frappe.utils import cint, cstr from frappe.utils import cint, cstr
from redisearch import AutoCompleter, Client, Query from redis.commands.search.query import Query
from erpnext.e_commerce.redisearch_utils import ( from erpnext.e_commerce.redisearch_utils import (
WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE, WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE,
WEBSITE_ITEM_INDEX, WEBSITE_ITEM_INDEX,
WEBSITE_ITEM_NAME_AUTOCOMPLETE, WEBSITE_ITEM_NAME_AUTOCOMPLETE,
is_redisearch_enabled, is_redisearch_enabled,
make_key,
) )
from erpnext.e_commerce.shopping_cart.product_info import set_product_info_for_website from erpnext.e_commerce.shopping_cart.product_info import set_product_info_for_website
from erpnext.setup.doctype.item_group.item_group import get_item_for_list_in_html from erpnext.setup.doctype.item_group.item_group import get_item_for_list_in_html
@ -88,15 +87,17 @@ def product_search(query, limit=10, fuzzy_search=True):
if not query: if not query:
return search_results return search_results
red = frappe.cache() redis = frappe.cache()
query = clean_up_query(query) query = clean_up_query(query)
# TODO: Check perf/correctness with Suggestions & Query vs only Query # TODO: Check perf/correctness with Suggestions & Query vs only Query
# TODO: Use Levenshtein Distance in Query (max=3) # TODO: Use Levenshtein Distance in Query (max=3)
ac = AutoCompleter(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE), conn=red) redisearch = redis.ft(WEBSITE_ITEM_INDEX)
client = Client(make_key(WEBSITE_ITEM_INDEX), conn=red) suggestions = redisearch.sugget(
suggestions = ac.get_suggestions( WEBSITE_ITEM_NAME_AUTOCOMPLETE,
query, num=limit, fuzzy=fuzzy_search and len(query) > 3 # Fuzzy on length < 3 can be real slow query,
num=limit,
fuzzy=fuzzy_search and len(query) > 3,
) )
# Build a query # Build a query
@ -106,8 +107,8 @@ def product_search(query, limit=10, fuzzy_search=True):
query_string += f"|('{clean_up_query(s.string)}')" query_string += f"|('{clean_up_query(s.string)}')"
q = Query(query_string) q = Query(query_string)
results = redisearch.search(q)
results = client.search(q)
search_results["results"] = list(map(convert_to_dict, results.docs)) search_results["results"] = list(map(convert_to_dict, results.docs))
search_results["results"] = sorted( search_results["results"] = sorted(
search_results["results"], key=lambda k: frappe.utils.cint(k["ranking"]), reverse=True search_results["results"], key=lambda k: frappe.utils.cint(k["ranking"]), reverse=True
@ -141,8 +142,8 @@ def get_category_suggestions(query):
if not query: if not query:
return search_results return search_results
ac = AutoCompleter(make_key(WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE), conn=frappe.cache()) ac = frappe.cache().ft()
suggestions = ac.get_suggestions(query, num=10, with_payloads=True) suggestions = ac.sugget(WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE, query, num=10, with_payloads=True)
results = [json.loads(s.payload) for s in suggestions] results = [json.loads(s.payload) for s in suggestions]

View File

@ -12,7 +12,6 @@ dependencies = [
"pycountry~=20.7.3", "pycountry~=20.7.3",
"python-stdnum~=1.16", "python-stdnum~=1.16",
"Unidecode~=1.2.0", "Unidecode~=1.2.0",
"redisearch~=2.1.0",
# integration dependencies # integration dependencies
"gocardless-pro~=1.22.0", "gocardless-pro~=1.22.0",