Merge branch 'develop' of https://github.com/frappe/erpnext into payment_request_flow

This commit is contained in:
Deepesh Garg 2022-12-20 11:42:15 +05:30
commit ff48634cbb
63 changed files with 683 additions and 222 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

@ -34,9 +34,6 @@ class JournalEntry(AccountsController):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(JournalEntry, self).__init__(*args, **kwargs) super(JournalEntry, self).__init__(*args, **kwargs)
def get_feed(self):
return self.voucher_type
def validate(self): def validate(self):
if self.voucher_type == "Opening Entry": if self.voucher_type == "Opening Entry":
self.is_opening = "Yes" self.is_opening = "Yes"

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)

View File

@ -25,7 +25,8 @@
"in_list_view": 1, "in_list_view": 1,
"label": "Type", "label": "Type",
"options": "DocType", "options": "DocType",
"reqd": 1 "reqd": 1,
"search_index": 1
}, },
{ {
"columns": 2, "columns": 2,
@ -35,7 +36,8 @@
"in_list_view": 1, "in_list_view": 1,
"label": "Name", "label": "Name",
"options": "reference_doctype", "options": "reference_doctype",
"reqd": 1 "reqd": 1,
"search_index": 1
}, },
{ {
"fieldname": "due_date", "fieldname": "due_date",
@ -104,7 +106,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-09-26 17:06:55.597389", "modified": "2022-12-12 12:31:44.919895",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Payment Entry Reference", "name": "Payment Entry Reference",
@ -113,5 +115,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

@ -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

@ -255,7 +255,7 @@ def apply_pricing_rule(args, doc=None):
for item in item_list: for item in item_list:
args_copy = copy.deepcopy(args) args_copy = copy.deepcopy(args)
args_copy.update(item) args_copy.update(item)
data = get_pricing_rule_for_item(args_copy, item.get("price_list_rate"), doc=doc) data = get_pricing_rule_for_item(args_copy, doc=doc)
out.append(data) out.append(data)
if ( if (
@ -292,7 +292,7 @@ def update_pricing_rule_uom(pricing_rule, args):
pricing_rule.uom = row.uom pricing_rule.uom = row.uom
def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=False): def get_pricing_rule_for_item(args, doc=None, for_validate=False):
from erpnext.accounts.doctype.pricing_rule.utils import ( from erpnext.accounts.doctype.pricing_rule.utils import (
get_applied_pricing_rules, get_applied_pricing_rules,
get_pricing_rule_items, get_pricing_rule_items,

View File

@ -1123,7 +1123,7 @@ def make_pricing_rule(**args):
"apply_on": args.apply_on or "Item Code", "apply_on": args.apply_on or "Item Code",
"applicable_for": args.applicable_for, "applicable_for": args.applicable_for,
"selling": args.selling or 0, "selling": args.selling or 0,
"currency": "USD", "currency": "INR",
"apply_discount_on_rate": args.apply_discount_on_rate or 0, "apply_discount_on_rate": args.apply_discount_on_rate or 0,
"buying": args.buying or 0, "buying": args.buying or 0,
"min_qty": args.min_qty or 0.0, "min_qty": args.min_qty or 0.0,

View File

@ -250,6 +250,17 @@ def get_other_conditions(conditions, values, args):
and ifnull(`tabPricing Rule`.valid_upto, '2500-12-31')""" and ifnull(`tabPricing Rule`.valid_upto, '2500-12-31')"""
values["transaction_date"] = args.get("transaction_date") values["transaction_date"] = args.get("transaction_date")
if args.get("doctype") in [
"Quotation",
"Sales Order",
"Delivery Note",
"Sales Invoice",
"POS Invoice",
]:
conditions += """ and ifnull(`tabPricing Rule`.selling, 0) = 1"""
else:
conditions += """ and ifnull(`tabPricing Rule`.buying, 0) = 1"""
return conditions return conditions
@ -669,7 +680,7 @@ def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None):
item_details.free_item_data.append(free_item_data_args) item_details.free_item_data.append(free_item_data_args)
def apply_pricing_rule_for_free_items(doc, pricing_rule_args, set_missing_values=False): def apply_pricing_rule_for_free_items(doc, pricing_rule_args):
if pricing_rule_args: if pricing_rule_args:
items = tuple((d.item_code, d.pricing_rules) for d in doc.items if d.is_free_item) items = tuple((d.item_code, d.pricing_rules) for d in doc.items if d.is_free_item)

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-25 12:44:29.935567", "modified": "2022-12-12 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",
@ -2122,6 +2123,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",
@ -2134,7 +2141,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

@ -280,7 +280,8 @@ class Subscription(Document):
self.validate_plans_billing_cycle(self.get_billing_cycle_and_interval()) self.validate_plans_billing_cycle(self.get_billing_cycle_and_interval())
self.validate_end_date() self.validate_end_date()
self.validate_to_follow_calendar_months() self.validate_to_follow_calendar_months()
self.cost_center = erpnext.get_default_cost_center(self.get("company")) if not self.cost_center:
self.cost_center = erpnext.get_default_cost_center(self.get("company"))
def validate_trial_period(self): def validate_trial_period(self):
""" """

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

@ -99,6 +99,9 @@ class ReceivablePayableReport(object):
# Get return entries # Get return entries
self.get_return_entries() self.get_return_entries()
# Get Exchange Rate Revaluations
self.get_exchange_rate_revaluations()
self.data = [] self.data = []
for ple in self.ple_entries: for ple in self.ple_entries:
@ -251,7 +254,8 @@ class ReceivablePayableReport(object):
row.invoice_grand_total = row.invoiced row.invoice_grand_total = row.invoiced
if (abs(row.outstanding) > 1.0 / 10**self.currency_precision) and ( if (abs(row.outstanding) > 1.0 / 10**self.currency_precision) and (
abs(row.outstanding_in_account_currency) > 1.0 / 10**self.currency_precision (abs(row.outstanding_in_account_currency) > 1.0 / 10**self.currency_precision)
or (row.voucher_no in self.err_journals)
): ):
# non-zero oustanding, we must consider this row # non-zero oustanding, we must consider this row
@ -1028,3 +1032,17 @@ class ReceivablePayableReport(object):
"data": {"labels": self.ageing_column_labels, "datasets": rows}, "data": {"labels": self.ageing_column_labels, "datasets": rows},
"type": "percentage", "type": "percentage",
} }
def get_exchange_rate_revaluations(self):
je = qb.DocType("Journal Entry")
results = (
qb.from_(je)
.select(je.name)
.where(
(je.company == self.filters.company)
& (je.posting_date.lte(self.filters.report_date))
& (je.voucher_type == "Exchange Rate Revaluation")
)
.run()
)
self.err_journals = [x[0] for x in results] if results else []

View File

@ -1,9 +1,10 @@
import unittest import unittest
import frappe import frappe
from frappe.tests.utils import FrappeTestCase from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import add_days, getdate, today from frappe.utils import add_days, flt, getdate, today
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.payment_entry import get_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.report.accounts_receivable.accounts_receivable import execute from erpnext.accounts.report.accounts_receivable.accounts_receivable import execute
@ -17,10 +18,37 @@ class TestAccountsReceivable(FrappeTestCase):
frappe.db.sql("delete from `tabPayment Entry` where company='_Test Company 2'") frappe.db.sql("delete from `tabPayment Entry` where company='_Test Company 2'")
frappe.db.sql("delete from `tabGL Entry` where company='_Test Company 2'") frappe.db.sql("delete from `tabGL Entry` where company='_Test Company 2'")
frappe.db.sql("delete from `tabPayment Ledger Entry` where company='_Test Company 2'") frappe.db.sql("delete from `tabPayment Ledger Entry` where company='_Test Company 2'")
frappe.db.sql("delete from `tabJournal Entry` where company='_Test Company 2'")
frappe.db.sql("delete from `tabExchange Rate Revaluation` where company='_Test Company 2'")
self.create_usd_account()
def tearDown(self): def tearDown(self):
frappe.db.rollback() frappe.db.rollback()
def create_usd_account(self):
name = "Debtors USD"
exists = frappe.db.get_list(
"Account", filters={"company": "_Test Company 2", "account_name": "Debtors USD"}
)
if exists:
self.debtors_usd = exists[0].name
else:
debtors = frappe.get_doc(
"Account",
frappe.db.get_list(
"Account", filters={"company": "_Test Company 2", "account_name": "Debtors"}
)[0].name,
)
debtors_usd = frappe.new_doc("Account")
debtors_usd.company = debtors.company
debtors_usd.account_name = "Debtors USD"
debtors_usd.account_currency = "USD"
debtors_usd.parent_account = debtors.parent_account
debtors_usd.account_type = debtors.account_type
self.debtors_usd = debtors_usd.save().name
def test_accounts_receivable(self): def test_accounts_receivable(self):
filters = { filters = {
"company": "_Test Company 2", "company": "_Test Company 2",
@ -33,7 +61,7 @@ class TestAccountsReceivable(FrappeTestCase):
} }
# check invoice grand total and invoiced column's value for 3 payment terms # check invoice grand total and invoiced column's value for 3 payment terms
name = make_sales_invoice() name = make_sales_invoice().name
report = execute(filters) report = execute(filters)
expected_data = [[100, 30], [100, 50], [100, 20]] expected_data = [[100, 30], [100, 50], [100, 20]]
@ -118,8 +146,74 @@ class TestAccountsReceivable(FrappeTestCase):
], ],
) )
@change_settings(
"Accounts Settings", {"allow_multi_currency_invoices_against_single_party_account": 1}
)
def test_exchange_revaluation_for_party(self):
"""
Exchange Revaluation for party on Receivable/Payable shoule be included
"""
def make_sales_invoice(): company = "_Test Company 2"
customer = "_Test Customer 2"
# Using Exchange Gain/Loss account for unrealized as well.
company_doc = frappe.get_doc("Company", company)
company_doc.unrealized_exchange_gain_loss_account = company_doc.exchange_gain_loss_account
company_doc.save()
si = make_sales_invoice(no_payment_schedule=True, do_not_submit=True)
si.currency = "USD"
si.conversion_rate = 0.90
si.debit_to = self.debtors_usd
si = si.save().submit()
# Exchange Revaluation
err = frappe.new_doc("Exchange Rate Revaluation")
err.company = company
err.posting_date = today()
accounts = err.get_accounts_data()
err.extend("accounts", accounts)
err.accounts[0].new_exchange_rate = 0.95
row = err.accounts[0]
row.new_balance_in_base_currency = flt(
row.new_exchange_rate * flt(row.balance_in_account_currency)
)
row.gain_loss = row.new_balance_in_base_currency - flt(row.balance_in_base_currency)
err.set_total_gain_loss()
err = err.save().submit()
# Submit JV for ERR
jv = frappe.get_doc(err.make_jv_entry())
jv = jv.save()
for x in jv.accounts:
x.cost_center = get_default_cost_center(jv.company)
jv.submit()
filters = {
"company": company,
"report_date": today(),
"range1": 30,
"range2": 60,
"range3": 90,
"range4": 120,
}
report = execute(filters)
expected_data_for_err = [0, -5, 0, 5]
row = [x for x in report[1] if x.voucher_type == jv.doctype and x.voucher_no == jv.name][0]
self.assertEqual(
expected_data_for_err,
[
row.invoiced,
row.paid,
row.credit_note,
row.outstanding,
],
)
def make_sales_invoice(no_payment_schedule=False, do_not_submit=False):
frappe.set_user("Administrator") frappe.set_user("Administrator")
si = create_sales_invoice( si = create_sales_invoice(
@ -134,22 +228,26 @@ def make_sales_invoice():
do_not_save=1, do_not_save=1,
) )
si.append( if not no_payment_schedule:
"payment_schedule", si.append(
dict(due_date=getdate(add_days(today(), 30)), invoice_portion=30.00, payment_amount=30), "payment_schedule",
) dict(due_date=getdate(add_days(today(), 30)), invoice_portion=30.00, payment_amount=30),
si.append( )
"payment_schedule", si.append(
dict(due_date=getdate(add_days(today(), 60)), invoice_portion=50.00, payment_amount=50), "payment_schedule",
) dict(due_date=getdate(add_days(today(), 60)), invoice_portion=50.00, payment_amount=50),
si.append( )
"payment_schedule", si.append(
dict(due_date=getdate(add_days(today(), 90)), invoice_portion=20.00, payment_amount=20), "payment_schedule",
) dict(due_date=getdate(add_days(today(), 90)), invoice_portion=20.00, payment_amount=20),
)
si.submit() si = si.save()
return si.name if not do_not_submit:
si = si.submit()
return si
def make_payment(docname): def make_payment(docname):

View File

@ -533,12 +533,13 @@ def get_accounts(root_type, companies):
], ],
filters={"company": company, "root_type": root_type}, filters={"company": company, "root_type": root_type},
): ):
if account.account_name not in added_accounts: if account.account_number:
account_key = account.account_number + "-" + account.account_name
else:
account_key = account.account_name
if account_key not in added_accounts:
accounts.append(account) accounts.append(account)
if account.account_number:
account_key = account.account_number + "-" + account.account_name
else:
account_key = account.account_name
added_accounts.append(account_key) added_accounts.append(account_key)
return accounts return accounts

View File

@ -503,7 +503,7 @@ class GrossProfitGenerator(object):
invoice_portion = 100 invoice_portion = 100
elif row.invoice_portion: elif row.invoice_portion:
invoice_portion = row.invoice_portion invoice_portion = row.invoice_portion
else: elif row.payment_amount:
invoice_portion = row.payment_amount * 100 / row.base_net_amount invoice_portion = row.payment_amount * 100 / row.base_net_amount
if i == 0: if i == 0:

View File

@ -234,8 +234,11 @@ def modify_report_columns(doctype, field, column):
if field in ["item_tax_rate", "base_net_amount"]: if field in ["item_tax_rate", "base_net_amount"]:
return None return None
if doctype == "GL Entry" and field in ["debit", "credit"]: if doctype == "GL Entry":
column.update({"label": _("Amount"), "fieldname": "amount"}) if field in ["debit", "credit"]:
column.update({"label": _("Amount"), "fieldname": "amount"})
elif field == "voucher_type":
column.update({"fieldtype": "Data", "options": ""})
if field == "taxes_and_charges": if field == "taxes_and_charges":
column.update({"label": _("Taxes and Charges Template")}) column.update({"label": _("Taxes and Charges Template")})

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

@ -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

@ -20,9 +20,6 @@ from erpnext.utilities.transaction_base import TransactionBase
class Supplier(TransactionBase): class Supplier(TransactionBase):
def get_feed(self):
return self.supplier_name
def onload(self): def onload(self):
"""Load address and contacts in `__onload`""" """Load address and contacts in `__onload`"""
load_address_and_contact(self) load_address_and_contact(self)

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

@ -197,7 +197,7 @@ class AccountsController(TransactionBase):
validate_einvoice_fields(self) validate_einvoice_fields(self)
if self.doctype != "Material Request": if self.doctype != "Material Request" and not self.ignore_pricing_rule:
apply_pricing_rule_on_transaction(self) apply_pricing_rule_on_transaction(self)
def before_cancel(self): def before_cancel(self):

View File

@ -25,10 +25,6 @@ class BuyingController(SubcontractingController):
def __setup__(self): def __setup__(self):
self.flags.ignore_permlevel_for_fields = ["buying_price_list", "price_list_currency"] self.flags.ignore_permlevel_for_fields = ["buying_price_list", "price_list_currency"]
def get_feed(self):
if self.get("supplier_name"):
return _("From {0} | {1} {2}").format(self.supplier_name, self.currency, self.grand_total)
def validate(self): def validate(self):
super(BuyingController, self).validate() super(BuyingController, self).validate()
if getattr(self, "supplier", None) and not self.supplier_name: if getattr(self, "supplier", None) and not self.supplier_name:

View File

@ -19,9 +19,6 @@ class SellingController(StockController):
def __setup__(self): def __setup__(self):
self.flags.ignore_permlevel_for_fields = ["selling_price_list", "price_list_currency"] self.flags.ignore_permlevel_for_fields = ["selling_price_list", "price_list_currency"]
def get_feed(self):
return _("To {0} | {1} {2}").format(self.customer_name, self.currency, self.grand_total)
def onload(self): def onload(self):
super(SellingController, self).onload() super(SellingController, self).onload()
if self.doctype in ("Sales Order", "Delivery Note", "Sales Invoice"): if self.doctype in ("Sales Order", "Delivery Note", "Sales Invoice"):

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

@ -14,9 +14,6 @@ from erpnext.crm.utils import CRMNote, copy_comments, link_communications, link_
class Lead(SellingController, CRMNote): class Lead(SellingController, CRMNote):
def get_feed(self):
return "{0}: {1}".format(_(self.status), self.lead_name)
def onload(self): def onload(self):
customer = frappe.db.get_value("Customer", {"lead_name": self.name}) customer = frappe.db.get_value("Customer", {"lead_name": self.name})
self.get("__onload").is_customer = customer self.get("__onload").is_customer = customer

View File

@ -420,6 +420,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

@ -10,9 +10,6 @@ from erpnext.utilities.transaction_base import TransactionBase
class MaintenanceVisit(TransactionBase): class MaintenanceVisit(TransactionBase):
def get_feed(self):
return _("To {0}").format(self.customer_name)
def validate_serial_no(self): def validate_serial_no(self):
for d in self.get("purposes"): for d in self.get("purposes"):
if d.serial_no and not frappe.db.exists("Serial No", d.serial_no): if d.serial_no and not frappe.db.exists("Serial No", d.serial_no):

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

@ -4,7 +4,7 @@
import frappe import frappe
from frappe import _ from frappe import _
from frappe.query_builder.functions import Floor, Sum from frappe.query_builder.functions import Sum
from pypika.terms import ExistsCriterion from pypika.terms import ExistsCriterion
@ -58,9 +58,9 @@ def get_bom_stock(filters):
bom_item.description, bom_item.description,
bom_item.stock_qty, bom_item.stock_qty,
bom_item.stock_uom, bom_item.stock_uom,
bom_item.stock_qty * qty_to_produce / bom.quantity, (bom_item.stock_qty / bom.quantity) * qty_to_produce,
Sum(bin.actual_qty).as_("actual_qty"), Sum(bin.actual_qty),
Sum(Floor(bin.actual_qty / (bom_item.stock_qty * qty_to_produce / bom.quantity))), Sum(bin.actual_qty) / (bom_item.stock_qty / bom.quantity),
) )
.where((bom_item.parent == filters.get("bom")) & (bom_item.parenttype == "BOM")) .where((bom_item.parent == filters.get("bom")) & (bom_item.parenttype == "BOM"))
.groupby(bom_item.item_code) .groupby(bom_item.item_code)

View File

@ -102,7 +102,7 @@ def create_party_contact(doctype, fullname, user, party_name):
contact = frappe.new_doc("Contact") contact = frappe.new_doc("Contact")
contact.update({"first_name": fullname, "email_id": user}) contact.update({"first_name": fullname, "email_id": user})
contact.append("links", dict(link_doctype=doctype, link_name=party_name)) contact.append("links", dict(link_doctype=doctype, link_name=party_name))
contact.append("email_ids", dict(email_id=user)) contact.append("email_ids", dict(email_id=user, is_primary=True))
contact.flags.ignore_mandatory = True contact.flags.ignore_mandatory = True
contact.insert(ignore_permissions=True) contact.insert(ignore_permissions=True)

View File

@ -15,9 +15,6 @@ from erpnext.setup.doctype.holiday_list.holiday_list import is_holiday
class Project(Document): class Project(Document):
def get_feed(self):
return "{0}: {1}".format(_(self.status), frappe.safe_decode(self.project_name))
def onload(self): def onload(self):
self.set_onload( self.set_onload(
"activity_summary", "activity_summary",

View File

@ -20,9 +20,6 @@ class CircularReferenceError(frappe.ValidationError):
class Task(NestedSet): class Task(NestedSet):
nsm_parent_field = "parent_task" nsm_parent_field = "parent_task"
def get_feed(self):
return "{0}: {1}".format(_(self.status), self.subject)
def get_customer_details(self): def get_customer_details(self):
cust = frappe.db.sql("select customer_name from `tabCustomer` where name=%s", self.customer) cust = frappe.db.sql("select customer_name from `tabCustomer` where name=%s", self.customer)
if cust: if cust:

View File

@ -58,7 +58,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
if ( if (
in_list(["Sales Invoice", "POS Invoice"], this.frm.doc.doctype) in_list(["Sales Invoice", "POS Invoice"], this.frm.doc.doctype)
&& this.frm.doc.s_pos && this.frm.doc.is_pos
&& this.frm.doc.is_return && this.frm.doc.is_return
) { ) {
this.set_total_amount_to_default_mop(); this.set_total_amount_to_default_mop();

View File

@ -1130,10 +1130,13 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
qty(doc, cdt, cdn) { qty(doc, cdt, cdn) {
let item = frappe.get_doc(cdt, cdn); let item = frappe.get_doc(cdt, cdn);
item.pricing_rules = '' // item.pricing_rules = ''
this.conversion_factor(doc, cdt, cdn, true); frappe.run_serially([
this.calculate_stock_uom_rate(doc, cdt, cdn); () => this.remove_pricing_rule(item),
this.apply_pricing_rule(item, true); () => this.conversion_factor(doc, cdt, cdn, true),
() => this.calculate_stock_uom_rate(doc, cdt, cdn),
() => this.apply_pricing_rule(item, true)
]);
} }
calculate_stock_uom_rate(doc, cdt, cdn) { calculate_stock_uom_rate(doc, cdt, cdn) {
@ -1357,16 +1360,21 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
var item_list = []; var item_list = [];
$.each(this.frm.doc["items"] || [], function(i, d) { $.each(this.frm.doc["items"] || [], function(i, d) {
if (d.item_code && !d.is_free_item) { if (d.item_code) {
item_list.push({ if (d.is_free_item) {
"doctype": d.doctype, // Simply remove free items
"name": d.name, me.frm.get_field("items").grid.grid_rows[i].remove();
"item_code": d.item_code, } else {
"pricing_rules": d.pricing_rules, item_list.push({
"parenttype": d.parenttype, "doctype": d.doctype,
"parent": d.parent, "name": d.name,
"price_list_rate": d.price_list_rate "item_code": d.item_code,
}) "pricing_rules": d.pricing_rules,
"parenttype": d.parenttype,
"parent": d.parent,
"price_list_rate": d.price_list_rate
})
}
} }
}); });
return this.frm.call({ return this.frm.call({

View File

@ -27,9 +27,6 @@ from erpnext.utilities.transaction_base import TransactionBase
class Customer(TransactionBase): class Customer(TransactionBase):
def get_feed(self):
return self.customer_name
def onload(self): def onload(self):
"""Load address and contacts in `__onload`""" """Load address and contacts in `__onload`"""
load_address_and_contact(self) load_address_and_contact(self)

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

@ -30,6 +30,24 @@ class TestQuotation(FrappeTestCase):
self.assertTrue(sales_order.get("payment_schedule")) self.assertTrue(sales_order.get("payment_schedule"))
def test_maintain_rate_in_sales_cycle_is_enforced(self):
from erpnext.selling.doctype.quotation.quotation import make_sales_order
maintain_rate = frappe.db.get_single_value("Selling Settings", "maintain_same_sales_rate")
frappe.db.set_single_value("Selling Settings", "maintain_same_sales_rate", 1)
quotation = frappe.copy_doc(test_records[0])
quotation.transaction_date = nowdate()
quotation.valid_till = add_months(quotation.transaction_date, 1)
quotation.insert()
quotation.submit()
sales_order = make_sales_order(quotation.name)
sales_order.items[0].rate = 1
self.assertRaises(frappe.ValidationError, sales_order.save)
frappe.db.set_single_value("Selling Settings", "maintain_same_sales_rate", maintain_rate)
def test_make_sales_order_with_different_currency(self): def test_make_sales_order_with_different_currency(self):
from erpnext.selling.doctype.quotation.quotation import make_sales_order from erpnext.selling.doctype.quotation.quotation import make_sales_order

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

@ -194,7 +194,7 @@ class SalesOrder(SellingController):
) )
if cint(frappe.db.get_single_value("Selling Settings", "maintain_same_sales_rate")): if cint(frappe.db.get_single_value("Selling Settings", "maintain_same_sales_rate")):
self.validate_rate_with_reference_doc([["Quotation", "prev_docname", "quotation_item"]]) self.validate_rate_with_reference_doc([["Quotation", "prevdoc_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(

View File

@ -12,7 +12,10 @@ def get_data():
"Auto Repeat": "reference_document", "Auto Repeat": "reference_document",
"Maintenance Visit": "prevdoc_docname", "Maintenance Visit": "prevdoc_docname",
}, },
"internal_links": {"Quotation": ["items", "prevdoc_docname"]}, "internal_links": {
"Quotation": ["items", "prevdoc_docname"],
"Material Request": ["items", "material_request"],
},
"transactions": [ "transactions": [
{ {
"label": _("Fulfillment"), "label": _("Fulfillment"),

View File

@ -44,6 +44,12 @@ frappe.query_reports["Sales Order Analysis"] = {
} }
} }
}, },
{
"fieldname": "warehouse",
"label": __("Warehouse"),
"fieldtype": "Link",
"options": "Warehouse"
},
{ {
"fieldname": "status", "fieldname": "status",
"label": __("Status"), "label": __("Status"),

View File

@ -53,6 +53,9 @@ def get_conditions(filters):
if filters.get("status"): if filters.get("status"):
conditions += " and so.status in %(status)s" conditions += " and so.status in %(status)s"
if filters.get("warehouse"):
conditions += " and soi.warehouse = %(warehouse)s"
return conditions return conditions

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

@ -22,9 +22,6 @@ form_grid_templates = {"items": "templates/form_grid/material_request_grid.html"
class MaterialRequest(BuyingController): class MaterialRequest(BuyingController):
def get_feed(self):
return
def check_if_already_pulled(self): def check_if_already_pulled(self):
pass pass

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
@ -83,9 +95,6 @@ class StockEntry(StockController):
} }
) )
def get_feed(self):
return self.stock_entry_type
def onload(self): def onload(self):
for item in self.get("items"): for item in self.get("items"):
item.update(get_bin_details(item.item_code, item.s_warehouse)) item.update(get_bin_details(item.item_code, item.s_warehouse))
@ -2239,16 +2248,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 +2273,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 +2712,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

@ -113,7 +113,7 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru
if args.get(key) is None: if args.get(key) is None:
args[key] = value args[key] = value
data = get_pricing_rule_for_item(args, out.price_list_rate, doc, for_validate=for_validate) data = get_pricing_rule_for_item(args, doc=doc, for_validate=for_validate)
out.update(data) out.update(data)
@ -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(
@ -1305,7 +1305,7 @@ def apply_price_list_on_item(args):
item_doc = frappe.db.get_value("Item", args.item_code, ["name", "variant_of"], as_dict=1) item_doc = frappe.db.get_value("Item", args.item_code, ["name", "variant_of"], as_dict=1)
item_details = get_price_list_rate(args, item_doc) item_details = get_price_list_rate(args, item_doc)
item_details.update(get_pricing_rule_for_item(args, item_details.price_list_rate)) item_details.update(get_pricing_rule_for_item(args))
return item_details return item_details

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

@ -18,9 +18,6 @@ from frappe.utils.user import is_website_user
class Issue(Document): class Issue(Document):
def get_feed(self):
return "{0}: {1}".format(_(self.status), self.subject)
def validate(self): def validate(self):
if self.is_new() and self.via_customer_portal: if self.is_new() and self.via_customer_portal:
self.flags.create_communication = True self.flags.create_communication = True

View File

@ -10,9 +10,6 @@ from erpnext.utilities.transaction_base import TransactionBase
class WarrantyClaim(TransactionBase): class WarrantyClaim(TransactionBase):
def get_feed(self):
return _("{0}: From {1}").format(self.status, self.customer_name)
def validate(self): def validate(self):
if session["user"] != "Guest" and not self.customer: if session["user"] != "Guest" and not self.customer:
frappe.throw(_("Customer is required")) frappe.throw(_("Customer is required"))

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:,
@ -9914,4 +9916,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

@ -110,6 +110,7 @@ def get_price(item_code, price_list, customer_group, company, qty=1):
"conversion_rate": 1, "conversion_rate": 1,
"for_shopping_cart": True, "for_shopping_cart": True,
"currency": frappe.db.get_value("Price List", price_list, "currency"), "currency": frappe.db.get_value("Price List", price_list, "currency"),
"doctype": "Quotation",
} }
) )