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

chore: release v14
This commit is contained in:
Deepesh Garg
2022-12-20 19:30:05 +05:30
committed by GitHub
44 changed files with 512 additions and 187 deletions

View File

@@ -485,6 +485,10 @@ def set_default_accounts(company):
"default_payable_account": frappe.db.get_value( "default_payable_account": frappe.db.get_value(
"Account", {"company": company.name, "account_type": "Payable", "is_group": 0} "Account", {"company": company.name, "account_type": "Payable", "is_group": 0}
), ),
"default_provisional_account": frappe.db.get_value(
"Account",
{"company": company.name, "account_type": "Service Received But Not Billed", "is_group": 0},
),
} }
) )

View File

@@ -305,6 +305,7 @@
"fieldname": "source_exchange_rate", "fieldname": "source_exchange_rate",
"fieldtype": "Float", "fieldtype": "Float",
"label": "Exchange Rate", "label": "Exchange Rate",
"precision": "9",
"print_hide": 1, "print_hide": 1,
"reqd": 1 "reqd": 1
}, },
@@ -334,6 +335,7 @@
"fieldname": "target_exchange_rate", "fieldname": "target_exchange_rate",
"fieldtype": "Float", "fieldtype": "Float",
"label": "Exchange Rate", "label": "Exchange Rate",
"precision": "9",
"print_hide": 1, "print_hide": 1,
"reqd": 1 "reqd": 1
}, },
@@ -731,7 +733,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2022-02-23 20:08:39.559814", "modified": "2022-12-08 16:25:43.824051",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Payment Entry", "name": "Payment Entry",

View File

@@ -684,35 +684,34 @@ class PaymentEntry(AccountsController):
) )
def validate_payment_against_negative_invoice(self): def validate_payment_against_negative_invoice(self):
if (self.payment_type == "Pay" and self.party_type == "Customer") or ( if (self.payment_type != "Pay" or self.party_type != "Customer") and (
self.payment_type == "Receive" and self.party_type == "Supplier" self.payment_type != "Receive" or self.party_type != "Supplier"
): ):
return
total_negative_outstanding = sum( total_negative_outstanding = sum(
abs(flt(d.outstanding_amount)) for d in self.get("references") if flt(d.outstanding_amount) < 0 abs(flt(d.outstanding_amount)) for d in self.get("references") if flt(d.outstanding_amount) < 0
)
paid_amount = self.paid_amount if self.payment_type == "Receive" else self.received_amount
additional_charges = sum(flt(d.amount) for d in self.deductions)
if not total_negative_outstanding:
if self.party_type == "Customer":
msg = _("Cannot pay to Customer without any negative outstanding invoice")
else:
msg = _("Cannot receive from Supplier without any negative outstanding invoice")
frappe.throw(msg, InvalidPaymentEntry)
elif paid_amount - additional_charges > total_negative_outstanding:
frappe.throw(
_("Paid Amount cannot be greater than total negative outstanding amount {0}").format(
total_negative_outstanding
),
InvalidPaymentEntry,
) )
paid_amount = self.paid_amount if self.payment_type == "Receive" else self.received_amount
additional_charges = sum([flt(d.amount) for d in self.deductions])
if not total_negative_outstanding:
frappe.throw(
_("Cannot {0} {1} {2} without any negative outstanding invoice").format(
_(self.payment_type),
(_("to") if self.party_type == "Customer" else _("from")),
self.party_type,
),
InvalidPaymentEntry,
)
elif paid_amount - additional_charges > total_negative_outstanding:
frappe.throw(
_("Paid Amount cannot be greater than total negative outstanding amount {0}").format(
total_negative_outstanding
),
InvalidPaymentEntry,
)
def set_title(self): def set_title(self):
if frappe.flags.in_import and self.title: if frappe.flags.in_import and self.title:
# do not set title dynamically if title exists during data import. # do not set title dynamically if title exists during data import.
@@ -1188,6 +1187,7 @@ def get_outstanding_reference_documents(args):
ple = qb.DocType("Payment Ledger Entry") ple = qb.DocType("Payment Ledger Entry")
common_filter = [] common_filter = []
accounting_dimensions_filter = []
posting_and_due_date = [] posting_and_due_date = []
# confirm that Supplier is not blocked # confirm that Supplier is not blocked
@@ -1217,7 +1217,7 @@ def get_outstanding_reference_documents(args):
# Add cost center condition # Add cost center condition
if args.get("cost_center"): if args.get("cost_center"):
condition += " and cost_center='%s'" % args.get("cost_center") condition += " and cost_center='%s'" % args.get("cost_center")
common_filter.append(ple.cost_center == args.get("cost_center")) accounting_dimensions_filter.append(ple.cost_center == args.get("cost_center"))
date_fields_dict = { date_fields_dict = {
"posting_date": ["from_posting_date", "to_posting_date"], "posting_date": ["from_posting_date", "to_posting_date"],
@@ -1243,6 +1243,7 @@ def get_outstanding_reference_documents(args):
posting_date=posting_and_due_date, posting_date=posting_and_due_date,
min_outstanding=args.get("outstanding_amt_greater_than"), min_outstanding=args.get("outstanding_amt_greater_than"),
max_outstanding=args.get("outstanding_amt_less_than"), max_outstanding=args.get("outstanding_amt_less_than"),
accounting_dimensions=accounting_dimensions_filter,
) )
outstanding_invoices = split_invoices_based_on_payment_terms(outstanding_invoices) outstanding_invoices = split_invoices_based_on_payment_terms(outstanding_invoices)
@@ -1639,7 +1640,7 @@ def get_payment_entry(
): ):
reference_doc = None reference_doc = None
doc = frappe.get_doc(dt, dn) doc = frappe.get_doc(dt, dn)
if dt in ("Sales Order", "Purchase Order") and flt(doc.per_billed, 2) > 0: if dt in ("Sales Order", "Purchase Order") and flt(doc.per_billed, 2) >= 99.99:
frappe.throw(_("Can only make payment against unbilled {0}").format(dt)) frappe.throw(_("Can only make payment against unbilled {0}").format(dt))
if not party_type: if not party_type:

View File

@@ -23,6 +23,7 @@ class PaymentReconciliation(Document):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(PaymentReconciliation, self).__init__(*args, **kwargs) super(PaymentReconciliation, self).__init__(*args, **kwargs)
self.common_filter_conditions = [] self.common_filter_conditions = []
self.accounting_dimension_filter_conditions = []
self.ple_posting_date_filter = [] self.ple_posting_date_filter = []
@frappe.whitelist() @frappe.whitelist()
@@ -193,6 +194,7 @@ class PaymentReconciliation(Document):
posting_date=self.ple_posting_date_filter, posting_date=self.ple_posting_date_filter,
min_outstanding=self.minimum_invoice_amount if self.minimum_invoice_amount else None, min_outstanding=self.minimum_invoice_amount if self.minimum_invoice_amount else None,
max_outstanding=self.maximum_invoice_amount if self.maximum_invoice_amount else None, max_outstanding=self.maximum_invoice_amount if self.maximum_invoice_amount else None,
accounting_dimensions=self.accounting_dimension_filter_conditions,
) )
if self.invoice_limit: if self.invoice_limit:
@@ -381,7 +383,7 @@ class PaymentReconciliation(Document):
self.common_filter_conditions.append(ple.company == self.company) self.common_filter_conditions.append(ple.company == self.company)
if self.get("cost_center") and (get_invoices or get_return_invoices): if self.get("cost_center") and (get_invoices or get_return_invoices):
self.common_filter_conditions.append(ple.cost_center == self.cost_center) self.accounting_dimension_filter_conditions.append(ple.cost_center == self.cost_center)
if get_invoices: if get_invoices:
if self.from_invoice_date: if self.from_invoice_date:

View File

@@ -8,6 +8,8 @@ from frappe import qb
from frappe.tests.utils import FrappeTestCase from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_days, nowdate from frappe.utils import add_days, nowdate
from erpnext import get_default_cost_center
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.party import get_party_account from erpnext.accounts.party import get_party_account
@@ -20,6 +22,7 @@ class TestPaymentReconciliation(FrappeTestCase):
self.create_item() self.create_item()
self.create_customer() self.create_customer()
self.create_account() self.create_account()
self.create_cost_center()
self.clear_old_entries() self.clear_old_entries()
def tearDown(self): def tearDown(self):
@@ -216,6 +219,22 @@ class TestPaymentReconciliation(FrappeTestCase):
) )
return je return je
def create_cost_center(self):
# Setup cost center
cc_name = "Sub"
self.main_cc = frappe.get_doc("Cost Center", get_default_cost_center(self.company))
cc_exists = frappe.db.get_list("Cost Center", filters={"cost_center_name": cc_name})
if cc_exists:
self.sub_cc = frappe.get_doc("Cost Center", cc_exists[0].name)
else:
sub_cc = frappe.new_doc("Cost Center")
sub_cc.cost_center_name = "Sub"
sub_cc.parent_cost_center = self.main_cc.parent_cost_center
sub_cc.company = self.main_cc.company
self.sub_cc = sub_cc.save()
def test_filter_min_max(self): def test_filter_min_max(self):
# check filter condition minimum and maximum amount # check filter condition minimum and maximum amount
self.create_sales_invoice(qty=1, rate=300) self.create_sales_invoice(qty=1, rate=300)
@@ -578,3 +597,24 @@ class TestPaymentReconciliation(FrappeTestCase):
self.assertEqual(len(pr.payments), 1) self.assertEqual(len(pr.payments), 1)
self.assertEqual(pr.payments[0].amount, amount) self.assertEqual(pr.payments[0].amount, amount)
self.assertEqual(pr.payments[0].currency, "EUR") self.assertEqual(pr.payments[0].currency, "EUR")
def test_differing_cost_center_on_invoice_and_payment(self):
"""
Cost Center filter should not affect outstanding amount calculation
"""
si = self.create_sales_invoice(qty=1, rate=100, do_not_submit=True)
si.cost_center = self.main_cc.name
si.submit()
pr = get_payment_entry(si.doctype, si.name)
pr.cost_center = self.sub_cc.name
pr = pr.save().submit()
pr = self.create_payment_reconciliation()
pr.cost_center = self.main_cc.name
pr.get_unreconciled_entries()
# check PR tool output
self.assertEqual(len(pr.get("invoices")), 0)
self.assertEqual(len(pr.get("payments")), 0)

View File

@@ -42,7 +42,7 @@ frappe.ui.form.on("Payment Request", "refresh", function(frm) {
}); });
} }
if(!frm.doc.payment_gateway_account && frm.doc.status == "Initiated") { if((!frm.doc.payment_gateway_account || frm.doc.payment_request_type == "Outward") && frm.doc.status == "Initiated") {
frm.add_custom_button(__('Create Payment Entry'), function(){ frm.add_custom_button(__('Create Payment Entry'), function(){
frappe.call({ frappe.call({
method: "erpnext.accounts.doctype.payment_request.payment_request.make_payment_entry", method: "erpnext.accounts.doctype.payment_request.payment_request.make_payment_entry",

View File

@@ -254,6 +254,7 @@ class PaymentRequest(Document):
payment_entry.update( payment_entry.update(
{ {
"mode_of_payment": self.mode_of_payment,
"reference_no": self.name, "reference_no": self.name,
"reference_date": nowdate(), "reference_date": nowdate(),
"remarks": "Payment Entry against {0} {1} via Payment Request {2}".format( "remarks": "Payment Entry against {0} {1} via Payment Request {2}".format(
@@ -403,25 +404,22 @@ def make_payment_request(**args):
else "" else ""
) )
existing_payment_request = None draft_payment_request = frappe.db.get_value(
if args.order_type == "Shopping Cart": "Payment Request",
existing_payment_request = frappe.db.get_value( {"reference_doctype": args.dt, "reference_name": args.dn, "docstatus": 0},
"Payment Request", )
{"reference_doctype": args.dt, "reference_name": args.dn, "docstatus": ("!=", 2)},
)
if existing_payment_request: existing_payment_request_amount = get_existing_payment_request_amount(args.dt, args.dn)
if existing_payment_request_amount:
grand_total -= existing_payment_request_amount
if draft_payment_request:
frappe.db.set_value( frappe.db.set_value(
"Payment Request", existing_payment_request, "grand_total", grand_total, update_modified=False "Payment Request", draft_payment_request, "grand_total", grand_total, update_modified=False
) )
pr = frappe.get_doc("Payment Request", existing_payment_request) pr = frappe.get_doc("Payment Request", draft_payment_request)
else: else:
if args.order_type != "Shopping Cart":
existing_payment_request_amount = get_existing_payment_request_amount(args.dt, args.dn)
if existing_payment_request_amount:
grand_total -= existing_payment_request_amount
pr = frappe.new_doc("Payment Request") pr = frappe.new_doc("Payment Request")
pr.update( pr.update(
{ {

View File

@@ -64,12 +64,13 @@
"tax_withholding_net_total", "tax_withholding_net_total",
"base_tax_withholding_net_total", "base_tax_withholding_net_total",
"taxes_section", "taxes_section",
"tax_category",
"taxes_and_charges", "taxes_and_charges",
"column_break_58", "column_break_58",
"tax_category",
"column_break_49",
"shipping_rule", "shipping_rule",
"column_break_49",
"incoterm", "incoterm",
"named_place",
"section_break_51", "section_break_51",
"taxes", "taxes",
"totals", "totals",
@@ -1541,13 +1542,19 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "Incoterm", "label": "Incoterm",
"options": "Incoterm" "options": "Incoterm"
},
{
"depends_on": "incoterm",
"fieldname": "named_place",
"fieldtype": "Data",
"label": "Named Place"
} }
], ],
"icon": "fa fa-file-text", "icon": "fa fa-file-text",
"idx": 204, "idx": 204,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2022-11-27 16:28:45.559785", "modified": "2022-12-14 18:37:38.142688",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Purchase Invoice", "name": "Purchase Invoice",

View File

@@ -61,12 +61,13 @@
"total", "total",
"net_total", "net_total",
"taxes_section", "taxes_section",
"tax_category",
"taxes_and_charges", "taxes_and_charges",
"column_break_38", "column_break_38",
"shipping_rule", "shipping_rule",
"incoterm",
"column_break_55", "column_break_55",
"tax_category", "incoterm",
"named_place",
"section_break_40", "section_break_40",
"taxes", "taxes",
"section_break_43", "section_break_43",
@@ -2105,6 +2106,12 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "Incoterm", "label": "Incoterm",
"options": "Incoterm" "options": "Incoterm"
},
{
"depends_on": "incoterm",
"fieldname": "named_place",
"fieldtype": "Data",
"label": "Named Place"
} }
], ],
"icon": "fa fa-file-text", "icon": "fa fa-file-text",
@@ -2117,7 +2124,7 @@
"link_fieldname": "consolidated_invoice" "link_fieldname": "consolidated_invoice"
} }
], ],
"modified": "2022-12-05 16:18:14.532114", "modified": "2022-12-12 18:34:33.409895",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Invoice", "name": "Sales Invoice",

View File

@@ -121,12 +121,24 @@ def get_party_tax_withholding_details(inv, tax_withholding_category=None):
else: else:
tax_row = get_tax_row_for_tcs(inv, tax_details, tax_amount, tax_deducted) tax_row = get_tax_row_for_tcs(inv, tax_details, tax_amount, tax_deducted)
cost_center = get_cost_center(inv)
tax_row.update({"cost_center": cost_center})
if inv.doctype == "Purchase Invoice": if inv.doctype == "Purchase Invoice":
return tax_row, tax_deducted_on_advances, voucher_wise_amount return tax_row, tax_deducted_on_advances, voucher_wise_amount
else: else:
return tax_row return tax_row
def get_cost_center(inv):
cost_center = frappe.get_cached_value("Company", inv.company, "cost_center")
if len(inv.get("taxes", [])) > 0:
cost_center = inv.get("taxes")[0].cost_center
return cost_center
def get_tax_withholding_details(tax_withholding_category, posting_date, company): def get_tax_withholding_details(tax_withholding_category, posting_date, company):
tax_withholding = frappe.get_doc("Tax Withholding Category", tax_withholding_category) tax_withholding = frappe.get_doc("Tax Withholding Category", tax_withholding_category)

View File

@@ -8,6 +8,7 @@ from frappe.utils import cint, cstr
from erpnext.accounts.report.financial_statements import ( from erpnext.accounts.report.financial_statements import (
get_columns, get_columns,
get_cost_centers_with_children,
get_data, get_data,
get_filtered_list_for_consolidated_report, get_filtered_list_for_consolidated_report,
get_period_list, get_period_list,
@@ -160,10 +161,11 @@ def get_account_type_based_data(company, account_type, period_list, accumulated_
total = 0 total = 0
for period in period_list: for period in period_list:
start_date = get_start_date(period, accumulated_values, company) start_date = get_start_date(period, accumulated_values, company)
filters.start_date = start_date
filters.end_date = period["to_date"]
filters.account_type = account_type
amount = get_account_type_based_gl_data( amount = get_account_type_based_gl_data(company, filters)
company, start_date, period["to_date"], account_type, filters
)
if amount and account_type == "Depreciation": if amount and account_type == "Depreciation":
amount *= -1 amount *= -1
@@ -175,7 +177,7 @@ def get_account_type_based_data(company, account_type, period_list, accumulated_
return data return data
def get_account_type_based_gl_data(company, start_date, end_date, account_type, filters=None): def get_account_type_based_gl_data(company, filters=None):
cond = "" cond = ""
filters = frappe._dict(filters or {}) filters = frappe._dict(filters or {})
@@ -191,17 +193,21 @@ def get_account_type_based_gl_data(company, start_date, end_date, account_type,
frappe.db.escape(cstr(filters.finance_book)) frappe.db.escape(cstr(filters.finance_book))
) )
if filters.get("cost_center"):
filters.cost_center = get_cost_centers_with_children(filters.cost_center)
cond += " and cost_center in %(cost_center)s"
gl_sum = frappe.db.sql_list( gl_sum = frappe.db.sql_list(
""" """
select sum(credit) - sum(debit) select sum(credit) - sum(debit)
from `tabGL Entry` from `tabGL Entry`
where company=%s and posting_date >= %s and posting_date <= %s where company=%(company)s and posting_date >= %(start_date)s and posting_date <= %(end_date)s
and voucher_type != 'Period Closing Voucher' and voucher_type != 'Period Closing Voucher'
and account in ( SELECT name FROM tabAccount WHERE account_type = %s) {cond} and account in ( SELECT name FROM tabAccount WHERE account_type = %(account_type)s) {cond}
""".format( """.format(
cond=cond cond=cond
), ),
(company, start_date, end_date, account_type), filters,
) )
return gl_sum[0] if gl_sum and gl_sum[0] else 0 return gl_sum[0] if gl_sum and gl_sum[0] else 0

View File

@@ -268,10 +268,12 @@ def get_cash_flow_data(fiscal_year, companies, filters):
def get_account_type_based_data(account_type, companies, fiscal_year, filters): def get_account_type_based_data(account_type, companies, fiscal_year, filters):
data = {} data = {}
total = 0 total = 0
filters.account_type = account_type
filters.start_date = fiscal_year.year_start_date
filters.end_date = fiscal_year.year_end_date
for company in companies: for company in companies:
amount = get_account_type_based_gl_data( amount = get_account_type_based_gl_data(company, filters)
company, fiscal_year.year_start_date, fiscal_year.year_end_date, account_type, filters
)
if amount and account_type == "Depreciation": if amount and account_type == "Depreciation":
amount *= -1 amount *= -1

View File

@@ -836,6 +836,7 @@ def get_outstanding_invoices(
posting_date=None, posting_date=None,
min_outstanding=None, min_outstanding=None,
max_outstanding=None, max_outstanding=None,
accounting_dimensions=None,
): ):
ple = qb.DocType("Payment Ledger Entry") ple = qb.DocType("Payment Ledger Entry")
@@ -866,6 +867,7 @@ def get_outstanding_invoices(
min_outstanding=min_outstanding, min_outstanding=min_outstanding,
max_outstanding=max_outstanding, max_outstanding=max_outstanding,
get_invoices=True, get_invoices=True,
accounting_dimensions=accounting_dimensions or [],
) )
for d in invoice_list: for d in invoice_list:
@@ -1615,6 +1617,7 @@ class QueryPaymentLedger(object):
.where(ple.delinked == 0) .where(ple.delinked == 0)
.where(Criterion.all(filter_on_voucher_no)) .where(Criterion.all(filter_on_voucher_no))
.where(Criterion.all(self.common_filter)) .where(Criterion.all(self.common_filter))
.where(Criterion.all(self.dimensions_filter))
.where(Criterion.all(self.voucher_posting_date)) .where(Criterion.all(self.voucher_posting_date))
.groupby(ple.voucher_type, ple.voucher_no, ple.party_type, ple.party) .groupby(ple.voucher_type, ple.voucher_no, ple.party_type, ple.party)
) )
@@ -1702,6 +1705,7 @@ class QueryPaymentLedger(object):
max_outstanding=None, max_outstanding=None,
get_payments=False, get_payments=False,
get_invoices=False, get_invoices=False,
accounting_dimensions=None,
): ):
""" """
Fetch voucher amount and outstanding amount from Payment Ledger using Database CTE Fetch voucher amount and outstanding amount from Payment Ledger using Database CTE
@@ -1717,6 +1721,7 @@ class QueryPaymentLedger(object):
self.reset() self.reset()
self.vouchers = vouchers self.vouchers = vouchers
self.common_filter = common_filter or [] self.common_filter = common_filter or []
self.dimensions_filter = accounting_dimensions or []
self.voucher_posting_date = posting_date or [] self.voucher_posting_date = posting_date or []
self.min_outstanding = min_outstanding self.min_outstanding = min_outstanding
self.max_outstanding = max_outstanding self.max_outstanding = max_outstanding

View File

@@ -235,11 +235,11 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e
cur_frm.add_custom_button(__('Purchase Invoice'), cur_frm.add_custom_button(__('Purchase Invoice'),
this.make_purchase_invoice, __('Create')); this.make_purchase_invoice, __('Create'));
if(flt(doc.per_billed)==0 && doc.status != "Delivered") { if(flt(doc.per_billed) < 100 && doc.status != "Delivered") {
cur_frm.add_custom_button(__('Payment'), cur_frm.cscript.make_payment_entry, __('Create')); cur_frm.add_custom_button(__('Payment'), cur_frm.cscript.make_payment_entry, __('Create'));
} }
if(flt(doc.per_billed)==0) { if(flt(doc.per_billed) < 100) {
this.frm.add_custom_button(__('Payment Request'), this.frm.add_custom_button(__('Payment Request'),
function() { me.make_payment_request() }, __('Create')); function() { me.make_payment_request() }, __('Create'));
} }

View File

@@ -62,12 +62,13 @@
"set_reserve_warehouse", "set_reserve_warehouse",
"supplied_items", "supplied_items",
"taxes_section", "taxes_section",
"tax_category",
"taxes_and_charges", "taxes_and_charges",
"column_break_53", "column_break_53",
"tax_category",
"column_break_50",
"shipping_rule", "shipping_rule",
"column_break_50",
"incoterm", "incoterm",
"named_place",
"section_break_52", "section_break_52",
"taxes", "taxes",
"totals", "totals",
@@ -1256,13 +1257,19 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "Incoterm", "label": "Incoterm",
"options": "Incoterm" "options": "Incoterm"
},
{
"depends_on": "incoterm",
"fieldname": "named_place",
"fieldtype": "Data",
"label": "Named Place"
} }
], ],
"icon": "fa fa-file-text", "icon": "fa fa-file-text",
"idx": 105, "idx": 105,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2022-11-17 17:28:07.729943", "modified": "2022-12-12 18:36:37.455134",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Purchase Order", "name": "Purchase Order",

View File

@@ -57,44 +57,96 @@ frappe.ui.form.on("Request for Quotation",{
}); });
}, __("Tools")); }, __("Tools"));
frm.add_custom_button(__('Download PDF'), () => { frm.add_custom_button(
var suppliers = []; __("Download PDF"),
const fields = [{ () => {
fieldtype: 'Link', frappe.prompt(
label: __('Select a Supplier'), [
fieldname: 'supplier', {
options: 'Supplier', fieldtype: "Link",
reqd: 1, label: "Select a Supplier",
get_query: () => { fieldname: "supplier",
return { options: "Supplier",
filters: [ reqd: 1,
["Supplier", "name", "in", frm.doc.suppliers.map((row) => {return row.supplier;})] default: frm.doc.suppliers?.length == 1 ? frm.doc.suppliers[0].supplier : "",
] get_query: () => {
} return {
} filters: [
}]; [
"Supplier",
frappe.prompt(fields, data => { "name",
var child = locals[cdt][cdn] "in",
frm.doc.suppliers.map((row) => {
var w = window.open( return row.supplier;
frappe.urllib.get_full_url("/api/method/erpnext.buying.doctype.request_for_quotation.request_for_quotation.get_pdf?" }),
+"doctype="+encodeURIComponent(frm.doc.doctype) ],
+"&name="+encodeURIComponent(frm.doc.name) ],
+"&supplier="+encodeURIComponent(data.supplier) };
+"&no_letterhead=0")); },
if(!w) { },
frappe.msgprint(__("Please enable pop-ups")); return; {
} fieldtype: "Section Break",
label: "Print Settings",
fieldname: "print_settings",
collapsible: 1,
},
{
fieldtype: "Link",
label: "Print Format",
fieldname: "print_format",
options: "Print Format",
placeholder: "Standard",
get_query: () => {
return {
filters: {
doc_type: "Request for Quotation",
},
};
},
},
{
fieldtype: "Link",
label: "Language",
fieldname: "language",
options: "Language",
default: frappe.boot.lang,
},
{
fieldtype: "Link",
label: "Letter Head",
fieldname: "letter_head",
options: "Letter Head",
default: frm.doc.letter_head,
},
],
(data) => {
var w = window.open(
frappe.urllib.get_full_url(
"/api/method/erpnext.buying.doctype.request_for_quotation.request_for_quotation.get_pdf?" +
new URLSearchParams({
doctype: frm.doc.doctype,
name: frm.doc.name,
supplier: data.supplier,
print_format: data.print_format || "Standard",
language: data.language || frappe.boot.lang,
letter_head: data.letter_head || frm.doc.letter_head || "",
}).toString()
)
);
if (!w) {
frappe.msgprint(__("Please enable pop-ups"));
return;
}
},
"Download PDF for Supplier",
"Download"
);
}, },
'Download PDF for Supplier', __("Tools")
'Download'); );
},
__("Tools"));
frm.page.set_inner_btn_group_as_primary(__('Create')); frm.page.set_inner_btn_group_as_primary(__("Create"));
} }
}, },
make_supplier_quotation: function(frm) { make_supplier_quotation: function(frm) {

View File

@@ -389,10 +389,17 @@ def create_rfq_items(sq_doc, supplier, data):
@frappe.whitelist() @frappe.whitelist()
def get_pdf(doctype, name, supplier): def get_pdf(doctype, name, supplier, print_format=None, language=None, letter_head=None):
doc = get_rfq_doc(doctype, name, supplier) # permissions get checked in `download_pdf`
if doc: if doc := get_rfq_doc(doctype, name, supplier):
download_pdf(doctype, name, doc=doc) download_pdf(
doctype,
name,
print_format,
doc=doc,
language=language,
letter_head=letter_head or None,
)
def get_rfq_doc(doctype, name, supplier): def get_rfq_doc(doctype, name, supplier):

View File

@@ -40,12 +40,13 @@
"total", "total",
"net_total", "net_total",
"taxes_section", "taxes_section",
"tax_category",
"taxes_and_charges", "taxes_and_charges",
"column_break_34", "column_break_34",
"tax_category",
"column_break_36",
"shipping_rule", "shipping_rule",
"column_break_36",
"incoterm", "incoterm",
"named_place",
"section_break_38", "section_break_38",
"taxes", "taxes",
"totals", "totals",
@@ -830,6 +831,12 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "Incoterm", "label": "Incoterm",
"options": "Incoterm" "options": "Incoterm"
},
{
"depends_on": "incoterm",
"fieldname": "named_place",
"fieldtype": "Data",
"label": "Named Place"
} }
], ],
"icon": "fa fa-shopping-cart", "icon": "fa fa-shopping-cart",
@@ -837,7 +844,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2022-11-17 17:27:32.179686", "modified": "2022-12-12 18:35:39.740974",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Supplier Quotation", "name": "Supplier Quotation",

View File

@@ -347,16 +347,21 @@ class StatusUpdater(Document):
) )
def warn_about_bypassing_with_role(self, item, qty_or_amount, role): def warn_about_bypassing_with_role(self, item, qty_or_amount, role):
action = _("Over Receipt/Delivery") if qty_or_amount == "qty" else _("Overbilling") if qty_or_amount == "qty":
msg = _("Over Receipt/Delivery of {0} {1} ignored for item {2} because you have {3} role.")
else:
msg = _("Overbilling of {0} {1} ignored for item {2} because you have {3} role.")
msg = _("{0} of {1} {2} ignored for item {3} because you have {4} role.").format( frappe.msgprint(
action, msg.format(
_(item["target_ref_field"].title()), _(item["target_ref_field"].title()),
frappe.bold(item["reduce_by"]), frappe.bold(item["reduce_by"]),
frappe.bold(item.get("item_code")), frappe.bold(item.get("item_code")),
role, role,
),
indicator="orange",
alert=True,
) )
frappe.msgprint(msg, indicator="orange", alert=True)
def update_qty(self, update_modified=True): def update_qty(self, update_modified=True):
"""Updates qty or amount at row level """Updates qty or amount at row level

View File

@@ -102,7 +102,7 @@
} }
], ],
"links": [], "links": [],
"modified": "2021-06-30 13:09:14.228756", "modified": "2022-12-15 11:11:02.131986",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "CRM", "module": "CRM",
"name": "Appointment", "name": "Appointment",
@@ -121,16 +121,6 @@
"share": 1, "share": 1,
"write": 1 "write": 1
}, },
{
"create": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Guest",
"share": 1
},
{ {
"create": 1, "create": 1,
"delete": 1, "delete": 1,
@@ -170,5 +160,6 @@
"quick_entry": 1, "quick_entry": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@@ -6,7 +6,9 @@ from collections import Counter
import frappe import frappe
from frappe import _ from frappe import _
from frappe.desk.form.assign_to import add as add_assignment
from frappe.model.document import Document from frappe.model.document import Document
from frappe.share import add_docshare
from frappe.utils import get_url, getdate, now from frappe.utils import get_url, getdate, now
from frappe.utils.verified_command import get_signed_params from frappe.utils.verified_command import get_signed_params
@@ -130,21 +132,18 @@ class Appointment(Document):
self.party = lead.name self.party = lead.name
def auto_assign(self): def auto_assign(self):
from frappe.desk.form.assign_to import add as add_assignemnt
existing_assignee = self.get_assignee_from_latest_opportunity() existing_assignee = self.get_assignee_from_latest_opportunity()
if existing_assignee: if existing_assignee:
# If the latest opportunity is assigned to someone # If the latest opportunity is assigned to someone
# Assign the appointment to the same # Assign the appointment to the same
add_assignemnt({"doctype": self.doctype, "name": self.name, "assign_to": [existing_assignee]}) self.assign_agent(existing_assignee)
return return
if self._assign: if self._assign:
return return
available_agents = _get_agents_sorted_by_asc_workload(getdate(self.scheduled_time)) available_agents = _get_agents_sorted_by_asc_workload(getdate(self.scheduled_time))
for agent in available_agents: for agent in available_agents:
if _check_agent_availability(agent, self.scheduled_time): if _check_agent_availability(agent, self.scheduled_time):
agent = agent[0] self.assign_agent(agent[0])
add_assignemnt({"doctype": self.doctype, "name": self.name, "assign_to": [agent]})
break break
def get_assignee_from_latest_opportunity(self): def get_assignee_from_latest_opportunity(self):
@@ -199,9 +198,15 @@ class Appointment(Document):
params = {"email": self.customer_email, "appointment": self.name} params = {"email": self.customer_email, "appointment": self.name}
return get_url(verify_route + "?" + get_signed_params(params)) return get_url(verify_route + "?" + get_signed_params(params))
def assign_agent(self, agent):
if not frappe.has_permission(doc=self, user=agent):
add_docshare(self.doctype, self.name, agent, flags={"ignore_share_permission": True})
add_assignment({"doctype": self.doctype, "name": self.name, "assign_to": [agent]})
def _get_agents_sorted_by_asc_workload(date): def _get_agents_sorted_by_asc_workload(date):
appointments = frappe.db.get_list("Appointment", fields="*") appointments = frappe.get_all("Appointment", fields="*")
agent_list = _get_agent_list_as_strings() agent_list = _get_agent_list_as_strings()
if not appointments: if not appointments:
return agent_list return agent_list
@@ -226,7 +231,7 @@ def _get_agent_list_as_strings():
def _check_agent_availability(agent_email, scheduled_time): def _check_agent_availability(agent_email, scheduled_time):
appointemnts_at_scheduled_time = frappe.get_list( appointemnts_at_scheduled_time = frappe.get_all(
"Appointment", filters={"scheduled_time": scheduled_time} "Appointment", filters={"scheduled_time": scheduled_time}
) )
for appointment in appointemnts_at_scheduled_time: for appointment in appointemnts_at_scheduled_time:

View File

@@ -1,4 +1,5 @@
{ {
"actions": [],
"creation": "2019-08-27 10:56:48.309824", "creation": "2019-08-27 10:56:48.309824",
"doctype": "DocType", "doctype": "DocType",
"editable_grid": 1, "editable_grid": 1,
@@ -101,7 +102,8 @@
} }
], ],
"issingle": 1, "issingle": 1,
"modified": "2019-11-26 12:14:17.669366", "links": [],
"modified": "2022-12-15 11:10:13.517742",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "CRM", "module": "CRM",
"name": "Appointment Booking Settings", "name": "Appointment Booking Settings",
@@ -117,13 +119,6 @@
"share": 1, "share": 1,
"write": 1 "write": 1
}, },
{
"email": 1,
"print": 1,
"read": 1,
"role": "Guest",
"share": 1
},
{ {
"create": 1, "create": 1,
"email": 1, "email": 1,
@@ -147,5 +142,6 @@
"quick_entry": 1, "quick_entry": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@@ -427,6 +427,7 @@ scheduler_events = {
"erpnext.loan_management.doctype.process_loan_security_shortfall.process_loan_security_shortfall.create_process_loan_security_shortfall", "erpnext.loan_management.doctype.process_loan_security_shortfall.process_loan_security_shortfall.create_process_loan_security_shortfall",
"erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual.process_loan_interest_accrual_for_term_loans", "erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual.process_loan_interest_accrual_for_term_loans",
"erpnext.crm.utils.open_leads_opportunities_based_on_todays_event", "erpnext.crm.utils.open_leads_opportunities_based_on_todays_event",
"erpnext.stock.doctype.stock_entry.stock_entry.audit_incorrect_valuation_entries",
], ],
"monthly_long": [ "monthly_long": [
"erpnext.accounts.deferred_revenue.process_deferred_accounting", "erpnext.accounts.deferred_revenue.process_deferred_accounting",

View File

@@ -1154,6 +1154,36 @@ class TestWorkOrder(FrappeTestCase):
except frappe.MandatoryError: except frappe.MandatoryError:
self.fail("Batch generation causing failing in Work Order") self.fail("Batch generation causing failing in Work Order")
@change_settings("Manufacturing Settings", {"make_serial_no_batch_from_work_order": 1})
def test_auto_serial_no_creation(self):
from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
fg_item = frappe.generate_hash(length=20)
child_item = frappe.generate_hash(length=20)
bom_tree = {fg_item: {child_item: {}}}
create_nested_bom(bom_tree, prefix="")
item = frappe.get_doc("Item", fg_item)
item.has_serial_no = 1
item.serial_no_series = f"{item.name}.#####"
item.save()
try:
wo_order = make_wo_order_test_record(item=fg_item, qty=2, skip_transfer=True)
serial_nos = wo_order.serial_no
stock_entry = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 10))
stock_entry.set_work_order_details()
stock_entry.set_serial_no_batch_for_finished_good()
for row in stock_entry.items:
if row.item_code == fg_item:
self.assertTrue(row.serial_no)
self.assertEqual(sorted(get_serial_nos(row.serial_no)), sorted(get_serial_nos(serial_nos)))
except frappe.MandatoryError:
self.fail("Batch generation causing failing in Work Order")
@change_settings( @change_settings(
"Manufacturing Settings", "Manufacturing Settings",
{"backflush_raw_materials_based_on": "Material Transferred for Manufacture"}, {"backflush_raw_materials_based_on": "Material Transferred for Manufacture"},

View File

@@ -298,7 +298,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
} }
make_payment_request() { make_payment_request() {
var me = this; let me = this;
const payment_request_type = (in_list(['Sales Order', 'Sales Invoice'], this.frm.doc.doctype)) const payment_request_type = (in_list(['Sales Order', 'Sales Invoice'], this.frm.doc.doctype))
? "Inward" : "Outward"; ? "Inward" : "Outward";
@@ -314,7 +314,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
}, },
callback: function(r) { callback: function(r) {
if(!r.exc){ if(!r.exc){
var doc = frappe.model.sync(r.message); frappe.model.sync(r.message);
frappe.set_route("Form", r.message.doctype, r.message.name); frappe.set_route("Form", r.message.doctype, r.message.name);
} }
} }

View File

@@ -43,12 +43,13 @@
"total", "total",
"net_total", "net_total",
"taxes_section", "taxes_section",
"tax_category",
"taxes_and_charges", "taxes_and_charges",
"column_break_36", "column_break_36",
"tax_category",
"column_break_34",
"shipping_rule", "shipping_rule",
"column_break_34",
"incoterm", "incoterm",
"named_place",
"section_break_36", "section_break_36",
"taxes", "taxes",
"section_break_39", "section_break_39",
@@ -1059,13 +1060,19 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "Incoterm", "label": "Incoterm",
"options": "Incoterm" "options": "Incoterm"
},
{
"depends_on": "incoterm",
"fieldname": "named_place",
"fieldtype": "Data",
"label": "Named Place"
} }
], ],
"icon": "fa fa-shopping-cart", "icon": "fa fa-shopping-cart",
"idx": 82, "idx": 82,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2022-11-17 17:20:54.984348", "modified": "2022-12-12 18:32:28.671332",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Quotation", "name": "Quotation",

View File

@@ -58,12 +58,13 @@
"total", "total",
"net_total", "net_total",
"taxes_section", "taxes_section",
"tax_category",
"taxes_and_charges", "taxes_and_charges",
"column_break_38", "column_break_38",
"tax_category",
"column_break_49",
"shipping_rule", "shipping_rule",
"column_break_49",
"incoterm", "incoterm",
"named_place",
"section_break_40", "section_break_40",
"taxes", "taxes",
"section_break_43", "section_break_43",
@@ -1630,13 +1631,19 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "Incoterm", "label": "Incoterm",
"options": "Incoterm" "options": "Incoterm"
},
{
"depends_on": "incoterm",
"fieldname": "named_place",
"fieldtype": "Data",
"label": "Named Place"
} }
], ],
"icon": "fa fa-file-text", "icon": "fa fa-file-text",
"idx": 105, "idx": 105,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2022-11-17 17:22:00.413878", "modified": "2022-12-12 18:34:00.681780",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Sales Order", "name": "Sales Order",

View File

@@ -70,9 +70,6 @@ class Company(NestedSet):
self.abbr = self.abbr.strip() self.abbr = self.abbr.strip()
# if self.get('__islocal') and len(self.abbr) > 5:
# frappe.throw(_("Abbreviation cannot have more than 5 characters"))
if not self.abbr.strip(): if not self.abbr.strip():
frappe.throw(_("Abbreviation is mandatory")) frappe.throw(_("Abbreviation is mandatory"))

View File

@@ -204,7 +204,7 @@ class TransactionDeletionRecord(Document):
@frappe.whitelist() @frappe.whitelist()
def get_doctypes_to_be_ignored(): def get_doctypes_to_be_ignored():
doctypes_to_be_ignored_list = [ doctypes_to_be_ignored = [
"Account", "Account",
"Cost Center", "Cost Center",
"Warehouse", "Warehouse",
@@ -223,4 +223,7 @@ def get_doctypes_to_be_ignored():
"Customer", "Customer",
"Supplier", "Supplier",
] ]
return doctypes_to_be_ignored_list
doctypes_to_be_ignored.extend(frappe.get_hooks("company_data_to_be_ignored") or [])
return doctypes_to_be_ignored

View File

@@ -57,12 +57,13 @@
"total", "total",
"net_total", "net_total",
"taxes_section", "taxes_section",
"tax_category",
"taxes_and_charges", "taxes_and_charges",
"column_break_43", "column_break_43",
"tax_category",
"column_break_39",
"shipping_rule", "shipping_rule",
"column_break_39",
"incoterm", "incoterm",
"named_place",
"section_break_41", "section_break_41",
"taxes", "taxes",
"section_break_44", "section_break_44",
@@ -1388,13 +1389,19 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "Incoterm", "label": "Incoterm",
"options": "Incoterm" "options": "Incoterm"
},
{
"depends_on": "incoterm",
"fieldname": "named_place",
"fieldtype": "Data",
"label": "Named Place"
} }
], ],
"icon": "fa fa-truck", "icon": "fa fa-truck",
"idx": 146, "idx": 146,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2022-11-17 17:22:42.860790", "modified": "2022-12-12 18:38:53.067799",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Delivery Note", "name": "Delivery Note",

View File

@@ -192,13 +192,13 @@ class PickList(Document):
if item_map.get(key): if item_map.get(key):
item_map[key].qty += item.qty item_map[key].qty += item.qty
item_map[key].stock_qty += item.stock_qty item_map[key].stock_qty += flt(item.stock_qty, item.precision("stock_qty"))
else: else:
item_map[key] = item item_map[key] = item
# maintain count of each item (useful to limit get query) # maintain count of each item (useful to limit get query)
self.item_count_map.setdefault(item_code, 0) self.item_count_map.setdefault(item_code, 0)
self.item_count_map[item_code] += item.stock_qty self.item_count_map[item_code] += flt(item.stock_qty, item.precision("stock_qty"))
return item_map.values() return item_map.values()

View File

@@ -58,12 +58,13 @@
"total", "total",
"net_total", "net_total",
"taxes_charges_section", "taxes_charges_section",
"tax_category",
"taxes_and_charges", "taxes_and_charges",
"shipping_col", "shipping_col",
"tax_category",
"column_break_53",
"shipping_rule", "shipping_rule",
"column_break_53",
"incoterm", "incoterm",
"named_place",
"taxes_section", "taxes_section",
"taxes", "taxes",
"totals", "totals",
@@ -1225,13 +1226,19 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "Incoterm", "label": "Incoterm",
"options": "Incoterm" "options": "Incoterm"
},
{
"depends_on": "incoterm",
"fieldname": "named_place",
"fieldtype": "Data",
"label": "Named Place"
} }
], ],
"icon": "fa fa-truck", "icon": "fa fa-truck",
"idx": 261, "idx": 261,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2022-11-17 17:29:30.067536", "modified": "2022-12-12 18:40:32.447752",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Purchase Receipt", "name": "Purchase Receipt",

View File

@@ -766,13 +766,13 @@ def get_delivery_note_serial_no(item_code, qty, delivery_note):
@frappe.whitelist() @frappe.whitelist()
def auto_fetch_serial_number( def auto_fetch_serial_number(
qty: float, qty: int,
item_code: str, item_code: str,
warehouse: str, warehouse: str,
posting_date: Optional[str] = None, posting_date: Optional[str] = None,
batch_nos: Optional[Union[str, List[str]]] = None, batch_nos: Optional[Union[str, List[str]]] = None,
for_doctype: Optional[str] = None, for_doctype: Optional[str] = None,
exclude_sr_nos: Optional[List[str]] = None, exclude_sr_nos=None,
) -> List[str]: ) -> List[str]:
filters = frappe._dict({"item_code": item_code, "warehouse": warehouse}) filters = frappe._dict({"item_code": item_code, "warehouse": warehouse})

View File

@@ -4,12 +4,24 @@
import json import json
from collections import defaultdict from collections import defaultdict
from typing import Dict
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.mapper import get_mapped_doc from frappe.model.mapper import get_mapped_doc
from frappe.query_builder.functions import Sum from frappe.query_builder.functions import Sum
from frappe.utils import cint, comma_or, cstr, flt, format_time, formatdate, getdate, nowdate from frappe.utils import (
add_days,
cint,
comma_or,
cstr,
flt,
format_time,
formatdate,
getdate,
nowdate,
today,
)
import erpnext import erpnext
from erpnext.accounts.general_ledger import process_gl_map from erpnext.accounts.general_ledger import process_gl_map
@@ -2239,16 +2251,16 @@ class StockEntry(StockController):
d.qty -= process_loss_dict[d.item_code][1] d.qty -= process_loss_dict[d.item_code][1]
def set_serial_no_batch_for_finished_good(self): def set_serial_no_batch_for_finished_good(self):
serial_nos = "" serial_nos = []
if self.pro_doc.serial_no: if self.pro_doc.serial_no:
serial_nos = self.get_serial_nos_for_fg() serial_nos = self.get_serial_nos_for_fg() or []
for row in self.items: for row in self.items:
if row.is_finished_item and row.item_code == self.pro_doc.production_item: if row.is_finished_item and row.item_code == self.pro_doc.production_item:
if serial_nos: if serial_nos:
row.serial_no = "\n".join(serial_nos[0 : cint(row.qty)]) row.serial_no = "\n".join(serial_nos[0 : cint(row.qty)])
def get_serial_nos_for_fg(self, args): def get_serial_nos_for_fg(self):
fields = [ fields = [
"`tabStock Entry`.`name`", "`tabStock Entry`.`name`",
"`tabStock Entry Detail`.`qty`", "`tabStock Entry Detail`.`qty`",
@@ -2264,9 +2276,7 @@ class StockEntry(StockController):
] ]
stock_entries = frappe.get_all("Stock Entry", fields=fields, filters=filters) stock_entries = frappe.get_all("Stock Entry", fields=fields, filters=filters)
return self.get_available_serial_nos(stock_entries)
if self.pro_doc.serial_no:
return self.get_available_serial_nos(stock_entries)
def get_available_serial_nos(self, stock_entries): def get_available_serial_nos(self, stock_entries):
used_serial_nos = [] used_serial_nos = []
@@ -2705,3 +2715,62 @@ def get_stock_entry_data(work_order):
) )
.orderby(stock_entry.creation, stock_entry_detail.item_code, stock_entry_detail.idx) .orderby(stock_entry.creation, stock_entry_detail.item_code, stock_entry_detail.idx)
).run(as_dict=1) ).run(as_dict=1)
def audit_incorrect_valuation_entries():
# Audit of stock transfer entries having incorrect valuation
from erpnext.controllers.stock_controller import create_repost_item_valuation_entry
stock_entries = get_incorrect_stock_entries()
for stock_entry, values in stock_entries.items():
reposting_data = frappe._dict(
{
"posting_date": values.posting_date,
"posting_time": values.posting_time,
"voucher_type": "Stock Entry",
"voucher_no": stock_entry,
"company": values.company,
}
)
create_repost_item_valuation_entry(reposting_data)
def get_incorrect_stock_entries() -> Dict:
stock_entry = frappe.qb.DocType("Stock Entry")
stock_ledger_entry = frappe.qb.DocType("Stock Ledger Entry")
transfer_purposes = [
"Material Transfer",
"Material Transfer for Manufacture",
"Send to Subcontractor",
]
query = (
frappe.qb.from_(stock_entry)
.inner_join(stock_ledger_entry)
.on(stock_entry.name == stock_ledger_entry.voucher_no)
.select(
stock_entry.name,
stock_entry.company,
stock_entry.posting_date,
stock_entry.posting_time,
Sum(stock_ledger_entry.stock_value_difference).as_("stock_value"),
)
.where(
(stock_entry.docstatus == 1)
& (stock_entry.purpose.isin(transfer_purposes))
& (stock_ledger_entry.modified > add_days(today(), -2))
)
.groupby(stock_ledger_entry.voucher_detail_no)
.having(Sum(stock_ledger_entry.stock_value_difference) != 0)
)
data = query.run(as_dict=True)
stock_entries = {}
for row in data:
if abs(row.stock_value) > 0.1 and row.name not in stock_entries:
stock_entries.setdefault(row.name, row)
return stock_entries

View File

@@ -5,7 +5,7 @@
import frappe import frappe
from frappe.permissions import add_user_permission, remove_user_permission from frappe.permissions import add_user_permission, remove_user_permission
from frappe.tests.utils import FrappeTestCase, change_settings from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import add_days, flt, nowdate, nowtime, today from frappe.utils import add_days, flt, now, nowdate, nowtime, today
from erpnext.accounts.doctype.account.test_account import get_inventory_account from erpnext.accounts.doctype.account.test_account import get_inventory_account
from erpnext.stock.doctype.item.test_item import ( from erpnext.stock.doctype.item.test_item import (
@@ -17,6 +17,8 @@ from erpnext.stock.doctype.item.test_item import (
from erpnext.stock.doctype.serial_no.serial_no import * # noqa from erpnext.stock.doctype.serial_no.serial_no import * # noqa
from erpnext.stock.doctype.stock_entry.stock_entry import ( from erpnext.stock.doctype.stock_entry.stock_entry import (
FinishedGoodError, FinishedGoodError,
audit_incorrect_valuation_entries,
get_incorrect_stock_entries,
move_sample_to_retention_warehouse, move_sample_to_retention_warehouse,
) )
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
@@ -1614,6 +1616,44 @@ class TestStockEntry(FrappeTestCase):
self.assertRaises(BatchExpiredError, se.save) self.assertRaises(BatchExpiredError, se.save)
def test_audit_incorrect_stock_entries(self):
item_code = "Test Incorrect Valuation Rate Item - 001"
create_item(item_code=item_code, is_stock_item=1)
make_stock_entry(
item_code=item_code,
purpose="Material Receipt",
posting_date=add_days(nowdate(), -10),
qty=2,
rate=500,
to_warehouse="_Test Warehouse - _TC",
)
transfer_entry = make_stock_entry(
item_code=item_code,
purpose="Material Transfer",
qty=2,
rate=500,
from_warehouse="_Test Warehouse - _TC",
to_warehouse="_Test Warehouse 1 - _TC",
)
sle_name = frappe.db.get_value(
"Stock Ledger Entry", {"voucher_no": transfer_entry.name, "actual_qty": (">", 0)}, "name"
)
frappe.db.set_value(
"Stock Ledger Entry", sle_name, {"modified": add_days(now(), -1), "stock_value_difference": 10}
)
stock_entries = get_incorrect_stock_entries()
self.assertTrue(transfer_entry.name in stock_entries)
audit_incorrect_valuation_entries()
stock_entries = get_incorrect_stock_entries()
self.assertFalse(transfer_entry.name in stock_entries)
def make_serialized_item(**args): def make_serialized_item(**args):
args = frappe._dict(args) args = frappe._dict(args)

View File

@@ -715,8 +715,8 @@ def get_itemwise_batch(warehouse, posting_date, company, item_code=None):
def get_stock_balance_for( def get_stock_balance_for(
item_code: str, item_code: str,
warehouse: str, warehouse: str,
posting_date: str, posting_date,
posting_time: str, posting_time,
batch_no: Optional[str] = None, batch_no: Optional[str] = None,
with_valuation_rate: bool = True, with_valuation_rate: bool = True,
): ):

View File

@@ -828,9 +828,9 @@ def insert_item_price(args):
): ):
if frappe.has_permission("Item Price", "write"): if frappe.has_permission("Item Price", "write"):
price_list_rate = ( price_list_rate = (
(args.rate + args.discount_amount) / args.get("conversion_factor") (flt(args.rate) + flt(args.discount_amount)) / args.get("conversion_factor")
if args.get("conversion_factor") if args.get("conversion_factor")
else (args.rate + args.discount_amount) else (flt(args.rate) + flt(args.discount_amount))
) )
item_price = frappe.db.get_value( item_price = frappe.db.get_value(

View File

@@ -82,7 +82,7 @@ def get_item_info(filters):
item.safety_stock, item.safety_stock,
item.lead_time_days, item.lead_time_days,
) )
.where(item.is_stock_item == 1) .where((item.is_stock_item == 1) & (item.disabled == 0))
) )
if brand := filters.get("brand"): if brand := filters.get("brand"):

View File

@@ -1849,6 +1849,8 @@ Outstanding Amt,Offener Betrag,
Outstanding Cheques and Deposits to clear,Ausstehende Schecks und Anzahlungen zum verbuchen, Outstanding Cheques and Deposits to clear,Ausstehende Schecks und Anzahlungen zum verbuchen,
Outstanding for {0} cannot be less than zero ({1}),Ausstände für {0} können nicht kleiner als Null sein ({1}), Outstanding for {0} cannot be less than zero ({1}),Ausstände für {0} können nicht kleiner als Null sein ({1}),
Outward taxable supplies(zero rated),Steuerpflichtige Lieferungen aus dem Ausland (null bewertet), Outward taxable supplies(zero rated),Steuerpflichtige Lieferungen aus dem Ausland (null bewertet),
Over Receipt/Delivery of {0} {1} ignored for item {2} because you have {3} role.,"Überhöhte Annahme bzw. Lieferung von Artikel {2} mit {0} {1} wurde ignoriert, weil Sie die Rolle {3} haben."
Overbilling of {0} {1} ignored for item {2} because you have {3} role.,"Überhöhte Abrechnung von Artikel {2} mit {0} {1} wurde ignoriert, weil Sie die Rolle {3} haben."
Overdue,Überfällig, Overdue,Überfällig,
Overlap in scoring between {0} and {1},Überlappung beim Scoring zwischen {0} und {1}, Overlap in scoring between {0} and {1},Überlappung beim Scoring zwischen {0} und {1},
Overlapping conditions found between:,Überlagernde Bedingungen gefunden zwischen:, Overlapping conditions found between:,Überlagernde Bedingungen gefunden zwischen:,
@@ -9916,4 +9918,3 @@ Cost and Freight,Kosten und Fracht,
Delivered at Place,Geliefert benannter Ort, Delivered at Place,Geliefert benannter Ort,
Delivered at Place Unloaded,Geliefert benannter Ort entladen, Delivered at Place Unloaded,Geliefert benannter Ort entladen,
Delivered Duty Paid,Geliefert verzollt, Delivered Duty Paid,Geliefert verzollt,
{0} of {1} {2} ignored for item {3} because you have {4} role,"{0} von Artikel {3} mit {1} {2} wurde ignoriert, weil Sie die Rolle {4} haben."
Can't render this file because it is too large.

View File

@@ -2,8 +2,6 @@ frappe.ready(async () => {
initialise_select_date(); initialise_select_date();
}) })
window.holiday_list = [];
async function initialise_select_date() { async function initialise_select_date() {
navigate_to_page(1); navigate_to_page(1);
await get_global_variables(); await get_global_variables();
@@ -20,7 +18,6 @@ async function get_global_variables() {
window.timezones = (await frappe.call({ window.timezones = (await frappe.call({
method:'erpnext.www.book_appointment.index.get_timezones' method:'erpnext.www.book_appointment.index.get_timezones'
})).message; })).message;
window.holiday_list = window.appointment_settings.holiday_list;
} }
function setup_timezone_selector() { function setup_timezone_selector() {

View File

@@ -26,8 +26,12 @@ def get_context(context):
@frappe.whitelist(allow_guest=True) @frappe.whitelist(allow_guest=True)
def get_appointment_settings(): def get_appointment_settings():
settings = frappe.get_doc("Appointment Booking Settings") settings = frappe.get_cached_value(
settings.holiday_list = frappe.get_doc("Holiday List", settings.holiday_list) "Appointment Booking Settings",
None,
["advance_booking_days", "appointment_duration", "success_redirect_url"],
as_dict=True,
)
return settings return settings
@@ -106,7 +110,7 @@ def create_appointment(date, time, tz, contact):
appointment.customer_details = contact.get("notes", None) appointment.customer_details = contact.get("notes", None)
appointment.customer_email = contact.get("email", None) appointment.customer_email = contact.get("email", None)
appointment.status = "Open" appointment.status = "Open"
appointment.insert() appointment.insert(ignore_permissions=True)
return appointment return appointment

View File

@@ -2,7 +2,6 @@ import frappe
from frappe.utils.verified_command import verify_request from frappe.utils.verified_command import verify_request
@frappe.whitelist(allow_guest=True)
def get_context(context): def get_context(context):
if not verify_request(): if not verify_request():
context.success = False context.success = False