diff --git a/erpnext/accounts/doctype/subscription/subscription.json b/erpnext/accounts/doctype/subscription/subscription.json index 9a965ede5c601e337f8c489f7f8c0e2a75d58c9f..8e4656849e49fb98a5d3981674bc3e2f4fd6ad23 100644 --- a/erpnext/accounts/doctype/subscription/subscription.json +++ b/erpnext/accounts/doctype/subscription/subscription.json @@ -33,6 +33,11 @@ "generate_invoice_at_period_start", "generate_invoice_before_payment", "order_generation_days_before_period", + "references_section", + "po_no", + "po_date", + "column_break_ddqc", + "external_reference", "sb_4", "select_subscription_plan", "plans", @@ -396,11 +401,35 @@ "fieldname": "no_automatic_payment", "fieldtype": "Check", "label": "Do not process payment automatically" + }, + { + "fieldname": "references_section", + "fieldtype": "Section Break", + "label": "References" + }, + { + "fieldname": "po_no", + "fieldtype": "Data", + "label": "Customer's Purchase Order" + }, + { + "fieldname": "po_date", + "fieldtype": "Date", + "label": "Customer's Purchase Order Date" + }, + { + "fieldname": "column_break_ddqc", + "fieldtype": "Column Break" + }, + { + "fieldname": "external_reference", + "fieldtype": "Data", + "label": "External Reference" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2024-04-30 18:31:23.372542", + "modified": "2024-08-07 09:44:51.598577", "modified_by": "Administrator", "module": "Accounts", "name": "Subscription", diff --git a/erpnext/accounts/doctype/subscription/subscription.py b/erpnext/accounts/doctype/subscription/subscription.py index 5f57a62d041c2cf6e12203955b7e883fdc82ee1a..f9f8da0c8d89c5d01d9727ac12435860876ac820 100644 --- a/erpnext/accounts/doctype/subscription/subscription.py +++ b/erpnext/accounts/doctype/subscription/subscription.py @@ -57,6 +57,7 @@ class Subscription(Document): current_invoice_start: DF.Date | None customer: DF.Link email_template: DF.Link | None + external_reference: DF.Data | None generate_invoice_at_period_start: DF.Check generate_invoice_before_payment: DF.Check grand_total: DF.Currency @@ -67,6 +68,8 @@ class Subscription(Document): order_generation_days_before_period: DF.Int outstanding_amount: DF.Currency plans: DF.Table[SubscriptionPlanDetail] + po_date: DF.Date | None + po_no: DF.Data | None print_format: DF.Link | None prorate_last_invoice: DF.Check recurrence_period: DF.Link diff --git a/erpnext/accounts/doctype/subscription/subscription_state_manager.py b/erpnext/accounts/doctype/subscription/subscription_state_manager.py index 78bcf7940ed727acf1b26f7b7e45ba074674118b..4b9753de260b84c4ba5c226ae59cbd3e0b420891 100644 --- a/erpnext/accounts/doctype/subscription/subscription_state_manager.py +++ b/erpnext/accounts/doctype/subscription/subscription_state_manager.py @@ -42,7 +42,9 @@ class SubscriptionPeriod: return None elif not self.subscription.current_invoice_start or not self.start: return ( - max(getdate(self.subscription.start), add_days(getdate(self.subscription.trial_period_end), 1)) + max( + getdate(self.subscription.start), add_days(getdate(self.subscription.trial_period_end), 1) + ) if self.subscription.trial_period_end else self.subscription.start ) @@ -62,7 +64,9 @@ class SubscriptionPeriod: ) != getdate(self.subscription.trial_period_end): self.subscription.load_doc_before_save() return ( - max(getdate(self.subscription.start), add_days(getdate(self.subscription.trial_period_end), 1)) + max( + getdate(self.subscription.start), add_days(getdate(self.subscription.trial_period_end), 1) + ) if self.subscription.trial_period_end else self.subscription.start ) @@ -136,7 +140,7 @@ class SubscriptionPeriod: ): return add_to_date( add_days(self.subscription.trial_period_end, 1), - **self.recurrence_period.get_billing_cycle_data() + **self.recurrence_period.get_billing_cycle_data(), ) elif not (self.subscription.generate_invoice_at_period_start): return add_days(self.subscription.current_invoice_end, 1) @@ -188,7 +192,10 @@ class SubscriptionStateManager: status = "Billable" elif self.is_draft(): status = "Draft invoices" - elif flt(self.subscription.outstanding_amount) > 0: + elif ( + self.subscription.get_state_value("sales_invoice") + and flt(self.subscription.outstanding_amount) > 0 + ): status = "Unpaid" elif self.is_paid(): status = "Paid" @@ -206,9 +213,9 @@ class SubscriptionStateManager: self.subscription.reload() def is_trial(self): - return self.subscription.trial_period_end and getdate( - self.subscription.trial_period_end - ) >= getdate(nowdate()) + return self.subscription.trial_period_end and getdate(self.subscription.trial_period_end) >= getdate( + nowdate() + ) def is_cancelled(self): return ( @@ -218,9 +225,9 @@ class SubscriptionStateManager: ) def is_billable(self): - if self.subscription.cancellation_date and getdate( - self.subscription.cancellation_date - ) < getdate(nowdate()): + if self.subscription.cancellation_date and getdate(self.subscription.cancellation_date) < getdate( + nowdate() + ): return False if self.subscription_state.sales_order: @@ -229,7 +236,9 @@ class SubscriptionStateManager: return False if self.subscription.generate_invoice_at_period_start: - return True + if getdate(self.subscription.start) <= getdate(nowdate()): + return True + return False elif self.order_can_be_generated_before_period_end(): return True @@ -244,24 +253,29 @@ class SubscriptionStateManager: self.subscription.order_generation_days_before_period and self.subscription.generate_invoice_at_period_start ): - return self.subscription.current_invoice_end and getdate(nowdate()) >= getdate( - add_days( - self.subscription.current_invoice_end, - cint(self.subscription.order_generation_days_before_period) * -1, + if self.subscription.current_invoice_end or self.subscription.start: + return getdate(nowdate()) >= getdate( + add_days( + self.subscription.current_invoice_end or self.subscription.start, + cint(self.subscription.order_generation_days_before_period) * -1, + ) ) - ) + + return False def order_can_be_generated_before_period_end(self): if ( self.subscription.order_generation_days_before_period and not self.subscription.generate_invoice_at_period_start ): - return self.subscription_state.previous_period_end and getdate(nowdate()) >= getdate( - add_days( - self.subscription_state.previous_period_end, - cint(self.subscription.order_generation_days_before_period) * -1, + if self.subscription_state.previous_period_end or self.subscription.current_invoice_end: + return getdate(nowdate()) >= getdate( + add_days( + self.subscription_state.previous_period_end or self.subscription.current_invoice_end, + cint(self.subscription.order_generation_days_before_period) * -1, + ) ) - ) + return False def is_draft(self): if self.sales_invoice: diff --git a/erpnext/accounts/doctype/subscription/subscription_transaction.py b/erpnext/accounts/doctype/subscription/subscription_transaction.py index a9d404e2ec2adce12f68a96255cad04c3d56045d..fec57003ab9c1e9b0efe282b15d17a8bc5e8fe57 100644 --- a/erpnext/accounts/doctype/subscription/subscription_transaction.py +++ b/erpnext/accounts/doctype/subscription/subscription_transaction.py @@ -50,8 +50,38 @@ class SubscriptionTransactionBase: return self.subscription.current_invoice_end def set_subscription_invoicing_details(self, document): - document.company = self.subscription.company - document.customer = self.subscription.customer + document_meta = frappe.get_meta(document.doctype) + + restricted_fieldtypes = [ + "Tab Break", + "Section Break", + "Column Break", + "HTML", + "Button", + "Attach", + "Table", + ] + restricted_map_fields = [ + "name", + "naming_series", + "creation", + "owner", + "modified", + "modified_by", + "idx", + "docstatus", + "status", + ] + + for field in document_meta.fields: + if field.fieldtype in restricted_fieldtypes: + continue + if field.fieldname in restricted_map_fields: + continue + + if self.subscription.get(field.fieldname): + document.set(field.fieldname, self.subscription.get(field.fieldname)) + document.customer_group, document.territory = frappe.db.get_value( "Customer", self.subscription.customer, ["customer_group", "territory"] ) @@ -90,15 +120,10 @@ class SubscriptionTransactionBase: document.apply_shipping_rule() # Discounts - if self.subscription.additional_discount_percentage: - document.additional_discount_percentage = self.subscription.additional_discount_percentage - if self.subscription.additional_discount_amount: document.discount_amount = self.subscription.additional_discount_amount - if ( - self.subscription.additional_discount_percentage or self.subscription.additional_discount_amount - ): + if self.subscription.additional_discount_percentage or self.subscription.additional_discount_amount: discount_on = self.subscription.apply_additional_discount document.apply_discount_on = discount_on if discount_on else "Grand Total" @@ -155,13 +180,12 @@ class SubscriptionTransactionBase: def get_items_from_plans(self, plans, document): prorata_factor = self.get_prorata_factor() - date = ( - document.posting_date if document.doctype == "Sales Invoice" else document.transaction_date - ) + date = document.posting_date if document.doctype == "Sales Invoice" else document.transaction_date items = [] for plan in plans: rate = ( - SubscriptionPlansManager(self.subscription).get_plan_rate(plan, getdate(date)) * prorata_factor + SubscriptionPlansManager(self.subscription).get_plan_rate(plan, getdate(date)) + * prorata_factor ) items.append( { @@ -204,6 +228,9 @@ class SubscriptionInvoiceGenerator(SubscriptionTransactionBase): invoice.tax_id = frappe.db.get_value("Customer", invoice.customer, "tax_id") invoice.currency = self.subscription.currency + invoice.external_reference = ( + self.subscription.external_reference + ) # Keep explicit as it is a no-copy field # Add dimensions in invoice for subscription: accounting_dimensions = get_accounting_dimensions() @@ -301,9 +328,7 @@ class SubscriptionPaymentEntryGenerator(SubscriptionTransactionBase): self.subscription.payment_gateway, ["gateway_settings", "gateway_controller"], ) - bank_account_name = frappe.db.get_value( - gateway_settings[0], gateway_settings[1], "bank_account" - ) + bank_account_name = frappe.db.get_value(gateway_settings[0], gateway_settings[1], "bank_account") if not bank_account_name: bank_account_name = frappe.db.get_value( diff --git a/erpnext/accounts/doctype/subscription/test_subscription.py b/erpnext/accounts/doctype/subscription/test_subscription.py index 250988f7aeeb50a2ad586b95af28a9d0b2ebd802..0bc05833b4486666d1e1f78928e14ba648c39e74 100644 --- a/erpnext/accounts/doctype/subscription/test_subscription.py +++ b/erpnext/accounts/doctype/subscription/test_subscription.py @@ -25,13 +25,6 @@ PLANS = [ class TestSubscription(FrappeTestCase): @classmethod def setUpClass(cls) -> None: - item = frappe.get_doc("Item", "_Test Non Stock Item") - item.is_recurring_item = True - - for period in ["Daily", "Monthly"]: - item.append("recurrence_periods", {"recurrence_period": period}) - item.save() - frappe.get_doc( { "doctype": "Recurrence Period", @@ -55,6 +48,13 @@ class TestSubscription(FrappeTestCase): } ).insert(ignore_if_duplicate=True) + item = frappe.get_doc("Item", "_Test Non Stock Item") + item.is_recurring_item = True + + for period in ["Daily", "Monthly", "Monthly EOP", "Monthly Generate 10 Days Before"]: + item.append("recurrence_periods", {"recurrence_period": period}) + item.save() + def tearDown(self) -> None: frappe.flags.current_date = today() diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index 90125092c86c49080850bd9150d67e00f7954a54..26f75ec7a68fab3ee29d0151613c64221043c8a4 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -603,10 +603,9 @@ frappe.ui.form.on("Sales Order", { ); } else if (frm.doc.subscription) { frm.dashboard.clear_headline(); + const link = frappe.utils.get_form_link("Subscription", frm.doc.subscription, true); frm.dashboard.set_headline_alert( - __("This order contains some recurring items and is linked to subscription {0}", [ - frm.doc.subscription, - ]), + __("This order contains some recurring items and is linked to subscription {0}", [link]), "green" ); } diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index bd0f6359d9b7cb2fcc9961fb44120ad3a82536bd..603f9920d4a65031dac78ea7f80b4fb73e4f7171 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -830,12 +830,9 @@ class SalesOrder(SellingController): self.to_date = recurrence_period.get_end_date(self.from_date) def make_subscription(self): - if not self.recurrence_period: + if (not self.recurrence_period) or self.get("subscription"): return - if self.get("subscription"): - return frappe.msgprint("This Sales Order is already linked to a Subscription") - self.run_method("validate_subscription_creation") # Make a subscription from the Sales Order @@ -1347,6 +1344,7 @@ def make_subscription(source_name, target_doc=None, ignore_permissions=False) -> ["from_date", "start"], ["taxes_and_charges", "tax_template"], ["name", "sales_order_item"], + ["external_reference", "external_reference"], ], }, "Sales Order Item": {