Compare commits

...

70 Commits

Author SHA1 Message Date
Deepesh Garg
8e30debc10 fix: future subscripition updates 2024-04-27 23:07:14 +05:30
Deepesh Garg
5f6bf4c369 Merge pull request #41106 from deepeshgarg007/heatmap_remove
chore: Remove heatmap from party dashboards
2024-04-24 12:01:14 +05:30
Deepesh Garg
cfa3c85f3c Merge pull request #41160 from ruchamahabal/cost-center-perm
fix: allow Employee role to select cost center & project (accounting dimensions)
2024-04-24 11:14:22 +05:30
Rucha Mahabal
d0d496a515 fix: allow Employee role to select cost center & project (accounting dimensions) 2024-04-24 10:57:41 +05:30
rohitwaghchaure
14bdd5da6e Merge pull request #41135 from rohitwaghchaure/available-batch-report
feat: Available batches as on specific date report
2024-04-23 22:17:08 +05:30
Rohit Waghchaure
b8f7979794 feat: Available batches report as on specific date 2024-04-23 17:18:31 +05:30
rohitwaghchaure
3bd598bd1b Merge pull request #41145 from rohitwaghchaure/fixed-13815
fix: incorrect stock posting for current qty
2024-04-23 16:50:09 +05:30
Rohit Waghchaure
d4fe313de2 fix: incorrect stock posting for current qty 2024-04-23 16:18:40 +05:30
ruthra kumar
6d51105082 Merge pull request #41147 from ruthra-kumar/better_description_and_validation
refactor: better description and pop up on Advance accounts
2024-04-23 14:45:04 +05:30
ruthra kumar
9dbd321133 refactor: popup to inform on limited support for Advance accounts 2024-04-23 13:57:12 +05:30
rohitwaghchaure
3b62237570 Merge pull request #41098 from rohitwaghchaure/fixed-stock-reco-negative-qty-validation-13584
fix: stock reconciliation negative qty validation
2024-04-23 13:08:41 +05:30
Rohit Waghchaure
289495c308 fix: stock reco negative qty validation 2024-04-23 12:45:33 +05:30
ruthra kumar
de9c8fc9d6 refactor: better description for advance account 2024-04-23 12:38:44 +05:30
ruthra kumar
6b8f65b7de Merge pull request #41142 from ruthra-kumar/toggle_auto_bank_account_update
fix: mode of payment has precedance in Payment Entry
2024-04-23 11:54:53 +05:30
rohitwaghchaure
eadd892b03 Merge pull request #41013 from Nihantra-Patel/fix_dc_qc
fix: inspection type when create a qc from dc
2024-04-23 11:33:43 +05:30
ruthra kumar
4aef969879 fix: mode of payment has precedance
Mode of Payment is given precedence over company/party bank account
2024-04-23 10:40:36 +05:30
Nabin Hait
4eee3f52a3 Merge pull request #41136 from nabinhait/installation-note-fix
fix: Missing args while fetching items from delivery note in Installation Note
2024-04-23 10:24:01 +05:30
ruthra kumar
3e7ccb24a6 Merge pull request #41086 from ruthra-kumar/prevent_multi_currency_advance_in_separate_party
fix: prevent foreign currency  accounts as advance accounts.
2024-04-23 07:14:16 +05:30
Nabin Hait
bbe323fbb4 fix: Missing args while fetching items from delivery note 2024-04-22 16:28:31 +05:30
Deepesh Garg
7286df86c5 Merge pull request #41104 from deepeshgarg007/payment_entry_test
fix: Payment entry against employee
2024-04-22 11:36:05 +05:30
Gursheen Kaur Anand
affe304d47 Merge pull request #40769 from GursheenK/budget-actual-expense
fix: budget validation for purchase orders
2024-04-21 16:54:44 +05:30
rohitwaghchaure
b4cbd5fafa Merge pull request #41120 from frappe/mergify/bp/develop/pr-36459
fix: search not working for so in the Production Plan (backport #36459)
2024-04-21 16:49:58 +05:30
rohitwaghchaure
9a6e762b8b fix: search not working for so in the Production Plan (#36459)
fix: search not working for so
(cherry picked from commit 8c57d56240)
2024-04-21 07:56:37 +00:00
rohitwaghchaure
41035ccba0 Merge pull request #41099 from rohitwaghchaure/fixed-report-13659
fix: not able to update default supplier from Supplier Quotation Comparison report
2024-04-20 11:20:31 +05:30
rohitwaghchaure
29153ed3b4 Merge pull request #41102 from rohitwaghchaure/fixed-stock-ledeger-report-balance-qty-13622
fix: balance qty for stock ledger report
2024-04-19 21:50:14 +05:30
Deepesh Garg
a8f03e8baa chore: Remove heatmap from party dashboards 2024-04-19 19:09:55 +05:30
Deepesh Garg
93e6c6ccab fix: Payment entry against employee 2024-04-19 17:38:11 +05:30
Rohit Waghchaure
f00ae0b92b fix: balance qty for stock ledger report 2024-04-19 17:30:27 +05:30
Deepesh Garg
a4b6eda8a9 Merge pull request #41085 from deepeshgarg007/ldc_permission
fix: Permission for lower deduction certificate
2024-04-19 15:39:35 +05:30
Rohit Waghchaure
ad8e189c26 fix: not able to update default supplier from Supplier Quotation Comparison report 2024-04-19 15:39:22 +05:30
rohitwaghchaure
7ad1bf5840 Merge pull request #41067 from rohitwaghchaure/fixed-validation-for-voucher-no-and-qty
fix: validation for zero qty in SABB
2024-04-19 15:10:19 +05:30
ruthra kumar
1ad065fc54 fix: advance account validation in company master 2024-04-18 17:15:52 +05:30
Rohit Waghchaure
497f560b4b fix: validation for zero qty in SABB 2024-04-18 15:53:42 +05:30
Deepesh Garg
f6f118855b fix: Permission for lower dedcution certificate 2024-04-18 15:26:03 +05:30
rohitwaghchaure
e706b4534c Merge pull request #41083 from rohitwaghchaure/fixed-work-order-qty-to-manuufacture
fix: validation for fraction number in Work Order
2024-04-18 14:44:18 +05:30
Rohit Waghchaure
f8305c2fc0 fix: validation for fraction number in Work Order 2024-04-18 14:24:51 +05:30
ruthra kumar
e3fc5990ee fix: validation to prevent foreign currency advance accounts in PE 2024-04-18 12:24:45 +05:30
s-aga-r
fb6b98c85d Merge pull request #41074 from s-aga-r/FIX-13339-2
fix: validate uom is integer for PR item
2024-04-17 22:29:38 +05:30
s-aga-r
9a290fdfc9 fix: validate uom is integer for PR item 2024-04-17 22:09:28 +05:30
Raffael Meyer
34fd290edb fix: don't attempt to set gender from salutation (#40997) 2024-04-17 17:26:08 +02:00
s-aga-r
244d62df70 Merge pull request #41061 from s-aga-r/FIX-13339
fix: do not add qty to supplied items
2024-04-17 16:33:38 +05:30
s-aga-r
8233c392fb fix: do not add qty to supplied items 2024-04-17 16:15:37 +05:30
rohitwaghchaure
e36f6453d5 Merge pull request #41053 from rohitwaghchaure/fixed-duplicate-serial-batch-in-stock-entry
fix: duplicate serial and batch bundle in stock entry and stock reco
2024-04-17 13:18:48 +05:30
Rohit Waghchaure
732b6e1417 fix: duplicate serial and batch bundle in stock entry and stock reco 2024-04-17 13:00:49 +05:30
Deepesh Garg
7ce6687b68 Merge pull request #40962 from deepeshgarg007/allow_on_submit_ad_v1
fix: Allow updating cost center and project for repostable doctypes
2024-04-17 11:21:50 +05:30
ruthra kumar
96d96bb500 Merge pull request #41047 from ruthra-kumar/incorrect_use_of_wildcard
fix: possible wildcard issue on variable name
2024-04-17 11:04:32 +05:30
ruthra kumar
5357b9e727 fix: possible wildcard issue on variable name 2024-04-17 10:46:00 +05:30
rohitwaghchaure
5b1493b56c fix: not able to submit subcontracting receipt (#41041) 2024-04-17 00:22:09 +05:30
rohitwaghchaure
d69a18b826 fix: Delayed Order Report not working (#41037) 2024-04-16 17:14:59 +05:30
ruthra kumar
42a5e61ac3 Merge pull request #40373 from Nihantra-Patel/fix_mutiple_address_so
fix: get address if multiple companies
2024-04-16 16:30:17 +05:30
rohitwaghchaure
03231e99ef fix: expense account set as COGS for stock entry Material Issue (#41026) 2024-04-16 13:55:35 +05:30
RitvikSardana
85c63f29c0 Merge pull request #41025 from RitvikSardana/develop-ritvik-remove-customer-check
fix: remove validate_name_in_customer function
2024-04-16 12:44:47 +05:30
RitvikSardana
2b32d3644f fix: remove validate_name_in_customer function 2024-04-16 12:20:16 +05:30
ruthra kumar
9445bd410c Merge pull request #41020 from ruthra-kumar/fix_incorrect_exc_gain_loss_for_pe_against_journals
fix: incorrect exc gain/loss for PE against JE for payable accounts
2024-04-16 10:58:49 +05:30
ruthra kumar
8821c98625 test: exc gain/loss journals booking in Payable accounts 2024-04-16 10:41:54 +05:30
ruthra kumar
81b574053f fix: incorrect exc gain/loss for PE against JE for payable accounts 2024-04-16 10:41:52 +05:30
Deepesh Garg
67a8f50711 Merge pull request #40812 from deepeshgarg007/multi_pr_pi
fix: Multiple partial payment requests against Purchase Invoice
2024-04-15 21:50:11 +05:30
Deepesh Garg
071e5ed648 fix: Test case 2024-04-15 20:49:40 +05:30
Deepesh Garg
b9243569ca Merge branch 'develop' of https://github.com/frappe/erpnext into multi_pr_pi 2024-04-15 19:58:48 +05:30
Nihantra Patel
098b62f0f6 fix: inspection type when create a qc from dc 2024-04-15 17:46:21 +05:30
Nabin Hait
760e341cd0 Merge pull request #40858 from nabinhait/quote-so-delivery-date
fix: Don't set delivery date as today while making SO from Quotation
2024-04-15 12:31:06 +05:30
Gursheen Anand
9a12376e29 refactor: show list for expense breakup 2024-04-14 17:06:01 +05:30
Gursheen Anand
59292a09c4 feat: show expense breakup 2024-04-14 17:03:22 +05:30
Deepesh Garg
c3845ac0f1 fix: Allow updating cost center and project for repostable doctypes 2024-04-11 11:30:56 +05:30
Nabin Hait
65c74fa3c7 fix: test cases 2024-04-11 11:01:28 +05:30
Nabin Hait
fec20decc1 fix: Don't set delivery date as today while making SO from Quotation 2024-04-04 15:25:46 +05:30
Deepesh Garg
45d5f6e00a fix: Multiple partial payment requests against Purchase Invoice 2024-04-01 21:21:46 +05:30
Gursheen Anand
af26ac96e9 fix: do not add actual expense twice for validating budget 2024-03-29 17:22:48 +05:30
Nihantra C. Patel
655a1797be fix: get address if multiple companies 2024-03-11 16:46:00 +05:30
Nihantra Patel
c6cf1bec76 fix: get address if multiple companies 2024-03-11 16:34:44 +05:30
58 changed files with 1030 additions and 313 deletions

View File

@@ -57,9 +57,12 @@ frappe.ui.form.on("Accounting Dimension", {
}
},
label: function (frm) {
frm.set_value("fieldname", frappe.model.scrub(frm.doc.label));
},
document_type: function (frm) {
frm.set_value("label", frm.doc.document_type);
frm.set_value("fieldname", frappe.model.scrub(frm.doc.document_type));
frappe.db.get_value(
"Accounting Dimension",

View File

@@ -224,12 +224,18 @@ def validate_expense_against_budget(args, expense_amount=0):
def validate_budget_records(args, budget_records, expense_amount):
for budget in budget_records:
if flt(budget.budget_amount):
amount = expense_amount or get_amount(args, budget)
yearly_action, monthly_action = get_actions(args, budget)
args["for_material_request"] = budget.for_material_request
args["for_purchase_order"] = budget.for_purchase_order
if yearly_action in ("Stop", "Warn"):
compare_expense_with_budget(
args, flt(budget.budget_amount), _("Annual"), yearly_action, budget.budget_against, amount
args,
flt(budget.budget_amount),
_("Annual"),
yearly_action,
budget.budget_against,
expense_amount,
)
if monthly_action in ["Stop", "Warn"]:
@@ -245,18 +251,27 @@ def validate_budget_records(args, budget_records, expense_amount):
_("Accumulated Monthly"),
monthly_action,
budget.budget_against,
amount,
expense_amount,
)
def compare_expense_with_budget(args, budget_amount, action_for, action, budget_against, amount=0):
actual_expense = get_actual_expense(args)
total_expense = actual_expense + amount
args.actual_expense, args.requested_amount, args.ordered_amount = get_actual_expense(args), 0, 0
if not amount:
args.requested_amount, args.ordered_amount = get_requested_amount(args), get_ordered_amount(args)
if args.get("doctype") == "Material Request" and args.for_material_request:
amount = args.requested_amount + args.ordered_amount
elif args.get("doctype") == "Purchase Order" and args.for_purchase_order:
amount = args.ordered_amount
total_expense = args.actual_expense + amount
if total_expense > budget_amount:
if actual_expense > budget_amount:
if args.actual_expense > budget_amount:
error_tense = _("is already")
diff = actual_expense - budget_amount
diff = args.actual_expense - budget_amount
else:
error_tense = _("will be")
diff = total_expense - budget_amount
@@ -273,6 +288,8 @@ def compare_expense_with_budget(args, budget_amount, action_for, action, budget_
frappe.bold(fmt_money(diff, currency=currency)),
)
msg += get_expense_breakup(args, currency, budget_against)
if frappe.flags.exception_approver_role and frappe.flags.exception_approver_role in frappe.get_roles(
frappe.session.user
):
@@ -284,6 +301,83 @@ def compare_expense_with_budget(args, budget_amount, action_for, action, budget_
frappe.msgprint(msg, indicator="orange", title=_("Budget Exceeded"))
def get_expense_breakup(args, currency, budget_against):
msg = "<hr>Total Expenses booked through - <ul>"
common_filters = frappe._dict(
{
args.budget_against_field: budget_against,
"account": args.account,
"company": args.company,
}
)
msg += (
"<li>"
+ frappe.utils.get_link_to_report(
"General Ledger",
label="Actual Expenses",
filters=common_filters.copy().update(
{
"from_date": frappe.get_cached_value("Fiscal Year", args.fiscal_year, "year_start_date"),
"to_date": frappe.get_cached_value("Fiscal Year", args.fiscal_year, "year_end_date"),
"is_cancelled": 0,
}
),
)
+ " - "
+ frappe.bold(fmt_money(args.actual_expense, currency=currency))
+ "</li>"
)
msg += (
"<li>"
+ frappe.utils.get_link_to_report(
"Material Request",
label="Material Requests",
report_type="Report Builder",
doctype="Material Request",
filters=common_filters.copy().update(
{
"status": [["!=", "Stopped"]],
"docstatus": 1,
"material_request_type": "Purchase",
"schedule_date": [["fiscal year", "2023-2024"]],
"item_code": args.item_code,
"per_ordered": [["<", 100]],
}
),
)
+ " - "
+ frappe.bold(fmt_money(args.requested_amount, currency=currency))
+ "</li>"
)
msg += (
"<li>"
+ frappe.utils.get_link_to_report(
"Purchase Order",
label="Unbilled Orders",
report_type="Report Builder",
doctype="Purchase Order",
filters=common_filters.copy().update(
{
"status": [["!=", "Closed"]],
"docstatus": 1,
"transaction_date": [["fiscal year", "2023-2024"]],
"item_code": args.item_code,
"per_billed": [["<", 100]],
}
),
)
+ " - "
+ frappe.bold(fmt_money(args.ordered_amount, currency=currency))
+ "</li></ul>"
)
return msg
def get_actions(args, budget):
yearly_action = budget.action_if_annual_budget_exceeded
monthly_action = budget.action_if_accumulated_monthly_budget_exceeded
@@ -299,23 +393,9 @@ def get_actions(args, budget):
return yearly_action, monthly_action
def get_amount(args, budget):
amount = 0
if args.get("doctype") == "Material Request" and budget.for_material_request:
amount = (
get_requested_amount(args, budget) + get_ordered_amount(args, budget) + get_actual_expense(args)
)
elif args.get("doctype") == "Purchase Order" and budget.for_purchase_order:
amount = get_ordered_amount(args, budget) + get_actual_expense(args)
return amount
def get_requested_amount(args, budget):
def get_requested_amount(args):
item_code = args.get("item_code")
condition = get_other_condition(args, budget, "Material Request")
condition = get_other_condition(args, "Material Request")
data = frappe.db.sql(
""" select ifnull((sum(child.stock_qty - child.ordered_qty) * rate), 0) as amount
@@ -329,9 +409,9 @@ def get_requested_amount(args, budget):
return data[0][0] if data else 0
def get_ordered_amount(args, budget):
def get_ordered_amount(args):
item_code = args.get("item_code")
condition = get_other_condition(args, budget, "Purchase Order")
condition = get_other_condition(args, "Purchase Order")
data = frappe.db.sql(
f""" select ifnull(sum(child.amount - child.billed_amt), 0) as amount
@@ -345,7 +425,7 @@ def get_ordered_amount(args, budget):
return data[0][0] if data else 0
def get_other_condition(args, budget, for_doc):
def get_other_condition(args, for_doc):
condition = "expense_account = '%s'" % (args.expense_account)
budget_against_field = args.get("budget_against_field")

View File

@@ -125,7 +125,7 @@
"idx": 1,
"is_tree": 1,
"links": [],
"modified": "2024-03-27 13:06:46.762208",
"modified": "2024-04-24 10:55:54.083042",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Cost Center",
@@ -163,6 +163,15 @@
{
"read": 1,
"role": "Purchase User"
},
{
"email": 1,
"export": 1,
"print": 1,
"report": 1,
"role": "Employee",
"select": 1,
"share": 1
}
],
"search_fields": "parent_cost_center, is_group",

View File

@@ -6,7 +6,7 @@ import json
import frappe
from frappe import _, msgprint, scrub
from frappe.utils import cstr, flt, fmt_money, formatdate, get_link_to_form, nowdate
from frappe.utils import comma_and, cstr, flt, fmt_money, formatdate, get_link_to_form, nowdate
import erpnext
from erpnext.accounts.deferred_revenue import get_deferred_booking_accounts
@@ -146,6 +146,7 @@ class JournalEntry(AccountsController):
self.validate_empty_accounts_table()
self.validate_inter_company_accounts()
self.validate_depr_entry_voucher_type()
self.validate_advance_accounts()
if self.docstatus == 0:
self.apply_tax_withholding()
@@ -153,6 +154,20 @@ class JournalEntry(AccountsController):
if not self.title:
self.title = self.get_title()
def validate_advance_accounts(self):
journal_accounts = set([x.account for x in self.accounts])
advance_accounts = set()
advance_accounts.add(
frappe.get_cached_value("Company", self.company, "default_advance_received_account")
)
advance_accounts.add(frappe.get_cached_value("Company", self.company, "default_advance_paid_account"))
if advance_accounts_used := journal_accounts & advance_accounts:
frappe.msgprint(
_(
"Making Journal Entries against advance accounts: {0} is not recommended. These Journals won't be available for Reconciliation."
).format(frappe.bold(comma_and(advance_accounts_used)))
)
def validate_for_repost(self):
validate_docs_for_voucher_types(["Journal Entry"])
validate_docs_for_deferred_accounting([self.name], [])

View File

@@ -1334,7 +1334,9 @@ frappe.ui.form.on("Payment Entry", {
},
callback: function (r) {
if (r.message) {
frm.set_value(field, r.message.account);
if (!frm.doc.mode_of_payment) {
frm.set_value(field, r.message.account);
}
frm.set_value("bank", r.message.bank);
frm.set_value("bank_account_no", r.message.bank_account_no);
}

View File

@@ -150,6 +150,7 @@
"reqd": 1
},
{
"allow_on_submit": 1,
"fieldname": "cost_center",
"fieldtype": "Link",
"label": "Cost Center",
@@ -477,6 +478,7 @@
"label": "More Information"
},
{
"allow_on_submit": 1,
"fieldname": "project",
"fieldtype": "Link",
"label": "Project",
@@ -777,7 +779,7 @@
"table_fieldname": "payment_entries"
}
],
"modified": "2024-03-27 13:10:09.131139",
"modified": "2024-04-11 11:25:07.366347",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry",
@@ -823,4 +825,4 @@
"states": [],
"title_field": "title",
"track_changes": 1
}
}

View File

@@ -158,6 +158,7 @@ class PaymentEntry(AccountsController):
self.setup_party_account_field()
self.set_missing_values()
self.set_liability_account()
self.validate_advance_account_currency()
self.set_missing_ref_details(force=True)
self.validate_payment_type()
self.validate_party_details()
@@ -240,6 +241,22 @@ class PaymentEntry(AccountsController):
alert=True,
)
def validate_advance_account_currency(self):
if self.book_advance_payments_in_separate_party_account is True:
company_currency = frappe.get_cached_value("Company", self.company, "default_currency")
if self.payment_type == "Receive" and self.paid_from_account_currency != company_currency:
frappe.throw(
_("Booking advances in foreign currency account: {0} ({1}) is not yet supported.").format(
frappe.bold(self.paid_from), frappe.bold(self.paid_from_account_currency)
)
)
if self.payment_type == "Pay" and self.paid_to_account_currency != company_currency:
frappe.throw(
_("Booking advances in foreign currency account: {0} ({1}) is not yet supported.").format(
frappe.bold(self.paid_to), frappe.bold(self.paid_to_account_currency)
)
)
def on_cancel(self):
self.ignore_linked_doctypes = (
"GL Entry",
@@ -2126,6 +2143,8 @@ def get_negative_outstanding_invoices(
@frappe.whitelist()
def get_party_details(company, party_type, party, date, cost_center=None):
bank_account = ""
party_bank_account = ""
if not frappe.db.exists(party_type, party):
frappe.throw(_("{0} {1} does not exist").format(_(party_type), party))
@@ -2137,8 +2156,8 @@ def get_party_details(company, party_type, party, date, cost_center=None):
party_balance = get_balance_on(party_type=party_type, party=party, cost_center=cost_center)
if party_type in ["Customer", "Supplier"]:
party_bank_account = get_party_bank_account(party_type, party)
bank_account = get_default_company_bank_account(company, party_type, party)
bank_account = get_default_company_bank_account(company, party_type, party)
return {
"party_account": party_account,
"party_name": party_name,

View File

@@ -10,6 +10,7 @@ from frappe.utils import add_days, flt, nowdate
from erpnext.accounts.doctype.account.test_account import create_account
from erpnext.accounts.doctype.payment_entry.payment_entry import (
get_outstanding_reference_documents,
get_party_details,
get_payment_entry,
get_reference_details,
)
@@ -1684,6 +1685,10 @@ def create_payment_entry(**args):
payment_entry.reference_no = "Test001"
payment_entry.reference_date = nowdate()
get_party_details(
payment_entry.company, payment_entry.party_type, payment_entry.party, payment_entry.posting_date
)
if args.get("save"):
payment_entry.save()
if args.get("submit"):

View File

@@ -196,6 +196,8 @@
},
{
"depends_on": "eval:doc.party",
"description": "Only 'Payment Entries' made against this advance account are supported.",
"documentation_url": "https://docs.erpnext.com/docs/user/manual/en/advance-in-separate-party-account",
"fieldname": "default_advance_account",
"fieldtype": "Link",
"label": "Default Advance Account",
@@ -230,7 +232,7 @@
"is_virtual": 1,
"issingle": 1,
"links": [],
"modified": "2024-03-27 13:10:10.488007",
"modified": "2024-04-23 12:38:29.557315",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Reconciliation",

View File

@@ -92,7 +92,7 @@ class PaymentRequest(Document):
self.status = "Draft"
self.validate_reference_document()
self.validate_payment_request_amount()
self.validate_currency()
# self.validate_currency()
self.validate_subscription_details()
def validate_reference_document(self):
@@ -335,21 +335,17 @@ class PaymentRequest(Document):
}
)
if party_account_currency == ref_doc.company_currency and party_account_currency != self.currency:
amount = payment_entry.base_paid_amount
else:
amount = self.grand_total
payment_entry.received_amount = amount
payment_entry.get("references")[0].allocated_amount = amount
for dimension in get_accounting_dimensions():
payment_entry.update({dimension: self.get(dimension)})
if payment_entry.difference_amount:
company_details = get_company_defaults(ref_doc.company)
payment_entry.append(
"deductions",
{
"account": company_details.exchange_gain_loss_account,
"cost_center": company_details.cost_center,
"amount": payment_entry.difference_amount,
},
)
if submit:
payment_entry.insert(ignore_permissions=True)
payment_entry.submit()
@@ -479,6 +475,12 @@ def make_payment_request(**args):
pr = frappe.get_doc("Payment Request", draft_payment_request)
else:
pr = frappe.new_doc("Payment Request")
if not args.get("payment_request_type"):
args["payment_request_type"] = (
"Outward" if args.get("dt") in ["Purchase Order", "Purchase Invoice"] else "Inward"
)
pr.update(
{
"payment_gateway_account": gateway_account.get("name"),
@@ -538,9 +540,9 @@ def get_amount(ref_doc, payment_account=None):
elif dt in ["Sales Invoice", "Purchase Invoice"]:
if not ref_doc.get("is_pos"):
if ref_doc.party_account_currency == ref_doc.currency:
grand_total = flt(ref_doc.outstanding_amount)
grand_total = flt(ref_doc.grand_total)
else:
grand_total = flt(ref_doc.outstanding_amount) / ref_doc.conversion_rate
grand_total = flt(ref_doc.base_grand_total) / ref_doc.conversion_rate
elif dt == "Sales Invoice":
for pay in ref_doc.payments:
if pay.type == "Phone" and pay.account == payment_account:

View File

@@ -86,6 +86,8 @@ class TestPaymentRequest(unittest.TestCase):
pr = make_payment_request(
dt="Purchase Invoice",
dn=si_usd.name,
party_type="Supplier",
party="_Test Supplier USD",
recipient_id="user@example.com",
mute_email=1,
payment_gateway_account="_Test Gateway - USD",
@@ -98,6 +100,51 @@ class TestPaymentRequest(unittest.TestCase):
self.assertEqual(pr.status, "Paid")
def test_multiple_payment_entry_against_purchase_invoice(self):
purchase_invoice = make_purchase_invoice(
customer="_Test Supplier USD",
debit_to="_Test Payable USD - _TC",
currency="USD",
conversion_rate=50,
)
pr = make_payment_request(
dt="Purchase Invoice",
party_type="Supplier",
party="_Test Supplier USD",
dn=purchase_invoice.name,
recipient_id="user@example.com",
mute_email=1,
payment_gateway_account="_Test Gateway - USD",
return_doc=1,
)
pr.grand_total = pr.grand_total / 2
pr.submit()
pr.create_payment_entry()
purchase_invoice.load_from_db()
self.assertEqual(purchase_invoice.status, "Partly Paid")
pr = make_payment_request(
dt="Purchase Invoice",
party_type="Supplier",
party="_Test Supplier USD",
dn=purchase_invoice.name,
recipient_id="user@example.com",
mute_email=1,
payment_gateway_account="_Test Gateway - USD",
return_doc=1,
)
pr.save()
pr.submit()
pr.create_payment_entry()
purchase_invoice.load_from_db()
self.assertEqual(purchase_invoice.status, "Paid")
def test_payment_entry(self):
frappe.db.set_value(
"Company", "_Test Company", "exchange_gain_loss_account", "_Test Exchange Gain/Loss - _TC"

View File

@@ -299,6 +299,7 @@
"remember_last_selected_value": 1
},
{
"allow_on_submit": 1,
"fieldname": "cost_center",
"fieldtype": "Link",
"label": "Cost Center",
@@ -1368,6 +1369,7 @@
"read_only": 1
},
{
"allow_on_submit": 1,
"fieldname": "project",
"fieldtype": "Link",
"label": "Project",
@@ -1638,7 +1640,7 @@
"idx": 204,
"is_submittable": 1,
"links": [],
"modified": "2024-03-27 13:10:23.476658",
"modified": "2024-04-11 11:28:42.802211",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",

View File

@@ -68,15 +68,11 @@ class PurchaseInvoice(BuyingController):
from erpnext.accounts.doctype.purchase_invoice_advance.purchase_invoice_advance import (
PurchaseInvoiceAdvance,
)
from erpnext.accounts.doctype.purchase_invoice_item.purchase_invoice_item import (
PurchaseInvoiceItem,
)
from erpnext.accounts.doctype.purchase_invoice_item.purchase_invoice_item import PurchaseInvoiceItem
from erpnext.accounts.doctype.purchase_taxes_and_charges.purchase_taxes_and_charges import (
PurchaseTaxesandCharges,
)
from erpnext.accounts.doctype.tax_withheld_vouchers.tax_withheld_vouchers import (
TaxWithheldVouchers,
)
from erpnext.accounts.doctype.tax_withheld_vouchers.tax_withheld_vouchers import TaxWithheldVouchers
from erpnext.buying.doctype.purchase_receipt_item_supplied.purchase_receipt_item_supplied import (
PurchaseReceiptItemSupplied,
)

View File

@@ -291,6 +291,7 @@
"read_only": 1
},
{
"allow_on_submit": 1,
"fieldname": "project",
"fieldtype": "Link",
"hide_days": 1,
@@ -356,6 +357,7 @@
"reqd": 1
},
{
"allow_on_submit": 1,
"fieldname": "cost_center",
"fieldtype": "Link",
"hide_days": 1,
@@ -2201,7 +2203,7 @@
"link_fieldname": "consolidated_invoice"
}
],
"modified": "2024-03-27 13:10:35.407256",
"modified": "2024-04-11 11:30:26.272441",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",

View File

@@ -58,13 +58,9 @@ class SalesInvoice(SellingController):
from erpnext.accounts.doctype.payment_schedule.payment_schedule import PaymentSchedule
from erpnext.accounts.doctype.pricing_rule_detail.pricing_rule_detail import PricingRuleDetail
from erpnext.accounts.doctype.sales_invoice_advance.sales_invoice_advance import (
SalesInvoiceAdvance,
)
from erpnext.accounts.doctype.sales_invoice_advance.sales_invoice_advance import SalesInvoiceAdvance
from erpnext.accounts.doctype.sales_invoice_item.sales_invoice_item import SalesInvoiceItem
from erpnext.accounts.doctype.sales_invoice_payment.sales_invoice_payment import (
SalesInvoicePayment,
)
from erpnext.accounts.doctype.sales_invoice_payment.sales_invoice_payment import SalesInvoicePayment
from erpnext.accounts.doctype.sales_invoice_timesheet.sales_invoice_timesheet import (
SalesInvoiceTimesheet,
)

View File

@@ -112,11 +112,7 @@ class Subscription(Document):
"""
_current_invoice_start = None
if (
self.is_new_subscription()
and self.trial_period_end
and getdate(self.trial_period_end) > getdate(self.start_date)
):
if self.trial_period_end and getdate(self.trial_period_end) > getdate(self.start_date):
_current_invoice_start = add_days(self.trial_period_end, 1)
elif self.trial_period_start and self.is_trialling():
_current_invoice_start = self.trial_period_start
@@ -143,7 +139,7 @@ class Subscription(Document):
else:
billing_cycle_info = self.get_billing_cycle_data()
if billing_cycle_info:
if self.is_new_subscription() and getdate(self.start_date) < getdate(date):
if getdate(self.start_date) < getdate(date):
_current_invoice_end = add_to_date(self.start_date, **billing_cycle_info)
# For cases where trial period is for an entire billing interval
@@ -234,14 +230,14 @@ class Subscription(Document):
self.cancelation_date = getdate(posting_date) if self.status == "Cancelled" else None
elif self.current_invoice_is_past_due() and not self.is_past_grace_period():
self.status = "Past Due Date"
elif not self.has_outstanding_invoice() or self.is_new_subscription():
elif not self.has_outstanding_invoice():
self.status = "Active"
def is_trialling(self) -> bool:
"""
Returns `True` if the `Subscription` is in trial period.
"""
return not self.period_has_passed(self.trial_period_end) and self.is_new_subscription()
return not self.period_has_passed(self.trial_period_end)
@staticmethod
def period_has_passed(
@@ -288,14 +284,6 @@ class Subscription(Document):
def invoice_document_type(self) -> str:
return "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice"
def is_new_subscription(self) -> bool:
"""
Returns `True` if `Subscription` has never generated an invoice
"""
return self.is_new() or not frappe.db.exists(
{"doctype": self.invoice_document_type, "subscription": self.name}
)
def validate(self) -> None:
self.validate_trial_period()
self.validate_plans_billing_cycle(self.get_billing_cycle_and_interval())
@@ -604,7 +592,7 @@ class Subscription(Document):
return False
if self.generate_invoice_at == "Beginning of the current subscription period" and (
getdate(posting_date) == getdate(self.current_invoice_start) or self.is_new_subscription()
getdate(posting_date) == getdate(self.current_invoice_start)
):
return True
elif self.generate_invoice_at == "Days before the current subscription period" and (

View File

@@ -754,52 +754,6 @@ def validate_party_frozen_disabled(party_type, party_name):
frappe.msgprint(_("{0} {1} is not active").format(party_type, party_name), alert=True)
def get_timeline_data(doctype, name):
"""returns timeline data for the past one year"""
from frappe.desk.form.load import get_communication_data
out = {}
after = add_years(None, -1).strftime("%Y-%m-%d")
data = get_communication_data(
doctype,
name,
after=after,
group_by="group by communication_date",
fields="C.communication_date as communication_date, count(C.name)",
as_dict=False,
)
# fetch and append data from Activity Log
activity_log = frappe.qb.DocType("Activity Log")
data += (
frappe.qb.from_(activity_log)
.select(activity_log.communication_date, Count(activity_log.name))
.where(
(
((activity_log.reference_doctype == doctype) & (activity_log.reference_name == name))
| ((activity_log.timeline_doctype == doctype) & (activity_log.timeline_name == name))
| (
(activity_log.reference_doctype.isin(["Quotation", "Opportunity"]))
& (activity_log.timeline_name == name)
)
)
& (activity_log.status != "Success")
& (activity_log.creation > after)
)
.groupby(activity_log.communication_date)
.orderby(activity_log.communication_date, order=frappe.qb.desc)
).run()
timeline_items = dict(data)
for date, count in timeline_items.items():
timestamp = get_timestamp(date)
out.update({timestamp: count})
return out
def get_dashboard_info(party_type, party, loyalty_program=None):
current_fiscal_year = get_fiscal_year(nowdate(), as_dict=True)

View File

@@ -655,13 +655,13 @@ class GrossProfitGenerator:
elif self.delivery_notes.get((row.parent, row.item_code), None):
# check if Invoice has delivery notes
dn = self.delivery_notes.get((row.parent, row.item_code))
parenttype, parent, item_row, _warehouse = (
parenttype, parent, item_row, dn_warehouse = (
"Delivery Note",
dn["delivery_note"],
dn["item_row"],
dn["warehouse"],
)
my_sle = self.get_stock_ledger_entries(item_code, _warehouse)
my_sle = self.get_stock_ledger_entries(item_code, dn_warehouse)
return self.calculate_buying_amount_from_sle(
row, my_sle, parenttype, parent, item_row, item_code
)

View File

@@ -305,6 +305,7 @@ def create_asset_repair(**args):
"serial_nos": args.serial_no,
"posting_date": today(),
"posting_time": nowtime(),
"do_not_submit": 1,
}
)
).name

View File

@@ -3,10 +3,6 @@ from frappe import _
def get_data():
return {
"heatmap": True,
"heatmap_message": _(
"This is based on transactions against this Supplier. See timeline below for details"
),
"fieldname": "supplier",
"non_standard_fieldnames": {"Payment Entry": "party", "Bank Account": "party"},
"transactions": [

View File

@@ -133,6 +133,13 @@ frappe.query_reports["Supplier Quotation Comparison"] = {
return row.supplier_name;
});
let items = [];
report.data.forEach((d) => {
if (!items.includes(d.item_code)) {
items.push(d.item_code);
}
});
// Create a dialog window for the user to pick their supplier
let dialog = new frappe.ui.Dialog({
title: __("Select Default Supplier"),
@@ -151,20 +158,34 @@ frappe.query_reports["Supplier Quotation Comparison"] = {
};
},
},
{
reqd: 1,
label: "Item",
fieldtype: "Link",
options: "Item",
fieldname: "item_code",
get_query: () => {
return {
filters: {
name: ["in", items],
},
};
},
},
],
});
dialog.set_primary_action(__("Set Default Supplier"), () => {
let values = dialog.get_values();
if (values) {
// Set the default_supplier field of the appropriate Item to the selected supplier
frappe.call({
method: "frappe.client.set_value",
method: "erpnext.buying.report.supplier_quotation_comparison.supplier_quotation_comparison.set_default_supplier",
args: {
doctype: "Item",
name: item_code,
fieldname: "default_supplier",
value: values.supplier,
item_code: values.item_code,
supplier: values.supplier,
company: filters.company,
},
freeze: true,
callback: (r) => {

View File

@@ -292,3 +292,13 @@ def get_message():
<span class="indicator red">
Expires today / Already Expired
</span>"""
@frappe.whitelist()
def set_default_supplier(item_code, supplier, company):
frappe.db.set_value(
"Item Default",
{"parent": item_code, "company": company},
"default_supplier",
supplier,
)

View File

@@ -1439,7 +1439,8 @@ class AccountsController(TransactionBase):
dr_or_cr = "debit" if d.exchange_gain_loss > 0 else "credit"
if d.reference_doctype == "Purchase Invoice":
# Inverse debit/credit for payable accounts
if self.is_payable_account(d.reference_doctype, party_account):
dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
@@ -1473,6 +1474,14 @@ class AccountsController(TransactionBase):
)
)
def is_payable_account(self, reference_doctype, account):
if reference_doctype == "Purchase Invoice" or (
reference_doctype == "Journal Entry"
and frappe.get_cached_value("Account", account, "account_type") == "Payable"
):
return True
return False
def update_against_document_in_jv(self):
"""
Links invoice and advance voucher:

View File

@@ -1361,7 +1361,7 @@ def repost_required_for_queue(doc: StockController) -> bool:
@frappe.whitelist()
def make_quality_inspections(doctype, docname, items):
def make_quality_inspections(doctype, docname, items, inspection_type):
if isinstance(items, str):
items = json.loads(items)
@@ -1381,7 +1381,7 @@ def make_quality_inspections(doctype, docname, items):
quality_inspection = frappe.get_doc(
{
"doctype": "Quality Inspection",
"inspection_type": "Incoming",
"inspection_type": inspection_type,
"inspected_by": frappe.session.user,
"reference_type": doctype,
"reference_name": docname,

View File

@@ -135,6 +135,27 @@ class TestAccountsController(FrappeTestCase):
acc = frappe.get_doc("Account", name)
self.debtors_usd = acc.name
account_name = "Creditors USD"
if not frappe.db.get_value(
"Account", filters={"account_name": account_name, "company": self.company}
):
acc = frappe.new_doc("Account")
acc.account_name = account_name
acc.parent_account = "Accounts Payable - " + self.company_abbr
acc.company = self.company
acc.account_currency = "USD"
acc.account_type = "Payable"
acc.insert()
else:
name = frappe.db.get_value(
"Account",
filters={"account_name": account_name, "company": self.company},
fieldname="name",
pluck=True,
)
acc = frappe.get_doc("Account", name)
self.creditors_usd = acc.name
def create_sales_invoice(
self,
qty=1,
@@ -174,7 +195,9 @@ class TestAccountsController(FrappeTestCase):
)
return sinv
def create_payment_entry(self, amount=1, source_exc_rate=75, posting_date=None, customer=None):
def create_payment_entry(
self, amount=1, source_exc_rate=75, posting_date=None, customer=None, submit=True
):
"""
Helper function to populate default values in payment entry
"""
@@ -1606,3 +1629,72 @@ class TestAccountsController(FrappeTestCase):
exc_je_for_je2 = self.get_journals_for(je2.doctype, je2.name)
self.assertEqual(exc_je_for_je1, [])
self.assertEqual(exc_je_for_je2, [])
def test_61_payment_entry_against_journal_for_payable_accounts(self):
# Invoices
exc_rate1 = 75
exc_rate2 = 77
amount = 1
je1 = self.create_journal_entry(
acc1=self.creditors_usd,
acc1_exc_rate=exc_rate1,
acc2=self.cash,
acc1_amount=-amount,
acc2_amount=(-amount * 75),
acc2_exc_rate=1,
)
je1.accounts[0].party_type = "Supplier"
je1.accounts[0].party = self.supplier
je1 = je1.save().submit()
# Payment
pe = create_payment_entry(
company=self.company,
payment_type="Pay",
party_type="Supplier",
party=self.supplier,
paid_from=self.cash,
paid_to=self.creditors_usd,
paid_amount=amount,
)
pe.target_exchange_rate = exc_rate2
pe.received_amount = amount
pe.paid_amount = amount * exc_rate2
pe.save().submit()
pr = frappe.get_doc(
{
"doctype": "Payment Reconciliation",
"company": self.company,
"party_type": "Supplier",
"party": self.supplier,
"receivable_payable_account": get_party_account("Supplier", self.supplier, self.company),
}
)
pr.from_invoice_date = pr.to_invoice_date = pr.from_payment_date = pr.to_payment_date = nowdate()
pr.get_unreconciled_entries()
self.assertEqual(len(pr.invoices), 1)
self.assertEqual(len(pr.payments), 1)
invoices = [x.as_dict() for x in pr.invoices]
payments = [x.as_dict() for x in pr.payments]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
pr.reconcile()
self.assertEqual(len(pr.invoices), 0)
self.assertEqual(len(pr.payments), 0)
# There should be no outstanding in both currencies
self.assert_ledger_outstanding(je1.doctype, je1.name, 0.0, 0.0)
# Exchange Gain/Loss Journal should've been created
exc_je_for_je1 = self.get_journals_for(je1.doctype, je1.name)
self.assertEqual(len(exc_je_for_je1), 1)
# Cancel Payment
pe.reload()
pe.cancel()
self.assert_ledger_outstanding(je1.doctype, je1.name, (amount * exc_rate1), amount)
# Exchange Gain/Loss Journal should've been cancelled
exc_je_for_je1 = self.get_journals_for(je1.doctype, je1.name)
self.assertEqual(exc_je_for_je1, [])

View File

@@ -1896,7 +1896,7 @@ def sales_order_query(doctype=None, txt=None, searchfield=None, start=None, page
query = query.where(so_table.name.isin(filters.get("sales_orders")))
if txt:
query = query.where(table.item_code.like(f"{txt}%"))
query = query.where(table.parent.like(f"%{txt}%"))
if page_len:
query = query.limit(page_len)

View File

@@ -948,6 +948,21 @@ class WorkOrder(Document):
if self.qty <= 0:
frappe.throw(_("Quantity to Manufacture must be greater than 0."))
if (
self.stock_uom
and frappe.get_cached_value("UOM", self.stock_uom, "must_be_whole_number")
and abs(cint(self.qty) - flt(self.qty, self.precision("qty"))) > 0.0000001
):
frappe.throw(
_(
"Qty To Manufacture ({0}) cannot be a fraction for the UOM {2}. To allow this, disable '{1}' in the UOM {2}."
).format(
flt(self.qty, self.precision("qty")),
frappe.bold(_("Must be Whole Number")),
frappe.bold(self.stock_uom),
),
)
if self.production_plan and self.production_plan_item and not self.production_plan_sub_assembly_item:
qty_dict = frappe.db.get_value(
"Production Plan Item", self.production_plan_item, ["planned_qty", "ordered_qty"], as_dict=1

View File

@@ -454,7 +454,7 @@
"index_web_pages_for_search": 1,
"links": [],
"max_attachments": 4,
"modified": "2024-03-27 13:10:21.057163",
"modified": "2024-04-24 10:56:16.001032",
"modified_by": "Administrator",
"module": "Projects",
"name": "Project",
@@ -489,6 +489,15 @@
"role": "Projects Manager",
"share": 1,
"write": 1
},
{
"email": 1,
"export": 1,
"print": 1,
"report": 1,
"role": "Employee",
"select": 1,
"share": 1
}
],
"quick_entry": 1,

View File

@@ -2203,6 +2203,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
];
const me = this;
const inspection_type = ["Purchase Receipt", "Purchase Invoice", "Subcontracting Receipt"].includes(this.frm.doc.doctype)
? "Incoming" : "Outgoing";
const dialog = new frappe.ui.Dialog({
title: __("Select Items for Quality Inspection"),
size: "extra-large",
@@ -2214,7 +2216,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
args: {
doctype: me.frm.doc.doctype,
docname: me.frm.doc.name,
items: data.items
items: data.items,
inspection_type: inspection_type
},
freeze: true,
callback: function (r) {

View File

@@ -51,38 +51,7 @@ $.extend(erpnext, {
},
setup_serial_or_batch_no: function () {
let grid_row = cur_frm.open_grid_row();
if (
!grid_row ||
!grid_row.grid_form.fields_dict.serial_no ||
grid_row.grid_form.fields_dict.serial_no.get_status() !== "Write"
)
return;
frappe.model.get_value(
"Item",
{ name: grid_row.doc.item_code },
["has_serial_no", "has_batch_no"],
({ has_serial_no, has_batch_no }) => {
Object.assign(grid_row.doc, { has_serial_no, has_batch_no });
if (has_serial_no) {
attach_selector_button(
__("Add Serial No"),
grid_row.grid_form.fields_dict.serial_no.$wrapper,
this,
grid_row
);
} else if (has_batch_no) {
attach_selector_button(
__("Pick Batch No"),
grid_row.grid_form.fields_dict.batch_no.$wrapper,
this,
grid_row
);
}
}
);
// Deprecated in v15
},
route_to_adjustment_jv: (args) => {
@@ -938,11 +907,14 @@ erpnext.utils.map_current_doc = function (opts) {
if (opts.source_doctype) {
let data_fields = [];
if (["Purchase Receipt", "Delivery Note"].includes(opts.source_doctype)) {
data_fields.push({
fieldname: "merge_taxes",
fieldtype: "Check",
label: __("Merge taxes from multiple documents"),
});
let target_meta = frappe.get_meta(cur_frm.doc.doctype);
if (target_meta.fields.find((f) => f.fieldname === "taxes")) {
data_fields.push({
fieldname: "merge_taxes",
fieldtype: "Check",
label: __("Merge taxes from multiple documents"),
});
}
}
const d = new frappe.ui.form.MultiSelectDialog({
doctype: opts.source_doctype,

View File

@@ -135,13 +135,50 @@
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2024-03-27 13:10:02.865812",
"modified": "2024-04-18 15:25:25.808355",
"modified_by": "Administrator",
"module": "Regional",
"name": "Lower Deduction Certificate",
"naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [],
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts User",
"share": 1,
"write": 1
}
],
"sort_field": "creation",
"sort_order": "DESC",
"states": [],

View File

@@ -3,10 +3,6 @@ from frappe import _
def get_data():
return {
"heatmap": True,
"heatmap_message": _(
"This is based on transactions against this Customer. See timeline below for details"
),
"fieldname": "customer",
"non_standard_fieldnames": {
"Payment Entry": "party",

View File

@@ -384,7 +384,6 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False):
)
target.flags.ignore_permissions = ignore_permissions
target.delivery_date = nowdate()
target.run_method("set_missing_values")
target.run_method("calculate_taxes_and_totals")

View File

@@ -134,6 +134,7 @@ class TestQuotation(FrappeTestCase):
sales_order.naming_series = "_T-Quotation-"
sales_order.transaction_date = nowdate()
sales_order.delivery_date = nowdate()
sales_order.insert()
def test_make_sales_order_with_terms(self):
@@ -164,6 +165,7 @@ class TestQuotation(FrappeTestCase):
sales_order.naming_series = "_T-Quotation-"
sales_order.transaction_date = nowdate()
sales_order.delivery_date = nowdate()
sales_order.insert()
# Remove any unknown taxes if applied

View File

@@ -169,6 +169,27 @@ frappe.ui.form.on("Sales Order", {
);
},
// When multiple companies are set up. in case company name is changed set default company address
company: function (frm) {
if (frm.doc.company) {
frappe.call({
method: "erpnext.setup.doctype.company.company.get_default_company_address",
args: {
name: frm.doc.company,
existing_address: frm.doc.company_address || "",
},
debounce: 2000,
callback: function (r) {
if (r.message) {
frm.set_value("company_address", r.message);
} else {
frm.set_value("company_address", "");
}
},
});
}
},
onload: function (frm) {
if (!frm.doc.transaction_date) {
frm.set_value("transaction_date", frappe.datetime.get_today());

View File

@@ -691,6 +691,8 @@
},
{
"depends_on": "eval:doc.book_advance_payments_in_separate_party_account",
"description": "Only 'Payment Entries' made against this advance account are supported.",
"documentation_url": "https://docs.erpnext.com/docs/user/manual/en/advance-in-separate-party-account",
"fieldname": "default_advance_received_account",
"fieldtype": "Link",
"label": "Default Advance Received Account",
@@ -699,6 +701,8 @@
},
{
"depends_on": "eval:doc.book_advance_payments_in_separate_party_account",
"description": "Only 'Payment Entries' made against this advance account are supported.",
"documentation_url": "https://docs.erpnext.com/docs/user/manual/en/advance-in-separate-party-account",
"fieldname": "default_advance_paid_account",
"fieldtype": "Link",
"label": "Default Advance Paid Account",
@@ -766,7 +770,7 @@
"image_field": "company_logo",
"is_tree": 1,
"links": [],
"modified": "2024-03-27 13:06:45.374715",
"modified": "2024-04-23 12:38:33.173938",
"modified_by": "Administrator",
"module": "Setup",
"name": "Company",

View File

@@ -35,7 +35,7 @@ class Company(NestedSet):
auto_exchange_rate_revaluation: DF.Check
book_advance_payments_in_separate_party_account: DF.Check
capital_work_in_progress_account: DF.Link | None
chart_of_accounts: DF.Literal
chart_of_accounts: DF.Literal[None]
company_description: DF.TextEditor | None
company_logo: DF.AttachImage | None
company_name: DF.Data
@@ -139,6 +139,7 @@ class Company(NestedSet):
self.validate_abbr()
self.validate_default_accounts()
self.validate_currency()
self.validate_advance_account_currency()
self.validate_coa_input()
self.validate_perpetual_inventory()
self.validate_provisional_account_for_non_stock_items()
@@ -191,6 +192,29 @@ class Company(NestedSet):
).format(frappe.bold(account[0]))
frappe.throw(error_message)
def validate_advance_account_currency(self):
if (
self.default_advance_received_account
and frappe.get_cached_value("Account", self.default_advance_received_account, "account_currency")
!= self.default_currency
):
frappe.throw(
_("'{0}' should be in company currency {1}.").format(
frappe.bold("Default Advance Received Account"), frappe.bold(self.default_currency)
)
)
if (
self.default_advance_paid_account
and frappe.get_cached_value("Account", self.default_advance_paid_account, "account_currency")
!= self.default_currency
):
frappe.throw(
_("'{0}' should be in company currency {1}.").format(
frappe.bold("Default Advance Paid Account"), frappe.bold(self.default_currency)
)
)
def validate_currency(self):
if self.is_new():
return

View File

@@ -40,14 +40,9 @@ class CustomerGroup(NestedSet):
self.parent_customer_group = get_root_of("Customer Group")
def on_update(self):
self.validate_name_with_customer()
super().on_update()
self.validate_one_root()
def validate_name_with_customer(self):
if frappe.db.exists("Customer", self.name):
frappe.msgprint(_("A customer with the same name already exists"), raise_exception=1)
def get_parent_customer_groups(customer_group):
lft, rgt = frappe.db.get_value("Customer Group", customer_group, ["lft", "rgt"])

View File

@@ -18,18 +18,6 @@ erpnext.setup.EmployeeController = class EmployeeController extends frappe.ui.fo
refresh() {
erpnext.toggle_naming_series();
}
salutation() {
if (this.frm.doc.salutation) {
this.frm.set_value(
"gender",
{
Mr: "Male",
Ms: "Female",
}[this.frm.doc.salutation]
);
}
}
};
frappe.ui.form.on("Employee", {

View File

@@ -86,6 +86,7 @@ class TestBatch(FrappeTestCase):
"batches": frappe._dict({batch_no: 20}),
"type_of_transaction": "Inward",
"company": receipt.company,
"do_not_submit": 1,
}
)
.make_serial_and_batch_bundle()
@@ -176,6 +177,7 @@ class TestBatch(FrappeTestCase):
"batches": frappe._dict({batch_no: batch_qty}),
"type_of_transaction": "Outward",
"company": receipt.company,
"do_not_submit": 1,
}
)
.make_serial_and_batch_bundle()
@@ -249,6 +251,7 @@ class TestBatch(FrappeTestCase):
"batches": frappe._dict({batch_no: batch_qty}),
"type_of_transaction": "Outward",
"company": receipt.company,
"do_not_submit": 1,
}
)
.make_serial_and_batch_bundle()
@@ -341,6 +344,7 @@ class TestBatch(FrappeTestCase):
"batches": frappe._dict({batch_name: 90}),
"type_of_transaction": "Inward",
"company": "_Test Company",
"do_not_submit": 1,
}
).make_serial_and_batch_bundle()

View File

@@ -1093,7 +1093,7 @@ def make_delivery_trip(source_name, target_doc=None):
@frappe.whitelist()
def make_installation_note(source_name, target_doc=None):
def make_installation_note(source_name, target_doc=None, kwargs=None):
def update_item(obj, target, source_parent):
target.qty = flt(obj.qty) - flt(obj.installed_qty)
target.serial_no = obj.serial_no

View File

@@ -239,8 +239,7 @@ class PurchaseReceipt(BuyingController):
self.po_required()
self.validate_items_quality_inspection()
self.validate_with_previous_doc()
self.validate_uom_is_integer("uom", ["qty", "received_qty"])
self.validate_uom_is_integer("stock_uom", "stock_qty")
self.validate_uom_is_integer()
self.validate_cwip_accounts()
self.validate_provisional_expense_account()
@@ -254,6 +253,10 @@ class PurchaseReceipt(BuyingController):
self.reset_default_field_value("rejected_warehouse", "items", "rejected_warehouse")
self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse")
def validate_uom_is_integer(self):
super().validate_uom_is_integer("uom", ["qty", "received_qty"], "Purchase Receipt Item")
super().validate_uom_is_integer("stock_uom", "stock_qty", "Purchase Receipt Item")
def validate_cwip_accounts(self):
for item in self.get("items"):
if item.is_fixed_asset and is_cwip_accounting_enabled(item.asset_category):

View File

@@ -3009,6 +3009,7 @@ def make_purchase_receipt(**args):
"serial_nos": serial_nos,
"posting_date": args.posting_date or today(),
"posting_time": args.posting_time,
"do_not_submit": 1,
}
)
).name

View File

@@ -138,9 +138,13 @@ class TestQualityInspection(FrappeTestCase):
def test_make_quality_inspections_from_linked_document(self):
dn = create_delivery_note(item_code="_Test Item with QA", do_not_submit=True)
if dn.doctype in ["Purchase Receipt", "Purchase Invoice", "Subcontracting Receipt"]:
inspection_type = "Incoming"
else:
inspection_type = "Outgoing"
for item in dn.items:
item.sample_size = item.qty
quality_inspections = make_quality_inspections(dn.doctype, dn.name, dn.items)
quality_inspections = make_quality_inspections(dn.doctype, dn.name, dn.items, inspection_type)
self.assertEqual(len(dn.items), len(quality_inspections))
# cleanup

View File

@@ -596,6 +596,13 @@ class SerialandBatchBundle(Document):
serial_batches = {}
for row in self.entries:
if not row.qty and row.batch_no and not row.serial_no:
frappe.throw(
_("At row {0}: Qty is mandatory for the batch {1}").format(
bold(row.idx), bold(row.batch_no)
)
)
if self.has_serial_no and not row.serial_no:
frappe.throw(
_("At row {0}: Serial No is mandatory for Item {1}").format(
@@ -831,7 +838,12 @@ class SerialandBatchBundle(Document):
for batch in batches:
frappe.db.set_value("Batch", batch.name, {"reference_name": None, "reference_doctype": None})
def validate_serial_and_batch_data(self):
if not self.voucher_no:
frappe.throw(_("Voucher No is mandatory"))
def before_submit(self):
self.validate_serial_and_batch_data()
self.validate_serial_and_batch_no_for_returned()
self.set_purchase_document_no()

View File

@@ -194,6 +194,7 @@ class StockEntry(StockController):
if self.work_order:
self.pro_doc = frappe.get_doc("Work Order", self.work_order)
self.validate_duplicate_serial_and_batch_bundle("items")
self.validate_posting_time()
self.validate_purpose()
self.validate_item()
@@ -1646,11 +1647,7 @@ class StockEntry(StockController):
ret.update(get_uom_details(args.get("item_code"), args.get("uom"), args.get("qty")))
if self.purpose == "Material Issue":
ret["expense_account"] = (
item.get("expense_account")
or item_group_defaults.get("expense_account")
or frappe.get_cached_value("Company", self.company, "default_expense_account")
)
ret["expense_account"] = item.get("expense_account") or item_group_defaults.get("expense_account")
for company_field, field in {
"stock_adjustment_account": "expense_account",

View File

@@ -1483,7 +1483,7 @@ def create_delivery_note_entries_for_batchwise_item_valuation_test(dn_entry_list
"posting_date": dn.posting_date,
"posting_time": dn.posting_time,
"voucher_type": "Delivery Note",
"do_not_submit": dn.name,
"do_not_submit": 1,
}
)
).name

View File

@@ -70,6 +70,7 @@ class StockReconciliation(StockController):
self.validate_posting_time()
self.set_current_serial_and_batch_bundle()
self.set_new_serial_and_batch_bundle()
self.validate_duplicate_serial_and_batch_bundle("items")
self.remove_items_with_no_change()
self.validate_data()
self.validate_expense_account()
@@ -822,11 +823,9 @@ class StockReconciliation(StockController):
else:
self._cancel()
def recalculate_current_qty(self, voucher_detail_no, sle_creation, add_new_sle=False):
def recalculate_current_qty(self, voucher_detail_no):
from erpnext.stock.stock_ledger import get_valuation_rate
sl_entries = []
for row in self.items:
if voucher_detail_no != row.name:
continue
@@ -880,32 +879,6 @@ class StockReconciliation(StockController):
}
)
if (
add_new_sle
and not frappe.db.get_value(
"Stock Ledger Entry",
{"voucher_detail_no": row.name, "actual_qty": ("<", 0), "is_cancelled": 0},
"name",
)
and (not row.current_serial_and_batch_bundle)
):
self.set_current_serial_and_batch_bundle(voucher_detail_no, save=True)
row.reload()
if row.current_qty > 0 and row.current_serial_and_batch_bundle:
new_sle = self.get_sle_for_items(row)
new_sle.actual_qty = row.current_qty * -1
new_sle.valuation_rate = row.current_valuation_rate
new_sle.creation_time = add_to_date(sle_creation, seconds=-1)
new_sle.serial_and_batch_bundle = row.current_serial_and_batch_bundle
new_sle.qty_after_transaction = 0.0
sl_entries.append(new_sle)
if sl_entries:
self.make_sl_entries(sl_entries, allow_negative_stock=self.has_negative_stock_allowed())
if not frappe.db.exists("Repost Item Valuation", {"voucher_no": self.name, "status": "Queued"}):
self.repost_future_sle_and_gle(force=True)
def has_negative_stock_allowed(self):
allow_negative_stock = cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock"))
if allow_negative_stock:

View File

@@ -756,66 +756,6 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin):
self.assertEqual(flt(sle[0].actual_qty), flt(-100.0))
def test_backdated_stock_reco_entry_with_batch(self):
item_code = self.make_item(
"Test New Batch Item ABCVSD",
{
"is_stock_item": 1,
"has_batch_no": 1,
"batch_number_series": "BNS9.####",
"create_new_batch": 1,
},
).name
warehouse = "_Test Warehouse - _TC"
# Stock Reco for 100, Balace Qty 100
stock_reco = create_stock_reconciliation(
item_code=item_code,
posting_date=nowdate(),
posting_time="11:00:00",
warehouse=warehouse,
qty=100,
rate=100,
)
sles = frappe.get_all(
"Stock Ledger Entry",
fields=["actual_qty"],
filters={"voucher_no": stock_reco.name, "is_cancelled": 0},
)
self.assertEqual(len(sles), 1)
stock_reco.reload()
batch_no = get_batch_from_bundle(stock_reco.items[0].serial_and_batch_bundle)
# Stock Reco for 100, Balace Qty 100
stock_reco1 = create_stock_reconciliation(
item_code=item_code,
posting_date=add_days(nowdate(), -1),
posting_time="11:00:00",
batch_no=batch_no,
warehouse=warehouse,
qty=60,
rate=100,
)
sles = frappe.get_all(
"Stock Ledger Entry",
fields=["actual_qty"],
filters={"voucher_no": stock_reco.name, "is_cancelled": 0},
)
stock_reco1.reload()
get_batch_from_bundle(stock_reco1.items[0].serial_and_batch_bundle)
self.assertEqual(len(sles), 2)
for row in sles:
if row.actual_qty < 0:
self.assertEqual(row.actual_qty, -60)
def test_update_stock_reconciliation_while_reposting(self):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
@@ -986,6 +926,150 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin):
active_serial_no = frappe.get_all("Serial No", filters={"status": "Active", "item_code": item_code})
self.assertEqual(len(active_serial_no), 5)
def test_balance_qty_for_batch_with_backdated_stock_reco_and_future_entries(self):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
item = self.make_item(
"Test Batch Item Original Test",
{
"is_stock_item": 1,
"has_batch_no": 1,
"create_new_batch": 1,
"batch_number_series": "TEST-BATCH-SRWFEE-.###",
},
)
warehouse = "_Test Warehouse - _TC"
se1 = make_stock_entry(
item_code=item.name,
target=warehouse,
qty=50,
basic_rate=100,
posting_date=add_days(nowdate(), -2),
)
batch1 = get_batch_from_bundle(se1.items[0].serial_and_batch_bundle)
se2 = make_stock_entry(
item_code=item.name,
target=warehouse,
qty=50,
basic_rate=100,
posting_date=add_days(nowdate(), -2),
)
batch2 = get_batch_from_bundle(se2.items[0].serial_and_batch_bundle)
se3 = make_stock_entry(
item_code=item.name,
target=warehouse,
qty=100,
basic_rate=100,
posting_date=add_days(nowdate(), -2),
)
batch3 = get_batch_from_bundle(se3.items[0].serial_and_batch_bundle)
se3 = make_stock_entry(
item_code=item.name,
target=warehouse,
qty=100,
basic_rate=100,
posting_date=nowdate(),
)
sle = frappe.get_all(
"Stock Ledger Entry",
filters={
"item_code": item.name,
"warehouse": warehouse,
"is_cancelled": 0,
"voucher_no": se3.name,
},
fields=["qty_after_transaction"],
order_by="posting_time desc, creation desc",
)
self.assertEqual(flt(sle[0].qty_after_transaction), flt(300.0))
sr = create_stock_reconciliation(
item_code=item.name,
warehouse=warehouse,
qty=0,
batch_no=batch1,
posting_date=add_days(nowdate(), -1),
use_serial_batch_fields=1,
do_not_save=1,
)
for batch in [batch2, batch3]:
sr.append(
"items",
{
"item_code": item.name,
"warehouse": warehouse,
"qty": 0,
"batch_no": batch,
"use_serial_batch_fields": 1,
},
)
sr.save()
sr.submit()
sle = frappe.get_all(
"Stock Ledger Entry",
filters={
"item_code": item.name,
"warehouse": warehouse,
"is_cancelled": 0,
"voucher_no": se3.name,
},
fields=["qty_after_transaction"],
order_by="posting_time desc, creation desc",
)
self.assertEqual(flt(sle[0].qty_after_transaction), flt(100.0))
def test_stock_reco_and_backdated_purchase_receipt(self):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
item = self.make_item(
"Test Batch Item Original STOCK RECO Test",
{
"is_stock_item": 1,
"has_batch_no": 1,
"create_new_batch": 1,
"batch_number_series": "TEST-BATCH-SRCOSRWFEE-.###",
},
)
warehouse = "_Test Warehouse - _TC"
sr = create_stock_reconciliation(
item_code=item.name,
warehouse=warehouse,
qty=100,
rate=100,
)
sr.reload()
self.assertTrue(sr.items[0].serial_and_batch_bundle)
self.assertFalse(sr.items[0].current_serial_and_batch_bundle)
batch = get_batch_from_bundle(sr.items[0].serial_and_batch_bundle)
se1 = make_stock_entry(
item_code=item.name,
target=warehouse,
qty=50,
basic_rate=100,
posting_date=add_days(nowdate(), -2),
)
batch1 = get_batch_from_bundle(se1.items[0].serial_and_batch_bundle)
self.assertFalse(batch1 == batch)
sr.reload()
self.assertTrue(sr.items[0].serial_and_batch_bundle)
self.assertFalse(sr.items[0].current_serial_and_batch_bundle)
def create_batch_item_with_batch(item_name, batch_id):
batch_item_doc = create_item(item_name, is_stock_item=1)
@@ -1085,7 +1169,7 @@ def create_stock_reconciliation(**args):
)
bundle_id = None
if not args.use_serial_batch_fields and (args.batch_no or args.serial_no):
if not args.use_serial_batch_fields and (args.batch_no or args.serial_no) and args.qty:
batches = frappe._dict({})
if args.batch_no:
batches[args.batch_no] = args.qty

View File

@@ -0,0 +1,91 @@
// Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.query_reports["Available Batch Report"] = {
filters: [
{
fieldname: "company",
label: __("Company"),
fieldtype: "Link",
width: "80",
options: "Company",
default: frappe.defaults.get_default("company"),
},
{
fieldname: "to_date",
label: __("On This Date"),
fieldtype: "Date",
width: "80",
reqd: 1,
default: frappe.datetime.add_months(frappe.datetime.get_today(), -1),
},
{
fieldname: "item_code",
label: __("Item"),
fieldtype: "Link",
width: "80",
options: "Item",
get_query: () => {
return {
filters: {
has_batch_no: 1,
disabled: 0,
},
};
},
},
{
fieldname: "warehouse",
label: __("Warehouse"),
fieldtype: "Link",
width: "80",
options: "Warehouse",
get_query: () => {
let warehouse_type = frappe.query_report.get_filter_value("warehouse_type");
let company = frappe.query_report.get_filter_value("company");
return {
filters: {
...(warehouse_type && { warehouse_type }),
...(company && { company }),
},
};
},
},
{
fieldname: "warehouse_type",
label: __("Warehouse Type"),
fieldtype: "Link",
width: "80",
options: "Warehouse Type",
},
{
fieldname: "batch_no",
label: __("Batch No"),
fieldtype: "Link",
width: "80",
options: "Batch",
get_query: () => {
let item = frappe.query_report.get_filter_value("item_code");
return {
filters: {
...(item && { item }),
},
};
},
},
{
fieldname: "include_expired_batches",
label: __("Include Expired Batches"),
fieldtype: "Check",
width: "80",
},
{
fieldname: "show_item_name",
label: __("Show Item Name"),
fieldtype: "Check",
width: "80",
},
],
};

View File

@@ -0,0 +1,31 @@
{
"add_total_row": 1,
"columns": [],
"creation": "2024-04-11 17:03:32.253275",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 0,
"is_standard": "Yes",
"json": "{}",
"letter_head": "",
"letterhead": null,
"modified": "2024-04-23 17:18:19.779036",
"modified_by": "Administrator",
"module": "Stock",
"name": "Available Batch Report",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "Stock Ledger Entry",
"report_name": "Available Batch Report",
"report_type": "Script Report",
"roles": [
{
"role": "Stock User"
},
{
"role": "Accounts Manager"
}
]
}

View File

@@ -0,0 +1,178 @@
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from collections import defaultdict
import frappe
from frappe import _
from frappe.query_builder.functions import Sum
from frappe.utils import flt, today
def execute(filters=None):
columns, data = [], []
data = get_data(filters)
columns = get_columns(filters)
return columns, data
def get_columns(filters):
columns = [
{
"label": _("Item Code"),
"fieldname": "item_code",
"fieldtype": "Link",
"options": "Item",
"width": 200,
}
]
if filters.show_item_name:
columns.append(
{
"label": _("Item Name"),
"fieldname": "item_name",
"fieldtype": "Link",
"options": "Item",
"width": 200,
}
)
columns.extend(
[
{
"label": _("Warehouse"),
"fieldname": "warehouse",
"fieldtype": "Link",
"options": "Warehouse",
"width": 200,
},
{
"label": _("Batch No"),
"fieldname": "batch_no",
"fieldtype": "Link",
"width": 150,
"options": "Batch",
},
{"label": _("Balance Qty"), "fieldname": "balance_qty", "fieldtype": "Float", "width": 150},
]
)
return columns
def get_data(filters):
data = []
batchwise_data = get_batchwise_data_from_stock_ledger(filters)
batchwise_data = get_batchwise_data_from_serial_batch_bundle(batchwise_data, filters)
data = parse_batchwise_data(batchwise_data)
return data
def parse_batchwise_data(batchwise_data):
data = []
for key in batchwise_data:
d = batchwise_data[key]
if d.balance_qty == 0:
continue
data.append(d)
return data
def get_batchwise_data_from_stock_ledger(filters):
batchwise_data = frappe._dict({})
table = frappe.qb.DocType("Stock Ledger Entry")
batch = frappe.qb.DocType("Batch")
query = (
frappe.qb.from_(table)
.inner_join(batch)
.on(table.batch_no == batch.name)
.select(
table.item_code,
table.batch_no,
table.warehouse,
Sum(table.actual_qty).as_("balance_qty"),
)
.where(table.is_cancelled == 0)
.groupby(table.batch_no, table.item_code, table.warehouse)
)
query = get_query_based_on_filters(query, batch, table, filters)
for d in query.run(as_dict=True):
key = (d.item_code, d.warehouse, d.batch_no)
batchwise_data.setdefault(key, d)
return batchwise_data
def get_batchwise_data_from_serial_batch_bundle(batchwise_data, filters):
table = frappe.qb.DocType("Stock Ledger Entry")
ch_table = frappe.qb.DocType("Serial and Batch Entry")
batch = frappe.qb.DocType("Batch")
query = (
frappe.qb.from_(table)
.inner_join(ch_table)
.on(table.serial_and_batch_bundle == ch_table.parent)
.inner_join(batch)
.on(ch_table.batch_no == batch.name)
.select(
table.item_code,
ch_table.batch_no,
table.warehouse,
Sum(ch_table.qty).as_("balance_qty"),
)
.where((table.is_cancelled == 0) & (table.docstatus == 1))
.groupby(ch_table.batch_no, table.item_code, ch_table.warehouse)
)
query = get_query_based_on_filters(query, batch, table, filters)
for d in query.run(as_dict=True):
key = (d.item_code, d.warehouse, d.batch_no)
if key in batchwise_data:
batchwise_data[key].balance_qty += flt(d.balance_qty)
else:
batchwise_data.setdefault(key, d)
return batchwise_data
def get_query_based_on_filters(query, batch, table, filters):
if filters.item_code:
query = query.where(table.item_code == filters.item_code)
if filters.batch_no:
query = query.where(batch.name == filters.batch_no)
if not filters.include_expired_batches:
query = query.where((batch.expiry_date >= today()) | (batch.expiry_date.isnull()))
if filters.to_date == today():
query = query.where(batch.batch_qty > 0)
if filters.warehouse:
lft, rgt = frappe.db.get_value("Warehouse", filters.warehouse, ["lft", "rgt"])
warehouses = frappe.get_all(
"Warehouse", filters={"lft": (">=", lft), "rgt": ("<=", rgt), "is_group": 0}, pluck="name"
)
query = query.where(table.warehouse.isin(warehouses))
elif filters.warehouse_type:
warehouses = frappe.get_all(
"Warehouse", filters={"warehouse_type": filters.warehouse_type, "is_group": 0}, pluck="name"
)
query = query.where(table.warehouse.isin(warehouses))
if filters.show_item_name:
query = query.select(batch.item_name)
return query

View File

@@ -86,7 +86,11 @@ class DelayedItemReport:
filters = {"parent": ("in", sales_orders), "name": ("in", sales_order_items)}
so_data = {}
for d in frappe.get_all(doctype, filters=filters, fields=["delivery_date", "parent", "name"]):
fields = ["delivery_date", "name"]
if frappe.db.has_column(doctype, "parent"):
fields.append("parent")
for d in frappe.get_all(doctype, filters=filters, fields=fields):
key = d.name if consolidated else (d.parent, d.name)
if key not in so_data:
so_data.setdefault(key, d.delivery_date)

View File

@@ -61,6 +61,8 @@ def execute(filters=None):
actual_qty += flt(sle.actual_qty, precision)
stock_value += sle.stock_value_difference
batch_balance_dict[sle.batch_no] += sle.actual_qty
if filters.get("segregate_serial_batch_bundle"):
actual_qty = batch_balance_dict[sle.batch_no]
if sle.voucher_type == "Stock Reconciliation" and not sle.actual_qty:
actual_qty = sle.qty_after_transaction

View File

@@ -840,7 +840,7 @@ class update_entries_after:
def reset_actual_qty_for_stock_reco(self, sle):
doc = frappe.get_cached_doc("Stock Reconciliation", sle.voucher_no)
doc.recalculate_current_qty(sle.voucher_detail_no, sle.creation, sle.actual_qty > 0)
doc.recalculate_current_qty(sle.voucher_detail_no)
if sle.actual_qty < 0:
sle.actual_qty = (
@@ -1728,6 +1728,10 @@ def get_stock_reco_qty_shift(args):
stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
else:
stock_reco_qty_shift = flt(args.actual_qty)
elif args.get("serial_and_batch_bundle"):
stock_reco_qty_shift = flt(args.actual_qty)
else:
# reco is being submitted
last_balance = get_previous_sle_of_current_voucher(args, "<=", exclude_current_voucher=True).get(
@@ -1799,6 +1803,15 @@ def get_datetime_limit_condition(detail):
def validate_negative_qty_in_future_sle(args, allow_negative_stock=False):
if allow_negative_stock or is_negative_stock_allowed(item_code=args.item_code):
return
if (
args.voucher_type == "Stock Reconciliation"
and args.actual_qty < 0
and args.get("serial_and_batch_bundle")
and frappe.db.get_value("Stock Reconciliation Item", args.voucher_detail_no, "qty") > 0
):
return
if args.actual_qty >= 0 and args.voucher_type != "Stock Reconciliation":
return

View File

@@ -722,6 +722,7 @@ def make_purchase_receipt(source_name, target_doc=None, save=False, submit=False
"purchase_order": item.purchase_order,
"purchase_order_item": item.purchase_order_item,
"subcontracting_receipt_item": item.name,
"project": po_item.project,
}
target_doc.append("items", item_row)

View File

@@ -30,8 +30,8 @@ class TransactionBase(StatusUpdater):
except ValueError:
frappe.throw(_("Invalid Posting Time"))
def validate_uom_is_integer(self, uom_field, qty_fields):
validate_uom_is_integer(self, uom_field, qty_fields)
def validate_uom_is_integer(self, uom_field, qty_fields, child_dt=None):
validate_uom_is_integer(self, uom_field, qty_fields, child_dt)
def validate_with_previous_doc(self, ref):
self.exclude_fields = ["conversion_factor", "uom"] if self.get("is_return") else []
@@ -210,12 +210,13 @@ def validate_uom_is_integer(doc, uom_field, qty_fields, child_dt=None):
for f in qty_fields:
qty = d.get(f)
if qty:
if abs(cint(qty) - flt(qty, d.precision(f))) > 0.0000001:
precision = d.precision(f)
if abs(cint(qty) - flt(qty, precision)) > 0.0000001:
frappe.throw(
_(
"Row {1}: Quantity ({0}) cannot be a fraction. To allow this, disable '{2}' in UOM {3}."
).format(
flt(qty, d.precision(f)),
flt(qty, precision),
d.idx,
frappe.bold(_("Must be Whole Number")),
frappe.bold(d.get(uom_field)),