Merge pull request #38852 from frappe/version-14-hotfix

chore: release v14
This commit is contained in:
Deepesh Garg
2023-12-20 10:14:59 +05:30
committed by GitHub
41 changed files with 560 additions and 191 deletions

View File

@@ -111,8 +111,6 @@ class PaymentReconciliation(Document):
def get_payment_entries(self): def get_payment_entries(self):
order_doctype = "Sales Order" if self.party_type == "Customer" else "Purchase Order" order_doctype = "Sales Order" if self.party_type == "Customer" else "Purchase Order"
condition = self.get_conditions(get_payments=True) condition = self.get_conditions(get_payments=True)
if self.payment_name:
condition += "name like '%%{0}%%'".format(self.payment_name)
payment_entries = get_advance_payment_entries_for_regional( payment_entries = get_advance_payment_entries_for_regional(
self.party_type, self.party_type,
@@ -122,6 +120,7 @@ class PaymentReconciliation(Document):
against_all_orders=True, against_all_orders=True,
limit=self.payment_limit, limit=self.payment_limit,
condition=condition, condition=condition,
payment_name=self.payment_name,
) )
return payment_entries return payment_entries

View File

@@ -15,6 +15,7 @@
"group_by", "group_by",
"cost_center", "cost_center",
"territory", "territory",
"ignore_exchange_rate_revaluation_journals",
"column_break_14", "column_break_14",
"to_date", "to_date",
"finance_book", "finance_book",
@@ -374,10 +375,16 @@
"fieldname": "pdf_name", "fieldname": "pdf_name",
"fieldtype": "Data", "fieldtype": "Data",
"label": "PDF Name" "label": "PDF Name"
},
{
"default": "0",
"fieldname": "ignore_exchange_rate_revaluation_journals",
"fieldtype": "Check",
"label": "Ignore Exchange Rate Revaluation Journals"
} }
], ],
"links": [], "links": [],
"modified": "2023-08-28 12:59:53.071334", "modified": "2023-12-18 12:20:08.965120",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Process Statement Of Accounts", "name": "Process Statement Of Accounts",

View File

@@ -64,6 +64,18 @@ def get_statement_dict(doc, get_statement_dict=False):
statement_dict = {} statement_dict = {}
ageing = "" ageing = ""
err_journals = None
if doc.report == "General Ledger" and doc.ignore_exchange_rate_revaluation_journals:
err_journals = frappe.db.get_all(
"Journal Entry",
filters={
"company": doc.company,
"docstatus": 1,
"voucher_type": ("in", ["Exchange Rate Revaluation", "Exchange Gain Or Loss"]),
},
as_list=True,
)
for entry in doc.customers: for entry in doc.customers:
if doc.include_ageing: if doc.include_ageing:
ageing = set_ageing(doc, entry) ageing = set_ageing(doc, entry)
@@ -76,6 +88,8 @@ def get_statement_dict(doc, get_statement_dict=False):
) )
filters = get_common_filters(doc) filters = get_common_filters(doc)
if err_journals:
filters.update({"voucher_no_not_in": [x[0] for x in err_journals]})
if doc.report == "General Ledger": if doc.report == "General Ledger":
filters.update(get_gl_filters(doc, entry, tax_id, presentation_currency)) filters.update(get_gl_filters(doc, entry, tax_id, presentation_currency))

View File

@@ -2195,9 +2195,18 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
def get_received_items(reference_name, doctype, reference_fieldname): def get_received_items(reference_name, doctype, reference_fieldname):
reference_field = "inter_company_invoice_reference"
if doctype == "Purchase Order":
reference_field = "inter_company_order_reference"
filters = {
reference_field: reference_name,
"docstatus": 1,
}
target_doctypes = frappe.get_all( target_doctypes = frappe.get_all(
doctype, doctype,
filters={"inter_company_invoice_reference": reference_name, "docstatus": 1}, filters=filters,
as_list=True, as_list=True,
) )

View File

@@ -244,8 +244,12 @@ class ReceivablePayableReport(object):
row.invoiced_in_account_currency += amount_in_account_currency row.invoiced_in_account_currency += amount_in_account_currency
else: else:
if self.is_invoice(ple): if self.is_invoice(ple):
row.credit_note -= amount if row.voucher_no == ple.voucher_no == ple.against_voucher_no:
row.credit_note_in_account_currency -= amount_in_account_currency row.paid -= amount
row.paid_in_account_currency -= amount_in_account_currency
else:
row.credit_note -= amount
row.credit_note_in_account_currency -= amount_in_account_currency
else: else:
row.paid -= amount row.paid -= amount
row.paid_in_account_currency -= amount_in_account_currency row.paid_in_account_currency -= amount_in_account_currency

View File

@@ -76,6 +76,41 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
return credit_note return credit_note
def test_pos_receivable(self):
filters = {
"company": self.company,
"party_type": "Customer",
"party": [self.customer],
"report_date": add_days(today(), 2),
"based_on_payment_terms": 0,
"range1": 30,
"range2": 60,
"range3": 90,
"range4": 120,
"show_remarks": False,
}
pos_inv = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
pos_inv.posting_date = add_days(today(), 2)
pos_inv.is_pos = 1
pos_inv.append(
"payments",
frappe._dict(
mode_of_payment="Cash",
amount=flt(pos_inv.grand_total / 2),
),
)
pos_inv.disable_rounded_total = 1
pos_inv.save()
pos_inv.submit()
report = execute(filters)
expected_data = [[pos_inv.grand_total, pos_inv.paid_amount, 0]]
row = report[1][-1]
self.assertEqual(expected_data[0], [row.invoiced, row.paid, row.credit_note])
pos_inv.cancel()
def test_accounts_receivable(self): def test_accounts_receivable(self):
filters = { filters = {
"company": self.company, "company": self.company,

View File

@@ -231,6 +231,9 @@ def get_conditions(filters):
if filters.get("voucher_no"): if filters.get("voucher_no"):
conditions.append("voucher_no=%(voucher_no)s") conditions.append("voucher_no=%(voucher_no)s")
if filters.get("voucher_no_not_in"):
conditions.append("voucher_no not in %(voucher_no_not_in)s")
if filters.get("group_by") == "Group by Party" and not filters.get("party_type"): if filters.get("group_by") == "Group by Party" and not filters.get("party_type"):
conditions.append("party_type in ('Customer', 'Supplier')") conditions.append("party_type in ('Customer', 'Supplier')")

View File

@@ -626,8 +626,10 @@ def update_reference_in_payment_entry(
"total_amount": d.grand_total, "total_amount": d.grand_total,
"outstanding_amount": d.outstanding_amount, "outstanding_amount": d.outstanding_amount,
"allocated_amount": d.allocated_amount, "allocated_amount": d.allocated_amount,
"exchange_rate": d.exchange_rate if d.exchange_gain_loss else payment_entry.get_exchange_rate(), "exchange_rate": d.exchange_rate
"exchange_gain_loss": d.exchange_gain_loss, if d.difference_amount is not None
else payment_entry.get_exchange_rate(),
"exchange_gain_loss": d.difference_amount,
} }
if d.voucher_detail_no: if d.voucher_detail_no:

View File

@@ -80,6 +80,15 @@ class RequestforQuotation(BuyingController):
supplier.quote_status = "Pending" supplier.quote_status = "Pending"
self.send_to_supplier() self.send_to_supplier()
def before_print(self, settings=None):
"""Use the first suppliers data to render the print preview."""
if self.vendor or not self.suppliers:
# If a specific supplier is already set, via Tools > Download PDF,
# we don't want to override it.
return
self.update_supplier_part_no(self.suppliers[0].supplier)
def on_cancel(self): def on_cancel(self):
self.db_set("status", "Cancelled") self.db_set("status", "Cancelled")

View File

@@ -165,6 +165,7 @@ class AccountsController(TransactionBase):
self.disable_pricing_rule_on_internal_transfer() self.disable_pricing_rule_on_internal_transfer()
self.disable_tax_included_prices_for_internal_transfer() self.disable_tax_included_prices_for_internal_transfer()
self.set_incoming_rate() self.set_incoming_rate()
self.init_internal_values()
if self.meta.get_field("currency"): if self.meta.get_field("currency"):
self.calculate_taxes_and_totals() self.calculate_taxes_and_totals()
@@ -224,6 +225,16 @@ class AccountsController(TransactionBase):
self.set_total_in_words() self.set_total_in_words()
def init_internal_values(self):
# init all the internal values as 0 on sa
if self.docstatus.is_draft():
# TODO: Add all such pending values here
fields = ["billed_amt", "delivered_qty"]
for item in self.get("items"):
for field in fields:
if hasattr(item, field):
item.set(field, 0)
def before_cancel(self): def before_cancel(self):
validate_einvoice_fields(self) validate_einvoice_fields(self)
@@ -629,6 +640,7 @@ class AccountsController(TransactionBase):
args["doctype"] = self.doctype args["doctype"] = self.doctype
args["name"] = self.name args["name"] = self.name
args["child_doctype"] = item.doctype
args["child_docname"] = item.name args["child_docname"] = item.name
args["ignore_pricing_rule"] = ( args["ignore_pricing_rule"] = (
self.ignore_pricing_rule if hasattr(self, "ignore_pricing_rule") else 0 self.ignore_pricing_rule if hasattr(self, "ignore_pricing_rule") else 0
@@ -2504,6 +2516,7 @@ def get_advance_payment_entries(
against_all_orders=False, against_all_orders=False,
limit=None, limit=None,
condition=None, condition=None,
payment_name=None,
): ):
party_account_field = "paid_from" if party_type == "Customer" else "paid_to" party_account_field = "paid_from" if party_type == "Customer" else "paid_to"
currency_field = ( currency_field = (
@@ -2526,6 +2539,10 @@ def get_advance_payment_entries(
reference_condition = "" reference_condition = ""
order_list = [] order_list = []
payment_name_filter = ""
if payment_name:
payment_name_filter = " and t1.name like '%%{0}%%'".format(payment_name)
if not condition: if not condition:
condition = "" condition = ""
@@ -2540,7 +2557,7 @@ def get_advance_payment_entries(
where where
t1.name = t2.parent and t1.{1} = %s and t1.payment_type = %s t1.name = t2.parent and t1.{1} = %s and t1.payment_type = %s
and t1.party_type = %s and t1.party = %s and t1.docstatus = 1 and t1.party_type = %s and t1.party = %s and t1.docstatus = 1
and t2.reference_doctype = %s {2} {3} and t2.reference_doctype = %s {2} {3} {6}
order by t1.posting_date {4} order by t1.posting_date {4}
""".format( """.format(
currency_field, currency_field,
@@ -2549,12 +2566,17 @@ def get_advance_payment_entries(
condition, condition,
limit_cond, limit_cond,
exchange_rate_field, exchange_rate_field,
payment_name_filter,
), ),
[party_account, payment_type, party_type, party, order_doctype] + order_list, [party_account, payment_type, party_type, party, order_doctype] + order_list,
as_dict=1, as_dict=1,
) )
if include_unallocated: if include_unallocated:
payment_name_filter = ""
if payment_name:
payment_name_filter = " and name like '%%{0}%%'".format(payment_name)
unallocated_payment_entries = frappe.db.sql( unallocated_payment_entries = frappe.db.sql(
""" """
select 'Payment Entry' as reference_type, name as reference_name, posting_date, select 'Payment Entry' as reference_type, name as reference_name, posting_date,
@@ -2562,10 +2584,15 @@ def get_advance_payment_entries(
from `tabPayment Entry` from `tabPayment Entry`
where where
{0} = %s and party_type = %s and party = %s and payment_type = %s {0} = %s and party_type = %s and party = %s and payment_type = %s
and docstatus = 1 and unallocated_amount > 0 {condition} and docstatus = 1 and unallocated_amount > 0 {condition} {4}
order by posting_date {1} order by posting_date {1}
""".format( """.format(
party_account_field, limit_cond, exchange_rate_field, currency_field, condition=condition or "" party_account_field,
limit_cond,
exchange_rate_field,
currency_field,
payment_name_filter,
condition=condition or "",
), ),
(party_account, party_type, party, payment_type), (party_account, party_type, party, payment_type),
as_dict=1, as_dict=1,

View File

@@ -354,7 +354,11 @@ class BuyingController(SubcontractingController):
rate = flt(outgoing_rate * (d.conversion_factor or 1), d.precision("rate")) rate = flt(outgoing_rate * (d.conversion_factor or 1), d.precision("rate"))
else: else:
field = "incoming_rate" if self.get("is_internal_supplier") else "rate" field = (
"incoming_rate"
if self.get("is_internal_supplier") and not self.doctype == "Purchase Order"
else "rate"
)
rate = flt( rate = flt(
frappe.db.get_value(ref_doctype, d.get(frappe.scrub(ref_doctype)), field) frappe.db.get_value(ref_doctype, d.get(frappe.scrub(ref_doctype)), field)
* (d.conversion_factor or 1), * (d.conversion_factor or 1),

View File

@@ -52,10 +52,24 @@ def make_variant_based_on_manufacturer(template, manufacturer, manufacturer_part
copy_attributes_to_variant(template, variant) copy_attributes_to_variant(template, variant)
variant.manufacturer = manufacturer
variant.manufacturer_part_no = manufacturer_part_no
variant.item_code = append_number_if_name_exists("Item", template.name) variant.item_code = append_number_if_name_exists("Item", template.name)
variant.flags.ignore_mandatory = True
variant.save()
if not frappe.db.exists(
"Item Manufacturer", {"item_code": variant.name, "manufacturer": manufacturer}
):
manufacturer_doc = frappe.new_doc("Item Manufacturer")
manufacturer_doc.update(
{
"item_code": variant.name,
"manufacturer": manufacturer,
"manufacturer_part_no": manufacturer_part_no,
}
)
manufacturer_doc.flags.ignore_mandatory = True
manufacturer_doc.save(ignore_permissions=True)
return variant return variant

View File

@@ -12,7 +12,7 @@ from erpnext.controllers.sales_and_purchase_return import get_rate_for_return
from erpnext.controllers.stock_controller import StockController from erpnext.controllers.stock_controller import StockController
from erpnext.stock.doctype.item.item import set_item_default from erpnext.stock.doctype.item.item import set_item_default
from erpnext.stock.get_item_details import get_bin_details, get_conversion_factor from erpnext.stock.get_item_details import get_bin_details, get_conversion_factor
from erpnext.stock.utils import get_incoming_rate from erpnext.stock.utils import get_incoming_rate, get_valuation_method
class SellingController(StockController): class SellingController(StockController):
@@ -422,11 +422,13 @@ class SellingController(StockController):
items = self.get("items") + (self.get("packed_items") or []) items = self.get("items") + (self.get("packed_items") or [])
for d in items: for d in items:
if not self.get("return_against"): if not self.get("return_against") or (
get_valuation_method(d.item_code) == "Moving Average" and self.get("is_return")
):
# Get incoming rate based on original item cost based on valuation method # Get incoming rate based on original item cost based on valuation method
qty = flt(d.get("stock_qty") or d.get("actual_qty")) qty = flt(d.get("stock_qty") or d.get("actual_qty"))
if not (self.get("is_return") and d.incoming_rate): if not d.incoming_rate:
d.incoming_rate = get_incoming_rate( d.incoming_rate = get_incoming_rate(
{ {
"item_code": d.item_code, "item_code": d.item_code,

View File

@@ -514,7 +514,7 @@
"idx": 5, "idx": 5,
"image_field": "image", "image_field": "image",
"links": [], "links": [],
"modified": "2022-10-13 12:42:04.277879", "modified": "2023-12-01 18:46:49.468526",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "CRM", "module": "CRM",
"name": "Lead", "name": "Lead",
@@ -576,10 +576,11 @@
], ],
"search_fields": "lead_name,lead_owner,status", "search_fields": "lead_name,lead_owner,status",
"sender_field": "email_id", "sender_field": "email_id",
"sender_name_field": "lead_name",
"show_name_in_global_search": 1, "show_name_in_global_search": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [], "states": [],
"subject_field": "title", "subject_field": "title",
"title_field": "title" "title_field": "title"
} }

View File

@@ -14,6 +14,7 @@ from frappe.utils import comma_and, get_link_to_form, has_gravatar, validate_ema
from erpnext.accounts.party import set_taxes from erpnext.accounts.party import set_taxes
from erpnext.controllers.selling_controller import SellingController from erpnext.controllers.selling_controller import SellingController
from erpnext.crm.utils import CRMNote, copy_comments, link_communications, link_open_events from erpnext.crm.utils import CRMNote, copy_comments, link_communications, link_open_events
from erpnext.selling.doctype.customer.customer import parse_full_name
class Lead(SellingController, CRMNote): class Lead(SellingController, CRMNote):
@@ -48,6 +49,10 @@ class Lead(SellingController, CRMNote):
return return
self.contact_doc = self.create_contact() self.contact_doc = self.create_contact()
# leads created by email inbox only have the full name set
if self.lead_name and not any([self.first_name, self.middle_name, self.last_name]):
self.first_name, self.middle_name, self.last_name = parse_full_name(self.lead_name)
def after_insert(self): def after_insert(self):
self.link_to_contact() self.link_to_contact()

View File

@@ -508,6 +508,7 @@ class ProductionPlan(Document):
if close: if close:
self.db_set("status", "Closed") self.db_set("status", "Closed")
self.update_bin_qty()
return return
if self.total_produced_qty > 0: if self.total_produced_qty > 0:
@@ -522,6 +523,9 @@ class ProductionPlan(Document):
if close is not None: if close is not None:
self.db_set("status", self.status) self.db_set("status", self.status)
if self.docstatus == 1 and self.status != "Completed":
self.update_bin_qty()
def update_ordered_status(self): def update_ordered_status(self):
update_status = False update_status = False
for d in self.po_items: for d in self.po_items:

View File

@@ -1447,6 +1447,47 @@ class TestProductionPlan(FrappeTestCase):
self.assertEqual(row.get("uom"), "Nos") self.assertEqual(row.get("uom"), "Nos")
self.assertEqual(row.get("conversion_factor"), 10.0) self.assertEqual(row.get("conversion_factor"), 10.0)
def test_unreserve_qty_on_closing_of_pp(self):
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
from erpnext.stock.utils import get_or_make_bin
fg_item = make_item(properties={"is_stock_item": 1, "stock_uom": "_Test UOM 1"}).name
rm_item = make_item(properties={"is_stock_item": 1, "stock_uom": "_Test UOM 1"}).name
store_warehouse = create_warehouse("Store Warehouse", company="_Test Company")
rm_warehouse = create_warehouse("RM Warehouse", company="_Test Company")
make_bom(item=fg_item, raw_materials=[rm_item], source_warehouse="_Test Warehouse - _TC")
pln = create_production_plan(
item_code=fg_item, planned_qty=10, stock_uom="_Test UOM 1", do_not_submit=1
)
pln.for_warehouse = rm_warehouse
mr_items = get_items_for_material_requests(pln.as_dict())
for d in mr_items:
pln.append("mr_items", d)
pln.save()
pln.submit()
bin_name = get_or_make_bin(rm_item, rm_warehouse)
before_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan"))
pln.reload()
pln.set_status(close=True)
bin_name = get_or_make_bin(rm_item, rm_warehouse)
after_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan"))
self.assertAlmostEqual(after_qty, before_qty - 10)
pln.reload()
pln.set_status(close=False)
bin_name = get_or_make_bin(rm_item, rm_warehouse)
after_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan"))
self.assertAlmostEqual(after_qty, before_qty)
def create_production_plan(**args): def create_production_plan(**args):
""" """

View File

@@ -354,3 +354,4 @@ execute:frappe.db.set_single_value("Buying Settings", "project_update_frequency"
erpnext.patches.v14_0.clear_reconciliation_values_from_singles erpnext.patches.v14_0.clear_reconciliation_values_from_singles
# below migration patch should always run last # below migration patch should always run last
erpnext.patches.v14_0.migrate_gl_to_payment_ledger erpnext.patches.v14_0.migrate_gl_to_payment_ledger
erpnext.stock.doctype.delivery_note.patches.drop_unused_return_against_index

View File

@@ -482,6 +482,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
cost_center: item.cost_center, cost_center: item.cost_center,
tax_category: me.frm.doc.tax_category, tax_category: me.frm.doc.tax_category,
item_tax_template: item.item_tax_template, item_tax_template: item.item_tax_template,
child_doctype: item.doctype,
child_docname: item.name, child_docname: item.name,
is_old_subcontracting_flow: me.frm.doc.is_old_subcontracting_flow, is_old_subcontracting_flow: me.frm.doc.is_old_subcontracting_flow,
} }

View File

@@ -971,7 +971,7 @@ function set_time_to_resolve_and_response(frm, apply_sla_for_resolution) {
} }
function get_time_left(timestamp, agreement_status) { function get_time_left(timestamp, agreement_status) {
const diff = moment(timestamp).diff(moment()); const diff = moment(timestamp).diff(frappe.datetime.system_datetime(true));
const diff_display = diff >= 44500 ? moment.duration(diff).humanize() : 'Failed'; const diff_display = diff >= 44500 ? moment.duration(diff).humanize() : 'Failed';
let indicator = (diff_display == 'Failed' && agreement_status != 'Fulfilled') ? 'red' : 'green'; let indicator = (diff_display == 'Failed' && agreement_status != 'Fulfilled') ? 'red' : 'green';
return {'diff_display': diff_display, 'indicator': indicator}; return {'diff_display': diff_display, 'indicator': indicator};

View File

@@ -307,18 +307,19 @@ class Customer(TransactionBase):
def create_contact(contact, party_type, party, email): def create_contact(contact, party_type, party, email):
"""Create contact based on given contact name""" """Create contact based on given contact name"""
contact = contact.split(" ") first, middle, last = parse_full_name(contact)
doc = frappe.get_doc(
contact = frappe.get_doc(
{ {
"doctype": "Contact", "doctype": "Contact",
"first_name": contact[0], "first_name": first,
"last_name": len(contact) > 1 and contact[1] or "", "middle_name": middle,
"last_name": last,
"is_primary_contact": 1,
} }
) )
contact.append("email_ids", dict(email_id=email, is_primary=1)) doc.append("email_ids", dict(email_id=email, is_primary=1))
contact.append("links", dict(link_doctype=party_type, link_name=party)) doc.append("links", dict(link_doctype=party_type, link_name=party))
contact.insert() return doc.insert()
@frappe.whitelist() @frappe.whitelist()
@@ -684,24 +685,42 @@ def get_credit_limit(customer, company):
def make_contact(args, is_primary_contact=1): def make_contact(args, is_primary_contact=1):
contact = frappe.get_doc( values = {
{ "doctype": "Contact",
"doctype": "Contact", "is_primary_contact": is_primary_contact,
"first_name": args.get("name"), "links": [{"link_doctype": args.get("doctype"), "link_name": args.get("name")}],
"is_primary_contact": is_primary_contact, }
"links": [{"link_doctype": args.get("doctype"), "link_name": args.get("name")}], if args.customer_type == "Individual":
} first, middle, last = parse_full_name(args.get("customer_name"))
) values.update(
{
"first_name": first,
"middle_name": middle,
"last_name": last,
}
)
else:
values.update(
{
"company_name": args.get("customer_name"),
}
)
contact = frappe.get_doc(values)
if args.get("email_id"): if args.get("email_id"):
contact.add_email(args.get("email_id"), is_primary=True) contact.add_email(args.get("email_id"), is_primary=True)
if args.get("mobile_no"): if args.get("mobile_no"):
contact.add_phone(args.get("mobile_no"), is_primary_mobile_no=True) contact.add_phone(args.get("mobile_no"), is_primary_mobile_no=True)
contact.insert()
if flags := args.get("flags"):
contact.insert(ignore_permissions=flags.get("ignore_permissions"))
else:
contact.insert()
return contact return contact
def make_address(args, is_primary_address=1): def make_address(args, is_primary_address=1, is_shipping_address=1):
reqd_fields = [] reqd_fields = []
for field in ["city", "country"]: for field in ["city", "country"]:
if not args.get(field): if not args.get(field):
@@ -717,16 +736,23 @@ def make_address(args, is_primary_address=1):
address = frappe.get_doc( address = frappe.get_doc(
{ {
"doctype": "Address", "doctype": "Address",
"address_title": args.get("name"), "address_title": args.get("customer_name"),
"address_line1": args.get("address_line1"), "address_line1": args.get("address_line1"),
"address_line2": args.get("address_line2"), "address_line2": args.get("address_line2"),
"city": args.get("city"), "city": args.get("city"),
"state": args.get("state"), "state": args.get("state"),
"pincode": args.get("pincode"), "pincode": args.get("pincode"),
"country": args.get("country"), "country": args.get("country"),
"is_primary_address": is_primary_address,
"is_shipping_address": is_shipping_address,
"links": [{"link_doctype": args.get("doctype"), "link_name": args.get("name")}], "links": [{"link_doctype": args.get("doctype"), "link_name": args.get("name")}],
} }
).insert() )
if flags := args.get("flags"):
address.insert(ignore_permissions=flags.get("ignore_permissions"))
else:
address.insert()
return address return address
@@ -747,3 +773,13 @@ def get_customer_primary_contact(doctype, txt, searchfield, start, page_len, fil
.where((dlink.link_name == customer) & (con.name.like(f"%{txt}%"))) .where((dlink.link_name == customer) & (con.name.like(f"%{txt}%")))
.run() .run()
) )
def parse_full_name(full_name: str) -> tuple[str, str | None, str | None]:
"""Parse full name into first name, middle name and last name"""
names = full_name.split()
first_name = names[0]
middle_name = " ".join(names[1:-1]) if len(names) > 2 else None
last_name = names[-1] if len(names) > 1 else None
return first_name, middle_name, last_name

View File

@@ -10,7 +10,11 @@ from frappe.utils import flt
from erpnext.accounts.party import get_due_date from erpnext.accounts.party import get_due_date
from erpnext.exceptions import PartyDisabled, PartyFrozen from erpnext.exceptions import PartyDisabled, PartyFrozen
from erpnext.selling.doctype.customer.customer import get_credit_limit, get_customer_outstanding from erpnext.selling.doctype.customer.customer import (
get_credit_limit,
get_customer_outstanding,
parse_full_name,
)
from erpnext.tests.utils import create_test_contact_and_address from erpnext.tests.utils import create_test_contact_and_address
test_ignore = ["Price List"] test_ignore = ["Price List"]
@@ -373,6 +377,22 @@ class TestCustomer(FrappeTestCase):
frappe.db.set_value("Selling Settings", None, "cust_master_name", "Customer Name") frappe.db.set_value("Selling Settings", None, "cust_master_name", "Customer Name")
def test_parse_full_name(self):
first, middle, last = parse_full_name("John")
self.assertEqual(first, "John")
self.assertEqual(middle, None)
self.assertEqual(last, None)
first, middle, last = parse_full_name("John Doe")
self.assertEqual(first, "John")
self.assertEqual(middle, None)
self.assertEqual(last, "Doe")
first, middle, last = parse_full_name("John Michael Doe")
self.assertEqual(first, "John")
self.assertEqual(middle, "Michael")
self.assertEqual(last, "Doe")
def get_customer_dict(customer_name): def get_customer_dict(customer_name):
return { return {

View File

@@ -1,5 +1,5 @@
import frappe import frappe
from frappe.utils import cint from frappe.utils.deprecations import deprecated
def get_leaderboards(): def get_leaderboards():
@@ -54,12 +54,13 @@ def get_leaderboards():
@frappe.whitelist() @frappe.whitelist()
def get_all_customers(date_range, company, field, limit=None): def get_all_customers(date_range, company, field, limit=None):
filters = [["docstatus", "=", "1"], ["company", "=", company]]
from_date, to_date = parse_date_range(date_range)
if field == "outstanding_amount": if field == "outstanding_amount":
filters = [["docstatus", "=", "1"], ["company", "=", company]] if from_date and to_date:
if date_range: filters.append(["posting_date", "between", [from_date, to_date]])
date_range = frappe.parse_json(date_range)
filters.append(["posting_date", ">=", "between", [date_range[0], date_range[1]]]) return frappe.get_list(
return frappe.db.get_all(
"Sales Invoice", "Sales Invoice",
fields=["customer as name", "sum(outstanding_amount) as value"], fields=["customer as name", "sum(outstanding_amount) as value"],
filters=filters, filters=filters,
@@ -69,26 +70,20 @@ def get_all_customers(date_range, company, field, limit=None):
) )
else: else:
if field == "total_sales_amount": if field == "total_sales_amount":
select_field = "sum(so_item.base_net_amount)" select_field = "base_net_total"
elif field == "total_qty_sold": elif field == "total_qty_sold":
select_field = "sum(so_item.stock_qty)" select_field = "total_qty"
date_condition = get_date_condition(date_range, "so.transaction_date") if from_date and to_date:
filters.append(["transaction_date", "between", [from_date, to_date]])
return frappe.db.sql( return frappe.get_list(
""" "Sales Order",
select so.customer as name, {0} as value fields=["customer as name", f"sum({select_field}) as value"],
FROM `tabSales Order` as so JOIN `tabSales Order Item` as so_item filters=filters,
ON so.name = so_item.parent group_by="customer",
where so.docstatus = 1 {1} and so.company = %s order_by="value desc",
group by so.customer limit=limit,
order by value DESC
limit %s
""".format(
select_field, date_condition
),
(company, cint(limit)),
as_dict=1,
) )
@@ -96,55 +91,58 @@ def get_all_customers(date_range, company, field, limit=None):
def get_all_items(date_range, company, field, limit=None): def get_all_items(date_range, company, field, limit=None):
if field in ("available_stock_qty", "available_stock_value"): if field in ("available_stock_qty", "available_stock_value"):
select_field = "sum(actual_qty)" if field == "available_stock_qty" else "sum(stock_value)" select_field = "sum(actual_qty)" if field == "available_stock_qty" else "sum(stock_value)"
return frappe.db.get_all( results = frappe.db.get_all(
"Bin", "Bin",
fields=["item_code as name", "{0} as value".format(select_field)], fields=["item_code as name", "{0} as value".format(select_field)],
group_by="item_code", group_by="item_code",
order_by="value desc", order_by="value desc",
limit=limit, limit=limit,
) )
readable_active_items = set(frappe.get_list("Item", filters={"disabled": 0}, pluck="name"))
return [item for item in results if item["name"] in readable_active_items]
else: else:
if field == "total_sales_amount": if field == "total_sales_amount":
select_field = "sum(order_item.base_net_amount)" select_field = "base_net_amount"
select_doctype = "Sales Order" select_doctype = "Sales Order"
elif field == "total_purchase_amount": elif field == "total_purchase_amount":
select_field = "sum(order_item.base_net_amount)" select_field = "base_net_amount"
select_doctype = "Purchase Order" select_doctype = "Purchase Order"
elif field == "total_qty_sold": elif field == "total_qty_sold":
select_field = "sum(order_item.stock_qty)" select_field = "stock_qty"
select_doctype = "Sales Order" select_doctype = "Sales Order"
elif field == "total_qty_purchased": elif field == "total_qty_purchased":
select_field = "sum(order_item.stock_qty)" select_field = "stock_qty"
select_doctype = "Purchase Order" select_doctype = "Purchase Order"
date_condition = get_date_condition(date_range, "sales_order.transaction_date") filters = [["docstatus", "=", "1"], ["company", "=", company]]
from_date, to_date = parse_date_range(date_range)
if from_date and to_date:
filters.append(["transaction_date", "between", [from_date, to_date]])
return frappe.db.sql( child_doctype = f"{select_doctype} Item"
""" return frappe.get_list(
select order_item.item_code as name, {0} as value select_doctype,
from `tab{1}` sales_order join `tab{1} Item` as order_item fields=[
on sales_order.name = order_item.parent f"`tab{child_doctype}`.item_code as name",
where sales_order.docstatus = 1 f"sum(`tab{child_doctype}`.{select_field}) as value",
and sales_order.company = %s {2} ],
group by order_item.item_code filters=filters,
order by value desc order_by="value desc",
limit %s group_by=f"`tab{child_doctype}`.item_code",
""".format( limit=limit,
select_field, select_doctype, date_condition )
),
(company, cint(limit)),
as_dict=1,
) # nosec
@frappe.whitelist() @frappe.whitelist()
def get_all_suppliers(date_range, company, field, limit=None): def get_all_suppliers(date_range, company, field, limit=None):
filters = [["docstatus", "=", "1"], ["company", "=", company]]
from_date, to_date = parse_date_range(date_range)
if field == "outstanding_amount": if field == "outstanding_amount":
filters = [["docstatus", "=", "1"], ["company", "=", company]] if from_date and to_date:
if date_range: filters.append(["posting_date", "between", [from_date, to_date]])
date_range = frappe.parse_json(date_range)
filters.append(["posting_date", "between", [date_range[0], date_range[1]]]) return frappe.get_list(
return frappe.db.get_all(
"Purchase Invoice", "Purchase Invoice",
fields=["supplier as name", "sum(outstanding_amount) as value"], fields=["supplier as name", "sum(outstanding_amount) as value"],
filters=filters, filters=filters,
@@ -154,48 +152,40 @@ def get_all_suppliers(date_range, company, field, limit=None):
) )
else: else:
if field == "total_purchase_amount": if field == "total_purchase_amount":
select_field = "sum(purchase_order_item.base_net_amount)" select_field = "base_net_total"
elif field == "total_qty_purchased": elif field == "total_qty_purchased":
select_field = "sum(purchase_order_item.stock_qty)" select_field = "total_qty"
date_condition = get_date_condition(date_range, "purchase_order.modified") if from_date and to_date:
filters.append(["transaction_date", "between", [from_date, to_date]])
return frappe.db.sql( return frappe.get_list(
""" "Purchase Order",
select purchase_order.supplier as name, {0} as value fields=["supplier as name", f"sum({select_field}) as value"],
FROM `tabPurchase Order` as purchase_order LEFT JOIN `tabPurchase Order Item` filters=filters,
as purchase_order_item ON purchase_order.name = purchase_order_item.parent group_by="supplier",
where order_by="value desc",
purchase_order.docstatus = 1 limit=limit,
{1} )
and purchase_order.company = %s
group by purchase_order.supplier
order by value DESC
limit %s""".format(
select_field, date_condition
),
(company, cint(limit)),
as_dict=1,
) # nosec
@frappe.whitelist() @frappe.whitelist()
def get_all_sales_partner(date_range, company, field, limit=None): def get_all_sales_partner(date_range, company, field, limit=None):
if field == "total_sales_amount": if field == "total_sales_amount":
select_field = "sum(`base_net_total`)" select_field = "base_net_total"
elif field == "total_commission": elif field == "total_commission":
select_field = "sum(`total_commission`)" select_field = "total_commission"
filters = {"sales_partner": ["!=", ""], "docstatus": 1, "company": company} filters = [["docstatus", "=", "1"], ["company", "=", company], ["sales_partner", "is", "set"]]
if date_range: from_date, to_date = parse_date_range(date_range)
date_range = frappe.parse_json(date_range) if from_date and to_date:
filters["transaction_date"] = ["between", [date_range[0], date_range[1]]] filters.append(["transaction_date", "between", [from_date, to_date]])
return frappe.get_list( return frappe.get_list(
"Sales Order", "Sales Order",
fields=[ fields=[
"`sales_partner` as name", "sales_partner as name",
"{} as value".format(select_field), f"sum({select_field}) as value",
], ],
filters=filters, filters=filters,
group_by="sales_partner", group_by="sales_partner",
@@ -206,27 +196,29 @@ def get_all_sales_partner(date_range, company, field, limit=None):
@frappe.whitelist() @frappe.whitelist()
def get_all_sales_person(date_range, company, field=None, limit=0): def get_all_sales_person(date_range, company, field=None, limit=0):
date_condition = get_date_condition(date_range, "sales_order.transaction_date") filters = [
["docstatus", "=", "1"],
["company", "=", company],
["Sales Team", "sales_person", "is", "set"],
]
from_date, to_date = parse_date_range(date_range)
if from_date and to_date:
filters.append(["transaction_date", "between", [from_date, to_date]])
return frappe.db.sql( return frappe.get_list(
""" "Sales Order",
select sales_team.sales_person as name, sum(sales_order.base_net_total) as value fields=[
from `tabSales Order` as sales_order join `tabSales Team` as sales_team "`tabSales Team`.sales_person as name",
on sales_order.name = sales_team.parent and sales_team.parenttype = 'Sales Order' "sum(`tabSales Team`.allocated_amount) as value",
where sales_order.docstatus = 1 ],
and sales_order.company = %s filters=filters,
{date_condition} group_by="`tabSales Team`.sales_person",
group by sales_team.sales_person order_by="value desc",
order by value DESC limit=limit,
limit %s
""".format(
date_condition=date_condition
),
(company, cint(limit)),
as_dict=1,
) )
@deprecated
def get_date_condition(date_range, field): def get_date_condition(date_range, field):
date_condition = "" date_condition = ""
if date_range: if date_range:
@@ -236,3 +228,11 @@ def get_date_condition(date_range, field):
field, frappe.db.escape(from_date), frappe.db.escape(to_date) field, frappe.db.escape(from_date), frappe.db.escape(to_date)
) )
return date_condition return date_condition
def parse_date_range(date_range):
if date_range:
date_range = frappe.parse_json(date_range)
return date_range[0], date_range[1]
return None, None

View File

@@ -301,7 +301,8 @@
"no_copy": 1, "no_copy": 1,
"options": "Delivery Note", "options": "Delivery Note",
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1,
"search_index": 1
}, },
{ {
"collapsible": 1, "collapsible": 1,
@@ -1400,7 +1401,7 @@
"idx": 146, "idx": 146,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2023-09-04 14:15:28.363184", "modified": "2023-12-18 17:19:39.368239",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Delivery Note", "name": "Delivery Note",

View File

@@ -997,7 +997,3 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
) )
return doclist return doclist
def on_doctype_update():
frappe.db.add_index("Delivery Note", ["customer", "is_return", "return_against"])

View File

@@ -0,0 +1,15 @@
import frappe
def execute():
"""Drop unused return_against index"""
try:
frappe.db.sql_ddl(
"ALTER TABLE `tabDelivery Note` DROP INDEX `customer_is_return_return_against_index`"
)
frappe.db.sql_ddl(
"ALTER TABLE `tabPurchase Receipt` DROP INDEX `supplier_is_return_return_against_index`"
)
except Exception:
frappe.log_error("Failed to drop unused index")

View File

@@ -1332,6 +1332,56 @@ class TestDeliveryNote(FrappeTestCase):
dn.reload() dn.reload()
self.assertFalse(dn.items[0].target_warehouse) self.assertFalse(dn.items[0].target_warehouse)
def test_sales_return_valuation_for_moving_average(self):
item_code = make_item(
"_Test Item Sales Return with MA", {"is_stock_item": 1, "valuation_method": "Moving Average"}
).name
make_stock_entry(
item_code=item_code,
target="_Test Warehouse - _TC",
qty=5,
basic_rate=100.0,
posting_date=add_days(nowdate(), -5),
)
dn = create_delivery_note(
item_code=item_code, qty=5, rate=500, posting_date=add_days(nowdate(), -4)
)
self.assertEqual(dn.items[0].incoming_rate, 100.0)
make_stock_entry(
item_code=item_code,
target="_Test Warehouse - _TC",
qty=5,
basic_rate=200.0,
posting_date=add_days(nowdate(), -3),
)
make_stock_entry(
item_code=item_code,
target="_Test Warehouse - _TC",
qty=5,
basic_rate=300.0,
posting_date=add_days(nowdate(), -2),
)
dn1 = create_delivery_note(
is_return=1,
item_code=item_code,
return_against=dn.name,
qty=-5,
rate=500,
company=dn.company,
expense_account="Cost of Goods Sold - _TC",
cost_center="Main - _TC",
do_not_submit=1,
posting_date=add_days(nowdate(), -1),
)
# (300 * 5) + (200 * 5) = 2500
# 2500 / 10 = 250
self.assertAlmostEqual(dn1.items[0].incoming_rate, 250.0)
def create_delivery_note(**args): def create_delivery_note(**args):
dn = frappe.new_doc("Delivery Note") dn = frappe.new_doc("Delivery Note")

View File

@@ -522,39 +522,25 @@ class TestItem(FrappeTestCase):
self.assertEqual(factor, 1.0) self.assertEqual(factor, 1.0)
def test_item_variant_by_manufacturer(self): def test_item_variant_by_manufacturer(self):
fields = [{"field_name": "description"}, {"field_name": "variant_based_on"}] template = make_item(
set_item_variant_settings(fields) "_Test Item Variant By Manufacturer", {"has_variants": 1, "variant_based_on": "Manufacturer"}
).name
if frappe.db.exists("Item", "_Test Variant Mfg"): for manufacturer in ["DFSS", "DASA", "ASAAS"]:
frappe.delete_doc("Item", "_Test Variant Mfg") if not frappe.db.exists("Manufacturer", manufacturer):
if frappe.db.exists("Item", "_Test Variant Mfg-1"): m_doc = frappe.new_doc("Manufacturer")
frappe.delete_doc("Item", "_Test Variant Mfg-1") m_doc.short_name = manufacturer
if frappe.db.exists("Manufacturer", "MSG1"): m_doc.insert()
frappe.delete_doc("Manufacturer", "MSG1")
template = frappe.get_doc( self.assertFalse(frappe.db.exists("Item Manufacturer", {"manufacturer": "DFSS"}))
dict( variant = get_variant(template, manufacturer="DFSS", manufacturer_part_no="DFSS-123")
doctype="Item",
item_code="_Test Variant Mfg",
has_variant=1,
item_group="Products",
variant_based_on="Manufacturer",
)
).insert()
manufacturer = frappe.get_doc(dict(doctype="Manufacturer", short_name="MSG1")).insert() item_manufacturer = frappe.db.exists(
"Item Manufacturer", {"manufacturer": "DFSS", "item_code": variant.name}
)
self.assertTrue(item_manufacturer)
variant = get_variant(template.name, manufacturer=manufacturer.name) frappe.delete_doc("Item Manufacturer", item_manufacturer)
self.assertEqual(variant.item_code, "_Test Variant Mfg-1")
self.assertEqual(variant.description, "_Test Variant Mfg")
self.assertEqual(variant.manufacturer, "MSG1")
variant.insert()
variant = get_variant(template.name, manufacturer=manufacturer.name, manufacturer_part_no="007")
self.assertEqual(variant.item_code, "_Test Variant Mfg-2")
self.assertEqual(variant.description, "_Test Variant Mfg")
self.assertEqual(variant.manufacturer, "MSG1")
self.assertEqual(variant.manufacturer_part_no, "007")
def test_stock_exists_against_template_item(self): def test_stock_exists_against_template_item(self):
stock_item = frappe.get_all("Stock Ledger Entry", fields=["item_code"], limit=1) stock_item = frappe.get_all("Stock Ledger Entry", fields=["item_code"], limit=1)

View File

@@ -199,9 +199,8 @@ frappe.ui.form.on('Material Request', {
get_item_data: function(frm, item, overwrite_warehouse=false) { get_item_data: function(frm, item, overwrite_warehouse=false) {
if (item && !item.item_code) { return; } if (item && !item.item_code) { return; }
frm.call({ frappe.call({
method: "erpnext.stock.get_item_details.get_item_details", method: "erpnext.stock.get_item_details.get_item_details",
child: item,
args: { args: {
args: { args: {
item_code: item.item_code, item_code: item.item_code,

View File

@@ -123,7 +123,9 @@ class MaterialRequest(BuyingController):
def on_submit(self): def on_submit(self):
self.update_requested_qty_in_production_plan() self.update_requested_qty_in_production_plan()
self.update_requested_qty() self.update_requested_qty()
if self.material_request_type == "Purchase": if self.material_request_type == "Purchase" and frappe.db.exists(
"Budget", {"applicable_on_material_request": 1, "docstatus": 1}
):
self.validate_budget() self.validate_budget()
def before_save(self): def before_save(self):

View File

@@ -13,7 +13,7 @@ from frappe.model.mapper import map_child_doc
from frappe.query_builder import Case from frappe.query_builder import Case
from frappe.query_builder.custom import GROUP_CONCAT from frappe.query_builder.custom import GROUP_CONCAT
from frappe.query_builder.functions import Coalesce, IfNull, Locate, Replace, Sum from frappe.query_builder.functions import Coalesce, IfNull, Locate, Replace, Sum
from frappe.utils import cint, floor, flt, today from frappe.utils import ceil, cint, floor, flt, today
from frappe.utils.nestedset import get_descendants_of from frappe.utils.nestedset import get_descendants_of
from erpnext.selling.doctype.sales_order.sales_order import ( from erpnext.selling.doctype.sales_order.sales_order import (
@@ -605,7 +605,7 @@ def get_available_item_locations_for_serialized_item(
.select(sn.name, sn.warehouse) .select(sn.name, sn.warehouse)
.where((sn.item_code == item_code) & (sn.company == company)) .where((sn.item_code == item_code) & (sn.company == company))
.orderby(sn.purchase_date) .orderby(sn.purchase_date)
.limit(cint(required_qty + total_picked_qty)) .limit(ceil(required_qty + total_picked_qty))
) )
if from_warehouses: if from_warehouses:
@@ -647,7 +647,7 @@ def get_available_item_locations_for_batched_item(
.groupby(sle.warehouse, sle.batch_no, sle.item_code) .groupby(sle.warehouse, sle.batch_no, sle.item_code)
.having(Sum(sle.actual_qty) > 0) .having(Sum(sle.actual_qty) > 0)
.orderby(IfNull(batch.expiry_date, "2200-01-01"), batch.creation, sle.batch_no, sle.warehouse) .orderby(IfNull(batch.expiry_date, "2200-01-01"), batch.creation, sle.batch_no, sle.warehouse)
.limit(cint(required_qty + total_picked_qty)) .limit(ceil(required_qty + total_picked_qty))
) )
if from_warehouses: if from_warehouses:
@@ -680,7 +680,7 @@ def get_available_item_locations_for_serial_and_batched_item(
(conditions) & (sn.batch_no == location.batch_no) & (sn.warehouse == location.warehouse) (conditions) & (sn.batch_no == location.batch_no) & (sn.warehouse == location.warehouse)
) )
.orderby(sn.purchase_date) .orderby(sn.purchase_date)
.limit(cint(location.qty + total_picked_qty)) .limit(ceil(location.qty + total_picked_qty))
).run(as_dict=True) ).run(as_dict=True)
serial_nos = [sn.name for sn in serial_nos] serial_nos = [sn.name for sn in serial_nos]
@@ -699,7 +699,7 @@ def get_available_item_locations_for_other_item(
.select(bin.warehouse, bin.actual_qty.as_("qty")) .select(bin.warehouse, bin.actual_qty.as_("qty"))
.where((bin.item_code == item_code) & (bin.actual_qty > 0)) .where((bin.item_code == item_code) & (bin.actual_qty > 0))
.orderby(bin.creation) .orderby(bin.creation)
.limit(cint(required_qty + total_picked_qty)) .limit(ceil(required_qty + total_picked_qty))
) )
if from_warehouses: if from_warehouses:

View File

@@ -288,7 +288,8 @@
"no_copy": 1, "no_copy": 1,
"options": "Purchase Receipt", "options": "Purchase Receipt",
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1,
"search_index": 1
}, },
{ {
"fieldname": "section_addresses", "fieldname": "section_addresses",
@@ -1241,7 +1242,7 @@
"idx": 261, "idx": 261,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2023-10-01 21:00:44.556816", "modified": "2023-12-18 17:26:41.279663",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Purchase Receipt", "name": "Purchase Receipt",

View File

@@ -1199,10 +1199,6 @@ def get_item_account_wise_additional_cost(purchase_document):
return item_account_wise_cost return item_account_wise_cost
def on_doctype_update():
frappe.db.add_index("Purchase Receipt", ["supplier", "is_return", "return_against"])
@erpnext.allow_regional @erpnext.allow_regional
def update_regional_gl_entries(gl_list, doc): def update_regional_gl_entries(gl_list, doc):
return return

View File

@@ -8,6 +8,7 @@ import frappe
from frappe import _, throw from frappe import _, throw
from frappe.model import child_table_fields, default_fields from frappe.model import child_table_fields, default_fields
from frappe.model.meta import get_field_precision from frappe.model.meta import get_field_precision
from frappe.model.utils import get_fetch_values
from frappe.query_builder.functions import CombineDatetime, IfNull, Sum from frappe.query_builder.functions import CombineDatetime, IfNull, Sum
from frappe.utils import add_days, add_months, cint, cstr, flt, getdate from frappe.utils import add_days, add_months, cint, cstr, flt, getdate
@@ -608,6 +609,9 @@ def get_item_tax_template(args, item, out):
item_tax_template = _get_item_tax_template(args, item_group_doc.taxes, out) item_tax_template = _get_item_tax_template(args, item_group_doc.taxes, out)
item_group = item_group_doc.parent_item_group item_group = item_group_doc.parent_item_group
if args.get("child_doctype") and item_tax_template:
out.update(get_fetch_values(args.get("child_doctype"), "item_tax_template", item_tax_template))
def _get_item_tax_template(args, taxes, out=None, for_validate=False): def _get_item_tax_template(args, taxes, out=None, for_validate=False):
if out is None: if out is None:

View File

@@ -397,7 +397,7 @@ class StockBalanceReport(object):
"fieldname": "bal_val", "fieldname": "bal_val",
"fieldtype": "Currency", "fieldtype": "Currency",
"width": 100, "width": 100,
"options": "currency", "options": "Company:company:default_currency",
}, },
{ {
"label": _("Opening Qty"), "label": _("Opening Qty"),
@@ -411,7 +411,7 @@ class StockBalanceReport(object):
"fieldname": "opening_val", "fieldname": "opening_val",
"fieldtype": "Currency", "fieldtype": "Currency",
"width": 110, "width": 110,
"options": "currency", "options": "Company:company:default_currency",
}, },
{ {
"label": _("In Qty"), "label": _("In Qty"),

View File

@@ -7,6 +7,7 @@ from frappe.model.mapper import get_mapped_doc
from frappe.utils import flt from frappe.utils import flt
from erpnext.buying.doctype.purchase_order.purchase_order import is_subcontracting_order_created from erpnext.buying.doctype.purchase_order.purchase_order import is_subcontracting_order_created
from erpnext.buying.doctype.purchase_order.purchase_order import update_status as update_po_status
from erpnext.controllers.subcontracting_controller import SubcontractingController from erpnext.controllers.subcontracting_controller import SubcontractingController
from erpnext.stock.stock_balance import update_bin_qty from erpnext.stock.stock_balance import update_bin_qty
from erpnext.stock.utils import get_bin from erpnext.stock.utils import get_bin
@@ -234,6 +235,9 @@ class SubcontractingOrder(SubcontractingController):
"Subcontracting Order", self.name, "status", status, update_modified=update_modified "Subcontracting Order", self.name, "status", status, update_modified=update_modified
) )
if status == "Closed":
update_po_status("Closed", self.purchase_order)
@frappe.whitelist() @frappe.whitelist()
def make_subcontracting_receipt(source_name, target_doc=None): def make_subcontracting_receipt(source_name, target_doc=None):

View File

@@ -111,13 +111,13 @@ class SubcontractingReceipt(SubcontractingController):
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation") self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation")
self.update_status_updater_args() self.update_status_updater_args()
self.update_prevdoc_status() self.update_prevdoc_status()
self.delete_auto_created_batches()
self.set_consumed_qty_in_subcontract_order() self.set_consumed_qty_in_subcontract_order()
self.set_subcontracting_order_status() self.set_subcontracting_order_status()
self.update_stock_ledger() self.update_stock_ledger()
self.make_gl_entries_on_cancel() self.make_gl_entries_on_cancel()
self.repost_future_sle_and_gle() self.repost_future_sle_and_gle()
self.update_status() self.update_status()
self.delete_auto_created_batches()
@frappe.whitelist() @frappe.whitelist()
def set_missing_values(self): def set_missing_values(self):

View File

@@ -655,6 +655,79 @@ class TestSubcontractingReceipt(FrappeTestCase):
self.assertEqual(rm_item.rate, 100) self.assertEqual(rm_item.rate, 100)
self.assertEqual(rm_item.amount, rm_item.consumed_qty * rm_item.rate) self.assertEqual(rm_item.amount, rm_item.consumed_qty * rm_item.rate)
def test_subcontracting_receipt_cancel_with_batch(self):
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
# Step - 1: Set Backflush Based On as "BOM"
set_backflush_based_on("BOM")
# Step - 2: Create FG and RM Items
fg_item = make_item(
properties={"is_stock_item": 1, "is_sub_contracted_item": 1, "has_batch_no": 1}
).name
rm_item1 = make_item(properties={"is_stock_item": 1}).name
rm_item2 = make_item(properties={"is_stock_item": 1}).name
make_item("Subcontracted Service Item Test For Batch 1", {"is_stock_item": 0})
# Step - 3: Create BOM for FG Item
bom = make_bom(item=fg_item, raw_materials=[rm_item1, rm_item2])
for rm_item in bom.items:
self.assertEqual(rm_item.rate, 0)
self.assertEqual(rm_item.amount, 0)
bom = bom.name
# Step - 4: Create PO and SCO
service_items = [
{
"warehouse": "_Test Warehouse - _TC",
"item_code": "Subcontracted Service Item Test For Batch 1",
"qty": 100,
"rate": 100,
"fg_item": fg_item,
"fg_item_qty": 100,
},
]
sco = get_subcontracting_order(service_items=service_items)
for rm_item in sco.supplied_items:
self.assertEqual(rm_item.rate, 0)
self.assertEqual(rm_item.amount, 0)
# Step - 5: Inward Raw Materials
rm_items = get_rm_items(sco.supplied_items)
for rm_item in rm_items:
rm_item["rate"] = 100
itemwise_details = make_stock_in_entry(rm_items=rm_items)
# Step - 6: Transfer RM's to Subcontractor
se = make_stock_transfer_entry(
sco_no=sco.name,
rm_items=rm_items,
itemwise_details=copy.deepcopy(itemwise_details),
)
for item in se.items:
self.assertEqual(item.qty, 100)
self.assertEqual(item.basic_rate, 100)
self.assertEqual(item.amount, item.qty * item.basic_rate)
batch_doc = frappe.get_doc(
{
"doctype": "Batch",
"item": fg_item,
"batch_id": frappe.generate_hash(length=10),
}
).insert(ignore_permissions=True)
# Step - 7: Create Subcontracting Receipt
scr = make_subcontracting_receipt(sco.name)
scr.items[0].batch_no = batch_doc.batch_id
scr.save()
scr.submit()
scr.load_from_db()
# Step - 8: Cancel Subcontracting Receipt
scr.cancel()
self.assertTrue(scr.docstatus == 2)
def make_return_subcontracting_receipt(**args): def make_return_subcontracting_receipt(**args):
args = frappe._dict(args) args = frappe._dict(args)

View File

@@ -58,7 +58,9 @@ frappe.ui.form.on("Issue", {
frappe.call("erpnext.support.doctype.service_level_agreement.service_level_agreement.reset_service_level_agreement", { frappe.call("erpnext.support.doctype.service_level_agreement.service_level_agreement.reset_service_level_agreement", {
reason: values.reason, reason: values.reason,
user: frappe.session.user_email user: frappe.session.user_email,
doctype: frm.doc.doctype,
docname: frm.doc.name,
}, () => { }, () => {
reset_sla.enable_primary_action(); reset_sla.enable_primary_action();
frm.refresh(); frm.refresh();

View File

@@ -739,10 +739,12 @@ def get_response_and_resolution_duration(doc):
return priority return priority
def reset_service_level_agreement(doc, reason, user): @frappe.whitelist()
def reset_service_level_agreement(doctype: str, docname: str, reason, user):
if not frappe.db.get_single_value("Support Settings", "allow_resetting_service_level_agreement"): if not frappe.db.get_single_value("Support Settings", "allow_resetting_service_level_agreement"):
frappe.throw(_("Allow Resetting Service Level Agreement from Support Settings.")) frappe.throw(_("Allow Resetting Service Level Agreement from Support Settings."))
doc = frappe.get_doc(doctype, docname)
frappe.get_doc( frappe.get_doc(
{ {
"doctype": "Comment", "doctype": "Comment",