mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-04 22:18:27 +00:00
Merge branch 'develop' into handle-party-type-erpenxt-crm-integration
This commit is contained in:
4
.github/release.yml
vendored
Normal file
4
.github/release.yml
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
changelog:
|
||||
exclude:
|
||||
labels:
|
||||
- skip-release-notes
|
||||
1
.github/workflows/lock.yml
vendored
1
.github/workflows/lock.yml
vendored
@@ -19,4 +19,3 @@ jobs:
|
||||
github-token: ${{ github.token }}
|
||||
issue-inactive-days: 14
|
||||
pr-inactive-days: 14
|
||||
exclude-pr-created-before: 2025-01-01
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
_(
|
||||
|
||||
@@ -98,7 +98,7 @@
|
||||
"Office Maintenance Expenses": {},
|
||||
"Office Rent": {},
|
||||
"Postal Expenses": {},
|
||||
"Print and Stationary": {},
|
||||
"Print and Stationery": {},
|
||||
"Rounded Off": {
|
||||
"account_type": "Round Off"
|
||||
},
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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": []
|
||||
|
||||
@@ -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")
|
||||
),
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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": []
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -10,6 +10,7 @@ frappe.ui.form.on("Tax Withholding Category", {
|
||||
filters: {
|
||||
company: child.company,
|
||||
root_type: ["in", ["Asset", "Liability"]],
|
||||
is_group: 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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"):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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 '' }}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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()
|
||||
|
||||
1462
erpnext/locale/ar.po
1462
erpnext/locale/ar.po
File diff suppressed because it is too large
Load Diff
60544
erpnext/locale/ar_SA.po
Normal file
60544
erpnext/locale/ar_SA.po
Normal file
File diff suppressed because it is too large
Load Diff
1534
erpnext/locale/bs.po
1534
erpnext/locale/bs.po
File diff suppressed because it is too large
Load Diff
60661
erpnext/locale/bs_BA.po
Normal file
60661
erpnext/locale/bs_BA.po
Normal file
File diff suppressed because it is too large
Load Diff
1462
erpnext/locale/de.po
1462
erpnext/locale/de.po
File diff suppressed because it is too large
Load Diff
60663
erpnext/locale/de_DE.po
Normal file
60663
erpnext/locale/de_DE.po
Normal file
File diff suppressed because it is too large
Load Diff
1462
erpnext/locale/eo.po
1462
erpnext/locale/eo.po
File diff suppressed because it is too large
Load Diff
60544
erpnext/locale/eo_UY.po
Normal file
60544
erpnext/locale/eo_UY.po
Normal file
File diff suppressed because it is too large
Load Diff
1462
erpnext/locale/es.po
1462
erpnext/locale/es.po
File diff suppressed because it is too large
Load Diff
60651
erpnext/locale/es_ES.po
Normal file
60651
erpnext/locale/es_ES.po
Normal file
File diff suppressed because it is too large
Load Diff
1462
erpnext/locale/fa.po
1462
erpnext/locale/fa.po
File diff suppressed because it is too large
Load Diff
60558
erpnext/locale/fa_IR.po
Normal file
60558
erpnext/locale/fa_IR.po
Normal file
File diff suppressed because it is too large
Load Diff
1462
erpnext/locale/fr.po
1462
erpnext/locale/fr.po
File diff suppressed because it is too large
Load Diff
60568
erpnext/locale/fr_FR.po
Normal file
60568
erpnext/locale/fr_FR.po
Normal file
File diff suppressed because it is too large
Load Diff
2096
erpnext/locale/hr.po
2096
erpnext/locale/hr.po
File diff suppressed because it is too large
Load Diff
60544
erpnext/locale/hr_HR.po
Normal file
60544
erpnext/locale/hr_HR.po
Normal file
File diff suppressed because it is too large
Load Diff
1462
erpnext/locale/hu.po
1462
erpnext/locale/hu.po
File diff suppressed because it is too large
Load Diff
60544
erpnext/locale/hu_HU.po
Normal file
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
1462
erpnext/locale/pl.po
1462
erpnext/locale/pl.po
File diff suppressed because it is too large
Load Diff
60595
erpnext/locale/pl_PL.po
Normal file
60595
erpnext/locale/pl_PL.po
Normal file
File diff suppressed because it is too large
Load Diff
1462
erpnext/locale/pt.po
1462
erpnext/locale/pt.po
File diff suppressed because it is too large
Load Diff
95583
erpnext/locale/pt_BR.po
95583
erpnext/locale/pt_BR.po
File diff suppressed because it is too large
Load Diff
1462
erpnext/locale/ru.po
1462
erpnext/locale/ru.po
File diff suppressed because it is too large
Load Diff
60544
erpnext/locale/ru_RU.po
Normal file
60544
erpnext/locale/ru_RU.po
Normal file
File diff suppressed because it is too large
Load Diff
1462
erpnext/locale/sv.po
1462
erpnext/locale/sv.po
File diff suppressed because it is too large
Load Diff
60667
erpnext/locale/sv_SE.po
Normal file
60667
erpnext/locale/sv_SE.po
Normal file
File diff suppressed because it is too large
Load Diff
1462
erpnext/locale/th.po
1462
erpnext/locale/th.po
File diff suppressed because it is too large
Load Diff
60544
erpnext/locale/th_TH.po
Normal file
60544
erpnext/locale/th_TH.po
Normal file
File diff suppressed because it is too large
Load Diff
1462
erpnext/locale/tr.po
1462
erpnext/locale/tr.po
File diff suppressed because it is too large
Load Diff
60667
erpnext/locale/tr_TR.po
Normal file
60667
erpnext/locale/tr_TR.po
Normal file
File diff suppressed because it is too large
Load Diff
1462
erpnext/locale/zh.po
1462
erpnext/locale/zh.po
File diff suppressed because it is too large
Load Diff
60648
erpnext/locale/zh_CN.po
Normal file
60648
erpnext/locale/zh_CN.po
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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": [],
|
||||
|
||||
@@ -21,6 +21,7 @@ class ProjectTemplate(Document):
|
||||
ProjectTemplateTask,
|
||||
)
|
||||
|
||||
disabled: DF.Check
|
||||
project_type: DF.Link | None
|
||||
tasks: DF.Table[ProjectTemplateTask]
|
||||
# end: auto-generated types
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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": []
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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": []
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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": [],
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user