Merge branch 'develop' into handle-party-type-erpenxt-crm-integration

This commit is contained in:
Shariq Ansari
2025-03-14 16:00:42 +05:30
committed by GitHub
95 changed files with 960548 additions and 70599 deletions

4
.github/release.yml vendored Normal file
View File

@@ -0,0 +1,4 @@
changelog:
exclude:
labels:
- skip-release-notes

View File

@@ -19,4 +19,3 @@ jobs:
github-token: ${{ github.token }}
issue-inactive-days: 14
pr-inactive-days: 14
exclude-pr-created-before: 2025-01-01

View File

@@ -114,10 +114,7 @@ To setup the repository locally follow the steps mentioned below:
2. In a separate terminal window, run the following commands:
```
# Create a new site
bench new-site erpnext.dev
# Map your site to localhost
bench --site erpnext.dev add-to-hosts
bench new-site erpnext.localhost
```
3. Get the ERPNext app and install it
@@ -126,10 +123,10 @@ To setup the repository locally follow the steps mentioned below:
bench get-app https://github.com/frappe/erpnext
# Install the app
bench --site erpnext.dev install-app erpnext
bench --site erpnext.localhost install-app erpnext
```
4. Open the URL `http://erpnext.dev:8000/app` in your browser, you should see the app running
4. Open the URL `http://erpnext.localhost:8000/app` in your browser, you should see the app running
## Learning and community

View File

@@ -1,6 +1,6 @@
files:
- source: /erpnext/locale/main.pot
translation: /erpnext/locale/%two_letters_code%.po
translation: /erpnext/locale/%locale_with_underscore%.po
pull_request_title: "fix: sync translations from crowdin"
pull_request_labels:
- translation

View File

@@ -500,7 +500,7 @@ def update_account_number(name, account_name, account_number=None, from_descenda
"name",
)
if old_name:
if old_name and not from_descendant:
# same account in parent company exists
allow_child_account_creation = _("Allow Account Creation Against Child Company")
@@ -605,6 +605,7 @@ def _ensure_idle_system():
if frappe.flags.in_test:
return
last_gl_update = None
try:
# We also lock inserts to GL entry table with for_update here.
last_gl_update = frappe.db.get_value("GL Entry", {}, "modified", for_update=True, wait=False)
@@ -612,6 +613,9 @@ def _ensure_idle_system():
# wait=False fails immediately if there's an active transaction.
last_gl_update = add_to_date(None, seconds=-1)
if not last_gl_update:
return
if last_gl_update > add_to_date(None, minutes=-5):
frappe.throw(
_(

View File

@@ -98,7 +98,7 @@
"Office Maintenance Expenses": {},
"Office Rent": {},
"Postal Expenses": {},
"Print and Stationary": {},
"Print and Stationery": {},
"Rounded Off": {
"account_type": "Round Off"
},

View File

@@ -374,9 +374,36 @@ def auto_reconcile_vouchers(
to_reference_date=None,
):
frappe.flags.auto_reconcile_vouchers = True
reconciled, partially_reconciled = set(), set()
bank_transactions = get_bank_transactions(bank_account)
if len(bank_transactions) > 10:
frappe.enqueue(
method="erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.start_auto_reconcile",
queue="long",
bank_transactions=bank_transactions,
from_date=from_date,
to_date=to_date,
filter_by_reference_date=filter_by_reference_date,
from_reference_date=from_reference_date,
to_reference_date=to_reference_date,
)
frappe.msgprint(_("Auto Reconciliation has started in the background"))
else:
start_auto_reconcile(
bank_transactions,
from_date,
to_date,
filter_by_reference_date,
from_reference_date,
to_reference_date,
)
def start_auto_reconcile(
bank_transactions, from_date, to_date, filter_by_reference_date, from_reference_date, to_reference_date
):
reconciled, partially_reconciled = set(), set()
for transaction in bank_transactions:
linked_payments = get_linked_payments(
transaction.name,
@@ -414,7 +441,6 @@ def auto_reconcile_vouchers(
frappe.msgprint(title=_("Auto Reconciliation"), msg=alert_message, indicator=indicator)
frappe.flags.auto_reconcile_vouchers = False
return reconciled, partially_reconciled
def get_auto_reconcile_message(partially_reconciled, reconciled):

View File

@@ -128,7 +128,7 @@ class TestCouponCode(IntegrationTestCase):
item_code="_Test Tesla Car",
rate=5000,
qty=1,
do_not_submit=True,
do_not_save=True,
)
self.assertEqual(so.items[0].rate, 5000)

View File

@@ -279,7 +279,8 @@
{
"fieldname": "transaction_exchange_rate",
"fieldtype": "Float",
"label": "Transaction Exchange Rate"
"label": "Transaction Exchange Rate",
"precision": "9"
},
{
"fieldname": "debit_in_transaction_currency",
@@ -357,7 +358,7 @@
"idx": 1,
"in_create": 1,
"links": [],
"modified": "2024-08-22 13:03:39.997475",
"modified": "2025-02-21 14:36:49.431166",
"modified_by": "Administrator",
"module": "Accounts",
"name": "GL Entry",

View File

@@ -1063,14 +1063,15 @@ class JournalEntry(AccountsController):
gl_map = []
company_currency = erpnext.get_company_currency(self.company)
self.transaction_currency = company_currency
self.transaction_exchange_rate = 1
if self.multi_currency:
for row in self.get("accounts"):
if row.account_currency != company_currency:
self.currency = row.account_currency
self.conversion_rate = row.exchange_rate
# Journal assumes the first foreign currency as transaction currency
self.transaction_currency = row.account_currency
self.transaction_exchange_rate = row.exchange_rate
break
else:
self.currency = company_currency
for d in self.get("accounts"):
if d.debit or d.credit or (self.voucher_type == "Exchange Gain Or Loss"):
@@ -1095,6 +1096,18 @@ class JournalEntry(AccountsController):
"credit_in_account_currency": flt(
d.credit_in_account_currency, d.precision("credit_in_account_currency")
),
"transaction_currency": self.transaction_currency,
"transaction_exchange_rate": self.transaction_exchange_rate,
"debit_in_transaction_currency": flt(
d.debit_in_account_currency, d.precision("debit_in_account_currency")
)
if self.transaction_currency == d.account_currency
else flt(d.debit, d.precision("debit")) / self.transaction_exchange_rate,
"credit_in_transaction_currency": flt(
d.credit_in_account_currency, d.precision("credit_in_account_currency")
)
if self.transaction_currency == d.account_currency
else flt(d.credit, d.precision("credit")) / self.transaction_exchange_rate,
"against_voucher_type": d.reference_type,
"against_voucher": d.reference_name,
"remarks": remarks,

View File

@@ -583,7 +583,7 @@ class TestJournalEntry(IntegrationTestCase):
order_by="account",
)
expected = [
{"account": "_Test Bank - _TC", "transaction_exchange_rate": 1.0},
{"account": "_Test Bank - _TC", "transaction_exchange_rate": 85.0},
{"account": "_Test Receivable USD - _TC", "transaction_exchange_rate": 85.0},
]
self.assertEqual(expected, actual)

View File

@@ -1311,15 +1311,22 @@ class PaymentEntry(AccountsController):
self.set("remarks", "\n".join(remarks))
def set_transaction_currency_and_rate(self):
company_currency = erpnext.get_company_currency(self.company)
self.transaction_currency = company_currency
self.transaction_exchange_rate = 1
if self.paid_from_account_currency != company_currency:
self.transaction_currency = self.paid_from_account_currency
self.transaction_exchange_rate = self.source_exchange_rate
elif self.paid_to_account_currency != company_currency:
self.transaction_currency = self.paid_to_account_currency
self.transaction_exchange_rate = self.target_exchange_rate
def build_gl_map(self):
if self.payment_type in ("Receive", "Pay") and not self.get("party_account_field"):
self.setup_party_account_field()
company_currency = erpnext.get_company_currency(self.company)
if self.paid_from_account_currency != company_currency:
self.currency = self.paid_from_account_currency
elif self.paid_to_account_currency != company_currency:
self.currency = self.paid_to_account_currency
self.set_transaction_currency_and_rate()
gl_entries = []
self.add_party_gl_entries(gl_entries)
@@ -1400,6 +1407,9 @@ class PaymentEntry(AccountsController):
"cost_center": cost_center,
dr_or_cr + "_in_account_currency": d.allocated_amount,
dr_or_cr: allocated_amount_in_company_currency,
dr_or_cr + "_in_transaction_currency": d.allocated_amount
if self.transaction_currency == self.party_account_currency
else allocated_amount_in_company_currency / self.transaction_exchange_rate,
},
item=self,
)
@@ -1444,6 +1454,9 @@ class PaymentEntry(AccountsController):
"account_currency": self.party_account_currency,
"cost_center": self.cost_center,
dr_or_cr + "_in_account_currency": self.unallocated_amount,
dr_or_cr + "_in_transaction_currency": self.unallocated_amount
if self.party_account_currency == self.transaction_currency
else base_unallocated_amount / self.transaction_exchange_rate,
dr_or_cr: base_unallocated_amount,
},
item=self,
@@ -1461,6 +1474,7 @@ class PaymentEntry(AccountsController):
def make_advance_gl_entries(
self, entry: object | dict = None, cancel: bool = 0, update_outstanding: str = "Yes"
):
self.set_transaction_currency_and_rate()
gl_entries = []
self.add_advance_gl_entries(gl_entries, entry)
@@ -1540,9 +1554,16 @@ class PaymentEntry(AccountsController):
frappe.db.set_value("Payment Entry Reference", invoice.name, "reconcile_effect_on", posting_date)
dr_or_cr, account = self.get_dr_and_account_for_advances(invoice)
base_allocated_amount = self.calculate_base_allocated_amount_for_reference(invoice)
args_dict["account"] = account
args_dict[dr_or_cr] = self.calculate_base_allocated_amount_for_reference(invoice)
args_dict[dr_or_cr] = base_allocated_amount
args_dict[dr_or_cr + "_in_account_currency"] = invoice.allocated_amount
args_dict[dr_or_cr + "_in_transaction_currency"] = (
invoice.allocated_amount
if self.party_account_currency == self.transaction_currency
else base_allocated_amount / self.transaction_exchange_rate
)
args_dict.update(
{
"against_voucher_type": invoice.reference_doctype,
@@ -1560,8 +1581,13 @@ class PaymentEntry(AccountsController):
args_dict[dr_or_cr + "_in_account_currency"] = 0
dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
args_dict["account"] = self.party_account
args_dict[dr_or_cr] = self.calculate_base_allocated_amount_for_reference(invoice)
args_dict[dr_or_cr] = base_allocated_amount
args_dict[dr_or_cr + "_in_account_currency"] = invoice.allocated_amount
args_dict[dr_or_cr + "_in_transaction_currency"] = (
invoice.allocated_amount
if self.party_account_currency == self.transaction_currency
else base_allocated_amount / self.transaction_exchange_rate
)
args_dict.update(
{
"against_voucher_type": "Payment Entry",
@@ -1583,6 +1609,9 @@ class PaymentEntry(AccountsController):
"account_currency": self.paid_from_account_currency,
"against": self.party if self.payment_type == "Pay" else self.paid_to,
"credit_in_account_currency": self.paid_amount,
"credit_in_transaction_currency": self.paid_amount
if self.paid_from_account_currency == self.transaction_currency
else self.base_paid_amount / self.transaction_exchange_rate,
"credit": self.base_paid_amount,
"cost_center": self.cost_center,
"post_net_value": True,
@@ -1598,6 +1627,9 @@ class PaymentEntry(AccountsController):
"account_currency": self.paid_to_account_currency,
"against": self.party if self.payment_type == "Receive" else self.paid_from,
"debit_in_account_currency": self.received_amount,
"debit_in_transaction_currency": self.received_amount
if self.paid_to_account_currency == self.transaction_currency
else self.base_received_amount / self.transaction_exchange_rate,
"debit": self.base_received_amount,
"cost_center": self.cost_center,
},
@@ -1633,6 +1665,8 @@ class PaymentEntry(AccountsController):
dr_or_cr + "_in_account_currency": base_tax_amount
if account_currency == self.company_currency
else d.tax_amount,
dr_or_cr + "_in_transaction_currency": base_tax_amount
/ self.transaction_exchange_rate,
"cost_center": d.cost_center,
"post_net_value": True,
},
@@ -1658,6 +1692,8 @@ class PaymentEntry(AccountsController):
rev_dr_or_cr + "_in_account_currency": base_tax_amount
if account_currency == self.company_currency
else d.tax_amount,
rev_dr_or_cr + "_in_transaction_currency": base_tax_amount
/ self.transaction_exchange_rate,
"cost_center": self.cost_center,
"post_net_value": True,
},
@@ -1680,6 +1716,7 @@ class PaymentEntry(AccountsController):
"account_currency": account_currency,
"against": self.party or self.paid_from,
"debit_in_account_currency": d.amount,
"debit_in_transaction_currency": d.amount / self.transaction_exchange_rate,
"debit": d.amount,
"cost_center": d.cost_center,
},
@@ -2950,7 +2987,9 @@ def get_payment_entry(
pe.paid_amount = paid_amount
pe.received_amount = received_amount
pe.letter_head = doc.get("letter_head")
pe.bank_account = frappe.db.get_value("Bank Account", {"is_company_account": 1, "is_default": 1}, "name")
pe.bank_account = frappe.db.get_value(
"Bank Account", {"is_company_account": 1, "is_default": 1, "company": doc.company}, "name"
)
if dt in ["Purchase Order", "Sales Order", "Sales Invoice", "Purchase Invoice"]:
pe.project = doc.get("project") or reduce(

View File

@@ -8,6 +8,7 @@ import frappe
from frappe import _
from frappe.model.document import Document
from frappe.model.mapper import map_child_doc, map_doc
from frappe.query_builder import DocType
from frappe.utils import cint, flt, get_time, getdate, nowdate, nowtime
from frappe.utils.background_jobs import enqueue, is_job_enqueued
from frappe.utils.scheduler import is_scheduler_inactive
@@ -119,17 +120,18 @@ class POSInvoiceMergeLog(Document):
returns = [d for d in pos_invoice_docs if d.get("is_return") == 1]
sales = [d for d in pos_invoice_docs if d.get("is_return") == 0]
sales_invoice, credit_note = "", ""
sales_invoice, credit_notes = "", {}
sales_invoice_doc = None
if sales:
sales_invoice_doc = self.process_merging_into_sales_invoice(sales)
sales_invoice = sales_invoice_doc.name
if returns:
credit_note = self.process_merging_into_credit_note(returns, sales_invoice_doc)
distinguished_returns = self.distinguish_return_pos_invoices(returns, sales_invoice_doc)
credit_notes = self.process_merging_into_credit_notes(distinguished_returns)
self.save() # save consolidated_sales_invoice & consolidated_credit_note ref in merge log
self.update_pos_invoices(pos_invoice_docs, sales_invoice, credit_note)
self.update_pos_invoices(pos_invoice_docs, sales_invoice, credit_notes)
def on_cancel(self):
pos_invoice_docs = [frappe.get_cached_doc("POS Invoice", d.pos_invoice) for d in self.pos_invoices]
@@ -159,34 +161,50 @@ class POSInvoiceMergeLog(Document):
return sales_invoice
def process_merging_into_credit_note(self, data, sales_invoice_doc=None):
credit_note = self.get_new_sales_invoice()
credit_note.is_return = 1
def process_merging_into_credit_notes(self, data):
credit_notes = {}
for key, value in data.items():
if not value:
continue
credit_note = self.merge_pos_invoice_into(credit_note, data)
referenes = {}
credit_note = self.get_new_sales_invoice()
credit_note.is_return = 1
if sales_invoice_doc:
credit_note.return_against = sales_invoice_doc.name
credit_note = self.merge_pos_invoice_into(credit_note, value)
credit_note.return_against = key
for d in sales_invoice_doc.items:
referenes[d.item_code] = d.name
credit_note.is_consolidated = 1
credit_note.set_posting_time = 1
credit_note.posting_date = getdate(self.posting_date)
credit_note.posting_time = get_time(self.posting_time)
# TODO: return could be against multiple sales invoice which could also have been consolidated?
# credit_note.return_against = self.consolidated_invoice
credit_note.save()
credit_note.submit()
for d in credit_note.items:
d.sales_invoice_item = referenes.get(d.item_code)
self.consolidated_credit_note = credit_note.name
credit_notes[credit_note.name] = [d.name for d in value]
credit_note.is_consolidated = 1
credit_note.set_posting_time = 1
credit_note.posting_date = getdate(self.posting_date)
credit_note.posting_time = get_time(self.posting_time)
# TODO: return could be against multiple sales invoice which could also have been consolidated?
# credit_note.return_against = self.consolidated_invoice
credit_note.save()
credit_note.submit()
return credit_notes
self.consolidated_credit_note = credit_note.name
def distinguish_return_pos_invoices(self, data, sales_invoice_doc=None):
return_invoices = {}
return credit_note.name
return_invoices[sales_invoice_doc.name if sales_invoice_doc else None] = []
for doc in data:
sales_invoices_of_return_against = frappe.db.get_value(
"POS Invoice", doc.return_against, "consolidated_invoice"
)
if sales_invoices_of_return_against:
if sales_invoices_of_return_against in return_invoices:
return_invoices[sales_invoices_of_return_against].append(doc)
else:
return_invoices[sales_invoices_of_return_against] = [doc]
else:
return_invoices[sales_invoice_doc.name if sales_invoice_doc else None].append(doc)
return return_invoices
def merge_pos_invoice_into(self, invoice, data):
items, payments, taxes = [], [], []
@@ -212,33 +230,20 @@ class POSInvoiceMergeLog(Document):
loyalty_amount_sum += doc.loyalty_amount
for item in doc.get("items"):
found = False
for i in items:
if (
i.item_code == item.item_code
and not i.serial_and_batch_bundle
and not i.serial_no
and not i.batch_no
and i.uom == item.uom
and i.net_rate == item.net_rate
and i.warehouse == item.warehouse
):
found = True
i.qty = i.qty + item.qty
i.amount = i.amount + item.net_amount
i.net_amount = i.amount
i.base_amount = i.base_amount + item.base_net_amount
i.base_net_amount = i.base_amount
if not found:
item.rate = item.net_rate
item.amount = item.net_amount
item.base_amount = item.base_net_amount
item.price_list_rate = 0
si_item = map_child_doc(item, invoice, {"doctype": "Sales Invoice Item"})
if item.serial_and_batch_bundle:
si_item.serial_and_batch_bundle = item.serial_and_batch_bundle
items.append(si_item)
item.rate = item.net_rate
item.amount = item.net_amount
item.base_amount = item.base_net_amount
item.price_list_rate = 0
si_item = map_child_doc(item, invoice, {"doctype": "Sales Invoice Item"})
si_item.pos_invoice = doc.name
si_item.pos_invoice_item = item.name
if doc.is_return:
si_item.sales_invoice_item = get_sales_invoice_item(
doc.return_against, item.pos_invoice_item
)
if item.serial_and_batch_bundle:
si_item.serial_and_batch_bundle = item.serial_and_batch_bundle
items.append(si_item)
for tax in doc.get("taxes"):
found = False
@@ -328,16 +333,16 @@ class POSInvoiceMergeLog(Document):
return sales_invoice
def update_pos_invoices(self, invoice_docs, sales_invoice="", credit_note=""):
def update_pos_invoices(self, invoice_docs, sales_invoice="", credit_notes=None):
for doc in invoice_docs:
doc.load_from_db()
doc.update(
{
"consolidated_invoice": None
if self.docstatus == 2
else (credit_note if doc.is_return else sales_invoice)
}
)
inv = sales_invoice
if doc.is_return:
for key, value in credit_notes.items():
if doc.name in value:
inv = key
break
doc.update({"consolidated_invoice": None if self.docstatus == 2 else inv})
doc.set_status(update=True)
doc.save()
@@ -628,3 +633,26 @@ def get_error_message(message) -> str:
return message["message"]
except Exception:
return str(message)
def get_sales_invoice_item(return_against_pos_invoice, pos_invoice_item):
try:
SalesInvoice = DocType("Sales Invoice")
SalesInvoiceItem = DocType("Sales Invoice Item")
query = (
frappe.qb.from_(SalesInvoice)
.from_(SalesInvoiceItem)
.select(SalesInvoiceItem.name)
.where(
(SalesInvoice.name == SalesInvoiceItem.parent)
& (SalesInvoice.is_return == 0)
& (SalesInvoiceItem.pos_invoice == return_against_pos_invoice)
& (SalesInvoiceItem.pos_invoice_item == pos_invoice_item)
)
)
result = query.run(as_dict=True)
return result[0].name if result else None
except Exception:
return None

View File

@@ -454,8 +454,7 @@ def get_pricing_rule_for_item(args, doc=None, for_validate=False):
if pricing_rule.coupon_code_based == 1:
if not args.coupon_code:
return item_details
continue
coupon_code = frappe.db.get_value(
doctype="Coupon Code", filters={"pricing_rule": pricing_rule.name}, fieldname="name"
)

View File

@@ -68,6 +68,14 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
if (this.frm.doc.supplier && this.frm.doc.__islocal) {
this.frm.trigger("supplier");
}
this.frm.set_query("supplier", function () {
return {
filters: {
is_transporter: 0,
},
};
});
}
refresh(doc) {

View File

@@ -873,6 +873,7 @@ class PurchaseInvoice(BuyingController):
self.make_payment_gl_entries(gl_entries)
self.make_write_off_gl_entry(gl_entries)
self.make_gle_for_rounding_adjustment(gl_entries)
self.set_transaction_currency_and_rate_in_gl_map(gl_entries)
return gl_entries
def check_asset_cwip_enabled(self):
@@ -918,6 +919,7 @@ class PurchaseInvoice(BuyingController):
"credit_in_account_currency": base_grand_total
if self.party_account_currency == self.company_currency
else grand_total,
"credit_in_transaction_currency": grand_total,
"against_voucher": against_voucher,
"against_voucher_type": self.doctype,
"project": self.project,
@@ -953,7 +955,7 @@ class PurchaseInvoice(BuyingController):
valuation_tax_accounts = [
d.account_head
for d in self.get("taxes")
if d.category in ("Valuation", "Total and Valuation")
if d.category in ("Valuation", "Valuation and Total")
and flt(d.base_tax_amount_after_discount_amount)
]
@@ -969,7 +971,6 @@ class PurchaseInvoice(BuyingController):
for item in self.get("items"):
if flt(item.base_net_amount):
account_currency = get_account_currency(item.expense_account)
if item.item_code:
frappe.get_cached_value("Item", item.item_code, "asset_category")
@@ -978,6 +979,7 @@ class PurchaseInvoice(BuyingController):
and self.auto_accounting_for_stock
and (item.item_code in stock_items or item.is_fixed_asset)
):
account_currency = get_account_currency(item.expense_account)
# warehouse account
warehouse_debit_amount = self.make_stock_adjustment_entry(
gl_entries, item, voucher_wise_stock_value, account_currency
@@ -993,6 +995,7 @@ class PurchaseInvoice(BuyingController):
"project": item.project or self.project,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"debit": warehouse_debit_amount,
"debit_in_transaction_currency": item.net_amount,
},
warehouse_account[item.warehouse]["account_currency"],
item=item,
@@ -1013,6 +1016,7 @@ class PurchaseInvoice(BuyingController):
"project": item.project or self.project,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"debit": -1 * flt(credit_amount, item.precision("base_net_amount")),
"debit_in_transaction_currency": item.net_amount,
},
warehouse_account[item.from_warehouse]["account_currency"],
item=item,
@@ -1027,6 +1031,7 @@ class PurchaseInvoice(BuyingController):
"account": item.expense_account,
"against": self.supplier,
"debit": flt(item.base_net_amount, item.precision("base_net_amount")),
"debit_in_transaction_currency": item.net_amount,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"cost_center": item.cost_center,
"project": item.project,
@@ -1044,6 +1049,10 @@ class PurchaseInvoice(BuyingController):
"account": item.expense_account,
"against": self.supplier,
"debit": warehouse_debit_amount,
"debit_in_transaction_currency": flt(
warehouse_debit_amount / self.conversion_rate,
item.precision("net_amount"),
),
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"cost_center": item.cost_center,
"project": item.project or self.project,
@@ -1056,7 +1065,9 @@ class PurchaseInvoice(BuyingController):
# Amount added through landed-cost-voucher
if landed_cost_entries:
if (item.item_code, item.name) in landed_cost_entries:
for account, amount in landed_cost_entries[(item.item_code, item.name)].items():
for account, base_amount in landed_cost_entries[
(item.item_code, item.name)
].items():
gl_entries.append(
self.get_gl_dict(
{
@@ -1064,8 +1075,9 @@ class PurchaseInvoice(BuyingController):
"against": item.expense_account,
"cost_center": item.cost_center,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"credit": flt(amount["base_amount"]),
"credit_in_account_currency": flt(amount["amount"]),
"credit": flt(base_amount["base_amount"]),
"credit_in_account_currency": flt(base_amount["amount"]),
"credit_in_transaction_currency": item.net_amount,
"project": item.project or self.project,
},
item=item,
@@ -1088,6 +1100,7 @@ class PurchaseInvoice(BuyingController):
"project": item.project or self.project,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"credit": flt(item.rm_supp_cost),
"credit_in_transaction_currency": item.net_amount,
},
warehouse_account[self.supplier_warehouse]["account_currency"],
item=item,
@@ -1101,7 +1114,8 @@ class PurchaseInvoice(BuyingController):
else item.deferred_expense_account
)
dummy, amount = self.get_amount_and_base_amount(item, None)
account_currency = get_account_currency(expense_account)
amount, base_amount = self.get_amount_and_base_amount(item, None)
if provisional_accounting_for_non_stock_items:
self.make_provisional_gl_entry(gl_entries, item)
@@ -1112,7 +1126,8 @@ class PurchaseInvoice(BuyingController):
{
"account": expense_account,
"against": self.supplier,
"debit": amount,
"debit": base_amount,
"debit_in_transaction_currency": amount,
"cost_center": item.cost_center,
"project": item.project or self.project,
},
@@ -1186,6 +1201,10 @@ class PurchaseInvoice(BuyingController):
"account": self.stock_received_but_not_billed,
"against": self.supplier,
"debit": flt(item.item_tax_amount, item.precision("item_tax_amount")),
"debit_in_transaction_currency": flt(
item.item_tax_amount / self.conversion_rate,
item.precision("item_tax_amount"),
),
"remarks": self.remarks or _("Accounting Entry for Stock"),
"cost_center": self.cost_center,
"project": item.project or self.project,
@@ -1305,6 +1324,7 @@ class PurchaseInvoice(BuyingController):
"account": cost_of_goods_sold_account,
"against": item.expense_account,
"debit": stock_adjustment_amt,
"debit_in_transaction_currency": item.net_amount,
"remarks": self.get("remarks") or _("Stock Adjustment"),
"cost_center": item.cost_center,
"project": item.project or self.project,
@@ -1338,6 +1358,7 @@ class PurchaseInvoice(BuyingController):
dr_or_cr + "_in_account_currency": base_amount
if account_currency == self.company_currency
else amount,
dr_or_cr + "_in_transaction_currency": amount,
"cost_center": tax.cost_center,
},
account_currency,
@@ -1384,6 +1405,10 @@ class PurchaseInvoice(BuyingController):
"cost_center": tax.cost_center,
"against": self.supplier,
"credit": applicable_amount,
"credit_in_transaction_currency": flt(
applicable_amount / self.conversion_rate,
frappe.get_precision("Purchase Invoice Item", "item_tax_amount"),
),
"remarks": self.remarks or _("Accounting Entry for Stock"),
},
item=tax,
@@ -1402,6 +1427,10 @@ class PurchaseInvoice(BuyingController):
"cost_center": tax.cost_center,
"against": self.supplier,
"credit": valuation_tax[tax.name],
"credit_in_transaction_currency": flt(
valuation_tax[tax.name] / self.conversion_rate,
frappe.get_precision("Purchase Invoice Item", "item_tax_amount"),
),
"remarks": self.remarks or _("Accounting Entry for Stock"),
},
item=tax,
@@ -1417,6 +1446,7 @@ class PurchaseInvoice(BuyingController):
"account": self.unrealized_profit_loss_account,
"against": self.supplier,
"credit": flt(self.total_taxes_and_charges),
"credit_in_transaction_currency": flt(self.total_taxes_and_charges),
"credit_in_account_currency": flt(self.base_total_taxes_and_charges),
"cost_center": self.cost_center,
},
@@ -1466,6 +1496,7 @@ class PurchaseInvoice(BuyingController):
"debit_in_account_currency": self.base_paid_amount
if self.party_account_currency == self.company_currency
else self.paid_amount,
"debit_in_transaction_currency": self.paid_amount,
"against_voucher": self.return_against
if cint(self.is_return) and self.return_against
else self.name,
@@ -1487,6 +1518,7 @@ class PurchaseInvoice(BuyingController):
"credit_in_account_currency": self.base_paid_amount
if bank_account_currency == self.company_currency
else self.paid_amount,
"credit_in_transaction_currency": self.paid_amount,
"cost_center": self.cost_center,
},
bank_account_currency,
@@ -1511,6 +1543,7 @@ class PurchaseInvoice(BuyingController):
"debit_in_account_currency": self.base_write_off_amount
if self.party_account_currency == self.company_currency
else self.write_off_amount,
"debit_in_transaction_currency": self.write_off_amount,
"against_voucher": self.return_against
if cint(self.is_return) and self.return_against
else self.name,
@@ -1531,6 +1564,7 @@ class PurchaseInvoice(BuyingController):
"credit_in_account_currency": self.base_write_off_amount
if write_off_account_currency == self.company_currency
else self.write_off_amount,
"credit_in_transaction_currency": self.write_off_amount,
"cost_center": self.cost_center or self.write_off_cost_center,
},
item=self,

View File

@@ -2656,6 +2656,50 @@ class TestPurchaseInvoice(IntegrationTestCase, StockTestMixin):
"Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice", original_value
)
def test_trx_currency_debit_credit_for_high_precision(self):
exc_rate = 0.737517516
pi = make_purchase_invoice(
currency="USD", conversion_rate=exc_rate, qty=1, rate=2000, do_not_save=True
)
pi.supplier = "_Test Supplier USD"
pi.save().submit()
expected = (
("_Test Account Cost for Goods Sold - _TC", 1475.04, 0.0, 2000.0, 0.0, "USD", exc_rate),
("_Test Payable USD - _TC", 0.0, 1475.04, 0.0, 2000.0, "USD", exc_rate),
)
actual = frappe.db.get_all(
"GL Entry",
filters={"voucher_no": pi.name},
fields=[
"account",
"debit",
"credit",
"debit_in_transaction_currency",
"credit_in_transaction_currency",
"transaction_currency",
"transaction_exchange_rate",
],
order_by="account",
as_list=1,
)
self.assertEqual(actual, expected)
def test_prevents_fully_returned_invoice_with_zero_quantity(self):
from erpnext.controllers.sales_and_purchase_return import StockOverReturnError, make_return_doc
invoice = make_purchase_invoice(qty=10)
return_doc = make_return_doc(invoice.doctype, invoice.name)
return_doc.items[0].qty = -10
return_doc.save().submit()
return_doc = make_return_doc(invoice.doctype, invoice.name)
return_doc.items[0].qty = 0
self.assertRaises(StockOverReturnError, return_doc.save)
def set_advance_flag(company, flag, default_account):
frappe.db.set_value(

View File

@@ -462,7 +462,8 @@
"depends_on": "eval:!doc.is_fixed_asset && doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
"fieldname": "serial_no",
"fieldtype": "Text",
"label": "Serial No"
"label": "Serial No",
"no_copy": 1
},
{
"depends_on": "eval:!doc.is_fixed_asset && doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
@@ -979,16 +980,18 @@
"options": "currency"
}
],
"grid_page_length": 50,
"idx": 1,
"istable": 1,
"links": [],
"modified": "2024-10-28 15:06:19.246141",
"modified": "2025-03-07 10:21:59.960021",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice Item",
"naming_rule": "Random",
"owner": "Administrator",
"permissions": [],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": []

View File

@@ -679,7 +679,13 @@ class SalesInvoice(SellingController):
"Account", self.debit_to, "account_currency", cache=True
)
if not self.due_date and self.customer:
self.due_date = get_due_date(self.posting_date, "Customer", self.customer, self.company)
self.due_date = get_due_date(
self.posting_date,
"Customer",
self.customer,
self.company,
template_name=self.payment_terms_template,
)
super().set_missing_values(for_validate)
@@ -1238,6 +1244,7 @@ class SalesInvoice(SellingController):
self.make_write_off_gl_entry(gl_entries)
self.make_gle_for_rounding_adjustment(gl_entries)
self.set_transaction_currency_and_rate_in_gl_map(gl_entries)
return gl_entries
def make_customer_gl_entry(self, gl_entries):
@@ -1271,6 +1278,7 @@ class SalesInvoice(SellingController):
"debit_in_account_currency": base_grand_total
if self.party_account_currency == self.company_currency
else grand_total,
"debit_in_transaction_currency": grand_total,
"against_voucher": against_voucher,
"against_voucher_type": self.doctype,
"cost_center": self.cost_center,
@@ -1302,6 +1310,9 @@ class SalesInvoice(SellingController):
if account_currency == self.company_currency
else flt(amount, tax.precision("tax_amount_after_discount_amount"))
),
"credit_in_transaction_currency": flt(
amount, tax.precision("tax_amount_after_discount_amount")
),
"cost_center": tax.cost_center,
},
account_currency,
@@ -1319,6 +1330,7 @@ class SalesInvoice(SellingController):
"against": self.customer,
"debit": flt(self.total_taxes_and_charges),
"debit_in_account_currency": flt(self.base_total_taxes_and_charges),
"debit_in_transaction_currency": flt(self.total_taxes_and_charges),
"cost_center": self.cost_center,
},
account_currency,
@@ -1417,6 +1429,7 @@ class SalesInvoice(SellingController):
if account_currency == self.company_currency
else flt(amount, item.precision("net_amount"))
),
"credit_in_transaction_currency": flt(amount, item.precision("net_amount")),
"cost_center": item.cost_center,
"project": item.project or self.project,
},
@@ -1468,6 +1481,7 @@ class SalesInvoice(SellingController):
+ cstr(self.loyalty_redemption_account)
+ " for the Loyalty Program",
"credit": self.loyalty_amount,
"credit_in_transaction_currency": self.loyalty_amount,
"against_voucher": self.return_against if cint(self.is_return) else self.name,
"against_voucher_type": self.doctype,
"cost_center": self.cost_center,
@@ -1482,6 +1496,7 @@ class SalesInvoice(SellingController):
"cost_center": self.cost_center or self.loyalty_redemption_cost_center,
"against": self.customer,
"debit": self.loyalty_amount,
"debit_in_transaction_currency": self.loyalty_amount,
"remark": "Loyalty Points redeemed by the customer",
},
item=self,
@@ -1515,6 +1530,7 @@ class SalesInvoice(SellingController):
"credit_in_account_currency": payment_mode.base_amount
if self.party_account_currency == self.company_currency
else payment_mode.amount,
"credit_in_transaction_currency": payment_mode.amount,
"against_voucher": against_voucher,
"against_voucher_type": self.doctype,
"cost_center": self.cost_center,
@@ -1534,6 +1550,7 @@ class SalesInvoice(SellingController):
"debit_in_account_currency": payment_mode.base_amount
if payment_mode_account_currency == self.company_currency
else payment_mode.amount,
"debit_in_transaction_currency": payment_mode.amount,
"cost_center": self.cost_center,
},
payment_mode_account_currency,
@@ -1562,6 +1579,7 @@ class SalesInvoice(SellingController):
"debit_in_account_currency": flt(self.base_change_amount)
if self.party_account_currency == self.company_currency
else flt(self.change_amount),
"debit_in_transaction_currency": flt(self.change_amount),
"against_voucher": self.return_against
if cint(self.is_return) and self.return_against
else self.name,
@@ -1577,6 +1595,7 @@ class SalesInvoice(SellingController):
"account": self.account_for_change_amount,
"against": self.customer,
"credit": self.base_change_amount,
"credit_in_transaction_currency": self.change_amount,
"cost_center": self.cost_center,
},
item=self,
@@ -1606,6 +1625,9 @@ class SalesInvoice(SellingController):
if self.party_account_currency == self.company_currency
else flt(self.write_off_amount, self.precision("write_off_amount"))
),
"credit_in_transaction_currency": flt(
self.write_off_amount, self.precision("write_off_amount")
),
"against_voucher": self.return_against if cint(self.is_return) else self.name,
"against_voucher_type": self.doctype,
"cost_center": self.cost_center,
@@ -1626,6 +1648,9 @@ class SalesInvoice(SellingController):
if write_off_account_currency == self.company_currency
else flt(self.write_off_amount, self.precision("write_off_amount"))
),
"debit_in_transaction_currency": flt(
self.write_off_amount, self.precision("write_off_amount")
),
"cost_center": self.cost_center or self.write_off_cost_center or default_cost_center,
},
write_off_account_currency,
@@ -1670,6 +1695,9 @@ class SalesInvoice(SellingController):
"credit_in_account_currency": flt(
self.rounding_adjustment, self.precision("rounding_adjustment")
),
"credit_in_transaction_currency": flt(
self.rounding_adjustment, self.precision("rounding_adjustment")
),
"credit": flt(
self.base_rounding_adjustment, self.precision("base_rounding_adjustment")
),

View File

@@ -4354,6 +4354,20 @@ class TestSalesInvoice(IntegrationTestCase):
pos_return = make_sales_return(pos.name)
self.assertEqual(abs(pos_return.payments[0].amount), pos.payments[0].amount)
def test_prevents_fully_returned_invoice_with_zero_quantity(self):
from erpnext.controllers.sales_and_purchase_return import StockOverReturnError, make_return_doc
invoice = create_sales_invoice(qty=10)
return_doc = make_return_doc(invoice.doctype, invoice.name)
return_doc.items[0].qty = -10
return_doc.save().submit()
return_doc = make_return_doc(invoice.doctype, invoice.name)
return_doc.items[0].qty = 0
self.assertRaises(StockOverReturnError, return_doc.save)
def set_advance_flag(company, flag, default_account):
frappe.db.set_value(

View File

@@ -106,6 +106,9 @@
"delivery_note",
"dn_detail",
"delivered_qty",
"column_break_vwhb",
"pos_invoice",
"pos_invoice_item",
"internal_transfer_section",
"purchase_order",
"column_break_92",
@@ -631,6 +634,7 @@
"fieldname": "serial_no",
"fieldtype": "Text",
"label": "Serial No",
"no_copy": 1,
"oldfieldname": "serial_no",
"oldfieldtype": "Small Text"
},
@@ -952,18 +956,42 @@
"no_copy": 1,
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "pos_invoice_item",
"fieldtype": "Data",
"ignore_user_permissions": 1,
"label": "POS Invoice Item",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "column_break_vwhb",
"fieldtype": "Column Break"
},
{
"fieldname": "pos_invoice",
"fieldtype": "Link",
"label": "POS Invoice",
"no_copy": 1,
"options": "POS Invoice",
"print_hide": 1,
"search_index": 1
}
],
"grid_page_length": 50,
"idx": 1,
"istable": 1,
"links": [],
"modified": "2024-11-25 16:27:33.287341",
"modified": "2025-03-07 10:25:30.275246",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice Item",
"naming_rule": "Random",
"owner": "Administrator",
"permissions": [],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": []

View File

@@ -71,6 +71,8 @@ class SalesInvoiceItem(Document):
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data
pos_invoice: DF.Link | None
pos_invoice_item: DF.Data | None
price_list_rate: DF.Currency
pricing_rules: DF.SmallText | None
project: DF.Link | None

View File

@@ -10,6 +10,7 @@ frappe.ui.form.on("Tax Withholding Category", {
filters: {
company: child.company,
root_type: ["in", ["Asset", "Liability"]],
is_group: 0,
},
};
}

View File

@@ -36,27 +36,38 @@ class TaxWithholdingCategory(Document):
def validate(self):
self.validate_dates()
self.validate_accounts()
self.validate_companies_and_accounts()
self.validate_thresholds()
def validate_dates(self):
last_date = None
for d in self.get("rates"):
last_to_date = None
rates = sorted(self.get("rates"), key=lambda d: getdate(d.from_date))
for d in rates:
if getdate(d.from_date) >= getdate(d.to_date):
frappe.throw(_("Row #{0}: From Date cannot be before To Date").format(d.idx))
# validate overlapping of dates
if last_date and getdate(d.to_date) < getdate(last_date):
if last_to_date and getdate(d.from_date) < getdate(last_to_date):
frappe.throw(_("Row #{0}: Dates overlapping with other row").format(d.idx))
def validate_accounts(self):
existing_accounts = []
last_to_date = d.to_date
def validate_companies_and_accounts(self):
existing_accounts = set()
companies = set()
for d in self.get("accounts"):
# validate duplicate company
if d.get("company") in companies:
frappe.throw(_("Company {0} added multiple times").format(frappe.bold(d.get("company"))))
companies.add(d.get("company"))
# validate duplicate account
if d.get("account") in existing_accounts:
frappe.throw(_("Account {0} added multiple times").format(frappe.bold(d.get("account"))))
validate_account_head(d.idx, d.get("account"), d.get("company"))
existing_accounts.append(d.get("account"))
existing_accounts.add(d.get("account"))
def validate_thresholds(self):
for d in self.get("rates"):

View File

@@ -529,7 +529,7 @@ class TestTaxWithholdingCategory(IntegrationTestCase):
payment = get_payment_entry(order.doctype, order.name)
payment.apply_tax_withholding_amount = 1
payment.tax_withholding_category = "Cumulative Threshold TDS"
payment.submit()
payment.save().submit()
self.assertEqual(payment.taxes[0].tax_amount, 4000)
def test_multi_category_single_supplier(self):

View File

@@ -431,7 +431,7 @@ def process_debit_credit_difference(gl_map):
voucher_no = gl_map[0].voucher_no
allowance = get_debit_credit_allowance(voucher_type, precision)
debit_credit_diff = get_debit_credit_difference(gl_map, precision)
debit_credit_diff, trx_cur_debit_credit_diff = get_debit_credit_difference(gl_map, precision)
if abs(debit_credit_diff) > allowance:
if not (
@@ -442,9 +442,9 @@ def process_debit_credit_difference(gl_map):
raise_debit_credit_not_equal_error(debit_credit_diff, voucher_type, voucher_no)
elif abs(debit_credit_diff) >= (1.0 / (10**precision)):
make_round_off_gle(gl_map, debit_credit_diff, precision)
make_round_off_gle(gl_map, debit_credit_diff, trx_cur_debit_credit_diff, precision)
debit_credit_diff = get_debit_credit_difference(gl_map, precision)
debit_credit_diff, trx_cur_debit_credit_diff = get_debit_credit_difference(gl_map, precision)
if abs(debit_credit_diff) > allowance:
if not (
voucher_type == "Journal Entry"
@@ -456,14 +456,23 @@ def process_debit_credit_difference(gl_map):
def get_debit_credit_difference(gl_map, precision):
debit_credit_diff = 0.0
trx_cur_debit_credit_diff = 0
for entry in gl_map:
entry.debit = flt(entry.debit, precision)
entry.credit = flt(entry.credit, precision)
debit_credit_diff += entry.debit - entry.credit
debit_credit_diff = flt(debit_credit_diff, precision)
entry.debit_in_transaction_currency = flt(entry.debit_in_transaction_currency, precision)
entry.credit_in_transaction_currency = flt(entry.credit_in_transaction_currency, precision)
trx_cur_debit_credit_diff += (
entry.debit_in_transaction_currency - entry.credit_in_transaction_currency
)
return debit_credit_diff
debit_credit_diff = flt(debit_credit_diff, precision)
trx_cur_debit_credit_diff = flt(trx_cur_debit_credit_diff, precision)
return debit_credit_diff, trx_cur_debit_credit_diff
def get_debit_credit_allowance(voucher_type, precision):
@@ -490,7 +499,7 @@ def has_opening_entries(gl_map: list) -> bool:
return False
def make_round_off_gle(gl_map, debit_credit_diff, precision):
def make_round_off_gle(gl_map, debit_credit_diff, trx_cur_debit_credit_diff, precision):
round_off_account, round_off_cost_center, round_off_for_opening = get_round_off_account_and_cost_center(
gl_map[0].company, gl_map[0].voucher_type, gl_map[0].voucher_no
)
@@ -535,6 +544,12 @@ def make_round_off_gle(gl_map, debit_credit_diff, precision):
"credit_in_account_currency": debit_credit_diff if debit_credit_diff > 0 else 0,
"debit": abs(debit_credit_diff) if debit_credit_diff < 0 else 0,
"credit": debit_credit_diff if debit_credit_diff > 0 else 0,
"debit_in_transaction_currency": abs(trx_cur_debit_credit_diff)
if trx_cur_debit_credit_diff < 0
else 0,
"credit_in_transaction_currency": trx_cur_debit_credit_diff
if trx_cur_debit_credit_diff > 0
else 0,
"cost_center": round_off_cost_center,
"party_type": None,
"party": None,

View File

@@ -577,12 +577,13 @@ def validate_party_accounts(doc):
@frappe.whitelist()
def get_due_date(posting_date, party_type, party, company=None, bill_date=None):
def get_due_date(posting_date, party_type, party, company=None, bill_date=None, template_name=None):
"""Get due date from `Payment Terms Template`"""
due_date = None
if (bill_date or posting_date) and party:
due_date = bill_date or posting_date
template_name = get_payment_terms_template(party, party_type, company)
if not template_name:
template_name = get_payment_terms_template(party, party_type, company)
if template_name:
due_date = get_due_date_from_template(template_name, posting_date, bill_date).strftime("%Y-%m-%d")

View File

@@ -52,7 +52,7 @@
{{ doc.address_display }}
</div>
<div class="col-xs-12">
{{ _("Conatct: ")+doc.contact_display if doc.contact_display else '' }}
{{ _("Contact: ")+doc.contact_display if doc.contact_display else '' }}
</div>
<div class="col-xs-12">
{{ _("Mobile: ")+doc.contact_mobile if doc.contact_mobile else '' }}

View File

@@ -38,23 +38,6 @@ class TestAccountsPayable(AccountsTestMixin, IntegrationTestCase):
self.assertEqual(data[1][0].get("outstanding"), 300)
self.assertEqual(data[1][0].get("currency"), "USD")
def test_account_payable_for_debit_note(self):
pi = self.create_purchase_invoice(do_not_submit=True)
pi.is_return = 1
pi.items[0].qty = -1
pi = pi.save().submit()
filters = {
"company": self.company,
"party_type": "Supplier",
"party": [self.supplier],
"report_date": today(),
"range": "30, 60, 90, 120",
}
data = execute(filters)
self.assertEqual(data[1][0].get("invoiced"), 300)
def create_purchase_invoice(self, do_not_submit=False):
frappe.set_user("Administrator")
pi = make_purchase_invoice(

View File

@@ -267,18 +267,6 @@ class ReceivablePayableReport:
row.invoiced_in_account_currency += amount_in_account_currency
else:
if self.is_invoice(ple):
# when invoice has is_return marked
if self.invoice_details.get(row.voucher_no, {}).get("is_return"):
# for Credit Note
if row.voucher_type == "Sales Invoice":
row.credit_note -= amount
row.credit_note_in_account_currency -= amount_in_account_currency
# for Debit Note
else:
row.invoiced -= amount
row.invoiced_in_account_currency -= amount_in_account_currency
return
if row.voucher_no == ple.voucher_no == ple.against_voucher_no:
row.paid -= amount
row.paid_in_account_currency -= amount_in_account_currency
@@ -433,7 +421,7 @@ class ReceivablePayableReport:
# nosemgrep
si_list = frappe.db.sql(
"""
select name, due_date, po_no, is_return
select name, due_date, po_no
from `tabSales Invoice`
where posting_date <= %s
and company = %s
@@ -465,7 +453,7 @@ class ReceivablePayableReport:
# nosemgrep
for pi in frappe.db.sql(
"""
select name, due_date, bill_no, bill_date, is_return
select name, due_date, bill_no, bill_date
from `tabPurchase Invoice`
where
posting_date <= %s
@@ -532,7 +520,7 @@ class ReceivablePayableReport:
ps.description, ps.paid_amount, ps.discounted_amount
from `tab{row.voucher_type}` si, `tabPayment Schedule` ps
where
si.name = ps.parent and
si.name = ps.parent and ps.parenttype = '{row.voucher_type}' and
si.name = %s and
si.is_return = 0
order by ps.paid_amount desc, due_date
@@ -741,11 +729,13 @@ class ReceivablePayableReport:
"company": self.filters.company,
"update_outstanding_for_self": 0,
}
or_filters = {}
for party_type in self.party_type:
if party_type := self.filters.party_type:
party_field = scrub(party_type)
if self.filters.get(party_field):
or_filters.update({party_field: self.filters.get(party_field)})
if parties := self.filters.get("party"):
or_filters.update({party_field: ["in", parties]})
self.return_entries = frappe._dict(
frappe.get_all(
doctype, filters=filters, or_filters=or_filters, fields=["name", "return_against"], as_list=1

View File

@@ -204,7 +204,7 @@ class TestAccountsReceivable(AccountsTestMixin, IntegrationTestCase):
expected_data_after_credit_note = [
[100.0, 100.0, 40.0, 0.0, 60.0, si.name],
[0, 0, 0, 100.0, -100.0, cr_note.name],
[0, 0, 100.0, 0.0, -100.0, cr_note.name],
]
self.assertEqual(len(report[1]), 2)
si_row = next(
@@ -478,19 +478,13 @@ class TestAccountsReceivable(AccountsTestMixin, IntegrationTestCase):
report = execute(filters)[1]
self.assertEqual(len(report), 2)
expected_data = {sr.name: [0.0, 10.0, -10.0, 0.0, -10], si.name: [100.0, 0.0, 100.0, 10.0, 90.0]}
expected_data = {sr.name: [10.0, -10.0, 0.0, -10], si.name: [100.0, 100.0, 10.0, 90.0]}
rows = report[:2]
for row in rows:
self.assertEqual(
expected_data[row.voucher_no],
[
row.invoiced or row.paid,
row.credit_note,
row.outstanding,
row.remaining_balance,
row.future_amount,
],
[row.invoiced or row.paid, row.outstanding, row.remaining_balance, row.future_amount],
)
pe.cancel()

View File

@@ -473,6 +473,16 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
}
}
onload() {
this.frm.set_query("supplier", function () {
return {
filters: {
is_transporter: 0,
},
};
});
}
get_items_from_open_material_requests() {
erpnext.utils.map_current_doc({
method: "erpnext.stock.doctype.material_request.material_request.make_purchase_order_based_on_supplier",

View File

@@ -933,6 +933,7 @@
},
{
"allow_on_submit": 1,
"depends_on": "eval:parent.is_subcontracted && !parent.is_old_subcontracting_flow",
"fieldname": "subcontracted_quantity",
"fieldtype": "Float",
"label": "Subcontracted Quantity",
@@ -946,7 +947,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2025-03-02 16:58:26.059601",
"modified": "2025-03-13 17:27:43.468602",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order Item",
@@ -960,4 +961,4 @@
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -274,6 +274,7 @@ class AccountsController(TransactionBase):
self.set_total_in_words()
self.set_default_letter_head()
self.validate_company_in_accounting_dimension()
self.validate_party_address_and_contact()
def set_default_letter_head(self):
if hasattr(self, "letter_head") and not self.letter_head:
@@ -445,6 +446,45 @@ class AccountsController(TransactionBase):
)
)
def validate_party_address_and_contact(self):
party, party_type = None, None
if self.get("customer"):
party, party_type = self.customer, "Customer"
billing_address, shipping_address = (
self.get("customer_address"),
self.get("shipping_address_name"),
)
self.validate_party_address(party, party_type, billing_address, shipping_address)
elif self.get("supplier"):
party, party_type = self.supplier, "Supplier"
billing_address = self.get("supplier_address")
self.validate_party_address(party, party_type, billing_address)
if party and party_type:
self.validate_party_contact(party, party_type)
def validate_party_address(self, party, party_type, billing_address, shipping_address=None):
if billing_address or shipping_address:
party_address = frappe.get_all(
"Dynamic Link",
{"link_doctype": party_type, "link_name": party, "parenttype": "Address"},
pluck="parent",
)
if billing_address and billing_address not in party_address:
frappe.throw(_("Billing Address does not belong to the {0}").format(party))
elif shipping_address and shipping_address not in party_address:
frappe.throw(_("Shipping Address does not belong to the {0}").format(party))
def validate_party_contact(self, party, party_type):
if self.get("contact_person"):
contact = frappe.get_all(
"Dynamic Link",
{"link_doctype": party_type, "link_name": party, "parenttype": "Contact"},
pluck="parent",
)
if self.contact_person and self.contact_person not in contact:
frappe.throw(_("Contact Person does not belong to the {0}").format(party))
def validate_return_against_account(self):
if self.doctype in ["Sales Invoice", "Purchase Invoice"] and self.is_return and self.return_against:
cr_dr_account_field = "debit_to" if self.doctype == "Sales Invoice" else "credit_to"
@@ -1125,20 +1165,19 @@ class AccountsController(TransactionBase):
)
# Update details in transaction currency
gl_dict.update(
{
"transaction_currency": self.get("currency") or self.company_currency,
"transaction_exchange_rate": item.get("exchange_rate", 1)
if self.doctype == "Journal Entry" and item
else self.get("conversion_rate", 1),
"debit_in_transaction_currency": self.get_value_in_transaction_currency(
account_currency, gl_dict, "debit"
),
"credit_in_transaction_currency": self.get_value_in_transaction_currency(
account_currency, gl_dict, "credit"
),
}
)
if self.doctype not in ["Purchase Invoice", "Sales Invoice", "Journal Entry", "Payment Entry"]:
gl_dict.update(
{
"transaction_currency": self.get("currency") or self.company_currency,
"transaction_exchange_rate": self.get("conversion_rate", 1),
"debit_in_transaction_currency": self.get_value_in_transaction_currency(
account_currency, gl_dict, "debit"
),
"credit_in_transaction_currency": self.get_value_in_transaction_currency(
account_currency, gl_dict, "credit"
),
}
)
if not args.get("against_voucher_type") and self.get("against_voucher_type"):
gl_dict.update({"against_voucher_type": self.get("against_voucher_type")})
@@ -2782,6 +2821,11 @@ class AccountsController(TransactionBase):
elif self.doctype == "Payment Entry":
self.make_advance_payment_ledger_for_payment()
def set_transaction_currency_and_rate_in_gl_map(self, gl_entries):
for x in gl_entries:
x["transaction_currency"] = self.currency
x["transaction_exchange_rate"] = self.get("conversion_rate") or 1
@frappe.whitelist()
def get_tax_rate(account_head):

View File

@@ -224,14 +224,18 @@ class BuyingController(SubcontractingController):
for item in self.get("items"):
if item.get("from_warehouse") and (item.get("from_warehouse") == item.get("warehouse")):
frappe.throw(
_("Row #{0}: Accepted Warehouse and Supplier Warehouse cannot be same").format(item.idx)
_("Row #{idx}: {from_warehouse_field} and {to_warehouse_field} cannot be same.").format(
idx=item.idx,
from_warehouse_field=_(item.meta.get_label("from_warehouse")),
to_warehouse_field=_(item.meta.get_label("warehouse")),
)
)
if item.get("from_warehouse") and self.get("is_subcontracted"):
frappe.throw(
_(
"Row #{0}: Cannot select Supplier Warehouse while suppling raw materials to subcontractor"
).format(item.idx)
"Row #{idx}: Cannot select Supplier Warehouse while suppling raw materials to subcontractor."
).format(idx=item.idx)
)
def set_supplier_address(self):
@@ -376,8 +380,8 @@ class BuyingController(SubcontractingController):
d.rate = d.sales_incoming_rate
frappe.msgprint(
_(
"Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer"
).format(d.idx),
"Row #{idx}: Item rate has been updated as per valuation rate since its an internal stock transfer."
).format(idx=d.idx),
alert=1,
)
@@ -427,17 +431,28 @@ class BuyingController(SubcontractingController):
def validate_for_subcontracting(self):
if self.is_subcontracted and self.get("is_old_subcontracting_flow"):
if self.doctype in ["Purchase Receipt", "Purchase Invoice"] and not self.supplier_warehouse:
frappe.throw(_("Supplier Warehouse mandatory for sub-contracted {0}").format(self.doctype))
frappe.throw(
_("{field_label} is mandatory for sub-contracted {doctype}.").format(
field_label=_(self.meta.get_label("supplier_warehouse")), doctype=_(self.doctype)
)
)
for item in self.get("items"):
if item in self.sub_contracted_items and not item.bom:
frappe.throw(_("Please select BOM in BOM field for Item {0}").format(item.item_code))
frappe.throw(
_("Please select BOM in BOM field for Item {item_code}.").format(
item_code=frappe.bold(item.item_code)
)
)
if self.doctype != "Purchase Order":
return
for row in self.get("supplied_items"):
if not row.reserve_warehouse:
msg = f"Reserved Warehouse is mandatory for the Item {frappe.bold(row.rm_item_code)} in Raw Materials supplied"
frappe.throw(_(msg))
frappe.throw(
_(
"Reserved Warehouse is mandatory for the Item {item_code} in Raw Materials supplied."
).format(item_code=frappe.bold(row.rm_item_code))
)
else:
for item in self.get("items"):
if item.get("bom"):
@@ -453,7 +468,12 @@ class BuyingController(SubcontractingController):
# Check if item code is present
# Conversion factor should not be mandatory for non itemized items
if not d.conversion_factor and d.item_code:
frappe.throw(_("Row {0}: Conversion Factor is mandatory").format(d.idx))
frappe.throw(
_("Row #{idx}: {field_label} is mandatory.").format(
idx=d.idx,
field_label=_(d.meta.get_label("conversion_factor")),
)
)
d.stock_qty = flt(d.qty) * flt(d.conversion_factor)
if self.doctype == "Purchase Receipt" and d.meta.get_field("received_stock_qty"):
@@ -470,7 +490,12 @@ class BuyingController(SubcontractingController):
def validate_purchase_return(self):
for d in self.get("items"):
if self.is_return and flt(d.rejected_qty) != 0:
frappe.throw(_("Row #{0}: Rejected Qty can not be entered in Purchase Return").format(d.idx))
frappe.throw(
_("Row #{idx}: {field_label} is not allowed in Purchase Return.").format(
idx=d.idx,
field_label=_(d.meta.get_label("rejected_qty")),
)
)
# validate rate with ref PR
@@ -486,8 +511,8 @@ class BuyingController(SubcontractingController):
val = flt(d.qty) + flt(d.rejected_qty)
if flt(val, d.precision("received_qty")) != flt(d.received_qty, d.precision("received_qty")):
message = _(
"Row #{0}: Received Qty must be equal to Accepted + Rejected Qty for Item {1}"
).format(d.idx, d.item_code)
"Row #{idx}: Received Qty must be equal to Accepted + Rejected Qty for Item {item_code}."
).format(idx=d.idx, item_code=frappe.bold(d.item_code))
frappe.throw(msg=message, title=_("Mismatch"), exc=QtyMismatchError)
def validate_negative_quantity(self, item_row, field_list):
@@ -498,10 +523,10 @@ class BuyingController(SubcontractingController):
for fieldname in field_list:
if flt(item_row[fieldname]) < 0:
frappe.throw(
_("Row #{0}: {1} can not be negative for item {2}").format(
item_row["idx"],
frappe.get_meta(item_row.doctype).get_label(fieldname),
item_row["item_code"],
_("Row #{idx}: {field_label} can not be negative for item {item_code}.").format(
idx=item_row["idx"],
field_label=frappe.get_meta(item_row.doctype).get_label(fieldname),
item_code=frappe.bold(item_row["item_code"]),
)
)
@@ -510,7 +535,13 @@ class BuyingController(SubcontractingController):
if d.get(ref_fieldname):
status = frappe.db.get_value(ref_doctype, d.get(ref_fieldname), "status")
if status in ("Closed", "On Hold"):
frappe.throw(_("{0} {1} is {2}").format(ref_doctype, d.get(ref_fieldname), status))
frappe.throw(
_("{ref_doctype} {ref_name} is {status}.").format(
ref_doctype=frappe.bold(_(ref_doctype)),
ref_name=frappe.bold(d.get(ref_fieldname)),
status=frappe.bold(_(status)),
)
)
def update_stock_ledger(self, allow_negative_stock=False, via_landed_cost_voucher=False):
self.update_ordered_and_reserved_qty()
@@ -675,7 +706,10 @@ class BuyingController(SubcontractingController):
if po_obj.status in ["Closed", "Cancelled"]:
frappe.throw(
_("{0} {1} is cancelled or closed").format(_("Purchase Order"), po),
_("{doctype} {name} is cancelled or closed.").format(
doctype=frappe.bold(_("Purchase Order")),
name=frappe.bold(po),
),
frappe.InvalidStatusError,
)
@@ -768,8 +802,8 @@ class BuyingController(SubcontractingController):
if len(created_assets) > 5:
# dont show asset form links if more than 5 assets are created
messages.append(
_("{} Assets created for {}").format(
len(created_assets), frappe.bold(d.item_code)
_("{count} Assets created for {item_code}").format(
count=len(created_assets), item_code=frappe.bold(d.item_code)
)
)
else:
@@ -778,25 +812,28 @@ class BuyingController(SubcontractingController):
)
assets_link = frappe.bold(",".join(assets_link))
is_plural = "s" if len(created_assets) != 1 else ""
messages.append(
_("Asset{is_plural} {assets_link} created for {item_code}").format(
is_plural=is_plural,
if len(created_assets) == 1:
msg = _("Asset {assets_link} created for {item_code}").format(
assets_link=assets_link,
item_code=frappe.bold(d.item_code),
)
)
else:
msg = _("Assets {assets_link} created for {item_code}").format(
assets_link=assets_link,
item_code=frappe.bold(d.item_code),
)
messages.append(msg)
else:
frappe.throw(
_(
"Row {}: Asset Naming Series is mandatory for the auto creation for item {}"
).format(d.idx, frappe.bold(d.item_code))
"Row {idx}: Asset Naming Series is mandatory for the auto creation of assets for item {item_code}."
).format(idx=d.idx, item_code=frappe.bold(d.item_code))
)
else:
messages.append(
_("Assets not created for {0}. You will have to create asset manually.").format(
frappe.bold(d.item_code)
)
_(
"Assets not created for {item_code}. You will have to create asset manually."
).format(item_code=frappe.bold(d.item_code))
)
alert = True
@@ -805,7 +842,12 @@ class BuyingController(SubcontractingController):
def make_asset(self, row, accounting_dimensions, is_grouped_asset=False):
if not row.asset_location:
frappe.throw(_("Row {0}: Enter location for the asset item {1}").format(row.idx, row.item_code))
frappe.throw(
_("Row #{idx}: Please enter a location for the asset item {item_code}.").format(
idx=row.idx,
item_code=frappe.bold(row.item_code),
)
)
item_data = frappe.get_cached_value(
"Item", row.item_code, ["asset_naming_series", "asset_category"], as_dict=1
@@ -880,8 +922,8 @@ class BuyingController(SubcontractingController):
if asset.docstatus == 1 and delete_asset:
frappe.throw(
_(
"Cannot cancel this document as it is linked with submitted asset {0}. Please cancel it to continue."
).format(frappe.utils.get_link_to_form("Asset", asset.name))
"Cannot cancel this document as it is linked with the submitted asset {asset_link}. Please cancel the asset to continue."
).format(asset_link=frappe.utils.get_link_to_form("Asset", asset.name))
)
asset.flags.ignore_validate_update_after_submit = True
@@ -918,9 +960,19 @@ class BuyingController(SubcontractingController):
and self.transaction_date
and getdate(d.schedule_date) < getdate(self.transaction_date)
):
frappe.throw(_("Row #{0}: Reqd by Date cannot be before Transaction Date").format(d.idx))
frappe.throw(
_("Row #{idx}: {schedule_date} cannot be before {transaction_date}.").format(
idx=d.idx,
schedule_date=_(self.meta.get_label("schedule_date")),
transaction_date=_(self.meta.get_label("transaction_date")),
)
)
else:
frappe.throw(_("Please enter Reqd by Date"))
frappe.throw(
_("Please enter the {schedule_date}.").format(
schedule_date=_(self.meta.get_label("schedule_date"))
)
)
def validate_items(self):
# validate items to see if they have is_purchase_item or is_subcontracted_item enabled
@@ -970,12 +1022,18 @@ def validate_item_type(doc, fieldname, message):
if len(invalid_items) > 1:
error_message = _(
"Following items {0} are not marked as {1} item. You can enable them as {1} item from its Item master"
).format(items, message)
"The items {items} are not marked as {type_of} item. You can enable them as {type_of} item from their Item masters."
).format(
items=items,
type_of=message,
)
else:
error_message = _(
"Following item {0} is not marked as {1} item. You can enable them as {1} item from its Item master"
).format(items, message)
"The item {item} is not marked as {type_of} item. You can enable it as {type_of} item from its Item master."
).format(
item=items,
type_of=message,
)
frappe.throw(error_message)

View File

@@ -25,9 +25,6 @@ def validate_return(doc):
if doc.return_against:
validate_return_against(doc)
if doc.doctype in ("Sales Invoice", "Purchase Invoice") and not doc.update_stock:
return
validate_returned_items(doc)
@@ -118,7 +115,7 @@ def validate_returned_items(doc):
elif doc.doctype == "Delivery Note":
key = (d.item_code, d.get("dn_detail"))
if d.item_code and (flt(d.qty) < 0 or flt(d.get("received_qty")) < 0):
if d.item_code and (flt(d.qty) <= 0 or flt(d.get("received_qty")) <= 0):
if key not in valid_items:
frappe.msgprint(
_("Row # {0}: Returned Item {1} does not exist in {2} {3}").format(
@@ -160,6 +157,9 @@ def validate_returned_items(doc):
def validate_quantity(doc, key, args, ref, valid_items, already_returned_items):
fields = ["stock_qty"]
if (doc.doctype == "Purchase Invoice" or doc.doctype == "Sales Invoice") and not doc.update_stock:
fields = ["qty"]
if doc.doctype in ["Purchase Receipt", "Purchase Invoice", "Subcontracting Receipt"]:
if not args.get("return_qty_from_rejected_warehouse"):
fields.extend(["received_qty", "rejected_qty"])
@@ -169,13 +169,16 @@ def validate_quantity(doc, key, args, ref, valid_items, already_returned_items):
already_returned_data = already_returned_items.get(key) or {}
company_currency = erpnext.get_company_currency(doc.company)
stock_qty_precision = get_field_precision(
frappe.get_meta(doc.doctype + " Item").get_field("stock_qty"), company_currency
field_precision = get_field_precision(
frappe.get_meta(doc.doctype + " Item").get_field(
"stock_qty" if doc.get("update_stock", "") else "qty"
),
company_currency,
)
for column in fields:
returned_qty = (
flt(already_returned_data.get(column, 0), stock_qty_precision)
flt(already_returned_data.get(column, 0), field_precision)
if len(already_returned_data) > 0
else 0
)
@@ -190,17 +193,17 @@ def validate_quantity(doc, key, args, ref, valid_items, already_returned_items):
reference_qty = ref.get(column) * ref.get("conversion_factor", 1.0)
current_stock_qty = args.get(column) * args.get("conversion_factor", 1.0)
max_returnable_qty = flt(flt(reference_qty, stock_qty_precision) - returned_qty, stock_qty_precision)
max_returnable_qty = flt(flt(reference_qty, field_precision) - returned_qty, field_precision)
label = column.replace("_", " ").title()
if reference_qty:
if flt(args.get(column)) > 0:
frappe.throw(_("{0} must be negative in return document").format(label))
elif returned_qty >= reference_qty and args.get(column):
elif returned_qty >= reference_qty and args.get(column) >= 0:
frappe.throw(
_("Item {0} has already been returned").format(args.item_code), StockOverReturnError
)
elif abs(flt(current_stock_qty, stock_qty_precision)) > max_returnable_qty:
elif abs(flt(current_stock_qty, field_precision)) > max_returnable_qty:
frappe.throw(
_("Row # {0}: Cannot return more than {1} for Item {2}").format(
args.idx, max_returnable_qty, args.item_code

View File

@@ -220,7 +220,9 @@ class StatusUpdater(Document):
}
"""
if self.doctype not in status_map:
return {"status": self.status}
return {
"status": self.get("status")
} # sometimes status field is not present on certain DocTypes such as Stock Entry
sl = status_map[self.doctype][:]
sl.reverse()
@@ -562,7 +564,8 @@ class StatusUpdater(Document):
target = frappe.get_doc(args["target_parent_dt"], args["name"])
target.update(update_data) # status calculus might depend on it
status = target.get_status()
update_data.update(status)
if status.get("status"):
update_data.update(status)
target.db_set(update_data, update_modified=update_modified, notify=True)
def _update_modified(self, args, update_modified):

View File

@@ -2179,3 +2179,59 @@ class TestAccountsController(IntegrationTestCase):
si_1 = create_sales_invoice(do_not_submit=True)
si_1.items[0].project = project.name
self.assertRaises(frappe.ValidationError, si_1.save)
def test_party_billing_and_shipping_address(self):
from erpnext.crm.doctype.prospect.test_prospect import make_address
customer_billing = make_address(address_title="Customer")
customer_billing.append("links", {"link_doctype": "Customer", "link_name": "_Test Customer"})
customer_billing.save()
supplier_billing = make_address(address_title="Supplier", address_line1="2", city="Ahmedabad")
supplier_billing.append("links", {"link_doctype": "Supplier", "link_name": "_Test Supplier"})
supplier_billing.save()
customer_shipping = make_address(
address_title="Customer", address_type="Shipping", address_line1="10"
)
customer_shipping.append("links", {"link_doctype": "Customer", "link_name": "_Test Customer"})
customer_shipping.save()
supplier_shipping = make_address(
address_title="Supplier", address_type="Shipping", address_line1="20", city="Ahmedabad"
)
supplier_shipping.append("links", {"link_doctype": "Supplier", "link_name": "_Test Supplier"})
supplier_shipping.save()
si = create_sales_invoice(do_not_save=True)
si.customer_address = supplier_billing.name
self.assertRaises(frappe.ValidationError, si.save)
si.customer_address = customer_billing.name
si.save()
si.shipping_address_name = supplier_shipping.name
self.assertRaises(frappe.ValidationError, si.save)
si.shipping_address_name = customer_shipping.name
si.reload()
si.save()
pi = make_purchase_invoice(do_not_save=True)
pi.supplier_address = customer_shipping.name
self.assertRaises(frappe.ValidationError, pi.save)
pi.supplier_address = supplier_shipping.name
pi.save()
def test_party_contact(self):
from frappe.contacts.doctype.contact.test_contact import create_contact
customer_contact = create_contact(name="Customer", salutation="Mr", save=False)
customer_contact.append("links", {"link_doctype": "Customer", "link_name": "_Test Customer"})
customer_contact.save()
supplier_contact = create_contact(name="Supplier", salutation="Mr", save=False)
supplier_contact.append("links", {"link_doctype": "Supplier", "link_name": "_Test Supplier"})
supplier_contact.save()
si = create_sales_invoice(do_not_save=True)
si.contact_person = supplier_contact.name
self.assertRaises(frappe.ValidationError, si.save)
si.contact_person = customer_contact.name
si.save()

File diff suppressed because it is too large Load Diff

60544
erpnext/locale/ar_SA.po Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

60661
erpnext/locale/bs_BA.po Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

60663
erpnext/locale/de_DE.po Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

60544
erpnext/locale/eo_UY.po Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

60651
erpnext/locale/es_ES.po Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

60558
erpnext/locale/fa_IR.po Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

60568
erpnext/locale/fr_FR.po Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

60544
erpnext/locale/hr_HR.po Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

60544
erpnext/locale/hu_HU.po Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

60595
erpnext/locale/pl_PL.po Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

60544
erpnext/locale/ru_RU.po Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

60667
erpnext/locale/sv_SE.po Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

60544
erpnext/locale/th_TH.po Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

60667
erpnext/locale/tr_TR.po Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

60648
erpnext/locale/zh_CN.po Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -217,7 +217,7 @@ frappe.ui.form.on("Job Card", {
label: __("Completed Quantity"),
fieldname: "qty",
reqd: 1,
default: frm.doc.for_quantity - frm.doc.manufactured_qty,
default: frm.doc.for_quantity - frm.doc.total_completed_qty,
},
{
fieldtype: "Datetime",

View File

@@ -426,6 +426,7 @@ class ProductionPlan(Document):
mr_item.item_code,
mr_item.warehouse,
mr_item.description,
mr_item.bom_no,
((mr_item.qty - mr_item.ordered_qty) * mr_item.conversion_factor).as_("pending_qty"),
)
.distinct()

View File

@@ -193,7 +193,7 @@ class WorkOrder(Document):
if self.source_warehouse:
self.set_warehouses()
validate_uom_is_integer(self, "stock_uom", ["qty", "produced_qty"])
validate_uom_is_integer(self, "stock_uom", ["required_qty"])
self.set_required_items(reset_only_qty=len(self.get("required_items")))
self.enable_auto_reserve_stock()

View File

@@ -51,9 +51,9 @@ def get_periodic_data(filters, entry):
]:
if d.status in ["Not Started", "Closed", "Stopped"]:
periodic_data = update_periodic_data(periodic_data, d.status, period)
elif today() > getdate(d.planned_end_date):
elif getdate(today()) > getdate(d.planned_end_date):
periodic_data = update_periodic_data(periodic_data, "Overdue", period)
elif today() < getdate(d.planned_end_date):
elif getdate(today()) < getdate(d.planned_end_date):
periodic_data = update_periodic_data(periodic_data, "Pending", period)
if (

View File

@@ -404,7 +404,7 @@ erpnext.patches.v14_0.disable_add_row_in_gross_profit
erpnext.patches.v14_0.update_posting_datetime
erpnext.patches.v15_0.rename_field_from_rate_difference_to_amount_difference
erpnext.patches.v15_0.recalculate_amount_difference_field
erpnext.patches.v15_0.rename_sla_fields
erpnext.patches.v15_0.rename_sla_fields #2025-03-12
erpnext.stock.doctype.stock_ledger_entry.patches.ensure_sle_indexes
erpnext.patches.v15_0.update_query_report
erpnext.patches.v15_0.set_purchase_receipt_row_item_to_capitalization_stock_item

View File

@@ -62,6 +62,14 @@ def execute():
):
posting_date = period_closing_voucher[0].period_end_date
acc_frozen_upto = frappe.db.get_single_value("Accounts Settings", "acc_frozen_upto")
if acc_frozen_upto and getdate(acc_frozen_upto) > getdate(posting_date):
posting_date = acc_frozen_upto
stock_frozen_upto = frappe.db.get_single_value("Stock Settings", "stock_frozen_upto")
if stock_frozen_upto and getdate(stock_frozen_upto) > getdate(posting_date):
posting_date = stock_frozen_upto
fiscal_year = get_fiscal_year(frappe.utils.datetime.date.today(), raise_on_missing=False)
if fiscal_year and getdate(fiscal_year[1]) > getdate(posting_date):
posting_date = fiscal_year[1]

View File

@@ -4,15 +4,19 @@ from frappe.model.utils.rename_field import rename_field
def execute():
doctypes = frappe.get_all("Service Level Agreement", pluck="document_type")
doctypes = frappe.get_all("Service Level Agreement", pluck="document_type", distinct=True)
for doctype in doctypes:
if doctype == "Issue":
continue
if frappe.db.exists("Custom Field", {"fieldname": doctype + "-resolution_by"}):
if frappe.db.exists(
"Custom Field", {"name": doctype + "-resolution_by", "fieldname": "resolution_by"}
):
rename_fieldname(doctype + "-resolution_by", "sla_resolution_by")
if frappe.db.exists("Custom Field", {"fieldname": doctype + "-resolution_date"}):
if frappe.db.exists(
"Custom Field", {"name": doctype + "-resolution_date", "fieldname": "resolution_date"}
):
rename_fieldname(doctype + "-resolution_date", "sla_resolution_date")
rename_field("Issue", "resolution_by", "sla_resolution_by")

View File

@@ -1,5 +1,6 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "Prompt",
"creation": "2019-02-18 17:23:11.708371",
"doctype": "DocType",
@@ -7,6 +8,7 @@
"engine": "InnoDB",
"field_order": [
"project_type",
"disabled",
"tasks"
],
"fields": [
@@ -23,13 +25,20 @@
"label": "Tasks",
"options": "Project Template Task",
"reqd": 1
},
{
"default": "0",
"fieldname": "disabled",
"fieldtype": "Check",
"label": "Disabled"
}
],
"links": [],
"modified": "2024-03-27 13:10:21.325199",
"modified": "2025-03-12 14:20:57.301906",
"modified_by": "Administrator",
"module": "Projects",
"name": "Project Template",
"naming_rule": "Set by user",
"owner": "Administrator",
"permissions": [
{
@@ -46,6 +55,7 @@
}
],
"quick_entry": 1,
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],

View File

@@ -21,6 +21,7 @@ class ProjectTemplate(Document):
ProjectTemplateTask,
)
disabled: DF.Check
project_type: DF.Link | None
tasks: DF.Table[ProjectTemplateTask]
# end: auto-generated types

View File

@@ -262,12 +262,11 @@ def sales_invoice_validate(doc):
doc.company_tax_id = frappe.get_cached_value("Company", doc.company, "tax_id")
doc.company_fiscal_code = frappe.get_cached_value("Company", doc.company, "fiscal_code")
if not doc.company_tax_id and not doc.company_fiscal_code:
if not doc.company_tax_id or not doc.company_fiscal_code:
frappe.throw(
_("Please set either the Tax ID or Fiscal Code on Company '%s'" % doc.company),
_("Please set both the Tax ID and Fiscal Code on Company {0}").format(doc.company),
title=_("E-Invoicing Information Missing"),
)
# Validate customer details
customer = frappe.get_doc("Customer", doc.customer)

View File

@@ -892,6 +892,7 @@ def make_material_request(source_name, target_doc=None):
"name": "sales_order_item",
"parent": "sales_order",
"delivery_date": "required_by",
"bom_no": "bom_no",
},
"condition": lambda item: not frappe.db.exists(
"Product Bundle", {"name": item.item_code, "disabled": 0}

View File

@@ -449,6 +449,7 @@ erpnext.PointOfSale.Controller = class {
init_order_summary() {
this.order_summary = new erpnext.PointOfSale.PastOrderSummary({
wrapper: this.$components_wrapper,
settings: this.settings,
events: {
get_frm: () => this.frm,
@@ -489,7 +490,6 @@ erpnext.PointOfSale.Controller = class {
]);
},
},
pos_profile: this.pos_profile,
});
}

View File

@@ -1,8 +1,8 @@
erpnext.PointOfSale.PastOrderSummary = class {
constructor({ wrapper, events, pos_profile }) {
constructor({ wrapper, settings, events }) {
this.wrapper = wrapper;
this.events = events;
this.pos_profile = pos_profile;
this.print_receipt_on_order_complete = settings.print_receipt_on_order_complete;
this.init_component();
}
@@ -391,8 +391,8 @@ erpnext.PointOfSale.PastOrderSummary = class {
this.add_summary_btns(condition_btns_map);
if (after_submission) {
this.print_receipt_on_order_complete();
if (after_submission && this.print_receipt_on_order_complete) {
this.print_receipt();
}
}
@@ -461,18 +461,6 @@ erpnext.PointOfSale.PastOrderSummary = class {
show ? this.$component.css("display", "flex") : this.$component.css("display", "none");
}
async print_receipt_on_order_complete() {
const res = await frappe.db.get_value(
"POS Profile",
this.pos_profile,
"print_receipt_on_order_complete"
);
if (res.message.print_receipt_on_order_complete) {
this.print_receipt();
}
}
async is_pos_invoice_returnable(invoice) {
const r = await frappe.call({
method: "erpnext.controllers.sales_and_purchase_return.is_pos_invoice_returnable",

View File

@@ -78,6 +78,7 @@ class Employee(NestedSet):
def on_update(self):
self.update_nsm_model()
frappe.clear_cache()
if self.user_id:
self.update_user()
self.update_user_permissions()

View File

@@ -937,17 +937,19 @@
"fieldtype": "Column Break"
}
],
"grid_page_length": 50,
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2025-02-05 14:27:32.322181",
"modified": "2025-03-07 12:33:40.868499",
"modified_by": "Administrator",
"module": "Stock",
"name": "Delivery Note Item",
"naming_rule": "Random",
"owner": "Administrator",
"permissions": [],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": []

View File

@@ -884,7 +884,7 @@ def create_pick_list(source_name, target_doc=None):
},
"Material Request Item": {
"doctype": "Pick List Item",
"field_map": {"name": "material_request_item", "qty": "stock_qty"},
"field_map": {"name": "material_request_item", "stock_qty": "stock_qty"},
},
},
target_doc,

View File

@@ -1189,7 +1189,6 @@ def create_delivery_note(source_name, target_doc=None):
if not all(item.sales_order for item in pick_list.locations):
delivery_note = create_dn_wo_so(pick_list)
frappe.msgprint(_("Delivery Note(s) created for the Pick List"))
return delivery_note
@@ -1206,7 +1205,6 @@ def create_dn_wo_so(pick_list):
},
}
map_pl_locations(pick_list, item_table_mapper_without_so, delivery_note)
delivery_note.insert(ignore_mandatory=True)
return delivery_note
@@ -1234,10 +1232,7 @@ def create_dn_with_so(sales_dict, pick_list):
# map all items of all sales orders of that customer
for so in sales_dict[customer]:
map_pl_locations(pick_list, item_table_mapper, delivery_note, so)
delivery_note.flags.ignore_mandatory = True
delivery_note.insert()
update_packed_item_details(pick_list, delivery_note)
delivery_note.save()
return delivery_note

View File

@@ -195,6 +195,16 @@ erpnext.stock.PurchaseReceiptController = class PurchaseReceiptController extend
super.setup(doc);
}
onload() {
this.frm.set_query("supplier", function () {
return {
filters: {
is_transporter: 0,
},
};
});
}
refresh() {
var me = this;
super.refresh();

View File

@@ -1018,7 +1018,8 @@
{
"fieldname": "serial_no",
"fieldtype": "Text",
"label": "Serial No"
"label": "Serial No",
"no_copy": 1
},
{
"fieldname": "rejected_serial_no",
@@ -1147,7 +1148,7 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2025-02-17 13:15:36.692202",
"modified": "2025-03-07 10:25:15.554985",
"modified_by": "Administrator",
"module": "Stock",
"name": "Purchase Receipt Item",
@@ -1155,6 +1156,7 @@
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": []

View File

@@ -2112,6 +2112,9 @@ def get_auto_batch_nos(kwargs):
picked_batches,
)
if available_batches and kwargs.get("posting_date"):
filter_zero_near_batches(available_batches, kwargs)
if not kwargs.consider_negative_batches:
available_batches = list(filter(lambda x: x.qty > 0, available_batches))
@@ -2121,6 +2124,26 @@ def get_auto_batch_nos(kwargs):
return get_qty_based_available_batches(available_batches, qty)
def filter_zero_near_batches(available_batches, kwargs):
kwargs.batch_no = [d.batch_no for d in available_batches]
del kwargs["posting_date"]
del kwargs["posting_time"]
available_batches_in_future = get_available_batches(kwargs)
for batch in available_batches:
if batch.qty <= 0:
continue
for future_batch in available_batches_in_future:
if (
batch.batch_no == future_batch.batch_no
and batch.warehouse == future_batch.warehouse
and future_batch.qty <= 0
):
batch.qty = 0
def get_qty_based_available_batches(available_batches, qty):
batches = []
for batch in available_batches:

View File

@@ -964,6 +964,9 @@ class StockReconciliation(StockController):
if voucher_detail_no != row.name:
continue
if row.current_qty < 0:
return
val_rate = 0.0
current_qty = 0.0
if row.current_serial_and_batch_bundle:

View File

@@ -110,7 +110,8 @@
"depends_on": "eval:doc.use_serial_batch_fields === 1",
"fieldname": "serial_no",
"fieldtype": "Long Text",
"label": "Serial No"
"label": "Serial No",
"no_copy": 1
},
{
"fieldname": "column_break_11",
@@ -253,15 +254,17 @@
"label": "Reconcile All Serial Nos / Batches"
}
],
"grid_page_length": 50,
"istable": 1,
"links": [],
"modified": "2024-05-30 23:20:00.947243",
"modified": "2025-03-07 10:26:25.856337",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Reconciliation Item",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],

View File

@@ -299,9 +299,12 @@ class StockBalanceReport:
elif entry.posting_date >= self.from_date and entry.posting_date <= self.to_date:
if flt(qty_diff, self.float_precision) >= 0:
qty_dict.in_qty += qty_diff
qty_dict.in_val += value_diff
else:
qty_dict.out_qty += abs(qty_diff)
if flt(value_diff, self.float_precision) >= 0:
qty_dict.in_val += value_diff
else:
qty_dict.out_val += abs(value_diff)
qty_dict.val_rate = entry.valuation_rate

View File

@@ -318,16 +318,29 @@ class TransactionBase(StatusUpdater):
"warehouse": item_obj.from_warehouse
if self.doctype in ["Purchase Receipt", "Purchase Invoice"]
else item_obj.warehouse,
"posting_date": self.posting_date,
"posting_time": self.posting_time,
"qty": item_obj.qty * item_obj.conversion_factor,
"serial_no": item_obj.serial_no,
"batch_no": item_obj.batch_no,
"voucher_type": self.doctype,
"company": self.company,
"allow_zero_valuation_rate": item_obj.allow_zero_valuation_rate,
}
)
if self.doctype in ["Purchase Order", "Sales Order"]:
args.update(
{
"posting_date": self.transaction_date,
}
)
else:
args.update(
{
"posting_date": self.posting_date,
"posting_time": self.posting_time,
"serial_no": item_obj.serial_no,
"batch_no": item_obj.batch_no,
"allow_zero_valuation_rate": item_obj.allow_zero_valuation_rate,
}
)
rate = get_incoming_rate(args=args)
item_obj.rate = rate * item_obj.conversion_factor
else: