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

chore: release v14
This commit is contained in:
Deepesh Garg
2022-11-15 18:17:10 +05:30
committed by GitHub
54 changed files with 1198 additions and 382 deletions

View File

@@ -3,10 +3,6 @@
frappe.ui.form.on('Accounting Dimension Filter', { frappe.ui.form.on('Accounting Dimension Filter', {
refresh: function(frm, cdt, cdn) { refresh: function(frm, cdt, cdn) {
if (frm.doc.accounting_dimension) {
frm.set_df_property('dimensions', 'label', frm.doc.accounting_dimension, cdn, 'dimension_value');
}
let help_content = let help_content =
`<table class="table table-bordered" style="background-color: var(--scrollbar-track-color);"> `<table class="table table-bordered" style="background-color: var(--scrollbar-track-color);">
<tr><td> <tr><td>
@@ -68,6 +64,7 @@ frappe.ui.form.on('Accounting Dimension Filter', {
frm.clear_table("dimensions"); frm.clear_table("dimensions");
let row = frm.add_child("dimensions"); let row = frm.add_child("dimensions");
row.accounting_dimension = frm.doc.accounting_dimension; row.accounting_dimension = frm.doc.accounting_dimension;
frm.fields_dict["dimensions"].grid.update_docfield_property("dimension_value", "label", frm.doc.accounting_dimension);
frm.refresh_field("dimensions"); frm.refresh_field("dimensions");
frm.trigger('setup_filters'); frm.trigger('setup_filters');
}, },

View File

@@ -43,20 +43,13 @@ frappe.ui.form.on('Bank Guarantee', {
reference_docname: function(frm) { reference_docname: function(frm) {
if (frm.doc.reference_docname && frm.doc.reference_doctype) { if (frm.doc.reference_docname && frm.doc.reference_doctype) {
let fields_to_fetch = ["grand_total"];
let party_field = frm.doc.reference_doctype == "Sales Order" ? "customer" : "supplier"; let party_field = frm.doc.reference_doctype == "Sales Order" ? "customer" : "supplier";
if (frm.doc.reference_doctype == "Sales Order") {
fields_to_fetch.push("project");
}
fields_to_fetch.push(party_field);
frappe.call({ frappe.call({
method: "erpnext.accounts.doctype.bank_guarantee.bank_guarantee.get_vouchar_detials", method: "erpnext.accounts.doctype.bank_guarantee.bank_guarantee.get_voucher_details",
args: { args: {
"column_list": fields_to_fetch, "bank_guarantee_type": frm.doc.bg_type,
"doctype": frm.doc.reference_doctype, "reference_name": frm.doc.reference_docname
"docname": frm.doc.reference_docname
}, },
callback: function(r) { callback: function(r) {
if (r.message) { if (r.message) {

View File

@@ -2,11 +2,8 @@
# For license information, please see license.txt # For license information, please see license.txt
import json
import frappe import frappe
from frappe import _ from frappe import _
from frappe.desk.search import sanitize_searchfield
from frappe.model.document import Document from frappe.model.document import Document
@@ -25,14 +22,18 @@ class BankGuarantee(Document):
@frappe.whitelist() @frappe.whitelist()
def get_vouchar_detials(column_list, doctype, docname): def get_voucher_details(bank_guarantee_type: str, reference_name: str):
column_list = json.loads(column_list) if not isinstance(reference_name, str):
for col in column_list: raise TypeError("reference_name must be a string")
sanitize_searchfield(col)
return frappe.db.sql( fields_to_fetch = ["grand_total"]
""" select {columns} from `tab{doctype}` where name=%s""".format(
columns=", ".join(column_list), doctype=doctype if bank_guarantee_type == "Receiving":
), doctype = "Sales Order"
docname, fields_to_fetch.append("customer")
as_dict=1, fields_to_fetch.append("project")
)[0] else:
doctype = "Purchase Order"
fields_to_fetch.append("supplier")
return frappe.db.get_value(doctype, reference_name, fields_to_fetch, as_dict=True)

View File

@@ -52,7 +52,7 @@ def validate_company(company):
if parent_company and (not allow_account_creation_against_child_company): if parent_company and (not allow_account_creation_against_child_company):
msg = _("{} is a child company.").format(frappe.bold(company)) + " " msg = _("{} is a child company.").format(frappe.bold(company)) + " "
msg += _("Please import accounts against parent company or enable {} in company master.").format( msg += _("Please import accounts against parent company or enable {} in company master.").format(
frappe.bold("Allow Account Creation Against Child Company") frappe.bold(_("Allow Account Creation Against Child Company"))
) )
frappe.throw(msg, title=_("Wrong Company")) frappe.throw(msg, title=_("Wrong Company"))

View File

@@ -62,7 +62,6 @@ class PaymentEntry(AccountsController):
self.set_missing_values() self.set_missing_values()
self.validate_payment_type() self.validate_payment_type()
self.validate_party_details() self.validate_party_details()
self.validate_bank_accounts()
self.set_exchange_rate() self.set_exchange_rate()
self.validate_mandatory() self.validate_mandatory()
self.validate_reference_documents() self.validate_reference_documents()
@@ -243,23 +242,6 @@ class PaymentEntry(AccountsController):
if not frappe.db.exists(self.party_type, self.party): if not frappe.db.exists(self.party_type, self.party):
frappe.throw(_("Invalid {0}: {1}").format(self.party_type, self.party)) frappe.throw(_("Invalid {0}: {1}").format(self.party_type, self.party))
if self.party_account and self.party_type in ("Customer", "Supplier"):
self.validate_account_type(
self.party_account, [erpnext.get_party_account_type(self.party_type)]
)
def validate_bank_accounts(self):
if self.payment_type in ("Pay", "Internal Transfer"):
self.validate_account_type(self.paid_from, ["Bank", "Cash"])
if self.payment_type in ("Receive", "Internal Transfer"):
self.validate_account_type(self.paid_to, ["Bank", "Cash"])
def validate_account_type(self, account, account_types):
account_type = frappe.db.get_value("Account", account, "account_type")
# if account_type not in account_types:
# frappe.throw(_("Account Type for {0} must be {1}").format(account, comma_or(account_types)))
def set_exchange_rate(self, ref_doc=None): def set_exchange_rate(self, ref_doc=None):
self.set_source_exchange_rate(ref_doc) self.set_source_exchange_rate(ref_doc)
self.set_target_exchange_rate(ref_doc) self.set_target_exchange_rate(ref_doc)

View File

@@ -1410,7 +1410,7 @@ class PurchaseInvoice(BuyingController):
self.repost_future_sle_and_gle() self.repost_future_sle_and_gle()
self.update_project() self.update_project()
frappe.db.set(self, "status", "Cancelled") self.db_set("status", "Cancelled")
unlink_inter_company_doc(self.doctype, self.name, self.inter_company_invoice_reference) unlink_inter_company_doc(self.doctype, self.name, self.inter_company_invoice_reference)
self.ignore_linked_doctypes = ( self.ignore_linked_doctypes = (
@@ -1463,6 +1463,7 @@ class PurchaseInvoice(BuyingController):
def update_billing_status_in_pr(self, update_modified=True): def update_billing_status_in_pr(self, update_modified=True):
updated_pr = [] updated_pr = []
po_details = []
for d in self.get("items"): for d in self.get("items"):
if d.pr_detail: if d.pr_detail:
billed_amt = frappe.db.sql( billed_amt = frappe.db.sql(
@@ -1480,7 +1481,10 @@ class PurchaseInvoice(BuyingController):
) )
updated_pr.append(d.purchase_receipt) updated_pr.append(d.purchase_receipt)
elif d.po_detail: elif d.po_detail:
updated_pr += update_billed_amount_based_on_po(d.po_detail, update_modified) po_details.append(d.po_detail)
if po_details:
updated_pr += update_billed_amount_based_on_po(po_details, update_modified)
for pr in set(updated_pr): for pr in set(updated_pr):
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import update_billing_percentage from erpnext.stock.doctype.purchase_receipt.purchase_receipt import update_billing_percentage

View File

@@ -0,0 +1,53 @@
// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Repost Payment Ledger', {
setup: function(frm) {
frm.set_query("voucher_type", () => {
return {
filters: {
name: ['in', ['Purchase Invoice', 'Sales Invoice', 'Payment Entry', 'Journal Entry']]
}
};
});
frm.fields_dict['repost_vouchers'].grid.get_field('voucher_type').get_query = function(doc) {
return {
filters: {
name: ['in', ['Purchase Invoice', 'Sales Invoice', 'Payment Entry', 'Journal Entry']]
}
}
}
frm.fields_dict['repost_vouchers'].grid.get_field('voucher_no').get_query = function(doc) {
if (doc.company) {
return {
filters: {
company: doc.company,
docstatus: 1
}
}
}
}
},
refresh: function(frm) {
if (frm.doc.docstatus==1 && ['Queued', 'Failed'].find(x => x == frm.doc.repost_status)) {
frm.set_intro(__("Use 'Repost in background' button to trigger background job. Job can only be triggered when document is in Queued or Failed status."));
var btn_label = __("Repost in background")
frm.add_custom_button(btn_label, () => {
frappe.call({
method: 'erpnext.accounts.doctype.repost_payment_ledger.repost_payment_ledger.execute_repost_payment_ledger',
args: {
docname: frm.doc.name,
}
});
frappe.msgprint(__('Reposting in the background.'));
});
}
}
});

View File

@@ -0,0 +1,159 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2022-10-19 21:59:33.553852",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"filters_section",
"company",
"posting_date",
"column_break_4",
"voucher_type",
"add_manually",
"status_section",
"repost_status",
"repost_error_log",
"selected_vouchers_section",
"repost_vouchers",
"amended_from"
],
"fields": [
{
"default": "Today",
"fieldname": "posting_date",
"fieldtype": "Date",
"label": "Posting Date",
"reqd": 1
},
{
"fieldname": "voucher_type",
"fieldtype": "Link",
"label": "Voucher Type",
"options": "DocType"
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Repost Payment Ledger",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "company",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Company",
"options": "Company",
"reqd": 1
},
{
"fieldname": "selected_vouchers_section",
"fieldtype": "Section Break",
"label": "Vouchers"
},
{
"fieldname": "filters_section",
"fieldtype": "Section Break",
"label": "Filters"
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"fieldname": "repost_vouchers",
"fieldtype": "Table",
"label": "Selected Vouchers",
"options": "Repost Payment Ledger Items"
},
{
"fieldname": "repost_status",
"fieldtype": "Select",
"label": "Repost Status",
"options": "\nQueued\nFailed\nCompleted",
"read_only": 1
},
{
"fieldname": "status_section",
"fieldtype": "Section Break",
"label": "Status"
},
{
"default": "0",
"description": "Ignore Voucher Type filter and Select Vouchers Manually",
"fieldname": "add_manually",
"fieldtype": "Check",
"label": "Add Manually"
},
{
"depends_on": "eval:doc.repost_error_log",
"fieldname": "repost_error_log",
"fieldtype": "Long Text",
"label": "Repost Error Log"
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2022-11-08 07:38:40.079038",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Repost Payment Ledger",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts Manager",
"share": 1,
"submit": 1,
"write": 1
},
{
"create": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts User",
"share": 1,
"write": 1
},
{
"email": 1,
"export": 1,
"permlevel": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View File

@@ -0,0 +1,110 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import copy
import frappe
from frappe import _, qb
from frappe.model.document import Document
from frappe.query_builder.custom import ConstantColumn
from erpnext.accounts.utils import _delete_pl_entries, create_payment_ledger_entry
VOUCHER_TYPES = ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"]
def repost_ple_for_voucher(voucher_type, voucher_no, gle_map=None):
if voucher_type and voucher_no and gle_map:
_delete_pl_entries(voucher_type, voucher_no)
create_payment_ledger_entry(gle_map, cancel=0)
@frappe.whitelist()
def start_payment_ledger_repost(docname=None):
"""
Repost Payment Ledger Entries for Vouchers through Background Job
"""
if docname:
repost_doc = frappe.get_doc("Repost Payment Ledger", docname)
if repost_doc.docstatus == 1 and repost_doc.repost_status in ["Queued", "Failed"]:
try:
for entry in repost_doc.repost_vouchers:
doc = frappe.get_doc(entry.voucher_type, entry.voucher_no)
if doc.doctype in ["Payment Entry", "Journal Entry"]:
gle_map = doc.build_gl_map()
else:
gle_map = doc.get_gl_entries()
repost_ple_for_voucher(entry.voucher_type, entry.voucher_no, gle_map)
frappe.db.set_value(repost_doc.doctype, repost_doc.name, "repost_error_log", "")
frappe.db.set_value(repost_doc.doctype, repost_doc.name, "repost_status", "Completed")
except Exception as e:
frappe.db.rollback()
traceback = frappe.get_traceback()
if traceback:
message = "Traceback: <br>" + traceback
frappe.db.set_value(repost_doc.doctype, repost_doc.name, "repost_error_log", message)
frappe.db.set_value(repost_doc.doctype, repost_doc.name, "repost_status", "Failed")
class RepostPaymentLedger(Document):
def __init__(self, *args, **kwargs):
super(RepostPaymentLedger, self).__init__(*args, **kwargs)
self.vouchers = []
def before_validate(self):
self.load_vouchers_based_on_filters()
self.set_status()
def load_vouchers_based_on_filters(self):
if not self.add_manually:
self.repost_vouchers.clear()
self.get_vouchers()
self.extend("repost_vouchers", copy.deepcopy(self.vouchers))
def get_vouchers(self):
self.vouchers.clear()
filter_on_voucher_types = [self.voucher_type] if self.voucher_type else VOUCHER_TYPES
for vtype in filter_on_voucher_types:
doc = qb.DocType(vtype)
doctype_name = ConstantColumn(vtype)
query = (
qb.from_(doc)
.select(doctype_name.as_("voucher_type"), doc.name.as_("voucher_no"))
.where(
(doc.docstatus == 1)
& (doc.company == self.company)
& (doc.posting_date.gte(self.posting_date))
)
)
entries = query.run(as_dict=True)
self.vouchers.extend(entries)
def set_status(self):
if self.docstatus == 0:
self.repost_status = "Queued"
def on_submit(self):
execute_repost_payment_ledger(self.name)
frappe.msgprint(_("Repost started in the background"))
@frappe.whitelist()
def execute_repost_payment_ledger(docname):
"""Repost Payment Ledger Entries by background job."""
job_name = "payment_ledger_repost_" + docname
if not frappe.utils.background_jobs.is_job_queued(job_name):
frappe.enqueue(
method="erpnext.accounts.doctype.repost_payment_ledger.repost_payment_ledger.start_payment_ledger_repost",
docname=docname,
is_async=True,
job_name=job_name,
)

View File

@@ -0,0 +1,12 @@
frappe.listview_settings["Repost Payment Ledger"] = {
add_fields: ["repost_status"],
get_indicator: function(doc) {
var colors = {
'Queued': 'orange',
'Completed': 'green',
'Failed': 'red',
};
let status = doc.repost_status;
return [__(status), colors[status], 'status,=,'+status];
},
};

View File

@@ -0,0 +1,9 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
from frappe.tests.utils import FrappeTestCase
class TestRepostPaymentLedger(FrappeTestCase):
pass

View File

@@ -0,0 +1,35 @@
{
"actions": [],
"creation": "2022-10-20 10:44:18.796489",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"voucher_type",
"voucher_no"
],
"fields": [
{
"fieldname": "voucher_type",
"fieldtype": "Link",
"label": "Voucher Type",
"options": "DocType"
},
{
"fieldname": "voucher_no",
"fieldtype": "Dynamic Link",
"label": "Voucher No",
"options": "voucher_type"
}
],
"istable": 1,
"links": [],
"modified": "2022-10-28 14:47:11.838109",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Repost Payment Ledger Items",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View File

@@ -0,0 +1,9 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class RepostPaymentLedgerItems(Document):
pass

View File

@@ -2090,7 +2090,7 @@
{ {
"collapsible": 1, "collapsible": 1,
"collapsible_depends_on": "write_off_amount", "collapsible_depends_on": "write_off_amount",
"depends_on": "grand_total", "depends_on": "is_pos",
"fieldname": "write_off_section", "fieldname": "write_off_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"hide_days": 1, "hide_days": 1,
@@ -2109,7 +2109,7 @@
"link_fieldname": "consolidated_invoice" "link_fieldname": "consolidated_invoice"
} }
], ],
"modified": "2022-10-11 13:07:36.488095", "modified": "2022-11-15 09:33:47.870616",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Invoice", "name": "Sales Invoice",

View File

@@ -367,7 +367,7 @@ class SalesInvoice(SellingController):
if self.update_stock == 1: if self.update_stock == 1:
self.repost_future_sle_and_gle() self.repost_future_sle_and_gle()
frappe.db.set(self, "status", "Cancelled") self.db_set("status", "Cancelled")
if ( if (
frappe.db.get_single_value("Selling Settings", "sales_update_frequency") == "Each Transaction" frappe.db.get_single_value("Selling Settings", "sales_update_frequency") == "Each Transaction"
@@ -1300,7 +1300,11 @@ class SalesInvoice(SellingController):
def make_write_off_gl_entry(self, gl_entries): def make_write_off_gl_entry(self, gl_entries):
# write off entries, applicable if only pos # write off entries, applicable if only pos
if self.write_off_account and flt(self.write_off_amount, self.precision("write_off_amount")): if (
self.is_pos
and self.write_off_account
and flt(self.write_off_amount, self.precision("write_off_amount"))
):
write_off_account_currency = get_account_currency(self.write_off_account) write_off_account_currency = get_account_currency(self.write_off_account)
default_cost_center = frappe.get_cached_value("Company", self.company, "cost_center") default_cost_center = frappe.get_cached_value("Company", self.company, "cost_center")
@@ -2306,7 +2310,7 @@ def get_loyalty_programs(customer):
lp_details = get_loyalty_programs(customer) lp_details = get_loyalty_programs(customer)
if len(lp_details) == 1: if len(lp_details) == 1:
frappe.db.set(customer, "loyalty_program", lp_details[0]) customer.db_set("loyalty_program", lp_details[0])
return lp_details return lp_details
else: else:
return lp_details return lp_details

View File

@@ -3,7 +3,8 @@
import frappe import frappe
from frappe import _, scrub from frappe import _, qb, scrub
from frappe.query_builder import Order
from frappe.utils import cint, flt, formatdate from frappe.utils import cint, flt, formatdate
from erpnext.controllers.queries import get_match_cond from erpnext.controllers.queries import get_match_cond
@@ -398,6 +399,7 @@ class GrossProfitGenerator(object):
self.average_buying_rate = {} self.average_buying_rate = {}
self.filters = frappe._dict(filters) self.filters = frappe._dict(filters)
self.load_invoice_items() self.load_invoice_items()
self.get_delivery_notes()
if filters.group_by == "Invoice": if filters.group_by == "Invoice":
self.group_items_by_invoice() self.group_items_by_invoice()
@@ -591,6 +593,21 @@ class GrossProfitGenerator(object):
return flt(buying_amount, self.currency_precision) return flt(buying_amount, self.currency_precision)
def calculate_buying_amount_from_sle(self, row, my_sle, parenttype, parent, item_row, item_code):
for i, sle in enumerate(my_sle):
# find the stock valution rate from stock ledger entry
if (
sle.voucher_type == parenttype
and parent == sle.voucher_no
and sle.voucher_detail_no == item_row
):
previous_stock_value = len(my_sle) > i + 1 and flt(my_sle[i + 1].stock_value) or 0.0
if previous_stock_value:
return abs(previous_stock_value - flt(sle.stock_value)) * flt(row.qty) / abs(flt(sle.qty))
else:
return flt(row.qty) * self.get_average_buying_rate(row, item_code)
def get_buying_amount(self, row, item_code): def get_buying_amount(self, row, item_code):
# IMP NOTE # IMP NOTE
# stock_ledger_entries should already be filtered by item_code and warehouse and # stock_ledger_entries should already be filtered by item_code and warehouse and
@@ -607,19 +624,22 @@ class GrossProfitGenerator(object):
if row.dn_detail: if row.dn_detail:
parenttype, parent = "Delivery Note", row.delivery_note parenttype, parent = "Delivery Note", row.delivery_note
for i, sle in enumerate(my_sle): return self.calculate_buying_amount_from_sle(
# find the stock valution rate from stock ledger entry row, my_sle, parenttype, parent, row.item_row, item_code
if ( )
sle.voucher_type == parenttype elif self.delivery_notes.get((row.parent, row.item_code), None):
and parent == sle.voucher_no # check if Invoice has delivery notes
and sle.voucher_detail_no == row.item_row dn = self.delivery_notes.get((row.parent, row.item_code))
): parenttype, parent, item_row, warehouse = (
previous_stock_value = len(my_sle) > i + 1 and flt(my_sle[i + 1].stock_value) or 0.0 "Delivery Note",
dn["delivery_note"],
if previous_stock_value: dn["item_row"],
return abs(previous_stock_value - flt(sle.stock_value)) * flt(row.qty) / abs(flt(sle.qty)) dn["warehouse"],
else: )
return flt(row.qty) * self.get_average_buying_rate(row, item_code) my_sle = self.sle.get((item_code, warehouse))
return self.calculate_buying_amount_from_sle(
row, my_sle, parenttype, parent, item_row, item_code
)
else: else:
return flt(row.qty) * self.get_average_buying_rate(row, item_code) return flt(row.qty) * self.get_average_buying_rate(row, item_code)
@@ -753,6 +773,29 @@ class GrossProfitGenerator(object):
as_dict=1, as_dict=1,
) )
def get_delivery_notes(self):
self.delivery_notes = frappe._dict({})
if self.si_list:
invoices = [x.parent for x in self.si_list]
dni = qb.DocType("Delivery Note Item")
delivery_notes = (
qb.from_(dni)
.select(
dni.against_sales_invoice.as_("sales_invoice"),
dni.item_code,
dni.warehouse,
dni.parent.as_("delivery_note"),
dni.name.as_("item_row"),
)
.where((dni.docstatus == 1) & (dni.against_sales_invoice.isin(invoices)))
.groupby(dni.against_sales_invoice, dni.item_code)
.orderby(dni.creation, order=Order.desc)
.run(as_dict=True)
)
for entry in delivery_notes:
self.delivery_notes[(entry.sales_invoice, entry.item_code)] = entry
def group_items_by_invoice(self): def group_items_by_invoice(self):
""" """
Turns list of Sales Invoice Items to a tree of Sales Invoices with their Items as children. Turns list of Sales Invoice Items to a tree of Sales Invoices with their Items as children.

View File

@@ -0,0 +1,209 @@
import frappe
from frappe import qb
from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_days, flt, nowdate
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_delivery_note
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.report.gross_profit.gross_profit import execute
from erpnext.stock.doctype.item.test_item import create_item
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
class TestGrossProfit(FrappeTestCase):
def setUp(self):
self.create_company()
self.create_item()
self.create_customer()
self.create_sales_invoice()
self.clear_old_entries()
def tearDown(self):
frappe.db.rollback()
def create_company(self):
company_name = "_Test Gross Profit"
abbr = "_GP"
if frappe.db.exists("Company", company_name):
company = frappe.get_doc("Company", company_name)
else:
company = frappe.get_doc(
{
"doctype": "Company",
"company_name": company_name,
"country": "India",
"default_currency": "INR",
"create_chart_of_accounts_based_on": "Standard Template",
"chart_of_accounts": "Standard",
}
)
company = company.save()
self.company = company.name
self.cost_center = company.cost_center
self.warehouse = "Stores - " + abbr
self.income_account = "Sales - " + abbr
self.expense_account = "Cost of Goods Sold - " + abbr
self.debit_to = "Debtors - " + abbr
self.creditors = "Creditors - " + abbr
def create_item(self):
item = create_item(
item_code="_Test GP Item", is_stock_item=1, company=self.company, warehouse=self.warehouse
)
self.item = item if isinstance(item, str) else item.item_code
def create_customer(self):
name = "_Test GP Customer"
if frappe.db.exists("Customer", name):
self.customer = name
else:
customer = frappe.new_doc("Customer")
customer.customer_name = name
customer.type = "Individual"
customer.save()
self.customer = customer.name
def create_sales_invoice(
self, qty=1, rate=100, posting_date=nowdate(), do_not_save=False, do_not_submit=False
):
"""
Helper function to populate default values in sales invoice
"""
sinv = create_sales_invoice(
qty=qty,
rate=rate,
company=self.company,
customer=self.customer,
item_code=self.item,
item_name=self.item,
cost_center=self.cost_center,
warehouse=self.warehouse,
debit_to=self.debit_to,
parent_cost_center=self.cost_center,
update_stock=0,
currency="INR",
is_pos=0,
is_return=0,
return_against=None,
income_account=self.income_account,
expense_account=self.expense_account,
do_not_save=do_not_save,
do_not_submit=do_not_submit,
)
return sinv
def clear_old_entries(self):
doctype_list = [
"Sales Invoice",
"GL Entry",
"Payment Ledger Entry",
"Stock Entry",
"Stock Ledger Entry",
"Delivery Note",
]
for doctype in doctype_list:
qb.from_(qb.DocType(doctype)).delete().where(qb.DocType(doctype).company == self.company).run()
def test_invoice_without_only_delivery_note(self):
"""
Test buying amount for Invoice without `update_stock` flag set but has Delivery Note
"""
se = make_stock_entry(
company=self.company,
item_code=self.item,
target=self.warehouse,
qty=1,
basic_rate=100,
do_not_submit=True,
)
item = se.items[0]
se.append(
"items",
{
"item_code": item.item_code,
"s_warehouse": item.s_warehouse,
"t_warehouse": item.t_warehouse,
"qty": 1,
"basic_rate": 200,
"conversion_factor": item.conversion_factor or 1.0,
"transfer_qty": flt(item.qty) * (flt(item.conversion_factor) or 1.0),
"serial_no": item.serial_no,
"batch_no": item.batch_no,
"cost_center": item.cost_center,
"expense_account": item.expense_account,
},
)
se = se.save().submit()
sinv = create_sales_invoice(
qty=1,
rate=100,
company=self.company,
customer=self.customer,
item_code=self.item,
item_name=self.item,
cost_center=self.cost_center,
warehouse=self.warehouse,
debit_to=self.debit_to,
parent_cost_center=self.cost_center,
update_stock=0,
currency="INR",
income_account=self.income_account,
expense_account=self.expense_account,
)
filters = frappe._dict(
company=self.company, from_date=nowdate(), to_date=nowdate(), group_by="Invoice"
)
columns, data = execute(filters=filters)
# Without Delivery Note, buying rate should be 150
expected_entry_without_dn = {
"parent_invoice": sinv.name,
"currency": "INR",
"sales_invoice": self.item,
"customer": self.customer,
"posting_date": frappe.utils.datetime.date.fromisoformat(nowdate()),
"item_code": self.item,
"item_name": self.item,
"warehouse": "Stores - _GP",
"qty": 1.0,
"avg._selling_rate": 100.0,
"valuation_rate": 150.0,
"selling_amount": 100.0,
"buying_amount": 150.0,
"gross_profit": -50.0,
"gross_profit_%": -50.0,
}
gp_entry = [x for x in data if x.parent_invoice == sinv.name]
self.assertDictContainsSubset(expected_entry_without_dn, gp_entry[0])
# make delivery note
dn = make_delivery_note(sinv.name)
dn.items[0].qty = 1
dn = dn.save().submit()
columns, data = execute(filters=filters)
# Without Delivery Note, buying rate should be 100
expected_entry_with_dn = {
"parent_invoice": sinv.name,
"currency": "INR",
"sales_invoice": self.item,
"customer": self.customer,
"posting_date": frappe.utils.datetime.date.fromisoformat(nowdate()),
"item_code": self.item,
"item_name": self.item,
"warehouse": "Stores - _GP",
"qty": 1.0,
"avg._selling_rate": 100.0,
"valuation_rate": 100.0,
"selling_amount": 100.0,
"buying_amount": 100.0,
"gross_profit": 0.0,
"gross_profit_%": 0.0,
}
gp_entry = [x for x in data if x.parent_invoice == sinv.name]
self.assertDictContainsSubset(expected_entry_with_dn, gp_entry[0])

View File

@@ -1146,10 +1146,10 @@ def repost_gle_for_stock_vouchers(
if not existing_gle or not compare_existing_and_expected_gle( if not existing_gle or not compare_existing_and_expected_gle(
existing_gle, expected_gle, precision existing_gle, expected_gle, precision
): ):
_delete_gl_entries(voucher_type, voucher_no) _delete_accounting_ledger_entries(voucher_type, voucher_no)
voucher_obj.make_gl_entries(gl_entries=expected_gle, from_repost=True) voucher_obj.make_gl_entries(gl_entries=expected_gle, from_repost=True)
else: else:
_delete_gl_entries(voucher_type, voucher_no) _delete_accounting_ledger_entries(voucher_type, voucher_no)
if not frappe.flags.in_test: if not frappe.flags.in_test:
frappe.db.commit() frappe.db.commit()
@@ -1161,18 +1161,28 @@ def repost_gle_for_stock_vouchers(
) )
def _delete_gl_entries(voucher_type, voucher_no): def _delete_pl_entries(voucher_type, voucher_no):
frappe.db.sql(
"""delete from `tabGL Entry`
where voucher_type=%s and voucher_no=%s""",
(voucher_type, voucher_no),
)
ple = qb.DocType("Payment Ledger Entry") ple = qb.DocType("Payment Ledger Entry")
qb.from_(ple).delete().where( qb.from_(ple).delete().where(
(ple.voucher_type == voucher_type) & (ple.voucher_no == voucher_no) (ple.voucher_type == voucher_type) & (ple.voucher_no == voucher_no)
).run() ).run()
def _delete_gl_entries(voucher_type, voucher_no):
gle = qb.DocType("GL Entry")
qb.from_(gle).delete().where(
(gle.voucher_type == voucher_type) & (gle.voucher_no == voucher_no)
).run()
def _delete_accounting_ledger_entries(voucher_type, voucher_no):
"""
Remove entries from both General and Payment Ledger for specified Voucher
"""
_delete_gl_entries(voucher_type, voucher_no)
_delete_pl_entries(voucher_type, voucher_no)
def sort_stock_vouchers_by_posting_date( def sort_stock_vouchers_by_posting_date(
stock_vouchers: List[Tuple[str, str]] stock_vouchers: List[Tuple[str, str]]
) -> List[Tuple[str, str]]: ) -> List[Tuple[str, str]]:

View File

@@ -361,7 +361,7 @@ class PurchaseOrder(BuyingController):
self.update_reserved_qty_for_subcontract() self.update_reserved_qty_for_subcontract()
self.check_on_hold_or_closed_status() self.check_on_hold_or_closed_status()
frappe.db.set(self, "status", "Cancelled") self.db_set("status", "Cancelled")
self.update_prevdoc_status() self.update_prevdoc_status()

View File

@@ -31,7 +31,7 @@ class RequestforQuotation(BuyingController):
if self.docstatus < 1: if self.docstatus < 1:
# after amend and save, status still shows as cancelled, until submit # after amend and save, status still shows as cancelled, until submit
frappe.db.set(self, "status", "Draft") self.db_set("status", "Draft")
def validate_duplicate_supplier(self): def validate_duplicate_supplier(self):
supplier_list = [d.supplier for d in self.suppliers] supplier_list = [d.supplier for d in self.suppliers]
@@ -73,14 +73,14 @@ class RequestforQuotation(BuyingController):
) )
def on_submit(self): def on_submit(self):
frappe.db.set(self, "status", "Submitted") self.db_set("status", "Submitted")
for supplier in self.suppliers: for supplier in self.suppliers:
supplier.email_sent = 0 supplier.email_sent = 0
supplier.quote_status = "Pending" supplier.quote_status = "Pending"
self.send_to_supplier() self.send_to_supplier()
def on_cancel(self): def on_cancel(self):
frappe.db.set(self, "status", "Cancelled") self.db_set("status", "Cancelled")
@frappe.whitelist() @frappe.whitelist()
def get_supplier_email_preview(self, supplier): def get_supplier_email_preview(self, supplier):

View File

@@ -10,34 +10,37 @@
"document_type": "Setup", "document_type": "Setup",
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"basic_info",
"naming_series", "naming_series",
"supplier_name", "supplier_name",
"country", "country",
"default_bank_account",
"tax_id",
"tax_category",
"tax_withholding_category",
"image",
"column_break0", "column_break0",
"supplier_group", "supplier_group",
"supplier_type", "supplier_type",
"allow_purchase_invoice_creation_without_purchase_order",
"allow_purchase_invoice_creation_without_purchase_receipt",
"is_internal_supplier",
"represents_company",
"disabled",
"is_transporter", "is_transporter",
"warn_rfqs", "image",
"warn_pos", "defaults_section",
"prevent_rfqs",
"prevent_pos",
"allowed_to_transact_section",
"companies",
"section_break_7",
"default_currency", "default_currency",
"default_bank_account",
"column_break_10", "column_break_10",
"default_price_list", "default_price_list",
"payment_terms",
"internal_supplier_section",
"is_internal_supplier",
"represents_company",
"column_break_16",
"companies",
"column_break2",
"supplier_details",
"column_break_30",
"website",
"language",
"dashboard_tab",
"tax_tab",
"tax_id",
"column_break_27",
"tax_category",
"tax_withholding_category",
"contact_and_address_tab",
"address_contacts", "address_contacts",
"address_html", "address_html",
"column_break1", "column_break1",
@@ -49,30 +52,25 @@
"column_break_44", "column_break_44",
"supplier_primary_address", "supplier_primary_address",
"primary_address", "primary_address",
"default_payable_accounts", "accounting_tab",
"accounts", "accounts",
"section_credit_limit", "settings_tab",
"payment_terms", "allow_purchase_invoice_creation_without_purchase_order",
"cb_21", "allow_purchase_invoice_creation_without_purchase_receipt",
"column_break_54",
"is_frozen",
"disabled",
"warn_rfqs",
"warn_pos",
"prevent_rfqs",
"prevent_pos",
"block_supplier_section",
"on_hold", "on_hold",
"hold_type", "hold_type",
"release_date", "column_break_59",
"default_tax_withholding_config", "release_date"
"column_break2",
"website",
"supplier_details",
"column_break_30",
"language",
"is_frozen"
], ],
"fields": [ "fields": [
{
"fieldname": "basic_info",
"fieldtype": "Section Break",
"label": "Name and Type",
"oldfieldtype": "Section Break",
"options": "fa fa-user"
},
{ {
"fieldname": "naming_series", "fieldname": "naming_series",
"fieldtype": "Select", "fieldtype": "Select",
@@ -192,6 +190,7 @@
"default": "0", "default": "0",
"fieldname": "warn_rfqs", "fieldname": "warn_rfqs",
"fieldtype": "Check", "fieldtype": "Check",
"hidden": 1,
"label": "Warn RFQs", "label": "Warn RFQs",
"read_only": 1 "read_only": 1
}, },
@@ -199,6 +198,7 @@
"default": "0", "default": "0",
"fieldname": "warn_pos", "fieldname": "warn_pos",
"fieldtype": "Check", "fieldtype": "Check",
"hidden": 1,
"label": "Warn POs", "label": "Warn POs",
"read_only": 1 "read_only": 1
}, },
@@ -206,6 +206,7 @@
"default": "0", "default": "0",
"fieldname": "prevent_rfqs", "fieldname": "prevent_rfqs",
"fieldtype": "Check", "fieldtype": "Check",
"hidden": 1,
"label": "Prevent RFQs", "label": "Prevent RFQs",
"read_only": 1 "read_only": 1
}, },
@@ -213,15 +214,10 @@
"default": "0", "default": "0",
"fieldname": "prevent_pos", "fieldname": "prevent_pos",
"fieldtype": "Check", "fieldtype": "Check",
"hidden": 1,
"label": "Prevent POs", "label": "Prevent POs",
"read_only": 1 "read_only": 1
}, },
{
"depends_on": "represents_company",
"fieldname": "allowed_to_transact_section",
"fieldtype": "Section Break",
"label": "Allowed To Transact With"
},
{ {
"depends_on": "represents_company", "depends_on": "represents_company",
"fieldname": "companies", "fieldname": "companies",
@@ -229,12 +225,6 @@
"label": "Allowed To Transact With", "label": "Allowed To Transact With",
"options": "Allowed To Transact With" "options": "Allowed To Transact With"
}, },
{
"collapsible": 1,
"fieldname": "section_break_7",
"fieldtype": "Section Break",
"label": "Currency and Price List"
},
{ {
"fieldname": "default_currency", "fieldname": "default_currency",
"fieldtype": "Link", "fieldtype": "Link",
@@ -254,22 +244,12 @@
"label": "Price List", "label": "Price List",
"options": "Price List" "options": "Price List"
}, },
{
"collapsible": 1,
"fieldname": "section_credit_limit",
"fieldtype": "Section Break",
"label": "Payment Terms"
},
{ {
"fieldname": "payment_terms", "fieldname": "payment_terms",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Default Payment Terms Template", "label": "Default Payment Terms Template",
"options": "Payment Terms Template" "options": "Payment Terms Template"
}, },
{
"fieldname": "cb_21",
"fieldtype": "Column Break"
},
{ {
"default": "0", "default": "0",
"fieldname": "on_hold", "fieldname": "on_hold",
@@ -315,13 +295,6 @@
"label": "Contact HTML", "label": "Contact HTML",
"read_only": 1 "read_only": 1
}, },
{
"collapsible": 1,
"collapsible_depends_on": "accounts",
"fieldname": "default_payable_accounts",
"fieldtype": "Section Break",
"label": "Default Payable Accounts"
},
{ {
"description": "Mention if non-standard payable account", "description": "Mention if non-standard payable account",
"fieldname": "accounts", "fieldname": "accounts",
@@ -329,12 +302,6 @@
"label": "Accounts", "label": "Accounts",
"options": "Party Account" "options": "Party Account"
}, },
{
"collapsible": 1,
"fieldname": "default_tax_withholding_config",
"fieldtype": "Section Break",
"label": "Default Tax Withholding Config"
},
{ {
"collapsible": 1, "collapsible": 1,
"collapsible_depends_on": "supplier_details", "collapsible_depends_on": "supplier_details",
@@ -383,7 +350,7 @@
{ {
"fieldname": "primary_address_and_contact_detail_section", "fieldname": "primary_address_and_contact_detail_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Primary Address and Contact Detail" "label": "Primary Address and Contact"
}, },
{ {
"description": "Reselect, if the chosen contact is edited after save", "description": "Reselect, if the chosen contact is edited after save",
@@ -420,6 +387,64 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "Supplier Primary Address", "label": "Supplier Primary Address",
"options": "Address" "options": "Address"
},
{
"fieldname": "dashboard_tab",
"fieldtype": "Tab Break",
"label": "Dashboard",
"show_dashboard": 1
},
{
"fieldname": "settings_tab",
"fieldtype": "Tab Break",
"label": "Settings"
},
{
"fieldname": "contact_and_address_tab",
"fieldtype": "Tab Break",
"label": "Contact & Address"
},
{
"fieldname": "accounting_tab",
"fieldtype": "Tab Break",
"label": "Accounting"
},
{
"fieldname": "defaults_section",
"fieldtype": "Section Break",
"label": "Defaults"
},
{
"fieldname": "tax_tab",
"fieldtype": "Tab Break",
"label": "Tax"
},
{
"collapsible": 1,
"fieldname": "internal_supplier_section",
"fieldtype": "Section Break",
"label": "Internal Supplier"
},
{
"fieldname": "column_break_16",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_27",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_54",
"fieldtype": "Column Break"
},
{
"fieldname": "block_supplier_section",
"fieldtype": "Section Break",
"label": "Block Supplier"
},
{
"fieldname": "column_break_59",
"fieldtype": "Column Break"
} }
], ],
"icon": "fa fa-user", "icon": "fa fa-user",
@@ -432,7 +457,7 @@
"link_fieldname": "party" "link_fieldname": "party"
} }
], ],
"modified": "2022-04-16 18:02:27.838623", "modified": "2022-11-09 18:02:59.075203",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Supplier", "name": "Supplier",

View File

@@ -145,7 +145,7 @@ class Supplier(TransactionBase):
def after_rename(self, olddn, newdn, merge=False): def after_rename(self, olddn, newdn, merge=False):
if frappe.defaults.get_global_default("supp_master_name") == "Supplier Name": if frappe.defaults.get_global_default("supp_master_name") == "Supplier Name":
frappe.db.set(self, "supplier_name", newdn) self.db_set("supplier_name", newdn)
@frappe.whitelist() @frappe.whitelist()

View File

@@ -30,11 +30,11 @@ class SupplierQuotation(BuyingController):
self.validate_valid_till() self.validate_valid_till()
def on_submit(self): def on_submit(self):
frappe.db.set(self, "status", "Submitted") self.db_set("status", "Submitted")
self.update_rfq_supplier_status(1) self.update_rfq_supplier_status(1)
def on_cancel(self): def on_cancel(self):
frappe.db.set(self, "status", "Cancelled") self.db_set("status", "Cancelled")
self.update_rfq_supplier_status(0) self.update_rfq_supplier_status(0)
def on_trash(self): def on_trash(self):

View File

@@ -100,7 +100,7 @@ class SubcontractingController(StockController):
and self._doc_before_save and self._doc_before_save
): ):
for row in self._doc_before_save.get("items"): for row in self._doc_before_save.get("items"):
item_dict[row.name] = (row.item_code, row.received_qty or row.qty) item_dict[row.name] = (row.item_code, row.qty)
return item_dict return item_dict
@@ -118,9 +118,7 @@ class SubcontractingController(StockController):
for row in self.items: for row in self.items:
self.__reference_name.append(row.name) self.__reference_name.append(row.name)
if (row.name not in item_dict) or (row.item_code, row.received_qty or row.qty) != item_dict[ if (row.name not in item_dict) or (row.item_code, row.qty) != item_dict[row.name]:
row.name
]:
self.__changed_name.append(row.name) self.__changed_name.append(row.name)
if item_dict.get(row.name): if item_dict.get(row.name):
@@ -463,13 +461,12 @@ class SubcontractingController(StockController):
def __get_qty_based_on_material_transfer(self, item_row, transfer_item): def __get_qty_based_on_material_transfer(self, item_row, transfer_item):
key = (item_row.item_code, item_row.get(self.subcontract_data.order_field)) key = (item_row.item_code, item_row.get(self.subcontract_data.order_field))
item_qty = item_row.received_qty or item_row.qty
if self.qty_to_be_received.get(key) == item_qty: if self.qty_to_be_received == item_row.qty:
return transfer_item.qty return transfer_item.qty
if self.qty_to_be_received: if self.qty_to_be_received:
qty = (flt(item_qty) * flt(transfer_item.qty)) / flt(self.qty_to_be_received.get(key, 0)) qty = (flt(item_row.qty) * flt(transfer_item.qty)) / flt(self.qty_to_be_received.get(key, 0))
transfer_item.item_details.required_qty = transfer_item.qty transfer_item.item_details.required_qty = transfer_item.qty
if transfer_item.serial_no or frappe.get_cached_value( if transfer_item.serial_no or frappe.get_cached_value(
@@ -494,11 +491,7 @@ class SubcontractingController(StockController):
for bom_item in self.__get_materials_from_bom( for bom_item in self.__get_materials_from_bom(
row.item_code, row.bom, row.get("include_exploded_items") row.item_code, row.bom, row.get("include_exploded_items")
): ):
qty = ( qty = flt(bom_item.qty_consumed_per_unit) * flt(row.qty) * row.conversion_factor
flt(bom_item.qty_consumed_per_unit)
* flt(row.received_qty or row.qty)
* row.conversion_factor
)
bom_item.main_item_code = row.item_code bom_item.main_item_code = row.item_code
self.__update_reserve_warehouse(bom_item, row) self.__update_reserve_warehouse(bom_item, row)
self.__set_alternative_item(bom_item) self.__set_alternative_item(bom_item)

View File

@@ -60,7 +60,7 @@ class Opportunity(TransactionBase, CRMNote):
if not self.get(field) and frappe.db.field_exists(self.opportunity_from, field): if not self.get(field) and frappe.db.field_exists(self.opportunity_from, field):
try: try:
value = frappe.db.get_value(self.opportunity_from, self.party_name, field) value = frappe.db.get_value(self.opportunity_from, self.party_name, field)
frappe.db.set(self, field, value) self.db_set(field, value)
except Exception: except Exception:
continue continue

View File

@@ -573,8 +573,8 @@ def regenerate_repayment_schedule(loan, cancel=0):
loan_doc = frappe.get_doc("Loan", loan) loan_doc = frappe.get_doc("Loan", loan)
next_accrual_date = None next_accrual_date = None
accrued_entries = 0 accrued_entries = 0
last_repayment_amount = 0 last_repayment_amount = None
last_balance_amount = 0 last_balance_amount = None
for term in reversed(loan_doc.get("repayment_schedule")): for term in reversed(loan_doc.get("repayment_schedule")):
if not term.is_accrued: if not term.is_accrued:
@@ -582,9 +582,9 @@ def regenerate_repayment_schedule(loan, cancel=0):
loan_doc.remove(term) loan_doc.remove(term)
else: else:
accrued_entries += 1 accrued_entries += 1
if not last_repayment_amount: if last_repayment_amount is None:
last_repayment_amount = term.total_payment last_repayment_amount = term.total_payment
if not last_balance_amount: if last_balance_amount is None:
last_balance_amount = term.balance_loan_amount last_balance_amount = term.balance_loan_amount
loan_doc.save() loan_doc.save()

View File

@@ -119,7 +119,7 @@ class MaintenanceSchedule(TransactionBase):
event.add_participant(self.doctype, self.name) event.add_participant(self.doctype, self.name)
event.insert(ignore_permissions=1) event.insert(ignore_permissions=1)
frappe.db.set(self, "status", "Submitted") self.db_set("status", "Submitted")
def create_schedule_list(self, start_date, end_date, no_of_visit, sales_person): def create_schedule_list(self, start_date, end_date, no_of_visit, sales_person):
schedule_list = [] schedule_list = []
@@ -245,7 +245,7 @@ class MaintenanceSchedule(TransactionBase):
self.generate_schedule() self.generate_schedule()
def on_update(self): def on_update(self):
frappe.db.set(self, "status", "Draft") self.db_set("status", "Draft")
def update_amc_date(self, serial_nos, amc_expiry_date=None): def update_amc_date(self, serial_nos, amc_expiry_date=None):
for serial_no in serial_nos: for serial_no in serial_nos:
@@ -344,7 +344,7 @@ class MaintenanceSchedule(TransactionBase):
if d.serial_no: if d.serial_no:
serial_nos = get_valid_serial_nos(d.serial_no) serial_nos = get_valid_serial_nos(d.serial_no)
self.update_amc_date(serial_nos) self.update_amc_date(serial_nos)
frappe.db.set(self, "status", "Cancelled") self.db_set("status", "Cancelled")
delete_events(self.doctype, self.name) delete_events(self.doctype, self.name)
def on_trash(self): def on_trash(self):

View File

@@ -125,12 +125,12 @@ class MaintenanceVisit(TransactionBase):
def on_submit(self): def on_submit(self):
self.update_customer_issue(1) self.update_customer_issue(1)
frappe.db.set(self, "status", "Submitted") self.db_set("status", "Submitted")
self.update_status_and_actual_date() self.update_status_and_actual_date()
def on_cancel(self): def on_cancel(self):
self.check_if_last_visit() self.check_if_last_visit()
frappe.db.set(self, "status", "Cancelled") self.db_set("status", "Cancelled")
self.update_status_and_actual_date(cancel=True) self.update_status_and_actual_date(cancel=True)
def on_update(self): def on_update(self):

View File

@@ -206,8 +206,8 @@ class BOM(WebsiteGenerator):
self.manage_default_bom() self.manage_default_bom()
def on_cancel(self): def on_cancel(self):
frappe.db.set(self, "is_active", 0) self.db_set("is_active", 0)
frappe.db.set(self, "is_default", 0) self.db_set("is_default", 0)
# check if used in any other bom # check if used in any other bom
self.validate_bom_links() self.validate_bom_links()
@@ -449,10 +449,10 @@ class BOM(WebsiteGenerator):
not frappe.db.exists(dict(doctype="BOM", docstatus=1, item=self.item, is_default=1)) not frappe.db.exists(dict(doctype="BOM", docstatus=1, item=self.item, is_default=1))
and self.is_active and self.is_active
): ):
frappe.db.set(self, "is_default", 1) self.db_set("is_default", 1)
frappe.db.set_value("Item", self.item, "default_bom", self.name) frappe.db.set_value("Item", self.item, "default_bom", self.name)
else: else:
frappe.db.set(self, "is_default", 0) self.db_set("is_default", 0)
item = frappe.get_doc("Item", self.item) item = frappe.get_doc("Item", self.item)
if item.default_bom == self.name: if item.default_bom == self.name:
frappe.db.set_value("Item", self.item, "default_bom", None) frappe.db.set_value("Item", self.item, "default_bom", None)

View File

@@ -7,6 +7,8 @@ import frappe
from frappe import _, bold from frappe import _, bold
from frappe.model.document import Document from frappe.model.document import Document
from frappe.model.mapper import get_mapped_doc from frappe.model.mapper import get_mapped_doc
from frappe.query_builder import Criterion
from frappe.query_builder.functions import IfNull, Max, Min
from frappe.utils import ( from frappe.utils import (
add_days, add_days,
add_to_date, add_to_date,
@@ -54,6 +56,9 @@ class JobCard(Document):
self.set_onload("job_card_excess_transfer", excess_transfer) self.set_onload("job_card_excess_transfer", excess_transfer)
self.set_onload("work_order_closed", self.is_work_order_closed()) self.set_onload("work_order_closed", self.is_work_order_closed())
def before_validate(self):
self.set_wip_warehouse()
def validate(self): def validate(self):
self.validate_time_logs() self.validate_time_logs()
self.set_status() self.set_status()
@@ -109,43 +114,44 @@ class JobCard(Document):
def get_overlap_for(self, args, check_next_available_slot=False): def get_overlap_for(self, args, check_next_available_slot=False):
production_capacity = 1 production_capacity = 1
jc = frappe.qb.DocType("Job Card")
jctl = frappe.qb.DocType("Job Card Time Log")
time_conditions = [
((jctl.from_time < args.from_time) & (jctl.to_time > args.from_time)),
((jctl.from_time < args.to_time) & (jctl.to_time > args.to_time)),
((jctl.from_time >= args.from_time) & (jctl.to_time <= args.to_time)),
]
if check_next_available_slot:
time_conditions.append(((jctl.from_time >= args.from_time) & (jctl.to_time >= args.to_time)))
query = (
frappe.qb.from_(jctl)
.from_(jc)
.select(jc.name.as_("name"), jctl.to_time)
.where(
(jctl.parent == jc.name)
& (Criterion.any(time_conditions))
& (jctl.name != f"{args.name or 'No Name'}")
& (jc.name != f"{args.parent or 'No Name'}")
& (jc.docstatus < 2)
)
.orderby(jctl.to_time, order=frappe.qb.desc)
)
if self.workstation: if self.workstation:
production_capacity = ( production_capacity = (
frappe.get_cached_value("Workstation", self.workstation, "production_capacity") or 1 frappe.get_cached_value("Workstation", self.workstation, "production_capacity") or 1
) )
validate_overlap_for = " and jc.workstation = %(workstation)s " query = query.where(jc.workstation == self.workstation)
if args.get("employee"): if args.get("employee"):
# override capacity for employee # override capacity for employee
production_capacity = 1 production_capacity = 1
validate_overlap_for = " and jctl.employee = %(employee)s " query = query.where(jctl.employee == args.get("employee"))
extra_cond = "" existing = query.run(as_dict=True)
if check_next_available_slot:
extra_cond = " or (%(from_time)s <= jctl.from_time and %(to_time)s <= jctl.to_time)"
existing = frappe.db.sql(
"""select jc.name as name, jctl.to_time from
`tabJob Card Time Log` jctl, `tabJob Card` jc where jctl.parent = jc.name and
(
(%(from_time)s > jctl.from_time and %(from_time)s < jctl.to_time) or
(%(to_time)s > jctl.from_time and %(to_time)s < jctl.to_time) or
(%(from_time)s <= jctl.from_time and %(to_time)s >= jctl.to_time) {0}
)
and jctl.name != %(name)s and jc.name != %(parent)s and jc.docstatus < 2 {1}
order by jctl.to_time desc""".format(
extra_cond, validate_overlap_for
),
{
"from_time": args.from_time,
"to_time": args.to_time,
"name": args.name or "No Name",
"parent": args.parent or "No Name",
"employee": args.get("employee"),
"workstation": self.workstation,
},
as_dict=True,
)
if existing and production_capacity > len(existing): if existing and production_capacity > len(existing):
return return
@@ -485,18 +491,21 @@ class JobCard(Document):
) )
def update_work_order_data(self, for_quantity, time_in_mins, wo): def update_work_order_data(self, for_quantity, time_in_mins, wo):
time_data = frappe.db.sql( jc = frappe.qb.DocType("Job Card")
""" jctl = frappe.qb.DocType("Job Card Time Log")
SELECT
min(from_time) as start_time, max(to_time) as end_time time_data = (
FROM `tabJob Card` jc, `tabJob Card Time Log` jctl frappe.qb.from_(jc)
WHERE .from_(jctl)
jctl.parent = jc.name and jc.work_order = %s and jc.operation_id = %s .select(Min(jctl.from_time).as_("start_time"), Max(jctl.to_time).as_("end_time"))
and jc.docstatus = 1 and IFNULL(jc.is_corrective_job_card, 0) = 0 .where(
""", (jctl.parent == jc.name)
(self.work_order, self.operation_id), & (jc.work_order == self.work_order)
as_dict=1, & (jc.operation_id == self.operation_id)
) & (jc.docstatus == 1)
& (IfNull(jc.is_corrective_job_card, 0) == 0)
)
).run(as_dict=True)
for data in wo.operations: for data in wo.operations:
if data.get("name") == self.operation_id: if data.get("name") == self.operation_id:
@@ -639,6 +648,12 @@ class JobCard(Document):
if update_status: if update_status:
self.db_set("status", self.status) self.db_set("status", self.status)
def set_wip_warehouse(self):
if not self.wip_warehouse:
self.wip_warehouse = frappe.db.get_single_value(
"Manufacturing Settings", "default_wip_warehouse"
)
def validate_operation_id(self): def validate_operation_id(self):
if ( if (
self.get("operation_id") self.get("operation_id")

View File

@@ -649,23 +649,13 @@ class ProductionPlan(Document):
else: else:
material_request = material_request_map[key] material_request = material_request_map[key]
conversion_factor = 1.0
if (
material_request_type == "Purchase"
and item_doc.purchase_uom
and item_doc.purchase_uom != item_doc.stock_uom
):
conversion_factor = (
get_conversion_factor(item_doc.name, item_doc.purchase_uom).get("conversion_factor") or 1.0
)
# add item # add item
material_request.append( material_request.append(
"items", "items",
{ {
"item_code": item.item_code, "item_code": item.item_code,
"from_warehouse": item.from_warehouse, "from_warehouse": item.from_warehouse,
"qty": item.quantity / conversion_factor, "qty": item.quantity,
"schedule_date": schedule_date, "schedule_date": schedule_date,
"warehouse": item.warehouse, "warehouse": item.warehouse,
"sales_order": item.sales_order, "sales_order": item.sales_order,
@@ -1053,11 +1043,25 @@ def get_material_request_items(
if include_safety_stock: if include_safety_stock:
required_qty += flt(row["safety_stock"]) required_qty += flt(row["safety_stock"])
item_details = frappe.get_cached_value(
"Item", row.item_code, ["purchase_uom", "stock_uom"], as_dict=1
)
conversion_factor = 1.0
if (
row.get("default_material_request_type") == "Purchase"
and item_details.purchase_uom
and item_details.purchase_uom != item_details.stock_uom
):
conversion_factor = (
get_conversion_factor(row.item_code, item_details.purchase_uom).get("conversion_factor") or 1.0
)
if required_qty > 0: if required_qty > 0:
return { return {
"item_code": row.item_code, "item_code": row.item_code,
"item_name": row.item_name, "item_name": row.item_name,
"quantity": required_qty, "quantity": required_qty / conversion_factor,
"required_bom_qty": total_qty, "required_bom_qty": total_qty,
"stock_uom": row.get("stock_uom"), "stock_uom": row.get("stock_uom"),
"warehouse": warehouse "warehouse": warehouse

View File

@@ -826,6 +826,11 @@ class TestProductionPlan(FrappeTestCase):
) )
pln.make_material_request() pln.make_material_request()
for row in pln.mr_items:
self.assertEqual(row.uom, "Nos")
self.assertEqual(row.quantity, 1)
for row in frappe.get_all( for row in frappe.get_all(
"Material Request Item", "Material Request Item",
filters={"production_plan": pln.name}, filters={"production_plan": pln.name},

View File

@@ -146,7 +146,7 @@ class WorkOrder(Document):
frappe.throw(_("Sales Order {0} is {1}").format(self.sales_order, status)) frappe.throw(_("Sales Order {0} is {1}").format(self.sales_order, status))
def set_default_warehouse(self): def set_default_warehouse(self):
if not self.wip_warehouse: if not self.wip_warehouse and not self.skip_transfer:
self.wip_warehouse = frappe.db.get_single_value( self.wip_warehouse = frappe.db.get_single_value(
"Manufacturing Settings", "default_wip_warehouse" "Manufacturing Settings", "default_wip_warehouse"
) )
@@ -373,7 +373,7 @@ class WorkOrder(Document):
def on_cancel(self): def on_cancel(self):
self.validate_cancel() self.validate_cancel()
frappe.db.set(self, "status", "Cancelled") self.db_set("status", "Cancelled")
if self.production_plan and frappe.db.exists( if self.production_plan and frappe.db.exists(
"Production Plan Item Reference", {"parent": self.production_plan} "Production Plan Item Reference", {"parent": self.production_plan}

View File

@@ -100,6 +100,7 @@ def get_default_holiday_list():
def check_if_within_operating_hours(workstation, operation, from_datetime, to_datetime): def check_if_within_operating_hours(workstation, operation, from_datetime, to_datetime):
if from_datetime and to_datetime: if from_datetime and to_datetime:
if not cint( if not cint(
frappe.db.get_value("Manufacturing Settings", "None", "allow_production_on_holidays") frappe.db.get_value("Manufacturing Settings", "None", "allow_production_on_holidays")
): ):

View File

@@ -92,18 +92,26 @@ frappe.ui.form.on("Timesheet", {
frm.fields_dict["time_logs"].grid.toggle_enable("billing_hours", false); frm.fields_dict["time_logs"].grid.toggle_enable("billing_hours", false);
frm.fields_dict["time_logs"].grid.toggle_enable("is_billable", false); frm.fields_dict["time_logs"].grid.toggle_enable("is_billable", false);
} }
let filters = {
"status": "Open"
};
if (frm.doc.customer) {
filters["customer"] = frm.doc.customer;
}
frm.set_query('parent_project', function(doc) {
return {
filters: filters
};
});
frm.trigger('setup_filters'); frm.trigger('setup_filters');
frm.trigger('set_dynamic_field_label'); frm.trigger('set_dynamic_field_label');
}, },
customer: function(frm) { customer: function(frm) {
frm.set_query('parent_project', function(doc) {
return {
filters: {
"customer": doc.customer
}
};
});
frm.set_query('project', 'time_logs', function(doc) { frm.set_query('project', 'time_logs', function(doc) {
return { return {
filters: { filters: {

View File

@@ -49,7 +49,7 @@ def make_custom_fields(update=True):
dict( dict(
fieldname="exempt_from_sales_tax", fieldname="exempt_from_sales_tax",
fieldtype="Check", fieldtype="Check",
insert_after="represents_company", insert_after="dn_required",
label="Is customer exempted from sales tax?", label="Is customer exempted from sales tax?",
) )
], ],

View File

@@ -14,30 +14,35 @@
"naming_series", "naming_series",
"salutation", "salutation",
"customer_name", "customer_name",
"customer_type",
"customer_group",
"column_break0",
"territory",
"gender", "gender",
"default_bank_account",
"tax_id",
"tax_category",
"tax_withholding_category",
"lead_name", "lead_name",
"opportunity_name", "opportunity_name",
"image",
"column_break0",
"customer_group",
"customer_type",
"territory",
"account_manager", "account_manager",
"so_required", "image",
"dn_required", "defaults_tab",
"default_price_list",
"default_bank_account",
"column_break_14",
"default_currency",
"internal_customer_section",
"is_internal_customer", "is_internal_customer",
"represents_company", "represents_company",
"disabled", "column_break_70",
"allowed_to_transact_section",
"companies", "companies",
"currency_and_price_list", "more_info",
"default_currency", "market_segment",
"column_break_14", "industry",
"default_price_list", "customer_pos_id",
"website",
"language",
"column_break_45",
"customer_details",
"dashboard_tab",
"contact_and_address_tab",
"address_contacts", "address_contacts",
"address_html", "address_html",
"column_break1", "column_break1",
@@ -49,34 +54,39 @@
"column_break_26", "column_break_26",
"customer_primary_address", "customer_primary_address",
"primary_address", "primary_address",
"default_receivable_accounts", "tax_tab",
"accounts", "taxation_section",
"tax_id",
"column_break_21",
"tax_category",
"tax_withholding_category",
"accounting_tab",
"credit_limit_section", "credit_limit_section",
"payment_terms", "payment_terms",
"credit_limits", "credit_limits",
"more_info", "default_receivable_accounts",
"customer_details", "accounts",
"column_break_45", "loyalty_points_tab",
"market_segment",
"industry",
"website",
"language",
"is_frozen",
"column_break_38",
"loyalty_program", "loyalty_program",
"column_break_54",
"loyalty_program_tier", "loyalty_program_tier",
"sales_team_section_break", "sales_team_tab",
"default_sales_partner",
"default_commission_rate",
"sales_team_section",
"sales_team", "sales_team",
"customer_pos_id" "sales_team_section",
"default_sales_partner",
"column_break_66",
"default_commission_rate",
"settings_tab",
"so_required",
"dn_required",
"column_break_53",
"is_frozen",
"disabled"
], ],
"fields": [ "fields": [
{ {
"fieldname": "basic_info", "fieldname": "basic_info",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Name and Type",
"oldfieldtype": "Section Break", "oldfieldtype": "Section Break",
"options": "fa fa-user" "options": "fa fa-user"
}, },
@@ -215,12 +225,6 @@
"options": "Company", "options": "Company",
"unique": 1 "unique": 1
}, },
{
"depends_on": "represents_company",
"fieldname": "allowed_to_transact_section",
"fieldtype": "Section Break",
"label": "Allowed To Transact With"
},
{ {
"depends_on": "represents_company", "depends_on": "represents_company",
"fieldname": "companies", "fieldname": "companies",
@@ -228,12 +232,6 @@
"label": "Allowed To Transact With", "label": "Allowed To Transact With",
"options": "Allowed To Transact With" "options": "Allowed To Transact With"
}, },
{
"collapsible": 1,
"fieldname": "currency_and_price_list",
"fieldtype": "Section Break",
"label": "Currency and Price List"
},
{ {
"fieldname": "default_currency", "fieldname": "default_currency",
"fieldtype": "Link", "fieldtype": "Link",
@@ -295,7 +293,7 @@
"description": "Select, to make the customer searchable with these fields", "description": "Select, to make the customer searchable with these fields",
"fieldname": "primary_address_and_contact_detail", "fieldname": "primary_address_and_contact_detail",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Primary Address and Contact Detail" "label": "Primary Address and Contact"
}, },
{ {
"description": "Reselect, if the chosen contact is edited after save", "description": "Reselect, if the chosen contact is edited after save",
@@ -334,20 +332,18 @@
"read_only": 1 "read_only": 1
}, },
{ {
"collapsible": 1,
"fieldname": "default_receivable_accounts", "fieldname": "default_receivable_accounts",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Default Receivable Accounts" "label": "Default Receivable Accounts"
}, },
{ {
"description": "Mention if non-standard receivable account", "description": "Mention if a non-standard receivable account",
"fieldname": "accounts", "fieldname": "accounts",
"fieldtype": "Table", "fieldtype": "Table",
"label": "Accounts", "label": "Receivable Accounts",
"options": "Party Account" "options": "Party Account"
}, },
{ {
"collapsible": 1,
"fieldname": "credit_limit_section", "fieldname": "credit_limit_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Credit Limit and Payment Terms" "label": "Credit Limit and Payment Terms"
@@ -397,12 +393,6 @@
"fieldtype": "Check", "fieldtype": "Check",
"label": "Is Frozen" "label": "Is Frozen"
}, },
{
"collapsible": 1,
"fieldname": "column_break_38",
"fieldtype": "Section Break",
"label": "Loyalty Points"
},
{ {
"fieldname": "loyalty_program", "fieldname": "loyalty_program",
"fieldtype": "Link", "fieldtype": "Link",
@@ -417,15 +407,6 @@
"no_copy": 1, "no_copy": 1,
"read_only": 1 "read_only": 1
}, },
{
"collapsible": 1,
"collapsible_depends_on": "default_sales_partner",
"fieldname": "sales_team_section_break",
"fieldtype": "Section Break",
"label": "Sales Partner and Commission",
"oldfieldtype": "Section Break",
"options": "fa fa-group"
},
{ {
"fieldname": "default_sales_partner", "fieldname": "default_sales_partner",
"fieldtype": "Link", "fieldtype": "Link",
@@ -446,13 +427,12 @@
"collapsible": 1, "collapsible": 1,
"collapsible_depends_on": "sales_team", "collapsible_depends_on": "sales_team",
"fieldname": "sales_team_section", "fieldname": "sales_team_section",
"fieldtype": "Section Break", "fieldtype": "Section Break"
"label": "Sales Team"
}, },
{ {
"fieldname": "sales_team", "fieldname": "sales_team",
"fieldtype": "Table", "fieldtype": "Table",
"label": "Sales Team Details", "label": "Sales Team",
"oldfieldname": "sales_team", "oldfieldname": "sales_team",
"oldfieldtype": "Table", "oldfieldtype": "Table",
"options": "Sales Team" "options": "Sales Team"
@@ -498,6 +478,83 @@
"no_copy": 1, "no_copy": 1,
"options": "Opportunity", "options": "Opportunity",
"print_hide": 1 "print_hide": 1
},
{
"fieldname": "contact_and_address_tab",
"fieldtype": "Tab Break",
"label": "Contact & Address"
},
{
"fieldname": "defaults_tab",
"fieldtype": "Section Break",
"label": "Defaults"
},
{
"fieldname": "settings_tab",
"fieldtype": "Tab Break",
"label": "Settings"
},
{
"collapsible": 1,
"collapsible_depends_on": "default_sales_partner",
"fieldname": "sales_team_tab",
"fieldtype": "Tab Break",
"label": "Sales Team",
"oldfieldtype": "Section Break",
"options": "fa fa-group"
},
{
"fieldname": "column_break_66",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_21",
"fieldtype": "Column Break"
},
{
"fieldname": "dashboard_tab",
"fieldtype": "Tab Break",
"label": "Dashboard",
"show_dashboard": 1
},
{
"fieldname": "column_break_53",
"fieldtype": "Column Break"
},
{
"collapsible": 1,
"fieldname": "loyalty_points_tab",
"fieldtype": "Section Break",
"label": "Loyalty Points"
},
{
"fieldname": "taxation_section",
"fieldtype": "Section Break"
},
{
"fieldname": "accounting_tab",
"fieldtype": "Tab Break",
"label": "Accounting"
},
{
"fieldname": "tax_tab",
"fieldtype": "Tab Break",
"label": "Tax"
},
{
"collapsible": 1,
"collapsible_depends_on": "is_internal_customer",
"fieldname": "internal_customer_section",
"fieldtype": "Section Break",
"label": "Internal Customer"
},
{
"fieldname": "column_break_70",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_54",
"fieldtype": "Column Break"
} }
], ],
"icon": "fa fa-user", "icon": "fa fa-user",
@@ -511,7 +568,7 @@
"link_fieldname": "party" "link_fieldname": "party"
} }
], ],
"modified": "2022-04-16 20:32:34.000304", "modified": "2022-11-08 15:52:34.462657",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Customer", "name": "Customer",

View File

@@ -294,7 +294,7 @@ class Customer(TransactionBase):
def after_rename(self, olddn, newdn, merge=False): def after_rename(self, olddn, newdn, merge=False):
if frappe.defaults.get_global_default("cust_master_name") == "Customer Name": if frappe.defaults.get_global_default("cust_master_name") == "Customer Name":
frappe.db.set(self, "customer_name", newdn) self.db_set("customer_name", newdn)
def set_loyalty_program(self): def set_loyalty_program(self):
if self.loyalty_program: if self.loyalty_program:

View File

@@ -12,7 +12,7 @@
], ],
"fields": [ "fields": [
{ {
"columns": 4, "columns": 3,
"fieldname": "credit_limit", "fieldname": "credit_limit",
"fieldtype": "Currency", "fieldtype": "Currency",
"in_list_view": 1, "in_list_view": 1,
@@ -31,6 +31,7 @@
"options": "Company" "options": "Company"
}, },
{ {
"columns": 3,
"default": "0", "default": "0",
"fieldname": "bypass_credit_limit_check", "fieldname": "bypass_credit_limit_check",
"fieldtype": "Check", "fieldtype": "Check",
@@ -40,7 +41,7 @@
], ],
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2019-12-31 15:43:05.822328", "modified": "2022-11-08 15:19:13.927194",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Customer Credit Limit", "name": "Customer Credit Limit",
@@ -48,5 +49,6 @@
"permissions": [], "permissions": [],
"quick_entry": 1, "quick_entry": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC" "sort_order": "DESC",
"states": []
} }

View File

@@ -87,13 +87,13 @@ class InstallationNote(TransactionBase):
frappe.throw(_("Please pull items from Delivery Note")) frappe.throw(_("Please pull items from Delivery Note"))
def on_update(self): def on_update(self):
frappe.db.set(self, "status", "Draft") self.db_set("status", "Draft")
def on_submit(self): def on_submit(self):
self.validate_serial_no() self.validate_serial_no()
self.update_prevdoc_status() self.update_prevdoc_status()
frappe.db.set(self, "status", "Submitted") self.db_set("status", "Submitted")
def on_cancel(self): def on_cancel(self):
self.update_prevdoc_status() self.update_prevdoc_status()
frappe.db.set(self, "status", "Cancelled") self.db_set("status", "Cancelled")

View File

@@ -119,10 +119,10 @@ class Quotation(SellingController):
if not (self.is_fully_ordered() or self.is_partially_ordered()): if not (self.is_fully_ordered() or self.is_partially_ordered()):
get_lost_reasons = frappe.get_list("Quotation Lost Reason", fields=["name"]) get_lost_reasons = frappe.get_list("Quotation Lost Reason", fields=["name"])
lost_reasons_lst = [reason.get("name") for reason in get_lost_reasons] lost_reasons_lst = [reason.get("name") for reason in get_lost_reasons]
frappe.db.set(self, "status", "Lost") self.db_set("status", "Lost")
if detailed_reason: if detailed_reason:
frappe.db.set(self, "order_lost_reason", detailed_reason) self.db_set("order_lost_reason", detailed_reason)
for reason in lost_reasons_list: for reason in lost_reasons_list:
if reason.get("lost_reason") in lost_reasons_lst: if reason.get("lost_reason") in lost_reasons_lst:
@@ -247,7 +247,7 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False):
"Quotation": {"doctype": "Sales Order", "validation": {"docstatus": ["=", 1]}}, "Quotation": {"doctype": "Sales Order", "validation": {"docstatus": ["=", 1]}},
"Quotation Item": { "Quotation Item": {
"doctype": "Sales Order Item", "doctype": "Sales Order Item",
"field_map": {"parent": "prevdoc_docname"}, "field_map": {"parent": "prevdoc_docname", "name": "quotation_item"},
"postprocess": update_item, "postprocess": update_item,
"condition": lambda doc: doc.qty > 0, "condition": lambda doc: doc.qty > 0,
}, },

View File

@@ -193,6 +193,9 @@ class SalesOrder(SellingController):
{"Quotation": {"ref_dn_field": "prevdoc_docname", "compare_fields": [["company", "="]]}} {"Quotation": {"ref_dn_field": "prevdoc_docname", "compare_fields": [["company", "="]]}}
) )
if cint(frappe.db.get_single_value("Selling Settings", "maintain_same_sales_rate")):
self.validate_rate_with_reference_doc([["Quotation", "prev_docname", "quotation_item"]])
def update_enquiry_status(self, prevdoc, flag): def update_enquiry_status(self, prevdoc, flag):
enq = frappe.db.sql( enq = frappe.db.sql(
"select t2.prevdoc_docname from `tabQuotation` t1, `tabQuotation Item` t2 where t2.parent = t1.name and t1.name=%s", "select t2.prevdoc_docname from `tabQuotation` t1, `tabQuotation Item` t2 where t2.parent = t1.name and t1.name=%s",
@@ -246,7 +249,7 @@ class SalesOrder(SellingController):
self.update_project() self.update_project()
self.update_prevdoc_status("cancel") self.update_prevdoc_status("cancel")
frappe.db.set(self, "status", "Cancelled") self.db_set("status", "Cancelled")
self.update_blanket_order() self.update_blanket_order()

View File

@@ -70,6 +70,7 @@
"warehouse", "warehouse",
"target_warehouse", "target_warehouse",
"prevdoc_docname", "prevdoc_docname",
"quotation_item",
"col_break4", "col_break4",
"against_blanket_order", "against_blanket_order",
"blanket_order", "blanket_order",
@@ -838,12 +839,20 @@
"label": "Purchase Order Item", "label": "Purchase Order Item",
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
},
{
"fieldname": "quotation_item",
"fieldtype": "Data",
"hidden": 1,
"label": "quotation_item",
"no_copy": 1,
"read_only": 1
} }
], ],
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2022-10-26 16:05:02.712705", "modified": "2022-11-10 18:20:30.137455",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Sales Order Item", "name": "Sales Order Item",

View File

@@ -207,15 +207,14 @@ class Company(NestedSet):
frappe.local.flags.ignore_root_company_validation = True frappe.local.flags.ignore_root_company_validation = True
create_charts(self.name, self.chart_of_accounts, self.existing_company) create_charts(self.name, self.chart_of_accounts, self.existing_company)
frappe.db.set( self.db_set(
self,
"default_receivable_account", "default_receivable_account",
frappe.db.get_value( frappe.db.get_value(
"Account", {"company": self.name, "account_type": "Receivable", "is_group": 0} "Account", {"company": self.name, "account_type": "Receivable", "is_group": 0}
), ),
) )
frappe.db.set(
self, self.db_set(
"default_payable_account", "default_payable_account",
frappe.db.get_value( frappe.db.get_value(
"Account", {"company": self.name, "account_type": "Payable", "is_group": 0} "Account", {"company": self.name, "account_type": "Payable", "is_group": 0}
@@ -491,12 +490,12 @@ class Company(NestedSet):
cc_doc.flags.ignore_mandatory = True cc_doc.flags.ignore_mandatory = True
cc_doc.insert() cc_doc.insert()
frappe.db.set(self, "cost_center", _("Main") + " - " + self.abbr) self.db_set("cost_center", _("Main") + " - " + self.abbr)
frappe.db.set(self, "round_off_cost_center", _("Main") + " - " + self.abbr) self.db_set("round_off_cost_center", _("Main") + " - " + self.abbr)
frappe.db.set(self, "depreciation_cost_center", _("Main") + " - " + self.abbr) self.db_set("depreciation_cost_center", _("Main") + " - " + self.abbr)
def after_rename(self, olddn, newdn, merge=False): def after_rename(self, olddn, newdn, merge=False):
frappe.db.set(self, "company_name", newdn) self.db_set("company_name", newdn)
frappe.db.sql( frappe.db.sql(
"""update `tabDefaultValue` set defvalue=%s """update `tabDefaultValue` set defvalue=%s

View File

@@ -120,7 +120,6 @@ class MaterialRequest(BuyingController):
self.title = _("{0} Request for {1}").format(self.material_request_type, items)[:100] self.title = _("{0} Request for {1}").format(self.material_request_type, items)[:100]
def on_submit(self): def on_submit(self):
# frappe.db.set(self, 'status', 'Submitted')
self.update_requested_qty() self.update_requested_qty()
self.update_requested_qty_in_production_plan() self.update_requested_qty_in_production_plan()
if self.material_request_type == "Purchase": if self.material_request_type == "Purchase":

View File

@@ -216,7 +216,7 @@ class TestMaterialRequest(FrappeTestCase):
po.load_from_db() po.load_from_db()
mr.update_status("Stopped") mr.update_status("Stopped")
self.assertRaises(frappe.InvalidStatusError, po.submit) self.assertRaises(frappe.InvalidStatusError, po.submit)
frappe.db.set(po, "docstatus", 1) po.db_set("docstatus", 1)
self.assertRaises(frappe.InvalidStatusError, po.cancel) self.assertRaises(frappe.InvalidStatusError, po.cancel)
# resubmit and check for per complete # resubmit and check for per complete

View File

@@ -6,7 +6,9 @@ import frappe
from frappe import _, throw from frappe import _, throw
from frappe.desk.notifications import clear_doctype_notifications from frappe.desk.notifications import clear_doctype_notifications
from frappe.model.mapper import get_mapped_doc from frappe.model.mapper import get_mapped_doc
from frappe.query_builder.functions import CombineDatetime
from frappe.utils import cint, flt, getdate, nowdate from frappe.utils import cint, flt, getdate, nowdate
from pypika import functions as fn
import erpnext import erpnext
from erpnext.accounts.utils import get_account_currency from erpnext.accounts.utils import get_account_currency
@@ -750,48 +752,38 @@ class PurchaseReceipt(BuyingController):
def update_billing_status(self, update_modified=True): def update_billing_status(self, update_modified=True):
updated_pr = [self.name] updated_pr = [self.name]
po_details = []
for d in self.get("items"): for d in self.get("items"):
if d.get("purchase_invoice") and d.get("purchase_invoice_item"): if d.get("purchase_invoice") and d.get("purchase_invoice_item"):
d.db_set("billed_amt", d.amount, update_modified=update_modified) d.db_set("billed_amt", d.amount, update_modified=update_modified)
elif d.purchase_order_item: elif d.purchase_order_item:
updated_pr += update_billed_amount_based_on_po(d.purchase_order_item, update_modified) po_details.append(d.purchase_order_item)
if po_details:
updated_pr += update_billed_amount_based_on_po(po_details, update_modified)
for pr in set(updated_pr): for pr in set(updated_pr):
pr_doc = self if (pr == self.name) else frappe.get_doc("Purchase Receipt", pr) pr_doc = self if (pr == self.name) else frappe.get_cached_doc("Purchase Receipt", pr)
update_billing_percentage(pr_doc, update_modified=update_modified) update_billing_percentage(pr_doc, update_modified=update_modified)
self.load_from_db() self.load_from_db()
def update_billed_amount_based_on_po(po_detail, update_modified=True): def update_billed_amount_based_on_po(po_details, update_modified=True):
# Billed against Sales Order directly po_billed_amt_details = get_billed_amount_against_po(po_details)
billed_against_po = frappe.db.sql(
"""select sum(amount) from `tabPurchase Invoice Item`
where po_detail=%s and (pr_detail is null or pr_detail = '') and docstatus=1""",
po_detail,
)
billed_against_po = billed_against_po and billed_against_po[0][0] or 0
# Get all Purchase Receipt Item rows against the Purchase Order Item row # Get all Purchase Receipt Item rows against the Purchase Order Items
pr_details = frappe.db.sql( pr_details = get_purchase_receipts_against_po_details(po_details)
"""select pr_item.name, pr_item.amount, pr_item.parent
from `tabPurchase Receipt Item` pr_item, `tabPurchase Receipt` pr pr_items = [pr_detail.name for pr_detail in pr_details]
where pr.name=pr_item.parent and pr_item.purchase_order_item=%s pr_items_billed_amount = get_billed_amount_against_pr(pr_items)
and pr.docstatus=1 and pr.is_return = 0
order by pr.posting_date asc, pr.posting_time asc, pr.name asc""",
po_detail,
as_dict=1,
)
updated_pr = [] updated_pr = []
for pr_item in pr_details: for pr_item in pr_details:
billed_against_po = flt(po_billed_amt_details.get(pr_item.purchase_order_item))
# Get billed amount directly against Purchase Receipt # Get billed amount directly against Purchase Receipt
billed_amt_agianst_pr = frappe.db.sql( billed_amt_agianst_pr = flt(pr_items_billed_amount.get(pr_item.name, 0))
"""select sum(amount) from `tabPurchase Invoice Item`
where pr_detail=%s and docstatus=1""",
pr_item.name,
)
billed_amt_agianst_pr = billed_amt_agianst_pr and billed_amt_agianst_pr[0][0] or 0
# Distribute billed amount directly against PO between PRs based on FIFO # Distribute billed amount directly against PO between PRs based on FIFO
if billed_against_po and billed_amt_agianst_pr < pr_item.amount: if billed_against_po and billed_amt_agianst_pr < pr_item.amount:
@@ -803,19 +795,90 @@ def update_billed_amount_based_on_po(po_detail, update_modified=True):
billed_amt_agianst_pr += billed_against_po billed_amt_agianst_pr += billed_against_po
billed_against_po = 0 billed_against_po = 0
frappe.db.set_value( po_billed_amt_details[pr_item.purchase_order_item] = billed_against_po
"Purchase Receipt Item",
pr_item.name,
"billed_amt",
billed_amt_agianst_pr,
update_modified=update_modified,
)
updated_pr.append(pr_item.parent) if pr_item.billed_amt != billed_amt_agianst_pr:
frappe.db.set_value(
"Purchase Receipt Item",
pr_item.name,
"billed_amt",
billed_amt_agianst_pr,
update_modified=update_modified,
)
updated_pr.append(pr_item.parent)
return updated_pr return updated_pr
def get_purchase_receipts_against_po_details(po_details):
# Get Purchase Receipts against Purchase Order Items
purchase_receipt = frappe.qb.DocType("Purchase Receipt")
purchase_receipt_item = frappe.qb.DocType("Purchase Receipt Item")
query = (
frappe.qb.from_(purchase_receipt)
.inner_join(purchase_receipt_item)
.on(purchase_receipt.name == purchase_receipt_item.parent)
.select(
purchase_receipt_item.name,
purchase_receipt_item.parent,
purchase_receipt_item.amount,
purchase_receipt_item.billed_amt,
purchase_receipt_item.purchase_order_item,
)
.where(
(purchase_receipt_item.purchase_order_item.isin(po_details))
& (purchase_receipt.docstatus == 1)
& (purchase_receipt.is_return == 0)
)
.orderby(CombineDatetime(purchase_receipt.posting_date, purchase_receipt.posting_time))
.orderby(purchase_receipt.name)
)
return query.run(as_dict=True)
def get_billed_amount_against_pr(pr_items):
# Get billed amount directly against Purchase Receipt
if not pr_items:
return {}
purchase_invoice_item = frappe.qb.DocType("Purchase Invoice Item")
query = (
frappe.qb.from_(purchase_invoice_item)
.select(fn.Sum(purchase_invoice_item.amount).as_("billed_amt"), purchase_invoice_item.pr_detail)
.where((purchase_invoice_item.pr_detail.isin(pr_items)) & (purchase_invoice_item.docstatus == 1))
.groupby(purchase_invoice_item.pr_detail)
).run(as_dict=1)
return {d.pr_detail: flt(d.billed_amt) for d in query}
def get_billed_amount_against_po(po_items):
# Get billed amount directly against Purchase Order
if not po_items:
return {}
purchase_invoice_item = frappe.qb.DocType("Purchase Invoice Item")
query = (
frappe.qb.from_(purchase_invoice_item)
.select(fn.Sum(purchase_invoice_item.amount).as_("billed_amt"), purchase_invoice_item.po_detail)
.where(
(purchase_invoice_item.po_detail.isin(po_items))
& (purchase_invoice_item.docstatus == 1)
& (purchase_invoice_item.pr_detail.isnull())
)
.groupby(purchase_invoice_item.po_detail)
).run(as_dict=1)
return {d.po_detail: flt(d.billed_amt) for d in query}
def update_billing_percentage(pr_doc, update_modified=True): def update_billing_percentage(pr_doc, update_modified=True):
# Reload as billed amount was set in db directly # Reload as billed amount was set in db directly
pr_doc.load_from_db() pr_doc.load_from_db()

View File

@@ -330,6 +330,9 @@ def get_basic_details(args, item, overwrite_warehouse=True):
else: else:
args.uom = item.stock_uom args.uom = item.stock_uom
# Set stock UOM in args, so that it can be used while fetching item price
args.stock_uom = item.stock_uom
if args.get("batch_no") and item.name != frappe.get_cached_value( if args.get("batch_no") and item.name != frappe.get_cached_value(
"Batch", args.get("batch_no"), "item" "Batch", args.get("batch_no"), "item"
): ):

View File

@@ -58,7 +58,6 @@ class SubcontractingReceipt(SubcontractingController):
def before_validate(self): def before_validate(self):
super(SubcontractingReceipt, self).before_validate() super(SubcontractingReceipt, self).before_validate()
self.set_items_bom() self.set_items_bom()
self.set_received_qty()
self.set_items_cost_center() self.set_items_cost_center()
self.set_items_expense_account() self.set_items_expense_account()
@@ -213,10 +212,6 @@ class SubcontractingReceipt(SubcontractingController):
"bom", "bom",
) )
def set_received_qty(self):
for item in self.items:
item.received_qty = flt(item.qty) + flt(item.rejected_qty)
def set_items_cost_center(self): def set_items_cost_center(self):
if self.company: if self.company:
cost_center = frappe.get_cached_value("Company", self.company, "cost_center") cost_center = frappe.get_cached_value("Company", self.company, "cost_center")

View File

@@ -4,6 +4,9 @@ from frappe import _
def get_data(): def get_data():
return { return {
"fieldname": "subcontracting_receipt_no", "fieldname": "subcontracting_receipt_no",
"non_standard_fieldnames": {
"Subcontracting Receipt": "return_against",
},
"internal_links": { "internal_links": {
"Subcontracting Order": ["items", "subcontracting_order"], "Subcontracting Order": ["items", "subcontracting_order"],
"Project": ["items", "project"], "Project": ["items", "project"],
@@ -11,5 +14,6 @@ def get_data():
}, },
"transactions": [ "transactions": [
{"label": _("Reference"), "items": ["Subcontracting Order", "Quality Inspection", "Project"]}, {"label": _("Reference"), "items": ["Subcontracting Order", "Quality Inspection", "Project"]},
{"label": _("Returns"), "items": ["Subcontracting Receipt"]},
], ],
} }

View File

@@ -515,17 +515,18 @@ class TestSubcontractingReceipt(FrappeTestCase):
scr.items[0].rejected_qty = 3 scr.items[0].rejected_qty = 3
scr.save() scr.save()
# consumed_qty should be ((received_qty) * (transfered_qty / qty)) = ((5 + 3) * (20 / 10)) = 16 # consumed_qty should be (accepted_qty * (transfered_qty / qty)) = (5 * (20 / 10)) = 10
self.assertEqual(scr.supplied_items[0].consumed_qty, 16) self.assertEqual(scr.supplied_items[0].consumed_qty, 10)
# Set Backflush Based On as "BOM" # Set Backflush Based On as "BOM"
set_backflush_based_on("BOM") set_backflush_based_on("BOM")
scr.items[0].qty = 6 # Accepted Qty
scr.items[0].rejected_qty = 4 scr.items[0].rejected_qty = 4
scr.save() scr.save()
# consumed_qty should be ((received_qty) * (qty_consumed_per_unit)) = ((5 + 4) * (1)) = 9 # consumed_qty should be (accepted_qty * qty_consumed_per_unit) = (6 * 1) = 6
self.assertEqual(scr.supplied_items[0].consumed_qty, 9) self.assertEqual(scr.supplied_items[0].consumed_qty, 6)
def make_return_subcontracting_receipt(**args): def make_return_subcontracting_receipt(**args):

View File

@@ -35,7 +35,7 @@ class WarrantyClaim(TransactionBase):
lst1 = ",".join(x[0] for x in lst) lst1 = ",".join(x[0] for x in lst)
frappe.throw(_("Cancel Material Visit {0} before cancelling this Warranty Claim").format(lst1)) frappe.throw(_("Cancel Material Visit {0} before cancelling this Warranty Claim").format(lst1))
else: else:
frappe.db.set(self, "status", "Cancelled") self.db_set("status", "Cancelled")
def on_update(self): def on_update(self):
pass pass